react-native-unified-player 0.3.9 → 0.3.10

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.
@@ -30,6 +30,7 @@ import com.facebook.react.bridge.ReactContext
30
30
  import com.facebook.react.uimanager.events.RCTEventEmitter
31
31
  import java.io.File
32
32
  import java.io.IOException
33
+ import java.util.Collections // Import for Collections.emptyList()
33
34
  import android.media.MediaCodec
34
35
  import android.media.MediaCodecInfo
35
36
  import android.media.MediaFormat
@@ -61,7 +62,11 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
61
62
  private const val TAG = "UnifiedPlayerView"
62
63
  }
63
64
 
64
- private var videoUrl: String? = null
65
+ // Player state
66
+ private var videoUrl: String? = null // Single video URL
67
+ private var videoUrls: List<String> = emptyList() // Playlist URLs
68
+ private var currentVideoIndex: Int = 0
69
+ private var isPlaylist: Boolean = false
65
70
  private var thumbnailUrl: String? = null
66
71
  private var autoplay: Boolean = true
67
72
  private var loop: Boolean = false
@@ -79,7 +84,7 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
79
84
  val duration = it.duration.toFloat() / 1000f
80
85
 
81
86
  // Log the actual values for debugging
82
- Log.d(TAG, "Progress values - currentTime: $currentTime, duration: $duration, raw duration: ${it.duration}")
87
+ // Log.d(TAG, "Progress values - currentTime: $currentTime, duration: $duration, raw duration: ${it.duration}")
83
88
 
84
89
  // Only send valid duration values
85
90
  if (it.duration > 0) {
@@ -87,10 +92,10 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
87
92
  event.putDouble("currentTime", currentTime.toDouble())
88
93
  event.putDouble("duration", duration.toDouble())
89
94
 
90
- Log.d(TAG, "Sending progress event: currentTime=$currentTime, duration=$duration")
95
+ // Log.d(TAG, "Sending progress event: currentTime=$currentTime, duration=$duration")
91
96
  sendEvent(EVENT_PROGRESS, event)
92
97
  } else {
93
- Log.d(TAG, "Not sending progress event because duration is $duration (raw: ${it.duration})")
98
+ // Log.d(TAG, "Not sending progress event because duration is $duration (raw: ${it.duration})")
94
99
  }
95
100
  } ?: Log.e(TAG, "Cannot send progress event: player is null")
96
101
 
@@ -137,15 +142,52 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
137
142
 
