Closed defagos closed 1 year ago
We stumbled upon a subtle issue when polishing our implementation.
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):
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.
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) }
// ....
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.
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.
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.
The above fix might also improve AirPlay behavior in the following cases (see #134):
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