diff --git a/mediaplayer/src/commonMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.kt b/mediaplayer/src/commonMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.kt index fc46cdf9..1c2cdf66 100644 --- a/mediaplayer/src/commonMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.kt +++ b/mediaplayer/src/commonMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.kt @@ -87,6 +87,14 @@ interface VideoPlayerState { var isFullscreen: Boolean val aspectRatio: Float + /** + * Whether a frame of the current media has been handed to the surface. False until the first frame after + * [openUri] / [openFile], so a caller can keep a placeholder or loader up until there is a picture to show. + * + * Reported by the desktop (JVM) players; false on the other platforms for now. + */ + val hasRenderedFirstFrame: Boolean get() = false + val isPipSupported: Boolean get() = false var isPipActive: Boolean get() = false set(value) {} diff --git a/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.jvm.kt b/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.jvm.kt index e0323d2e..328e48f2 100644 --- a/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.jvm.kt +++ b/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.jvm.kt @@ -84,6 +84,7 @@ open class DefaultVideoPlayerState : VideoPlayerState { override val metadata: VideoMetadata get() = delegate.metadata override val aspectRatio: Float get() = delegate.aspectRatio + override val hasRenderedFirstFrame: Boolean get() = delegate.hasRenderedFirstFrame override var subtitlesEnabled = delegate.subtitlesEnabled override var currentSubtitleTrack: SubtitleTrack? = delegate.currentSubtitleTrack diff --git a/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/linux/LinuxVideoPlayerState.kt b/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/linux/LinuxVideoPlayerState.kt index bb7d8ec5..ef7eb35b 100644 --- a/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/linux/LinuxVideoPlayerState.kt +++ b/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/linux/LinuxVideoPlayerState.kt @@ -47,6 +47,10 @@ class LinuxVideoPlayerState : VideoPlayerState { private val _currentFrameState = MutableStateFlow(null) internal val currentFrameState: State = mutableStateOf(null) + // The last frame stays on the surface across an open, so the first frame of a media is tracked on its own + private var _hasRenderedFirstFrame by mutableStateOf(false) + override val hasRenderedFirstFrame: Boolean get() = _hasRenderedFirstFrame + // Double-buffered Skia bitmaps private var skiaBitmapWidth: Int = 0 private var skiaBitmapHeight: Int = 0 @@ -174,6 +178,7 @@ class LinuxVideoPlayerState : VideoPlayerState { ensureActive() withContext(Dispatchers.Main) { (currentFrameState as MutableState).value = newFrame + if (newFrame != null) _hasRenderedFirstFrame = true } } } @@ -235,6 +240,7 @@ class LinuxVideoPlayerState : VideoPlayerState { isLoading = true error = null playbackSpeed = 1.0f + _hasRenderedFirstFrame = false } try { diff --git a/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacVideoPlayerState.kt b/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacVideoPlayerState.kt index 035445b6..3ca4791e 100644 --- a/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacVideoPlayerState.kt +++ b/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacVideoPlayerState.kt @@ -183,6 +183,9 @@ class MacVideoPlayerState : VideoPlayerState { private val bitmapLock = ReentrantReadWriteLock() internal val currentFrameState = mutableStateOf(null) + // currentFrameState is cleared on every open (releaseAllResources) and set by the first consumed frame + override val hasRenderedFirstFrame: Boolean get() = currentFrameState.value != null + // Aspect ratio — driven by the live frame (see syncAspectRatioToFrame). private var _aspectRatio by mutableStateOf(16f / 9f) override val aspectRatio: Float get() = _aspectRatio 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..f8f05343 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 @@ -207,6 +207,9 @@ class WindowsVideoPlayerState : VideoPlayerState { private val bitmapLock = ReentrantReadWriteLock() internal val currentFrameState = mutableStateOf(null) + // currentFrameState is cleared on every open (releaseAllResources) and set by the first consumed frame + override val hasRenderedFirstFrame: Boolean get() = currentFrameState.value != null + // Aspect ratio property override val aspectRatio: Float get() =