react-native-unified-player 0.3.9 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -82,13 +82,14 @@ export default MyPlayerComponent;
82
82
 
83
83
  | Prop | Type | Required | Description |
84
84
  |------|------|----------|-------------|
85
- | `videoUrl` | `string` | Yes | Video source URL |
85
+ | `videoUrl` | `string` \| `string[]` | Yes | Video source URL or an array of URLs for a playlist. |
86
86
  | `style` | `ViewStyle` | Yes | Apply custom styling |
87
87
  | `autoplay` | `boolean` | No | Autoplay video when loaded |
88
- | `loop` | `boolean` | No | Should video loop when finished |
88
+ | `loop` | `boolean` | No | Should the video/playlist loop when finished. **Note:** Playlist advancement and looping are handled in the JavaScript layer via the `onPlaybackComplete` callback. The native player only loops single videos based on this prop. |
89
+ | `onLoadStart` | `(event: { nativeEvent?: { index?: number } }) => void` | No | Callback when video begins loading. The `event.nativeEvent` may contain an `index` property on Android when playing a playlist. |
89
90
  | `onReadyToPlay` | `() => void` | No | Callback when video is ready to play |
90
91
  | `onError` | `(error: any) => void` | No | Callback when an error occurs |
91
- | `onPlaybackComplete` | `() => void` | No | Callback when video playback finishes |
92
+ | `onPlaybackComplete` | `() => void` | No | Callback when video playback finishes. Use this callback to implement playlist advancement logic in your JavaScript code. |
92
93
  | `onProgress` | `(data: { currentTime: number; duration: number }) => void` | No | Callback for playback progress |
93
94
 
94
95
  ## Events
@@ -21,7 +21,8 @@ class UnifiedPlayerEventEmitter(private val reactContext: ReactApplicationContex
21
21
  const val EVENT_RESUMED = "onPlaybackResumed"
22
22
  const val EVENT_PLAYING = "onPlaying"
23
23
  const val EVENT_PAUSED = "onPaused"
24
-
24
+ const val EVENT_FULLSCREEN_CHANGED = "onFullscreenChanged"
25
+
25
26
  // Singleton instance for access from other classes
26
27
  private var instance: UnifiedPlayerEventEmitter? = null
27
28
 
@@ -238,6 +238,32 @@ class UnifiedPlayerModule(private val reactContext: ReactApplicationContext) : R
238
238
  }
239
239
  }
240
240
 
241
+ @ReactMethod
242
+ fun toggleFullscreen(viewTag: Int, isFullscreen: Boolean, promise: Promise) {
243
+ Log.d(TAG, "Native toggleFullscreen method called with viewTag: $viewTag, isFullscreen: $isFullscreen")
244
+ try {
245
+ val playerView = getPlayerViewByTag(viewTag)
246
+ if (playerView != null) {
247
+ UiThreadUtil.runOnUiThread {
248
+ try {
249
+ playerView.setIsFullscreen(isFullscreen)
250
+ Log.d(TAG, "toggleFullscreen executed successfully")
251
+ promise.resolve(true)
252
+ } catch (e: Exception) {
253
+ Log.e(TAG, "Error toggling fullscreen: ${e.message}", e)
254
+ promise.reject("FULLSCREEN_ERROR", "Error toggling fullscreen: ${e.message}", e)
255
+ }
256
+ }
257
+ } else {
258
+ Log.e(TAG, "Player view not found for tag: $viewTag")
259
+ promise.reject("VIEW_NOT_FOUND", "Player view not found for tag: $viewTag")
260
+ }
261
+ } catch (e: Exception) {
262
+ Log.e(TAG, "Error in toggleFullscreen method: ${e.message}", e)
263
+ promise.reject("FULLSCREEN_ERROR", "Error in toggleFullscreen method: ${e.message}", e)
264
+ }
265
+ }
266
+
241
267
  @ReactMethod
