@bluebillywig/react-native-bb-player 8.44.0 → 8.45.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.
Files changed (54) hide show
  1. package/README.md +80 -59
  2. package/android/build.gradle +2 -1
  3. package/android/src/main/java/com/bluebillywig/bbplayer/BBPlayerModule.kt +146 -28
  4. package/android/src/main/java/com/bluebillywig/bbplayer/BBPlayerView.kt +74 -165
  5. package/android/src/main/java/com/bluebillywig/bbplayer/BBPlayerViewManager.kt +0 -6
  6. package/android/src/paper/java/com/bluebillywig/bbplayer/NativeBBPlayerModuleSpec.java +19 -8
  7. package/ios/BBPlayerModule.mm +17 -8
  8. package/ios/BBPlayerModule.swift +138 -26
  9. package/ios/BBPlayerView.swift +41 -140
  10. package/ios/BBPlayerViewManager.m +0 -2
  11. package/lib/commonjs/BBModalPlayer.js +21 -0
  12. package/lib/commonjs/BBModalPlayer.js.map +1 -0
  13. package/lib/commonjs/BBOutstreamView.js +0 -1
  14. package/lib/commonjs/BBOutstreamView.js.map +1 -1
  15. package/lib/commonjs/BBPlayerView.js +0 -1
  16. package/lib/commonjs/BBPlayerView.js.map +1 -1
  17. package/lib/commonjs/NativeCommands.js +24 -24
  18. package/lib/commonjs/NativeCommands.js.map +1 -1
  19. package/lib/commonjs/index.js +9 -1
  20. package/lib/commonjs/index.js.map +1 -1
  21. package/lib/commonjs/specs/NativeBBPlayerModule.js.map +1 -1
  22. package/lib/module/BBModalPlayer.js +17 -0
  23. package/lib/module/BBModalPlayer.js.map +1 -0
  24. package/lib/module/BBOutstreamView.js +0 -1
  25. package/lib/module/BBOutstreamView.js.map +1 -1
  26. package/lib/module/BBPlayerView.js +0 -1
  27. package/lib/module/BBPlayerView.js.map +1 -1
  28. package/lib/module/NativeCommands.js +24 -24
  29. package/lib/module/NativeCommands.js.map +1 -1
  30. package/lib/module/index.js +1 -0
  31. package/lib/module/index.js.map +1 -1
  32. package/lib/module/specs/NativeBBPlayerModule.js.map +1 -1
  33. package/lib/typescript/src/BBModalPlayer.d.ts +13 -0
  34. package/lib/typescript/src/BBModalPlayer.d.ts.map +1 -0
  35. package/lib/typescript/src/BBOutstreamView.d.ts.map +1 -1
  36. package/lib/typescript/src/BBPlayer.types.d.ts +24 -20
  37. package/lib/typescript/src/BBPlayer.types.d.ts.map +1 -1
  38. package/lib/typescript/src/BBPlayerView.d.ts +0 -2
  39. package/lib/typescript/src/BBPlayerView.d.ts.map +1 -1
  40. package/lib/typescript/src/NativeCommands.d.ts +8 -9
  41. package/lib/typescript/src/NativeCommands.d.ts.map +1 -1
  42. package/lib/typescript/src/index.d.ts +2 -0
  43. package/lib/typescript/src/index.d.ts.map +1 -1
  44. package/lib/typescript/src/specs/NativeBBPlayerModule.d.ts +13 -8
  45. package/lib/typescript/src/specs/NativeBBPlayerModule.d.ts.map +1 -1
  46. package/package.json +8 -11
  47. package/src/BBModalPlayer.ts +32 -0
  48. package/src/BBOutstreamView.tsx +0 -1
  49. package/src/BBPlayer.types.ts +31 -17
  50. package/src/BBPlayerView.tsx +0 -12
  51. package/src/NativeCommands.ts +37 -26
  52. package/src/index.ts +2 -0
  53. package/src/specs/NativeBBPlayerModule.ts +25 -8
  54. package/android/proguard-rules.pro +0 -59
package/README.md CHANGED
@@ -85,6 +85,7 @@ See the [Expo Setup Guide](docs/guides/expo-setup.md) for detailed configuration
85
85
  ### Guides
86
86
 
87
87
  - [Expo Setup Guide](docs/guides/expo-setup.md) - Expo configuration and prebuild
