@bluebillywig/react-native-bb-player 8.42.14 → 8.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ios/BBPlayerModule.swift +58 -8
- package/ios/BBPlayerView.swift +408 -305
- package/ios/BBPlayerViewManager.swift +14 -0
- package/ios/BBShortsViewManager.swift +6 -0
- package/package.json +1 -1
- package/plugin/build/withIos.d.ts +6 -0
- package/plugin/build/withIos.d.ts.map +1 -1
- package/plugin/build/withIos.js +6 -33
- package/react-native-bb-player.podspec +12 -21
- package/ios/BBPlayerViewController.swift +0 -352
- package/ios/BBPlayerViewControllerDelegate.swift +0 -48
package/ios/BBPlayerView.swift
CHANGED
|
@@ -33,8 +33,13 @@ private func log(_ message: String, level: LogLevel = .debug) {
|
|
|
33
33
|
#endif
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
// MARK: - BBPlayerView
|
|
37
|
+
// Simplified architecture: BBPlayerView directly hosts BBNativePlayerView without intermediate view controller
|
|
38
|
+
// This reduces view hierarchy depth and eliminates extra layout passes for better performance/battery life
|
|
39
|
+
|
|
40
|
+
class BBPlayerView: UIView, BBNativePlayerViewDelegate {
|
|
41
|
+
// Direct reference to SDK player view (no intermediate view controller)
|
|
42
|
+
private var playerView: BBNativePlayerView?
|
|
38
43
|
private var hasSetup: Bool = false
|
|
39
44
|
|
|
40
45
|
// Timer for periodic time updates (opt-in for performance)
|
|
@@ -42,28 +47,26 @@ class BBPlayerView: UIView, BBPlayerViewControllerDelegate {
|
|
|
42
47
|
private var isPlaying: Bool = false
|
|
43
48
|
private var currentDuration: Double = 0.0
|
|
44
49
|
private var lastKnownTime: Double = 0.0
|
|
45
|
-
private var playbackStartTimestamp: CFTimeInterval = 0
|
|
50
|
+
private var playbackStartTimestamp: CFTimeInterval = 0
|
|
46
51
|
private var lastEmittedTime: Double = 0.0
|
|
47
52
|
private var isInFullscreen: Bool = false
|
|
48
53
|
private var backgroundObserver: NSObjectProtocol?
|
|
49
54
|
private var foregroundObserver: NSObjectProtocol?
|
|
50
55
|
// Independent Google Cast button for showing the cast picker
|
|
51
56
|
private var independentCastButton: GCKUICastButton?
|
|
57
|
+
// Store parent view controller reference for SDK
|
|
58
|
+
private weak var parentViewController: UIViewController?
|
|
52
59
|
|
|
53
60
|
// MARK: - Props (set from React Native)
|
|
54
61
|
|
|
55
62
|
@objc var jsonUrl: String = "" {
|
|
56
63
|
didSet {
|
|
57
|
-
playerController.jsonUrl = jsonUrl
|
|
58
64
|
setupPlayerIfNeeded()
|
|
59
65
|
}
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
@objc var options: NSDictionary = [:] {
|
|
63
69
|
didSet {
|
|
64
|
-
if let optionsDict = options as? [String: Any] {
|
|
65
|
-
playerController.options = optionsDict
|
|
66
|
-
}
|
|
67
70
|
setupPlayerIfNeeded()
|
|
68
71
|
}
|
|
69
72
|
}
|
|
@@ -72,12 +75,9 @@ class BBPlayerView: UIView, BBPlayerViewControllerDelegate {
|
|
|
72
75
|
didSet {
|
|
73
76
|
log("Time updates \(enableTimeUpdates ? "enabled" : "disabled")")
|
|
74
77
|
|
|
75
|
-
// If disabling while timer is running, stop it
|
|
76
78
|
if !enableTimeUpdates && timeUpdateTimer != nil {
|
|
77
79
|
stopTimeUpdates()
|
|
78
|
-
}
|
|
79
|
-
// If enabling while playing, start it
|
|
80
|
-
else if enableTimeUpdates && isPlaying && timeUpdateTimer == nil {
|
|
80
|
+
} else if enableTimeUpdates && isPlaying && timeUpdateTimer == nil {
|
|
81
81
|
startTimeUpdates()
|
|
82
82
|
}
|
|
83
83
|
}
|
|
@@ -126,13 +126,11 @@ class BBPlayerView: UIView, BBPlayerViewControllerDelegate {
|
|
|
126
126
|
@objc var onDidTriggerTimeUpdate: RCTDirectEventBlock?
|
|
127
127
|
@objc var onDidTriggerApiReady: RCTDirectEventBlock?
|
|
128
128
|
|
|
129
|
-
// Override intrinsicContentSize to tell React Native this view wants to fill available space
|
|
130
129
|
override var intrinsicContentSize: CGSize {
|
|
131
130
|
return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
|
|
132
131
|
}
|
|
133
132
|
|
|
134
133
|
private func setupPlayerIfNeeded() {
|
|
135
|
-
// Only setup once we have both jsonUrl and we're in the window
|
|
136
134
|
guard !jsonUrl.isEmpty, window != nil, !hasSetup else { return }
|
|
137
135
|
hasSetup = true
|
|
138
136
|
setupPlayer()
|
|
@@ -143,86 +141,62 @@ class BBPlayerView: UIView, BBPlayerViewControllerDelegate {
|
|
|
143
141
|
|
|
144
142
|
if window != nil {
|
|
145
143
|
log("BBPlayerView.didMoveToWindow - view added to window")
|
|
146
|
-
|
|
147
|
-
|
|
144
|
+
self.clipsToBounds = false
|
|
145
|
+
|
|
146
|
+
// Optimize layer for video playback - reduce compositing overhead
|
|
147
|
+
self.layer.isOpaque = true
|
|
148
|
+
self.layer.drawsAsynchronously = true
|
|
149
|
+
self.isOpaque = true
|
|
150
|
+
|
|
151
|
+
// Register with view registry for command dispatch (supports New Architecture)
|
|
152
|
+
if let tag = self.reactTag {
|
|
153
|
+
BBPlayerViewRegistry.shared.register(self, tag: tag.intValue)
|
|
154
|
+
}
|
|
148
155
|
|
|
149
|
-
// Set up lifecycle observers to pause timer when app goes to background (saves battery)
|
|
150
156
|
setupAppLifecycleObservers()
|
|
151
157
|
|
|
152
158
|
// Find the parent view controller from the responder chain
|
|
153
|
-
var parentVC: UIViewController?
|
|
154
159
|
var responder = self.next
|
|
155
160
|
while responder != nil {
|
|
156
161
|
if let viewController = responder as? UIViewController {
|
|
157
|
-
|
|
162
|
+
parentViewController = viewController
|
|
158
163
|
break
|
|
159
164
|
}
|
|
160
165
|
responder = responder?.next
|
|
161
166
|
}
|
|
162
167
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
log("Found parent view controller: \(type(of: parentVC))")
|
|
166
|
-
parentVC.addChild(playerController)
|
|
167
|
-
addSubview(playerController.view)
|
|
168
|
-
|
|
169
|
-
playerController.view.translatesAutoresizingMaskIntoConstraints = false
|
|
170
|
-
|
|
171
|
-
NSLayoutConstraint.activate([
|
|
172
|
-
playerController.view.topAnchor.constraint(equalTo: topAnchor),
|
|
173
|
-
playerController.view.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
174
|
-
playerController.view.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
175
|
-
playerController.view.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
176
|
-
])
|
|
177
|
-
|
|
178
|
-
playerController.didMove(toParent: parentVC)
|
|
179
|
-
playerController.setViewSize = self.setViewSize
|
|
180
|
-
playerController.delegate = self
|
|
181
|
-
log("Player controller added to parent VC, delegate set to BBPlayerView")
|
|
182
|
-
|
|
183
|
-
// Try to setup if we have jsonUrl
|
|
184
|
-
setupPlayerIfNeeded()
|
|
168
|
+
if parentViewController != nil {
|
|
169
|
+
log("Found parent view controller: \(type(of: parentViewController!))")
|
|
185
170
|
} else {
|
|
186
171
|
log("WARNING - Could not find parent view controller!", level: .warning)
|
|
187
172
|
}
|
|
188
173
|
|
|
189
|
-
|
|
174
|
+
setupPlayerIfNeeded()
|
|
175
|
+
} else {
|
|
190
176
|
log("BBPlayerView.didMoveToWindow - view removed from window, isInFullscreen: \(isInFullscreen)")
|
|
191
177
|
|
|
192
|
-
//
|
|
193
|
-
|
|
178
|
+
// Unregister from view registry when removed from window (unless in fullscreen)
|
|
179
|
+
if !isInFullscreen, let tag = self.reactTag {
|
|
180
|
+
BBPlayerViewRegistry.shared.unregister(tag: tag.intValue)
|
|
181
|
+
}
|
|
182
|
+
|
|
194
183
|
if !isInFullscreen {
|
|
195
184
|
stopTimeUpdates()
|
|
196
185
|
}
|
|
197
|
-
|
|
198
|
-
// Don't tear down the view controller hierarchy when the view is removed from the window.
|
|
199
|
-
// This happens during fullscreen transitions, and the SDK needs the hierarchy intact
|
|
200
|
-
// to properly restore the player after exiting fullscreen.
|
|
201
|
-
// React Native will handle actual cleanup when the component unmounts.
|
|
202
186
|
}
|
|
203
187
|
}
|
|
204
188
|
|
|
205
|
-
// Callback for height changes (used with allowCollapseExpand)
|
|
206
|
-
private func setViewSize(_ size: CGSize) {
|
|
207
|
-
// This is called by the player controller when the view size changes
|
|
208
|
-
// In React Native, we generally let the parent handle sizing
|
|
209
|
-
}
|
|
210
|
-
|
|
211
189
|
// Start periodic time updates (1x per second, only if enabled)
|
|
212
190
|
private func startTimeUpdates() {
|
|
213
|
-
// Skip if time updates are disabled or timer already running
|
|
214
191
|
guard enableTimeUpdates, timeUpdateTimer == nil else { return }
|
|
215
192
|
|
|
216
193
|
timeUpdateTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
|
|
217
194
|
guard let self = self, self.isPlaying else { return }
|
|
218
195
|
|
|
219
|
-
// Use CACurrentMediaTime() instead of Date() - much more efficient (no system call overhead)
|
|
220
196
|
let elapsedSeconds = CACurrentMediaTime() - self.playbackStartTimestamp
|
|
221
197
|
let estimatedTime = self.lastKnownTime + elapsedSeconds
|
|
222
198
|
let currentTime = min(estimatedTime, self.currentDuration)
|
|
223
199
|
|
|
224
|
-
// Only emit if we have valid time values and time changed significantly (>0.5s)
|
|
225
|
-
// This reduces unnecessary bridge calls and React re-renders
|
|
226
200
|
if self.currentDuration > 0 && abs(currentTime - self.lastEmittedTime) >= 0.5 {
|
|
227
201
|
self.lastEmittedTime = currentTime
|
|
228
202
|
self.onDidTriggerTimeUpdate?([
|
|
@@ -233,34 +207,33 @@ class BBPlayerView: UIView, BBPlayerViewControllerDelegate {
|
|
|
233
207
|
}
|
|
234
208
|
}
|
|
235
209
|
|
|
236
|
-
// Stop periodic time updates
|
|
237
210
|
private func stopTimeUpdates() {
|
|
238
211
|
timeUpdateTimer?.invalidate()
|
|
239
212
|
timeUpdateTimer = nil
|
|
240
213
|
}
|
|
241
214
|
|
|
242
|
-
// Clean up timers, observers, and views to prevent memory leaks
|
|
243
215
|
deinit {
|
|
216
|
+
// Unregister from view registry
|
|
217
|
+
if let tag = self.reactTag {
|
|
218
|
+
BBPlayerViewRegistry.shared.unregister(tag: tag.intValue)
|
|
219
|
+
}
|
|
244
220
|
stopTimeUpdates()
|
|
245
221
|
removeAppLifecycleObservers()
|
|
246
222
|
independentCastButton?.removeFromSuperview()
|
|
247
223
|
independentCastButton = nil
|
|
248
224
|
}
|
|
249
225
|
|
|
250
|
-
// MARK: - App Lifecycle Management
|
|
226
|
+
// MARK: - App Lifecycle Management
|
|
251
227
|
|
|
252
228
|
private func setupAppLifecycleObservers() {
|
|
253
|
-
// Only set up once
|
|
254
229
|
guard backgroundObserver == nil else { return }
|
|
255
230
|
|
|
256
|
-
// Pause timer when app goes to background to save battery
|
|
257
231
|
backgroundObserver = NotificationCenter.default.addObserver(
|
|
258
232
|
forName: UIApplication.didEnterBackgroundNotification,
|
|
259
233
|
object: nil,
|
|
260
234
|
queue: .main
|
|
261
235
|
) { [weak self] _ in
|
|
262
236
|
guard let self = self else { return }
|
|
263
|
-
// Save current time before pausing timer so we can resume accurately
|
|
264
237
|
if self.isPlaying {
|
|
265
238
|
self.lastKnownTime = self.calculateCurrentTime()
|
|
266
239
|
}
|
|
@@ -268,7 +241,6 @@ class BBPlayerView: UIView, BBPlayerViewControllerDelegate {
|
|
|
268
241
|
log("Timer paused - app entered background", level: .debug)
|
|
269
242
|
}
|
|
270
243
|
|
|
271
|
-
// Resume timer when app comes back to foreground
|
|
272
244
|
foregroundObserver = NotificationCenter.default.addObserver(
|
|
273
245
|
forName: UIApplication.willEnterForegroundNotification,
|
|
274
246
|
object: nil,
|
|
@@ -276,7 +248,6 @@ class BBPlayerView: UIView, BBPlayerViewControllerDelegate {
|
|
|
276
248
|
) { [weak self] _ in
|
|
277
249
|
guard let self = self else { return }
|
|
278
250
|
if self.isPlaying && self.enableTimeUpdates {
|
|
279
|
-
// Reset timestamp to now for accurate time calculation after background
|
|
280
251
|
self.playbackStartTimestamp = CACurrentMediaTime()
|
|
281
252
|
self.startTimeUpdates()
|
|
282
253
|
log("Timer resumed - app entered foreground", level: .debug)
|
|
@@ -295,300 +266,377 @@ class BBPlayerView: UIView, BBPlayerViewControllerDelegate {
|
|
|
295
266
|
}
|
|
296
267
|
}
|
|
297
268
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
269
|
+
// MARK: - Player Setup (Simplified - no intermediate view controller)
|
|
270
|
+
|
|
271
|
+
func setupPlayer() {
|
|
272
|
+
guard let parentVC = parentViewController else {
|
|
273
|
+
log("ERROR - Cannot setup player without parent view controller", level: .error)
|
|
274
|
+
return
|
|
275
|
+
}
|
|
304
276
|
|
|
305
|
-
|
|
306
|
-
onDidRequestExpand?([:])
|
|
277
|
+
log("BBPlayerView.setupPlayer() - creating player with jsonUrl: \(jsonUrl)")
|
|
307
278
|
|
|
308
|
-
|
|
309
|
-
|
|
279
|
+
// Remove any existing player view
|
|
280
|
+
playerView?.removeFromSuperview()
|
|
310
281
|
|
|
311
|
-
|
|
312
|
-
|
|
282
|
+
// Convert options dictionary
|
|
283
|
+
var optionsDict: [String: Any] = [:]
|
|
284
|
+
if let opts = options as? [String: Any] {
|
|
285
|
+
optionsDict = opts
|
|
286
|
+
}
|
|
313
287
|
|
|
314
|
-
|
|
315
|
-
|
|
288
|
+
// Create player view directly using SDK factory method
|
|
289
|
+
// Pass the parent view controller so SDK can present fullscreen modals
|
|
290
|
+
playerView = BBNativePlayer.createPlayerView(
|
|
291
|
+
uiViewController: parentVC,
|
|
292
|
+
frame: bounds,
|
|
293
|
+
jsonUrl: jsonUrl,
|
|
294
|
+
options: optionsDict
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
if let pv = playerView {
|
|
298
|
+
// Add player view directly to BBPlayerView (no intermediate view controller)
|
|
299
|
+
addSubview(pv)
|
|
300
|
+
|
|
301
|
+
pv.translatesAutoresizingMaskIntoConstraints = false
|
|
302
|
+
NSLayoutConstraint.activate([
|
|
303
|
+
pv.topAnchor.constraint(equalTo: topAnchor),
|
|
304
|
+
pv.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
305
|
+
pv.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
306
|
+
pv.bottomAnchor.constraint(equalTo: bottomAnchor)
|
|
307
|
+
])
|
|
316
308
|
|
|
317
|
-
|
|
318
|
-
|
|
309
|
+
// Set ourselves as the delegate directly
|
|
310
|
+
pv.delegate = self
|
|
319
311
|
|
|
320
|
-
|
|
321
|
-
|
|
312
|
+
log("Player view created and added directly to BBPlayerView")
|
|
313
|
+
} else {
|
|
314
|
+
log("ERROR - playerView is nil after createPlayerView!", level: .error)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
322
317
|
|
|
323
|
-
|
|
324
|
-
onDidTriggerAdLoaded?([:])
|
|
318
|
+
// MARK: - BBNativePlayerViewDelegate Implementation
|
|
325
319
|
|
|
326
|
-
|
|
327
|
-
|
|
320
|
+
func bbNativePlayerView(didRequestCollapse playerView: BBNativePlayerView) {
|
|
321
|
+
onDidRequestCollapse?([:])
|
|
322
|
+
}
|
|
328
323
|
|
|
329
|
-
|
|
330
|
-
|
|
324
|
+
func bbNativePlayerView(didRequestExpand playerView: BBNativePlayerView) {
|
|
325
|
+
onDidRequestExpand?([:])
|
|
326
|
+
}
|
|
331
327
|
|
|
332
|
-
|
|
333
|
-
|
|
328
|
+
func bbNativePlayerView(playerView: BBNativePlayerView, didFailWithError error: String?) {
|
|
329
|
+
onDidFailWithError?(["payload": error as Any])
|
|
330
|
+
}
|
|
334
331
|
|
|
335
|
-
|
|
336
|
-
|
|
332
|
+
func didRequestOpenUrl(url: String?) {
|
|
333
|
+
onDidRequestOpenUrl?(["payload": url as Any])
|
|
334
|
+
}
|
|
337
335
|
|
|
338
|
-
|
|
339
|
-
|
|
336
|
+
func didSetupWithJsonUrl(url: String?) {
|
|
337
|
+
onDidSetupWithJsonUrl?(["payload": url as Any])
|
|
338
|
+
}
|
|
340
339
|
|
|
341
|
-
|
|
342
|
-
|
|
340
|
+
func bbNativePlayerView(playerView: BBNativePlayerView, didTriggerAdError error: String?) {
|
|
341
|
+
onDidTriggerAdError?(["payload": error as Any])
|
|
342
|
+
}
|
|
343
343
|
|
|
344
|
-
|
|
345
|
-
|
|
344
|
+
func bbNativePlayerView(didTriggerAdFinished playerView: BBNativePlayerView) {
|
|
345
|
+
onDidTriggerAdFinished?([:])
|
|
346
|
+
}
|
|
346
347
|
|
|
347
|
-
|
|
348
|
-
|
|
348
|
+
func bbNativePlayerView(didTriggerAdLoadStart playerView: BBNativePlayerView) {
|
|
349
|
+
onDidTriggerAdLoadStart?([:])
|
|
350
|
+
}
|
|
349
351
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
+
func bbNativePlayerView(didTriggerAdLoaded playerView: BBNativePlayerView) {
|
|
353
|
+
onDidTriggerAdLoaded?([:])
|
|
354
|
+
}
|
|
352
355
|
|
|
353
|
-
|
|
354
|
-
|
|
356
|
+
func bbNativePlayerView(didTriggerAdNotFound playerView: BBNativePlayerView) {
|
|
357
|
+
onDidTriggerAdNotFound?([:])
|
|
358
|
+
}
|
|
355
359
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
"ev": ev,
|
|
360
|
-
"aux": aux,
|
|
361
|
-
])
|
|
360
|
+
func bbNativePlayerView(didTriggerAdQuartile1 playerView: BBNativePlayerView) {
|
|
361
|
+
onDidTriggerAdQuartile1?([:])
|
|
362
|
+
}
|
|
362
363
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
364
|
+
func bbNativePlayerView(didTriggerAdQuartile2 playerView: BBNativePlayerView) {
|
|
365
|
+
onDidTriggerAdQuartile2?([:])
|
|
366
|
+
}
|
|
366
367
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
onDidTriggerEnded?([:])
|
|
368
|
+
func bbNativePlayerView(didTriggerAdQuartile3 playerView: BBNativePlayerView) {
|
|
369
|
+
onDidTriggerAdQuartile3?([:])
|
|
370
|
+
}
|
|
371
371
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
372
|
+
func bbNativePlayerView(didTriggerAdStarted playerView: BBNativePlayerView) {
|
|
373
|
+
onDidTriggerAdStarted?([:])
|
|
374
|
+
}
|
|
375
375
|
|
|
376
|
-
|
|
377
|
-
|
|
376
|
+
func bbNativePlayerView(didTriggerAllAdsCompleted playerView: BBNativePlayerView) {
|
|
377
|
+
onDidTriggerAllAdsCompleted?([:])
|
|
378
|
+
}
|
|
378
379
|
|
|
379
|
-
|
|
380
|
-
|
|
380
|
+
func bbNativePlayerView(didTriggerAutoPause playerView: BBNativePlayerView) {
|
|
381
|
+
onDidTriggerAutoPause?([:])
|
|
382
|
+
}
|
|
381
383
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
+
func bbNativePlayerView(didTriggerAutoPausePlay playerView: BBNativePlayerView) {
|
|
385
|
+
onDidTriggerAutoPausePlay?([:])
|
|
386
|
+
}
|
|
384
387
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
onDidTriggerPause?([:])
|
|
388
|
+
func bbNativePlayerView(didTriggerCanPlay playerView: BBNativePlayerView) {
|
|
389
|
+
onDidTriggerCanPlay?([:])
|
|
390
|
+
}
|
|
389
391
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
+
func bbNativePlayerView(
|
|
393
|
+
didTriggerCustomStatistics playerView: BBNativePlayerView, ident: String, ev: String,
|
|
394
|
+
aux: [String: String]
|
|
395
|
+
) {
|
|
396
|
+
onDidTriggerCustomStatistics?([
|
|
397
|
+
"ident": ident,
|
|
398
|
+
"ev": ev,
|
|
399
|
+
"aux": aux,
|
|
400
|
+
])
|
|
401
|
+
}
|
|
392
402
|
|
|
393
|
-
|
|
394
|
-
|
|
403
|
+
func bbNativePlayerView(
|
|
404
|
+
playerView: BBNativePlayerView, didTriggerDurationChange duration: Double
|
|
405
|
+
) {
|
|
406
|
+
currentDuration = duration
|
|
407
|
+
onDidTriggerDurationChange?(["duration": duration as Any])
|
|
408
|
+
}
|
|
395
409
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
startTimeUpdates()
|
|
402
|
-
onDidTriggerPlaying?([:])
|
|
410
|
+
func bbNativePlayerView(didTriggerEnded playerView: BBNativePlayerView) {
|
|
411
|
+
isPlaying = false
|
|
412
|
+
stopTimeUpdates()
|
|
413
|
+
onDidTriggerEnded?([:])
|
|
414
|
+
}
|
|
403
415
|
|
|
404
|
-
|
|
405
|
-
|
|
416
|
+
func bbNativePlayerView(didTriggerFullscreen playerView: BBNativePlayerView) {
|
|
417
|
+
isInFullscreen = true
|
|
418
|
+
log("FULLSCREEN ENTRY")
|
|
406
419
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
onDidTriggerRetractFullscreen?([:])
|
|
420
|
+
// Enable landscape orientation for fullscreen
|
|
421
|
+
if let orientationLockClass = NSClassFromString("OrientationLock") as? NSObject.Type {
|
|
422
|
+
orientationLockClass.setValue(true, forKey: "isFullscreen")
|
|
423
|
+
}
|
|
412
424
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
425
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
|
426
|
+
guard let self = self, let parentVC = self.parentViewController else { return }
|
|
427
|
+
if let presentedVC = parentVC.presentedViewController {
|
|
428
|
+
if #available(iOS 16.0, *) {
|
|
429
|
+
presentedVC.setNeedsUpdateOfSupportedInterfaceOrientations()
|
|
430
|
+
}
|
|
431
|
+
UIViewController.attemptRotationToDeviceOrientation()
|
|
432
|
+
}
|
|
433
|
+
}
|
|
419
434
|
|
|
420
|
-
|
|
421
|
-
|
|
435
|
+
onDidTriggerFullscreen?([:])
|
|
436
|
+
}
|
|
422
437
|
|
|
423
|
-
|
|
424
|
-
|
|
438
|
+
func bbNativePlayerView(didTriggerMediaClipFailed playerView: BBNativePlayerView) {
|
|
439
|
+
onDidTriggerMediaClipFailed?([:])
|
|
440
|
+
}
|
|
425
441
|
|
|
426
|
-
|
|
427
|
-
|
|
442
|
+
func bbNativePlayerView(
|
|
443
|
+
playerView: BBNativePlayerView, didTriggerMediaClipLoaded data: MediaClip
|
|
444
|
+
) {
|
|
445
|
+
onDidTriggerMediaClipLoaded?(data.toDictionary() as [String: Any])
|
|
446
|
+
}
|
|
428
447
|
|
|
429
|
-
|
|
430
|
-
|
|
448
|
+
func bbNativePlayerView(didTriggerModeChange playerView: BBNativePlayerView, mode: String?) {
|
|
449
|
+
onDidTriggerModeChange?(["mode": mode as Any])
|
|
450
|
+
}
|
|
431
451
|
|
|
432
|
-
|
|
433
|
-
|
|
452
|
+
func bbNativePlayerView(didTriggerPause playerView: BBNativePlayerView) {
|
|
453
|
+
isPlaying = false
|
|
454
|
+
stopTimeUpdates()
|
|
455
|
+
onDidTriggerPause?([:])
|
|
456
|
+
}
|
|
434
457
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
"muted": (volume == 0.0)
|
|
439
|
-
])
|
|
458
|
+
func bbNativePlayerView(playerView: BBNativePlayerView, didTriggerPhaseChange phase: Phase?) {
|
|
459
|
+
onDidTriggerPhaseChange?(["phase": (phase?.name ?? nil) as Any])
|
|
460
|
+
}
|
|
440
461
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
// This avoids timing issues with Google Cast SDK initialization
|
|
444
|
-
onDidTriggerApiReady?([:])
|
|
445
|
-
}
|
|
462
|
+
func bbNativePlayerView(didTriggerPlay playerView: BBNativePlayerView) {
|
|
463
|
+
onDidTriggerPlay?([:])
|
|
446
464
|
}
|
|
447
465
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
466
|
+
func bbNativePlayerView(didTriggerPlaying playerView: BBNativePlayerView) {
|
|
467
|
+
isPlaying = true
|
|
468
|
+
playbackStartTimestamp = CACurrentMediaTime()
|
|
469
|
+
lastEmittedTime = 0.0
|
|
470
|
+
lastKnownTime = calculateCurrentTime()
|
|
471
|
+
startTimeUpdates()
|
|
472
|
+
onDidTriggerPlaying?([:])
|
|
473
|
+
}
|
|
455
474
|
|
|
456
|
-
|
|
475
|
+
func bbNativePlayerView(playerView: BBNativePlayerView, didTriggerProjectLoaded data: Project) {
|
|
476
|
+
onDidTriggerProjectLoaded?(data.toDictionary() as [String: Any])
|
|
477
|
+
}
|
|
457
478
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
// so it will automatically detect and handle any cast sessions we create
|
|
462
|
-
independentCastButton = GCKUICastButton(frame: CGRect(x: -1000, y: -1000, width: 1, height: 1))
|
|
479
|
+
func bbNativePlayerView(didTriggerRetractFullscreen playerView: BBNativePlayerView) {
|
|
480
|
+
isInFullscreen = false
|
|
481
|
+
log("FULLSCREEN EXIT")
|
|
463
482
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
483
|
+
// Disable landscape orientation
|
|
484
|
+
if let orientationLockClass = NSClassFromString("OrientationLock") as? NSObject.Type {
|
|
485
|
+
orientationLockClass.setValue(false, forKey: "isFullscreen")
|
|
467
486
|
}
|
|
468
487
|
|
|
469
|
-
//
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
488
|
+
// Force rotation back to portrait
|
|
489
|
+
if #available(iOS 16.0, *) {
|
|
490
|
+
if let windowScene = window?.windowScene {
|
|
491
|
+
windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: .portrait))
|
|
492
|
+
}
|
|
493
|
+
} else {
|
|
494
|
+
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
|
|
495
|
+
}
|
|
474
496
|
|
|
475
|
-
|
|
476
|
-
}
|
|
497
|
+
UIViewController.attemptRotationToDeviceOrientation()
|
|
477
498
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
log("Cast button tapped")
|
|
499
|
+
onDidTriggerRetractFullscreen?([:])
|
|
500
|
+
}
|
|
481
501
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
502
|
+
func bbNativePlayerView(playerView: BBNativePlayerView, didTriggerSeeked seekOffset: Double) {
|
|
503
|
+
lastKnownTime = seekOffset
|
|
504
|
+
playbackStartTimestamp = CACurrentMediaTime()
|
|
505
|
+
lastEmittedTime = 0.0
|
|
506
|
+
onDidTriggerSeeked?(["payload": seekOffset as Any])
|
|
507
|
+
}
|
|
487
508
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
}
|
|
509
|
+
func bbNativePlayerView(didTriggerSeeking playerView: BBNativePlayerView) {
|
|
510
|
+
onDidTriggerSeeking?([:])
|
|
511
|
+
}
|
|
492
512
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}
|
|
513
|
+
func bbNativePlayerView(didTriggerStall playerView: BBNativePlayerView) {
|
|
514
|
+
onDidTriggerStall?([:])
|
|
515
|
+
}
|
|
497
516
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
log("Triggering independent GCKUICastButton to show cast picker")
|
|
501
|
-
castButton.sendActions(for: .touchUpInside)
|
|
517
|
+
func bbNativePlayerView(playerView: BBNativePlayerView, didTriggerStateChange state: State?) {
|
|
518
|
+
onDidTriggerStateChange?(["state": (state?.name ?? nil) as Any])
|
|
502
519
|
}
|
|
503
520
|
|
|
504
|
-
func
|
|
505
|
-
|
|
506
|
-
playerController.setupPlayer()
|
|
507
|
-
log("BBPlayerView.setupPlayer() completed")
|
|
521
|
+
func bbNativePlayerView(didTriggerViewFinished playerView: BBNativePlayerView) {
|
|
522
|
+
onDidTriggerViewFinished?([:])
|
|
508
523
|
}
|
|
509
524
|
|
|
510
|
-
func
|
|
511
|
-
|
|
525
|
+
func bbNativePlayerView(didTriggerViewStarted playerView: BBNativePlayerView) {
|
|
526
|
+
onDidTriggerViewStarted?([:])
|
|
512
527
|
}
|
|
513
528
|
|
|
514
|
-
func
|
|
515
|
-
|
|
529
|
+
func bbNativePlayerView(didTriggerVolumeChange playerView: BBNativePlayerView, volume: Double) {
|
|
530
|
+
onDidTriggerVolumeChange?([
|
|
531
|
+
"volume": volume,
|
|
532
|
+
"muted": (volume == 0.0)
|
|
533
|
+
])
|
|
516
534
|
}
|
|
517
535
|
|
|
518
|
-
func
|
|
519
|
-
|
|
536
|
+
func bbNativePlayerView(didTriggerApiReady playerView: BBNativePlayerView) {
|
|
537
|
+
onDidTriggerApiReady?([:])
|
|
520
538
|
}
|
|
521
539
|
|
|
522
|
-
|
|
523
|
-
|
|
540
|
+
// MARK: - Cast Button Setup
|
|
541
|
+
|
|
542
|
+
private func setupIndependentCastButton() {
|
|
543
|
+
if !GCKCastContext.isSharedInstanceInitialized() {
|
|
544
|
+
log("ERROR - Cannot create cast button: Google Cast SDK not initialized yet", level: .error)
|
|
545
|
+
return
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
independentCastButton = GCKUICastButton(frame: CGRect(x: -1000, y: -1000, width: 1, height: 1))
|
|
549
|
+
|
|
550
|
+
guard let castButton = independentCastButton else {
|
|
551
|
+
log("ERROR - Failed to create independent cast button", level: .error)
|
|
552
|
+
return
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
castButton.alpha = 0.0
|
|
556
|
+
castButton.isUserInteractionEnabled = false
|
|
557
|
+
addSubview(castButton)
|
|
524
558
|
}
|
|
525
559
|
|
|
526
560
|
// MARK: - Private Helper Methods
|
|
527
561
|
|
|
528
|
-
/// Calculate estimated current time based on playback state
|
|
529
|
-
/// iOS SDK doesn't expose direct currentTime property, so we estimate it
|
|
530
562
|
private func calculateCurrentTime() -> Double {
|
|
531
563
|
if isPlaying && playbackStartTimestamp > 0 {
|
|
532
|
-
// Use CACurrentMediaTime() - more efficient than Date() (no system call overhead)
|
|
533
564
|
let elapsedSeconds = CACurrentMediaTime() - playbackStartTimestamp
|
|
534
565
|
let estimatedTime = lastKnownTime + elapsedSeconds
|
|
535
566
|
return min(estimatedTime, currentDuration)
|
|
536
567
|
} else {
|
|
537
|
-
// When paused or not playing, return last known time
|
|
538
568
|
return lastKnownTime
|
|
539
569
|
}
|
|
540
570
|
}
|
|
541
571
|
|
|
572
|
+
// MARK: - Public API Methods
|
|
573
|
+
|
|
574
|
+
func adMediaHeight() -> Int? {
|
|
575
|
+
return playerView?.player.adMediaHeight
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
func adMediaWidth() -> Int? {
|
|
579
|
+
return playerView?.player.adMediaWidth
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
func clipData() -> Any? {
|
|
583
|
+
return playerView?.player.clipData?.toDictionary()
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
func controls() -> Bool? {
|
|
587
|
+
return nil
|
|
588
|
+
}
|
|
589
|
+
|
|
542
590
|
func currentTime() -> Double? {
|
|
543
591
|
return calculateCurrentTime()
|
|
544
592
|
}
|
|
545
593
|
|
|
546
594
|
func duration() -> Double? {
|
|
547
|
-
return
|
|
595
|
+
return playerView?.player.duration
|
|
548
596
|
}
|
|
549
597
|
|
|
550
598
|
func inView() -> Bool? {
|
|
551
|
-
return
|
|
599
|
+
return playerView?.player.inView
|
|
552
600
|
}
|
|
553
601
|
|
|
554
602
|
func mode() -> String? {
|
|
555
|
-
return
|
|
603
|
+
return playerView?.player.mode
|
|
556
604
|
}
|
|
557
605
|
|
|
558
606
|
func muted() -> Bool? {
|
|
559
|
-
return
|
|
607
|
+
return playerView?.player.muted
|
|
560
608
|
}
|
|
561
609
|
|
|
562
610
|
func phase() -> Any? {
|
|
563
|
-
return
|
|
611
|
+
return playerView?.player.phase?.name
|
|
564
612
|
}
|
|
565
613
|
|
|
566
614
|
func playoutData() -> Any? {
|
|
567
|
-
return
|
|
615
|
+
return playerView?.player.playoutData?.toDictionary()
|
|
568
616
|
}
|
|
569
617
|
|
|
570
618
|
func projectData() -> Any? {
|
|
571
|
-
return
|
|
619
|
+
return playerView?.player.projectData?.toDictionary()
|
|
572
620
|
}
|
|
573
621
|
|
|
574
622
|
func state() -> Any? {
|
|
575
|
-
return
|
|
623
|
+
return playerView?.player.state?.name
|
|
576
624
|
}
|
|
577
625
|
|
|
578
626
|
func volume() -> Float? {
|
|
579
|
-
return
|
|
627
|
+
return playerView?.player.volume
|
|
580
628
|
}
|
|
581
629
|
|
|
582
630
|
func autoPlayNextCancel() {
|
|
583
|
-
|
|
631
|
+
playerView?.player.autoPlayNextCancel()
|
|
584
632
|
}
|
|
585
633
|
|
|
586
634
|
func collapse() {
|
|
587
|
-
|
|
635
|
+
playerView?.player.collapse()
|
|
588
636
|
}
|
|
589
637
|
|
|
590
638
|
func expand() {
|
|
591
|
-
|
|
639
|
+
playerView?.player.expand()
|
|
592
640
|
}
|
|
593
641
|
|
|
594
642
|
func enterFullscreen() {
|
|
@@ -600,131 +648,188 @@ class BBPlayerView: UIView, BBPlayerViewControllerDelegate {
|
|
|
600
648
|
}
|
|
601
649
|
|
|
602
650
|
private func enterFullscreenWithLandscapeForce(forceLandscape: Bool) {
|
|
603
|
-
//
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
// is stuck in portrait orientation.
|
|
607
|
-
if let playerView = playerController.playerView?.player as? NSObject {
|
|
608
|
-
// Access the private bbNativePlayerViewController using Key-Value Coding
|
|
609
|
-
if let bbViewController = playerView.value(forKey: "bbNativePlayerViewController") as? NSObject {
|
|
651
|
+
// Set goingFullScreen flag on BBNativePlayerViewController for proper orientation support
|
|
652
|
+
if let player = playerView?.player as? NSObject {
|
|
653
|
+
if let bbViewController = player.value(forKey: "bbNativePlayerViewController") as? NSObject {
|
|
610
654
|
bbViewController.setValue(true, forKey: "goingFullScreen")
|
|
611
|
-
log("Set goingFullScreen = true on BBNativePlayerViewController
|
|
612
|
-
} else {
|
|
613
|
-
log("WARNING: Could not access bbNativePlayerViewController to set goingFullScreen flag", level: .warning)
|
|
655
|
+
log("Set goingFullScreen = true on BBNativePlayerViewController", level: .info)
|
|
614
656
|
}
|
|
615
657
|
}
|
|
616
658
|
|
|
617
|
-
|
|
618
|
-
playerController.playerView?.player.enterFullScreen()
|
|
659
|
+
playerView?.player.enterFullScreen()
|
|
619
660
|
|
|
620
|
-
// For landscape mode, force rotation after fullscreen is presented
|
|
621
661
|
if forceLandscape {
|
|
622
|
-
// Use a small delay to ensure fullscreen modal is presented before rotating
|
|
623
662
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
|
624
663
|
if #available(iOS 16.0, *) {
|
|
625
664
|
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
|
|
626
665
|
windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: .landscape))
|
|
627
666
|
log("Requested landscape rotation", level: .info)
|
|
628
|
-
|
|
629
|
-
// Also update the supported orientations for the fullscreen view controller
|
|
630
|
-
if let playerView = self?.playerController.playerView?.player as? NSObject {
|
|
631
|
-
if let bbViewController = playerView.value(forKey: "bbNativePlayerViewController") as? NSObject {
|
|
632
|
-
if let avPlayerVC = bbViewController.value(forKey: "avPlayerViewController") as? UIViewController {
|
|
633
|
-
avPlayerVC.setNeedsUpdateOfSupportedInterfaceOrientations()
|
|
634
|
-
log("Updated supported orientations for AVPlayerViewController", level: .info)
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
667
|
}
|
|
639
|
-
} else {
|
|
640
|
-
log("WARNING: requestGeometryUpdate requires iOS 16+, landscape rotation not available", level: .warning)
|
|
641
668
|
}
|
|
642
669
|
}
|
|
643
670
|
}
|
|
644
671
|
}
|
|
645
672
|
|
|
646
673
|
func exitFullscreen() {
|
|
647
|
-
|
|
648
|
-
// Orientation reset is handled by BBPlayerViewController.bbNativePlayerView(didTriggerRetractFullscreen:)
|
|
649
|
-
// to avoid duplicate orientation update calls which cause unnecessary CPU/GPU work
|
|
650
|
-
playerController.playerView?.player.exitFullScreen()
|
|
674
|
+
playerView?.player.exitFullScreen()
|
|
651
675
|
}
|
|
652
676
|
|
|
653
677
|
func destroy() {
|
|
654
|
-
// iOS SDK
|
|
655
|
-
// The player is automatically cleaned up when the view is removed
|
|
678
|
+
// iOS SDK cleans up automatically when view is removed
|
|
656
679
|
}
|
|
657
680
|
|
|
658
681
|
func pause() {
|
|
659
|
-
|
|
682
|
+
playerView?.player.pause()
|
|
660
683
|
}
|
|
661
684
|
|
|
662
685
|
func play() {
|
|
663
|
-
|
|
686
|
+
playerView?.player.play()
|
|
664
687
|
}
|
|
665
688
|
|
|
666
689
|
func seek(_ offsetInSeconds: Int) {
|
|
667
|
-
|
|
690
|
+
playerView?.player.seek(offsetInSeconds: offsetInSeconds as NSNumber)
|
|
668
691
|
}
|
|
669
692
|
|
|
670
693
|
func seekRelative(_ offsetInSeconds: Double) {
|
|
671
|
-
// Use the shared time calculation helper
|
|
672
694
|
let currentTime = calculateCurrentTime()
|
|
673
|
-
|
|
674
|
-
// Calculate new position and clamp to valid range [0, duration]
|
|
675
695
|
let newPosition = max(0, min(currentDuration, currentTime + offsetInSeconds))
|
|
676
|
-
|
|
677
|
-
// Seek to the new position using the standard seek method
|
|
678
|
-
playerController.playerView?.player.seek(offsetInSeconds: newPosition as NSNumber)
|
|
696
|
+
playerView?.player.seek(offsetInSeconds: newPosition as NSNumber)
|
|
679
697
|
}
|
|
680
698
|
|
|
681
699
|
func setMuted(_ muted: Bool) {
|
|
682
|
-
|
|
683
|
-
playerController.playerView?.setApiProperty(property: .muted, value: muted)
|
|
700
|
+
playerView?.setApiProperty(property: .muted, value: muted)
|
|
684
701
|
}
|
|
685
702
|
|
|
686
703
|
func setVolume(_ volume: Double) {
|
|
687
|
-
|
|
688
|
-
playerController.playerView?.setApiProperty(property: .volume, value: Float(volume))
|
|
704
|
+
playerView?.setApiProperty(property: .volume, value: Float(volume))
|
|
689
705
|
}
|
|
690
706
|
|
|
691
707
|
func loadWithClipId(_ clipId: String, initiator: String?, autoPlay: Bool?, seekTo: Double?) {
|
|
692
|
-
|
|
708
|
+
playerView?.player.loadWithClipId(clipId: clipId, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?)
|
|
693
709
|
}
|
|
694
710
|
|
|
695
711
|
func loadWithClipListId(_ clipListId: String, initiator: String?, autoPlay: Bool?, seekTo: Double?) {
|
|
696
|
-
|
|
712
|
+
playerView?.player.loadWithClipListId(clipListId: clipListId, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?)
|
|
697
713
|
}
|
|
698
714
|
|
|
699
715
|
func loadWithProjectId(_ projectId: String, initiator: String?, autoPlay: Bool?, seekTo: Double?) {
|
|
700
|
-
|
|
716
|
+
playerView?.player.loadWithProjectId(projectId: projectId, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?)
|
|
701
717
|
}
|
|
702
718
|
|
|
703
719
|
func loadWithClipJson(_ clipJson: String, initiator: String?, autoPlay: Bool?, seekTo: Double?) {
|
|
704
|
-
|
|
720
|
+
playerView?.player.loadWithClipJson(clipJson: clipJson, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?)
|
|
705
721
|
}
|
|
706
722
|
|
|
707
723
|
func loadWithClipListJson(_ clipListJson: String, initiator: String?, autoPlay: Bool?, seekTo: Double?) {
|
|
708
|
-
|
|
724
|
+
playerView?.player.loadWithClipListJson(clipListJson: clipListJson, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?)
|
|
709
725
|
}
|
|
710
726
|
|
|
711
727
|
func loadWithProjectJson(_ projectJson: String, initiator: String?, autoPlay: Bool?, seekTo: Double?) {
|
|
712
|
-
|
|
713
|
-
}
|
|
728
|
+
playerView?.player.loadWithProjectJson(projectJson: projectJson, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?)
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Load content from a JSON URL into the existing player.
|
|
733
|
+
* Extracts IDs from the URL and uses the native SDK's loadWithXxxId methods.
|
|
734
|
+
* This is more reliable than parsing JSON because the SDK handles all the loading internally.
|
|
735
|
+
*
|
|
736
|
+
* Note: Shorts URLs (/sh/{id}.json) are NOT supported here - use BBShortsView instead.
|
|
737
|
+
*/
|
|
738
|
+
func loadWithJsonUrl(_ url: String, autoPlay: Bool) {
|
|
739
|
+
NSLog("BBPlayerView.loadWithJsonUrl called - url: %@, autoPlay: %d", url, autoPlay)
|
|
740
|
+
guard playerView != nil else {
|
|
741
|
+
NSLog("BBPlayerView.loadWithJsonUrl ERROR - playerView not initialized")
|
|
742
|
+
return
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
NSLog("BBPlayerView.loadWithJsonUrl - playerView exists, parsing URL")
|
|
746
|
+
|
|
747
|
+
// Extract ID from URL patterns like:
|
|
748
|
+
// /c/{id}.json or /mediaclip/{id}.json -> clip ID
|
|
749
|
+
// /l/{id}.json or /mediacliplist/{id}.json -> clip list ID
|
|
750
|
+
// /pj/{id}.json or /project/{id}.json -> project ID
|
|
751
|
+
|
|
752
|
+
let clipIdPattern = "/c/([0-9]+)\\.json|/mediaclip/([0-9]+)"
|
|
753
|
+
let clipListIdPattern = "/l/([0-9]+)\\.json|/mediacliplist/([0-9]+)"
|
|
754
|
+
let projectIdPattern = "/pj/([0-9]+)\\.json|/project/([0-9]+)"
|
|
755
|
+
let shortsIdPattern = "/sh/([0-9]+)\\.json"
|
|
756
|
+
|
|
757
|
+
if let shortsMatch = url.range(of: shortsIdPattern, options: .regularExpression) {
|
|
758
|
+
// Shorts require a separate BBShortsView component
|
|
759
|
+
log("ERROR - Shorts URLs are not supported in BBPlayerView. Use BBShortsView instead.", level: .error)
|
|
760
|
+
onDidFailWithError?(["payload": "Shorts URLs are not supported in BBPlayerView. Use BBShortsView instead."])
|
|
761
|
+
return
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if let match = url.range(of: clipListIdPattern, options: .regularExpression) {
|
|
765
|
+
// Extract the cliplist ID
|
|
766
|
+
if let clipListId = extractIdFromUrl(url, pattern: clipListIdPattern) {
|
|
767
|
+
NSLog("BBPlayerView.loadWithJsonUrl - Loading ClipList by ID: %@", clipListId)
|
|
768
|
+
playerView?.player.loadWithClipListId(clipListId: clipListId, initiator: "external", autoPlay: autoPlay, seekTo: nil)
|
|
769
|
+
} else {
|
|
770
|
+
NSLog("BBPlayerView.loadWithJsonUrl ERROR - Failed to extract cliplist ID from URL: %@", url)
|
|
771
|
+
}
|
|
772
|
+
return
|
|
773
|
+
}
|
|
714
774
|
|
|
715
|
-
|
|
716
|
-
|
|
775
|
+
if let match = url.range(of: projectIdPattern, options: .regularExpression) {
|
|
776
|
+
// Extract the project ID
|
|
777
|
+
if let projectId = extractIdFromUrl(url, pattern: projectIdPattern) {
|
|
778
|
+
NSLog("BBPlayerView.loadWithJsonUrl - Loading Project by ID: %@", projectId)
|
|
779
|
+
playerView?.player.loadWithProjectId(projectId: projectId, initiator: "external", autoPlay: autoPlay, seekTo: nil)
|
|
780
|
+
} else {
|
|
781
|
+
NSLog("BBPlayerView.loadWithJsonUrl ERROR - Failed to extract project ID from URL: %@", url)
|
|
782
|
+
}
|
|
783
|
+
return
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if let match = url.range(of: clipIdPattern, options: .regularExpression) {
|
|
787
|
+
// Extract the clip ID
|
|
788
|
+
if let clipId = extractIdFromUrl(url, pattern: clipIdPattern) {
|
|
789
|
+
NSLog("BBPlayerView.loadWithJsonUrl - Loading Clip by ID: %@", clipId)
|
|
790
|
+
playerView?.player.loadWithClipId(clipId: clipId, initiator: "external", autoPlay: autoPlay, seekTo: nil)
|
|
791
|
+
} else {
|
|
792
|
+
NSLog("BBPlayerView.loadWithJsonUrl ERROR - Failed to extract clip ID from URL: %@", url)
|
|
793
|
+
}
|
|
794
|
+
return
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
NSLog("BBPlayerView.loadWithJsonUrl ERROR - Unknown URL format, cannot extract ID: %@", url)
|
|
798
|
+
onDidFailWithError?(["payload": "Cannot load content: unsupported URL format"])
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Helper to extract numeric ID from URL using regex pattern with capture groups
|
|
803
|
+
*/
|
|
804
|
+
private func extractIdFromUrl(_ url: String, pattern: String) -> String? {
|
|
805
|
+
do {
|
|
806
|
+
let regex = try NSRegularExpression(pattern: pattern, options: [])
|
|
807
|
+
let range = NSRange(url.startIndex..., in: url)
|
|
808
|
+
if let match = regex.firstMatch(in: url, options: [], range: range) {
|
|
809
|
+
// Try each capture group (pattern has multiple alternatives with |)
|
|
810
|
+
for i in 1..<match.numberOfRanges {
|
|
811
|
+
if let groupRange = Range(match.range(at: i), in: url) {
|
|
812
|
+
let extracted = String(url[groupRange])
|
|
813
|
+
if !extracted.isEmpty {
|
|
814
|
+
return extracted
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
} catch {
|
|
820
|
+
log("ERROR - Regex error: \(error)", level: .error)
|
|
821
|
+
}
|
|
822
|
+
return nil
|
|
823
|
+
}
|
|
717
824
|
|
|
718
825
|
func showCastPicker() {
|
|
719
826
|
log("showCastPicker called")
|
|
720
827
|
|
|
721
|
-
// CRITICAL: Verify Google Cast SDK is initialized before proceeding
|
|
722
828
|
if !GCKCastContext.isSharedInstanceInitialized() {
|
|
723
829
|
log("ERROR - showCastPicker called but Google Cast SDK not initialized yet", level: .error)
|
|
724
830
|
return
|
|
725
831
|
}
|
|
726
832
|
|
|
727
|
-
// Create the button lazily if it doesn't exist yet
|
|
728
833
|
if independentCastButton == nil {
|
|
729
834
|
setupIndependentCastButton()
|
|
730
835
|
}
|
|
@@ -734,8 +839,6 @@ class BBPlayerView: UIView, BBPlayerViewControllerDelegate {
|
|
|
734
839
|
return
|
|
735
840
|
}
|
|
736
841
|
|
|
737
|
-
// Trigger the independent cast button to show the cast device picker
|
|
738
|
-
// This works with the SDK because they both use the shared GCKSessionManager
|
|
739
842
|
DispatchQueue.main.async {
|
|
740
843
|
log("showCastPicker triggering independent GCKUICastButton")
|
|
741
844
|
castButton.sendActions(for: .touchUpInside)
|