242
268
  fun stopRecording(viewTag: Int, promise: Promise) {
243
269
  Log.d(TAG, "Native stopRecording method called with viewTag: $viewTag")
@@ -2,6 +2,8 @@ package com.unifiedplayer
2
2
 
3
3
  import android.annotation.SuppressLint
4
4
  import android.content.Context
5
+ import android.app.Activity
6
+ import android.content.pm.ActivityInfo
5
7
  import android.graphics.Bitmap
6
8
  import android.graphics.Canvas
7
9
  import android.graphics.Color
@@ -13,6 +15,7 @@ import android.os.Handler
13
15
  import android.os.Looper
14
16
  import android.view.Gravity
15
17
  import android.view.View
18
+ import android.view.Surface
16
19
  import android.widget.FrameLayout
17
20
  import android.widget.ImageView
18
21
  import com.bumptech.glide.Glide
@@ -30,11 +33,11 @@ import com.facebook.react.bridge.ReactContext
30
33
  import com.facebook.react.uimanager.events.RCTEventEmitter
31
34
  import java.io.File
32
35
  import java.io.IOException
36
+ import java.util.Collections // Import for Collections.emptyList()
33
37
  import android.media.MediaCodec
34
38
  import android.media.MediaCodecInfo
35
39
  import android.media.MediaFormat
36
40
  import android.media.MediaMuxer
37
- import android.view.Surface
38
41
  import java.nio.ByteBuffer
39
42
  import android.os.Environment
40
43
  import com.unifiedplayer.UnifiedPlayerEventEmitter.Companion.EVENT_COMPLETE
@@ -46,6 +49,10 @@ import com.unifiedplayer.UnifiedPlayerEventEmitter.Companion.EVENT_PROGRESS
46
49
  import com.unifiedplayer.UnifiedPlayerEventEmitter.Companion.EVENT_READY
47
50
  import com.unifiedplayer.UnifiedPlayerEventEmitter.Companion.EVENT_RESUMED
48
51
  import com.unifiedplayer.UnifiedPlayerEventEmitter.Companion.EVENT_STALLED
52
+ import com.unifiedplayer.UnifiedPlayerEventEmitter.Companion.EVENT_FULLSCREEN_CHANGED
53
+ import android.view.ViewGroup
54
+ import android.view.WindowManager
55
+ import android.os.Build
49
56
 
50
57
  class UnifiedPlayerView(context: Context) : FrameLayout(context) {
51
58
  // Recording related variables
@@ -61,7 +68,11 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
61
68
  private const val TAG = "UnifiedPlayerView"
62
69
  }
63
70
 
64
- private var videoUrl: String? = null
71
+ // Player state
72
+ private var videoUrl: String? = null // Single video URL
73
+ private var videoUrls: List<String> = emptyList() // Playlist URLs
74
+ private var currentVideoIndex: Int = 0
75
+ private var isPlaylist: Boolean = false
65
76
  private var thumbnailUrl: String? = null
66
77
  private var autoplay: Boolean = true
67
78
  private var loop: Boolean = false
@@ -70,6 +81,13 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
70
81
  internal var player: ExoPlayer? = null
71
82
  private var currentProgress = 0
72
83
  private var isPaused = false
84
+ private var isFullscreen = false
85
+ private var originalOrientation: Int = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
86
+ private var originalSystemUiVisibility: Int = 0
87
+ private var originalLayoutParams: ViewGroup.LayoutParams? = null
88
+ private var originalParent: ViewGroup? = null
89
+ private var originalIndex: Int = 0
90
+ private var fullscreenContainer: ViewGroup? = null
73
91
 
74
92
  private val progressHandler = Handler(Looper.getMainLooper())
75
93
  private val progressRunnable: Runnable = object : Runnable {
@@ -79,7 +97,7 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
79
97
  val duration = it.duration.toFloat() / 1000f
80
98
 
81
99
  // Log the actual values for debugging
82
- Log.d(TAG, "Progress values - currentTime: $currentTime, duration: $duration, raw duration: ${it.duration}")
100
+ // Log.d(TAG, "Progress values - currentTime: $currentTime, duration: $duration, raw duration: ${it.duration}")
83
101
 
84
102
  // Only send valid duration values
85
103
  if (it.duration > 0) {
@@ -87,10 +105,10 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
87
105
  event.putDouble("currentTime", currentTime.toDouble())
88
106
  event.putDouble("duration", duration.toDouble())
89
107
 
90
- Log.d(TAG, "Sending progress event: currentTime=$currentTime, duration=$duration")
108
+ // Log.d(TAG, "Sending progress event: currentTime=$currentTime, duration=$duration")
91
109
  sendEvent(EVENT_PROGRESS, event)
92
110
  } else {
93
- Log.d(TAG, "Not sending progress event because duration is $duration (raw: ${it.duration})")
111
+ // Log.d(TAG, "Not sending progress event because duration is $duration (raw: ${it.duration})")
94
112
  }
95
113
  } ?: Log.e(TAG, "Cannot send progress event: player is null")
96
114
 
@@ -137,15 +155,52 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
137
155
 
138
156
  player?.addListener(object : Player.Listener {
139
157
  override fun onPlaybackStateChanged(playbackState: Int) {
140
- Log.d(TAG, "onPlaybackStateChanged: $playbackState") // Added log
158
+ Log.d(TAG, "onPlaybackStateChanged: $playbackState")
141
159
  when (playbackState) {
142
160
  Player.STATE_READY -> {
143
161
  Log.d(TAG, "ExoPlayer STATE_READY")
162
+ // Ensure thumbnail is hidden when ready (might be needed if autoplay=false)
163
+ if (player?.isPlaying == false) { // Check if not already playing
164
+ thumbnailImageView?.visibility = View.GONE
165
+ }
144
166
  sendEvent(EVENT_READY, Arguments.createMap())
167
+ // Start progress updates when ready
168
+ startProgressUpdates()
145
169
  }
146
170
  Player.STATE_ENDED -> {
147
171
  Log.d(TAG, "ExoPlayer STATE_ENDED")
148
- sendEvent(EVENT_COMPLETE, Arguments.createMap())
172
+ if (isPlaylist) {
173
+ // Playlist logic
174
+ val nextIndex = currentVideoIndex + 1
175
+ if (nextIndex < videoUrls.size) {
176
+ // Play next video in the list
177
+ Log.d(TAG, "Playlist: Loading next video at index $nextIndex")
178
+ loadVideoAtIndex(nextIndex)
179
+ // Don't send EVENT_COMPLETE for individual items in playlist
180
+ } else {
181
+ // Reached the end of the playlist
182
+ if (loop) {
183
+ // Loop playlist: Go back to the first video
184
+ Log.d(TAG, "Playlist: Looping back to start")
185
+ loadVideoAtIndex(0)
186
+ // Don't send EVENT_COMPLETE when looping playlist
187
+ } else {
188
+ // End of playlist, not looping
189
+ Log.d(TAG, "Playlist: Reached end, not looping")
190
+ currentVideoIndex = 0 // Reset index for potential future play
191
+ sendEvent(EVENT_COMPLETE, Arguments.createMap()) // Send completion for the whole list
192
+ }
193
+ }
194
+ } else {
195
+ // Single video logic (ExoPlayer handles looping via repeatMode)
196
+ if (!loop) {
197
+ // Send completion event only if not looping a single video
198
+ sendEvent(EVENT_COMPLETE, Arguments.createMap())
199
+ } else {
200
+ Log.d(TAG, "Single video ended and loop is ON - ExoPlayer will repeat.")
201
+ // Optionally send an event here if needed for single loop cycle completion
202
+ }
203
+ }
149
204
  }
150
205
  Player.STATE_BUFFERING -> {
151
206
  Log.d(TAG, "ExoPlayer STATE_BUFFERING")
@@ -225,55 +280,34 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
225
280
  })
226
281
  }
227
282
 
228
- fun setVideoUrl(url: String?) {
229
- Log.d(TAG, "Setting video URL: $url")
230
-
231
- if (url == null || url.isEmpty()) {
232
- Log.e(TAG, "Empty or null URL provided")
233
- return
234
- }
235
-
236
- videoUrl = url
237
-
283
+ // Helper function to load and prepare a video URL
284
+ private fun loadVideoSource(url: String) {
285
+ Log.d(TAG, "Loading video source: $url")
238
286
  try {
239
- // Create a MediaItem
240
287
  val mediaItem = MediaItem.fromUri(url)
241
-
242
- // Reset the player to ensure clean state
243
- player?.stop()
244
- player?.clearMediaItems()
245
-
246
- // Set the media item
288
+ player?.stop() // Stop previous playback
289
+ player?.clearMediaItems() // Clear previous items
247
290
  player?.setMediaItem(mediaItem)
248
-
249
- // Prepare the player (this will start loading the media)
250
291
  player?.prepare()
292
+ player?.playWhenReady = autoplay && !isPaused // Apply autoplay and paused state
251
293
 
252
- // Set playWhenReady based on autoplay setting
253
- player?.playWhenReady = autoplay && !isPaused
254
-
255
- // Set repeat mode based on loop setting
256
- player?.repeatMode = if (loop) Player.REPEAT_MODE_ALL else Player.REPEAT_MODE_OFF
257
-
258
- // Log that we've set up the player
259
- Log.d(TAG, "ExoPlayer configured with URL: $url, autoplay: $autoplay, loop: $loop")
260
-
261
- // Add a listener to check when the player is ready and has duration
262
- player?.addListener(object : Player.Listener {
263
- override fun onPlaybackStateChanged(state: Int) {
264
- if (state == Player.STATE_READY) {
265
- val duration = player?.duration ?: 0
266
- Log.d(TAG, "Player ready with duration: ${duration / 1000f} seconds")
267
-
268
- // Force a progress update immediately
269
- progressRunnable.run()
270
- }
271
- }
272
- })
294
+ // Explicitly set repeat mode here based on current state
295
+ if (isPlaylist) {
296
+ player?.repeatMode = Player.REPEAT_MODE_OFF // Force OFF for playlists
297
+ } else {
298
+ player?.repeatMode = if (loop) Player.REPEAT_MODE_ONE else Player.REPEAT_MODE_OFF // Use loop prop for single videos
299
+ }
300
+
301
+ Log.d(TAG, "ExoPlayer configured with URL: $url, autoplay: $autoplay, loop: $loop, isPaused: $isPaused, repeatMode: ${player?.repeatMode}")
302
+ // Send load start event, include index if it's a playlist
303
+ val loadStartEvent = Arguments.createMap()
304
+ if (isPlaylist) {
305
+ loadStartEvent.putInt("index", currentVideoIndex)
306
+ }
307
+ sendEvent(EVENT_LOAD_START, loadStartEvent)
308
+
273
309
  } catch (e: Exception) {
274
- Log.e(TAG, "Error setting video URL: ${e.message}", e)
275
-
276
- // Send error event
310
+ Log.e(TAG, "Error setting video source: ${e.message}", e)
277
311
  val event = Arguments.createMap()
278
312
  event.putString("code", "SOURCE_ERROR")
279
313
  event.putString("message", "Failed to load video source: $url")
@@ -281,14 +315,71 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
281
315
  }
282
316
  }
283
317
 
318
+ // Method to load a specific video from the playlist
319
+ private fun loadVideoAtIndex(index: Int) {
320
+ if (index >= 0 && index < videoUrls.size) {
321
+ currentVideoIndex = index
322
+ val url = videoUrls[index]
323
+ Log.d(TAG, "Loading playlist item at index $index: $url")
324
+ loadVideoSource(url)
325
+ } else {
326
+ Log.e(TAG, "Invalid index $index for playlist size ${videoUrls.size}")
327
+ }
328
+ }
329
+
330
+ // Called by ViewManager for single URL
331
+ fun setVideoUrl(url: String?) {
332
+ Log.d(TAG, "Setting single video URL: $url")
333
+ isPlaylist = false // Mark as not a playlist
334
+ videoUrls = emptyList() // Clear any previous playlist
335
+ currentVideoIndex = 0
336
+
337
+ if (url != null && url.isNotEmpty()) { // Check for non-null and non-empty
338
+ videoUrl = url // Store the non-null url
339
+ loadVideoSource(url) // Call loadVideoSource only when url is guaranteed non-null
340
+ } else {
341
+ Log.w(TAG, "Received null or empty URL for single video.")
342
+ player?.stop()
343
+ player?.clearMediaItems()
344
+ videoUrl = null // Ensure internal state is cleared
345
+ // Optionally show thumbnail or placeholder if URL is cleared
346
+ }
347
+ }
348
+
349
+ // Called by ViewManager for URL list (playlist)
350
+ fun setVideoUrls(urls: List<String>) {
351
+ Log.d(TAG, "Setting video URL list (playlist) with ${urls.size} items.")
352
+ if (urls.isEmpty()) {
353
+ Log.w(TAG, "Received empty URL list.")
354
+ setVideoUrl(null) // Treat empty list as clearing the source
355
+ return
356
+ }
357
+ isPlaylist = true // Mark as a playlist
358
+ videoUrl = null // Clear single video URL
359
+ videoUrls = urls
360
+ currentVideoIndex = 0 // Start from the beginning
361
+
362
+ // Load the first video in the playlist
363
+ loadVideoAtIndex(currentVideoIndex)
364
+ }
365
+
284
366
  fun setAutoplay(value: Boolean) {
285
367
  autoplay = value
286
368
  player?.playWhenReady = value
287
369
  }
288
370
 
289
371
  fun setLoop(value: Boolean) {
372
+ Log.d(TAG, "Setting loop to: $value, isPlaylist: $isPlaylist")
290
373
  loop = value
291
- player?.repeatMode = if (loop) Player.REPEAT_MODE_ALL else Player.REPEAT_MODE_OFF
374
+ // Only set ExoPlayer's repeatMode if NOT in playlist mode.
375
+ // Playlist looping is handled manually in onPlaybackStateChanged.
376
+ if (!isPlaylist) {
377
+ // Use REPEAT_MODE_ONE for single item looping
378
+ player?.repeatMode = if (loop) Player.REPEAT_MODE_ONE else Player.REPEAT_MODE_OFF
379
+ } else {
380
+ // Ensure repeat mode is off when handling playlists manually
381
+ player?.repeatMode = Player.REPEAT_MODE_OFF
382
+ }
292
383
  }
293
384
 
294
385
  fun setThumbnailUrl(url: String?) {
@@ -328,6 +419,89 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
328
419
  }
329
420
  }
330
421
 
422
+ fun setIsFullscreen(fullscreen: Boolean) {
423
+ Log.d(TAG, "setIsFullscreen called with value: $fullscreen")
424
+ if (this.isFullscreen == fullscreen) {
425
+ return // Already in the requested state
426
+ }
427
+
428
+ this.isFullscreen = fullscreen
429
+ val reactContext = context as? ReactContext ?: return
430
+ val activity = reactContext.currentActivity ?: return
431
+
432
+ if (fullscreen) {
433
+ enterFullscreen(activity)
434
+ } else {
435
+ exitFullscreen(activity)
436
+ }
437
+
438
+ // Send event about fullscreen state change
439
+ val event = Arguments.createMap()
440
+ event.putBoolean("isFullscreen", fullscreen)
441
+ sendEvent(EVENT_FULLSCREEN_CHANGED, event)
442
+ }
443
+
444
+ private fun enterFullscreen(activity: Activity) {
445
+ Log.d(TAG, "Entering fullscreen mode")
446
+
447
+ // Save current orientation
448
+ originalOrientation = activity.requestedOrientation
449
+
450
+ // Force landscape orientation
451
+ activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
452
+
453
+ // Hide system UI for fullscreen
454
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
455
+ activity.window.insetsController?.let { controller ->
456
+ controller.hide(android.view.WindowInsets.Type.statusBars())
457
+ controller.hide(android.view.WindowInsets.Type.navigationBars())
458
+ controller.systemBarsBehavior = android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
459
+ }
460
+ } else {
461
+ @Suppress("DEPRECATION")
462
+ originalSystemUiVisibility = activity.window.decorView.systemUiVisibility
463
+ @Suppress("DEPRECATION")
464
+ activity.window.decorView.systemUiVisibility = (
465
+ android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
466
+ or android.view.View.SYSTEM_UI_FLAG_FULLSCREEN
467
+ or android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
468
+ or android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE
469
+ or android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
470
+ or android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
471
+ )
472
+ }
473
+
474
+ // Add FLAG_KEEP_SCREEN_ON to prevent screen from turning off during playback
475
+ activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
476
+
477
+ // Simply inform React Native that we want fullscreen
478
+ // The React Native side should handle hiding other UI elements
479
+ Log.d(TAG, "Fullscreen mode activated - orientation changed to landscape")
480
+ }
481
+
482
+ private fun exitFullscreen(activity: Activity) {
483
+ Log.d(TAG, "Exiting fullscreen mode")
484
+
485
+ // Force back to portrait orientation
486
+ activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
487
+
488
+ // Restore system UI
489
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
490
+ activity.window.insetsController?.let { controller ->
491
+ controller.show(android.view.WindowInsets.Type.statusBars())
492
+ controller.show(android.view.WindowInsets.Type.navigationBars())
493
+ }
494
+ } else {
495
+ @Suppress("DEPRECATION")
496
+ activity.window.decorView.systemUiVisibility = originalSystemUiVisibility
497
+ }
498
+
499
+ // Remove FLAG_KEEP_SCREEN_ON
500
+ activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
501
+
502
+ Log.d(TAG, "Fullscreen mode exited - orientation restored")
503
+ }
504
+
331
505
  fun play() {
332
506
  Log.d(TAG, "Play method called")
333
507
  player?.playWhenReady = true
@@ -346,7 +520,7 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
346
520
  it.seekTo(milliseconds)
347
521
 
348
522
  // Force a progress update after seeking
349
- progressRunnable.run()
523
+ progressRunnable.run()
350
524
  } ?: Log.e(TAG, "Cannot seek: player is null")
351
525
  }
