react-native-unified-player 0.3.8 → 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.
@@ -17,6 +17,7 @@ buildscript {
17
17
 
18
18
  apply plugin: "com.android.library"
19
19
  apply plugin: "kotlin-android"
20
+ apply plugin: "kotlin-kapt"
20
21
 
21
22
  def getExtOrIntegerDefault(name) {
22
23
  return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["UnifiedPlayer_" + name]).toInteger()
@@ -79,4 +80,8 @@ dependencies {
79
80
 
80
81
  implementation "com.google.android.exoplayer:exoplayer-core:2.19.0"
81
82
  implementation "com.google.android.exoplayer:exoplayer-ui:2.19.0"
83
+
84
+ // Glide for image loading
85
+ implementation "com.github.bumptech.glide:glide:4.16.0"
86
+ kapt "com.github.bumptech.glide:compiler:4.16.0"
82
87
  }
@@ -14,6 +14,9 @@ import android.os.Looper
14
14
  import android.view.Gravity
15
15
  import android.view.View
16
16
  import android.widget.FrameLayout
17
+ import android.widget.ImageView
18
+ import com.bumptech.glide.Glide
19
+ import com.bumptech.glide.request.RequestOptions
17
20
  import com.facebook.react.bridge.Arguments
18
21
  import com.google.android.exoplayer2.ExoPlayer
19
22
  import com.google.android.exoplayer2.MediaItem
@@ -27,6 +30,7 @@ import com.facebook.react.bridge.ReactContext
27
30
  import com.facebook.react.uimanager.events.RCTEventEmitter
28
31
  import java.io.File
29
32
  import java.io.IOException
33
+ import java.util.Collections // Import for Collections.emptyList()
30
34
  import android.media.MediaCodec
31
35
  import android.media.MediaCodecInfo
32
36
  import android.media.MediaFormat
@@ -58,10 +62,16 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
58
62
  private const val TAG = "UnifiedPlayerView"
59
63
  }
60
64
 
61
- 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
70
+ private var thumbnailUrl: String? = null
62
71
  private var autoplay: Boolean = true
63
72
  private var loop: Boolean = false
64
73
  private var textureView: android.view.TextureView
74
+ private var thumbnailImageView: ImageView? = null
65
75
  internal var player: ExoPlayer? = null
66
76
  private var currentProgress = 0
67
77
  private var isPaused = false
@@ -74,7 +84,7 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
74
84
  val duration = it.duration.toFloat() / 1000f
75
85
 
76
86
  // Log the actual values for debugging
77
- 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}")
78
88
 
79
89
  // Only send valid duration values
80
90
  if (it.duration > 0) {
@@ -82,10 +92,10 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
82
92
  event.putDouble("currentTime", currentTime.toDouble())
83
93
  event.putDouble("duration", duration.toDouble())
84
94
 
85
- Log.d(TAG, "Sending progress event: currentTime=$currentTime, duration=$duration")
95
+ // Log.d(TAG, "Sending progress event: currentTime=$currentTime, duration=$duration")
86
96
  sendEvent(EVENT_PROGRESS, event)
87
97
  } else {
88
- 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})")
89
99
  }
90
100
  } ?: Log.e(TAG, "Cannot send progress event: player is null")
91
101
 
@@ -100,16 +110,27 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
100
110
  // Create ExoPlayer
101
111
  player = ExoPlayer.Builder(context).build()
102
112
 
103
- // Create TextureView for video rendering
104
- textureView = android.view.TextureView(context).apply {
105
- layoutParams = LayoutParams(
106
- LayoutParams.MATCH_PARENT,
107
- LayoutParams.MATCH_PARENT
108
- )
109
- }
113
+ // Create TextureView for video rendering
114
+ textureView = android.view.TextureView(context).apply {
115
+ layoutParams = LayoutParams(
116
+ LayoutParams.MATCH_PARENT,
117
+ LayoutParams.MATCH_PARENT
118
+ )
119
+ }
120
+
121
+ // Create ImageView for thumbnail
122
+ thumbnailImageView = ImageView(context).apply {
123
+ layoutParams = LayoutParams(
124
+ LayoutParams.MATCH_PARENT,
125
+ LayoutParams.MATCH_PARENT
126
+ )
127
+ scaleType = ImageView.ScaleType.CENTER_CROP
128
+ visibility = View.GONE
129
+ }
110
130
 
111
- // Add TextureView to the view hierarchy
112
- addView(textureView)
131
+ // Add views to the layout (thumbnail on top of TextureView)
132
+ addView(textureView)
133
+ addView(thumbnailImageView)
113
134
 
114
135
  // We'll set the video surface when the TextureView's surface is available
115
136
  // in the onSurfaceTextureAvailable callback
