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