From f0a53e26a877a1270528ed45871445173d57238e Mon Sep 17 00:00:00 2001 From: "muthuraj.mr" Date: Sat, 24 Oct 2026 16:21:39 +0530 Subject: [PATCH] fix(windows): play audio-only media; re-extract native lib when its content changes - OpenMedia no longer fails with MF_E_INVALIDSTREAMNUMBER when the source has no video stream: the video stream is optional, and media without both video and audio is still rejected. - The audio thread now sees MF_SOURCE_READERF_ENDOFSTREAM (the flag was checked after the null-sample early return) and, for audio-only media, sets EOF. - Seeking after the audio thread exited (end of media) restarts it. - The producer keeps running after onPlaybackEnded, so seek + play after the end works and the next end is reported again. - Audio-only position/progress come from GetMediaPosition. - NativeLibraryLoader compared only the file size, so an updated DLL of the same size kept the stale cached copy; it now compares the content. Tested on Windows 11 (x64) with MP3, WAV and Ogg Opus files (duration, position, seek, end, replay) and an HLS video (unchanged). --- .../util/NativeLibraryLoader.kt | 14 ++++--- .../windows/WindowsVideoPlayerState.kt | 39 +++++++++++++++---- .../jvmMain/native/windows/AudioManager.cpp | 9 ++++- .../native/windows/NativeVideoPlayer.cpp | 22 ++++++++--- .../native/windows/VideoPlayerInstance.h | 1 + 5 files changed, 64 insertions(+), 21 deletions(-) diff --git a/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/util/NativeLibraryLoader.kt b/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/util/NativeLibraryLoader.kt index 272b1d3a..53d2d3f4 100644 --- a/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/util/NativeLibraryLoader.kt +++ b/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/util/NativeLibraryLoader.kt @@ -52,18 +52,20 @@ internal object NativeLibraryLoader { cacheDir.mkdirs() val cachedFile = File(cacheDir, fileName) - // Validate cache: re-extract if size differs or file is missing - val resourceSize = resourceUrl.openConnection().contentLengthLong - if (cachedFile.exists() && cachedFile.length() == resourceSize) { + // Validate cache: re-extract if the content differs or the file is missing. Comparing only the size would keep + // an outdated library after an update that happens to produce a file of the same size. + val resourceBytes = resourceUrl.openStream().use { it.readBytes() } + if (cachedFile.exists() && + cachedFile.length() == resourceBytes.size.toLong() && + cachedFile.readBytes().contentEquals(resourceBytes) + ) { return cachedFile } // Atomic extract: write to temp then move val tmpFile = File(cacheDir, "$fileName.tmp") try { - resourceUrl.openStream().use { input -> - Files.copy(input, tmpFile.toPath(), StandardCopyOption.REPLACE_EXISTING) - } + tmpFile.writeBytes(resourceBytes) Files.move(tmpFile.toPath(), cachedFile.toPath(), StandardCopyOption.REPLACE_EXISTING) cachedFile.setExecutable(true) } finally { 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 a2eb89e7..fcfbb837 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 @@ -547,16 +547,12 @@ class WindowsVideoPlayerState : VideoPlayerState { // Get the video dimensions 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 - } + // 0 x 0 = audio-only media (the native side opened it without a video stream). videoWidth = sizeArr[0] videoHeight = sizeArr[1] // Scale output to match display surface (saves memory for 4K+ video) - if (surfaceWidth > 0 && surfaceHeight > 0) { + if (videoWidth > 0 && surfaceWidth > 0 && surfaceHeight > 0) { val hrScale = player.SetOutputSize(instance, surfaceWidth, surfaceHeight) if (hrScale >= 0) { player.GetVideoSize(instance, sizeArr) @@ -701,7 +697,12 @@ class WindowsVideoPlayerState : VideoPlayerState { } pause() onPlaybackEnded?.invoke() - break + // Stay alive: a seek clears EOF and play() resumes without restarting this job, so the + // next end of media must be detected here too. + while (scope.isActive && _hasMedia && !isDisposing.get() && player.IsEOF(videoPlayerInstance)) { + delay(100) + } + continue } } @@ -727,6 +728,12 @@ class WindowsVideoPlayerState : VideoPlayerState { continue } + // Audio-only: no frames to read; the consumer reports the position. + if (videoWidth <= 0) { + delay(50) + continue + } + val produced = try { videoReaderMutex.withLock { processOneFrame(instance) @@ -872,6 +879,11 @@ class WindowsVideoPlayerState : VideoPlayerState { try { val frameData = frameChannel.tryReceive().getOrNull() ?: run { + if (videoWidth <= 0) { + updateAudioOnlyPosition() + delay(100) + return@run null + } if (isLoading && !frameReceived) { loadingTimeout++ if (loadingTimeout > 200) { @@ -1196,6 +1208,19 @@ class WindowsVideoPlayerState : VideoPlayerState { } } + /** Audio-only media has no frame timestamps: take the position from the native audio clock. */ + private fun updateAudioOnlyPosition() { + val instance = videoPlayerInstance + if (instance == 0L || !_hasMedia || isSeeking.get()) return + val posArr = LongArray(1) + if (player.GetMediaPosition(instance, posArr) < 0) return + _currentTime = posArr[0] / 10000000.0 + if (!_userDragging) { + _progress = if (_duration > 0.0) (_currentTime / _duration).toFloat().coerceIn(0f, 1f) else 0f + } + isLoading = false + } + /** * Sets an error state with the given message * diff --git a/mediaplayer/src/jvmMain/native/windows/AudioManager.cpp b/mediaplayer/src/jvmMain/native/windows/AudioManager.cpp index 57179b03..279eb7ab 100644 --- a/mediaplayer/src/jvmMain/native/windows/AudioManager.cpp +++ b/mediaplayer/src/jvmMain/native/windows/AudioManager.cpp @@ -117,8 +117,9 @@ int FeedOneSample(VideoPlayerInstance* inst, IMFSourceReader* audioReader, 0, nullptr, &flags, &ts100n, mfSample.GetAddressOf()); if (FAILED(hr)) return -1; } - if (!mfSample) return 0; + // End of stream comes as a null sample with the flag set: check the flag first. if (flags & MF_SOURCE_READERF_ENDOFSTREAM) return -1; + if (!mfSample) return 0; // If a seek started while ReadSample was executing, the returned sample // may be from the old position — drop it. @@ -268,7 +269,11 @@ DWORD WINAPI AudioThreadProc(LPVOID lpParam) { const float speed = inst->playbackSpeed.load(std::memory_order_relaxed); int result = FeedOneSample(inst, audioReader, engineBufferFrames, blockAlign, channels, formatTag, bitsPerSample, speed); - if (result < 0) break; // EOF or fatal error + if (result < 0) { + // EOF or fatal error. Without a video stream nothing else reports the end. + if (!inst->bHasVideo) inst->bEOF.store(true); + break; + } } { diff --git a/mediaplayer/src/jvmMain/native/windows/NativeVideoPlayer.cpp b/mediaplayer/src/jvmMain/native/windows/NativeVideoPlayer.cpp index 1f17d13d..3e9035eb 100644 --- a/mediaplayer/src/jvmMain/native/windows/NativeVideoPlayer.cpp +++ b/mediaplayer/src/jvmMain/native/windows/NativeVideoPlayer.cpp @@ -408,6 +408,7 @@ NATIVEVIDEOPLAYER_API HRESULT OpenMedia(VideoPlayerInstance* pInstance, const wc pInstance->bEOF.store(false); pInstance->videoWidth = pInstance->videoHeight = 0; pInstance->bHasAudio = false; + pInstance->bHasVideo = false; pInstance->bHasInitialFrame = false; pInstance->pCachedSample.Reset(); @@ -438,11 +439,11 @@ NATIVEVIDEOPLAYER_API HRESULT OpenMedia(VideoPlayerInstance* pInstance, const wc // ---- Video stream: RGB32 ---- hr = pInstance->pSourceReader->SetStreamSelection(MF_SOURCE_READER_ALL_STREAMS, FALSE); - if (SUCCEEDED(hr)) - hr = pInstance->pSourceReader->SetStreamSelection(MF_SOURCE_READER_FIRST_VIDEO_STREAM, TRUE); if (FAILED(hr)) return hr; + // Audio-only media (no video stream) is allowed: it plays through the audio reader below. + pInstance->bHasVideo = SUCCEEDED(pInstance->pSourceReader->SetStreamSelection(MF_SOURCE_READER_FIRST_VIDEO_STREAM, TRUE)); - { + if (pInstance->bHasVideo) { ComPtr type; hr = MFCreateMediaType(type.GetAddressOf()); if (SUCCEEDED(hr)) { @@ -454,7 +455,7 @@ NATIVEVIDEOPLAYER_API HRESULT OpenMedia(VideoPlayerInstance* pInstance, const wc if (FAILED(hr)) return hr; } - { + if (pInstance->bHasVideo) { ComPtr current; if (SUCCEEDED(pInstance->pSourceReader->GetCurrentMediaType( MF_SOURCE_READER_FIRST_VIDEO_STREAM, current.GetAddressOf()))) { @@ -541,6 +542,8 @@ NATIVEVIDEOPLAYER_API HRESULT OpenMedia(VideoPlayerInstance* pInstance, const wc } } + if (!pInstance->bHasVideo && !pInstance->bHasAudio) return MF_E_INVALIDSTREAMNUMBER; + // ---- Presentation clock ---- if (pInstance->bUseClockSync) { if (SUCCEEDED(pInstance->pSourceReader->GetServiceForStream( @@ -592,7 +595,7 @@ NATIVEVIDEOPLAYER_API HRESULT ReadVideoFrame(VideoPlayerInstance* pInstance, BYT if (!pInstance->pSourceReader) return OP_E_NOT_INITIALIZED; if (pInstance->pLockedBuffer) UnlockVideoFrame(pInstance); - if (pInstance->bEOF.load()) { *pData = nullptr; *pDataSize = 0; return S_FALSE; } + if (pInstance->bEOF.load() || !pInstance->bHasVideo) { *pData = nullptr; *pDataSize = 0; return S_FALSE; } ComPtr sample; HRESULT hr = AcquireNextSample(pInstance, sample.GetAddressOf()); @@ -647,7 +650,7 @@ NATIVEVIDEOPLAYER_API HRESULT ReadVideoFrameInto(VideoPlayerInstance* pInstance, if (!pInstance->pSourceReader) return OP_E_NOT_INITIALIZED; if (pInstance->pLockedBuffer) UnlockVideoFrame(pInstance); - if (pInstance->bEOF.load()) { + if (pInstance->bEOF.load() || !pInstance->bHasVideo) { if (pTimestamp) *pTimestamp = pInstance->llCurrentPosition.load(std::memory_order_relaxed); return S_FALSE; } @@ -855,6 +858,13 @@ NATIVEVIDEOPLAYER_API HRESULT SeekMedia(VideoPlayerInstance* pInstance, LONGLONG pInstance->llPauseStart.store(wasPlaying ? 0 : now, std::memory_order_relaxed); } + // The audio thread exits at end of stream; restart it so a seek after the end plays again. + if (pInstance->bHasAudio && pInstance->bAudioInitialized && pInstance->pSourceReaderAudio && + pInstance->hAudioThread && WaitForSingleObject(pInstance->hAudioThread.Get(), 0) == WAIT_OBJECT_0) { + StartAudioThread(pInstance); + if (!wasPlaying) SignalPause(pInstance); + } + // 7. Resume or stay paused. if (wasPlaying) { if (pInstance->bHasAudio && pInstance->bAudioInitialized) { diff --git a/mediaplayer/src/jvmMain/native/windows/VideoPlayerInstance.h b/mediaplayer/src/jvmMain/native/windows/VideoPlayerInstance.h index 996f5ac4..70d4cdd3 100644 --- a/mediaplayer/src/jvmMain/native/windows/VideoPlayerInstance.h +++ b/mediaplayer/src/jvmMain/native/windows/VideoPlayerInstance.h @@ -45,6 +45,7 @@ struct VideoPlayerInstance { Microsoft::WRL::ComPtr pAudioEndpointVolume; bool bHasAudio = false; + bool bHasVideo = false; // false for audio-only media (mp3, wav, ogg, ...) bool bAudioInitialized = false; WAVEFORMATEX* pSourceAudioFormat = nullptr; // allocated with CoTaskMemAlloc