diff --git a/.gitignore b/.gitignore index dac05bd5..c83928ad 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,8 @@ Pods/ /mediaplayer/src/jvmMain/resources/composemediaplayer/native/ /mediaplayer/src/jvmMain/resources/win32-x86-64/ /mediaplayer/src/jvmMain/resources/win32-arm64/ +/mediaplayer/src/jvmMain/resources/darwin-aarch64/ +/mediaplayer/src/jvmMain/resources/darwin-x86-64/ # Native build artifacts /mediaplayer/src/jvmMain/native/windows/build-x64/ 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 b3547324..cb636c16 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 @@ -681,22 +681,23 @@ class MacVideoPlayerState : VideoPlayerState { } // Check for looping - checkLoopingAsync(current, duration) + checkLoopingAsync() } catch (e: Exception) { if (e is CancellationException) throw e macLogger.e { "Error in updatePositionAsync: ${e.message}" } } } - /** Checks if looping is enabled and restarts the video if needed. */ - private suspend fun checkLoopingAsync( - current: Double, - duration: Double, - ) { + /** Checks if playback has ended and triggers loop or stop accordingly. */ + private suspend fun checkLoopingAsync() { val ptr = playerPtr - val ended = ptr != 0L && MacNativeBridge.nConsumeDidPlayToEnd(ptr) - // Also check position as fallback for content where the notification may not fire - if (!ended && (duration <= 0 || current < duration - 0.5)) return + if (ptr == 0L) return + + // Trust AVPlayerItemDidPlayToEndTime: it fires reliably on macOS for both + // file and HLS playback. A position-based fallback (current >= duration - x) + // is dangerous because it stops playback x seconds early — the slider + // freezes at (duration - x) / duration instead of reaching 100%. + if (!MacNativeBridge.nConsumeDidPlayToEnd(ptr)) return if (loop) { macLogger.d { "checkLoopingAsync() - Loop enabled, restarting video" } diff --git a/mediaplayer/src/jvmMain/native/macos/NativeVideoPlayer.swift b/mediaplayer/src/jvmMain/native/macos/NativeVideoPlayer.swift index a5e8fac2..54f3bddf 100644 --- a/mediaplayer/src/jvmMain/native/macos/NativeVideoPlayer.swift +++ b/mediaplayer/src/jvmMain/native/macos/NativeVideoPlayer.swift @@ -747,15 +747,22 @@ class MacVideoPlayer { self.nativeVideoWidth = self.frameWidth self.nativeVideoHeight = self.frameHeight + // Build a video composition that applies the preferred transform so + // that AVPlayerItemVideoOutput delivers pixel buffers already rotated + // to display orientation (fixes portrait videos rendering sideways). + let videoComposition: AVVideoComposition? = self.isHLSStream + ? nil + : (try? await AVVideoComposition.videoComposition(withPropertiesOf: asset)) + // Continue with player setup - self.setupVideoOutputAndPlayer(with: asset) + self.setupVideoOutputAndPlayer(with: asset, videoComposition: videoComposition) } catch { print("Error loading video track properties: \(error.localizedDescription)") // Use default dimensions for HLS if loading fails if self.isHLSStream { self.frameWidth = 1920 self.frameHeight = 1080 - self.setupVideoOutputAndPlayer(with: asset) + self.setupVideoOutputAndPlayer(with: asset, videoComposition: nil) } } } @@ -770,8 +777,14 @@ class MacVideoPlayer { nativeVideoWidth = frameWidth nativeVideoHeight = frameHeight + // Build a video composition that applies the preferred transform (see modern + // path above for rationale). Skip for HLS streams. + let videoComposition: AVVideoComposition? = isHLSStream + ? nil + : AVMutableVideoComposition(propertiesOf: asset) + // Continue with player setup - setupVideoOutputAndPlayer(with: asset) + setupVideoOutputAndPlayer(with: asset, videoComposition: videoComposition) } } } @@ -825,7 +838,7 @@ class MacVideoPlayer { } // Helper method to setup video output and player - private func setupVideoOutputAndPlayer(with asset: AVAsset) { + 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, @@ -837,6 +850,12 @@ class MacVideoPlayer { let item = AVPlayerItem(asset: asset) + // Apply the video composition (if any) so that pixel buffers delivered to + // AVPlayerItemVideoOutput are pre-rotated to the display orientation. + if let videoComposition = videoComposition { + item.videoComposition = videoComposition + } + // Configure for HLS if needed if isHLSStream { // Set buffer duration for HLS