diff --git a/experimental/components/VideoPlayer/demo/src/jvmMain/kotlin/org/jetbrains/compose/videoplayer/demo/Main.kt b/experimental/components/VideoPlayer/demo/src/jvmMain/kotlin/org/jetbrains/compose/videoplayer/demo/Main.kt index 7cdec64a32b..1cd25213a0b 100644 --- a/experimental/components/VideoPlayer/demo/src/jvmMain/kotlin/org/jetbrains/compose/videoplayer/demo/Main.kt +++ b/experimental/components/VideoPlayer/demo/src/jvmMain/kotlin/org/jetbrains/compose/videoplayer/demo/Main.kt @@ -1,10 +1,18 @@ package org.jetbrains.compose.videoplayer.demo +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.compose.ui.window.WindowState import androidx.compose.ui.window.singleWindowApplication -import org.jetbrains.compose.videoplayer.VideoPlayer +import org.jetbrains.compose.videoplayer.VideoPlayerCompose +import org.jetbrains.compose.videoplayer.VideoPlayerSwing fun main() { singleWindowApplication( @@ -12,11 +20,37 @@ fun main() { state = WindowState(width = 800.dp, height = 800.dp) ) { MaterialTheme { - VideoPlayer( - url = "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", - width = 640, - height = 480 - ) + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Swing Video Player", + fontSize = 18.sp, + color = Color.Black + ) + Spacer(modifier = Modifier.height(8.dp)) + VideoPlayerSwing( + url = "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + width = 480, + height = 360 + ) + Spacer(modifier = Modifier.height(8.dp)) + Box(modifier = Modifier.background(Color.Black), contentAlignment = Alignment.Center) { + VideoPlayerCompose( + url = "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + width = 480, + height = 360 + ) + Text( + text = "Compose Video Player", + fontSize = 18.sp, + color = Color.White + ) + } + } } } } diff --git a/experimental/components/VideoPlayer/library/src/commonMain/kotlin/org/jetbrains/compose/videoplayer/VideoPlayer.kt b/experimental/components/VideoPlayer/library/src/commonMain/kotlin/org/jetbrains/compose/videoplayer/VideoPlayer.kt index 68007a12b25..f1520083769 100644 --- a/experimental/components/VideoPlayer/library/src/commonMain/kotlin/org/jetbrains/compose/videoplayer/VideoPlayer.kt +++ b/experimental/components/VideoPlayer/library/src/commonMain/kotlin/org/jetbrains/compose/videoplayer/VideoPlayer.kt @@ -3,8 +3,15 @@ package org.jetbrains.compose.videoplayer import androidx.compose.runtime.Composable @Composable -fun VideoPlayer(url: String, width: Int, height: Int) { - VideoPlayerImpl(url, width, height) +fun VideoPlayerSwing(url: String, width: Int, height: Int) { + VideoPlayerSwingImpl(url, width, height) } -internal expect fun VideoPlayerImpl(url: String, width: Int, height: Int) +@Composable +fun VideoPlayerCompose(url: String, width: Int, height: Int) { + VideoPlayerComposeImpl(url, width, height) +} + +internal expect fun VideoPlayerSwingImpl(url: String, width: Int, height: Int) + +internal expect fun VideoPlayerComposeImpl(url: String, width: Int, height: Int) diff --git a/experimental/components/VideoPlayer/library/src/desktopMain/kotlin/org/jetbrains/compose/videoplayer/DesktopVideoPlayer.kt b/experimental/components/VideoPlayer/library/src/desktopMain/kotlin/org/jetbrains/compose/videoplayer/DesktopVideoPlayer.kt index 2c19741c020..19150f28a5d 100644 --- a/experimental/components/VideoPlayer/library/src/desktopMain/kotlin/org/jetbrains/compose/videoplayer/DesktopVideoPlayer.kt +++ b/experimental/components/VideoPlayer/library/src/desktopMain/kotlin/org/jetbrains/compose/videoplayer/DesktopVideoPlayer.kt @@ -1,47 +1,115 @@ package org.jetbrains.compose.videoplayer -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.SideEffect import androidx.compose.runtime.remember +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.awt.SwingPanel import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asComposeImageBitmap +import androidx.compose.ui.unit.dp +import org.jetbrains.skia.Bitmap +import org.jetbrains.skia.ColorAlphaType +import org.jetbrains.skia.ImageInfo import uk.co.caprica.vlcj.factory.discovery.NativeDiscovery import uk.co.caprica.vlcj.player.base.MediaPlayer import uk.co.caprica.vlcj.player.component.CallbackMediaPlayerComponent import uk.co.caprica.vlcj.player.component.EmbeddedMediaPlayerComponent +import uk.co.caprica.vlcj.player.embedded.EmbeddedMediaPlayer +import uk.co.caprica.vlcj.player.embedded.videosurface.CallbackVideoSurface +import uk.co.caprica.vlcj.player.embedded.videosurface.VideoSurfaceAdapters +import uk.co.caprica.vlcj.player.embedded.videosurface.callback.BufferFormat +import uk.co.caprica.vlcj.player.embedded.videosurface.callback.BufferFormatCallback +import uk.co.caprica.vlcj.player.embedded.videosurface.callback.RenderCallback +import uk.co.caprica.vlcj.player.embedded.videosurface.callback.format.RV32BufferFormat +import java.nio.ByteBuffer import java.util.* +import javax.swing.JComponent + @Composable -internal actual fun VideoPlayerImpl(url: String, width: Int, height: Int) { +internal actual fun VideoPlayerSwingImpl(url: String, width: Int, height: Int) { NativeDiscovery().discover() - val mediaPlayerComponent = remember { - // see https://github.com/caprica/vlcj/issues/887#issuecomment-503288294 for why we're using CallbackMediaPlayerComponent for macOS. - if (isMacOS()) { - CallbackMediaPlayerComponent() - } else { - EmbeddedMediaPlayerComponent() - } - } - SideEffect { + val mediaPlayerComponent = rememberMediaPlayerComponent() + DisposableEffect(Unit) { mediaPlayerComponent.mediaPlayer().media().play(url) + onDispose { + mediaPlayerComponent.mediaPlayer().release() + } } + return SwingPanel(background = Color.Transparent, modifier = Modifier.width(width.dp).height(height.dp), factory = { + mediaPlayerComponent + }) +} +@Composable +internal actual fun VideoPlayerComposeImpl(url: String, width: Int, height: Int) { + NativeDiscovery().discover() + var imageBitmap by remember { mutableStateOf(ImageBitmap(width, height)) } + Image(modifier = Modifier.width(width.dp).height(height.dp), bitmap = imageBitmap, contentDescription = "Video") + val mediaPlayerComponent = rememberMediaPlayerComponent() DisposableEffect(Unit) { + var byteArray: ByteArray? = null + var imageInfo: ImageInfo? = null + val mediaPlayer = mediaPlayerComponent.mediaPlayer() + val callbackVideoSurface = CallbackVideoSurface( + object : BufferFormatCallback { + + override fun getBufferFormat(sourceWidth: Int, sourceHeight: Int): BufferFormat { + imageInfo = ImageInfo.makeN32(sourceWidth, sourceHeight, ColorAlphaType.OPAQUE) + return RV32BufferFormat(sourceWidth, sourceHeight) + } + + override fun allocatedBuffers(buffers: Array) { + byteArray = ByteArray(buffers[0].limit()) + } + }, + object : RenderCallback { + + override fun display( + mediaPlayer: MediaPlayer, + nativeBuffers: Array, + bufferFormat: BufferFormat + ) { + imageInfo?.let { + val byteBuffer = nativeBuffers[0] + byteBuffer.get(byteArray) + byteBuffer.rewind() + imageBitmap = Bitmap().apply { + allocPixels(it) + installPixels(byteArray) + }.asComposeImageBitmap() + } + } + }, + true, + VideoSurfaceAdapters.getVideoSurfaceAdapter(), + ) + mediaPlayer.videoSurface().set(callbackVideoSurface) + mediaPlayer.media().play(url) onDispose { - mediaPlayerComponent.mediaPlayer().release() + mediaPlayer.release() } } +} - return SwingPanel( - background = Color.Transparent, - modifier = Modifier.fillMaxSize(), - factory = { - mediaPlayerComponent +@Composable +fun rememberMediaPlayerComponent(): JComponent { + return remember { + // see https://github.com/caprica/vlcj/issues/887#issuecomment-503288294 for why we're using CallbackMediaPlayerComponent for macOS. + if (isMacOS()) { + CallbackMediaPlayerComponent() + } else { + EmbeddedMediaPlayerComponent() } - ) + } } /** @@ -49,7 +117,7 @@ internal actual fun VideoPlayerImpl(url: String, width: Int, height: Int) { * The method names are same, but they don't share the same parent/interface. * That's why need this method. */ -private fun Any.mediaPlayer(): MediaPlayer { +private fun Any.mediaPlayer(): EmbeddedMediaPlayer { return when (this) { is CallbackMediaPlayerComponent -> mediaPlayer() is EmbeddedMediaPlayerComponent -> mediaPlayer()