352
526
 
@@ -381,7 +555,7 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
381
555
  private fun sendEvent(eventName: String, params: WritableMap) {
382
556
  try {
383
557
  // Log the event for debugging
384
- Log.d(TAG, "Sending direct event: $eventName with params: $params")
558
+ // Log.d(TAG, "Sending direct event: $eventName with params: $params")
385
559
 
386
560
  // Map event names to their corresponding top event names
387
561
  val topEventName = when (eventName) {
@@ -394,6 +568,7 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
394
568
  EVENT_PLAYING -> "topPlaying"
395
569
  EVENT_PAUSED -> "topPlaybackPaused"
396
570
  EVENT_LOAD_START -> "topLoadStart"
571
+ EVENT_FULLSCREEN_CHANGED -> "topFullscreenChanged"
397
572
  else -> "top${eventName.substring(2)}" // Fallback for any other events
398
573
  }
399
574
 
@@ -406,13 +581,22 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
406
581
  }
407
582
  }
408
583
 
409
- // Add a method to explicitly start progress updates
584
+ // Method to explicitly start progress updates
410
585
  private fun startProgressUpdates() {
411
- Log.d(TAG, "Starting progress updates")
412
- // Remove any existing callbacks to avoid duplicates
586
+ // Only start if player is ready and has duration
587
+ if (player?.playbackState == Player.STATE_READY && (player?.duration ?: 0) > 0) {
588
+ Log.d(TAG, "Starting progress updates")
589
+ progressHandler.removeCallbacks(progressRunnable) // Remove existing callbacks
590
+ progressHandler.post(progressRunnable) // Post the runnable
591
+ } else {
592
+ Log.d(TAG, "Skipping progress updates start: Player not ready or duration is 0")
593
+ }
594
+ }
595
+
596
+ // Method to stop progress updates
597
+ private fun stopProgressUpdates() {
598
+ Log.d(TAG, "Stopping progress updates")
413
599
  progressHandler.removeCallbacks(progressRunnable)
414
- // Post the runnable to start updates
415
- progressHandler.post(progressRunnable)
416
600
  }
