From ccb53baaea0b4f40bc40b875e1605df483426e28 Mon Sep 17 00:00:00 2001 From: rgarcia <72655+rgarcia@users.noreply.github.com> Date: Mon, 15 Jun 2026 14:54:53 +0000 Subject: [PATCH 1/2] recorder: add named markers injected as MP4 chapters Add a Mark(name) API to the recorder and a POST /recording/mark endpoint that records named, timestamped markers during an active recording. At finalize the markers are written into the output MP4 as chapter markers via an ffmetadata input, so they can be scrubbed to in any tool that reads MP4 chapters (ffprobe, QuickTime, VLC, NLEs). The feature is additive and backwards-compatible: with no markers the remux command is byte-identical to before, and any marker/metadata failure falls back to the plain remux so a recording is never corrupted. Co-Authored-By: Claude Opus 4.7 --- server/cmd/api/api/api.go | 36 + server/cmd/api/api/api_test.go | 78 ++ server/lib/oapi/oapi.go | 1012 +++++++++++------ server/lib/recorder/ffmpeg.go | 77 +- server/lib/recorder/markers.go | 92 ++ .../lib/recorder/markers_integration_test.go | 119 ++ server/lib/recorder/markers_test.go | 247 ++++ server/lib/recorder/recorder.go | 4 + server/openapi.yaml | 54 + 9 files changed, 1356 insertions(+), 363 deletions(-) create mode 100644 server/lib/recorder/markers.go create mode 100644 server/lib/recorder/markers_integration_test.go create mode 100644 server/lib/recorder/markers_test.go diff --git a/server/cmd/api/api/api.go b/server/cmd/api/api/api.go index b9b0a346..1ee18820 100644 --- a/server/cmd/api/api/api.go +++ b/server/cmd/api/api/api.go @@ -234,6 +234,42 @@ func (s *ApiService) StopRecording(ctx context.Context, req oapi.StopRecordingRe return oapi.StopRecording200Response{}, nil } +func (s *ApiService) MarkRecording(ctx context.Context, req oapi.MarkRecordingRequestObject) (oapi.MarkRecordingResponseObject, error) { + log := logger.FromContext(ctx) + + if req.Body == nil { + return oapi.MarkRecording400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "request body is required"}}, nil + } + + recorderID := s.defaultRecorderID + if req.Body.Id != nil && *req.Body.Id != "" { + recorderID = *req.Body.Id + } + + rec, exists := s.recordManager.GetRecorder(recorderID) + if !exists { + log.Error("attempted to mark non-existent recording", "recorder_id", recorderID) + return oapi.MarkRecording409JSONResponse{ConflictErrorJSONResponse: oapi.ConflictErrorJSONResponse{Message: "no active recording to mark"}}, nil + } + + name, offsetMs, err := rec.Mark(req.Body.Name) + if err != nil { + switch { + case errors.Is(err, recorder.ErrNotRecording): + log.Error("attempted to mark recording that is not in progress", "recorder_id", recorderID) + return oapi.MarkRecording409JSONResponse{ConflictErrorJSONResponse: oapi.ConflictErrorJSONResponse{Message: "no active recording to mark"}}, nil + case errors.Is(err, recorder.ErrInvalidMarkerName): + log.Warn("invalid marker name", "recorder_id", recorderID) + return oapi.MarkRecording400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "marker name must be non-empty and at most 200 characters"}}, nil + default: + log.Error("failed to mark recording", "err", err, "recorder_id", recorderID) + return oapi.MarkRecording500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to mark recording"}}, nil + } + } + + return oapi.MarkRecording201JSONResponse{Name: name, OffsetMs: offsetMs}, nil +} + const ( minRecordingSizeInBytes = 100 ) diff --git a/server/cmd/api/api/api_test.go b/server/cmd/api/api/api_test.go index 4f2c2165..7945450b 100644 --- a/server/cmd/api/api/api_test.go +++ b/server/cmd/api/api/api_test.go @@ -126,6 +126,71 @@ func TestApiService_StopRecording(t *testing.T) { }) } +func TestApiService_MarkRecording(t *testing.T) { + ctx := context.Background() + name := "checkpoint" + + t.Run("no recorder maps to 409", func(t *testing.T) { + mgr := recorder.NewFFmpegManager() + svc, err := newSvc(t, mgr) + require.NoError(t, err) + + resp, err := svc.MarkRecording(ctx, oapi.MarkRecordingRequestObject{Body: &oapi.MarkRecordingJSONRequestBody{Name: name}}) + require.NoError(t, err) + require.IsType(t, oapi.MarkRecording409JSONResponse{}, resp) + }) + + t.Run("missing body maps to 400", func(t *testing.T) { + mgr := recorder.NewFFmpegManager() + svc, err := newSvc(t, mgr) + require.NoError(t, err) + + resp, err := svc.MarkRecording(ctx, oapi.MarkRecordingRequestObject{}) + require.NoError(t, err) + require.IsType(t, oapi.MarkRecording400JSONResponse{}, resp) + }) + + t.Run("not recording maps to 409", func(t *testing.T) { + mgr := recorder.NewFFmpegManager() + rec := &mockRecorder{id: "default", markErr: recorder.ErrNotRecording} + require.NoError(t, mgr.RegisterRecorder(ctx, rec)) + + svc, err := newSvc(t, mgr) + require.NoError(t, err) + resp, err := svc.MarkRecording(ctx, oapi.MarkRecordingRequestObject{Body: &oapi.MarkRecordingJSONRequestBody{Name: name}}) + require.NoError(t, err) + require.IsType(t, oapi.MarkRecording409JSONResponse{}, resp) + require.True(t, rec.markCalled) + }) + + t.Run("invalid name maps to 400", func(t *testing.T) { + mgr := recorder.NewFFmpegManager() + rec := &mockRecorder{id: "default", markErr: recorder.ErrInvalidMarkerName} + require.NoError(t, mgr.RegisterRecorder(ctx, rec)) + + svc, err := newSvc(t, mgr) + require.NoError(t, err) + resp, err := svc.MarkRecording(ctx, oapi.MarkRecordingRequestObject{Body: &oapi.MarkRecordingJSONRequestBody{Name: " "}}) + require.NoError(t, err) + require.IsType(t, oapi.MarkRecording400JSONResponse{}, resp) + }) + + t.Run("success returns 201 with offset", func(t *testing.T) { + mgr := recorder.NewFFmpegManager() + rec := &mockRecorder{id: "default", isRecordingFlag: true, markOffsetMs: 1234} + require.NoError(t, mgr.RegisterRecorder(ctx, rec)) + + svc, err := newSvc(t, mgr) + require.NoError(t, err) + resp, err := svc.MarkRecording(ctx, oapi.MarkRecordingRequestObject{Body: &oapi.MarkRecordingJSONRequestBody{Name: name}}) + require.NoError(t, err) + r, ok := resp.(oapi.MarkRecording201JSONResponse) + require.True(t, ok, "expected 201 response, got %T", resp) + assert.Equal(t, name, r.Name) + assert.Equal(t, int64(1234), r.OffsetMs) + }) +} + func TestApiService_DownloadRecording(t *testing.T) { ctx := context.Background() @@ -226,6 +291,11 @@ type mockRecorder struct { recordingErr error recordingData []byte deleted bool + + markCalled bool + markName string + markOffsetMs int64 + markErr error } func (m *mockRecorder) ID() string { return m.id } @@ -257,6 +327,14 @@ func (m *mockRecorder) ForceStop(ctx context.Context) error { return nil } +func (m *mockRecorder) Mark(name string) (string, int64, error) { + m.markCalled = true + if m.markErr != nil { + return "", 0, m.markErr + } + return name, m.markOffsetMs, nil +} + func (m *mockRecorder) IsRecording(ctx context.Context) bool { return m.isRecordingFlag } func (m *mockRecorder) Recording(ctx context.Context) (io.ReadCloser, *recorder.RecordingMetadata, error) { diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index 901aa0a9..7ad8ef0d 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -3260,6 +3260,24 @@ type LogEvent struct { Timestamp time.Time `json:"timestamp"` } +// MarkRecordingRequest defines model for MarkRecordingRequest. +type MarkRecordingRequest struct { + // Id Identifier of the recorder to mark. Alphanumeric or hyphen. + Id *string `json:"id,omitempty"` + + // Name Name of the marker, used as the MP4 chapter title. + Name string `json:"name"` +} + +// MarkRecordingResult defines model for MarkRecordingResult. +type MarkRecordingResult struct { + // Name Name of the recorded marker. + Name string `json:"name"` + + // OffsetMs Provisional offset of the marker from the recording start, in milliseconds, measured against the start time at mark time. The authoritative offset is the chapter start written at finalize. + OffsetMs int64 `json:"offsetMs"` +} + // MousePositionResponse defines model for MousePositionResponse. type MousePositionResponse struct { // X X coordinate of the cursor @@ -3955,6 +3973,9 @@ type ProcessStdinJSONRequestBody = ProcessStdinRequest // DeleteRecordingJSONRequestBody defines body for DeleteRecording for application/json ContentType. type DeleteRecordingJSONRequestBody = DeleteRecordingRequest +// MarkRecordingJSONRequestBody defines body for MarkRecording for application/json ContentType. +type MarkRecordingJSONRequestBody = MarkRecordingRequest + // StartRecordingJSONRequestBody defines body for StartRecording for application/json ContentType. type StartRecordingJSONRequestBody = StartRecordingRequest @@ -5162,6 +5183,11 @@ type ClientInterface interface { // ListRecorders request ListRecorders(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + // MarkRecordingWithBody request with any body + MarkRecordingWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + MarkRecording(ctx context.Context, body MarkRecordingJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // StartRecordingWithBody request with any body StartRecordingWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -6064,6 +6090,30 @@ func (c *Client) ListRecorders(ctx context.Context, reqEditors ...RequestEditorF return c.Client.Do(req) } +func (c *Client) MarkRecordingWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewMarkRecordingRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) MarkRecording(ctx context.Context, body MarkRecordingJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewMarkRecordingRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) StartRecordingWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewStartRecordingRequestWithBody(c.Server, contentType, body) if err != nil { @@ -8101,6 +8151,46 @@ func NewListRecordersRequest(server string) (*http.Request, error) { return req, nil } +// NewMarkRecordingRequest calls the generic MarkRecording builder with application/json body +func NewMarkRecordingRequest(server string, body MarkRecordingJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewMarkRecordingRequestWithBody(server, "application/json", bodyReader) +} + +// NewMarkRecordingRequestWithBody generates requests for MarkRecording with any type of body +func NewMarkRecordingRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/recording/mark") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewStartRecordingRequest calls the generic StartRecording builder with application/json body func NewStartRecordingRequest(server string, body StartRecordingJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -8679,6 +8769,11 @@ type ClientWithResponsesInterface interface { // ListRecordersWithResponse request ListRecordersWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListRecordersResponse, error) + // MarkRecordingWithBodyWithResponse request with any body + MarkRecordingWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*MarkRecordingResponse, error) + + MarkRecordingWithResponse(ctx context.Context, body MarkRecordingJSONRequestBody, reqEditors ...RequestEditorFn) (*MarkRecordingResponse, error) + // StartRecordingWithBodyWithResponse request with any body StartRecordingWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*StartRecordingResponse, error) @@ -9815,6 +9910,31 @@ func (r ListRecordersResponse) StatusCode() int { return 0 } +type MarkRecordingResponse struct { + Body []byte + HTTPResponse *http.Response + JSON201 *MarkRecordingResult + JSON400 *BadRequestError + JSON409 *ConflictError + JSON500 *InternalError +} + +// Status returns HTTPResponse.Status +func (r MarkRecordingResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r MarkRecordingResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type StartRecordingResponse struct { Body []byte HTTPResponse *http.Response @@ -10645,6 +10765,23 @@ func (c *ClientWithResponses) ListRecordersWithResponse(ctx context.Context, req return ParseListRecordersResponse(rsp) } +// MarkRecordingWithBodyWithResponse request with arbitrary body returning *MarkRecordingResponse +func (c *ClientWithResponses) MarkRecordingWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*MarkRecordingResponse, error) { + rsp, err := c.MarkRecordingWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseMarkRecordingResponse(rsp) +} + +func (c *ClientWithResponses) MarkRecordingWithResponse(ctx context.Context, body MarkRecordingJSONRequestBody, reqEditors ...RequestEditorFn) (*MarkRecordingResponse, error) { + rsp, err := c.MarkRecording(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseMarkRecordingResponse(rsp) +} + // StartRecordingWithBodyWithResponse request with arbitrary body returning *StartRecordingResponse func (c *ClientWithResponses) StartRecordingWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*StartRecordingResponse, error) { rsp, err := c.StartRecordingWithBody(ctx, contentType, body, reqEditors...) @@ -12543,6 +12680,53 @@ func ParseListRecordersResponse(rsp *http.Response) (*ListRecordersResponse, err return response, nil } +// ParseMarkRecordingResponse parses an HTTP response from a MarkRecordingWithResponse call +func ParseMarkRecordingResponse(rsp *http.Response) (*MarkRecordingResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &MarkRecordingResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: + var dest MarkRecordingResult + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON201 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest BadRequestError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest ConflictError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON409 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + // ParseStartRecordingResponse parses an HTTP response from a StartRecordingWithResponse call func ParseStartRecordingResponse(rsp *http.Response) (*StartRecordingResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -12984,6 +13168,9 @@ type ServerInterface interface { // List all recorders // (GET /recording/list) ListRecorders(w http.ResponseWriter, r *http.Request) + // Add a named marker to an in-progress recording + // (POST /recording/mark) + MarkRecording(w http.ResponseWriter, r *http.Request) // Start a screen recording. Only one recording per ID can be registered at a time. // (POST /recording/start) StartRecording(w http.ResponseWriter, r *http.Request) @@ -13293,6 +13480,12 @@ func (_ Unimplemented) ListRecorders(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotImplemented) } +// Add a named marker to an in-progress recording +// (POST /recording/mark) +func (_ Unimplemented) MarkRecording(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + // Start a screen recording. Only one recording per ID can be registered at a time. // (POST /recording/start) func (_ Unimplemented) StartRecording(w http.ResponseWriter, r *http.Request) { @@ -14270,6 +14463,20 @@ func (siw *ServerInterfaceWrapper) ListRecorders(w http.ResponseWriter, r *http. handler.ServeHTTP(w, r) } +// MarkRecording operation middleware +func (siw *ServerInterfaceWrapper) MarkRecording(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.MarkRecording(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // StartRecording operation middleware func (siw *ServerInterfaceWrapper) StartRecording(w http.ResponseWriter, r *http.Request) { @@ -14681,6 +14888,9 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/recording/list", wrapper.ListRecorders) }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/recording/mark", wrapper.MarkRecording) + }) r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/recording/start", wrapper.StartRecording) }) @@ -16693,6 +16903,50 @@ func (response ListRecorders500JSONResponse) VisitListRecordersResponse(w http.R return json.NewEncoder(w).Encode(response) } +type MarkRecordingRequestObject struct { + Body *MarkRecordingJSONRequestBody +} + +type MarkRecordingResponseObject interface { + VisitMarkRecordingResponse(w http.ResponseWriter) error +} + +type MarkRecording201JSONResponse MarkRecordingResult + +func (response MarkRecording201JSONResponse) VisitMarkRecordingResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(201) + + return json.NewEncoder(w).Encode(response) +} + +type MarkRecording400JSONResponse struct{ BadRequestErrorJSONResponse } + +func (response MarkRecording400JSONResponse) VisitMarkRecordingResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type MarkRecording409JSONResponse struct{ ConflictErrorJSONResponse } + +func (response MarkRecording409JSONResponse) VisitMarkRecordingResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(409) + + return json.NewEncoder(w).Encode(response) +} + +type MarkRecording500JSONResponse struct{ InternalErrorJSONResponse } + +func (response MarkRecording500JSONResponse) VisitMarkRecordingResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + type StartRecordingRequestObject struct { Body *StartRecordingJSONRequestBody } @@ -17162,6 +17416,9 @@ type StrictServerInterface interface { // List all recorders // (GET /recording/list) ListRecorders(ctx context.Context, request ListRecordersRequestObject) (ListRecordersResponseObject, error) + // Add a named marker to an in-progress recording + // (POST /recording/mark) + MarkRecording(ctx context.Context, request MarkRecordingRequestObject) (MarkRecordingResponseObject, error) // Start a screen recording. Only one recording per ID can be registered at a time. // (POST /recording/start) StartRecording(ctx context.Context, request StartRecordingRequestObject) (StartRecordingResponseObject, error) @@ -18579,6 +18836,37 @@ func (sh *strictHandler) ListRecorders(w http.ResponseWriter, r *http.Request) { } } +// MarkRecording operation middleware +func (sh *strictHandler) MarkRecording(w http.ResponseWriter, r *http.Request) { + var request MarkRecordingRequestObject + + var body MarkRecordingJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.MarkRecording(ctx, request.(MarkRecordingRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "MarkRecording") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(MarkRecordingResponseObject); ok { + if err := validResponse.VisitMarkRecordingResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // StartRecording operation middleware func (sh *strictHandler) StartRecording(w http.ResponseWriter, r *http.Request) { var request StartRecordingRequestObject @@ -18844,366 +19132,370 @@ func (sh *strictHandler) StreamTelemetryEvents(w http.ResponseWriter, r *http.Re // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "AQA=", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/lib/recorder/ffmpeg.go b/server/lib/recorder/ffmpeg.go index 5308d30d..fc4fb747 100644 --- a/server/lib/recorder/ffmpeg.go +++ b/server/lib/recorder/ffmpeg.go @@ -41,6 +41,18 @@ var ErrRecordingFinalizing = errors.New("recording is being finalized") // can use errors.Is to surface a client-facing error instead of a 500. var ErrInvalidParams = errors.New("invalid recording parameters") +// ErrNotRecording is returned by Mark when no recording is actively running. +// Callers can use errors.Is to surface a 409. +var ErrNotRecording = errors.New("no active recording") + +// ErrInvalidMarkerName is returned by Mark when the marker name is empty or too +// long. Callers can use errors.Is to surface a 400. +var ErrInvalidMarkerName = errors.New("invalid marker name") + +// maxMarkerNameLen bounds marker names so a chapter title can't be abused to +// bloat the metadata file. +const maxMarkerNameLen = 200 + // FFmpegRecorder encapsulates an FFmpeg recording session with platform-specific screen capture. // It manages the lifecycle of a single FFmpeg process and provides thread-safe operations. type FFmpegRecorder struct { @@ -57,6 +69,7 @@ type FFmpegRecorder struct { exitCode int exited chan struct{} deleted bool + markers []Marker stz *scaletozero.Oncer // flight coordinates concurrent operations using different keys: @@ -394,6 +407,9 @@ func (fr *FFmpegRecorder) finalizeRecording(ctx context.Context) error { } outputPath := fr.outputPath binaryPath := fr.binaryPath + markers := fr.markers + startTime := fr.startTime + durationMs := fr.endTime.Sub(fr.startTime).Milliseconds() fr.mu.Unlock() // Check if the recording file exists @@ -409,15 +425,30 @@ func (fr *FFmpegRecorder) finalizeRecording(ctx context.Context) error { // Create temp file for the remuxed output tempPath := outputPath + ".tmp" + // When markers exist, build an ffmetadata file and inject it as a second + // input so the remux carries MP4 chapters. A failure here must never fail + // or corrupt the recording: fall back to the plain no-chapter remux. + metaPath := "" + if len(markers) > 0 { + path, ok, err := buildChapterMetadata(outputPath, markers, startTime, 0, durationMs) + if err != nil { + log.Warn("failed to build chapter metadata, finalizing without chapters", "err", err) + } else if ok { + metaPath = path + defer os.Remove(metaPath) + } + } + // Remux: copy streams without re-encoding, move moov atom to start with faststart - args := []string{ - "-i", outputPath, + args := []string{"-i", outputPath} + args = append(args, chapterInputArgs(metaPath)...) + args = append(args, "-c", "copy", "-movflags", "+faststart", "-f", "mp4", // Explicitly specify format since .tmp extension isn't recognized "-y", tempPath, - } + ) log.Info("finalizing recording", "cmd", fmt.Sprintf("%s %s", binaryPath, strings.Join(args, " "))) @@ -449,6 +480,46 @@ func (fr *FFmpegRecorder) finalizeRecording(ctx context.Context) error { return err } +// Mark records a named marker at the current time. It returns the stored +// (trimmed) marker name and the offset from the recording start in +// milliseconds. The offset is provisional: it is measured against startTime at +// mark time, while the authoritative value is the chapter start computed at +// finalize. +// +// Returns ErrNotRecording if the recording isn't actively running and +// ErrInvalidMarkerName if the name is empty or exceeds maxMarkerNameLen. +func (fr *FFmpegRecorder) Mark(name string) (string, int64, error) { + fr.mu.Lock() + defer fr.mu.Unlock() + + if fr.cmd == nil || fr.exitCode >= exitCodeProcessDoneMinValue { + return "", 0, ErrNotRecording + } + + name = strings.TrimSpace(name) + if name == "" || len(name) > maxMarkerNameLen { + return "", 0, ErrInvalidMarkerName + } + + now := time.Now() + fr.markers = append(fr.markers, Marker{Name: name, At: now}) + return name, now.Sub(fr.startTime).Milliseconds(), nil +} + +// chapterInputArgs returns the extra remux arguments that pull MP4 chapters +// from the ffmetadata input at metaPath: the second input plus the stream and +// chapter mapping. When metaPath is empty it returns nil so the remux args are +// byte-identical to the no-marker path. The ffmetadata demuxer must be named +// explicitly with -f since the .ffmeta extension isn't auto-detected. -map 0 +// preserves all streams from the recording; -map_chapters 1 takes chapters from +// the metadata input. +func chapterInputArgs(metaPath string) []string { + if metaPath == "" { + return nil + } + return []string{"-f", "ffmetadata", "-i", metaPath, "-map", "0", "-map_chapters", "1"} +} + // IsRecording returns true if a recording is currently in progress. func (fr *FFmpegRecorder) IsRecording(ctx context.Context) bool { fr.mu.Lock() diff --git a/server/lib/recorder/markers.go b/server/lib/recorder/markers.go new file mode 100644 index 00000000..e3611681 --- /dev/null +++ b/server/lib/recorder/markers.go @@ -0,0 +1,92 @@ +package recorder + +import ( + "fmt" + "os" + "sort" + "strings" + "time" +) + +// Marker records a named point in time during a recording. At finalize the +// markers are written into the output MP4 as chapters so they can be scrubbed +// to in any tool that reads MP4 chapter metadata. +type Marker struct { + Name string + At time.Time +} + +// sentinelChapterName is the synthetic first chapter covering the span before +// the first real marker. ffmpeg forces the first chapter to start at 0, so +// without it the first real marker's timestamp would be clamped to 0. +const sentinelChapterName = "_recording_start" + +// buildChapterMetadata writes an ffmetadata file describing one MP4 chapter per +// marker and returns its path. originShiftMs is subtracted from every marker's +// raw offset before it becomes a chapter start; this repo passes 0, but a +// downstream fork passes a nonzero value, so it stays a parameter. durationMs +// is the recording length and becomes the END of the final chapter. +// +// ok is false when there are no usable markers (all dropped or none supplied), +// signalling the caller to fall back to the normal no-chapter remux. The +// metadata file is written next to outputPath with a ".ffmeta" suffix. +func buildChapterMetadata(outputPath string, markers []Marker, startTime time.Time, originShiftMs int64, durationMs int64) (string, bool, error) { + type chapter struct { + startMs int64 + title string + } + + chapters := make([]chapter, 0, len(markers)) + for _, m := range markers { + startMs := m.At.Sub(startTime).Milliseconds() - originShiftMs + if startMs < 0 { + continue + } + chapters = append(chapters, chapter{startMs: startMs, title: m.Name}) + } + if len(chapters) == 0 { + return "", false, nil + } + + sort.SliceStable(chapters, func(i, j int) bool { + return chapters[i].startMs < chapters[j].startMs + }) + + // Prepend the sentinel so the first real marker keeps its true start. + if chapters[0].startMs > 0 { + chapters = append([]chapter{{startMs: 0, title: sentinelChapterName}}, chapters...) + } + + var b strings.Builder + b.WriteString("FFMETADATA1\n") + for i, c := range chapters { + endMs := durationMs + if i+1 < len(chapters) { + endMs = chapters[i+1].startMs + } + b.WriteString("[CHAPTER]\n") + b.WriteString("TIMEBASE=1/1000\n") + fmt.Fprintf(&b, "START=%d\n", c.startMs) + fmt.Fprintf(&b, "END=%d\n", endMs) + fmt.Fprintf(&b, "title=%s\n", escapeFFMetadata(c.title)) + } + + path := outputPath + ".ffmeta" + if err := os.WriteFile(path, []byte(b.String()), 0o644); err != nil { + return "", false, fmt.Errorf("failed to write chapter metadata: %w", err) + } + return path, true, nil +} + +// escapeFFMetadata escapes the characters ffmpeg treats specially in +// ffmetadata values: backslash, equals, semicolon, hash, and newline. +func escapeFFMetadata(s string) string { + replacer := strings.NewReplacer( + "\\", "\\\\", + "=", "\\=", + ";", "\\;", + "#", "\\#", + "\n", "\\\n", + ) + return replacer.Replace(s) +} diff --git a/server/lib/recorder/markers_integration_test.go b/server/lib/recorder/markers_integration_test.go new file mode 100644 index 00000000..a2e2547d --- /dev/null +++ b/server/lib/recorder/markers_integration_test.go @@ -0,0 +1,119 @@ +package recorder + +import ( + "context" + "encoding/json" + "os" + "os/exec" + "path/filepath" + "testing" + "time" + + "github.com/kernel/kernel-images/server/lib/scaletozero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// ffprobeChapters is the subset of `ffprobe -show_chapters -print_format json` +// we assert on. +type ffprobeChapters struct { + Chapters []struct { + TimeBase string `json:"time_base"` + Start int64 `json:"start"` + End int64 `json:"end"` + Tags struct { + Title string `json:"title"` + } `json:"tags"` + } `json:"chapters"` +} + +// TestFinalize_InjectsChaptersWithRealFFmpeg records a short synthetic clip with +// real ffmpeg, marks at known offsets, finalizes, and asserts via ffprobe that +// the chapters land at the expected millisecond offsets. It uses lavfi testsrc +// (not x11grab) so it needs no display, only ffmpeg/ffprobe on PATH. +func TestFinalize_InjectsChaptersWithRealFFmpeg(t *testing.T) { + ffmpegBin, err := exec.LookPath("ffmpeg") + if err != nil { + t.Skipf("ffmpeg not available: %v", err) + } + if _, err := exec.LookPath("ffprobe"); err != nil { + t.Skipf("ffprobe not available: %v", err) + } + + const ( + fps = 25 + durationSecs = 4 + ) + tempDir := t.TempDir() + outputPath := filepath.Join(tempDir, "rec.mp4") + + // Produce a fragmented MP4 the same shape finalize expects to remux. + genArgs := []string{ + "-f", "lavfi", + "-i", "testsrc=size=320x240:rate=25", + "-t", "4", + "-c:v", "libx264", + "-pix_fmt", "yuv420p", + "-movflags", "+frag_keyframe+empty_moov", + "-y", outputPath, + } + gen := exec.CommandContext(context.Background(), ffmpegBin, genArgs...) + out, err := gen.CombinedOutput() + require.NoError(t, err, "failed to generate test clip: %s", out) + + start := time.Now() + rec := &FFmpegRecorder{ + id: "finalize-integration", + binaryPath: ffmpegBin, + outputPath: outputPath, + startTime: start, + endTime: start.Add(durationSecs * time.Second), + exitCode: 0, + markers: []Marker{ + {Name: "intro", At: start.Add(1 * time.Second)}, + {Name: "middle", At: start.Add(2500 * time.Millisecond)}, + }, + stz: scaletozero.NewOncer(scaletozero.NewNoopController()), + } + + require.NoError(t, rec.finalizeRecording(context.Background())) + + // The temp metadata file must be cleaned up after the remux. + _, statErr := os.Stat(outputPath + ".ffmeta") + assert.True(t, os.IsNotExist(statErr), "metadata temp file should be removed") + + probe := exec.CommandContext(context.Background(), "ffprobe", + "-show_chapters", "-print_format", "json", "-v", "quiet", outputPath) + probeOut, err := probe.Output() + require.NoError(t, err) + + var parsed ffprobeChapters + require.NoError(t, json.Unmarshal(probeOut, &parsed)) + require.Len(t, parsed.Chapters, 3, "sentinel + two markers") + + assert.Equal(t, "1/1000", parsed.Chapters[0].TimeBase) + + // One frame interval in ms; chapters should land within ~1.5 frames. + const tolMs = int64(1000.0 / fps * 1.5) + assertNear := func(name string, got, want int64) { + assert.LessOrEqualf(t, abs(got-want), tolMs, "%s: got %dms want ~%dms", name, got, want) + } + + assert.Equal(t, sentinelChapterName, parsed.Chapters[0].Tags.Title) + assertNear("sentinel start", parsed.Chapters[0].Start, 0) + assertNear("sentinel end", parsed.Chapters[0].End, 1000) + + assert.Equal(t, "intro", parsed.Chapters[1].Tags.Title) + assertNear("intro start", parsed.Chapters[1].Start, 1000) + assertNear("intro end", parsed.Chapters[1].End, 2500) + + assert.Equal(t, "middle", parsed.Chapters[2].Tags.Title) + assertNear("middle start", parsed.Chapters[2].Start, 2500) +} + +func abs(v int64) int64 { + if v < 0 { + return -v + } + return v +} diff --git a/server/lib/recorder/markers_test.go b/server/lib/recorder/markers_test.go new file mode 100644 index 00000000..407ebfc4 --- /dev/null +++ b/server/lib/recorder/markers_test.go @@ -0,0 +1,247 @@ +package recorder + +import ( + "os" + "path/filepath" + "strings" + "sync" + "testing" + "time" + + "github.com/kernel/kernel-images/server/lib/scaletozero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func readMeta(t *testing.T, path string) string { + t.Helper() + b, err := os.ReadFile(path) + require.NoError(t, err) + return string(b) +} + +func TestBuildChapterMetadata_SentinelAndOrdering(t *testing.T) { + start := time.Unix(1000, 0) + out := filepath.Join(t.TempDir(), "rec.mp4") + markers := []Marker{ + {Name: "second", At: start.Add(5 * time.Second)}, + {Name: "first", At: start.Add(2 * time.Second)}, + } + + path, ok, err := buildChapterMetadata(out, markers, start, 0, 10000) + require.NoError(t, err) + require.True(t, ok) + assert.Equal(t, out+".ffmeta", path) + + meta := readMeta(t, path) + assert.True(t, strings.HasPrefix(meta, "FFMETADATA1\n"), "must start with the ffmetadata header") + + // Sentinel covers [0, firstRealStart); markers are sorted ascending and the + // final chapter ends at durationMs. + expected := "FFMETADATA1\n" + + "[CHAPTER]\nTIMEBASE=1/1000\nSTART=0\nEND=2000\ntitle=" + sentinelChapterName + "\n" + + "[CHAPTER]\nTIMEBASE=1/1000\nSTART=2000\nEND=5000\ntitle=first\n" + + "[CHAPTER]\nTIMEBASE=1/1000\nSTART=5000\nEND=10000\ntitle=second\n" + assert.Equal(t, expected, meta) +} + +func TestBuildChapterMetadata_DropsNegativeOffsets(t *testing.T) { + start := time.Unix(1000, 0) + out := filepath.Join(t.TempDir(), "rec.mp4") + markers := []Marker{ + {Name: "before-start", At: start.Add(-3 * time.Second)}, + {Name: "kept", At: start.Add(4 * time.Second)}, + } + + path, ok, err := buildChapterMetadata(out, markers, start, 0, 8000) + require.NoError(t, err) + require.True(t, ok) + + meta := readMeta(t, path) + assert.NotContains(t, meta, "before-start") + assert.Contains(t, meta, "title=kept") + // Sentinel + one kept marker = two chapters. + assert.Equal(t, 2, strings.Count(meta, "[CHAPTER]")) +} + +func TestBuildChapterMetadata_OriginShift(t *testing.T) { + start := time.Unix(1000, 0) + out := filepath.Join(t.TempDir(), "rec.mp4") + markers := []Marker{ + {Name: "shifted", At: start.Add(5 * time.Second)}, + } + + // Shifting the origin forward by 2s moves the marker from 5000ms to 3000ms. + path, ok, err := buildChapterMetadata(out, markers, start, 2000, 8000) + require.NoError(t, err) + require.True(t, ok) + assert.Contains(t, readMeta(t, path), "START=3000\nEND=8000\ntitle=shifted") + + // A shift larger than the offset drops the marker, leaving none usable. + _, ok, err = buildChapterMetadata(out, markers, start, 6000, 8000) + require.NoError(t, err) + assert.False(t, ok) +} + +func TestBuildChapterMetadata_EscapesSpecialChars(t *testing.T) { + start := time.Unix(1000, 0) + out := filepath.Join(t.TempDir(), "rec.mp4") + markers := []Marker{ + {Name: "a=b;c#d\\e", At: start.Add(1 * time.Second)}, + } + + path, ok, err := buildChapterMetadata(out, markers, start, 0, 4000) + require.NoError(t, err) + require.True(t, ok) + assert.Contains(t, readMeta(t, path), `title=a\=b\;c\#d\\e`) +} + +func TestBuildChapterMetadata_NoUsableMarkers(t *testing.T) { + start := time.Unix(1000, 0) + out := filepath.Join(t.TempDir(), "rec.mp4") + + path, ok, err := buildChapterMetadata(out, nil, start, 0, 4000) + require.NoError(t, err) + assert.False(t, ok) + assert.Empty(t, path) + _, statErr := os.Stat(out + ".ffmeta") + assert.True(t, os.IsNotExist(statErr), "no metadata file should be written") +} + +func TestBuildChapterMetadata_NoSentinelWhenFirstAtZero(t *testing.T) { + start := time.Unix(1000, 0) + out := filepath.Join(t.TempDir(), "rec.mp4") + markers := []Marker{ + {Name: "at-zero", At: start}, + } + + path, ok, err := buildChapterMetadata(out, markers, start, 0, 4000) + require.NoError(t, err) + require.True(t, ok) + meta := readMeta(t, path) + assert.NotContains(t, meta, sentinelChapterName) + assert.Equal(t, 1, strings.Count(meta, "[CHAPTER]")) + assert.Contains(t, meta, "START=0\nEND=4000\ntitle=at-zero") +} + +func TestMark_NotRecording(t *testing.T) { + rec := &FFmpegRecorder{ + id: "mark-idle", + exitCode: exitCodeInitValue, + stz: scaletozero.NewOncer(scaletozero.NewNoopController()), + } + _, _, err := rec.Mark("anything") + assert.ErrorIs(t, err, ErrNotRecording) +} + +func TestMark_AppendsAndReturnsOffset(t *testing.T) { + tempDir := t.TempDir() + rec := &FFmpegRecorder{ + id: "mark-active", + binaryPath: mockBin, + params: defaultParams(tempDir), + outputPath: filepath.Join(tempDir, "mark-active.mp4"), + stz: scaletozero.NewOncer(scaletozero.NewNoopController()), + } + require.NoError(t, rec.Start(t.Context())) + t.Cleanup(func() { _ = rec.ForceStop(t.Context()) }) + + name, offset, err := rec.Mark(" checkpoint ") + require.NoError(t, err) + assert.Equal(t, "checkpoint", name, "name is trimmed") + assert.GreaterOrEqual(t, offset, int64(0)) + + rec.mu.Lock() + markers := rec.markers + rec.mu.Unlock() + require.Len(t, markers, 1) + assert.Equal(t, "checkpoint", markers[0].Name) +} + +func TestMark_RejectsBadNames(t *testing.T) { + tempDir := t.TempDir() + rec := &FFmpegRecorder{ + id: "mark-bad", + binaryPath: mockBin, + params: defaultParams(tempDir), + outputPath: filepath.Join(tempDir, "mark-bad.mp4"), + stz: scaletozero.NewOncer(scaletozero.NewNoopController()), + } + require.NoError(t, rec.Start(t.Context())) + t.Cleanup(func() { _ = rec.ForceStop(t.Context()) }) + + _, _, err := rec.Mark(" ") + assert.ErrorIs(t, err, ErrInvalidMarkerName) + + _, _, err = rec.Mark(strings.Repeat("x", maxMarkerNameLen+1)) + assert.ErrorIs(t, err, ErrInvalidMarkerName) +} + +func TestMark_ConcurrentCallsAreSafe(t *testing.T) { + tempDir := t.TempDir() + rec := &FFmpegRecorder{ + id: "mark-race", + binaryPath: mockBin, + params: defaultParams(tempDir), + outputPath: filepath.Join(tempDir, "mark-race.mp4"), + stz: scaletozero.NewOncer(scaletozero.NewNoopController()), + } + require.NoError(t, rec.Start(t.Context())) + t.Cleanup(func() { _ = rec.ForceStop(t.Context()) }) + + const n = 50 + var wg sync.WaitGroup + wg.Add(n) + for i := 0; i < n; i++ { + go func() { + defer wg.Done() + _, _, _ = rec.Mark("m") + }() + } + wg.Wait() + + rec.mu.Lock() + count := len(rec.markers) + rec.mu.Unlock() + assert.Equal(t, n, count) +} + +// chapterInputArgs is the only thing finalize adds for chapters; locking its +// output both ways guarantees the no-marker remux is byte-identical to before. +func TestChapterInputArgs(t *testing.T) { + assert.Nil(t, chapterInputArgs("")) + assert.Equal(t, []string{"-f", "ffmetadata", "-i", "/tmp/x.ffmeta", "-map", "0", "-map_chapters", "1"}, chapterInputArgs("/tmp/x.ffmeta")) +} + +// finalizeRemuxArgs mirrors how finalizeRecording assembles the remux command +// so the backward-compat assertion below can lock both shapes. +func finalizeRemuxArgs(outputPath, tempPath, metaPath string) []string { + args := []string{"-i", outputPath} + args = append(args, chapterInputArgs(metaPath)...) + args = append(args, + "-c", "copy", + "-movflags", "+faststart", + "-f", "mp4", + "-y", + tempPath, + ) + return args +} + +func TestFinalizeRemuxArgs_BackwardsCompat(t *testing.T) { + // No markers: byte-identical to the pre-feature remux command. + preFeature := []string{ + "-i", "/r/out.mp4", + "-c", "copy", + "-movflags", "+faststart", + "-f", "mp4", + "-y", + "/r/out.mp4.tmp", + } + assert.Equal(t, preFeature, finalizeRemuxArgs("/r/out.mp4", "/r/out.mp4.tmp", "")) + + // Markers present: the chapter input and mapping are injected. + withChapters := finalizeRemuxArgs("/r/out.mp4", "/r/out.mp4.tmp", "/r/out.mp4.ffmeta") + assert.Contains(t, withChapters, "-map_chapters") + assert.Contains(t, withChapters, "/r/out.mp4.ffmeta") +} diff --git a/server/lib/recorder/recorder.go b/server/lib/recorder/recorder.go index 8b3b53e3..ed028edd 100644 --- a/server/lib/recorder/recorder.go +++ b/server/lib/recorder/recorder.go @@ -12,6 +12,10 @@ type Recorder interface { Start(ctx context.Context) error Stop(ctx context.Context) error ForceStop(ctx context.Context) error + // Mark records a named marker at the current time. It returns the stored + // (trimmed) marker name and the provisional offset from the recording start + // in milliseconds. + Mark(name string) (string, int64, error) IsRecording(ctx context.Context) bool Metadata() *RecordingMetadata Recording(ctx context.Context) (io.ReadCloser, *RecordingMetadata, error) diff --git a/server/openapi.yaml b/server/openapi.yaml index fcbc991a..28fbbe13 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -308,6 +308,33 @@ paths: $ref: "#/components/responses/ConflictError" "500": $ref: "#/components/responses/InternalError" + /recording/mark: + post: + summary: Add a named marker to an in-progress recording + description: >- + Records a named point in time during an active recording. At finalize + the markers are injected into the output MP4 as chapter markers. + operationId: markRecording + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/MarkRecordingRequest" + responses: + "201": + description: Marker recorded + content: + application/json: + schema: + $ref: "#/components/schemas/MarkRecordingResult" + "400": + $ref: "#/components/responses/BadRequestError" + "409": + description: No recording is currently in progress + $ref: "#/components/responses/ConflictError" + "500": + $ref: "#/components/responses/InternalError" /computer/click_mouse: post: summary: Simulate a mouse click action on the host computer @@ -3450,6 +3477,33 @@ components: description: Identifier of the recording to delete. Alphanumeric or hyphen. pattern: "^[a-zA-Z0-9-]+$" additionalProperties: false + MarkRecordingRequest: + type: object + required: [name] + properties: + name: + type: string + description: Name of the marker, used as the MP4 chapter title. + id: + type: string + description: Identifier of the recorder to mark. Alphanumeric or hyphen. + pattern: "^[a-zA-Z0-9-]+$" + additionalProperties: false + MarkRecordingResult: + type: object + required: [name, offsetMs] + properties: + name: + type: string + description: Name of the recorded marker. + offsetMs: + type: integer + format: int64 + description: >- + Provisional offset of the marker from the recording start, in + milliseconds, measured against the start time at mark time. The + authoritative offset is the chapter start written at finalize. + additionalProperties: false FileInfo: type: object required: [name, path, size_bytes, is_dir, mod_time, mode] From 81d787d28cc9f66b65f080107cd22b9723f49d9b Mon Sep 17 00:00:00 2001 From: rgarcia <72655+rgarcia@users.noreply.github.com> Date: Mon, 15 Jun 2026 15:01:37 +0000 Subject: [PATCH 2/2] Drop unused originShiftMs parameter from chapter metadata buildChapterMetadata always received 0 for the origin shift, so remove the parameter, its drop-when-shifted branch, and the corresponding test. Chapter starts are now computed directly from each marker's offset. Co-Authored-By: Claude Opus 4.7 --- server/lib/recorder/ffmpeg.go | 2 +- server/lib/recorder/markers.go | 11 +++++------ server/lib/recorder/markers_test.go | 29 +++++------------------------ 3 files changed, 11 insertions(+), 31 deletions(-) diff --git a/server/lib/recorder/ffmpeg.go b/server/lib/recorder/ffmpeg.go index fc4fb747..1f0c736e 100644 --- a/server/lib/recorder/ffmpeg.go +++ b/server/lib/recorder/ffmpeg.go @@ -430,7 +430,7 @@ func (fr *FFmpegRecorder) finalizeRecording(ctx context.Context) error { // or corrupt the recording: fall back to the plain no-chapter remux. metaPath := "" if len(markers) > 0 { - path, ok, err := buildChapterMetadata(outputPath, markers, startTime, 0, durationMs) + path, ok, err := buildChapterMetadata(outputPath, markers, startTime, durationMs) if err != nil { log.Warn("failed to build chapter metadata, finalizing without chapters", "err", err) } else if ok { diff --git a/server/lib/recorder/markers.go b/server/lib/recorder/markers.go index e3611681..3b98f6a1 100644 --- a/server/lib/recorder/markers.go +++ b/server/lib/recorder/markers.go @@ -22,15 +22,14 @@ type Marker struct { const sentinelChapterName = "_recording_start" // buildChapterMetadata writes an ffmetadata file describing one MP4 chapter per -// marker and returns its path. originShiftMs is subtracted from every marker's -// raw offset before it becomes a chapter start; this repo passes 0, but a -// downstream fork passes a nonzero value, so it stays a parameter. durationMs -// is the recording length and becomes the END of the final chapter. +// marker and returns its path. Each marker's chapter starts at its offset from +// startTime; markers before startTime are dropped. durationMs is the recording +// length and becomes the END of the final chapter. // // ok is false when there are no usable markers (all dropped or none supplied), // signalling the caller to fall back to the normal no-chapter remux. The // metadata file is written next to outputPath with a ".ffmeta" suffix. -func buildChapterMetadata(outputPath string, markers []Marker, startTime time.Time, originShiftMs int64, durationMs int64) (string, bool, error) { +func buildChapterMetadata(outputPath string, markers []Marker, startTime time.Time, durationMs int64) (string, bool, error) { type chapter struct { startMs int64 title string @@ -38,7 +37,7 @@ func buildChapterMetadata(outputPath string, markers []Marker, startTime time.Ti chapters := make([]chapter, 0, len(markers)) for _, m := range markers { - startMs := m.At.Sub(startTime).Milliseconds() - originShiftMs + startMs := m.At.Sub(startTime).Milliseconds() if startMs < 0 { continue } diff --git a/server/lib/recorder/markers_test.go b/server/lib/recorder/markers_test.go index 407ebfc4..8f959017 100644 --- a/server/lib/recorder/markers_test.go +++ b/server/lib/recorder/markers_test.go @@ -28,7 +28,7 @@ func TestBuildChapterMetadata_SentinelAndOrdering(t *testing.T) { {Name: "first", At: start.Add(2 * time.Second)}, } - path, ok, err := buildChapterMetadata(out, markers, start, 0, 10000) + path, ok, err := buildChapterMetadata(out, markers, start, 10000) require.NoError(t, err) require.True(t, ok) assert.Equal(t, out+".ffmeta", path) @@ -53,7 +53,7 @@ func TestBuildChapterMetadata_DropsNegativeOffsets(t *testing.T) { {Name: "kept", At: start.Add(4 * time.Second)}, } - path, ok, err := buildChapterMetadata(out, markers, start, 0, 8000) + path, ok, err := buildChapterMetadata(out, markers, start, 8000) require.NoError(t, err) require.True(t, ok) @@ -64,25 +64,6 @@ func TestBuildChapterMetadata_DropsNegativeOffsets(t *testing.T) { assert.Equal(t, 2, strings.Count(meta, "[CHAPTER]")) } -func TestBuildChapterMetadata_OriginShift(t *testing.T) { - start := time.Unix(1000, 0) - out := filepath.Join(t.TempDir(), "rec.mp4") - markers := []Marker{ - {Name: "shifted", At: start.Add(5 * time.Second)}, - } - - // Shifting the origin forward by 2s moves the marker from 5000ms to 3000ms. - path, ok, err := buildChapterMetadata(out, markers, start, 2000, 8000) - require.NoError(t, err) - require.True(t, ok) - assert.Contains(t, readMeta(t, path), "START=3000\nEND=8000\ntitle=shifted") - - // A shift larger than the offset drops the marker, leaving none usable. - _, ok, err = buildChapterMetadata(out, markers, start, 6000, 8000) - require.NoError(t, err) - assert.False(t, ok) -} - func TestBuildChapterMetadata_EscapesSpecialChars(t *testing.T) { start := time.Unix(1000, 0) out := filepath.Join(t.TempDir(), "rec.mp4") @@ -90,7 +71,7 @@ func TestBuildChapterMetadata_EscapesSpecialChars(t *testing.T) { {Name: "a=b;c#d\\e", At: start.Add(1 * time.Second)}, } - path, ok, err := buildChapterMetadata(out, markers, start, 0, 4000) + path, ok, err := buildChapterMetadata(out, markers, start, 4000) require.NoError(t, err) require.True(t, ok) assert.Contains(t, readMeta(t, path), `title=a\=b\;c\#d\\e`) @@ -100,7 +81,7 @@ func TestBuildChapterMetadata_NoUsableMarkers(t *testing.T) { start := time.Unix(1000, 0) out := filepath.Join(t.TempDir(), "rec.mp4") - path, ok, err := buildChapterMetadata(out, nil, start, 0, 4000) + path, ok, err := buildChapterMetadata(out, nil, start, 4000) require.NoError(t, err) assert.False(t, ok) assert.Empty(t, path) @@ -115,7 +96,7 @@ func TestBuildChapterMetadata_NoSentinelWhenFirstAtZero(t *testing.T) { {Name: "at-zero", At: start}, } - path, ok, err := buildChapterMetadata(out, markers, start, 0, 4000) + path, ok, err := buildChapterMetadata(out, markers, start, 4000) require.NoError(t, err) require.True(t, ok) meta := readMeta(t, path)