diff --git a/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/windows/WindowsVideoPlayerState.kt b/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/windows/WindowsVideoPlayerState.kt index 1517582a..a2eb89e7 100644 --- a/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/windows/WindowsVideoPlayerState.kt +++ b/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/windows/WindowsVideoPlayerState.kt @@ -47,6 +47,7 @@ import org.jetbrains.skia.ImageInfo import java.io.File import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.concurrent.write @@ -72,7 +73,18 @@ class WindowsVideoPlayerState : VideoPlayerState { val hr = WindowsNativeBridge.InitMediaFoundation() if (hr < 0) { windowsLogger.e { "Media Foundation initialization failed (hr=0x${hr.toString(16)})" } + return } + // Tear MF down on JVM exit — otherwise MF worker threads stay + // alive while the DLL is unloaded, corrupting KERNELBASE + // internals on shutdown (crash 0x87A). + try { + Runtime.getRuntime().addShutdownHook( + Thread { + try { WindowsNativeBridge.ShutdownMediaFoundation() } catch (_: Throwable) {} + } + ) + } catch (_: Throwable) { /* best effort */ } } } @@ -243,11 +255,26 @@ class WindowsVideoPlayerState : VideoPlayerState { private var videoJob: Job? = null private var resizeJob: Job? = null - // Memory optimization for frame processing - private val frameQueueSize = 1 + // Seek coalescing: rapid slider drags overwrite the target; only the + // latest value is actually seeked. seekInFlight acts as the "a loop is + // draining the target" claim. + private val pendingSeekTarget = AtomicLong(Long.MIN_VALUE) + private val seekInFlight = AtomicBoolean(false) + +// Serializes the native video reader: ReadVideoFrame / UnlockVideoFrame + // (held by the producer coroutine) and SeekMedia (held by the seek flow). + // This lets us seek *without* cancelling & restarting the producer — a + // pattern that turned out to behave inconsistently under GraalVM native + // image, leaving the video frozen after the first seek. + private val videoReaderMutex = Mutex() + private val isSeeking = AtomicBoolean(false) + + // Frame channel: one slot, drop-oldest. With triple-buffering on the + // producer side, overflow simply means the consumer was slow — safe to + // drop. Capacity >1 would just let the pipeline pile up. private val frameChannel = Channel( - capacity = frameQueueSize, + capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST, ) @@ -257,14 +284,26 @@ class WindowsVideoPlayerState : VideoPlayerState { val timestamp: Double, ) - // Double-buffering for zero-copy frame rendering - private var skiaBitmapA: Bitmap? = null - private var skiaBitmapB: Bitmap? = null - private var nextSkiaBitmapA: Boolean = true + // Triple-buffering for zero-copy frame rendering: the consumer may still + // be driving a frame onto Compose (via currentFrameState) when the + // producer writes the next frame. Two bitmaps is racy — with three, the + // buffer the producer writes is guaranteed to be distinct from both the + // one currently bound to ImageBitmap and the one Compose just finished. + private val skiaBitmaps = arrayOfNulls(3) + private var nextBitmapIndex: Int = 0 + @Volatile private var lastFrameHash: Int = Int.MIN_VALUE private var skiaBitmapWidth: Int = 0 private var skiaBitmapHeight: Int = 0 + // Bitmaps awaiting safe closure. When the video resolution changes mid-stream + // (HLS adaptive bitrate) the old double-buffer bitmaps may still be read by + // Compose on the AWT thread via currentFrameState. We defer close() by a few + // consumed frames so Compose has swapped to the new bitmap first. + private data class PendingCloseBitmap(val bitmap: Bitmap, var framesLeft: Int) + private val pendingCloseBitmaps = ArrayDeque() + private val pendingCloseGraceFrames: Int = 4 + // Adaptive frame interval (ms) based on the video's native frame rate. // Mirrors macOS approach: poll at the video frame rate, not faster. // This prevents starving the audio thread on the shared SourceReader. @@ -299,77 +338,72 @@ class WindowsVideoPlayerState : VideoPlayerState { return // Already disposing } - // Stop coroutines first — non-blocking - videoJob?.cancel() + // Stop coroutines first. The producer reads native state under + // videoReaderMutex; we must wait for it to exit its critical section + // before tearing down the native reader, otherwise CloseMedia can + // free memory the producer is still dereferencing (exit 2170). + val jobToJoin = videoJob + videoJob = null + jobToJoin?.cancel() resizeJob?.cancel() _isPlaying = false _hasMedia = false - // Release Kotlin-side resources immediately (bitmaps, channel) releaseAllResources() - // Native cleanup on a background thread so dispose() never blocks the UI. - // scope is about to be cancelled, so use a detached thread. val instance = videoPlayerInstance videoPlayerInstance = 0L lastUri = null + // Native cleanup must run SYNCHRONOUSLY. Compose Desktop's window close + // ultimately calls System.exit, which will not wait for an arbitrary + // background thread: the DLL gets unloaded while the native audio + // thread is still running against freed globals, crashing the process + // (exit 2170). Doing it here blocks the caller briefly (<500 ms for + // StopAudioThread + MF teardown) but guarantees a clean shutdown. if (instance != 0L) { - Thread { - try { - player.SetPlaybackState(instance, false, true) - } catch (e: Exception) { - windowsLogger.e { "Exception stopping playback: ${e.message}" } - } - try { - player.CloseMedia(instance) - } catch (e: Exception) { - windowsLogger.e { "Exception closing media: ${e.message}" } - } - instanceVolumes.remove(instance) - try { - WindowsNativeBridge.destroyInstance(instance) - } catch (e: Exception) { - windowsLogger.e { "Exception destroying instance: ${e.message}" } + if (jobToJoin != null) { + // Avoid runBlocking on the AWT Event Dispatch Thread: if any + // child coroutine of `scope` ever chains on Dispatchers.Main + // (even indirectly, e.g. Compose effects), joining here would + // deadlock. Fall back to a plain Thread.join on EDT — the + // 500 ms cap keeps the UI from hanging if the native side + // stalls. + val deadlineNs = System.nanoTime() + 500_000_000L + if (java.awt.EventQueue.isDispatchThread()) { + while (jobToJoin.isActive && System.nanoTime() < deadlineNs) { + try { Thread.sleep(10) } catch (_: InterruptedException) { break } + } + } else { + try { + kotlinx.coroutines.runBlocking { + kotlinx.coroutines.withTimeoutOrNull(500) { + jobToJoin.join() + } + } + } catch (_: Exception) { /* ignore */ } } - }.start() - } - - scope.cancel() - } - - private fun clearAllResourcesSync() { - // Clear the frame channel synchronously - while (frameChannel.tryReceive().isSuccess) { - // Drain the channel - } - - // Free bitmaps and frame buffers - bitmapLock.write { - _currentFrame = null - currentFrameState.value = null + } - // Don't close bitmaps — see comment in releaseAllResources(). - skiaBitmapA = null - skiaBitmapB = null - skiaBitmapWidth = 0 - skiaBitmapHeight = 0 - nextSkiaBitmapA = true - lastFrameHash = Int.MIN_VALUE + try { + player.SetPlaybackState(instance, false, true) + } catch (e: Exception) { + windowsLogger.e { "Exception stopping playback: ${e.message}" } + } + try { + player.CloseMedia(instance) + } catch (e: Exception) { + windowsLogger.e { "Exception closing media: ${e.message}" } + } + instanceVolumes.remove(instance) + try { + WindowsNativeBridge.destroyInstance(instance) + } catch (e: Exception) { + windowsLogger.e { "Exception destroying instance: ${e.message}" } + } } - // Reset all state - _currentTime = 0.0 - _duration = 0.0 - _progress = 0f - _metadata = VideoMetadata() - userPaused = false - isLoading = false - errorMessage = null - _error = null - - // Reset initialFrameRead flag to ensure we read an initial frame when reinitialized - initialFrameRead.set(false) + scope.cancel() } private fun releaseAllResources() { @@ -377,38 +411,33 @@ class WindowsVideoPlayerState : VideoPlayerState { videoJob?.cancel() resizeJob?.cancel() - // Drain the frame channel (tryReceive is non-suspending) clearFrameChannel() - // Free bitmaps and frame buffers + // Free bitmaps and frame buffers. + // Do NOT close the triple-buffer bitmaps here: the ImageBitmap exposed + // via currentFrameState shares the same native pixel memory + // (asComposeImageBitmap is zero-copy). Compose may still be rendering + // the last frame on the AWT-EventQueue thread. Closing now would free + // the native memory while Skia reads it, causing an access violation. + // Nullifying the references lets the Skia Managed cleaner release them + // once Compose (and any other holder) drops its reference. bitmapLock.write { _currentFrame = null currentFrameState.value = null - // Do NOT close the double-buffer bitmaps here: the ImageBitmap - // exposed via currentFrameState shares the same native pixel memory - // (asComposeImageBitmap is zero-copy). Compose may still be rendering - // the last frame on the AWT-EventQueue thread. Closing now would free - // the native memory while Skia reads it, causing an access violation. - // Nullifying the references lets the Skia Managed cleaner release them - // once Compose (and any other holder) drops its reference. - skiaBitmapA = null - skiaBitmapB = null + for (i in skiaBitmaps.indices) skiaBitmaps[i] = null skiaBitmapWidth = 0 skiaBitmapHeight = 0 - nextSkiaBitmapA = true + nextBitmapIndex = 0 lastFrameHash = Int.MIN_VALUE + pendingCloseBitmaps.clear() } - // Reset initialFrameRead flag to ensure we read an initial frame when reinitialized initialFrameRead.set(false) } private fun clearFrameChannel() { - // Drain the frame channel to ensure all items are removed - while (frameChannel.tryReceive().isSuccess) { - // Intentionally empty - just draining the channel - } + while (frameChannel.tryReceive().isSuccess) { /* drain */ } } /** @@ -610,11 +639,7 @@ class WindowsVideoPlayerState : VideoPlayerState { _isPlaying = startPlayback // Start video processing - videoJob = - scope.launch { - launch { produceFrames() } - launch { consumeFrames() } - } + videoJob = startVideoPipeline() } } catch (e: Exception) { setError("Error while opening media: ${e.message}") @@ -626,6 +651,15 @@ class WindowsVideoPlayerState : VideoPlayerState { } } + /** + * Launches the producer/consumer coroutine pair that reads frames from + * the native side and pushes them to Compose. + */ + private fun startVideoPipeline(): Job = scope.launch { + launch { produceFrames() } + launch { consumeFrames() } + } + /** * Zero-copy optimized frame producer using double-buffering and direct memory access. * @@ -686,124 +720,141 @@ class WindowsVideoPlayerState : VideoPlayerState { continue } - try { - val hrArr = IntArray(1) - val srcBuffer = player.ReadVideoFrame(instance, hrArr) + // Short-circuit while a seek is in progress — avoids contending + // on videoReaderMutex which the seek flow is holding. + if (isSeeking.get()) { + delay(5) + continue + } - if (hrArr[0] < 0 || srcBuffer == null) { - yield() - continue + val produced = try { + videoReaderMutex.withLock { + processOneFrame(instance) } - - // Re-query video size — HLS adaptive bitrate may change resolution - val sizeArr = IntArray(2) - player.GetVideoSize(instance, sizeArr) - if (sizeArr[0] > 0 && - sizeArr[1] > 0 && - (sizeArr[0] != videoWidth || sizeArr[1] != videoHeight) - ) { - videoWidth = sizeArr[0] - videoHeight = sizeArr[1] + } catch (e: CancellationException) { + break + } catch (e: Exception) { + if (scope.isActive && _hasMedia && !isDisposing.get()) { + setError("Error while reading a frame: ${e.message}") } + delay(100) + null + } - val width = videoWidth - val height = videoHeight - - if (width <= 0 || height <= 0) { - player.UnlockVideoFrame(instance) - yield() - continue + when (produced) { + ProduceOutcome.NotReady -> delay(2) + ProduceOutcome.SkipIteration -> yield() + is ProduceOutcome.Frame -> { + frameChannel.trySend(FrameData(produced.bitmap, produced.timestamp)) + delay(1) } + null -> { /* exception already handled */ } + } + } + } + + /** + * Outcome of a single frame-read pass, consumed by the produceFrames loop. + */ + private sealed interface ProduceOutcome { + data object NotReady : ProduceOutcome // native says "retry later" + data object SkipIteration : ProduceOutcome // frame dropped / duplicate + data class Frame(val bitmap: Bitmap, val timestamp: Double) : ProduceOutcome + } - srcBuffer.rewind() + /** + * Reads one frame from the native reader, copies it to the next Skia + * bitmap, and returns the outcome. Must be called under + * [videoReaderMutex] — this method calls ReadVideoFrame / UnlockVideoFrame. + */ + private fun processOneFrame(instance: Long): ProduceOutcome { + val hrArr = IntArray(1) + val srcBuffer = player.ReadVideoFrame(instance, hrArr) ?: return ProduceOutcome.NotReady + if (hrArr[0] < 0) return ProduceOutcome.NotReady + + // HLS adaptive bitrate may change the decoded size mid-stream. + val sizeArr = IntArray(2) + player.GetVideoSize(instance, sizeArr) + if (sizeArr[0] > 0 && sizeArr[1] > 0 && + (sizeArr[0] != videoWidth || sizeArr[1] != videoHeight) + ) { + videoWidth = sizeArr[0] + videoHeight = sizeArr[1] + } - val pixelCount = width * height + val width = videoWidth + val height = videoHeight + if (width <= 0 || height <= 0) { + player.UnlockVideoFrame(instance) + return ProduceOutcome.SkipIteration + } - // Calculate frame hash to detect identical frames - val newHash = calculateFrameHash(srcBuffer, pixelCount) - if (newHash == lastFrameHash) { - player.UnlockVideoFrame(instance) - yield() - continue - } - lastFrameHash = newHash - - // Reallocate bitmaps if dimensions changed - if (skiaBitmapA == null || skiaBitmapWidth != width || skiaBitmapHeight != height) { - bitmapLock.write { - skiaBitmapA?.close() - skiaBitmapB?.close() - - val imageInfo = createVideoImageInfo() - skiaBitmapA = Bitmap().apply { allocPixels(imageInfo) } - skiaBitmapB = Bitmap().apply { allocPixels(imageInfo) } - skiaBitmapWidth = width - skiaBitmapHeight = height - nextSkiaBitmapA = true + srcBuffer.rewind() + val pixelCount = width * height + val newHash = calculateFrameHash(srcBuffer, pixelCount) + if (newHash == lastFrameHash) { + player.UnlockVideoFrame(instance) + return ProduceOutcome.SkipIteration + } + lastFrameHash = newHash + + if (skiaBitmaps[0] == null || skiaBitmapWidth != width || skiaBitmapHeight != height) { + bitmapLock.write { + // Queue previous bitmaps for deferred close instead of leaking them + // to the Skia managed cleaner: closing now would race with Compose + // still drawing the last frame on the AWT thread. + for (i in skiaBitmaps.indices) { + skiaBitmaps[i]?.let { + pendingCloseBitmaps.addLast(PendingCloseBitmap(it, pendingCloseGraceFrames)) } + skiaBitmaps[i] = null } - - // Select the target bitmap (double-buffering) - val targetBitmap = if (nextSkiaBitmapA) skiaBitmapA!! else skiaBitmapB!! - nextSkiaBitmapA = !nextSkiaBitmapA - - // Get direct access to bitmap pixels via peekPixels (zero-copy access) - val pixmap = targetBitmap.peekPixels() - if (pixmap == null) { - player.UnlockVideoFrame(instance) - windowsLogger.e { "Failed to get pixmap from bitmap" } - yield() - continue + val imageInfo = createVideoImageInfo() + for (i in skiaBitmaps.indices) { + skiaBitmaps[i] = Bitmap().apply { allocPixels(imageInfo) } } + skiaBitmapWidth = width + skiaBitmapHeight = height + nextBitmapIndex = 0 + } + } - val pixelsAddr = pixmap.addr - if (pixelsAddr == 0L) { - player.UnlockVideoFrame(instance) - windowsLogger.e { "Invalid pixel address" } - yield() - continue - } + drainPendingCloseBitmaps() - // Single memory copy: native buffer → Skia bitmap - val dstRowBytes = pixmap.rowBytes - val dstSizeBytes = dstRowBytes.toLong() * height.toLong() - val dstBuffer = - WindowsNativeBridge.nWrapPointer(pixelsAddr, dstSizeBytes) - ?: run { - player.UnlockVideoFrame(instance) - yield() - continue - } + val targetBitmap = skiaBitmaps[nextBitmapIndex]!! + nextBitmapIndex = (nextBitmapIndex + 1) % skiaBitmaps.size - srcBuffer.rewind() - copyBgraFrame(srcBuffer, dstBuffer, width, height, dstRowBytes) + val pixmap = targetBitmap.peekPixels() + if (pixmap == null) { + player.UnlockVideoFrame(instance) + windowsLogger.e { "Failed to get pixmap from bitmap" } + return ProduceOutcome.SkipIteration + } + val pixelsAddr = pixmap.addr + if (pixelsAddr == 0L) { + player.UnlockVideoFrame(instance) + windowsLogger.e { "Invalid pixel address" } + return ProduceOutcome.SkipIteration + } - player.UnlockVideoFrame(instance) + val dstRowBytes = pixmap.rowBytes + val dstSizeBytes = dstRowBytes.toLong() * height.toLong() + val dstBuffer = WindowsNativeBridge.nWrapPointer(pixelsAddr, dstSizeBytes) + if (dstBuffer == null) { + player.UnlockVideoFrame(instance) + return ProduceOutcome.SkipIteration + } - // Get frame timestamp - val posArr = LongArray(1) - val frameTime = - if (player.GetMediaPosition(instance, posArr) >= 0) { - posArr[0] / 10000000.0 - } else { - 0.0 - } + srcBuffer.rewind() + copyBgraFrame(srcBuffer, dstBuffer, width, height, dstRowBytes) + player.UnlockVideoFrame(instance) - // Send frame to channel - frameChannel.trySend(FrameData(targetBitmap, frameTime)) + val posArr = LongArray(1) + val frameTime = + if (player.GetMediaPosition(instance, posArr) >= 0) posArr[0] / 10000000.0 + else 0.0 - // Native AcquireNextSample already paces video to the audio - // clock via PreciseSleepHighRes — no additional delay needed. - delay(1) - } catch (e: CancellationException) { - break - } catch (e: Exception) { - if (scope.isActive && _hasMedia && !isDisposing.get()) { - setError("Error while reading a frame: ${e.message}") - } - delay(100) - } - } + return ProduceOutcome.Frame(targetBitmap, frameTime) } /** @@ -842,12 +893,20 @@ class WindowsVideoPlayerState : VideoPlayerState { } _currentTime = frameData.timestamp - _progress = - if (_duration > 0.0) { - (_currentTime / _duration).toFloat().coerceIn(0f, 1f) - } else { - 0f // Live stream — no meaningful progress - } + // Don't clobber _progress while the user is dragging the + // slider: sliderPos is backed by _progress, and seekFinished() + // reads sliderPos to decide where to seek. Overwriting it with + // the current playback position would make the drag seek land + // wherever the video happened to be, not where the user + // released. + if (!_userDragging) { + _progress = + if (_duration > 0.0) { + (_currentTime / _duration).toFloat().coerceIn(0f, 1f) + } else { + 0f // Live stream — no meaningful progress + } + } isLoading = false delay(1) @@ -915,11 +974,7 @@ class WindowsVideoPlayerState : VideoPlayerState { } if (_hasMedia && (videoJob == null || videoJob?.isActive == false)) { - videoJob = - scope.launch { - launch { produceFrames() } - launch { consumeFrames() } - } + videoJob = startVideoPipeline() } } @@ -978,74 +1033,105 @@ class WindowsVideoPlayerState : VideoPlayerState { if (isDisposing.get()) return if (_duration <= 0.0) return // Live stream — seeking not supported - executeMediaOperation( - operation = "seek", - precondition = _hasMedia && videoPlayerInstance != 0L, - ) { - val instance = videoPlayerInstance - if (instance != 0L) { - try { - isLoading = true - // If the video was playing before seeking, we should reset userPaused - if (_isPlaying) { - userPaused = false - } + val clamped = value.coerceIn(0f, 1000f) + val targetPos = (_duration * (clamped / 1000f) * 10000000).toLong() - // Reset initialFrameRead flag to ensure we read a new frame after seeking - // This is especially important if the player is paused - initialFrameRead.set(false) + // Latch the newest target; whoever is running the seek loop will see it. + pendingSeekTarget.set(targetPos) - // Reset frame hash to ensure the first frame after seek is always processed - lastFrameHash = Int.MIN_VALUE + // Optimistic UI so the slider tracks the drag smoothly even while the + // native seek is still settling. + _progress = (clamped / 1000f).coerceIn(0f, 1f) + _currentTime = _duration * _progress - videoJob?.cancelAndJoin() - clearFrameChannel() + scheduleSeek() + } - val targetPos = (_duration * (value / 1000f) * 10000000).toLong() - var hr = player.SeekMedia(instance, targetPos) - if (hr < 0) { - delay(50) - hr = player.SeekMedia(instance, targetPos) - if (hr < 0) { - setError("Seek failed (hr=0x${hr.toString(16)})") - return@executeMediaOperation - } - } + /** + * Launches the seek loop if no other loop is currently draining the target. + * If multiple `seekTo` calls arrive in quick succession, only the latest + * target is actually processed — intermediate values are coalesced. + */ + private fun scheduleSeek() { + if (!seekInFlight.compareAndSet(false, true)) return - val posArr2 = LongArray(1) - if (player.GetMediaPosition(instance, posArr2) >= 0) { - _currentTime = posArr2[0] / 10000000.0 - _progress = - if (_duration > 0.0) { - (_currentTime / _duration).toFloat().coerceIn(0f, 1f) - } else { - 0f - } - } + scope.launch { + try { + while (true) { + val target = pendingSeekTarget.getAndSet(Long.MIN_VALUE) + if (target == Long.MIN_VALUE) break + performSeek(target) + } + } finally { + seekInFlight.set(false) + // Tiny race: a caller may have enqueued a target between our + // last getAndSet and releasing the claim. Re-check & re-launch. + if (pendingSeekTarget.get() != Long.MIN_VALUE) scheduleSeek() + } + } + } - if (!isDisposing.get()) { - videoJob = - scope.launch { - launch { produceFrames() } - launch { consumeFrames() } - } - } + /** + * Executes a single native seek. + * + * Strategy: keep the producer/consumer coroutines alive and instead + * serialize native reader access with [videoReaderMutex] + [isSeeking]. + * Cancelling & relaunching `videoJob` on every seek proved fragile + * under GraalVM native-image (the relaunched job sometimes never ran, + * leaving audio but no video). + */ + private suspend fun performSeek(targetPos: Long) { + val loadingTrigger = scope.launch { + delay(200) + if (!isDisposing.get()) isLoading = true + } + + try { + mediaOperationMutex.withLock { + if (isDisposing.get()) return@withLock + val instance = videoPlayerInstance + if (instance == 0L || !_hasMedia) return@withLock - delay(8) + isSeeking.set(true) + try { + videoReaderMutex.withLock { + // Inside the reader mutex: no concurrent ReadVideoFrame. + initialFrameRead.set(false) + lastFrameHash = Int.MIN_VALUE + clearFrameChannel() - // If the player is paused, ensure isLoading is set to false - // This prevents the UI from showing loading state indefinitely after seeking when paused - if (userPaused) { - isLoading = false + var hr = player.SeekMedia(instance, targetPos) + if (hr < 0) { + delay(30) + hr = player.SeekMedia(instance, targetPos) + } + if (hr < 0) { + setError("Seek failed (hr=0x${hr.toString(16)})") + return@withLock + } + + val posArr = LongArray(1) + if (player.GetMediaPosition(instance, posArr) >= 0) { + _currentTime = posArr[0] / 10000000.0 + _progress = + if (_duration > 0.0) + (_currentTime / _duration).toFloat().coerceIn(0f, 1f) + else 0f + } } - } catch (e: Exception) { - setError("Error during seek: ${e.message}") } finally { - // Ensure isLoading is always set to false, even if an exception occurs - // This is especially important when the player is paused - isLoading = false + isSeeking.set(false) + } + + // If the producer was never started (e.g. stop() was called + // before the first play), start it now so the new frame shows. + if (!isDisposing.get() && (videoJob == null || videoJob?.isActive == false)) { + videoJob = startVideoPipeline() } } + } finally { + loadingTrigger.cancel() + isLoading = false } } @@ -1129,6 +1215,25 @@ class WindowsVideoPlayerState : VideoPlayerState { */ private fun createVideoImageInfo() = ImageInfo(videoWidth, videoHeight, ColorType.BGRA_8888, ColorAlphaType.OPAQUE) + private fun drainPendingCloseBitmaps() { + if (pendingCloseBitmaps.isEmpty()) return + bitmapLock.write { + val iterator = pendingCloseBitmaps.iterator() + while (iterator.hasNext()) { + val entry = iterator.next() + entry.framesLeft -= 1 + if (entry.framesLeft <= 0) { + try { + entry.bitmap.close() + } catch (_: Throwable) { + // Ignore: bitmap may already be released by Skia cleaner. + } + iterator.remove() + } + } + } + } + /** * Sets the playback state (playing or paused) * @@ -1178,19 +1283,27 @@ class WindowsVideoPlayerState : VideoPlayerState { private suspend fun waitForPlaybackState(allowInitialFrame: Boolean = false): Boolean { if (_isPlaying) return true - // When paused, allow the producer to read exactly one frame for display + // When paused, allow the producer to read exactly one frame for display. if (userPaused && allowInitialFrame && !initialFrameRead.getAndSet(true)) { return true } if (isLoading) isLoading = false - try { - snapshotFlow { _isPlaying }.filter { it }.first() - } catch (e: CancellationException) { - throw e + // Polling wait — wakes up on either _isPlaying turning true OR + // initialFrameRead being reset (e.g. after a paused seek, where the + // producer must fetch & display the new frame without needing + // cancellation/restart of its coroutine). + while (scope.isActive && _hasMedia && !isDisposing.get()) { + if (_isPlaying) return true + if (userPaused && allowInitialFrame && !initialFrameRead.getAndSet(true)) return true + try { + delay(40) + } catch (e: CancellationException) { + throw e + } } - return _isPlaying + return false } /** Tracks how many consecutive iterations we've been waiting for resize */ diff --git a/mediaplayer/src/jvmMain/native/windows/AudioManager.cpp b/mediaplayer/src/jvmMain/native/windows/AudioManager.cpp index 94ebfb03..57179b03 100644 --- a/mediaplayer/src/jvmMain/native/windows/AudioManager.cpp +++ b/mediaplayer/src/jvmMain/native/windows/AudioManager.cpp @@ -1,12 +1,14 @@ -// AudioManager.cpp – WASAPI audio rendering with resampling for playback speed. -// ----------------------------------------------------------------------------- -// Audio is the timing master (like AVPlayer on macOS). The audio thread feeds -// decoded PCM to WASAPI as fast as the buffer allows — no wall-clock drift -// correction, no sleep, no sample dropping. Video compensates via audioLatencyMs. +// AudioManager.cpp — WASAPI audio rendering with linear interpolation for +// playback-speed changes. // -// This eliminates the class of stutter bugs caused by drift correction -// sleeping/dropping samples after seek, resume, or speed changes. -// ----------------------------------------------------------------------------- +// Audio is the timing master (like AVPlayer on macOS). The audio thread +// feeds decoded PCM to WASAPI as fast as the buffer allows; no wall-clock +// drift correction. Video compensates via audioLatencyMs. +// +// Suspension strategy: while paused or seeking, the thread waits on a +// manual-reset event (hAudioResumeEvent) — no busy loop, no CPU burn. +// MMCSS registration ("Pro Audio") boosts thread priority and reduces +// glitches under load. #include "AudioManager.h" #include "VideoPlayerInstance.h" @@ -16,6 +18,10 @@ #include #include #include +#include +#include + +using Microsoft::WRL::ComPtr; // WAVE_FORMAT_EXTENSIBLE sub-format GUIDs static const GUID kSubtypePCM = @@ -23,27 +29,23 @@ static const GUID kSubtypePCM = static const GUID kSubtypeIEEEFloat = {0x00000003, 0x0000, 0x0010, {0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71}}; -using namespace VideoPlayerUtils; - namespace AudioManager { +namespace { -// ---------------------- Helper constants ---------------------- constexpr REFERENCE_TIME kTargetBufferDuration100ns = 2'000'000; // 200 ms -// --------------------------------------------------------------------------- -static void ResolveFormatTag(const WAVEFORMATEX* fmt, WORD* outTag, WORD* outBps) { +void ResolveFormatTag(const WAVEFORMATEX* fmt, WORD* outTag, WORD* outBps) { *outTag = fmt->wFormatTag; *outBps = fmt->wBitsPerSample; if (*outTag == WAVE_FORMAT_EXTENSIBLE && fmt->cbSize >= 22) { auto* ext = reinterpret_cast(fmt); - if (ext->SubFormat == kSubtypePCM) *outTag = WAVE_FORMAT_PCM; - else if (ext->SubFormat == kSubtypeIEEEFloat) *outTag = WAVE_FORMAT_IEEE_FLOAT; + if (ext->SubFormat == kSubtypePCM) *outTag = WAVE_FORMAT_PCM; + else if (ext->SubFormat == kSubtypeIEEEFloat) *outTag = WAVE_FORMAT_IEEE_FLOAT; } } -// --------------------------------------------------------------------------- -static void ApplyVolume(BYTE* data, UINT32 frames, UINT32 blockAlign, - float vol, WORD formatTag, WORD bitsPerSample) { +void ApplyVolume(BYTE* data, UINT32 frames, UINT32 blockAlign, + float vol, WORD formatTag, WORD bitsPerSample) { if (vol >= 0.999f) return; if (formatTag == WAVE_FORMAT_PCM && bitsPerSample == 16) { @@ -69,135 +71,69 @@ static void ApplyVolume(BYTE* data, UINT32 frames, UINT32 blockAlign, } } -// ------------------------------------------------------------------------------------ -// InitWASAPI -// ------------------------------------------------------------------------------------ -HRESULT InitWASAPI(VideoPlayerInstance* inst, const WAVEFORMATEX* srcFmt) -{ - if (!inst) return E_INVALIDARG; - if (inst->pAudioClient && inst->pRenderClient) { - inst->bAudioInitialized = TRUE; - return S_OK; +// RAII helper for MMCSS Pro Audio registration. +class MmcssRegistration { +public: + MmcssRegistration() { + DWORD taskIndex = 0; + handle_ = AvSetMmThreadCharacteristicsW(L"Pro Audio", &taskIndex); } - - HRESULT hr = S_OK; - WAVEFORMATEX* deviceMixFmt = nullptr; - - IMMDeviceEnumerator* enumerator = MediaFoundation::GetDeviceEnumerator(); - if (!enumerator) return E_FAIL; - - hr = enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &inst->pDevice); - if (FAILED(hr)) goto fail; - - hr = inst->pDevice->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, - reinterpret_cast(&inst->pAudioClient)); - if (FAILED(hr)) goto fail; - - hr = inst->pDevice->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_ALL, nullptr, - reinterpret_cast(&inst->pAudioEndpointVolume)); - if (FAILED(hr)) goto fail; - - if (!srcFmt) { - hr = inst->pAudioClient->GetMixFormat(&deviceMixFmt); - if (FAILED(hr)) goto fail; - srcFmt = deviceMixFmt; - } - inst->pSourceAudioFormat = reinterpret_cast( - CoTaskMemAlloc(srcFmt->cbSize + sizeof(WAVEFORMATEX))); - if (!inst->pSourceAudioFormat) { hr = E_OUTOFMEMORY; goto fail; } - memcpy(inst->pSourceAudioFormat, srcFmt, srcFmt->cbSize + sizeof(WAVEFORMATEX)); - - if (!inst->hAudioSamplesReadyEvent) { - inst->hAudioSamplesReadyEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); - if (!inst->hAudioSamplesReadyEvent) { - hr = HRESULT_FROM_WIN32(GetLastError()); - goto fail; - } + ~MmcssRegistration() { + if (handle_) AvRevertMmThreadCharacteristics(handle_); } - - hr = inst->pAudioClient->Initialize(AUDCLNT_SHAREMODE_SHARED, - AUDCLNT_STREAMFLAGS_EVENTCALLBACK, - kTargetBufferDuration100ns, 0, srcFmt, nullptr); - if (FAILED(hr)) goto fail; - - hr = inst->pAudioClient->SetEventHandle(inst->hAudioSamplesReadyEvent); - if (FAILED(hr)) goto fail; - - hr = inst->pAudioClient->GetService(__uuidof(IAudioRenderClient), - reinterpret_cast(&inst->pRenderClient)); - if (FAILED(hr)) goto fail; - - inst->bAudioInitialized = TRUE; - if (deviceMixFmt) CoTaskMemFree(deviceMixFmt); - return S_OK; - -fail: - if (inst->pRenderClient) { inst->pRenderClient->Release(); inst->pRenderClient = nullptr; } - if (inst->pAudioClient) { inst->pAudioClient->Release(); inst->pAudioClient = nullptr; } - if (inst->pAudioEndpointVolume) { inst->pAudioEndpointVolume->Release(); inst->pAudioEndpointVolume = nullptr; } - if (inst->pDevice) { inst->pDevice->Release(); inst->pDevice = nullptr; } - if (inst->pSourceAudioFormat) { CoTaskMemFree(inst->pSourceAudioFormat); inst->pSourceAudioFormat = nullptr; } - if (inst->hAudioSamplesReadyEvent) { CloseHandle(inst->hAudioSamplesReadyEvent); inst->hAudioSamplesReadyEvent = nullptr; } - if (deviceMixFmt) CoTaskMemFree(deviceMixFmt); - inst->bAudioInitialized = FALSE; - return hr; -} - -// --------------------------------------------------------------------------- -// FeedSamplesToWASAPI — reads audio from MF and feeds to WASAPI render buffer. -// Used by both AudioThreadProc (main loop) and PreFillAudioBuffer (seek). -// Returns the number of output frames written, or -1 on EOF/error. -// --------------------------------------------------------------------------- -static int FeedOneSample(VideoPlayerInstance* inst, IMFSourceReader* audioReader, - UINT32 engineBufferFrames, UINT32 blockAlign, UINT32 channels, - WORD formatTag, WORD bitsPerSample, float speed) + MmcssRegistration(const MmcssRegistration&) = delete; + MmcssRegistration& operator=(const MmcssRegistration&) = delete; +private: + HANDLE handle_ = nullptr; +}; + +// Feeds a single MF audio sample into the WASAPI render buffer. Returns the +// number of output frames written, or -1 on EOF/fatal error. +int FeedOneSample(VideoPlayerInstance* inst, IMFSourceReader* audioReader, + UINT32 engineBufferFrames, UINT32 blockAlign, UINT32 channels, + WORD formatTag, WORD bitsPerSample, float speed) { - // How many frames can we write? UINT32 framesPadding = 0; - if (FAILED(inst->pAudioClient->GetCurrentPadding(&framesPadding))) - return -1; - UINT32 framesFree = engineBufferFrames - framesPadding; - if (framesFree == 0) return 0; // buffer full, try later + if (FAILED(inst->pAudioClient->GetCurrentPadding(&framesPadding))) return -1; + UINT32 framesFree = (framesPadding < engineBufferFrames) + ? engineBufferFrames - framesPadding : 0; + if (framesFree == 0) return 0; - // Update latency for video-side compensation - const UINT32 sampleRate = inst->pSourceAudioFormat ? inst->pSourceAudioFormat->nSamplesPerSec : 48000; + const UINT32 sampleRate = inst->pSourceAudioFormat + ? inst->pSourceAudioFormat->nSamplesPerSec : 48000; inst->audioLatencyMs.store( static_cast(framesPadding) * 1000.0 / sampleRate, std::memory_order_relaxed); - // Read one decoded audio sample - IMFSample* mfSample = nullptr; - DWORD flags = 0; - LONGLONG ts100n = 0; - HRESULT hr = audioReader->ReadSample( - MF_SOURCE_READER_FIRST_AUDIO_STREAM, - 0, nullptr, &flags, &ts100n, &mfSample); - if (FAILED(hr)) return -1; - if (!mfSample) return 0; // decoder starved - if (flags & MF_SOURCE_READERF_ENDOFSTREAM) { - mfSample->Release(); - return -1; + ComPtr mfSample; + DWORD flags = 0; + LONGLONG ts100n = 0; + { + // Hold csAudioFeed during ReadSample so SeekMedia's SetCurrentPosition + // on the same reader never interleaves with this call. + VideoPlayerUtils::ScopedLock lock(inst->csAudioFeed); + HRESULT hr = audioReader->ReadSample( + MF_SOURCE_READER_FIRST_AUDIO_STREAM, + 0, nullptr, &flags, &ts100n, mfSample.GetAddressOf()); + if (FAILED(hr)) return -1; } + if (!mfSample) return 0; + if (flags & MF_SOURCE_READERF_ENDOFSTREAM) return -1; + + // If a seek started while ReadSample was executing, the returned sample + // may be from the old position — drop it. + if (inst->bSeekInProgress.load(std::memory_order_acquire)) return 0; - // Update position from audio PTS (audio is the timing master) if (ts100n > 0) { - inst->llCurrentPosition = ts100n; + inst->llCurrentPosition.store(ts100n, std::memory_order_relaxed); } - // Lock sample buffer - IMFMediaBuffer* mediaBuf = nullptr; - if (FAILED(mfSample->ConvertToContiguousBuffer(&mediaBuf)) || !mediaBuf) { - mfSample->Release(); - return 0; - } + ComPtr mediaBuf; + if (FAILED(mfSample->ConvertToContiguousBuffer(mediaBuf.GetAddressOf()))) return 0; BYTE* srcData = nullptr; DWORD srcSize = 0, srcMax = 0; - if (FAILED(mediaBuf->Lock(&srcData, &srcMax, &srcSize))) { - mediaBuf->Release(); - mfSample->Release(); - return 0; - } + if (FAILED(mediaBuf->Lock(&srcData, &srcMax, &srcSize))) return 0; const UINT32 srcFrames = srcSize / blockAlign; const bool needsResample = std::abs(speed - 1.0f) >= 0.01f; @@ -207,34 +143,28 @@ static int FeedOneSample(VideoPlayerInstance* inst, IMFSourceReader* audioReader totalOutputFrames = static_cast(std::ceil(srcFrames / speed)); UINT32 outputDone = 0; - double fracPos = inst->resampleFracPos; - - while (outputDone < totalOutputFrames && inst->bAudioThreadRunning) { - // Abort if seek started - { - EnterCriticalSection(&inst->csClockSync); - bool seeking = inst->bSeekInProgress; - LeaveCriticalSection(&inst->csClockSync); - if (seeking) break; - } + const double fracPos = inst->resampleFracPos; + + while (outputDone < totalOutputFrames && inst->bAudioThreadRunning.load()) { + if (inst->bSeekInProgress.load(std::memory_order_acquire)) break; - UINT32 wantFrames = std::min(totalOutputFrames - outputDone, framesFree); + UINT32 wantFrames = (std::min)(totalOutputFrames - outputDone, framesFree); if (wantFrames == 0) { - // Buffer full — wait briefly for WASAPI to consume - WaitForSingleObject(inst->hAudioSamplesReadyEvent, 5); + // Render buffer full. Wait on hAudioSamplesReadyEvent for the + // driver to free room — short timeout keeps us responsive to + // seek/shutdown signals. + WaitForSingleObject(inst->hAudioSamplesReadyEvent.Get(), 5); if (FAILED(inst->pAudioClient->GetCurrentPadding(&framesPadding))) break; - framesFree = engineBufferFrames - framesPadding; + framesFree = (framesPadding < engineBufferFrames) + ? engineBufferFrames - framesPadding : 0; continue; } - EnterCriticalSection(&inst->csAudioFeed); + VideoPlayerUtils::ScopedLock lock(inst->csAudioFeed); BYTE* dstData = nullptr; - HRESULT hrBuf = inst->pRenderClient->GetBuffer(wantFrames, &dstData); - if (FAILED(hrBuf) || !dstData) { - LeaveCriticalSection(&inst->csAudioFeed); + if (FAILED(inst->pRenderClient->GetBuffer(wantFrames, &dstData)) || !dstData) break; - } if (needsResample) { double localFrac = fracPos + outputDone * static_cast(speed); @@ -246,7 +176,7 @@ static int FeedOneSample(VideoPlayerInstance* inst, IMFSourceReader* audioReader break; } UINT32 idx0 = static_cast(localFrac); - UINT32 idx1 = std::min(idx0 + 1, srcFrames - 1); + UINT32 idx1 = (std::min)(idx0 + 1, srcFrames - 1); float frac = static_cast(localFrac - idx0); if (formatTag == WAVE_FORMAT_IEEE_FLOAT && bitsPerSample == 32) { @@ -277,15 +207,14 @@ static int FeedOneSample(VideoPlayerInstance* inst, IMFSourceReader* audioReader ApplyVolume(dstData, wantFrames, blockAlign, vol, formatTag, bitsPerSample); inst->pRenderClient->ReleaseBuffer(wantFrames, 0); - LeaveCriticalSection(&inst->csAudioFeed); outputDone += wantFrames; if (FAILED(inst->pAudioClient->GetCurrentPadding(&framesPadding))) break; - framesFree = engineBufferFrames - framesPadding; + framesFree = (framesPadding < engineBufferFrames) + ? engineBufferFrames - framesPadding : 0; } - // Save fractional position for next sample if (needsResample) { double endPos = fracPos + outputDone * static_cast(speed); inst->resampleFracPos = endPos - srcFrames; @@ -295,76 +224,133 @@ static int FeedOneSample(VideoPlayerInstance* inst, IMFSourceReader* audioReader } mediaBuf->Unlock(); - mediaBuf->Release(); - mfSample->Release(); return static_cast(outputDone); } -// --------------------------------------------------------------------------- -// PreFillAudioBuffer — fills WASAPI buffer BEFORE Start() so there's no -// gap at the beginning of playback / after seek. -// --------------------------------------------------------------------------- -HRESULT PreFillAudioBuffer(VideoPlayerInstance* inst) -{ - if (!inst || !inst->pAudioClient || !inst->pRenderClient) - return E_INVALIDARG; +DWORD WINAPI AudioThreadProc(LPVOID lpParam) { + auto* inst = static_cast(lpParam); + if (!inst || !inst->pAudioClient || !inst->pRenderClient) return 0; + + MmcssRegistration mmcss; // boost priority while this thread lives IMFSourceReader* audioReader = inst->pSourceReaderAudio - ? inst->pSourceReaderAudio - : inst->pSourceReader; - if (!audioReader) return E_FAIL; + ? inst->pSourceReaderAudio.Get() + : inst->pSourceReader.Get(); + if (!audioReader) return 0; UINT32 engineBufferFrames = 0; - if (FAILED(inst->pAudioClient->GetBufferSize(&engineBufferFrames))) - return E_FAIL; + if (FAILED(inst->pAudioClient->GetBufferSize(&engineBufferFrames))) return 0; - const UINT32 blockAlign = inst->pSourceAudioFormat ? inst->pSourceAudioFormat->nBlockAlign : 4; - const UINT32 channels = inst->pSourceAudioFormat ? inst->pSourceAudioFormat->nChannels : 2; + // Wait for the resume event before feeding (handles opened-in-paused case). + WaitForSingleObject(inst->hAudioResumeEvent.Get(), INFINITE); + + const UINT32 blockAlign = inst->pSourceAudioFormat + ? inst->pSourceAudioFormat->nBlockAlign : 4; + const UINT32 channels = inst->pSourceAudioFormat + ? inst->pSourceAudioFormat->nChannels : 2; WORD formatTag = WAVE_FORMAT_PCM, bitsPerSample = 16; if (inst->pSourceAudioFormat) ResolveFormatTag(inst->pSourceAudioFormat, &formatTag, &bitsPerSample); - float speed = inst->playbackSpeed.load(std::memory_order_relaxed); inst->resampleFracPos = 0.0; - // Fill until the buffer is at least half full - UINT32 targetFrames = engineBufferFrames / 2; - UINT32 totalFed = 0; - for (int attempts = 0; attempts < 20 && totalFed < targetFrames; ++attempts) { - int fed = FeedOneSample(inst, audioReader, engineBufferFrames, - blockAlign, channels, formatTag, bitsPerSample, speed); - if (fed < 0) break; // EOF or error - if (fed == 0) continue; - totalFed += fed; + while (inst->bAudioThreadRunning.load()) { + // Block efficiently while paused/seeking — no CPU burn. + WaitForSingleObject(inst->hAudioResumeEvent.Get(), INFINITE); + if (!inst->bAudioThreadRunning.load()) break; + + // Wait for the audio engine to signal buffer availability. + WaitForSingleObject(inst->hAudioSamplesReadyEvent.Get(), 10); + + if (inst->bSeekInProgress.load(std::memory_order_acquire)) continue; + + const float speed = inst->playbackSpeed.load(std::memory_order_relaxed); + int result = FeedOneSample(inst, audioReader, engineBufferFrames, + blockAlign, channels, formatTag, bitsPerSample, speed); + if (result < 0) break; // EOF or fatal error } + { + VideoPlayerUtils::ScopedLock lock(inst->csAudioFeed); + inst->pAudioClient->Stop(); + } + inst->audioLatencyMs.store(0.0, std::memory_order_relaxed); + return 0; +} + +} // namespace + +HRESULT InitWASAPI(VideoPlayerInstance* inst, const WAVEFORMATEX* srcFmt) { + if (!inst || !srcFmt) return E_INVALIDARG; + if (inst->pAudioClient && inst->pRenderClient) { + inst->bAudioInitialized = true; + return S_OK; + } + + IMMDeviceEnumerator* enumerator = MediaFoundation::GetDeviceEnumerator(); + if (!enumerator) return E_FAIL; + + // RAII cleanup: released by name on the single success return. + struct CleanupGuard { + VideoPlayerInstance* inst; + bool armed = true; + ~CleanupGuard() { + if (!armed) return; + inst->pRenderClient.Reset(); + inst->pAudioClient.Reset(); + inst->pAudioEndpointVolume.Reset(); + inst->pDevice.Reset(); + inst->hAudioSamplesReadyEvent.Reset(); + inst->bAudioInitialized = false; + } + } guard{inst}; + + HRESULT hr = enumerator->GetDefaultAudioEndpoint( + eRender, eConsole, inst->pDevice.ReleaseAndGetAddressOf()); + if (FAILED(hr)) return hr; + + hr = inst->pDevice->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, + reinterpret_cast(inst->pAudioClient.ReleaseAndGetAddressOf())); + if (FAILED(hr)) return hr; + + hr = inst->pDevice->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_ALL, nullptr, + reinterpret_cast(inst->pAudioEndpointVolume.ReleaseAndGetAddressOf())); + if (FAILED(hr)) return hr; + + if (!inst->hAudioSamplesReadyEvent) { + inst->hAudioSamplesReadyEvent.Reset(CreateEventW(nullptr, FALSE, FALSE, nullptr)); + if (!inst->hAudioSamplesReadyEvent) return HRESULT_FROM_WIN32(GetLastError()); + } + + hr = inst->pAudioClient->Initialize(AUDCLNT_SHAREMODE_SHARED, + AUDCLNT_STREAMFLAGS_EVENTCALLBACK, + kTargetBufferDuration100ns, 0, + srcFmt, nullptr); + if (FAILED(hr)) return hr; + + hr = inst->pAudioClient->SetEventHandle(inst->hAudioSamplesReadyEvent.Get()); + if (FAILED(hr)) return hr; + + hr = inst->pAudioClient->GetService(__uuidof(IAudioRenderClient), + reinterpret_cast(inst->pRenderClient.ReleaseAndGetAddressOf())); + if (FAILED(hr)) return hr; + + inst->bAudioInitialized = true; + guard.armed = false; return S_OK; } -// --------------------------------------------------------------------------- -// AudioThreadProc — simple feed loop, no drift correction. -// Audio is the timing master: it feeds WASAPI as fast as the buffer allows. -// WASAPI's hardware clock determines the actual playback rate. -// Video compensates via audioLatencyMs. -// --------------------------------------------------------------------------- -DWORD WINAPI AudioThreadProc(LPVOID lpParam) -{ - auto* inst = static_cast(lpParam); - if (!inst || !inst->pAudioClient || !inst->pRenderClient) - return 0; +HRESULT PreFillAudioBuffer(VideoPlayerInstance* inst) { + if (!inst || !inst->pAudioClient || !inst->pRenderClient) return E_INVALIDARG; IMFSourceReader* audioReader = inst->pSourceReaderAudio - ? inst->pSourceReaderAudio - : inst->pSourceReader; - if (!audioReader) return 0; + ? inst->pSourceReaderAudio.Get() + : inst->pSourceReader.Get(); + if (!audioReader) return E_FAIL; UINT32 engineBufferFrames = 0; - if (FAILED(inst->pAudioClient->GetBufferSize(&engineBufferFrames))) - return 0; - - if (inst->hAudioReadyEvent) - WaitForSingleObject(inst->hAudioReadyEvent, INFINITE); + if (FAILED(inst->pAudioClient->GetBufferSize(&engineBufferFrames))) return E_FAIL; const UINT32 blockAlign = inst->pSourceAudioFormat ? inst->pSourceAudioFormat->nBlockAlign : 4; const UINT32 channels = inst->pSourceAudioFormat ? inst->pSourceAudioFormat->nChannels : 2; @@ -373,98 +359,83 @@ DWORD WINAPI AudioThreadProc(LPVOID lpParam) if (inst->pSourceAudioFormat) ResolveFormatTag(inst->pSourceAudioFormat, &formatTag, &bitsPerSample); + const float speed = inst->playbackSpeed.load(std::memory_order_relaxed); inst->resampleFracPos = 0.0; - while (inst->bAudioThreadRunning) { - // Wait for WASAPI to signal buffer space (or 10ms timeout) - WaitForSingleObject(inst->hAudioSamplesReadyEvent, 10); - - // Pause / seek: spin until resumed - { - EnterCriticalSection(&inst->csClockSync); - bool suspended = inst->bSeekInProgress || inst->llPauseStart != 0; - LeaveCriticalSection(&inst->csClockSync); - if (suspended) { - PreciseSleepHighRes(5); - continue; - } - } - - float speed = inst->playbackSpeed.load(std::memory_order_relaxed); - int result = FeedOneSample(inst, audioReader, engineBufferFrames, - blockAlign, channels, formatTag, bitsPerSample, speed); - if (result < 0) break; // EOF or fatal error + const UINT32 targetFrames = engineBufferFrames / 2; + UINT32 totalFed = 0; + for (int attempts = 0; attempts < 20 && totalFed < targetFrames; ++attempts) { + int fed = FeedOneSample(inst, audioReader, engineBufferFrames, + blockAlign, channels, formatTag, bitsPerSample, speed); + if (fed < 0) break; + if (fed == 0) continue; + totalFed += fed; } - - EnterCriticalSection(&inst->csAudioFeed); - inst->pAudioClient->Stop(); - LeaveCriticalSection(&inst->csAudioFeed); - inst->audioLatencyMs.store(0.0, std::memory_order_relaxed); - return 0; + return S_OK; } -// ------------------------------------------------------------- -// Thread management -// ------------------------------------------------------------- -HRESULT StartAudioThread(VideoPlayerInstance* inst) -{ - if (!inst || !inst->bHasAudio || !inst->bAudioInitialized) - return E_INVALIDARG; +HRESULT StartAudioThread(VideoPlayerInstance* inst) { + if (!inst || !inst->bHasAudio || !inst->bAudioInitialized) return E_INVALIDARG; if (inst->hAudioThread) { - WaitForSingleObject(inst->hAudioThread, 5000); - CloseHandle(inst->hAudioThread); - inst->hAudioThread = nullptr; + WaitForSingleObject(inst->hAudioThread.Get(), 5000); + inst->hAudioThread.Reset(); } - inst->bAudioThreadRunning = TRUE; - inst->hAudioThread = CreateThread(nullptr, 0, AudioThreadProc, inst, 0, nullptr); - if (!inst->hAudioThread) { - inst->bAudioThreadRunning = FALSE; - return HRESULT_FROM_WIN32(GetLastError()); + if (!inst->hAudioResumeEvent) { + inst->hAudioResumeEvent.Reset(CreateEventW(nullptr, TRUE, TRUE, nullptr)); // manual-reset, initially signaled + } else { + SetEvent(inst->hAudioResumeEvent.Get()); } - if (inst->hAudioReadyEvent) SetEvent(inst->hAudioReadyEvent); + inst->bAudioThreadRunning.store(true); + HANDLE h = CreateThread(nullptr, 0, AudioThreadProc, inst, 0, nullptr); + if (!h) { + inst->bAudioThreadRunning.store(false); + return HRESULT_FROM_WIN32(GetLastError()); + } + inst->hAudioThread.Reset(h); return S_OK; } -void StopAudioThread(VideoPlayerInstance* inst) -{ +void StopAudioThread(VideoPlayerInstance* inst) { if (!inst) return; - inst->bAudioThreadRunning = FALSE; - if (inst->hAudioReadyEvent) SetEvent(inst->hAudioReadyEvent); - if (inst->hAudioSamplesReadyEvent) SetEvent(inst->hAudioSamplesReadyEvent); + inst->bAudioThreadRunning.store(false); + if (inst->hAudioResumeEvent) SetEvent(inst->hAudioResumeEvent.Get()); + if (inst->hAudioSamplesReadyEvent) SetEvent(inst->hAudioSamplesReadyEvent.Get()); if (inst->hAudioThread) { - WaitForSingleObject(inst->hAudioThread, 5000); - CloseHandle(inst->hAudioThread); - inst->hAudioThread = nullptr; + WaitForSingleObject(inst->hAudioThread.Get(), 5000); + inst->hAudioThread.Reset(); } if (inst->pAudioClient) { - EnterCriticalSection(&inst->csAudioFeed); + VideoPlayerUtils::ScopedLock lock(inst->csAudioFeed); inst->pAudioClient->Stop(); - LeaveCriticalSection(&inst->csAudioFeed); } inst->audioLatencyMs.store(0.0, std::memory_order_relaxed); } -// ----------------------------------------- -// Volume helpers -// ----------------------------------------- -HRESULT SetVolume(VideoPlayerInstance* inst, float vol) -{ +HRESULT SetVolume(VideoPlayerInstance* inst, float vol) { if (!inst) return E_INVALIDARG; inst->instanceVolume.store(std::clamp(vol, 0.0f, 1.0f), std::memory_order_relaxed); return S_OK; } -HRESULT GetVolume(const VideoPlayerInstance* inst, float* out) -{ +HRESULT GetVolume(const VideoPlayerInstance* inst, float* out) { if (!inst || !out) return E_INVALIDARG; *out = inst->instanceVolume.load(std::memory_order_relaxed); return S_OK; } +void SignalResume(VideoPlayerInstance* inst) { + if (inst && inst->hAudioResumeEvent) SetEvent(inst->hAudioResumeEvent.Get()); + if (inst && inst->hAudioSamplesReadyEvent) SetEvent(inst->hAudioSamplesReadyEvent.Get()); +} + +void SignalPause(VideoPlayerInstance* inst) { + if (inst && inst->hAudioResumeEvent) ResetEvent(inst->hAudioResumeEvent.Get()); +} + } // namespace AudioManager diff --git a/mediaplayer/src/jvmMain/native/windows/AudioManager.h b/mediaplayer/src/jvmMain/native/windows/AudioManager.h index d01900e7..f1dfe9bd 100644 --- a/mediaplayer/src/jvmMain/native/windows/AudioManager.h +++ b/mediaplayer/src/jvmMain/native/windows/AudioManager.h @@ -1,68 +1,30 @@ #pragma once +#include "ErrorCodes.h" #include #include #include #include #include -// Error code definitions -#define OP_E_NOT_INITIALIZED ((HRESULT)0x80000001L) -#define OP_E_ALREADY_INITIALIZED ((HRESULT)0x80000002L) -#define OP_E_INVALID_PARAMETER ((HRESULT)0x80000003L) - -// Forward declarations struct VideoPlayerInstance; namespace AudioManager { -/** - * @brief Initializes WASAPI for audio playback. - * @param pInstance Pointer to the video player instance. - * @param pSourceFormat Optional source audio format. - * @return S_OK on success, or an error code. - */ -HRESULT InitWASAPI(VideoPlayerInstance* pInstance, const WAVEFORMATEX* pSourceFormat = nullptr); - -/** - * @brief Audio processing thread procedure. - * @param lpParam Pointer to the video player instance. - * @return Thread exit code. - */ -DWORD WINAPI AudioThreadProc(LPVOID lpParam); - -/** - * @brief Starts the audio thread for a video player instance. - * @param pInstance Pointer to the video player instance. - * @return S_OK on success, or an error code. - */ -/** - * @brief Pre-fills the WASAPI buffer before Start() to avoid gaps after seek. - */ +// InitWASAPI does NOT take ownership of pSourceFormat. The caller is +// responsible for freeing it (or transferring ownership to the instance +// via VideoPlayerInstance::pSourceAudioFormat) after the call returns. +HRESULT InitWASAPI(VideoPlayerInstance* pInstance, const WAVEFORMATEX* pSourceFormat); HRESULT PreFillAudioBuffer(VideoPlayerInstance* pInstance); - HRESULT StartAudioThread(VideoPlayerInstance* pInstance); +void StopAudioThread(VideoPlayerInstance* pInstance); -/** - * @brief Stops the audio thread for a video player instance. - * @param pInstance Pointer to the video player instance. - */ -void StopAudioThread(VideoPlayerInstance* pInstance); - -/** - * @brief Sets the audio volume for a video player instance. - * @param pInstance Pointer to the video player instance. - * @param volume Volume level (0.0 to 1.0). - * @return S_OK on success, or an error code. - */ HRESULT SetVolume(VideoPlayerInstance* pInstance, float volume); - -/** - * @brief Gets the audio volume for a video player instance. - * @param pInstance Pointer to the video player instance. - * @param volume Pointer to receive the volume level. - * @return S_OK on success, or an error code. - */ HRESULT GetVolume(const VideoPlayerInstance* pInstance, float* volume); +// Called by the video player when playback is resumed/paused so the audio +// thread can block efficiently instead of busy-waiting. +void SignalResume(VideoPlayerInstance* pInstance); +void SignalPause(VideoPlayerInstance* pInstance); + } // namespace AudioManager diff --git a/mediaplayer/src/jvmMain/native/windows/CMakeLists.txt b/mediaplayer/src/jvmMain/native/windows/CMakeLists.txt index e2ff0172..a063fd58 100644 --- a/mediaplayer/src/jvmMain/native/windows/CMakeLists.txt +++ b/mediaplayer/src/jvmMain/native/windows/CMakeLists.txt @@ -2,11 +2,11 @@ cmake_minimum_required(VERSION 3.15) project(NativeVideoPlayer LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) -# Find JNI find_package(JNI REQUIRED) -# Check target architecture if(DEFINED ENV{NATIVE_LIBS_OUTPUT_DIR}) set(BASE_OUTPUT_DIR "$ENV{NATIVE_LIBS_OUTPUT_DIR}") else() @@ -16,23 +16,21 @@ endif() if(CMAKE_GENERATOR_PLATFORM STREQUAL "x64" OR CMAKE_GENERATOR_PLATFORM STREQUAL "") set(TARGET_ARCH "x64") set(OUTPUT_DIR "${BASE_OUTPUT_DIR}/win32-x86-64") - add_compile_options("/arch:AVX2") elseif(CMAKE_GENERATOR_PLATFORM STREQUAL "ARM64") set(TARGET_ARCH "ARM64") set(OUTPUT_DIR "${BASE_OUTPUT_DIR}/win32-arm64") - add_compile_options("/arch:arm64") else() message(FATAL_ERROR "Unsupported architecture: ${CMAKE_GENERATOR_PLATFORM}") endif() -# Ensure output directory exists file(MAKE_DIRECTORY ${OUTPUT_DIR}) -# Define the target add_library(NativeVideoPlayer SHARED NativeVideoPlayer.cpp NativeVideoPlayer.h VideoPlayerInstance.h + ErrorCodes.h + ComHelpers.h Utils.cpp Utils.h MediaFoundationManager.cpp @@ -44,17 +42,40 @@ add_library(NativeVideoPlayer SHARED jni_bridge.cpp ) -# JNI include directories target_include_directories(NativeVideoPlayer PRIVATE ${JNI_INCLUDE_DIRS}) -# Compilation definitions target_compile_definitions(NativeVideoPlayer PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX NATIVEVIDEOPLAYER_EXPORTS + _CRT_SECURE_NO_WARNINGS ) -# Linked libraries +# Baseline is SSE2 (guaranteed on x64). AVX2 codepaths are runtime-detected +# via __cpuid in ForceAlphaOpaque — MSVC allows AVX2 intrinsics without +# /arch:AVX2, so we keep the binary runnable on older CPUs. +if(MSVC) + target_compile_options(NativeVideoPlayer PRIVATE + /W4 + /permissive- + /Zc:__cplusplus + /Zc:preprocessor + /EHsc + /MP # parallel compilation + /wd4245 # MF_SOURCE_READER_* macros are unsigned-cast-from-signed + /wd4505 # unreferenced inline helpers + ) + # Release-only: disable RTTI & enable whole-program optimization. + target_compile_options(NativeVideoPlayer PRIVATE + $<$:/GR-> + $<$:/GL> + $<$:/Oi> + ) + target_link_options(NativeVideoPlayer PRIVATE + $<$:/LTCG> + ) +endif() + target_link_libraries(NativeVideoPlayer PRIVATE mf mfplat @@ -71,7 +92,6 @@ target_link_libraries(NativeVideoPlayer PRIVATE evr ) -# Configure output directory set_target_properties(NativeVideoPlayer PROPERTIES OUTPUT_NAME "NativeVideoPlayer" LIBRARY_OUTPUT_DIRECTORY "${OUTPUT_DIR}" @@ -82,6 +102,5 @@ set_target_properties(NativeVideoPlayer PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE "${OUTPUT_DIR}" ) -# Display target architecture and output directory message(STATUS "Target architecture: ${TARGET_ARCH}") message(STATUS "Output directory: ${OUTPUT_DIR}") diff --git a/mediaplayer/src/jvmMain/native/windows/ComHelpers.h b/mediaplayer/src/jvmMain/native/windows/ComHelpers.h new file mode 100644 index 00000000..0154ce00 --- /dev/null +++ b/mediaplayer/src/jvmMain/native/windows/ComHelpers.h @@ -0,0 +1,76 @@ +#pragma once + +// Small RAII helpers around Win32 primitives used throughout the native +// player. Kept header-only to stay dependency-free. + +#include +#include +#include + +namespace VideoPlayerUtils { + +// RAII wrapper for CRITICAL_SECTION with spin count tuned for tight audio/video +// feed loops (reduces context switches under contention). +class CriticalSection { +public: + CriticalSection() { + InitializeCriticalSectionAndSpinCount(&cs_, 4000); + } + ~CriticalSection() { DeleteCriticalSection(&cs_); } + + CriticalSection(const CriticalSection&) = delete; + CriticalSection& operator=(const CriticalSection&) = delete; + + void Enter() { EnterCriticalSection(&cs_); } + void Leave() { LeaveCriticalSection(&cs_); } + + CRITICAL_SECTION* Raw() { return &cs_; } + +private: + CRITICAL_SECTION cs_{}; +}; + +class ScopedLock { +public: + explicit ScopedLock(CriticalSection& cs) : cs_(&cs) { cs_->Enter(); } + ~ScopedLock() { cs_->Leave(); } + ScopedLock(const ScopedLock&) = delete; + ScopedLock& operator=(const ScopedLock&) = delete; +private: + CriticalSection* cs_; +}; + +// RAII wrapper for Win32 HANDLEs representing events/timers. +class UniqueHandle { +public: + UniqueHandle() = default; + explicit UniqueHandle(HANDLE h) : h_(h) {} + ~UniqueHandle() { Reset(); } + + UniqueHandle(const UniqueHandle&) = delete; + UniqueHandle& operator=(const UniqueHandle&) = delete; + + UniqueHandle(UniqueHandle&& other) noexcept : h_(other.h_) { other.h_ = nullptr; } + UniqueHandle& operator=(UniqueHandle&& other) noexcept { + if (this != &other) { + Reset(); + h_ = other.h_; + other.h_ = nullptr; + } + return *this; + } + + void Reset(HANDLE h = nullptr) { + if (h_ && h_ != INVALID_HANDLE_VALUE) CloseHandle(h_); + h_ = h; + } + + HANDLE Get() const { return h_; } + HANDLE Release() { HANDLE h = h_; h_ = nullptr; return h; } + explicit operator bool() const { return h_ != nullptr && h_ != INVALID_HANDLE_VALUE; } + +private: + HANDLE h_ = nullptr; +}; + +} // namespace VideoPlayerUtils diff --git a/mediaplayer/src/jvmMain/native/windows/ErrorCodes.h b/mediaplayer/src/jvmMain/native/windows/ErrorCodes.h new file mode 100644 index 00000000..d8705fa9 --- /dev/null +++ b/mediaplayer/src/jvmMain/native/windows/ErrorCodes.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +// Custom error codes (single source of truth). +#define OP_E_NOT_INITIALIZED ((HRESULT)0x80000001L) +#define OP_E_ALREADY_INITIALIZED ((HRESULT)0x80000002L) +#define OP_E_INVALID_PARAMETER ((HRESULT)0x80000003L) diff --git a/mediaplayer/src/jvmMain/native/windows/HLSPlayer.cpp b/mediaplayer/src/jvmMain/native/windows/HLSPlayer.cpp index 40e72992..26b84ca1 100644 --- a/mediaplayer/src/jvmMain/native/windows/HLSPlayer.cpp +++ b/mediaplayer/src/jvmMain/native/windows/HLSPlayer.cpp @@ -1,12 +1,13 @@ -// HLSPlayer.cpp — IMFMediaEngine-based HLS streaming player +// HLSPlayer.cpp — IMFMediaEngine-based HLS streaming player. // -// On Windows 10+, IMFMediaEngine supports HLS natively (unlike IMFSourceReader -// which only handles HLS in UWP/Edge contexts). This file wraps the engine to -// provide frame-by-frame access compatible with the existing player API. +// On Windows 10+, IMFMediaEngine supports HLS natively. This file wraps the +// engine to provide frame-by-frame access compatible with the existing +// player API. #include "HLSPlayer.h" #include #include +#include #ifdef _DEBUG #define HLS_LOG(msg, ...) fprintf(stderr, "[HLS] " msg "\n", ##__VA_ARGS__) @@ -14,19 +15,14 @@ #define HLS_LOG(msg, ...) ((void)0) #endif -static const DXGI_FORMAT kTextureFormat = DXGI_FORMAT_B8G8R8A8_UNORM; +using Microsoft::WRL::ComPtr; -// ============================================================================ -// IUnknown -// ============================================================================ +static const DXGI_FORMAT kTextureFormat = DXGI_FORMAT_B8G8R8A8_UNORM; -HLSPlayer::HLSPlayer() { - InitializeCriticalSection(&m_cs); -} +HLSPlayer::HLSPlayer() = default; HLSPlayer::~HLSPlayer() { Close(); - DeleteCriticalSection(&m_cs); } STDMETHODIMP HLSPlayer::QueryInterface(REFIID riid, void** ppv) { @@ -40,108 +36,80 @@ STDMETHODIMP HLSPlayer::QueryInterface(REFIID riid, void** ppv) { return E_NOINTERFACE; } -STDMETHODIMP_(ULONG) HLSPlayer::AddRef() { return InterlockedIncrement(&m_refCount); } +STDMETHODIMP_(ULONG) HLSPlayer::AddRef() { return InterlockedIncrement(&m_refCount); } STDMETHODIMP_(ULONG) HLSPlayer::Release() { LONG c = InterlockedDecrement(&m_refCount); if (c == 0) delete this; return c; } -// ============================================================================ -// IMFMediaEngineNotify — called on an internal MF thread -// ============================================================================ - STDMETHODIMP HLSPlayer::EventNotify(DWORD event, DWORD_PTR param1, DWORD param2) { + (void)param1; + (void)param2; switch (event) { case MF_MEDIA_ENGINE_EVENT_LOADEDMETADATA: case MF_MEDIA_ENGINE_EVENT_FORMATCHANGE: { - // Resolution may have changed (HLS adaptive bitrate) if (m_pEngine) { DWORD w = 0, h = 0; m_pEngine->GetNativeVideoSize(&w, &h); if (w > 0 && h > 0) { - EnterCriticalSection(&m_cs); + VideoPlayerUtils::ScopedLock lock(m_cs); m_nativeWidth = w; m_nativeHeight = h; - LeaveCriticalSection(&m_cs); HLS_LOG("Video size: %ux%u", w, h); } } break; } - case MF_MEDIA_ENGINE_EVENT_CANPLAY: case MF_MEDIA_ENGINE_EVENT_CANPLAYTHROUGH: m_bReady.store(true); - if (m_hReadyEvent) SetEvent(m_hReadyEvent); + if (m_hReadyEvent) SetEvent(m_hReadyEvent.Get()); break; - case MF_MEDIA_ENGINE_EVENT_ENDED: m_bEOF.store(true); - HLS_LOG("End of stream"); break; - case MF_MEDIA_ENGINE_EVENT_ERROR: m_bError.store(true); HLS_LOG("Error: param1=%llu param2=%u", (unsigned long long)param1, param2); - if (m_hReadyEvent) SetEvent(m_hReadyEvent); // unblock Open + if (m_hReadyEvent) SetEvent(m_hReadyEvent.Get()); break; - default: break; } return S_OK; } -// ============================================================================ -// Lifecycle -// ============================================================================ - HRESULT HLSPlayer::Initialize(ID3D11Device* pDevice, IMFDXGIDeviceManager* pDXGIManager) { if (!pDevice || !pDXGIManager) return E_INVALIDARG; m_pDevice = pDevice; - m_pDevice->GetImmediateContext(&m_pContext); + m_pDevice->GetImmediateContext(m_pContext.ReleaseAndGetAddressOf()); - // Create the Media Engine via class factory - IMFMediaEngineClassFactory* pFactory = nullptr; + ComPtr factory; HRESULT hr = CoCreateInstance(CLSID_MFMediaEngineClassFactory, nullptr, - CLSCTX_ALL, IID_PPV_ARGS(&pFactory)); - if (FAILED(hr)) { - HLS_LOG("CoCreateInstance MFMediaEngineClassFactory failed: 0x%08x", (unsigned)hr); - return hr; - } + CLSCTX_ALL, IID_PPV_ARGS(factory.GetAddressOf())); + if (FAILED(hr)) { HLS_LOG("CoCreateInstance failed: 0x%08x", (unsigned)hr); return hr; } - IMFAttributes* pAttrs = nullptr; - hr = MFCreateAttributes(&pAttrs, 3); - if (FAILED(hr)) { pFactory->Release(); return hr; } + ComPtr attrs; + hr = MFCreateAttributes(attrs.GetAddressOf(), 3); + if (FAILED(hr)) return hr; - pAttrs->SetUnknown(MF_MEDIA_ENGINE_CALLBACK, - static_cast(this)); - pAttrs->SetUnknown(MF_MEDIA_ENGINE_DXGI_MANAGER, pDXGIManager); - pAttrs->SetUINT32(MF_MEDIA_ENGINE_VIDEO_OUTPUT_FORMAT, kTextureFormat); + attrs->SetUnknown(MF_MEDIA_ENGINE_CALLBACK, static_cast(this)); + attrs->SetUnknown(MF_MEDIA_ENGINE_DXGI_MANAGER, pDXGIManager); + attrs->SetUINT32(MF_MEDIA_ENGINE_VIDEO_OUTPUT_FORMAT, kTextureFormat); - IMFMediaEngine* pEngine = nullptr; - hr = pFactory->CreateInstance(0, pAttrs, &pEngine); - pAttrs->Release(); - pFactory->Release(); - if (FAILED(hr)) { - HLS_LOG("CreateInstance failed: 0x%08x", (unsigned)hr); - return hr; - } + ComPtr engine; + hr = factory->CreateInstance(0, attrs.Get(), engine.GetAddressOf()); + if (FAILED(hr)) { HLS_LOG("CreateInstance failed: 0x%08x", (unsigned)hr); return hr; } - // QI for IMFMediaEngineEx (needed for SetCurrentTime seek) - hr = pEngine->QueryInterface(IID_PPV_ARGS(&m_pEngine)); - pEngine->Release(); - if (FAILED(hr)) { - HLS_LOG("QI for IMFMediaEngineEx failed: 0x%08x", (unsigned)hr); - return hr; - } + hr = engine.As(&m_pEngine); + if (FAILED(hr)) { HLS_LOG("QI IMFMediaEngineEx failed: 0x%08x", (unsigned)hr); return hr; } return S_OK; } -HRESULT HLSPlayer::Open(const wchar_t* url) { +HRESULT HLSPlayer::Open(const wchar_t* url, DWORD timeoutMs) { if (!m_pEngine || !url) return E_INVALIDARG; m_bEOF.store(false); @@ -149,61 +117,43 @@ HRESULT HLSPlayer::Open(const wchar_t* url) { m_bReady.store(false); m_lastPts = -1; - // Create a ready event for synchronous wait if (!m_hReadyEvent) - m_hReadyEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr); + m_hReadyEvent.Reset(CreateEventW(nullptr, TRUE, FALSE, nullptr)); else - ResetEvent(m_hReadyEvent); + ResetEvent(m_hReadyEvent.Get()); - // SetSource requires a BSTR BSTR bstrUrl = SysAllocString(url); if (!bstrUrl) return E_OUTOFMEMORY; HRESULT hr = m_pEngine->SetSource(bstrUrl); SysFreeString(bstrUrl); - if (FAILED(hr)) { - HLS_LOG("SetSource failed: 0x%08x", (unsigned)hr); - return hr; - } + if (FAILED(hr)) { HLS_LOG("SetSource failed: 0x%08x", (unsigned)hr); return hr; } hr = m_pEngine->Load(); - if (FAILED(hr)) { - HLS_LOG("Load failed: 0x%08x", (unsigned)hr); - return hr; - } + if (FAILED(hr)) { HLS_LOG("Load failed: 0x%08x", (unsigned)hr); return hr; } - // Wait for the engine to reach a playable state (up to 15 s). - // EventNotify signals m_hReadyEvent on CANPLAY or ERROR. - // We also poll GetReadyState as a safety net. - for (int i = 0; i < 150; i++) { - DWORD wait = WaitForSingleObject(m_hReadyEvent, 100); - if (wait == WAIT_OBJECT_0) break; + const DWORD stepMs = 100; + const int steps = static_cast(timeoutMs / stepMs); + for (int i = 0; i < steps; ++i) { + if (WaitForSingleObject(m_hReadyEvent.Get(), stepMs) == WAIT_OBJECT_0) break; - // Poll readyState directly USHORT state = m_pEngine->GetReadyState(); if (state >= MF_MEDIA_ENGINE_READY_HAVE_FUTURE_DATA) { m_bReady.store(true); break; } - // Check for error - IMFMediaError* pErr = nullptr; - m_pEngine->GetError(&pErr); - if (pErr) { - USHORT code = pErr->GetErrorCode(); - pErr->Release(); - HLS_LOG("Engine error code: %u", code); + ComPtr err; + m_pEngine->GetError(err.GetAddressOf()); + if (err) { + HLS_LOG("Engine error code: %u", err->GetErrorCode()); return MF_E_INVALIDMEDIATYPE; } } - if (m_bError.load()) { - HLS_LOG("Open aborted due to engine error"); - return MF_E_INVALIDMEDIATYPE; - } + if (m_bError.load()) return MF_E_INVALIDMEDIATYPE; if (!m_bReady.load()) { - // One more check USHORT state = m_pEngine->GetReadyState(); if (state >= MF_MEDIA_ENGINE_READY_HAVE_METADATA) { m_bReady.store(true); @@ -213,44 +163,30 @@ HRESULT HLSPlayer::Open(const wchar_t* url) { } } - // Retrieve native video dimensions + DWORD w = 0, h = 0; + m_pEngine->GetNativeVideoSize(&w, &h); { - DWORD w = 0, h = 0; - m_pEngine->GetNativeVideoSize(&w, &h); - EnterCriticalSection(&m_cs); + VideoPlayerUtils::ScopedLock lock(m_cs); m_nativeWidth = w; m_nativeHeight = h; - LeaveCriticalSection(&m_cs); - HLS_LOG("Opened: %ux%u", w, h); } - + HLS_LOG("Opened: %ux%u", w, h); return S_OK; } void HLSPlayer::Close() { if (m_pEngine) { m_pEngine->Shutdown(); - m_pEngine->Release(); - m_pEngine = nullptr; + m_pEngine.Reset(); } - ReleaseTextures(); - if (m_pFrameBuffer) { - delete[] m_pFrameBuffer; - m_pFrameBuffer = nullptr; - m_frameBufferSize = 0; - } + m_frameBuffer.clear(); + m_frameBuffer.shrink_to_fit(); - if (m_pContext) { - m_pContext->Release(); - m_pContext = nullptr; - } - - if (m_hReadyEvent) { - CloseHandle(m_hReadyEvent); - m_hReadyEvent = nullptr; - } + m_pContext.Reset(); + m_pDevice.Reset(); + m_hReadyEvent.Reset(); m_nativeWidth = m_nativeHeight = 0; m_outputWidth = m_outputHeight = 0; @@ -260,23 +196,16 @@ void HLSPlayer::Close() { m_bError.store(false); } -// ============================================================================ -// D3D11 texture management -// ============================================================================ - HRESULT HLSPlayer::EnsureTextures(UINT32 w, UINT32 h) { if (w == 0 || h == 0) return E_INVALIDARG; - // Check if existing textures are the right size if (m_pRenderTarget) { D3D11_TEXTURE2D_DESC desc; m_pRenderTarget->GetDesc(&desc); - if (desc.Width == w && desc.Height == h) - return S_OK; + if (desc.Width == w && desc.Height == h) return S_OK; ReleaseTextures(); } - // Render target (GPU, for TransferVideoFrame) D3D11_TEXTURE2D_DESC desc = {}; desc.Width = w; desc.Height = h; @@ -287,122 +216,100 @@ HRESULT HLSPlayer::EnsureTextures(UINT32 w, UINT32 h) { desc.Usage = D3D11_USAGE_DEFAULT; desc.BindFlags = D3D11_BIND_RENDER_TARGET; - HRESULT hr = m_pDevice->CreateTexture2D(&desc, nullptr, &m_pRenderTarget); + HRESULT hr = m_pDevice->CreateTexture2D(&desc, nullptr, m_pRenderTarget.ReleaseAndGetAddressOf()); if (FAILED(hr)) return hr; - // Staging texture (CPU-readable) desc.Usage = D3D11_USAGE_STAGING; desc.BindFlags = 0; desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; - hr = m_pDevice->CreateTexture2D(&desc, nullptr, &m_pStagingTexture); + hr = m_pDevice->CreateTexture2D(&desc, nullptr, m_pStagingTexture.ReleaseAndGetAddressOf()); if (FAILED(hr)) { - m_pRenderTarget->Release(); - m_pRenderTarget = nullptr; + m_pRenderTarget.Reset(); return hr; } - // Resize CPU frame buffer - DWORD needed = w * h * 4; - if (m_frameBufferSize < needed) { - delete[] m_pFrameBuffer; - m_pFrameBuffer = new (std::nothrow) BYTE[needed]; - m_frameBufferSize = m_pFrameBuffer ? needed : 0; - if (!m_pFrameBuffer) return E_OUTOFMEMORY; + const size_t needed = static_cast(w) * h * 4; + if (m_frameBuffer.size() < needed) { + try { + m_frameBuffer.resize(needed); + } catch (const std::bad_alloc&) { + m_frameBuffer.clear(); + m_frameBuffer.shrink_to_fit(); + return E_OUTOFMEMORY; + } } - return S_OK; } void HLSPlayer::ReleaseTextures() { - if (m_pRenderTarget) { m_pRenderTarget->Release(); m_pRenderTarget = nullptr; } - if (m_pStagingTexture) { m_pStagingTexture->Release(); m_pStagingTexture = nullptr; } + m_pRenderTarget.Reset(); + m_pStagingTexture.Reset(); } -// ============================================================================ -// Frame access -// ============================================================================ - HRESULT HLSPlayer::ReadFrame(BYTE** ppData, DWORD* pDataSize) { if (!ppData || !pDataSize) return E_INVALIDARG; - *ppData = nullptr; + *ppData = nullptr; *pDataSize = 0; - if (!m_pEngine || !m_bReady.load()) return S_OK; // not ready yet + if (!m_pEngine || !m_bReady.load()) return S_OK; if (m_bEOF.load()) return S_FALSE; - if (!m_pEngine->HasVideo()) return S_OK; - // Check for a new frame LONGLONG pts = 0; HRESULT hr = m_pEngine->OnVideoStreamTick(&pts); - if (hr == S_FALSE) return S_OK; // no new frame yet + if (hr == S_FALSE) return S_OK; if (FAILED(hr)) return hr; - - // Skip duplicate frames (same pts) if (pts == m_lastPts) return S_OK; - // Query current native dimensions (may change with HLS ABR) DWORD natW = 0, natH = 0; m_pEngine->GetNativeVideoSize(&natW, &natH); if (natW == 0 || natH == 0) return S_OK; - EnterCriticalSection(&m_cs); - m_nativeWidth = natW; - m_nativeHeight = natH; - UINT32 w = EffectiveWidth(); - UINT32 h = EffectiveHeight(); - LeaveCriticalSection(&m_cs); + UINT32 w, h; + { + VideoPlayerUtils::ScopedLock lock(m_cs); + m_nativeWidth = natW; + m_nativeHeight = natH; + w = EffectiveWidth(); + h = EffectiveHeight(); + } - // Ensure D3D textures are the right size hr = EnsureTextures(w, h); if (FAILED(hr)) return hr; - // Transfer the current video frame to our render target RECT destRect = { 0, 0, (LONG)w, (LONG)h }; MFARGB borderColor = { 0, 0, 0, 255 }; - hr = m_pEngine->TransferVideoFrame(m_pRenderTarget, nullptr, &destRect, &borderColor); - if (FAILED(hr)) { - HLS_LOG("TransferVideoFrame failed: 0x%08x", (unsigned)hr); - return hr; - } + hr = m_pEngine->TransferVideoFrame(m_pRenderTarget.Get(), nullptr, &destRect, &borderColor); + if (FAILED(hr)) { HLS_LOG("TransferVideoFrame failed: 0x%08x", (unsigned)hr); return hr; } - // Copy render target → staging texture - m_pContext->CopyResource(m_pStagingTexture, m_pRenderTarget); + m_pContext->CopyResource(m_pStagingTexture.Get(), m_pRenderTarget.Get()); - // Map staging texture → CPU frame buffer D3D11_MAPPED_SUBRESOURCE mapped = {}; - hr = m_pContext->Map(m_pStagingTexture, 0, D3D11_MAP_READ, 0, &mapped); + hr = m_pContext->Map(m_pStagingTexture.Get(), 0, D3D11_MAP_READ, 0, &mapped); if (FAILED(hr)) return hr; const DWORD dstRowBytes = w * 4; - if ((UINT)mapped.RowPitch == dstRowBytes) { - memcpy(m_pFrameBuffer, mapped.pData, dstRowBytes * h); + if (static_cast(mapped.RowPitch) == dstRowBytes) { + memcpy(m_frameBuffer.data(), mapped.pData, dstRowBytes * h); } else { const BYTE* pSrc = static_cast(mapped.pData); - BYTE* pDst = m_pFrameBuffer; - for (UINT32 y = 0; y < h; y++) { + BYTE* pDst = m_frameBuffer.data(); + for (UINT32 y = 0; y < h; ++y) { memcpy(pDst, pSrc, dstRowBytes); pSrc += mapped.RowPitch; pDst += dstRowBytes; } } - - m_pContext->Unmap(m_pStagingTexture, 0); + m_pContext->Unmap(m_pStagingTexture.Get(), 0); m_lastPts = pts; - *ppData = m_pFrameBuffer; + *ppData = m_frameBuffer.data(); *pDataSize = w * h * 4; return S_OK; } -void HLSPlayer::UnlockFrame() { - // No-op: frame buffer is owned by HLSPlayer and reused across calls -} - -// ============================================================================ -// Playback control -// ============================================================================ +void HLSPlayer::UnlockFrame() { /* frame buffer reused */ } HRESULT HLSPlayer::SetPlaying(BOOL bPlaying, BOOL bStop) { if (!m_pEngine) return E_FAIL; @@ -413,43 +320,33 @@ HRESULT HLSPlayer::SetPlaying(BOOL bPlaying, BOOL bStop) { m_bEOF.store(false); return S_OK; } - if (bPlaying) { m_bEOF.store(false); return m_pEngine->Play(); - } else { - return m_pEngine->Pause(); } + return m_pEngine->Pause(); } HRESULT HLSPlayer::Seek(LONGLONG position100ns) { if (!m_pEngine) return E_FAIL; double seconds = position100ns / 10000000.0; m_bEOF.store(false); - m_lastPts = -1; // force next frame to be read + m_lastPts = -1; m_pEngine->SetCurrentTime(seconds); return S_OK; } -// ============================================================================ -// Properties -// ============================================================================ - void HLSPlayer::GetVideoSize(UINT32* pW, UINT32* pH) const { - EnterCriticalSection(const_cast(&m_cs)); + VideoPlayerUtils::ScopedLock lock(m_cs); if (pW) *pW = EffectiveWidth(); if (pH) *pH = EffectiveHeight(); - LeaveCriticalSection(const_cast(&m_cs)); } HRESULT HLSPlayer::GetDuration(LONGLONG* pDuration) const { if (!m_pEngine || !pDuration) return E_INVALIDARG; double dur = m_pEngine->GetDuration(); - if (std::isnan(dur) || std::isinf(dur) || dur <= 0.0) { - *pDuration = 0; // live stream - } else { - *pDuration = static_cast(dur * 10000000.0); - } + if (std::isnan(dur) || std::isinf(dur) || dur <= 0.0) *pDuration = 0; + else *pDuration = static_cast(dur * 10000000.0); return S_OK; } @@ -483,32 +380,25 @@ HRESULT HLSPlayer::GetPlaybackSpeed(float* pSpeed) const { } HRESULT HLSPlayer::SetOutputSize(UINT32 targetW, UINT32 targetH) { - EnterCriticalSection(&m_cs); + VideoPlayerUtils::ScopedLock lock(m_cs); if (targetW == 0 || targetH == 0) { - // Reset to native m_outputWidth = m_outputHeight = 0; - LeaveCriticalSection(&m_cs); return S_OK; } - // Don't scale up if (targetW > m_nativeWidth || targetH > m_nativeHeight) { targetW = m_nativeWidth; targetH = m_nativeHeight; } - // Preserve aspect ratio if (m_nativeWidth > 0 && m_nativeHeight > 0) { - double srcAspect = (double)m_nativeWidth / m_nativeHeight; - double dstAspect = (double)targetW / targetH; - if (srcAspect > dstAspect) - targetH = (UINT32)(targetW / srcAspect); - else - targetW = (UINT32)(targetH * srcAspect); + double srcAspect = static_cast(m_nativeWidth) / m_nativeHeight; + double dstAspect = static_cast(targetW) / targetH; + if (srcAspect > dstAspect) targetH = static_cast(targetW / srcAspect); + else targetW = static_cast(targetH * srcAspect); } - // Even dimensions targetW = (targetW + 1) & ~1u; targetH = (targetH + 1) & ~1u; if (targetW < 2) targetW = 2; @@ -516,6 +406,5 @@ HRESULT HLSPlayer::SetOutputSize(UINT32 targetW, UINT32 targetH) { m_outputWidth = targetW; m_outputHeight = targetH; - LeaveCriticalSection(&m_cs); return S_OK; } diff --git a/mediaplayer/src/jvmMain/native/windows/HLSPlayer.h b/mediaplayer/src/jvmMain/native/windows/HLSPlayer.h index c76b95fb..18ffa17b 100644 --- a/mediaplayer/src/jvmMain/native/windows/HLSPlayer.h +++ b/mediaplayer/src/jvmMain/native/windows/HLSPlayer.h @@ -1,56 +1,48 @@ #pragma once +#include "ComHelpers.h" #include #include #include #include +#include #include #include +#include -// Forward declaration struct VideoPlayerInstance; -/** - * HLS streaming player using IMFMediaEngine. - * - * IMFMediaEngine has native HLS support on Windows 10+ (unlike IMFSourceReader - * which only supports HLS in UWP/Edge contexts). This class wraps the engine - * and exposes a frame-server API compatible with the existing ReadVideoFrame - * lock/unlock pattern. - * - * Audio playback is handled internally by the engine — no WASAPI setup needed. - * Frame extraction uses TransferVideoFrame -> D3D11 staging texture -> CPU copy. - */ +// HLS streaming player using IMFMediaEngine. IMFMediaEngine has native HLS +// support on Windows 10+ (unlike IMFSourceReader which only supports HLS in +// UWP/Edge contexts). Audio playback is handled internally by the engine — +// no WASAPI setup needed. Frame extraction goes through TransferVideoFrame +// -> D3D11 staging texture -> CPU copy. class HLSPlayer : public IMFMediaEngineNotify { public: HLSPlayer(); - ~HLSPlayer(); + virtual ~HLSPlayer(); - // ---- IUnknown ---- - STDMETHODIMP QueryInterface(REFIID riid, void** ppv) override; + // IUnknown + STDMETHODIMP QueryInterface(REFIID riid, void** ppv) override; STDMETHODIMP_(ULONG) AddRef() override; STDMETHODIMP_(ULONG) Release() override; - // ---- IMFMediaEngineNotify ---- + // IMFMediaEngineNotify STDMETHODIMP EventNotify(DWORD event, DWORD_PTR param1, DWORD param2) override; - // ---- Lifecycle ---- HRESULT Initialize(ID3D11Device* pDevice, IMFDXGIDeviceManager* pDXGIManager); - HRESULT Open(const wchar_t* url); + HRESULT Open(const wchar_t* url, DWORD timeoutMs = 15000); void Close(); - // ---- Frame access (matches ReadVideoFrame / UnlockVideoFrame pattern) ---- HRESULT ReadFrame(BYTE** ppData, DWORD* pDataSize); - void UnlockFrame(); // no-op — buffer owned by HLSPlayer + void UnlockFrame(); - // ---- Playback control ---- HRESULT SetPlaying(BOOL bPlaying, BOOL bStop = FALSE); HRESULT Seek(LONGLONG position100ns); - // ---- Properties ---- - BOOL IsEOF() const { return m_bEOF.load(); } - BOOL IsReady() const { return m_bReady.load(); } - BOOL HasAudio() const { return TRUE; } // engine handles audio + BOOL IsEOF() const { return m_bEOF.load(); } + BOOL IsReady() const { return m_bReady.load(); } + BOOL HasAudio() const { return TRUE; } void GetVideoSize(UINT32* pW, UINT32* pH) const; HRESULT GetDuration(LONGLONG* pDuration) const; HRESULT GetPosition(LONGLONG* pPosition) const; @@ -58,46 +50,34 @@ class HLSPlayer : public IMFMediaEngineNotify { HRESULT GetVolume(float* pVol) const; HRESULT SetPlaybackSpeed(float speed); HRESULT GetPlaybackSpeed(float* pSpeed) const; - - // ---- Output scaling (mirrors SetOutputSize) ---- HRESULT SetOutputSize(UINT32 targetW, UINT32 targetH); private: LONG m_refCount = 1; - // Media Engine - IMFMediaEngineEx* m_pEngine = nullptr; - - // D3D11 (not owned — borrowed from MediaFoundationManager) - ID3D11Device* m_pDevice = nullptr; - ID3D11DeviceContext* m_pContext = nullptr; - - // Textures for frame extraction - ID3D11Texture2D* m_pRenderTarget = nullptr; - ID3D11Texture2D* m_pStagingTexture = nullptr; + Microsoft::WRL::ComPtr m_pEngine; + Microsoft::WRL::ComPtr m_pDevice; + Microsoft::WRL::ComPtr m_pContext; + Microsoft::WRL::ComPtr m_pRenderTarget; + Microsoft::WRL::ComPtr m_pStagingTexture; - // CPU frame buffer (returned by ReadFrame) - BYTE* m_pFrameBuffer = nullptr; - DWORD m_frameBufferSize = 0; + std::vector m_frameBuffer; - // Video dimensions UINT32 m_nativeWidth = 0; UINT32 m_nativeHeight = 0; - UINT32 m_outputWidth = 0; // 0 = use native + UINT32 m_outputWidth = 0; UINT32 m_outputHeight = 0; - // State LONGLONG m_lastPts = -1; std::atomic m_bReady{false}; std::atomic m_bEOF{false}; std::atomic m_bError{false}; - HANDLE m_hReadyEvent = nullptr; + VideoPlayerUtils::UniqueHandle m_hReadyEvent; - CRITICAL_SECTION m_cs; + mutable VideoPlayerUtils::CriticalSection m_cs; - // Internal helpers - UINT32 EffectiveWidth() const { return m_outputWidth > 0 ? m_outputWidth : m_nativeWidth; } - UINT32 EffectiveHeight() const { return m_outputHeight > 0 ? m_outputHeight : m_nativeHeight; } + UINT32 EffectiveWidth() const { return m_outputWidth > 0 ? m_outputWidth : m_nativeWidth; } + UINT32 EffectiveHeight() const { return m_outputHeight > 0 ? m_outputHeight : m_nativeHeight; } HRESULT EnsureTextures(UINT32 w, UINT32 h); void ReleaseTextures(); }; diff --git a/mediaplayer/src/jvmMain/native/windows/MediaFoundationManager.cpp b/mediaplayer/src/jvmMain/native/windows/MediaFoundationManager.cpp index 30092a2c..bd92b6d7 100644 --- a/mediaplayer/src/jvmMain/native/windows/MediaFoundationManager.cpp +++ b/mediaplayer/src/jvmMain/native/windows/MediaFoundationManager.cpp @@ -1,139 +1,130 @@ +// MediaFoundationManager.cpp — process-wide Media Foundation / D3D11 / audio +// enumerator bootstrap. Refcounts instances so Shutdown() is a no-op while +// players are alive. + #include "MediaFoundationManager.h" -#include #include #include #include +#include +#include + +using Microsoft::WRL::ComPtr; namespace MediaFoundation { +namespace { + +std::atomic g_initialized{false}; +std::atomic g_instanceCount{0}; +std::atomic g_comInitialized{false}; -// Global resources shared across all instances -static bool g_bMFInitialized = false; -static ID3D11Device* g_pD3DDevice = nullptr; -static IMFDXGIDeviceManager* g_pDXGIDeviceManager = nullptr; -static UINT32 g_dwResetToken = 0; -static IMMDeviceEnumerator* g_pEnumerator = nullptr; -static std::atomic g_instanceCount{0}; +ComPtr g_device; +ComPtr g_dxgiManager; +ComPtr g_enumerator; +UINT32 g_resetToken = 0; + +HRESULT CreateDX11Device() { + HRESULT hr = D3D11CreateDevice( + nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, + D3D11_CREATE_DEVICE_VIDEO_SUPPORT, nullptr, 0, + D3D11_SDK_VERSION, g_device.ReleaseAndGetAddressOf(), nullptr, nullptr); + if (FAILED(hr)) return hr; + + ComPtr multithread; + if (SUCCEEDED(g_device.As(&multithread))) { + multithread->SetMultithreadProtected(TRUE); + } + return S_OK; +} + +} // namespace HRESULT Initialize() { - if (g_bMFInitialized) - return OP_E_ALREADY_INITIALIZED; + if (g_initialized.load()) return OP_E_ALREADY_INITIALIZED; HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); - if (SUCCEEDED(hr)) - hr = MFStartup(MF_VERSION); - if (FAILED(hr)) - return hr; + const bool comOwned = SUCCEEDED(hr); // false if RPC_E_CHANGED_MODE + if (hr == RPC_E_CHANGED_MODE) hr = S_OK; // caller picked a different mode; fine + if (FAILED(hr)) return hr; + g_comInitialized.store(comOwned); - hr = CreateDX11Device(); - if (FAILED(hr)) { - MFShutdown(); - return hr; + hr = MFStartup(MF_VERSION); + if (FAILED(hr)) { + if (comOwned) CoUninitialize(); + g_comInitialized.store(false); + return hr; } - hr = MFCreateDXGIDeviceManager(&g_dwResetToken, &g_pDXGIDeviceManager); - if (SUCCEEDED(hr)) - hr = g_pDXGIDeviceManager->ResetDevice(g_pD3DDevice, g_dwResetToken); + hr = CreateDX11Device(); if (FAILED(hr)) { - if (g_pD3DDevice) { - g_pD3DDevice->Release(); - g_pD3DDevice = nullptr; - } MFShutdown(); + if (comOwned) CoUninitialize(); + g_comInitialized.store(false); return hr; } - // Create the audio device enumerator eagerly so it is released in Shutdown() - hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, - IID_PPV_ARGS(&g_pEnumerator)); + hr = MFCreateDXGIDeviceManager(&g_resetToken, g_dxgiManager.ReleaseAndGetAddressOf()); + if (SUCCEEDED(hr)) + hr = g_dxgiManager->ResetDevice(g_device.Get(), g_resetToken); + + if (SUCCEEDED(hr)) { + hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, + IID_PPV_ARGS(g_enumerator.ReleaseAndGetAddressOf())); + } + if (FAILED(hr)) { - g_pDXGIDeviceManager->Release(); - g_pDXGIDeviceManager = nullptr; - g_pD3DDevice->Release(); - g_pD3DDevice = nullptr; + g_enumerator.Reset(); + g_dxgiManager.Reset(); + g_device.Reset(); MFShutdown(); + if (comOwned) CoUninitialize(); + g_comInitialized.store(false); return hr; } - g_bMFInitialized = true; + g_initialized.store(true); return S_OK; } HRESULT Shutdown() { - if (g_instanceCount > 0) - return E_FAIL; // Instances still active - - HRESULT hr = S_OK; - - // Release DXGI and D3D resources - if (g_pDXGIDeviceManager) { - g_pDXGIDeviceManager->Release(); - g_pDXGIDeviceManager = nullptr; + // No guard on instance count: this is called from a JVM shutdown hook + // after all Kotlin-side player instances have been told to dispose. MF + // worker threads MUST be stopped before the DLL is unloaded, or Windows + // crashes inside KERNELBASE on shutdown (exit 0x87A). + if (!g_initialized.load()) return S_OK; + + const int live = g_instanceCount.load(); + if (live > 0) { + // Misuse signal: a caller invoked ShutdownMediaFoundation() while + // players are still alive. The shutdown proceeds anyway (JVM-exit + // semantics) but any surviving instance will crash on its next call. + fprintf(stderr, + "[ComposeMediaPlayer] ShutdownMediaFoundation called with %d " + "live instance(s). Dispose all players before shutdown.\n", + live); } - if (g_pD3DDevice) { - g_pD3DDevice->Release(); - g_pD3DDevice = nullptr; - } + g_enumerator.Reset(); + g_dxgiManager.Reset(); + g_device.Reset(); - // Release audio enumerator - if (g_pEnumerator) { - g_pEnumerator->Release(); - g_pEnumerator = nullptr; - } + HRESULT hr = MFShutdown(); + g_initialized.store(false); - // Shutdown Media Foundation last - if (g_bMFInitialized) { - hr = MFShutdown(); - g_bMFInitialized = false; + if (g_comInitialized.load()) { + CoUninitialize(); + g_comInitialized.store(false); } - - // Uninitialize COM - CoUninitialize(); return hr; } -HRESULT CreateDX11Device() { - HRESULT hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, - D3D11_CREATE_DEVICE_VIDEO_SUPPORT, nullptr, 0, - D3D11_SDK_VERSION, &g_pD3DDevice, nullptr, nullptr); - if (FAILED(hr)) - return hr; - - ID3D10Multithread* pMultithread = nullptr; - if (SUCCEEDED(g_pD3DDevice->QueryInterface(__uuidof(ID3D10Multithread), reinterpret_cast(&pMultithread)))) { - pMultithread->SetMultithreadProtected(TRUE); - pMultithread->Release(); - } - - return hr; -} +ID3D11Device* GetD3DDevice() { return g_device.Get(); } +IMFDXGIDeviceManager* GetDXGIDeviceManager() { return g_dxgiManager.Get(); } +IMMDeviceEnumerator* GetDeviceEnumerator() { return g_enumerator.Get(); } -ID3D11Device* GetD3DDevice() { - return g_pD3DDevice; -} - -IMFDXGIDeviceManager* GetDXGIDeviceManager() { - return g_pDXGIDeviceManager; -} - -IMMDeviceEnumerator* GetDeviceEnumerator() { - return g_pEnumerator; -} - -void IncrementInstanceCount() { - g_instanceCount++; -} - -void DecrementInstanceCount() { - g_instanceCount--; -} - -bool IsInitialized() { - return g_bMFInitialized; -} - -int GetInstanceCount() { - return g_instanceCount; -} +void IncrementInstanceCount() { ++g_instanceCount; } +void DecrementInstanceCount() { --g_instanceCount; } +bool IsInitialized() { return g_initialized.load(); } +int GetInstanceCount() { return g_instanceCount.load(); } -} // namespace MediaFoundation \ No newline at end of file +} // namespace MediaFoundation diff --git a/mediaplayer/src/jvmMain/native/windows/MediaFoundationManager.h b/mediaplayer/src/jvmMain/native/windows/MediaFoundationManager.h index 495e04eb..e34a092d 100644 --- a/mediaplayer/src/jvmMain/native/windows/MediaFoundationManager.h +++ b/mediaplayer/src/jvmMain/native/windows/MediaFoundationManager.h @@ -1,74 +1,24 @@ #pragma once +#include "ErrorCodes.h" #include #include #include #include #include -// Error code definitions -#define OP_E_NOT_INITIALIZED ((HRESULT)0x80000001L) -#define OP_E_ALREADY_INITIALIZED ((HRESULT)0x80000002L) -#define OP_E_INVALID_PARAMETER ((HRESULT)0x80000003L) - namespace MediaFoundation { -/** - * @brief Initializes Media Foundation, Direct3D11, and the DXGI device manager. - * @return S_OK on success, or an error code. - */ HRESULT Initialize(); - -/** - * @brief Shuts down Media Foundation and releases global resources. - * @return S_OK on success, or an error code. - */ HRESULT Shutdown(); -/** - * @brief Creates a Direct3D11 device with video support. - * @return S_OK on success, or an error code. - */ -HRESULT CreateDX11Device(); - -/** - * @brief Gets the D3D11 device. - * @return Pointer to the D3D11 device. - */ -ID3D11Device* GetD3DDevice(); - -/** - * @brief Gets the DXGI device manager. - * @return Pointer to the DXGI device manager. - */ +ID3D11Device* GetD3DDevice(); IMFDXGIDeviceManager* GetDXGIDeviceManager(); +IMMDeviceEnumerator* GetDeviceEnumerator(); -/** - * @brief Gets the device enumerator for audio devices. - * @return Pointer to the device enumerator. - */ -IMMDeviceEnumerator* GetDeviceEnumerator(); - -/** - * @brief Increments the instance count. - */ void IncrementInstanceCount(); - -/** - * @brief Decrements the instance count. - */ void DecrementInstanceCount(); - -/** - * @brief Checks if Media Foundation is initialized. - * @return True if initialized, false otherwise. - */ bool IsInitialized(); - -/** - * @brief Gets the current instance count. - * @return The number of active instances. - */ -int GetInstanceCount(); +int GetInstanceCount(); } // namespace MediaFoundation diff --git a/mediaplayer/src/jvmMain/native/windows/NativeVideoPlayer.cpp b/mediaplayer/src/jvmMain/native/windows/NativeVideoPlayer.cpp index abd7550a..1f17d13d 100644 --- a/mediaplayer/src/jvmMain/native/windows/NativeVideoPlayer.cpp +++ b/mediaplayer/src/jvmMain/native/windows/NativeVideoPlayer.cpp @@ -7,28 +7,93 @@ #include "HLSPlayer.h" #include #include +#include +#include #include #include #include #include - -// For IMF2DBuffer and IMF2DBuffer2 interfaces #include +#include +#include +#if defined(_M_IX86) || defined(_M_X64) + #include + #define NVP_HAS_AVX2_INTRINSICS 1 +#else + #define NVP_HAS_AVX2_INTRINSICS 0 +#endif +using Microsoft::WRL::ComPtr; using namespace VideoPlayerUtils; using namespace MediaFoundation; using namespace AudioManager; // --------------------------------------------------------------------------- -// Helper: detect HTTP/HTTPS URLs (network streaming sources incl. HLS) +// Constants // --------------------------------------------------------------------------- -static bool IsNetworkUrl(const wchar_t* url) { - return (_wcsnicmp(url, L"http://", 7) == 0 || _wcsnicmp(url, L"https://", 8) == 0); +static constexpr UINT kDefaultFrameRateNum = 30; +static constexpr UINT kDefaultFrameRateDenom = 1; +static constexpr double kFrameSkipThreshold = 3.0; // frame intervals +static constexpr double kFrameAheadMinMs = 1.0; + +// --------------------------------------------------------------------------- +// Debug printing +// --------------------------------------------------------------------------- +#ifdef _DEBUG + #define PrintHR(msg, hr) fprintf(stderr, "%s (hr=0x%08x)\n", msg, static_cast(hr)) +#else + #define PrintHR(msg, hr) ((void)0) +#endif + + +// --------------------------------------------------------------------------- +// VideoPlayerInstance dtor — RAII teardown +// --------------------------------------------------------------------------- +VideoPlayerInstance::~VideoPlayerInstance() { + CloseMedia(this); } // --------------------------------------------------------------------------- -// Helper: detect HLS URLs (.m3u8 anywhere in URL, case-insensitive) +// Alpha fix (MFVideoFormat_RGB32 leaves the alpha byte undefined). +// On x86/x64: runtime-dispatched AVX2 with scalar fallback. +// On ARM64 (and anywhere AVX2 intrinsics aren't available): scalar only. // --------------------------------------------------------------------------- +#if NVP_HAS_AVX2_INTRINSICS +static bool DetectAvx2() { + int info[4] = {}; + __cpuid(info, 0); + if (info[0] < 7) return false; + __cpuidex(info, 7, 0); + return (info[1] & (1 << 5)) != 0; // EBX bit 5 = AVX2 +} +#endif + +static void ForceAlphaOpaque(BYTE* data, size_t pixelCount) { + uint32_t* px = reinterpret_cast(data); + size_t i = 0; + +#if NVP_HAS_AVX2_INTRINSICS + static const bool kHasAvx2 = DetectAvx2(); + if (kHasAvx2) { + const __m256i mask = _mm256_set1_epi32(static_cast(0xFF000000u)); + for (; i + 8 <= pixelCount; i += 8) { + __m256i v = _mm256_loadu_si256(reinterpret_cast<__m256i*>(px + i)); + v = _mm256_or_si256(v, mask); + _mm256_storeu_si256(reinterpret_cast<__m256i*>(px + i), v); + } + } +#endif + + for (; i < pixelCount; ++i) px[i] |= 0xFF000000u; +} + +// --------------------------------------------------------------------------- +// URL helpers +// --------------------------------------------------------------------------- +static bool IsNetworkUrl(const wchar_t* url) { + return _wcsnicmp(url, L"http://", 7) == 0 || _wcsnicmp(url, L"https://", 8) == 0; +} + static bool IsHLSUrl(const wchar_t* url) { if (!url) return false; std::wstring lower(url); @@ -37,100 +102,94 @@ static bool IsHLSUrl(const wchar_t* url) { } // --------------------------------------------------------------------------- -// Helper: open HLS media via IMFMediaEngine +// MediaType change handler — extracted to kill duplication. // --------------------------------------------------------------------------- -static HRESULT OpenMediaHLS(VideoPlayerInstance* pInstance, const wchar_t* url, BOOL startPlayback) { - auto* hlsPlayer = new (std::nothrow) HLSPlayer(); - if (!hlsPlayer) return E_OUTOFMEMORY; - - HRESULT hr = hlsPlayer->Initialize(MediaFoundation::GetD3DDevice(), - MediaFoundation::GetDXGIDeviceManager()); - if (FAILED(hr)) { - delete hlsPlayer; - return hr; +static void HandleMediaTypeChanges(VideoPlayerInstance* inst, DWORD flags) { + if (flags & MF_SOURCE_READERF_NATIVEMEDIATYPECHANGED) { + ComPtr newType; + if (SUCCEEDED(MFCreateMediaType(newType.GetAddressOf()))) { + newType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video); + newType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_RGB32); + inst->pSourceReader->SetCurrentMediaType( + MF_SOURCE_READER_FIRST_VIDEO_STREAM, nullptr, newType.Get()); + } + } + if (flags & MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED) { + ComPtr current; + if (SUCCEEDED(inst->pSourceReader->GetCurrentMediaType( + MF_SOURCE_READER_FIRST_VIDEO_STREAM, current.GetAddressOf()))) { + UINT32 newW = 0, newH = 0; + MFGetAttributeSize(current.Get(), MF_MT_FRAME_SIZE, &newW, &newH); + if (newW > 0 && newH > 0) { + inst->videoWidth = newW; + inst->videoHeight = newH; + } + } } +} - hr = hlsPlayer->Open(url); - if (FAILED(hr)) { - hlsPlayer->Close(); - delete hlsPlayer; - return hr; +// --------------------------------------------------------------------------- +// Copy a decoded frame into a caller-provided buffer. +// --------------------------------------------------------------------------- +static void CopyPlane(const BYTE* src, LONG srcPitch, + BYTE* dst, DWORD dstPitch, + DWORD rowBytes, UINT32 height) { + if (static_cast(dstPitch) == srcPitch && static_cast(rowBytes) == srcPitch) { + memcpy(dst, src, static_cast(rowBytes) * height); + return; + } + const DWORD copyBytes = (std::min)(rowBytes, dstPitch); + for (UINT32 y = 0; y < height; ++y) { + memcpy(dst, src, copyBytes); + src += srcPitch; + dst += dstPitch; } +} - pInstance->pHLSPlayer = hlsPlayer; - pInstance->bIsNetworkSource = TRUE; +// --------------------------------------------------------------------------- +// HLS fallback for network URLs +// --------------------------------------------------------------------------- +static HRESULT OpenMediaHLS(VideoPlayerInstance* pInstance, const wchar_t* url, BOOL startPlayback) { + // HLSPlayer starts at refcount 1 — Attach takes ownership without AddRef. + ComPtr hls; + hls.Attach(new (std::nothrow) HLSPlayer()); + if (!hls) return E_OUTOFMEMORY; + + HRESULT hr = hls->Initialize(GetD3DDevice(), GetDXGIDeviceManager()); + if (SUCCEEDED(hr)) hr = hls->Open(url); + if (FAILED(hr)) return hr; // ComPtr releases on scope exit. - // Dimensions - hlsPlayer->GetVideoSize(&pInstance->videoWidth, &pInstance->videoHeight); + pInstance->pHLSPlayer = hls; + pInstance->bIsNetworkSource = true; + + hls->GetVideoSize(&pInstance->videoWidth, &pInstance->videoHeight); pInstance->nativeWidth = pInstance->videoWidth; pInstance->nativeHeight = pInstance->videoHeight; - // Duration (0 → live stream) LONGLONG duration = 0; - hlsPlayer->GetDuration(&duration); - pInstance->bIsLiveStream = (duration == 0) ? TRUE : FALSE; - - // Audio is handled internally by the engine - pInstance->bHasAudio = TRUE; + hls->GetDuration(&duration); + pInstance->bIsLiveStream = (duration == 0); + pInstance->bHasAudio = true; if (startPlayback) { - hlsPlayer->SetPlaying(TRUE); - pInstance->llPlaybackStartTime = GetCurrentTimeMs(); - pInstance->llTotalPauseTime = 0; - pInstance->llPauseStart = 0; + hls->SetPlaying(TRUE); + pInstance->llPlaybackStartTime.store(GetCurrentTimeMs(), std::memory_order_relaxed); + pInstance->llTotalPauseTime.store(0, std::memory_order_relaxed); + pInstance->llPauseStart.store(0, std::memory_order_relaxed); } - return S_OK; } -// Error code definitions from header -#define OP_E_NOT_INITIALIZED ((HRESULT)0x80000001L) -#define OP_E_ALREADY_INITIALIZED ((HRESULT)0x80000002L) -#define OP_E_INVALID_PARAMETER ((HRESULT)0x80000003L) - -// Debug print macro -#ifdef _DEBUG -#define PrintHR(msg, hr) fprintf(stderr, "%s (hr=0x%08x)\n", msg, static_cast(hr)) -#else -#define PrintHR(msg, hr) ((void)0) -#endif - -// --------------------------------------------------------------------------- -// Named constants for synchronization thresholds (issue #6) -// --------------------------------------------------------------------------- - -// Default frame rate used when the actual rate cannot be determined -static constexpr UINT kDefaultFrameRateNum = 30; -static constexpr UINT kDefaultFrameRateDenom = 1; - -// A video frame that is more than this many frame intervals late is skipped -static constexpr double kFrameSkipThreshold = 3.0; - -// Minimum "ahead" time (ms) before the renderer sleeps to pace the output -static constexpr double kFrameAheadMinMs = 1.0; - -// Maximum wait time is clamped to this many frame intervals -static constexpr double kFrameMaxWaitIntervals = 2.0; - -// Stabilisation delay (ms) used around audio client stop/start during seeks -static constexpr DWORD kSeekAudioSettleMs = 5; - -// --------------------------------------------------------------------------- -// Helper: safely release a COM object -// --------------------------------------------------------------------------- -static inline void SafeRelease(IUnknown* obj) { if (obj) obj->Release(); } - // --------------------------------------------------------------------------- -// Helper: configure an MF audio media type with the given parameters. -// If channels/sampleRate are 0, defaults of 2 / 48000 are used. +// Audio format configuration // --------------------------------------------------------------------------- static HRESULT ConfigureAudioType(IMFMediaType* pType, UINT32 channels, UINT32 sampleRate) { - if (channels == 0) channels = 2; - if (sampleRate == 0) sampleRate = 48000; + if (channels == 0) channels = 2; + if (sampleRate == 0) sampleRate = 48000; - UINT32 bitsPerSample = 16; - UINT32 blockAlign = channels * (bitsPerSample / 8); - UINT32 avgBytesPerSec = sampleRate * blockAlign; + const UINT32 bitsPerSample = 16; + const UINT32 blockAlign = channels * (bitsPerSample / 8); + const UINT32 avgBytesPerSec = sampleRate * blockAlign; pType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio); pType->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM); @@ -142,604 +201,437 @@ static HRESULT ConfigureAudioType(IMFMediaType* pType, UINT32 channels, UINT32 s return S_OK; } -// --------------------------------------------------------------------------- -// Helper: query the native channel count and sample rate of the first audio -// stream so that the PCM conversion preserves them (issue #2). -// --------------------------------------------------------------------------- -static void QueryNativeAudioParams(IMFSourceReader* pReader, UINT32* pChannels, UINT32* pSampleRate) { - *pChannels = 0; - *pSampleRate = 0; - if (!pReader) return; - - IMFMediaType* pNativeType = nullptr; - HRESULT hr = pReader->GetNativeMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, 0, &pNativeType); - if (SUCCEEDED(hr) && pNativeType) { - pNativeType->GetUINT32(MF_MT_AUDIO_NUM_CHANNELS, pChannels); - pNativeType->GetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, pSampleRate); - pNativeType->Release(); +static void QueryNativeAudioParams(IMFSourceReader* reader, UINT32* channels, UINT32* sampleRate) { + *channels = 0; + *sampleRate = 0; + if (!reader) return; + + ComPtr nativeType; + if (SUCCEEDED(reader->GetNativeMediaType( + MF_SOURCE_READER_FIRST_AUDIO_STREAM, 0, nativeType.GetAddressOf()))) { + nativeType->GetUINT32(MF_MT_AUDIO_NUM_CHANNELS, channels); + nativeType->GetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, sampleRate); } } // --------------------------------------------------------------------------- -// Helper: acquire the next video sample (handles pause/cache and timing sync). -// -// Returns: -// S_OK – *ppSample is set (may be nullptr if the frame was skipped). -// S_FALSE – end of stream reached (bEOF set on the instance). -// other – error HRESULT. +// Compute the presentation reference time used to decide whether a decoded +// frame should be displayed now, skipped, or cached for later. // --------------------------------------------------------------------------- -static HRESULT AcquireNextSample(VideoPlayerInstance* pInstance, IMFSample** ppSample) { - *ppSample = nullptr; +static double ComputeReferenceMs(const VideoPlayerInstance* inst) { + if (inst->bHasAudio) { + const double audioFedMs = inst->llCurrentPosition.load(std::memory_order_relaxed) / 10000.0; + const double latencyMs = inst->audioLatencyMs.load(std::memory_order_relaxed); + return audioFedMs - latencyMs; + } + const LONGLONG now = static_cast(GetCurrentTimeMs()); + const LONGLONG start = static_cast(inst->llPlaybackStartTime.load(std::memory_order_relaxed)); + const LONGLONG pauseTotal = static_cast(inst->llTotalPauseTime.load(std::memory_order_relaxed)); + return static_cast(now - start - pauseTotal) * inst->playbackSpeed.load(std::memory_order_relaxed); +} - BOOL isPaused = (pInstance->llPauseStart != 0); - IMFSample* pSample = nullptr; - HRESULT hr = S_OK; - DWORD streamIndex = 0, dwFlags = 0; - LONGLONG llTimestamp = 0; +// --------------------------------------------------------------------------- +// Read the next video frame. Returns a sample ready to be displayed or +// nullptr when the frame is not yet due (caller should try again later). +// No blocking sleeps: early frames are cached to avoid stalling the JNI +// render thread. +// --------------------------------------------------------------------------- +static HRESULT AcquireNextSample(VideoPlayerInstance* inst, IMFSample** ppOut) { + *ppOut = nullptr; + + const bool isPaused = (inst->llPauseStart.load(std::memory_order_relaxed) != 0); + ComPtr sample; + LONGLONG ts = 0; + + UINT frNum = kDefaultFrameRateNum, frDenom = kDefaultFrameRateDenom; + GetVideoFrameRate(inst, &frNum, &frDenom); + if (frNum == 0) { frNum = kDefaultFrameRateNum; frDenom = kDefaultFrameRateDenom; } + const double frameIntervalMs = 1000.0 * frDenom / frNum; + const double lateThresholdMs = -frameIntervalMs * kFrameSkipThreshold; + + // 1) Cached-sample path: a previously-read frame that was "too early". + if (inst->pCachedSample) { + if (isPaused) { + inst->pCachedSample.CopyTo(sample.GetAddressOf()); + ts = inst->llCachedTimestamp; + } else { + const double frameTimeMs = inst->llCachedTimestamp / 10000.0; + const double refMs = ComputeReferenceMs(inst); + const ULONGLONG nowMs = GetCurrentTimeMs(); + const ULONGLONG insertedAt = inst->llCachedInsertedAtMs; + // Guard against clock skew / reinit: nowMs < insertedAt would + // wrap to a huge ULONGLONG and force-deliver a stale frame. + const ULONGLONG heldMs = (insertedAt != 0 && nowMs >= insertedAt) + ? (nowMs - insertedAt) : 0; + // Deliver if due, OR if the sample has been sitting too long — + // avoids an indefinite freeze when the audio clock stalls or + // drifts (which would otherwise leave refMs permanently behind). + if (frameTimeMs - refMs > kFrameAheadMinMs && heldMs < 300) { + return S_OK; // still too early, wait + } + sample = std::move(inst->pCachedSample); + inst->pCachedSample.Reset(); + inst->llCachedInsertedAtMs = 0; + ts = inst->llCachedTimestamp; + } + } - if (isPaused) { - // ----- Paused path: read one frame and cache, or reuse cached frame ----- - if (!pInstance->bHasInitialFrame) { - hr = pInstance->pSourceReader->ReadSample( + // 2) Fresh-read path: drop anything late, return the first in-window + // frame (or cache the first too-early one). We never hand a late + // sample to the caller — stale frames are pure waste, the picture + // should jump to "now", not replay what was missed. + if (!sample) { + constexpr int kMaxReadIterations = 64; + constexpr ULONGLONG kMaxReadBudgetMs = 25; + const ULONGLONG budgetStart = GetCurrentTimeMs(); + + for (int iter = 0; iter < kMaxReadIterations; ++iter) { + DWORD streamIndex = 0, flags = 0; + LONGLONG sampleTs = 0; + ComPtr s; + HRESULT hr = inst->pSourceReader->ReadSample( MF_SOURCE_READER_FIRST_VIDEO_STREAM, 0, - &streamIndex, &dwFlags, &llTimestamp, &pSample); + &streamIndex, &flags, &sampleTs, s.GetAddressOf()); if (FAILED(hr)) return hr; - if (dwFlags & MF_SOURCE_READERF_ENDOFSTREAM) { - pInstance->bEOF = TRUE; - if (pSample) pSample->Release(); + if (flags & MF_SOURCE_READERF_ENDOFSTREAM) { + inst->bEOF.store(true); return S_FALSE; } - // HLS adaptive bitrate: handle media type changes (resolution switch) - if (dwFlags & MF_SOURCE_READERF_NATIVEMEDIATYPECHANGED) { - // Re-apply desired output format after native format change - IMFMediaType* pNewType = nullptr; - if (SUCCEEDED(MFCreateMediaType(&pNewType))) { - pNewType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video); - pNewType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_RGB32); - pInstance->pSourceReader->SetCurrentMediaType( - MF_SOURCE_READER_FIRST_VIDEO_STREAM, nullptr, pNewType); - SafeRelease(pNewType); - } + HandleMediaTypeChanges(inst, flags); + + if (!s) { + // Decoder starved — yield back to the caller. + return S_OK; } - if (dwFlags & MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED) { - IMFMediaType* pCurrent = nullptr; - if (SUCCEEDED(pInstance->pSourceReader->GetCurrentMediaType( - MF_SOURCE_READER_FIRST_VIDEO_STREAM, &pCurrent))) { - UINT32 newW = 0, newH = 0; - MFGetAttributeSize(pCurrent, MF_MT_FRAME_SIZE, &newW, &newH); - if (newW > 0 && newH > 0) { - pInstance->videoWidth = newW; - pInstance->videoHeight = newH; - } - SafeRelease(pCurrent); + + // Paused path: cache the first frame for initial display. + if (isPaused) { + if (!inst->bHasInitialFrame) { + s.CopyTo(inst->pCachedSample.ReleaseAndGetAddressOf()); + inst->llCachedTimestamp = sampleTs; + inst->llCachedInsertedAtMs = GetCurrentTimeMs(); + inst->bHasInitialFrame = true; } + sample = std::move(s); + ts = sampleTs; + break; } - if (!pSample) return S_OK; // decoder starved - - if (pInstance->pCachedSample) { - pInstance->pCachedSample->Release(); - pInstance->pCachedSample = nullptr; + inst->bHasInitialFrame = true; + if (!inst->bHasAudio) { + inst->llCurrentPosition.store(sampleTs, std::memory_order_relaxed); } - pSample->AddRef(); - pInstance->pCachedSample = pSample; - pInstance->bHasInitialFrame = TRUE; - } else { - if (pInstance->pCachedSample) { - pSample = pInstance->pCachedSample; - pSample->AddRef(); - } else { - return S_OK; // no cached sample available - } - } - } else { - // ----- Playing path: decode a new frame ----- - hr = pInstance->pSourceReader->ReadSample( - MF_SOURCE_READER_FIRST_VIDEO_STREAM, 0, - &streamIndex, &dwFlags, &llTimestamp, &pSample); - if (FAILED(hr)) return hr; - if (dwFlags & MF_SOURCE_READERF_ENDOFSTREAM) { - pInstance->bEOF = TRUE; - if (pSample) pSample->Release(); - return S_FALSE; - } - - // HLS adaptive bitrate: handle media type changes (resolution switch) - if (dwFlags & MF_SOURCE_READERF_NATIVEMEDIATYPECHANGED) { - IMFMediaType* pNewType = nullptr; - if (SUCCEEDED(MFCreateMediaType(&pNewType))) { - pNewType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video); - pNewType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_RGB32); - pInstance->pSourceReader->SetCurrentMediaType( - MF_SOURCE_READER_FIRST_VIDEO_STREAM, nullptr, pNewType); - SafeRelease(pNewType); + // No timestamp → hand it over unconditionally. + if (sampleTs <= 0) { + sample = std::move(s); + ts = sampleTs; + break; } - } - if (dwFlags & MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED) { - IMFMediaType* pCurrent = nullptr; - if (SUCCEEDED(pInstance->pSourceReader->GetCurrentMediaType( - MF_SOURCE_READER_FIRST_VIDEO_STREAM, &pCurrent))) { - UINT32 newW = 0, newH = 0; - MFGetAttributeSize(pCurrent, MF_MT_FRAME_SIZE, &newW, &newH); - if (newW > 0 && newH > 0) { - pInstance->videoWidth = newW; - pInstance->videoHeight = newH; + + const double frameTimeMs = sampleTs / 10000.0; + const double refMs = ComputeReferenceMs(inst); + const double diffMs = frameTimeMs - refMs; + + if (diffMs < lateThresholdMs) { + // Stale — discard and keep reading. Do not cache, do not + // deliver: we want to display what's happening NOW, not + // replay pre-seek keyframes or frames skipped during a + // UI stall. + if (iter >= 3 && GetCurrentTimeMs() - budgetStart > kMaxReadBudgetMs) { + // Budget exhausted; yield so the caller can do something + // else. Next call resumes draining from here. + return S_OK; } - SafeRelease(pCurrent); + continue; } - } - - if (!pSample) return S_OK; // decoder starved - - // Release any cached sample from a previous pause — not needed during playback - if (pInstance->pCachedSample) { - pInstance->pCachedSample->Release(); - pInstance->pCachedSample = nullptr; - } - - pInstance->bHasInitialFrame = TRUE; - - // Only update position from video if there's no audio track. - if (!pInstance->bHasAudio) { - pInstance->llCurrentPosition = llTimestamp; - } - } - // ----- Frame timing synchronization ----- - // Audio-master model (like AVPlayer on macOS): video syncs to the audio - // position, not to a wall clock. This guarantees lip-sync because both - // streams share the same time reference. - // - // For video-only files (no audio), fall back to wall-clock sync. - if (!isPaused && llTimestamp > 0) { - - double frameTimeMs = llTimestamp / 10000.0; - - UINT frameRateNum = kDefaultFrameRateNum, frameRateDenom = kDefaultFrameRateDenom; - GetVideoFrameRate(pInstance, &frameRateNum, &frameRateDenom); - if (frameRateNum == 0) { - frameRateNum = kDefaultFrameRateNum; - frameRateDenom = kDefaultFrameRateDenom; - } - double frameIntervalMs = 1000.0 * frameRateDenom / frameRateNum; - - double referenceMs; - if (pInstance->bHasAudio) { - // Audio-master: use the audio position heard by the user right now. - // llCurrentPosition = PTS of the last sample fed to WASAPI. - // audioLatencyMs = how much of the WASAPI buffer hasn't played yet. - double audioFedMs = pInstance->llCurrentPosition / 10000.0; - double latencyMs = pInstance->audioLatencyMs.load(std::memory_order_relaxed); - referenceMs = audioFedMs - latencyMs; - } else { - // No audio: wall-clock fallback - LONGLONG currentTimeMs = GetCurrentTimeMs(); - LONGLONG elapsedMs = currentTimeMs - pInstance->llPlaybackStartTime - pInstance->llTotalPauseTime; - referenceMs = elapsedMs * pInstance->playbackSpeed.load(); - } - - double diffMs = frameTimeMs - referenceMs; + if (diffMs > frameIntervalMs) { + // Too early — cache so the next call on the normal render + // cadence can deliver it. + s.CopyTo(inst->pCachedSample.ReleaseAndGetAddressOf()); + inst->llCachedTimestamp = sampleTs; + inst->llCachedInsertedAtMs = GetCurrentTimeMs(); + return S_OK; + } - if (diffMs < -frameIntervalMs * kFrameSkipThreshold) { - // Frame is very late — skip it - pSample->Release(); - *ppSample = nullptr; - return S_OK; - } else if (diffMs > kFrameAheadMinMs) { - double waitTime = std::min(diffMs, frameIntervalMs * kFrameMaxWaitIntervals); - PreciseSleepHighRes(waitTime); + // In display window — deliver. + sample = std::move(s); + ts = sampleTs; + break; } } - *ppSample = pSample; + if (!sample) return S_OK; + sample.CopyTo(ppOut); return S_OK; } // ==================================================================== -// API Implementation +// Exported API // ==================================================================== -NATIVEVIDEOPLAYER_API int GetNativeVersion() { - return NATIVE_VIDEO_PLAYER_VERSION; -} +NATIVEVIDEOPLAYER_API int GetNativeVersion() { return NATIVE_VIDEO_PLAYER_VERSION; } -NATIVEVIDEOPLAYER_API HRESULT InitMediaFoundation() { - return Initialize(); -} +NATIVEVIDEOPLAYER_API HRESULT InitMediaFoundation() { return Initialize(); } NATIVEVIDEOPLAYER_API HRESULT CreateVideoPlayerInstance(VideoPlayerInstance** ppInstance) { - if (!ppInstance) - return E_INVALIDARG; + if (!ppInstance) return E_INVALIDARG; - // Ensure Media Foundation is initialized if (!IsInitialized()) { HRESULT hr = Initialize(); - if (FAILED(hr)) - return hr; + if (FAILED(hr)) return hr; } - auto* pInstance = new (std::nothrow) VideoPlayerInstance(); - if (!pInstance) - return E_OUTOFMEMORY; - - InitializeCriticalSection(&pInstance->csClockSync); - InitializeCriticalSection(&pInstance->csAudioFeed); - pInstance->bUseClockSync = TRUE; - - pInstance->hAudioReadyEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); - if (!pInstance->hAudioReadyEvent) { - DeleteCriticalSection(&pInstance->csAudioFeed); - DeleteCriticalSection(&pInstance->csClockSync); - delete pInstance; - return HRESULT_FROM_WIN32(GetLastError()); - } + auto inst = std::unique_ptr(new (std::nothrow) VideoPlayerInstance()); + if (!inst) return E_OUTOFMEMORY; + inst->bUseClockSync = true; IncrementInstanceCount(); - *ppInstance = pInstance; + *ppInstance = inst.release(); return S_OK; } NATIVEVIDEOPLAYER_API void DestroyVideoPlayerInstance(VideoPlayerInstance* pInstance) { - if (pInstance) { - CloseMedia(pInstance); - - if (pInstance->pCachedSample) { - pInstance->pCachedSample->Release(); - pInstance->pCachedSample = nullptr; - } - - DeleteCriticalSection(&pInstance->csAudioFeed); - DeleteCriticalSection(&pInstance->csClockSync); - delete pInstance; - DecrementInstanceCount(); - } + if (!pInstance) return; + delete pInstance; // dtor calls CloseMedia + DecrementInstanceCount(); } NATIVEVIDEOPLAYER_API HRESULT OpenMedia(VideoPlayerInstance* pInstance, const wchar_t* url, BOOL startPlayback) { - if (!pInstance || !url) - return OP_E_INVALID_PARAMETER; - if (!IsInitialized()) - return OP_E_NOT_INITIALIZED; + if (!pInstance || !url) return OP_E_INVALID_PARAMETER; + if (!IsInitialized()) return OP_E_NOT_INITIALIZED; - // Close previous media and reset state CloseMedia(pInstance); - pInstance->bEOF = FALSE; + pInstance->bEOF.store(false); pInstance->videoWidth = pInstance->videoHeight = 0; - pInstance->bHasAudio = FALSE; - - pInstance->bHasInitialFrame = FALSE; - if (pInstance->pCachedSample) { - pInstance->pCachedSample->Release(); - pInstance->pCachedSample = nullptr; - } - - HRESULT hr = S_OK; + pInstance->bHasAudio = false; + pInstance->bHasInitialFrame = false; + pInstance->pCachedSample.Reset(); - // Detect network sources (HTTP/HTTPS — includes HLS .m3u8 streams) const bool isNetwork = IsNetworkUrl(url); - pInstance->bIsNetworkSource = isNetwork ? TRUE : FALSE; - pInstance->bIsLiveStream = FALSE; + pInstance->bIsNetworkSource = isNetwork; + pInstance->bIsLiveStream = false; - // HLS streams (.m3u8): use IMFMediaEngine which has native HLS support - if (isNetwork && IsHLSUrl(url)) { + if (isNetwork && IsHLSUrl(url)) return OpenMediaHLS(pInstance, url, startPlayback); - } - - // 1. Configure and open media source with both audio and video streams - // ------------------------------------------------------------------ - IMFAttributes* pAttributes = nullptr; - hr = MFCreateAttributes(&pAttributes, 6); - if (FAILED(hr)) - return hr; - pAttributes->SetUINT32(MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS, TRUE); - pAttributes->SetUINT32(MF_SOURCE_READER_DISABLE_DXVA, FALSE); - pAttributes->SetUnknown(MF_SOURCE_READER_D3D_MANAGER, GetDXGIDeviceManager()); - pAttributes->SetUINT32(MF_SOURCE_READER_ENABLE_ADVANCED_VIDEO_PROCESSING, TRUE); + // ---- Configure and open source reader ---- + ComPtr attrs; + HRESULT hr = MFCreateAttributes(attrs.GetAddressOf(), 6); + if (FAILED(hr)) return hr; - // For network/HLS sources: hint the pipeline to reduce buffering latency - if (isNetwork) { - pAttributes->SetUINT32(MF_LOW_LATENCY, TRUE); - } + attrs->SetUINT32(MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS, TRUE); + attrs->SetUINT32(MF_SOURCE_READER_DISABLE_DXVA, FALSE); + attrs->SetUnknown(MF_SOURCE_READER_D3D_MANAGER, GetDXGIDeviceManager()); + attrs->SetUINT32(MF_SOURCE_READER_ENABLE_ADVANCED_VIDEO_PROCESSING, TRUE); + if (isNetwork) attrs->SetUINT32(MF_LOW_LATENCY, TRUE); - hr = MFCreateSourceReaderFromURL(url, pAttributes, &pInstance->pSourceReader); - SafeRelease(pAttributes); + hr = MFCreateSourceReaderFromURL(url, attrs.Get(), pInstance->pSourceReader.ReleaseAndGetAddressOf()); if (FAILED(hr)) { - // Fallback: for network sources that fail with "unsupported byte stream", - // try the IMFMediaEngine path (handles HLS and other streaming formats) - if (isNetwork && hr == static_cast(0xC00D36C4)) { + if (isNetwork && hr == MF_E_UNSUPPORTED_BYTESTREAM_TYPE) return OpenMediaHLS(pInstance, url, startPlayback); - } return hr; } - // 2. Configure video stream (RGB32) - // ------------------------------------------ + // ---- Video stream: RGB32 ---- hr = pInstance->pSourceReader->SetStreamSelection(MF_SOURCE_READER_ALL_STREAMS, FALSE); if (SUCCEEDED(hr)) hr = pInstance->pSourceReader->SetStreamSelection(MF_SOURCE_READER_FIRST_VIDEO_STREAM, TRUE); - if (FAILED(hr)) - return hr; + if (FAILED(hr)) return hr; - IMFMediaType* pType = nullptr; - hr = MFCreateMediaType(&pType); - if (SUCCEEDED(hr)) { - hr = pType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video); - if (SUCCEEDED(hr)) - hr = pType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_RGB32); - if (SUCCEEDED(hr)) - hr = pInstance->pSourceReader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, nullptr, pType); - SafeRelease(pType); + { + ComPtr type; + hr = MFCreateMediaType(type.GetAddressOf()); + if (SUCCEEDED(hr)) { + type->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video); + type->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_RGB32); + hr = pInstance->pSourceReader->SetCurrentMediaType( + MF_SOURCE_READER_FIRST_VIDEO_STREAM, nullptr, type.Get()); + } + if (FAILED(hr)) return hr; } - if (FAILED(hr)) - return hr; - // Retrieve video dimensions (this is the native resolution of the video) - IMFMediaType* pCurrent = nullptr; - hr = pInstance->pSourceReader->GetCurrentMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, &pCurrent); - if (SUCCEEDED(hr)) { - hr = MFGetAttributeSize(pCurrent, MF_MT_FRAME_SIZE, &pInstance->videoWidth, &pInstance->videoHeight); - pInstance->nativeWidth = pInstance->videoWidth; - pInstance->nativeHeight = pInstance->videoHeight; - SafeRelease(pCurrent); + { + ComPtr current; + if (SUCCEEDED(pInstance->pSourceReader->GetCurrentMediaType( + MF_SOURCE_READER_FIRST_VIDEO_STREAM, current.GetAddressOf()))) { + MFGetAttributeSize(current.Get(), MF_MT_FRAME_SIZE, + &pInstance->videoWidth, &pInstance->videoHeight); + pInstance->nativeWidth = pInstance->videoWidth; + pInstance->nativeHeight = pInstance->videoHeight; + } } - // 3. Configure audio stream (if available) - // ------------------------------------------ - hr = pInstance->pSourceReader->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, TRUE); - if (SUCCEEDED(hr)) { - // Try native audio params first, fall back to 2ch/48kHz if WASAPI rejects them - UINT32 nativeChannels = 0, nativeSampleRate = 0; - QueryNativeAudioParams(pInstance->pSourceReader, &nativeChannels, &nativeSampleRate); + // ---- Audio stream (best effort) ---- + if (SUCCEEDED(pInstance->pSourceReader->SetStreamSelection( + MF_SOURCE_READER_FIRST_AUDIO_STREAM, TRUE))) { + + UINT32 nativeCh = 0, nativeSr = 0; + QueryNativeAudioParams(pInstance->pSourceReader.Get(), &nativeCh, &nativeSr); + // ConfigureAudioType normalizes 0/0 to 2/48000 internally, so do the + // same here to avoid issuing a redundant fallback attempt with the + // exact same parameters. + if (nativeCh == 0) nativeCh = 2; + if (nativeSr == 0) nativeSr = 48000; - // Helper lambda: configure audio on reader, init WASAPI, return success auto tryAudioFormat = [&](UINT32 ch, UINT32 sr) -> bool { - IMFMediaType* pWantedType = nullptr; - HRESULT hrt = MFCreateMediaType(&pWantedType); - if (FAILED(hrt)) return false; - ConfigureAudioType(pWantedType, ch, sr); - hrt = pInstance->pSourceReader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, nullptr, pWantedType); - SafeRelease(pWantedType); - if (FAILED(hrt)) return false; - - IMFMediaType* pActualType = nullptr; - hrt = pInstance->pSourceReader->GetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, &pActualType); - if (FAILED(hrt) || !pActualType) return false; + ComPtr wanted; + if (FAILED(MFCreateMediaType(wanted.GetAddressOf()))) return false; + ConfigureAudioType(wanted.Get(), ch, sr); + if (FAILED(pInstance->pSourceReader->SetCurrentMediaType( + MF_SOURCE_READER_FIRST_AUDIO_STREAM, nullptr, wanted.Get()))) return false; + + ComPtr actual; + if (FAILED(pInstance->pSourceReader->GetCurrentMediaType( + MF_SOURCE_READER_FIRST_AUDIO_STREAM, actual.GetAddressOf())) || !actual) + return false; WAVEFORMATEX* pWfx = nullptr; UINT32 size = 0; - hrt = MFCreateWaveFormatExFromMFMediaType(pActualType, &pWfx, &size); - SafeRelease(pActualType); - if (FAILED(hrt) || !pWfx) return false; + if (FAILED(MFCreateWaveFormatExFromMFMediaType(actual.Get(), &pWfx, &size)) || !pWfx) + return false; - hrt = InitWASAPI(pInstance, pWfx); - if (FAILED(hrt)) { - PrintHR("InitWASAPI failed", hrt); + HRESULT hrInit = InitWASAPI(pInstance, pWfx); + if (FAILED(hrInit)) { + PrintHR("InitWASAPI failed", hrInit); CoTaskMemFree(pWfx); return false; } + // Transfer ownership of pWfx to the instance — InitWASAPI does + // not copy it. if (pInstance->pSourceAudioFormat) CoTaskMemFree(pInstance->pSourceAudioFormat); pInstance->pSourceAudioFormat = pWfx; - pInstance->bHasAudio = TRUE; + pInstance->bHasAudio = true; return true; }; - // First try native format, then fall back to safe stereo 48kHz - if (!tryAudioFormat(nativeChannels, nativeSampleRate)) { - if (nativeChannels != 2 || nativeSampleRate != 48000) { - tryAudioFormat(2, 48000); - } + if (!tryAudioFormat(nativeCh, nativeSr)) { + // Only retry with the canonical fallback if the first attempt + // actually differed from it. + if (nativeCh != 2 || nativeSr != 48000) tryAudioFormat(2, 48000); } - // Create a dedicated audio SourceReader so the audio thread is never - // blocked by video decoding (ReadSample is serialized within a single - // reader). Both readers share the same container timestamps. - IMFAttributes* pAudioAttrs = nullptr; - hr = MFCreateAttributes(&pAudioAttrs, 2); - if (SUCCEEDED(hr)) { - if (isNetwork) pAudioAttrs->SetUINT32(MF_LOW_LATENCY, TRUE); - hr = MFCreateSourceReaderFromURL(url, pAudioAttrs, &pInstance->pSourceReaderAudio); - SafeRelease(pAudioAttrs); - } - if (SUCCEEDED(hr) && pInstance->pSourceReaderAudio) { - pInstance->pSourceReaderAudio->SetStreamSelection(MF_SOURCE_READER_ALL_STREAMS, FALSE); - pInstance->pSourceReaderAudio->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, TRUE); - - UINT32 usedCh = pInstance->pSourceAudioFormat ? pInstance->pSourceAudioFormat->nChannels : 2; - UINT32 usedSr = pInstance->pSourceAudioFormat ? pInstance->pSourceAudioFormat->nSamplesPerSec : 48000; - IMFMediaType* pWantedAudioType = nullptr; - if (SUCCEEDED(MFCreateMediaType(&pWantedAudioType))) { - ConfigureAudioType(pWantedAudioType, usedCh, usedSr); - pInstance->pSourceReaderAudio->SetCurrentMediaType( - MF_SOURCE_READER_FIRST_AUDIO_STREAM, nullptr, pWantedAudioType); - SafeRelease(pWantedAudioType); + // Dedicated audio SourceReader so the audio thread is never blocked + // by the video decoding path (ReadSample serializes within a reader). + if (pInstance->bHasAudio) { + ComPtr audioAttrs; + if (SUCCEEDED(MFCreateAttributes(audioAttrs.GetAddressOf(), 2))) { + if (isNetwork) audioAttrs->SetUINT32(MF_LOW_LATENCY, TRUE); + HRESULT hrA = MFCreateSourceReaderFromURL( + url, audioAttrs.Get(), pInstance->pSourceReaderAudio.ReleaseAndGetAddressOf()); + if (SUCCEEDED(hrA) && pInstance->pSourceReaderAudio) { + pInstance->pSourceReaderAudio->SetStreamSelection(MF_SOURCE_READER_ALL_STREAMS, FALSE); + pInstance->pSourceReaderAudio->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, TRUE); + + const UINT32 usedCh = pInstance->pSourceAudioFormat->nChannels; + const UINT32 usedSr = pInstance->pSourceAudioFormat->nSamplesPerSec; + ComPtr wanted; + if (SUCCEEDED(MFCreateMediaType(wanted.GetAddressOf()))) { + ConfigureAudioType(wanted.Get(), usedCh, usedSr); + pInstance->pSourceReaderAudio->SetCurrentMediaType( + MF_SOURCE_READER_FIRST_AUDIO_STREAM, nullptr, wanted.Get()); + } + } else { + PrintHR("Failed to create audio source reader", hrA); + } } - } else { - PrintHR("Failed to create audio source reader", hr); } } + // ---- Presentation clock ---- if (pInstance->bUseClockSync) { - // 4. Set up presentation clock for synchronization - // ---------------------------------------------------------- - hr = pInstance->pSourceReader->GetServiceForStream( - MF_SOURCE_READER_MEDIASOURCE, - GUID_NULL, - IID_PPV_ARGS(&pInstance->pMediaSource)); + if (SUCCEEDED(pInstance->pSourceReader->GetServiceForStream( + MF_SOURCE_READER_MEDIASOURCE, GUID_NULL, + IID_PPV_ARGS(pInstance->pMediaSource.ReleaseAndGetAddressOf())))) { - if (SUCCEEDED(hr)) { - hr = MFCreatePresentationClock(&pInstance->pPresentationClock); - if (SUCCEEDED(hr)) { - IMFPresentationTimeSource* pTimeSource = nullptr; - hr = MFCreateSystemTimeSource(&pTimeSource); - if (SUCCEEDED(hr)) { - hr = pInstance->pPresentationClock->SetTimeSource(pTimeSource); - if (SUCCEEDED(hr)) { - IMFRateControl* pRateControl = nullptr; - hr = pInstance->pPresentationClock->QueryInterface(IID_PPV_ARGS(&pRateControl)); - if (SUCCEEDED(hr)) { - hr = pRateControl->SetRate(FALSE, 1.0f); - if (FAILED(hr)) { - PrintHR("Failed to set initial presentation clock rate", hr); - } - pRateControl->Release(); - } - - IMFMediaSink* pMediaSink = nullptr; - hr = pInstance->pMediaSource->QueryInterface(IID_PPV_ARGS(&pMediaSink)); - if (SUCCEEDED(hr)) { - IMFClockStateSink* pClockStateSink = nullptr; - hr = pMediaSink->QueryInterface(IID_PPV_ARGS(&pClockStateSink)); - if (SUCCEEDED(hr)) { - if (startPlayback) { - hr = pInstance->pPresentationClock->Start(0); - if (FAILED(hr)) { - PrintHR("Failed to start presentation clock", hr); - } - } else { - // Keep the player paused until explicitly started - hr = pInstance->pPresentationClock->Pause(); - if (FAILED(hr)) { - PrintHR("Failed to pause presentation clock", hr); - } - } - pClockStateSink->Release(); - } - pMediaSink->Release(); - } else { - PrintHR("Failed to get media sink from media source", hr); - } - } - SafeRelease(pTimeSource); + if (SUCCEEDED(MFCreatePresentationClock(pInstance->pPresentationClock.ReleaseAndGetAddressOf()))) { + ComPtr timeSource; + if (SUCCEEDED(MFCreateSystemTimeSource(timeSource.GetAddressOf()))) { + pInstance->pPresentationClock->SetTimeSource(timeSource.Get()); + + ComPtr rateControl; + if (SUCCEEDED(pInstance->pPresentationClock.As(&rateControl))) + rateControl->SetRate(FALSE, 1.0f); + + if (startPlayback) pInstance->pPresentationClock->Start(0); + else pInstance->pPresentationClock->Pause(); } } } } - // 5. Initialize playback timing and start audio thread - // ---------------------------------------------------- + // ---- Timing init + audio thread start ---- if (startPlayback) { - pInstance->llPlaybackStartTime = GetCurrentTimeMs(); - pInstance->llTotalPauseTime = 0; - pInstance->llPauseStart = 0; + pInstance->llPlaybackStartTime.store(GetCurrentTimeMs(), std::memory_order_relaxed); + pInstance->llTotalPauseTime.store(0, std::memory_order_relaxed); + pInstance->llPauseStart.store(0, std::memory_order_relaxed); - // Pre-fill WASAPI buffer before starting audio thread if (pInstance->bHasAudio && pInstance->bAudioInitialized) { PreFillAudioBuffer(pInstance); + if (pInstance->pSourceReaderAudio) StartAudioThread(pInstance); } - - if (pInstance->bHasAudio && pInstance->bAudioInitialized && pInstance->pSourceReaderAudio) { - hr = StartAudioThread(pInstance); - if (FAILED(hr)) { - PrintHR("StartAudioThread failed", hr); - } - } + } else if (pInstance->bHasAudio && pInstance->bAudioInitialized && pInstance->pSourceReaderAudio) { + // Start the thread but leave it suspended until SetPlaybackState(TRUE). + StartAudioThread(pInstance); + SignalPause(pInstance); } return S_OK; } -// --------------------------------------------------------------------------- -// ReadVideoFrame — locks a frame buffer and returns a pointer to the caller // --------------------------------------------------------------------------- NATIVEVIDEOPLAYER_API HRESULT ReadVideoFrame(VideoPlayerInstance* pInstance, BYTE** pData, DWORD* pDataSize) { - if (!pInstance || !pData || !pDataSize) - return OP_E_NOT_INITIALIZED; + if (!pInstance || !pData || !pDataSize) return OP_E_NOT_INITIALIZED; - // HLS path — delegate to IMFMediaEngine - if (pInstance->pHLSPlayer) { + if (pInstance->pHLSPlayer) return pInstance->pHLSPlayer->ReadFrame(pData, pDataSize); - } - - if (!pInstance->pSourceReader) - return OP_E_NOT_INITIALIZED; - - if (pInstance->pLockedBuffer) - UnlockVideoFrame(pInstance); - if (pInstance->bEOF) { - *pData = nullptr; - *pDataSize = 0; - return S_FALSE; - } + if (!pInstance->pSourceReader) return OP_E_NOT_INITIALIZED; + if (pInstance->pLockedBuffer) UnlockVideoFrame(pInstance); - IMFSample* pSample = nullptr; - HRESULT hr = AcquireNextSample(pInstance, &pSample); + if (pInstance->bEOF.load()) { *pData = nullptr; *pDataSize = 0; return S_FALSE; } - if (hr == S_FALSE) { - // End of stream - *pData = nullptr; - *pDataSize = 0; - return S_FALSE; - } - if (FAILED(hr)) - return hr; - if (!pSample) { - // Frame was skipped or decoder starved - *pData = nullptr; - *pDataSize = 0; - return S_OK; - } + ComPtr sample; + HRESULT hr = AcquireNextSample(pInstance, sample.GetAddressOf()); + if (hr == S_FALSE) { *pData = nullptr; *pDataSize = 0; return S_FALSE; } + if (FAILED(hr)) return hr; + if (!sample) { *pData = nullptr; *pDataSize = 0; return S_OK; } - // Lock the buffer and expose its pointer to the caller - IMFMediaBuffer* pBuffer = nullptr; + ComPtr buffer; DWORD bufferCount = 0; - hr = pSample->GetBufferCount(&bufferCount); - if (SUCCEEDED(hr) && bufferCount == 1) { - hr = pSample->GetBufferByIndex(0, &pBuffer); + if (SUCCEEDED(sample->GetBufferCount(&bufferCount)) && bufferCount == 1) { + hr = sample->GetBufferByIndex(0, buffer.GetAddressOf()); } else { - hr = pSample->ConvertToContiguousBuffer(&pBuffer); - } - if (FAILED(hr)) { - PrintHR("Failed to get contiguous buffer", hr); - pSample->Release(); - return hr; + hr = sample->ConvertToContiguousBuffer(buffer.GetAddressOf()); } + if (FAILED(hr)) { PrintHR("GetBuffer failed", hr); return hr; } - BYTE* pBytes = nullptr; - DWORD cbMax = 0, cbCurr = 0; - hr = pBuffer->Lock(&pBytes, &cbMax, &cbCurr); - if (FAILED(hr)) { - PrintHR("Buffer->Lock failed", hr); - pBuffer->Release(); - pSample->Release(); - return hr; - } + BYTE* bytes = nullptr; + DWORD maxSz = 0, curSz = 0; + hr = buffer->Lock(&bytes, &maxSz, &curSz); + if (FAILED(hr)) { PrintHR("Lock failed", hr); return hr; } - // Force alpha byte to 0xFF — MFVideoFormat_RGB32 (X8R8G8B8) leaves the - // high byte undefined, which causes washed-out colours when Skia - // composites the frame against the window background. - { - const DWORD pixelCount = cbCurr / 4; - DWORD* px = reinterpret_cast(pBytes); - for (DWORD i = 0; i < pixelCount; ++i) - px[i] |= 0xFF000000; - } + ForceAlphaOpaque(bytes, curSz / 4); - pInstance->pLockedBuffer = pBuffer; - pInstance->pLockedBytes = pBytes; - pInstance->lockedMaxSize = cbMax; - pInstance->lockedCurrSize = cbCurr; - *pData = pBytes; - *pDataSize = cbCurr; - pSample->Release(); + pInstance->pLockedBuffer = buffer; + pInstance->pLockedBytes = bytes; + pInstance->lockedMaxSize = maxSz; + pInstance->lockedCurrSize = curSz; + *pData = bytes; + *pDataSize = curSz; return S_OK; } NATIVEVIDEOPLAYER_API HRESULT UnlockVideoFrame(VideoPlayerInstance* pInstance) { - if (!pInstance) - return E_INVALIDARG; - if (pInstance->pHLSPlayer) { - pInstance->pHLSPlayer->UnlockFrame(); - return S_OK; - } + if (!pInstance) return E_INVALIDARG; + if (pInstance->pHLSPlayer) { pInstance->pHLSPlayer->UnlockFrame(); return S_OK; } + if (pInstance->pLockedBuffer) { pInstance->pLockedBuffer->Unlock(); - pInstance->pLockedBuffer->Release(); - pInstance->pLockedBuffer = nullptr; + pInstance->pLockedBuffer.Reset(); } pInstance->pLockedBytes = nullptr; pInstance->lockedMaxSize = pInstance->lockedCurrSize = 0; @@ -747,617 +639,480 @@ NATIVEVIDEOPLAYER_API HRESULT UnlockVideoFrame(VideoPlayerInstance* pInstance) { } // --------------------------------------------------------------------------- -// ReadVideoFrameInto — copies the decoded frame into a caller-owned buffer -// --------------------------------------------------------------------------- -NATIVEVIDEOPLAYER_API HRESULT ReadVideoFrameInto( - VideoPlayerInstance* pInstance, - BYTE* pDst, - DWORD dstRowBytes, - DWORD dstCapacity, - LONGLONG* pTimestamp) { - +NATIVEVIDEOPLAYER_API HRESULT ReadVideoFrameInto(VideoPlayerInstance* pInstance, + BYTE* pDst, DWORD dstRowBytes, DWORD dstCapacity, + LONGLONG* pTimestamp) { if (!pInstance || !pDst || dstRowBytes == 0 || dstCapacity == 0) return OP_E_INVALID_PARAMETER; - if (!pInstance->pSourceReader) - return OP_E_NOT_INITIALIZED; - if (pInstance->pLockedBuffer) - UnlockVideoFrame(pInstance); + if (!pInstance->pSourceReader) return OP_E_NOT_INITIALIZED; + if (pInstance->pLockedBuffer) UnlockVideoFrame(pInstance); - if (pInstance->bEOF) { - if (pTimestamp) *pTimestamp = pInstance->llCurrentPosition; + if (pInstance->bEOF.load()) { + if (pTimestamp) *pTimestamp = pInstance->llCurrentPosition.load(std::memory_order_relaxed); return S_FALSE; } - IMFSample* pSample = nullptr; - HRESULT hr = AcquireNextSample(pInstance, &pSample); - + ComPtr sample; + HRESULT hr = AcquireNextSample(pInstance, sample.GetAddressOf()); if (hr == S_FALSE) { - if (pTimestamp) *pTimestamp = pInstance->llCurrentPosition; + if (pTimestamp) *pTimestamp = pInstance->llCurrentPosition.load(std::memory_order_relaxed); return S_FALSE; } - if (FAILED(hr)) - return hr; - if (!pSample) { - if (pTimestamp) *pTimestamp = pInstance->llCurrentPosition; + if (FAILED(hr)) return hr; + if (!sample) { + if (pTimestamp) *pTimestamp = pInstance->llCurrentPosition.load(std::memory_order_relaxed); return S_OK; } - if (pTimestamp) - *pTimestamp = pInstance->llCurrentPosition; + if (pTimestamp) *pTimestamp = pInstance->llCurrentPosition.load(std::memory_order_relaxed); - const UINT32 width = pInstance->videoWidth; + const UINT32 width = pInstance->videoWidth; const UINT32 height = pInstance->videoHeight; - if (width == 0 || height == 0) { - pSample->Release(); - return S_FALSE; - } + if (width == 0 || height == 0) return S_FALSE; const DWORD requiredDst = dstRowBytes * height; - if (dstCapacity < requiredDst) { - pSample->Release(); - return OP_E_INVALID_PARAMETER; - } + if (dstCapacity < requiredDst) return OP_E_INVALID_PARAMETER; - // Try to use IMF2DBuffer2 for optimized zero-copy access - IMFMediaBuffer* pBuffer = nullptr; - hr = pSample->ConvertToContiguousBuffer(&pBuffer); - if (FAILED(hr)) { - pSample->Release(); - return hr; - } + ComPtr buffer; + hr = sample->ConvertToContiguousBuffer(buffer.GetAddressOf()); + if (FAILED(hr)) return hr; - // Attempt IMF2DBuffer2 for direct 2D access (most efficient) - IMF2DBuffer2* p2DBuffer2 = nullptr; - IMF2DBuffer* p2DBuffer = nullptr; - BYTE* pScanline0 = nullptr; - LONG srcPitch = 0; - BYTE* pBufferStart = nullptr; - DWORD cbBufferLength = 0; - bool usedDirect2D = false; - - hr = pBuffer->QueryInterface(IID_PPV_ARGS(&p2DBuffer2)); - if (SUCCEEDED(hr) && p2DBuffer2) { - hr = p2DBuffer2->Lock2DSize(MF2DBuffer_LockFlags_Read, &pScanline0, &srcPitch, &pBufferStart, &cbBufferLength); - if (SUCCEEDED(hr)) { - usedDirect2D = true; - const DWORD srcRowBytes = width * 4; - - if (static_cast(dstRowBytes) == srcPitch && static_cast(srcRowBytes) == srcPitch) { - memcpy(pDst, pScanline0, srcRowBytes * height); - } else { - BYTE* pSrc = pScanline0; - BYTE* pDstRow = pDst; - const DWORD copyBytes = std::min(srcRowBytes, dstRowBytes); - for (UINT32 y = 0; y < height; y++) { - memcpy(pDstRow, pSrc, copyBytes); - pSrc += srcPitch; - pDstRow += dstRowBytes; - } - } - p2DBuffer2->Unlock2D(); - } - p2DBuffer2->Release(); - } + const DWORD srcRowBytes = width * 4; + bool copied = false; - // Fallback to IMF2DBuffer - if (!usedDirect2D) { - hr = pBuffer->QueryInterface(IID_PPV_ARGS(&p2DBuffer)); - if (SUCCEEDED(hr) && p2DBuffer) { - hr = p2DBuffer->Lock2D(&pScanline0, &srcPitch); - if (SUCCEEDED(hr)) { - usedDirect2D = true; - const DWORD srcRowBytes = width * 4; - - if (static_cast(dstRowBytes) == srcPitch && static_cast(srcRowBytes) == srcPitch) { - memcpy(pDst, pScanline0, srcRowBytes * height); - } else { - BYTE* pSrc = pScanline0; - BYTE* pDstRow = pDst; - const DWORD copyBytes = std::min(srcRowBytes, dstRowBytes); - for (UINT32 y = 0; y < height; y++) { - memcpy(pDstRow, pSrc, copyBytes); - pSrc += srcPitch; - pDstRow += dstRowBytes; - } - } - p2DBuffer->Unlock2D(); + // Preferred path: IMF2DBuffer2. + { + ComPtr b2; + if (SUCCEEDED(buffer.As(&b2))) { + BYTE* scan0 = nullptr; + LONG pitch = 0; + BYTE* bufStart = nullptr; + DWORD cbLen = 0; + if (SUCCEEDED(b2->Lock2DSize(MF2DBuffer_LockFlags_Read, &scan0, &pitch, &bufStart, &cbLen))) { + CopyPlane(scan0, pitch, pDst, dstRowBytes, srcRowBytes, height); + b2->Unlock2D(); + copied = true; } - p2DBuffer->Release(); } } - // Ultimate fallback to standard buffer lock - if (!usedDirect2D) { - BYTE* pBytes = nullptr; - DWORD cbMax = 0, cbCurr = 0; - hr = pBuffer->Lock(&pBytes, &cbMax, &cbCurr); - if (SUCCEEDED(hr)) { - const DWORD srcRowBytes = width * 4; - const DWORD requiredSrc = srcRowBytes * height; - if (cbCurr >= requiredSrc) { - MFCopyImage(pDst, dstRowBytes, pBytes, srcRowBytes, srcRowBytes, height); + // Fallback: IMF2DBuffer. + if (!copied) { + ComPtr b2; + if (SUCCEEDED(buffer.As(&b2))) { + BYTE* scan0 = nullptr; + LONG pitch = 0; + if (SUCCEEDED(b2->Lock2D(&scan0, &pitch))) { + CopyPlane(scan0, pitch, pDst, dstRowBytes, srcRowBytes, height); + b2->Unlock2D(); + copied = true; } - pBuffer->Unlock(); } } - // Force alpha byte to 0xFF — same fix as ReadVideoFrame. - // MFVideoFormat_RGB32 (X8R8G8B8) leaves the high byte undefined. - { - const DWORD pixelCount = (dstRowBytes * height) / 4; - DWORD* px = reinterpret_cast(pDst); - for (DWORD i = 0; i < pixelCount; ++i) - px[i] |= 0xFF000000; + // Final fallback: linear Lock. + if (!copied) { + BYTE* bytes = nullptr; + DWORD maxSz = 0, curSz = 0; + if (SUCCEEDED(buffer->Lock(&bytes, &maxSz, &curSz))) { + if (curSz >= srcRowBytes * height) + MFCopyImage(pDst, dstRowBytes, bytes, srcRowBytes, srcRowBytes, height); + buffer->Unlock(); + } } - pBuffer->Release(); - pSample->Release(); + ForceAlphaOpaque(pDst, (dstRowBytes * height) / 4); return S_OK; } NATIVEVIDEOPLAYER_API BOOL IsEOF(const VideoPlayerInstance* pInstance) { - if (!pInstance) - return FALSE; - if (pInstance->pHLSPlayer) - return pInstance->pHLSPlayer->IsEOF(); - return pInstance->bEOF; + if (!pInstance) return FALSE; + if (pInstance->pHLSPlayer) return pInstance->pHLSPlayer->IsEOF(); + return pInstance->bEOF.load(); } NATIVEVIDEOPLAYER_API void GetVideoSize(const VideoPlayerInstance* pInstance, UINT32* pWidth, UINT32* pHeight) { - if (!pInstance) - return; - if (pInstance->pHLSPlayer) { - pInstance->pHLSPlayer->GetVideoSize(pWidth, pHeight); - return; - } - if (pWidth) *pWidth = pInstance->videoWidth; + if (!pInstance) return; + if (pInstance->pHLSPlayer) { pInstance->pHLSPlayer->GetVideoSize(pWidth, pHeight); return; } + if (pWidth) *pWidth = pInstance->videoWidth; if (pHeight) *pHeight = pInstance->videoHeight; } NATIVEVIDEOPLAYER_API HRESULT GetVideoFrameRate(const VideoPlayerInstance* pInstance, UINT* pNum, UINT* pDenom) { if (!pInstance || !pNum || !pDenom) return OP_E_NOT_INITIALIZED; - // HLS: frame rate is variable, default to 30fps - if (pInstance->pHLSPlayer) { - *pNum = 30; - *pDenom = 1; - return S_OK; - } - + if (pInstance->pHLSPlayer) { *pNum = 30; *pDenom = 1; return S_OK; } if (!pInstance->pSourceReader) return OP_E_NOT_INITIALIZED; - IMFMediaType* pType = nullptr; - HRESULT hr = pInstance->pSourceReader->GetCurrentMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, &pType); - if (SUCCEEDED(hr)) { - hr = MFGetAttributeRatio(pType, MF_MT_FRAME_RATE, pNum, pDenom); - pType->Release(); - } + ComPtr type; + HRESULT hr = pInstance->pSourceReader->GetCurrentMediaType( + MF_SOURCE_READER_FIRST_VIDEO_STREAM, type.GetAddressOf()); + if (SUCCEEDED(hr)) + hr = MFGetAttributeRatio(type.Get(), MF_MT_FRAME_RATE, pNum, pDenom); return hr; } -NATIVEVIDEOPLAYER_API HRESULT SeekMedia(VideoPlayerInstance* pInstance, LONGLONG llPositionIn100Ns) { +// --------------------------------------------------------------------------- +// SeekMedia — robust seek with full reader / WASAPI synchronization. +// +// Contract with the caller: no other thread may call ReadVideoFrame (video +// reader) while SeekMedia is running. The Kotlin side cancels its producer +// coroutine before invoking this. The audio reader is protected internally +// by csAudioFeed. +// +// Flow: +// 1. Snapshot wasPlaying under csClockSync (consistent with timing fields). +// 2. Raise bSeekInProgress so the audio thread discards any sample it is +// currently decoding and stops feeding WASAPI. +// 3. Stop the presentation clock. +// 4. Seek the video reader. +// 5. Under csAudioFeed: Stop WASAPI, seek audio reader, Reset WASAPI buffer. +// Holding csAudioFeed for all three ops guarantees the audio thread +// (which also takes this lock around ReadSample and GetBuffer) cannot +// interleave a stale sample into the freshly reset buffer. +// 6. Reset timing / audio state atomically. +// 7. If wasPlaying: pre-fill WASAPI, Start WASAPI (under lock), Start clock. +// If paused: leave WASAPI stopped, clock stopped, player quiet. +// 8. Clear bSeekInProgress (release barrier) and SignalResume if playing. +// --------------------------------------------------------------------------- +NATIVEVIDEOPLAYER_API HRESULT SeekMedia(VideoPlayerInstance* pInstance, LONGLONG llPosition) { if (!pInstance) return OP_E_NOT_INITIALIZED; + if (pInstance->pHLSPlayer) return pInstance->pHLSPlayer->Seek(llPosition); + if (!pInstance->pSourceReader) return OP_E_NOT_INITIALIZED; - if (pInstance->pHLSPlayer) - return pInstance->pHLSPlayer->Seek(llPositionIn100Ns); - - if (!pInstance->pSourceReader) - return OP_E_NOT_INITIALIZED; - - EnterCriticalSection(&pInstance->csClockSync); - pInstance->bSeekInProgress = TRUE; - LeaveCriticalSection(&pInstance->csClockSync); + if (llPosition < 0) llPosition = 0; - if (pInstance->llPauseStart != 0) { - pInstance->llTotalPauseTime += (GetCurrentTimeMs() - pInstance->llPauseStart); - pInstance->llPauseStart = GetCurrentTimeMs(); + // 1. Snapshot current playing state. + bool wasPlaying; + { + ScopedLock lock(pInstance->csClockSync); + wasPlaying = (pInstance->llPauseStart.load(std::memory_order_relaxed) == 0) + && (pInstance->llPlaybackStartTime.load(std::memory_order_relaxed) != 0); } - if (pInstance->pLockedBuffer) - UnlockVideoFrame(pInstance); + // 2. Announce seek. Audio thread will: + // - break out of its feed loop on next inner-loop iteration, + // - drop any post-ReadSample sample as stale. + pInstance->bSeekInProgress.store(true, std::memory_order_release); - // Release cached sample when seeking - if (pInstance->pCachedSample) { - pInstance->pCachedSample->Release(); - pInstance->pCachedSample = nullptr; - } - pInstance->bHasInitialFrame = FALSE; + // Defensive cleanups. + if (pInstance->pLockedBuffer) UnlockVideoFrame(pInstance); + pInstance->pCachedSample.Reset(); + pInstance->bHasInitialFrame = false; + // 3. Stop presentation clock. + if (pInstance->bUseClockSync && pInstance->pPresentationClock) + pInstance->pPresentationClock->Stop(); + + // 4. Seek video reader (no concurrent ReadVideoFrame thanks to Kotlin contract). PROPVARIANT var; PropVariantInit(&var); var.vt = VT_I8; - var.hVal.QuadPart = llPositionIn100Ns; - - bool wasPlaying = false; - if (pInstance->bHasAudio && pInstance->pAudioClient) { - wasPlaying = (pInstance->llPauseStart == 0); - // Stop WASAPI under csAudioFeed to ensure the audio thread is not - // in the middle of GetBuffer/ReleaseBuffer. - EnterCriticalSection(&pInstance->csAudioFeed); - pInstance->pAudioClient->Stop(); - LeaveCriticalSection(&pInstance->csAudioFeed); - } - - // Stop the presentation clock - if (pInstance->bUseClockSync && pInstance->pPresentationClock) { - pInstance->pPresentationClock->Stop(); - } - - // Seek video reader + var.hVal.QuadPart = llPosition; HRESULT hr = pInstance->pSourceReader->SetCurrentPosition(GUID_NULL, var); if (FAILED(hr)) { - EnterCriticalSection(&pInstance->csClockSync); - pInstance->bSeekInProgress = FALSE; - LeaveCriticalSection(&pInstance->csClockSync); + pInstance->bSeekInProgress.store(false, std::memory_order_release); PropVariantClear(&var); + if (wasPlaying) SignalResume(pInstance); return hr; } - // Seek audio reader independently — never blocks on video decoding - if (pInstance->pSourceReaderAudio) { - PROPVARIANT varAudio; - PropVariantInit(&varAudio); - varAudio.vt = VT_I8; - varAudio.hVal.QuadPart = llPositionIn100Ns; - pInstance->pSourceReaderAudio->SetCurrentPosition(GUID_NULL, varAudio); - PropVariantClear(&varAudio); - } + // Catch-up is now handled inside AcquireNextSample's internal loop; no + // separate fast-forward is needed here. - // Reset WASAPI buffer under csAudioFeed - if (pInstance->bHasAudio && pInstance->pRenderClient && pInstance->pAudioClient) { - EnterCriticalSection(&pInstance->csAudioFeed); - pInstance->pAudioClient->Reset(); - LeaveCriticalSection(&pInstance->csAudioFeed); + // 5. Atomic audio-side seek: Stop + SetCurrentPosition + Reset under one lock. + // Wake any audio-thread wait so it notices bSeekInProgress and bails out + // of its feed loop promptly — otherwise it may hold csAudioFeed for up to + // 10 ms (hAudioSamplesReadyEvent wait budget) while we're stuck waiting + // for the lock below. + if (pInstance->bHasAudio) { + if (pInstance->hAudioSamplesReadyEvent) + SetEvent(pInstance->hAudioSamplesReadyEvent.Get()); + ScopedLock lock(pInstance->csAudioFeed); + if (pInstance->pAudioClient) pInstance->pAudioClient->Stop(); + if (pInstance->pSourceReaderAudio) + pInstance->pSourceReaderAudio->SetCurrentPosition(GUID_NULL, var); + if (pInstance->pAudioClient) pInstance->pAudioClient->Reset(); } - PropVariantClear(&var); - pInstance->bEOF = FALSE; + // 6. Reset state. + pInstance->bEOF.store(false, std::memory_order_relaxed); pInstance->resampleFracPos = 0.0; pInstance->audioLatencyMs.store(0.0, std::memory_order_relaxed); + pInstance->llCurrentPosition.store(llPosition, std::memory_order_relaxed); - // Reset timing for A/V sync after seek. - EnterCriticalSection(&pInstance->csClockSync); - pInstance->llCurrentPosition = llPositionIn100Ns; - if (pInstance->bUseClockSync) { - double seekPositionMs = llPositionIn100Ns / 10000.0; - double adjustedSeekMs = seekPositionMs / static_cast(pInstance->playbackSpeed.load()); - pInstance->llPlaybackStartTime = GetCurrentTimeMs() - static_cast(adjustedSeekMs); - pInstance->llTotalPauseTime = 0; - - if (!wasPlaying) { - pInstance->llPauseStart = GetCurrentTimeMs(); - } else { - pInstance->llPauseStart = 0; + { + ScopedLock lock(pInstance->csClockSync); + const float speed = pInstance->playbackSpeed.load(std::memory_order_relaxed); + const ULONGLONG now = GetCurrentTimeMs(); + const double posMs = llPosition / 10000.0; + const double adjMs = posMs / static_cast(speed); + const ULONGLONG startT = (static_cast(adjMs) >= now) + ? 0ULL : (now - static_cast(adjMs)); + pInstance->llPlaybackStartTime.store(startT, std::memory_order_relaxed); + pInstance->llTotalPauseTime.store(0, std::memory_order_relaxed); + pInstance->llPauseStart.store(wasPlaying ? 0 : now, std::memory_order_relaxed); + } + + // 7. Resume or stay paused. + if (wasPlaying) { + if (pInstance->bHasAudio && pInstance->bAudioInitialized) { + // Pre-fill runs under csAudioFeed (recursive CS, safe to re-enter). + PreFillAudioBuffer(pInstance); } - } - pInstance->bSeekInProgress = FALSE; - LeaveCriticalSection(&pInstance->csClockSync); - // Restart the presentation clock - if (pInstance->bUseClockSync && pInstance->pPresentationClock) { - hr = pInstance->pPresentationClock->Start(llPositionIn100Ns); - if (FAILED(hr)) { - PrintHR("Failed to restart presentation clock after seek", hr); + if (pInstance->bHasAudio && pInstance->pAudioClient) { + ScopedLock lock(pInstance->csAudioFeed); + pInstance->pAudioClient->Start(); } + if (pInstance->bUseClockSync && pInstance->pPresentationClock) + pInstance->pPresentationClock->Start(llPosition); } - // Pre-fill the WASAPI buffer BEFORE Start() so audio plays immediately - // with no gap. This is the key to stutter-free seek: the buffer has - // ~100ms of audio ready before the hardware starts consuming. - if (pInstance->bHasAudio && pInstance->bAudioInitialized) { - PreFillAudioBuffer(pInstance); - } - - // Now start the audio client — buffer already has data, no gap - if (pInstance->bHasAudio && pInstance->pAudioClient && wasPlaying) { - EnterCriticalSection(&pInstance->csAudioFeed); - pInstance->pAudioClient->Start(); - LeaveCriticalSection(&pInstance->csAudioFeed); - } - - // Signal audio thread to resume its feed loop - if (pInstance->hAudioReadyEvent) - SetEvent(pInstance->hAudioReadyEvent); - if (pInstance->hAudioSamplesReadyEvent) - SetEvent(pInstance->hAudioSamplesReadyEvent); - + // 8. Release barrier. + pInstance->bSeekInProgress.store(false, std::memory_order_release); + if (wasPlaying) SignalResume(pInstance); return S_OK; } NATIVEVIDEOPLAYER_API HRESULT GetMediaDuration(const VideoPlayerInstance* pInstance, LONGLONG* pDuration) { if (!pInstance || !pDuration) return OP_E_NOT_INITIALIZED; + if (pInstance->pHLSPlayer) return pInstance->pHLSPlayer->GetDuration(pDuration); + if (!pInstance->pSourceReader) return OP_E_NOT_INITIALIZED; - if (pInstance->pHLSPlayer) - return pInstance->pHLSPlayer->GetDuration(pDuration); + *pDuration = 0; - if (!pInstance->pSourceReader) - return OP_E_NOT_INITIALIZED; + ComPtr source; + HRESULT hr = pInstance->pSourceReader->GetServiceForStream( + MF_SOURCE_READER_MEDIASOURCE, GUID_NULL, IID_PPV_ARGS(source.GetAddressOf())); + if (FAILED(hr)) return hr; - *pDuration = 0; + ComPtr pd; + hr = source->CreatePresentationDescriptor(pd.GetAddressOf()); + if (FAILED(hr)) return hr; - IMFMediaSource* pMediaSource = nullptr; - IMFPresentationDescriptor* pPresentationDescriptor = nullptr; - HRESULT hr = pInstance->pSourceReader->GetServiceForStream(MF_SOURCE_READER_MEDIASOURCE, GUID_NULL, IID_PPV_ARGS(&pMediaSource)); - if (SUCCEEDED(hr)) { - hr = pMediaSource->CreatePresentationDescriptor(&pPresentationDescriptor); - if (SUCCEEDED(hr)) { - HRESULT hrDur = pPresentationDescriptor->GetUINT64(MF_PD_DURATION, reinterpret_cast(pDuration)); - if (FAILED(hrDur)) { - // Duration unavailable — live HLS stream or network source - *pDuration = 0; - } - pPresentationDescriptor->Release(); - } - pMediaSource->Release(); + UINT64 dur = 0; + hr = pd->GetUINT64(MF_PD_DURATION, &dur); + if (FAILED(hr)) { + // Live / duration-less source — distinguish from a hard error by + // returning S_FALSE with pDuration=0 so callers can gate HLS-style + // behavior without treating it as a failure. + return S_FALSE; } - // Return S_OK even when duration is 0 (live stream) — caller checks the value + *pDuration = static_cast(dur); return S_OK; } NATIVEVIDEOPLAYER_API HRESULT GetMediaPosition(const VideoPlayerInstance* pInstance, LONGLONG* pPosition) { - if (!pInstance || !pPosition) - return OP_E_NOT_INITIALIZED; - - if (pInstance->pHLSPlayer) - return pInstance->pHLSPlayer->GetPosition(pPosition); - - *pPosition = pInstance->llCurrentPosition; + if (!pInstance || !pPosition) return OP_E_NOT_INITIALIZED; + if (pInstance->pHLSPlayer) return pInstance->pHLSPlayer->GetPosition(pPosition); + *pPosition = pInstance->llCurrentPosition.load(std::memory_order_relaxed); return S_OK; } NATIVEVIDEOPLAYER_API HRESULT SetPlaybackState(VideoPlayerInstance* pInstance, BOOL bPlaying, BOOL bStop) { - if (!pInstance) - return OP_E_NOT_INITIALIZED; - - if (pInstance->pHLSPlayer) - return pInstance->pHLSPlayer->SetPlaying(bPlaying, bStop); + if (!pInstance) return OP_E_NOT_INITIALIZED; + if (pInstance->pHLSPlayer) return pInstance->pHLSPlayer->SetPlaying(bPlaying, bStop); HRESULT hr = S_OK; if (bStop && !bPlaying) { - // Stop playback completely - if (pInstance->llPlaybackStartTime != 0) { - pInstance->llTotalPauseTime = 0; - pInstance->llPauseStart = 0; - pInstance->llPlaybackStartTime = 0; - - if (pInstance->bUseClockSync && pInstance->pPresentationClock) { - pInstance->pPresentationClock->Stop(); - } + if (pInstance->llPlaybackStartTime.load(std::memory_order_relaxed) != 0) { + pInstance->llTotalPauseTime.store(0, std::memory_order_relaxed); + pInstance->llPauseStart.store(0, std::memory_order_relaxed); + pInstance->llPlaybackStartTime.store(0, std::memory_order_relaxed); - if (pInstance->bAudioThreadRunning) { - StopAudioThread(pInstance); - } + // Stop the audio thread BEFORE the presentation clock: otherwise + // the audio thread keeps calling GetCurrentPadding on an audio + // client whose clock was just stopped, yielding spurious errors. + if (pInstance->bAudioThreadRunning.load()) StopAudioThread(pInstance); - pInstance->bHasInitialFrame = FALSE; + if (pInstance->bUseClockSync && pInstance->pPresentationClock) + pInstance->pPresentationClock->Stop(); - if (pInstance->pCachedSample) { - pInstance->pCachedSample->Release(); - pInstance->pCachedSample = nullptr; - } + pInstance->bHasInitialFrame = false; + pInstance->pCachedSample.Reset(); } } else if (bPlaying) { - // Start or resume playback - if (pInstance->llPlaybackStartTime == 0) { - pInstance->llPlaybackStartTime = GetCurrentTimeMs(); - } else if (pInstance->llPauseStart != 0) { - pInstance->llTotalPauseTime += (GetCurrentTimeMs() - pInstance->llPauseStart); - pInstance->llPauseStart = 0; + if (pInstance->llPlaybackStartTime.load(std::memory_order_relaxed) == 0) { + pInstance->llPlaybackStartTime.store(GetCurrentTimeMs(), std::memory_order_relaxed); + } else { + const ULONGLONG ps = pInstance->llPauseStart.load(std::memory_order_relaxed); + if (ps != 0) { + pInstance->llTotalPauseTime.fetch_add(GetCurrentTimeMs() - ps, std::memory_order_relaxed); + pInstance->llPauseStart.store(0, std::memory_order_relaxed); + } } - pInstance->bHasInitialFrame = FALSE; + pInstance->bHasInitialFrame = false; - // Start audio client if available (under csAudioFeed for thread safety) if (pInstance->pAudioClient && pInstance->bAudioInitialized) { - EnterCriticalSection(&pInstance->csAudioFeed); + ScopedLock lock(pInstance->csAudioFeed); hr = pInstance->pAudioClient->Start(); - LeaveCriticalSection(&pInstance->csAudioFeed); - if (FAILED(hr)) { - PrintHR("Failed to start audio client", hr); - } + if (FAILED(hr)) PrintHR("Failed to start audio client", hr); } - // Start audio thread if it is not already running - // (important when the player was opened in paused state and then play() is called) if (pInstance->bHasAudio && pInstance->bAudioInitialized && pInstance->pSourceReaderAudio) { - if (!pInstance->bAudioThreadRunning || pInstance->hAudioThread == nullptr) { - hr = StartAudioThread(pInstance); - if (FAILED(hr)) { - PrintHR("Failed to start audio thread on play", hr); - } + if (!pInstance->bAudioThreadRunning.load() || !pInstance->hAudioThread) { + HRESULT hrT = StartAudioThread(pInstance); + if (FAILED(hrT)) PrintHR("Failed to start audio thread", hrT); } } - // Start or resume presentation clock from the current stored position if (pInstance->bUseClockSync && pInstance->pPresentationClock) { - hr = pInstance->pPresentationClock->Start(pInstance->llCurrentPosition); - if (FAILED(hr)) { - PrintHR("Failed to start presentation clock", hr); - } + hr = pInstance->pPresentationClock->Start( + pInstance->llCurrentPosition.load(std::memory_order_relaxed)); + if (FAILED(hr)) PrintHR("Failed to start presentation clock", hr); } - if (pInstance->hAudioReadyEvent) - SetEvent(pInstance->hAudioReadyEvent); - if (pInstance->hAudioSamplesReadyEvent) - SetEvent(pInstance->hAudioSamplesReadyEvent); + SignalResume(pInstance); } else { - // Pause playback - if (pInstance->llPauseStart == 0) { - pInstance->llPauseStart = GetCurrentTimeMs(); - } + if (pInstance->llPauseStart.load(std::memory_order_relaxed) == 0) + pInstance->llPauseStart.store(GetCurrentTimeMs(), std::memory_order_relaxed); - pInstance->bHasInitialFrame = FALSE; + pInstance->bHasInitialFrame = false; if (pInstance->pAudioClient && pInstance->bAudioInitialized) { - EnterCriticalSection(&pInstance->csAudioFeed); + ScopedLock lock(pInstance->csAudioFeed); pInstance->pAudioClient->Stop(); - LeaveCriticalSection(&pInstance->csAudioFeed); } + if (pInstance->bUseClockSync && pInstance->pPresentationClock) + pInstance->pPresentationClock->Pause(); - if (pInstance->bUseClockSync && pInstance->pPresentationClock) { - hr = pInstance->pPresentationClock->Pause(); - if (FAILED(hr)) { - PrintHR("Failed to pause presentation clock", hr); - } - } - // Note: the audio thread is not stopped on pause — it simply waits on sync events + SignalPause(pInstance); } return hr; } -NATIVEVIDEOPLAYER_API HRESULT ShutdownMediaFoundation() { - return Shutdown(); -} +NATIVEVIDEOPLAYER_API HRESULT ShutdownMediaFoundation() { return Shutdown(); } NATIVEVIDEOPLAYER_API void CloseMedia(VideoPlayerInstance* pInstance) { - if (!pInstance) - return; + if (!pInstance) return; - // Shut down HLS player first (before releasing D3D resources) if (pInstance->pHLSPlayer) { - pInstance->pHLSPlayer->Close(); - delete pInstance->pHLSPlayer; - pInstance->pHLSPlayer = nullptr; + pInstance->pHLSPlayer.Reset(); // dtor handles Close() } StopAudioThread(pInstance); - if (pInstance->pLockedBuffer) { - UnlockVideoFrame(pInstance); - } - - if (pInstance->pCachedSample) { - pInstance->pCachedSample->Release(); - pInstance->pCachedSample = nullptr; - } - pInstance->bHasInitialFrame = FALSE; - - #define SAFE_RELEASE(obj) if (obj) { obj->Release(); obj = nullptr; } + if (pInstance->pLockedBuffer) UnlockVideoFrame(pInstance); + pInstance->pCachedSample.Reset(); + pInstance->bHasInitialFrame = false; if (pInstance->pAudioClient) { pInstance->pAudioClient->Stop(); - SAFE_RELEASE(pInstance->pAudioClient); + pInstance->pAudioClient.Reset(); } if (pInstance->pPresentationClock) { pInstance->pPresentationClock->Stop(); - SAFE_RELEASE(pInstance->pPresentationClock); + pInstance->pPresentationClock.Reset(); } - SAFE_RELEASE(pInstance->pMediaSource); - SAFE_RELEASE(pInstance->pRenderClient); - SAFE_RELEASE(pInstance->pDevice); - SAFE_RELEASE(pInstance->pAudioEndpointVolume); - SAFE_RELEASE(pInstance->pSourceReader); - SAFE_RELEASE(pInstance->pSourceReaderAudio); + pInstance->pMediaSource.Reset(); + pInstance->pRenderClient.Reset(); + pInstance->pDevice.Reset(); + pInstance->pAudioEndpointVolume.Reset(); + pInstance->pSourceReader.Reset(); + pInstance->pSourceReaderAudio.Reset(); if (pInstance->pSourceAudioFormat) { CoTaskMemFree(pInstance->pSourceAudioFormat); pInstance->pSourceAudioFormat = nullptr; } - #define SAFE_CLOSE_HANDLE(handle) if (handle) { CloseHandle(handle); handle = nullptr; } - - SAFE_CLOSE_HANDLE(pInstance->hAudioSamplesReadyEvent); - SAFE_CLOSE_HANDLE(pInstance->hAudioReadyEvent); + pInstance->hAudioSamplesReadyEvent.Reset(); + pInstance->hAudioResumeEvent.Reset(); - pInstance->bEOF = FALSE; + pInstance->bEOF.store(false); pInstance->videoWidth = pInstance->videoHeight = 0; - pInstance->bHasAudio = FALSE; - pInstance->bAudioInitialized = FALSE; - pInstance->llPlaybackStartTime = 0; - pInstance->llTotalPauseTime = 0; - pInstance->llPauseStart = 0; - pInstance->llCurrentPosition = 0; - pInstance->bSeekInProgress = FALSE; - pInstance->playbackSpeed = 1.0f; + pInstance->bHasAudio = false; + pInstance->bAudioInitialized = false; + pInstance->llPlaybackStartTime.store(0, std::memory_order_relaxed); + pInstance->llTotalPauseTime.store(0, std::memory_order_relaxed); + pInstance->llPauseStart.store(0, std::memory_order_relaxed); + pInstance->llCurrentPosition.store(0, std::memory_order_relaxed); + pInstance->bSeekInProgress.store(false, std::memory_order_relaxed); + pInstance->playbackSpeed.store(1.0f, std::memory_order_relaxed); pInstance->resampleFracPos = 0.0; pInstance->audioLatencyMs.store(0.0, std::memory_order_relaxed); - pInstance->bIsNetworkSource = FALSE; - pInstance->bIsLiveStream = FALSE; - - #undef SAFE_RELEASE - #undef SAFE_CLOSE_HANDLE + pInstance->bIsNetworkSource = false; + pInstance->bIsLiveStream = false; } NATIVEVIDEOPLAYER_API HRESULT SetAudioVolume(VideoPlayerInstance* pInstance, float volume) { - if (pInstance && pInstance->pHLSPlayer) - return pInstance->pHLSPlayer->SetVolume(volume); + if (pInstance && pInstance->pHLSPlayer) return pInstance->pHLSPlayer->SetVolume(volume); return SetVolume(pInstance, volume); } NATIVEVIDEOPLAYER_API HRESULT GetAudioVolume(const VideoPlayerInstance* pInstance, float* volume) { - if (pInstance && pInstance->pHLSPlayer) - return pInstance->pHLSPlayer->GetVolume(volume); + if (pInstance && pInstance->pHLSPlayer) return pInstance->pHLSPlayer->GetVolume(volume); return GetVolume(pInstance, volume); } NATIVEVIDEOPLAYER_API HRESULT SetPlaybackSpeed(VideoPlayerInstance* pInstance, float speed) { - if (!pInstance) - return OP_E_NOT_INITIALIZED; + if (!pInstance) return OP_E_NOT_INITIALIZED; + if (pInstance->pHLSPlayer) return pInstance->pHLSPlayer->SetPlaybackSpeed(speed); - if (pInstance->pHLSPlayer) - return pInstance->pHLSPlayer->SetPlaybackSpeed(speed); - - speed = std::max(0.5f, std::min(speed, 2.0f)); - - // Recalibrate the wall-clock reference so that the position accumulated - // at the old speed is preserved when switching to the new speed. - // Without this, `elapsed * newSpeed` would produce a wrong position. - if (pInstance->bUseClockSync && pInstance->llPlaybackStartTime != 0) { - float oldSpeed = pInstance->playbackSpeed.load(); - EnterCriticalSection(&pInstance->csClockSync); - LONGLONG now = GetCurrentTimeMs(); - LONGLONG elapsedMs = now - pInstance->llPlaybackStartTime - pInstance->llTotalPauseTime; - double currentPositionMs = elapsedMs * static_cast(oldSpeed); - // Solve: (now - newStart - pause) * newSpeed = currentPositionMs - pInstance->llPlaybackStartTime = now - pInstance->llTotalPauseTime - - static_cast(currentPositionMs / speed); - LeaveCriticalSection(&pInstance->csClockSync); + speed = std::clamp(speed, NVP_MIN_PLAYBACK_SPEED, NVP_MAX_PLAYBACK_SPEED); + + if (pInstance->bUseClockSync + && pInstance->llPlaybackStartTime.load(std::memory_order_relaxed) != 0) { + const float oldSpeed = pInstance->playbackSpeed.load(std::memory_order_relaxed); + ScopedLock lock(pInstance->csClockSync); + const ULONGLONG now = GetCurrentTimeMs(); + const ULONGLONG startT = pInstance->llPlaybackStartTime.load(std::memory_order_relaxed); + const ULONGLONG pauseT = pInstance->llTotalPauseTime.load(std::memory_order_relaxed); + const LONGLONG elapsedMs = static_cast(now - startT - pauseT); + const double currentPosMs = elapsedMs * static_cast(oldSpeed); + pInstance->llPlaybackStartTime.store( + now - pauseT - static_cast(currentPosMs / speed), + std::memory_order_relaxed); } - pInstance->playbackSpeed = speed; + pInstance->playbackSpeed.store(speed, std::memory_order_relaxed); pInstance->resampleFracPos = 0.0; if (pInstance->bUseClockSync && pInstance->pPresentationClock) { - IMFRateControl* pRateControl = nullptr; - HRESULT hr = pInstance->pPresentationClock->QueryInterface(IID_PPV_ARGS(&pRateControl)); - if (SUCCEEDED(hr)) { - hr = pRateControl->SetRate(FALSE, speed); - if (FAILED(hr)) { - PrintHR("Failed to set presentation clock rate", hr); - } - pRateControl->Release(); - } + ComPtr rateControl; + if (SUCCEEDED(pInstance->pPresentationClock.As(&rateControl))) + rateControl->SetRate(FALSE, speed); } - return S_OK; } NATIVEVIDEOPLAYER_API HRESULT GetPlaybackSpeed(const VideoPlayerInstance* pInstance, float* pSpeed) { - if (!pInstance || !pSpeed) - return OP_E_INVALID_PARAMETER; - - if (pInstance->pHLSPlayer) - return pInstance->pHLSPlayer->GetPlaybackSpeed(pSpeed); - - *pSpeed = pInstance->playbackSpeed; + if (!pInstance || !pSpeed) return OP_E_INVALID_PARAMETER; + if (pInstance->pHLSPlayer) return pInstance->pHLSPlayer->GetPlaybackSpeed(pSpeed); + *pSpeed = pInstance->playbackSpeed.load(std::memory_order_relaxed); return S_OK; } // --------------------------------------------------------------------------- -// GetVideoMetadata — retrieves all available metadata (issue #5: improved) +// Metadata // --------------------------------------------------------------------------- +static const wchar_t* MimeTypeForSubtype(const GUID& s) { + if (s == MFVideoFormat_H264) return L"video/h264"; + if (s == MFVideoFormat_HEVC) return L"video/hevc"; + if (s == MFVideoFormat_MPEG2) return L"video/mpeg2"; + if (s == MFVideoFormat_WMV3 || s == MFVideoFormat_WMV2 || s == MFVideoFormat_WMV1) + return L"video/x-ms-wmv"; + if (s == MFVideoFormat_VP80) return L"video/vp8"; + if (s == MFVideoFormat_VP90) return L"video/vp9"; + if (s == MFVideoFormat_MJPG) return L"video/x-motion-jpeg"; + if (s == MFVideoFormat_MP4V) return L"video/mp4v-es"; + if (s == MFVideoFormat_MP43) return L"video/x-msmpeg4v3"; + return L"video/unknown"; +} + NATIVEVIDEOPLAYER_API HRESULT GetVideoMetadata(const VideoPlayerInstance* pInstance, VideoMetadata* pMetadata) { - if (!pInstance || !pMetadata) - return OP_E_INVALID_PARAMETER; + if (!pInstance || !pMetadata) return OP_E_INVALID_PARAMETER; - // HLS path: build basic metadata from engine properties if (pInstance->pHLSPlayer) { ZeroMemory(pMetadata, sizeof(VideoMetadata)); pInstance->pHLSPlayer->GetVideoSize(&pMetadata->width, &pMetadata->height); - pMetadata->hasWidth = pMetadata->width > 0; + pMetadata->hasWidth = pMetadata->width > 0; pMetadata->hasHeight = pMetadata->height > 0; LONGLONG dur = 0; if (SUCCEEDED(pInstance->pHLSPlayer->GetDuration(&dur)) && dur > 0) { @@ -1369,193 +1124,122 @@ NATIVEVIDEOPLAYER_API HRESULT GetVideoMetadata(const VideoPlayerInstance* pInsta return S_OK; } - if (!pInstance->pSourceReader) - return OP_E_NOT_INITIALIZED; + if (!pInstance->pSourceReader) return OP_E_NOT_INITIALIZED; ZeroMemory(pMetadata, sizeof(VideoMetadata)); - HRESULT hr = S_OK; - IMFMediaSource* pMediaSource = nullptr; - IMFPresentationDescriptor* pPresentationDescriptor = nullptr; + ComPtr source; + HRESULT hr = pInstance->pSourceReader->GetServiceForStream( + MF_SOURCE_READER_MEDIASOURCE, GUID_NULL, IID_PPV_ARGS(source.GetAddressOf())); - hr = pInstance->pSourceReader->GetServiceForStream( - MF_SOURCE_READER_MEDIASOURCE, - GUID_NULL, - IID_PPV_ARGS(&pMediaSource)); - - if (SUCCEEDED(hr) && pMediaSource) { - hr = pMediaSource->CreatePresentationDescriptor(&pPresentationDescriptor); - - if (SUCCEEDED(hr) && pPresentationDescriptor) { - // Duration + if (SUCCEEDED(hr) && source) { + ComPtr pd; + if (SUCCEEDED(source->CreatePresentationDescriptor(pd.GetAddressOf()))) { UINT64 duration = 0; - if (SUCCEEDED(pPresentationDescriptor->GetUINT64(MF_PD_DURATION, &duration))) { + if (SUCCEEDED(pd->GetUINT64(MF_PD_DURATION, &duration))) { pMetadata->duration = static_cast(duration); pMetadata->hasDuration = TRUE; } - // ---- Title via IMFMetadataProvider (issue #5) ---- - IMFMetadataProvider* pMetaProvider = nullptr; - hr = MFGetService(pMediaSource, MF_METADATA_PROVIDER_SERVICE, - IID_PPV_ARGS(&pMetaProvider)); - if (SUCCEEDED(hr) && pMetaProvider) { - IMFMetadata* pMeta = nullptr; - hr = pMetaProvider->GetMFMetadata(pPresentationDescriptor, 0, 0, &pMeta); - if (SUCCEEDED(hr) && pMeta) { + // Title + ComPtr metaProvider; + if (SUCCEEDED(MFGetService(source.Get(), MF_METADATA_PROVIDER_SERVICE, + IID_PPV_ARGS(metaProvider.GetAddressOf())))) { + ComPtr meta; + if (SUCCEEDED(metaProvider->GetMFMetadata(pd.Get(), 0, 0, meta.GetAddressOf())) && meta) { PROPVARIANT valTitle; PropVariantInit(&valTitle); - if (SUCCEEDED(pMeta->GetProperty(L"Title", &valTitle)) && - valTitle.vt == VT_LPWSTR && valTitle.pwszVal) { + if (SUCCEEDED(meta->GetProperty(L"Title", &valTitle)) + && valTitle.vt == VT_LPWSTR && valTitle.pwszVal) { wcsncpy_s(pMetadata->title, valTitle.pwszVal, _TRUNCATE); pMetadata->hasTitle = TRUE; } PropVariantClear(&valTitle); - pMeta->Release(); } - pMetaProvider->Release(); } - // Process each stream for video/audio metadata + // Streams DWORD streamCount = 0; - hr = pPresentationDescriptor->GetStreamDescriptorCount(&streamCount); - + pd->GetStreamDescriptorCount(&streamCount); LONGLONG totalBitrate = 0; - bool hasBitrateInfo = false; - - if (SUCCEEDED(hr)) { - for (DWORD i = 0; i < streamCount; i++) { - BOOL selected = FALSE; - IMFStreamDescriptor* pStreamDescriptor = nullptr; - - if (SUCCEEDED(pPresentationDescriptor->GetStreamDescriptorByIndex(i, &selected, &pStreamDescriptor))) { - IMFMediaTypeHandler* pHandler = nullptr; - if (SUCCEEDED(pStreamDescriptor->GetMediaTypeHandler(&pHandler))) { - GUID majorType; - if (SUCCEEDED(pHandler->GetMajorType(&majorType))) { - if (majorType == MFMediaType_Video) { - IMFMediaType* pMediaType = nullptr; - if (SUCCEEDED(pHandler->GetCurrentMediaType(&pMediaType))) { - // Dimensions - UINT32 width = 0, height = 0; - if (SUCCEEDED(MFGetAttributeSize(pMediaType, MF_MT_FRAME_SIZE, &width, &height))) { - pMetadata->width = width; - pMetadata->height = height; - pMetadata->hasWidth = TRUE; - pMetadata->hasHeight = TRUE; - } - - // Frame rate - UINT32 numerator = 0, denominator = 1; - if (SUCCEEDED(MFGetAttributeRatio(pMediaType, MF_MT_FRAME_RATE, &numerator, &denominator))) { - if (denominator > 0) { - pMetadata->frameRate = static_cast(numerator) / static_cast(denominator); - pMetadata->hasFrameRate = TRUE; - } - } - - // Video bitrate (issue #5) - UINT32 videoBitrate = 0; - if (SUCCEEDED(pMediaType->GetUINT32(MF_MT_AVG_BITRATE, &videoBitrate))) { - totalBitrate += videoBitrate; - hasBitrateInfo = true; - } - - // MIME type from codec subtype (issue #5: extended mapping) - GUID subtype; - if (SUCCEEDED(pMediaType->GetGUID(MF_MT_SUBTYPE, &subtype))) { - if (subtype == MFVideoFormat_H264) { - wcscpy_s(pMetadata->mimeType, L"video/h264"); - } else if (subtype == MFVideoFormat_HEVC) { - wcscpy_s(pMetadata->mimeType, L"video/hevc"); - } else if (subtype == MFVideoFormat_MPEG2) { - wcscpy_s(pMetadata->mimeType, L"video/mpeg2"); - } else if (subtype == MFVideoFormat_WMV3) { - wcscpy_s(pMetadata->mimeType, L"video/x-ms-wmv"); - } else if (subtype == MFVideoFormat_WMV2) { - wcscpy_s(pMetadata->mimeType, L"video/x-ms-wmv"); - } else if (subtype == MFVideoFormat_WMV1) { - wcscpy_s(pMetadata->mimeType, L"video/x-ms-wmv"); - } else if (subtype == MFVideoFormat_VP80) { - wcscpy_s(pMetadata->mimeType, L"video/vp8"); - } else if (subtype == MFVideoFormat_VP90) { - wcscpy_s(pMetadata->mimeType, L"video/vp9"); - } else if (subtype == MFVideoFormat_MJPG) { - wcscpy_s(pMetadata->mimeType, L"video/x-motion-jpeg"); - } else if (subtype == MFVideoFormat_MP4V) { - wcscpy_s(pMetadata->mimeType, L"video/mp4v-es"); - } else if (subtype == MFVideoFormat_MP43) { - wcscpy_s(pMetadata->mimeType, L"video/x-msmpeg4v3"); - } else { - wcscpy_s(pMetadata->mimeType, L"video/unknown"); - } - pMetadata->hasMimeType = TRUE; - } - - pMediaType->Release(); - } - } - else if (majorType == MFMediaType_Audio) { - IMFMediaType* pMediaType = nullptr; - if (SUCCEEDED(pHandler->GetCurrentMediaType(&pMediaType))) { - UINT32 channels = 0; - if (SUCCEEDED(pMediaType->GetUINT32(MF_MT_AUDIO_NUM_CHANNELS, &channels))) { - pMetadata->audioChannels = channels; - pMetadata->hasAudioChannels = TRUE; - } - - UINT32 sampleRate = 0; - if (SUCCEEDED(pMediaType->GetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, &sampleRate))) { - pMetadata->audioSampleRate = sampleRate; - pMetadata->hasAudioSampleRate = TRUE; - } - - // Audio bitrate (issue #5) - UINT32 audioBytesPerSec = 0; - if (SUCCEEDED(pMediaType->GetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND, &audioBytesPerSec))) { - totalBitrate += static_cast(audioBytesPerSec) * 8; - hasBitrateInfo = true; - } - - pMediaType->Release(); - } - } - } - pHandler->Release(); - } - pStreamDescriptor->Release(); + bool hasBitrate = false; + + for (DWORD i = 0; i < streamCount; ++i) { + BOOL selected = FALSE; + ComPtr sd; + if (FAILED(pd->GetStreamDescriptorByIndex(i, &selected, sd.GetAddressOf()))) continue; + + ComPtr handler; + if (FAILED(sd->GetMediaTypeHandler(handler.GetAddressOf()))) continue; + + GUID major{}; + if (FAILED(handler->GetMajorType(&major))) continue; + + ComPtr mt; + if (FAILED(handler->GetCurrentMediaType(mt.GetAddressOf()))) continue; + + if (major == MFMediaType_Video) { + UINT32 w = 0, h = 0; + if (SUCCEEDED(MFGetAttributeSize(mt.Get(), MF_MT_FRAME_SIZE, &w, &h))) { + pMetadata->width = w; pMetadata->height = h; + pMetadata->hasWidth = TRUE; pMetadata->hasHeight = TRUE; + } + UINT32 num = 0, den = 1; + if (SUCCEEDED(MFGetAttributeRatio(mt.Get(), MF_MT_FRAME_RATE, &num, &den)) && den > 0) { + pMetadata->frameRate = static_cast(num) / den; + pMetadata->hasFrameRate = TRUE; + } + UINT32 vb = 0; + if (SUCCEEDED(mt->GetUINT32(MF_MT_AVG_BITRATE, &vb))) { + totalBitrate += vb; + hasBitrate = true; + } + GUID sub{}; + if (SUCCEEDED(mt->GetGUID(MF_MT_SUBTYPE, &sub))) { + wcscpy_s(pMetadata->mimeType, MimeTypeForSubtype(sub)); + pMetadata->hasMimeType = TRUE; + } + } else if (major == MFMediaType_Audio) { + UINT32 ch = 0; + if (SUCCEEDED(mt->GetUINT32(MF_MT_AUDIO_NUM_CHANNELS, &ch))) { + pMetadata->audioChannels = ch; + pMetadata->hasAudioChannels = TRUE; + } + UINT32 sr = 0; + if (SUCCEEDED(mt->GetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, &sr))) { + pMetadata->audioSampleRate = sr; + pMetadata->hasAudioSampleRate = TRUE; + } + UINT32 abps = 0; + if (SUCCEEDED(mt->GetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND, &abps))) { + totalBitrate += static_cast(abps) * 8; + hasBitrate = true; } } } - // Report combined bitrate if we gathered any info - if (hasBitrateInfo) { + if (hasBitrate) { pMetadata->bitrate = totalBitrate; pMetadata->hasBitrate = TRUE; } - - pPresentationDescriptor->Release(); } - pMediaSource->Release(); } - // Fallback: fill in from instance state if the media source did not provide values + // Fallbacks if (!pMetadata->hasWidth || !pMetadata->hasHeight) { if (pInstance->videoWidth > 0 && pInstance->videoHeight > 0) { pMetadata->width = pInstance->videoWidth; pMetadata->height = pInstance->videoHeight; - pMetadata->hasWidth = TRUE; - pMetadata->hasHeight = TRUE; + pMetadata->hasWidth = TRUE; pMetadata->hasHeight = TRUE; } } - if (!pMetadata->hasFrameRate) { - UINT numerator = 0, denominator = 1; - if (SUCCEEDED(GetVideoFrameRate(pInstance, &numerator, &denominator)) && denominator > 0) { - pMetadata->frameRate = static_cast(numerator) / static_cast(denominator); + UINT num = 0, den = 1; + if (SUCCEEDED(GetVideoFrameRate(pInstance, &num, &den)) && den > 0) { + pMetadata->frameRate = static_cast(num) / den; pMetadata->hasFrameRate = TRUE; } } - if (!pMetadata->hasDuration) { LONGLONG dur = 0; if (SUCCEEDED(GetMediaDuration(pInstance, &dur))) { @@ -1563,105 +1247,68 @@ NATIVEVIDEOPLAYER_API HRESULT GetVideoMetadata(const VideoPlayerInstance* pInsta pMetadata->hasDuration = TRUE; } } - if (!pMetadata->hasAudioChannels && pInstance->bHasAudio && pInstance->pSourceAudioFormat) { pMetadata->audioChannels = pInstance->pSourceAudioFormat->nChannels; pMetadata->hasAudioChannels = TRUE; pMetadata->audioSampleRate = pInstance->pSourceAudioFormat->nSamplesPerSec; pMetadata->hasAudioSampleRate = TRUE; } - return S_OK; } -// --------------------------------------------------------------------------- -// SetOutputSize — reconfigure the source reader to produce scaled frames // --------------------------------------------------------------------------- NATIVEVIDEOPLAYER_API HRESULT SetOutputSize(VideoPlayerInstance* pInstance, UINT32 targetWidth, UINT32 targetHeight) { if (!pInstance) return OP_E_NOT_INITIALIZED; if (pInstance->pHLSPlayer) { HRESULT hr = pInstance->pHLSPlayer->SetOutputSize(targetWidth, targetHeight); - if (SUCCEEDED(hr)) { + if (SUCCEEDED(hr)) pInstance->pHLSPlayer->GetVideoSize(&pInstance->videoWidth, &pInstance->videoHeight); - } return hr; } + if (!pInstance->pSourceReader) return OP_E_NOT_INITIALIZED; - if (!pInstance->pSourceReader) - return OP_E_NOT_INITIALIZED; - - // 0,0 means "reset to native resolution" if (targetWidth == 0 || targetHeight == 0) { targetWidth = pInstance->nativeWidth; targetHeight = pInstance->nativeHeight; } - - // Don't scale UP beyond the native resolution if (targetWidth > pInstance->nativeWidth || targetHeight > pInstance->nativeHeight) { targetWidth = pInstance->nativeWidth; targetHeight = pInstance->nativeHeight; } - // Preserve aspect ratio: fit inside the target bounding box if (pInstance->nativeWidth > 0 && pInstance->nativeHeight > 0) { - double srcAspect = static_cast(pInstance->nativeWidth) / pInstance->nativeHeight; - double dstAspect = static_cast(targetWidth) / targetHeight; - if (srcAspect > dstAspect) { - // Width-limited - targetHeight = static_cast(targetWidth / srcAspect); - } else { - // Height-limited - targetWidth = static_cast(targetHeight * srcAspect); - } + const double srcAspect = static_cast(pInstance->nativeWidth) / pInstance->nativeHeight; + const double dstAspect = static_cast(targetWidth) / targetHeight; + if (srcAspect > dstAspect) targetHeight = static_cast(targetWidth / srcAspect); + else targetWidth = static_cast(targetHeight * srcAspect); } - // MF requires even dimensions targetWidth = (targetWidth + 1) & ~1u; targetHeight = (targetHeight + 1) & ~1u; - // Skip if already at this size - if (targetWidth == pInstance->videoWidth && targetHeight == pInstance->videoHeight) - return S_OK; - - // Minimum size guard - if (targetWidth < 2 || targetHeight < 2) - return E_INVALIDARG; + if (targetWidth == pInstance->videoWidth && targetHeight == pInstance->videoHeight) return S_OK; + if (targetWidth < 2 || targetHeight < 2) return E_INVALIDARG; - // Reconfigure the output media type with the new frame size - IMFMediaType* pType = nullptr; - HRESULT hr = MFCreateMediaType(&pType); + ComPtr type; + HRESULT hr = MFCreateMediaType(type.GetAddressOf()); if (FAILED(hr)) return hr; - hr = pType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video); - if (SUCCEEDED(hr)) - hr = pType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_RGB32); - if (SUCCEEDED(hr)) - hr = MFSetAttributeSize(pType, MF_MT_FRAME_SIZE, targetWidth, targetHeight); - if (SUCCEEDED(hr)) - hr = pInstance->pSourceReader->SetCurrentMediaType( - MF_SOURCE_READER_FIRST_VIDEO_STREAM, nullptr, pType); - SafeRelease(pType); - - if (FAILED(hr)) - return hr; + type->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video); + type->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_RGB32); + MFSetAttributeSize(type.Get(), MF_MT_FRAME_SIZE, targetWidth, targetHeight); + hr = pInstance->pSourceReader->SetCurrentMediaType( + MF_SOURCE_READER_FIRST_VIDEO_STREAM, nullptr, type.Get()); + if (FAILED(hr)) return hr; - // Verify and update the actual output dimensions - IMFMediaType* pActual = nullptr; - hr = pInstance->pSourceReader->GetCurrentMediaType( - MF_SOURCE_READER_FIRST_VIDEO_STREAM, &pActual); - if (SUCCEEDED(hr)) { - MFGetAttributeSize(pActual, MF_MT_FRAME_SIZE, + ComPtr actual; + if (SUCCEEDED(pInstance->pSourceReader->GetCurrentMediaType( + MF_SOURCE_READER_FIRST_VIDEO_STREAM, actual.GetAddressOf()))) { + MFGetAttributeSize(actual.Get(), MF_MT_FRAME_SIZE, &pInstance->videoWidth, &pInstance->videoHeight); - SafeRelease(pActual); - } - - // Invalidate cached sample since dimensions changed - if (pInstance->pCachedSample) { - pInstance->pCachedSample->Release(); - pInstance->pCachedSample = nullptr; } - pInstance->bHasInitialFrame = FALSE; + pInstance->pCachedSample.Reset(); + pInstance->bHasInitialFrame = false; return S_OK; } diff --git a/mediaplayer/src/jvmMain/native/windows/NativeVideoPlayer.h b/mediaplayer/src/jvmMain/native/windows/NativeVideoPlayer.h index 6a934542..9b155b0a 100644 --- a/mediaplayer/src/jvmMain/native/windows/NativeVideoPlayer.h +++ b/mediaplayer/src/jvmMain/native/windows/NativeVideoPlayer.h @@ -3,6 +3,7 @@ #ifndef NATIVE_VIDEO_PLAYER_H #define NATIVE_VIDEO_PLAYER_H +#include "ErrorCodes.h" #include #include #include @@ -11,245 +12,74 @@ #include // Native API version — bump when the exported API changes. -// Kotlin JNA bindings should call GetNativeVersion() and compare. #define NATIVE_VIDEO_PLAYER_VERSION 2 -// Structure to hold video metadata +// Playback speed bounds — kept in sync with +// io.github.kdroidfilter.composemediaplayer.VideoPlayerState.{MIN,MAX}_PLAYBACK_SPEED. +static const float NVP_MIN_PLAYBACK_SPEED = 0.5f; +static const float NVP_MAX_PLAYBACK_SPEED = 2.0f; + typedef struct VideoMetadata { - wchar_t title[256]; // Title of the video (empty if not available) - LONGLONG duration; // Duration in 100-ns units - UINT32 width; // Width in pixels - UINT32 height; // Height in pixels - LONGLONG bitrate; // Bitrate in bits per second - float frameRate; // Frame rate in frames per second - wchar_t mimeType[64]; // MIME type of the video - UINT32 audioChannels; // Number of audio channels - UINT32 audioSampleRate; // Audio sample rate in Hz - BOOL hasTitle; // TRUE if title is available - BOOL hasDuration; // TRUE if duration is available - BOOL hasWidth; // TRUE if width is available - BOOL hasHeight; // TRUE if height is available - BOOL hasBitrate; // TRUE if bitrate is available - BOOL hasFrameRate; // TRUE if frame rate is available - BOOL hasMimeType; // TRUE if MIME type is available - BOOL hasAudioChannels; // TRUE if audio channels is available - BOOL hasAudioSampleRate; // TRUE if audio sample rate is available + wchar_t title[256]; + LONGLONG duration; + UINT32 width; + UINT32 height; + LONGLONG bitrate; + float frameRate; + wchar_t mimeType[64]; + UINT32 audioChannels; + UINT32 audioSampleRate; + BOOL hasTitle; + BOOL hasDuration; + BOOL hasWidth; + BOOL hasHeight; + BOOL hasBitrate; + BOOL hasFrameRate; + BOOL hasMimeType; + BOOL hasAudioChannels; + BOOL hasAudioSampleRate; } VideoMetadata; -// DLL export macro #ifdef _WIN32 -#ifdef NATIVEVIDEOPLAYER_EXPORTS -#define NATIVEVIDEOPLAYER_API __declspec(dllexport) -#else -#define NATIVEVIDEOPLAYER_API __declspec(dllimport) -#endif + #ifdef NATIVEVIDEOPLAYER_EXPORTS + #define NATIVEVIDEOPLAYER_API __declspec(dllexport) + #else + #define NATIVEVIDEOPLAYER_API __declspec(dllimport) + #endif #else -#define NATIVEVIDEOPLAYER_API + #define NATIVEVIDEOPLAYER_API #endif -// Custom error codes -#define OP_E_NOT_INITIALIZED ((HRESULT)0x80000001L) -#define OP_E_ALREADY_INITIALIZED ((HRESULT)0x80000002L) -#define OP_E_INVALID_PARAMETER ((HRESULT)0x80000003L) - -// Forward declaration for the video player instance state struct VideoPlayerInstance; #ifdef __cplusplus extern "C" { #endif -// ==================================================================== -// Exported functions for instance management and media playback -// ==================================================================== - -/** - * @brief Returns the native API version number. - * - * Kotlin JNA bindings should check that this value matches the expected - * version to detect DLL/binding mismatches at load time. - * - * @return The version number (NATIVE_VIDEO_PLAYER_VERSION). - */ -NATIVEVIDEOPLAYER_API int GetNativeVersion(); - -/** - * @brief Initializes Media Foundation, Direct3D11 and the DXGI manager (once for all instances). - * @return S_OK on success, or an error code. - */ +NATIVEVIDEOPLAYER_API int GetNativeVersion(); NATIVEVIDEOPLAYER_API HRESULT InitMediaFoundation(); - -/** - * @brief Creates a new video player instance. - * @param ppInstance Pointer to receive the handle to the new instance. - * @return S_OK on success, or an error code. - */ NATIVEVIDEOPLAYER_API HRESULT CreateVideoPlayerInstance(VideoPlayerInstance** ppInstance); - -/** - * @brief Destroys a video player instance and releases its resources. - * @param pInstance Handle to the instance to destroy. - */ -NATIVEVIDEOPLAYER_API void DestroyVideoPlayerInstance(VideoPlayerInstance* pInstance); - -/** - * @brief Opens a media file or URL and prepares hardware-accelerated decoding for a specific instance. - * @param pInstance Handle to the instance. - * @param url Path or URL to the media (wide string). - * @param startPlayback TRUE to start playback immediately, FALSE to remain paused. - * @return S_OK on success, or an error code. - */ +NATIVEVIDEOPLAYER_API void DestroyVideoPlayerInstance(VideoPlayerInstance* pInstance); NATIVEVIDEOPLAYER_API HRESULT OpenMedia(VideoPlayerInstance* pInstance, const wchar_t* url, BOOL startPlayback = TRUE); - -/** - * @brief Reads the next video frame in RGB32 format for a specific instance. - * @param pInstance Handle to the instance. - * @param pData Receives a pointer to the frame data (do not free). - * @param pDataSize Receives the buffer size in bytes. - * @return S_OK if a frame is read, S_FALSE at end of stream, or an error code. - */ NATIVEVIDEOPLAYER_API HRESULT ReadVideoFrame(VideoPlayerInstance* pInstance, BYTE** pData, DWORD* pDataSize); - -/** - * @brief Unlocks the previously locked video frame buffer for a specific instance. - * @param pInstance Handle to the instance. - * @return S_OK on success. - */ NATIVEVIDEOPLAYER_API HRESULT UnlockVideoFrame(VideoPlayerInstance* pInstance); - -/** - * @brief Reads the next video frame and copies it into a destination buffer. - * @param pTimestamp Receives the 100ns timestamp when available. - */ -NATIVEVIDEOPLAYER_API HRESULT ReadVideoFrameInto( - VideoPlayerInstance* pInstance, - BYTE* pDst, - DWORD dstRowBytes, - DWORD dstCapacity, - LONGLONG* pTimestamp); - -/** - * @brief Closes the media and releases associated resources for a specific instance. - * @param pInstance Handle to the instance. - */ -NATIVEVIDEOPLAYER_API void CloseMedia(VideoPlayerInstance* pInstance); - -/** - * @brief Indicates whether the end of the media stream has been reached for a specific instance. - * @param pInstance Handle to the instance. - * @return TRUE if end of stream, FALSE otherwise. - */ -NATIVEVIDEOPLAYER_API BOOL IsEOF(const VideoPlayerInstance* pInstance); - -/** - * @brief Retrieves the video dimensions for a specific instance. - * @param pInstance Handle to the instance. - * @param pWidth Pointer to receive the width in pixels. - * @param pHeight Pointer to receive the height in pixels. - */ -NATIVEVIDEOPLAYER_API void GetVideoSize(const VideoPlayerInstance* pInstance, UINT32* pWidth, UINT32* pHeight); - -/** - * @brief Retrieves the video frame rate for a specific instance. - * @param pInstance Handle to the instance. - * @param pNum Pointer to receive the numerator. - * @param pDenom Pointer to receive the denominator. - * @return S_OK on success, or an error code. - */ +NATIVEVIDEOPLAYER_API HRESULT ReadVideoFrameInto(VideoPlayerInstance* pInstance, + BYTE* pDst, DWORD dstRowBytes, DWORD dstCapacity, + LONGLONG* pTimestamp); +NATIVEVIDEOPLAYER_API void CloseMedia(VideoPlayerInstance* pInstance); +NATIVEVIDEOPLAYER_API BOOL IsEOF(const VideoPlayerInstance* pInstance); +NATIVEVIDEOPLAYER_API void GetVideoSize(const VideoPlayerInstance* pInstance, UINT32* pWidth, UINT32* pHeight); NATIVEVIDEOPLAYER_API HRESULT GetVideoFrameRate(const VideoPlayerInstance* pInstance, UINT* pNum, UINT* pDenom); - -/** - * @brief Seeks to a specific position in the media for a specific instance. - * @param pInstance Handle to the instance. - * @param llPosition Position (in 100-ns units) to seek to. - * @return S_OK on success, or an error code. - */ NATIVEVIDEOPLAYER_API HRESULT SeekMedia(VideoPlayerInstance* pInstance, LONGLONG llPosition); - -/** - * @brief Gets the total duration of the media for a specific instance. - * @param pInstance Handle to the instance. - * @param pDuration Pointer to receive the duration (in 100-ns units). - * @return S_OK on success, or an error code. - */ NATIVEVIDEOPLAYER_API HRESULT GetMediaDuration(const VideoPlayerInstance* pInstance, LONGLONG* pDuration); - -/** - * @brief Gets the current playback position for a specific instance. - * @param pInstance Handle to the instance. - * @param pPosition Pointer to receive the position (in 100-ns units). - * @return S_OK on success, or an error code. - */ NATIVEVIDEOPLAYER_API HRESULT GetMediaPosition(const VideoPlayerInstance* pInstance, LONGLONG* pPosition); - -/** - * @brief Sets the playback state (playing or paused) for a specific instance. - * @param pInstance Handle to the instance. - * @param bPlaying TRUE for playback, FALSE for pause. - * @param bStop TRUE for a full stop, FALSE for a simple pause. - * @return S_OK on success, or an error code. - */ NATIVEVIDEOPLAYER_API HRESULT SetPlaybackState(VideoPlayerInstance* pInstance, BOOL bPlaying, BOOL bStop = FALSE); - -/** - * @brief Shuts down Media Foundation and releases global resources (after all instances are destroyed). - * @return S_OK on success, or an error code. - */ NATIVEVIDEOPLAYER_API HRESULT ShutdownMediaFoundation(); - -/** - * @brief Sets the audio volume level for a specific instance. - * @param pInstance Handle to the instance. - * @param volume Volume level (0.0 to 1.0). - * @return S_OK on success, or an error code. - */ NATIVEVIDEOPLAYER_API HRESULT SetAudioVolume(VideoPlayerInstance* pInstance, float volume); - -/** - * @brief Gets the current audio volume level for a specific instance. - * @param pInstance Handle to the instance. - * @param volume Pointer to receive the volume level (0.0 to 1.0). - * @return S_OK on success, or an error code. - */ NATIVEVIDEOPLAYER_API HRESULT GetAudioVolume(const VideoPlayerInstance* pInstance, float* volume); - -/** - * @brief Sets the playback speed for a specific instance. - * @param pInstance Handle to the instance. - * @param speed Playback speed (0.5 to 2.0, where 1.0 is normal speed). - * @return S_OK on success, or an error code. - */ NATIVEVIDEOPLAYER_API HRESULT SetPlaybackSpeed(VideoPlayerInstance* pInstance, float speed); - -/** - * @brief Gets the current playback speed for a specific instance. - * @param pInstance Handle to the instance. - * @param pSpeed Pointer to receive the playback speed. - * @return S_OK on success, or an error code. - */ NATIVEVIDEOPLAYER_API HRESULT GetPlaybackSpeed(const VideoPlayerInstance* pInstance, float* pSpeed); - -/** - * @brief Retrieves all available metadata for the current media. - * @param pInstance Handle to the instance. - * @param pMetadata Pointer to receive the metadata structure. - * @return S_OK on success, or an error code. - */ NATIVEVIDEOPLAYER_API HRESULT GetVideoMetadata(const VideoPlayerInstance* pInstance, VideoMetadata* pMetadata); - -/** - * @brief Sets the desired output resolution for decoded video frames. - * - * Reconfigures the MF source reader output type to produce frames at the - * requested size (hardware-scaled via DXVA2). The aspect ratio of the - * original video is preserved; the requested size acts as a bounding box. - * Passing 0,0 resets to the native video resolution. - * - * @param pInstance Handle to the instance. - * @param targetWidth Desired output width (0 = native). - * @param targetHeight Desired output height (0 = native). - * @return S_OK on success, or an error code. - */ NATIVEVIDEOPLAYER_API HRESULT SetOutputSize(VideoPlayerInstance* pInstance, UINT32 targetWidth, UINT32 targetHeight); #ifdef __cplusplus diff --git a/mediaplayer/src/jvmMain/native/windows/Utils.cpp b/mediaplayer/src/jvmMain/native/windows/Utils.cpp index 91731f73..9be54d7f 100644 --- a/mediaplayer/src/jvmMain/native/windows/Utils.cpp +++ b/mediaplayer/src/jvmMain/native/windows/Utils.cpp @@ -4,13 +4,32 @@ namespace VideoPlayerUtils { +// Creates a high-resolution waitable timer (100 µs granularity on Windows 10 +// 1803+) without touching the global timer period. Falls back to a classic +// waitable timer, then to std::this_thread::sleep_for, if unavailable. +static HANDLE CreateHighResolutionTimer() { + using CreateExPtr = HANDLE (WINAPI*)(LPSECURITY_ATTRIBUTES, LPCWSTR, DWORD, DWORD); + static const CreateExPtr pCreateEx = []() -> CreateExPtr { + HMODULE kernel = GetModuleHandleW(L"kernel32.dll"); + return kernel ? reinterpret_cast( + GetProcAddress(kernel, "CreateWaitableTimerExW")) : nullptr; + }(); + + // 0x00000002 = CREATE_WAITABLE_TIMER_HIGH_RESOLUTION (Win10 1803+) + constexpr DWORD kHighRes = 0x00000002; + if (pCreateEx) { + HANDLE h = pCreateEx(nullptr, nullptr, kHighRes, TIMER_ALL_ACCESS); + if (h) return h; + } + return CreateWaitableTimerW(nullptr, TRUE, nullptr); +} + void PreciseSleepHighRes(double ms) { if (ms <= 0.1) return; - // Each thread gets its own waitable timer to avoid race conditions - // when multiple threads (audio + video) call this concurrently. - thread_local HANDLE hTimer = CreateWaitableTimer(nullptr, TRUE, nullptr); + // Thread-local timer: no sharing between threads, no locking overhead. + thread_local HANDLE hTimer = CreateHighResolutionTimer(); if (!hTimer) { std::this_thread::sleep_for(std::chrono::duration(ms)); return; @@ -18,8 +37,11 @@ void PreciseSleepHighRes(double ms) { LARGE_INTEGER liDueTime; liDueTime.QuadPart = -static_cast(ms * 10000.0); - SetWaitableTimer(hTimer, &liDueTime, 0, nullptr, nullptr, FALSE); - WaitForSingleObject(hTimer, INFINITE); + if (SetWaitableTimer(hTimer, &liDueTime, 0, nullptr, nullptr, FALSE)) { + WaitForSingleObject(hTimer, INFINITE); + } else { + std::this_thread::sleep_for(std::chrono::duration(ms)); + } } -} // namespace VideoPlayerUtils \ No newline at end of file +} // namespace VideoPlayerUtils diff --git a/mediaplayer/src/jvmMain/native/windows/VideoPlayerInstance.h b/mediaplayer/src/jvmMain/native/windows/VideoPlayerInstance.h index b7617f75..996f5ac4 100644 --- a/mediaplayer/src/jvmMain/native/windows/VideoPlayerInstance.h +++ b/mediaplayer/src/jvmMain/native/windows/VideoPlayerInstance.h @@ -1,5 +1,6 @@ #pragma once +#include "ComHelpers.h" #include #include #include @@ -7,74 +8,85 @@ #include #include #include +#include #include -// Forward declaration -class HLSPlayer; +#include "HLSPlayer.h" -/** - * @brief Structure to encapsulate the state of a video player instance. - */ +// Per-player state. All COM pointers use ComPtr, events/critical sections use +// RAII wrappers. The destructor performs a full teardown; CloseMedia() resets +// the fields that describe the *current* media so the object can be reused. struct VideoPlayerInstance { - // Video related members - IMFSourceReader* pSourceReader = nullptr; // Single reader for both audio & video - IMFMediaBuffer* pLockedBuffer = nullptr; - BYTE* pLockedBytes = nullptr; - DWORD lockedMaxSize = 0; - DWORD lockedCurrSize = 0; - UINT32 videoWidth = 0; - UINT32 videoHeight = 0; - UINT32 nativeWidth = 0; // Original video resolution (before scaling) - UINT32 nativeHeight = 0; - BOOL bEOF = FALSE; - - // Frame caching for paused state - IMFSample* pCachedSample = nullptr; // Cached sample for paused state - BOOL bHasInitialFrame = FALSE; // Whether we've read an initial frame when paused - - // Audio related members - IMFSourceReader* pSourceReaderAudio = nullptr; // Separate reader for audio (no serialization with video) - BOOL bHasAudio = FALSE; - BOOL bAudioInitialized = FALSE; - IAudioClient* pAudioClient = nullptr; - IAudioRenderClient* pRenderClient = nullptr; - IMMDevice* pDevice = nullptr; - WAVEFORMATEX* pSourceAudioFormat = nullptr; - HANDLE hAudioSamplesReadyEvent = nullptr; - HANDLE hAudioThread = nullptr; - BOOL bAudioThreadRunning = FALSE; - HANDLE hAudioReadyEvent = nullptr; - IAudioEndpointVolume* pAudioEndpointVolume = nullptr; - - // WASAPI latency: updated by audio thread, read by video thread for A/V sync + // ---- Video source reader ---- + Microsoft::WRL::ComPtr pSourceReader; + Microsoft::WRL::ComPtr pLockedBuffer; + BYTE* pLockedBytes = nullptr; + DWORD lockedMaxSize = 0; + DWORD lockedCurrSize = 0; + UINT32 videoWidth = 0; + UINT32 videoHeight = 0; + UINT32 nativeWidth = 0; + UINT32 nativeHeight = 0; + std::atomic bEOF{false}; + + // Frame caching: used when paused, and when the decoded frame arrived + // earlier than its presentation time (replaces the sleep-in-render-path + // pattern so the JNI thread never blocks). + Microsoft::WRL::ComPtr pCachedSample; + LONGLONG llCachedTimestamp = 0; + ULONGLONG llCachedInsertedAtMs = 0; // wall-clock time when pCachedSample was stored + bool bHasInitialFrame = false; + + // ---- Audio ---- + Microsoft::WRL::ComPtr pSourceReaderAudio; + Microsoft::WRL::ComPtr pAudioClient; + Microsoft::WRL::ComPtr pRenderClient; + Microsoft::WRL::ComPtr pDevice; + Microsoft::WRL::ComPtr pAudioEndpointVolume; + + bool bHasAudio = false; + bool bAudioInitialized = false; + + WAVEFORMATEX* pSourceAudioFormat = nullptr; // allocated with CoTaskMemAlloc + + VideoPlayerUtils::UniqueHandle hAudioSamplesReadyEvent; + VideoPlayerUtils::UniqueHandle hAudioResumeEvent; // manual-reset; signaled while playing + VideoPlayerUtils::UniqueHandle hAudioThread; + std::atomic bAudioThreadRunning{false}; + + // WASAPI latency (ms), updated by audio thread, read by video thread. std::atomic audioLatencyMs{0.0}; - // Protects WASAPI GetBuffer/ReleaseBuffer vs Stop/Reset/Start during seeks - CRITICAL_SECTION csAudioFeed{}; + // Protects GetBuffer/ReleaseBuffer vs Stop/Reset/Start during seeks. + VideoPlayerUtils::CriticalSection csAudioFeed; - // Media Foundation clock for synchronization - IMFPresentationClock* pPresentationClock = nullptr; - IMFMediaSource* pMediaSource = nullptr; - BOOL bUseClockSync = FALSE; + // ---- Presentation clock ---- + Microsoft::WRL::ComPtr pPresentationClock; + Microsoft::WRL::ComPtr pMediaSource; + bool bUseClockSync = false; - // Timing and synchronization - LONGLONG llCurrentPosition = 0; - ULONGLONG llPlaybackStartTime = 0; - ULONGLONG llTotalPauseTime = 0; - ULONGLONG llPauseStart = 0; - CRITICAL_SECTION csClockSync{}; - BOOL bSeekInProgress = FALSE; + // ---- Timing ---- + // Shared across JNI, audio, and render threads: all atomic. + std::atomic llCurrentPosition{0}; + std::atomic llPlaybackStartTime{0}; + std::atomic llTotalPauseTime{0}; + std::atomic llPauseStart{0}; + std::atomic bSeekInProgress{false}; + VideoPlayerUtils::CriticalSection csClockSync; // guards composite seek operations - // Playback control (atomic for lock-free access from the audio thread) - std::atomic instanceVolume{1.0f}; // Volume specific to this instance (1.0 = 100%) - std::atomic playbackSpeed{1.0f}; // Playback speed (1.0 = 100%) + // ---- Playback control ---- + std::atomic instanceVolume{1.0f}; + std::atomic playbackSpeed{1.0f}; + double resampleFracPos = 0.0; // audio thread only - // Audio resampling fractional position for playback speed (audio thread only) - double resampleFracPos = 0.0; + // ---- Network / HLS ---- + bool bIsNetworkSource = false; + bool bIsLiveStream = false; + Microsoft::WRL::ComPtr pHLSPlayer; + VideoPlayerInstance() = default; + ~VideoPlayerInstance(); - // Network / HLS streaming - BOOL bIsNetworkSource = FALSE; // TRUE when URL is http:// or https:// - BOOL bIsLiveStream = FALSE; // TRUE when duration is unknown (live HLS) - HLSPlayer* pHLSPlayer = nullptr; // Non-null when playing HLS via IMFMediaEngine + VideoPlayerInstance(const VideoPlayerInstance&) = delete; + VideoPlayerInstance& operator=(const VideoPlayerInstance&) = delete; }; diff --git a/sample/composeApp/build.gradle.kts b/sample/composeApp/build.gradle.kts index ff1a8710..4f753a3d 100644 --- a/sample/composeApp/build.gradle.kts +++ b/sample/composeApp/build.gradle.kts @@ -132,7 +132,8 @@ nucleus.application { jvmVendor = JvmVendorSpec.BELLSOFT buildArgs.addAll( "-H:+AddAllCharsets", - "-Djava.awt.headless=false" + "-Djava.awt.headless=false", + "--enable-url-protocols=http,https" ) } }