-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathPlayerView.swift
202 lines (173 loc) · 5.5 KB
/
PlayerView.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
//
// 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()
}
.aspectRatio(16 / 9, contentMode: .fit)
.frame(width: Self.width)
.clipShape(RoundedRectangle(cornerRadius: 5))
.saturation(isHighlighted ? 1 : 0)
.scaleEffect17(isHighlighted ? 1.07 : 1)
.animation(.defaultLinear, value: isHighlighted)
}
private func imageView() -> some View {
ZStack {
Color(white: 1, opacity: 0.2)
LazyImage(source: chapter.imageSource) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
}
}
.animation(.defaultLinear, value: chapter.imageSource)
.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)
}
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
@Binding var layout: PlaybackView.Layout
let supportsPictureInPicture: Bool
private var chapters: [Chapter] {
player.metadata.chapters
}
private var currentLayout: Binding<PlaybackView.Layout> {
!chapters.isEmpty ? $layout : .constant(.inline)
}
var body: some View {
VStack {
PlaybackView(player: player, layout: currentLayout)
.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 supportsPictureInPicture = false
var body: some View {
MainView(
player: model.player,
layout: $model.layout,
supportsPictureInPicture: supportsPictureInPicture
)
.enabledForInAppPictureInPicture(persisting: model)
.background(.black)
.onAppear(perform: play)
.tracked(name: "player")
}
init(media: Media) {
self.media = media
}
private func play() {
model.media = media
model.play()
}
}
extension PlayerView {
func supportsPictureInPicture(_ supportsPictureInPicture: Bool = true) -> PlayerView {
var view = self
view.supportsPictureInPicture = supportsPictureInPicture
return view
}
}
extension PlayerView: SourceCodeViewable {
static let filePath = #file
}
#Preview {
PlayerView(media: URNMedia.onDemandHorizontalVideo)
}