@@ -121,15 +142,52 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
121
142
 
122
143
  player?.addListener(object : Player.Listener {
123
144
  override fun onPlaybackStateChanged(playbackState: Int) {
124
- Log.d(TAG, "onPlaybackStateChanged: $playbackState") // Added log
145
+ Log.d(TAG, "onPlaybackStateChanged: $playbackState")
125
146
  when (playbackState) {
126
147
  Player.STATE_READY -> {
127
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
+ }
128
153
  sendEvent(EVENT_READY, Arguments.createMap())
154
+ // Start progress updates when ready
155
+ startProgressUpdates()
129
156
  }
130
157
  Player.STATE_ENDED -> {
131
158
  Log.d(TAG, "ExoPlayer STATE_ENDED")
132
- 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
+ }
133
191
  }
134
192
  Player.STATE_BUFFERING -> {
135
193
  Log.d(TAG, "ExoPlayer STATE_BUFFERING")
@@ -145,6 +203,8 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
145
203
  Log.d(TAG, "onIsPlayingChanged: $isPlaying") // Added log
146
204
  if (isPlaying) {
147
205
  Log.d(TAG, "ExoPlayer is now playing")
206
+ // Hide thumbnail when video starts playing
207
+ thumbnailImageView?.visibility = View.GONE
148
208
  sendEvent(EVENT_RESUMED, Arguments.createMap())
149
209
  sendEvent(EVENT_PLAYING, Arguments.createMap())
150
210
  } else {
@@ -207,55 +267,34 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
207
267
  })
208
268
  }
209
269
 
210
- fun setVideoUrl(url: String?) {
211
- Log.d(TAG, "Setting video URL: $url")
212
-
213
- if (url == null || url.isEmpty()) {
214
- Log.e(TAG, "Empty or null URL provided")
215
- return
216
- }
217
-
218
- videoUrl = url
219
-
270
+ // Helper function to load and prepare a video URL
271
+ private fun loadVideoSource(url: String) {
272
+ Log.d(TAG, "Loading video source: $url")
220
273
  try {
221
- // Create a MediaItem
222
274
  val mediaItem = MediaItem.fromUri(url)
223
-
224
- // Reset the player to ensure clean state
225
- player?.stop()
226
- player?.clearMediaItems()
227
-
228
- // Set the media item
275
+ player?.stop() // Stop previous playback
276
+ player?.clearMediaItems() // Clear previous items
229
277
  player?.setMediaItem(mediaItem)
230
-
231
- // Prepare the player (this will start loading the media)
232
278
  player?.prepare()
279
+ player?.playWhenReady = autoplay && !isPaused // Apply autoplay and paused state
233
280
 
234
- // Set playWhenReady based on autoplay setting
235
- player?.playWhenReady = autoplay && !isPaused
236
-
237
- // Set repeat mode based on loop setting
238
- player?.repeatMode = if (loop) Player.REPEAT_MODE_ALL else Player.REPEAT_MODE_OFF
239
-
240
- // Log that we've set up the player
241
- Log.d(TAG, "ExoPlayer configured with URL: $url, autoplay: $autoplay, loop: $loop")
242
-
243
- // Add a listener to check when the player is ready and has duration
244
- player?.addListener(object : Player.Listener {
245
- override fun onPlaybackStateChanged(state: Int) {
246
- if (state == Player.STATE_READY) {
247
- val duration = player?.duration ?: 0
248
- Log.d(TAG, "Player ready with duration: ${duration / 1000f} seconds")
249
-
250
- // Force a progress update immediately
251
- progressRunnable.run()
252
- }
253
- }
254
- })
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
+
255
296
  } catch (e: Exception) {
256
- Log.e(TAG, "Error setting video URL: ${e.message}", e)
257
-
258
- // Send error event
297
+ Log.e(TAG, "Error setting video source: ${e.message}", e)
259
298
  val event = Arguments.createMap()
260
299
  event.putString("code", "SOURCE_ERROR")
261
300
  event.putString("message", "Failed to load video source: $url")
@@ -263,14 +302,98 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
263
302
  }
264
303
  }
265
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
+
266
353
  fun setAutoplay(value: Boolean) {
267
354
  autoplay = value
268
355
  player?.playWhenReady = value
269
356
  }
270
357
 
271
358
  fun setLoop(value: Boolean) {
359
+ Log.d(TAG, "Setting loop to: $value, isPlaylist: $isPlaylist")
272
360
  loop = value
273
- 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
+ }
370
+ }
371
+
372
+ fun setThumbnailUrl(url: String?) {
373
+ Log.d(TAG, "Setting thumbnail URL: $url")
374
+
375
+ thumbnailUrl = url
376
+
377
+ if (url != null && url.isNotEmpty()) {
378
+ // Show the thumbnail ImageView
379
+ thumbnailImageView?.visibility = View.VISIBLE
380
+
381
+ // Load the thumbnail image using Glide
382
+ try {
383
+ Glide.with(context)
384
+ .load(url)
385
+ .apply(RequestOptions().centerCrop())
386
+ .into(thumbnailImageView!!)
387
+
388
+ Log.d(TAG, "Thumbnail image loading started")
389
+ } catch (e: Exception) {
390
+ Log.e(TAG, "Error loading thumbnail image: ${e.message}", e)
391
+ thumbnailImageView?.visibility = View.GONE
392
+ }
393
+ } else {
394
+ // Hide the thumbnail if URL is null or empty
395
+ thumbnailImageView?.visibility = View.GONE
396
+ }
274
397
  }
275
398
 
276
399
  fun setIsPaused(isPaused: Boolean) {
@@ -301,7 +424,7 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
301
424
  it.seekTo(milliseconds)
302
425
 
303
426
  // Force a progress update after seeking
304
- progressRunnable.run()
427
+ progressRunnable.run()
305
428
  } ?: Log.e(TAG, "Cannot seek: player is null")
306
429
  }
