Skip to content

Commit 13d3802

Browse files
committed
Refactor FFmpegRecorder
1 parent 36c7f00 commit 13d3802

File tree

2 files changed

+34
-59
lines changed

2 files changed

+34
-59
lines changed

server/cmd/api/api/api.go

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -182,29 +182,8 @@ func (s *ApiService) DownloadRecording(ctx context.Context, req oapi.DownloadRec
182182

183183
out, meta, err := rec.Recording(ctx)
184184
if err != nil {
185-
if errors.Is(err, recorder.ErrRecordingFinalizing) {
186-
// Wait for finalization to complete instead of asking client to retry
187-
log.Info("waiting for recording finalization", "recorder_id", recorderID)
188-
ffmpegRec, ok := rec.(*recorder.FFmpegRecorder)
189-
if !ok {
190-
log.Error("failed to cast recorder to FFmpegRecorder", "recorder_id", recorderID)
191-
return oapi.DownloadRecording500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "internal error"}}, nil
192-
}
193-
// WaitForFinalization blocks until finalization completes and returns the result
194-
if finalizeErr := ffmpegRec.WaitForFinalization(ctx); finalizeErr != nil {
195-
log.Error("finalization failed", "err", finalizeErr, "recorder_id", recorderID)
196-
return oapi.DownloadRecording500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to finalize recording"}}, nil
197-
}
198-
// Finalization complete, retry getting the recording
199-
out, meta, err = rec.Recording(ctx)
200-
if err != nil {
201-
log.Error("failed to get recording after finalization", "err", err, "recorder_id", recorderID)
202-
return oapi.DownloadRecording500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to get recording"}}, nil
203-
}
204-
} else {
205-
log.Error("failed to get recording", "err", err, "recorder_id", recorderID)
206-
return oapi.DownloadRecording500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to get recording"}}, nil
207-
}
185+
log.Error("failed to get recording", "err", err, "recorder_id", recorderID)
186+
return oapi.DownloadRecording500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to get recording"}}, nil
208187
}
209188

210189
// short-circuit if the recording is still in progress and the file is arbitrary small
@@ -246,15 +225,9 @@ func (s *ApiService) DeleteRecording(ctx context.Context, req oapi.DeleteRecordi
246225
return oapi.DeleteRecording400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "recording must be stopped first"}}, nil
247226
}
248227

