tvOS 15.2 regression: Crashes when changing the player instance associated with an AVPlayerViewController

Number:rdar://FB9938231 Date Originated:Mar 2, 2022
Status:Open Resolved:
Product:AVKit Product Version:15.2
Classification:Crash Reproducible:Always

`AVPlayerViewController` provides a mutable property so that implementations can switch the associated `player` instance on-the-fly when the user picks another content (e.g. with a dedicated user interface presented as info tab).

This is what our code does, with a twist: For some reason not relevant to the present discussion we go through an intermediate "reset" state which means the new player instance is not installed right away, but that it is briefly set to `nil` before being set to the new instance:

playerViewController.player = AVPlayer(url: url1)
// .... and a while later
playerViewController.player = nil
playerViewController.player = AVPlayer(url: url2)

This was working fine until tvOS 15.2 but, for reasons likely related to internal key-value observation of interstitials, which I assume was incorrectly updated in tvOS 15.2, changing the player several times in a row crashes with an error logged to the console:

void * _Nullable NSMapGet(NSMapTable * _Nonnull, const void * _Nullable): map table argument is NULL

and a KVO crash (`KVO_IS_RETAINING_ALL_OBSERVERS_OF_THIS_OBJECT_IF_IT_CRASHES_AN_OBSERVER_WAS_OVERRELEASED_OR_SMASHED`) involving `AVInterstitialController`, whose stack trace is the following:

thread #1, queue = '', stop reason = EXC_BAD_ACCESS (code=1, address=0x1c)
    frame #0: 0x00000001800a35bc libobjc.A.dylib`object_isClass + 16
    frame #2: 0x0000000181253d60 Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:] + 268
    frame #3: 0x000000018125461c Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:] + 68
    frame #4: 0x000000018124d1d4 Foundation`_NSSetObjectValueAndNotify + 284
    frame #5: 0x0000000194248a44 AVKit`-[AVInterstitialController dealloc] + 32
    frame #6: 0x00000001800c0954 libobjc.A.dylib`AutoreleasePoolPage::releaseUntil(objc_object**) + 204
    frame #7: 0x00000001800c0824 libobjc.A.dylib`objc_autoreleasePoolPop + 236
    frame #8: 0x0000000180745e10 CoreFoundation`_CFAutoreleasePoolPop + 28
    frame #9: 0x00000001806a470c CoreFoundation`__CFRunLoopPerCalloutARPEnd + 44
    frame #10: 0x000000018069f9c4 CoreFoundation`__CFRunLoopRun + 2516
    frame #11: 0x000000018069eae4 CoreFoundation`CFRunLoopRunSpecific + 572
    frame #12: 0x00000001835bd5ec GraphicsServices`GSEventRunModal + 160
    frame #13: 0x00000001adc2eac4 UIKitCore`-[UIApplication _run] + 992
    frame #14: 0x00000001adc335e0 UIKitCore`UIApplicationMain + 112
    frame #15: 0x0000000198451e88 libswiftUIKit.dylib`UIKit.UIApplicationMain(Swift.Int32, Swift.Optional<Swift.UnsafeMutablePointer<Swift.UnsafeMutablePointer<Swift.Int8>>>, Swift.Optional<Swift.String>, Swift.Optional<Swift.String>) -> Swift.Int32 + 100
    frame #16: 0x00000001009e8564 InterstitialsCrash`static UIApplicationDelegate.main() at <compiler-generated>:0
  * frame #17: 0x00000001009e84ec InterstitialsCrash`static AppDelegate.$main(self=InterstitialsCrash.AppDelegate) at AppDelegate.swift:3:1
    frame #18: 0x00000001009e85e4 InterstitialsCrash`main at <compiler-generated>:0
    frame #19: 0x0000000100c0dca0 dyld_sim`start_sim + 20
    frame #20: 0x0000000100d990f4 dyld`start + 520

Note that, though the crash is likely related to KVO of interstitials, no interstitials actually have to be set on any `AVPlayerItem` for the crash to happen. Moreover, since interstitials are a tvOS feature, the crash does not happen if the same code is run on iOS. Starting with iOS 15.0, though, the replaced player instance seems to leak, as the sound of both medias can be heard at the same time, an issue not experienced on iOS 14. This might be a related matter but likely requires a separate investigation on its own.

Remark: I could only test the behavior on 15.0, 15.2 and 15.4 due to a lack of test devices or simulators. I assume the crash was introduced in 15.2, but it might have been introduced in 15.1.

How to reproduce the problem

I attached a simple sample project (also available at which:

- Instantiates an `AVPlayerViewController` and plays some content with an `AVPlayer`.
- After 3 seconds switches the `AVPlayer` instance with another one, but in two steps (first set to `nil`, then immediately to the new instance).

To reproduce the issue:

1. Build and run the sample project on tvOS 15.2 (tested with device and M1 simulator in my case).
2. The app should crash after 3 seconds when attempting to play the new content. If not please run the app again, as the crash might rarely not happen.

If you do the same with tvOS 14, 15.0 or if the intermediate `playerViewController.player = nil` step is commented out in the code, you can observe that the crash never occurs.

Remark: The project can similarly be run on iOS to reproduce the leak issue I mention above.

Expected results

No crash occurs when the `AVPlayerViewController` player instance is updated, no matter how.

Actual results

A crash occurs when the `AVPlayerViewController` player instance is updated several times in a row.


We have currently ensured our implementation does not unnecessarily update the `AVPlayerViewController` instance several times in a row. It is also likely (but impractical for us at the moment) that using a single `AVQueuePlayer` instance to manage playback of several items could avoid this issue.


Please note: Reports posted here will not necessarily be seen by Apple. All problems should be submitted at before they are posted here. Please only post information for Radars that you have filed yourself, and please do not include Apple confidential information in your posts. Thank you!