feat: gracefully stop and restart recordings during display resize#158
feat: gracefully stop and restart recordings during display resize#158hiroTamada merged 7 commits intomainfrom
Conversation
Instead of refusing display resize requests (409) when recordings are active, PatchDisplay now stops active recordings, performs the resize, then restarts them with the same ID and params. The pre-resize segment is preserved by renaming it (e.g. `<id>-before-resize-<ts>.mp4`). Live view sessions still block the resize as before. Co-authored-by: Cursor <cursoragent@cursor.com>
- Don't block recording restart when rename of old file fails (rename is best-effort to preserve the pre-resize segment) - Deep copy pointer fields in FFmpegRecordingParams.clone() so Params() callers cannot mutate recorder internals Co-authored-by: Cursor <cursoragent@cursor.com>
If DeregisterRecorder fails, the old recorder remains registered. Appending to the stopped list would cause RegisterRecorder to fail with a duplicate ID during restart. Co-authored-by: Cursor <cursoragent@cursor.com>
Instead of deregistering old recorders and renaming files, stopped recorders now remain registered (keeping their finalized files discoverable via the API) and new segments are started with unique suffixed IDs. This removes file-rename complexity and keeps all recording segments accessible through existing list/download endpoints. Also moves the graceful stop/restart logic outside the requireIdle block so it applies unconditionally when recordings are active. Made-with: Cursor
| } | ||
| } | ||
|
|
||
| // Gracefully stop active recordings so the resize can proceed. |
There was a problem hiding this comment.
Recording check blocks graceful stop/restart under defaults
High Severity
The requireIdle block at line 68–82 still includes isRecording in the resizableNow condition ((live == 0) && !isRecording), so any active recording causes a 409 return before the new stopActiveRecordings code at line 86 is ever reached. Since requireIdle defaults to true, the graceful stop-and-restart feature introduced by this PR is effectively unreachable under normal usage. Per the PR description, the live-view check and recording check were supposed to be separated, with only live sessions triggering a 409.
Additional Locations (1)
There was a problem hiding this comment.
This is intentional. The two modes are by design:
require_idle=true(default): strict mode — returns 409 if anything is active (live views or recordings). This preserves the original safety behavior.require_idle=false: graceful mode — stops active recordings, resizes, then starts new recording segments with suffixed IDs.
The graceful stop/restart path is reachable when the caller explicitly opts in via require_idle=false. A follow-up change in the kernel API will thread a user-facing flag down to this parameter.
Sayan-
left a comment
There was a problem hiding this comment.
I'm not opposed to this direction but I think there's material trade offs here:
- Users now get segments (e.g., rec-123 then rec-123-) instead of a single uninterrupted file.
- The original recording ID no longer represents the full post-resize timeline by itself.
- There’s no stitching in this PR, so consumers need to handle multiple files if they want a continuous playback experience.
So the UX benefit is reliability during resize. The UX cost is fragmentation unless product/UI layers present these segments as one logical recording, which is confusing in a different way
server/cmd/api/api/display.go
Outdated
| // New recording segments (with unique IDs) will be started after the resize. | ||
| stopped, stopErr := s.stopActiveRecordings(ctx) | ||
| if len(stopped) > 0 { | ||
| defer s.startNewRecordingSegments(context.WithoutCancel(ctx), stopped) |
There was a problem hiding this comment.
This leaves me a bit split brained on the design here. We don't block on all recordings being started so we're exposed to data loss but also increases the latency for this endpoint further
| // stopActiveRecordings gracefully stops every recording that is currently in | ||
| // progress. The old recorders remain registered in the manager so their | ||
| // finalized files stay discoverable and downloadable. It returns info needed | ||
| // to start a new recording segment for each stopped recorder. | ||
| func (s *ApiService) stopActiveRecordings(ctx context.Context) ([]stoppedRecordingInfo, error) { | ||
| log := logger.FromContext(ctx) | ||
| var stopped []stoppedRecordingInfo |
There was a problem hiding this comment.
I think this is another place where we're breaking the record manager interface. I wonder if it's simpler in thinking of these operations as stop and clone but nbd
also segments is a new concept. you could encode it into the record manager interface and treat the replays as disjoint and perhaps join them back together at the end. unclear
| if p.MaxSizeInMB != nil { | ||
| v := *p.MaxSizeInMB | ||
| c.MaxSizeInMB = &v | ||
| } | ||
| if p.MaxDurationInSeconds != nil { | ||
| v := *p.MaxDurationInSeconds | ||
| c.MaxDurationInSeconds = &v | ||
| } |
There was a problem hiding this comment.
users don't really use these params but these break entirely over the boundary (e.g. initial replay was expected to be bound to size X. due to resize we'll exceed)
|
yeah maybe I will do stitching @Sayan- |
When a display resize splits a recording into segments, the new segment's MaxDurationInSeconds and MaxSizeInMB are now reduced by what the prior segment already consumed, keeping cumulative usage within the originally requested limits. Made-with: Cursor
Move segment restart from a defer (which fires on all return paths) to an explicit call after the resize succeeds. This avoids restarting recordings at the old resolution when the stop partially fails or the resize itself errors out. Also round up fractional MB when computing consumed file size budget to prevent cumulative overestimation across multiple resize cycles. Made-with: Cursor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Move segment restart back to a defer so recordings are restarted regardless of whether the resize succeeds. If the resize fails the display is still at the old resolution, so restarting there is correct. Losing recording data is worse than a brief gap. The defer is placed after the stopErr check so it only fires when all stops succeeded cleanly. Made-with: Cursor


Summary
require_idle=false, active FFmpeg recordings are gracefully stopped and new recording segments are started with unique suffixed IDs (e.g.my-rec->my-rec-1709312345678).GET /recording/listand downloadable viaGET /recording/download.require_idle=true, the original strict behavior is preserved: resize is refused with409 Conflictif any live view or recording is active.Params()accessor with deep-copyclone()onFFmpegRecordingParamsto safely capture recording parameters before restart.Test plan
stopActiveRecordings(verifies old recorder stays registered but stopped)startNewRecordingSegments(verifies new segment created with suffixed ID)Params()deep-copy testNote
Medium Risk
Touches recording lifecycle during display resize (stop/finalize/restart) and introduces async segment creation, which could affect recording continuity and resource usage if edge cases aren’t handled.
Overview
Display resize (
PATCH /display) now coordinates with active recordings. The handler stops any active FFmpeg recordings before attempting the resolution change, and if the resize succeeds it starts new recording segments with unique suffixed IDs so old segments remain discoverable/downloadable.Recording restart preserves original limits. New segments reuse captured recorder params via a new
FFmpegRecorder.Params()deep-copy accessor, and adjust remainingMaxDurationInSeconds/MaxSizeInMBbased on prior segment metadata and file size.Adds focused unit tests covering stop-and-preserve behavior, segment restart/ID suffixing, round-trip stop+restart, budget adjustment logic, and the new params deep-copy behavior.
Written by Cursor Bugbot for commit f4d3313. This will update automatically on new commits. Configure here.