249-
if err := rec.Delete(ctx); err != nil {
250-
if errors.Is(err, recorder.ErrRecordingFinalizing) {
251-
log.Info("recording is being finalized, client should retry", "recorder_id", recorderID)
252-
return oapi.DeleteRecording409JSONResponse{ConflictErrorJSONResponse: oapi.ConflictErrorJSONResponse{Message: "recording is being finalized, please retry in a few seconds"}}, nil
253-
}
254-
if !errors.Is(err, os.ErrNotExist) {
255-
log.Error("failed to delete recording", "err", err, "recorder_id", recorderID)
256-
return oapi.DeleteRecording500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to delete recording"}}, nil
257-
}
228+
if err := rec.Delete(ctx); err != nil && !errors.Is(err, os.ErrNotExist) {
229+
log.Error("failed to delete recording", "err", err, "recorder_id", recorderID)
230+
return oapi.DeleteRecording500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to delete recording"}}, nil
258231
}
259232

260233
log.Info("recording deleted", "recorder_id", recorderID)

server/lib/recorder/ffmpeg.go

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,6 @@ const (
3232
exitCodeProcessDoneMinValue = -1
3333
)
3434

35-
// ErrRecordingFinalizing is returned when attempting to access a recording that is
36-
// currently being finalized (remuxed to add duration metadata).
37-
var ErrRecordingFinalizing = errors.New("recording is being finalized")
38-
3935
// FFmpegRecorder encapsulates an FFmpeg recording session with platform-specific screen capture.
4036
// It manages the lifecycle of a single FFmpeg process and provides thread-safe operations.
4137
type FFmpegRecorder struct {
@@ -237,14 +233,6 @@ func (fr *FFmpegRecorder) Stop(ctx context.Context) error {
237233
return fr.finalizeRecording(ctx)
238234
}
239235

240-
// WaitForFinalization blocks until finalization completes and returns the result.
241-
// If finalization hasn't started, it will be triggered. If already complete, returns
242-
// the cached result immediately. This is useful for callers like the download handler
243-
// that need to wait for finalization before accessing the recording.
244-
func (fr *FFmpegRecorder) WaitForFinalization(ctx context.Context) error {
245-
return fr.finalizeRecording(ctx)
246-
}
247-
248236
// ForceStop immediately terminates the recording process.
249237
func (fr *FFmpegRecorder) ForceStop(ctx context.Context) error {
250238
log := logger.FromContext(ctx)
@@ -378,22 +366,25 @@ func (fr *FFmpegRecorder) Metadata() *RecordingMetadata {
378366
}
379367

380368
// Recording returns the recording file as an io.ReadCloser.
381-
// Returns ErrRecordingFinalizing if the recording is currently being finalized.
369+
// If the recording has exited but finalization is pending, this method blocks
370+
// until finalization completes.
382371
func (fr *FFmpegRecorder) Recording(ctx context.Context) (io.ReadCloser, *RecordingMetadata, error) {
383372
fr.mu.Lock()
384373
if fr.deleted {
385374
fr.mu.Unlock()
386375
return nil, nil, fmt.Errorf("recording deleted: %w", os.ErrNotExist)
387376
}
388-
// Block access while finalization is pending or in progress.
389-
// This covers both the race window (after process exit, before finalization starts)
390-
// and during active finalization.
391-
if fr.exitCode >= exitCodeProcessDoneMinValue && !fr.finalizeComplete {
392-
fr.mu.Unlock()
393-
return nil, nil, ErrRecordingFinalizing
394-
}
377+
// If recording has exited but finalization is pending or in progress, wait for it.
378+
// This hides finalization as an implementation detail from callers.
379+
needsFinalization := fr.exitCode >= exitCodeProcessDoneMinValue && !fr.finalizeComplete
395380
fr.mu.Unlock()
396381

382+
if needsFinalization {
383+
if err := fr.finalizeRecording(ctx); err != nil {
384+
return nil, nil, fmt.Errorf("failed to finalize recording: %w", err)
385+
}
386+
}
387+
397388
file, err := os.Open(fr.outputPath)
398389
if err != nil {
399390
return nil, nil, fmt.Errorf("failed to open recording file: %w", err)
@@ -416,18 +407,29 @@ func (fr *FFmpegRecorder) Recording(ctx context.Context) (io.ReadCloser, *Record
416407
}
417408

418409
// Delete removes the recording file from disk.
419-
// Returns ErrRecordingFinalizing if the recording is currently being finalized.
410+
// If the recording has exited but finalization is pending, this method blocks
411+
// until finalization completes before deleting.
420412
func (fr *FFmpegRecorder) Delete(ctx context.Context) error {
421413
fr.mu.Lock()
422-
defer fr.mu.Unlock()
423414
if fr.deleted {
415+
fr.mu.Unlock()
424416
return nil // already deleted
425417
}
426-
// Block deletion while finalization is pending or in progress.
427-
// This covers both the race window (after process exit, before finalization starts)
428-
// and during active finalization.
429-
if fr.exitCode >= exitCodeProcessDoneMinValue && !fr.finalizeComplete {
430-
return ErrRecordingFinalizing
418+
// If recording has exited but finalization is pending or in progress, wait for it.
419+
// We need to wait because deleting during finalization would corrupt the remux operation.
420+
needsFinalization := fr.exitCode >= exitCodeProcessDoneMinValue && !fr.finalizeComplete
421+
fr.mu.Unlock()
422+
423+
if needsFinalization {
424+
// Wait for finalization to complete. We don't fail the delete if finalization
425+
// fails - the caller wants the file gone regardless.
426+
_ = fr.finalizeRecording(ctx)
427+
}
428+
429+
fr.mu.Lock()
430+
defer fr.mu.Unlock()
431+
if fr.deleted {
432+
return nil // deleted while we were waiting
431433
}
432434
if err := os.Remove(fr.outputPath); err != nil && !os.IsNotExist(err) {
433435
return fmt.Errorf("failed to delete recording file: %w", err)

0 commit comments

Comments
 (0)