From 0602fd80d29fafebb9490ee7d641fe1e47498fc2 Mon Sep 17 00:00:00 2001 From: "federico.gambassi@tomatoai.it" Date: Fri, 30 Oct 2026 18:54:03 +0100 Subject: [PATCH] fix(macos): replay an HLS clip from its end without a spurious end Seeked back from its end, AVPlayer plays an HLS VOD up to the first EXT-X-DISCONTINUITY and then jumps to the end and stops, when the first segment carries no audio sample. Frigate's fMP4 clips of a camera that records audio only sparsely (one AAC packet per 10 s file) are like that whenever a clip starts inside a recording file. It happens on every replay, including loop = true, which restarts with seekTo(0) from the end. On first play the same boundary goes through, and switching the audio track off does not help. seekTo now rebuilds the player on the same, already loaded asset when the HLS item has reached its end since the last rebuild. The replay costs about as much as the seek did, and the clip plays through. Co-Authored-By: Claude Opus 5.5 --- .../native/macos/NativeVideoPlayer.swift | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/mediaplayer/src/jvmMain/native/macos/NativeVideoPlayer.swift b/mediaplayer/src/jvmMain/native/macos/NativeVideoPlayer.swift index 36d1318a..3cb3852c 100644 --- a/mediaplayer/src/jvmMain/native/macos/NativeVideoPlayer.swift +++ b/mediaplayer/src/jvmMain/native/macos/NativeVideoPlayer.swift @@ -81,6 +81,8 @@ class MacVideoPlayer { // End-of-playback flag (set by AVPlayerItemDidPlayToEndTime, consumed once by the Kotlin side) private var didPlayToEnd: Bool = false + // Set when the item reaches its end, cleared when seekTo rebuilds it (not consumed by the Kotlin side) + private var reachedEnd: Bool = false private var playbackEndObserver: NSObjectProtocol? // HLS Error tracking @@ -907,6 +909,7 @@ class MacVideoPlayer { queue: nil ) { [weak self] _ in self?.didPlayToEnd = true + self?.reachedEnd = true } // Configure player for HLS @@ -1308,6 +1311,7 @@ class MacVideoPlayer { /// Seeks to the specified time (in seconds). func seekTo(time: Double) { + if isHLSStream && reachedEnd { reloadEndedItem() } guard let player = player else { return } let newTime = CMTime(seconds: time, preferredTimescale: 600) @@ -1330,6 +1334,24 @@ class MacVideoPlayer { } } + /// Rebuilds the player on the same, already loaded asset once the HLS item has played to its end. + /// + /// Seeked back from its end, an AVPlayerItem of an HLS VOD whose first segment carries no audio sample + /// (an fMP4 playlist with EXT-X-DISCONTINUITY between segments, where the camera records audio only + /// sparsely) plays that segment and then jumps to the end at the first discontinuity, every time. On first + /// play the same boundary goes through, and a fresh item from the same asset plays it through too. + private func reloadEndedItem() { + guard let asset = player?.currentItem?.asset else { return } + let composition = player?.currentItem?.videoComposition + reachedEnd = false + let wasPlaying = isPlaying + stopDisplayLink() + player?.pause() + cleanupObservers() + setupVideoOutputAndPlayer(with: asset, videoComposition: composition) + if wasPlaying { play() } + } + /// Consumes the end-of-playback flag. Returns true once per playback completion. func consumeDidPlayToEnd() -> Bool { if didPlayToEnd {