expo icon indicating copy to clipboard operation
expo copied to clipboard

expo-image stopAnimating() with AVIF does not work on iOS

Open andreialecu opened this issue 1 year ago • 15 comments

Minimal reproducible example

https://snack.expo.dev/-nigAhGm4zHjdMK3opUrW

What platform(s) does this occur on?

iOS

Where did you reproduce the issue?

in a development build, in Expo Go, in a standalone app

Summary

I believe this is an upstream bug in SDWebImage. I'll open an issue there too.

Environment

expo-env-info 1.2.0 environment info: System: OS: macOS 14.6.1 Shell: 5.9 - /bin/zsh Binaries: Node: 20.11.1 - ~/.nvm/versions/node/v20.11.1/bin/node Yarn: 4.4.0 - ~/.nvm/versions/node/v20.11.1/bin/yarn npm: 10.2.4 - ~/.nvm/versions/node/v20.11.1/bin/npm Watchman: 2024.07.15.00 - /opt/homebrew/bin/watchman SDKs: iOS SDK: Platforms: DriverKit 23.5, iOS 17.5, macOS 14.5, tvOS 17.5, visionOS 1.2, watchOS 10.5 Android SDK: API Levels: 28, 29, 30, 31, 33, 34 Build Tools: 28.0.3, 29.0.2, 29.0.3, 30.0.2, 30.0.3, 31.0.0, 32.0.0, 33.0.0, 33.0.1, 34.0.0 System Images: android-30 | Google APIs ARM 64 v8a, android-30 | Google APIs Intel x86 Atom, android-30 | Google Play ARM 64 v8a, android-32 | Google APIs ARM 64 v8a, android-33 | Google APIs ARM 64 v8a, android-34 | Google APIs ARM 64 v8a IDEs: Android Studio: 2022.3 AI-223.8836.35.2231.11090377 Xcode: 15.4/15F31d - /usr/bin/xcodebuild npmPackages: expo: ^51.0.17 => 51.0.17 react: 18.2.0 => 18.2.0 react-native: ^0.74.2 => 0.74.3 npmGlobalPackages: eas-cli: 9.1.0 Expo Workflow: bare

Expo Doctor Diagnostics

irrelevant

andreialecu avatar Aug 26 '24 09:08 andreialecu

Upstream report: https://github.com/SDWebImage/SDWebImage/issues/3745

andreialecu avatar Aug 26 '24 09:08 andreialecu

Can you have a test with APNG/GIF/AWebP other format's behavior as well? Example URL can be found here:

webp: http://littlesvr.ca/apng/images/SteamEngine.webp apng: http://apng.onevcat.com/assets/elephant.png gif: http://assets.sbnation.com/assets/2512203/dogflops.gif heics: https://nokiatech.github.io/heif/content/image_sequences/starfield_animation.heic

dreampiggy avatar Aug 27 '24 07:08 dreampiggy

@dreampiggy updated snack with other formats: https://snack.expo.dev/0HsEdxNWBI7Lzk2Wb2BH3

Everything seems fine except AVIF.

Video below:

https://github.com/user-attachments/assets/afa2e996-30b2-4b85-b1e8-71a1edd3c0e2

andreialecu avatar Aug 27 '24 08:08 andreialecu

Is the AVIF coder generated the SDAnimatedImage or _UIAnimatedImage (UIKit image ?)

Can you attach a debugger to print the object ? Print a breakpoint at animatedImageFrameAtIndex: and check return's object

_UIAnimatedImage it's Apple's own animation solution, which has some known limitations. See: https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#rendering

(Note UIImage/NSImage also can represent an animated one using animatedImageWithImages:duration: API)

dreampiggy avatar Aug 27 '24 08:08 dreampiggy

Will try to check that, perhaps this is relevant?

https://github.com/expo/expo/blob/1fe71d007c5c5aab676e46257a90727483543bc5/packages/expo-image/ios/AnimatedImage.swift

andreialecu avatar Aug 27 '24 08:08 andreialecu

On a side note, I noticed that having 3 AVIF animations simultaneously completely wrecks app performance on an iPhone 15 Pro Max. I'm not sure if that's what is expected.

andreialecu avatar Aug 27 '24 08:08 andreialecu

Update: It appears that if I download the snack locally and run it in the iOS simulator, the animation stops properly.

https://github.com/user-attachments/assets/35cd1b8c-d28e-4244-b94f-41af821f7412

I initially discovered this issue in our real app, and the Snack confirmed it. I'm not sure why downloading it locally makes the snack work, while it doesn't work via the iOS web simulator at snack.expo.dev.