417
601
 
418
602
  override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
@@ -453,14 +637,17 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
453
637
  // Log.d(TAG, "TextureView onSurfaceTextureUpdated")
454
638
  }
455
639
  }
456
- startProgressUpdates() // Use the new method to start progress updates
640
+ // Don't start progress updates here automatically, wait for STATE_READY
641
+ // startProgressUpdates()
457
642
  }
458
643
 
459
644
  override fun onDetachedFromWindow() {
460
645
  super.onDetachedFromWindow()
461
646
  Log.d(TAG, "UnifiedPlayerView onDetachedFromWindow")
462
- progressHandler.removeCallbacks(progressRunnable) // Stop progress updates
647
+ stopProgressUpdates() // Stop progress updates
463
648
  player?.release()
649
+ player = null // Ensure player is nullified
650
+ cleanupRecording() // Clean up recording resources if any
464
651
  }
465
652
 
466
653
  fun capture(): String {
@@ -6,6 +6,8 @@ import com.facebook.react.uimanager.annotations.ReactProp
6
6
  import com.facebook.react.common.MapBuilder
7
7
  import android.util.Log
8
8
  import com.facebook.react.bridge.ReadableArray
9
+ import com.facebook.react.bridge.Dynamic // Import Dynamic
10
+ import com.facebook.react.bridge.ReadableType // Import ReadableType
9
11
  import com.facebook.react.uimanager.events.RCTEventEmitter
10
12
 
11
13
  class UnifiedPlayerViewManager : SimpleViewManager<UnifiedPlayerView>() {
@@ -19,8 +21,38 @@ class UnifiedPlayerViewManager : SimpleViewManager<UnifiedPlayerView>() {
19
21
  }
20
22
 
21
23
  @ReactProp(name = "videoUrl")
22
- fun setVideoUrl(view: UnifiedPlayerView, url: String?) {
23
- view.setVideoUrl(url)
24
+ fun setVideoUrl(view: UnifiedPlayerView, videoUrl: Dynamic?) {
25
+ if (videoUrl == null) {
26
+ view.setVideoUrl(null)
27
+ return
28
+ }
29
+
30
+ when (videoUrl.type) {
31
+ ReadableType.String -> {
32
+ view.setVideoUrl(videoUrl.asString())
33
+ }
34
+ ReadableType.Array -> {
35
+ val urlList = mutableListOf<String>()
36
+ val array = videoUrl.asArray()
37
+ for (i in 0 until array.size()) {
38
+ if (array.getType(i) == ReadableType.String) {
39
+ val urlString = array.getString(i) // Get nullable string
40
+ if (urlString != null) { // Check if it's not null
41
+ urlList.add(urlString) // Add the non-null string
42
+ } else {
43
+ Log.w(TAG, "Null string found in videoUrl array at index $i.")
44
+ }
45
+ } else {
46
+ Log.w(TAG, "Invalid type in videoUrl array at index $i. Expected String.")
47
+ }
48
+ }
49
+ view.setVideoUrls(urlList) // Call the new method for playlists
50
+ }
51
+ else -> {
52
+ Log.w(TAG, "Invalid type for videoUrl prop. Expected String or Array.")
53
+ view.setVideoUrl(null) // Or handle error appropriately
54
+ }
55
+ }
24
56
  }
25
57
 
26
58
  @ReactProp(name = "thumbnailUrl")
@@ -43,6 +75,11 @@ class UnifiedPlayerViewManager : SimpleViewManager<UnifiedPlayerView>() {
43
75
  view.setIsPaused(isPaused)
44
76
  }
45
77
 
78
+ @ReactProp(name = "isFullscreen")
79
+ fun setIsFullscreen(view: UnifiedPlayerView, isFullscreen: Boolean) {
80
+ view.setIsFullscreen(isFullscreen)
81
+ }
82
+
46
83
 
47
84
 
48
85
  // Register direct events
@@ -60,6 +97,7 @@ class UnifiedPlayerViewManager : SimpleViewManager<UnifiedPlayerView>() {
60
97
  .put("topPlaybackPaused", MapBuilder.of("registrationName", "onPaused"))
61
98
  .put("topPlaying", MapBuilder.of("registrationName", "onPlaying"))
62
99
  .put("topLoadStart", MapBuilder.of("registrationName", "onLoadStart"))
100
+ .put("topFullscreenChanged", MapBuilder.of("registrationName", "onFullscreenChanged"))
63
101
  .build()
64
102
  }
65
103
  }
@@ -248,6 +248,25 @@ RCT_EXPORT_METHOD(startRecording:(nonnull NSNumber *)reactTag
248
248
  }];
249
249
  }
