소스 검색

background fixes

milan jurkulák 2 년 전
부모
커밋
8ae9926690

+ 3 - 11
build.gradle.kts

@@ -70,19 +70,11 @@ allprojects {
         implementation(libs.coil.svg)
 //        implementation(libs.coil.gif)
         implementation(libs.coil.network.okhttp)
-        // video vlc
-        implementation("uk.co.caprica:vlcj:4.8.2")
-        // media player
-        implementation("io.github.khubaibkhan4:mediaplayer-kmp:1.0.9")
+        // ffmpeg
+        implementation("org.bytedeco:ffmpeg-platform:6.1.1-1.5.10")
         // music
-        implementation(libs.korau)
-        // gif
-//        implementation(libs.accompanist.drawablepainter)
-        // glide
-//        implementation(libs.glide)
-//        implementation(libs.landscapist.glide)
+//        implementation(libs.korau)
         // palette
-//        implementation(libs.landscapist.palette)
         implementation(libs.material.kolor)
         // json
         implementation(libs.google.gson)

+ 3 - 3
gradle/libs.versions.toml

@@ -18,7 +18,7 @@ materialKolor = "1.7.0"
 coil3 = "3.0.0-alpha06"
 ktor = "3.0.0-wasm2"
 kotlinxCoroutinesSwing = "1.8.0"
-korau = "4.0.10"
+#korau = "4.0.10"
 #lanscapist = "2.3.8"
 #generativeai = "0.5.0-1.0.0-wasm"
 #compose-webview = "1.9.20"
@@ -39,8 +39,8 @@ coil-compose-core = { module = "io.coil-kt.coil3:coil-compose-core", version.ref
 coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil3" }
 coil-mp = { module = "io.coil-kt.coil3:coil", version.ref = "coil3" }
 coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil3" }
-coil-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil3" }
-korau = { module = "com.soywiz.korlibs.korau:korau", version.ref = "korau" }
+#coil-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil3" }
+#korau = { module = "com.soywiz.korlibs.korau:korau", version.ref = "korau" }
 #accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version = "0.35.0-alpha" }
 #coil-gif = { module = "io.coil-kt.coil3:coil-gif", version = "2.2.7" }
 #landscapist-glide = { module = "com.github.skydoves:landscapist-glide", version.ref = "lanscapist" }

+ 337 - 0
src/main/kotlin/eu/mjdev/desktop/components/video/KFFMpeg.kt

@@ -0,0 +1,337 @@
+package eu.mjdev.desktop.components.video;
+
+import androidx.compose.ui.graphics.asComposeImageBitmap
+import androidx.compose.ui.unit.IntSize
+import org.bytedeco.ffmpeg.avcodec.AVCodecContext
+import org.bytedeco.ffmpeg.avcodec.AVCodecHWConfig
+import org.bytedeco.ffmpeg.avcodec.AVCodecParameters
+import org.bytedeco.ffmpeg.avcodec.AVPacket
+import org.bytedeco.ffmpeg.avformat.AVFormatContext
+import org.bytedeco.ffmpeg.avutil.AVBufferRef
+import org.bytedeco.ffmpeg.avutil.AVDictionaryEntry
+import org.bytedeco.ffmpeg.global.avcodec
+import org.bytedeco.ffmpeg.global.avformat
+import org.bytedeco.ffmpeg.global.avutil
+import org.bytedeco.ffmpeg.global.swscale
+import org.bytedeco.javacpp.BytePointer
+import org.bytedeco.javacpp.DoublePointer
+import org.bytedeco.javacpp.PointerPointer
+import org.jetbrains.skia.Bitmap
+import org.jetbrains.skia.ColorAlphaType
+import org.jetbrains.skia.ColorType
+import org.jetbrains.skia.ImageInfo
+import java.nio.charset.Charset
+import kotlin.math.absoluteValue
+import kotlin.math.min
+
+enum class KAVMediaType(val id: Int) {
+    UNKNOWN(avutil.AVMEDIA_TYPE_UNKNOWN),
+    VIDEO(avutil.AVMEDIA_TYPE_VIDEO),
+    AUDIO(avutil.AVMEDIA_TYPE_AUDIO),
+    DATA(avutil.AVMEDIA_TYPE_DATA),
+    SUBTITLE(avutil.AVMEDIA_TYPE_SUBTITLE),
+    ATTACHMENT(avutil.AVMEDIA_TYPE_ATTACHMENT),
+    NB(avutil.AVMEDIA_TYPE_NB);
+
+    companion object {
+        private val idMap = entries.associateBy { it.id }
+
+        fun fromId(id: Int) = idMap[id] ?: UNKNOWN
+    }
+}
+
+data class KAVCodec(
+    val id: Int,
+    val name: String,
+    val type: Int,
+    val hwDecoder: List<KHWDecoder>
+)
+
+data class KHWDecoder(
+    val type: Int,
+    val name: String
+)
+
+sealed interface KAVStream {
+    val id: Int
+}
+
+data class KVideoStream(
+    override val id: Int,
+    val width: Int,
+    val height: Int,
+    val durationMillis: Long,
+    val fps: Float,
+    val bitrate: Long,
+    val codec: String
+) : KAVStream
+
+data class KAudioStream(
+    override val id: Int
+) : KAVStream
+
+class KAVFormatContext {
+    val fmtCtx = AVFormatContext(null)
+
+    fun openInput(filePath: String) {
+        if (avformat.avformat_open_input(fmtCtx, filePath, null, null) < 0) {
+            throw IllegalStateException("Open file $filePath failed!")
+        }
+        if (avformat.avformat_find_stream_info(fmtCtx, null as PointerPointer<*>?) < 0) {
+            throw IllegalStateException("Unable to open stream_info!")
+        }
+    }
+
+    fun closeInput() {
+        avformat.avformat_close_input(fmtCtx)
+    }
+
+    fun findMetadata(): Map<String, String> {
+        val metadataMap = mutableMapOf<String, String>()
+        var entry: AVDictionaryEntry? = null
+        do {
+            entry = avutil.av_dict_iterate(fmtCtx.metadata(), entry)
+            if (entry != null) {
+                val key = entry.key().getString(Charset.defaultCharset())
+                val value = entry.value().getString(Charset.defaultCharset())
+                metadataMap[key] = value
+            }
+        } while (entry != null)
+        return metadataMap
+    }
+
+    fun findVideoStreams(): List<KVideoStream> = (0 until fmtCtx.nb_streams()).mapNotNull { i ->
+        val type = KAVMediaType.fromId(fmtCtx.streams(i).codecpar().codec_type())
+        println("Stream: $i type: $type")
+        if (type == KAVMediaType.VIDEO) {
+            val stream = fmtCtx.streams(i)
+            val width = stream.codecpar().width()
+            val height = stream.codecpar().height()
+            val duration =
+                stream.duration() * 1000L * stream.time_base().num().toLong() / stream.time_base().den().toLong()
+            val fps = stream.avg_frame_rate().num().toFloat() / stream.avg_frame_rate().den().toFloat()
+            val bitrate = stream.codecpar().bit_rate()
+            val codecName =
+                avcodec.avcodec_get_name(stream.codecpar().codec_id()).getString(Charset.defaultCharset())
+            KVideoStream(
+                id = i,
+                width = width,
+                height = height,
+                durationMillis = duration,
+                fps = fps,
+                bitrate = bitrate,
+                codec = codecName
+            )
+        } else {
+            null
+        }
+    }
+
+    fun findCodec(videoStream: KVideoStream): KAVCodec {
+        val stream = fmtCtx.streams(videoStream.id)
+        val codecId = stream.codecpar().codec_id()
+        val type = stream.codecpar().codec_type() // Normally this is 0 -> avutil.AV_HWDEVICE_TYPE_NONE
+        val name = avcodec.avcodec_get_name(codecId).getString(Charset.defaultCharset())
+        val hwDecoder = findHWDecoder(codecId) // search for hardware accelerated decoders
+        return KAVCodec(
+            id = codecId,
+            name = name,
+            type = type,
+            hwDecoder = hwDecoder
+        )
+    }
+
+    private fun findHWDecoder(codecId: Int): List<KHWDecoder> {
+        val codec = avcodec.avcodec_find_decoder(codecId) ?: throw IllegalStateException("No codec found!")
+        val codecType = codec.type()
+        val supportHwTypes = mutableSetOf<Int>()
+        var index = 0
+        do {
+            val config: AVCodecHWConfig? = avcodec.avcodec_get_hw_config(codec, index)
+            if (config != null && (config.methods() and avcodec.AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX > 0)) {
+                supportHwTypes.add(config.device_type())
+            }
+        } while (config != null && index++ < 10)
+        val decoder = mutableListOf<KHWDecoder>()
+        var hwType = avutil.av_hwdevice_iterate_types(codecType)
+        index = 0
+        while (hwType != avutil.AV_HWDEVICE_TYPE_NONE && index++ < 20) {
+            val name = avutil.av_hwdevice_get_type_name(hwType).getString(Charset.defaultCharset())
+            if (hwType in supportHwTypes)
+                decoder.add(KHWDecoder(hwType, name))
+            hwType = avutil.av_hwdevice_iterate_types(hwType)
+        }
+        return decoder
+    }
+}
+
+class KAVCodecContext(codecParams: AVCodecParameters, hwDecoder: KHWDecoder? = null) {
+    val codecCtx: AVCodecContext = avcodec.avcodec_alloc_context3(null)
+
+    init {
+        avcodec.avcodec_parameters_to_context(codecCtx, codecParams)
+        val codec = avcodec.avcodec_find_decoder(codecParams.codec_id())
+            ?: throw IllegalStateException("No codec found!")
+        if (hwDecoder != null) {
+            //val hwConfig = avcodec.avcodec_get_hw_config(codec, 0)
+            // HWAccel
+            val hwDeviceCtx = AVBufferRef()
+            errCheck(
+                avutil.av_hwdevice_ctx_create(hwDeviceCtx, hwDecoder.type, null as BytePointer?, null, 0),
+                "Unable to create hw decoder ${hwDecoder.type}:${hwDecoder.name} for ${
+                    codec.name().getString(Charset.defaultCharset())
+                }"
+            )
+            codecCtx.hw_device_ctx(avutil.av_buffer_ref(hwDeviceCtx))
+        }
+        //open
+        if (avcodec.avcodec_open2(codecCtx, codec, null as PointerPointer<*>?) < 0)
+            throw IllegalStateException("Can not open codec!")
+    }
+
+    fun close() {
+        avcodec.avcodec_close(codecCtx)
+        avcodec.avcodec_free_context(codecCtx)
+    }
+}
+
+class KFrameGrabber(
+    kStream: KVideoStream,
+    kFmtCtx: KAVFormatContext,
+    hwDecoder: KHWDecoder? = null,
+    targetSize: IntSize? = null
+) {
+    private val fmtCtx = kFmtCtx.fmtCtx
+    private val stream = fmtCtx.streams(kStream.id)
+    private val kCodecContext = KAVCodecContext(stream.codecpar(), hwDecoder)
+    private val codecCtx = kCodecContext.codecCtx
+    val targetWidth = targetSize?.width ?: codecCtx.width()
+    val targetHeight = targetSize?.height ?: codecCtx.height()
+    private val dstFormat = avutil.AV_PIX_FMT_BGRA // Format matches the internal skia image format
+    private val skiaFormat = ImageInfo(
+        targetWidth,
+        targetHeight,
+        ColorType.BGRA_8888,
+        ColorAlphaType.OPAQUE
+    ) //N32 == BGRA_8888 Native format
+
+    //private val dstFormat = avutil.AV_PIX_FMT_RGB565 // 2 Byte per pixel -> half memory size for one image
+    //private val skiaFormat = ImageInfo(targetWidth, targetHeight, ColorType.RGB_565, ColorAlphaType.OPAQUE)
+    private val timeBaseNum = stream.time_base().num().toLong()
+    private val timeBaseDen = stream.time_base().den().toLong()
+
+    //Prepare rgb image
+    private val numBytes = avutil.av_image_get_buffer_size(dstFormat, targetWidth, targetHeight, 1)
+    private val jArray = ByteArray(numBytes)
+    private val frame = avutil.av_frame_alloc() //TODO maybe null pointer handling
+    private val hwFrame = avutil.av_frame_alloc()
+    private val pFrameRGB = avutil.av_frame_alloc().also { frame ->
+        val buffer = BytePointer(avutil.av_malloc(numBytes.toLong()))
+        avutil.av_image_fill_arrays(frame.data(), frame.linesize(), buffer, dstFormat, targetWidth, targetHeight, 1)
+    }
+    private val bufferBitmap = Bitmap().also {
+        it.allocPixels(skiaFormat)
+    }
+    val composeImage = bufferBitmap.asComposeImageBitmap()
+
+    fun close() {
+        avutil.av_frame_free(frame)
+        avutil.av_frame_free(hwFrame)
+        kCodecContext.close()
+    }
+
+    private val pkt = AVPacket()
+    var decodedFrameCounter = 0L
+        private set
+    var bitmapFrameCounter = 0L
+        private set
+    private var currentImageTS: Long = 0L
+
+    fun grabNextFrame(ts: Long) {
+        //TODO seek to ts when diff is to high or negative diff
+        /*if (frameCounter == 0) {
+            avformat.av_seek_frame(fmtCtx, stream, 120, avformat.AVSEEK_FLAG_BACKWARD)
+        }*/
+        val timeDiff = currentImageTS - ts
+        //println("pts: ${hwFrame.pts()} td: $timeDiff")
+        if (timeDiff.absoluteValue > 500) {
+            val seekPts = ts * timeBaseDen / timeBaseNum / 1000L
+//            println("Time diff: $timeDiff seek to $seekPts ${hwFrame.pts()}")
+            avformat.av_seek_frame(fmtCtx, stream.index(), seekPts, avformat.AVSEEK_FLAG_BACKWARD)
+        } else if (timeDiff in 11..499) {
+            return
+        }
+        var repeatCounter = 0
+        do {
+            do {
+                val rf = avformat.av_read_frame(fmtCtx, pkt)
+                if (rf < 0) return
+            } while (pkt.stream_index() != stream.index())
+            val r1 = avcodec.avcodec_send_packet(codecCtx, pkt)
+            avcodec.av_packet_unref(pkt)
+            val r2 = avcodec.avcodec_receive_frame(codecCtx, hwFrame)
+            decodedFrameCounter++
+            if (r2 < 0) {
+                println("ret1 $r1 ret2 $r2")
+                println("Frame skipped!")
+            }
+            val frameTime = hwFrame.pts() * 1000L * timeBaseNum / timeBaseDen
+            currentImageTS = frameTime
+            val isPast = frameTime < ts
+        } while (r2 < 0 || (isPast && repeatCounter++ < 10))
+        val hwFormat = codecCtx.hwaccel()?.pix_fmt() ?: -1
+        val srcFrame = if (hwFrame.format() == hwFormat) {
+            //Transfer data from hardware accel device to memory
+            errCheck(
+                avutil.av_hwframe_transfer_data(frame, hwFrame, 0),
+                "Error transferring data to system memory!"
+            )
+            frame
+        } else {
+            hwFrame
+        }
+        //val frameFormat = avutil.av_get_pix_fmt_name(hwFrame.format()).getString(Charset.defaultCharset())
+        //val srcFormat = avutil.av_get_pix_fmt_name(srcFrame.format()).getString(Charset.defaultCharset())
+        //println("hwFormat: $frameFormat -> $srcFormat")
+        //println("Frame: ${hwFrame.best_effort_timestamp()} format: ${frame.format()} (hw:${hwFrame.format()})")
+
+        val swsCtx = swscale.sws_getContext(
+            codecCtx.width(), codecCtx.height(), srcFrame.format(), //Src size, format
+            targetWidth, targetHeight, dstFormat, //Dst size, format
+            swscale.SWS_BILINEAR, null, null, null as DoublePointer?
+        )
+        //println("Frame format: ${frame.format()}")
+        errCheck(
+            swscale.sws_scale(
+                swsCtx, srcFrame.data(), srcFrame.linesize(),
+                0, codecCtx.height(),
+                pFrameRGB.data(),
+                pFrameRGB.linesize()
+            ),
+            "sws_scale error!"
+        )
+        // Copy data to java array
+        pFrameRGB.data(0).get(jArray)
+        // Copy java array to bitmap
+        bufferBitmap.installPixels(jArray)
+        bitmapFrameCounter++
+    }
+}
+
+fun errCheck(returnCode: Int, errorMessage: String? = null) {
+    if (returnCode < 0) {
+        val message = if (errorMessage != null)
+            "$errorMessage - code $returnCode: ${av_err2str(returnCode)}"
+        else
+            "$returnCode: ${av_err2str(returnCode)}"
+        throw IllegalStateException(message)
+    }
+}
+
+fun av_err2str(errNum: Int): String {
+    val charArray = ByteArray(avutil.AV_ERROR_MAX_STRING_SIZE)
+    avutil.av_strerror(errNum, charArray, avutil.AV_ERROR_MAX_STRING_SIZE.toLong())
+    val string = charArray.decodeToString()
+    val endIndex = min(string.indexOf(Char(0)), avutil.AV_ERROR_MAX_STRING_SIZE)
+    return string.subSequence(0, endIndex).toString()
+}

+ 0 - 176
src/main/kotlin/eu/mjdev/desktop/components/video/VideoPlayerDirect.kt

@@ -1,176 +0,0 @@
-package eu.mjdev.desktop.components.video
-
-import androidx.compose.foundation.Canvas
-import androidx.compose.runtime.*
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.ImageBitmap
-import androidx.compose.ui.graphics.asComposeImageBitmap
-import androidx.compose.ui.graphics.drawscope.scale
-import org.jetbrains.skia.Bitmap
-import uk.co.caprica.vlcj.factory.discovery.NativeDiscovery
-import uk.co.caprica.vlcj.player.base.MediaPlayer
-import uk.co.caprica.vlcj.player.base.MediaPlayerEventAdapter
-import uk.co.caprica.vlcj.player.base.State
-import uk.co.caprica.vlcj.player.component.CallbackMediaPlayerComponent
-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.lang.Float.min
-import java.nio.ByteBuffer
-
-@Composable
-fun VideoPlayerDirect(
-    modifier: Modifier = Modifier,
-    state: VlcjVideoPlayerState = remember { VlcjVideoPlayerState() },
-    url: String
-) {
-    var frameCounter = 0L
-    var lastTs = 0L
-    NativeDiscovery().discover()
-    DisposableEffect(state) {
-        val eventListener = object : MediaPlayerEventAdapter() {
-            override fun playing(mediaPlayer: MediaPlayer) {
-                println("Playing: ${mediaPlayer.media().info().textTracks()}")
-            }
-
-            override fun timeChanged(mediaPlayer: MediaPlayer, newTime: Long) {
-                state.time = newTime
-            }
-
-            override fun positionChanged(mediaPlayer: MediaPlayer, newPosition: Float) {
-                //println("Position: $newPosition")
-                state.progress = newPosition
-            }
-        }
-        state.mediaPlayer.events().addMediaPlayerEventListener(eventListener)
-        onDispose {
-            state.mediaPlayer.events().removeMediaPlayerEventListener(eventListener)
-            state.mediaPlayer.release()
-        }
-    }
-    var frameTime: Long by remember { mutableStateOf(0L) }
-    LaunchedEffect(url) {
-        println("Sideffect started")
-        state.mediaPlayer.media()?.start(url)
-        state.mediaPlayer.subpictures().setTrack(-1)
-        while (true) {
-            withFrameMillis {
-                frameTime = it
-            }
-        }
-    }
-    return Canvas(modifier) {
-        state.internalState.updateComposeImage(frameTime)?.let { image ->
-            val scaleW = size.width / image.width.toFloat()
-            val scaleH = size.height / image.height.toFloat()
-            //val frame = renderState.frame // needed to enforce high fps redrawing
-            //val fts = frameTime
-            scale(scale = min(scaleH, scaleW), pivot = Offset.Zero) {
-                drawImage(image)
-            }
-        }
-        frameCounter++
-        if (frameCounter % 60L == 0L) {
-            val time = System.currentTimeMillis()
-            val delta = time - lastTs
-            println("FPS: ${1000 / (delta / 60)}")
-            lastTs = time
-        }
-    }
-}
-
-interface VideoPlayerState {
-    val time: Long //Timestamp milliseconds
-    val progress: Float //from 0 to 1 -> 0 is start and 1 is end
-    val aspectRatio: Float
-    fun seek(position: Float)
-    fun togglePause()
-}
-
-class VlcjVideoPlayerState : VideoPlayerState {
-    internal val internalState = RenderState()
-    override var time: Long = 0L
-        internal set
-    override var progress: Float by mutableStateOf(0f)
-        internal set
-    val mediaPlayer: MediaPlayer get() = internalState.mediaPlayerComponent.mediaPlayer()
-    override val aspectRatio: Float get() = internalState.aspectRatio
-
-    override fun seek(position: Float) {
-        mediaPlayer.controls().setPosition(position)
-    }
-
-    override fun togglePause() {
-        val state = mediaPlayer.media().info().state()
-        if (state == State.PLAYING) {
-            mediaPlayer.controls().pause()
-        } else {
-            mediaPlayer.controls().play()
-        }
-    }
-
-    init {
-        println("New VideoPlayerState")
-    }
-}
-
-internal class RenderState {
-    var aspectRatio: Float by mutableStateOf(1f)
-        private set
-
-    var currentBuffer: ByteBuffer? = null
-
-    private var buffer: ByteArray = ByteArray(0)
-    private var bufferBitmap: Bitmap = Bitmap()
-    private var composeImage: ImageBitmap? = null
-
-    init {
-        println("New RenderState")
-    }
-
-    fun updateComposeImage(frameTime: Long): ImageBitmap? {
-        currentBuffer?.let {
-            it.get(buffer)
-            it.rewind()
-            bufferBitmap.installPixels(buffer)
-            return composeImage
-        }
-        return null
-    }
-
-    private val bufferFormatCallback = object : BufferFormatCallback {
-        override fun getBufferFormat(sourceWidth: Int, sourceHeight: Int): BufferFormat {
-            println("Size: $sourceWidth x $sourceHeight")
-            bufferBitmap = Bitmap().also {
-                it.allocN32Pixels(sourceWidth, sourceHeight, true)
-            }
-            composeImage = bufferBitmap.asComposeImageBitmap()
-            buffer = ByteArray(sourceWidth * sourceHeight * 4)
-            //renderCallback.setBuffer(IntArray(sourceWidth * sourceHeight * 4))
-            with(bufferBitmap) {
-                aspectRatio = if (width <= 0 || height <= 0) 1f else width.toFloat() / height.toFloat()
-                println("Set aspectRatio to: $aspectRatio")
-            }
-            return RV32BufferFormat(sourceWidth, sourceHeight)
-        }
-
-        override fun allocatedBuffers(buffers: Array<out ByteBuffer>) {
-            currentBuffer = buffers.firstOrNull()
-        }
-    }
-    private val renderCallback = RenderCallback { _, nativeBuffers, _ ->
-        //currentBuffer = nativeBuffers.first()
-    }
-    val mediaPlayerComponent = CallbackMediaPlayerComponent(
-        null,
-        null,
-        null,
-        true,
-        null,
-        renderCallback,
-        bufferFormatCallback,
-        null
-    )
-}

+ 142 - 0
src/main/kotlin/eu/mjdev/desktop/components/video/VideoPlayerFFMpeg.kt

@@ -0,0 +1,142 @@
+package eu.mjdev.desktop.components.video
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.drawscope.scale
+import androidx.compose.ui.unit.IntSize
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.isActive
+import kotlin.math.min
+
+class FFmpegVideoPlayerState {
+    private val kContext = KAVFormatContext()
+
+    var time: Long = 0L
+        internal set
+    var progress: Float by mutableStateOf(0f)
+        internal set
+    var aspectRatio: Float by mutableStateOf(1f)
+        private set
+    var displayFPS: Float by mutableStateOf(0f)
+        internal set
+    var decodedFPS: Float by mutableStateOf(0f)
+        internal set
+
+    var frameGrabber: KFrameGrabber? by mutableStateOf(null)
+        private set
+
+    var metadata: Map<String, String> by mutableStateOf(emptyMap())
+        private set
+
+    fun open(file: String) {
+        kContext.openInput(file)
+        metadata = kContext.findMetadata()
+    }
+
+    fun close() {
+        kContext.closeInput()
+    }
+
+    fun streams(): List<KVideoStream> = kContext.findVideoStreams()
+    fun codec(stream: KVideoStream): KAVCodec = kContext.findCodec(stream)
+
+    fun play(
+        stream: KVideoStream,
+        hwDecoder: KHWDecoder? = null,
+        targetSize: IntSize? = null
+    ) {
+        frameGrabber?.close() // close running frame grabber
+        frameGrabber = KFrameGrabber(stream, kContext, hwDecoder, targetSize)
+        aspectRatio = stream.width.toFloat() / stream.height.toFloat()
+    }
+
+    fun stop() {
+        frameGrabber?.close()
+        frameGrabber = null
+    }
+
+    internal var seekPosition: Float = -1f
+
+    fun seek(position: Float) {
+        seekPosition = position
+    }
+
+    fun togglePause() {
+        //TODO
+    }
+}
+
+@Composable
+fun VideoPlayerFFMpeg(
+    modifier: Modifier = Modifier,
+    state: FFmpegVideoPlayerState = remember { FFmpegVideoPlayerState() },
+    file: String
+) {
+    var frame by remember { mutableStateOf(0) }
+    val videoImage = mutableStateOf<ImageBitmap?>(null)
+    LaunchedEffect(file, Dispatchers.IO) {
+        state.open(file)
+        val stream = state.streams().first()
+        val codec = state.codec(stream)
+        println("Codec: ${codec.name}")
+        codec.hwDecoder.forEach { println("   Decoder: ${it.name}") }
+        val scale = 1
+        val targetSize = IntSize(stream.width / scale, stream.height / scale)
+        state.play(stream, codec.hwDecoder.firstOrNull(), targetSize) // Hw accel
+        val frameGrabber = requireNotNull(state.frameGrabber) { "Frame grabber not initialized!" }
+        var startTs = -1L
+        var lastDisplayFrameCount = 0L
+        var lastDecodedFrameCount = 0L
+        var lastTs = -1L
+        while (isActive) {
+            withFrameMillis { currentTs ->
+                if (startTs < 0) {
+                    startTs = currentTs
+                    lastTs = currentTs
+                }
+                if (state.seekPosition >= 0) {
+                    val seekMillis: Long = (stream.durationMillis * state.seekPosition.toDouble()).toLong()
+                    startTs = currentTs - seekMillis
+                    state.seekPosition = -1f
+                }
+                val pos = currentTs - startTs
+                frameGrabber.grabNextFrame(pos)
+                videoImage.value = frameGrabber.composeImage
+                state.time = pos
+                state.progress = (pos.toDouble() / stream.durationMillis.toDouble()).toFloat()
+                frame++
+                if (frame % 60 == 0) {
+                    val time = (currentTs - lastTs).toFloat() / 1000f
+                    val newDisplayFrameCount = frameGrabber.bitmapFrameCounter
+                    val displayFrameCount = newDisplayFrameCount - lastDisplayFrameCount
+                    state.displayFPS = displayFrameCount.toFloat() / time
+                    val newDecodedFrameCount = frameGrabber.decodedFrameCounter
+                    val decodedFrameCount = newDecodedFrameCount - lastDecodedFrameCount
+                    state.decodedFPS = decodedFrameCount.toFloat() / time
+                    lastDisplayFrameCount = newDisplayFrameCount
+                    lastDecodedFrameCount = newDecodedFrameCount
+                    lastTs = currentTs
+                }
+            }
+        }
+        state.stop()
+        state.close()
+    }
+    videoImage.value?.let { image ->
+        Spacer(modifier.drawBehind {
+            val scaleW = size.width / image.width.toFloat()
+            val scaleH = size.height / image.height.toFloat()
+            scale(
+                scale = min(scaleH, scaleW),
+                pivot = Offset.Zero
+            ) {
+                println("$image")
+                drawImage(image)
+            }
+        })
+    }
+}

+ 8 - 19
src/main/kotlin/eu/mjdev/desktop/components/video/VideoPlayerView.kt

@@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.Box
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.alpha
 import androidx.compose.ui.graphics.ColorFilter
 import androidx.compose.ui.layout.ContentScale
 import java.io.File
@@ -14,11 +13,11 @@ import java.io.File
 fun VideoPlayerView(
     modifier: Modifier = Modifier,
     src: Any?,
-    alignment: Alignment,
-    alpha: Float,
-    contentDescription: String?,
-    colorFilter: ColorFilter?,
-    contentScale: ContentScale,
+    alignment: Alignment = Alignment.Center,
+    alpha: Float = 1f,
+    contentDescription: String? = "",
+    colorFilter: ColorFilter? = null,
+    contentScale: ContentScale = ContentScale.Crop,
     onFail: (Throwable) -> Unit = { e -> e.printStackTrace() }
 ) = Box(
     modifier = modifier,
@@ -29,18 +28,8 @@ fun VideoPlayerView(
         is File -> src.absolutePath
         else -> src.toString()
     }
-    VideoPlayerDirect(
-        modifier = Modifier.alpha(alpha).align(alignment),
-        url = mediaUrl
+    VideoPlayerFFMpeg(
+        modifier = Modifier.align(alignment),
+        file = mediaUrl
     )
-//    runCatching {
-//        DesktopWebView(
-//            modifier = Modifier.alpha(alpha).align(alignment),
-//            url = mediaUrl
-//        ) {
-//            println("Loading Video: $it")
-//        }
-//    }.onFailure { e->
-//        onFail(e)
-//    }
 }