Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
33 changes: 33 additions & 0 deletions mediaplayer/src/jvmMain/native/macos/NativeVideoPlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand Down