250
250
 
251
+ RCT_EXPORT_METHOD(toggleFullscreen:(nonnull NSNumber *)reactTag
252
+ isFullscreen:(BOOL)isFullscreen
253
+ resolver:(RCTPromiseResolveBlock)resolve
254
+ rejecter:(RCTPromiseRejectBlock)reject)
255
+ {
256
+ [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
257
+ UIView *view = viewRegistry[reactTag];
258
+ if (![view isKindOfClass:[UnifiedPlayerUIView class]]) {
259
+ RCTLogError(@"Invalid view returned from registry, expecting UnifiedPlayerUIView, got: %@", view);
260
+ reject(@"E_INVALID_VIEW", @"Expected UnifiedPlayerUIView", nil);
261
+ return;
262
+ }
263
+
264
+ UnifiedPlayerUIView *playerView = (UnifiedPlayerUIView *)view;
265
+ [playerView toggleFullscreen:isFullscreen];
266
+ resolve(@(YES));
267
+ }];
268
+ }
269
+
251
270
  RCT_EXPORT_METHOD(stopRecording:(nonnull NSNumber *)reactTag
252
271
  resolver:(RCTPromiseResolveBlock)resolve
253
272
  rejecter:(RCTPromiseRejectBlock)reject)
@@ -10,11 +10,15 @@ NS_ASSUME_NONNULL_BEGIN
10
10
  @interface UnifiedPlayerUIView : UIView <VLCMediaPlayerDelegate>