138
143
  player?.addListener(object : Player.Listener {
139
144
  override fun onPlaybackStateChanged(playbackState: Int) {
140
- Log.d(TAG, "onPlaybackStateChanged: $playbackState") // Added log
145
+ Log.d(TAG, "onPlaybackStateChanged: $playbackState")
141
146
  when (playbackState) {
142
147
  Player.STATE_READY -> {
143
148
  Log.d(TAG, "ExoPlayer STATE_READY")
149
+ // Ensure thumbnail is hidden when ready (might be needed if autoplay=false)
150
+ if (player?.isPlaying == false) { // Check if not already playing
151
+ thumbnailImageView?.visibility = View.GONE
152
+ }
144
153
  sendEvent(EVENT_READY, Arguments.createMap())
154
+ // Start progress updates when ready
155
+ startProgressUpdates()
145
156
  }
146
157
  Player.STATE_ENDED -> {
147
158
  Log.d(TAG, "ExoPlayer STATE_ENDED")
148
- sendEvent(EVENT_COMPLETE, Arguments.createMap())
159
+ if (isPlaylist) {
160
+ // Playlist logic
161
+ val nextIndex = currentVideoIndex + 1
162
+ if (nextIndex < videoUrls.size) {
163
+ // Play next video in the list
164
+ Log.d(TAG, "Playlist: Loading next video at index $nextIndex")
165
+ loadVideoAtIndex(nextIndex)
166
+ // Don't send EVENT_COMPLETE for individual items in playlist
167
+ } else {
168
+ // Reached the end of the playlist
169
+ if (loop) {
170
+ // Loop playlist: Go back to the first video
171
+ Log.d(TAG, "Playlist: Looping back to start")
172
+ loadVideoAtIndex(0)
173
+ // Don't send EVENT_COMPLETE when looping playlist
174
+ } else {
175
+ // End of playlist, not looping
176
+ Log.d(TAG, "Playlist: Reached end, not looping")
177
+ currentVideoIndex = 0 // Reset index for potential future play
178
+ sendEvent(EVENT_COMPLETE, Arguments.createMap()) // Send completion for the whole list
179
+ }
180
+ }
181
+ } else {
182
+ // Single video logic (ExoPlayer handles looping via repeatMode)
183
+ if (!loop) {
184
+ // Send completion event only if not looping a single video
185
+ sendEvent(EVENT_COMPLETE, Arguments.createMap())
186
+ } else {
187
+ Log.d(TAG, "Single video ended and loop is ON - ExoPlayer will repeat.")
188
+ // Optionally send an event here if needed for single loop cycle completion
189
+ }
190
+ }
149
191
  }
150
192
  Player.STATE_BUFFERING -> {
151
193
  Log.d(TAG, "ExoPlayer STATE_BUFFERING")
@@ -225,55 +267,34 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
225
267
  })
226
268
  }
227
269
 
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
-
270
+ // Helper function to load and prepare a video URL
271
+ private fun loadVideoSource(url: String) {
272
+ Log.d(TAG, "Loading video source: $url")
238
273
  try {
239
- // Create a MediaItem
240
274
  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
275
+ player?.stop() // Stop previous playback
276
+ player?.clearMediaItems() // Clear previous items
247
277
  player?.setMediaItem(mediaItem)
248
-
249
- // Prepare the player (this will start loading the media)
250
278
  player?.prepare()
279
+ player?.playWhenReady = autoplay && !isPaused // Apply autoplay and paused state
251
280
 
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
- })
281
+ // Explicitly set repeat mode here based on current state
282
+ if (isPlaylist) {
283
+ player?.repeatMode = Player.REPEAT_MODE_OFF // Force OFF for playlists
284
+ } else {
285
+ player?.repeatMode = if (loop) Player.REPEAT_MODE_ONE else Player.REPEAT_MODE_OFF // Use loop prop for single videos
286
+ }
287
+
288
+ Log.d(TAG, "ExoPlayer configured with URL: $url, autoplay: $autoplay, loop: $loop, isPaused: $isPaused, repeatMode: ${player?.repeatMode}")
289
+ // Send load start event, include index if it's a playlist
290
+ val loadStartEvent = Arguments.createMap()
291
+ if (isPlaylist) {
292
+ loadStartEvent.putInt("index", currentVideoIndex)
293
+ }
294
+ sendEvent(EVENT_LOAD_START, loadStartEvent)
295
+
273
296
  } catch (e: Exception) {
274
- Log.e(TAG, "Error setting video URL: ${e.message}", e)
275
-
276
- // Send error event
297
+ Log.e(TAG, "Error setting video source: ${e.message}", e)
277
298
  val event = Arguments.createMap()
278
299
  event.putString("code", "SOURCE_ERROR")
279
300
  event.putString("message", "Failed to load video source: $url")
@@ -281,14 +302,71 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
281
302
  }
282
303
  }
283
304
 
305
+ // Method to load a specific video from the playlist
306
+ private fun loadVideoAtIndex(index: Int) {
307
+ if (index >= 0 && index < videoUrls.size) {
308
+ currentVideoIndex = index
309
+ val url = videoUrls[index]
310
+ Log.d(TAG, "Loading playlist item at index $index: $url")
311
+ loadVideoSource(url)
312
+ } else {
313
+ Log.e(TAG, "Invalid index $index for playlist size ${videoUrls.size}")
314
+ }
315
+ }
316
+
317
+ // Called by ViewManager for single URL
318
+ fun setVideoUrl(url: String?) {
319
+ Log.d(TAG, "Setting single video URL: $url")
320
+ isPlaylist = false // Mark as not a playlist
321
+ videoUrls = emptyList() // Clear any previous playlist
322
+ currentVideoIndex = 0
323
+
324
+ if (url != null && url.isNotEmpty()) { // Check for non-null and non-empty
325
+ videoUrl = url // Store the non-null url
326
+ loadVideoSource(url) // Call loadVideoSource only when url is guaranteed non-null
327
+ } else {
328
+ Log.w(TAG, "Received null or empty URL for single video.")
329
+ player?.stop()
330
+ player?.clearMediaItems()
331
+ videoUrl = null // Ensure internal state is cleared
332
+ // Optionally show thumbnail or placeholder if URL is cleared
333
+ }
334
+ }
335
+
336
+ // Called by ViewManager for URL list (playlist)
337
+ fun setVideoUrls(urls: List<String>) {
338
+ Log.d(TAG, "Setting video URL list (playlist) with ${urls.size} items.")
339
+ if (urls.isEmpty()) {
340
+ Log.w(TAG, "Received empty URL list.")
341
+ setVideoUrl(null) // Treat empty list as clearing the source
342
+ return
343
+ }
344
+ isPlaylist = true // Mark as a playlist
345
+ videoUrl = null // Clear single video URL
346
+ videoUrls = urls
347
+ currentVideoIndex = 0 // Start from the beginning
348
+
349
+ // Load the first video in the playlist
350
+ loadVideoAtIndex(currentVideoIndex)
351
+ }
352
+
284
353
  fun setAutoplay(value: Boolean) {
285
354
  autoplay = value
286
355
  player?.playWhenReady = value
287
356
  }
