@@ -25,13 +25,15 @@ import org.freedesktop.gstreamer.Format
2525import org.freedesktop.gstreamer.event.SeekFlags
2626import org.freedesktop.gstreamer.event.SeekType
2727import org.freedesktop.gstreamer.message.MessageType
28+ import com.sun.jna.Pointer
2829import org.jetbrains.skia.Bitmap
2930import org.jetbrains.skia.ColorAlphaType
3031import org.jetbrains.skia.ColorType
3132import org.jetbrains.skia.ImageInfo
3233import java.awt.EventQueue
3334import java.io.File
3435import java.net.URI
36+ import java.nio.ByteBuffer
3537import java.util.EnumSet
3638import javax.swing.Timer
3739import kotlin.math.abs
@@ -71,6 +73,12 @@ class LinuxVideoPlayerState : PlatformVideoPlayerState {
7173 private var frameWidth = 0
7274 private var frameHeight = 0
7375
76+ // Double-buffering for zero-copy frame rendering
77+ private var skiaBitmapA: Bitmap ? = null
78+ private var skiaBitmapB: Bitmap ? = null
79+ private var nextSkiaBitmapA: Boolean = true
80+ private var lastFrameHash: Int = Int .MIN_VALUE
81+
7482 private var bufferingPercent by mutableStateOf(100 )
7583 private var isUserPaused by mutableStateOf(false )
7684 private var hasReceivedFirstFrame by mutableStateOf(false )
@@ -776,6 +784,7 @@ class LinuxVideoPlayerState : PlatformVideoPlayerState {
776784 _isSeeking = false
777785 hasReceivedFirstFrame = false
778786 _currentFrame = null
787+ lastFrameHash = Int .MIN_VALUE
779788 }
780789
781790 override fun seekTo (value : Float ) {
@@ -813,10 +822,16 @@ class LinuxVideoPlayerState : PlatformVideoPlayerState {
813822
814823 // ---- Processing of a video sample ----
815824 /* *
816- * Directly reads in RGBA and copies to a Skia Bitmap in the RGBA_8888 format
817- * (non-premultiplied). This avoids redundant conversions to maintain accurate colors and performance.
818- *
819- * Optimized for better performance, especially in fullscreen mode.
825+ * Zero-copy optimized frame processing using double-buffering and direct memory access.
826+ *
827+ * Optimizations applied:
828+ * 1. Double-buffering: Reuses two Bitmap objects, alternating between them to avoid
829+ * allocating new bitmaps every frame while the UI draws from the previous one.
830+ * 2. Frame hashing: Skips processing if the frame content hasn't changed (identical frames).
831+ * 3. peekPixels(): Direct access to Skia bitmap memory, avoiding intermediate ByteArray allocation.
832+ * 4. Single memory copy: GStreamer buffer → Skia bitmap pixels (true zero-copy beyond this necessary transfer).
833+ *
834+ * Memory flow: GStreamer native buffer → Skia bitmap pixels (1 copy via bulk ByteBuffer.put)
820835 */
821836 private fun processSample (sample : Sample ) {
822837 try {
@@ -826,40 +841,66 @@ class LinuxVideoPlayerState : PlatformVideoPlayerState {
826841 val width = structure.getInteger(" width" )
827842 val height = structure.getInteger(" height" )
828843
844+ if (width <= 0 || height <= 0 ) return
845+
846+ // Handle dimension changes
829847 if (width != frameWidth || height != frameHeight) {
830848 frameWidth = width
831849 frameHeight = height
850+
851+ // Reallocate bitmaps for new dimensions
852+ skiaBitmapA?.close()
853+ skiaBitmapB?.close()
854+
855+ val imageInfo = ImageInfo (width, height, ColorType .RGBA_8888 , ColorAlphaType .UNPREMUL )
856+ skiaBitmapA = Bitmap ().apply { allocPixels(imageInfo) }
857+ skiaBitmapB = Bitmap ().apply { allocPixels(imageInfo) }
858+ nextSkiaBitmapA = true
859+ lastFrameHash = Int .MIN_VALUE
860+
832861 updateAspectRatio()
833862 }
834863
835864 val buffer = sample.buffer ? : return
836- val byteBuffer = buffer.map(false ) ? : return
837- byteBuffer.rewind()
838-
839- // Prepare a Skia Bitmap
840- val imageInfo = ImageInfo (
841- width,
842- height,
843- ColorType .RGBA_8888 ,
844- ColorAlphaType .UNPREMUL
845- )
865+ val srcBuffer = buffer.map(false ) ? : return
866+ srcBuffer.rewind()
867+
868+ val pixelCount = width * height
846869
847- val bitmap = Bitmap ()
848- bitmap.allocPixels(imageInfo)
870+ // Calculate frame hash to detect identical frames
871+ val newHash = calculateFrameHash(srcBuffer, pixelCount)
872+ if (newHash == lastFrameHash) {
873+ buffer.unmap()
874+ return
875+ }
876+ lastFrameHash = newHash
877+
878+ // Select the target bitmap (double-buffering)
879+ val targetBitmap = if (nextSkiaBitmapA) skiaBitmapA!! else skiaBitmapB!!
880+ nextSkiaBitmapA = ! nextSkiaBitmapA
849881
850- // Get the byte array from the buffer directly
851- val totalBytes = width * height * 4
852- val byteArray = ByteArray (totalBytes)
882+ // Get direct access to bitmap pixels via peekPixels (zero-copy access)
883+ val pixmap = targetBitmap.peekPixels() ? : run {
884+ buffer.unmap()
885+ return
886+ }
853887
854- // Bulk copy the bytes from the buffer to the array
855- // This is much more efficient than copying pixel by pixel
856- byteBuffer.get(byteArray, 0 , totalBytes)
888+ val pixelsAddr = pixmap.addr
889+ if (pixelsAddr == 0L ) {
890+ buffer.unmap()
891+ return
892+ }
857893
858- // Install these pixels into the Bitmap
859- bitmap.installPixels(imageInfo, byteArray, width * 4 )
894+ // Single memory copy: GStreamer buffer → Skia bitmap
895+ val dstRowBytes = pixmap.rowBytes.toInt()
896+ val dstSizeBytes = dstRowBytes.toLong() * height.toLong()
897+ val dstBuffer = Pointer (pixelsAddr).getByteBuffer(0 , dstSizeBytes)
860898
861- // Convert the Skia Bitmap into a Compose ImageBitmap
862- val imageBitmap = bitmap.asComposeImageBitmap()
899+ srcBuffer.rewind()
900+ copyRgbaFrame(srcBuffer, dstBuffer, width, height, dstRowBytes)
901+
902+ // Convert to Compose ImageBitmap
903+ val imageBitmap = targetBitmap.asComposeImageBitmap()
863904
864905 // Update on the AWT thread
865906 EventQueue .invokeLater {
@@ -884,6 +925,14 @@ class LinuxVideoPlayerState : PlatformVideoPlayerState {
884925 playbin.stop()
885926 playbin.dispose()
886927 videoSink.dispose()
928+
929+ // Clean up double-buffering bitmaps
930+ skiaBitmapA?.close()
931+ skiaBitmapB?.close()
932+ skiaBitmapA = null
933+ skiaBitmapB = null
934+ lastFrameHash = Int .MIN_VALUE
935+
887936 // Don't call Gst.deinit() here as it would affect all instances
888937 // Each instance should only clean up its own resources
889938 }
0 commit comments