11
11
 
12
12
  @property (nonatomic, strong) VLCMediaPlayer *player;
13
- @property (nonatomic, copy) NSString *videoUrlString;
14
- @property (nonatomic, copy) NSString *thumbnailUrlString;
13
+ @property (nonatomic, copy, nullable) NSString *videoUrlString; // Single URL
14
+ @property (nonatomic, copy, nullable) NSArray<NSString *> *videoUrlArray; // Playlist URLs
15
+ @property (nonatomic, assign) NSInteger currentVideoIndex;
16
+ @property (nonatomic, assign) BOOL isPlaylist;
17
+ @property (nonatomic, copy, nullable) NSString *thumbnailUrlString;
15
18
  @property (nonatomic, assign) BOOL autoplay;
16
19
  @property (nonatomic, assign) BOOL loop;
17
20
  @property (nonatomic, assign) BOOL isPaused;
21
+ @property (nonatomic, assign) BOOL isFullscreen;
18
22
  @property (nonatomic, strong) NSArray *mediaOptions;
19
23
  @property (nonatomic, weak) RCTBridge *bridge;
20
24
  @property (nonatomic, assign) VLCMediaPlayerState previousState;
@@ -38,10 +42,13 @@ NS_ASSUME_NONNULL_BEGIN
38
42
  @property (nonatomic, copy) RCTDirectEventBlock onPlaybackResumed;
39
43
  @property (nonatomic, copy) RCTDirectEventBlock onPlaying;
40
44
  @property (nonatomic, copy) RCTDirectEventBlock onPaused;
45
+ @property (nonatomic, copy) RCTDirectEventBlock onFullscreenChanged;
41
46
 
42
47
  // Method declarations
43
- - (void)setupWithVideoUrlString:(NSString *)videoUrlString;
44
- - (void)setupThumbnailWithUrlString:(NSString *)thumbnailUrlString;
48
+ - (void)toggleFullscreen:(BOOL)fullscreen;
49
+ - (void)setupWithVideoUrlString:(nullable NSString *)videoUrlString;
50
+ - (void)setupWithVideoUrlArray:(NSArray<NSString *> *)urlArray; // New method for playlists
51
+ - (void)setupThumbnailWithUrlString:(nullable NSString *)thumbnailUrlString;
45
52
  - (void)play;
46
53
  - (void)pause;
47
54
  - (void)seekToTime:(NSNumber *)timeNumber;