From c0b352dc1dd8a8cfd5dbf016e447f321e239bd2a Mon Sep 17 00:00:00 2001 From: Loewy Date: Tue, 30 Dec 2025 15:01:41 -0500 Subject: [PATCH] fix fps (alter tfhd and trun size, add logs --- .../java/com/mrousavy/camera/core/HlsMuxer.kt | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/HlsMuxer.kt b/package/android/src/main/java/com/mrousavy/camera/core/HlsMuxer.kt index 7b9787e..1e41589 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/HlsMuxer.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/HlsMuxer.kt @@ -885,9 +885,11 @@ class HlsMuxer( ): ByteArray { // Calculate sizes to determine data offset val mfhdBox = buildMfhdBox(sequenceNumber) - val tfhdSize = 8 + 8 // box header + content (version/flags + track_id) + // tfhd: 8 header + 4 version/flags + 4 track_id + 4 duration + 4 size + 4 flags = 28 bytes + val tfhdSize = 8 + 20 val tfdtSize = 8 + 12 // box header + version 1 content - val trunSize = 8 + 12 + (samples.size * 12) // header + fixed + per-sample (no composition offset) + // trun: 8 header + 12 fixed + per-sample (size + flags only, no duration) + val trunSize = 8 + 12 + (samples.size * 8) val trafSize = 8 + tfhdSize + tfdtSize + trunSize val moofSize = 8 + mfhdBox.size + trafSize @@ -922,9 +924,21 @@ class HlsMuxer( val output = ByteArrayOutputStream() val dos = DataOutputStream(output) - // Flags: default-base-is-moof (0x020000) - dos.writeInt(0x00020000) + // Calculate default sample duration for this fragment + // This helps ffprobe calculate correct frame rate when reading via HLS + val defaultSampleDuration = timescale / fps // e.g., 30000/30 = 1000 + + // Match iOS AVFoundation's tfhd structure (28 bytes total) + // Flags: default-base-is-moof (0x020000) + default-sample-duration (0x000008) + // + default-sample-size (0x000010) + default-sample-flags (0x000020) + val flags = 0x00020000 or 0x000008 or 0x000010 or 0x000020 + dos.writeInt(flags) dos.writeInt(1) // track ID + dos.writeInt(defaultSampleDuration) // default sample duration in timescale units + dos.writeInt(0) // default sample size (0 = variable, specified in trun) + dos.writeInt(0x01010000) // default sample flags (non-keyframe, depends on others) + + Log.d(TAG, "tfhd: default_sample_duration=$defaultSampleDuration (timescale=$timescale, fps=$fps)") return wrapBox("tfhd", output.toByteArray()) } @@ -947,16 +961,17 @@ class HlsMuxer( val output = ByteArrayOutputStream() val dos = DataOutputStream(output) - // Flags: data-offset + sample-duration + sample-size + sample-flags - val flags = 0x000001 or 0x000100 or 0x000200 or 0x000400 + // Flags: data-offset + sample-size + sample-flags + // NOTE: We intentionally OMIT sample-duration (0x000100) so ffprobe uses + // the default_sample_duration from tfhd instead of per-sample durations. + // This ensures consistent frame rate calculation via HLS. + val flags = 0x000001 or 0x000200 or 0x000400 dos.writeInt(flags) dos.writeInt(samples.size) dos.writeInt(dataOffset) for (sample in samples) { - // Convert duration to timescale units - val durationInTimescale = ((sample.durationUs * timescale) / 1_000_000).toInt() - dos.writeInt(durationInTimescale) + // No duration - using default from tfhd dos.writeInt(sample.data.size) dos.writeInt(buildSampleFlags(sample.isKeyFrame)) }