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.
- package/android/src/main/java/com/unifiedplayer/UnifiedPlayerView.kt +149 -59
- package/android/src/main/java/com/unifiedplayer/UnifiedPlayerViewManager.kt +34 -2
- package/ios/UnifiedPlayerUIView.h +8 -4
- package/ios/UnifiedPlayerViewManager.m +258 -197
- package/lib/typescript/src/index.d.ts +6 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.tsx +4 -4
|
@@ -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
|
-
|
|
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")
|
|
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
|
-
|
|
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
|
-
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
Log.d(TAG, "ExoPlayer configured with URL: $url, autoplay: $autoplay, loop: $loop")
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
487
|
+
// Method to explicitly start progress updates
|
|
410
488
|
private fun startProgressUpdates() {
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
23
|
-
|
|
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 *
|
|
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)
|
|
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;
|