@@ -23,6 +23,9 @@ import androidx.compose.ui.graphics.asComposeImageBitmap
2323import androidx.compose.ui.layout.ContentScale
2424import androidx.compose.ui.unit.IntSize
2525import com.sun.jna.Pointer
26+ import com.tunjid.heron.data.logging.LogPriority
27+ import com.tunjid.heron.data.logging.logcat
28+ import com.tunjid.heron.data.logging.loggableText
2629import com.tunjid.heron.media.video.PlayerStatus
2730import com.tunjid.heron.media.video.VideoPlayerState
2831import com.tunjid.heron.ui.shapes.RoundedPolygonShape
@@ -98,8 +101,7 @@ internal class AVFoundationPlayerState(
98101
99102 // Frame rendering state
100103 internal var currentFrame by mutableStateOf<ImageBitmap ?>(null )
101- private var skiaBitmapWidth: Int = 0
102- private var skiaBitmapHeight: Int = 0
104+ private var currentFrameSize = IntSize .Zero
103105 private var skiaBitmapA: Bitmap ? = null
104106 private var skiaBitmapB: Bitmap ? = null
105107 private var nextSkiaBitmapA: Boolean = true
@@ -236,64 +238,71 @@ internal class AVFoundationPlayerState(
236238 private suspend fun updateFrameAsync () {
237239 val ptr = playerPointer ? : return
238240
239- val width = AVFoundationVideoPlayer .getFrameWidth(ptr)
240- val height = AVFoundationVideoPlayer .getFrameHeight(ptr)
241- if (width <= 0 || height <= 0 ) return
242-
243- val framePtr = AVFoundationVideoPlayer .getLatestFrame(ptr) ? : return
244-
245- val pixelCount = width * height
246- val frameSizeBytes = pixelCount.toLong() * 4L
247-
248- withContext(serialDispatcher) {
249- val srcBuf = framePtr.getByteBuffer(0 , frameSizeBytes)
250-
251- if (skiaBitmapA == null || skiaBitmapWidth != width || skiaBitmapHeight != height) {
252- skiaBitmapA?.close()
253- skiaBitmapB?.close()
254-
255- val imageInfo = ImageInfo (
256- width = width,
257- height = height,
258- colorType = ColorType .BGRA_8888 ,
259- alphaType = ColorAlphaType .OPAQUE ,
260- )
261- skiaBitmapA = Bitmap ().apply { allocPixels(imageInfo) }
262- skiaBitmapB = Bitmap ().apply { allocPixels(imageInfo) }
263- skiaBitmapWidth = width
264- skiaBitmapHeight = height
265- nextSkiaBitmapA = true
266- }
267-
268- val targetBitmap =
269- if (nextSkiaBitmapA) requireNotNull(skiaBitmapA)
270- else requireNotNull(skiaBitmapB)
271-
272- nextSkiaBitmapA = ! nextSkiaBitmapA
273-
274- val pixmap = targetBitmap.peekPixels() ? : return @withContext
275- val pixelsAddr = pixmap.addr
276- if (pixelsAddr == 0L ) return @withContext
277-
278- srcBuf.rewind()
279- val destRowBytes = pixmap.rowBytes
280- val destSizeBytes = destRowBytes.toLong() * height.toLong()
281- val destBuf = Pointer (pixelsAddr).getByteBuffer(0 , destSizeBytes)
282- copyBgraFrame(
283- src = srcBuf,
284- dst = destBuf,
285- width = width,
286- height = height,
287- dstRowBytes = destRowBytes,
241+ try {
242+ val latestFrameSize = IntSize (
243+ width = AVFoundationVideoPlayer .getFrameWidth(ptr),
244+ height = AVFoundationVideoPlayer .getFrameHeight(ptr),
288245 )
246+ if (latestFrameSize.width <= 0 || latestFrameSize.height <= 0 ) return
247+
248+ val framePtr = AVFoundationVideoPlayer .getLatestFrame(ptr) ? : return
249+
250+ val pixelCount = latestFrameSize.width * latestFrameSize.height
251+ val frameSizeBytes = pixelCount.toLong() * 4L
252+
253+ withContext(serialDispatcher) {
254+ val srcBuf = framePtr.getByteBuffer(0 , frameSizeBytes)
255+
256+ if (skiaBitmapA == null || currentFrameSize != latestFrameSize) {
257+ skiaBitmapA?.close()
258+ skiaBitmapB?.close()
259+
260+ val imageInfo = ImageInfo (
261+ width = latestFrameSize.width,
262+ height = latestFrameSize.height,
263+ colorType = ColorType .BGRA_8888 ,
264+ alphaType = ColorAlphaType .OPAQUE ,
265+ )
266+ skiaBitmapA = Bitmap ().apply { allocPixels(imageInfo) }
267+ skiaBitmapB = Bitmap ().apply { allocPixels(imageInfo) }
268+ currentFrameSize = latestFrameSize
269+ nextSkiaBitmapA = true
270+ }
271+
272+ val targetBitmap =
273+ if (nextSkiaBitmapA) requireNotNull(skiaBitmapA)
274+ else requireNotNull(skiaBitmapB)
275+
276+ nextSkiaBitmapA = ! nextSkiaBitmapA
277+
278+ val pixmap = targetBitmap.peekPixels() ? : return @withContext
279+ val pixelsAddr = pixmap.addr
280+ if (pixelsAddr == 0L ) return @withContext
281+
282+ srcBuf.rewind()
283+ val destRowBytes = pixmap.rowBytes
284+ val destSizeBytes = destRowBytes.toLong() * latestFrameSize.height.toLong()
285+ val destBuf = Pointer (pixelsAddr).getByteBuffer(0 , destSizeBytes)
286+ copyBgraFrame(
287+ src = srcBuf,
288+ dst = destBuf,
289+ width = latestFrameSize.width,
290+ height = latestFrameSize.height,
291+ dstRowBytes = destRowBytes,
292+ )
289293
290- currentFrame = targetBitmap.asComposeImageBitmap()
291- }
294+ currentFrame = targetBitmap.asComposeImageBitmap()
295+ }
292296
293- lastFrameUpdateTime = System .currentTimeMillis()
297+ lastFrameUpdateTime = System .currentTimeMillis()
294298
295- if (! hasRenderedFirstFrame) {
296- hasRenderedFirstFrame = true
299+ if (! hasRenderedFirstFrame) {
300+ hasRenderedFirstFrame = true
301+ }
302+ } catch (e: Exception ) {
303+ logcat(LogPriority .ERROR ) {
304+ " Video frame extraction failed: ${e.loggableText()} "
305+ }
297306 }
298307 }
299308
@@ -318,8 +327,7 @@ internal class AVFoundationPlayerState(
318327 skiaBitmapB?.close()
319328 skiaBitmapA = null
320329 skiaBitmapB = null
321- skiaBitmapWidth = 0
322- skiaBitmapHeight = 0
330+ currentFrameSize = IntSize .Zero
323331 nextSkiaBitmapA = true
324332
325333 pointerToDispose?.let (AVFoundationVideoPlayer ::disposeVideoPlayer)
0 commit comments