Skip to content

Commit e519e09

Browse files
committed
fix(windows): rewrite audio to use timing-master model and adaptive frame polling
Rewrite the Windows AudioManager to use the audio-as-timing-master pattern (matching macOS AVPlayer). The audio thread feeds decoded PCM to WASAPI as fast as the buffer allows, with no wall-clock drift correction, sleeps, or sample dropping. Video compensates via audioLatencyMs. This eliminates stutter bugs after seek, resume, or speed changes. Also: - Add adaptive frame polling interval based on native video frame rate (prevents starving the audio thread on the shared SourceReader) - Add pre-fill audio buffer on seek for gapless playback - Include pre-built DLLs for x86-64 and ARM64 - Add jdk.accessibility module for Linux desktop packaging
1 parent bfdc842 commit e519e09

9 files changed

Lines changed: 472 additions & 261 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,14 @@ Pods/
1717
*yarn.lock
1818
# Native compiled binaries (built locally or in CI)
1919
/mediaplayer/src/jvmMain/resources/composemediaplayer/native/
20+
/mediaplayer/src/jvmMain/resources/win32-x86-64/
21+
/mediaplayer/src/jvmMain/resources/win32-arm64/
2022

2123
# Native build artifacts
2224
/mediaplayer/src/jvmMain/native/windows/build-x64/
2325
/mediaplayer/src/jvmMain/native/windows/build-arm64/
26+
/mediaplayer/src/jvmMain/native/windows/build-test/
2427
*.log
2528
/sample/composeApp/debug/
29+
NUL
30+
.claude/

mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/windows/WindowsVideoPlayerState.kt

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,11 @@ class WindowsVideoPlayerState : VideoPlayerState {
263263
private var skiaBitmapWidth: Int = 0
264264
private var skiaBitmapHeight: Int = 0
265265

266+
// Adaptive frame interval (ms) based on the video's native frame rate.
267+
// Mirrors macOS approach: poll at the video frame rate, not faster.
268+
// This prevents starving the audio thread on the shared SourceReader.
269+
private var frameIntervalMs: Long = 16L // Default ~60fps, updated after open
270+
266271
// Variable to store the last opened URI
267272
private var lastUri: String? = null
268273

@@ -592,6 +597,16 @@ class WindowsVideoPlayerState : VideoPlayerState {
592597
)
593598
}
594599

600+
// Query the native frame rate to compute an adaptive polling interval
601+
// like macOS does with captureFrameRate.
602+
val rateArr = IntArray(2)
603+
if (player.nGetVideoFrameRate(instance, rateArr) >= 0 && rateArr[0] > 0) {
604+
val fps = rateArr[0].toDouble() / rateArr[1].coerceAtLeast(1).toDouble()
605+
frameIntervalMs = (1000.0 / fps).toLong().coerceIn(8L, 50L)
606+
} else {
607+
frameIntervalMs = 16L // fallback ~60fps
608+
}
609+
595610
// Set _hasMedia to true only if everything succeeded
596611
_hasMedia = true
597612

@@ -807,7 +822,11 @@ class WindowsVideoPlayerState : VideoPlayerState {
807822
// Send frame to channel
808823
frameChannel.trySend(FrameData(targetBitmap, frameTime))
809824

810-
delay(1)
825+
// Yield to the audio thread on the shared SourceReader.
826+
// Native AcquireNextSample already sleeps to pace video to
827+
// the presentation clock, so this delay just prevents tight
828+
// looping when frames are skipped or the decoder is fast.
829+
delay(frameIntervalMs)
811830
} catch (e: CancellationException) {
812831
break
813832
} catch (e: Exception) {
@@ -863,7 +882,7 @@ class WindowsVideoPlayerState : VideoPlayerState {
863882
}
864883
isLoading = false
865884

866-
delay(1)
885+
delay(frameIntervalMs)
867886
} catch (e: CancellationException) {
868887
break
869888
} catch (e: Exception) {

mediaplayer/src/jvmMain/native/windows/AudioManager.cpp

Lines changed: 289 additions & 175 deletions
Large diffs are not rendered by default.

mediaplayer/src/jvmMain/native/windows/AudioManager.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ DWORD WINAPI AudioThreadProc(LPVOID lpParam);
3636
* @param pInstance Pointer to the video player instance.
3737
* @return S_OK on success, or an error code.
3838
*/
39+
/**
40+
* @brief Pre-fills the WASAPI buffer before Start() to avoid gaps after seek.
41+
*/
42+
HRESULT PreFillAudioBuffer(VideoPlayerInstance* pInstance);
43+
3944
HRESULT StartAudioThread(VideoPlayerInstance* pInstance);
4045

4146
/**

0 commit comments

Comments
 (0)