288
357
 
289
358
  fun setLoop(value: Boolean) {
359
+ Log.d(TAG, "Setting loop to: $value, isPlaylist: $isPlaylist")
290
360
  loop = value
291
- player?.repeatMode = if (loop) Player.REPEAT_MODE_ALL else Player.REPEAT_MODE_OFF
361
+ // Only set ExoPlayer's repeatMode if NOT in playlist mode.
362
+ // Playlist looping is handled manually in onPlaybackStateChanged.
363
+ if (!isPlaylist) {
364
+ // Use REPEAT_MODE_ONE for single item looping
365
+ player?.repeatMode = if (loop) Player.REPEAT_MODE_ONE else Player.REPEAT_MODE_OFF
366
+ } else {
367
+ // Ensure repeat mode is off when handling playlists manually
368
+ player?.repeatMode = Player.REPEAT_MODE_OFF
369
+ }
292
370
  }
293
371
 
294
372
  fun setThumbnailUrl(url: String?) {
@@ -346,7 +424,7 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
346
424
  it.seekTo(milliseconds)
347
425
 
348
426
  // Force a progress update after seeking
349
- progressRunnable.run()
427
+ progressRunnable.run()
350
428
  } ?: Log.e(TAG, "Cannot seek: player is null")
351
429
  }
352
430
 
@@ -381,7 +459,7 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
381
459
  private fun sendEvent(eventName: String, params: WritableMap) {
382
460
  try {
383
461
  // Log the event for debugging
384
- Log.d(TAG, "Sending direct event: $eventName with params: $params")
462
+ // Log.d(TAG, "Sending direct event: $eventName with params: $params")
385
463
 
386
464
  // Map event names to their corresponding top event names
387
465
  val topEventName = when (eventName) {
@@ -406,13 +484,22 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
406
484
  }
407
485
  }
408
486
 
409
- // Add a method to explicitly start progress updates
487
+ // Method to explicitly start progress updates
410
488
  private fun startProgressUpdates() {
411
- Log.d(TAG, "Starting progress updates")
412
- // Remove any existing callbacks to avoid duplicates
489
+ // Only start if player is ready and has duration
490
+ if (player?.playbackState == Player.STATE_READY && (player?.duration ?: 0) > 0) {
491
+ Log.d(TAG, "Starting progress updates")
492
+ progressHandler.removeCallbacks(progressRunnable) // Remove existing callbacks
493
+ progressHandler.post(progressRunnable) // Post the runnable
494
+ } else {
495
+ Log.d(TAG, "Skipping progress updates start: Player not ready or duration is 0")
496
+ }
497
+ }
498
+
499
+ // Method to stop progress updates
500
+ private fun stopProgressUpdates() {
501
+ Log.d(TAG, "Stopping progress updates")
413
502
  progressHandler.removeCallbacks(progressRunnable)
414
- // Post the runnable to start updates
415
- progressHandler.post(progressRunnable)
416
503
  }
417
504
 
418
505
  override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
@@ -453,14 +540,17 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
453
540
  // Log.d(TAG, "TextureView onSurfaceTextureUpdated")
454
541
  }
455
542
  }
456
- startProgressUpdates() // Use the new method to start progress updates
543
+ // Don't start progress updates here automatically, wait for STATE_READY
544
+ // startProgressUpdates()
457
545
  }
458
546
 
459
547
  override fun onDetachedFromWindow() {
460
548
  super.onDetachedFromWindow()
461
549
  Log.d(TAG, "UnifiedPlayerView onDetachedFromWindow")
462
- progressHandler.removeCallbacks(progressRunnable) // Stop progress updates
550
+ stopProgressUpdates() // Stop progress updates
463
551
  player?.release()
552
+ player = null // Ensure player is nullified
553
+ cleanupRecording() // Clean up recording resources if any
464
554
  }
465
555
 
466
556
  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")
@@ -10,8 +10,11 @@ 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;
@@ -40,8 +43,9 @@ NS_ASSUME_NONNULL_BEGIN
40
43
  @property (nonatomic, copy) RCTDirectEventBlock onPaused;
41
44
 
42
45
  // Method declarations
43
- - (void)setupWithVideoUrlString:(NSString *)videoUrlString;
44
- - (void)setupThumbnailWithUrlString:(NSString *)thumbnailUrlString;
46
+ - (void)setupWithVideoUrlString:(nullable NSString *)videoUrlString;
47
+ - (void)setupWithVideoUrlArray:(NSArray<NSString *> *)urlArray; // New method for playlists
48
+ - (void)setupThumbnailWithUrlString:(nullable NSString *)thumbnailUrlString;
45
49
  - (void)play;
46
50
  - (void)pause;
47
51
  - (void)seekToTime:(NSNumber *)timeNumber;