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 recording controls enhancement #733

Open
wants to merge 37 commits into
base: main
Choose a base branch
from

Conversation

p-malecki
Copy link
Contributor

@p-malecki p-malecki commented Nov 15, 2024

This PR introduces recording controls enhancement:

  • introduces a toggle button to start/stop recordings
  • opens a save dialog immediately after pressing stop (in future, we could reuse replayOverlay for video previews and edits)
  • ignores and does not save recordings under 1 second
  • adds a maximum limit of 5 minutes for recordings, after which it automatically pauses and opens the save dialog
  • adds timestamps to the filenames of video recordings
  • eliminates text labels from the Replay and Logs buttons to create more space in the top button group
  • updates the Replay button icon

Test plan:

  1. Start a recording, interact with the test app, then stop the recording after a few seconds and save the file to a selected location. Open the saved recording to verify that the app's screen was correctly captured.
  2. Start a recording and stop it immediately to confirm that the save dialog does not appear for recordings under 1 second.
  3. Test the maximum recording time to ensure that it automatically stops and opens the save dialog after the 5-minute limit is reached.
  4. Ensure that the Replay button is disabled during recording.
  5. After capturing a recording, test the Replay functionality to ensure it still works correctly.
  6. Verify that the timestamps on the recorded video filenames reflect the correct date and time, matching the system clock.

Before:
Screenshot 2024-11-15 at 17 22 48

After:
Screenshot 2024-11-15 at 17 23 34
Screenshot 2024-11-18 at 18 52 57

Screen.Recording.2024-11-18.at.18.53.39.mov

resolves: #708

Copy link

vercel bot commented Nov 15, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
radon-ide ✅ Ready (Inspect) Visit Preview 💬 Add feedback Nov 19, 2024 3:58pm

Copy link
Member

@kmagiera kmagiera left a 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) {
Copy link
Member

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.

Copy link
Contributor Author

@p-malecki p-malecki Nov 18, 2024

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.

Comment on lines 149 to 150
stopRecording(): void;
captureRecording(): Promise<RecordingData>;
Copy link
Member

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?

Copy link
Contributor Author

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.

@@ -145,6 +145,8 @@ export interface ProjectInterface {
getDeepLinksHistory(): Promise<string[]>;
openDeepLink(link: string): Promise<void>;

startRecording(): void;
captureAndStopRecording(): Promise<RecordingData>;
Copy link
Member

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?

Copy link
Contributor Author

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

Copy link
Member

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)

Comment on lines 62 to 70
public stopRecording() {
return this.preview?.stopRecording();
}

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

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

Copy link
Contributor Author

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.

Comment on lines 80 to 82
public stopReplays() {
return this.preview?.stopReplays();
}
Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, names enableReplayand disableReplay seem more accurate.


const lastPromiseKey =
promiseType === "recording" ? "lastRecordingPromise" : "lastReplayPromise";
const lastPromise = this[lastPromiseKey];
Copy link
Member

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

Copy link
Contributor Author

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.
Comment on lines 18 to 19
VideoRecordingPromiseType,
VideoRecordingPromiseHandlers | undefined
Copy link
Member

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.

Comment on lines 25 to 28
this.videoRecordingPromises = new Map<
VideoRecordingPromiseType,
VideoRecordingPromiseHandlers | undefined
>();
Copy link
Member

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

Comment on lines 30 to 31
this.videoRecordingPromises.set("recording", undefined);
this.videoRecordingPromises.set("replay", undefined);
Copy link
Member

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)) {
Copy link
Member

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);
Copy link
Member

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);
Copy link
Member

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");
Copy link
Member

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;
Copy link
Member

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

Comment on lines 159 to 168
const interval = setInterval(() => {
setRecordingTime((prevRecordingTime) => {
if (prevRecordingTime >= MAX_RECORDING_TIME - 1) {
clearInterval(interval);
handleRecording();
return MAX_RECORDING_TIME;
}
return prevRecordingTime + 1;
});
}, 1000);
Copy link
Member

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?

Comment on lines 66 to 68
const recordingDataPromise = this.preview.captureRecording();
this.preview?.stopRecording();
return recordingDataPromise;
Copy link
Member

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

Comment on lines 108 to 112
if (!this.videoRecordingPromises.has(videoId)) {
throw new Error(`Invalid video ID: ${videoId}`);
}

const handlers = this.videoRecordingPromises.get(videoId);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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}`);
}

Copy link
Member

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

Comment on lines 159 to 167
setTimeout(() => {
setRecordingTime((prevRecordingTime) => {
if (prevRecordingTime >= MAX_RECORDING_TIME_SEC - 1) {
handleRecording();
return MAX_RECORDING_TIME_SEC;
}
return prevRecordingTime + 1;
});
}, 1000);
Copy link
Member

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.

Copy link
Contributor Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add controls for normal screen recording (start / stop)
2 participants