Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to capture replays #579

Merged
merged 38 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
969b7dc
Rename react native IDE to Radon IDE (the parts visible to the user)
kmagiera Sep 17, 2024
a05b266
Stashing work
kmagiera Sep 18, 2024
f06713f
Merge remote-tracking branch 'origin/main' into kmagiera/replays
kmagiera Sep 19, 2024
69b5349
Merge main and stash more work
kmagiera Sep 19, 2024
e9629b9
Some styling
kmagiera Sep 20, 2024
0781d64
Stashing more work
kmagiera Sep 24, 2024
a843f1c
Merge remote-tracking branch 'origin/main' into kmagiera/replays
kmagiera Sep 27, 2024
1862cbf
Cleanup old commands
kmagiera Sep 27, 2024
20650c0
Adapt to recent changes in sim-server
kmagiera Sep 27, 2024
f90567d
Cleanup
kmagiera Sep 30, 2024
7ef1743
Add length controls
kmagiera Sep 30, 2024
ff7ee2b
Properly set start time when 'full' option is selected
kmagiera Sep 30, 2024
583e5f4
Moar stuff
kmagiera Oct 1, 2024
a70c454
Merge remote-tracking branch 'origin/main' into kmagiera/replays
kmagiera Oct 1, 2024
afe3242
Fix length change logic
kmagiera Oct 1, 2024
1c1871b
Handle rewind when startTime is set and also restart video upon chang…
kmagiera Oct 1, 2024
1308140
Make Preview display on top of button groups using z-index
p-malecki Oct 1, 2024
fe59733
Fix ReplayButton icons to display inline
p-malecki Oct 1, 2024
8d635bd
Add z-index to modals, dropdowns and menus
p-malecki Oct 1, 2024
4ea73e9
Add smaller size of switch
p-malecki Oct 1, 2024
050080e
Stylize enabling Replays in DeviceSettingsDropdown
p-malecki Oct 1, 2024
6fb8bac
Disable Replay button when device is not loaded
p-malecki Oct 1, 2024
17ea87d
Move replay controls to ReplayOverlay
p-malecki Oct 1, 2024
d42ce87
Remove unused props and state
p-malecki Oct 4, 2024
eb61a99
Move saveVideoRecording btn to replay-controls bar
p-malecki Oct 4, 2024
23c6200
Remove dot from number of seconds labels in LengthSelector
p-malecki Oct 4, 2024
0f365fa
Add light theme to Replay
p-malecki Oct 7, 2024
814002d
Add light theme to Replay
p-malecki Oct 7, 2024
47697b4
Merge branches 'kmagiera/replays' and 'kmagiera/replays' of https://g…
p-malecki Oct 7, 2024
7c48245
Merge branch 'main' into kmagiera/replays
kmagiera Oct 7, 2024
a281d69
Use updated sim server
kmagiera Oct 7, 2024
45f1f20
Some final touches
kmagiera Oct 7, 2024
0a2f9fe
Fix log counter
kmagiera Oct 7, 2024
fd5d10c
Remove z-index from css
p-malecki Oct 7, 2024
d13c38e
Remove whitespaces from saved replay filename
p-malecki Oct 7, 2024
81f19a9
Refactor ReplayOverlay props
p-malecki Oct 8, 2024
2c107af
Merge remote-tracking branch 'origin/main' into kmagiera/replays
kmagiera Oct 8, 2024
3f3c954
Add more context to data images defined in css files
kmagiera Oct 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/simulator-server
9 changes: 9 additions & 0 deletions packages/vscode-extension/src/common/Project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type DeviceSettings = {
};
hasEnrolledBiometrics: boolean;
locale: Locale;
replaysEnabled: boolean;
};

