From a3a3b8b65e97fe8924def67ec9797cd6881a48f8 Mon Sep 17 00:00:00 2001 From: "federico.gambassi@tomatoai.it" Date: Fri, 30 Oct 2026 18:46:03 +0100 Subject: [PATCH] fix(macos): report the audio format of HLS streams metadata.audioChannels and audioSampleRate were read from the asset's tracks, which an HLS asset does not list: both stayed null for every HLS stream, even with an AAC track playing. For HLS, read them from the AVPlayerItem's tracks once the item has loaded them, and backfill the metadata from the position poll, since that happens after open. Co-Authored-By: Claude Opus 5.5 --- .../mac/MacVideoPlayerState.kt | 8 +++++ .../native/macos/NativeVideoPlayer.swift | 33 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacVideoPlayerState.kt b/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacVideoPlayerState.kt index 035445b6..b80c09bd 100644 --- a/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacVideoPlayerState.kt +++ b/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacVideoPlayerState.kt @@ -671,6 +671,14 @@ class MacVideoPlayerState : VideoPlayerState { while (scope.isActive && _hasMedia && !isDisposing.get()) { val ptr = playerPtr if (ptr != 0L) { + // Backfill the audio format of an HLS stream, which the native side only learns once the + // item has loaded its tracks (after open) + if (metadata.audioChannels == null) { + MacNativeBridge.nGetAudioChannels(ptr).takeIf { it != 0 }?.let { channels -> + metadata.audioChannels = channels + metadata.audioSampleRate = MacNativeBridge.nGetAudioSampleRate(ptr).takeIf { it != 0 } + } + } // Backfill a duration that arrived after open (e.g. HLS VOD) regardless of play // state, so the total-time label appears even when the media was opened paused. // metadata.duration is what consumers read for the total-time label. diff --git a/mediaplayer/src/jvmMain/native/macos/NativeVideoPlayer.swift b/mediaplayer/src/jvmMain/native/macos/NativeVideoPlayer.swift index 36d1318a..f273c3b4 100644 --- a/mediaplayer/src/jvmMain/native/macos/NativeVideoPlayer.swift +++ b/mediaplayer/src/jvmMain/native/macos/NativeVideoPlayer.swift @@ -79,6 +79,9 @@ class MacVideoPlayer { private let aspectLock = NSLock() private var cachedDisplayAspectRatio: Double = 0.0 + // The asset of an HLS stream lists no tracks: its audio format is read from the item's tracks instead + private var tracksObserver: NSKeyValueObservation? + // End-of-playback flag (set by AVPlayerItemDidPlayToEndTime, consumed once by the Kotlin side) private var didPlayToEnd: Bool = false private var playbackEndObserver: NSObjectProtocol? @@ -626,6 +629,24 @@ class MacVideoPlayer { } } + /// Reads the channel count and sample rate of an audio track + private func readAudioFormat(of track: AVAssetTrack) { + func apply(_ formatDescriptions: [CMFormatDescription]) { + guard let description = formatDescriptions.first, + let basic = CMAudioFormatDescriptionGetStreamBasicDescription(description) + else { return } + audioChannels = Int(basic.pointee.mChannelsPerFrame) + audioSampleRate = Int(basic.pointee.mSampleRate) + } + if #available(macOS 13.0, *) { + Task { + if let formatDescriptions = try? await track.load(.formatDescriptions) { apply(formatDescriptions) } + } + } else if let formatDescriptions = track.formatDescriptions as? [CMFormatDescription] { + apply(formatDescriptions) + } + } + /// Detects the video's native frame rate from its asset private func detectVideoFrameRate(from asset: AVAsset) { // For HLS streams, default to 30 fps as it's variable @@ -900,6 +921,16 @@ class MacVideoPlayer { self?.updateCachedDisplayAspectRatio(from: item.presentationSize) } + // HLS: the audio channels and sample rate come from the item's tracks, once it has loaded them + if isHLSStream { + tracksObserver = item.observe(\.tracks, options: [.initial, .new]) { [weak self] item, _ in + guard let self = self, self.audioChannels == 0, + let audioTrack = item.tracks.first(where: { $0.assetTrack?.mediaType == .audio })?.assetTrack + else { return } + self.readAudioFormat(of: audioTrack) + } + } + // Observe end of playback for all media types playbackEndObserver = NotificationCenter.default.addObserver( forName: .AVPlayerItemDidPlayToEndTime, @@ -1349,6 +1380,8 @@ class MacVideoPlayer { bufferFullObserver?.invalidate() presentationSizeObserver?.invalidate() presentationSizeObserver = nil + tracksObserver?.invalidate() + tracksObserver = nil // Drop the cached aspect ratio so a reopened player can't briefly show the old video's ratio. updateCachedDisplayAspectRatio(from: .zero)