From c040521d1c7c94b2980b59d505d82e17b414fb82 Mon Sep 17 00:00:00 2001 From: Sunghyun Kim Date: Mon, 26 Oct 2026 19:42:51 +0900 Subject: [PATCH 1/7] fix(video): repair an MP4 whose writer forced the decode ladder monotonic A writer that drops ctts and then pushes each B picture 1001 ticks past the picture decoded before it leaves PTS == DTS with no single ladder step, so the #409 probe declined it and both routes showed decode order. Take the step from the stream's average frame rate, remeasure it on the anchors and rebuild DTS from each picture's decode slot, under the same picture-order checks. Adds a twin-pair test, a fixture script and docs. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 9 + Scripts/make-clamped-ladder-fixture.sh | 59 ++ .../Video/H264CompositionOffsetRepair.swift | 154 ++++- .../DocumentedConstantsTests.swift | 5 + ...H264ForcedMonotonicLadderRepairTests.swift | 567 ++++++++++++++++++ docs/architecture.md | 2 +- docs/formats.md | 18 + 7 files changed, 799 insertions(+), 15 deletions(-) create mode 100755 Scripts/make-clamped-ladder-fixture.sh create mode 100644 Tests/AetherEngineTests/H264ForcedMonotonicLadderRepairTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 311afd129..efca48619 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -121,6 +121,15 @@ the public-API contract. ### Fixed +- **MP4s whose writer dropped `ctts` and forced the decode ladder monotonic play in order.** + Such a file reports `PTS == DTS` on every sample, keeps each anchor on its own presentation time + and pushes each B picture 1001 ticks past the picture decoded before it, so the ladder has no single + step and the #409 probe left the stream untouched ("decode ladder is not uniform"). Both routes then + showed the B pictures in decode order, as QuickTime and the Files app do. The repair now takes the + step from the stream's average frame rate, remeasures it on the anchors and rebuilds DTS from each + picture's decode slot, under the same picture-order checks. `Scripts/make-clamped-ladder-fixture.sh` + generates a twin pair. See `docs/formats.md`. + - **A refused HDR master no longer latches when the panel is shown to present HDR (AE#887).** A live PQ master refused with -11868 on an Apple TV whose output was fixed to 4K HDR latched the process-wide panel refusal, so every later HDR title went media-direct without its subtitle diff --git a/Scripts/make-clamped-ladder-fixture.sh b/Scripts/make-clamped-ladder-fixture.sh new file mode 100755 index 000000000..94690c586 --- /dev/null +++ b/Scripts/make-clamped-ladder-fixture.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Reproduces an MP4 whose writer dropped `ctts` and then forced its decode-order presentation times +# to be monotonic: every sample reports PTS == DTS, an anchor keeps its own time, and each B picture +# that follows it in decode order is pushed 1001 ticks (a third of a 29.97 fps frame) past the picture +# decoded before it. The decode ladder then has no single step, which the #409 classifier needs. +# QuickTime and the Files app (AVFoundation) show such a file with its pictures stepping back and +# forth; FFmpeg-based players reorder by picture order count and look right. +# +# bframes-healthy.mp4 H.264 High 1280x720 29.97 fps, B pyramid, GOP 90, 30 s + AAC (the twin) +# clamped-ladder.mp4 the same stream, ctts dropped, ladder clamped as above +# +# testsrc2 draws a frame counter and a moving bar, so the order is visible by eye. +# +# Usage: +# ./Scripts/make-clamped-ladder-fixture.sh [output-dir] [--check] +# +# --check builds aetherctl and prints the #409 verdict (pktdump) and the decoded order (swdecode) +# for the clamped file. Requires ffmpeg with libx264 (macOS: `brew install ffmpeg`). + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +OUT="$REPO_ROOT/Fixtures/clamped-ladder" +CHECK=0 +for arg in "$@"; do + case "$arg" in + --check) CHECK=1 ;; + *) OUT="$arg" ;; + esac +done +mkdir -p "$OUT" +FF=(ffmpeg -hide_banner -loglevel error -y) + +"${FF[@]}" \ + -f lavfi -i "testsrc2=size=1280x720:rate=30000/1001:duration=30" \ + -f lavfi -i "sine=frequency=440:duration=30" \ + -c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p -bf 3 -g 90 -x264-params b-pyramid=normal \ + -c:a aac -b:a 128k -video_track_timescale 90000 -movflags +faststart \ + "$OUT/bframes-healthy.mp4" + +# setts runs per packet in decode order, so PREV_OUTDTS is the timestamp just written: the clamp +# keeps every anchor (its PTS is already past everything before it) and pushes each B picture. +"${FF[@]}" -i "$OUT/bframes-healthy.mp4" -map 0 -c copy \ + -bsf:v 'setts=ts=max(PTS\,PREV_OUTDTS+1001)' -video_track_timescale 90000 -movflags +faststart \ + "$OUT/clamped-ladder.mp4" + +echo "wrote $OUT/bframes-healthy.mp4" +echo "wrote $OUT/clamped-ladder.mp4" +ffprobe -v error -select_streams v:0 -show_entries packet=pts,dts -of csv=p=0 "$OUT/clamped-ladder.mp4" \ + | awk -F, 'NR > 1 { step[$2 - prev]++ } { if ($1 == $2) same++; prev = $2; n++ } + END { printf "clamped-ladder.mp4: %d packets, PTS == DTS on %d, DTS steps:", n, same + for (s in step) printf " %s x%d", s, step[s]; print "" }' + +if [[ $CHECK -eq 1 ]]; then + swift build --package-path "$REPO_ROOT" --product aetherctl >/dev/null 2>&1 + CTL="$(swift build --package-path "$REPO_ROOT" --product aetherctl --show-bin-path 2>/dev/null)/aetherctl" + "$CTL" pktdump --count 12 "$OUT/clamped-ladder.mp4" 2>&1 | grep -E '#409' || echo "no #409 verdict line" + "$CTL" swdecode --frames 300 "$OUT/clamped-ladder.mp4" 2>&1 | grep -E 'Steps backwards' || true +fi diff --git a/Sources/AetherEngine/Video/H264CompositionOffsetRepair.swift b/Sources/AetherEngine/Video/H264CompositionOffsetRepair.swift index 1f0329007..586354373 100644 --- a/Sources/AetherEngine/Video/H264CompositionOffsetRepair.swift +++ b/Sources/AetherEngine/Video/H264CompositionOffsetRepair.swift @@ -129,6 +129,11 @@ enum H264CompositionOffsetRepair { /// reorder head, and the values between when it trims part of one. The ladder cannot say /// which, so this is read from the container the same way `shift` is. var ladderOrdinalOffset: Int64 = 0 + /// Set when the writer's decode ladder is not a ladder at all and `step` came from the stream's + /// average frame rate instead (`nominalStep`). DTS is then rebuilt from the decode ordinal as + /// well, because the container's DTS of a B picture can sit after the presentation time it + /// belongs to. + var synthesizesDecodeLadder = false /// The timestamp of a presentation ordinal, nil unless this plan carries a lattice. func presentationTimestamp(ordinal: Int64) -> Int64? { @@ -206,7 +211,8 @@ enum H264CompositionOffsetRepair { samples: [Sample], videoDelay: Int, streamStartTime: Int64, - ladderStart: Int64 + ladderStart: Int64, + nominalStep: Int64? = nil ) -> Verdict { guard videoDelay > 0, videoDelay <= 16 else { return .inconclusive("reorder delay \(videoDelay) outside 1...16") @@ -247,14 +253,24 @@ enum H264CompositionOffsetRepair { } var cadence: Cadence? var step = shortestStep + var synthesizesDecodeLadder = false if shortestStep != longestStep { - guard longestStep - shortestStep == 1, - let quantized = quantizedCadence(deltas: deltas), - let roundedStep = quantized.timestamp(at: 1) else { + if longestStep - shortestStep == 1, + let quantized = quantizedCadence(deltas: deltas), + let roundedStep = quantized.timestamp(at: 1) { + cadence = quantized + step = roundedStep + } else if let nominalStep, + nominalLadderFits(samples: samples, step: nominalStep, videoDelay: videoDelay) { + // A writer that dropped `ctts` and then forced its decode-order presentation times to + // be monotonic (each B picture pushed one third of a frame past the anchor decoded + // before it) leaves no ladder to read a step from, but the pictures still come at the + // stream's constant rate. + step = nominalStep + synthesizesDecodeLadder = true + } else { return .inconclusive("decode ladder is not uniform") } - cadence = quantized - step = roundedStep } guard step > 0 else { return .inconclusive("no ladder step") } @@ -301,6 +317,13 @@ enum H264CompositionOffsetRepair { return .inconclusive("display indices do not fill the sampled window") } + // The average frame rate is only an estimate (short files, rounded durations: a 60-picture + // 29.97 clip reports 29.80). The writer's anchors carry their own presentation times untouched, + // so when they agree they give the exact step. + if synthesizesDecodeLadder, let measured = anchorStep(samples: samples, pocStep: pocStep) { + step = measured + } + // The reorder head is one decode lead long, and with a fractional cadence that lead is a // rounded distance on the lattice rather than a multiple of a whole step. let ladderOrigin = ladderStart == Int64.min ? (samples.first?.dts ?? Int64.min) : ladderStart @@ -316,8 +339,15 @@ enum H264CompositionOffsetRepair { decodeLead = lead } guard decodeLead > 0 else { return .inconclusive("no decode lead") } - let shift = presentationShift( - streamStartTime: streamStartTime, ladderStart: ladderOrigin, decodeLead: decodeLead) + // A synthesized ladder anchors every coded video sequence on its keyframe's DTS, so the shift + // is simply what puts the first keyframe on the presentation start. The writer that produces + // this shape can leave that keyframe more than one decode lead ahead (measured: 12012 ticks at + // a 3003 step and delay 2), so it is bounded by a generous number of pictures instead of the + // decode lead. + let shift = synthesizesDecodeLadder + ? nominalShift(streamStartTime: streamStartTime, ladderStart: ladderOrigin, step: step) + : presentationShift( + streamStartTime: streamStartTime, ladderStart: ladderOrigin, decodeLead: decodeLead) // A fractional cadence is read back off a lattice, so the lattice has to be located first, // and that takes two readings the ladder cannot both give. The phase and the origin come from @@ -354,11 +384,77 @@ enum H264CompositionOffsetRepair { cadence: cadence, latticeOrigin: latticeOrigin, ladderPhase: ladderPhase, - ladderOrdinalOffset: ladderOrdinalOffset + ladderOrdinalOffset: ladderOrdinalOffset, + synthesizesDecodeLadder: synthesizesDecodeLadder ) ) } + /// Ticks per picture from the stream's average frame rate, rounded. nil when the rate is missing or + /// describes a picture shorter than two ticks. + static func nominalFrameStep(frameRate: AVRational, timeBase: AVRational) -> Int64? { + guard frameRate.num > 0, frameRate.den > 0, timeBase.num > 0, timeBase.den > 0 else { return nil } + let ticks = (Double(timeBase.den) * Double(frameRate.den)) / (Double(timeBase.num) * Double(frameRate.num)) + guard ticks.isFinite, ticks >= 2, ticks < Double(Int64.max / 4) else { return nil } + return Int64(ticks.rounded()) + } + + /// Whether a non-uniform sampled ladder still spans the time its pictures would take at `step`. The + /// writer's forced-monotonic ladder runs ahead of that by at most the reorder head plus the anchor + /// it pushed past (measured 3.5 pictures on the reporting file at delay 2), and falls behind only + /// by what the next anchor has not yet reclaimed; a genuinely variable-rate head misses it by far + /// more. + static func nominalLadderFits(samples: [Sample], step: Int64, videoDelay: Int) -> Bool { + guard step >= 2, let first = samples.first?.dts, let last = samples.last?.dts, last > first else { + return false + } + let pictures = Int64(samples.count - 1) + let slack = Int64(videoDelay + 2) + let (span, overflow) = last.subtractingReportingOverflow(first) + let (low, lowOverflow) = max(0, pictures - slack).multipliedReportingOverflow(by: step) + let (high, highOverflow) = (pictures + slack).multipliedReportingOverflow(by: step) + guard !overflow, !lowOverflow, !highOverflow else { return false } + return span >= low && span <= high + } + + /// Ticks per picture read off the sampled anchors, the pictures that display later than everything + /// decoded before them. A forced-monotonic writer leaves their timestamps on the presentation axis, + /// so each one sits `displayIndex * step` after the opening keyframe. nil unless every anchor + /// agrees to within a tick per picture. + static func anchorStep(samples: [Sample], pocStep: Int64) -> Int64? { + guard pocStep > 0, let first = samples.first, first.pictureOrderCount == 0 else { return nil } + var anchors: [(index: Int64, offset: Int64)] = [] + var latest: Int64 = 0 + for sample in samples.dropFirst() where sample.pictureOrderCount > latest { + latest = sample.pictureOrderCount + let (offset, overflow) = sample.dts.subtractingReportingOverflow(first.dts) + guard !overflow, offset > 0, sample.pictureOrderCount % pocStep == 0 else { return nil } + anchors.append((sample.pictureOrderCount / pocStep, offset)) + } + guard let farthest = anchors.last, farthest.index > 0 else { return nil } + let step = (farthest.offset + farthest.index / 2) / farthest.index + guard step >= 2 else { return nil } + for anchor in anchors { + let (expected, overflow) = anchor.index.multipliedReportingOverflow(by: step) + guard !overflow, abs(anchor.offset - expected) <= anchor.index + 1 else { return nil } + } + return step + } + + /// Pictures a synthesized ladder's presentation shift may reach (`nominalShift`). + static let maximumNominalShiftPictures: Int64 = 16 + + /// The presentation shift for a synthesized ladder, clamped to + /// `[0, maximumNominalShiftPictures * step]` so a malformed header cannot drag the video off its + /// audio. + static func nominalShift(streamStartTime: Int64, ladderStart: Int64, step: Int64) -> Int64 { + guard streamStartTime != Int64.min, ladderStart != Int64.min else { return 0 } + let (raw, overflow) = streamStartTime.subtractingReportingOverflow(ladderStart) + let (limit, limitOverflow) = maximumNominalShiftPictures.multipliedReportingOverflow(by: step) + guard !overflow, !limitOverflow else { return 0 } + return min(max(raw, 0), limit) + } + /// True when some decode-order prefix of at least `minimumSamples` display indices covers its /// own range without a gap, i.e. a whole number of mini-GOPs closed inside the window. static func hasContiguousPrefix(_ indices: [Int64]) -> Bool { @@ -448,6 +544,9 @@ enum H264CompositionOffsetRepair { /// order (an open GOP's leading pictures). The landing is displayed that many slots after /// the one it is decoded in, so the re-anchor has to know the count. private var landingLeadingPictures: Int64 = 0 + /// The decode slot of the next picture in the current sequence, counted from its anchor. Only a + /// synthesized ladder reads it. + private var decodeOrdinal: Int64 = 0 /// Pictures emitted untouched because no anchor was available or the arithmetic did not /// close. Surfaced so a stream this repair does not describe is visible as a number. private(set) var unrepairedPictures = 0 @@ -460,6 +559,7 @@ enum H264CompositionOffsetRepair { sequenceAnchorDTS = nil sequenceBaseOrdinal = nil landingLeadingPictures = 0 + decodeOrdinal = 0 } mutating func noteLanding(leadingPictures: Int64) { @@ -485,6 +585,7 @@ enum H264CompositionOffsetRepair { sequenceAnchorDTS = dts sequenceBaseOrdinal = plan.presentationOrdinal(ladderTimestamp: dts) awaitingReanchor = false + decodeOrdinal = 0 } else if awaitingReanchor, isKeyframe { guard pictureOrderCount % plan.pocStep == 0 else { unrepairedPictures += 1 @@ -509,9 +610,15 @@ enum H264CompositionOffsetRepair { return overflow ? nil : base } awaitingReanchor = false + decodeOrdinal = anchorSlots + } + guard let anchor = sequenceAnchorDTS, !awaitingReanchor else { + unrepairedPictures += 1 + return nil } - guard let anchor = sequenceAnchorDTS, !awaitingReanchor, - pictureOrderCount >= 0, pictureOrderCount % plan.pocStep == 0 else { + let slot = decodeOrdinal + decodeOrdinal &+= 1 + guard pictureOrderCount >= 0, pictureOrderCount % plan.pocStep == 0 else { unrepairedPictures += 1 return nil } @@ -526,7 +633,9 @@ enum H264CompositionOffsetRepair { return overflow ? nil : plan.presentationTimestamp(ordinal: ordinal) } ?? (anchor &+ plan.shift &+ displayIndex &* plan.step) - let newDTS = dts &+ plan.shift &- plan.decodeLead + let newDTS = plan.synthesizesDecodeLadder + ? anchor &+ plan.shift &+ slot &* plan.step &- plan.decodeLead + : dts &+ plan.shift &- plan.decodeLead // The muxer invariant. A picture that lands before its own decode time means the // arithmetic no longer describes this stream, and passing it through unchanged is // better than handing the muxer something it will silently clamp. @@ -617,6 +726,9 @@ final class H264CompositionOffsetRepairSession { private let streamStartTime: Int64 private let ladderStart: Int64 private let codecName: String + /// Ticks per picture at the stream's average frame rate, for a head whose decode ladder has no step + /// of its own. + private let nominalStep: Int64? private var reader: H264PictureOrderReader? private var rewriter: H264CompositionOffsetRepair.Rewriter? private let partialRepairCandidate: H264PartialCompositionRepairSession? @@ -653,6 +765,8 @@ final class H264CompositionOffsetRepairSession { self.videoDelay = Int(codecpar.pointee.video_delay) self.streamStartTime = stream.pointee.start_time self.ladderStart = ladderStart + self.nominalStep = H264CompositionOffsetRepair.nominalFrameStep( + frameRate: stream.pointee.avg_frame_rate, timeBase: stream.pointee.time_base) self.reader = reader let (lead, overflow) = stream.pointee.start_time.subtractingReportingOverflow(ladderStart) // The partial-region repair reads H.264 NAL types to find its IDRs, so HEVC gets the @@ -847,11 +961,18 @@ final class H264CompositionOffsetRepairSession { } private func decide() { + decide(using: nil) + } + + /// `nominalStep` is nil on the first pass, so the missing-head partial repair below keeps first + /// claim on a non-uniform ladder; only when it declines does the nominal-rate rebuild get a turn. + private func decide(using nominalStep: Int64?) { let verdict = H264CompositionOffsetRepair.classify( samples: samples, videoDelay: videoDelay, streamStartTime: streamStartTime, - ladderStart: ladderStart + ladderStart: ladderStart, + nominalStep: nominalStep ) switch verdict { case .repair(let plan): @@ -864,6 +985,7 @@ final class H264CompositionOffsetRepairSession { } ?? "" verdictDescription = "repair step=\(plan.step) lead=\(plan.decodeLead) " + "shift=\(plan.shift) pocStep=\(plan.pocStep)" + cadenceDescription + + (plan.synthesizesDecodeLadder ? " ladder=nominal-rate" : "") phase = .repairing var rewriter = H264CompositionOffsetRepair.Rewriter(plan: plan) for entry in held where entry.packet.pointee.stream_index == streamIndex { @@ -885,7 +1007,7 @@ final class H264CompositionOffsetRepairSession { verdictDescription = "healthy" disarm() case .inconclusive(let reason): - if reason == "decode ladder is not uniform", sampledFramePictures, + if reason == "decode ladder is not uniform", nominalStep == nil, sampledFramePictures, let candidate = partialRepairCandidate, let forwardDepth = H264PartialCompositionRepair.missingHeadForwardDepth( dts: samples.map(\.dts), pts: samples.map(\.pts), @@ -904,6 +1026,10 @@ final class H264CompositionOffsetRepairSession { samples.removeAll(keepingCapacity: false) return } + if reason == "decode ladder is not uniform", nominalStep == nil, let step = self.nominalStep { + decide(using: step) + return + } verdictDescription = "inconclusive (\(reason))" // Only worth a line when the stream looked like a candidate: a file that simply carries // composition offsets is the normal case and says nothing. diff --git a/Tests/AetherEngineTests/DocumentedConstantsTests.swift b/Tests/AetherEngineTests/DocumentedConstantsTests.swift index baeec4f1f..6653af11e 100644 --- a/Tests/AetherEngineTests/DocumentedConstantsTests.swift +++ b/Tests/AetherEngineTests/DocumentedConstantsTests.swift @@ -46,6 +46,11 @@ final class DocumentedConstantsTests: XCTestCase { assertDocumented("five entries per item", docs) } + func testNominalRateCompositionShiftMatchesDocumentation() throws { + XCTAssertEqual(H264CompositionOffsetRepair.maximumNominalShiftPictures, 16) + assertDocumented("clamped to **16 pictures**", try documentation()) + } + func testPartialCompositionHoldBoundsMatchDocumentation() throws { let docs = try documentation() XCTAssertEqual(H264PartialCompositionRepair.maximumReorderDepth, 16) diff --git a/Tests/AetherEngineTests/H264ForcedMonotonicLadderRepairTests.swift b/Tests/AetherEngineTests/H264ForcedMonotonicLadderRepairTests.swift new file mode 100644 index 000000000..3b6f9f8f0 --- /dev/null +++ b/Tests/AetherEngineTests/H264ForcedMonotonicLadderRepairTests.swift @@ -0,0 +1,567 @@ +import Foundation +import AetherLibavcodec +import Testing +@testable import AetherEngine + +/// An MP4 writer that dropped `ctts` and then forced its decode-order presentation times to be +/// monotonic. Every sample reports `PTS == DTS`, anchors keep their own time, and each B picture is +/// pushed 1001 ticks past the picture decoded before it, so the decode ladder has no step for the #409 +/// classifier to read. The pictures still come at a constant rate, and the repair rebuilds both axes +/// from the picture order and that rate. +@Suite("H.264 missing composition offsets on a forced-monotonic ladder") +struct H264ForcedMonotonicLadderRepairTests { + + private struct Timestamps: Equatable, CustomStringConvertible { + var pts: Int64 + var dts: Int64 + var description: String { "pts=\(pts) dts=\(dts)" } + } + + private static func videoTimestamps(base64: String, seekingTo seconds: Double? = nil) throws -> [Timestamps] { + let data = try #require(Data(base64Encoded: base64, options: .ignoreUnknownCharacters)) + let demuxer = Demuxer() + try demuxer.open(reader: DataIOReader(data: data), formatHint: "mp4") + defer { demuxer.close() } + if let seconds { #expect(demuxer.seek(to: seconds)) } + let videoIndex = demuxer.videoStreamIndex + var result: [Timestamps] = [] + while let packet = try? demuxer.readPacket() { + if packet.pointee.stream_index == videoIndex { + result.append(Timestamps(pts: packet.pointee.pts, dts: packet.pointee.dts)) + } + var owned: UnsafeMutablePointer? = packet + trackedPacketFree(&owned) + } + return result + } + + @Test("the clamped twin is repaired to the healthy twin's timestamps") + func clampedLadderMatchesHealthyTwin() throws { + let healthy = try Self.videoTimestamps(base64: Self.healthyFixtureBase64) + let clamped = try Self.videoTimestamps(base64: Self.clampedFixtureBase64) + #expect(healthy.count == 60) + #expect(healthy.contains { $0.pts != $0.dts }) + #expect(clamped == healthy) + } + + @Test("a seek onto the second GOP lands on the healthy axis") + func seekLandsOnHealthyAxis() throws { + let healthy = try Self.videoTimestamps(base64: Self.healthyFixtureBase64, seekingTo: 1.2) + let clamped = try Self.videoTimestamps(base64: Self.clampedFixtureBase64, seekingTo: 1.2) + #expect(!healthy.isEmpty) + #expect(clamped == healthy) + } + + @Test("classify takes the step from the anchors, not the rounded average rate") + func classifyMeasuresAnchorStep() { + // Decode order I0 P4 B2 b1 b3 P8 B6 b5 b7 P12 B10 b9: what the writer left on the ladder. + let pocs: [Int64] = [0, 8, 4, 2, 6, 16, 12, 10, 14, 24, 20, 18] + let ladder: [Int64] = [0, 12012, 13013, 14014, 15015, 24024, 25025, 26026, 27027, 36036, 37037, 38038] + let samples = zip(pocs, ladder).enumerated().map { index, pair in + H264CompositionOffsetRepair.Sample(dts: pair.1, pts: pair.1, pictureOrderCount: pair.0, isKeyframe: index == 0) + } + let withoutRate = H264CompositionOffsetRepair.classify( + samples: samples, videoDelay: 2, streamStartTime: 0, ladderStart: 0) + #expect(withoutRate == .inconclusive("decode ladder is not uniform")) + guard case .repair(let plan) = H264CompositionOffsetRepair.classify( + samples: samples, videoDelay: 2, streamStartTime: 0, ladderStart: 0, nominalStep: 3020) + else { + Issue.record("expected a repair") + return + } + #expect(plan.synthesizesDecodeLadder) + #expect(plan.step == 3003) + #expect(plan.decodeLead == 6006) + #expect(plan.shift == 0) + } + + @Test("a variable-rate head is still left alone") + func variableRateHeadIsInconclusive() { + let pocs: [Int64] = [0, 8, 4, 2, 6, 16, 12, 10, 14, 24, 20, 18] + // Same order, but the span is twice what twelve pictures take at the nominal rate. + let ladder: [Int64] = [0, 24024, 26026, 28028, 30030, 48048, 50050, 52052, 54054, 72072, 74074, 76076] + let samples = zip(pocs, ladder).enumerated().map { index, pair in + H264CompositionOffsetRepair.Sample(dts: pair.1, pts: pair.1, pictureOrderCount: pair.0, isKeyframe: index == 0) + } + let verdict = H264CompositionOffsetRepair.classify( + samples: samples, videoDelay: 2, streamStartTime: 0, ladderStart: 0, nominalStep: 3003) + #expect(verdict == .inconclusive("decode ladder is not uniform")) + } + + /// 64x64 H.264 High, 29.97 fps, 60 frames, three B pictures per pyramid mini-GOP, two closed GOPs. + /// + /// ffmpeg -f lavfi -i 'testsrc=s=64x64:r=30000/1001:d=3' -frames:v 60 -c:v libx264 \ + /// -preset ultrafast -pix_fmt yuv420p -bf 3 \ + /// -x264-params 'b-pyramid=normal:b-adapt=0:keyint=30:min-keyint=30:scenecut=0' \ + /// -video_track_timescale 90000 -movflags +faststart healthy.mp4 + /// ffmpeg -i healthy.mp4 -map 0:v:0 -c:v copy -bsf:v 'setts=ts=max(PTS\,PREV_OUTDTS+1001)' \ + /// -video_track_timescale 90000 -movflags +faststart clamped.mp4 + static let healthyFixtureBase64 = """ + AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAX8bW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAB1MAAA6pwAAQAAAQAA + AAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAA + BSd0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAAA6pwAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAA + AAAAAAAAAAAAAABAAAAAAEAAAABAAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAOqcAAAXdgABAAAAAASfbWRpYQAAACBtZGhk + AAAAAAAAAAAAAAAAAAFfkAACv9RVxAAAAAAALWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAAESm1p + bmYAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAABApzdGJsAAAAtnN0c2QA + AAAAAAAAAQAAAKZhdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAEAAQABIAAAASAAAAAAAAAABFExhdmM2My4xLjEwMiBsaWJ4 + MjY0AAAAAAAAAAAAAAAAGP//AAAALGF2Y0MBTUAK/+EAFWdNQArsohNgIgAAB9IAAdTAHiRLLAEABGjOD8gAAAAQcGFzcAAAAAEA + AAABAAAAFGJ0cnQAAAAAAAD3gAAAAAAAAAAYc3R0cwAAAAAAAAABAAAAPAAAC7sAAAAYc3RzcwAAAAAAAAACAAAAAQAAAB8AAAHo + Y3R0cwAAAAAAAAA7AAAAAQAAF3YAAAABAAA6pwAAAAEAABd2AAAAAQAAAAAAAAABAAALuwAAAAEAADqnAAAAAQAAF3YAAAABAAAA + AAAAAAEAAAu7AAAAAQAAOqcAAAABAAAXdgAAAAEAAAAAAAAAAQAAC7sAAAABAAA6pwAAAAEAABd2AAAAAQAAAAAAAAABAAALuwAA + AAEAADqnAAAAAQAAF3YAAAABAAAAAAAAAAEAAAu7AAAAAQAAOqcAAAABAAAXdgAAAAEAAAAAAAAAAQAAC7sAAAABAAA6pwAAAAEA + ABd2AAAAAQAAAAAAAAABAAALuwAAAAIAABd2AAAAAQAAOqcAAAABAAAXdgAAAAEAAAAAAAAAAQAAC7sAAAABAAA6pwAAAAEAABd2 + AAAAAQAAAAAAAAABAAALuwAAAAEAADqnAAAAAQAAF3YAAAABAAAAAAAAAAEAAAu7AAAAAQAAOqcAAAABAAAXdgAAAAEAAAAAAAAA + AQAAC7sAAAABAAA6pwAAAAEAABd2AAAAAQAAAAAAAAABAAALuwAAAAEAADqnAAAAAQAAF3YAAAABAAAAAAAAAAEAAAu7AAAAAQAA + OqcAAAABAAAXdgAAAAEAAAAAAAAAAQAAC7sAAAABAAAXdgAAABxzdHNjAAAAAAAAAAEAAAABAAAAPAAAAAEAAAEEc3RzegAAAAAA + AAAAAAAAPAAAB9AAAAP1AAAA0gAAAHUAAAAqAAACRgAAAFgAAAAxAAAAJQAAAssAAABaAAAAKgAAACkAAAJDAAAAfgAAADIAAAAe + AAACMAAAAGsAAAAvAAAALQAAAkYAAAB+AAAAMQAAADsAAAIHAAAAYQAAADAAAAA/AAABBwAACNcAAAG6AAAAQQAAACkAAAAwAAAB + 8gAAAE0AAAApAAAAKQAAAiMAAABPAAAANQAAACsAAAIiAAAAagAAACcAAAAzAAACMQAAAHIAAAA1AAAAIwAAAh4AAABuAAAAJAAA + ACAAAAIzAAAAdwAAACsAAAAhAAABAwAAABRzdGNvAAAAAAAAAAEAAAYsAAAAYXVkdGEAAABZbWV0YQAAAAAAAAAhaGRscgAAAAAA + AAAAbWRpcmFwcGwAAAAAAAAAAAAAAAAsaWxzdAAAACSpdG9vAAAAHGRhdGEAAAABAAAAAExhdmY2My4xLjEwMgAAAAhmcmVlAAA9 + +G1kYXQAAAKeBgX//5rcRem95tlIt5Ys2CDZI+7veDI2NCAtIGNvcmUgMTY1IHIzMjIyIGIzNTYwNWEgLSBILjI2NC9NUEVHLTQg + QVZDIGNvZGVjIC0gQ29weWxlZnQgMjAwMy0yMDI1IC0gaHR0cDovL3d3dy52aWRlb2xhbi5vcmcveDI2NC5odG1sIC0gb3B0aW9u + czogY2FiYWM9MCByZWY9MSBkZWJsb2NrPTA6MDowIGFuYWx5c2U9MDowIG1lPWRpYSBzdWJtZT0wIHBzeT0xIHBzeV9yZD0xLjAw + OjAuMDAgbWl4ZWRfcmVmPTAgbWVfcmFuZ2U9MTYgY2hyb21hX21lPTEgdHJlbGxpcz0wIDh4OGRjdD0wIGNxbT0wIGRlYWR6b25l + PTIxLDExIGZhc3RfcHNraXA9MSBjaHJvbWFfcXBfb2Zmc2V0PTAgdGhyZWFkcz0yIGxvb2thaGVhZF90aHJlYWRzPTEgc2xpY2Vk + X3RocmVhZHM9MCBucj0wIGRlY2ltYXRlPTEgaW50ZXJsYWNlZD0wIGJsdXJheV9jb21wYXQ9MCBjb25zdHJhaW5lZF9pbnRyYT0w + IGJmcmFtZXM9MyBiX3B5cmFtaWQ9MiBiX2FkYXB0PTAgYl9iaWFzPTAgZGlyZWN0PTEgd2VpZ2h0Yj0wIG9wZW5fZ29wPTAgd2Vp + Z2h0cD0wIGtleWludD0zMCBrZXlpbnRfbWluPTE2IHNjZW5lY3V0PTAgaW50cmFfcmVmcmVzaD0wIHJjPWNyZiBtYnRyZWU9MCBj + cmY9MjMuMCBxY29tcD0wLjYwIHFwbWluPTAgcXBtYXg9NjkgcXBzdGVwPTQgaXBfcmF0aW89MS40MCBwYl9yYXRpbz0xLjMwIGFx + PTAAgAAABSpliIQA6DGAAgAQNZwNmdWWOzHthiOY//wAeC/FtjLYNd/XXAB4AAgAFKAVI57JANlf66AB4P8S2Mto139dBhxAABAE + AFIgAAgngACBkYAQEzyBIUoQjpnmDUpQgAaGrsGK54Wi84oFxIzBiucFIPOIMYAFohHABFx0JBh4AUAP7AAgvJW9I1XpvrwAs7ca + SpSQHwMf8Wa5/cpAYGriAABAAAAwBUsP/+EAARIAAIAYAAgNBAACAeAAIVoAIAKb4IFQ0TKSQD1XW/AAfW9QAoPqOD/XxAFwJE2s + 8AFtuIkKWHhjAB5pMMAKm6tIJMgCgAAguBwABABAA4BcsOAAIAIAHALl+OEHMgGyyxo6fa//hAAIAAgHANAAELsIAAQCQEAB3yAA + DQAgIlgC+jAJBIxYgKYn+/DpnLAFuhgnFRjwgFtz/fhj8AAEA4DkhQxi4XNyHhR2Ew0l4OAB4bIqMCXk4HK79/+AB5O0WfIC4Yb+ + 3wFQRRHsQaYl4r6iodAAAgAkKAVA5/JENFf668IAAQBAAPYAcEAAIJ4AAhSqAAEAjH1Q9xvc3vgwKZcZaHkU/QiL+4IuFA9Y6YcG + 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 + """ + + static let clampedFixtureBase64 = """ + AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAT8bW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAFfkAACw70AAQAAAQAA + AAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAA + BCd0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAACw70AAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAA + AAAAAAAAAAAAAABAAAAAAEAAAABAAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAsO9AAAAAAABAAAAAAOfbWRpYQAAACBtZGhk + AAAAAAAAAAAAAAAAAAFfkAACw71VxAAAAAAALWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAADSm1p + bmYAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAwpzdGJsAAAAtnN0c2QA + AAAAAAAAAQAAAKZhdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAEAAQABIAAAASAAAAAAAAAABFExhdmM2My4xLjEwMiBsaWJ4 + MjY0AAAAAAAAAAAAAAAAGP//AAAALGF2Y0MBTUAK/+EAFWdNQArsohNgIgAAB9IAAdTAHiRLLAEABGjOD8gAAAAQcGFzcAAAAAEA + AAABAAAAFGJ0cnQAAAAAAAD3gAAA9iIAAAEAc3R0cwAAAAAAAAAeAAAAAQAALuwAAAADAAAD6QAAAAEAACMxAAAAAwAAA+kAAAAB + AAAjMQAAAAMAAAPpAAAAAQAAIzEAAAADAAAD6QAAAAEAACMxAAAAAwAAA+kAAAABAAAjMQAAAAMAAAPpAAAAAQAAIzEAAAAEAAAD + 6QAAAAEAAAfSAAAAAQAALuwAAAADAAAD6QAAAAEAACMxAAAAAwAAA+kAAAABAAAjMQAAAAMAAAPpAAAAAQAAIzEAAAADAAAD6QAA + AAEAACMxAAAAAwAAA+kAAAABAAAjMQAAAAMAAAPpAAAAAQAAIzEAAAAEAAAD6QAAAAEAAAu7AAAAGHN0c3MAAAAAAAAAAgAAAAEA + AAAfAAAAHHN0c2MAAAAAAAAAAQAAAAEAAAA8AAAAAQAAAQRzdHN6AAAAAAAAAAAAAAA8AAAH0AAAA/UAAADSAAAAdQAAACoAAAJG + AAAAWAAAADEAAAAlAAACywAAAFoAAAAqAAAAKQAAAkMAAAB+AAAAMgAAAB4AAAIwAAAAawAAAC8AAAAtAAACRgAAAH4AAAAxAAAA + OwAAAgcAAABhAAAAMAAAAD8AAAEHAAAI1wAAAboAAABBAAAAKQAAADAAAAHyAAAATQAAACkAAAApAAACIwAAAE8AAAA1AAAAKwAA + AiIAAABqAAAAJwAAADMAAAIxAAAAcgAAADUAAAAjAAACHgAAAG4AAAAkAAAAIAAAAjMAAAB3AAAAKwAAACEAAAEDAAAAFHN0Y28A + AAAAAAAAAQAABSwAAABhdWR0YQAAAFltZXRhAAAAAAAAACFoZGxyAAAAAAAAAABtZGlyYXBwbAAAAAAAAAAAAAAAACxpbHN0AAAA + JKl0b28AAAAcZGF0YQAAAAEAAAAATGF2ZjYzLjEuMTAyAAAACGZyZWUAAD34bWRhdAAAAp4GBf//mtxF6b3m2Ui3lizYINkj7u94 + MjY0IC0gY29yZSAxNjUgcjMyMjIgYjM1NjA1YSAtIEguMjY0L01QRUctNCBBVkMgY29kZWMgLSBDb3B5bGVmdCAyMDAzLTIwMjUg + LSBodHRwOi8vd3d3LnZpZGVvbGFuLm9yZy94MjY0Lmh0bWwgLSBvcHRpb25zOiBjYWJhYz0wIHJlZj0xIGRlYmxvY2s9MDowOjAg + YW5hbHlzZT0wOjAgbWU9ZGlhIHN1Ym1lPTAgcHN5PTEgcHN5X3JkPTEuMDA6MC4wMCBtaXhlZF9yZWY9MCBtZV9yYW5nZT0xNiBj + aHJvbWFfbWU9MSB0cmVsbGlzPTAgOHg4ZGN0PTAgY3FtPTAgZGVhZHpvbmU9MjEsMTEgZmFzdF9wc2tpcD0xIGNocm9tYV9xcF9v + ZmZzZXQ9MCB0aHJlYWRzPTIgbG9va2FoZWFkX3RocmVhZHM9MSBzbGljZWRfdGhyZWFkcz0wIG5yPTAgZGVjaW1hdGU9MSBpbnRl + cmxhY2VkPTAgYmx1cmF5X2NvbXBhdD0wIGNvbnN0cmFpbmVkX2ludHJhPTAgYmZyYW1lcz0zIGJfcHlyYW1pZD0yIGJfYWRhcHQ9 + MCBiX2JpYXM9MCBkaXJlY3Q9MSB3ZWlnaHRiPTAgb3Blbl9nb3A9MCB3ZWlnaHRwPTAga2V5aW50PTMwIGtleWludF9taW49MTYg + c2NlbmVjdXQ9MCBpbnRyYV9yZWZyZXNoPTAgcmM9Y3JmIG1idHJlZT0wIGNyZj0yMy4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1h + eD02OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIHBiX3JhdGlvPTEuMzAgYXE9MACAAAAFKmWIhADoMYACABA1nA2Z1ZY7Me2GI5j/ + /AB4L8W2Mtg139dcAHgACAAUoBUjnskA2V/roAHg/xLYy2jXf10GHEAAEAQAUiAACCeAAIGRgBATPIEhShCOmeYNSlCABoauwYrn + haLzigXEjMGK5wUg84gxgAWiEcAEXHQkGHgBQA/sACC8lb0jVem+vACztxpKlJAfAx/xZrn9ykBgauIAAEAAADAFSw//4QABEgAA + gBgACA0EAAIB4AAhWgAgApvggVDRMpJAPVdb8AB9b1ACg+o4P9fEAXAkTazwAW24iQpYeGMAHmkwwAqbq0gkyAKAACC4HAAEAEAD + gFyw4AAgAgAcAuX44QcyAbLLGjp9r/+EAAgACAcA0AAQuwgABAJAQAHfIAANACAiWAL6MAkEjFiApif78OmcsAW6GCcVGPCAW3P9 + +GPwAAQDgOSFDGLhc3IeFHYTDSXg4AHhsiowJeTgcrv3/4AHk7RZ8gLhhv7fAVBFEexBpiXivqKh0AACACQoBUDn8kQ0V/rrwgAB + AEAA9gBwQAAgngACFKoAAQCMfVD3G9ze+DAplxloeRT9CIv7gi4UD1jphwYEGQjkcI/FmLe4MdgAIAGDvAT8iRXGXjwj3IwIhCAA + dBlEcxJpqXCvKq8MMor2pNNGCHd+r7gGQFF+pnCinufVAXIAAEAYAAQBAAy4H/AIgIK9XOVFRG2/UQkAACAMAAIAgAZcIAAEAsAA + QBwA+4HAAEAYAAQBAAy5+np4WcABAABABAIeAWsRILhIHx/Q6yEfj//IALYAAgAChCSmMkTBoN77lAUEUEFfrjKqohbdoIfXhhFF + c1BpiXC/KKqGvB0OHwtgA4AQMeAhdZZcYbt3G7h69/8DgACBsAAIAwLuYBCDDsSOUAop+Ver3/EyzaDGl47LWyxTyoryipaqXlFS + 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 + qdsbK5c= + """ +} diff --git a/docs/architecture.md b/docs/architecture.md index d207e70b5..2b429c22a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -605,7 +605,7 @@ Sources/AetherEngine/ │ ├── PlanBoundaryAxis.swift Which axis a container stamps its index entries on, and therefore which packet timestamp the cutter gate may compare against a plan boundary: decode for a mov/mp4 sample table, presentation for a Matroska Cue (#358, AE#561) │ ├── VideoConfigRecord.swift The `hvcC` / `avcC` config record and the framing question hanging off it (#365): movenc decides whether to Annex-B-convert every sample from the EXTRADATA rather than the packet, so a source carrying Annex-B parameter sets while muxing length-prefixed NALs has each sample rewritten by a converter that finds no start codes. Mirrors movenc's own two tests instead of an equivalent-looking predicate, since predicting that decision wrong is the whole defect │ ├── H264SPS.swift Hand-rolled H.264 SPS parser (SSAI ad-creative coded dimensions / codec config) -│ ├── H264CompositionOffsetRepair.swift Rebuilds the presentation axis of an MP4 whose writer dropped `ctts` while the bitstream still reorders pictures: libavcodec's H.264 or HEVC parser supplies each access unit's picture order count without decoding, a fail-closed head sample settles the ladder and the shift, and packets are rewritten to the timeline a correct muxer would have written, so the native path and hardware decode are kept (#409, HEVC #699) +│ ├── H264CompositionOffsetRepair.swift Rebuilds the presentation axis of an MP4 whose writer dropped `ctts` while the bitstream still reorders pictures: libavcodec's H.264 or HEVC parser supplies each access unit's picture order count without decoding, a fail-closed head sample settles the ladder and the shift, and packets are rewritten to the timeline a correct muxer would have written, so the native path and hardware decode are kept (#409, HEVC #699); a writer that forced the decode ladder monotonic is rebuilt at the stream's frame rate, measured on its anchors │ ├── OutputTimestampSanitizer.swift Final-stage DTS/PTS monotonicity guard before the fMP4 mux (SSAI splices, program restarts) │ ├── RestartCoalescer.swift Coalesces a burst of producer-restart requests into one in-flight + one settled target (rapid-seek, AetherEngine#35) │ ├── LiveWindow.swift Live path: session-relative DVR timeline (seconds since first frame), shared by the native and SW live paths diff --git a/docs/formats.md b/docs/formats.md index 5d42734f4..41b14fa22 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -99,6 +99,24 @@ exactly matches the observed reorder head, and a complete, distinct picture-orde timestamps pass the slot policy. It assigns existing slots plus that lead rather than fitting a frame rate; DTS, audio and the keyframe index remain unchanged. An ambiguous head stays untouched. +Some writers go one step further: after dropping `ctts` they force the decode-order presentation +times to be monotonic, so each anchor keeps its own presentation time and each B picture decoded +after it is pushed 1001 ticks (at 90 kHz, a third of a 29.97 fps frame) past the picture before +it. There is then no ladder to read a step from: the reporting file (H.264 High, 720p, GOP 90) +stepped 3003 on 87% of its samples and 1001, 2252, 9009 or 12012 on the rest, so the probe skipped it +as non-uniform and QuickTime, the Files app and both engine routes showed the B pictures out of +order. When nothing above takes such a head, it is repaired with the step taken from the stream's +average frame rate, provided the sampled span is the time its pictures take at that rate within the +reorder delay plus two pictures, and every other check above still passes. The step is then +remeasured on the anchors, which the writer left on the presentation axis (the average rate is only +an estimate: a 60-picture 29.97 fps clip reports 29.80), and used only when every anchor agrees to +within a tick per picture. DTS is rebuilt from each picture's decode slot rather than shifted, since a +pushed B picture's DTS can sit after its own presentation time. The shift is what puts the first +keyframe on the stream's start time, clamped to **16 pictures** because this writer left the keyframe +four pictures ahead. Measured on a generated twin (`Scripts/make-clamped-ladder-fixture.sh`, 900 +pictures at 29.97 fps), the AVPlayer route stepped backwards 169 times before and 0 after, and the +software route 44 times before and 0 after. + ### Matroska with presentation slots in coding order Matroska block timestamps are presentation timestamps by specification, and the format has no From d6bd6460dd4672a2ad66a7b59e7d68bd747838ea Mon Sep 17 00:00:00 2001 From: Sunghyun Kim Date: Mon, 26 Oct 2026 20:03:04 +0900 Subject: [PATCH 2/7] fix(video): place an open-GOP landing on a forced-monotonic ladder On this ladder a keyframe kept its own presentation time, so a seek landing re-anchors from its display rank, not its decode slot. After the seek the parser is reset and an H.264 non-IDR landing reads a relative picture order (-4 on the twin), which the non-negative guard left in decode order; a sequence anchored on a landing now accepts relative counts. A head whose anchors disagree on a step is left untouched instead of falling back to the rounded average rate. Adds open-GOP twin tests (play-through and seek) and a disagreeing-anchors test. Co-Authored-By: Claude Opus 5.5 --- .../Video/H264CompositionOffsetRepair.swift | 24 +- ...H264ForcedMonotonicLadderRepairTests.swift | 500 ++++++++++++++++++ docs/formats.md | 8 +- 3 files changed, 524 insertions(+), 8 deletions(-) diff --git a/Sources/AetherEngine/Video/H264CompositionOffsetRepair.swift b/Sources/AetherEngine/Video/H264CompositionOffsetRepair.swift index 586354373..9c93cc629 100644 --- a/Sources/AetherEngine/Video/H264CompositionOffsetRepair.swift +++ b/Sources/AetherEngine/Video/H264CompositionOffsetRepair.swift @@ -319,8 +319,11 @@ enum H264CompositionOffsetRepair { // The average frame rate is only an estimate (short files, rounded durations: a 60-picture // 29.97 clip reports 29.80). The writer's anchors carry their own presentation times untouched, - // so when they agree they give the exact step. - if synthesizesDecodeLadder, let measured = anchorStep(samples: samples, pocStep: pocStep) { + // so when they agree they give the exact step, and when they do not the head is not this shape. + if synthesizesDecodeLadder { + guard let measured = anchorStep(samples: samples, pocStep: pocStep) else { + return .inconclusive("anchors do not agree on a step") + } step = measured } @@ -547,6 +550,10 @@ enum H264CompositionOffsetRepair { /// The decode slot of the next picture in the current sequence, counted from its anchor. Only a /// synthesized ladder reads it. private var decodeOrdinal: Int64 = 0 + /// The current sequence was anchored on a seek landing rather than an IDR. The parser was reset + /// for the seek, so an H.264 open-GOP landing reads its picture order relative to itself (a + /// non-IDR I picture can come out at -4), and only differences within the sequence mean anything. + private var anchoredOnLanding = false /// Pictures emitted untouched because no anchor was available or the arithmetic did not /// close. Surfaced so a stream this repair does not describe is visible as a number. private(set) var unrepairedPictures = 0 @@ -560,6 +567,7 @@ enum H264CompositionOffsetRepair { sequenceBaseOrdinal = nil landingLeadingPictures = 0 decodeOrdinal = 0 + anchoredOnLanding = false } mutating func noteLanding(leadingPictures: Int64) { @@ -586,6 +594,7 @@ enum H264CompositionOffsetRepair { sequenceBaseOrdinal = plan.presentationOrdinal(ladderTimestamp: dts) awaitingReanchor = false decodeOrdinal = 0 + anchoredOnLanding = false } else if awaitingReanchor, isKeyframe { guard pictureOrderCount % plan.pocStep == 0 else { unrepairedPictures += 1 @@ -595,9 +604,12 @@ enum H264CompositionOffsetRepair { // The landing is decoded in slot k and displayed in slot k + leading, so the // sequence origin sits `landingIndex - leading` slots before its decode time. let (anchorSlots, slotsOverflow) = landingIndex.subtractingReportingOverflow(landingLeadingPictures) + // A synthesized ladder's keyframe kept its own presentation time (the writer only + // pushed the B pictures), so its origin is `landingIndex` slots back, not its decode slot. + let originSlots = plan.synthesizesDecodeLadder ? landingIndex : anchorSlots // Audit BIT-101: a 64-bit `tfdt` step and a landing order near -2^31 leave Int64. let (landingTicks, productOverflow) = slotsOverflow - ? (0, true) : anchorSlots.multipliedReportingOverflow(by: plan.step) + ? (0, true) : originSlots.multipliedReportingOverflow(by: plan.step) let (anchor, anchorOverflow) = dts.subtractingReportingOverflow(landingTicks) guard !productOverflow, !anchorOverflow else { unrepairedPictures += 1 @@ -606,11 +618,12 @@ enum H264CompositionOffsetRepair { sequenceAnchorDTS = anchor sequenceBaseOrdinal = plan.presentationOrdinal(ladderTimestamp: dts) .flatMap { ordinal -> Int64? in - let (base, overflow) = ordinal.subtractingReportingOverflow(anchorSlots) + let (base, overflow) = ordinal.subtractingReportingOverflow(originSlots) return overflow ? nil : base } awaitingReanchor = false decodeOrdinal = anchorSlots + anchoredOnLanding = true } guard let anchor = sequenceAnchorDTS, !awaitingReanchor else { unrepairedPictures += 1 @@ -618,7 +631,8 @@ enum H264CompositionOffsetRepair { } let slot = decodeOrdinal decodeOrdinal &+= 1 - guard pictureOrderCount >= 0, pictureOrderCount % plan.pocStep == 0 else { + let acceptsRelativeOrder = plan.synthesizesDecodeLadder && anchoredOnLanding + guard pictureOrderCount >= 0 || acceptsRelativeOrder, pictureOrderCount % plan.pocStep == 0 else { unrepairedPictures += 1 return nil } diff --git a/Tests/AetherEngineTests/H264ForcedMonotonicLadderRepairTests.swift b/Tests/AetherEngineTests/H264ForcedMonotonicLadderRepairTests.swift index 3b6f9f8f0..53d5e0423 100644 --- a/Tests/AetherEngineTests/H264ForcedMonotonicLadderRepairTests.swift +++ b/Tests/AetherEngineTests/H264ForcedMonotonicLadderRepairTests.swift @@ -52,6 +52,22 @@ struct H264ForcedMonotonicLadderRepairTests { #expect(clamped == healthy) } + @Test("a seek onto an open-GOP keyframe lands on the healthy axis") + func openGOPSeekLandsOnHealthyAxis() throws { + let healthy = try Self.videoTimestamps(base64: Self.openGOPHealthyFixtureBase64, seekingTo: 1.2) + let clamped = try Self.videoTimestamps(base64: Self.openGOPClampedFixtureBase64, seekingTo: 1.2) + #expect(!healthy.isEmpty) + #expect(clamped == healthy) + } + + @Test("the open-GOP clamped twin plays through on the healthy axis") + func openGOPClampedLadderMatchesHealthyTwin() throws { + let healthy = try Self.videoTimestamps(base64: Self.openGOPHealthyFixtureBase64) + let clamped = try Self.videoTimestamps(base64: Self.openGOPClampedFixtureBase64) + #expect(healthy.count == 60) + #expect(clamped == healthy) + } + @Test("classify takes the step from the anchors, not the rounded average rate") func classifyMeasuresAnchorStep() { // Decode order I0 P4 B2 b1 b3 P8 B6 b5 b7 P12 B10 b9: what the writer left on the ladder. @@ -75,6 +91,20 @@ struct H264ForcedMonotonicLadderRepairTests { #expect(plan.shift == 0) } + @Test("a head whose anchors disagree on the step is left alone") + func disagreeingAnchorsAreInconclusive() { + let pocs: [Int64] = [0, 8, 4, 2, 6, 16, 12, 10, 14, 24, 20, 18] + // The span fits the nominal rate, but the second anchor sits 2000 ticks off the step the + // others measure, so the rounded average rate would be the only source of a step. + let ladder: [Int64] = [0, 12012, 13013, 14014, 15015, 26024, 27025, 28026, 29027, 36036, 37037, 38038] + let samples = zip(pocs, ladder).enumerated().map { index, pair in + H264CompositionOffsetRepair.Sample(dts: pair.1, pts: pair.1, pictureOrderCount: pair.0, isKeyframe: index == 0) + } + let verdict = H264CompositionOffsetRepair.classify( + samples: samples, videoDelay: 2, streamStartTime: 0, ladderStart: 0, nominalStep: 3003) + #expect(verdict == .inconclusive("anchors do not agree on a step")) + } + @Test("a variable-rate head is still left alone") func variableRateHeadIsInconclusive() { let pocs: [Int64] = [0, 8, 4, 2, 6, 16, 12, 10, 14, 24, 20, 18] @@ -564,4 +594,474 @@ struct H264ForcedMonotonicLadderRepairTests { RFIWUjzvVLrgO4VViFVmjPGxIABZgDoBIgPAALMB0AkQFgDFAGWAMUAY+JtLE1USEf9bL+Gr5bgMQQxBgAEFQdJ3NCiKpLsogibd qdsbK5c= """ + /// The same 60 pictures with `open-gop=1`: the second keyframe is a non-IDR I picture whose + /// leading B pictures are decoded after it and displayed before it. + /// + /// ffmpeg -f lavfi -i 'testsrc=s=64x64:r=30000/1001:d=3' -frames:v 60 -c:v libx264 \ + /// -preset ultrafast -pix_fmt yuv420p -bf 3 \ + /// -x264-params 'b-pyramid=normal:b-adapt=0:keyint=30:min-keyint=30:scenecut=0:open-gop=1' \ + /// -video_track_timescale 90000 -movflags +faststart healthy.mp4 + /// (clamped.mp4 as above) + static let openGOPHealthyFixtureBase64 = """ + AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAYEbW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAB1MAAA6pwAAQAAAQAA + AAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAA + BS90cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAAA6pwAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAA + AAAAAAAAAAAAAABAAAAAAEAAAABAAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAOqcAAAXdgABAAAAAASnbWRpYQAAACBtZGhk + AAAAAAAAAAAAAAAAAAFfkAACv9RVxAAAAAAALWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAAEUm1p + bmYAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAABBJzdGJsAAAAtnN0c2QA + AAAAAAAAAQAAAKZhdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAEAAQABIAAAASAAAAAAAAAABFExhdmM2My4xLjEwMiBsaWJ4 + MjY0AAAAAAAAAAAAAAAAGP//AAAALGF2Y0MBTUAK/+EAFWdNQArsohNgIgAAB9IAAdTAHiRLLAEABGjOD8gAAAAQcGFzcAAAAAEA + AAABAAAAFGJ0cnQAAAAAAAD0AQAAAAAAAAAYc3R0cwAAAAAAAAABAAAAPAAAC7sAAAAYc3RzcwAAAAAAAAACAAAAAQAAAB4AAAHw + Y3R0cwAAAAAAAAA8AAAAAQAAF3YAAAABAAA6pwAAAAEAABd2AAAAAQAAAAAAAAABAAALuwAAAAEAADqnAAAAAQAAF3YAAAABAAAA + AAAAAAEAAAu7AAAAAQAAOqcAAAABAAAXdgAAAAEAAAAAAAAAAQAAC7sAAAABAAA6pwAAAAEAABd2AAAAAQAAAAAAAAABAAALuwAA + AAEAADqnAAAAAQAAF3YAAAABAAAAAAAAAAEAAAu7AAAAAQAAOqcAAAABAAAXdgAAAAEAAAAAAAAAAQAAC7sAAAABAAA6pwAAAAEA + ABd2AAAAAQAAAAAAAAABAAALuwAAAAEAACMxAAAAAQAAC7sAAAABAAA6pwAAAAEAABd2AAAAAQAAAAAAAAABAAALuwAAAAEAADqn + AAAAAQAAF3YAAAABAAAAAAAAAAEAAAu7AAAAAQAAOqcAAAABAAAXdgAAAAEAAAAAAAAAAQAAC7sAAAABAAA6pwAAAAEAABd2AAAA + AQAAAAAAAAABAAALuwAAAAEAADqnAAAAAQAAF3YAAAABAAAAAAAAAAEAAAu7AAAAAQAAOqcAAAABAAAXdgAAAAEAAAAAAAAAAQAA + C7sAAAABAAA6pwAAAAEAABd2AAAAAQAAAAAAAAABAAALuwAAAAEAABd2AAAAHHN0c2MAAAAAAAAAAQAAAAEAAAA8AAAAAQAAAQRz + dHN6AAAAAAAAAAAAAAA8AAAH0AAAA/UAAADSAAAAdQAAACoAAAJGAAAAWAAAADEAAAAlAAACywAAAFoAAAAqAAAAKQAAAkMAAAB+ + AAAAMgAAAB4AAAIwAAAAawAAAC8AAAAtAAACRgAAAH4AAAAxAAAAOwAAAgcAAABhAAAAMAAAAD8AAAjjAAAAFwAAAb4AAABBAAAA + KQAAADAAAAHyAAAATQAAACkAAAApAAACIwAAAE8AAAA1AAAAKwAAAiIAAABqAAAAJwAAADMAAAIxAAAAcgAAADUAAAAjAAACHgAA + AG4AAAAkAAAAIAAAAjMAAAB3AAAAKwAAACEAAAEDAAAAFHN0Y28AAAAAAAAAAQAABjQAAABhdWR0YQAAAFltZXRhAAAAAAAAACFo + ZGxyAAAAAAAAAABtZGlyYXBwbAAAAAAAAAAAAAAAACxpbHN0AAAAJKl0b28AAAAcZGF0YQAAAAEAAAAATGF2ZjYzLjEuMTAyAAAA + CGZyZWUAAD0YbWRhdAAAAp4GBf//mtxF6b3m2Ui3lizYINkj7u94MjY0IC0gY29yZSAxNjUgcjMyMjIgYjM1NjA1YSAtIEguMjY0 + L01QRUctNCBBVkMgY29kZWMgLSBDb3B5bGVmdCAyMDAzLTIwMjUgLSBodHRwOi8vd3d3LnZpZGVvbGFuLm9yZy94MjY0Lmh0bWwg + LSBvcHRpb25zOiBjYWJhYz0wIHJlZj0xIGRlYmxvY2s9MDowOjAgYW5hbHlzZT0wOjAgbWU9ZGlhIHN1Ym1lPTAgcHN5PTEgcHN5 + X3JkPTEuMDA6MC4wMCBtaXhlZF9yZWY9MCBtZV9yYW5nZT0xNiBjaHJvbWFfbWU9MSB0cmVsbGlzPTAgOHg4ZGN0PTAgY3FtPTAg + ZGVhZHpvbmU9MjEsMTEgZmFzdF9wc2tpcD0xIGNocm9tYV9xcF9vZmZzZXQ9MCB0aHJlYWRzPTIgbG9va2FoZWFkX3RocmVhZHM9 + MSBzbGljZWRfdGhyZWFkcz0wIG5yPTAgZGVjaW1hdGU9MSBpbnRlcmxhY2VkPTAgYmx1cmF5X2NvbXBhdD0wIGNvbnN0cmFpbmVk + X2ludHJhPTAgYmZyYW1lcz0zIGJfcHlyYW1pZD0yIGJfYWRhcHQ9MCBiX2JpYXM9MCBkaXJlY3Q9MSB3ZWlnaHRiPTAgb3Blbl9n + b3A9MSB3ZWlnaHRwPTAga2V5aW50PTMwIGtleWludF9taW49MTYgc2NlbmVjdXQ9MCBpbnRyYV9yZWZyZXNoPTAgcmM9Y3JmIG1i + dHJlZT0wIGNyZj0yMy4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIHBiX3JhdGlv + PTEuMzAgYXE9MACAAAAFKmWIhADoMYACABA1nA2Z1ZY7Me2GI5j//AB4L8W2Mtg139dcAHgACAAUoBUjnskA2V/roAHg/xLYy2jX + f10GHEAAEAQAUiAACCeAAIGRgBATPIEhShCOmeYNSlCABoauwYrnhaLzigXEjMGK5wUg84gxgAWiEcAEXHQkGHgBQA/sACC8lb0j + Vem+vACztxpKlJAfAx/xZrn9ykBgauIAAEAAADAFSw//4QABEgAAgBgACA0EAAIB4AAhWgAgApvggVDRMpJAPVdb8AB9b1ACg+o4 + P9fEAXAkTazwAW24iQpYeGMAHmkwwAqbq0gkyAKAACC4HAAEAEADgFyw4AAgAgAcAuX44QcyAbLLGjp9r/+EAAgACAcA0AAQuwgA + BAJAQAHfIAANACAiWAL6MAkEjFiApif78OmcsAW6GCcVGPCAW3P9+GPwAAQDgOSFDGLhc3IeFHYTDSXg4AHhsiowJeTgcrv3/4AH + k7RZ8gLhhv7fAVBFEexBpiXivqKh0AACACQoBUDn8kQ0V/rrwgABAEAA9gBwQAAgngACFKoAAQCMfVD3G9ze+DAplxloeRT9CIv7 + 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 + """ + + static let openGOPClampedFixtureBase64 = """ + AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAUEbW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAFfkAACw70AAQAAAQAA + AAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAA + BC90cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAACw70AAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAA + AAAAAAAAAAAAAABAAAAAAEAAAABAAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAsO9AAAAAAABAAAAAAOnbWRpYQAAACBtZGhk + AAAAAAAAAAAAAAAAAAFfkAACw71VxAAAAAAALWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAADUm1p + bmYAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAxJzdGJsAAAAtnN0c2QA + AAAAAAAAAQAAAKZhdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAEAAQABIAAAASAAAAAAAAAABFExhdmM2My4xLjEwMiBsaWJ4 + MjY0AAAAAAAAAAAAAAAAGP//AAAALGF2Y0MBTUAK/+EAFWdNQArsohNgIgAAB9IAAdTAHiRLLAEABGjOD8gAAAAQcGFzcAAAAAEA + AAABAAAAFGJ0cnQAAAAAAAD0AQAA8qgAAAEIc3R0cwAAAAAAAAAfAAAAAQAALuwAAAADAAAD6QAAAAEAACMxAAAAAwAAA+kAAAAB + AAAjMQAAAAMAAAPpAAAAAQAAIzEAAAADAAAD6QAAAAEAACMxAAAAAwAAA+kAAAABAAAjMQAAAAMAAAPpAAAAAQAAIzEAAAADAAAD + 6QAAAAEAAAu7AAAAAQAAA+kAAAABAAArAwAAAAMAAAPpAAAAAQAAIzEAAAADAAAD6QAAAAEAACMxAAAAAwAAA+kAAAABAAAjMQAA + AAMAAAPpAAAAAQAAIzEAAAADAAAD6QAAAAEAACMxAAAAAwAAA+kAAAABAAAjMQAAAAQAAAPpAAAAAQAAC7sAAAAYc3RzcwAAAAAA + AAACAAAAAQAAAB4AAAAcc3RzYwAAAAAAAAABAAAAAQAAADwAAAABAAABBHN0c3oAAAAAAAAAAAAAADwAAAfQAAAD9QAAANIAAAB1 + AAAAKgAAAkYAAABYAAAAMQAAACUAAALLAAAAWgAAACoAAAApAAACQwAAAH4AAAAyAAAAHgAAAjAAAABrAAAALwAAAC0AAAJGAAAA + fgAAADEAAAA7AAACBwAAAGEAAAAwAAAAPwAACOMAAAAXAAABvgAAAEEAAAApAAAAMAAAAfIAAABNAAAAKQAAACkAAAIjAAAATwAA + ADUAAAArAAACIgAAAGoAAAAnAAAAMwAAAjEAAAByAAAANQAAACMAAAIeAAAAbgAAACQAAAAgAAACMwAAAHcAAAArAAAAIQAAAQMA + AAAUc3RjbwAAAAAAAAABAAAFNAAAAGF1ZHRhAAAAWW1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAG1kaXJhcHBsAAAAAAAAAAAAAAAA + LGlsc3QAAAAkqXRvbwAAABxkYXRhAAAAAQAAAABMYXZmNjMuMS4xMDIAAAAIZnJlZQAAPRhtZGF0AAACngYF//+a3EXpvebZSLeW + LNgg2SPu73gyNjQgLSBjb3JlIDE2NSByMzIyMiBiMzU2MDVhIC0gSC4yNjQvTVBFRy00IEFWQyBjb2RlYyAtIENvcHlsZWZ0IDIw + MDMtMjAyNSAtIGh0dHA6Ly93d3cudmlkZW9sYW4ub3JnL3gyNjQuaHRtbCAtIG9wdGlvbnM6IGNhYmFjPTAgcmVmPTEgZGVibG9j + az0wOjA6MCBhbmFseXNlPTA6MCBtZT1kaWEgc3VibWU9MCBwc3k9MSBwc3lfcmQ9MS4wMDowLjAwIG1peGVkX3JlZj0wIG1lX3Jh + bmdlPTE2IGNocm9tYV9tZT0xIHRyZWxsaXM9MCA4eDhkY3Q9MCBjcW09MCBkZWFkem9uZT0yMSwxMSBmYXN0X3Bza2lwPTEgY2hy + b21hX3FwX29mZnNldD0wIHRocmVhZHM9MiBsb29rYWhlYWRfdGhyZWFkcz0xIHNsaWNlZF90aHJlYWRzPTAgbnI9MCBkZWNpbWF0 + ZT0xIGludGVybGFjZWQ9MCBibHVyYXlfY29tcGF0PTAgY29uc3RyYWluZWRfaW50cmE9MCBiZnJhbWVzPTMgYl9weXJhbWlkPTIg + Yl9hZGFwdD0wIGJfYmlhcz0wIGRpcmVjdD0xIHdlaWdodGI9MCBvcGVuX2dvcD0xIHdlaWdodHA9MCBrZXlpbnQ9MzAga2V5aW50 + X21pbj0xNiBzY2VuZWN1dD0wIGludHJhX3JlZnJlc2g9MCByYz1jcmYgbWJ0cmVlPTAgY3JmPTIzLjAgcWNvbXA9MC42MCBxcG1p + bj0wIHFwbWF4PTY5IHFwc3RlcD00IGlwX3JhdGlvPTEuNDAgcGJfcmF0aW89MS4zMCBhcT0wAIAAAAUqZYiEAOgxgAIAEDWcDZnV + ljsx7YYjmP/8AHgvxbYy2DXf11wAeAAIABSgFSOeyQDZX+ugAeD/EtjLaNd/XQYcQAAQBABSIAAIJ4AAgZGAEBM8gSFKEI6Z5g1K + UIAGhq7BiueFovOKBcSMwYrnBSDziDGABaIRwARcdCQYeAFAD+wAILyVvSNV6b68ALO3GkqUkB8DH/Fmuf3KQGBq4gAAQAAAMAVL + D//hAAESAACAGAAIDQQAAgHgACFaACACm+CBUNEykkA9V1vwAH1vUAKD6jg/18QBcCRNrPABbbiJClh4YwAeaTDACpurSCTIAoAA + ILgcAAQAQAOAXLDgACACABwC5fjhBzIBsssaOn2v/4QACAAIBwDQABC7CAAEAkBAAd8gAA0AICJYAvowCQSMWICmJ/vw6ZywBboY + JxUY8IBbc/34Y/AABAOA5IUMYuFzch4UdhMNJeDgAeGyKjAl5OByu/f/gAeTtFnyAuGG/t8BUEUR7EGmJeK+oqHQAAIAJCgFQOfy + RDRX+uvCAAEAQAD2AHBAACCeAAIUqgABAIx9UPcb3N74MCmXGWh5FP0Ii/uCLhQPWOmHBgQZCORwj8WYt7gx2AAgAYO8BPyJFcZe + PCPcjAiEIAB0GURzEmmpcK8qrwwyivak00YId36vuAZAUX6mcKKe59UBcgAAQBgABAEADLgf8AiAgr1c5UVEbb9RCQAAIAwAAgCA + BlwgAAQCwABAHAD7gcAAQBgABAEADLn6enhZwAEAAEAEAh4BaxEguEgfH9DrIR+P/8gAtgACAAKEJKYyRMGg3vuUBQRQQV+uMqqi + Ft2gh9eGEUVzUGmJcL8oqoa8HQ4fC2ADgBAx4CF1llxhu3cbuHr3/wOAAIGwAAgDAu5gEIMOxI5QCin5V6vf8TLNoMaXjstbLFPK + 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 + NCiKpLsogibdqdsbK5c= + """ } diff --git a/docs/formats.md b/docs/formats.md index 41b14fa22..8f1706b09 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -109,9 +109,11 @@ order. When nothing above takes such a head, it is repaired with the step taken average frame rate, provided the sampled span is the time its pictures take at that rate within the reorder delay plus two pictures, and every other check above still passes. The step is then remeasured on the anchors, which the writer left on the presentation axis (the average rate is only -an estimate: a 60-picture 29.97 fps clip reports 29.80), and used only when every anchor agrees to -within a tick per picture. DTS is rebuilt from each picture's decode slot rather than shifted, since a -pushed B picture's DTS can sit after its own presentation time. The shift is what puts the first +an estimate: a 60-picture 29.97 fps clip reports 29.80); a head whose anchors do not agree to +within a tick per picture is left untouched. DTS is rebuilt from each picture's decode slot rather than shifted, since a +pushed B picture's DTS can sit after its own presentation time. After a seek onto an open-GOP +keyframe, which kept its own presentation time, the sequence origin is taken from its display rank +rather than its decode slot. The shift is what puts the first keyframe on the stream's start time, clamped to **16 pictures** because this writer left the keyframe four pictures ahead. Measured on a generated twin (`Scripts/make-clamped-ladder-fixture.sh`, 900 pictures at 29.97 fps), the AVPlayer route stepped backwards 169 times before and 0 after, and the From 4530d684e6a88b4370192a7b9b91fac2014cc65e Mon Sep 17 00:00:00 2001 From: Sunghyun Kim Date: Tue, 27 Oct 2026 06:56:04 +0900 Subject: [PATCH 3/7] fix(video): take a standard frame rate when a forced-monotonic ladder pushed its anchors too The reporting file pushes its P pictures 1001 ticks past the picture before them just like the B pictures, so no two anchors agree on a step and the probe left it untouched ("anchors do not agree on a step"). When the anchors give no step, the nominal-rate repair now uses the standard frame rate nearest the average rate, if it is within 1% and a whole number of ticks; a head with neither is still left alone. Scripts/make-clamped-ladder-fixture.sh also writes pushed-anchors.mp4 with that shape, and the tests embed a 60-picture twin of it (plays and seeks onto the healthy twin's timestamps; both fail without this change). On the reporting file (23 min, 41487 pictures) every presentation time is now distinct and on the 3003 lattice, and seeks to five positions land on the continuous run's times. --- CHANGELOG.md | 7 +- Scripts/make-clamped-ladder-fixture.sh | 35 +- .../Video/H264CompositionOffsetRepair.swift | 46 ++- ...H264ForcedMonotonicLadderRepairTests.swift | 298 +++++++++++++++++- docs/formats.md | 16 +- 5 files changed, 378 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index efca48619..bac435fd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -122,11 +122,12 @@ the public-API contract. ### Fixed - **MP4s whose writer dropped `ctts` and forced the decode ladder monotonic play in order.** - Such a file reports `PTS == DTS` on every sample, keeps each anchor on its own presentation time - and pushes each B picture 1001 ticks past the picture decoded before it, so the ladder has no single + Such a file reports `PTS == DTS` on every sample and pushes each B picture (on some files each anchor + too) 1001 ticks past the picture decoded before it, so the ladder has no single step and the #409 probe left the stream untouched ("decode ladder is not uniform"). Both routes then showed the B pictures in decode order, as QuickTime and the Files app do. The repair now takes the - step from the stream's average frame rate, remeasures it on the anchors and rebuilds DTS from each + step from the stream's average frame rate, remeasures it on the anchors (or, when the anchors were + pushed as well, snaps it to the nearest standard frame rate within 1%) and rebuilds DTS from each picture's decode slot, under the same picture-order checks. `Scripts/make-clamped-ladder-fixture.sh` generates a twin pair. See `docs/formats.md`. diff --git a/Scripts/make-clamped-ladder-fixture.sh b/Scripts/make-clamped-ladder-fixture.sh index 94690c586..389b03e71 100755 --- a/Scripts/make-clamped-ladder-fixture.sh +++ b/Scripts/make-clamped-ladder-fixture.sh @@ -6,8 +6,14 @@ # QuickTime and the Files app (AVFoundation) show such a file with its pictures stepping back and # forth; FFmpeg-based players reorder by picture order count and look right. # +# Some writers push the anchors as well (the reporting file has P pictures 1001 ticks past the picture +# before them), so the anchors no longer measure a step either. pushed-anchors.mp4 reproduces that by +# adding 1001 ticks to every other picture before the clamp, away from the end of each GOP so the +# keyframes keep their own time. +# # bframes-healthy.mp4 H.264 High 1280x720 29.97 fps, B pyramid, GOP 90, 30 s + AAC (the twin) # clamped-ladder.mp4 the same stream, ctts dropped, ladder clamped as above +# pushed-anchors.mp4 the same stream, ctts dropped, anchors pushed too # # testsrc2 draws a frame counter and a moving bar, so the order is visible by eye. # @@ -15,7 +21,7 @@ # ./Scripts/make-clamped-ladder-fixture.sh [output-dir] [--check] # # --check builds aetherctl and prints the #409 verdict (pktdump) and the decoded order (swdecode) -# for the clamped file. Requires ffmpeg with libx264 (macOS: `brew install ffmpeg`). +# for both clamped files. Requires ffmpeg with libx264 (macOS: `brew install ffmpeg`). set -euo pipefail @@ -44,16 +50,29 @@ FF=(ffmpeg -hide_banner -loglevel error -y) -bsf:v 'setts=ts=max(PTS\,PREV_OUTDTS+1001)' -video_track_timescale 90000 -movflags +faststart \ "$OUT/clamped-ladder.mp4" +# N counts packets in decode order, so mod(N, 2) pushes P and B pictures alike. The last ten pictures +# of each GOP are left alone so the push does not run on into the next keyframe. +"${FF[@]}" -i "$OUT/bframes-healthy.mp4" -map 0 -c copy \ + -bsf:v 'setts=ts=max(PTS+1001*mod(N\,2)*lt(mod(N\,90)\,80)\,PREV_OUTDTS+1001)' \ + -video_track_timescale 90000 -movflags +faststart \ + "$OUT/pushed-anchors.mp4" + +FIXTURES=(clamped-ladder.mp4 pushed-anchors.mp4) echo "wrote $OUT/bframes-healthy.mp4" -echo "wrote $OUT/clamped-ladder.mp4" -ffprobe -v error -select_streams v:0 -show_entries packet=pts,dts -of csv=p=0 "$OUT/clamped-ladder.mp4" \ - | awk -F, 'NR > 1 { step[$2 - prev]++ } { if ($1 == $2) same++; prev = $2; n++ } - END { printf "clamped-ladder.mp4: %d packets, PTS == DTS on %d, DTS steps:", n, same - for (s in step) printf " %s x%d", s, step[s]; print "" }' +for name in "${FIXTURES[@]}"; do + echo "wrote $OUT/$name" + ffprobe -v error -select_streams v:0 -show_entries packet=pts,dts -of csv=p=0 "$OUT/$name" \ + | awk -F, -v name="$name" 'NR > 1 { step[$2 - prev]++ } { if ($1 == $2) same++; prev = $2; n++ } + END { printf "%s: %d packets, PTS == DTS on %d, DTS steps:", name, n, same + for (s in step) printf " %s x%d", s, step[s]; print "" }' +done if [[ $CHECK -eq 1 ]]; then swift build --package-path "$REPO_ROOT" --product aetherctl >/dev/null 2>&1 CTL="$(swift build --package-path "$REPO_ROOT" --product aetherctl --show-bin-path 2>/dev/null)/aetherctl" - "$CTL" pktdump --count 12 "$OUT/clamped-ladder.mp4" 2>&1 | grep -E '#409' || echo "no #409 verdict line" - "$CTL" swdecode --frames 300 "$OUT/clamped-ladder.mp4" 2>&1 | grep -E 'Steps backwards' || true + for name in "${FIXTURES[@]}"; do + echo "$name:" + "$CTL" pktdump --count 12 "$OUT/$name" 2>&1 | grep -E '#409' || echo "no #409 verdict line" + "$CTL" swdecode --frames 300 "$OUT/$name" 2>&1 | grep -E 'Steps backwards' || true + done fi diff --git a/Sources/AetherEngine/Video/H264CompositionOffsetRepair.swift b/Sources/AetherEngine/Video/H264CompositionOffsetRepair.swift index 9c93cc629..9efa1d3fb 100644 --- a/Sources/AetherEngine/Video/H264CompositionOffsetRepair.swift +++ b/Sources/AetherEngine/Video/H264CompositionOffsetRepair.swift @@ -212,7 +212,8 @@ enum H264CompositionOffsetRepair { videoDelay: Int, streamStartTime: Int64, ladderStart: Int64, - nominalStep: Int64? = nil + nominalStep: Int64? = nil, + standardStep: Int64? = nil ) -> Verdict { guard videoDelay > 0, videoDelay <= 16 else { return .inconclusive("reorder delay \(videoDelay) outside 1...16") @@ -318,13 +319,18 @@ enum H264CompositionOffsetRepair { } // The average frame rate is only an estimate (short files, rounded durations: a 60-picture - // 29.97 clip reports 29.80). The writer's anchors carry their own presentation times untouched, - // so when they agree they give the exact step, and when they do not the head is not this shape. + // 29.97 clip reports 29.80). When the writer left its anchors on the presentation axis they give + // the exact step. The reporting file pushed its anchors as well (a P picture 1001 ticks after the + // P before it), so they agree on nothing; the step is then the standard rate the estimate is + // within 1% of, and a head with neither is left alone. if synthesizesDecodeLadder { - guard let measured = anchorStep(samples: samples, pocStep: pocStep) else { + if let measured = anchorStep(samples: samples, pocStep: pocStep) { + step = measured + } else if let standardStep { + step = standardStep + } else { return .inconclusive("anchors do not agree on a step") } - step = measured } // The reorder head is one decode lead long, and with a fractional cadence that lead is a @@ -402,6 +408,28 @@ enum H264CompositionOffsetRepair { return Int64(ticks.rounded()) } + /// Frame rates a camera, encoder or broadcast chain actually produces. + static let standardFrameRates: [(num: Int64, den: Int64)] = [ + (24000, 1001), (24, 1), (25, 1), (30000, 1001), (30, 1), (48, 1), (50, 1), (60000, 1001), (60, 1), + ] + + /// The whole-tick picture length of the standard frame rate closest to `estimate`, within 1%. 29.97 + /// and 30 are both that close to either, so the nearer one wins and a tie names neither. nil when no + /// standard rate is that close, or when the nearest one is not a whole number of ticks in + /// `timeBase` (23.976 at 90 kHz): a neighbouring rate that happens to be whole would drift. + static func standardFrameStep(near estimate: Int64, timeBase: AVRational) -> Int64? { + guard estimate >= 2, timeBase.num > 0, timeBase.den > 0 else { return nil } + let ranked = standardFrameRates.map { rate -> (num: Int64, den: Int64, distance: Double) in + let numerator = Int64(timeBase.den) * rate.den + let denominator = Int64(timeBase.num) * rate.num + return (numerator, denominator, abs(Double(numerator) / Double(denominator) - Double(estimate))) + }.sorted { $0.distance < $1.distance } + guard let nearest = ranked.first, nearest.distance * 100 <= Double(estimate) else { return nil } + if ranked.count > 1, ranked[1].distance == nearest.distance { return nil } + guard nearest.num % nearest.den == 0 else { return nil } + return nearest.num / nearest.den + } + /// Whether a non-uniform sampled ladder still spans the time its pictures would take at `step`. The /// writer's forced-monotonic ladder runs ahead of that by at most the reorder head plus the anchor /// it pushed past (measured 3.5 pictures on the reporting file at delay 2), and falls behind only @@ -743,6 +771,8 @@ final class H264CompositionOffsetRepairSession { /// Ticks per picture at the stream's average frame rate, for a head whose decode ladder has no step /// of its own. private let nominalStep: Int64? + /// `nominalStep` snapped to the standard rate it estimates, for a head whose anchors give no step. + private let standardStep: Int64? private var reader: H264PictureOrderReader? private var rewriter: H264CompositionOffsetRepair.Rewriter? private let partialRepairCandidate: H264PartialCompositionRepairSession? @@ -781,6 +811,9 @@ final class H264CompositionOffsetRepairSession { self.ladderStart = ladderStart self.nominalStep = H264CompositionOffsetRepair.nominalFrameStep( frameRate: stream.pointee.avg_frame_rate, timeBase: stream.pointee.time_base) + self.standardStep = nominalStep.flatMap { + H264CompositionOffsetRepair.standardFrameStep(near: $0, timeBase: stream.pointee.time_base) + } self.reader = reader let (lead, overflow) = stream.pointee.start_time.subtractingReportingOverflow(ladderStart) // The partial-region repair reads H.264 NAL types to find its IDRs, so HEVC gets the @@ -986,7 +1019,8 @@ final class H264CompositionOffsetRepairSession { videoDelay: videoDelay, streamStartTime: streamStartTime, ladderStart: ladderStart, - nominalStep: nominalStep + nominalStep: nominalStep, + standardStep: nominalStep == nil ? nil : standardStep ) switch verdict { case .repair(let plan): diff --git a/Tests/AetherEngineTests/H264ForcedMonotonicLadderRepairTests.swift b/Tests/AetherEngineTests/H264ForcedMonotonicLadderRepairTests.swift index 53d5e0423..b53d4c1d5 100644 --- a/Tests/AetherEngineTests/H264ForcedMonotonicLadderRepairTests.swift +++ b/Tests/AetherEngineTests/H264ForcedMonotonicLadderRepairTests.swift @@ -68,6 +68,22 @@ struct H264ForcedMonotonicLadderRepairTests { #expect(clamped == healthy) } + @Test("a twin whose anchors were pushed too is repaired to the healthy twin's timestamps") + func pushedAnchorsMatchHealthyTwin() throws { + let healthy = try Self.videoTimestamps(base64: Self.healthyFixtureBase64) + let pushed = try Self.videoTimestamps(base64: Self.pushedAnchorsFixtureBase64) + #expect(healthy.count == 60) + #expect(pushed == healthy) + } + + @Test("a seek on the pushed-anchor twin lands on the healthy axis") + func pushedAnchorsSeekLandsOnHealthyAxis() throws { + let healthy = try Self.videoTimestamps(base64: Self.healthyFixtureBase64, seekingTo: 1.2) + let pushed = try Self.videoTimestamps(base64: Self.pushedAnchorsFixtureBase64, seekingTo: 1.2) + #expect(!healthy.isEmpty) + #expect(pushed == healthy) + } + @Test("classify takes the step from the anchors, not the rounded average rate") func classifyMeasuresAnchorStep() { // Decode order I0 P4 B2 b1 b3 P8 B6 b5 b7 P12 B10 b9: what the writer left on the ladder. @@ -91,7 +107,7 @@ struct H264ForcedMonotonicLadderRepairTests { #expect(plan.shift == 0) } - @Test("a head whose anchors disagree on the step is left alone") + @Test("a head whose anchors disagree on the step is left alone without a standard rate") func disagreeingAnchorsAreInconclusive() { let pocs: [Int64] = [0, 8, 4, 2, 6, 16, 12, 10, 14, 24, 20, 18] // The span fits the nominal rate, but the second anchor sits 2000 ticks off the step the @@ -105,6 +121,47 @@ struct H264ForcedMonotonicLadderRepairTests { #expect(verdict == .inconclusive("anchors do not agree on a step")) } + @Test("a head that pushed its anchors too takes the standard rate the estimate names") + func pushedAnchorsTakeStandardStep() { + // The reporting file's head: the keyframe four pictures ahead, and P pictures pushed 1001 + // ticks past the picture before them just like the B pictures, so no two anchors agree. + let pocs: [Int64] = [0, 2, 8, 4, 6, 10, 18, 14, 12, 16, 26, 22] + let ladder: [Int64] = [-12012, 0, 1001, 2002, 3003, 15015, 18018, 21021, 24024, 27027, 29279, 31531] + let samples = zip(pocs, ladder).enumerated().map { index, pair in + H264CompositionOffsetRepair.Sample(dts: pair.1, pts: pair.1, pictureOrderCount: pair.0, isKeyframe: index == 0) + } + let withoutStandard = H264CompositionOffsetRepair.classify( + samples: samples, videoDelay: 2, streamStartTime: 0, ladderStart: -12012, nominalStep: 3003) + #expect(withoutStandard == .inconclusive("anchors do not agree on a step")) + guard case .repair(let plan) = H264CompositionOffsetRepair.classify( + samples: samples, videoDelay: 2, streamStartTime: 0, ladderStart: -12012, + nominalStep: 3003, standardStep: 3003) + else { + Issue.record("expected a repair") + return + } + #expect(plan.synthesizesDecodeLadder) + #expect(plan.step == 3003) + #expect(plan.decodeLead == 6006) + #expect(plan.shift == 12012) + } + + @Test("the standard step is the nearest standard rate within 1%") + func standardFrameStepSnapsToNearestRate() { + let ninetyKilohertz = AVRational(num: 1, den: 90000) + // 29.97 and 30 are both within 1% of each other; the nearer one wins. + #expect(H264CompositionOffsetRepair.standardFrameStep(near: 3003, timeBase: ninetyKilohertz) == 3003) + #expect(H264CompositionOffsetRepair.standardFrameStep(near: 3001, timeBase: ninetyKilohertz) == 3000) + // A 60-picture 29.97 clip reports 29.80. + #expect(H264CompositionOffsetRepair.standardFrameStep(near: 3020, timeBase: ninetyKilohertz) == 3003) + #expect(H264CompositionOffsetRepair.standardFrameStep(near: 3600, timeBase: ninetyKilohertz) == 3600) + // 27 fps names nothing. + #expect(H264CompositionOffsetRepair.standardFrameStep(near: 3333, timeBase: ninetyKilohertz) == nil) + // 23.976 is not a whole number of 90 kHz ticks, and 24 fps (3750) would drift. + #expect(H264CompositionOffsetRepair.standardFrameStep(near: 3754, timeBase: ninetyKilohertz) == nil) + #expect(H264CompositionOffsetRepair.standardFrameStep(near: 1001, timeBase: AVRational(num: 1, den: 24000)) == 1001) + } + @Test("a variable-rate head is still left alone") func variableRateHeadIsInconclusive() { let pocs: [Int64] = [0, 8, 4, 2, 6, 16, 12, 10, 14, 24, 20, 18] @@ -1064,4 +1121,243 @@ struct H264ForcedMonotonicLadderRepairTests { ABDoQkIGaIi0RFIWUjzvVLrgO4VViFVmjPGxIABZgDoBIgPAALMB0AkQFgDFAGWAMUAY+JtLE1USEf9bL+Gr5bgMQQxBgAEFQdJ3 NCiKpLsogibdqdsbK5c= """ + + /// The healthy 60 pictures with every other picture, anchors included, pushed 1001 ticks before the + /// clamp, except the last ten of each GOP so the push does not reach the next keyframe: + /// + /// ffmpeg -i healthy.mp4 -map 0:v:0 -c:v copy \ + /// -bsf:v 'setts=ts=max(PTS+1001*mod(N\,2)*lt(mod(N\,30)\,20)\,PREV_OUTDTS+1001)' \ + /// -video_track_timescale 90000 -movflags +faststart pushed.mp4 + static let pushedAnchorsFixtureBase64 = """ + AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAT8bW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAFfkAACw70AAQAAAQAA + AAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAA + BCd0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAACw70AAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAA + AAAAAAAAAAAAAABAAAAAAEAAAABAAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAsO9AAAAAAABAAAAAAOfbWRpYQAAACBtZGhk + AAAAAAAAAAAAAAAAAAFfkAACw71VxAAAAAAALWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAADSm1p + bmYAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAwpzdGJsAAAAtnN0c2QA + AAAAAAAAAQAAAKZhdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAEAAQABIAAAASAAAAAAAAAABFExhdmM2My4xLjEwMiBsaWJ4 + MjY0AAAAAAAAAAAAAAAAGP//AAAALGF2Y0MBTUAK/+EAFWdNQArsohNgIgAAB9IAAdTAHiRLLAEABGjOD8gAAAAQcGFzcAAAAAEA + AAABAAAAFGJ0cnQAAAAAAAD3gAAA9iIAAAEAc3R0cwAAAAAAAAAeAAAAAQAAMtUAAAADAAAD6QAAAAEAACMxAAAAAwAAA+kAAAAB + AAAjMQAAAAMAAAPpAAAAAQAAIzEAAAADAAAD6QAAAAEAACMxAAAAAwAAA+kAAAABAAAfSAAAAAMAAAPpAAAAAQAAIzEAAAAEAAAD + 6QAAAAEAAAfSAAAAAQAAMtUAAAADAAAD6QAAAAEAACMxAAAAAwAAA+kAAAABAAAjMQAAAAMAAAPpAAAAAQAAIzEAAAADAAAD6QAA + AAEAACMxAAAAAwAAA+kAAAABAAAfSAAAAAMAAAPpAAAAAQAAIzEAAAAEAAAD6QAAAAEAAAu7AAAAGHN0c3MAAAAAAAAAAgAAAAEA + AAAfAAAAHHN0c2MAAAAAAAAAAQAAAAEAAAA8AAAAAQAAAQRzdHN6AAAAAAAAAAAAAAA8AAAH0AAAA/UAAADSAAAAdQAAACoAAAJG + AAAAWAAAADEAAAAlAAACywAAAFoAAAAqAAAAKQAAAkMAAAB+AAAAMgAAAB4AAAIwAAAAawAAAC8AAAAtAAACRgAAAH4AAAAxAAAA + OwAAAgcAAABhAAAAMAAAAD8AAAEHAAAI1wAAAboAAABBAAAAKQAAADAAAAHyAAAATQAAACkAAAApAAACIwAAAE8AAAA1AAAAKwAA + AiIAAABqAAAAJwAAADMAAAIxAAAAcgAAADUAAAAjAAACHgAAAG4AAAAkAAAAIAAAAjMAAAB3AAAAKwAAACEAAAEDAAAAFHN0Y28A + AAAAAAAAAQAABSwAAABhdWR0YQAAAFltZXRhAAAAAAAAACFoZGxyAAAAAAAAAABtZGlyYXBwbAAAAAAAAAAAAAAAACxpbHN0AAAA + JKl0b28AAAAcZGF0YQAAAAEAAAAATGF2ZjYzLjEuMTAyAAAACGZyZWUAAD34bWRhdAAAAp4GBf//mtxF6b3m2Ui3lizYINkj7u94 + MjY0IC0gY29yZSAxNjUgcjMyMjIgYjM1NjA1YSAtIEguMjY0L01QRUctNCBBVkMgY29kZWMgLSBDb3B5bGVmdCAyMDAzLTIwMjUg + LSBodHRwOi8vd3d3LnZpZGVvbGFuLm9yZy94MjY0Lmh0bWwgLSBvcHRpb25zOiBjYWJhYz0wIHJlZj0xIGRlYmxvY2s9MDowOjAg + YW5hbHlzZT0wOjAgbWU9ZGlhIHN1Ym1lPTAgcHN5PTEgcHN5X3JkPTEuMDA6MC4wMCBtaXhlZF9yZWY9MCBtZV9yYW5nZT0xNiBj + aHJvbWFfbWU9MSB0cmVsbGlzPTAgOHg4ZGN0PTAgY3FtPTAgZGVhZHpvbmU9MjEsMTEgZmFzdF9wc2tpcD0xIGNocm9tYV9xcF9v + ZmZzZXQ9MCB0aHJlYWRzPTIgbG9va2FoZWFkX3RocmVhZHM9MSBzbGljZWRfdGhyZWFkcz0wIG5yPTAgZGVjaW1hdGU9MSBpbnRl + cmxhY2VkPTAgYmx1cmF5X2NvbXBhdD0wIGNvbnN0cmFpbmVkX2ludHJhPTAgYmZyYW1lcz0zIGJfcHlyYW1pZD0yIGJfYWRhcHQ9 + MCBiX2JpYXM9MCBkaXJlY3Q9MSB3ZWlnaHRiPTAgb3Blbl9nb3A9MCB3ZWlnaHRwPTAga2V5aW50PTMwIGtleWludF9taW49MTYg + c2NlbmVjdXQ9MCBpbnRyYV9yZWZyZXNoPTAgcmM9Y3JmIG1idHJlZT0wIGNyZj0yMy4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1h + eD02OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIHBiX3JhdGlvPTEuMzAgYXE9MACAAAAFKmWIhADoMYACABA1nA2Z1ZY7Me2GI5j/ + /AB4L8W2Mtg139dcAHgACAAUoBUjnskA2V/roAHg/xLYy2jXf10GHEAAEAQAUiAACCeAAIGRgBATPIEhShCOmeYNSlCABoauwYrn + haLzigXEjMGK5wUg84gxgAWiEcAEXHQkGHgBQA/sACC8lb0jVem+vACztxpKlJAfAx/xZrn9ykBgauIAAEAAADAFSw//4QABEgAA + gBgACA0EAAIB4AAhWgAgApvggVDRMpJAPVdb8AB9b1ACg+o4P9fEAXAkTazwAW24iQpYeGMAHmkwwAqbq0gkyAKAACC4HAAEAEAD + gFyw4AAgAgAcAuX44QcyAbLLGjp9r/+EAAgACAcA0AAQuwgABAJAQAHfIAANACAiWAL6MAkEjFiApif78OmcsAW6GCcVGPCAW3P9 + +GPwAAQDgOSFDGLhc3IeFHYTDSXg4AHhsiowJeTgcrv3/4AHk7RZ8gLhhv7fAVBFEexBpiXivqKh0AACACQoBUDn8kQ0V/rrwgAB + AEAA9gBwQAAgngACFKoAAQCMfVD3G9ze+DAplxloeRT9CIv7gi4UD1jphwYEGQjkcI/FmLe4MdgAIAGDvAT8iRXGXjwj3IwIhCAA + dBlEcxJpqXCvKq8MMor2pNNGCHd+r7gGQFF+pnCinufVAXIAAEAYAAQBAAy4H/AIgIK9XOVFRG2/UQkAACAMAAIAgAZcIAAEAsAA + QBwA+4HAAEAYAAQBAAy5+np4WcABAABABAIeAWsRILhIHx/Q6yEfj//IALYAAgAChCSmMkTBoN77lAUEUEFfrjKqohbdoIfXhhFF + c1BpiXC/KKqGvB0OHwtgA4AQMeAhdZZcYbt3G7h69/8DgACBsAAIAwLuYBCDDsSOUAop+Ver3/EyzaDGl47LWyxTyoryipaqXlFS + 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 + qdsbK5c= + """ } diff --git a/docs/formats.md b/docs/formats.md index 8f1706b09..854cbab74 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -100,24 +100,28 @@ timestamps pass the slot policy. It assigns existing slots plus that lead rather frame rate; DTS, audio and the keyframe index remain unchanged. An ambiguous head stays untouched. Some writers go one step further: after dropping `ctts` they force the decode-order presentation -times to be monotonic, so each anchor keeps its own presentation time and each B picture decoded -after it is pushed 1001 ticks (at 90 kHz, a third of a 29.97 fps frame) past the picture before +times to be monotonic, so each B picture (and on some files each anchor too) decoded after another +picture is pushed 1001 ticks (at 90 kHz, a third of a 29.97 fps frame) past the picture before it. There is then no ladder to read a step from: the reporting file (H.264 High, 720p, GOP 90) stepped 3003 on 87% of its samples and 1001, 2252, 9009 or 12012 on the rest, so the probe skipped it as non-uniform and QuickTime, the Files app and both engine routes showed the B pictures out of order. When nothing above takes such a head, it is repaired with the step taken from the stream's average frame rate, provided the sampled span is the time its pictures take at that rate within the reorder delay plus two pictures, and every other check above still passes. The step is then -remeasured on the anchors, which the writer left on the presentation axis (the average rate is only -an estimate: a 60-picture 29.97 fps clip reports 29.80); a head whose anchors do not agree to -within a tick per picture is left untouched. DTS is rebuilt from each picture's decode slot rather than shifted, since a +remeasured on the anchors when the writer left them on the presentation axis (the average rate is +only an estimate: a 60-picture 29.97 fps clip reports 29.80). The reporting file pushed its P pictures +as well (each anchor 1001 ticks past the picture before it), so no two anchors agree; the step is +then the standard frame rate (23.976, 24, 25, 29.97, 30, 48, 50, 59.94 or 60) nearest the average +rate, if that is within 1% and a whole number of ticks. A head with neither is left untouched. DTS is rebuilt from each picture's decode slot rather than shifted, since a pushed B picture's DTS can sit after its own presentation time. After a seek onto an open-GOP keyframe, which kept its own presentation time, the sequence origin is taken from its display rank rather than its decode slot. The shift is what puts the first keyframe on the stream's start time, clamped to **16 pictures** because this writer left the keyframe four pictures ahead. Measured on a generated twin (`Scripts/make-clamped-ladder-fixture.sh`, 900 pictures at 29.97 fps), the AVPlayer route stepped backwards 169 times before and 0 after, and the -software route 44 times before and 0 after. +software route 44 times before and 0 after. Its `pushed-anchors.mp4`, which pushes the anchors as +well, was left untouched before ("anchors do not agree on a step", 44 backward steps on the software +route) and is repaired with 0 after. ### Matroska with presentation slots in coding order From dea36f46a169bfab42fc69c317b94e66b55614e3 Mon Sep 17 00:00:00 2001 From: Sunghyun Kim Date: Tue, 27 Oct 2026 07:24:25 +0900 Subject: [PATCH 4/7] fix(video): continue a forced-monotonic GOP past a pushed keyframe, sample past a leading P run A second reporting file (H.264 High, 23 fps, GOP 90, 53 min) opens every GOP on twelve P pictures, so the twelve-picture head never reordered and the probe left it untouched ("picture order never regresses"). A head with PTS == DTS throughout that has not reordered by then is now sampled on to its first regression plus a mini-GOP, up to 48 pictures. Both reporting files also push their keyframes, always later, 0 to 4 pictures past the end of the GOP before them. The repair anchored each GOP on its keyframe, which left a gap or put the next GOP in front of the last pictures of the previous one on 522 of 823 boundaries of the second file. A later IDR now continues the sequence before it, unless it is later than the reorder delay plus two pictures or within half a tick per picture; an earlier one keeps its own time, so a chain started on a pushed keyframe after a seek moves back into place. The standard rates now include every whole rate up to 240 and the NTSC ones, rounded to a tick (23 fps is 3913.04 ticks at 90 kHz). Measured: every presentation time of both files distinct and in order (one gap left at the end of the second), seeks settle within 0.42 pictures of the continuous run. New twins: a pushed keyframe (continues on the healthy axis) and a leading P run (sampled until it reorders); both fail without this change. The fixture script writes p-run-clamped.mp4 when the x264 CLI is available. --- CHANGELOG.md | 6 +- Scripts/make-clamped-ladder-fixture.sh | 30 + .../Video/H264CompositionOffsetRepair.swift | 85 +- .../DocumentedConstantsTests.swift | 5 + ...H264ForcedMonotonicLadderRepairTests.swift | 777 +++++++++++++++++- docs/formats.md | 20 +- 6 files changed, 896 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bac435fd1..44a6ba648 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -128,8 +128,10 @@ the public-API contract. showed the B pictures in decode order, as QuickTime and the Files app do. The repair now takes the step from the stream's average frame rate, remeasures it on the anchors (or, when the anchors were pushed as well, snaps it to the nearest standard frame rate within 1%) and rebuilds DTS from each - picture's decode slot, under the same picture-order checks. `Scripts/make-clamped-ladder-fixture.sh` - generates a twin pair. See `docs/formats.md`. + picture's decode slot, under the same picture-order checks. A keyframe the writer pushed late + continues the GOP before it instead of leaving a gap or overlapping it, and a head that opens on a + run of P pictures is sampled on to its first B picture (up to 48 pictures). + `Scripts/make-clamped-ladder-fixture.sh` generates the twins. See `docs/formats.md`. - **A refused HDR master no longer latches when the panel is shown to present HDR (AE#887).** A live PQ master refused with -11868 on an Apple TV whose output was fixed to 4K HDR latched the diff --git a/Scripts/make-clamped-ladder-fixture.sh b/Scripts/make-clamped-ladder-fixture.sh index 389b03e71..b9a9d3874 100755 --- a/Scripts/make-clamped-ladder-fixture.sh +++ b/Scripts/make-clamped-ladder-fixture.sh @@ -14,6 +14,13 @@ # bframes-healthy.mp4 H.264 High 1280x720 29.97 fps, B pyramid, GOP 90, 30 s + AAC (the twin) # clamped-ladder.mp4 the same stream, ctts dropped, ladder clamped as above # pushed-anchors.mp4 the same stream, ctts dropped, anchors pushed too +# p-run-healthy.mp4 (x264 CLI only) every GOP opens on twelve P pictures before its first B +# p-run-clamped.mp4 the same stream, ctts dropped, clamped as clamped-ladder.mp4 +# +# The clamp also pushes some keyframes (the 630th picture here, by 1001 ticks), as the reporting +# files do on most GOP boundaries; the repair continues the sequence before them instead. +# p-run-healthy.mp4 is written through Matroska, so its timestamps sit on whole milliseconds and the +# twins agree in display order rather than in value. # # testsrc2 draws a frame counter and a moving bar, so the order is visible by eye. # @@ -58,6 +65,29 @@ FF=(ffmpeg -hide_banner -loglevel error -y) "$OUT/pushed-anchors.mp4" FIXTURES=(clamped-ladder.mp4 pushed-anchors.mp4) + +# A writer that opens each GOP on a run of P pictures leaves the first twelve sampled pictures in +# order, so the probe has to sample on to the first B picture. ffmpeg's libx264 wrapper cannot force +# frame types; the x264 CLI does it with a qpfile. +if command -v x264 >/dev/null 2>&1; then + TMP="$(mktemp -d)" + { echo "0 I -1"; for i in $(seq 1 12); do echo "$i P -1"; echo "$((i + 90)) P -1"; echo "$((i + 180)) P -1"; done; echo "90 I -1"; echo "180 I -1"; } \ + | sort -n > "$TMP/qp.txt" + "${FF[@]}" -f lavfi -i "testsrc2=size=1280x720:rate=30000/1001:duration=10" -pix_fmt yuv420p \ + -f yuv4mpegpipe "$TMP/src.y4m" + x264 --quiet --preset veryfast --crf 23 --bframes 3 --b-pyramid normal --b-adapt 0 --keyint 90 \ + --min-keyint 90 --scenecut 0 --qpfile "$TMP/qp.txt" -o "$TMP/p-run.mkv" "$TMP/src.y4m" 2>/dev/null + "${FF[@]}" -i "$TMP/p-run.mkv" -c:v copy -video_track_timescale 90000 -movflags +faststart \ + "$OUT/p-run-healthy.mp4" + "${FF[@]}" -i "$OUT/p-run-healthy.mp4" -map 0 -c copy \ + -bsf:v 'setts=ts=max(PTS\,PREV_OUTDTS+1001)' -video_track_timescale 90000 -movflags +faststart \ + "$OUT/p-run-clamped.mp4" + rm -rf "$TMP" + echo "wrote $OUT/p-run-healthy.mp4" + FIXTURES+=(p-run-clamped.mp4) +else + echo "x264 not found: skipping p-run-*.mp4" +fi echo "wrote $OUT/bframes-healthy.mp4" for name in "${FIXTURES[@]}"; do echo "wrote $OUT/$name" diff --git a/Sources/AetherEngine/Video/H264CompositionOffsetRepair.swift b/Sources/AetherEngine/Video/H264CompositionOffsetRepair.swift index 9efa1d3fb..a92849ba9 100644 --- a/Sources/AetherEngine/Video/H264CompositionOffsetRepair.swift +++ b/Sources/AetherEngine/Video/H264CompositionOffsetRepair.swift @@ -174,6 +174,10 @@ enum H264CompositionOffsetRepair { /// shows an offset; only the broken shape pays the full window, which has to span at least one /// mini-GOP for the picture order to prove reordering at all. static let sampleTarget = 12 + /// How far the sample may grow when the first `sampleTarget` pictures never reorder. A writer can + /// open every GOP on a run of P pictures (measured: twelve, then the first B picture), and without + /// a regression the head proves nothing either way. + static let extendedSampleTarget = 48 /// Below this the ladder and the reorder evidence are too thin to trust; sampling stops early /// only on a healthy verdict or EOF. static let minimumSamples = 9 @@ -408,26 +412,27 @@ enum H264CompositionOffsetRepair { return Int64(ticks.rounded()) } - /// Frame rates a camera, encoder or broadcast chain actually produces. - static let standardFrameRates: [(num: Int64, den: Int64)] = [ - (24000, 1001), (24, 1), (25, 1), (30000, 1001), (30, 1), (48, 1), (50, 1), (60000, 1001), (60, 1), - ] - - /// The whole-tick picture length of the standard frame rate closest to `estimate`, within 1%. 29.97 - /// and 30 are both that close to either, so the nearer one wins and a tie names neither. nil when no - /// standard rate is that close, or when the nearest one is not a whole number of ticks in - /// `timeBase` (23.976 at 90 kHz): a neighbouring rate that happens to be whole would drift. + /// Frame rates a camera, encoder or broadcast chain actually produces: every whole rate up to 240, + /// and the NTSC ones (`n * 1000 / 1001`). + static let standardFrameRates: [(num: Int64, den: Int64)] = + (1...240).map { (Int64($0), 1) } + [24, 30, 48, 60, 120, 240].map { (Int64($0) * 1000, 1001) } + + /// The picture length, rounded to a tick, of the standard frame rate closest to `estimate`, within 1%. + /// 29.97 and 30 are both that close to either, so the nearer one wins and a tie names neither. A + /// rate that is not a whole number of ticks (23.976 or 23 at 90 kHz) is rounded: the synthesized + /// ladder re-anchors on every keyframe, so the half tick per picture never adds up across a GOP the + /// way an estimate's error does (29.80 against 29.97 is 17 ticks per picture). nil when no standard + /// rate is that close. static func standardFrameStep(near estimate: Int64, timeBase: AVRational) -> Int64? { guard estimate >= 2, timeBase.num > 0, timeBase.den > 0 else { return nil } - let ranked = standardFrameRates.map { rate -> (num: Int64, den: Int64, distance: Double) in - let numerator = Int64(timeBase.den) * rate.den - let denominator = Int64(timeBase.num) * rate.num - return (numerator, denominator, abs(Double(numerator) / Double(denominator) - Double(estimate))) + let ranked = standardFrameRates.map { rate -> (ticks: Double, distance: Double) in + let ticks = Double(timeBase.den) * Double(rate.den) / (Double(timeBase.num) * Double(rate.num)) + return (ticks, abs(ticks - Double(estimate))) }.sorted { $0.distance < $1.distance } guard let nearest = ranked.first, nearest.distance * 100 <= Double(estimate) else { return nil } if ranked.count > 1, ranked[1].distance == nearest.distance { return nil } - guard nearest.num % nearest.den == 0 else { return nil } - return nearest.num / nearest.den + guard nearest.ticks < Double(Int64.max / 4) else { return nil } + return Int64(nearest.ticks.rounded()) } /// Whether a non-uniform sampled ladder still spans the time its pictures would take at `step`. The @@ -582,6 +587,9 @@ enum H264CompositionOffsetRepair { /// for the seek, so an H.264 open-GOP landing reads its picture order relative to itself (a /// non-IDR I picture can come out at -4), and only differences within the sequence mean anything. private var anchoredOnLanding = false + /// The highest display index placed in the current sequence. A synthesized ladder's next IDR + /// is expected right after it. + private var sequenceLastDisplayIndex: Int64? /// Pictures emitted untouched because no anchor was available or the arithmetic did not /// close. Surfaced so a stream this repair does not describe is visible as a number. private(set) var unrepairedPictures = 0 @@ -596,6 +604,7 @@ enum H264CompositionOffsetRepair { landingLeadingPictures = 0 decodeOrdinal = 0 anchoredOnLanding = false + sequenceLastDisplayIndex = nil } mutating func noteLanding(leadingPictures: Int64) { @@ -618,11 +627,12 @@ enum H264CompositionOffsetRepair { // point can be placed at all; mid-sequence keyframes must not re-anchor, or every open // GOP would restart the display axis under a picture that has not moved. if isKeyframe, pictureOrderCount == 0 { - sequenceAnchorDTS = dts + sequenceAnchorDTS = awaitingReanchor ? dts : continuedSequenceAnchor(keyframeDTS: dts) sequenceBaseOrdinal = plan.presentationOrdinal(ladderTimestamp: dts) awaitingReanchor = false decodeOrdinal = 0 anchoredOnLanding = false + sequenceLastDisplayIndex = nil } else if awaitingReanchor, isKeyframe { guard pictureOrderCount % plan.pocStep == 0 else { unrepairedPictures += 1 @@ -652,6 +662,7 @@ enum H264CompositionOffsetRepair { awaitingReanchor = false decodeOrdinal = anchorSlots anchoredOnLanding = true + sequenceLastDisplayIndex = nil } guard let anchor = sequenceAnchorDTS, !awaitingReanchor else { unrepairedPictures += 1 @@ -665,6 +676,7 @@ enum H264CompositionOffsetRepair { return nil } let displayIndex = pictureOrderCount / plan.pocStep + sequenceLastDisplayIndex = max(sequenceLastDisplayIndex ?? displayIndex, displayIndex) // On a lattice the picture is placed by its ordinal, which reproduces the muxer exactly // even where the fraction rounds the other way. Without one, or when the sequence could // not be read back onto the lattice, the rounded step still describes the stream to @@ -688,6 +700,30 @@ enum H264CompositionOffsetRepair { repairedPictures += 1 return (pts, newDTS) } + + /// Where the IDR that follows a placed sequence starts. A forced-monotonic writer pushes the + /// keyframe too, and only ever later: on both reporting files the keyframes sat 0 to 4 pictures + /// after the end of the sequence before them, which left a gap or put the next GOP in front of + /// the last pictures of this one. A later keyframe therefore continues the sequence right after + /// its last picture. One that comes earlier than that is the better reading, because the chain + /// itself started on a pushed keyframe (a seek lands on whatever the writer left there) and + /// moves back each time a keyframe was pushed less. Within half a tick per picture of the + /// sequence the keyframe keeps its own time too: that is all a rounded step can be off by (23 fps + /// is 3913.04 ticks at 90 kHz), so the chain does not drift. One later than the reorder head + /// plus two pictures is a real discontinuity. + private func continuedSequenceAnchor(keyframeDTS dts: Int64) -> Int64 { + guard plan.synthesizesDecodeLadder, let previous = sequenceAnchorDTS, + let last = sequenceLastDisplayIndex, last >= 0 else { return dts } + let (pictures, countOverflow) = last.addingReportingOverflow(1) + let (length, lengthOverflow) = pictures.multipliedReportingOverflow(by: plan.step) + let (expected, expectedOverflow) = previous.addingReportingOverflow(length) + let (drift, driftOverflow) = dts.subtractingReportingOverflow(expected) + let (limit, limitOverflow) = plan.step.multipliedReportingOverflow(by: 2) + guard !countOverflow, !lengthOverflow, !expectedOverflow, !driftOverflow, !limitOverflow + else { return dts } + guard drift > pictures / 2 + 1, drift <= plan.decodeLead &+ limit else { return dts } + return expected + } } } @@ -875,7 +911,7 @@ final class H264CompositionOffsetRepairSession { } held.append((packet, pictureOrderCount)) heldBytes += Int(max(0, packet.pointee.size)) - if samples.count >= H264CompositionOffsetRepair.sampleTarget + if (samples.count >= H264CompositionOffsetRepair.sampleTarget && !awaitsReordering) || heldBytes >= H264CompositionOffsetRepair.sampleByteBudget || held.count >= H264CompositionOffsetRepair.heldPacketCeiling || earlyHealthyVerdict { @@ -937,6 +973,21 @@ final class H264CompositionOffsetRepairSession { landingPictureOrder = nil } + /// The sample is past `sampleTarget` without a picture-order regression, every picture so far reports + /// `PTS == DTS`, and it may still grow: keep sampling until the first regression plus the rest of + /// its mini-GOP (the reorder delay and two pictures, which is what the window checks need to see + /// it close). A head that already reordered inside `sampleTarget` decides there, as before. + private var awaitsReordering: Bool { + guard samples.count < H264CompositionOffsetRepair.extendedSampleTarget, + samples.allSatisfy({ $0.dts != Int64.min && $0.pts == $0.dts && $0.pictureOrderCount >= 0 }) + else { return false } + guard let regression = samples.indices.dropFirst().first(where: { + samples[$0].pictureOrderCount < samples[$0 - 1].pictureOrderCount + }) else { return true } + guard regression >= H264CompositionOffsetRepair.sampleTarget else { return false } + return samples.count - regression < videoDelay + 2 + } + /// A healthy file usually proves itself on its first packet, and paying twelve packets of hold /// for it would tax every well-formed MP4 with B-frames on the platform. private var earlyHealthyVerdict: Bool { diff --git a/Tests/AetherEngineTests/DocumentedConstantsTests.swift b/Tests/AetherEngineTests/DocumentedConstantsTests.swift index 6653af11e..7e99337cc 100644 --- a/Tests/AetherEngineTests/DocumentedConstantsTests.swift +++ b/Tests/AetherEngineTests/DocumentedConstantsTests.swift @@ -51,6 +51,11 @@ final class DocumentedConstantsTests: XCTestCase { assertDocumented("clamped to **16 pictures**", try documentation()) } + func testCompositionSampleExtensionMatchesDocumentation() throws { + XCTAssertEqual(H264CompositionOffsetRepair.extendedSampleTarget, 48) + assertDocumented("up to\n**48 pictures**", try documentation()) + } + func testPartialCompositionHoldBoundsMatchDocumentation() throws { let docs = try documentation() XCTAssertEqual(H264PartialCompositionRepair.maximumReorderDepth, 16) diff --git a/Tests/AetherEngineTests/H264ForcedMonotonicLadderRepairTests.swift b/Tests/AetherEngineTests/H264ForcedMonotonicLadderRepairTests.swift index b53d4c1d5..da930dd74 100644 --- a/Tests/AetherEngineTests/H264ForcedMonotonicLadderRepairTests.swift +++ b/Tests/AetherEngineTests/H264ForcedMonotonicLadderRepairTests.swift @@ -84,6 +84,30 @@ struct H264ForcedMonotonicLadderRepairTests { #expect(pushed == healthy) } + @Test("a keyframe the writer pushed late continues its sequence on the healthy axis") + func pushedKeyframeContinuesSequence() throws { + let healthy = try Self.videoTimestamps(base64: Self.healthyFixtureBase64) + let pushed = try Self.videoTimestamps(base64: Self.pushedKeyframeFixtureBase64) + #expect(healthy.count == 60) + #expect(pushed == healthy) + } + + @Test("a head that opens on a run of P pictures is sampled until it reorders") + func leadingPRunIsSampledUntilReordering() throws { + let healthy = try Self.videoTimestamps(base64: Self.leadingPRunHealthyFixtureBase64) + let clamped = try Self.videoTimestamps(base64: Self.leadingPRunClampedFixtureBase64) + #expect(healthy.count == 60) + #expect(clamped.count == healthy.count) + #expect(clamped.contains { $0.pts != $0.dts }) + func ranks(_ values: [Int64]) -> [Int] { + let sorted = values.sorted() + return values.map { sorted.firstIndex(of: $0) ?? -1 } + } + #expect(ranks(clamped.map(\.pts)) == ranks(healthy.map(\.pts))) + let presentation = clamped.map(\.pts).sorted() + #expect(zip(presentation, presentation.dropFirst()).allSatisfy { $1 - $0 == 3003 }) + } + @Test("classify takes the step from the anchors, not the rounded average rate") func classifyMeasuresAnchorStep() { // Decode order I0 P4 B2 b1 b3 P8 B6 b5 b7 P12 B10 b9: what the writer left on the ladder. @@ -155,10 +179,12 @@ struct H264ForcedMonotonicLadderRepairTests { // A 60-picture 29.97 clip reports 29.80. #expect(H264CompositionOffsetRepair.standardFrameStep(near: 3020, timeBase: ninetyKilohertz) == 3003) #expect(H264CompositionOffsetRepair.standardFrameStep(near: 3600, timeBase: ninetyKilohertz) == 3600) - // 27 fps names nothing. - #expect(H264CompositionOffsetRepair.standardFrameStep(near: 3333, timeBase: ninetyKilohertz) == nil) - // 23.976 is not a whole number of 90 kHz ticks, and 24 fps (3750) would drift. - #expect(H264CompositionOffsetRepair.standardFrameStep(near: 3754, timeBase: ninetyKilohertz) == nil) + // Whole rates count too, and a rate that is not a whole number of ticks is rounded: 23 fps is + // 3913.04 ticks (the second reporting file), 23.976 is 3753.75 rather than 24 fps's 3750. + #expect(H264CompositionOffsetRepair.standardFrameStep(near: 3913, timeBase: ninetyKilohertz) == 3913) + #expect(H264CompositionOffsetRepair.standardFrameStep(near: 3754, timeBase: ninetyKilohertz) == 3754) + // Half a frame per second is further than 1% from every standard rate. + #expect(H264CompositionOffsetRepair.standardFrameStep(near: 180_000, timeBase: ninetyKilohertz) == nil) #expect(H264CompositionOffsetRepair.standardFrameStep(near: 1001, timeBase: AVRational(num: 1, den: 24000)) == 1001) } @@ -1360,4 +1386,747 @@ struct H264ForcedMonotonicLadderRepairTests { RFIWUjzvVLrgO4VViFVmjPGxIABZgDoBIgPAALMB0AkQFgDFAGWAMUAY+JtLE1USEf9bL+Gr5bgMQQxBgAEFQdJ3NCiKpLsogibd qdsbK5c= """ + + /// The healthy 60 pictures pushed so that the second keyframe lands 2002 ticks late as well: + /// + /// ffmpeg -i healthy.mp4 -map 0:v:0 -c:v copy \ + /// -bsf:v 'setts=ts=max(PTS+1001*mod(N\,2)*lt(N\,40)+3003*eq(N\,29)\,PREV_OUTDTS+1001)' \ + /// -video_track_timescale 90000 -movflags +faststart pushed-keyframe.mp4 + static let pushedKeyframeFixtureBase64 = """ + AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAUEbW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAFfkAACw70AAQAAAQAA + AAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAA + BC90cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAACw70AAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAA + AAAAAAAAAAAAAABAAAAAAEAAAABAAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAsO9AAAAAAABAAAAAAOnbWRpYQAAACBtZGhk + AAAAAAAAAAAAAAAAAAFfkAACw71VxAAAAAAALWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAADUm1p + bmYAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAxJzdGJsAAAAtnN0c2QA + AAAAAAAAAQAAAKZhdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAEAAQABIAAAASAAAAAAAAAABFExhdmM2My4xLjEwMiBsaWJ4 + MjY0AAAAAAAAAAAAAAAAGP//AAAALGF2Y0MBTUAK/+EAFWdNQArsohNgIgAAB9IAAdTAHiRLLAEABGjOD8gAAAAQcGFzcAAAAAEA + AAABAAAAFGJ0cnQAAAAAAAD3gAAA9iIAAAEIc3R0cwAAAAAAAAAfAAAAAQAAMtUAAAADAAAD6QAAAAEAACMxAAAAAwAAA+kAAAAB + AAAjMQAAAAMAAAPpAAAAAQAAIzEAAAADAAAD6QAAAAEAACMxAAAAAwAAA+kAAAABAAAjMQAAAAMAAAPpAAAAAQAAIzEAAAADAAAD + 6QAAAAEAAAu7AAAAAQAAA+kAAAABAAArAwAAAAMAAAPpAAAAAQAAIzEAAAADAAAD6QAAAAEAACMxAAAAAwAAA+kAAAABAAAfSAAA + AAMAAAPpAAAAAQAAIzEAAAADAAAD6QAAAAEAACMxAAAAAwAAA+kAAAABAAAjMQAAAAQAAAPpAAAAAQAAC7sAAAAYc3RzcwAAAAAA + AAACAAAAAQAAAB8AAAAcc3RzYwAAAAAAAAABAAAAAQAAADwAAAABAAABBHN0c3oAAAAAAAAAAAAAADwAAAfQAAAD9QAAANIAAAB1 + AAAAKgAAAkYAAABYAAAAMQAAACUAAALLAAAAWgAAACoAAAApAAACQwAAAH4AAAAyAAAAHgAAAjAAAABrAAAALwAAAC0AAAJGAAAA + fgAAADEAAAA7AAACBwAAAGEAAAAwAAAAPwAAAQcAAAjXAAABugAAAEEAAAApAAAAMAAAAfIAAABNAAAAKQAAACkAAAIjAAAATwAA + ADUAAAArAAACIgAAAGoAAAAnAAAAMwAAAjEAAAByAAAANQAAACMAAAIeAAAAbgAAACQAAAAgAAACMwAAAHcAAAArAAAAIQAAAQMA + AAAUc3RjbwAAAAAAAAABAAAFNAAAAGF1ZHRhAAAAWW1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAG1kaXJhcHBsAAAAAAAAAAAAAAAA + LGlsc3QAAAAkqXRvbwAAABxkYXRhAAAAAQAAAABMYXZmNjMuMS4xMDIAAAAIZnJlZQAAPfhtZGF0AAACngYF//+a3EXpvebZSLeW + LNgg2SPu73gyNjQgLSBjb3JlIDE2NSByMzIyMiBiMzU2MDVhIC0gSC4yNjQvTVBFRy00IEFWQyBjb2RlYyAtIENvcHlsZWZ0IDIw + MDMtMjAyNSAtIGh0dHA6Ly93d3cudmlkZW9sYW4ub3JnL3gyNjQuaHRtbCAtIG9wdGlvbnM6IGNhYmFjPTAgcmVmPTEgZGVibG9j + az0wOjA6MCBhbmFseXNlPTA6MCBtZT1kaWEgc3VibWU9MCBwc3k9MSBwc3lfcmQ9MS4wMDowLjAwIG1peGVkX3JlZj0wIG1lX3Jh + bmdlPTE2IGNocm9tYV9tZT0xIHRyZWxsaXM9MCA4eDhkY3Q9MCBjcW09MCBkZWFkem9uZT0yMSwxMSBmYXN0X3Bza2lwPTEgY2hy + b21hX3FwX29mZnNldD0wIHRocmVhZHM9MiBsb29rYWhlYWRfdGhyZWFkcz0xIHNsaWNlZF90aHJlYWRzPTAgbnI9MCBkZWNpbWF0 + ZT0xIGludGVybGFjZWQ9MCBibHVyYXlfY29tcGF0PTAgY29uc3RyYWluZWRfaW50cmE9MCBiZnJhbWVzPTMgYl9weXJhbWlkPTIg + Yl9hZGFwdD0wIGJfYmlhcz0wIGRpcmVjdD0xIHdlaWdodGI9MCBvcGVuX2dvcD0wIHdlaWdodHA9MCBrZXlpbnQ9MzAga2V5aW50 + X21pbj0xNiBzY2VuZWN1dD0wIGludHJhX3JlZnJlc2g9MCByYz1jcmYgbWJ0cmVlPTAgY3JmPTIzLjAgcWNvbXA9MC42MCBxcG1p + bj0wIHFwbWF4PTY5IHFwc3RlcD00IGlwX3JhdGlvPTEuNDAgcGJfcmF0aW89MS4zMCBhcT0wAIAAAAUqZYiEAOgxgAIAEDWcDZnV + ljsx7YYjmP/8AHgvxbYy2DXf11wAeAAIABSgFSOeyQDZX+ugAeD/EtjLaNd/XQYcQAAQBABSIAAIJ4AAgZGAEBM8gSFKEI6Z5g1K + UIAGhq7BiueFovOKBcSMwYrnBSDziDGABaIRwARcdCQYeAFAD+wAILyVvSNV6b68ALO3GkqUkB8DH/Fmuf3KQGBq4gAAQAAAMAVL + D//hAAESAACAGAAIDQQAAgHgACFaACACm+CBUNEykkA9V1vwAH1vUAKD6jg/18QBcCRNrPABbbiJClh4YwAeaTDACpurSCTIAoAA + ILgcAAQAQAOAXLDgACACABwC5fjhBzIBsssaOn2v/4QACAAIBwDQABC7CAAEAkBAAd8gAA0AICJYAvowCQSMWICmJ/vw6ZywBboY + JxUY8IBbc/34Y/AABAOA5IUMYuFzch4UdhMNJeDgAeGyKjAl5OByu/f/gAeTtFnyAuGG/t8BUEUR7EGmJeK+oqHQAAIAJCgFQOfy + RDRX+uvCAAEAQAD2AHBAACCeAAIUqgABAIx9UPcb3N74MCmXGWh5FP0Ii/uCLhQPWOmHBgQZCORwj8WYt7gx2AAgAYO8BPyJFcZe + PCPcjAiEIAB0GURzEmmpcK8qrwwyivak00YId36vuAZAUX6mcKKe59UBcgAAQBgABAEADLgf8AiAgr1c5UVEbb9RCQAAIAwAAgCA + BlwgAAQCwABAHAD7gcAAQBgABAEADLn6enhZwAEAAEAEAh4BaxEguEgfH9DrIR+P/8gAtgACAAKEJKYyRMGg3vuUBQRQQV+uMqqi + Ft2gh9eGEUVzUGmJcL8oqoa8HQ4fC2ADgBAx4CF1llxhu3cbuHr3/wOAAIGwAAgDAu5gEIMOxI5QCin5V6vf8TLNoMaXjstbLFPK + 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 + KIqkuyiCJt2p2xsrlw== + """ + + /// 64x64 H.264 High, 29.97 fps, 60 frames, whose first GOP opens on twelve P pictures before its + /// first B picture (x264 frame types from a qpfile: `0 I -1`, then `1 P -1` to `12 P -1`). Written + /// through Matroska, so its timestamps sit on whole milliseconds rather than the 3003 lattice; the + /// test compares display order instead of values. + /// + /// ffmpeg -f lavfi -i 'testsrc=s=64x64:r=30000/1001:d=3' -frames:v 60 -pix_fmt yuv420p src.y4m + /// x264 --preset ultrafast --bframes 3 --b-pyramid normal --b-adapt 0 --keyint 30 \ + /// --min-keyint 30 --scenecut 0 --qpfile qp.txt -o p-run.mkv src.y4m + /// ffmpeg -i p-run.mkv -c:v copy -video_track_timescale 90000 -movflags +faststart healthy.mp4 + /// (clamped.mp4 as above) + static let leadingPRunHealthyFixtureBase64 = """ + AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAfUbW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAABOIAAAnGgAAQAAAQAA + AAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAA + Bv90cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAAAnGgAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAA + AAAAAAAAAAAAAABAAAAAAEAAAABAAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAJxnAAAXeAABAAAAAAZ3bWRpYQAAACBtZGhk + AAAAAAAAAAAAAAAAAAFfkAACv9UVxwAAAAAALWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAAGIm1p + bmYAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAABeJzdGJsAAAAtnN0c2QA + AAAAAAAAAQAAAKZhdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAEAAQABIAAAASAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAGP//AAAALGF2Y0MBTUAK/+EAFWdNQArsohNgIgAAB9IAAdTBHiRLLAEABGjOD8gAAAAQcGFzcAAAAAEA + AAABAAAAFGJ0cnQAAAAAAAEDUQABA1EAAAHwc3R0cwAAAAAAAAA8AAAAAQAAC7oAAAABAAALvgAAAAEAAAu6AAAAAQAAC7kAAAAB + AAALvgAAAAEAAAu6AAAAAQAAC7kAAAABAAALvgAAAAEAAAu6AAAAAQAAC7kAAAABAAALvgAAAAEAAAu6AAAAAQAAC7kAAAABAAAL + vgAAAAEAAAu6AAAAAQAAC7kAAAABAAALvgAAAAEAAAu6AAAAAQAAC7kAAAABAAALvgAAAAEAAAu6AAAAAQAAC7kAAAABAAALvgAA + AAEAAAu6AAAAAQAAC7kAAAABAAALvgAAAAEAAAu6AAAAAQAAC7kAAAABAAALvgAAAAEAAAu6AAAAAQAAC7kAAAABAAALvgAAAAEA + AAu6AAAAAQAAC7kAAAABAAALvgAAAAEAAAu6AAAAAQAAC7kAAAABAAALvgAAAAEAAAu6AAAAAQAAC7kAAAABAAALvgAAAAEAAAu6 + AAAAAQAAC7kAAAABAAALvgAAAAEAAAu6AAAAAQAAC7kAAAABAAALvgAAAAEAAAu6AAAAAQAAC7kAAAABAAALvgAAAAEAAAu6AAAA + AQAAC7kAAAABAAALvgAAAAEAAAu6AAAAAQAAC7kAAAABAAALvgAAAAEAAAu6AAAAAQAAC7kAAAABAAALvgAAAAEAAAu6AAAAGHN0 + c3MAAAAAAAAAAgAAAAEAAAAfAAAB6GN0dHMAAAAAAAAAOwAAAAIAABd4AAAAAQAAF3MAAAABAAAXdwAAAAEAABd4AAAAAQAAF3MA + AAABAAAXdwAAAAEAABd4AAAAAQAAF3MAAAABAAAXdwAAAAEAABd4AAAAAQAAF3MAAAABAAAXdwAAAAEAADqpAAAAAQAAF3MAAAAB + AAAAAAAAAAEAAAu+AAAAAQAAOqQAAAABAAAXdwAAAAEAAAAAAAAAAQAAC7oAAAABAAA6qAAAAAEAABd4AAAAAQAAAAAAAAABAAAL + uQAAAAEAADqpAAAAAQAAF3MAAAABAAAAAAAAAAEAAAu+AAAAAQAAF3MAAAABAAAXdwAAAAEAADqpAAAAAQAAF3MAAAABAAAAAAAA + AAEAAAu+AAAAAQAAOqQAAAABAAAXdwAAAAEAAAAAAAAAAQAAC7oAAAABAAA6qAAAAAEAABd4AAAAAQAAAAAAAAABAAALuQAAAAEA + ADqpAAAAAQAAF3MAAAABAAAAAAAAAAEAAAu+AAAAAQAAOqQAAAABAAAXdwAAAAEAAAAAAAAAAQAAC7oAAAABAAA6qAAAAAEAABd4 + AAAAAQAAAAAAAAABAAALuQAAAAEAADqpAAAAAQAAF3MAAAABAAAAAAAAAAEAAAu+AAAAAQAAF3MAAAAcc3RzYwAAAAAAAAABAAAA + AQAAADwAAAABAAABBHN0c3oAAAAAAAAAAAAAADwAAAfQAAADBQAAAPEAAAEmAAABDgAAARgAAAD+AAABKgAAARUAAAD6AAABBgAA + AQAAAAFSAAACMQAAAGoAAAAuAAAALAAAAjYAAABjAAAAMwAAACoAAAJBAAAAdgAAACYAAAA6AAACDQAAAF8AAAApAAAAMgAAARQA + AAkUAAABtwAAAEMAAAApAAAAMgAAAdoAAABPAAAAIwAAAC0AAAIpAAAAUAAAAC8AAAAvAAACKQAAAHIAAAAhAAAAOgAAAikAAABp + AAAANQAAACEAAAIXAAAAcAAAAC0AAAAjAAACMgAAAHQAAAAsAAAAJgAAAQEAAAAUc3RjbwAAAAAAAAABAAAIBAAAAGF1ZHRhAAAA + WW1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAG1kaXJhcHBsAAAAAAAAAAAAAAAALGlsc3QAAAAkqXRvbwAAABxkYXRhAAAAAQAAAABM + YXZmNjMuMS4xMDIAAAAIZnJlZQAAQO1tZGF0AAACngYF//+a3EXpvebZSLeWLNgg2SPu73gyNjQgLSBjb3JlIDE2NSByMzIyMiBi + MzU2MDVhIC0gSC4yNjQvTVBFRy00IEFWQyBjb2RlYyAtIENvcHlsZWZ0IDIwMDMtMjAyNSAtIGh0dHA6Ly93d3cudmlkZW9sYW4u + b3JnL3gyNjQuaHRtbCAtIG9wdGlvbnM6IGNhYmFjPTAgcmVmPTEgZGVibG9jaz0wOjA6MCBhbmFseXNlPTA6MCBtZT1kaWEgc3Vi + bWU9MCBwc3k9MSBwc3lfcmQ9MS4wMDowLjAwIG1peGVkX3JlZj0wIG1lX3JhbmdlPTE2IGNocm9tYV9tZT0xIHRyZWxsaXM9MCA4 + eDhkY3Q9MCBjcW09MCBkZWFkem9uZT0yMSwxMSBmYXN0X3Bza2lwPTEgY2hyb21hX3FwX29mZnNldD0wIHRocmVhZHM9MiBsb29r + YWhlYWRfdGhyZWFkcz0xIHNsaWNlZF90aHJlYWRzPTAgbnI9MCBkZWNpbWF0ZT0xIGludGVybGFjZWQ9MCBibHVyYXlfY29tcGF0 + PTAgY29uc3RyYWluZWRfaW50cmE9MCBiZnJhbWVzPTMgYl9weXJhbWlkPTIgYl9hZGFwdD0wIGJfYmlhcz0wIGRpcmVjdD0xIHdl + aWdodGI9MCBvcGVuX2dvcD0wIHdlaWdodHA9MCBrZXlpbnQ9MzAga2V5aW50X21pbj0xNiBzY2VuZWN1dD0wIGludHJhX3JlZnJl + c2g9MCByYz1jcmYgbWJ0cmVlPTAgY3JmPTIzLjAgcWNvbXA9MC42MCBxcG1pbj0wIHFwbWF4PTY5IHFwc3RlcD00IGlwX3JhdGlv + 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 + """ + + static let leadingPRunClampedFixtureBase64 = """ + AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAUsbW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAFfkAACw7sAAQAAAQAA + AAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAA + BFd0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAACw7sAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAA + AAAAAAAAAAAAAABAAAAAAEAAAABAAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAsO7AAAAAAABAAAAAAPPbWRpYQAAACBtZGhk + AAAAAAAAAAAAAAAAAAFfkAACw7sVxwAAAAAALWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAADem1p + bmYAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAzpzdGJsAAAAtnN0c2QA + AAAAAAAAAQAAAKZhdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAEAAQABIAAAASAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAGP//AAAALGF2Y0MBTUAK/+EAFWdNQArsohNgIgAAB9IAAdTBHiRLLAEABGjOD8gAAAAQcGFzcAAAAAEA + AAABAAAAFGJ0cnQAAAAAAAEDUQABAeMAAAEwc3R0cwAAAAAAAAAkAAAAAQAAC7oAAAABAAALuQAAAAEAAAu+AAAAAQAAC7oAAAAB + AAALuQAAAAEAAAu+AAAAAQAAC7oAAAABAAALuQAAAAEAAAu+AAAAAQAAC7oAAAABAAALuQAAAAEAAAu+AAAAAQAALusAAAADAAAD + 6QAAAAEAACMvAAAAAwAAA+kAAAABAAAjNAAAAAMAAAPpAAAAAQAAIzAAAAAEAAAD6QAAAAEAAAfTAAAAAQAALusAAAADAAAD6QAA + AAEAACMvAAAAAwAAA+kAAAABAAAjNAAAAAMAAAPpAAAAAQAAIzAAAAADAAAD6QAAAAEAACMvAAAAAwAAA+kAAAABAAAjNAAAAAMA + AAPpAAAAAQAAIzAAAAAEAAAD6QAAAAEAAAu6AAAAGHN0c3MAAAAAAAAAAgAAAAEAAAAfAAAAHHN0c2MAAAAAAAAAAQAAAAEAAAA8 + AAAAAQAAAQRzdHN6AAAAAAAAAAAAAAA8AAAH0AAAAwUAAADxAAABJgAAAQ4AAAEYAAAA/gAAASoAAAEVAAAA+gAAAQYAAAEAAAAB + UgAAAjEAAABqAAAALgAAACwAAAI2AAAAYwAAADMAAAAqAAACQQAAAHYAAAAmAAAAOgAAAg0AAABfAAAAKQAAADIAAAEUAAAJFAAA + AbcAAABDAAAAKQAAADIAAAHaAAAATwAAACMAAAAtAAACKQAAAFAAAAAvAAAALwAAAikAAAByAAAAIQAAADoAAAIpAAAAaQAAADUA + AAAhAAACFwAAAHAAAAAtAAAAIwAAAjIAAAB0AAAALAAAACYAAAEBAAAAFHN0Y28AAAAAAAAAAQAABVwAAABhdWR0YQAAAFltZXRh + AAAAAAAAACFoZGxyAAAAAAAAAABtZGlyYXBwbAAAAAAAAAAAAAAAACxpbHN0AAAAJKl0b28AAAAcZGF0YQAAAAEAAAAATGF2ZjYz + LjEuMTAyAAAACGZyZWUAAEDtbWRhdAAAAp4GBf//mtxF6b3m2Ui3lizYINkj7u94MjY0IC0gY29yZSAxNjUgcjMyMjIgYjM1NjA1 + YSAtIEguMjY0L01QRUctNCBBVkMgY29kZWMgLSBDb3B5bGVmdCAyMDAzLTIwMjUgLSBodHRwOi8vd3d3LnZpZGVvbGFuLm9yZy94 + MjY0Lmh0bWwgLSBvcHRpb25zOiBjYWJhYz0wIHJlZj0xIGRlYmxvY2s9MDowOjAgYW5hbHlzZT0wOjAgbWU9ZGlhIHN1Ym1lPTAg + cHN5PTEgcHN5X3JkPTEuMDA6MC4wMCBtaXhlZF9yZWY9MCBtZV9yYW5nZT0xNiBjaHJvbWFfbWU9MSB0cmVsbGlzPTAgOHg4ZGN0 + PTAgY3FtPTAgZGVhZHpvbmU9MjEsMTEgZmFzdF9wc2tpcD0xIGNocm9tYV9xcF9vZmZzZXQ9MCB0aHJlYWRzPTIgbG9va2FoZWFk + X3RocmVhZHM9MSBzbGljZWRfdGhyZWFkcz0wIG5yPTAgZGVjaW1hdGU9MSBpbnRlcmxhY2VkPTAgYmx1cmF5X2NvbXBhdD0wIGNv + bnN0cmFpbmVkX2ludHJhPTAgYmZyYW1lcz0zIGJfcHlyYW1pZD0yIGJfYWRhcHQ9MCBiX2JpYXM9MCBkaXJlY3Q9MSB3ZWlnaHRi + PTAgb3Blbl9nb3A9MCB3ZWlnaHRwPTAga2V5aW50PTMwIGtleWludF9taW49MTYgc2NlbmVjdXQ9MCBpbnRyYV9yZWZyZXNoPTAg + cmM9Y3JmIG1idHJlZT0wIGNyZj0yMy4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQw + IHBiX3JhdGlvPTEuMzAgYXE9MACAAAAFKmWIhADoMYACABA1nA2Z1ZY7Me2GI5j//AB4L8W2Mtg139dcAHgACAAUoBUjnskA2V/r + oAHg/xLYy2jXf10GHEAAEAQAUiAACCeAAIGRgBATPIEhShCOmeYNSlCABoauwYrnhaLzigXEjMGK5wUg84gxgAWiEcAEXHQkGHgB + QA/sACC8lb0jVem+vACztxpKlJAfAx/xZrn9ykBgauIAAEAAADAFSw//4QABEgAAgBgACA0EAAIB4AAhWgAgApvggVDRMpJAPVdb + 8AB9b1ACg+o4P9fEAXAkTazwAW24iQpYeGMAHmkwwAqbq0gkyAKAACC4HAAEAEADgFyw4AAgAgAcAuX44QcyAbLLGjp9r/+EAAgA + CAcA0AAQuwgABAJAQAHfIAANACAiWAL6MAkEjFiApif78OmcsAW6GCcVGPCAW3P9+GPwAAQDgOSFDGLhc3IeFHYTDSXg4AHhsiow + JeTgcrv3/4AHk7RZ8gLhhv7fAVBFEexBpiXivqKh0AACACQoBUDn8kQ0V/rrwgABAEAA9gBwQAAgngACFKoAAQCMfVD3G9ze+DAp + lxloeRT9CIv7gi4UD1jphwYEGQjkcI/FmLe4MdgAIAGDvAT8iRXGXjwj3IwIhCAAdBlEcxJpqXCvKq8MMor2pNNGCHd+r7gGQFF+ + pnCinufVAXIAAEAYAAQBAAy4H/AIgIK9XOVFRG2/UQkAACAMAAIAgAZcIAAEAsAAQBwA+4HAAEAYAAQBAAy5+np4WcABAABABAIe + AWsRILhIHx/Q6yEfj//IALYAAgAChCSmMkTBoN77lAUEUEFfrjKqohbdoIfXhhFFc1BpiXC/KKqGvB0OHwtgA4AQMeAhdZZcYbt3 + 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 + """ } diff --git a/docs/formats.md b/docs/formats.md index 854cbab74..b1414a240 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -111,9 +111,19 @@ reorder delay plus two pictures, and every other check above still passes. The s remeasured on the anchors when the writer left them on the presentation axis (the average rate is only an estimate: a 60-picture 29.97 fps clip reports 29.80). The reporting file pushed its P pictures as well (each anchor 1001 ticks past the picture before it), so no two anchors agree; the step is -then the standard frame rate (23.976, 24, 25, 29.97, 30, 48, 50, 59.94 or 60) nearest the average -rate, if that is within 1% and a whole number of ticks. A head with neither is left untouched. DTS is rebuilt from each picture's decode slot rather than shifted, since a -pushed B picture's DTS can sit after its own presentation time. After a seek onto an open-GOP +then the standard frame rate (any whole rate up to 240, or an NTSC one such as 23.976 or 29.97) +nearest the average rate, if that is within 1%, rounded to a tick (a second reporting file runs at +23 fps, 3913.04 ticks). A head with neither is left untouched. DTS is rebuilt from each picture's +decode slot rather than shifted, since a pushed B picture's DTS can sit after its own presentation +time. The writer pushes keyframes as well, and only ever later: on both reporting files they sat 0 to +4 pictures after the end of the GOP before them, which left a gap or put the next GOP in front of the +last pictures of the previous one. A later IDR therefore continues the sequence before it, unless it +is later than the reorder delay plus two pictures (a real discontinuity) or within half a tick per +picture (all a rounded step can be off by); an earlier one keeps its own time, which is how a chain +started on a pushed keyframe after a seek moves back into place. The second reporting file also opens +every GOP on twelve P pictures before its first B, so a head that has not reordered after twelve +pictures, with `PTS == DTS` throughout, is sampled on to its first regression plus a mini-GOP, up to +**48 pictures**. After a seek onto an open-GOP keyframe, which kept its own presentation time, the sequence origin is taken from its display rank rather than its decode slot. The shift is what puts the first keyframe on the stream's start time, clamped to **16 pictures** because this writer left the keyframe @@ -121,7 +131,9 @@ four pictures ahead. Measured on a generated twin (`Scripts/make-clamped-ladder- pictures at 29.97 fps), the AVPlayer route stepped backwards 169 times before and 0 after, and the software route 44 times before and 0 after. Its `pushed-anchors.mp4`, which pushes the anchors as well, was left untouched before ("anchors do not agree on a step", 44 backward steps on the software -route) and is repaired with 0 after. +route) and is repaired with 0 after; both now demux to the healthy twin's timestamps, including +the GOP whose keyframe the clamp pushed. Its `p-run-clamped.mp4` (x264 CLI only) opens every GOP on +twelve P pictures: 99 of 239 backward steps before, 0 after. ### Matroska with presentation slots in coding order From 0bed68cd164c4e4d06f03cd76a80e76081b0e064 Mon Sep 17 00:00:00 2001 From: Sunghyun Kim Date: Tue, 27 Oct 2026 07:37:36 +0900 Subject: [PATCH 5/7] fix(video): give the forced-monotonic bounds room for a rounded step and a four-picture push A third reporting file (H.264 High 720p, 29 fps, GOP 90, 19 min) was left untouched: its head runs exactly the reorder delay plus two pictures ahead, and with 29 fps rounded from 3103.45 to 3103 ticks the span missed the nominal-rate bound by 3 ticks. The span check now allows a tick per picture each way. The same file keeps its keyframes four pictures (plus up to 31 ticks) past the GOP before them, which sat just past the continuation bound and left a five-picture gap at 3.1 s and 100 s. The bound is now the reorder delay plus two and a half pictures. Measured: every presentation time of the file distinct and on the 3103 lattice (the last picture aside), seeks settle within 0.34 pictures; the other two reporting files and the fixture twins are unchanged. --- .../Video/H264CompositionOffsetRepair.swift | 11 ++++++---- ...H264ForcedMonotonicLadderRepairTests.swift | 21 +++++++++++++++++++ docs/formats.md | 6 ++++-- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/Sources/AetherEngine/Video/H264CompositionOffsetRepair.swift b/Sources/AetherEngine/Video/H264CompositionOffsetRepair.swift index a92849ba9..711914fc8 100644 --- a/Sources/AetherEngine/Video/H264CompositionOffsetRepair.swift +++ b/Sources/AetherEngine/Video/H264CompositionOffsetRepair.swift @@ -439,7 +439,8 @@ enum H264CompositionOffsetRepair { /// writer's forced-monotonic ladder runs ahead of that by at most the reorder head plus the anchor /// it pushed past (measured 3.5 pictures on the reporting file at delay 2), and falls behind only /// by what the next anchor has not yet reclaimed; a genuinely variable-rate head misses it by far - /// more. + /// more. `step` is rounded, so each bound also gives a tick per picture: a third reporting file at + /// 29 fps (3103.45 ticks) ran exactly that four pictures ahead and missed the upper bound by 3. static func nominalLadderFits(samples: [Sample], step: Int64, videoDelay: Int) -> Bool { guard step >= 2, let first = samples.first?.dts, let last = samples.last?.dts, last > first else { return false @@ -450,7 +451,7 @@ enum H264CompositionOffsetRepair { let (low, lowOverflow) = max(0, pictures - slack).multipliedReportingOverflow(by: step) let (high, highOverflow) = (pictures + slack).multipliedReportingOverflow(by: step) guard !overflow, !lowOverflow, !highOverflow else { return false } - return span >= low && span <= high + return span >= low - pictures && span <= high + pictures } /// Ticks per picture read off the sampled anchors, the pictures that display later than everything @@ -710,7 +711,9 @@ enum H264CompositionOffsetRepair { /// moves back each time a keyframe was pushed less. Within half a tick per picture of the /// sequence the keyframe keeps its own time too: that is all a rounded step can be off by (23 fps /// is 3913.04 ticks at 90 kHz), so the chain does not drift. One later than the reorder head - /// plus two pictures is a real discontinuity. + /// plus two and a half pictures is a real discontinuity: a third reporting file at 29 fps kept + /// its keyframes four pictures (plus up to 31 ticks) past the sequence before them for a + /// hundred seconds. private func continuedSequenceAnchor(keyframeDTS dts: Int64) -> Int64 { guard plan.synthesizesDecodeLadder, let previous = sequenceAnchorDTS, let last = sequenceLastDisplayIndex, last >= 0 else { return dts } @@ -721,7 +724,7 @@ enum H264CompositionOffsetRepair { let (limit, limitOverflow) = plan.step.multipliedReportingOverflow(by: 2) guard !countOverflow, !lengthOverflow, !expectedOverflow, !driftOverflow, !limitOverflow else { return dts } - guard drift > pictures / 2 + 1, drift <= plan.decodeLead &+ limit else { return dts } + guard drift > pictures / 2 + 1, drift <= plan.decodeLead &+ limit &+ plan.step / 2 else { return dts } return expected } } diff --git a/Tests/AetherEngineTests/H264ForcedMonotonicLadderRepairTests.swift b/Tests/AetherEngineTests/H264ForcedMonotonicLadderRepairTests.swift index da930dd74..d94ee34b0 100644 --- a/Tests/AetherEngineTests/H264ForcedMonotonicLadderRepairTests.swift +++ b/Tests/AetherEngineTests/H264ForcedMonotonicLadderRepairTests.swift @@ -170,6 +170,27 @@ struct H264ForcedMonotonicLadderRepairTests { #expect(plan.shift == 12012) } + @Test("a head four pictures ahead of a rounded 29 fps step still fits the nominal rate") + func roundedStepHeadFitsNominalRate() { + // The third reporting file's head: 29 fps is 3103.45 ticks, the step rounds to 3103, and the + // ladder runs exactly the reorder head plus two pictures ahead, 3 ticks past the bound without + // a tick per picture for the rounding. + let pocs: [Int64] = [0, 8, 4, 2, 6, 16, 12, 10, 14, 22, 18, 20] + let ladder: [Int64] = [0, 6207, 7758, 9309, 24826, 27153, 29480, 31807, 34134, 38272, 42410, 46548] + let samples = zip(pocs, ladder).enumerated().map { index, pair in + H264CompositionOffsetRepair.Sample(dts: pair.1, pts: pair.1, pictureOrderCount: pair.0, isKeyframe: index == 0) + } + #expect(H264CompositionOffsetRepair.nominalLadderFits(samples: samples, step: 3103, videoDelay: 2)) + guard case .repair(let plan) = H264CompositionOffsetRepair.classify( + samples: samples, videoDelay: 2, streamStartTime: 0, ladderStart: 0, + nominalStep: 3103, standardStep: 3103) + else { + Issue.record("expected a repair") + return + } + #expect(plan.step == 3103) + } + @Test("the standard step is the nearest standard rate within 1%") func standardFrameStepSnapsToNearestRate() { let ninetyKilohertz = AVRational(num: 1, den: 90000) diff --git a/docs/formats.md b/docs/formats.md index b1414a240..487e00f62 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -118,12 +118,14 @@ decode slot rather than shifted, since a pushed B picture's DTS can sit after it time. The writer pushes keyframes as well, and only ever later: on both reporting files they sat 0 to 4 pictures after the end of the GOP before them, which left a gap or put the next GOP in front of the last pictures of the previous one. A later IDR therefore continues the sequence before it, unless it -is later than the reorder delay plus two pictures (a real discontinuity) or within half a tick per +is later than the reorder delay plus two and a half pictures (a real discontinuity; a third reporting +file at 29 fps kept its keyframes four pictures late for a hundred seconds) or within half a tick per picture (all a rounded step can be off by); an earlier one keeps its own time, which is how a chain started on a pushed keyframe after a seek moves back into place. The second reporting file also opens every GOP on twelve P pictures before its first B, so a head that has not reordered after twelve pictures, with `PTS == DTS` throughout, is sampled on to its first regression plus a mini-GOP, up to -**48 pictures**. After a seek onto an open-GOP +**48 pictures**. The sampled span is checked against the rounded step with a tick per picture to +spare, which is what the third file's head needed. After a seek onto an open-GOP keyframe, which kept its own presentation time, the sequence origin is taken from its display rank rather than its decode slot. The shift is what puts the first keyframe on the stream's start time, clamped to **16 pictures** because this writer left the keyframe From 8bbb7cd9df8ba2518b8b54f63004185bbb5fdeaa Mon Sep 17 00:00:00 2001 From: Sunghyun Kim Date: Tue, 27 Oct 2026 08:20:40 +0900 Subject: [PATCH 6/7] fix(demuxer): follow the writer's clock on a synthesized #409 ladder and cut HLS on moved keyframes The nominal-rate ladder now takes its rate from the bitstream's VUI timing when it agrees with the stream within 1% (snapped to a standard rate within 0.01%), so a short 24 fps clip is no longer read as 23.976, and places pictures on that rate as an exact fraction of a tick. A continued sequence also leans toward the earliest origin the pictures before it point to, by at most an eighth of a picture per GOP: the 29 fps reporting file's writer runs at 29.003 fps, and counting alone drifted 61 ms behind it over 19 minutes and overlapped three GOPs. A seek landing holds its first 8 pictures and takes the earliest origin they point to, never further back than a continued sequence could. A keyframe placed back onto its sequence sits up to the reorder delay plus two and a half pictures before its index entry, and the HLS segment plan is built from the index. The VOD cutter and the restart gate now reach that far for the keyframe each boundary names, bounded per boundary by the keyframe before it. Before, segverify found 25 of 40 segments of the 23 fps file and 22 of 40 of the 29.97 fps file starting mid-GOP; all are independent now, including restarted ones. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 8 +- Sources/AetherEngine/Demuxer/Demuxer.swift | 9 + .../Video/H264CompositionOffsetRepair.swift | 256 ++++++++++++++---- .../Video/H264MatroskaSlotPermutation.swift | 3 + .../Video/HLSSegmentProducer.swift | 17 +- .../HLSVideoEngine+SegmentPlanning.swift | 21 ++ .../AetherEngine/Video/HLSVideoEngine.swift | 8 + .../AetherEngine/Video/VODSegmentCutter.swift | 18 +- .../DocumentedConstantsTests.swift | 5 + ...H264ForcedMonotonicLadderRepairTests.swift | 99 ++++++- .../RepairedKeyframeSegmentReachTests.swift | 69 +++++ docs/formats.md | 27 +- 12 files changed, 466 insertions(+), 74 deletions(-) create mode 100644 Tests/AetherEngineTests/RepairedKeyframeSegmentReachTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 44a6ba648..41a702fd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -130,7 +130,13 @@ the public-API contract. pushed as well, snaps it to the nearest standard frame rate within 1%) and rebuilds DTS from each picture's decode slot, under the same picture-order checks. A keyframe the writer pushed late continues the GOP before it instead of leaving a gap or overlapping it, and a head that opens on a - run of P pictures is sampled on to its first B picture (up to 48 pictures). + run of P pictures is sampled on to its first B picture (up to 48 pictures). The rate is the one the + bitstream's VUI states when it agrees with the stream (24 is no longer read as 23.976 off a short + clip), applied as an exact fraction of a tick, and a continued sequence leans toward where the + writer's own pictures say it starts, so a writer clock slightly off its stated rate no longer drifts + the picture 61 ms and then overlaps a GOP. Over HLS, a keyframe placed back onto its sequence sits + before the index entry the segment plan names, and the cutter and the restart gate now reach for + it: before, most segments of such a file opened mid-GOP and could not be decoded on their own. `Scripts/make-clamped-ladder-fixture.sh` generates the twins. See `docs/formats.md`. - **A refused HDR master no longer latches when the panel is shown to present HDR (AE#887).** diff --git a/Sources/AetherEngine/Demuxer/Demuxer.swift b/Sources/AetherEngine/Demuxer/Demuxer.swift index 1529f6749..58615b678 100644 --- a/Sources/AetherEngine/Demuxer/Demuxer.swift +++ b/Sources/AetherEngine/Demuxer/Demuxer.swift @@ -2184,6 +2184,15 @@ public final class Demuxer: @unchecked Sendable { return result } + /// Ticks (stream time base) by which a delivered keyframe may precede its `indexedKeyframeEntries` + /// timestamp: a #409 repair that synthesizes the decode ladder moves a late-written IDR back onto + /// its sequence. 0 when packets sit exactly on the index. + var indexedKeyframeAdvanceTicks: Int64 { + accessLock.lock() + defer { accessLock.unlock() } + return compositionRepair?.keyframeAdvanceTicks ?? 0 + } + func readPacket(isCurrent: @Sendable () -> Bool = { true }) throws -> UnsafeMutablePointer? { accessLock.lock() defer { accessLock.unlock() } diff --git a/Sources/AetherEngine/Video/H264CompositionOffsetRepair.swift b/Sources/AetherEngine/Video/H264CompositionOffsetRepair.swift index 711914fc8..4f3a8ab1a 100644 --- a/Sources/AetherEngine/Video/H264CompositionOffsetRepair.swift +++ b/Sources/AetherEngine/Video/H264CompositionOffsetRepair.swift @@ -134,6 +134,34 @@ enum H264CompositionOffsetRepair { /// well, because the container's DTS of a B picture can sit after the presentation time it /// belongs to. var synthesizesDecodeLadder = false + /// The exact picture length of a synthesized ladder, when the rate is known as a fraction (23 fps + /// is 90000/23 ticks at 90 kHz). Offsets inside a sequence and the length of a sequence are read + /// off it, so a rounded `step` never adds up across a long file. + var pictureCadence: Cadence? + /// The reorder delay in pictures, which `decodeLead` is the length of. Only a synthesized + /// ladder reads it; 0 on every other plan. + var decodeLeadPictures: Int64 = 0 + + /// The most a synthesized ladder places a keyframe before its written decode time: the reorder + /// delay plus two and a half pictures, which is how late the writers this ladder describes + /// were measured to push an IDR (four pictures on the 29 fps reporting asset). Continuing a + /// sequence and landing on a seek both stay inside it, so the container index, read on the + /// written axis, names every keyframe to within this many ticks. 0 for every other plan, + /// which keeps each keyframe exactly where the index says. + var keyframeAdvanceLimit: Int64 { + guard synthesizesDecodeLadder else { return 0 } + let (pictures, pictureOverflow) = step.multipliedReportingOverflow(by: 2) + let (limit, limitOverflow) = decodeLead.addingReportingOverflow(pictures) + let (total, totalOverflow) = limit.addingReportingOverflow(step / 2) + return pictureOverflow || limitOverflow || totalOverflow ? 0 : total + } + + /// Ticks from a sequence's anchor to its picture `ordinal`, on `pictureCadence` when there is one. + func pictureOffset(_ ordinal: Int64) -> Int64? { + if let pictureCadence { return pictureCadence.timestamp(at: ordinal) } + let (offset, overflow) = ordinal.multipliedReportingOverflow(by: step) + return overflow ? nil : offset + } /// The timestamp of a presentation ordinal, nil unless this plan carries a lattice. func presentationTimestamp(ordinal: Int64) -> Int64? { @@ -178,6 +206,11 @@ enum H264CompositionOffsetRepair { /// open every GOP on a run of P pictures (measured: twelve, then the first B picture), and without /// a regression the head proves nothing either way. static let extendedSampleTarget = 48 + /// Pictures a seek onto a synthesized ladder holds before it places the landing. The writer only + /// ever pushes a picture later, so the earliest of `written time - offset of its display slot` over + /// the first pictures is a better start than the keyframe alone: measured on the three reporting + /// files, 8 pictures cut the spread of the landing from 0.5 to 4.3 pictures to -0.9 to 1.3. + static let landingEnvelopePictures = 8 /// Below this the ladder and the reorder evidence are too thin to trust; sampling stops early /// only on a healthy verdict or EOF. static let minimumSamples = 9 @@ -217,7 +250,7 @@ enum H264CompositionOffsetRepair { streamStartTime: Int64, ladderStart: Int64, nominalStep: Int64? = nil, - standardStep: Int64? = nil + rateCadence: Cadence? = nil ) -> Verdict { guard videoDelay > 0, videoDelay <= 16 else { return .inconclusive("reorder delay \(videoDelay) outside 1...16") @@ -325,13 +358,16 @@ enum H264CompositionOffsetRepair { // The average frame rate is only an estimate (short files, rounded durations: a 60-picture // 29.97 clip reports 29.80). When the writer left its anchors on the presentation axis they give // the exact step. The reporting file pushed its anchors as well (a P picture 1001 ticks after the - // P before it), so they agree on nothing; the step is then the standard rate the estimate is - // within 1% of, and a head with neither is left alone. + // P before it), so they agree on nothing; the step is then the stream's picture rate + // (`rateCadence`), and a head with neither is left alone. + var pictureCadence: Cadence? if synthesizesDecodeLadder { if let measured = anchorStep(samples: samples, pocStep: pocStep) { step = measured - } else if let standardStep { - step = standardStep + pictureCadence = Cadence(numerator: measured, denominator: 1) + } else if let rateCadence, let rounded = rateCadence.timestamp(at: 1), rounded >= 2 { + step = rounded + pictureCadence = rateCadence } else { return .inconclusive("anchors do not agree on a step") } @@ -346,6 +382,11 @@ enum H264CompositionOffsetRepair { return .inconclusive("cadence does not describe a reorder head") } decodeLead = -head + } else if let pictureCadence { + guard let lead = pictureCadence.timestamp(at: Int64(videoDelay)) else { + return .inconclusive("reorder head does not fit the timeline") + } + decodeLead = lead } else { let (lead, overflow) = Int64(videoDelay).multipliedReportingOverflow(by: step) guard !overflow else { return .inconclusive("reorder head does not fit the timeline") } @@ -398,7 +439,9 @@ enum H264CompositionOffsetRepair { latticeOrigin: latticeOrigin, ladderPhase: ladderPhase, ladderOrdinalOffset: ladderOrdinalOffset, - synthesizesDecodeLadder: synthesizesDecodeLadder + synthesizesDecodeLadder: synthesizesDecodeLadder, + pictureCadence: pictureCadence, + decodeLeadPictures: synthesizesDecodeLadder ? Int64(videoDelay) : 0 ) ) } @@ -417,22 +460,50 @@ enum H264CompositionOffsetRepair { static let standardFrameRates: [(num: Int64, den: Int64)] = (1...240).map { (Int64($0), 1) } + [24, 30, 48, 60, 120, 240].map { (Int64($0) * 1000, 1001) } - /// The picture length, rounded to a tick, of the standard frame rate closest to `estimate`, within 1%. - /// 29.97 and 30 are both that close to either, so the nearer one wins and a tie names neither. A - /// rate that is not a whole number of ticks (23.976 or 23 at 90 kHz) is rounded: the synthesized - /// ladder re-anchors on every keyframe, so the half tick per picture never adds up across a GOP the - /// way an estimate's error does (29.80 against 29.97 is 17 ticks per picture). nil when no standard - /// rate is that close. - static func standardFrameStep(near estimate: Int64, timeBase: AVRational) -> Int64? { + /// The picture length, as an exact fraction of a tick, of the standard frame rate closest to + /// `estimate`, within 1%. 29.97 and 30 are both that close to either, so the nearer one wins and a + /// tie names neither. nil when no standard rate is that close. + static func standardFrameCadence(near estimate: Int64, timeBase: AVRational) -> Cadence? { guard estimate >= 2, timeBase.num > 0, timeBase.den > 0 else { return nil } - let ranked = standardFrameRates.map { rate -> (ticks: Double, distance: Double) in + let ranked = standardFrameRates.map { rate -> (rate: (num: Int64, den: Int64), distance: Double) in let ticks = Double(timeBase.den) * Double(rate.den) / (Double(timeBase.num) * Double(rate.num)) - return (ticks, abs(ticks - Double(estimate))) + return (rate, abs(ticks - Double(estimate))) }.sorted { $0.distance < $1.distance } guard let nearest = ranked.first, nearest.distance * 100 <= Double(estimate) else { return nil } if ranked.count > 1, ranked[1].distance == nearest.distance { return nil } - guard nearest.ticks < Double(Int64.max / 4) else { return nil } - return Int64(nearest.ticks.rounded()) + return pictureCadence(rate: nearest.rate, timeBase: timeBase) + } + + /// The picture rate a synthesized ladder is rebuilt at. The bitstream states it (the H.264 or HEVC + /// VUI timing the parser reads into the codec context): on all three reporting files it named the + /// exact rate (23, 29.97, 29.0002 fps), where the average rate of a short file with a pushed tail + /// cannot tell 24 from 23.976. It is trusted only within 1% of the average rate, and snapped to the + /// nearest standard rate within 0.01% (29.97 and 30 are 0.1% apart and must stay apart). Without + /// it, the standard rate nearest the average rate. + static func rateCadence(bitstreamRate: AVRational?, estimate: Int64, timeBase: AVRational) -> Cadence? { + guard timeBase.num > 0, timeBase.den > 0 else { return nil } + if let rate = bitstreamRate, rate.num > 0, rate.den > 0, + let stated = pictureCadence(rate: (Int64(rate.num), Int64(rate.den)), timeBase: timeBase) { + let ticks = Double(stated.numerator) / Double(stated.denominator) + if abs(ticks - Double(estimate)) * 100 <= Double(estimate) { + let distance = { (cadence: Cadence) in abs(Double(cadence.numerator) / Double(cadence.denominator) - ticks) } + let standard = standardFrameRates.compactMap { pictureCadence(rate: $0, timeBase: timeBase) } + .filter { distance($0) * 10_000 <= ticks } + .min { distance($0) < distance($1) } + return standard ?? stated + } + } + return standardFrameCadence(near: estimate, timeBase: timeBase) + } + + /// Ticks per picture at `rate` in `timeBase`, as a reduced fraction. nil when it overflows or is + /// shorter than two ticks, which the lattice arithmetic refuses. + static func pictureCadence(rate: (num: Int64, den: Int64), timeBase: AVRational) -> Cadence? { + let (numerator, overflow) = Int64(timeBase.den).multipliedReportingOverflow(by: rate.den) + let (denominator, denominatorOverflow) = Int64(timeBase.num).multipliedReportingOverflow(by: rate.num) + guard !overflow, !denominatorOverflow, let cadence = Cadence(numerator: numerator, denominator: denominator), + cadence.wholeTicks >= 2 else { return nil } + return cadence } /// Whether a non-uniform sampled ladder still spans the time its pictures would take at `step`. The @@ -591,6 +662,12 @@ enum H264CompositionOffsetRepair { /// The highest display index placed in the current sequence. A synthesized ladder's next IDR /// is expected right after it. private var sequenceLastDisplayIndex: Int64? + /// The earliest sequence origin the pictures held after a seek point to (`noteLandingEnvelope`). + private var landingEnvelopeAnchor: Int64? + /// The earliest origin, `written DTS - pictureOffset(display index)`, any picture of the + /// current sequence pointed to. The writer only ever pushes a picture later, so this is the + /// best reading of where the sequence really started (`continuedSequenceAnchor`). + private var sequenceEnvelopeOrigin: Int64? /// Pictures emitted untouched because no anchor was available or the arithmetic did not /// close. Surfaced so a stream this repair does not describe is visible as a number. private(set) var unrepairedPictures = 0 @@ -606,6 +683,15 @@ enum H264CompositionOffsetRepair { decodeOrdinal = 0 anchoredOnLanding = false sequenceLastDisplayIndex = nil + landingEnvelopeAnchor = nil + sequenceEnvelopeOrigin = nil + } + + /// The earliest origin the held landing pictures imply, `written DTS - pictureOffset(display + /// index)`, which the landing takes when it is earlier than its keyframe's own. Synthesized + /// ladders only. + mutating func noteLandingEnvelope(anchor: Int64?) { + landingEnvelopeAnchor = plan.synthesizesDecodeLadder ? anchor : nil } mutating func noteLanding(leadingPictures: Int64) { @@ -634,6 +720,7 @@ enum H264CompositionOffsetRepair { decodeOrdinal = 0 anchoredOnLanding = false sequenceLastDisplayIndex = nil + sequenceEnvelopeOrigin = nil } else if awaitingReanchor, isKeyframe { guard pictureOrderCount % plan.pocStep == 0 else { unrepairedPictures += 1 @@ -647,14 +734,22 @@ enum H264CompositionOffsetRepair { // pushed the B pictures), so its origin is `landingIndex` slots back, not its decode slot. let originSlots = plan.synthesizesDecodeLadder ? landingIndex : anchorSlots // Audit BIT-101: a 64-bit `tfdt` step and a landing order near -2^31 leave Int64. - let (landingTicks, productOverflow) = slotsOverflow - ? (0, true) : originSlots.multipliedReportingOverflow(by: plan.step) + let (landingTicks, productOverflow): (Int64, Bool) = slotsOverflow + ? (0, true) + : plan.synthesizesDecodeLadder + ? plan.pictureOffset(originSlots).map { ($0, false) } ?? (0, true) + : originSlots.multipliedReportingOverflow(by: plan.step) let (anchor, anchorOverflow) = dts.subtractingReportingOverflow(landingTicks) guard !productOverflow, !anchorOverflow else { unrepairedPictures += 1 return nil } - sequenceAnchorDTS = anchor + // Never further back than a continued sequence could have put it, so the segment plan + // built from the index still finds this keyframe (`keyframeAdvanceLimit`). + sequenceAnchorDTS = landingEnvelopeAnchor.map { + max(min($0, anchor), anchor &- plan.keyframeAdvanceLimit) + } ?? anchor + landingEnvelopeAnchor = nil sequenceBaseOrdinal = plan.presentationOrdinal(ladderTimestamp: dts) .flatMap { ordinal -> Int64? in let (base, overflow) = ordinal.subtractingReportingOverflow(originSlots) @@ -663,6 +758,7 @@ enum H264CompositionOffsetRepair { awaitingReanchor = false decodeOrdinal = anchorSlots anchoredOnLanding = true + sequenceEnvelopeOrigin = nil sequenceLastDisplayIndex = nil } guard let anchor = sequenceAnchorDTS, !awaitingReanchor else { @@ -678,6 +774,11 @@ enum H264CompositionOffsetRepair { } let displayIndex = pictureOrderCount / plan.pocStep sequenceLastDisplayIndex = max(sequenceLastDisplayIndex ?? displayIndex, displayIndex) + if plan.synthesizesDecodeLadder, !anchoredOnLanding, displayIndex >= 0, + let offset = plan.pictureOffset(displayIndex) { + let (origin, overflow) = dts.subtractingReportingOverflow(offset) + if !overflow { sequenceEnvelopeOrigin = min(sequenceEnvelopeOrigin ?? origin, origin) } + } // On a lattice the picture is placed by its ordinal, which reproduces the muxer exactly // even where the fraction rounds the other way. Without one, or when the sequence could // not be read back onto the lattice, the rounded step still describes the stream to @@ -687,9 +788,13 @@ enum H264CompositionOffsetRepair { let (ordinal, overflow) = base.addingReportingOverflow(displayIndex) return overflow ? nil : plan.presentationTimestamp(ordinal: ordinal) } + ?? plan.pictureOffset(displayIndex).map { anchor &+ plan.shift &+ $0 } ?? (anchor &+ plan.shift &+ displayIndex &* plan.step) + // A synthesized DTS sits `decodeLeadPictures` pictures behind its decode slot on the same + // lattice as the presentation times, so rounding cannot put it a tick past its own picture. let newDTS = plan.synthesizesDecodeLadder - ? anchor &+ plan.shift &+ slot &* plan.step &- plan.decodeLead + ? plan.pictureOffset(slot &- plan.decodeLeadPictures).map { anchor &+ plan.shift &+ $0 } + ?? (anchor &+ plan.shift &+ slot &* plan.step &- plan.decodeLead) : dts &+ plan.shift &- plan.decodeLead // The muxer invariant. A picture that lands before its own decode time means the // arithmetic no longer describes this stream, and passing it through unchanged is @@ -706,26 +811,34 @@ enum H264CompositionOffsetRepair { /// keyframe too, and only ever later: on both reporting files the keyframes sat 0 to 4 pictures /// after the end of the sequence before them, which left a gap or put the next GOP in front of /// the last pictures of this one. A later keyframe therefore continues the sequence right after - /// its last picture. One that comes earlier than that is the better reading, because the chain - /// itself started on a pushed keyframe (a seek lands on whatever the writer left there) and - /// moves back each time a keyframe was pushed less. Within half a tick per picture of the - /// sequence the keyframe keeps its own time too: that is all a rounded step can be off by (23 fps - /// is 3913.04 ticks at 90 kHz), so the chain does not drift. One later than the reorder head - /// plus two and a half pictures is a real discontinuity: a third reporting file at 29 fps kept - /// its keyframes four pictures (plus up to 31 ticks) past the sequence before them for a - /// hundred seconds. + /// its last picture, measured on `pictureCadence`. One that comes earlier than that is the better + /// reading, because the chain itself started on a pushed keyframe (a seek lands on whatever the + /// writer left there) and moves back each time a keyframe was pushed less. Within half a tick per + /// picture of the sequence the keyframe keeps its own time too: the writer's clock does not run + /// at exactly the stated rate (the third reporting file states 29.0002 fps and writes 29.003), + /// and following an on-time keyframe is what keeps the chain on it; a chain that only counted + /// drifted 57 ms behind over 19 minutes and then overlapped where an earlier keyframe pulled it + /// back. One later than the reorder head plus two and a half pictures is a real + /// discontinuity: the same file kept its keyframes four pictures (plus up to 31 ticks) past the + /// sequence before them for a hundred seconds. private func continuedSequenceAnchor(keyframeDTS dts: Int64) -> Int64 { guard plan.synthesizesDecodeLadder, let previous = sequenceAnchorDTS, let last = sequenceLastDisplayIndex, last >= 0 else { return dts } let (pictures, countOverflow) = last.addingReportingOverflow(1) - let (length, lengthOverflow) = pictures.multipliedReportingOverflow(by: plan.step) + guard !countOverflow, let length = plan.pictureOffset(pictures) else { return dts } let (expected, expectedOverflow) = previous.addingReportingOverflow(length) let (drift, driftOverflow) = dts.subtractingReportingOverflow(expected) - let (limit, limitOverflow) = plan.step.multipliedReportingOverflow(by: 2) - guard !countOverflow, !lengthOverflow, !expectedOverflow, !driftOverflow, !limitOverflow - else { return dts } - guard drift > pictures / 2 + 1, drift <= plan.decodeLead &+ limit &+ plan.step / 2 else { return dts } - return expected + guard !expectedOverflow, !driftOverflow else { return dts } + guard drift > pictures / 2 + 1, drift <= plan.keyframeAdvanceLimit else { return dts } + // Counting alone drifts with the writer's clock, so the continued sequence also leans + // toward where the pictures it follows say it ends, by at most an eighth of a picture per + // sequence: too little to see as a gap, enough to follow a clock 0.01% off the stated rate. + guard let origin = sequenceEnvelopeOrigin else { return expected } + let (envelope, envelopeOverflow) = origin.addingReportingOverflow(length) + guard !envelopeOverflow else { return expected } + let slew = plan.step / 8 + let leaned = expected &+ min(max(envelope &- expected, -slew), slew) + return min(dts, max(dts &- plan.keyframeAdvanceLimit, leaned)) } } } @@ -739,6 +852,11 @@ final class H264PictureOrderReader { private var context: UnsafeMutablePointer? private let codecID: AVCodecID var isFramePicture: Bool { parser?.pointee.picture_structure == AV_PICTURE_STRUCTURE_FRAME } + /// The picture rate the bitstream's VUI timing states, once the parser has read a parameter set. + var frameRate: AVRational? { + guard let rate = context?.pointee.framerate, rate.num > 0, rate.den > 0 else { return nil } + return rate + } init?(codecParameters: UnsafePointer, timeBase: AVRational) { codecID = codecParameters.pointee.codec_id @@ -810,8 +928,7 @@ final class H264CompositionOffsetRepairSession { /// Ticks per picture at the stream's average frame rate, for a head whose decode ladder has no step /// of its own. private let nominalStep: Int64? - /// `nominalStep` snapped to the standard rate it estimates, for a head whose anchors give no step. - private let standardStep: Int64? + private let timeBase: AVRational private var reader: H264PictureOrderReader? private var rewriter: H264CompositionOffsetRepair.Rewriter? private let partialRepairCandidate: H264PartialCompositionRepairSession? @@ -825,6 +942,9 @@ final class H264CompositionOffsetRepairSession { /// order while the packets behind it are held to count its leading pictures. private var relanding = false private var landingPictureOrder: Int64? + /// Index in `held` of the first picture displayed after the landing; the leading pictures are the + /// ones before it. + private var landingTrailingIndex: Int? /// nil unless this stream is the exact shape the defect needs: ISO-BMFF, H.264 or HEVC, and a /// bitstream that declares reordered pictures. Everything else never sees a parser or a held @@ -850,9 +970,7 @@ final class H264CompositionOffsetRepairSession { self.ladderStart = ladderStart self.nominalStep = H264CompositionOffsetRepair.nominalFrameStep( frameRate: stream.pointee.avg_frame_rate, timeBase: stream.pointee.time_base) - self.standardStep = nominalStep.flatMap { - H264CompositionOffsetRepair.standardFrameStep(near: $0, timeBase: stream.pointee.time_base) - } + self.timeBase = stream.pointee.time_base self.reader = reader let (lead, overflow) = stream.pointee.start_time.subtractingReportingOverflow(ladderStart) // The partial-region repair reads H.264 NAL types to find its IDRs, so HEVC gets the @@ -887,6 +1005,15 @@ final class H264CompositionOffsetRepairSession { return rewriter.plan.shift - rewriter.plan.decodeLead } + /// How far before its index entry (read through `decodeTimestampOffset`) a repaired keyframe can + /// be placed. 0 when every keyframe stays on the index. Measured on the 23 fps reporting asset: a + /// cutter that wanted the keyframe at or past its entry missed 25 of 40 segments, which then + /// opened on audio mid-GOP. + var keyframeAdvanceTicks: Int64 { + guard phase == .repairing, let rewriter else { return 0 } + return rewriter.plan.keyframeAdvanceLimit + } + /// Returns true when the packet was taken over by the session and must not be emitted yet. /// A packet the session keeps is owned by it until `dequeue()` hands it back. func ingest(_ packet: UnsafeMutablePointer) -> Bool { @@ -942,17 +1069,21 @@ final class H264CompositionOffsetRepairSession { return false } landingPictureOrder = pictureOrderCount - } else if let landing = landingPictureOrder, let pictureOrderCount, pictureOrderCount > landing { - held.append((packet, pictureOrderCount)) - finishLanding() - return true + } else if landingTrailingIndex == nil, let landing = landingPictureOrder, let pictureOrderCount, + pictureOrderCount > landing { + landingTrailingIndex = held.count } } else if landingPictureOrder == nil { return false } held.append((packet, pictureOrderCount)) heldBytes += Int(max(0, packet.pointee.size)) - if heldBytes >= H264CompositionOffsetRepair.sampleByteBudget + // A synthesized ladder holds a few pictures more to read the landing's envelope off them. + let wantsEnvelope = rewriter?.plan.synthesizesDecodeLadder == true + && held.lazy.filter { $0.packet.pointee.stream_index == self.streamIndex }.count + < H264CompositionOffsetRepair.landingEnvelopePictures + if (landingTrailingIndex != nil && !wantsEnvelope) + || heldBytes >= H264CompositionOffsetRepair.sampleByteBudget || held.count >= H264CompositionOffsetRepair.heldPacketCeiling { finishLanding() } @@ -963,12 +1094,14 @@ final class H264CompositionOffsetRepairSession { relanding = false guard var rewriter else { return } if let landing = landingPictureOrder { - let leading = held.filter { + let leading = held.prefix(landingTrailingIndex ?? held.count).filter { $0.packet.pointee.stream_index == streamIndex && ($0.pictureOrderCount.map { $0 < landing } ?? false) }.count rewriter.noteLanding(leadingPictures: Int64(leading)) + rewriter.noteLandingEnvelope(anchor: landingEnvelope(plan: rewriter.plan)) } + landingTrailingIndex = nil for entry in held where entry.packet.pointee.stream_index == streamIndex { applyRepair(to: entry.packet, pictureOrderCount: entry.pictureOrderCount, using: &rewriter) } @@ -976,6 +1109,28 @@ final class H264CompositionOffsetRepairSession { landingPictureOrder = nil } + /// The earliest origin the held landing pictures imply: each was written no earlier than its own + /// time, so `DTS - pictureOffset(display index)` over the landing's sequence bounds where the + /// sequence really starts. Stops at the next IDR, whose picture order starts over. + private func landingEnvelope(plan: H264CompositionOffsetRepair.Plan) -> Int64? { + guard plan.synthesizesDecodeLadder, plan.pocStep > 0 else { return nil } + var earliest: Int64? + var first = true + for entry in held where entry.packet.pointee.stream_index == streamIndex { + let isKeyframe = (entry.packet.pointee.flags & AV_PKT_FLAG_KEY) != 0 + guard let pictureOrderCount = entry.pictureOrderCount else { continue } + if !first, isKeyframe, pictureOrderCount == 0 { break } + first = false + let dts = entry.packet.pointee.dts + guard dts != Int64.min, pictureOrderCount % plan.pocStep == 0, + let offset = plan.pictureOffset(pictureOrderCount / plan.pocStep) else { continue } + let (origin, overflow) = dts.subtractingReportingOverflow(offset) + guard !overflow else { continue } + earliest = min(earliest ?? origin, origin) + } + return earliest + } + /// The sample is past `sampleTarget` without a picture-order regression, every picture so far reports /// `PTS == DTS`, and it may still grow: keep sampling until the first regression plus the rest of /// its mini-GOP (the reorder delay and two pictures, which is what the window checks need to see @@ -1024,6 +1179,7 @@ final class H264CompositionOffsetRepairSession { reader?.reset() relanding = true landingPictureOrder = nil + landingTrailingIndex = nil case .off: break } @@ -1068,13 +1224,17 @@ final class H264CompositionOffsetRepairSession { /// `nominalStep` is nil on the first pass, so the missing-head partial repair below keeps first /// claim on a non-uniform ladder; only when it declines does the nominal-rate rebuild get a turn. private func decide(using nominalStep: Int64?) { + let rate = nominalStep.flatMap { + H264CompositionOffsetRepair.rateCadence(bitstreamRate: reader?.frameRate, estimate: $0, timeBase: timeBase) + } let verdict = H264CompositionOffsetRepair.classify( samples: samples, videoDelay: videoDelay, streamStartTime: streamStartTime, ladderStart: ladderStart, - nominalStep: nominalStep, - standardStep: nominalStep == nil ? nil : standardStep + // The span check reads the rate too, and the stated one is the better reading of it. + nominalStep: rate?.timestamp(at: 1) ?? nominalStep, + rateCadence: rate ) switch verdict { case .repair(let plan): diff --git a/Sources/AetherEngine/Video/H264MatroskaSlotPermutation.swift b/Sources/AetherEngine/Video/H264MatroskaSlotPermutation.swift index e8252c856..27d6f0a48 100644 --- a/Sources/AetherEngine/Video/H264MatroskaSlotPermutation.swift +++ b/Sources/AetherEngine/Video/H264MatroskaSlotPermutation.swift @@ -12,6 +12,8 @@ protocol H264TimestampRepairSession: AnyObject { var isDecided: Bool { get } /// Ticks the container's own index has to move by to describe the same axis as the packets. var decodeTimestampOffset: Int64? { get } + /// Ticks a delivered keyframe may sit before its index entry once `decodeTimestampOffset` is applied. + var keyframeAdvanceTicks: Int64 { get } /// True when the packet was taken over and must not be emitted yet. func ingest(_ packet: UnsafeMutablePointer) -> Bool func dequeue() -> UnsafeMutablePointer? @@ -452,6 +454,7 @@ final class H264MatroskaSlotPermutationSession: H264TimestampRepairSession { /// keyframe is the first picture of its own sequence, and a permutation leaves that slot exactly /// where the container wrote it. The index and the packets already describe one axis. var decodeTimestampOffset: Int64? { nil } + var keyframeAdvanceTicks: Int64 { 0 } func ingest(_ packet: UnsafeMutablePointer) -> Bool { guard phase != .off else { return false } diff --git a/Sources/AetherEngine/Video/HLSSegmentProducer.swift b/Sources/AetherEngine/Video/HLSSegmentProducer.swift index f5d155d9a..a00a98528 100644 --- a/Sources/AetherEngine/Video/HLSSegmentProducer.swift +++ b/Sources/AetherEngine/Video/HLSSegmentProducer.swift @@ -540,6 +540,12 @@ final class HLSSegmentProducer: @unchecked Sendable { /// AE#561: the axis `segmentBoundaries` are stamped on, so the cutter gate compares like with /// like. Decode for a mov/mp4 index, presentation for Matroska Cues; see `PlanBoundaryAxis`. private let planBoundaryAxis: PlanBoundaryAxis + /// `VODSegmentCutter.reachTicks` of the restart boundary, which the gate honours too: the + /// keyframe that boundary names can arrive that far before it, and a gate that skipped it opened + /// a GOP late (or re-aimed). Only for the boundary itself, never for a re-aim. + private var restartGateReachTicks: Int64 { + effectiveGateTargetPts == restartTargetVideoPts ? vodCutter.reach(at: 0) : 0 + } /// AE#408: the gate target actually in force. Starts at `restartTargetVideoPts` and moves BACK when /// the boundary turns out not to be openable, so the segment covers its own advertised start @@ -1008,10 +1014,11 @@ final class HLSSegmentProducer: @unchecked Sendable { /// presentation time is the most permissive reading and cannot starve on a skew in either /// direction. What the axis buys here is knowing how much of the resulting overshoot is real, /// which is what `boundaryOpenToleranceTicks` spends it on. - static func videoGateTargetSatisfied(pts: Int64, dts: Int64, targetPts: Int64) -> Bool { + static func videoGateTargetSatisfied(pts: Int64, dts: Int64, targetPts: Int64, + reachTicks: Int64 = 0) -> Bool { if targetPts == Int64.min { return true } let ts = pts != Int64.min ? pts : dts - return ts != Int64.min && ts >= targetPts + return ts != Int64.min && SourceTimestampBounds.sum(ts, max(0, reachTicks)) >= targetPts } // MARK: - AE#408: a boundary that is not a random-access point @@ -1514,6 +1521,7 @@ final class HLSSegmentProducer: @unchecked Sendable { restartTargetVideoPts: Int64 = Int64.min, boundaryClaimsRandomAccess: Bool = false, planBoundaryAxis: PlanBoundaryAxis = .decode, + keyframeReachTicks: [Int64] = [], closedCaptionStreamIndex: Int32 = -1, subtitleTapStreamIndices: Set = [], subtitlePacketStreamIndices: Set = [], @@ -1589,7 +1597,8 @@ final class HLSSegmentProducer: @unchecked Sendable { self.vodCutter = VODSegmentCutter( sourceBoundaries: segmentBoundaries, planAnchorPts: planAnchorVideoPts, - baseIndex: baseIndex + baseIndex: baseIndex, + reachTicks: keyframeReachTicks ) self.isLive = isLive self.foldsSequentialTimeline = foldsSequentialTimeline @@ -4038,7 +4047,7 @@ final class HLSSegmentProducer: @unchecked Sendable { let isKey = (packet.pointee.flags & AV_PKT_FLAG_KEY) != 0 let targetSatisfied = Self.videoGateTargetSatisfied( pts: packet.pointee.pts, dts: packet.pointee.dts, - targetPts: effectiveGateTargetPts) + targetPts: effectiveGateTargetPts, reachTicks: isKey && !isLive ? restartGateReachTicks : 0) // #133: on a live H.264 Annex-B mid-stream join, opening on a bare keyframe flag is not // enough. A join packet must carry a decodable access unit (in-band SPS+PPS and an // IDR or immediate intra recovery point, AE#627); otherwise the decoder renders references diff --git a/Sources/AetherEngine/Video/HLSVideoEngine+SegmentPlanning.swift b/Sources/AetherEngine/Video/HLSVideoEngine+SegmentPlanning.swift index 2d86daced..d58baa2ff 100644 --- a/Sources/AetherEngine/Video/HLSVideoEngine+SegmentPlanning.swift +++ b/Sources/AetherEngine/Video/HLSVideoEngine+SegmentPlanning.swift @@ -69,6 +69,27 @@ extension HLSVideoEngine { /// producer can never finish and AVPlayer waits on forever. The 60 s default leaves room for a /// long final GOP and for a container duration padded by a trailing audio or subtitle track. /// + /// The cutter's and the restart gate's reach for each boundary of a keyframe-aligned plan: + /// `advance` (how far a repaired keyframe can precede its index entry), held below the distance + /// to the keyframe before that boundary so that one never reaches it. Per boundary, not one + /// figure for the plan: the 23 fps reporting asset keeps its keyframes 15 s apart but has one + /// pair four pictures apart, and a plan-wide bound left every other boundary without the reach + /// it needed. Empty when nothing moves. + static func keyframeReachTicks(advance: Int64, keyframes: [Int64], boundaries: [Int64]) -> [Int64] { + guard advance > 0 else { return [] } + let sorted = keyframes.sorted() + return boundaries.map { boundary in + var low = 0, high = sorted.count + while low < high { + let mid = (low + high) / 2 + if sorted[mid] < boundary { low = mid + 1 } else { high = mid } + } + guard low > 0 else { return advance } + let (gap, overflow) = boundary.subtractingReportingOverflow(sorted[low - 1]) + return overflow ? 0 : max(0, min(advance, gap - 1)) + } + } + /// An index failing any witness is routed to the uniform-stride fallback. static func keyframeIndexIsTrustworthy( keyframes: [Int64], diff --git a/Sources/AetherEngine/Video/HLSVideoEngine.swift b/Sources/AetherEngine/Video/HLSVideoEngine.swift index 70227e17e..aa3af4bdb 100644 --- a/Sources/AetherEngine/Video/HLSVideoEngine.swift +++ b/Sources/AetherEngine/Video/HLSVideoEngine.swift @@ -861,6 +861,10 @@ public final class HLSVideoEngine: @unchecked Sendable { /// which is what the cutter compared before the axis existed. var planBoundaryAxis: PlanBoundaryAxis = .decode + /// How far before its boundary the keyframe each `segmentPlan` entry names may arrive + /// (`VODSegmentCutter.reachTicks`). Empty unless a #409 repair moves keyframes off the index. + var planKeyframeReachTicks: [Int64] = [] + /// Guards subsystem refs + `sessionEpoch`. Never held across waits or network I/O so /// `stop()` on the main thread is never blocked behind a restart's 5 s waitForFinish. let restartLock = NSLock() @@ -1559,6 +1563,9 @@ public final class HLSVideoEngine: @unchecked Sendable { // stamping. Read from the demuxer that produced them, never from the URL or the // host's metadata: on a remux session the delivered container is the one indexed. planBoundaryAxis = PlanBoundaryAxis.forContainer(formatName: dem.containerFormatName) + planKeyframeReachTicks = Self.keyframeReachTicks( + advance: dem.indexedKeyframeAdvanceTicks, keyframes: keyframes, + boundaries: plan.map(\.startPts)) let firstKeyframePts = keyframes.sorted().first ?? 0 self.firstKeyframePts = firstKeyframePts let firstKeyframeSeconds = Double(firstKeyframePts) * Double(videoTimeBase.num) / Double(videoTimeBase.den) @@ -3482,6 +3489,7 @@ public final class HLSVideoEngine: @unchecked Sendable { restartTargetVideoPts: videoTarget, boundaryClaimsRandomAccess: planBoundariesClaimRandomAccess, planBoundaryAxis: planBoundaryAxis, + keyframeReachTicks: Array(planKeyframeReachTicks.dropFirst(min(baseIndex, planKeyframeReachTicks.count))), closedCaptionStreamIndex: closedCaptionStreamIndexForSession, subtitleTapStreamIndices: Set(nativeSubtitleSourceStreamIndicesForSession.compactMap { $0 }), subtitlePacketStreamIndices: allEmbeddedSubtitleStreamIndices, // #112 rework diff --git a/Sources/AetherEngine/Video/VODSegmentCutter.swift b/Sources/AetherEngine/Video/VODSegmentCutter.swift index 0b02d4c5d..62b1fde5d 100644 --- a/Sources/AetherEngine/Video/VODSegmentCutter.swift +++ b/Sources/AetherEngine/Video/VODSegmentCutter.swift @@ -36,14 +36,27 @@ struct VODSegmentCutter { /// never advance). let boundaries: [Int64] let baseIndex: Int + /// How far before each boundary (same local indices as `boundaries`, missing entries 0) the + /// keyframe that boundary names may arrive. A #409 repair that synthesizes the decode ladder + /// places a late-written IDR back on its sequence, before the index entry the plan was built from + /// (`Demuxer.indexedKeyframeAdvanceTicks`); without the reach that keyframe stays in the previous + /// segment and the next one opens mid-GOP (25 of 40 segments on the 23 fps reporting asset). The + /// engine keeps each reach below the distance to the keyframe before, so that one never reaches. + let reachTicks: [Int64] private(set) var current: Int - init(sourceBoundaries: [Int64], planAnchorPts: Int64, baseIndex: Int) { + init(sourceBoundaries: [Int64], planAnchorPts: Int64, baseIndex: Int, reachTicks: [Int64] = []) { self.boundaries = sourceBoundaries.map { $0 &- planAnchorPts } self.baseIndex = baseIndex + self.reachTicks = reachTicks.map { max(0, $0) } self.current = baseIndex } + /// The reach of local boundary `index`. + func reach(at index: Int) -> Int64 { + index >= 0 && index < reachTicks.count ? reachTicks[index] : 0 + } + /// Segment index for a video packet, in decode order, from its ITEM-axis presentation time. /// Advances on a keyframe that has reached the next boundary; every other packet (and an /// intra-segment keyframe that has not yet reached the next boundary, e.g. when the GOP is shorter @@ -53,7 +66,8 @@ struct VODSegmentCutter { // boundaries[count-1] is the end of the final segment, not a segment start, so the last segment // a keyframe can open is local (count-2): advance only while the next entry is a real start. var nextLocal = (current - baseIndex) + 1 - while nextLocal < boundaries.count - 1, pts >= boundaries[nextLocal] { + while nextLocal < boundaries.count - 1, + SourceTimestampBounds.sum(pts, reach(at: nextLocal)) >= boundaries[nextLocal] { current += 1 nextLocal += 1 } diff --git a/Tests/AetherEngineTests/DocumentedConstantsTests.swift b/Tests/AetherEngineTests/DocumentedConstantsTests.swift index 7e99337cc..412fcb447 100644 --- a/Tests/AetherEngineTests/DocumentedConstantsTests.swift +++ b/Tests/AetherEngineTests/DocumentedConstantsTests.swift @@ -56,6 +56,11 @@ final class DocumentedConstantsTests: XCTestCase { assertDocumented("up to\n**48 pictures**", try documentation()) } + func testLandingEnvelopeHoldMatchesDocumentation() throws { + XCTAssertEqual(H264CompositionOffsetRepair.landingEnvelopePictures, 8) + assertDocumented("first **8 pictures**", try documentation()) + } + func testPartialCompositionHoldBoundsMatchDocumentation() throws { let docs = try documentation() XCTAssertEqual(H264PartialCompositionRepair.maximumReorderDepth, 16) diff --git a/Tests/AetherEngineTests/H264ForcedMonotonicLadderRepairTests.swift b/Tests/AetherEngineTests/H264ForcedMonotonicLadderRepairTests.swift index d94ee34b0..33314eaa3 100644 --- a/Tests/AetherEngineTests/H264ForcedMonotonicLadderRepairTests.swift +++ b/Tests/AetherEngineTests/H264ForcedMonotonicLadderRepairTests.swift @@ -92,6 +92,52 @@ struct H264ForcedMonotonicLadderRepairTests { #expect(pushed == healthy) } + @Test("a keyframe moved back onto its sequence stays within the reach its index entry is read with") + func movedKeyframeStaysWithinIndexReach() throws { + let data = try #require(Data(base64Encoded: Self.pushedKeyframeFixtureBase64, options: .ignoreUnknownCharacters)) + let demuxer = Demuxer() + try demuxer.open(reader: DataIOReader(data: data), formatHint: "mp4") + defer { demuxer.close() } + let videoIndex = demuxer.videoStreamIndex + var keyframes: [Int64] = [] + while let packet = try? demuxer.readPacket() { + if packet.pointee.stream_index == videoIndex, packet.pointee.flags & AV_PKT_FLAG_KEY != 0 { + keyframes.append(packet.pointee.dts) + } + var owned: UnsafeMutablePointer? = packet + trackedPacketFree(&owned) + } + let index = demuxer.indexedKeyframes(streamIndex: videoIndex).sorted() + let advance = demuxer.indexedKeyframeAdvanceTicks + // Reorder delay 2 plus two and a half pictures at 29.97 fps. + #expect(advance == 6006 + 2 * 3003 + 1501) + #expect(keyframes.count == index.count) + // The pushed keyframe is delivered before its entry, which is why a cutter needs the reach. + #expect(zip(keyframes, index).contains { $0 < $1 }) + #expect(zip(keyframes, index).allSatisfy { $0 <= $1 && $0 >= $1 - advance }) + let reach = HLSVideoEngine.keyframeReachTicks(advance: advance, keyframes: index, boundaries: index) + #expect(zip(zip(keyframes, index), reach).allSatisfy { pair, reach in pair.0 + reach >= pair.1 }) + } + + @Test("a continued sequence leans toward the earliest origin its pictures point to, an eighth of a picture at a time") + func continuedSequenceLeansTowardEnvelope() { + var plan = H264CompositionOffsetRepair.Plan(step: 3000, decodeLead: 6000, shift: 0, pocStep: 2) + plan.synthesizesDecodeLadder = true + plan.pictureCadence = H264CompositionOffsetRepair.Cadence(numerator: 3000, denominator: 1) + plan.decodeLeadPictures = 2 + var rewriter = H264CompositionOffsetRepair.Rewriter(plan: plan) + // The writer pushed the first IDR 600 ticks late and left the rest of its sequence on time, + // so counting from the IDR runs 600 ticks behind where the sequence really is. + _ = rewriter.rewrite(dts: 600, pictureOrderCount: 0, isKeyframe: true) + for picture in 1..<10 { + _ = rewriter.rewrite(dts: Int64(picture) * 3000, pictureOrderCount: Int64(picture) * 2, isKeyframe: false) + } + // The next IDR is pushed three pictures: it continues the sequence at 30 600 by count, and + // leans 3000 / 8 ticks toward the 30 000 the on-time pictures point to. + let idr = rewriter.rewrite(dts: 39_000, pictureOrderCount: 0, isKeyframe: true) + #expect(idr.map(\.pts) == Int64(30_600 - 375)) + } + @Test("a head that opens on a run of P pictures is sampled until it reorders") func leadingPRunIsSampledUntilReordering() throws { let healthy = try Self.videoTimestamps(base64: Self.leadingPRunHealthyFixtureBase64) @@ -159,7 +205,7 @@ struct H264ForcedMonotonicLadderRepairTests { #expect(withoutStandard == .inconclusive("anchors do not agree on a step")) guard case .repair(let plan) = H264CompositionOffsetRepair.classify( samples: samples, videoDelay: 2, streamStartTime: 0, ladderStart: -12012, - nominalStep: 3003, standardStep: 3003) + nominalStep: 3003, rateCadence: H264CompositionOffsetRepair.Cadence(numerator: 3003, denominator: 1)) else { Issue.record("expected a repair") return @@ -183,30 +229,55 @@ struct H264ForcedMonotonicLadderRepairTests { #expect(H264CompositionOffsetRepair.nominalLadderFits(samples: samples, step: 3103, videoDelay: 2)) guard case .repair(let plan) = H264CompositionOffsetRepair.classify( samples: samples, videoDelay: 2, streamStartTime: 0, ladderStart: 0, - nominalStep: 3103, standardStep: 3103) + nominalStep: 3103, rateCadence: H264CompositionOffsetRepair.Cadence(numerator: 90000, denominator: 29)) else { Issue.record("expected a repair") return } #expect(plan.step == 3103) + // The offsets come off the exact 29 fps length, so a sequence of 1000 pictures ends where + // 29 fps puts it rather than 450 ticks early on the rounded step. + #expect(plan.pictureOffset(1000) == 3_103_448) } - @Test("the standard step is the nearest standard rate within 1%") - func standardFrameStepSnapsToNearestRate() { + @Test("the standard cadence is the nearest standard rate within 1%, as an exact fraction") + func standardFrameCadenceSnapsToNearestRate() { let ninetyKilohertz = AVRational(num: 1, den: 90000) + func ticks(_ estimate: Int64, _ timeBase: AVRational = ninetyKilohertz) -> (Int64, Int64)? { + H264CompositionOffsetRepair.standardFrameCadence(near: estimate, timeBase: timeBase) + .map { ($0.numerator, $0.denominator) } + } // 29.97 and 30 are both within 1% of each other; the nearer one wins. - #expect(H264CompositionOffsetRepair.standardFrameStep(near: 3003, timeBase: ninetyKilohertz) == 3003) - #expect(H264CompositionOffsetRepair.standardFrameStep(near: 3001, timeBase: ninetyKilohertz) == 3000) + #expect(ticks(3003)! == (3003, 1)) + #expect(ticks(3001)! == (3000, 1)) // A 60-picture 29.97 clip reports 29.80. - #expect(H264CompositionOffsetRepair.standardFrameStep(near: 3020, timeBase: ninetyKilohertz) == 3003) - #expect(H264CompositionOffsetRepair.standardFrameStep(near: 3600, timeBase: ninetyKilohertz) == 3600) - // Whole rates count too, and a rate that is not a whole number of ticks is rounded: 23 fps is - // 3913.04 ticks (the second reporting file), 23.976 is 3753.75 rather than 24 fps's 3750. - #expect(H264CompositionOffsetRepair.standardFrameStep(near: 3913, timeBase: ninetyKilohertz) == 3913) - #expect(H264CompositionOffsetRepair.standardFrameStep(near: 3754, timeBase: ninetyKilohertz) == 3754) + #expect(ticks(3020)! == (3003, 1)) + #expect(ticks(3600)! == (3600, 1)) + // Whole rates count too, kept as the fraction they are: 23 fps is 3913.04 ticks (the second + // reporting file), 23.976 is 3753.75 rather than 24 fps's 3750. + #expect(ticks(3913)! == (90000, 23)) + #expect(ticks(3754)! == (15015, 4)) // Half a frame per second is further than 1% from every standard rate. - #expect(H264CompositionOffsetRepair.standardFrameStep(near: 180_000, timeBase: ninetyKilohertz) == nil) - #expect(H264CompositionOffsetRepair.standardFrameStep(near: 1001, timeBase: AVRational(num: 1, den: 24000)) == 1001) + #expect(ticks(180_000) == nil) + #expect(ticks(1001, AVRational(num: 1, den: 24000))! == (1001, 1)) + } + + @Test("the bitstream's stated rate wins over an average that cannot tell 24 from 23.976") + func bitstreamRateNamesTheCadence() { + let ninetyKilohertz = AVRational(num: 1, den: 90000) + func cadence(_ rate: AVRational?, _ estimate: Int64) -> (Int64, Int64)? { + H264CompositionOffsetRepair.rateCadence(bitstreamRate: rate, estimate: estimate, timeBase: ninetyKilohertz) + .map { ($0.numerator, $0.denominator) } + } + // A 24 fps clip with a pushed tail averages 23.98: the average alone picks 23.976. + #expect(cadence(nil, 3753)! == (15015, 4)) + #expect(cadence(AVRational(num: 24, den: 1), 3753)! == (3750, 1)) + // 30000/1001 stays 29.97 and does not snap to 30, which is 0.1% away. + #expect(cadence(AVRational(num: 30000, den: 1001), 3003)! == (3003, 1)) + // The third reporting file states 590325/20356 (29.0002 fps), which is 29 to within 0.01%. + #expect(cadence(AVRational(num: 590325, den: 20356), 3103)! == (90000, 29)) + // A stated rate that disagrees with the stream by more than 1% is not believed. + #expect(cadence(AVRational(num: 60, den: 1), 3003)! == (3003, 1)) } @Test("a variable-rate head is still left alone") diff --git a/Tests/AetherEngineTests/RepairedKeyframeSegmentReachTests.swift b/Tests/AetherEngineTests/RepairedKeyframeSegmentReachTests.swift new file mode 100644 index 000000000..b7e8b26c4 --- /dev/null +++ b/Tests/AetherEngineTests/RepairedKeyframeSegmentReachTests.swift @@ -0,0 +1,69 @@ +import Testing +@testable import AetherEngine + +/// A #409 repair that synthesizes the decode ladder moves a late-written IDR back onto its sequence, +/// so the keyframe arrives before the index entry the segment plan was built from. The cutter and the +/// restart gate reach back for it; without that the keyframe stayed in the previous segment and the +/// next one opened on audio mid-GOP (25 of 40 segments on the 23 fps reporting asset). +@Suite("Segment boundaries reach a repaired keyframe placed before its index entry") +struct RepairedKeyframeSegmentReachTests { + + // 3913 ticks per picture (23 fps at 90 kHz); boundaries are the index entries. + private let boundaries: [Int64] = [0, 352_170, 716_087, 1_056_522] + + @Test("a keyframe three pictures early opens its segment once the boundary reaches for it") + func earlyKeyframeOpensSegmentWithinReach() { + var without = VODSegmentCutter(sourceBoundaries: boundaries, planAnchorPts: 0, baseIndex: 0) + _ = without.index(pts: 0, isKeyframe: true) + #expect(without.index(pts: 352_170, isKeyframe: true) == 1) + #expect(without.index(pts: 716_087 - 3 * 3913, isKeyframe: true) == 1) + + var with = VODSegmentCutter(sourceBoundaries: boundaries, planAnchorPts: 0, baseIndex: 0, + reachTicks: [0, 17_608, 17_608, 17_608]) + _ = with.index(pts: 0, isKeyframe: true) + #expect(with.index(pts: 352_170, isKeyframe: true) == 1) + #expect(with.index(pts: 716_087 - 3 * 3913, isKeyframe: true) == 2) + // Pictures after it stay with it, and non-keyframes never cut, reach or not. + #expect(with.index(pts: 1_056_522 - 3913, isKeyframe: false) == 2) + } + + @Test("each boundary's reach stops short of the keyframe before it") + func reachStopsShortOfPreviousKeyframe() { + // Two keyframes four pictures apart at the third boundary: the advance (17 608) would let the + // first one open that boundary's segment. + let keyframes: [Int64] = [0, 352_170, 716_087 - 4 * 3913, 716_087, 1_056_522] + let reach = HLSVideoEngine.keyframeReachTicks(advance: 17_608, keyframes: keyframes, + boundaries: [0, 352_170, 716_087, 1_056_522]) + #expect(reach == [17_608, 17_608, 4 * 3913 - 1, 17_608]) + var cutter = VODSegmentCutter(sourceBoundaries: [0, 352_170, 716_087, 1_056_522, 1_400_000], + planAnchorPts: 0, baseIndex: 0, reachTicks: reach) + _ = cutter.index(pts: 0, isKeyframe: true) + _ = cutter.index(pts: 352_170, isKeyframe: true) + #expect(cutter.index(pts: 716_087 - 4 * 3913, isKeyframe: true) == 1) + #expect(cutter.index(pts: 716_087, isKeyframe: true) == 2) + } + + @Test("no repair, no reach") + func noAdvanceNoReach() { + #expect(HLSVideoEngine.keyframeReachTicks(advance: 0, keyframes: [0, 1000], boundaries: [0, 1000]).isEmpty) + let cutter = VODSegmentCutter(sourceBoundaries: boundaries, planAnchorPts: 0, baseIndex: 0) + #expect(cutter.reach(at: 1) == 0) + } + + @Test("the restart gate opens on the early keyframe instead of waiting a GOP") + func restartGateTakesEarlyKeyframe() { + let target: Int64 = 716_087 + #expect(!HLSSegmentProducer.videoGateTargetSatisfied(pts: target - 2 * 3913, dts: target - 4 * 3913, + targetPts: target)) + #expect(HLSSegmentProducer.videoGateTargetSatisfied(pts: target - 2 * 3913, dts: target - 4 * 3913, + targetPts: target, reachTicks: 17_608)) + } + + @Test("only a synthesized ladder moves keyframes, by the reorder delay plus two and a half pictures") + func keyframeAdvanceLimit() { + var plan = H264CompositionOffsetRepair.Plan(step: 3913, decodeLead: 7826, shift: 7826, pocStep: 2) + #expect(plan.keyframeAdvanceLimit == 0) + plan.synthesizesDecodeLadder = true + #expect(plan.keyframeAdvanceLimit == 7826 + 2 * 3913 + 1956) + } +} diff --git a/docs/formats.md b/docs/formats.md index 487e00f62..184539eab 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -110,10 +110,14 @@ average frame rate, provided the sampled span is the time its pictures take at t reorder delay plus two pictures, and every other check above still passes. The step is then remeasured on the anchors when the writer left them on the presentation axis (the average rate is only an estimate: a 60-picture 29.97 fps clip reports 29.80). The reporting file pushed its P pictures -as well (each anchor 1001 ticks past the picture before it), so no two anchors agree; the step is -then the standard frame rate (any whole rate up to 240, or an NTSC one such as 23.976 or 29.97) -nearest the average rate, if that is within 1%, rounded to a tick (a second reporting file runs at -23 fps, 3913.04 ticks). A head with neither is left untouched. DTS is rebuilt from each picture's +as well (each anchor 1001 ticks past the picture before it), so no two anchors agree; the rate is +then the one the bitstream's VUI timing states, if that is within 1% of the average rate, snapped to +a standard frame rate (any whole rate up to 240, or an NTSC one such as 23.976 or 29.97) within +0.01%. Without a VUI rate it is the standard rate nearest the average rate, if that is within 1%. The +VUI is what tells 24 from 23.976 on a short clip whose pushed tail drags the average, and on all three +reporting files it named the exact rate (23, 29.97 and 29.0002 fps). Pictures are placed on that +rate as an exact fraction of a tick, so a 23 fps file (3913.04 ticks at 90 kHz) does not lose a tick +every 25 pictures. A head with neither is left untouched. DTS is rebuilt from each picture's decode slot rather than shifted, since a pushed B picture's DTS can sit after its own presentation time. The writer pushes keyframes as well, and only ever later: on both reporting files they sat 0 to 4 pictures after the end of the GOP before them, which left a gap or put the next GOP in front of the @@ -121,7 +125,20 @@ last pictures of the previous one. A later IDR therefore continues the sequence is later than the reorder delay plus two and a half pictures (a real discontinuity; a third reporting file at 29 fps kept its keyframes four pictures late for a hundred seconds) or within half a tick per picture (all a rounded step can be off by); an earlier one keeps its own time, which is how a chain -started on a pushed keyframe after a seek moves back into place. The second reporting file also opens +started on a pushed keyframe after a seek moves back into place. Counting alone follows the stated +rate, and the writer's clock is not quite that (the third file states 29.0002 fps and writes 29.003): +the chain drifted 61 ms behind the writer over 19 minutes and overlapped 3 times where an earlier +keyframe pulled it back. A continued sequence therefore also leans toward the earliest origin the +pictures before it point to (the writer only ever pushes a picture later, so the earliest is the +nearest the truth), by at most an eighth of a picture per GOP, which removed both. A seek landing +holds its first **8 pictures** and takes the earliest origin they point to in the same way. A keyframe +placed back like this sits before its index entry, by at most the reorder delay plus two and a half +pictures, while the HLS segment plan is built from the index. The VOD cutter and the restart gate +therefore reach that far back for the keyframe each boundary names, never as far as the keyframe +before it: without the reach the keyframe stayed in the previous segment and the next one opened on +audio, mid-GOP, which `aetherctl segverify` counted on 25 of 40 segments of the 23 fps file and 22 of +the 29.97 fps one. All 40 are independent now, and so are segments a restart produced. The second +reporting file also opens every GOP on twelve P pictures before its first B, so a head that has not reordered after twelve pictures, with `PTS == DTS` throughout, is sampled on to its first regression plus a mini-GOP, up to **48 pictures**. The sampled span is checked against the rounded step with a tick per picture to From 734477ee274c05ed80cb4c909ef35fc1002439f9 Mon Sep 17 00:00:00 2001 From: Sunghyun Kim Date: Tue, 27 Oct 2026 09:25:25 +0900 Subject: [PATCH 7/7] fix(hls): read the keyframe reach off the collapsed segment plan The reach for each boundary was computed on the keyframe plan before `collapseShortSegments` folded it, then indexed against the folded plan. A pair of keyframes a few pictures apart is what makes a folded segment, so from that fold on every boundary took the reach of the one before: the earlier keyframe of the pair could open the boundary after it, and the next boundary lost the reach its own keyframe needs. `finalizedPlan` collapses first and reads the reach off what it leaves. The 23 fps reporting asset folds three times; all 788 of its segments decode on their own either way, so this is caught by the new test rather than by that file. Co-Authored-By: Claude Opus 5.5 --- .../HLSVideoEngine+SegmentPlanning.swift | 13 +++++++++++++ .../AetherEngine/Video/HLSVideoEngine.swift | 12 ++++++++---- .../RepairedKeyframeSegmentReachTests.swift | 19 +++++++++++++++++++ 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/Sources/AetherEngine/Video/HLSVideoEngine+SegmentPlanning.swift b/Sources/AetherEngine/Video/HLSVideoEngine+SegmentPlanning.swift index d58baa2ff..ef0598cb0 100644 --- a/Sources/AetherEngine/Video/HLSVideoEngine+SegmentPlanning.swift +++ b/Sources/AetherEngine/Video/HLSVideoEngine+SegmentPlanning.swift @@ -90,6 +90,19 @@ extension HLSVideoEngine { } } + /// `collapseShortSegments`, then the reach for each boundary of the plan it leaves. The order is + /// the point: a pair of keyframes a few pictures apart is what makes a segment that gets folded, + /// and reaches read off the unfolded plan sat one boundary off from that fold on (three folds on + /// the 23 fps reporting asset). Off by one, a boundary takes the reach of the one before, which + /// can let the earlier keyframe of the pair open it, and hands its own to the next boundary, + /// whose keyframe can then miss it. + static func finalizedPlan(_ plan: [Segment], minDurationSeconds: Double, keyframeAdvance: Int64, + keyframes: [Int64]) -> (plan: [Segment], reach: [Int64]) { + let collapsed = collapseShortSegments(plan, minDurationSeconds: minDurationSeconds) + return (collapsed, keyframeReachTicks(advance: keyframeAdvance, keyframes: keyframes, + boundaries: collapsed.map(\.startPts))) + } + /// An index failing any witness is routed to the uniform-stride fallback. static func keyframeIndexIsTrustworthy( keyframes: [Int64], diff --git a/Sources/AetherEngine/Video/HLSVideoEngine.swift b/Sources/AetherEngine/Video/HLSVideoEngine.swift index aa3af4bdb..aa1ce5e87 100644 --- a/Sources/AetherEngine/Video/HLSVideoEngine.swift +++ b/Sources/AetherEngine/Video/HLSVideoEngine.swift @@ -863,7 +863,10 @@ public final class HLSVideoEngine: @unchecked Sendable { /// How far before its boundary the keyframe each `segmentPlan` entry names may arrive /// (`VODSegmentCutter.reachTicks`). Empty unless a #409 repair moves keyframes off the index. + /// Set with the final plan by `finalizedPlan`, never off the plan before it is collapsed. var planKeyframeReachTicks: [Int64] = [] + /// `Demuxer.indexedKeyframeAdvanceTicks` of a keyframe-aligned plan, held until the plan is final. + var planKeyframeAdvanceTicks: Int64 = 0 /// Guards subsystem refs + `sessionEpoch`. Never held across waits or network I/O so /// `stop()` on the main thread is never blocked behind a restart's 5 s waitForFinish. @@ -1460,6 +1463,7 @@ public final class HLSVideoEngine: @unchecked Sendable { let durationSeconds = dem.duration var plan: [Segment] var keyframeBytePositions: [Int64: Int64] = [:] + planKeyframeAdvanceTicks = 0 if isLiveSession { sourceBitrate = dem.bitRate self.firstKeyframePts = 0 @@ -1563,9 +1567,7 @@ public final class HLSVideoEngine: @unchecked Sendable { // stamping. Read from the demuxer that produced them, never from the URL or the // host's metadata: on a remux session the delivered container is the one indexed. planBoundaryAxis = PlanBoundaryAxis.forContainer(formatName: dem.containerFormatName) - planKeyframeReachTicks = Self.keyframeReachTicks( - advance: dem.indexedKeyframeAdvanceTicks, keyframes: keyframes, - boundaries: plan.map(\.startPts)) + planKeyframeAdvanceTicks = dem.indexedKeyframeAdvanceTicks let firstKeyframePts = keyframes.sorted().first ?? 0 self.firstKeyframePts = firstKeyframePts let firstKeyframeSeconds = Double(firstKeyframePts) * Double(videoTimeBase.num) / Double(videoTimeBase.den) @@ -1890,7 +1892,9 @@ public final class HLSVideoEngine: @unchecked Sendable { // Fold degenerate sub-frame segments (keyframe clusters make buildKeyframeSegmentPlan emit // ~40 ms segments the producer cannot cut, wedging the near-EOF resume; the plan and producer // share these boundaries, so the merge fixes both at once). - plan = Self.collapseShortSegments(plan, minDurationSeconds: Self.minSegmentDurationSeconds) + (plan, planKeyframeReachTicks) = Self.finalizedPlan( + plan, minDurationSeconds: Self.minSegmentDurationSeconds, + keyframeAdvance: planKeyframeAdvanceTicks, keyframes: Array(keyframeBytePositions.keys)) self.segmentPlan = plan if !keyframeBytePositions.isEmpty { sourcePeakBitrate = Self.peakSegmentBitrate(plan: plan, keyframeBytePositions: keyframeBytePositions) diff --git a/Tests/AetherEngineTests/RepairedKeyframeSegmentReachTests.swift b/Tests/AetherEngineTests/RepairedKeyframeSegmentReachTests.swift index b7e8b26c4..4da204f92 100644 --- a/Tests/AetherEngineTests/RepairedKeyframeSegmentReachTests.swift +++ b/Tests/AetherEngineTests/RepairedKeyframeSegmentReachTests.swift @@ -43,6 +43,25 @@ struct RepairedKeyframeSegmentReachTests { #expect(cutter.index(pts: 716_087, isKeyframe: true) == 2) } + @Test("the reach is read off the plan after its short segments are folded") + func reachFollowsCollapsedPlan() { + // 90 kHz. Keyframes at 8.0 s and 8.11 s make a 0.11 s segment, which folds into the one before. + let starts: [Int64] = [0, 360_000, 720_000, 730_000, 1_080_000] + let plan = zip(starts, starts.dropFirst() + [1_440_000]).map { start, end in + HLSVideoEngine.Segment(startPts: start, endPts: end, startSeconds: Double(start) / 90_000, + durationSeconds: Double(end - start) / 90_000) + } + let final = HLSVideoEngine.finalizedPlan(plan, minDurationSeconds: 1, keyframeAdvance: 17_608, + keyframes: starts) + // The fold keeps the later keyframe as the boundary, so its reach stops short of the one at 8.0 s. + #expect(final.plan.map(\.startPts) == [0, 360_000, 730_000, 1_080_000]) + #expect(final.reach == [17_608, 17_608, 9_999, 17_608]) + // Read off the unfolded plan instead, 730 000 took 720 000's reach (so the keyframe before it + // could open its segment) and 1 080 000 took 730 000's (so its own keyframe could miss it). + let unfolded = HLSVideoEngine.keyframeReachTicks(advance: 17_608, keyframes: starts, boundaries: starts) + #expect(Array(unfolded[2...3]) == [17_608, 9_999]) + } + @Test("no repair, no reach") func noAdvanceNoReach() { #expect(HLSVideoEngine.keyframeReachTicks(advance: 0, keyframes: [0, 1000], boundaries: [0, 1000]).isEmpty)