307
430
 
@@ -336,7 +459,7 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
336
459
  private fun sendEvent(eventName: String, params: WritableMap) {
337
460
  try {
338
461
  // Log the event for debugging
339
- Log.d(TAG, "Sending direct event: $eventName with params: $params")
462
+ // Log.d(TAG, "Sending direct event: $eventName with params: $params")
340
463
 
341
464
  // Map event names to their corresponding top event names
342
465
  val topEventName = when (eventName) {
@@ -361,13 +484,22 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
361
484
  }
362
485
  }
363
486
 
364
- // Add a method to explicitly start progress updates
487
+ // Method to explicitly start progress updates
365
488
  private fun startProgressUpdates() {
366
- Log.d(TAG, "Starting progress updates")
367
- // 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")
368
502
  progressHandler.removeCallbacks(progressRunnable)
369
- // Post the runnable to start updates
370
- progressHandler.post(progressRunnable)
371
503
  }
372
504
 
373
505
  override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
@@ -408,14 +540,17 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
408
540
  // Log.d(TAG, "TextureView onSurfaceTextureUpdated")
409
541
  }
410
542
  }
411
- startProgressUpdates() // Use the new method to start progress updates
543
+ // Don't start progress updates here automatically, wait for STATE_READY
544
+ // startProgressUpdates()
412
545
  }
413
546
 
414
547
  override fun onDetachedFromWindow() {
415
548
  super.onDetachedFromWindow()
416
549
  Log.d(TAG, "UnifiedPlayerView onDetachedFromWindow")
417
- progressHandler.removeCallbacks(progressRunnable) // Stop progress updates
550
+ stopProgressUpdates() // Stop progress updates
418
551
  player?.release()
552
+ player = null // Ensure player is nullified
553
+ cleanupRecording() // Clean up recording resources if any
419
554
  }
420
555
 
421
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,43 @@ 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
+ }
56
+ }
57
+
58
+ @ReactProp(name = "thumbnailUrl")
59
+ fun setThumbnailUrl(view: UnifiedPlayerView, url: String?) {
60
+ view.setThumbnailUrl(url)
24
61
  }
25
62
 
26
63
  @ReactProp(name = "autoplay")
@@ -57,4 +94,4 @@ class UnifiedPlayerViewManager : SimpleViewManager<UnifiedPlayerView>() {
57
94
  .put("topLoadStart", MapBuilder.of("registrationName", "onLoadStart"))
58
95
  .build()
59
96
  }
60
- }
97
+ }
@@ -10,7 +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;
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;
14
18
  @property (nonatomic, assign) BOOL autoplay;
15
19
  @property (nonatomic, assign) BOOL loop;
16
20
  @property (nonatomic, assign) BOOL isPaused;
@@ -39,7 +43,9 @@ NS_ASSUME_NONNULL_BEGIN
39
43
  @property (nonatomic, copy) RCTDirectEventBlock onPaused;
40
44
 
41
45
  // Method declarations
42
- - (void)setupWithVideoUrlString:(NSString *)videoUrlString;
46
+ - (void)setupWithVideoUrlString:(nullable NSString *)videoUrlString;
47
+ - (void)setupWithVideoUrlArray:(NSArray<NSString *> *)urlArray; // New method for playlists
48
+ - (void)setupThumbnailWithUrlString:(nullable NSString *)thumbnailUrlString;
43
49
  - (void)play;
44
50
  - (void)pause;
45
51
  - (void)seekToTime:(NSNumber *)timeNumber;