SRGSSR / pillarbox-apple

A next-generation reactive media playback ecosystem for Apple platforms.
https://testflight.apple.com/join/TS6ngLqf
MIT License
54 stars 8 forks source link

Now playing session time updates #206

Closed defagos closed 1 year ago

defagos commented 1 year ago

As a user I want to be able to know the current / remaining time from the control center. I want to be able to seek in the content as well.

Acceptance criteria

Out of scope

Possible slider hiccups will be addressed later.

Tasks

defagos commented 1 year ago

We stumbled upon a subtle issue when polishing our implementation.

Issue and how to reproduce

Sometimes the control center was not displaying any metadata, in particular when proceeding as follows (reproduced on 16.2 and 16.3 devices, but likely affecting older versions as well):

  1. Open the empty demo.
  2. Add a URN-based content (e.g. Telegiornale).
  3. Check the control center. Metadata should be visible.
  4. Clear the playlist, then add the same content back. It is very likely that no metadata is now visible in the control center, otherwise repeat this step as needed.

Investigations

We observed that this issue only affects URN-based content. We can investigate what happens in the metadata delivery we implemented in this issue as follows:

func nowPlayingInfoMetadataPublisher() -> AnyPublisher<NowPlaying.Info?, Never> {
    currentPublisher()
        .map { current in
            guard let current else {
                return Just(Optional<NowPlaying.Info>.none).eraseToAnyPublisher()
            }
            return current.item.$source
                .print("--> source")
                .map { $0.asset.nowPlayingInfo() }
                .eraseToAnyPublisher()
        }
        .switchToLatest()
        .print("--> info")
        .eraseToAnyPublisher()
}

The first time info metadata is correct and the log looks as follows:

