diff --git a/Demo/Pillarbox-demo.xcodeproj/project.pbxproj b/Demo/Pillarbox-demo.xcodeproj/project.pbxproj index bce0ad8e8..52f5fce51 100644 --- a/Demo/Pillarbox-demo.xcodeproj/project.pbxproj +++ b/Demo/Pillarbox-demo.xcodeproj/project.pbxproj @@ -10,7 +10,7 @@ 0E011D1A2B2DF9BE00DAAD3D /* MediaCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E011D192B2DF9BE00DAAD3D /* MediaCardView.swift */; }; 0E4128BD2AFB7F2A00D67759 /* InlineSystemPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E4128BC2AFB7F2A00D67759 /* InlineSystemPlayerView.swift */; }; 0E4128BF2AFB959B00D67759 /* PlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E4128BE2AFB959B00D67759 /* PlayerViewModel.swift */; }; - 0E46D3012BD2754400133AE2 /* ChaptersPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E46D3002BD2754400133AE2 /* ChaptersPlayerView.swift */; }; + 0E46D3012BD2754400133AE2 /* PlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E46D3002BD2754400133AE2 /* PlayerView.swift */; }; 0E48F3FC2B2DBAD4001982BB /* CustomNavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E48F3FB2B2DBAD4001982BB /* CustomNavigationLink.swift */; }; 0E6B995C29D43E4200D0276D /* OptInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E6B995B29D43E4200D0276D /* OptInView.swift */; }; 0EB94A1F2B5AE29000FF3175 /* HighSpeedView~ios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB94A1E2B5AE29000FF3175 /* HighSpeedView~ios.swift */; }; @@ -67,7 +67,6 @@ 6F59E89B29CF31E20093E6FB /* SystemPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F59E86D29CF31E10093E6FB /* SystemPlayerView.swift */; }; 6F59E89C29CF31E20093E6FB /* BasicPlaybackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F59E86E29CF31E10093E6FB /* BasicPlaybackView.swift */; }; 6F59E89D29CF31E20093E6FB /* VanillaPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F59E86F29CF31E10093E6FB /* VanillaPlayerView.swift */; }; - 6F59E89E29CF31E20093E6FB /* PlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F59E87029CF31E10093E6FB /* PlayerView.swift */; }; 6F59E8A029CF31E20093E6FB /* Constant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F59E87329CF31E10093E6FB /* Constant.swift */; }; 6F59E8A229CF31E20093E6FB /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F59E87529CF31E10093E6FB /* Logger.swift */; }; 6F59E8A329CF31E20093E6FB /* ExamplesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F59E87729CF31E10093E6FB /* ExamplesView.swift */; }; @@ -99,7 +98,7 @@ 0E011D192B2DF9BE00DAAD3D /* MediaCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaCardView.swift; sourceTree = ""; }; 0E4128BC2AFB7F2A00D67759 /* InlineSystemPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineSystemPlayerView.swift; sourceTree = ""; }; 0E4128BE2AFB959B00D67759 /* PlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewModel.swift; sourceTree = ""; }; - 0E46D3002BD2754400133AE2 /* ChaptersPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChaptersPlayerView.swift; sourceTree = ""; }; + 0E46D3002BD2754400133AE2 /* PlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerView.swift; sourceTree = ""; }; 0E48F3FB2B2DBAD4001982BB /* CustomNavigationLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNavigationLink.swift; sourceTree = ""; }; 0E6B995B29D43E4200D0276D /* OptInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptInView.swift; sourceTree = ""; }; 0EB94A1E2B5AE29000FF3175 /* HighSpeedView~ios.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HighSpeedView~ios.swift"; sourceTree = ""; }; @@ -161,7 +160,6 @@ 6F59E86D29CF31E10093E6FB /* SystemPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemPlayerView.swift; sourceTree = ""; }; 6F59E86E29CF31E10093E6FB /* BasicPlaybackView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasicPlaybackView.swift; sourceTree = ""; }; 6F59E86F29CF31E10093E6FB /* VanillaPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VanillaPlayerView.swift; sourceTree = ""; }; - 6F59E87029CF31E10093E6FB /* PlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerView.swift; sourceTree = ""; }; 6F59E87329CF31E10093E6FB /* Constant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constant.swift; sourceTree = ""; }; 6F59E87529CF31E10093E6FB /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 6F59E87729CF31E10093E6FB /* ExamplesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExamplesView.swift; sourceTree = ""; }; @@ -404,11 +402,10 @@ isa = PBXGroup; children = ( 6F59E86E29CF31E10093E6FB /* BasicPlaybackView.swift */, - 0E46D3002BD2754400133AE2 /* ChaptersPlayerView.swift */, 0E4128BC2AFB7F2A00D67759 /* InlineSystemPlayerView.swift */, 6F59E86B29CF31E10093E6FB /* PlaybackView.swift */, 6F59E86A29CF31E10093E6FB /* PlayerConfiguration.swift */, - 6F59E87029CF31E10093E6FB /* PlayerView.swift */, + 0E46D3002BD2754400133AE2 /* PlayerView.swift */, 0E4128BE2AFB959B00D67759 /* PlayerViewModel.swift */, 6F59E86C29CF31E10093E6FB /* SimplePlayerView.swift */, 6F59E86D29CF31E10093E6FB /* SystemPlayerView.swift */, @@ -643,7 +640,6 @@ 6F7EAA552B17755C00194D03 /* TrackingProgressTutorial~ios.swift in Sources */, 6F59E88229CF31E10093E6FB /* Media.swift in Sources */, 6F59E87E29CF31E10093E6FB /* ServerSetting.swift in Sources */, - 6F59E89E29CF31E20093E6FB /* PlayerView.swift in Sources */, 6F59E87929CF31E10093E6FB /* SearchView.swift in Sources */, 6F59E88029CF31E10093E6FB /* RadioChannel.swift in Sources */, 6F0E5CD32B3394EA0031E313 /* PiPButton.swift in Sources */, @@ -681,7 +677,7 @@ 6FCB9DDE29E024E900961B69 /* BlurredView.swift in Sources */, 6F59E89129CF31E20093E6FB /* WrappedView.swift in Sources */, 6F59E88629CF31E20093E6FB /* ContentListView.swift in Sources */, - 0E46D3012BD2754400133AE2 /* ChaptersPlayerView.swift in Sources */, + 0E46D3012BD2754400133AE2 /* PlayerView.swift in Sources */, 6FC5D6A22A6FA8D20012BC89 /* Modal.swift in Sources */, 6F59E89B29CF31E20093E6FB /* SystemPlayerView.swift in Sources */, 6F59E87B29CF31E10093E6FB /* DemoApp.swift in Sources */, diff --git a/Demo/Sources/ContentLists/ContentListView.swift b/Demo/Sources/ContentLists/ContentListView.swift index fd8b73c70..cf63a20be 100644 --- a/Demo/Sources/ContentLists/ContentListView.swift +++ b/Demo/Sources/ContentLists/ContentListView.swift @@ -104,11 +104,7 @@ private struct ContentCell: View { type: .urn(media.urn, server: serverSetting.server), isMonoscopic: media.isMonoscopic ) -#if os(iOS) - router.presented = .chaptersPlayer(media: media) -#else router.presented = .player(media: media) -#endif } #if os(iOS) .swipeActions { CopyButton(text: media.urn) } diff --git a/Demo/Sources/Players/ChaptersPlayerView.swift b/Demo/Sources/Players/ChaptersPlayerView.swift deleted file mode 100644 index 74ecb82bf..000000000 --- a/Demo/Sources/Players/ChaptersPlayerView.swift +++ /dev/null @@ -1,179 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -import CoreMedia -import PillarboxPlayer -import SwiftUI - -private struct ChapterCell: View { - private static let width: CGFloat = 200 - - private static let durationFormatter: DateComponentsFormatter = { - let formatter = DateComponentsFormatter() - formatter.allowedUnits = [.second, .minute] - formatter.zeroFormattingBehavior = .pad - return formatter - }() - - let chapter: Chapter - let isHighlighted: Bool - - private var formattedDuration: String? { - Self.durationFormatter.string(from: Double(chapter.timeRange.duration.seconds)) - } - - var body: some View { - ZStack(alignment: .bottom) { - imageView() - titleView() - } - .frame(width: Self.width, height: Self.width * 9 / 16) - .clipShape(RoundedRectangle(cornerRadius: 5)) - .saturation(isHighlighted ? 1 : 0) - .scaleEffect17(isHighlighted ? 1.07 : 1) - .animation(.defaultLinear, value: isHighlighted) - } - - @ViewBuilder - private func imageView() -> some View { - ZStack { - Color(white: 1, opacity: 0.2) - if let image = chapter.image { - Image(uiImage: image) - .resizable() - .aspectRatio(contentMode: .fill) - } - } - .animation(.defaultLinear, value: chapter.image) - .overlay { - LinearGradient( - gradient: Gradient(colors: [.black.opacity(0.7), .clear]), - startPoint: .bottom, - endPoint: .top - ) - } - .overlay(alignment: .topTrailing) { - durationLabel() - } - } - - @ViewBuilder - private func titleView() -> some View { - if let title = chapter.title { - Text(title) - .foregroundStyle(.white) - .font(.footnote) - .fontWeight(.semibold) - .lineLimit(2) - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) - } - } - - @ViewBuilder - private func durationLabel() -> some View { - if let formattedDuration { - Text(formattedDuration) - .font(.caption2) - .foregroundStyle(.white) - .padding(.horizontal, 4) - .padding(.vertical, 2) - .background(Color(white: 0, opacity: 0.8)) - .clipShape(RoundedRectangle(cornerRadius: 2)) - .padding(8) - } - } -} - -private struct ChaptersList: View { - @ObservedObject var player: Player - - @StateObject private var progressTracker = ProgressTracker(interval: .init(value: 1, timescale: 1)) - - private var chapters: [Chapter] { - player.metadata.chapters - } - - private var currentChapter: Chapter? { - chapters.first { $0.timeRange.containsTime(progressTracker.time) } - } - - var body: some View { - ScrollView(.horizontal) { - chaptersList() - } - .scrollIndicators(.hidden) - .scrollClipDisabled17() - .bind(progressTracker, to: player) - ._debugBodyCounter(color: .purple) - } - - @ViewBuilder - private func chaptersList() -> some View { - HStack(spacing: 15) { - ForEach(chapters, id: \.timeRange) { chapter in - Button { - player.seek(to: chapter) - } label: { - ChapterCell(chapter: chapter, isHighlighted: chapter == currentChapter) - } - .buttonStyle(PlainButtonStyle()) - } - } - .padding(.horizontal) - } -} - -private struct MainView: View { - @ObservedObject var player: Player - @State private var layout: PlaybackView.Layout = .minimized - - private var chapters: [Chapter] { - player.metadata.chapters - } - - private var currentLayout: Binding { - !chapters.isEmpty ? $layout : .constant(.inline) - } - - var body: some View { - VStack { - PlaybackView(player: player, layout: currentLayout) - .supportsPictureInPicture() - if layout != .maximized, !chapters.isEmpty { - ChaptersList(player: player) - } - } - .animation(.defaultLinear, values: layout, chapters) - } -} - -struct ChaptersPlayerView: View { - @StateObject private var model = PlayerViewModel.persisted ?? PlayerViewModel() - - let media: Media - - var body: some View { - MainView(player: model.player) - .enabledForInAppPictureInPicture(persisting: model) - .background(.black) - .onAppear(perform: play) - .tracked(name: "chapters-player") - } - - private func play() { - model.media = media - model.play() - } -} - -extension ChaptersPlayerView: SourceCodeViewable { - static let filePath = #file -} - -#Preview { - ChaptersPlayerView(media: Media(from: URNTemplate.onDemandHorizontalVideo)) -} diff --git a/Demo/Sources/Players/PlayerView.swift b/Demo/Sources/Players/PlayerView.swift index 223583f8d..b11c4a214 100644 --- a/Demo/Sources/Players/PlayerView.swift +++ b/Demo/Sources/Players/PlayerView.swift @@ -4,24 +4,173 @@ // License information is available from the LICENSE file. // +import CoreMedia import PillarboxPlayer import SwiftUI -/// A standalone player view with standard controls. +private struct ChapterCell: View { + private static let width: CGFloat = 200 + + private static let durationFormatter: DateComponentsFormatter = { + let formatter = DateComponentsFormatter() + formatter.allowedUnits = [.second, .minute] + formatter.zeroFormattingBehavior = .pad + return formatter + }() + + let chapter: Chapter + let isHighlighted: Bool + + private var formattedDuration: String? { + Self.durationFormatter.string(from: Double(chapter.timeRange.duration.seconds)) + } + + var body: some View { + ZStack(alignment: .bottom) { + imageView() + titleView() + } + .frame(width: Self.width, height: Self.width * 9 / 16) + .clipShape(RoundedRectangle(cornerRadius: 5)) + .saturation(isHighlighted ? 1 : 0) + .scaleEffect17(isHighlighted ? 1.07 : 1) + .animation(.defaultLinear, value: isHighlighted) + } + + @ViewBuilder + private func imageView() -> some View { + ZStack { + Color(white: 1, opacity: 0.2) + if let image = chapter.image { + Image(uiImage: image) + .resizable() + .aspectRatio(contentMode: .fill) + } + } + .animation(.defaultLinear, value: chapter.image) + .overlay { + LinearGradient( + gradient: Gradient(colors: [.black.opacity(0.7), .clear]), + startPoint: .bottom, + endPoint: .top + ) + } + .overlay(alignment: .topTrailing) { + durationLabel() + } + } + + @ViewBuilder + private func titleView() -> some View { + if let title = chapter.title { + Text(title) + .foregroundStyle(.white) + .font(.footnote) + .fontWeight(.semibold) + .lineLimit(2) + .padding() + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) + } + } + + @ViewBuilder + private func durationLabel() -> some View { + if let formattedDuration { + Text(formattedDuration) + .font(.caption2) + .foregroundStyle(.white) + .padding(.horizontal, 4) + .padding(.vertical, 2) + .background(Color(white: 0, opacity: 0.8)) + .clipShape(RoundedRectangle(cornerRadius: 2)) + .padding(8) + } + } +} + +private struct ChaptersList: View { + @ObservedObject var player: Player + + @StateObject private var progressTracker = ProgressTracker(interval: .init(value: 1, timescale: 1)) + + private var chapters: [Chapter] { + player.metadata.chapters + } + + private var currentChapter: Chapter? { + chapters.first { $0.timeRange.containsTime(progressTracker.time) } + } + + var body: some View { + ScrollView(.horizontal) { + chaptersList() + } + .scrollIndicators(.hidden) + .scrollClipDisabled17() + .bind(progressTracker, to: player) + ._debugBodyCounter(color: .purple) + } + + @ViewBuilder + private func chaptersList() -> some View { + HStack(spacing: 15) { + ForEach(chapters, id: \.timeRange) { chapter in + Button { + player.seek(to: chapter) + } label: { + ChapterCell(chapter: chapter, isHighlighted: chapter == currentChapter) + } + .buttonStyle(PlainButtonStyle()) + } + } + .padding(.horizontal) + } +} + +private struct MainView: View { + @ObservedObject var player: Player + + let isMonoscopic: Bool + let supportsPictureInPicture: Bool + + @State private var layout: PlaybackView.Layout = .minimized + + private var chapters: [Chapter] { + player.metadata.chapters + } + + private var currentLayout: Binding { + !chapters.isEmpty ? $layout : .constant(.inline) + } + + var body: some View { + VStack { + PlaybackView(player: player, layout: currentLayout) + .monoscopic(isMonoscopic) + .supportsPictureInPicture(supportsPictureInPicture) +#if os(iOS) + if layout != .maximized, !chapters.isEmpty { + ChaptersList(player: player) + } +#endif + } + .animation(.defaultLinear, values: layout, chapters) + } +} + +/// A standalone player view with standard controls and support for chapters. /// Behavior: h-exp, v-exp struct PlayerView: View { let media: Media @StateObject private var model = PlayerViewModel.persisted ?? PlayerViewModel() - private var isMonoscopic = false private var supportsPictureInPicture = false var body: some View { - PlaybackView(player: model.player) - .monoscopic(media.isMonoscopic) - .supportsPictureInPicture(supportsPictureInPicture) + MainView(player: model.player, isMonoscopic: media.isMonoscopic, supportsPictureInPicture: supportsPictureInPicture) .enabledForInAppPictureInPicture(persisting: model) + .background(.black) .onAppear(perform: play) .tracked(name: "player") } @@ -37,12 +186,6 @@ struct PlayerView: View { } extension PlayerView { - func monoscopic(_ isMonoscopic: Bool = true) -> PlayerView { - var view = self - view.isMonoscopic = isMonoscopic - return view - } - func supportsPictureInPicture(_ supportsPictureInPicture: Bool = true) -> PlayerView { var view = self view.supportsPictureInPicture = supportsPictureInPicture @@ -55,5 +198,5 @@ extension PlayerView: SourceCodeViewable { } #Preview { - PlayerView(media: Media(from: URLTemplate.onDemandVideoLocalHLS)) + PlayerView(media: Media(from: URNTemplate.onDemandHorizontalVideo)) } diff --git a/Demo/Sources/Router/Router.swift b/Demo/Sources/Router/Router.swift index 553dfe2e7..e5ff138ed 100644 --- a/Demo/Sources/Router/Router.swift +++ b/Demo/Sources/Router/Router.swift @@ -47,7 +47,7 @@ final class Router: ObservableObject { extension Router: PictureInPictureDelegate { func pictureInPictureWillStart() { switch presented { - case .player, .systemPlayer, .chaptersPlayer, .playlist, .multi: + case .player, .systemPlayer, .playlist, .multi: previousPresented = presented presented = nil case .inlineSystemPlayer: diff --git a/Demo/Sources/Router/RouterDestination.swift b/Demo/Sources/Router/RouterDestination.swift index e16e0e3eb..51f6aaf25 100644 --- a/Demo/Sources/Router/RouterDestination.swift +++ b/Demo/Sources/Router/RouterDestination.swift @@ -13,7 +13,6 @@ enum RouterDestination: Identifiable, Hashable { case inlineSystemPlayer(media: Media) case simplePlayer(media: Media) case optInPlayer(media: Media) - case chaptersPlayer(media: Media) case vanillaPlayer(item: AVPlayerItem) @@ -44,8 +43,6 @@ enum RouterDestination: Identifiable, Hashable { return "optInPlayer" case .vanillaPlayer: return "vanillaPlayer" - case .chaptersPlayer: - return "chaptersPlayer" case .blurred: return "blurred" case .twins: @@ -115,8 +112,6 @@ enum RouterDestination: Identifiable, Hashable { PlaylistView(templates: templates) case let .contentList(configuration: configuration): ContentListView(configuration: configuration) - case let .chaptersPlayer(media: media): - ChaptersPlayerView(media: media) #if os(iOS) case let .webView(url: url): WebView(url: url) diff --git a/Demo/Sources/Showcase/ShowcaseView.swift b/Demo/Sources/Showcase/ShowcaseView.swift index ecf68c07a..1ce558175 100644 --- a/Demo/Sources/Showcase/ShowcaseView.swift +++ b/Demo/Sources/Showcase/ShowcaseView.swift @@ -70,13 +70,6 @@ struct ShowcaseView: View { destination: .stories ) .sourceCode(of: StoriesView.self) - - cell( - title: "Chapters", - subtitle: "A visual way to handle chapters", - destination: .chaptersPlayer(media: Media(from: URNTemplate.onDemandHorizontalVideo)) - ) - .sourceCode(of: ChaptersPlayerView.self) } }