From fea6d81246235a71a72febffd44a15e03ef55feb Mon Sep 17 00:00:00 2001 From: Kilian Boute Date: Thu, 6 Aug 2026 17:46:38 +0200 Subject: [PATCH] prevent fps rounding --- machinery/src/capture/main.go | 2 +- machinery/src/capture/main_test.go | 8 ++++++-- machinery/src/cloud/recording_metadata.go | 4 ++-- machinery/src/cloud/tus_client_test.go | 7 ++++--- machinery/src/models/recording_upload_metadata.go | 10 +++++----- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/machinery/src/capture/main.go b/machinery/src/capture/main.go index 87a4aad..c23e75c 100644 --- a/machinery/src/capture/main.go +++ b/machinery/src/capture/main.go @@ -62,7 +62,7 @@ func recordingUploadMetadata(name, deviceKey string, timestamp int64, mp4Video * } value := mp4Video.AverageFPS() if value > 0 && value <= 240 && !math.IsInf(value, 0) && !math.IsNaN(value) { - metadata.FPS = int(math.Floor(value)) + metadata.FPS = value } return metadata } diff --git a/machinery/src/capture/main_test.go b/machinery/src/capture/main_test.go index 72d6f53..9b64af8 100644 --- a/machinery/src/capture/main_test.go +++ b/machinery/src/capture/main_test.go @@ -29,9 +29,13 @@ func TestQueueRecordingForUploadStoresFinalizedMetadata(t *testing.T) { if err := json.Unmarshal(got, &stored); err != nil { t.Fatalf("decode upload marker: %v", err) } - if stored.FileName != "recording.mp4" || stored.DeviceKey != "device-key" || stored.Timestamp != 1785934709414 || stored.Duration != 20452 || stored.FPS != 29 { + expectedFPS := mp4Video.AverageFPS() + if stored.FileName != "recording.mp4" || stored.DeviceKey != "device-key" || stored.Timestamp != 1785934709414 || stored.Duration != 20452 || math.Abs(stored.FPS-expectedFPS) > 1e-9 { t.Fatalf("upload marker = %+v", stored) } + if stored.FPS == math.Floor(stored.FPS) { + t.Fatalf("upload marker FPS = %v, want fractional precision", stored.FPS) + } } func TestQueueRecordingForUploadKeepsUnknownFPSCompatible(t *testing.T) { @@ -44,7 +48,7 @@ func TestQueueRecordingForUploadKeepsUnknownFPSCompatible(t *testing.T) { metadata := models.RecordingUploadMetadata{FileName: "recording.mp4"} if fps >= 1 && fps <= 240 && !math.IsNaN(fps) && !math.IsInf(fps, 0) { - metadata.FPS = int(math.Floor(fps)) + metadata.FPS = fps } queueRecordingForUpload(configDirectory, metadata) diff --git a/machinery/src/cloud/recording_metadata.go b/machinery/src/cloud/recording_metadata.go index 50c5979..48f4d63 100644 --- a/machinery/src/cloud/recording_metadata.go +++ b/machinery/src/cloud/recording_metadata.go @@ -28,10 +28,10 @@ func queuedRecordingFPS(fileName string) string { marker := strings.TrimSpace(string(value)) if strings.HasPrefix(marker, "{") { metadata, ok := decodeRecordingUploadMetadata(value) - if !ok || metadata.FPS <= 0 || metadata.FPS > 240 { + if !ok || metadata.FPS <= 0 || metadata.FPS > 240 || math.IsInf(metadata.FPS, 0) || math.IsNaN(metadata.FPS) { return "" } - return strconv.Itoa(metadata.FPS) + return strconv.FormatFloat(metadata.FPS, 'f', -1, 64) } // Compatibility with markers created before upload metadata used JSON. diff --git a/machinery/src/cloud/tus_client_test.go b/machinery/src/cloud/tus_client_test.go index 81a2c5e..5bfca63 100644 --- a/machinery/src/cloud/tus_client_test.go +++ b/machinery/src/cloud/tus_client_test.go @@ -272,7 +272,7 @@ func TestUploadVaultResumable_HappyPath(t *testing.T) { fileName := "1564859471_6-474162_oprit_577-283-727-375_1153_27.mp4" payload := bytes.Repeat([]byte("x"), 4096) withRecording(t, fileName, payload) - withQueuedRecordingFPS(t, fileName, `{"filename":"recording.mp4","device_key":"device-key","timestamp":1785934709414,"duration":20452,"fps":29}`) + withQueuedRecordingFPS(t, fileName, `{"filename":"recording.mp4","device_key":"device-key","timestamp":1785934709414,"duration":20452,"fps":29.97}`) uploaded, responded, supported, _, err := uploadVaultResumable(testVault(ts.URL), "pk", "dev", fileName, "test", "primary") if err != nil { @@ -289,8 +289,8 @@ func TestUploadVaultResumable_HappyPath(t *testing.T) { } posts := srv.requestsForMethod(http.MethodPost) metadata := decodeTusMetadata(posts[0].header.Get("Upload-Metadata")) - if got := metadata["fps"]; got != "29" { - t.Fatalf("POST metadata fps = %q, want %q", got, "29") + if got := metadata["fps"]; got != "29.97" { + t.Fatalf("POST metadata fps = %q, want %q", got, "29.97") } if got := metadata["duration"]; got != "20452" { t.Fatalf("POST metadata duration = %q, want %q", got, "20452") @@ -307,6 +307,7 @@ func TestQueuedRecordingFPSValidation(t *testing.T) { want string }{ {name: "json", fps: `{"fps":29}`, want: "29"}, + {name: "json fractional", fps: `{"fps":17.35}`, want: "17.35"}, {name: "json with future field", fps: `{"fps":29,"codec":"h264"}`, want: "29"}, {name: "json without fps", fps: `{}`}, {name: "json invalid fps", fps: `{"fps":241}`}, diff --git a/machinery/src/models/recording_upload_metadata.go b/machinery/src/models/recording_upload_metadata.go index 5975e6b..504134a 100644 --- a/machinery/src/models/recording_upload_metadata.go +++ b/machinery/src/models/recording_upload_metadata.go @@ -11,11 +11,11 @@ const RecordingUploadMetadataExtension = ".metadata" // with a recording. New optional fields can be added without changing the queue // mechanism or breaking older agents. type RecordingUploadMetadata struct { - FileName string `json:"filename"` - DeviceKey string `json:"device_key"` - Timestamp int64 `json:"timestamp"` // Unix milliseconds. - Duration uint64 `json:"duration"` // Milliseconds. - FPS int `json:"fps,omitempty"` + FileName string `json:"filename"` + DeviceKey string `json:"device_key"` + Timestamp int64 `json:"timestamp"` // Unix milliseconds. + Duration uint64 `json:"duration"` // Milliseconds. + FPS float64 `json:"fps,omitempty"` } // RecordingUploadMetadataFileName returns the queue marker name associated