Investigating...

andreialecu avatar Aug 27 '24 08:08 andreialecu

I suspected there's some interaction with React Navigation so I made a new Snack with it.

Some very bizarre findings here:

https://snack.expo.dev/Sf0Ja0uTG0fDY_R8BU2FI

It appears that when the images are inside react-navigation, the stop/start animation behaves erratically. There are also some messages logged in the Snack:

iPhone 15 Pro:
Unhandled promise rejection Error: "Calling the 'startAnimating' function has failed
→ Caused by: The 1st argument cannot be cast to type View<ImageView>
→ Caused by: Unable to find the 'ImageView' view with tag '1597'" in Error: Calling the 'startAnimating' function has failed << → Caused by: The 1st argument cannot be cast to type View<ImageView> << → Caused by: Unable to find the 'ImageView' view with tag '1597'

In our real app, we tried to stop animations on background screens and reenable them when the screen was focused. I found that .stopAnimation() doesn't do anything if the image is on an inactive screen - it will continue playing and cannot be paused.

Perhaps this is indeed some Expo Image bug? Hopefully, the Snack above should help investigate.

Video:

https://github.com/user-attachments/assets/9ddd12fd-d5e4-48e1-aaa5-b8540d2a5a81

This issue also occurs on Android, so it cannot be caused by SDWebImage there.

andreialecu avatar Aug 27 '24 09:08 andreialecu

Need a real demo (upload all contents including the Pods folder), you can upload to some CDN server like AmazonS3/OneDrive/GoogleDrive

Because for me, the expo is the downstream dependencies, it's hard to set up the development environment to debug.

I guess this related some AVIFCoder logic which breaks the SDWebImage's animation design.

(But the strange thing is, AVIFCoder has its own demo, and it shows great without any issue, including start/stop)

See: 1.mov.zip

dreampiggy avatar Aug 27 '24 09:08 dreampiggy

Did expo register the SDWebImageAVIFCoder ? This is not the default coder

Note, from iOS 17, Apple system decoder can render AVIF as well.

dreampiggy avatar Aug 27 '24 09:08 dreampiggy

@dreampiggy yes, I believe your library is probably working correctly here. As I mentioned in a previous comment, downloading the source and running the code locally with an XCode compiled project works. It's only in Expo Go where it misbehaves and doesn't stop the AVIF. There's probably some old dependency in Expo Go that was buggy, but latest SDWebImage seems fine.

I'll close the issue on your side while we wait for an Expo maintainer to chip in.

andreialecu avatar Aug 27 '24 09:08 andreialecu

Thank you for filing this issue! This comment acknowledges we believe this may be a bug and there’s enough information to investigate it. However, we can’t promise any sort of timeline for resolution. We prioritize issues based on severity, breadth of impact, and alignment with our roadmap. If you’d like to help move it more quickly, you can continue to investigate it more deeply and/or you can open a pull request that fixes the cause.

expo-bot avatar Aug 27 '24 17:08 expo-bot

can confirm this issue on ios dev-build, it's not just expo-go and should be something inside expo-image. it could relate to our customized SDWebImageContextAnimatedImageClass. not sure whether avif coder supports it. will try to investigate further but temporarily assign to @tsapeta for some spotlight.

Kudo avatar Aug 27 '24 17:08 Kudo

Hey @Kudo, thanks for looking at this!

I reported a bunch of issues, one of which also happens on Android (the react navigation one)

In my case I was not able to reproduce the main AVIF issue in a full dev build. I shared a video where it seems to work properly.

So I was wondering which one you were referring to.

andreialecu avatar Aug 27 '24 17:08 andreialecu

SDWebImageContextAnimatedImageClass

For https://github.com/SDWebImage/SDWebImageAVIFCoder, the latest version 0.11.0 supports the SDAnimatedImage one. It should just behaves like the other custom coder including SDWebImageWebPCoder

I guess there are something other cases which cause the issue.

dreampiggy avatar Aug 28 '24 06:08 dreampiggy

@andreialecu i was looking for the first issue that stopAnimating doesn't work on AVIF (ios only).

having some findings today for the issue. it was because our implementation for downscaling. SDAnimatedImageView seems not well support UIImage.animatedImage. when render the image, SDAnimatedImageView will not create a player. disable downscale will workaround the issue.

for the solution side, i could fix the issue by converting UIImage.animatedImage to GIF Data and create SDAnimatedImage(data:). not sure whether there are any better solutions to resize a SDAnimatedImage? cc @dreampiggy if you have more thought for this.

