-
-
Notifications
You must be signed in to change notification settings - Fork 72
/
Copy pathYouTubePlayer.swift
310 lines (265 loc) · 9.65 KB
/
YouTubePlayer.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
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
import Combine
import Foundation
// MARK: - YouTubePlayer
/// A YouTube player that provides a native interface to the [YouTube iFrame Player API](https://developers.google.com/youtube/iframe_api_reference).
///
/// Enables embedding and controlling YouTube videos in your app, including playback controls,
/// playlist management, and video information retrieval.
///
/// - Important: The following limitations apply:
/// Audio background playback is not supported,
/// Simultaneous playback of multiple YouTube players is not supported,
/// Controlling playback of 360° videos is not supported.
/// - SeeAlso: [YouTubePlayerKit on GitHub](https://github.com/SvenTiigi/YouTubePlayerKit?tab=readme-ov-file)
@MainActor
public final class YouTubePlayer: ObservableObject {
// MARK: Properties
/// The source.
public internal(set) var source: Source? {
didSet {
// Verify that the source has changed.
guard self.source != oldValue else {
// Otherwise return out of function
return
}
// Send object will change
self.objectWillChange.send()
}
}
/// The parameters.
/// - Important: Updating this property will result in a reload of YouTube player.
public var parameters: Parameters {
didSet {
// Verify that the parameters have changed.
guard self.parameters != oldValue else {
// Otherwise return out of function
return
}
// Send object will change
self.objectWillChange.send()
// Reload the web view to apply the new parameters
try? self.webView.load()
}
}
/// The configuration.
public let configuration: Configuration
/// A Boolean value that determines whether logging is enabled.
public var isLoggingEnabled: Bool {
didSet {
// Send object will change
self.objectWillChange.send()
}
}
/// The state subject.
private(set) lazy var stateSubject = CurrentValueSubject<State, Never>(.idle)
/// The playback state subject.
private(set) lazy var playbackStateSubject = CurrentValueSubject<PlaybackState?, Never>(nil)
/// The YouTube player web view.
private(set) lazy var webView: YouTubePlayerWebView = {
let webView = YouTubePlayerWebView(player: self)
self.webViewEventSubscription = webView
.eventSubject
.receive(on: DispatchQueue.main)
.sink { [weak self] event in
self?.handle(
webViewEvent: event
)
}
return webView
}()
/// The YouTubePlayer WebView Event Subscription
private var webViewEventSubscription: AnyCancellable?
// MARK: Initializer
/// Creates a new instance of ``YouTubePlayer``
/// - Parameters:
/// - source: The source. Default value `nil`
/// - parameters: The parameters. Default value `.init()`
/// - configuration: The configuration. Default value `.init()`
/// - isLoggingEnabled: A Boolean value that determines whether logging is enabled. Default value `false`
nonisolated public init(
source: Source? = nil,
parameters: Parameters = .init(),
configuration: Configuration = .init(),
isLoggingEnabled: Bool = false
) {
self.source = source
self.parameters = parameters
self.configuration = configuration
self.isLoggingEnabled = isLoggingEnabled
}
}
// MARK: - Convenience Initializers
public extension YouTubePlayer {
/// Creates a new instance of ``YouTubePlayer`` from a URL
/// - Parameters:
/// - url: The URL.
nonisolated convenience init(
url: URL
) {
self.init(
source: .init(url: url),
parameters: .init(url: url) ?? .init(),
configuration: .init(url: url) ?? .init()
)
}
/// Creates a new instance of ``YouTubePlayer`` from a URL string.
/// - Parameters:
/// - urlString: The URL string.
nonisolated convenience init(
urlString: String
) {
self.init(
source: .init(urlString: urlString),
parameters: .init(urlString: urlString) ?? .init(),
configuration: .init(urlString: urlString) ?? .init()
)
}
}
// MARK: - ExpressibleByStringLiteral
extension YouTubePlayer: ExpressibleByStringLiteral {
/// Creates a new instance of ``YouTubePlayer``
/// - Parameter urlString: The url string.
nonisolated public convenience init(
stringLiteral urlString: String
) {
self.init(
urlString: urlString
)
}
}
// MARK: - Decodable
extension YouTubePlayer: Decodable {
/// The coding keys.
private enum CodingKeys: CodingKey {
case source
case parameters
case configuration
case isLoggingEnabled
}
/// Creates a new instance of ``YouTubePlayer``
/// - Parameter decoder: The decoder.
nonisolated public convenience init(
from decoder: Decoder
) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
try self.init(
source: container.decodeIfPresent(Source.self, forKey: .source),
parameters: container.decodeIfPresent(Parameters.self, forKey: .parameters) ?? .init(),
configuration: container.decodeIfPresent(Configuration.self, forKey: .configuration) ?? .init(),
isLoggingEnabled: container.decodeIfPresent(Bool.self, forKey: .isLoggingEnabled) ?? false
)
}
}
// MARK: - Identifiable
extension YouTubePlayer: Identifiable {
/// The stable identity of the entity associated with this instance.
nonisolated public var id: ObjectIdentifier {
.init(self)
}
}
// MARK: - Equatable
extension YouTubePlayer: @preconcurrency Equatable {
/// Returns a Boolean value indicating whether two values are equal.
/// - Parameters:
/// - lhs: A value to compare.
/// - rhs: Another value to compare.
public static func == (
lhs: YouTubePlayer,
rhs: YouTubePlayer
) -> Bool {
lhs.source == rhs.source
&& lhs.parameters == rhs.parameters
&& lhs.configuration == rhs.configuration
&& lhs.isLoggingEnabled == rhs.isLoggingEnabled
}
}
// MARK: - Hashable
extension YouTubePlayer: @preconcurrency Hashable {
/// Hashes the essential components of this value by feeding them into the given hasher.
/// - Parameter hasher: The hasher to use when combining the components of this instance.
public func hash(
into hasher: inout Hasher
) {
hasher.combine(self.source)
hasher.combine(self.parameters)
hasher.combine(self.configuration)
hasher.combine(self.isLoggingEnabled)
}
}
// MARK: - Handle Event
private extension YouTubePlayer {
/// Handles a `YoutubePlayerWebView.Event`
/// - Parameter webViewEvent: The web view event to handle.
func handle(
webViewEvent: YouTubePlayerWebView.Event
) {
switch webViewEvent {
case .receivedPlayerEvent(let playerEvent):
// Handle player event
self.handle(
playerEvent: playerEvent
)
case .didFailProvisionalNavigation(let error):
// Send did fail provisional navigation error
self.stateSubject.send(
.error(.didFailProvisionalNavigation(error))
)
case .didFailNavigation(let error):
// Send did fail navigation error
self.stateSubject.send(
.error(.didFailNavigation(error))
)
case .webContentProcessDidTerminate:
// Send web content process did terminate error
self.stateSubject.send(
.error(.webContentProcessDidTerminate)
)
}
}
/// Handles an incoming ``YouTubePlayer/Event``
/// - Parameter event: The event to handle.
func handle(
playerEvent: Event
) {
// Switch on event name
switch playerEvent.name {
case .iFrameApiFailedToLoad, .connectionIssue:
// Send error state
self.stateSubject.send(.error(.iFrameApiFailedToLoad))
case .error:
// Send error state
playerEvent
.data?
.value(as: Int.self)
.flatMap(YouTubePlayer.Error.init)
.map { self.stateSubject.send(.error($0)) }
case .ready:
// Send ready state
self.stateSubject.send(.ready)
case .stateChange:
// Verify YouTubePlayer PlaybackState is available
guard let playbackState = playerEvent
.data?
.value(as: Int.self)
.flatMap(PlaybackState.init(value:)) else {
// Otherwise return out of function
return
}
// Check if playback state is not equal to unstarted which mostly is an error
// and the state is currently set to error
if playbackState != .unstarted, case .error = self.state {
// Send ready state as the player has recovered from an error
self.stateSubject.send(.ready)
}
// Send PlaybackState
self.playbackStateSubject.send(playbackState)
case .reloadRequired:
// Reload player
Task { [weak self] in
try? await self?.reload()
}
default:
break
}
}
}