saket / telephoto

Building blocks for designing media experiences in Compose UI
https://saket.github.io/telephoto/
Apache License 2.0
970 stars 31 forks source link

IllegalStateException: getWidth called on recycled region decoder #95

Open RadoslawGorczyca opened 3 months ago

RadoslawGorczyca commented 3 months ago

I have a HorizontalPager of zoomable pictures. I noticed that quite often when you swipe from first picture to the next, then back to the first picture and try to double-tap to zoom, you get this crash. It happens even in other situations when scrolling through pager and zooming. Would really appreciate some help :)

Log:

Process: pl.bigstarltd.app.beta, PID: 30252
java.lang.IllegalStateException: getWidth called on recycled region decoder
    at android.graphics.BitmapRegionDecoder.checkRecycled(BitmapRegionDecoder.java:303)
    at android.graphics.BitmapRegionDecoder.getWidth(BitmapRegionDecoder.java:255)
    at me.saket.telephoto.subsamplingimage.internal.AndroidImageRegionDecoder.size-YEO4UFw(AndroidImageRegionDecoder.kt:80)
    at me.saket.telephoto.subsamplingimage.internal.AndroidImageRegionDecoder.decodeRegion(AndroidImageRegionDecoder.kt:50)
    at me.saket.telephoto.subsamplingimage.internal.PooledImageRegionDecoder$decodeRegion$2.invokeSuspend(PooledImageRegionDecoder.kt:25)
    at me.saket.telephoto.subsamplingimage.internal.PooledImageRegionDecoder$decodeRegion$2.invoke(Unknown Source:8)
    at me.saket.telephoto.subsamplingimage.internal.PooledImageRegionDecoder$decodeRegion$2.invoke(Unknown Source:4)
    at me.saket.telephoto.subsamplingimage.internal.ResourcePool.borrow(PooledImageRegionDecoder.kt:81)
    at me.saket.telephoto.subsamplingimage.internal.ResourcePool$borrow$1.invokeSuspend(Unknown Source:15)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
    at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:111)
    at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:99)
    at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:811)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:715)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:702)
    Suppressed: java.lang.IllegalStateException: getWidth called on recycled region decoder
        ... 17 more
    Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [androidx.compose.ui.platform.MotionDurationScaleImpl@d9f5671, androidx.compose.runtime.BroadcastFrameClock@5946c56, StandaloneCoroutine{Cancelling}@9b884d7, Dispatchers.IO]
saket commented 3 months ago

Are you able to reproduce this in telephoto's sample app? If not, please share a reproducer?

saket commented 3 months ago

I wonder if this is happening because I'm not clearing the reference to the decoder after it's recycled. Can you try out https://github.com/saket/telephoto/commit/4dd6effd3f0e86d308f7372b32155dd65b0f90a6 by using 0.13.0-SNAPSHOT?

RadoslawGorczyca commented 2 months ago

I can't use this version, I believe it is not published?

saket commented 2 months ago

You'll have to add maven's snapshot directory to your buildscript:

repositories {
  google()
  mavenCentral()
  maven("https://oss.sonatype.org/content/repositories/snapshots/")
}

If you cannot use snapshots, I'll publish a new version soon.

saket commented 2 months ago

Fixed in version 0.13.0: https://github.com/saket/telephoto/releases/tag/0.13.0. Please let me know if you still see any crashes?

RadoslawGorczyca commented 2 months ago

It seems to be fixed, thank you very much!

RadoslawGorczyca commented 2 months ago

@saket Unfortunately this bug still remains, with the same log.

Process: pl.bigstarltd.app.beta, PID: 7685
java.lang.IllegalStateException: getWidth called on recycled region decoder
    at android.graphics.BitmapRegionDecoder.checkRecycled(BitmapRegionDecoder.java:303)
    at android.graphics.BitmapRegionDecoder.getWidth(BitmapRegionDecoder.java:255)
    at me.saket.telephoto.subsamplingimage.internal.AndroidImageRegionDecoder.size-YEO4UFw(AndroidImageRegionDecoder.kt:80)
    at me.saket.telephoto.subsamplingimage.internal.AndroidImageRegionDecoder.decodeRegion(AndroidImageRegionDecoder.kt:50)
    at me.saket.telephoto.subsamplingimage.internal.PooledImageRegionDecoder$decodeRegion$2.invokeSuspend(PooledImageRegionDecoder.kt:25)
    at me.saket.telephoto.subsamplingimage.internal.PooledImageRegionDecoder$decodeRegion$2.invoke(Unknown Source:8)
    at me.saket.telephoto.subsamplingimage.internal.PooledImageRegionDecoder$decodeRegion$2.invoke(Unknown Source:4)
    at me.saket.telephoto.subsamplingimage.internal.ResourcePool.borrow(PooledImageRegionDecoder.kt:81)
    at me.saket.telephoto.subsamplingimage.internal.ResourcePool$borrow$1.invokeSuspend(Unknown Source:15)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
    at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:111)
    at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:99)
    at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:811)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:715)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:702)
    Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [androidx.compose.ui.platform.MotionDurationScaleImpl@e4b71ba, androidx.compose.runtime.BroadcastFrameClock@2a2486b, StandaloneCoroutine{Cancelling}@eeb9fc8, Dispatchers.IO]
saket commented 1 month ago

Looks like I have a race condition in BitmapCache. In the meantime, are you setting beyondBoundsPageCount to a non-zero value in your pager?

RadoslawGorczyca commented 1 month ago

I do not

HorizontalPager(
            state = pagerState,
            modifier = Modifier.fillMaxSize(),
            userScrollEnabled = !zoomed,
        ) {
            val page = mod(it, pagesCount)
            Box(
                modifier = Modifier.fillMaxSize(),
            ) {
                when {
                    page < images.size || video == null -> imageComposable(page)
                    else -> VideoPlayer(
                        url = video,
                        modifier = Modifier.fillMaxSize(),
                    )
                }
            }
        } 
saket commented 1 month ago

I'd suggest setting it to 1 if possible. It'll workaround this issue and offer a better scrolling experience to your users.

RadoslawGorczyca commented 1 month ago
HorizontalPager(
            state = pagerState,
            modifier = Modifier.fillMaxSize(),
            userScrollEnabled = !zoomed,
            beyondViewportPageCount = 1,
        ) {
            val page = mod(it, pagesCount)
            Box(
                modifier = Modifier.fillMaxSize(),
            ) {
                when {
                    page < images.size || video == null -> imageComposable(page)
                    else -> VideoPlayer(
                        url = video,
                        modifier = Modifier.fillMaxSize(),
                    )
                }
            }
        }

I was able to reproduce the same error multiple times with your suggested change.

saket commented 1 month ago

🪦

I wonder what is different about your setup that's causing this issue vs the sample app. Can you help me find the differences so that I can write a regression test?

saket commented 1 month ago

Additionally, a video would also be helpful.

RadoslawGorczyca commented 1 month ago

I don't see major differences between implementations. I'm sorry, but it's extremely hard to record video of it, trying to reproduce the bug and avoid content hidden under NDA. Tried it for longer than I have time for, I'm sorry. But as I described it - you enter screen that have nothing more than fullscreen HorizontalPager with few pages which contains only image and indicators. You scroll to the second picture, then you scroll back to the first one and double tap it. Once in a while it crashes with said bug.

latsson commented 2 weeks ago

I also experience this issue with 0.13.0, also inside a HorizontalPager

saket commented 2 weeks ago

This should be fixed with https://github.com/saket/telephoto/commit/ab1b5b51a3f7bb344f4dc3bf4db9af8f69aeccd0. I'll make a release soon!