88
+ - [Fullscreen & Modal Player Guide](docs/guides/fullscreen.md) - Fullscreen, landscape, and modal-style presentation
88
89
  - [Advertising Guide](docs/guides/advertising.md) - Ad integration and VAST/VPAID
89
90
  - [Analytics Guide](docs/guides/analytics.md) - Analytics integration and custom statistics
90
91
  - [Shorts Guide](docs/guides/shorts.md) - Vertical video player (TikTok-style experience)
@@ -201,7 +202,6 @@ export function EventListenerExample() {
201
202
  const [playerState, setPlayerState] = useState<State>('IDLE');
202
203
  const [playerPhase, setPlayerPhase] = useState<Phase>('INIT');
203
204
  const [duration, setDuration] = useState(0);
204
- const [currentTime, setCurrentTime] = useState(0);
205
205
 
206
206
  return (
207
207
  <View style={{ flex: 1 }}>
@@ -212,7 +212,6 @@ export function EventListenerExample() {
212
212
  onDidTriggerStateChange={(state) => setPlayerState(state)}
213
213
  onDidTriggerPhaseChange={(phase) => setPlayerPhase(phase)}
214
214
  onDidTriggerDurationChange={(dur) => setDuration(dur)}
215
- onDidTriggerTimeUpdate={(time) => setCurrentTime(time)}
216
215
  onDidTriggerPlay={() => console.log('Playback started')}
217
216
  onDidTriggerPause={() => console.log('Playback paused')}
218
217
  onDidTriggerEnded={() => console.log('Playback ended')}
@@ -220,7 +219,7 @@ export function EventListenerExample() {
220
219
  <View style={{ padding: 20, backgroundColor: '#f0f0f0' }}>
221
220
  <Text>State: {playerState}</Text>
222
221
  <Text>Phase: {playerPhase}</Text>
223
- <Text>Time: {currentTime.toFixed(1)}s / {duration.toFixed(1)}s</Text>
222
+ <Text>Duration: {duration.toFixed(1)}s</Text>
224
223
  </View>
225
224
  </View>
226
225
  );
@@ -267,6 +266,55 @@ export function DynamicLoading() {
267
266
  }
268
267
  ```
269
268
 
269
+ ### Loading with Playlist Context
270
+
271
+ When loading clips within a playlist, pass context to enable "next up" navigation and proper playlist handling:
272
+
273
+ ```tsx
274
+ import React, { useRef } from 'react';
275
+ import { View, Button } from 'react-native';
276
+ import { BBPlayerView, type BBPlayerViewMethods, type LoadContext } from '@bluebillywig/react-native-bb-player';
277
+
278
+ export function PlaylistPlayer() {
279
+ const playerRef = useRef<BBPlayerViewMethods>(null);
280
+ const playlistId = '12345';
281
+
282
+ // Load a clip from a playlist with context for "next up" navigation
283
+ const loadClipFromPlaylist = (clipId: string) => {
284
+ playerRef.current?.loadClip(clipId, {
285
+ autoPlay: true,
286
+ playout: 'default',
287
+ context: {
288
+ contextEntityType: 'MediaClipList',
289
+ contextEntityId: playlistId,
290
+ contextCollectionType: 'MediaClipList',
291
+ contextCollectionId: playlistId,
292
+ },
293
+ });
294
+ };
295
+
296
+ return (
297
+ <View style={{ flex: 1 }}>
298
+ <BBPlayerView
299
+ ref={playerRef}
300
+ jsonUrl="https://demo.bbvms.com/p/default/c/4701337.json"
301
+ style={{ flex: 1 }}
302
+ />
303
+ <View style={{ flexDirection: 'row', padding: 10 }}>
304
+ <Button title="Load Video 1" onPress={() => loadClipFromPlaylist('clip1')} />
305
+ <Button title="Load Video 2" onPress={() => loadClipFromPlaylist('clip2')} />
306
+ <Button title="Load Video 3" onPress={() => loadClipFromPlaylist('clip3')} />
307
+ </View>
308
+ </View>
309
+ );
310
+ }
311
+ ```
312
+
313
+ The context enables:
314
+ - **Next Up List**: Shows upcoming videos from the playlist
315
+ - **Playlist Navigation**: Previous/next buttons work correctly within the playlist
316
+ - **Proper Analytics**: Analytics correctly attribute views to the playlist context
317
+
270
318
  ### Fullscreen Player
271
319
 
272
320
  Launch player in fullscreen mode:
@@ -278,30 +326,26 @@ import { BBPlayerView, type BBPlayerViewMethods } from '@bluebillywig/react-nati
278
326
  export function FullscreenPlayer() {
279
327
  const playerRef = useRef<BBPlayerViewMethods>(null);
280
328
 
281
- useEffect(() => {
282
- // Enter fullscreen after player is ready
283
- const timer = setTimeout(() => {
284
- playerRef.current?.enterFullscreen();
285
- }, 1000);
286
-
287
- return () => clearTimeout(timer);
288
- }, []);
289
-
290
329
  return (
291
330
  <BBPlayerView
292
331
  ref={playerRef}
293
332
  jsonUrl="https://demo.bbvms.com/p/default/c/4701337.json"
294
- options={{
295
- autoPlay: true,
296
- }}
333
+ options={{ autoPlay: true }}
297
334
  style={{ flex: 1 }}
298
- onDidTriggerFullscreen={() => console.log('Entered fullscreen')}
299
- onDidTriggerRetractFullscreen={() => console.log('Exited fullscreen')}
335
+ onDidTriggerApiReady={() => {
336
+ // Enter fullscreen landscape once the player is ready
337
+ playerRef.current?.enterFullscreenLandscape();
338
+ }}
339
+ onDidTriggerRetractFullscreen={() => {
340
+ console.log('User exited fullscreen');
341
+ }}
300
342
  />
301
343
  );
302
344
  }
303
345
  ```
304
346
 
347
+ See the [Fullscreen & Modal Player Guide](docs/guides/fullscreen.md) for advanced patterns including modal-style presentation and handling orientation.
348
+
305
349
  ### Chromecast Support
306
350
 
307
351
  Open the Google Cast device picker to cast video to Chromecast devices:
@@ -344,7 +388,6 @@ export function ChromecastPlayer() {
344
388
  | `jwt` | `string` | No | JWT token for authenticated playback |
345
389
  | `options` | `Record<string, unknown>` | No | Player configuration options |
346
390
  | `style` | `ViewStyle` | No | React Native style object |
347
- | `enableTimeUpdates` | `boolean` | No | Enable time update events (default: false) |
348
391
  | Event props | See below | No | Event callback handlers |
349
392
 
350
393
  ### Methods (via ref)
@@ -373,21 +416,20 @@ showCastPicker(): void
373
416
  // Load Content (Primary API)
374
417
  loadClip(clipId: string, options?: LoadClipOptions): void
375
418
 
376
- // Load Content (Legacy)
377
- loadWithClipId(clipId: string, initiator?: string, autoPlay?: boolean, seekTo?: number): void
378
- loadWithClipListId(clipListId: string, initiator?: string, autoPlay?: boolean, seekTo?: number): void
379
- loadWithProjectId(projectId: string, initiator?: string, autoPlay?: boolean, seekTo?: number): void
380
- loadWithClipJson(clipJson: string, initiator?: string, autoPlay?: boolean, seekTo?: number): void
381
- loadWithClipListJson(clipListJson: string, initiator?: string, autoPlay?: boolean, seekTo?: number): void
382
- loadWithProjectJson(projectJson: string, initiator?: string, autoPlay?: boolean, seekTo?: number): void
383
- loadWithJsonUrl(jsonUrl: string, autoPlay?: boolean): void
419
+ // Load Content (Legacy - all support optional context parameter)
420
+ loadWithClipId(clipId: string, initiator?: string, autoPlay?: boolean, seekTo?: number, context?: LoadContext): void
421
+ loadWithClipListId(clipListId: string, initiator?: string, autoPlay?: boolean, seekTo?: number, context?: LoadContext): void
422
+ loadWithProjectId(projectId: string, initiator?: string, autoPlay?: boolean, seekTo?: number, context?: LoadContext): void
423
+ loadWithClipJson(clipJson: string, initiator?: string, autoPlay?: boolean, seekTo?: number, context?: LoadContext): void
424
+ loadWithClipListJson(clipListJson: string, initiator?: string, autoPlay?: boolean, seekTo?: number, context?: LoadContext): void
425
+ loadWithProjectJson(projectJson: string, initiator?: string, autoPlay?: boolean, seekTo?: number, context?: LoadContext): void
426
+ loadWithJsonUrl(jsonUrl: string, autoPlay?: boolean, context?: LoadContext): void
384
427
 
385
428
  // Async Getters (Primary API)
386
429
  getPlayerState(): Promise<BBPlayerState | null> // Returns complete player state
387
430
 
388
431
  // Async Getters (Individual)
389
432
  getDuration(): Promise<number | null>
390
- getCurrentTime(): Promise<number | null>
391
433
  getMuted(): Promise<boolean | null>
392
434
  getVolume(): Promise<number | null>
393
435
  getPhase(): Promise<string | null>
@@ -419,7 +461,6 @@ onDidTriggerCanPlay?: () => void
419
461
  onDidTriggerSeeking?: () => void
420
462
  onDidTriggerSeeked?: (position: number) => void
421
463
  onDidTriggerStall?: () => void
422
- onDidTriggerTimeUpdate?: (currentTime: number, duration: number) => void
423
464
  onDidTriggerDurationChange?: (duration: number) => void
424
465
  onDidTriggerVolumeChange?: (volume: number) => void
425
466
 
@@ -481,6 +522,15 @@ type LoadClipOptions = {
481
522
  autoPlay?: boolean; // Auto-play after loading
482
523
  seekTo?: number; // Seek to position (seconds)
483
524
  initiator?: string; // Analytics initiator
525
+ context?: LoadContext; // Playlist/collection context
526
+ };
527
+
528
+ // Context for playlist/collection navigation
529
+ type LoadContext = {
530
+ contextEntityType?: 'MediaClipList'; // Type of the containing entity
531
+ contextEntityId?: string; // Playlist ID for "next up" list
532
+ contextCollectionType?: 'MediaClipList'; // Type of the collection
533
+ contextCollectionId?: string; // Collection ID if playing within a collection
484
534
  };
485
535
 
486
536
  // Complete player state object (returned by getPlayerState())
@@ -488,7 +538,6 @@ type BBPlayerState = {
488
538
  state: State;
489
539
  phase: Phase;
490
540
  mode: string | null;
491
- currentTime: number;
492
541
  duration: number;
493
542
  muted: boolean;
494
543
  volume: number;
@@ -503,39 +552,11 @@ type BBPlayerEventPayloads = {
503
552
  pause: void;
504
553
  stateChange: { state: State };
505
554
  phaseChange: { phase: Phase };
506
- timeUpdate: { currentTime: number; duration: number };
507
555
  // ... and more
508
556
  };
509
557
  ```
510
558
 
511
- ## Performance Optimization
512
-
513
- ### Time Updates (Opt-In)
514
-
515
- By default, the player does **not** emit `onDidTriggerTimeUpdate` events to reduce CPU overhead. Enable only when needed:
516
-
517
- ```tsx
518
- <BBPlayerView
519
- enableTimeUpdates={true}
520
- onDidTriggerTimeUpdate={(time, dur) => {
521
- setCurrentTime(time);
522
- setDuration(dur);
523
- }}
524
- />
525
- ```
526
-
527
- ### Polling Current Time (Alternative)
528
-
529
- If you only need the current time occasionally, use the async getter:
530
-
531
- ```tsx
532
- const handleGetTime = async () => {
533
- const time = await playerRef.current?.getCurrentTime();
534
- console.log('Current time:', time);
535
- };
536
- ```
537
-
538
- ### Getting Complete Player State
559
+ ## Getting Complete Player State
539
560
 
540
561
  Use `getPlayerState()` to fetch all player state at once:
541
562
 
@@ -544,7 +565,7 @@ const handleGetState = async () => {
544
565
  const state = await playerRef.current?.getPlayerState();
545
566
  if (state) {
546
567
  console.log(`Playing: ${state.state === 'PLAYING'}`);
547
- console.log(`Progress: ${state.currentTime}/${state.duration}`);
568
+ console.log(`Duration: ${state.duration}`);
548
569
  console.log(`Clip: ${state.clip?.title}`);
549
570
  console.log(`Volume: ${state.volume}, Muted: ${state.muted}`);
550
571
  }
@@ -64,6 +64,7 @@ android {
64
64
  }
65
65
 
66
66
  repositories {
67
+ mavenLocal()
67
68
  mavenCentral()
68
69
  google()
69
70
  maven {
@@ -78,7 +79,7 @@ dependencies {
78
79
  // Blue Billywig Native Player SDK
79
80
  // Flexible version constraint allows patch updates (8.42.x)
80
81
  // Customers can override by using resolutionStrategy in their app's build.gradle
81
- implementation 'com.bluebillywig.bbnativeplayersdk:bbnativeplayersdk:8.42.+'
82
+ implementation 'com.bluebillywig.bbnativeplayersdk:bbnativeplayersdk:0.0.11.8-SNAPSHOT'
82
83
 
83
84
  // AndroidX MediaRouter for Chromecast support
84
85
  implementation 'androidx.mediarouter:mediarouter:1.7.0'
@@ -1,14 +1,21 @@
1
1
  package com.bluebillywig.bbplayer
2
2
 
3
3
  import android.util.Log
4
+ import androidx.appcompat.app.AppCompatActivity
5
+ import com.bluebillywig.bbnativeplayersdk.BBNativePlayer
6
+ import com.bluebillywig.bbnativeplayersdk.BBNativePlayerView
7
+ import com.bluebillywig.bbnativeplayersdk.BBNativePlayerViewDelegate
8
+ import com.bluebillywig.bbnativeplayersdk.ModalPlayerViewHolder
4
9
  import com.facebook.react.bridge.Arguments
5
10
  import com.facebook.react.bridge.Promise
6
11
  import com.facebook.react.bridge.ReactApplicationContext
7
12
  import com.facebook.react.bridge.ReactMethod
8
13
  import com.facebook.react.bridge.UiThreadUtil
9
14
  import com.facebook.react.module.annotations.ReactModule
15
+ import com.facebook.react.modules.core.DeviceEventManagerModule
10
16
  import com.facebook.react.uimanager.UIManagerHelper
11
17
  import com.facebook.react.uimanager.common.UIManagerType
18
+ import org.json.JSONObject
12
19
 
13
20
  /**
14
21
  * Native Module for BBPlayer commands.
@@ -136,15 +143,17 @@ class BBPlayerModule(private val reactContext: ReactApplicationContext) :
136
143
  clipId: String,
137
144
  initiator: String?,
138
145
  autoPlay: Boolean,
139
- seekTo: Double
146
+ seekTo: Double,
147
+ contextJson: String?
140
148
  ) {
141
- Log.d("BBPlayerModule", "loadWithClipId called - viewTag: $viewTag, clipId: $clipId, autoPlay: $autoPlay")
149
+ Log.d("BBPlayerModule", "loadWithClipId called - viewTag: $viewTag, clipId: $clipId, autoPlay: $autoPlay, context: $contextJson")
142
150
  runOnUiThread(viewTag.toInt()) {
143
151
  it.loadWithClipId(
144
152
  clipId,
145
153
  initiator,
146
154
  if (autoPlay) true else null,
147
- if (seekTo > 0) seekTo else null
155
+ if (seekTo > 0) seekTo else null,
156
+ contextJson
148
157
  )
149
158
  }
150
159
  }
@@ -155,14 +164,16 @@ class BBPlayerModule(private val reactContext: ReactApplicationContext) :
155
164
  clipListId: String,
156
165
  initiator: String?,
157
166
  autoPlay: Boolean,
158
- seekTo: Double
167
+ seekTo: Double,
168
+ contextJson: String?
159
169
  ) {
160
170
  runOnUiThread(viewTag.toInt()) {
161
171
  it.loadWithClipListId(
162
172
  clipListId,
163
173
  initiator,
164
174
  if (autoPlay) true else null,
165
- if (seekTo > 0) seekTo else null
175
+ if (seekTo > 0) seekTo else null,
176
+ contextJson
166
177
  )
167
178
  }
168
179
  }
@@ -173,14 +184,16 @@ class BBPlayerModule(private val reactContext: ReactApplicationContext) :
173
184
  projectId: String,
174
185
  initiator: String?,
175
186
  autoPlay: Boolean,
176
- seekTo: Double
187
+ seekTo: Double,
188
+ contextJson: String?
177
189
  ) {
178
190
  runOnUiThread(viewTag.toInt()) {
179
191
  it.loadWithProjectId(
180
192
  projectId,
181
193
  initiator,
182
194
  if (autoPlay) true else null,
183
- if (seekTo > 0) seekTo else null
195
+ if (seekTo > 0) seekTo else null,
196
+ contextJson
184
197
  )
185
198
  }
186
199
  }
@@ -191,14 +204,16 @@ class BBPlayerModule(private val reactContext: ReactApplicationContext) :
191
204
  clipJson: String,
192
205
  initiator: String?,
193
206
  autoPlay: Boolean,
194
- seekTo: Double
207
+ seekTo: Double,
208
+ contextJson: String?
195
209
  ) {
196
210
  runOnUiThread(viewTag.toInt()) {
197
211
  it.loadWithClipJson(
198
212
  clipJson,
199
213
  initiator,
200
214
  if (autoPlay) true else null,
201
- if (seekTo > 0) seekTo else null
215
+ if (seekTo > 0) seekTo else null,
216
+ contextJson
202
217
  )
203
218
  }
204
219
  }
@@ -209,14 +224,16 @@ class BBPlayerModule(private val reactContext: ReactApplicationContext) :
209
224
  clipListJson: String,
210
225
  initiator: String?,
211
226
  autoPlay: Boolean,
212
- seekTo: Double
227
+ seekTo: Double,
228
+ contextJson: String?
213
229
  ) {
214
230
  runOnUiThread(viewTag.toInt()) {
215
231
  it.loadWithClipListJson(
216
232
  clipListJson,
217
233
  initiator,
218
234
  if (autoPlay) true else null,
219
- if (seekTo > 0) seekTo else null
235
+ if (seekTo > 0) seekTo else null,
236
+ contextJson
220
237
  )
221
238
  }
222
239
  }
@@ -227,14 +244,16 @@ class BBPlayerModule(private val reactContext: ReactApplicationContext) :
227
244
  projectJson: String,
228
245
  initiator: String?,
229
246
  autoPlay: Boolean,
230
- seekTo: Double
247
+ seekTo: Double,
248
+ contextJson: String?
231
249
  ) {
232
250
  runOnUiThread(viewTag.toInt()) {
233
251
  it.loadWithProjectJson(
234
252
  projectJson,
235
253
  initiator,
236
254
  if (autoPlay) true else null,
237
- if (seekTo > 0) seekTo else null
255
+ if (seekTo > 0) seekTo else null,
256
+ contextJson
238
257
  )
239
258
  }
240
259
  }
@@ -243,37 +262,136 @@ class BBPlayerModule(private val reactContext: ReactApplicationContext) :
243
262
  override fun loadWithJsonUrl(
244
263
  viewTag: Double,
245
264
  jsonUrl: String,
246
- autoPlay: Boolean
265
+ autoPlay: Boolean,
266
+ contextJson: String?
247
267
  ) {
248
268
  runOnUiThread(viewTag.toInt()) {
249
- it.loadWithJsonUrl(jsonUrl, autoPlay)
269
+ it.loadWithJsonUrl(jsonUrl, autoPlay, contextJson)
250
270
  }
251
271
  }
252
272
 
253
- // Note: loadWithShortsId is NOT supported on BBPlayerView.
254
- // For Shorts playback, use the BBShortsView component instead.
273
+ @ReactMethod
274
+ override fun presentModal(viewTag: Double) {
275
+ runOnUiThread(viewTag.toInt()) { it.presentModal() }
276
+ }
255
277
 
256
- // Getter methods with Promise support
257
278
  @ReactMethod
258
- override fun getDuration(viewTag: Double, promise: Promise) {
279
+ override fun closeModal(viewTag: Double) {
280
+ runOnUiThread(viewTag.toInt()) { it.closeModal() }
281
+ }
282
+
283
+ // MARK: - Modal Player API (module-level, no React view needed)
284
+
285
+ private var modalPlayerView: BBNativePlayerView? = null
286
+
287
+ private fun emitEvent(eventName: String, params: com.facebook.react.bridge.WritableMap? = null) {
288
+ reactContext
289
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
290
+ .emit(eventName, params ?: Arguments.createMap())
291
+ }
292
+
293
+ private val modalPlayerDelegate = object : BBNativePlayerViewDelegate {
294
+ override fun didTriggerPlay(playerView: BBNativePlayerView) {
295
+ emitEvent("modalPlayerPlay")
296
+ }
297
+ override fun didTriggerPause(playerView: BBNativePlayerView) {
298
+ emitEvent("modalPlayerPause")
299
+ }
300
+ override fun didTriggerEnded(playerView: BBNativePlayerView) {
301
+ emitEvent("modalPlayerEnded")
302
+ }
303
+ override fun didFailWithError(playerView: BBNativePlayerView, error: String?) {
304
+ emitEvent("modalPlayerError", Arguments.createMap().apply {
305
+ putString("error", error ?: "Unknown error")
306
+ })
307
+ }
308
+ override fun didTriggerApiReady(playerView: BBNativePlayerView) {
309
+ emitEvent("modalPlayerApiReady")
310
+ }
311
+ override fun didTriggerCanPlay(playerView: BBNativePlayerView) {
312
+ emitEvent("modalPlayerCanPlay")
313
+ }
314
+ override fun didCloseModalPlayer(playerView: BBNativePlayerView) {
315
+ emitEvent("modalPlayerDismissed")
316
+ modalPlayerView = null
317
+ }
318
+ }
319
+
320
+ @ReactMethod
321
+ override fun presentModalPlayer(jsonUrl: String, optionsJson: String?) {
259
322
  UiThreadUtil.runOnUiThread {
260
- val view = findPlayerView(viewTag.toInt())
261
- if (view != null) {
262
- val duration = view.getDuration()
263
- promise.resolve(duration)
264
- } else {
265
- promise.resolve(null)
323
+ val activity = reactContext.currentActivity as? AppCompatActivity
324
+ if (activity == null) {
325
+ Log.w(NAME, "presentModalPlayer: no AppCompatActivity")
326
+ return@runOnUiThread
266
327
  }
328
+
329
+ // Parse options from JSON string
330
+ val options = mutableMapOf<String, Any?>()
331
+ if (optionsJson != null) {
332
+ try {
333
+ val json = JSONObject(optionsJson)
334
+ json.keys().forEach { key ->
335
+ options[key] = when {
336
+ json.isNull(key) -> null
337
+ else -> {
338
+ val value = json.get(key)
339
+ when (value) {
340
+ is Boolean -> value
341
+ is Int -> value
342
+ is Double -> value
343
+ is String -> value
344
+ else -> value.toString()
345
+ }
346
+ }
347
+ }
348
+ }
349
+ } catch (e: Exception) {
350
+ Log.w(NAME, "presentModalPlayer: failed to parse options JSON", e)
351
+ }
352
+ }
353
+
354
+ val playerView = BBNativePlayer.createModalPlayerView(activity, jsonUrl, options)
355
+ playerView.delegate = modalPlayerDelegate
356
+ modalPlayerView = playerView
357
+
358
+ Log.d(NAME, "Modal player presented with URL: $jsonUrl")
267
359
  }
268
360
  }
269
361
 
270
362
  @ReactMethod
271
- override fun getCurrentTime(viewTag: Double, promise: Promise) {
363
+ override fun dismissModalPlayer() {
364
+ UiThreadUtil.runOnUiThread {
365
+ // ModalActivity is the top activity — finish it to close the modal
366
+ val activity = reactContext.currentActivity
367
+ if (activity is com.bluebillywig.bbnativeplayersdk.ModalActivity) {
368
+ activity.finish()
369
+ }
370
+ modalPlayerView = null
371
+ }
372
+ }
373
+
374
+ @ReactMethod
375
+ override fun addListener(eventName: String) {
376
+ // Required for NativeEventEmitter
377
+ }
378
+
379
+ @ReactMethod
380
+ override fun removeListeners(count: Double) {
381
+ // Required for NativeEventEmitter
382
+ }
383
+
384
+ // Note: loadWithShortsId is NOT supported on BBPlayerView.
385
+ // For Shorts playback, use the BBShortsView component instead.
386
+
387
+ // Getter methods with Promise support
388
+ @ReactMethod
389
+ override fun getDuration(viewTag: Double, promise: Promise) {
272
390
  UiThreadUtil.runOnUiThread {
273
391
  val view = findPlayerView(viewTag.toInt())
274
392
  if (view != null) {
275
- val currentTime = view.getCurrentTime()
276
- promise.resolve(currentTime)
393
+ val duration = view.getDuration()
394
+ promise.resolve(duration)
277
395
  } else {
278
396
  promise.resolve(null)
279
397
  }