--> source will be updated: receive subscription: (Concatenate)
--> source will be updated: request unlimited
--> source will be updated: receive value: (Source(id: 34211598-CE53-4D0D-BB37-5D64B0D72307, asset: Player.Asset(type: Player.Asset.(unknown context at $10493f6ac).Type.custom(url: pillarbox://loading.m3u8, delegate: <Player.LoadingResourceLoaderDelegate: 0x280f78ea0>), metadata: nil, configuration: (Function))))
--> source (0x0000000283f71440: receive subscription: (PublishedSubject)
--> source (0x0000000283f71440: request unlimited
--> source (0x0000000283f71440: receive value: (Source(id: 34211598-CE53-4D0D-BB37-5D64B0D72307, asset: Player.Asset(type: Player.Asset.(unknown context at $10493f6ac).Type.custom(url: pillarbox://loading.m3u8, delegate: <Player.LoadingResourceLoaderDelegate: 0x280f78ea0>), metadata: nil, configuration: (Function))))
--> info: receive value: (nil)
--> source will be updated: receive value: (Source(id: 34211598-CE53-4D0D-BB37-5D64B0D72307, asset: Player.Asset(type: Player.Asset.(unknown context at $10493f6ac).Type.custom(url: akamai+699CA493-63FF-4F43-B4C8-92294763E0E4+https://rsi-vod-amd.akamaized.net/ww/15916771/de9f9f735c8f758d99722035901d7348b710db10562ba46ac6affd74a6e20635/master.m3u8?start=0.0&end=313.072, delegate: <CoreBusiness.AkamaiResourceLoaderDelegate: 0x280c7c700>), metadata: Optional(Player.Asset.Metadata(title: Optional("Telegiornale flash"), subtitle: Optional("Telegiornale"), description: nil)), configuration: (Function))))
--> source (0x0000000283f71440: receive value: (Source(id: 34211598-CE53-4D0D-BB37-5D64B0D72307, asset: Player.Asset(type: Player.Asset.(unknown context at $10493f6ac).Type.custom(url: akamai+699CA493-63FF-4F43-B4C8-92294763E0E4+https://rsi-vod-amd.akamaized.net/ww/15916771/de9f9f735c8f758d99722035901d7348b710db10562ba46ac6affd74a6e20635/master.m3u8?start=0.0&end=313.072, delegate: <CoreBusiness.AkamaiResourceLoaderDelegate: 0x280c7c700>), metadata: Optional(Player.Asset.Metadata(title: Optional("Telegiornale flash"), subtitle: Optional("Telegiornale"), description: nil)), configuration: (Function))))
--> info: receive value: (Optional(["title": "Telegiornale flash", "artist": "Telegiornale"]))
--> source will be updated: receive finished

The second time the info metadata is wrong and the log looks as follows:

--> source will be updated: receive subscription: (Concatenate)
--> source will be updated: request unlimited
--> source will be updated: receive value: (Source(id: 3AD60300-1FBB-4716-AE9D-1F9238A0E5FA, asset: Player.Asset(type: Player.Asset.(unknown context at $10493f6ac).Type.custom(url: pillarbox://loading.m3u8, delegate: <Player.LoadingResourceLoaderDelegate: 0x280f78bf0>), metadata: nil, configuration: (Function))))
--> source will be updated: receive value: (Source(id: 3AD60300-1FBB-4716-AE9D-1F9238A0E5FA, asset: Player.Asset(type: Player.Asset.(unknown context at $10493f6ac).Type.custom(url: akamai+2C2578DE-F656-4A47-BA5E-D546160A63EE+https://rsi-vod-amd.akamaized.net/ww/15916771/de9f9f735c8f758d99722035901d7348b710db10562ba46ac6affd74a6e20635/master.m3u8?start=0.0&end=313.072, delegate: <CoreBusiness.AkamaiResourceLoaderDelegate: 0x280d936e0>), metadata: Optional(Player.Asset.Metadata(title: Optional("Telegiornale flash"), subtitle: Optional("Telegiornale"), description: nil)), configuration: (Function))))
--> source (0x0000000283f713b0: receive subscription: (PublishedSubject)
--> source (0x0000000283f713b0: request unlimited
--> source (0x0000000283f713b0: receive value: (Source(id: 3AD60300-1FBB-4716-AE9D-1F9238A0E5FA, asset: Player.Asset(type: Player.Asset.(unknown context at $10493f6ac).Type.custom(url: pillarbox://loading.m3u8, delegate: <Player.LoadingResourceLoaderDelegate: 0x280f78bf0>), metadata: nil, configuration: (Function))))
--> info: receive value: (nil)
--> source will be updated: receive finished

Note that waiting for quite some time between both attempts also makes the second attempt correctly deliver metadata to the control center.

Looking at the above logs we can observe that, when things go wrong, the final item is delivered but overridden with a loading item afterwards. This order is clearly incorrect. When things work correctly, though, the final item is delivered last.

Origin of the issue

Since the issue does not arise the first time we add the item to the playlist or when enough time has passed between attempts, we concluded the issue might be related to URL session caching.

Disabling the local cache on the session used to retrieve the media composition:

session = URLSession(configuration: {
     let configuration = URLSessionConfiguration.default
     configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
     return configuration
 }())

the issue namely does not arise anymore, suggesting this intuition is likely correct.

Another way to fix the issue is to use .main as delegate queue for the session:

session = URLSession(configuration: .default, delegate: nil, delegateQueue: .main)

Note that the issue is really related to URLSession publishers. Using other dummy async publishers or futures we namely observe no issues. Attempting to wrap a session task in a future does not work either, though, but the following works as a replacement of a proper data task:

return Just(url)
    .receive(on: DispatchQueue(label: "ch.srgssr.url_data"))
    .tryMap { try Data(contentsOf: $0) }
    // ....

Workarounds

Our PlayerItem expects just a publisher so we cannot only fix the problem in our URN-delivery implementation. Clients of our library will namely be able to provide any kind of publisher directly, and we cannot control what they provide (we cannot force them to disable the URL cache, for example). So we rather have to implement a fix in Player.PlayerItem instead.

The fact we can use .main as delegate queue provides for a workaround: Before updating the $source in our Player.PlayerItem implementation we can namely simply send the result on the main queue first. This makes the problem disappear.

Related issues

In the past we sometimes experienced content endlessly loading for no special reason (player initially stuck on the loading indicator), without being able to find our why.

It is likely that the issue we found here was making the loading player item be delivered after the loaded player item, which was leading the player to spin indefinitely. The above fix should therefore also fix this other issue in the process.

Bug reporting

We should likely report an issue to Apple with URLSession task publishers when caching is involved, but the problem is likely not easy to reproduce in a demo app. We will try, though.

defagos commented 1 year ago

The above fix might also improve AirPlay behavior in the following cases (see #134):

Answers