diff --git a/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/windows/MediaFoundationLib.kt b/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/windows/MediaFoundationLib.kt index 5b3db14..c28a42b 100644 --- a/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/windows/MediaFoundationLib.kt +++ b/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/windows/MediaFoundationLib.kt @@ -1,126 +1,123 @@ package io.github.kdroidfilter.composemediaplayer.windows -import com.sun.jna.Native -import com.sun.jna.Pointer -import com.sun.jna.Structure -import com.sun.jna.WString -import com.sun.jna.ptr.FloatByReference -import com.sun.jna.ptr.IntByReference -import com.sun.jna.ptr.LongByReference -import com.sun.jna.ptr.PointerByReference import io.github.kdroidfilter.composemediaplayer.VideoMetadata +import java.io.File +import java.nio.ByteBuffer +import java.nio.file.Files internal object MediaFoundationLib { - /** - * Register the native library for JNA direct mapping - */ - init { - Native.register("NativeVideoPlayer") - } + /** Expected native API version — must match NATIVE_VIDEO_PLAYER_VERSION in the DLL. */ + private const val EXPECTED_NATIVE_VERSION = 2 - /** - * JNA structure that maps to the C++ VideoMetadata structure - */ - @Structure.FieldOrder( - "title", "duration", "width", "height", "bitrate", "frameRate", "mimeType", - "audioChannels", "audioSampleRate", "hasTitle", "hasDuration", "hasWidth", - "hasHeight", "hasBitrate", "hasFrameRate", "hasMimeType", "hasAudioChannels", - "hasAudioSampleRate" - ) - class NativeVideoMetadata : Structure() { - @JvmField var title = CharArray(256) - @JvmField var duration: Long = 0 - @JvmField var width: Int = 0 - @JvmField var height: Int = 0 - @JvmField var bitrate: Long = 0 - @JvmField var frameRate: Float = 0f - @JvmField var mimeType = CharArray(64) - @JvmField var audioChannels: Int = 0 - @JvmField var audioSampleRate: Int = 0 - @JvmField var hasTitle: Boolean = false - @JvmField var hasDuration: Boolean = false - @JvmField var hasWidth: Boolean = false - @JvmField var hasHeight: Boolean = false - @JvmField var hasBitrate: Boolean = false - @JvmField var hasFrameRate: Boolean = false - @JvmField var hasMimeType: Boolean = false - @JvmField var hasAudioChannels: Boolean = false - @JvmField var hasAudioSampleRate: Boolean = false - - /** - * Converts this native structure to a Kotlin VideoMetadata object - */ - fun toVideoMetadata(): VideoMetadata { - return VideoMetadata( - title = if (hasTitle) String(title).trim { it <= ' ' || it == '\u0000' } else null, - duration = if (hasDuration) duration / 10000 else null, // Convert from 100ns to ms - width = if (hasWidth) width else null, - height = if (hasHeight) height else null, - bitrate = if (hasBitrate) bitrate else null, - frameRate = if (hasFrameRate) frameRate else null, - mimeType = if (hasMimeType) String(mimeType).trim { it <= ' ' || it == '\u0000' } else null, - audioChannels = if (hasAudioChannels) audioChannels else null, - audioSampleRate = if (hasAudioSampleRate) audioSampleRate else null - ) + init { + loadNativeLibrary() + val nativeVersion = nGetNativeVersion() + require(nativeVersion == EXPECTED_NATIVE_VERSION) { + "NativeVideoPlayer DLL version mismatch: expected $EXPECTED_NATIVE_VERSION but got $nativeVersion. " + + "Please rebuild the native DLL or update the Kotlin bindings." } } - /** - * Helper: Creates a new instance of the native video player - * @return A pointer to the native instance or null if creation failed - */ - fun createInstance(): Pointer? { - val ptrRef = PointerByReference() - val hr = CreateVideoPlayerInstance(ptrRef) - return if (hr >= 0 && ptrRef.value != null) ptrRef.value else null + private fun loadNativeLibrary() { + val osArch = System.getProperty("os.arch", "").lowercase() + val resourceDir = + if (osArch == "aarch64" || osArch == "arm64") "win32-arm64" else "win32-x86-64" + val libName = "NativeVideoPlayer.dll" + + val stream = MediaFoundationLib::class.java.getResourceAsStream("/$resourceDir/$libName") + ?: throw UnsatisfiedLinkError("Native library not found in resources: /$resourceDir/$libName") + + val tempDir = Files.createTempDirectory("nativevideoplayer").toFile() + val tempFile = File(tempDir, libName) + stream.use { input -> tempFile.outputStream().use { input.copyTo(it) } } + System.load(tempFile.absolutePath) + tempFile.deleteOnExit() + tempDir.deleteOnExit() } - /** - * Helper: Destroys a native video player instance - * @param instance The pointer to the native instance to destroy - */ - fun destroyInstance(instance: Pointer) { - DestroyVideoPlayerInstance(instance) + // ----- Helpers ----- + + fun createInstance(): Long { + val handle = nCreateInstance() + return if (handle != 0L) handle else 0L } - /** - * Helper: Retrieves metadata for the current media - * @param instance Pointer to the native instance - * @return VideoMetadata object containing all available metadata, or null if retrieval failed - */ - fun getVideoMetadata(instance: Pointer): VideoMetadata? { - val metadata = NativeVideoMetadata() - val hr = GetVideoMetadata(instance, metadata) - return if (hr >= 0) metadata.toVideoMetadata() else null + fun destroyInstance(handle: Long) = nDestroyInstance(handle) + + fun getVideoMetadata(handle: Long): VideoMetadata? { + val title = CharArray(256) + val mimeType = CharArray(64) + val longVals = LongArray(2) + val intVals = IntArray(4) + val floatVals = FloatArray(1) + val hasFlags = BooleanArray(9) + + val hr = nGetVideoMetadata(handle, title, mimeType, longVals, intVals, floatVals, hasFlags) + if (hr < 0) return null + + return VideoMetadata( + title = if (hasFlags[0]) String(title).trim { it <= ' ' || it == '\u0000' } else null, + duration = if (hasFlags[1]) longVals[0] / 10000 else null, + width = if (hasFlags[2]) intVals[0] else null, + height = if (hasFlags[3]) intVals[1] else null, + bitrate = if (hasFlags[4]) longVals[1] else null, + frameRate = if (hasFlags[5]) floatVals[0] else null, + mimeType = if (hasFlags[6]) String(mimeType).trim { it <= ' ' || it == '\u0000' } else null, + audioChannels = if (hasFlags[7]) intVals[2] else null, + audioSampleRate = if (hasFlags[8]) intVals[3] else null, + ) } - // === Direct mapped native methods === - @JvmStatic external fun InitMediaFoundation(): Int - @JvmStatic external fun CreateVideoPlayerInstance(ppInstance: PointerByReference): Int - @JvmStatic external fun DestroyVideoPlayerInstance(pInstance: Pointer) - @JvmStatic external fun OpenMedia(pInstance: Pointer, url: WString, startPlayback: Boolean): Int - @JvmStatic external fun ReadVideoFrame(pInstance: Pointer, pData: PointerByReference, pDataSize: IntByReference): Int - @JvmStatic external fun UnlockVideoFrame(pInstance: Pointer): Int - @JvmStatic external fun CloseMedia(pInstance: Pointer) - @JvmStatic external fun IsEOF(pInstance: Pointer): Boolean - @JvmStatic external fun GetVideoSize(pInstance: Pointer, pWidth: IntByReference, pHeight: IntByReference) - @JvmStatic external fun GetVideoFrameRate(pInstance: Pointer, pNum: IntByReference, pDenom: IntByReference): Int - @JvmStatic external fun SeekMedia(pInstance: Pointer, lPosition: Long): Int - @JvmStatic external fun GetMediaDuration(pInstance: Pointer, pDuration: LongByReference): Int - @JvmStatic external fun GetMediaPosition(pInstance: Pointer, pPosition: LongByReference): Int - @JvmStatic external fun SetPlaybackState(pInstance: Pointer, isPlaying: Boolean, bStop: Boolean): Int - @JvmStatic external fun ShutdownMediaFoundation(): Int - @JvmStatic external fun SetAudioVolume(pInstance: Pointer, volume: Float): Int - @JvmStatic external fun GetAudioVolume(pInstance: Pointer, volume: FloatByReference): Int - @JvmStatic external fun GetAudioLevels(pInstance: Pointer, pLeftLevel: FloatByReference, pRightLevel: FloatByReference): Int - @JvmStatic external fun SetPlaybackSpeed(pInstance: Pointer, speed: Float): Int - @JvmStatic external fun GetPlaybackSpeed(pInstance: Pointer, pSpeed: FloatByReference): Int - - /** - * Retrieves all available metadata for the current media - * @param pInstance Pointer to the native instance - * @param pMetadata Pointer to receive the metadata structure - * @return S_OK on success, or an error code - */ - @JvmStatic external fun GetVideoMetadata(pInstance: Pointer, pMetadata: NativeVideoMetadata): Int + // ----- JNI native methods (registered via JNI_OnLoad / RegisterNatives) ----- + + @JvmStatic external fun nGetNativeVersion(): Int + @JvmStatic external fun nInitMediaFoundation(): Int + @JvmStatic external fun nCreateInstance(): Long + @JvmStatic external fun nDestroyInstance(handle: Long) + @JvmStatic external fun nOpenMedia(handle: Long, url: String, startPlayback: Boolean): Int + @JvmStatic external fun nReadVideoFrame(handle: Long, outResult: IntArray): ByteBuffer? + @JvmStatic external fun nUnlockVideoFrame(handle: Long): Int + @JvmStatic external fun nCloseMedia(handle: Long) + @JvmStatic external fun nIsEOF(handle: Long): Boolean + @JvmStatic external fun nGetVideoSize(handle: Long, outSize: IntArray) + @JvmStatic external fun nGetVideoFrameRate(handle: Long, outRate: IntArray): Int + @JvmStatic external fun nSeekMedia(handle: Long, position: Long): Int + @JvmStatic external fun nGetMediaDuration(handle: Long, outDuration: LongArray): Int + @JvmStatic external fun nGetMediaPosition(handle: Long, outPosition: LongArray): Int + @JvmStatic external fun nSetPlaybackState(handle: Long, isPlaying: Boolean, stop: Boolean): Int + @JvmStatic external fun nShutdownMediaFoundation(): Int + @JvmStatic external fun nSetAudioVolume(handle: Long, volume: Float): Int + @JvmStatic external fun nGetAudioVolume(handle: Long, outVolume: FloatArray): Int + @JvmStatic external fun nGetAudioLevels(handle: Long, outLevels: FloatArray): Int + @JvmStatic external fun nSetPlaybackSpeed(handle: Long, speed: Float): Int + @JvmStatic external fun nGetPlaybackSpeed(handle: Long, outSpeed: FloatArray): Int + @JvmStatic external fun nWrapPointer(address: Long, size: Long): ByteBuffer? + @JvmStatic external fun nSetOutputSize(handle: Long, width: Int, height: Int): Int + + @JvmStatic private external fun nGetVideoMetadata( + handle: Long, title: CharArray, mimeType: CharArray, + longVals: LongArray, intVals: IntArray, floatVals: FloatArray, hasFlags: BooleanArray + ): Int + + // ----- Convenience wrappers (keep old API names for minimal caller changes) ----- + + fun InitMediaFoundation(): Int = nInitMediaFoundation() + fun ShutdownMediaFoundation(): Int = nShutdownMediaFoundation() + fun OpenMedia(handle: Long, url: String, startPlayback: Boolean): Int = nOpenMedia(handle, url, startPlayback) + fun CloseMedia(handle: Long) = nCloseMedia(handle) + fun IsEOF(handle: Long): Boolean = nIsEOF(handle) + fun UnlockVideoFrame(handle: Long): Int = nUnlockVideoFrame(handle) + fun SeekMedia(handle: Long, position: Long): Int = nSeekMedia(handle, position) + fun SetPlaybackState(handle: Long, isPlaying: Boolean, stop: Boolean): Int = nSetPlaybackState(handle, isPlaying, stop) + fun SetAudioVolume(handle: Long, volume: Float): Int = nSetAudioVolume(handle, volume) + fun SetPlaybackSpeed(handle: Long, speed: Float): Int = nSetPlaybackSpeed(handle, speed) + + fun ReadVideoFrame(handle: Long, outResult: IntArray): ByteBuffer? = nReadVideoFrame(handle, outResult) + fun GetVideoSize(handle: Long, outSize: IntArray) = nGetVideoSize(handle, outSize) + fun GetMediaDuration(handle: Long, outDuration: LongArray): Int = nGetMediaDuration(handle, outDuration) + fun GetMediaPosition(handle: Long, outPosition: LongArray): Int = nGetMediaPosition(handle, outPosition) + fun GetAudioVolume(handle: Long, outVolume: FloatArray): Int = nGetAudioVolume(handle, outVolume) + fun GetAudioLevels(handle: Long, outLevels: FloatArray): Int = nGetAudioLevels(handle, outLevels) + fun GetPlaybackSpeed(handle: Long, outSpeed: FloatArray): Int = nGetPlaybackSpeed(handle, outSpeed) + fun SetOutputSize(handle: Long, width: Int, height: Int): Int = nSetOutputSize(handle, width, height) } 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 3db9060..0b862d6 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 @@ -14,12 +14,6 @@ import androidx.compose.ui.unit.sp import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger.Companion.setMinSeverity import co.touchlab.kermit.Severity -import com.sun.jna.Pointer -import com.sun.jna.WString -import com.sun.jna.ptr.FloatByReference -import com.sun.jna.ptr.IntByReference -import com.sun.jna.ptr.LongByReference -import com.sun.jna.ptr.PointerByReference import io.github.kdroidfilter.composemediaplayer.InitialPlayerState import io.github.kdroidfilter.composemediaplayer.SubtitleTrack import io.github.kdroidfilter.composemediaplayer.VideoMetadata @@ -48,7 +42,6 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout -import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.yield import org.jetbrains.skia.Bitmap import org.jetbrains.skia.ColorAlphaType @@ -77,7 +70,7 @@ class WindowsVideoPlayerState : VideoPlayerState { private val isMfBootstrapped = AtomicBoolean(false) /** Map to store volume settings for each player instance */ - private val instanceVolumes = ConcurrentHashMap() + private val instanceVolumes = ConcurrentHashMap() /** * Initialize Media Foundation only once for all instances. @@ -116,7 +109,7 @@ class WindowsVideoPlayerState : VideoPlayerState { private var userPaused = false /** Video player instance handle */ - private var videoPlayerInstance: Pointer? = null + private var videoPlayerInstance: Long = 0L /** Deferred completed when initialization is ready */ private val initReady = CompletableDeferred() @@ -139,7 +132,7 @@ class WindowsVideoPlayerState : VideoPlayerState { _volume = newVolume scope.launch { mediaOperationMutex.withLock { - videoPlayerInstance?.let { instance -> + videoPlayerInstance.takeIf { it != 0L }?.let { instance -> // Store the volume setting for this instance instanceVolumes[instance] = newVolume @@ -181,7 +174,7 @@ class WindowsVideoPlayerState : VideoPlayerState { _playbackSpeed = newSpeed scope.launch { mediaOperationMutex.withLock { - videoPlayerInstance?.let { instance -> + videoPlayerInstance.takeIf { it != 0L }?.let { instance -> val hr = player.SetPlaybackSpeed(instance, newSpeed) if (hr < 0) { setError("Error updating playback speed (hr=0x${hr.toString(16)})") @@ -239,7 +232,10 @@ class WindowsVideoPlayerState : VideoPlayerState { // Video properties var videoWidth by mutableStateOf(0) var videoHeight by mutableStateOf(0) - private var frameBufferSize = 1 + + // Surface display size (pixels) — used to scale native output resolution + private var surfaceWidth = 0 + private var surfaceHeight = 0 // Synchronization private val mediaOperationMutex = Mutex() @@ -269,10 +265,6 @@ class WindowsVideoPlayerState : VideoPlayerState { private var skiaBitmapWidth: Int = 0 private var skiaBitmapHeight: Int = 0 - // Legacy buffer - kept for fallback but no longer used in optimized path - private var sharedFrameBuffer: ByteArray? = null - private var frameBitmapRecycler: Bitmap? = null - // Variable to store the last opened URI private var lastUri: String? = null @@ -280,15 +272,15 @@ class WindowsVideoPlayerState : VideoPlayerState { // Kick off native initialization immediately scope.launch { try { - val instance = MediaFoundationLib.createInstance() - if (instance == null) { + val handle = MediaFoundationLib.createInstance() + if (handle == 0L) { setError("Failed to create video player instance") return@launch } - videoPlayerInstance = instance + videoPlayerInstance = handle // Store default volume so that later instances inherit it - instanceVolumes[instance] = _volume + instanceVolumes[handle] = _volume initReady.complete(Unit) } catch (e: Exception) { initReady.completeExceptionally(e) @@ -320,7 +312,7 @@ class WindowsVideoPlayerState : VideoPlayerState { // Stop playing if active _isPlaying = false val instance = videoPlayerInstance - if (instance != null) { + if (instance != 0L) { try { // Stop playback before releasing resources val hr = player.SetPlaybackState(instance, false, true) @@ -348,7 +340,7 @@ class WindowsVideoPlayerState : VideoPlayerState { windowsLogger.e { "Exception destroying instance: ${e.message}" } } - videoPlayerInstance = null + videoPlayerInstance = 0L } // Clear all resources @@ -375,15 +367,12 @@ class WindowsVideoPlayerState : VideoPlayerState { val currentFrame = _currentFrame if (currentFrame != null && currentFrame !== skiaBitmapA && - currentFrame !== skiaBitmapB && - currentFrame !== frameBitmapRecycler + currentFrame !== skiaBitmapB ) { currentFrame.close() } _currentFrame = null currentFrameState.value = null - frameBitmapRecycler?.close() - frameBitmapRecycler = null // Clean up double-buffering bitmaps skiaBitmapA?.close() @@ -396,9 +385,6 @@ class WindowsVideoPlayerState : VideoPlayerState { lastFrameHash = Int.MIN_VALUE } - // Clear any shared buffer allocated for frames - sharedFrameBuffer = null - frameBufferSize = 0 // Reset all state _currentTime = 0.0 @@ -412,9 +398,6 @@ class WindowsVideoPlayerState : VideoPlayerState { // Reset initialFrameRead flag to ensure we read an initial frame when reinitialized initialFrameRead.set(false) - - // Hint the GC after freeing big objects synchronously - System.gc() } private fun releaseAllResources() { @@ -423,24 +406,20 @@ class WindowsVideoPlayerState : VideoPlayerState { audioLevelsJob?.cancel() resizeJob?.cancel() - // Ensure the frame channel is emptied - runBlocking { clearFrameChannel() } + // Drain the frame channel (tryReceive is non-suspending) + clearFrameChannel() // Free bitmaps and frame buffers bitmapLock.write { val currentFrame = _currentFrame if (currentFrame != null && currentFrame !== skiaBitmapA && - currentFrame !== skiaBitmapB && - currentFrame !== frameBitmapRecycler + currentFrame !== skiaBitmapB ) { currentFrame.close() } _currentFrame = null - // Reset the currentFrameState currentFrameState.value = null - frameBitmapRecycler?.close() // Recycle the bitmap if any - frameBitmapRecycler = null // Clean up double-buffering bitmaps skiaBitmapA?.close() @@ -453,15 +432,8 @@ class WindowsVideoPlayerState : VideoPlayerState { lastFrameHash = Int.MIN_VALUE } - // Clear any shared buffer allocated for frames - sharedFrameBuffer = null - frameBufferSize = 0 // Reset frame buffer size - // Reset initialFrameRead flag to ensure we read an initial frame when reinitialized initialFrameRead.set(false) - - // Hint GC after releasing frame buffers and bitmaps - System.gc() } private fun clearFrameChannel() { @@ -527,7 +499,7 @@ class WindowsVideoPlayerState : VideoPlayerState { // Stop playback and release existing resources val wasPlaying = _isPlaying val instance = videoPlayerInstance - if (instance == null) { + if (instance == 0L) { setError("Video player instance is null") return@withLock } @@ -558,44 +530,49 @@ class WindowsVideoPlayerState : VideoPlayerState { return@withLock } - // Open the media with appropriate initial playback state - // Pass startPlayback=false to the native library when InitialPlayerState.PAUSE is specified - // This prevents the native library from starting to read the video immediately - // and fixes the issue where the video would start playing even when paused on Windows + // Always open media in paused state to avoid starting the native + // playback clock before we've finished setup (SetOutputSize, metadata, etc.). + // We explicitly call SetPlaybackState(true) later, right before starting + // the frame-reading coroutine, so the wall-clock is in sync with frame production. val startPlayback = initializeplayerState == InitialPlayerState.PLAY - val hrOpen = player.OpenMedia(instance, WString(uri), startPlayback) + val hrOpen = player.OpenMedia(instance, uri, false) if (hrOpen < 0) { setError("Failed to open media (hr=0x${hrOpen.toString(16)}): $uri") return@withLock } // Get the video dimensions - val wRef = IntByReference() - val hRef = IntByReference() - player.GetVideoSize(instance, wRef, hRef) - if (wRef.value <= 0 || hRef.value <= 0) { + val sizeArr = IntArray(2) + player.GetVideoSize(instance, sizeArr) + if (sizeArr[0] <= 0 || sizeArr[1] <= 0) { setError("Failed to retrieve video size") player.CloseMedia(instance) return@withLock } - videoWidth = wRef.value - videoHeight = hRef.value - - // Calculate the buffer size for frames - frameBufferSize = videoWidth * videoHeight * 4 - - // Allocate the shared buffer - sharedFrameBuffer = ByteArray(frameBufferSize) + videoWidth = sizeArr[0] + videoHeight = sizeArr[1] + + // Scale output to match display surface (saves memory for 4K+ video) + if (surfaceWidth > 0 && surfaceHeight > 0) { + val hrScale = player.SetOutputSize(instance, surfaceWidth, surfaceHeight) + if (hrScale >= 0) { + player.GetVideoSize(instance, sizeArr) + if (sizeArr[0] > 0 && sizeArr[1] > 0) { + videoWidth = sizeArr[0] + videoHeight = sizeArr[1] + } + } + } // Get the media duration - val durationRef = LongByReference() - val hrDuration = player.GetMediaDuration(instance, durationRef) + val durArr = LongArray(1) + val hrDuration = player.GetMediaDuration(instance, durArr) if (hrDuration < 0) { setError("Failed to retrieve duration (hr=0x${hrDuration.toString(16)})") player.CloseMedia(instance) return@withLock } - _duration = durationRef.value / 10000000.0 + _duration = durArr[0] / 10000000.0 // Retrieve metadata using the native function val retrievedMetadata = MediaFoundationLib.getVideoMetadata(instance) @@ -613,14 +590,37 @@ class WindowsVideoPlayerState : VideoPlayerState { // Set _hasMedia to true only if everything succeeded _hasMedia = true - // Explicitly seek to the beginning of the video - val hrSeek = player.SeekMedia(instance, 0) - if (hrSeek < 0) { - windowsLogger.e { "Failed to seek to beginning (hr=0x${hrSeek.toString(16)})" } - } - - // Only start jobs if not disposing if (!isDisposing.get()) { + // Restore the volume setting BEFORE starting playback + val storedVolume = instanceVolumes[instance] + if (storedVolume != null) { + val volArr = FloatArray(1) + val hr = player.GetAudioVolume(instance, volArr) + if (hr >= 0 && storedVolume != volArr[0]) { + val setHr = player.SetAudioVolume(instance, storedVolume) + if (setHr < 0) { + windowsLogger.e { "Error restoring volume (hr=0x${setHr.toString(16)})" } + } + } + } + + if (!startPlayback) { + userPaused = true + initialFrameRead.set(false) + isLoading = false + } + + // Start native playback as late as possible — this sets + // the wall-clock origin (llPlaybackStartTime) to NOW, + // minimising the gap before produceFrames reads its first frame. + if (startPlayback) { + val hrPlay = player.SetPlaybackState(instance, true, false) + if (hrPlay < 0) { + windowsLogger.e { "Failed to start playback (hr=0x${hrPlay.toString(16)})" } + } + } + _isPlaying = startPlayback + // Start video processing videoJob = scope.launch { launch { produceFrames() } @@ -636,43 +636,6 @@ class WindowsVideoPlayerState : VideoPlayerState { } } - // Restore the volume setting for this instance - val storedVolume = instanceVolumes[instance] - if (storedVolume != null) { - val volumeRef = FloatByReference() - val hr = player.GetAudioVolume(instance, volumeRef) - if (hr >= 0 && storedVolume != volumeRef.value) { - val setHr = player.SetAudioVolume(instance, storedVolume) - if (setHr < 0) { - windowsLogger.e { "Error restoring volume (hr=0x${setHr.toString(16)})" } - } - } - } - - delay(100) - if (!isDisposing.get()) { - // Set the Kotlin state to match the native player state - _isPlaying = initializeplayerState == InitialPlayerState.PLAY - _hasMedia = true - - // If we're in PAUSE state, make sure userPaused is set to true - // This is critical for correct behavior when InitialPlayerState.PAUSE is specified - // The waitForPlaybackState method has logic that tries to - // restart playback if it's not playing and userPaused is false - // By setting userPaused to true, we prevent this automatic restart behavior - // when the user explicitly wants to start in a paused state - if (initializeplayerState == InitialPlayerState.PAUSE) { - userPaused = true - // Reset initialFrameRead flag to ensure we read one frame for display - initialFrameRead.set(false) - - // Explicitly set isLoading to false when in PAUSE state - // This ensures the UI doesn't show loading state indefinitely - // when the player is initialized with PAUSE state - isLoading = false - } - } - } catch (e: Exception) { setError("Error while opening media: ${e.message}") _hasMedia = false @@ -686,19 +649,23 @@ class WindowsVideoPlayerState : VideoPlayerState { /** * Updates the audio level meters */ - private suspend fun updateAudioLevels() { + private fun updateAudioLevels() { if (isDisposing.get()) return - mediaOperationMutex.withLock { - videoPlayerInstance?.let { instance -> - val leftRef = FloatByReference() - val rightRef = FloatByReference() - val hr = player.GetAudioLevels(instance, leftRef, rightRef) + // Use tryLock to avoid blocking media operations (open, seek, etc.) + // when polling audio levels. Skipped updates are retried in 50ms. + if (!mediaOperationMutex.tryLock()) return + try { + videoPlayerInstance.takeIf { it != 0L }?.let { instance -> + val levelsArr = FloatArray(2) + val hr = player.GetAudioLevels(instance, levelsArr) if (hr >= 0) { - _leftLevel = leftRef.value - _rightLevel = rightRef.value + _leftLevel = levelsArr[0] + _rightLevel = levelsArr[1] } } + } finally { + mediaOperationMutex.unlock() } } @@ -713,7 +680,8 @@ class WindowsVideoPlayerState : VideoPlayerState { */ private suspend fun produceFrames() { while (scope.isActive && _hasMedia && !isDisposing.get()) { - val instance = videoPlayerInstance ?: break + val instance = videoPlayerInstance + if (instance == 0L) break if (player.IsEOF(instance)) { if (loop) { @@ -748,11 +716,10 @@ class WindowsVideoPlayerState : VideoPlayerState { } try { - val ptrRef = PointerByReference() - val sizeRef = IntByReference() - val readResult = player.ReadVideoFrame(instance, ptrRef, sizeRef) + val hrArr = IntArray(1) + val srcBuffer = player.ReadVideoFrame(instance, hrArr) - if (readResult < 0 || ptrRef.value == null || sizeRef.value <= 0) { + if (hrArr[0] < 0 || srcBuffer == null) { yield() continue } @@ -766,13 +733,6 @@ class WindowsVideoPlayerState : VideoPlayerState { continue } - // Get the native frame buffer - val srcBuffer = ptrRef.value.getByteBuffer(0, sizeRef.value.toLong()) - if (srcBuffer == null) { - player.UnlockVideoFrame(instance) - yield() - continue - } srcBuffer.rewind() val pixelCount = width * height @@ -825,7 +785,12 @@ class WindowsVideoPlayerState : VideoPlayerState { // Single memory copy: native buffer → Skia bitmap val dstRowBytes = pixmap.rowBytes val dstSizeBytes = dstRowBytes.toLong() * height.toLong() - val dstBuffer = Pointer(pixelsAddr).getByteBuffer(0, dstSizeBytes) + val dstBuffer = MediaFoundationLib.nWrapPointer(pixelsAddr, dstSizeBytes) + ?: run { + player.UnlockVideoFrame(instance) + yield() + continue + } srcBuffer.rewind() copyBgraFrame(srcBuffer, dstBuffer, width, height, dstRowBytes) @@ -833,9 +798,9 @@ class WindowsVideoPlayerState : VideoPlayerState { player.UnlockVideoFrame(instance) // Get frame timestamp - val posRef = LongByReference() - val frameTime = if (player.GetMediaPosition(instance, posRef) >= 0) { - posRef.value / 10000000.0 + val posArr = LongArray(1) + val frameTime = if (player.GetMediaPosition(instance, posArr) >= 0) { + posArr[0] / 10000000.0 } else { 0.0 } @@ -862,32 +827,16 @@ class WindowsVideoPlayerState : VideoPlayerState { * and should not be closed here. */ private suspend fun consumeFrames() { - // Timeout mechanism to prevent getting stuck in loading state var frameReceived = false var loadingTimeout = 0 while (scope.isActive && _hasMedia && !isDisposing.get()) { - try { - // Wait for playback state, allowing initial frame when paused - // If the return value is false, we should wait and not process frames - if (!waitForPlaybackState(allowInitialFrame = true)) { - delay(100) // Add a small delay to prevent busy waiting - continue - } - } catch (e: CancellationException) { - break - } - - if (waitIfResizing()) { - continue - } + if (waitIfResizing()) continue try { val frameData = frameChannel.tryReceive().getOrNull() ?: run { - // If we're still loading and haven't received a frame yet, increment timeout counter if (isLoading && !frameReceived) { loadingTimeout++ - // After ~3 seconds (16ms * 200) of no frames while loading, force isLoading to false if (loadingTimeout > 200) { windowsLogger.w { "No frames received for 3 seconds, forcing isLoading to false" } isLoading = false @@ -898,21 +847,17 @@ class WindowsVideoPlayerState : VideoPlayerState { return@run null } ?: continue - // Reset timeout counter and mark that we've received a frame loadingTimeout = 0 frameReceived = true - // With double-buffering, we don't close old bitmaps - they're reused - // Just update the reference and create a new ImageBitmap view bitmapLock.write { _currentFrame = frameData.bitmap - // Update the currentFrameState with the new frame currentFrameState.value = frameData.bitmap.asComposeImageBitmap() } _currentTime = frameData.timestamp _progress = (_currentTime / _duration).toFloat().coerceIn(0f, 1f) - isLoading = false // Once frames start arriving, set isLoading to false + isLoading = false delay(1) @@ -928,61 +873,61 @@ class WindowsVideoPlayerState : VideoPlayerState { } /** - * Starts or resumes playback - * If no media is loaded but a previous URI exists, it will try to open and play it + * Starts or resumes playback. + * If media is not loaded yet (openUri in progress), waits for it to finish + * instead of triggering a second open which would race with the first. */ override fun play() { if (isDisposing.get()) return - if (!readyForPlayback()) { - lastUri?.takeIf { it.isNotEmpty() }?.let { uri -> - scope.launch { - openUri(uri) - delay(100) - if (readyForPlayback()) { - executeMediaOperation( - operation = "play after init", - precondition = true - ) { - setPlaybackState(true, "Error while starting playback after initialization") - } - } - } + if (readyForPlayback()) { + // Fast path: media is loaded, just resume + executeMediaOperation(operation = "play") { + resumePlayback() } return } - executeMediaOperation( - operation = "play", - precondition = true - ) { - userPaused = false - // Reset initialFrameRead flag when switching to play state - // This ensures that if we pause again, we'll read a new initial frame - initialFrameRead.set(false) - - setPlaybackState(true, "Error while starting playback") - if (_hasMedia && (videoJob == null || videoJob?.isActive == false)) { - videoJob = scope.launch { - launch { produceFrames() } - launch { consumeFrames() } + // Slow path: wait for any in-progress openUri to complete, then resume + scope.launch { + try { + withTimeout(10_000) { initReady.await() } + // Wait for _hasMedia to become true (set by openUriInternal) + withTimeout(10_000) { + snapshotFlow { _hasMedia }.filter { it }.first() } - } - - // Restore the volume setting for this instance - val instance = videoPlayerInstance - if (instance != null) { - val storedVolume = instanceVolumes[instance] - if (storedVolume != null) { - val volumeRef = FloatByReference() - val hr = player.GetAudioVolume(instance, volumeRef) - if (hr >= 0 && storedVolume != volumeRef.value) { - val setHr = player.SetAudioVolume(instance, storedVolume) - if (setHr < 0) { - windowsLogger.e { "Error restoring volume during play (hr=0x${setHr.toString(16)})" } - } + } catch (_: Exception) { + // Timeout or cancellation — if we still have a URI, try a fresh open + if (!_hasMedia) { + lastUri?.takeIf { it.isNotEmpty() }?.let { uri -> + openUriInternal(uri, InitialPlayerState.PLAY) } } + return@launch + } + + // Media is loaded — resume playback + mediaOperationMutex.withLock { + if (!isDisposing.get()) resumePlayback() + } + } + } + + /** + * Resumes playback — must be called under mediaOperationMutex. + */ + private fun resumePlayback() { + userPaused = false + initialFrameRead.set(false) + + if (!_isPlaying) { + setPlaybackState(true, "Error while starting playback") + } + + if (_hasMedia && (videoJob == null || videoJob?.isActive == false)) { + videoJob = scope.launch { + launch { produceFrames() } + launch { consumeFrames() } } } } @@ -1032,7 +977,7 @@ class WindowsVideoPlayerState : VideoPlayerState { // Reset initialFrameRead flag to ensure we read a new frame when playing again initialFrameRead.set(false) - videoPlayerInstance?.let { instance -> + videoPlayerInstance.takeIf { it != 0L }?.let { instance -> player.CloseMedia(instance) } } @@ -1043,10 +988,10 @@ class WindowsVideoPlayerState : VideoPlayerState { executeMediaOperation( operation = "seek", - precondition = _hasMedia && videoPlayerInstance != null + precondition = _hasMedia && videoPlayerInstance != 0L ) { val instance = videoPlayerInstance - if (instance != null) { + if (instance != 0L) { try { isLoading = true // If the video was playing before seeking, we should reset userPaused @@ -1075,9 +1020,9 @@ class WindowsVideoPlayerState : VideoPlayerState { } } - val posRef = LongByReference() - if (player.GetMediaPosition(instance, posRef) >= 0) { - _currentTime = posRef.value / 10000000.0 + val posArr2 = LongArray(1) + if (player.GetMediaPosition(instance, posArr2) >= 0) { + _currentTime = posArr2[0] / 10000000.0 _progress = (_currentTime / _duration).toFloat().coerceIn(0f, 1f) } @@ -1111,17 +1056,55 @@ class WindowsVideoPlayerState : VideoPlayerState { * Temporarily pauses frame processing to avoid artifacts during resize * For 4K videos, we need a longer delay to prevent memory pressure */ - fun onResized() { + fun onResized(width: Int = 0, height: Int = 0) { if (isDisposing.get()) return - // Mark resizing in progress and debounce rapid events without heavy operations + if (width <= 0 || height <= 0) return + + if (width == surfaceWidth && height == surfaceHeight) return + + surfaceWidth = width + surfaceHeight = height + + // Mark resizing in progress and debounce rapid events isResizing.set(true) - // Cancel any pending end-of-resize job and schedule a shorter debounce resizeJob?.cancel() resizeJob = scope.launch { - // Short debounce to smooth out successive resize events delay(120) - isResizing.set(false) + try { + applyOutputScaling() + } finally { + isResizing.set(false) + } + } + } + + /** + * Asks Media Foundation to produce frames at the display surface size + * instead of full native resolution. Saves significant memory for 4K+ video. + */ + private suspend fun applyOutputScaling() { + if (isDisposing.get() || !_hasMedia) return + val sw = surfaceWidth + val sh = surfaceHeight + if (sw <= 0 || sh <= 0) return + + val instance = videoPlayerInstance + if (instance == 0L) return + + mediaOperationMutex.withLock { + val hr = player.SetOutputSize(instance, sw, sh) + if (hr >= 0) { + // Update dimensions from native side + val sizeArr = IntArray(2) + player.GetVideoSize(instance, sizeArr) + if (sizeArr[0] > 0 && sizeArr[1] > 0) { + videoWidth = sizeArr[0] + videoHeight = sizeArr[1] + // Reset bitmaps so they are reallocated at the new size + lastFrameHash = Int.MIN_VALUE + } + } } } @@ -1154,7 +1137,7 @@ class WindowsVideoPlayerState : VideoPlayerState { * @return True if the operation succeeded, false otherwise */ private fun setPlaybackState(playing: Boolean, errorMessage: String, bStop: Boolean = false): Boolean { - return videoPlayerInstance?.let { instance -> + return videoPlayerInstance.takeIf { it != 0L }?.let { instance -> for (attempt in 1..3) { val res = player.SetPlaybackState(instance, playing, bStop) if (res >= 0) { @@ -1191,67 +1174,42 @@ class WindowsVideoPlayerState : VideoPlayerState { * @return True if the method should continue processing frames, false if it should wait */ private suspend fun waitForPlaybackState(allowInitialFrame: Boolean = false): Boolean { - // If playing, continue processing frames - if (_isPlaying) { - return true - } - - // If paused but we need an initial frame and haven't read one yet, allow one frame + if (_isPlaying) return true + + // 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 { - // If we're not playing and user has intentionally paused, wait indefinitely - // This prevents reading frames and advancing position when paused - if (userPaused) { - // Set isLoading to false to ensure UI doesn't show loading state indefinitely - if (isLoading) { - isLoading = false - } - - // Wait until the player starts playing - snapshotFlow { _isPlaying }.filter { it }.first() - return true - } - - // If we're not playing but not intentionally paused, wait with timeout - withTimeoutOrNull(5000) { - snapshotFlow { _isPlaying }.filter { it }.first() - } ?: run { - // Only attempt to restart playback if the user hasn't intentionally paused - if (_hasMedia && videoPlayerInstance != null && !userPaused && !isDisposing.get()) { - setPlaybackState(true, "Error while restarting playback after timeout") - delay(100) - if (!_isPlaying) { - yield() - } - } else { - // If user paused, just yield to allow other coroutines to run - yield() - } - } + snapshotFlow { _isPlaying }.filter { it }.first() } catch (e: CancellationException) { throw e - } catch (e: Exception) { - windowsLogger.e { "Error in waitForPlaybackState: ${e.message}" } - yield() } - - // Continue processing frames if playing, otherwise wait return _isPlaying } + /** Tracks how many consecutive iterations we've been waiting for resize */ + private var resizeWaitCount = 0 + /** - * Waits if the player is currently resizing - * Uses a longer delay for 4K videos to reduce memory pressure + * Waits if the player is currently resizing. + * Has a safety timeout to prevent infinite blocking. * * @return True if resizing is in progress and we waited, false otherwise */ private suspend fun waitIfResizing(): Boolean { if (isResizing.get()) { + resizeWaitCount++ + if (resizeWaitCount > 200) { // ~1.6s max wait + windowsLogger.w { "waitIfResizing: timeout after ${resizeWaitCount} iterations, forcing isResizing=false" } + isResizing.set(false) + resizeWaitCount = 0 + return false + } try { - // Keep the pipeline responsive during resize while avoiding busy-wait yield() delay(8) } catch (e: CancellationException) { @@ -1259,6 +1217,7 @@ class WindowsVideoPlayerState : VideoPlayerState { } return true } + resizeWaitCount = 0 return false } @@ -1268,7 +1227,7 @@ class WindowsVideoPlayerState : VideoPlayerState { * @return True if the player is initialized and has media loaded, false otherwise */ private fun readyForPlayback(): Boolean { - return initReady.isCompleted && videoPlayerInstance != null && _hasMedia && !isDisposing.get() + return initReady.isCompleted && videoPlayerInstance != 0L && _hasMedia && !isDisposing.get() } /** diff --git a/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/windows/WindowsVideoPlayerSurface.kt b/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/windows/WindowsVideoPlayerSurface.kt index bae0457..5c7bcd1 100644 --- a/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/windows/WindowsVideoPlayerSurface.kt +++ b/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/windows/WindowsVideoPlayerSurface.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -40,19 +39,9 @@ fun WindowsVideoPlayerSurface( overlay: @Composable () -> Unit = {}, isInFullscreenWindow: Boolean = false, ) { - // Keep track of when this instance is first composed with this player state - val isFirstComposition = remember(playerState) { true } - - // Only trigger resizing on first composition with this player state - LaunchedEffect(playerState) { - if (isFirstComposition) { - playerState.onResized() - } - } - Box( - modifier = modifier.onSizeChanged { - playerState.onResized() + modifier = modifier.onSizeChanged { size -> + playerState.onResized(size.width, size.height) }, contentAlignment = Alignment.Center ) { diff --git a/mediaplayer/src/jvmMain/native/windows/AudioManager.cpp b/mediaplayer/src/jvmMain/native/windows/AudioManager.cpp index f83af94..6fdf045 100644 --- a/mediaplayer/src/jvmMain/native/windows/AudioManager.cpp +++ b/mediaplayer/src/jvmMain/native/windows/AudioManager.cpp @@ -1,4 +1,4 @@ -// AudioManager_improved.cpp – full rewrite with tighter A/V synchronisation +// AudioManager.cpp – full rewrite with tighter A/V synchronisation // ----------------------------------------------------------------------------- // * Keeps the original public API so that existing call‑sites still compile. // * Uses an event‑driven render loop instead of busy‑wait polling where possible. @@ -16,6 +16,13 @@ #include #include #include +#include +// WAVE_FORMAT_EXTENSIBLE sub-format GUIDs for volume scaling. +// Defined inline to avoid pulling in / which may conflict. +static const GUID kSubtypePCM = + {0x00000001, 0x0000, 0x0010, {0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71}}; +static const GUID kSubtypeIEEEFloat = + {0x00000003, 0x0000, 0x0010, {0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71}}; using namespace VideoPlayerUtils; @@ -34,7 +41,7 @@ HRESULT InitWASAPI(VideoPlayerInstance* inst, const WAVEFORMATEX* srcFmt) { if (!inst) return E_INVALIDARG; - // Re‑use previously initialised client if still valid + // Reuse previously initialized client if still valid if (inst->pAudioClient && inst->pRenderClient) { inst->bAudioInitialized = TRUE; return S_OK; @@ -48,56 +55,69 @@ HRESULT InitWASAPI(VideoPlayerInstance* inst, const WAVEFORMATEX* srcFmt) if (!enumerator) return E_FAIL; hr = enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &inst->pDevice); - if (FAILED(hr)) return hr; + if (FAILED(hr)) goto fail; // 2. Activate IAudioClient + IAudioEndpointVolume hr = inst->pDevice->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, reinterpret_cast(&inst->pAudioClient)); - if (FAILED(hr)) return hr; + if (FAILED(hr)) goto fail; hr = inst->pDevice->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_ALL, nullptr, reinterpret_cast(&inst->pAudioEndpointVolume)); - if (FAILED(hr)) return hr; + if (FAILED(hr)) goto fail; // 3. Determine the format that will be rendered if (!srcFmt) { hr = inst->pAudioClient->GetMixFormat(&deviceMixFmt); - if (FAILED(hr)) return hr; - srcFmt = deviceMixFmt; // use mix format as fall‑back + if (FAILED(hr)) goto fail; + srcFmt = deviceMixFmt; } - inst->pSourceAudioFormat = reinterpret_cast(CoTaskMemAlloc(srcFmt->cbSize + sizeof(WAVEFORMATEX))); + 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)); - // 4. Create (or re‑use) the render‑ready event + // 4. Create (or reuse) the render-ready event if (!inst->hAudioSamplesReadyEvent) { inst->hAudioSamplesReadyEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); if (!inst->hAudioSamplesReadyEvent) { hr = HRESULT_FROM_WIN32(GetLastError()); - goto cleanup; + goto fail; } } - // 5. Initialise the audio client in shared, event‑callback mode + // 5. Initialize the audio client in shared, event-callback mode hr = inst->pAudioClient->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_EVENTCALLBACK, - kTargetBufferDuration100ns, // buffer dur - 0, // periodicity → let system decide + kTargetBufferDuration100ns, + 0, srcFmt, nullptr); - if (FAILED(hr)) goto cleanup; + if (FAILED(hr)) goto fail; hr = inst->pAudioClient->SetEventHandle(inst->hAudioSamplesReadyEvent); - if (FAILED(hr)) goto cleanup; + if (FAILED(hr)) goto fail; - // 6. Grab the render‑client service interface + // 6. Grab the render-client service interface hr = inst->pAudioClient->GetService(__uuidof(IAudioRenderClient), reinterpret_cast(&inst->pRenderClient)); - if (FAILED(hr)) goto cleanup; + if (FAILED(hr)) goto fail; inst->bAudioInitialized = TRUE; + if (deviceMixFmt) CoTaskMemFree(deviceMixFmt); + return S_OK; -cleanup: +fail: + // Release any partially-created COM objects so that CloseMedia does not + // call methods (e.g. pAudioClient->Stop()) on an uninitialized client. + 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; } @@ -165,7 +185,7 @@ DWORD WINAPI AudioThreadProc(LPVOID lpParam) LONGLONG elapsedMs = currentTimeMs - inst->llPlaybackStartTime - inst->llTotalPauseTime; // Apply playback speed to elapsed time - double adjustedElapsedMs = elapsedMs * inst->playbackSpeed; + double adjustedElapsedMs = elapsedMs * inst->playbackSpeed.load(std::memory_order_relaxed); // Convert sample timestamp from 100ns units to milliseconds double sampleTimeMs = ts100n / 10000.0; @@ -217,18 +237,44 @@ DWORD WINAPI AudioThreadProc(LPVOID lpParam) const BYTE* chunkStart = srcData + (offsetFrames * blockAlign); memcpy(dstData, chunkStart, framesWanted * blockAlign); - // Apply per‑instance volume in‑place (16‑bit PCM or IEEE‑float) - if (inst->instanceVolume < 0.999f) { - if (inst->pSourceAudioFormat->wFormatTag == WAVE_FORMAT_PCM && - inst->pSourceAudioFormat->wBitsPerSample == 16) { + // Apply per-instance volume in-place. + // Supports PCM 16-bit, PCM 24-bit, IEEE float 32-bit, and + // WAVE_FORMAT_EXTENSIBLE wrappers around those sub-formats. + const float vol = inst->instanceVolume.load(std::memory_order_relaxed); + if (vol < 0.999f) { + WORD formatTag = inst->pSourceAudioFormat->wFormatTag; + WORD bitsPerSample = inst->pSourceAudioFormat->wBitsPerSample; + + // Unwrap WAVE_FORMAT_EXTENSIBLE to the actual sub-format + if (formatTag == WAVE_FORMAT_EXTENSIBLE && inst->pSourceAudioFormat->cbSize >= 22) { + auto* ext = reinterpret_cast(inst->pSourceAudioFormat); + if (ext->SubFormat == kSubtypePCM) + formatTag = WAVE_FORMAT_PCM; + else if (ext->SubFormat == kSubtypeIEEEFloat) + formatTag = WAVE_FORMAT_IEEE_FLOAT; + } + + if (formatTag == WAVE_FORMAT_PCM && bitsPerSample == 16) { auto* s = reinterpret_cast(dstData); size_t n = (framesWanted * blockAlign) / sizeof(int16_t); - for (size_t i = 0; i < n; ++i) s[i] = static_cast(s[i] * inst->instanceVolume); - } else if (inst->pSourceAudioFormat->wFormatTag == WAVE_FORMAT_IEEE_FLOAT && - inst->pSourceAudioFormat->wBitsPerSample == 32) { + for (size_t i = 0; i < n; ++i) + s[i] = static_cast(s[i] * vol); + } else if (formatTag == WAVE_FORMAT_PCM && bitsPerSample == 24) { + // 24-bit PCM: 3 bytes per sample, little-endian + size_t totalBytes = framesWanted * blockAlign; + for (size_t i = 0; i + 2 < totalBytes; i += 3) { + int32_t sample = static_cast(dstData[i + 2]); + sample = (sample << 8) | dstData[i + 1]; + sample = (sample << 8) | dstData[i]; + sample = static_cast(sample * vol); + dstData[i] = static_cast(sample & 0xFF); + dstData[i + 1] = static_cast((sample >> 8) & 0xFF); + dstData[i + 2] = static_cast((sample >> 16) & 0xFF); + } + } else if (formatTag == WAVE_FORMAT_IEEE_FLOAT && bitsPerSample == 32) { auto* s = reinterpret_cast(dstData); size_t n = (framesWanted * blockAlign) / sizeof(float); - for (size_t i = 0; i < n; ++i) s[i] *= inst->instanceVolume; + for (size_t i = 0; i < n; ++i) s[i] *= vol; } } @@ -296,14 +342,14 @@ void StopAudioThread(VideoPlayerInstance* inst) HRESULT SetVolume(VideoPlayerInstance* inst, float vol) { if (!inst) return E_INVALIDARG; - inst->instanceVolume = std::clamp(vol, 0.0f, 1.0f); + inst->instanceVolume.store(std::clamp(vol, 0.0f, 1.0f), std::memory_order_relaxed); return S_OK; } HRESULT GetVolume(const VideoPlayerInstance* inst, float* out) { if (!inst || !out) return E_INVALIDARG; - *out = inst->instanceVolume; + *out = inst->instanceVolume.load(std::memory_order_relaxed); return S_OK; } diff --git a/mediaplayer/src/jvmMain/native/windows/CMakeLists.txt b/mediaplayer/src/jvmMain/native/windows/CMakeLists.txt index a036a3a..4dd8955 100644 --- a/mediaplayer/src/jvmMain/native/windows/CMakeLists.txt +++ b/mediaplayer/src/jvmMain/native/windows/CMakeLists.txt @@ -3,6 +3,9 @@ project(NativeVideoPlayer LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) +# Find JNI +find_package(JNI REQUIRED) + # Check target architecture if(CMAKE_GENERATOR_PLATFORM STREQUAL "x64" OR CMAKE_GENERATOR_PLATFORM STREQUAL "") set(TARGET_ARCH "x64") @@ -30,8 +33,12 @@ add_library(NativeVideoPlayer SHARED MediaFoundationManager.h AudioManager.cpp AudioManager.h + 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 diff --git a/mediaplayer/src/jvmMain/native/windows/MediaFoundationManager.cpp b/mediaplayer/src/jvmMain/native/windows/MediaFoundationManager.cpp index 74e1f93..30092a2 100644 --- a/mediaplayer/src/jvmMain/native/windows/MediaFoundationManager.cpp +++ b/mediaplayer/src/jvmMain/native/windows/MediaFoundationManager.cpp @@ -2,6 +2,7 @@ #include #include #include +#include namespace MediaFoundation { @@ -11,7 +12,7 @@ static ID3D11Device* g_pD3DDevice = nullptr; static IMFDXGIDeviceManager* g_pDXGIDeviceManager = nullptr; static UINT32 g_dwResetToken = 0; static IMMDeviceEnumerator* g_pEnumerator = nullptr; -static int g_instanceCount = 0; +static std::atomic g_instanceCount{0}; HRESULT Initialize() { if (g_bMFInitialized) @@ -33,14 +34,26 @@ HRESULT Initialize() { if (SUCCEEDED(hr)) hr = g_pDXGIDeviceManager->ResetDevice(g_pD3DDevice, g_dwResetToken); if (FAILED(hr)) { - if (g_pD3DDevice) { - g_pD3DDevice->Release(); - g_pD3DDevice = nullptr; + if (g_pD3DDevice) { + g_pD3DDevice->Release(); + g_pD3DDevice = nullptr; } MFShutdown(); 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)); + if (FAILED(hr)) { + g_pDXGIDeviceManager->Release(); + g_pDXGIDeviceManager = nullptr; + g_pD3DDevice->Release(); + g_pD3DDevice = nullptr; + MFShutdown(); + return hr; + } + g_bMFInitialized = true; return S_OK; } @@ -104,10 +117,6 @@ IMFDXGIDeviceManager* GetDXGIDeviceManager() { } IMMDeviceEnumerator* GetDeviceEnumerator() { - if (!g_pEnumerator) { - CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, - IID_PPV_ARGS(&g_pEnumerator)); - } return g_pEnumerator; } diff --git a/mediaplayer/src/jvmMain/native/windows/NativeVideoPlayer.cpp b/mediaplayer/src/jvmMain/native/windows/NativeVideoPlayer.cpp index 893e5cb..0615998 100644 --- a/mediaplayer/src/jvmMain/native/windows/NativeVideoPlayer.cpp +++ b/mediaplayer/src/jvmMain/native/windows/NativeVideoPlayer.cpp @@ -28,13 +28,202 @@ using namespace AudioManager; #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. +// --------------------------------------------------------------------------- +static HRESULT ConfigureAudioType(IMFMediaType* pType, UINT32 channels, UINT32 sampleRate) { + if (channels == 0) channels = 2; + if (sampleRate == 0) sampleRate = 48000; + + UINT32 bitsPerSample = 16; + UINT32 blockAlign = channels * (bitsPerSample / 8); + UINT32 avgBytesPerSec = sampleRate * blockAlign; + + pType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio); + pType->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM); + pType->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, channels); + pType->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, sampleRate); + pType->SetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, blockAlign); + pType->SetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND, avgBytesPerSec); + pType->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, bitsPerSample); + 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(); + } +} + +// --------------------------------------------------------------------------- +// 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. +// --------------------------------------------------------------------------- +static HRESULT AcquireNextSample(VideoPlayerInstance* pInstance, IMFSample** ppSample) { + *ppSample = nullptr; + + BOOL isPaused = (pInstance->llPauseStart != 0); + IMFSample* pSample = nullptr; + HRESULT hr = S_OK; + DWORD streamIndex = 0, dwFlags = 0; + LONGLONG llTimestamp = 0; + + if (isPaused) { + // ----- Paused path: read one frame and cache, or reuse cached frame ----- + if (!pInstance->bHasInitialFrame) { + 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; + } + if (!pSample) return S_OK; // decoder starved + + if (pInstance->pCachedSample) { + pInstance->pCachedSample->Release(); + pInstance->pCachedSample = nullptr; + } + 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; + } + 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; + } + + // On the first decoded frame after play/seek, recalibrate the wall clock + // so that any decode or network latency doesn't cause mass frame skipping. + // This is critical for HTTP sources where ReadSample may block for seconds. + if (!pInstance->bHasInitialFrame) { + if (pInstance->bUseClockSync && pInstance->llPlaybackStartTime != 0) { + double frameTimeMs = llTimestamp / 10000.0; + double adjustedMs = frameTimeMs / static_cast(pInstance->playbackSpeed.load()); + pInstance->llPlaybackStartTime = GetCurrentTimeMs() - static_cast(adjustedMs); + pInstance->llTotalPauseTime = 0; + } + pInstance->bHasInitialFrame = TRUE; + } + + pInstance->llCurrentPosition = llTimestamp; + } + + // ----- Frame timing synchronization (wall-clock based) ----- + if (!isPaused && pInstance->bUseClockSync && + pInstance->llPlaybackStartTime != 0 && llTimestamp > 0) { + + LONGLONG currentTimeMs = GetCurrentTimeMs(); + LONGLONG elapsedMs = currentTimeMs - pInstance->llPlaybackStartTime - pInstance->llTotalPauseTime; + double adjustedElapsedMs = elapsedMs * pInstance->playbackSpeed.load(); + double frameTimeMs = llTimestamp / 10000.0; + + // Determine frame interval, guarding against division by zero (issue #3) + UINT frameRateNum = kDefaultFrameRateNum, frameRateDenom = kDefaultFrameRateDenom; + GetVideoFrameRate(pInstance, &frameRateNum, &frameRateDenom); + if (frameRateNum == 0) { + frameRateNum = kDefaultFrameRateNum; + frameRateDenom = kDefaultFrameRateDenom; + } + double frameIntervalMs = 1000.0 * frameRateDenom / frameRateNum; + + double diffMs = frameTimeMs - adjustedElapsedMs; + + 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); + } + } + + *ppSample = pSample; + return S_OK; +} + +// ==================================================================== // API Implementation +// ==================================================================== + +NATIVEVIDEOPLAYER_API int GetNativeVersion() { + return NATIVE_VIDEO_PLAYER_VERSION; +} + NATIVEVIDEOPLAYER_API HRESULT InitMediaFoundation() { return Initialize(); } NATIVEVIDEOPLAYER_API HRESULT CreateVideoPlayerInstance(VideoPlayerInstance** ppInstance) { - // Parameter validation if (!ppInstance) return E_INVALIDARG; @@ -45,17 +234,13 @@ NATIVEVIDEOPLAYER_API HRESULT CreateVideoPlayerInstance(VideoPlayerInstance** pp return hr; } - // Allocate and initialize a new instance auto* pInstance = new (std::nothrow) VideoPlayerInstance(); if (!pInstance) return E_OUTOFMEMORY; - // Initialize critical section for synchronization InitializeCriticalSection(&pInstance->csClockSync); - pInstance->bUseClockSync = TRUE; - // Create audio synchronization event pInstance->hAudioReadyEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); if (!pInstance->hAudioReadyEvent) { DeleteCriticalSection(&pInstance->csClockSync); @@ -63,7 +248,6 @@ NATIVEVIDEOPLAYER_API HRESULT CreateVideoPlayerInstance(VideoPlayerInstance** pp return HRESULT_FROM_WIN32(GetLastError()); } - // Increment instance count and return the instance IncrementInstanceCount(); *ppInstance = pInstance; return S_OK; @@ -71,27 +255,20 @@ NATIVEVIDEOPLAYER_API HRESULT CreateVideoPlayerInstance(VideoPlayerInstance** pp NATIVEVIDEOPLAYER_API void DestroyVideoPlayerInstance(VideoPlayerInstance* pInstance) { if (pInstance) { - // Ensure all media resources are released CloseMedia(pInstance); - // Double-check that cached sample is released - // This is already done in CloseMedia, but we do it again as a safety measure if (pInstance->pCachedSample) { pInstance->pCachedSample->Release(); pInstance->pCachedSample = nullptr; } - // Delete critical section DeleteCriticalSection(&pInstance->csClockSync); - - // Delete instance and decrement counter delete pInstance; DecrementInstanceCount(); } } NATIVEVIDEOPLAYER_API HRESULT OpenMedia(VideoPlayerInstance* pInstance, const wchar_t* url, BOOL startPlayback) { - // Parameter validation if (!pInstance || !url) return OP_E_INVALID_PARAMETER; if (!IsInitialized()) @@ -103,7 +280,6 @@ NATIVEVIDEOPLAYER_API HRESULT OpenMedia(VideoPlayerInstance* pInstance, const wc pInstance->videoWidth = pInstance->videoHeight = 0; pInstance->bHasAudio = FALSE; - // Initialize frame caching for paused state pInstance->bHasInitialFrame = FALSE; if (pInstance->pCachedSample) { pInstance->pCachedSample->Release(); @@ -112,9 +288,6 @@ NATIVEVIDEOPLAYER_API HRESULT OpenMedia(VideoPlayerInstance* pInstance, const wc HRESULT hr = S_OK; - // Helper function to safely release COM objects - auto safeRelease = [](IUnknown* obj) { if (obj) obj->Release(); }; - // 1. Configure and open media source with both audio and video streams // ------------------------------------------------------------------ IMFAttributes* pAttributes = nullptr; @@ -122,30 +295,24 @@ NATIVEVIDEOPLAYER_API HRESULT OpenMedia(VideoPlayerInstance* pInstance, const wc if (FAILED(hr)) return hr; - // Configure attributes for hardware acceleration pAttributes->SetUINT32(MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS, TRUE); pAttributes->SetUINT32(MF_SOURCE_READER_DISABLE_DXVA, FALSE); pAttributes->SetUnknown(MF_SOURCE_READER_D3D_MANAGER, GetDXGIDeviceManager()); - - // Enable advanced video processing for better synchronization pAttributes->SetUINT32(MF_SOURCE_READER_ENABLE_ADVANCED_VIDEO_PROCESSING, TRUE); - // Create source reader for both audio and video hr = MFCreateSourceReaderFromURL(url, pAttributes, &pInstance->pSourceReader); - safeRelease(pAttributes); + SafeRelease(pAttributes); if (FAILED(hr)) return hr; - // 2. Configure video stream + // 2. Configure video stream (RGB32) // ------------------------------------------ - // Enable video stream 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; - // Configure video format (RGB32) IMFMediaType* pType = nullptr; hr = MFCreateMediaType(&pType); if (SUCCEEDED(hr)) { @@ -154,93 +321,92 @@ NATIVEVIDEOPLAYER_API HRESULT OpenMedia(VideoPlayerInstance* pInstance, const wc 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); + SafeRelease(pType); } if (FAILED(hr)) return hr; - // Get video dimensions + // 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); - safeRelease(pCurrent); + pInstance->nativeWidth = pInstance->videoWidth; + pInstance->nativeHeight = pInstance->videoHeight; + SafeRelease(pCurrent); } // 3. Configure audio stream (if available) // ------------------------------------------ - // Try to enable audio stream hr = pInstance->pSourceReader->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, TRUE); if (SUCCEEDED(hr)) { - // Configure audio format (PCM 16-bit stereo 48kHz) - IMFMediaType* pWantedType = nullptr; - hr = MFCreateMediaType(&pWantedType); - if (SUCCEEDED(hr)) { - pWantedType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio); - pWantedType->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM); - pWantedType->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, 2); - pWantedType->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, 48000); - pWantedType->SetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, 4); - pWantedType->SetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND, 192000); - pWantedType->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, 16); - hr = pInstance->pSourceReader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, nullptr, pWantedType); - safeRelease(pWantedType); - } + // Try native audio params first, fall back to 2ch/48kHz if WASAPI rejects them + UINT32 nativeChannels = 0, nativeSampleRate = 0; + QueryNativeAudioParams(pInstance->pSourceReader, &nativeChannels, &nativeSampleRate); + + // 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; - if (SUCCEEDED(hr)) { - // Get the actual audio format for WASAPI IMFMediaType* pActualType = nullptr; - hr = pInstance->pSourceReader->GetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, &pActualType); - if (SUCCEEDED(hr) && pActualType) { - WAVEFORMATEX* pWfx = nullptr; - UINT32 size = 0; - hr = MFCreateWaveFormatExFromMFMediaType(pActualType, &pWfx, &size); - if (SUCCEEDED(hr) && pWfx) { - hr = InitWASAPI(pInstance, pWfx); - if (FAILED(hr)) { - PrintHR("InitWASAPI failed", hr); - if (pWfx) CoTaskMemFree(pWfx); - safeRelease(pActualType); - } else { - if (pInstance->pSourceAudioFormat) - CoTaskMemFree(pInstance->pSourceAudioFormat); - pInstance->pSourceAudioFormat = pWfx; - pInstance->bHasAudio = TRUE; - } - } - safeRelease(pActualType); + hrt = pInstance->pSourceReader->GetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, &pActualType); + if (FAILED(hrt) || !pActualType) return false; + + WAVEFORMATEX* pWfx = nullptr; + UINT32 size = 0; + hrt = MFCreateWaveFormatExFromMFMediaType(pActualType, &pWfx, &size); + SafeRelease(pActualType); + if (FAILED(hrt) || !pWfx) return false; + + hrt = InitWASAPI(pInstance, pWfx); + if (FAILED(hrt)) { + PrintHR("InitWASAPI failed", hrt); + CoTaskMemFree(pWfx); + return false; + } + if (pInstance->pSourceAudioFormat) CoTaskMemFree(pInstance->pSourceAudioFormat); + pInstance->pSourceAudioFormat = pWfx; + 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); } } // Create a separate audio source reader for the audio thread - // This is needed even with automatic synchronization hr = MFCreateSourceReaderFromURL(url, nullptr, &pInstance->pSourceReaderAudio); if (SUCCEEDED(hr)) { - // Select only audio stream hr = pInstance->pSourceReaderAudio->SetStreamSelection(MF_SOURCE_READER_ALL_STREAMS, FALSE); if (SUCCEEDED(hr)) hr = pInstance->pSourceReaderAudio->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, TRUE); if (SUCCEEDED(hr)) { - // Configure audio format (same as main reader) + // Use the same format that succeeded for the main reader + UINT32 usedCh = pInstance->pSourceAudioFormat ? pInstance->pSourceAudioFormat->nChannels : 2; + UINT32 usedSr = pInstance->pSourceAudioFormat ? pInstance->pSourceAudioFormat->nSamplesPerSec : 48000; + IMFMediaType* pWantedAudioType = nullptr; hr = MFCreateMediaType(&pWantedAudioType); if (SUCCEEDED(hr)) { - pWantedAudioType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio); - pWantedAudioType->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM); - pWantedAudioType->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, 2); - pWantedAudioType->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, 48000); - pWantedAudioType->SetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, 4); - pWantedAudioType->SetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND, 192000); - pWantedAudioType->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, 16); + ConfigureAudioType(pWantedAudioType, usedCh, usedSr); hr = pInstance->pSourceReaderAudio->SetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, nullptr, pWantedAudioType); - safeRelease(pWantedAudioType); + SafeRelease(pWantedAudioType); } } if (FAILED(hr)) { PrintHR("Failed to configure audio source reader", hr); - safeRelease(pInstance->pSourceReaderAudio); + SafeRelease(pInstance->pSourceReaderAudio); pInstance->pSourceReaderAudio = nullptr; } } else { @@ -251,28 +417,22 @@ NATIVEVIDEOPLAYER_API HRESULT OpenMedia(VideoPlayerInstance* pInstance, const wc if (pInstance->bUseClockSync) { // 4. Set up presentation clock for synchronization // ---------------------------------------------------------- - // Get the media source from the source reader hr = pInstance->pSourceReader->GetServiceForStream( MF_SOURCE_READER_MEDIASOURCE, GUID_NULL, IID_PPV_ARGS(&pInstance->pMediaSource)); if (SUCCEEDED(hr)) { - // Create the presentation clock hr = MFCreatePresentationClock(&pInstance->pPresentationClock); if (SUCCEEDED(hr)) { - // Create a system time source IMFPresentationTimeSource* pTimeSource = nullptr; hr = MFCreateSystemTimeSource(&pTimeSource); if (SUCCEEDED(hr)) { - // Set the time source on the presentation clock hr = pInstance->pPresentationClock->SetTimeSource(pTimeSource); if (SUCCEEDED(hr)) { - // Set the rate control on the presentation clock IMFRateControl* pRateControl = nullptr; hr = pInstance->pPresentationClock->QueryInterface(IID_PPV_ARGS(&pRateControl)); if (SUCCEEDED(hr)) { - // Explicitly set rate to 1.0 to ensure correct initial playback speed hr = pRateControl->SetRate(FALSE, 1.0f); if (FAILED(hr)) { PrintHR("Failed to set initial presentation clock rate", hr); @@ -280,30 +440,22 @@ NATIVEVIDEOPLAYER_API HRESULT OpenMedia(VideoPlayerInstance* pInstance, const wc pRateControl->Release(); } - // Get the media sink from the media source IMFMediaSink* pMediaSink = nullptr; hr = pInstance->pMediaSource->QueryInterface(IID_PPV_ARGS(&pMediaSink)); if (SUCCEEDED(hr)) { - // Set the presentation clock on the media sink IMFClockStateSink* pClockStateSink = nullptr; hr = pMediaSink->QueryInterface(IID_PPV_ARGS(&pClockStateSink)); if (SUCCEEDED(hr)) { - // Start the presentation clock only if startPlayback is TRUE - // This allows the player to be initialized in a paused state - // when InitialPlayerState.PAUSE is specified in the Kotlin code if (startPlayback) { hr = pInstance->pPresentationClock->Start(0); if (FAILED(hr)) { PrintHR("Failed to start presentation clock", hr); } } else { - // If not starting playback, initialize the clock but don't start it - // This keeps the player in a paused state until explicitly started + // Keep the player paused until explicitly started hr = pInstance->pPresentationClock->Pause(); if (FAILED(hr)) { PrintHR("Failed to pause presentation clock", hr); - // Continue even if pause fails - this is not a critical error - // The player will still be usable, just not in the ideal initial state } } pClockStateSink->Release(); @@ -313,7 +465,7 @@ NATIVEVIDEOPLAYER_API HRESULT OpenMedia(VideoPlayerInstance* pInstance, const wc PrintHR("Failed to get media sink from media source", hr); } } - safeRelease(pTimeSource); + SafeRelease(pTimeSource); } } } @@ -322,13 +474,10 @@ NATIVEVIDEOPLAYER_API HRESULT OpenMedia(VideoPlayerInstance* pInstance, const wc // 5. Initialize playback timing and start audio thread // ---------------------------------------------------- if (startPlayback) { - // IMPORTANT: Initialize llPlaybackStartTime when starting playback - // This is crucial for A/V synchronization - without this, the sync code won't work pInstance->llPlaybackStartTime = GetCurrentTimeMs(); pInstance->llTotalPauseTime = 0; pInstance->llPauseStart = 0; - // Start audio thread if audio is available if (pInstance->bHasAudio && pInstance->bAudioInitialized && pInstance->pSourceReaderAudio) { hr = StartAudioThread(pInstance); if (FAILED(hr)) { @@ -340,6 +489,9 @@ NATIVEVIDEOPLAYER_API HRESULT OpenMedia(VideoPlayerInstance* pInstance, const wc 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 || !pInstance->pSourceReader || !pData || !pDataSize) return OP_E_NOT_INITIALIZED; @@ -353,126 +505,25 @@ NATIVEVIDEOPLAYER_API HRESULT ReadVideoFrame(VideoPlayerInstance* pInstance, BYT return S_FALSE; } - // Check if player is paused - BOOL isPaused = (pInstance->llPauseStart != 0); IMFSample* pSample = nullptr; - HRESULT hr = S_OK; - DWORD streamIndex = 0, dwFlags = 0; - LONGLONG llTimestamp = 0; - - if (isPaused) { - // Player is paused - check if we need to read an initial frame - if (!pInstance->bHasInitialFrame) { - // Read one frame when paused and cache it - 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(); - *pData = nullptr; - *pDataSize = 0; - return S_FALSE; - } + HRESULT hr = AcquireNextSample(pInstance, &pSample); - if (!pSample) { - *pData = nullptr; - *pDataSize = 0; - return S_OK; - } - - // Store the frame for future use - if (pInstance->pCachedSample) { - pInstance->pCachedSample->Release(); - pInstance->pCachedSample = nullptr; - } - pSample->AddRef(); // Add reference for the cached sample - pInstance->pCachedSample = pSample; - pInstance->bHasInitialFrame = TRUE; - - // Don't update position when paused - keep the current position - } else { - // Already have an initial frame, use the cached sample - if (pInstance->pCachedSample) { - pSample = pInstance->pCachedSample; - pSample->AddRef(); // Add reference for this function's use - // Don't update position when paused - } else { - // No cached sample available (shouldn't happen) - *pData = nullptr; - *pDataSize = 0; - return S_OK; - } - } - } else { - // Player is playing - read 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(); - *pData = nullptr; - *pDataSize = 0; - return S_FALSE; - } - - if (!pSample) { - *pData = nullptr; - *pDataSize = 0; - return S_OK; - } - - // Update cached sample for future paused state - if (pInstance->pCachedSample) { - pInstance->pCachedSample->Release(); - pInstance->pCachedSample = nullptr; - } - pSample->AddRef(); // Add reference for the cached sample - pInstance->pCachedSample = pSample; - - // Store current position when playing - pInstance->llCurrentPosition = llTimestamp; + if (hr == S_FALSE) { + // End of stream + *pData = nullptr; + *pDataSize = 0; + return S_FALSE; } - - // Synchronization using wall clock time (real elapsed time since playback started) - // This is more reliable than the presentation clock which is not tied to the source reader - if (pInstance->bUseClockSync && pInstance->llPlaybackStartTime != 0 && llTimestamp > 0) { - // Calculate elapsed time since playback started (in milliseconds) - LONGLONG currentTimeMs = GetCurrentTimeMs(); - LONGLONG elapsedMs = currentTimeMs - pInstance->llPlaybackStartTime - pInstance->llTotalPauseTime; - - // Apply playback speed to elapsed time - double adjustedElapsedMs = elapsedMs * pInstance->playbackSpeed; - - // Convert frame timestamp from 100ns units to milliseconds - double frameTimeMs_ts = llTimestamp / 10000.0; - - // Calculate frame rate for skip threshold - UINT frameRateNum = 60, frameRateDenom = 1; - GetVideoFrameRate(pInstance, &frameRateNum, &frameRateDenom); - double frameIntervalMs = 1000.0 * frameRateDenom / frameRateNum; - - // Calculate difference: positive means frame is ahead, negative means frame is late - double diffMs = frameTimeMs_ts - adjustedElapsedMs; - - // If frame is very late (more than 3 frames behind), skip it - if (diffMs < -frameIntervalMs * 3) { - pSample->Release(); - *pData = nullptr; - *pDataSize = 0; - return S_OK; - } - // If frame is ahead of schedule, wait to maintain correct frame rate - else if (diffMs > 1.0) { - // Limit maximum wait time to avoid freezing if timestamps are far apart - double waitTime = std::min(diffMs, frameIntervalMs * 2); - PreciseSleepHighRes(waitTime); - } + if (FAILED(hr)) + return hr; + if (!pSample) { + // Frame was skipped or decoder starved + *pData = nullptr; + *pDataSize = 0; + return S_OK; } + // Lock the buffer and expose its pointer to the caller IMFMediaBuffer* pBuffer = nullptr; DWORD bufferCount = 0; hr = pSample->GetBufferCount(&bufferCount); @@ -497,6 +548,16 @@ NATIVEVIDEOPLAYER_API HRESULT ReadVideoFrame(VideoPlayerInstance* pInstance, BYT 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; + } + pInstance->pLockedBuffer = pBuffer; pInstance->pLockedBytes = pBytes; pInstance->lockedMaxSize = cbMax; @@ -520,19 +581,20 @@ NATIVEVIDEOPLAYER_API HRESULT UnlockVideoFrame(VideoPlayerInstance* pInstance) { return S_OK; } +// --------------------------------------------------------------------------- +// ReadVideoFrameInto — copies the decoded frame into a caller-owned buffer +// --------------------------------------------------------------------------- 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 || !pDst || dstRowBytes == 0 || dstCapacity == 0) + return OP_E_INVALID_PARAMETER; if (!pInstance->pSourceReader) return OP_E_NOT_INITIALIZED; - if (pInstance->pLockedBuffer) UnlockVideoFrame(pInstance); @@ -541,98 +603,22 @@ NATIVEVIDEOPLAYER_API HRESULT ReadVideoFrameInto( return S_FALSE; } - // Check if player is paused - BOOL isPaused = (pInstance->llPauseStart != 0); IMFSample* pSample = nullptr; - HRESULT hr = S_OK; - DWORD streamIndex = 0, dwFlags = 0; - LONGLONG llTimestamp = 0; - - if (isPaused) { - if (!pInstance->bHasInitialFrame) { - 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(); - if (pTimestamp) *pTimestamp = pInstance->llCurrentPosition; - return S_FALSE; - } + HRESULT hr = AcquireNextSample(pInstance, &pSample); - if (!pSample) { - if (pTimestamp) *pTimestamp = pInstance->llCurrentPosition; - return S_OK; - } - - if (pInstance->pCachedSample) { - pInstance->pCachedSample->Release(); - pInstance->pCachedSample = nullptr; - } - pSample->AddRef(); - pInstance->pCachedSample = pSample; - pInstance->bHasInitialFrame = TRUE; - } else { - if (pInstance->pCachedSample) { - pSample = pInstance->pCachedSample; - pSample->AddRef(); - } else { - if (pTimestamp) *pTimestamp = pInstance->llCurrentPosition; - return S_OK; - } - } - } else { - 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(); - if (pTimestamp) *pTimestamp = pInstance->llCurrentPosition; - return S_FALSE; - } - - if (!pSample) { - if (pTimestamp) *pTimestamp = pInstance->llCurrentPosition; - return S_OK; - } - - if (pInstance->pCachedSample) { - pInstance->pCachedSample->Release(); - pInstance->pCachedSample = nullptr; - } - pSample->AddRef(); - pInstance->pCachedSample = pSample; - pInstance->llCurrentPosition = llTimestamp; + if (hr == S_FALSE) { + if (pTimestamp) *pTimestamp = pInstance->llCurrentPosition; + return S_FALSE; } - - // Frame timing synchronization - if (pInstance->bUseClockSync && pInstance->llPlaybackStartTime != 0 && llTimestamp > 0) { - LONGLONG currentTimeMs = GetCurrentTimeMs(); - LONGLONG elapsedMs = currentTimeMs - pInstance->llPlaybackStartTime - pInstance->llTotalPauseTime; - double adjustedElapsedMs = elapsedMs * pInstance->playbackSpeed; - double frameTimeMs_ts = llTimestamp / 10000.0; - - UINT frameRateNum = 60, frameRateDenom = 1; - GetVideoFrameRate(pInstance, &frameRateNum, &frameRateDenom); - double frameIntervalMs = 1000.0 * frameRateDenom / frameRateNum; - - double diffMs = frameTimeMs_ts - adjustedElapsedMs; - - if (diffMs < -frameIntervalMs * 3) { - pSample->Release(); - if (pTimestamp) *pTimestamp = pInstance->llCurrentPosition; - return S_OK; - } - else if (diffMs > 1.0) { - double waitTime = std::min(diffMs, frameIntervalMs * 2); - PreciseSleepHighRes(waitTime); - } + if (FAILED(hr)) + return hr; + if (!pSample) { + if (pTimestamp) *pTimestamp = pInstance->llCurrentPosition; + return S_OK; } - if (pTimestamp) { + if (pTimestamp) *pTimestamp = pInstance->llCurrentPosition; - } const UINT32 width = pInstance->videoWidth; const UINT32 height = pInstance->videoHeight; @@ -666,17 +652,14 @@ NATIVEVIDEOPLAYER_API HRESULT ReadVideoFrameInto( hr = pBuffer->QueryInterface(IID_PPV_ARGS(&p2DBuffer2)); if (SUCCEEDED(hr) && p2DBuffer2) { - // Use Lock2DSize for optimal access - avoids internal copies hr = p2DBuffer2->Lock2DSize(MF2DBuffer_LockFlags_Read, &pScanline0, &srcPitch, &pBufferStart, &cbBufferLength); if (SUCCEEDED(hr)) { usedDirect2D = true; const DWORD srcRowBytes = width * 4; - // Zero-copy path: if strides match exactly, use memcpy for the entire buffer if (static_cast(dstRowBytes) == srcPitch && static_cast(srcRowBytes) == srcPitch) { memcpy(pDst, pScanline0, srcRowBytes * height); } else { - // Strides differ - must copy row by row but still more efficient than MFCopyImage BYTE* pSrc = pScanline0; BYTE* pDstRow = pDst; const DWORD copyBytes = std::min(srcRowBytes, dstRowBytes); @@ -691,7 +674,7 @@ NATIVEVIDEOPLAYER_API HRESULT ReadVideoFrameInto( p2DBuffer2->Release(); } - // Fallback to IMF2DBuffer if IMF2DBuffer2 failed + // Fallback to IMF2DBuffer if (!usedDirect2D) { hr = pBuffer->QueryInterface(IID_PPV_ARGS(&p2DBuffer)); if (SUCCEEDED(hr) && p2DBuffer) { @@ -727,7 +710,6 @@ NATIVEVIDEOPLAYER_API HRESULT ReadVideoFrameInto( const DWORD srcRowBytes = width * 4; const DWORD requiredSrc = srcRowBytes * height; if (cbCurr >= requiredSrc) { - // Use MFCopyImage as last resort MFCopyImage(pDst, dstRowBytes, pBytes, srcRowBytes, srcRowBytes, height); } pBuffer->Unlock(); @@ -780,14 +762,12 @@ NATIVEVIDEOPLAYER_API HRESULT SeekMedia(VideoPlayerInstance* pInstance, LONGLONG if (pInstance->pLockedBuffer) UnlockVideoFrame(pInstance); - + // Release cached sample when seeking if (pInstance->pCachedSample) { pInstance->pCachedSample->Release(); pInstance->pCachedSample = nullptr; } - - // Reset initial frame flag to ensure we read a new frame at the new position pInstance->bHasInitialFrame = FALSE; PROPVARIANT var; @@ -799,7 +779,7 @@ NATIVEVIDEOPLAYER_API HRESULT SeekMedia(VideoPlayerInstance* pInstance, LONGLONG if (pInstance->bHasAudio && pInstance->pAudioClient) { wasPlaying = (pInstance->llPauseStart == 0); pInstance->pAudioClient->Stop(); - Sleep(5); + Sleep(kSeekAudioSettleMs); } // Stop the presentation clock @@ -831,7 +811,6 @@ NATIVEVIDEOPLAYER_API HRESULT SeekMedia(VideoPlayerInstance* pInstance, LONGLONG PropVariantClear(&varAudio); } - // Reset audio client if needed if (pInstance->bHasAudio && pInstance->pRenderClient && pInstance->pAudioClient) { UINT32 bufferFrameCount = 0; @@ -850,18 +829,14 @@ NATIVEVIDEOPLAYER_API HRESULT SeekMedia(VideoPlayerInstance* pInstance, LONGLONG pInstance->bEOF = FALSE; - // IMPORTANT: Reset timing for A/V sync after seek - // We adjust llPlaybackStartTime so that the elapsed time calculation matches the seek position - // Formula: elapsedMs should equal seekPositionMs after seek - // elapsedMs = currentTimeMs - llPlaybackStartTime - llTotalPauseTime - // So: llPlaybackStartTime = currentTimeMs - seekPositionMs / playbackSpeed + // Reset timing for A/V sync after seek: + // Adjust llPlaybackStartTime so that elapsed time matches the seek position. if (pInstance->bUseClockSync) { double seekPositionMs = llPositionIn100Ns / 10000.0; - double adjustedSeekMs = seekPositionMs / static_cast(pInstance->playbackSpeed); + double adjustedSeekMs = seekPositionMs / static_cast(pInstance->playbackSpeed.load()); pInstance->llPlaybackStartTime = GetCurrentTimeMs() - static_cast(adjustedSeekMs); pInstance->llTotalPauseTime = 0; - // If paused, set pause start to now so pause time accounting works correctly if (!wasPlaying) { pInstance->llPauseStart = GetCurrentTimeMs(); } else { @@ -879,7 +854,7 @@ NATIVEVIDEOPLAYER_API HRESULT SeekMedia(VideoPlayerInstance* pInstance, LONGLONG // Restart audio if it was playing if (pInstance->bHasAudio && pInstance->pAudioClient && wasPlaying) { - Sleep(5); + Sleep(kSeekAudioSettleMs); pInstance->pAudioClient->Start(); } @@ -929,20 +904,16 @@ NATIVEVIDEOPLAYER_API HRESULT SetPlaybackState(VideoPlayerInstance* pInstance, B pInstance->llPauseStart = 0; pInstance->llPlaybackStartTime = 0; - // Stop presentation clock if (pInstance->bUseClockSync && pInstance->pPresentationClock) { pInstance->pPresentationClock->Stop(); } - // Stop audio thread if running if (pInstance->bAudioThreadRunning) { StopAudioThread(pInstance); } - // Reset initial frame flag when stopping pInstance->bHasInitialFrame = FALSE; - // Release cached sample when stopping if (pInstance->pCachedSample) { pInstance->pCachedSample->Release(); pInstance->pCachedSample = nullptr; @@ -951,15 +922,12 @@ NATIVEVIDEOPLAYER_API HRESULT SetPlaybackState(VideoPlayerInstance* pInstance, B } else if (bPlaying) { // Start or resume playback if (pInstance->llPlaybackStartTime == 0) { - // First start pInstance->llPlaybackStartTime = GetCurrentTimeMs(); } else if (pInstance->llPauseStart != 0) { - // Resume from pause pInstance->llTotalPauseTime += (GetCurrentTimeMs() - pInstance->llPauseStart); pInstance->llPauseStart = 0; } - // Reset initial frame flag when switching to playing state pInstance->bHasInitialFrame = FALSE; // Start audio client if available @@ -970,28 +938,25 @@ NATIVEVIDEOPLAYER_API HRESULT SetPlaybackState(VideoPlayerInstance* pInstance, B } } - // IMPORTANT: Démarrer le thread audio s'il n'est pas déjà en cours d'exécution - // Ceci est crucial pour le cas où on démarre en pause puis on fait play() + // 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); - // Continue anyway - video can still play without audio } } } - // Start or resume presentation clock + // Start or resume presentation clock from the current stored position if (pInstance->bUseClockSync && pInstance->pPresentationClock) { - // IMPORTANT: Démarrer depuis la position actuelle stockée hr = pInstance->pPresentationClock->Start(pInstance->llCurrentPosition); if (FAILED(hr)) { PrintHR("Failed to start presentation clock", hr); } } - // Signal audio thread to continue if it was waiting if (pInstance->hAudioReadyEvent) { SetEvent(pInstance->hAudioReadyEvent); } @@ -1001,24 +966,19 @@ NATIVEVIDEOPLAYER_API HRESULT SetPlaybackState(VideoPlayerInstance* pInstance, B pInstance->llPauseStart = GetCurrentTimeMs(); } - // Reset initial frame flag when switching to paused state pInstance->bHasInitialFrame = FALSE; - // Pause audio client if available if (pInstance->pAudioClient && pInstance->bAudioInitialized) { pInstance->pAudioClient->Stop(); } - // Pause presentation clock if (pInstance->bUseClockSync && pInstance->pPresentationClock) { hr = pInstance->pPresentationClock->Pause(); if (FAILED(hr)) { PrintHR("Failed to pause presentation clock", hr); } } - - // Note: On ne stoppe PAS le thread audio en pause, on le laisse tourner - // Il va simplement attendre sur les événements de synchronisation + // Note: the audio thread is not stopped on pause — it simply waits on sync events } return hr; } @@ -1031,61 +991,47 @@ NATIVEVIDEOPLAYER_API void CloseMedia(VideoPlayerInstance* pInstance) { if (!pInstance) return; - // Stop audio thread StopAudioThread(pInstance); - // Release video buffer if (pInstance->pLockedBuffer) { UnlockVideoFrame(pInstance); } - - // Release cached sample + if (pInstance->pCachedSample) { pInstance->pCachedSample->Release(); pInstance->pCachedSample = nullptr; } - - // Reset initial frame flag pInstance->bHasInitialFrame = FALSE; - // Macro for safely releasing COM interfaces #define SAFE_RELEASE(obj) if (obj) { obj->Release(); obj = nullptr; } - // Stop and release audio resources if (pInstance->pAudioClient) { pInstance->pAudioClient->Stop(); SAFE_RELEASE(pInstance->pAudioClient); } - // Stop and release presentation clock if (pInstance->pPresentationClock) { pInstance->pPresentationClock->Stop(); SAFE_RELEASE(pInstance->pPresentationClock); } - // Release media source SAFE_RELEASE(pInstance->pMediaSource); - - // Release other COM resources SAFE_RELEASE(pInstance->pRenderClient); SAFE_RELEASE(pInstance->pDevice); SAFE_RELEASE(pInstance->pAudioEndpointVolume); SAFE_RELEASE(pInstance->pSourceReader); SAFE_RELEASE(pInstance->pSourceReaderAudio); - // Release audio format if (pInstance->pSourceAudioFormat) { CoTaskMemFree(pInstance->pSourceAudioFormat); pInstance->pSourceAudioFormat = nullptr; } - // Close event handles #define SAFE_CLOSE_HANDLE(handle) if (handle) { CloseHandle(handle); handle = nullptr; } SAFE_CLOSE_HANDLE(pInstance->hAudioSamplesReadyEvent); SAFE_CLOSE_HANDLE(pInstance->hAudioReadyEvent); - // Reset state variables pInstance->bEOF = FALSE; pInstance->videoWidth = pInstance->videoHeight = 0; pInstance->bHasAudio = FALSE; @@ -1117,19 +1063,13 @@ NATIVEVIDEOPLAYER_API HRESULT SetPlaybackSpeed(VideoPlayerInstance* pInstance, f if (!pInstance) return OP_E_NOT_INITIALIZED; - // Limit speed between 0.5 and 2.0 speed = std::max(0.5f, std::min(speed, 2.0f)); - - // Store speed in instance pInstance->playbackSpeed = speed; - // Update the presentation clock rate if (pInstance->bUseClockSync && pInstance->pPresentationClock) { - // Get the rate control interface from the presentation clock IMFRateControl* pRateControl = nullptr; HRESULT hr = pInstance->pPresentationClock->QueryInterface(IID_PPV_ARGS(&pRateControl)); if (SUCCEEDED(hr)) { - // Set the playback rate hr = pRateControl->SetRate(FALSE, speed); if (FAILED(hr)) { PrintHR("Failed to set presentation clock rate", hr); @@ -1145,88 +1085,83 @@ NATIVEVIDEOPLAYER_API HRESULT GetPlaybackSpeed(const VideoPlayerInstance* pInsta if (!pInstance || !pSpeed) return OP_E_INVALID_PARAMETER; - // Return instance-specific playback speed *pSpeed = pInstance->playbackSpeed; - return S_OK; } +// --------------------------------------------------------------------------- +// GetVideoMetadata — retrieves all available metadata (issue #5: improved) +// --------------------------------------------------------------------------- NATIVEVIDEOPLAYER_API HRESULT GetVideoMetadata(const VideoPlayerInstance* pInstance, VideoMetadata* pMetadata) { if (!pInstance || !pMetadata) return OP_E_INVALID_PARAMETER; if (!pInstance->pSourceReader) return OP_E_NOT_INITIALIZED; - // Initialize metadata structure with default values ZeroMemory(pMetadata, sizeof(VideoMetadata)); HRESULT hr = S_OK; - - // Get media source for property access IMFMediaSource* pMediaSource = nullptr; IMFPresentationDescriptor* pPresentationDescriptor = nullptr; - // Get media source from source reader hr = pInstance->pSourceReader->GetServiceForStream( - MF_SOURCE_READER_MEDIASOURCE, - GUID_NULL, + MF_SOURCE_READER_MEDIASOURCE, + GUID_NULL, IID_PPV_ARGS(&pMediaSource)); if (SUCCEEDED(hr) && pMediaSource) { - // Get presentation descriptor hr = pMediaSource->CreatePresentationDescriptor(&pPresentationDescriptor); if (SUCCEEDED(hr) && pPresentationDescriptor) { - // Get duration + // Duration UINT64 duration = 0; if (SUCCEEDED(pPresentationDescriptor->GetUINT64(MF_PD_DURATION, &duration))) { pMetadata->duration = static_cast(duration); pMetadata->hasDuration = TRUE; } - // Get stream descriptors to access more metadata - DWORD streamCount = 0; - hr = pPresentationDescriptor->GetStreamDescriptorCount(&streamCount); - - if (SUCCEEDED(hr)) { - // Try to get title and other metadata from attributes - IMFAttributes* pAttributes = nullptr; - if (SUCCEEDED(pPresentationDescriptor->QueryInterface(IID_PPV_ARGS(&pAttributes)))) { - // We can't directly access some metadata attributes due to missing definitions - // Set a default title based on the file path if available - if (pInstance->pSourceReader) { - // For now, we'll leave title empty as we can't reliably extract it - // without the proper attribute definitions - pMetadata->hasTitle = FALSE; + // ---- 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) { + PROPVARIANT valTitle; + PropVariantInit(&valTitle); + if (SUCCEEDED(pMeta->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(); + } - // Try to estimate bitrate from stream properties - UINT64 duration = 0; - if (SUCCEEDED(pPresentationDescriptor->GetUINT64(MF_PD_DURATION, &duration)) && duration > 0) { - // We'll try to estimate bitrate later from individual streams - pMetadata->hasBitrate = FALSE; - } + // Process each stream for video/audio metadata + DWORD streamCount = 0; + hr = pPresentationDescriptor->GetStreamDescriptorCount(&streamCount); - pAttributes->Release(); - } + LONGLONG totalBitrate = 0; + bool hasBitrateInfo = false; - // Process each stream to get more metadata + if (SUCCEEDED(hr)) { for (DWORD i = 0; i < streamCount; i++) { BOOL selected = FALSE; IMFStreamDescriptor* pStreamDescriptor = nullptr; if (SUCCEEDED(pPresentationDescriptor->GetStreamDescriptorByIndex(i, &selected, &pStreamDescriptor))) { - // Get media type handler IMFMediaTypeHandler* pHandler = nullptr; if (SUCCEEDED(pStreamDescriptor->GetMediaTypeHandler(&pHandler))) { - // Get major type to determine if video or audio GUID majorType; if (SUCCEEDED(pHandler->GetMajorType(&majorType))) { if (majorType == MFMediaType_Video) { - // Get current media type IMFMediaType* pMediaType = nullptr; if (SUCCEEDED(pHandler->GetCurrentMediaType(&pMediaType))) { - // Get video dimensions + // Dimensions UINT32 width = 0, height = 0; if (SUCCEEDED(MFGetAttributeSize(pMediaType, MF_MT_FRAME_SIZE, &width, &height))) { pMetadata->width = width; @@ -1235,7 +1170,7 @@ NATIVEVIDEOPLAYER_API HRESULT GetVideoMetadata(const VideoPlayerInstance* pInsta pMetadata->hasHeight = TRUE; } - // Get frame rate + // Frame rate UINT32 numerator = 0, denominator = 1; if (SUCCEEDED(MFGetAttributeRatio(pMediaType, MF_MT_FRAME_RATE, &numerator, &denominator))) { if (denominator > 0) { @@ -1244,53 +1179,69 @@ NATIVEVIDEOPLAYER_API HRESULT GetVideoMetadata(const VideoPlayerInstance* pInsta } } - // Get subtype (format) for mime type + // 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))) { - // Convert subtype to mime type string if (subtype == MFVideoFormat_H264) { wcscpy_s(pMetadata->mimeType, L"video/h264"); - pMetadata->hasMimeType = TRUE; - } - else if (subtype == MFVideoFormat_HEVC) { + } else if (subtype == MFVideoFormat_HEVC) { wcscpy_s(pMetadata->mimeType, L"video/hevc"); - pMetadata->hasMimeType = TRUE; - } - else if (subtype == MFVideoFormat_MPEG2) { + } else if (subtype == MFVideoFormat_MPEG2) { wcscpy_s(pMetadata->mimeType, L"video/mpeg2"); - pMetadata->hasMimeType = TRUE; - } - else if (subtype == MFVideoFormat_WMV3) { - wcscpy_s(pMetadata->mimeType, L"video/wmv"); - pMetadata->hasMimeType = TRUE; - } - else { + } 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; } + pMetadata->hasMimeType = TRUE; } pMediaType->Release(); } } else if (majorType == MFMediaType_Audio) { - // Get current media type IMFMediaType* pMediaType = nullptr; if (SUCCEEDED(pHandler->GetCurrentMediaType(&pMediaType))) { - // Get audio channels UINT32 channels = 0; if (SUCCEEDED(pMediaType->GetUINT32(MF_MT_AUDIO_NUM_CHANNELS, &channels))) { pMetadata->audioChannels = channels; pMetadata->hasAudioChannels = TRUE; } - // Get audio sample rate 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(); } } @@ -1301,12 +1252,19 @@ NATIVEVIDEOPLAYER_API HRESULT GetVideoMetadata(const VideoPlayerInstance* pInsta } } } + + // Report combined bitrate if we gathered any info + if (hasBitrateInfo) { + pMetadata->bitrate = totalBitrate; + pMetadata->hasBitrate = TRUE; + } + pPresentationDescriptor->Release(); } pMediaSource->Release(); } - // If we couldn't get some metadata from the media source, try to get it from the instance + // Fallback: fill in from instance state if the media source did not provide values if (!pMetadata->hasWidth || !pMetadata->hasHeight) { if (pInstance->videoWidth > 0 && pInstance->videoHeight > 0) { pMetadata->width = pInstance->videoWidth; @@ -1316,7 +1274,6 @@ NATIVEVIDEOPLAYER_API HRESULT GetVideoMetadata(const VideoPlayerInstance* pInsta } } - // If we couldn't get frame rate from media source, try to get it directly if (!pMetadata->hasFrameRate) { UINT numerator = 0, denominator = 1; if (SUCCEEDED(GetVideoFrameRate(pInstance, &numerator, &denominator)) && denominator > 0) { @@ -1325,25 +1282,102 @@ NATIVEVIDEOPLAYER_API HRESULT GetVideoMetadata(const VideoPlayerInstance* pInsta } } - // If we couldn't get duration from media source, try to get it directly if (!pMetadata->hasDuration) { - LONGLONG duration = 0; - if (SUCCEEDED(GetMediaDuration(pInstance, &duration))) { - pMetadata->duration = duration; + LONGLONG dur = 0; + if (SUCCEEDED(GetMediaDuration(pInstance, &dur))) { + pMetadata->duration = dur; pMetadata->hasDuration = TRUE; } } - // If we couldn't get audio channels, check if audio is available - if (!pMetadata->hasAudioChannels && pInstance->bHasAudio) { - if (pInstance->pSourceAudioFormat) { - pMetadata->audioChannels = pInstance->pSourceAudioFormat->nChannels; - pMetadata->hasAudioChannels = 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 || !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; + } - pMetadata->audioSampleRate = pInstance->pSourceAudioFormat->nSamplesPerSec; - pMetadata->hasAudioSampleRate = TRUE; + // 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); } } + // 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; + + // Reconfigure the output media type with the new frame size + IMFMediaType* pType = nullptr; + HRESULT hr = MFCreateMediaType(&pType); + 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; + + // 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, + &pInstance->videoWidth, &pInstance->videoHeight); + SafeRelease(pActual); + } + + // Invalidate cached sample since dimensions changed + if (pInstance->pCachedSample) { + pInstance->pCachedSample->Release(); + pInstance->pCachedSample = nullptr; + } + 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 fc060fc..9d2e5f9 100644 --- a/mediaplayer/src/jvmMain/native/windows/NativeVideoPlayer.h +++ b/mediaplayer/src/jvmMain/native/windows/NativeVideoPlayer.h @@ -10,6 +10,10 @@ #include #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 typedef struct VideoMetadata { wchar_t title[256]; // Title of the video (empty if not available) @@ -32,7 +36,7 @@ typedef struct VideoMetadata { BOOL hasAudioSampleRate; // TRUE if audio sample rate is available } VideoMetadata; -// Macro d'exportation pour la DLL Windows +// DLL export macro #ifdef _WIN32 #ifdef NATIVEVIDEOPLAYER_EXPORTS #define NATIVEVIDEOPLAYER_API __declspec(dllexport) @@ -43,12 +47,12 @@ typedef struct VideoMetadata { #define NATIVEVIDEOPLAYER_API #endif -// Codes d'erreur personnalisés +// Custom error codes #define OP_E_NOT_INITIALIZED ((HRESULT)0x80000001L) #define OP_E_ALREADY_INITIALIZED ((HRESULT)0x80000002L) #define OP_E_INVALID_PARAMETER ((HRESULT)0x80000003L) -// Structure pour encapsuler l'état d'une instance de lecteur vidéo +// Forward declaration for the video player instance state struct VideoPlayerInstance; #ifdef __cplusplus @@ -56,56 +60,66 @@ extern "C" { #endif // ==================================================================== -// Fonctions exportées pour la gestion des instances et la lecture multimédia +// Exported functions for instance management and media playback // ==================================================================== /** - * @brief Initialise Media Foundation, Direct3D11 et le gestionnaire DXGI (une seule fois pour toutes les instances). - * @return S_OK en cas de succès, ou un code d'erreur. + * @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 HRESULT InitMediaFoundation(); /** - * @brief Crée une nouvelle instance de lecteur vidéo. - * @param ppInstance Pointeur pour recevoir le handle de l'instance créée. - * @return S_OK en cas de succès, ou un code d'erreur. + * @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 Détruit une instance de lecteur vidéo et libère ses ressources. - * @param pInstance Handle de l'instance à détruire. + * @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 Ouvre un média (fichier ou URL) et prépare le décodage avec accélération matérielle pour une instance spécifique. - * @param pInstance Handle de l'instance. - * @param url Chemin ou URL du média (chaîne large). - * @param startPlayback TRUE pour démarrer la lecture immédiatement, FALSE pour rester en pause. - * @return S_OK en cas de succès, ou un code d'erreur. + * @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 HRESULT OpenMedia(VideoPlayerInstance* pInstance, const wchar_t* url, BOOL startPlayback = TRUE); /** - * @brief Lit la prochaine frame vidéo en format RGB32 pour une instance spécifique. - * @param pInstance Handle de l'instance. - * @param pData Reçoit un pointeur sur les données de la frame (à ne pas libérer). - * @param pDataSize Reçoit la taille en octets du tampon. - * @return S_OK si une frame est lue, S_FALSE en fin de flux, ou un code d'erreur. + * @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 Déverrouille le tampon de la frame vidéo précédemment verrouillé pour une instance spécifique. - * @param pInstance Handle de l'instance. - * @return S_OK en cas de succès. + * @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); -/* - * Reads the next video frame and copies it into a destination buffer. - * pTimestamp receives the 100ns timestamp when available. +/** + * @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, @@ -115,112 +129,112 @@ NATIVEVIDEOPLAYER_API HRESULT ReadVideoFrameInto( LONGLONG* pTimestamp); /** - * @brief Ferme le média et libère les ressources associées pour une instance spécifique. - * @param pInstance Handle de l'instance. + * @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 Indique si la fin du flux média a été atteinte pour une instance spécifique. - * @param pInstance Handle de l'instance. - * @return TRUE si fin de flux, FALSE sinon. + * @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 Récupère les dimensions de la vidéo pour une instance spécifique. - * @param pInstance Handle de l'instance. - * @param pWidth Pointeur pour recevoir la largeur en pixels. - * @param pHeight Pointeur pour recevoir la hauteur en pixels. + * @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 Récupère le taux de rafraîchissement (frame rate) de la vidéo pour une instance spécifique. - * @param pInstance Handle de l'instance. - * @param pNum Pointeur pour recevoir le numérateur. - * @param pDenom Pointeur pour recevoir le dénominateur. - * @return S_OK en cas de succès, ou un code d'erreur. + * @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 GetVideoFrameRate(const VideoPlayerInstance* pInstance, UINT* pNum, UINT* pDenom); /** - * @brief Recherche une position spécifique dans le média pour une instance spécifique. - * @param pInstance Handle de l'instance. - * @param llPosition Position (en 100-ns) à atteindre. - * @return S_OK en cas de succès, ou un code d'erreur. + * @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 Obtient la durée totale du média pour une instance spécifique. - * @param pInstance Handle de l'instance. - * @param pDuration Pointeur pour recevoir la durée (en 100-ns). - * @return S_OK en cas de succès, ou un code d'erreur. + * @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 Obtient la position de lecture courante pour une instance spécifique. - * @param pInstance Handle de l'instance. - * @param pPosition Pointeur pour recevoir la position (en 100-ns). - * @return S_OK en cas de succès, ou un code d'erreur. + * @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 Définit l'état de lecture (lecture ou pause) pour une instance spécifique. - * @param pInstance Handle de l'instance. - * @param bPlaying TRUE pour lecture, FALSE pour pause. - * @param bStop TRUE si c'est un arrêt complet, FALSE si c'est simplement une pause. - * @return S_OK en cas de succès, ou un code d'erreur. + * @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 Arrête Media Foundation et libère les ressources globales (après destruction de toutes les instances). - * @return S_OK en cas de succès, ou un code d'erreur. + * @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 Définit le niveau de volume audio pour une instance spécifique. - * @param pInstance Handle de l'instance. - * @param volume Niveau de volume (0.0 à 1.0). - * @return S_OK en cas de succès, ou un code d'erreur. + * @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 Récupère le niveau de volume audio actuel pour une instance spécifique. - * @param pInstance Handle de l'instance. - * @param volume Pointeur pour recevoir le niveau de volume (0.0 à 1.0). - * @return S_OK en cas de succès, ou un code d'erreur. + * @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 Récupère les niveaux audio pour les canaux gauche et droit pour une instance spécifique. - * @param pInstance Handle de l'instance. - * @param pLeftLevel Pointeur pour le niveau du canal gauche. - * @param pRightLevel Pointeur pour le niveau du canal droit. - * @return S_OK en cas de succès, ou un code d'erreur. + * @brief Gets the audio levels for left and right channels for a specific instance. + * @param pInstance Handle to the instance. + * @param pLeftLevel Pointer for the left channel level. + * @param pRightLevel Pointer for the right channel level. + * @return S_OK on success, or an error code. */ NATIVEVIDEOPLAYER_API HRESULT GetAudioLevels(const VideoPlayerInstance* pInstance, float* pLeftLevel, float* pRightLevel); /** - * @brief Définit la vitesse de lecture pour une instance spécifique. - * @param pInstance Handle de l'instance. - * @param speed Vitesse de lecture (0.5 à 2.0, où 1.0 est la vitesse normale). - * @return S_OK en cas de succès, ou un code d'erreur. + * @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 Récupère la vitesse de lecture actuelle pour une instance spécifique. - * @param pInstance Handle de l'instance. - * @param pSpeed Pointeur pour recevoir la vitesse de lecture. - * @return S_OK en cas de succès, ou un code d'erreur. + * @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); @@ -232,6 +246,21 @@ NATIVEVIDEOPLAYER_API HRESULT GetPlaybackSpeed(const VideoPlayerInstance* pInsta */ 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 } #endif diff --git a/mediaplayer/src/jvmMain/native/windows/VideoPlayerInstance.h b/mediaplayer/src/jvmMain/native/windows/VideoPlayerInstance.h index 02581ef..e4f8d38 100644 --- a/mediaplayer/src/jvmMain/native/windows/VideoPlayerInstance.h +++ b/mediaplayer/src/jvmMain/native/windows/VideoPlayerInstance.h @@ -7,6 +7,7 @@ #include #include #include +#include /** * @brief Structure to encapsulate the state of a video player instance. @@ -20,6 +21,8 @@ struct VideoPlayerInstance { 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 @@ -53,7 +56,7 @@ struct VideoPlayerInstance { CRITICAL_SECTION csClockSync{}; BOOL bSeekInProgress = FALSE; - // Playback control - float instanceVolume = 1.0f; // Volume specific to this instance (1.0 = 100%) - float playbackSpeed = 1.0f; // Playback speed (1.0 = 100%) + // 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%) }; diff --git a/mediaplayer/src/jvmMain/native/windows/jni_bridge.cpp b/mediaplayer/src/jvmMain/native/windows/jni_bridge.cpp new file mode 100644 index 0000000..1ddcbdf --- /dev/null +++ b/mediaplayer/src/jvmMain/native/windows/jni_bridge.cpp @@ -0,0 +1,253 @@ +// jni_bridge.cpp — JNI bridge for NativeVideoPlayer +// Maps Kotlin external functions to the existing C API. + +#include +#include "NativeVideoPlayer.h" +#include + +// --------------------------------------------------------------------------- +// Utility +// --------------------------------------------------------------------------- +static inline VideoPlayerInstance* toInstance(jlong handle) { + return reinterpret_cast(handle); +} + +// --------------------------------------------------------------------------- +// JNI implementations +// --------------------------------------------------------------------------- + +static jint JNICALL jni_GetNativeVersion(JNIEnv*, jclass) { + return GetNativeVersion(); +} + +static jint JNICALL jni_InitMediaFoundation(JNIEnv*, jclass) { + return InitMediaFoundation(); +} + +static jlong JNICALL jni_CreateInstance(JNIEnv*, jclass) { + VideoPlayerInstance* p = nullptr; + HRESULT hr = CreateVideoPlayerInstance(&p); + if (FAILED(hr) || !p) return 0; + return reinterpret_cast(p); +} + +static void JNICALL jni_DestroyInstance(JNIEnv*, jclass, jlong handle) { + if (handle) DestroyVideoPlayerInstance(toInstance(handle)); +} + +static jint JNICALL jni_OpenMedia(JNIEnv* env, jclass, jlong handle, jstring url, jboolean startPlayback) { + if (!handle || !url) return OP_E_INVALID_PARAMETER; + const jchar* chars = env->GetStringChars(url, nullptr); + if (!chars) return E_OUTOFMEMORY; + HRESULT hr = OpenMedia(toInstance(handle), + reinterpret_cast(chars), + startPlayback ? TRUE : FALSE); + env->ReleaseStringChars(url, chars); + return hr; +} + +// Returns a direct ByteBuffer wrapping the locked frame, or null. +// outResult[0] receives the HRESULT. +static jobject JNICALL jni_ReadVideoFrame(JNIEnv* env, jclass, jlong handle, jintArray outResult) { + if (!handle) { + if (outResult) { jint v = OP_E_NOT_INITIALIZED; env->SetIntArrayRegion(outResult, 0, 1, &v); } + return nullptr; + } + BYTE* pData = nullptr; + DWORD dataSize = 0; + HRESULT hr = ReadVideoFrame(toInstance(handle), &pData, &dataSize); + if (outResult) { jint v = static_cast(hr); env->SetIntArrayRegion(outResult, 0, 1, &v); } + if (FAILED(hr) || !pData || dataSize == 0) return nullptr; + return env->NewDirectByteBuffer(pData, dataSize); +} + +static jint JNICALL jni_UnlockVideoFrame(JNIEnv*, jclass, jlong handle) { + return handle ? UnlockVideoFrame(toInstance(handle)) : E_INVALIDARG; +} + +static void JNICALL jni_CloseMedia(JNIEnv*, jclass, jlong handle) { + if (handle) CloseMedia(toInstance(handle)); +} + +static jboolean JNICALL jni_IsEOF(JNIEnv*, jclass, jlong handle) { + return (handle && IsEOF(toInstance(handle))) ? JNI_TRUE : JNI_FALSE; +} + +static void JNICALL jni_GetVideoSize(JNIEnv* env, jclass, jlong handle, jintArray outSize) { + UINT32 w = 0, h = 0; + if (handle) GetVideoSize(toInstance(handle), &w, &h); + jint vals[2] = { static_cast(w), static_cast(h) }; + env->SetIntArrayRegion(outSize, 0, 2, vals); +} + +static jint JNICALL jni_GetVideoFrameRate(JNIEnv* env, jclass, jlong handle, jintArray outRate) { + if (!handle) return E_INVALIDARG; + UINT num = 0, denom = 0; + HRESULT hr = GetVideoFrameRate(toInstance(handle), &num, &denom); + jint vals[2] = { static_cast(num), static_cast(denom) }; + env->SetIntArrayRegion(outRate, 0, 2, vals); + return hr; +} + +static jint JNICALL jni_SeekMedia(JNIEnv*, jclass, jlong handle, jlong pos) { + return handle ? SeekMedia(toInstance(handle), pos) : E_INVALIDARG; +} + +static jint JNICALL jni_GetMediaDuration(JNIEnv* env, jclass, jlong handle, jlongArray out) { + if (!handle) return E_INVALIDARG; + LONGLONG v = 0; + HRESULT hr = GetMediaDuration(toInstance(handle), &v); + jlong jv = static_cast(v); + env->SetLongArrayRegion(out, 0, 1, &jv); + return hr; +} + +static jint JNICALL jni_GetMediaPosition(JNIEnv* env, jclass, jlong handle, jlongArray out) { + if (!handle) return E_INVALIDARG; + LONGLONG v = 0; + HRESULT hr = GetMediaPosition(toInstance(handle), &v); + jlong jv = static_cast(v); + env->SetLongArrayRegion(out, 0, 1, &jv); + return hr; +} + +static jint JNICALL jni_SetPlaybackState(JNIEnv*, jclass, jlong handle, jboolean playing, jboolean stop) { + return handle ? SetPlaybackState(toInstance(handle), playing ? TRUE : FALSE, stop ? TRUE : FALSE) : E_INVALIDARG; +} + +static jint JNICALL jni_ShutdownMediaFoundation(JNIEnv*, jclass) { + return ShutdownMediaFoundation(); +} + +static jint JNICALL jni_SetAudioVolume(JNIEnv*, jclass, jlong handle, jfloat vol) { + return handle ? SetAudioVolume(toInstance(handle), vol) : E_INVALIDARG; +} + +static jint JNICALL jni_GetAudioVolume(JNIEnv* env, jclass, jlong handle, jfloatArray out) { + if (!handle) return E_INVALIDARG; + float v = 0; + HRESULT hr = GetAudioVolume(toInstance(handle), &v); + jfloat jv = v; + env->SetFloatArrayRegion(out, 0, 1, &jv); + return hr; +} + +static jint JNICALL jni_GetAudioLevels(JNIEnv* env, jclass, jlong handle, jfloatArray out) { + if (!handle) return E_INVALIDARG; + float l = 0, r = 0; + HRESULT hr = GetAudioLevels(toInstance(handle), &l, &r); + jfloat vals[2] = { l, r }; + env->SetFloatArrayRegion(out, 0, 2, vals); + return hr; +} + +static jint JNICALL jni_SetPlaybackSpeed(JNIEnv*, jclass, jlong handle, jfloat speed) { + return handle ? SetPlaybackSpeed(toInstance(handle), speed) : E_INVALIDARG; +} + +static jint JNICALL jni_GetPlaybackSpeed(JNIEnv* env, jclass, jlong handle, jfloatArray out) { + if (!handle) return E_INVALIDARG; + float v = 0; + HRESULT hr = GetPlaybackSpeed(toInstance(handle), &v); + jfloat jv = v; + env->SetFloatArrayRegion(out, 0, 1, &jv); + return hr; +} + +// Metadata — fills parallel arrays so the Kotlin side can construct VideoMetadata. +static jint JNICALL jni_GetVideoMetadata(JNIEnv* env, jclass, jlong handle, + jcharArray outTitle, jcharArray outMimeType, + jlongArray outLongVals, jintArray outIntVals, + jfloatArray outFloatVals, jbooleanArray outHasFlags) { + if (!handle) return E_INVALIDARG; + + VideoMetadata m; + HRESULT hr = GetVideoMetadata(toInstance(handle), &m); + if (FAILED(hr)) return hr; + + if (outTitle) + env->SetCharArrayRegion(outTitle, 0, 256, reinterpret_cast(m.title)); + if (outMimeType) + env->SetCharArrayRegion(outMimeType, 0, 64, reinterpret_cast(m.mimeType)); + if (outLongVals) { + jlong lv[2] = { static_cast(m.duration), static_cast(m.bitrate) }; + env->SetLongArrayRegion(outLongVals, 0, 2, lv); + } + if (outIntVals) { + jint iv[4] = { static_cast(m.width), static_cast(m.height), + static_cast(m.audioChannels), static_cast(m.audioSampleRate) }; + env->SetIntArrayRegion(outIntVals, 0, 4, iv); + } + if (outFloatVals) { + jfloat fv = m.frameRate; + env->SetFloatArrayRegion(outFloatVals, 0, 1, &fv); + } + if (outHasFlags) { + jboolean flags[9] = { + static_cast(m.hasTitle), static_cast(m.hasDuration), + static_cast(m.hasWidth), static_cast(m.hasHeight), + static_cast(m.hasBitrate), static_cast(m.hasFrameRate), + static_cast(m.hasMimeType), static_cast(m.hasAudioChannels), + static_cast(m.hasAudioSampleRate) + }; + env->SetBooleanArrayRegion(outHasFlags, 0, 9, flags); + } + return hr; +} + +// Wrap an arbitrary native address as a direct ByteBuffer (used for Skia pixel access). +static jobject JNICALL jni_WrapPointer(JNIEnv* env, jclass, jlong address, jlong size) { + if (!address || size <= 0) return nullptr; + return env->NewDirectByteBuffer(reinterpret_cast(address), static_cast(size)); +} + +static jint JNICALL jni_SetOutputSize(JNIEnv*, jclass, jlong handle, jint width, jint height) { + return handle ? SetOutputSize(toInstance(handle), + static_cast(width), + static_cast(height)) + : E_INVALIDARG; +} + +// --------------------------------------------------------------------------- +// Registration table +// --------------------------------------------------------------------------- +static const JNINativeMethod g_methods[] = { + { const_cast("nGetNativeVersion"), const_cast("()I"), (void*)jni_GetNativeVersion }, + { const_cast("nInitMediaFoundation"),const_cast("()I"), (void*)jni_InitMediaFoundation }, + { const_cast("nCreateInstance"), const_cast("()J"), (void*)jni_CreateInstance }, + { const_cast("nDestroyInstance"), const_cast("(J)V"), (void*)jni_DestroyInstance }, + { const_cast("nOpenMedia"), const_cast("(JLjava/lang/String;Z)I"), (void*)jni_OpenMedia }, + { const_cast("nReadVideoFrame"), const_cast("(J[I)Ljava/nio/ByteBuffer;"), (void*)jni_ReadVideoFrame }, + { const_cast("nUnlockVideoFrame"), const_cast("(J)I"), (void*)jni_UnlockVideoFrame }, + { const_cast("nCloseMedia"), const_cast("(J)V"), (void*)jni_CloseMedia }, + { const_cast("nIsEOF"), const_cast("(J)Z"), (void*)jni_IsEOF }, + { const_cast("nGetVideoSize"), const_cast("(J[I)V"), (void*)jni_GetVideoSize }, + { const_cast("nGetVideoFrameRate"), const_cast("(J[I)I"), (void*)jni_GetVideoFrameRate }, + { const_cast("nSeekMedia"), const_cast("(JJ)I"), (void*)jni_SeekMedia }, + { const_cast("nGetMediaDuration"), const_cast("(J[J)I"), (void*)jni_GetMediaDuration }, + { const_cast("nGetMediaPosition"), const_cast("(J[J)I"), (void*)jni_GetMediaPosition }, + { const_cast("nSetPlaybackState"), const_cast("(JZZ)I"), (void*)jni_SetPlaybackState }, + { const_cast("nShutdownMediaFoundation"), const_cast("()I"), (void*)jni_ShutdownMediaFoundation }, + { const_cast("nSetAudioVolume"), const_cast("(JF)I"), (void*)jni_SetAudioVolume }, + { const_cast("nGetAudioVolume"), const_cast("(J[F)I"), (void*)jni_GetAudioVolume }, + { const_cast("nGetAudioLevels"), const_cast("(J[F)I"), (void*)jni_GetAudioLevels }, + { const_cast("nSetPlaybackSpeed"), const_cast("(JF)I"), (void*)jni_SetPlaybackSpeed }, + { const_cast("nGetPlaybackSpeed"), const_cast("(J[F)I"), (void*)jni_GetPlaybackSpeed }, + { const_cast("nGetVideoMetadata"), const_cast("(J[C[C[J[I[F[Z)I"), (void*)jni_GetVideoMetadata }, + { const_cast("nWrapPointer"), const_cast("(JJ)Ljava/nio/ByteBuffer;"), (void*)jni_WrapPointer }, + { const_cast("nSetOutputSize"), const_cast("(JII)I"), (void*)jni_SetOutputSize }, +}; + +extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) { + JNIEnv* env = nullptr; + if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) + return -1; + + jclass cls = env->FindClass("io/github/kdroidfilter/composemediaplayer/windows/MediaFoundationLib"); + if (!cls) return -1; + + if (env->RegisterNatives(cls, g_methods, sizeof(g_methods) / sizeof(g_methods[0])) < 0) + return -1; + + return JNI_VERSION_1_6; +}