Kudo avatar Aug 28 '24 15:08 Kudo

In our app we already disable downscaling, but we cannot stop the animation. Probably because of react navigation. See my other snack.

We use long avifs, like up to a minute. HD videos. I found that the file size is as good as mp4 or better.

Or is it not recommended to do so, and should instead use expo-video?

I did notice some performance issues so I wanted to work around them by pausing the animation on unfocused screens.

In this case, downscaling, especially to gif, would be a bad idea.

andreialecu avatar Aug 28 '24 18:08 andreialecu

SDAnimatedImageView seems not well support UIImage.animatedImage

It should support. In my initial design. When called with UIAnimatedImage, it should always call super (UIImageView itself) implementation.

Is this cause the issue during the recently refractory ? I can have a test on myself, wait for my reply.

dreampiggy avatar Aug 29 '24 08:08 dreampiggy

Well, reproduced.

I tested SDWebImage's own demo, it can use SDAnimatedImageView with UIAnimatedImage. But yes, the animation start/stop function totally broken.

image

Let me fix this firstly

dreampiggy avatar Aug 29 '24 08:08 dreampiggy

Interesting, UIImageView seems to report self.isAnimating as false, even it's visible animating...🤔

image image

UIKitCore`-[UIImageView _hasInstalledContentsAnimation]:
->  0x10f3e1610 <+0>:   stp    x22, x21, [sp, #-0x30]!
    0x10f3e1614 <+4>:   stp    x20, x19, [sp, #0x10]
    0x10f3e1618 <+8>:   stp    x29, x30, [sp, #0x20]
    0x10f3e161c <+12>:  add    x29, sp, #0x20
    0x10f3e1620 <+16>:  mov    x20, x0
    0x10f3e1624 <+20>:  bl     0x10fbfb320               ; objc_msgSend$layer
    0x10f3e1628 <+24>:  bl     0x10f4e8f18               ; symbol stub for: objc_claimAutoreleasedReturnValue
    0x10f3e162c <+28>:  mov    x21, x0
    0x10f3e1630 <+32>:  adrp   x2, 2342
    0x10f3e1634 <+36>:  add    x2, x2, #0x128            ; @"contents"
    0x10f3e1638 <+40>:  bl     0x10fb95980               ; objc_msgSend$animationForKey:
    0x10f3e163c <+44>:  bl     0x10f4e8f18               ; symbol stub for: objc_claimAutoreleasedReturnValue
    0x10f3e1640 <+48>:  mov    x19, x0
    0x10f3e1644 <+52>:  bl     0x10f4e9098               ; symbol stub for: objc_release_x21
    0x10f3e1648 <+56>:  mov    x0, x19
    0x10f3e164c <+60>:  bl     0x10fbbad60               ; objc_msgSend$delegate
    0x10f3e1650 <+64>:  bl     0x10f4e8f18               ; symbol stub for: objc_claimAutoreleasedReturnValue
    0x10f3e1654 <+68>:  mov    w21, #0x0
    0x10f3e1658 <+72>:  cbz    x19, 0x10f3e1674          ; <+100>
    0x10f3e165c <+76>:  cbz    x0, 0x10f3e1674           ; <+100>
    0x10f3e1660 <+80>:  adrp   x8, 3215
    0x10f3e1664 <+84>:  ldrsw  x8, [x8, #0xf20]
    0x10f3e1668 <+88>:  ldr    x8, [x20, x8]
    0x10f3e166c <+92>:  cmp    x0, x8
    0x10f3e1670 <+96>:  cset   w21, eq
    0x10f3e1674 <+100>: bl     0x10f4e9050               ; symbol stub for: objc_release
    0x10f3e1678 <+104>: bl     0x10f4e9074               ; symbol stub for: objc_release_x19
    0x10f3e167c <+108>: mov    x0, x21
    0x10f3e1680 <+112>: ldp    x29, x30, [sp, #0x20]
    0x10f3e1684 <+116>: ldp    x20, x19, [sp, #0x10]
    0x10f3e1688 <+120>: ldp    x22, x21, [sp], #0x30
    0x10f3e168c <+124>: ret    

--- Debugging Disaeembler ---

-(BOOL)hasInstalledContentsAnimation {
    CALayer *layer = self.layer;
    CAAnimation *animation = [layer animationForKey:@"contents"]; // nil
    id<CAAnimationDelegate> animationDelegate = [animation delegate]; // nil
    if (!animation) {
        // goto 0x10f255674
        return NO;
    } else {
        
    }
    if (!animationDelegate) {
        // goto 0x10f255674
        return NO;
    }
}

Seems the [SDAniamtedImageView.layer animationForKey:@"contents"] returns nil, which cause isAnimating status wrong.

dreampiggy avatar Aug 29 '24 08:08 dreampiggy

There are something internal changes after iOS 16.3

https://www.developer.limneos.net/?ios=16.3&framework=UIKitCore.framework&header=UIImageView.h

@Kudo Do you have iOS 15 devices to test again ? I guess this should behave correctly on iOS 15, but broken on iOS 16+ (Since we inherited UIImageView, some UIKit team internal changes cause the issue)

dreampiggy avatar Aug 29 '24 08:08 dreampiggy

Is this UIImageView's (UIKit team) 's bug ?

I found even use UIAnimatedImage and assign to UIImageView, you can not stop the animation as well.

Not just SDAnimatedImageView😣

dreampiggy avatar Aug 29 '24 09:08 dreampiggy

How come everything stops correctly here?

https://github.com/expo/expo/issues/31176#issuecomment-2311870239

However as soon as the image is put in a navigation container it does indeed stop working.

andreialecu avatar Aug 29 '24 09:08 andreialecu

How come everything stops correctly here?

Only your AVIF coder use _UIAnimatedImage, others you can debug, which use SDAnimatedImage

These are two different thing. The SDAnimatedImage based animation is implemented by ourselves. Do not use any UIKit solution

See: https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#rendering

When the image is a UIImage/NSImage, it will call super method instead to behave like a normal UIImageView/NSImageView (Note UIImage/NSImage also can represent an animated one using animatedImageWithImages:duration: API)

dreampiggy avatar Aug 29 '24 09:08 dreampiggy

Try yourself. This is Apple's bug, not SDWebImage's bug. UIImageView's stopAnimating is broken

UIImageViewStopAnimatingBug.zip

See: https://github.com/dreampiggy/UIImageViewStopAnimatingBug/

radar ID: FB14976073

dreampiggy avatar Aug 29 '24 09:08 dreampiggy

@Kudo A stupid workaround if you really really want UIAnimatedImage image

But why not use SDAnimatedImage ? It supports downscale as well. Try pass @{SDWebImageContextImageThumbnailPixelSize: @(CGSizeMake(100, 100))} to see what happends.

Or if you don't want to use SDWebImage's own loading system, you can create SDAnimatedImage with the API initWithData:scale:options: or initWithAnimatedCoder:options, pass @{SDImageCoderDecodeThumbnailPixelSize: @(CGSizeMake(100, 100)} in options arg.

Note: Always register the AVIFCoder before you create SDAnimatedImage: https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#coder-usage

image

dreampiggy avatar Aug 30 '24 08:08 dreampiggy

A question regarding downscaling. As mentioned we were trying to use this with long AVIFs. HD and up to a minute and maybe longer.

The downscaling wrecks performance and is unusable. I assume the only solution is to disable it, right? What would be the recommendations there?

Or should we use a video player component instead?

andreialecu avatar Aug 30 '24 10:08 andreialecu

@andreialecu AVIF is just AV1 video. Stored the bitstream into HEIF container (container is like a zip wrapper, not about its contents)

Use a native video player (Apple supports AV1 from iOS 17) or ffmpeg based player is always the best choice

dreampiggy avatar Aug 30 '24 12:08 dreampiggy

I see, we just wanted a simpler solution because we wanted to be able to support avif, webp and other formats with the same code.

So SDWebImage is not entirely suited for long form avif playback, correct?

andreialecu avatar Aug 30 '24 13:08 andreialecu

So SDWebImage is not entirely suited for long form avif playback, correct?

No huge difference between any image format. Actually

Seems out of topic, sorry to reply with some personal idea, if this annoys someone

All animated inage which are long than minutes are always bad for customers. Whatever the format. Which will drain your customers's bandwidth/ battery and disk storage.

This is why all of media social App like Telegram or https://giphy.com/ convert you image to video (on server-side use ffmepg) and sent it to browser or client to render.

I have no knowledge about some small App's arch model and server impl detail, but it's always a bad idea to use "image" to do the thing which "video" is designed for. Animated image was created for short/small resolution sequences, but not for short video with large resolution or long videos

The best choice is to use server to check if the image is longer enough, then convert it into video. Only render small images on the client use Image Technology

dreampiggy avatar Aug 31 '24 04:08 dreampiggy