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 +4 -3
- package/android/src/main/java/com/unifiedplayer/UnifiedPlayerEventEmitter.kt +2 -1
- package/android/src/main/java/com/unifiedplayer/UnifiedPlayerModule.kt +26 -0
- package/android/src/main/java/com/unifiedplayer/UnifiedPlayerView.kt +247 -60
- package/android/src/main/java/com/unifiedplayer/UnifiedPlayerViewManager.kt +40 -2
- package/ios/UnifiedPlayerModule.m +19 -0
- package/ios/UnifiedPlayerUIView.h +11 -4
- package/ios/UnifiedPlayerViewManager.m +396 -197
- package/lib/module/index.js +21 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/index.d.ts +15 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.tsx +48 -4
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
|
-
|
|
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")
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
})
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
584
|
+
// Method to explicitly start progress updates
|
|
410
585
|
private fun startProgressUpdates() {
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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")
|
|
@@ -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 *
|
|
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)
|
|
44
|
-
- (void)
|
|
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;
|