@bluebillywig/react-native-bb-player 8.42.14 → 8.42.15
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 +1 -1
- package/ios/BBPlayerView.swift +300 -305
- package/ios/BBPlayerViewManager.swift +6 -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,52 @@ 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
|
|
148
150
|
|
|
149
|
-
// Set up lifecycle observers to pause timer when app goes to background (saves battery)
|
|
150
151
|
setupAppLifecycleObservers()
|
|
151
152
|
|
|
152
153
|
// Find the parent view controller from the responder chain
|
|
153
|
-
var parentVC: UIViewController?
|
|
154
154
|
var responder = self.next
|
|
155
155
|
while responder != nil {
|
|
156
156
|
if let viewController = responder as? UIViewController {
|
|
157
|
-
|
|
157
|
+
parentViewController = viewController
|
|
158
158
|
break
|
|
159
159
|
}
|
|
160
160
|
responder = responder?.next
|
|
161
161
|
}
|
|
162
162
|
|
|
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()
|
|
163
|
+
if parentViewController != nil {
|
|
164
|
+
log("Found parent view controller: \(type(of: parentViewController!))")
|
|
185
165
|
} else {
|
|
186
166
|
log("WARNING - Could not find parent view controller!", level: .warning)
|
|
187
167
|
}
|
|
188
168
|
|
|
189
|
-
|
|
169
|
+
setupPlayerIfNeeded()
|
|
170
|
+
} else {
|
|
190
171
|
log("BBPlayerView.didMoveToWindow - view removed from window, isInFullscreen: \(isInFullscreen)")
|
|
191
172
|
|
|
192
|
-
// Stop time update timer to save CPU/battery when view is not visible
|
|
193
|
-
// Skip this during fullscreen transitions to avoid interrupting playback
|
|
194
173
|
if !isInFullscreen {
|
|
195
174
|
stopTimeUpdates()
|
|
196
175
|
}
|
|
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
176
|
}
|
|
203
177
|
}
|
|
204
178
|
|
|
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
179
|
// Start periodic time updates (1x per second, only if enabled)
|
|
212
180
|
private func startTimeUpdates() {
|
|
213
|
-
// Skip if time updates are disabled or timer already running
|
|
214
181
|
guard enableTimeUpdates, timeUpdateTimer == nil else { return }
|
|
215
182
|
|
|
216
183
|
timeUpdateTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
|
|
217
184
|
guard let self = self, self.isPlaying else { return }
|
|
218
185
|
|
|
219
|
-
// Use CACurrentMediaTime() instead of Date() - much more efficient (no system call overhead)
|
|
220
186
|
let elapsedSeconds = CACurrentMediaTime() - self.playbackStartTimestamp
|
|
221
187
|
let estimatedTime = self.lastKnownTime + elapsedSeconds
|
|
222
188
|
let currentTime = min(estimatedTime, self.currentDuration)
|
|
223
189
|
|
|
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
190
|
if self.currentDuration > 0 && abs(currentTime - self.lastEmittedTime) >= 0.5 {
|
|
227
191
|
self.lastEmittedTime = currentTime
|
|
228
192
|
self.onDidTriggerTimeUpdate?([
|
|
@@ -233,13 +197,11 @@ class BBPlayerView: UIView, BBPlayerViewControllerDelegate {
|
|
|
233
197
|
}
|
|
234
198
|
}
|
|
235
199
|
|
|
236
|
-
// Stop periodic time updates
|
|
237
200
|
private func stopTimeUpdates() {
|
|
238
201
|
timeUpdateTimer?.invalidate()
|
|
239
202
|
timeUpdateTimer = nil
|
|
240
203
|
}
|
|
241
204
|
|
|
242
|
-
// Clean up timers, observers, and views to prevent memory leaks
|
|
243
205
|
deinit {
|
|
244
206
|
stopTimeUpdates()
|
|
245
207
|
removeAppLifecycleObservers()
|
|
@@ -247,20 +209,17 @@ class BBPlayerView: UIView, BBPlayerViewControllerDelegate {
|
|
|
247
209
|
independentCastButton = nil
|
|
248
210
|
}
|
|
249
211
|
|
|
250
|
-
// MARK: - App Lifecycle Management
|
|
212
|
+
// MARK: - App Lifecycle Management
|
|
251
213
|
|
|
252
214
|
private func setupAppLifecycleObservers() {
|
|
253
|
-
// Only set up once
|
|
254
215
|
guard backgroundObserver == nil else { return }
|
|
255
216
|
|
|
256
|
-
// Pause timer when app goes to background to save battery
|
|
257
217
|
backgroundObserver = NotificationCenter.default.addObserver(
|
|
258
218
|
forName: UIApplication.didEnterBackgroundNotification,
|
|
259
219
|
object: nil,
|
|
260
220
|
queue: .main
|
|
261
221
|
) { [weak self] _ in
|
|
262
222
|
guard let self = self else { return }
|
|
263
|
-
// Save current time before pausing timer so we can resume accurately
|
|
264
223
|
if self.isPlaying {
|
|
265
224
|
self.lastKnownTime = self.calculateCurrentTime()
|
|
266
225
|
}
|
|
@@ -268,7 +227,6 @@ class BBPlayerView: UIView, BBPlayerViewControllerDelegate {
|
|
|
268
227
|
log("Timer paused - app entered background", level: .debug)
|
|
269
228
|
}
|
|
270
229
|
|
|
271
|
-
// Resume timer when app comes back to foreground
|
|
272
230
|
foregroundObserver = NotificationCenter.default.addObserver(
|
|
273
231
|
forName: UIApplication.willEnterForegroundNotification,
|
|
274
232
|
object: nil,
|
|
@@ -276,7 +234,6 @@ class BBPlayerView: UIView, BBPlayerViewControllerDelegate {
|
|
|
276
234
|
) { [weak self] _ in
|
|
277
235
|
guard let self = self else { return }
|
|
278
236
|
if self.isPlaying && self.enableTimeUpdates {
|
|
279
|
-
// Reset timestamp to now for accurate time calculation after background
|
|
280
237
|
self.playbackStartTimestamp = CACurrentMediaTime()
|
|
281
238
|
self.startTimeUpdates()
|
|
282
239
|
log("Timer resumed - app entered foreground", level: .debug)
|
|
@@ -295,300 +252,377 @@ class BBPlayerView: UIView, BBPlayerViewControllerDelegate {
|
|
|
295
252
|
}
|
|
296
253
|
}
|
|
297
254
|
|
|
298
|
-
|
|
299
|
-
_ controller: BBPlayerViewController, didTriggerEvent event: BBPlayerEvent
|
|
300
|
-
) {
|
|
301
|
-
switch event {
|
|
302
|
-
case .requestCollapse:
|
|
303
|
-
onDidRequestCollapse?([:])
|
|
255
|
+
// MARK: - Player Setup (Simplified - no intermediate view controller)
|
|
304
256
|
|
|
305
|
-
|
|
306
|
-
|
|
257
|
+
func setupPlayer() {
|
|
258
|
+
guard let parentVC = parentViewController else {
|
|
259
|
+
log("ERROR - Cannot setup player without parent view controller", level: .error)
|
|
260
|
+
return
|
|
261
|
+
}
|
|
307
262
|
|
|
308
|
-
|
|
309
|
-
onDidFailWithError?(["payload": error as Any])
|
|
263
|
+
log("BBPlayerView.setupPlayer() - creating player with jsonUrl: \(jsonUrl)")
|
|
310
264
|
|
|
311
|
-
|
|
312
|
-
|
|
265
|
+
// Remove any existing player view
|
|
266
|
+
playerView?.removeFromSuperview()
|
|
313
267
|
|
|
314
|
-
|
|
315
|
-
|
|
268
|
+
// Convert options dictionary
|
|
269
|
+
var optionsDict: [String: Any] = [:]
|
|
270
|
+
if let opts = options as? [String: Any] {
|
|
271
|
+
optionsDict = opts
|
|
272
|
+
}
|
|
316
273
|
|
|
317
|
-
|
|
318
|
-
|
|
274
|
+
// Create player view directly using SDK factory method
|
|
275
|
+
// Pass the parent view controller so SDK can present fullscreen modals
|
|
276
|
+
playerView = BBNativePlayer.createPlayerView(
|
|
277
|
+
uiViewController: parentVC,
|
|
278
|
+
frame: bounds,
|
|
279
|
+
jsonUrl: jsonUrl,
|
|
280
|
+
options: optionsDict
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
if let pv = playerView {
|
|
284
|
+
// Add player view directly to BBPlayerView (no intermediate view controller)
|
|
285
|
+
addSubview(pv)
|
|
286
|
+
|
|
287
|
+
pv.translatesAutoresizingMaskIntoConstraints = false
|
|
288
|
+
NSLayoutConstraint.activate([
|
|
289
|
+
pv.topAnchor.constraint(equalTo: topAnchor),
|
|
290
|
+
pv.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
291
|
+
pv.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
292
|
+
pv.bottomAnchor.constraint(equalTo: bottomAnchor)
|
|
293
|
+
])
|
|
319
294
|
|
|
320
|
-
|
|
321
|
-
|
|
295
|
+
// Set ourselves as the delegate directly
|
|
296
|
+
pv.delegate = self
|
|
322
297
|
|
|
323
|
-
|
|
324
|
-
|
|
298
|
+
log("Player view created and added directly to BBPlayerView")
|
|
299
|
+
} else {
|
|
300
|
+
log("ERROR - playerView is nil after createPlayerView!", level: .error)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
325
303
|
|
|
326
|
-
|
|
327
|
-
onDidTriggerAdLoadStart?([:])
|
|
304
|
+
// MARK: - BBNativePlayerViewDelegate Implementation
|
|
328
305
|
|
|
329
|
-
|
|
330
|
-
|
|
306
|
+
func bbNativePlayerView(didRequestCollapse playerView: BBNativePlayerView) {
|
|
307
|
+
onDidRequestCollapse?([:])
|
|
308
|
+
}
|
|
331
309
|
|
|
332
|
-
|
|
333
|
-
|
|
310
|
+
func bbNativePlayerView(didRequestExpand playerView: BBNativePlayerView) {
|
|
311
|
+
onDidRequestExpand?([:])
|
|
312
|
+
}
|
|
334
313
|
|
|
335
|
-
|
|
336
|
-
|
|
314
|
+
func bbNativePlayerView(playerView: BBNativePlayerView, didFailWithError error: String?) {
|
|
315
|
+
onDidFailWithError?(["payload": error as Any])
|
|
316
|
+
}
|
|
337
317
|
|
|
338
|
-
|
|
339
|
-
|
|
318
|
+
func didRequestOpenUrl(url: String?) {
|
|
319
|
+
onDidRequestOpenUrl?(["payload": url as Any])
|
|
320
|
+
}
|
|
340
321
|
|
|
341
|
-
|
|
342
|
-
|
|
322
|
+
func didSetupWithJsonUrl(url: String?) {
|
|
323
|
+
onDidSetupWithJsonUrl?(["payload": url as Any])
|
|
324
|
+
}
|
|
343
325
|
|
|
344
|
-
|
|
345
|
-
|
|
326
|
+
func bbNativePlayerView(playerView: BBNativePlayerView, didTriggerAdError error: String?) {
|
|
327
|
+
onDidTriggerAdError?(["payload": error as Any])
|
|
328
|
+
}
|
|
346
329
|
|
|
347
|
-
|
|
348
|
-
|
|
330
|
+
func bbNativePlayerView(didTriggerAdFinished playerView: BBNativePlayerView) {
|
|
331
|
+
onDidTriggerAdFinished?([:])
|
|
332
|
+
}
|
|
349
333
|
|
|
350
|
-
|
|
351
|
-
|
|
334
|
+
func bbNativePlayerView(didTriggerAdLoadStart playerView: BBNativePlayerView) {
|
|
335
|
+
onDidTriggerAdLoadStart?([:])
|
|
336
|
+
}
|
|
352
337
|
|
|
353
|
-
|
|
354
|
-
|
|
338
|
+
func bbNativePlayerView(didTriggerAdLoaded playerView: BBNativePlayerView) {
|
|
339
|
+
onDidTriggerAdLoaded?([:])
|
|
340
|
+
}
|
|
355
341
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
"ev": ev,
|
|
360
|
-
"aux": aux,
|
|
361
|
-
])
|
|
342
|
+
func bbNativePlayerView(didTriggerAdNotFound playerView: BBNativePlayerView) {
|
|
343
|
+
onDidTriggerAdNotFound?([:])
|
|
344
|
+
}
|
|
362
345
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
346
|
+
func bbNativePlayerView(didTriggerAdQuartile1 playerView: BBNativePlayerView) {
|
|
347
|
+
onDidTriggerAdQuartile1?([:])
|
|
348
|
+
}
|
|
366
349
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
onDidTriggerEnded?([:])
|
|
350
|
+
func bbNativePlayerView(didTriggerAdQuartile2 playerView: BBNativePlayerView) {
|
|
351
|
+
onDidTriggerAdQuartile2?([:])
|
|
352
|
+
}
|
|
371
353
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
354
|
+
func bbNativePlayerView(didTriggerAdQuartile3 playerView: BBNativePlayerView) {
|
|
355
|
+
onDidTriggerAdQuartile3?([:])
|
|
356
|
+
}
|
|
375
357
|
|
|
376
|
-
|
|
377
|
-
|
|
358
|
+
func bbNativePlayerView(didTriggerAdStarted playerView: BBNativePlayerView) {
|
|
359
|
+
onDidTriggerAdStarted?([:])
|
|
360
|
+
}
|
|
378
361
|
|
|
379
|
-
|
|
380
|
-
|
|
362
|
+
func bbNativePlayerView(didTriggerAllAdsCompleted playerView: BBNativePlayerView) {
|
|
363
|
+
onDidTriggerAllAdsCompleted?([:])
|
|
364
|
+
}
|
|
381
365
|
|
|
382
|
-
|
|
383
|
-
|
|
366
|
+
func bbNativePlayerView(didTriggerAutoPause playerView: BBNativePlayerView) {
|
|
367
|
+
onDidTriggerAutoPause?([:])
|
|
368
|
+
}
|
|
384
369
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
onDidTriggerPause?([:])
|
|
370
|
+
func bbNativePlayerView(didTriggerAutoPausePlay playerView: BBNativePlayerView) {
|
|
371
|
+
onDidTriggerAutoPausePlay?([:])
|
|
372
|
+
}
|
|
389
373
|
|
|
390
|
-
|
|
391
|
-
|
|
374
|
+
func bbNativePlayerView(didTriggerCanPlay playerView: BBNativePlayerView) {
|
|
375
|
+
onDidTriggerCanPlay?([:])
|
|
376
|
+
}
|
|
392
377
|
|
|
393
|
-
|
|
394
|
-
|
|
378
|
+
func bbNativePlayerView(
|
|
379
|
+
didTriggerCustomStatistics playerView: BBNativePlayerView, ident: String, ev: String,
|
|
380
|
+
aux: [String: String]
|
|
381
|
+
) {
|
|
382
|
+
onDidTriggerCustomStatistics?([
|
|
383
|
+
"ident": ident,
|
|
384
|
+
"ev": ev,
|
|
385
|
+
"aux": aux,
|
|
386
|
+
])
|
|
387
|
+
}
|
|
395
388
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
onDidTriggerPlaying?([:])
|
|
389
|
+
func bbNativePlayerView(
|
|
390
|
+
playerView: BBNativePlayerView, didTriggerDurationChange duration: Double
|
|
391
|
+
) {
|
|
392
|
+
currentDuration = duration
|
|
393
|
+
onDidTriggerDurationChange?(["duration": duration as Any])
|
|
394
|
+
}
|
|
403
395
|
|
|
404
|
-
|
|
405
|
-
|
|
396
|
+
func bbNativePlayerView(didTriggerEnded playerView: BBNativePlayerView) {
|
|
397
|
+
isPlaying = false
|
|
398
|
+
stopTimeUpdates()
|
|
399
|
+
onDidTriggerEnded?([:])
|
|
400
|
+
}
|
|
406
401
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
// to avoid duplicate calls which cause unnecessary CPU/GPU work
|
|
411
|
-
onDidTriggerRetractFullscreen?([:])
|
|
402
|
+
func bbNativePlayerView(didTriggerFullscreen playerView: BBNativePlayerView) {
|
|
403
|
+
isInFullscreen = true
|
|
404
|
+
log("FULLSCREEN ENTRY")
|
|
412
405
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
lastEmittedTime = 0.0 // Reset to ensure immediate time update after seek
|
|
418
|
-
onDidTriggerSeeked?(["payload": seekOffset as Any])
|
|
406
|
+
// Enable landscape orientation for fullscreen
|
|
407
|
+
if let orientationLockClass = NSClassFromString("OrientationLock") as? NSObject.Type {
|
|
408
|
+
orientationLockClass.setValue(true, forKey: "isFullscreen")
|
|
409
|
+
}
|
|
419
410
|
|
|
420
|
-
|
|
421
|
-
|
|
411
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
|
412
|
+
guard let self = self, let parentVC = self.parentViewController else { return }
|
|
413
|
+
if let presentedVC = parentVC.presentedViewController {
|
|
414
|
+
if #available(iOS 16.0, *) {
|
|
415
|
+
presentedVC.setNeedsUpdateOfSupportedInterfaceOrientations()
|
|
416
|
+
}
|
|
417
|
+
UIViewController.attemptRotationToDeviceOrientation()
|
|
418
|
+
}
|
|
419
|
+
}
|
|
422
420
|
|
|
423
|
-
|
|
424
|
-
|
|
421
|
+
onDidTriggerFullscreen?([:])
|
|
422
|
+
}
|
|
425
423
|
|
|
426
|
-
|
|
427
|
-
|
|
424
|
+
func bbNativePlayerView(didTriggerMediaClipFailed playerView: BBNativePlayerView) {
|
|
425
|
+
onDidTriggerMediaClipFailed?([:])
|
|
426
|
+
}
|
|
428
427
|
|
|
429
|
-
|
|
430
|
-
|
|
428
|
+
func bbNativePlayerView(
|
|
429
|
+
playerView: BBNativePlayerView, didTriggerMediaClipLoaded data: MediaClip
|
|
430
|
+
) {
|
|
431
|
+
onDidTriggerMediaClipLoaded?(data.toDictionary() as [String: Any])
|
|
432
|
+
}
|
|
431
433
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
+
func bbNativePlayerView(didTriggerModeChange playerView: BBNativePlayerView, mode: String?) {
|
|
435
|
+
onDidTriggerModeChange?(["mode": mode as Any])
|
|
436
|
+
}
|
|
434
437
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
438
|
+
func bbNativePlayerView(didTriggerPause playerView: BBNativePlayerView) {
|
|
439
|
+
isPlaying = false
|
|
440
|
+
stopTimeUpdates()
|
|
441
|
+
onDidTriggerPause?([:])
|
|
442
|
+
}
|
|
440
443
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
// This avoids timing issues with Google Cast SDK initialization
|
|
444
|
-
onDidTriggerApiReady?([:])
|
|
445
|
-
}
|
|
444
|
+
func bbNativePlayerView(playerView: BBNativePlayerView, didTriggerPhaseChange phase: Phase?) {
|
|
445
|
+
onDidTriggerPhaseChange?(["phase": (phase?.name ?? nil) as Any])
|
|
446
446
|
}
|
|
447
447
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
if !GCKCastContext.isSharedInstanceInitialized() {
|
|
452
|
-
log("ERROR - Cannot create cast button: Google Cast SDK not initialized yet", level: .error)
|
|
453
|
-
return
|
|
454
|
-
}
|
|
448
|
+
func bbNativePlayerView(didTriggerPlay playerView: BBNativePlayerView) {
|
|
449
|
+
onDidTriggerPlay?([:])
|
|
450
|
+
}
|
|
455
451
|
|
|
456
|
-
|
|
452
|
+
func bbNativePlayerView(didTriggerPlaying playerView: BBNativePlayerView) {
|
|
453
|
+
isPlaying = true
|
|
454
|
+
playbackStartTimestamp = CACurrentMediaTime()
|
|
455
|
+
lastEmittedTime = 0.0
|
|
456
|
+
lastKnownTime = calculateCurrentTime()
|
|
457
|
+
startTimeUpdates()
|
|
458
|
+
onDidTriggerPlaying?([:])
|
|
459
|
+
}
|
|
457
460
|
|
|
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))
|
|
461
|
+
func bbNativePlayerView(playerView: BBNativePlayerView, didTriggerProjectLoaded data: Project) {
|
|
462
|
+
onDidTriggerProjectLoaded?(data.toDictionary() as [String: Any])
|
|
463
|
+
}
|
|
463
464
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
465
|
+
func bbNativePlayerView(didTriggerRetractFullscreen playerView: BBNativePlayerView) {
|
|
466
|
+
isInFullscreen = false
|
|
467
|
+
log("FULLSCREEN EXIT")
|
|
468
|
+
|
|
469
|
+
// Disable landscape orientation
|
|
470
|
+
if let orientationLockClass = NSClassFromString("OrientationLock") as? NSObject.Type {
|
|
471
|
+
orientationLockClass.setValue(false, forKey: "isFullscreen")
|
|
467
472
|
}
|
|
468
473
|
|
|
469
|
-
//
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
+
// Force rotation back to portrait
|
|
475
|
+
if #available(iOS 16.0, *) {
|
|
476
|
+
if let windowScene = window?.windowScene {
|
|
477
|
+
windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: .portrait))
|
|
478
|
+
}
|
|
479
|
+
} else {
|
|
480
|
+
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
|
|
481
|
+
}
|
|
474
482
|
|
|
475
|
-
|
|
476
|
-
}
|
|
483
|
+
UIViewController.attemptRotationToDeviceOrientation()
|
|
477
484
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
log("Cast button tapped")
|
|
485
|
+
onDidTriggerRetractFullscreen?([:])
|
|
486
|
+
}
|
|
481
487
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
488
|
+
func bbNativePlayerView(playerView: BBNativePlayerView, didTriggerSeeked seekOffset: Double) {
|
|
489
|
+
lastKnownTime = seekOffset
|
|
490
|
+
playbackStartTimestamp = CACurrentMediaTime()
|
|
491
|
+
lastEmittedTime = 0.0
|
|
492
|
+
onDidTriggerSeeked?(["payload": seekOffset as Any])
|
|
493
|
+
}
|
|
487
494
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
}
|
|
495
|
+
func bbNativePlayerView(didTriggerSeeking playerView: BBNativePlayerView) {
|
|
496
|
+
onDidTriggerSeeking?([:])
|
|
497
|
+
}
|
|
492
498
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}
|
|
499
|
+
func bbNativePlayerView(didTriggerStall playerView: BBNativePlayerView) {
|
|
500
|
+
onDidTriggerStall?([:])
|
|
501
|
+
}
|
|
497
502
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
log("Triggering independent GCKUICastButton to show cast picker")
|
|
501
|
-
castButton.sendActions(for: .touchUpInside)
|
|
503
|
+
func bbNativePlayerView(playerView: BBNativePlayerView, didTriggerStateChange state: State?) {
|
|
504
|
+
onDidTriggerStateChange?(["state": (state?.name ?? nil) as Any])
|
|
502
505
|
}
|
|
503
506
|
|
|
504
|
-
func
|
|
505
|
-
|
|
506
|
-
playerController.setupPlayer()
|
|
507
|
-
log("BBPlayerView.setupPlayer() completed")
|
|
507
|
+
func bbNativePlayerView(didTriggerViewFinished playerView: BBNativePlayerView) {
|
|
508
|
+
onDidTriggerViewFinished?([:])
|
|
508
509
|
}
|
|
509
510
|
|
|
510
|
-
func
|
|
511
|
-
|
|
511
|
+
func bbNativePlayerView(didTriggerViewStarted playerView: BBNativePlayerView) {
|
|
512
|
+
onDidTriggerViewStarted?([:])
|
|
512
513
|
}
|
|
513
514
|
|
|
514
|
-
func
|
|
515
|
-
|
|
515
|
+
func bbNativePlayerView(didTriggerVolumeChange playerView: BBNativePlayerView, volume: Double) {
|
|
516
|
+
onDidTriggerVolumeChange?([
|
|
517
|
+
"volume": volume,
|
|
518
|
+
"muted": (volume == 0.0)
|
|
519
|
+
])
|
|
516
520
|
}
|
|
517
521
|
|
|
518
|
-
func
|
|
519
|
-
|
|
522
|
+
func bbNativePlayerView(didTriggerApiReady playerView: BBNativePlayerView) {
|
|
523
|
+
onDidTriggerApiReady?([:])
|
|
520
524
|
}
|
|
521
525
|
|
|
522
|
-
|
|
523
|
-
|
|
526
|
+
// MARK: - Cast Button Setup
|
|
527
|
+
|
|
528
|
+
private func setupIndependentCastButton() {
|
|
529
|
+
if !GCKCastContext.isSharedInstanceInitialized() {
|
|
530
|
+
log("ERROR - Cannot create cast button: Google Cast SDK not initialized yet", level: .error)
|
|
531
|
+
return
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
independentCastButton = GCKUICastButton(frame: CGRect(x: -1000, y: -1000, width: 1, height: 1))
|
|
535
|
+
|
|
536
|
+
guard let castButton = independentCastButton else {
|
|
537
|
+
log("ERROR - Failed to create independent cast button", level: .error)
|
|
538
|
+
return
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
castButton.alpha = 0.0
|
|
542
|
+
castButton.isUserInteractionEnabled = false
|
|
543
|
+
addSubview(castButton)
|
|
524
544
|
}
|
|
525
545
|
|
|
526
546
|
// MARK: - Private Helper Methods
|
|
527
547
|
|
|
528
|
-
/// Calculate estimated current time based on playback state
|
|
529
|
-
/// iOS SDK doesn't expose direct currentTime property, so we estimate it
|
|
530
548
|
private func calculateCurrentTime() -> Double {
|
|
531
549
|
if isPlaying && playbackStartTimestamp > 0 {
|
|
532
|
-
// Use CACurrentMediaTime() - more efficient than Date() (no system call overhead)
|
|
533
550
|
let elapsedSeconds = CACurrentMediaTime() - playbackStartTimestamp
|
|
534
551
|
let estimatedTime = lastKnownTime + elapsedSeconds
|
|
535
552
|
return min(estimatedTime, currentDuration)
|
|
536
553
|
} else {
|
|
537
|
-
// When paused or not playing, return last known time
|
|
538
554
|
return lastKnownTime
|
|
539
555
|
}
|
|
540
556
|
}
|
|
541
557
|
|
|
558
|
+
// MARK: - Public API Methods
|
|
559
|
+
|
|
560
|
+
func adMediaHeight() -> Int? {
|
|
561
|
+
return playerView?.player.adMediaHeight
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
func adMediaWidth() -> Int? {
|
|
565
|
+
return playerView?.player.adMediaWidth
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
func clipData() -> Any? {
|
|
569
|
+
return playerView?.player.clipData?.toDictionary()
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
func controls() -> Bool? {
|
|
573
|
+
return nil
|
|
574
|
+
}
|
|
575
|
+
|
|
542
576
|
func currentTime() -> Double? {
|
|
543
577
|
return calculateCurrentTime()
|
|
544
578
|
}
|
|
545
579
|
|
|
546
580
|
func duration() -> Double? {
|
|
547
|
-
return
|
|
581
|
+
return playerView?.player.duration
|
|
548
582
|
}
|
|
549
583
|
|
|
550
584
|
func inView() -> Bool? {
|
|
551
|
-
return
|
|
585
|
+
return playerView?.player.inView
|
|
552
586
|
}
|
|
553
587
|
|
|
554
588
|
func mode() -> String? {
|
|
555
|
-
return
|
|
589
|
+
return playerView?.player.mode
|
|
556
590
|
}
|
|
557
591
|
|
|
558
592
|
func muted() -> Bool? {
|
|
559
|
-
return
|
|
593
|
+
return playerView?.player.muted
|
|
560
594
|
}
|
|
561
595
|
|
|
562
596
|
func phase() -> Any? {
|
|
563
|
-
return
|
|
597
|
+
return playerView?.player.phase?.name
|
|
564
598
|
}
|
|
565
599
|
|
|
566
600
|
func playoutData() -> Any? {
|
|
567
|
-
return
|
|
601
|
+
return playerView?.player.playoutData?.toDictionary()
|
|
568
602
|
}
|
|
569
603
|
|
|
570
604
|
func projectData() -> Any? {
|
|
571
|
-
return
|
|
605
|
+
return playerView?.player.projectData?.toDictionary()
|
|
572
606
|
}
|
|
573
607
|
|
|
574
608
|
func state() -> Any? {
|
|
575
|
-
return
|
|
609
|
+
return playerView?.player.state?.name
|
|
576
610
|
}
|
|
577
611
|
|
|
578
612
|
func volume() -> Float? {
|
|
579
|
-
return
|
|
613
|
+
return playerView?.player.volume
|
|
580
614
|
}
|
|
581
615
|
|
|
582
616
|
func autoPlayNextCancel() {
|
|
583
|
-
|
|
617
|
+
playerView?.player.autoPlayNextCancel()
|
|
584
618
|
}
|
|
585
619
|
|
|
586
620
|
func collapse() {
|
|
587
|
-
|
|
621
|
+
playerView?.player.collapse()
|
|
588
622
|
}
|
|
589
623
|
|
|
590
624
|
func expand() {
|
|
591
|
-
|
|
625
|
+
playerView?.player.expand()
|
|
592
626
|
}
|
|
593
627
|
|
|
594
628
|
func enterFullscreen() {
|
|
@@ -600,131 +634,94 @@ class BBPlayerView: UIView, BBPlayerViewControllerDelegate {
|
|
|
600
634
|
}
|
|
601
635
|
|
|
602
636
|
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 {
|
|
637
|
+
// Set goingFullScreen flag on BBNativePlayerViewController for proper orientation support
|
|
638
|
+
if let player = playerView?.player as? NSObject {
|
|
639
|
+
if let bbViewController = player.value(forKey: "bbNativePlayerViewController") as? NSObject {
|
|
610
640
|
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)
|
|
641
|
+
log("Set goingFullScreen = true on BBNativePlayerViewController", level: .info)
|
|
614
642
|
}
|
|
615
643
|
}
|
|
616
644
|
|
|
617
|
-
|
|
618
|
-
playerController.playerView?.player.enterFullScreen()
|
|
645
|
+
playerView?.player.enterFullScreen()
|
|
619
646
|
|
|
620
|
-
// For landscape mode, force rotation after fullscreen is presented
|
|
621
647
|
if forceLandscape {
|
|
622
|
-
// Use a small delay to ensure fullscreen modal is presented before rotating
|
|
623
648
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
|
624
649
|
if #available(iOS 16.0, *) {
|
|
625
650
|
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
|
|
626
651
|
windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: .landscape))
|
|
627
652
|
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
653
|
}
|
|
639
|
-
} else {
|
|
640
|
-
log("WARNING: requestGeometryUpdate requires iOS 16+, landscape rotation not available", level: .warning)
|
|
641
654
|
}
|
|
642
655
|
}
|
|
643
656
|
}
|
|
644
657
|
}
|
|
645
658
|
|
|
646
659
|
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()
|
|
660
|
+
playerView?.player.exitFullScreen()
|
|
651
661
|
}
|
|
652
662
|
|
|
653
663
|
func destroy() {
|
|
654
|
-
// iOS SDK
|
|
655
|
-
// The player is automatically cleaned up when the view is removed
|
|
664
|
+
// iOS SDK cleans up automatically when view is removed
|
|
656
665
|
}
|
|
657
666
|
|
|
658
667
|
func pause() {
|
|
659
|
-
|
|
668
|
+
playerView?.player.pause()
|
|
660
669
|
}
|
|
661
670
|
|
|
662
671
|
func play() {
|
|
663
|
-
|
|
672
|
+
playerView?.player.play()
|
|
664
673
|
}
|
|
665
674
|
|
|
666
675
|
func seek(_ offsetInSeconds: Int) {
|
|
667
|
-
|
|
676
|
+
playerView?.player.seek(offsetInSeconds: offsetInSeconds as NSNumber)
|
|
668
677
|
}
|
|
669
678
|
|
|
670
679
|
func seekRelative(_ offsetInSeconds: Double) {
|
|
671
|
-
// Use the shared time calculation helper
|
|
672
680
|
let currentTime = calculateCurrentTime()
|
|
673
|
-
|
|
674
|
-
// Calculate new position and clamp to valid range [0, duration]
|
|
675
681
|
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)
|
|
682
|
+
playerView?.player.seek(offsetInSeconds: newPosition as NSNumber)
|
|
679
683
|
}
|
|
680
684
|
|
|
681
685
|
func setMuted(_ muted: Bool) {
|
|
682
|
-
|
|
683
|
-
playerController.playerView?.setApiProperty(property: .muted, value: muted)
|
|
686
|
+
playerView?.setApiProperty(property: .muted, value: muted)
|
|
684
687
|
}
|
|
685
688
|
|
|
686
689
|
func setVolume(_ volume: Double) {
|
|
687
|
-
|
|
688
|
-
playerController.playerView?.setApiProperty(property: .volume, value: Float(volume))
|
|
690
|
+
playerView?.setApiProperty(property: .volume, value: Float(volume))
|
|
689
691
|
}
|
|
690
692
|
|
|
691
693
|
func loadWithClipId(_ clipId: String, initiator: String?, autoPlay: Bool?, seekTo: Double?) {
|
|
692
|
-
|
|
694
|
+
playerView?.player.loadWithClipId(clipId: clipId, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?)
|
|
693
695
|
}
|
|
694
696
|
|
|
695
697
|
func loadWithClipListId(_ clipListId: String, initiator: String?, autoPlay: Bool?, seekTo: Double?) {
|
|
696
|
-
|
|
698
|
+
playerView?.player.loadWithClipListId(clipListId: clipListId, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?)
|
|
697
699
|
}
|
|
698
700
|
|
|
699
701
|
func loadWithProjectId(_ projectId: String, initiator: String?, autoPlay: Bool?, seekTo: Double?) {
|
|
700
|
-
|
|
702
|
+
playerView?.player.loadWithProjectId(projectId: projectId, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?)
|
|
701
703
|
}
|
|
702
704
|
|
|
703
705
|
func loadWithClipJson(_ clipJson: String, initiator: String?, autoPlay: Bool?, seekTo: Double?) {
|
|
704
|
-
|
|
706
|
+
playerView?.player.loadWithClipJson(clipJson: clipJson, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?)
|
|
705
707
|
}
|
|
706
708
|
|
|
707
709
|
func loadWithClipListJson(_ clipListJson: String, initiator: String?, autoPlay: Bool?, seekTo: Double?) {
|
|
708
|
-
|
|
710
|
+
playerView?.player.loadWithClipListJson(clipListJson: clipListJson, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?)
|
|
709
711
|
}
|
|
710
712
|
|
|
711
713
|
func loadWithProjectJson(_ projectJson: String, initiator: String?, autoPlay: Bool?, seekTo: Double?) {
|
|
712
|
-
|
|
714
|
+
playerView?.player.loadWithProjectJson(projectJson: projectJson, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?)
|
|
713
715
|
}
|
|
714
716
|
|
|
715
|
-
// Note: loadWithShortsId is NOT supported on BBPlayerView.
|
|
716
|
-
// For Shorts playback, use the BBShortsView component instead.
|
|
717
|
-
|
|
718
717
|
func showCastPicker() {
|
|
719
718
|
log("showCastPicker called")
|
|
720
719
|
|
|
721
|
-
// CRITICAL: Verify Google Cast SDK is initialized before proceeding
|
|
722
720
|
if !GCKCastContext.isSharedInstanceInitialized() {
|
|
723
721
|
log("ERROR - showCastPicker called but Google Cast SDK not initialized yet", level: .error)
|
|
724
722
|
return
|
|
725
723
|
}
|
|
726
724
|
|
|
727
|
-
// Create the button lazily if it doesn't exist yet
|
|
728
725
|
if independentCastButton == nil {
|
|
729
726
|
setupIndependentCastButton()
|
|
730
727
|
}
|
|
@@ -734,8 +731,6 @@ class BBPlayerView: UIView, BBPlayerViewControllerDelegate {
|
|
|
734
731
|
return
|
|
735
732
|
}
|
|
736
733
|
|
|
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
734
|
DispatchQueue.main.async {
|
|
740
735
|
log("showCastPicker triggering independent GCKUICastButton")
|
|
741
736
|
castButton.sendActions(for: .touchUpInside)
|