Skip to content

Commit

Permalink
Improve support for long playlists (#766)
Browse files Browse the repository at this point in the history
Co-authored-by: Walid Kayhal <[email protected]>
  • Loading branch information
defagos and waliid authored Feb 12, 2024
1 parent 4680e95 commit 801834e
Show file tree
Hide file tree
Showing 59 changed files with 831 additions and 254 deletions.
1 change: 1 addition & 0 deletions Demo/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
}
},
"%g× %@" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
Expand Down
2 changes: 1 addition & 1 deletion Demo/Sources/ContentLists/ContentListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ private struct LoadedView: View {
}

private func openPlaylist() {
router.presented = .playlist(templates: Array(templates().prefix(20)))
router.presented = .playlist(templates: templates())
}

private func templates() -> [Template] {
Expand Down
46 changes: 31 additions & 15 deletions Demo/Sources/Model/Playlist.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
// License information is available from the LICENSE file.
//

enum URLTemplates {
static let videos: [Template] = [
enum Playlist {
static let videoUrls: [Template] = [
Template(
title: "Le R. - Légumes trop chers",
description: "Playlist item 1",
Expand Down Expand Up @@ -63,10 +63,8 @@ enum URLTemplates {
)
)
]
}

enum URNTemplates {
static let videos: [Template] = [
static let videoUrns: [Template] = [
Template(
title: "Le R. - Légumes trop chers",
description: "Playlist item 1",
Expand Down Expand Up @@ -109,7 +107,7 @@ enum URNTemplates {
)
]

static let longVideos: [Template] = [
static let longVideoUrns: [Template] = [
Template(
title: "J'ai pas l'air malade mais… (#1)",
description: "Playlist item 1",
Expand Down Expand Up @@ -157,21 +155,39 @@ enum URNTemplates {
URLTemplate.appleTvMorningShowSeason2Trailer
]

static let videosWithOneError: [Template] = [
static let audios: [Template] = [
Template(title: "Le Journal horaire 1", type: .urn("urn:rts:audio:13605286")),
Template(title: "Forum", type: .urn("urn:rts:audio:13598743")),
Template(title: "Vertigo", type: .urn("urn:rts:audio:13579611")),
Template(title: "Le Journal horaire 2", type: .urn("urn:rts:audio:13605216"))
]

static let videosWithOneFailingUrl: [Template] = [
URLTemplate.shortOnDemandVideoHLS,
URNTemplate.unknown,
URLTemplate.unknown,
URLTemplate.onDemandVideoHLS
]

static let videosWithErrors: [Template] = [
static let videosWithOneFailingUrn: [Template] = [
URNTemplate.onDemandVideo,
URNTemplate.unknown,
URNTemplate.onDemandSquareVideo
]

static let videosWithOnlyFailingUrns: [Template] = [
URNTemplate.unknown,
URNTemplate.expired
]

static let videosWithOnlyFailingUrls: [Template] = [
URLTemplate.unknown,
URNTemplate.unknown
URLTemplate.unauthorized
]

static let audios: [Template] = [
Template(title: "Le Journal horaire 1", type: .urn("urn:rts:audio:13605286")),
Template(title: "Forum", type: .urn("urn:rts:audio:13598743")),
Template(title: "Vertigo", type: .urn("urn:rts:audio:13579611")),
Template(title: "Le Journal horaire 2", type: .urn("urn:rts:audio:13605216"))
static let videosWithFailingUrlsAndUrns: [Template] = [
URNTemplate.unknown,
URLTemplate.unknown,
URNTemplate.expired,
URLTemplate.unauthorized
]
}
2 changes: 2 additions & 0 deletions Demo/Sources/Showcase/Playlist/PlaylistViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ final class PlaylistViewModel: ObservableObject, PictureInPicturePersistable {
URLTemplate.uhdVideoHLS,
URNTemplate.gothard_360,
URLTemplate.bitmovin_360,
URLTemplate.unauthorized,
URLTemplate.unknown,
URNTemplate.expired,
URNTemplate.unknown
]
Expand Down
30 changes: 21 additions & 9 deletions Demo/Sources/Showcase/ShowcaseView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,31 +79,43 @@ struct ShowcaseView: View {
CustomSection("Playlists") {
cell(
title: "Video URLs",
destination: .playlist(templates: URLTemplates.videos)
destination: .playlist(templates: Playlist.videoUrls)
)
cell(
title: "Video URNs",
destination: .playlist(templates: URNTemplates.videos)
destination: .playlist(templates: Playlist.videoUrns)
)
cell(
title: "Long video URNs",
destination: .playlist(templates: URNTemplates.longVideos)
destination: .playlist(templates: Playlist.longVideoUrns)
)
cell(
title: "Videos with media selections",
destination: .playlist(templates: URNTemplates.videosWithMediaSelections)
destination: .playlist(templates: Playlist.videosWithMediaSelections)
)
cell(
title: "Audios",
destination: .playlist(templates: URNTemplates.audios)
destination: .playlist(templates: Playlist.audios)
)
cell(
title: "Videos (one failed item)",
destination: .playlist(templates: URNTemplates.videosWithOneError)
title: "Videos (URLs, one failing)",
destination: .playlist(templates: Playlist.videosWithOneFailingUrl)
)
cell(
title: "Videos (all failing)",
destination: .playlist(templates: URNTemplates.videosWithErrors)
title: "Videos (URNs, one failing)",
destination: .playlist(templates: Playlist.videosWithOneFailingUrn)
)
cell(
title: "Videos (URLs, all failing)",
destination: .playlist(templates: Playlist.videosWithOnlyFailingUrls)
)
cell(
title: "Videos (URNs, all failing)",
destination: .playlist(templates: Playlist.videosWithOnlyFailingUrns)
)
cell(
title: "Videos (URLs and URNs, all failing)",
destination: .playlist(templates: Playlist.videosWithFailingUrlsAndUrns)
)
cell(
title: "Empty",
Expand Down
2 changes: 1 addition & 1 deletion Demo/Sources/Showcase/Stories/StoriesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ private struct TimeProgress: View {

// Behavior: h-exp, v-exp
struct StoriesView: View {
@StateObject private var model = StoriesViewModel(stories: Story.stories(from: URLTemplates.videos))
@StateObject private var model = StoriesViewModel(stories: Story.stories(from: Playlist.videoUrls))

var body: some View {
TabView(selection: $model.currentStory) {
Expand Down
7 changes: 1 addition & 6 deletions Sources/Analytics/CommandersAct/CommandersActService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,11 @@
import TCServerSide

final class CommandersActService {
private static var isDesktopApp: Bool {
let processInfo = ProcessInfo.processInfo
return processInfo.isMacCatalystApp || processInfo.isiOSAppOnMac
}

private var serverSide: ServerSide?
private var vendor: Vendor?

private static func device() -> String {
guard !isDesktopApp else { return "desktop" }
guard !ProcessInfo.processInfo.isDesktopApp else { return "desktop" }
switch UIDevice.current.userInterfaceIdiom {
case .phone:
return "phone"
Expand Down
File renamed without changes.
File renamed without changes.
13 changes: 13 additions & 0 deletions Sources/Analytics/Extensions/ProcessInfo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// Copyright (c) SRG SSR. All rights reserved.
//
// License information is available from the LICENSE file.
//

import Foundation

extension ProcessInfo {
var isDesktopApp: Bool {
isMacCatalystApp || isiOSAppOnMac
}
}
File renamed without changes.
2 changes: 1 addition & 1 deletion Sources/Circumspect/Similarity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public func equalDiff<T>(_ expectedValue: T?) -> Matcher<T> where T: Similar {
}

/// Matches against an expected similar value.
public func equal<T>(_ expectedValue: T?) -> Matcher<T> where T: Similar {
public func beSimilarTo<T>(_ expectedValue: T?) -> Matcher<T> where T: Similar {
equal(expectedValue, by: ~~)
}

Expand Down
12 changes: 12 additions & 0 deletions Sources/Core/Publisher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,18 @@ public extension Publisher {
.removeDuplicates()
.eraseToAnyPublisher()
}

/// Make the upstream publisher wait until a second signal publisher emits some value.
///
/// - Parameter signal: The signal publisher.
/// - Returns: A publisher emitting values after the signal publisher emits a value.
func wait<S>(untilOutputFrom signal: S) -> AnyPublisher<Output, Failure> where S: Publisher, S.Failure == Never {
prepend(
Empty(completeImmediately: false)
.prefix(untilOutputFrom: signal)
)
.eraseToAnyPublisher()
}
}

// Borrowed from https://www.swiftbysundell.com/articles/combine-self-cancellable-memory-management/
Expand Down
47 changes: 23 additions & 24 deletions Sources/Player/Asset.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,24 +135,37 @@ public struct Asset<M>: Assetable where M: AssetMetadata {
}
}

func nowPlayingInfo() -> NowPlayingInfo {
var nowPlayingInfo = NowPlayingInfo()
func nowPlayingInfo() -> NowPlayingInfo? {
if let metadata = metadata?.nowPlayingMetadata() {
var nowPlayingInfo = NowPlayingInfo()
nowPlayingInfo[MPMediaItemPropertyTitle] = metadata.title
nowPlayingInfo[MPMediaItemPropertyArtist] = metadata.subtitle
nowPlayingInfo[MPMediaItemPropertyComments] = metadata.description
if let image = metadata.image {
nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in image }
}
return nowPlayingInfo
}
else {
return nil
}
return nowPlayingInfo
}

func playerItem() -> AVPlayerItem {
let item = resource.playerItem().withId(id)
configuration(item)
update(item: item)
return item
func playerItem(reload: Bool) -> AVPlayerItem {
if reload, resource.isFailing {
let item = Resource.loading.playerItem().withId(id)
configuration(item)
update(item: item)
PlayerItem.reload(for: id)
return item
}
else {
let item = resource.playerItem().withId(id)
configuration(item)
update(item: item)
PlayerItem.load(for: id)
return item
}
}

func update(item: AVPlayerItem) {
Expand Down Expand Up @@ -227,25 +240,11 @@ public extension Asset where M == Never {

extension Asset {
static var loading: Self {
// Provides a playlist extension so that resource loader errors are correctly forwarded through the resource loader.
.init(
id: UUID(),
resource: .custom(url: URL(string: "pillarbox://loading.m3u8")!, delegate: LoadingResourceLoaderDelegate()),
metadata: nil,
configuration: { _ in },
trackerAdapters: []
)
.init(id: UUID(), resource: .loading, metadata: nil, configuration: { _ in }, trackerAdapters: [])
}

static func failed(error: Error) -> Self {
// Provides a playlist extension so that resource loader errors are correctly forwarded through the resource loader.
.init(
id: UUID(),
resource: .custom(url: URL(string: "pillarbox://failing.m3u8")!, delegate: FailedResourceLoaderDelegate(error: error)),
metadata: nil,
configuration: { _ in },
trackerAdapters: []
)
.init(id: UUID(), resource: .failing(error: error), metadata: nil, configuration: { _ in }, trackerAdapters: [])
}
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/Player/Extensions/AVPlayerItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ extension AVPlayerItem {
TimeProperties.timeRange(loadedTimeRanges: loadedTimeRanges, seekableTimeRanges: seekableTimeRanges)
}

static func playerItems(from items: [PlayerItem]) -> [AVPlayerItem] {
playerItems(from: items.map(\.asset))
static func playerItems(from items: [PlayerItem], length: Int, reload: Bool) -> [AVPlayerItem] {
playerItems(from: items.prefix(length).map(\.asset), reload: reload)
}
}
26 changes: 16 additions & 10 deletions Sources/Player/Interfaces/Assetable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,19 @@ protocol Assetable {
func updateMetadata()
func disable()

func nowPlayingInfo() -> NowPlayingInfo
func playerItem() -> AVPlayerItem
func nowPlayingInfo() -> NowPlayingInfo?
func playerItem(reload: Bool) -> AVPlayerItem
func update(item: AVPlayerItem)
}

extension Assetable {
func matches(_ playerItem: AVPlayerItem?) -> Bool {
id == playerItem?.id
}

func playerItem() -> AVPlayerItem {
playerItem(reload: false)
}
}

extension AVPlayerItem {
Expand All @@ -38,29 +42,31 @@ extension AVPlayerItem {
static func playerItems(
for currentAssets: [any Assetable],
replacing previousAssets: [any Assetable],
currentItem: AVPlayerItem?
currentItem: AVPlayerItem?,
length: Int
) -> [AVPlayerItem] {
guard let currentItem else { return playerItems(from: currentAssets) }
assert(length > 1)
guard let currentItem else { return playerItems(from: Array(currentAssets.prefix(length))) }
if let currentIndex = matchingIndex(for: currentItem, in: currentAssets) {
let currentAsset = currentAssets[currentIndex]
if findAsset(currentAsset, in: previousAssets) {
currentAsset.update(item: currentItem)
return [currentItem] + playerItems(from: Array(currentAssets.suffix(from: currentIndex + 1)))
return [currentItem] + playerItems(from: Array(currentAssets.suffix(from: currentIndex + 1).prefix(length - 1)))
}
else {
return playerItems(from: Array(currentAssets.suffix(from: currentIndex)))
return playerItems(from: Array(currentAssets.suffix(from: currentIndex).prefix(length)))
}
}
else if let commonIndex = firstCommonIndex(in: currentAssets, matching: previousAssets, after: currentItem) {
return playerItems(from: Array(currentAssets.suffix(from: commonIndex)))
return playerItems(from: Array(currentAssets.suffix(from: commonIndex).prefix(length)))
}
else {
return playerItems(from: currentAssets)
return playerItems(from: Array(currentAssets.prefix(length)))
}
}

static func playerItems(from assets: [any Assetable]) -> [AVPlayerItem] {
assets.map { $0.playerItem() }
static func playerItems(from assets: [any Assetable], reload: Bool = false) -> [AVPlayerItem] {
assets.map { $0.playerItem(reload: reload) }
}

private static func matchingIndex(for item: AVPlayerItem, in assets: [any Assetable]) -> Int? {
Expand Down
6 changes: 2 additions & 4 deletions Sources/Player/Internal/QueuePlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,10 @@ extension AVQueuePlayer {

if let firstItem = items.first {
if firstItem !== self.items().first {
removeAll(from: 1)
remove(firstItem)
replaceCurrentItem(with: firstItem)
}
else {
removeAll(from: 1)
}
removeAll(from: 1)
if items.count > 1 {
append(Array(items.suffix(from: 1)))
}
Expand Down
3 changes: 2 additions & 1 deletion Sources/Player/Player+ControlCenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ extension Player {
return Just(NowPlayingInfo()).eraseToAnyPublisher()
}
return current.item.$asset
.map { $0.nowPlayingInfo() }
.filter { !$0.resource.isLoading }
.compactMap { $0.nowPlayingInfo() }
.eraseToAnyPublisher()
}
.switchToLatest()
Expand Down
Loading

0 comments on commit 801834e

Please sign in to comment.