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 @@ -62,6 +62,9 @@ internal val macLogger = TaggedLogger("MacVideoPlayerState")
*/
private const val POSITION_POLL_INTERVAL_MS = 200L

// Polls (250 ms apart) without a video size before media with an audio track counts as audio-only.
private const val AUDIO_ONLY_MIN_POLLS = 8

/**
* macOS implementation of the video player state.
*
Expand Down Expand Up @@ -418,15 +421,19 @@ class MacVideoPlayerState : VideoPlayerState {
try {
val w = MacNativeBridge.nGetFrameWidth(ptr)
val h = MacNativeBridge.nGetFrameHeight(ptr)
if (w <= 0 || h <= 0) {
// Audio-only media (an mp3, an m4a) has no video size but still plays: only a
// source with neither a picture nor any audio or duration is an error.
val isAudioOnly = (w <= 0 || h <= 0) &&
(resolvedDuration > 0.0 || MacNativeBridge.nGetAudioChannels(ptr) > 0)
if ((w <= 0 || h <= 0) && !isAudioOnly) {
setError("Failed to retrieve video size")
return@withLock
}
videoWidth = w
videoHeight = h
videoWidth = w.coerceAtLeast(0)
videoHeight = h.coerceAtLeast(0)

// Scale output to match the display surface (saves memory for 4K+ video).
if (surfaceWidth > 0 && surfaceHeight > 0) {
if (!isAudioOnly && surfaceWidth > 0 && surfaceHeight > 0) {
videoReaderMutex.withLock {
MacNativeBridge.nSetOutputSize(ptr, surfaceWidth, surfaceHeight)
val sw = MacNativeBridge.nGetFrameWidth(ptr)
Expand Down Expand Up @@ -461,6 +468,8 @@ class MacVideoPlayerState : VideoPlayerState {
MacNativeBridge.nPlay(ptr)
}
_isPlaying = startPlayback
// No frame will clear the loading state of audio-only media.
if (isAudioOnly) isLoading = false

videoJob = startVideoPipeline()
} catch (e: Exception) {
Expand Down Expand Up @@ -496,6 +505,13 @@ class MacVideoPlayerState : VideoPlayerState {
macLogger.d { "Dimensions validated (w=$width, h=$height) after $attempt attempts" }
return
}
// Audio-only media never gets dimensions: once the asset reports an audio track and a
// while has passed without a video size, stop waiting (a late video size is still
// picked up by the frame pipeline, which keeps reading).
if (attempt >= AUDIO_ONLY_MIN_POLLS && MacNativeBridge.nGetAudioChannels(ptr) > 0) {
macLogger.d { "No video size after $attempt attempts but an audio track: treating as audio-only" }
return
}
delay(250)
}
macLogger.e { "Unable to retrieve valid dimensions after $maxAttempts attempts" }
Expand Down
38 changes: 26 additions & 12 deletions mediaplayer/src/jvmMain/native/macos/NativeVideoPlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -726,16 +726,25 @@ class MacVideoPlayer {
// Retrieve the video track to obtain the actual dimensions
asset.loadTracks(withMediaType: .video) { [self] tracks, error in
guard let videoTrack = tracks?.first, error == nil else {
print(
"Error loading video tracks: \(error?.localizedDescription ?? "Unknown")"
)
// For HLS streams without video track info yet, use default dimensions
if isHLSStream {
print("No video track info yet for the HLS stream: \(error?.localizedDescription ?? "none")")
frameWidth = 1920
frameHeight = 1080
nativeVideoWidth = frameWidth
nativeVideoHeight = frameHeight
setupVideoOutputAndPlayer(with: asset)
} else if error == nil {
// Audio-only media (an MP3, an M4A): no video track, but it plays. The player is set up with
// no video output; the Kotlin side reports a 0x0 size as audio-only.
print("No video track: playing as audio-only")
frameWidth = 0
frameHeight = 0
nativeVideoWidth = 0
nativeVideoHeight = 0
setupVideoOutputAndPlayer(with: asset, hasVideo: false)
} else {
print("Error loading video tracks: \(error?.localizedDescription ?? "Unknown")")
}
return
}
Expand Down Expand Up @@ -845,15 +854,20 @@ class MacVideoPlayer {
}

// Helper method to setup video output and player
private func setupVideoOutputAndPlayer(with asset: AVAsset, videoComposition: AVVideoComposition? = nil) {
// Create attributes for the CVPixelBuffer (BGRA format) with IOSurface for better performance
let pixelBufferAttributes: [String: Any] = [
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA,
kCVPixelBufferWidthKey as String: frameWidth,
kCVPixelBufferHeightKey as String: frameHeight,
kCVPixelBufferIOSurfacePropertiesKey as String: [:],
]
videoOutput = AVPlayerItemVideoOutput(pixelBufferAttributes: pixelBufferAttributes)
private func setupVideoOutputAndPlayer(with asset: AVAsset, videoComposition: AVVideoComposition? = nil, hasVideo: Bool = true) {
if hasVideo {
// Create attributes for the CVPixelBuffer (BGRA format) with IOSurface for better performance
let pixelBufferAttributes: [String: Any] = [
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA,
kCVPixelBufferWidthKey as String: frameWidth,
kCVPixelBufferHeightKey as String: frameHeight,
kCVPixelBufferIOSurfacePropertiesKey as String: [:],
]
videoOutput = AVPlayerItemVideoOutput(pixelBufferAttributes: pixelBufferAttributes)
} else {
// Audio-only: no frames to read (captureFrame and captureInitialFrame need an output and return early).
videoOutput = nil
}

let item = AVPlayerItem(asset: asset)

Expand Down