-
Notifications
You must be signed in to change notification settings - Fork 33
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?
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
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.
I like the changes UI-wise, one suggestion I'd have is to maybe try moving the red dot and timer into the recording button. That is, when we hit record, the red dot displays there instead of "stop" button. This way it should be cleaner that the recording is in progress as the top bar is much more visible than the recording progress component that suddenly appears in top left edge when no one expects it.
@@ -102,23 +116,23 @@ export class Preview implements Disposable { | |||
this.subprocess?.stdin?.write("pointer show false\n"); | |||
} | |||
|
|||
public startReplays() { | |||
public startVideoRecording(videoId: string) { |
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.
It is a wrong level of abstraction here. We assume in the callback that IDs can be "recording" and "replay" so we shouldn't allow any string as videoID, as if anything else is passed, the callback would never trigger.
One way would be to specify allowed values in typescript, but I think that this method shouldn't expose videoID as a parameter and just make it an implementation detail.
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.
I've made the changes. Let me know if they look as you expected. Additionally, I've added sendCommandOrThrow
and handleVideoRecordingPromise
to reduce code duplication.
stopRecording(): void; | ||
captureRecording(): Promise<RecordingData>; |
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.
can we only have one method for stopping that returns the data? What is the point of having both?
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.
You're right, one is enough.
…nd captureRecording
@@ -145,6 +145,8 @@ export interface ProjectInterface { | |||
getDeepLinksHistory(): Promise<string[]>; | |||
openDeepLink(link: string): Promise<void>; | |||
|
|||
startRecording(): void; | |||
captureAndStopRecording(): Promise<RecordingData>; |
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)
public stopRecording() { | ||
return this.preview?.stopRecording(); | ||
} | ||
|
||
public async captureRecording() { | ||
if (!this.preview) { | ||
throw new Error("Preview not started"); | ||
} | ||
return this.preview.captureRecording(); |
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.
If we only have one method in the Project interface, I don't think there's a lot of value in having separate methods at this level given we only forward the call down
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.
I removed the stopRecording calls after reloading and changing device settings, as the first was causing a bug with recordings not stopping in UI, and the second was unwanted behaviour (we don't want to stop recording on eg. font change), so there is no need for the stopRecording method, and we now use captureAndStopRecording instead.
public stopReplays() { | ||
return this.preview?.stopReplays(); | ||
} |
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.
I think I understand what was the confusion with stop / start vs capture. With replays we have option to enable / disable replays and then we use "capture" when we want to save replay. Maybe we should rename startReplay
and stopReplay
to enableReplay
/ disableReplay
to avoid this confusion?
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.
I agree, names enableReplay
and disableReplay
seem more accurate.
|
||
const lastPromiseKey = | ||
promiseType === "recording" ? "lastRecordingPromise" : "lastReplayPromise"; | ||
const lastPromise = this[lastPromiseKey]; |
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.
I don't like using this
like that. If we access it this way, I'd rather have a single map of promises that we remember. This will be a bit over-abstracted but more clear to read:
So instead of haveing lastRecordingPromise
and lastReplayPromise
we'd have:
private videoRecordingPromises: Map<string, VideoRecordingPromiseHandlers>
Then we'd use the map to lookup the last promise.
This way we wouldn't need to match videoId agains constants in section where we wait for video_ready
and instead just lookup the ID in that map
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.
Right, this mapping could be useful in the future if we decide to introduce additional recordings.
It also removes stopRecording calls after reload or device settings change.
VideoRecordingPromiseType, | ||
VideoRecordingPromiseHandlers | undefined |
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.
I think we can just use string
here as key, the keys that we use are not important here because we don't iterate over the map anywhere and we also don't have any special handling based on the key now.
We should not allow undefined value. Just remove entry from map when we want to reset it.
this.videoRecordingPromises = new Map< | ||
VideoRecordingPromiseType, | ||
VideoRecordingPromiseHandlers | undefined | ||
>(); |
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.
this is an empty map, we can construct it in the line where we define it
this.videoRecordingPromises.set("recording", undefined); | ||
this.videoRecordingPromises.set("replay", undefined); |
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.
we don't need undefined value there. If its not there map.get(key)
will return undefined
? videoErrorMatch[1] | ||
: ""; | ||
|
||
if (!this.videoRecordingPromises.has(videoId as VideoRecordingPromiseType)) { |
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.
if we just use string as key, we wouldn't need as
here
|
||
const handlers = this.videoRecordingPromises.get(videoId as VideoRecordingPromiseType); |
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.
same comment about as
. We should just get it earlier and throw when handlers
is undefined rather than using .has
|
||
const handlers = this.videoRecordingPromises.get(videoId as VideoRecordingPromiseType); | ||
this.videoRecordingPromises.set(videoId as VideoRecordingPromiseType, undefined); |
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.
.delete(videoId)
this.lastReplayPromise = { resolve: resolvePromise!, reject: rejectPromise! }; | ||
stdin.write(`video replay save\n`); | ||
return promise; | ||
return this.handleVideoRecordingPromise("replay"); |
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.
handleVideoRecordingPromise
is a weird name. There's no promise at this point, so what is this method "handling"??
I'd just name it saveVideo
or saveVideoWithID
@@ -126,6 +130,46 @@ function PreviewView() { | |||
} | |||
}; | |||
|
|||
const MAX_RECORDING_TIME = 300; |
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.
append units to this constant such that it is clear, e.g. MAX_RECORDING_TIME_SEC
const interval = setInterval(() => { | ||
setRecordingTime((prevRecordingTime) => { | ||
if (prevRecordingTime >= MAX_RECORDING_TIME - 1) { | ||
clearInterval(interval); | ||
handleRecording(); | ||
return MAX_RECORDING_TIME; | ||
} | ||
return prevRecordingTime + 1; | ||
}); | ||
}, 1000); |
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.
why there's an interval here, can't we just use setTimeout here?
const recordingDataPromise = this.preview.captureRecording(); | ||
this.preview?.stopRecording(); | ||
return recordingDataPromise; |
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, it'd be better if this method just proxied the call down to preview instead of making two calls
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 comment
The reason will be displayed to describe this comment to others. Learn more.
if (!this.videoRecordingPromises.has(videoId)) { | |
throw new Error(`Invalid video ID: ${videoId}`); | |
} | |
const handlers = this.videoRecordingPromises.get(videoId); | |
const handlers = this.videoRecordingPromises.get(videoId); | |
if (!handlers) { | |
throw new Error(`Invalid video ID: ${videoId}`); | |
} | |
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.
After this we don't need to check if handlers is set as we do in the lines below
setTimeout(() => { | ||
setRecordingTime((prevRecordingTime) => { | ||
if (prevRecordingTime >= MAX_RECORDING_TIME_SEC - 1) { | ||
handleRecording(); | ||
return MAX_RECORDING_TIME_SEC; | ||
} | ||
return prevRecordingTime + 1; | ||
}); | ||
}, 1000); |
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.
I don't understand this bit. Maybe it should use interval actually. Now it doesn't do effect cleanup which may result in this timer running indefinitely until the timeout is hit regardless of whether the video is stopped or not.
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.
I've tested it and it behaves weird. After stopping recording before the time limit, setTimeout runs once more with prevRecordingTime set to 0 by handleRecording.
Adding a cleanup function would be a better solution.
This PR introduces recording controls enhancement:
Test plan:
Before:
After:
Screen.Recording.2024-11-18.at.18.53.39.mov
resolves: #708