diff --git a/mediaplayer/src/androidMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.android.kt b/mediaplayer/src/androidMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.android.kt index 52c9b214..cfd9d7a5 100644 --- a/mediaplayer/src/androidMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.android.kt +++ b/mediaplayer/src/androidMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.android.kt @@ -41,8 +41,11 @@ internal val androidVideoLogger = Logger.withTag("AndroidVideoPlayerSurface") @UnstableApi @Stable -actual open class VideoPlayerState { - private val context: Context = ContextProvider.getContext() +actual open class VideoPlayerState internal constructor(isInPreview: Boolean) { + actual constructor() : this(false) + + private var appContext: Context? = null + internal var previewMode: Boolean = isInPreview internal var exoPlayer: ExoPlayer? = null private var updateJob: Job? = null private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) @@ -214,12 +217,29 @@ actual open class VideoPlayerState { init { - audioProcessor.setOnAudioLevelUpdateListener { left, right -> - _leftLevel = left - _rightLevel = right + if (!previewMode) { + audioProcessor.setOnAudioLevelUpdateListener { left, right -> + _leftLevel = left + _rightLevel = right + } + ensureInitialized() + } + } + + private fun ensureInitialized(): Boolean { + synchronized(playerInitializationLock) { + if (isPlayerReleased) return false + if (exoPlayer != null) return true + + val context = appContext ?: runCatching { ContextProvider.getContext().applicationContext } + .getOrNull() + ?: return false + + appContext = context + initializePlayer(context) + registerScreenLockReceiver(context) + return exoPlayer != null } - initializePlayer() - registerScreenLockReceiver() } private fun shouldUseConservativeCodecHandling(): Boolean { @@ -239,8 +259,8 @@ actual open class VideoPlayerState { manufacturer.equals("mediatek", ignoreCase = true) } - private fun registerScreenLockReceiver() { - unregisterScreenLockReceiver() + private fun registerScreenLockReceiver(context: Context) { + unregisterScreenLockReceiver(context) screenLockReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { @@ -288,11 +308,16 @@ actual open class VideoPlayerState { addAction(Intent.ACTION_SCREEN_OFF) addAction(Intent.ACTION_SCREEN_ON) } - context.registerReceiver(screenLockReceiver, filter) - androidVideoLogger.d { "Screen lock receiver registered" } + try { + context.registerReceiver(screenLockReceiver, filter) + androidVideoLogger.d { "Screen lock receiver registered" } + } catch (e: Exception) { + androidVideoLogger.e { "Error registering screen lock receiver: ${e.message}" } + screenLockReceiver = null + } } - private fun unregisterScreenLockReceiver() { + private fun unregisterScreenLockReceiver(context: Context) { screenLockReceiver?.let { try { context.unregisterReceiver(it) @@ -304,45 +329,50 @@ actual open class VideoPlayerState { } } - private fun initializePlayer() { + private fun initializePlayer(context: Context) { synchronized(playerInitializationLock) { - if (isPlayerReleased) return - - val audioSink = DefaultAudioSink.Builder(context) - .setAudioProcessors(arrayOf(audioProcessor)) - .build() + if (isPlayerReleased || exoPlayer != null) return - val renderersFactory = object : DefaultRenderersFactory(context) { - override fun buildAudioSink( - context: Context, - enableFloatOutput: Boolean, - enableAudioTrackPlaybackParams: Boolean - ): AudioSink = audioSink - }.apply { - setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER) - // Activer le fallback du décodeur pour une meilleure stabilité - setEnableDecoderFallback(true) - - // Sur les appareils problématiques, utiliser des paramètres plus conservateurs - if (shouldUseConservativeCodecHandling()) { - // On ne peut pas désactiver l'async queueing car la méthode n'existe pas - // Mais on peut utiliser le MediaCodecSelector par défaut - setMediaCodecSelector(MediaCodecSelector.DEFAULT) + try { + val audioSink = DefaultAudioSink.Builder(context) + .setAudioProcessors(arrayOf(audioProcessor)) + .build() + + val renderersFactory = object : DefaultRenderersFactory(context) { + override fun buildAudioSink( + context: Context, + enableFloatOutput: Boolean, + enableAudioTrackPlaybackParams: Boolean + ): AudioSink = audioSink + }.apply { + setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER) + // Activer le fallback du décodeur pour une meilleure stabilité + setEnableDecoderFallback(true) + + // Sur les appareils problématiques, utiliser des paramètres plus conservateurs + if (shouldUseConservativeCodecHandling()) { + // On ne peut pas désactiver l'async queueing car la méthode n'existe pas + // Mais on peut utiliser le MediaCodecSelector par défaut + setMediaCodecSelector(MediaCodecSelector.DEFAULT) + } } - } - exoPlayer = ExoPlayer.Builder(context) - .setRenderersFactory(renderersFactory) - .setHandleAudioBecomingNoisy(true) - .setWakeMode(C.WAKE_MODE_LOCAL) - .setPauseAtEndOfMediaItems(false) - .setReleaseTimeoutMs(2000) // Augmenter le timeout de libération - .build() - .apply { - playerListener = createPlayerListener() - addListener(playerListener!!) - volume = _volume - } + exoPlayer = ExoPlayer.Builder(context) + .setRenderersFactory(renderersFactory) + .setHandleAudioBecomingNoisy(true) + .setWakeMode(C.WAKE_MODE_LOCAL) + .setPauseAtEndOfMediaItems(false) + .setReleaseTimeoutMs(2000) // Augmenter le timeout de libération + .build() + .apply { + playerListener = createPlayerListener() + addListener(playerListener!!) + volume = _volume + } + } catch (e: Exception) { + androidVideoLogger.e { "Error initializing player: ${e.message}" } + exoPlayer = null + } } } @@ -460,7 +490,12 @@ actual open class VideoPlayerState { player.release() // Réinitialiser - initializePlayer() + exoPlayer = null + playerListener = null + appContext?.let { context -> + initializePlayer(context) + registerScreenLockReceiver(context) + } // Restaurer l'élément média et la position currentMediaItem?.let { @@ -509,12 +544,32 @@ actual open class VideoPlayerState { } actual fun openUri(uri: String, initializeplayerState: InitialPlayerState) { + if (previewMode) { + _error = null + _hasMedia = true + _isPlaying = initializeplayerState == InitialPlayerState.PLAY + return + } + if (!ensureInitialized()) { + _error = VideoPlayerError.UnknownError("Android context is not available (preview or missing ContextProvider initialization).") + return + } val mediaItemBuilder = MediaItem.Builder().setUri(uri) val mediaItem = mediaItemBuilder.build() openFromMediaItem(mediaItem, initializeplayerState) } actual fun openFile(file: PlatformFile, initializeplayerState: InitialPlayerState) { + if (previewMode) { + _error = null + _hasMedia = true + _isPlaying = initializeplayerState == InitialPlayerState.PLAY + return + } + if (!ensureInitialized()) { + _error = VideoPlayerError.UnknownError("Android context is not available (preview or missing ContextProvider initialization).") + return + } val mediaItemBuilder = MediaItem.Builder() val videoUri: Uri = when (val androidFile = file.androidFile) { is AndroidFile.UriWrapper -> androidFile.uri @@ -529,36 +584,41 @@ actual open class VideoPlayerState { synchronized(playerInitializationLock) { if (isPlayerReleased) return - exoPlayer?.let { player -> - player.stop() - player.clearMediaItems() - try { - _error = null - resetStates(keepMedia = true) + val player = exoPlayer ?: run { + _isPlaying = false + _hasMedia = false + _error = VideoPlayerError.UnknownError("Video player is not initialized.") + return + } - // Extraire les métadonnées avant de préparer le lecteur - extractMediaItemMetadata(mediaItem) - - player.setMediaItem(mediaItem) - player.prepare() - player.volume = volume - player.repeatMode = if (loop) Player.REPEAT_MODE_ALL else Player.REPEAT_MODE_OFF - - // Contrôler l'état de lecture initial - if (initializeplayerState == InitialPlayerState.PLAY) { - player.play() - _hasMedia = true - } else { - player.pause() - _isPlaying = false - _hasMedia = true - } - } catch (e: Exception) { - androidVideoLogger.d { "Error opening media: ${e.message}" } + player.stop() + player.clearMediaItems() + try { + _error = null + resetStates(keepMedia = true) + + // Extraire les métadonnées avant de préparer le lecteur + extractMediaItemMetadata(mediaItem) + + player.setMediaItem(mediaItem) + player.prepare() + player.volume = volume + player.repeatMode = if (loop) Player.REPEAT_MODE_ALL else Player.REPEAT_MODE_OFF + + // Contrôler l'état de lecture initial + if (initializeplayerState == InitialPlayerState.PLAY) { + player.play() + _hasMedia = true + } else { + player.pause() _isPlaying = false - _hasMedia = false - _error = VideoPlayerError.SourceError("Failed to load media: ${e.message}") + _hasMedia = true } + } catch (e: Exception) { + androidVideoLogger.d { "Error opening media: ${e.message}" } + _isPlaying = false + _hasMedia = false + _error = VideoPlayerError.SourceError("Failed to load media: ${e.message}") } } } @@ -566,6 +626,13 @@ actual open class VideoPlayerState { actual fun play() { synchronized(playerInitializationLock) { if (!isPlayerReleased) { + if (previewMode && exoPlayer == null) { + _hasMedia = true + _isPlaying = true + return + } + + ensureInitialized() exoPlayer?.let { player -> if (player.playbackState == Player.STATE_IDLE) { player.prepare() @@ -580,6 +647,12 @@ actual open class VideoPlayerState { actual fun pause() { synchronized(playerInitializationLock) { if (!isPlayerReleased) { + if (previewMode && exoPlayer == null) { + _isPlaying = false + return + } + + ensureInitialized() exoPlayer?.pause() } } @@ -588,6 +661,14 @@ actual open class VideoPlayerState { actual fun stop() { synchronized(playerInitializationLock) { if (!isPlayerReleased) { + if (previewMode && exoPlayer == null) { + _hasMedia = false + _isPlaying = false + resetStates(keepMedia = true) + return + } + + ensureInitialized() exoPlayer?.let { player -> player.stop() player.seekTo(0) @@ -711,8 +792,11 @@ actual open class VideoPlayerState { playerListener = null exoPlayer = null - unregisterScreenLockReceiver() + appContext?.let { unregisterScreenLockReceiver(it) } resetStates() } } -} \ No newline at end of file +} + +internal actual fun createVideoPlayerState(isInPreview: Boolean): VideoPlayerState = + VideoPlayerState(isInPreview) diff --git a/mediaplayer/src/androidMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurface.android.kt b/mediaplayer/src/androidMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurface.android.kt index 127ec5d8..e36d75ae 100644 --- a/mediaplayer/src/androidMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurface.android.kt +++ b/mediaplayer/src/androidMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurface.android.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.viewinterop.AndroidView import androidx.media3.common.util.UnstableApi import androidx.media3.ui.AspectRatioFrameLayout @@ -66,6 +67,10 @@ private fun VideoPlayerSurfaceInternal( surfaceType: SurfaceType, overlay: @Composable () -> Unit ) { + if (LocalInspectionMode.current) { + VideoPlayerSurfacePreview(modifier = modifier, overlay = overlay) + return + } // Use rememberSaveable to preserve fullscreen state across configuration changes var isFullscreen by rememberSaveable { mutableStateOf(playerState.isFullscreen) @@ -297,4 +302,4 @@ private fun createPlayerViewWithSurfaceType(context: Context, surfaceType: Surfa throw RuntimeException("Unable to create PlayerView", e2) } } -} \ No newline at end of file +} diff --git a/mediaplayer/src/androidUnitTest/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerStatePreviewTest.kt b/mediaplayer/src/androidUnitTest/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerStatePreviewTest.kt new file mode 100644 index 00000000..177afd63 --- /dev/null +++ b/mediaplayer/src/androidUnitTest/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerStatePreviewTest.kt @@ -0,0 +1,32 @@ +package io.github.kdroidfilter.composemediaplayer + +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain + +@OptIn(ExperimentalCoroutinesApi::class) +class VideoPlayerStatePreviewTest { + private val mainDispatcher = UnconfinedTestDispatcher() + + @BeforeTest + fun setUp() { + Dispatchers.setMain(mainDispatcher) + } + + @AfterTest + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun createStateWithoutInitializedContextProviderDoesNotThrow() { + val playerState = VideoPlayerState() + playerState.dispose() + } +} + 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 e8b6c6ed..03b110cc 100644 --- a/mediaplayer/src/commonMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.kt +++ b/mediaplayer/src/commonMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.text.TextStyle import io.github.vinceglb.filekit.PlatformFile @@ -70,6 +71,8 @@ expect open class VideoPlayerState() { fun dispose() } +internal expect fun createVideoPlayerState(isInPreview: Boolean): VideoPlayerState + /** * Creates and manages an instance of `VideoPlayerState` within a composable function, ensuring * proper disposal of the player state when the composable leaves the composition. This function @@ -80,7 +83,19 @@ expect open class VideoPlayerState() { */ @Composable fun rememberVideoPlayerState(): VideoPlayerState { - val playerState = remember { VideoPlayerState() } + val isInPreview = LocalInspectionMode.current + val playerState = remember { createVideoPlayerState(isInPreview) } + DisposableEffect(Unit) { + onDispose { + playerState.dispose() + } + } + return playerState +} + +@Composable +fun rememberPreviewVideoPlayerState(): VideoPlayerState { + val playerState = remember { createVideoPlayerState(isInPreview = true) } DisposableEffect(Unit) { onDispose { playerState.dispose() diff --git a/mediaplayer/src/commonMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurfacePreview.kt b/mediaplayer/src/commonMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurfacePreview.kt new file mode 100644 index 00000000..ab245a86 --- /dev/null +++ b/mediaplayer/src/commonMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurfacePreview.kt @@ -0,0 +1,31 @@ +package io.github.kdroidfilter.composemediaplayer + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle + +@Composable +internal fun VideoPlayerSurfacePreview( + modifier: Modifier, + message: String = "VideoPlayerSurface (preview)", + overlay: @Composable () -> Unit, +) { + Box( + modifier = modifier.background(Color.Black.copy(alpha = 0.08f)), + contentAlignment = Alignment.Center + ) { + BasicText( + text = message, + style = TextStyle(color = Color.Gray) + ) + Box(modifier = Modifier.fillMaxSize()) { + overlay() + } + } +} diff --git a/mediaplayer/src/iosMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.ios.kt b/mediaplayer/src/iosMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.ios.kt index db14d389..d7ae7230 100644 --- a/mediaplayer/src/iosMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.ios.kt +++ b/mediaplayer/src/iosMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.ios.kt @@ -685,4 +685,6 @@ actual open class VideoPlayerState { // iOS uses Compose-based subtitles, so we don't need to configure // the native player for subtitle display } -} \ No newline at end of file +} + +internal actual fun createVideoPlayerState(isInPreview: Boolean): VideoPlayerState = VideoPlayerState() 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 9faa8d6f..1593e298 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 @@ -1,5 +1,8 @@ package io.github.kdroidfilter.composemediaplayer +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.runtime.Stable import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle @@ -39,13 +42,10 @@ import io.github.vinceglb.filekit.PlatformFile * - `dispose()`: Releases resources used by the video player and disposes of the state. */ @Stable -actual open class VideoPlayerState { - val delegate: PlatformVideoPlayerState = when { - Platform.isWindows() -> WindowsVideoPlayerState() - Platform.isMac() -> MacVideoPlayerState() - Platform.isLinux() -> LinuxVideoPlayerState() - else -> throw UnsupportedOperationException("Unsupported platform") - } +actual open class VideoPlayerState internal constructor( + val delegate: PlatformVideoPlayerState, +) { + actual constructor() : this(createPlatformDelegate()) actual open val hasMedia: Boolean get() = delegate.hasMedia actual open val isPlaying: Boolean get() = delegate.isPlaying @@ -87,9 +87,21 @@ actual open class VideoPlayerState { actual open val metadata: VideoMetadata get() = delegate.metadata actual open val aspectRatio: Float get() = delegate.aspectRatio - actual var subtitlesEnabled = delegate.subtitlesEnabled - actual var currentSubtitleTrack : SubtitleTrack? = delegate.currentSubtitleTrack - actual val availableSubtitleTracks = delegate.availableSubtitleTracks + actual var subtitlesEnabled: Boolean + get() = delegate.subtitlesEnabled + set(value) { + delegate.subtitlesEnabled = value + } + + actual var currentSubtitleTrack: SubtitleTrack? + get() = delegate.currentSubtitleTrack + set(value) { + delegate.currentSubtitleTrack = value + } + + actual val availableSubtitleTracks: MutableList + get() = delegate.availableSubtitleTracks + actual var subtitleTextStyle: TextStyle get() = delegate.subtitleTextStyle set(value) { @@ -120,3 +132,99 @@ actual open class VideoPlayerState { actual open fun clearError() = delegate.clearError() } + +private fun createPlatformDelegate(): PlatformVideoPlayerState = when { + Platform.isWindows() -> WindowsVideoPlayerState() + Platform.isMac() -> MacVideoPlayerState() + Platform.isLinux() -> LinuxVideoPlayerState() + else -> throw UnsupportedOperationException("Unsupported platform") +} + +internal actual fun createVideoPlayerState(isInPreview: Boolean): VideoPlayerState { + return if (isInPreview) { + VideoPlayerState(PreviewPlatformVideoPlayerState()) + } else { + VideoPlayerState() + } +} + +@Stable +private class PreviewPlatformVideoPlayerState : PlatformVideoPlayerState { + private var _hasMedia by mutableStateOf(false) + override val hasMedia: Boolean get() = _hasMedia + + private var _isPlaying by mutableStateOf(false) + override val isPlaying: Boolean get() = _isPlaying + + override var volume: Float by mutableStateOf(1f) + override var sliderPos: Float by mutableStateOf(0f) + override var userDragging: Boolean by mutableStateOf(false) + override var loop: Boolean by mutableStateOf(false) + override var playbackSpeed: Float by mutableStateOf(1f) + + override val leftLevel: Float get() = 0f + override val rightLevel: Float get() = 0f + override val positionText: String get() = "00:00" + override val durationText: String get() = "00:00" + override val currentTime: Double get() = 0.0 + + override val isLoading: Boolean get() = false + + private var _error by mutableStateOf(null) + override val error: VideoPlayerError? get() = _error + + override var isFullscreen: Boolean by mutableStateOf(false) + + override val metadata: VideoMetadata = VideoMetadata() + override val aspectRatio: Float get() = 16f / 9f + + override var subtitlesEnabled: Boolean by mutableStateOf(false) + override var currentSubtitleTrack: SubtitleTrack? by mutableStateOf(null) + override val availableSubtitleTracks: MutableList = mutableListOf() + override var subtitleTextStyle: TextStyle by mutableStateOf(TextStyle(color = Color.White)) + override var subtitleBackgroundColor: Color by mutableStateOf(Color.Black.copy(alpha = 0.5f)) + + override fun selectSubtitleTrack(track: SubtitleTrack?) { + currentSubtitleTrack = track + subtitlesEnabled = track != null + } + + override fun disableSubtitles() { + currentSubtitleTrack = null + subtitlesEnabled = false + } + + override fun openUri(uri: String, initializeplayerState: InitialPlayerState) { + _hasMedia = true + _isPlaying = initializeplayerState == InitialPlayerState.PLAY + _error = VideoPlayerError.UnknownError("Video playback is not available in preview.") + } + + override fun play() { + _hasMedia = true + _isPlaying = true + } + + override fun pause() { + _isPlaying = false + } + + override fun stop() { + _isPlaying = false + sliderPos = 0f + } + + override fun seekTo(value: Float) { + sliderPos = value.coerceIn(0f, 1000f) + } + + override fun toggleFullscreen() { + isFullscreen = !isFullscreen + } + + override fun dispose() = Unit + + override fun clearError() { + _error = null + } +} diff --git a/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurface.kt b/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurface.kt index e6f5c7c0..5ae03a0f 100644 --- a/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurface.kt +++ b/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurface.kt @@ -3,6 +3,7 @@ package io.github.kdroidfilter.composemediaplayer import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalInspectionMode import io.github.kdroidfilter.composemediaplayer.linux.LinuxVideoPlayerState import io.github.kdroidfilter.composemediaplayer.linux.LinuxVideoPlayerSurface import io.github.kdroidfilter.composemediaplayer.mac.MacVideoPlayerState @@ -32,10 +33,14 @@ actual fun VideoPlayerSurface( contentScale: ContentScale, overlay: @Composable () -> Unit ) { + if (LocalInspectionMode.current) { + VideoPlayerSurfacePreview(modifier = modifier, overlay = overlay) + return + } when (val delegate = playerState.delegate) { is WindowsVideoPlayerState -> WindowsVideoPlayerSurface(delegate, modifier, contentScale, overlay) is MacVideoPlayerState -> MacVideoPlayerSurface(delegate, modifier, contentScale, overlay) is LinuxVideoPlayerState -> LinuxVideoPlayerSurface(delegate, modifier, contentScale, overlay) - else -> throw IllegalArgumentException("Unsupported player state type") + else -> VideoPlayerSurfacePreview(modifier = modifier, message = "VideoPlayerSurface (preview)", overlay = overlay) } }