Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
}

Expand All @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
*
Expand Down
9 changes: 7 additions & 2 deletions mediaplayer/src/jvmMain/native/windows/AudioManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}
}

{
Expand Down
22 changes: 16 additions & 6 deletions mediaplayer/src/jvmMain/native/windows/NativeVideoPlayer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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<IMFMediaType> type;
hr = MFCreateMediaType(type.GetAddressOf());
if (SUCCEEDED(hr)) {
Expand All @@ -454,7 +455,7 @@ NATIVEVIDEOPLAYER_API HRESULT OpenMedia(VideoPlayerInstance* pInstance, const wc
if (FAILED(hr)) return hr;
}

{
if (pInstance->bHasVideo) {
ComPtr<IMFMediaType> current;
if (SUCCEEDED(pInstance->pSourceReader->GetCurrentMediaType(
MF_SOURCE_READER_FIRST_VIDEO_STREAM, current.GetAddressOf()))) {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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<IMFSample> sample;
HRESULT hr = AcquireNextSample(pInstance, sample.GetAddressOf());
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ struct VideoPlayerInstance {
Microsoft::WRL::ComPtr<IAudioEndpointVolume> pAudioEndpointVolume;

bool bHasAudio = false;
bool bHasVideo = false; // false for audio-only media (mp3, wav, ogg, ...)
bool bAudioInitialized = false;

WAVEFORMATEX* pSourceAudioFormat = nullptr; // allocated with CoTaskMemAlloc
Expand Down