-
Notifications
You must be signed in to change notification settings - Fork 34
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 recording controls enhancement #733
base: main
Are you sure you want to change the base?
Changes from 36 commits
46e31d8
176488a
966399a
cc311ee
60b3576
2110684
0b9f997
4b77af6
cd2140a
4d7dcb4
f0868de
7ce0a4f
d5ddbb9
60a0c86
5704638
1809870
c03cda5
b7f6a40
5831986
7aa47cb
c3780f1
fb88a61
7ef8c3d
55f0763
4a07610
ff937f9
899f74b
f3f9647
7af6ec9
e23b6f3
598c678
c01b50c
1612f80
8e86486
1d9cc95
1126d84
94e7f28
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -52,17 +52,33 @@ export abstract class DeviceBase implements Disposable { | |
return this.preview?.hideTouches(); | ||
} | ||
|
||
public stopReplays() { | ||
return this.preview?.stopReplays(); | ||
public startRecording() { | ||
if (!this.preview) { | ||
throw new Error("Preview not started"); | ||
} | ||
return this.preview.startRecording(); | ||
} | ||
|
||
public async captureAndStopRecording() { | ||
if (!this.preview) { | ||
throw new Error("Preview not started"); | ||
} | ||
const recordingDataPromise = this.preview.captureRecording(); | ||
this.preview?.stopRecording(); | ||
return recordingDataPromise; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for symmetry, it'd be better if this method just proxied the call down to preview instead of making two calls |
||
} | ||
|
||
public startReplays() { | ||
public enableReplay() { | ||
if (!this.preview) { | ||
throw new Error("Preview not started"); | ||
} | ||
return this.preview.startReplays(); | ||
} | ||
|
||
public disableReplays() { | ||
return this.preview?.stopReplays(); | ||
} | ||
|
||
public async captureReplay() { | ||
if (!this.preview) { | ||
throw new Error("Preview not started"); | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -6,22 +6,54 @@ import { Logger } from "../Logger"; | |||||||||||||||||||||||
import { Platform } from "../utilities/platform"; | ||||||||||||||||||||||||
import { RecordingData, TouchPoint } from "../common/Project"; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
interface ReplayPromiseHandlers { | ||||||||||||||||||||||||
interface VideoRecordingPromiseHandlers { | ||||||||||||||||||||||||
resolve: (value: RecordingData) => void; | ||||||||||||||||||||||||
reject: (reason?: any) => void; | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
export class Preview implements Disposable { | ||||||||||||||||||||||||
private videoRecordingPromises = new Map<string, VideoRecordingPromiseHandlers>(); | ||||||||||||||||||||||||
private subprocess?: ChildProcess; | ||||||||||||||||||||||||
public streamURL?: string; | ||||||||||||||||||||||||
private lastReplayPromise?: ReplayPromiseHandlers; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
constructor(private args: string[]) {} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
dispose() { | ||||||||||||||||||||||||
this.subprocess?.kill(); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
private sendCommandOrThrow(command: string) { | ||||||||||||||||||||||||
const stdin = this.subprocess?.stdin; | ||||||||||||||||||||||||
if (!stdin) { | ||||||||||||||||||||||||
throw new Error("sim-server process not available"); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
stdin.write(command); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
private saveVideoWithID(videoId: string): Promise<RecordingData> { | ||||||||||||||||||||||||
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; | ||||||||||||||||||||||||
}); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
const lastPromise = this.videoRecordingPromises.get(videoId); | ||||||||||||||||||||||||
if (lastPromise) { | ||||||||||||||||||||||||
promise.then(lastPromise.resolve, lastPromise.reject); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
const newPromiseHandler = { resolve: resolvePromise!, reject: rejectPromise! }; | ||||||||||||||||||||||||
this.videoRecordingPromises.set(videoId, newPromiseHandler); | ||||||||||||||||||||||||
stdin.write(`video ${videoId} save\n`); | ||||||||||||||||||||||||
return promise; | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
async start() { | ||||||||||||||||||||||||
const simControllerBinary = path.join( | ||||||||||||||||||||||||
extensionContext.extensionPath, | ||||||||||||||||||||||||
|
@@ -60,33 +92,43 @@ export class Preview implements Disposable { | |||||||||||||||||||||||
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+)/); | ||||||||||||||||||||||||
const videoErrorMatch = line.match(/video_error replay (.*)/); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
const handlers = this.lastReplayPromise; | ||||||||||||||||||||||||
this.lastReplayPromise = undefined; | ||||||||||||||||||||||||
} else if (line.includes("video_ready") || line.includes("video_error")) { | ||||||||||||||||||||||||
// video response format for recordings looks as follows: | ||||||||||||||||||||||||
// video_ready <VIDEO_ID> <HTTP_URL> <FILE_URL> | ||||||||||||||||||||||||
// video_error <VIDEO_ID> <Error message> | ||||||||||||||||||||||||
const videoReadyMatch = line.match(/video_ready (\S+) (\S+) (\S+)/); | ||||||||||||||||||||||||
const videoErrorMatch = line.match(/video_error (\S+) (.*)/); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
const videoId = videoReadyMatch | ||||||||||||||||||||||||
? videoReadyMatch[1] | ||||||||||||||||||||||||
: videoErrorMatch | ||||||||||||||||||||||||
? videoErrorMatch[1] | ||||||||||||||||||||||||
: ""; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
if (!this.videoRecordingPromises.has(videoId)) { | ||||||||||||||||||||||||
throw new Error(`Invalid video ID: ${videoId}`); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
const handlers = this.videoRecordingPromises.get(videoId); | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After this we don't need to check if handlers is set as we do in the lines below |
||||||||||||||||||||||||
this.videoRecordingPromises.delete(videoId); | ||||||||||||||||||||||||
if (handlers && videoReadyMatch) { | ||||||||||||||||||||||||
// match array looks as follows: | ||||||||||||||||||||||||
// [0] - full match | ||||||||||||||||||||||||
// [1] - URL or error message | ||||||||||||||||||||||||
// [2] - File URL | ||||||||||||||||||||||||
const tempFileLocation = videoReadyMatch[2]; | ||||||||||||||||||||||||
// [1] - ID of the video | ||||||||||||||||||||||||
// [2] - URL or error message | ||||||||||||||||||||||||
// [3] - File URL | ||||||||||||||||||||||||
const tempFileLocation = videoReadyMatch[3]; | ||||||||||||||||||||||||
const ext = path.extname(tempFileLocation); | ||||||||||||||||||||||||
const fileName = workspace.name | ||||||||||||||||||||||||
? `${workspace.name}-RadonIDE-replay${ext}` | ||||||||||||||||||||||||
: `RadonIDE-replay${ext}`; | ||||||||||||||||||||||||
? `${workspace.name}-RadonIDE-${videoId}${ext}` | ||||||||||||||||||||||||
: `RadonIDE-${videoId}${ext}`; | ||||||||||||||||||||||||
handlers.resolve({ | ||||||||||||||||||||||||
url: videoReadyMatch[1], | ||||||||||||||||||||||||
url: videoReadyMatch[2], | ||||||||||||||||||||||||
tempFileLocation, | ||||||||||||||||||||||||
fileName, | ||||||||||||||||||||||||
}); | ||||||||||||||||||||||||
} else if (handlers && videoErrorMatch) { | ||||||||||||||||||||||||
handlers.reject(new Error(videoErrorMatch[1])); | ||||||||||||||||||||||||
handlers.reject(new Error(videoErrorMatch[2])); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
Logger.info("sim-server:", line); | ||||||||||||||||||||||||
|
@@ -102,39 +144,28 @@ export class Preview implements Disposable { | |||||||||||||||||||||||
this.subprocess?.stdin?.write("pointer show false\n"); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
public startRecording() { | ||||||||||||||||||||||||
this.sendCommandOrThrow(`video recording start -m -b 50\n`); // 50MB buffer for in-memory video | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
public stopRecording() { | ||||||||||||||||||||||||
this.sendCommandOrThrow(`video recording stop\n`); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
public captureRecording() { | ||||||||||||||||||||||||
return this.saveVideoWithID("recording"); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
public startReplays() { | ||||||||||||||||||||||||
const stdin = this.subprocess?.stdin; | ||||||||||||||||||||||||
if (!stdin) { | ||||||||||||||||||||||||
throw new Error("sim-server process not available"); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
stdin.write(`video replay start -m -b 50\n`); // 50MB buffer for in-memory video | ||||||||||||||||||||||||
this.sendCommandOrThrow(`video replay 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 stop\n`); | ||||||||||||||||||||||||
this.sendCommandOrThrow(`video replay 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 save\n`); | ||||||||||||||||||||||||
return promise; | ||||||||||||||||||||||||
return this.saveVideoWithID("replay"); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
public sendTouches(touches: Array<TouchPoint>, type: "Up" | "Move" | "Down") { | ||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
interface RecordingIconProps { | ||
color?: string; | ||
} | ||
|
||
const RecordingIcon = ({ color = "currentColor", ...rest }: RecordingIconProps) => ( | ||
<svg | ||
xmlns="http://www.w3.org/2000/svg" | ||
width={18} | ||
height={18} | ||
viewBox="0 0 24 24" | ||
fill="none" | ||
{...rest}> | ||
<path | ||
stroke={color} | ||
strokeWidth={1.25} | ||
strokeLinecap="round" | ||
strokeLinejoin="round" | ||
d="m17 13 5.223 3.482a.5.5 0 0 0 .777-.416V7.87a.5.5 0 0 0-.752-.432L17 10.5" | ||
/> | ||
<rect | ||
x="3" | ||
y="6" | ||
width="14" | ||
height="12" | ||
rx="2" | ||
stroke={color} | ||
strokeWidth={1.25} | ||
strokeLinecap="round" | ||
strokeLinejoin="round" | ||
/> | ||
</svg> | ||
); | ||
|
||
export default RecordingIcon; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
interface ReplayIconProps { | ||
color?: string; | ||
} | ||
|
||
const ReplayIcon = ({ color = "currentColor", ...rest }: ReplayIconProps) => ( | ||
<svg | ||
xmlns="http://www.w3.org/2000/svg" | ||
width={20} | ||
height={20} | ||
viewBox="0 0 24 24" | ||
fill="none" | ||
{...rest}> | ||
<path | ||
d="M12 13.8L19 18V6L12 10.2M12 18L12 6L3 12L12 18Z" | ||
stroke={color} | ||
strokeWidth={1.5} | ||
strokeLinecap="round" | ||
strokeLinejoin="round" | ||
/> | ||
</svg> | ||
); | ||
|
||
export default ReplayIcon; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For symmetry we could just call it "stopRecording" – I believe there's no way for sim server to stop recording without producing the recorded file anyways, is there?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, simserver allows starting, stopping, and saving video recordings without stopping them. CaptureReplay is responsible only for saving, unlike captureAndStopRecording, which saves and stops recordings, so name captureAndStopRecording is more appropriate
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what if we change it to
stopRecording(capture: boolean)