export type ProjectState = {
Expand Down Expand Up @@ -110,6 +111,12 @@ export interface ProjectEventListener<T> {
(event: T): void;
}

export type RecordingData = {
url: string;
tempFileLocation: string;
fileName: string;
};

export interface ProjectInterface {
getProjectState(): Promise<ProjectState>;
reload(type: ReloadAction): Promise<boolean>;
Expand All @@ -132,6 +139,8 @@ export interface ProjectInterface {

resetAppPermissions(permissionType: AppPermissionType): Promise<void>;

captureReplay(): Promise<RecordingData>;

dispatchTouches(touches: Array<TouchPoint>, type: "Up" | "Move" | "Down"): Promise<void>;
dispatchKeyPress(keyCode: number, direction: "Up" | "Down"): Promise<void>;
dispatchPaste(text: string): Promise<void>;
Expand Down
4 changes: 4 additions & 0 deletions packages/vscode-extension/src/common/utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { RecordingData } from "./Project";

export interface UtilsInterface {
getCommandsCurrentKeyBinding(commandName: string): Promise<string | undefined>;

reportIssue(): Promise<void>;

openFileAt(filePath: string, line0Based: number, column0Based: number): Promise<void>;

saveVideoRecording(recordingData: RecordingData): Promise<boolean>;

movePanelToNewWindow(): Promise<void>;

showDismissableError(errorMessage: string): Promise<void>;
Expand Down
18 changes: 18 additions & 0 deletions packages/vscode-extension/src/devices/DeviceBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,24 @@ export abstract class DeviceBase implements Disposable {
this.preview?.dispose();
}

public stopReplays() {
return this.preview?.stopReplays();
}

public startReplays() {
if (!this.preview) {
throw new Error("Preview not started");
}
return this.preview.startReplays();
}

public async captureReplay() {
if (!this.preview) {
throw new Error("Preview not started");
}
return this.preview.captureReplay();
}

public sendTouches(touches: Array<TouchPoint>, type: "Up" | "Move" | "Down") {
this.preview?.sendTouches(touches, type);
}
Expand Down
92 changes: 83 additions & 9 deletions packages/vscode-extension/src/devices/preview.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { Disposable, workspace } from "vscode";
import path from "path";

Check warning on line 2 in packages/vscode-extension/src/devices/preview.ts

View workflow job for this annotation

GitHub Actions / check

`path` import should occur before import of `vscode`
import { Disposable } from "vscode";
import { exec, ChildProcess, lineReader } from "../utilities/subprocess";
import { extensionContext } from "../utilities/extensionContext";
import { Logger } from "../Logger";
import { Platform } from "../utilities/platform";
import { TouchPoint } from "../common/Project";
import { RecordingData, TouchPoint } from "../common/Project";

interface ReplayPromiseHandlers {
resolve: (value: RecordingData) => void;
reject: (reason?: any) => void;
}

export class Preview implements Disposable {
private subprocess?: ChildProcess;
public streamURL?: string;
private lastReplayPromise?: ReplayPromiseHandlers;

constructor(private args: string[]) {}

Expand Down Expand Up @@ -38,27 +44,95 @@
reject(new Error("Preview server exited without URL"));
});

const streamURLRegex = /(http:\/\/[^ ]*stream\.mjpeg)/;

lineReader(subprocess).onLineRead((line, stderr) => {
if (stderr) {
Logger.info("sim-server:", line);
return;
}

const match = line.match(streamURLRegex);
if (line.includes("stream_ready")) {
const streamURLRegex = /(http:\/\/[^ ]*stream\.mjpeg)/;
const match = line.match(streamURLRegex);

if (match) {
Logger.info(`Stream ready ${match[1]}`);

if (match) {
Logger.info(`Stream ready ${match[1]}`);
this.streamURL = match[1];
resolve(this.streamURL);
}
} else if (line.includes("video_ready replay") || line.includes("video_error replay")) {
// video response format for replays looks as follows:
// video_ready replay <HTTP_URL> <FILE_URL>
// video_error replay <Error message>
const videoReadyMatch = line.match(/video_ready replay\S+ (\S+) (\S+)/);
const videoErrorMatch = line.match(/video_error replay\S+ (.*)/);

this.streamURL = match[1];
resolve(this.streamURL);
const handlers = this.lastReplayPromise;
this.lastReplayPromise = undefined;

if (handlers && videoReadyMatch) {
// match array looks as follows:
// [0] - full match
// [1] - URL or error message
// [2] - File URL
const tempFileLocation = videoReadyMatch[2];
const ext = path.extname(tempFileLocation);
const fileName = workspace.name
? `${workspace.name}-RadonIDE-replay${ext}`
: `RadonIDE-replay${ext}`;
handlers.resolve({
url: videoReadyMatch[1],
tempFileLocation,
fileName,
});
} else if (handlers && videoErrorMatch) {
handlers.reject(new Error(videoErrorMatch[1]));
}
}
Logger.info("sim-server:", line);
});
});
}

static replayID = 0;

public startReplays() {
const stdin = this.subprocess?.stdin;
if (!stdin) {
throw new Error("sim-server process not available");
}
stdin.write(`video replay_${++Preview.replayID} start -m -b 50\n`); // 50MB buffer for in-memory video
}

public stopReplays() {
const stdin = this.subprocess?.stdin;
if (!stdin) {
throw new Error("sim-server process not available");
}
stdin.write(`video replay_${Preview.replayID} stop\n`);
}

public captureReplay() {
const stdin = this.subprocess?.stdin;
if (!stdin) {
throw new Error("sim-server process not available");
}
let resolvePromise: (value: RecordingData) => void;
let rejectPromise: (reason?: any) => void;
const promise = new Promise<RecordingData>((resolve, reject) => {
resolvePromise = resolve;
rejectPromise = reject;
});
if (this.lastReplayPromise) {
promise.then(this.lastReplayPromise.resolve, this.lastReplayPromise.reject);
}
this.lastReplayPromise = { resolve: resolvePromise!, reject: rejectPromise! };
stdin.write(`video replay_${Preview.replayID} stop\n`);
// immediately restart replays
this.startReplays();
return promise;
}

public sendTouches(touches: Array<TouchPoint>, type: "Up" | "Move" | "Down") {
const touchesCoords = touches.map((pt) => `${pt.xRatio},${pt.yRatio}`).join(" ");
this.subprocess?.stdin?.write(`touch ${type} ${touchesCoords}\n`);
Expand Down
8 changes: 6 additions & 2 deletions packages/vscode-extension/src/panels/WebviewController.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import vscode, { Webview, Disposable, window, commands } from "vscode";
import vscode, { Webview, Disposable, window, commands, Uri } from "vscode";
import { DependencyManager } from "../dependency/DependencyManager";
import { DeviceManager } from "../devices/DeviceManager";
import { Project } from "../project/project";
Expand Down Expand Up @@ -73,6 +73,10 @@ export class WebviewController implements Disposable {
getTelemetryReporter().sendTelemetryEvent("panelOpened");
}

public asWebviewUri(uri: Uri) {
return this.webview.asWebviewUri(uri);
}

public dispose() {
commands.executeCommand("setContext", "RNIDE.panelIsOpen", false);

Expand Down Expand Up @@ -146,7 +150,7 @@ export class WebviewController implements Disposable {
this.webview.postMessage({
command: "callResult",
callId,
error,
error: { name: error.name, message: error.message },
});
});
} else {
Expand Down
21 changes: 21 additions & 0 deletions packages/vscode-extension/src/project/deviceSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export class DeviceSession implements Disposable {
private debugSession: DebugSession | undefined;
private disposableBuild: DisposableBuild<BuildResult> | undefined;
private buildManager: BuildManager;
private deviceSettings: DeviceSettings | undefined;
private isLaunching = true;

private get buildResult() {
if (!this.maybeBuildResult) {
Expand Down Expand Up @@ -106,6 +108,9 @@ export class DeviceSession implements Disposable {
platform: this.device.platform,
});

this.isLaunching = true;
this.device.stopReplays();

// FIXME: Windows getting stuck waiting for the promise to resolve. This
// seems like a problem with app connecting to Metro and using embedded
// bundle instead.
Expand Down Expand Up @@ -133,6 +138,11 @@ export class DeviceSession implements Disposable {
this.eventDelegate.onStateChange(StartupMessage.AttachingDebugger);
await this.startDebugger();

this.isLaunching = false;
if (this.deviceSettings?.replaysEnabled) {
this.device.startReplays();
}

const launchDurationSec = (Date.now() - launchRequestTime) / 1000;
Logger.info("App launched in", launchDurationSec.toFixed(2), "sec.");
getTelemetryReporter().sendTelemetryEvent(
Expand Down Expand Up @@ -184,6 +194,7 @@ export class DeviceSession implements Disposable {
}

public async start(deviceSettings: DeviceSettings, { cleanBuild }: StartOptions) {
this.deviceSettings = deviceSettings;
await this.waitForMetroReady();
// TODO(jgonet): Build and boot simultaneously, with predictable state change updates
await this.bootDevice(deviceSettings);
Expand Down Expand Up @@ -220,6 +231,10 @@ export class DeviceSession implements Disposable {
return false;
}

public async captureReplay() {
return this.device.captureReplay();
}

public sendTouches(touches: Array<TouchPoint>, type: "Up" | "Move" | "Down") {
this.device.sendTouches(touches, type);
}
Expand Down Expand Up @@ -272,6 +287,12 @@ export class DeviceSession implements Disposable {
}

public async changeDeviceSettings(settings: DeviceSettings): Promise<boolean> {
this.deviceSettings = settings;
if (settings.replaysEnabled && !this.isLaunching) {
this.device.startReplays();
} else {
this.device.stopReplays();
}
return this.device.changeSettings(settings);
}

Expand Down
23 changes: 10 additions & 13 deletions packages/vscode-extension/src/project/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
ProjectEventMap,
ProjectInterface,
ProjectState,
RecordingData,
ReloadAction,
StartupMessage,
TouchPoint,
Expand Down Expand Up @@ -60,19 +61,7 @@ export class Project
selectedDevice: undefined,
};

private deviceSettings: DeviceSettings = extensionContext.workspaceState.get(
DEVICE_SETTINGS_KEY
) ?? {
appearance: "dark",
contentSize: "normal",
location: {
latitude: 50.048653,
longitude: 19.965474,
isDisabled: true,
},
hasEnrolledBiometrics: false,
locale: "en_US",
};
private deviceSettings: DeviceSettings;

constructor(
private readonly deviceManager: DeviceManager,
Expand All @@ -89,6 +78,7 @@ export class Project
},
hasEnrolledBiometrics: false,
locale: "en_US",
replaysEnabled: false,
};
this.devtools = new Devtools();
this.metro = new Metro(this.devtools, this);
Expand Down Expand Up @@ -162,6 +152,13 @@ export class Project
}
//#endregion

async captureReplay(): Promise<RecordingData> {
if (!this.deviceSession) {
throw new Error("No device session available");
}
return this.deviceSession.captureReplay();
}

async dispatchPaste(text: string) {
this.deviceSession?.sendPaste(text);
}
Expand Down
24 changes: 23 additions & 1 deletion packages/vscode-extension/src/utilities/utils.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { commands, env, Uri, window, workspace } from "vscode";

Check warning on line 1 in packages/vscode-extension/src/utilities/utils.ts

View workflow job for this annotation

GitHub Actions / check

`vscode` import should occur after import of `path`
import { homedir } from "node:os";
import fs from "fs";
import path from "path";
import JSON5 from "json5";
import { commands, window, env, Uri } from "vscode";
import vscode from "vscode";
import { Logger } from "../Logger";
import { extensionContext } from "./extensionContext";
import { openFileAtPosition } from "./openFileAtPosition";
import { UtilsInterface } from "../common/utils";
import { Platform } from "./platform";
import { RecordingData } from "../common/Project";

type KeybindingType = {
command: string;
Expand Down Expand Up @@ -79,6 +80,27 @@
openFileAtPosition(filePath, line0Based, column0Based);
}

public async saveVideoRecording(recordingData: RecordingData) {
const extension = path.extname(recordingData.tempFileLocation);
const defaultUri = Uri.file(
path.join(workspace.workspaceFolders![0].uri.fsPath, recordingData.fileName)
);
// save dialog open the location dialog, it also warns the user if the file already exists
let saveUri = await window.showSaveDialog({
defaultUri: defaultUri,
filters: {
"Video Files": [extension],
},
});

if (!saveUri) {
return false;
}

await fs.promises.copyFile(recordingData.tempFileLocation, saveUri.fsPath);
return true;
}

public async movePanelToNewWindow() {
commands.executeCommand("workbench.action.moveEditorToNewWindow");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,8 @@
.device-settings-margin {
margin-bottom: 24px;
}

.icons-container {
display: flex;
align-items: center;
}
Loading
Loading