react-native-unified-player 0.3.7 → 0.3.9
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/build.gradle +5 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/com/unifiedplayer/UnifiedPlayerModule.kt +69 -0
- package/android/src/main/java/com/unifiedplayer/UnifiedPlayerView.kt +289 -9
- package/android/src/main/java/com/unifiedplayer/UnifiedPlayerViewManager.kt +6 -1
- package/ios/UnifiedPlayerModule.m +94 -0
- package/ios/UnifiedPlayerUIView.h +15 -0
- package/ios/UnifiedPlayerViewManager.m +316 -1
- package/lib/module/index.js +41 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/index.d.ts +14 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.tsx +62 -0
package/android/build.gradle
CHANGED
|
@@ -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
|
}
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
package="com.unifiedplayer">
|
|
3
3
|
<uses-permission android:name="android.permission.INTERNET" />
|
|
4
4
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
5
|
+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
|
6
|
+
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
|
7
|
+
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
|
5
8
|
<application android:hardwareAccelerated="true">
|
|
6
9
|
</application>
|
|
7
10
|
</manifest>
|
|
@@ -8,6 +8,8 @@ import com.facebook.react.uimanager.UIManagerModule
|
|
|
8
8
|
import android.util.Log
|
|
9
9
|
import com.facebook.react.bridge.UiThreadUtil
|
|
10
10
|
import android.view.View
|
|
11
|
+
import android.os.Environment
|
|
12
|
+
import java.io.File
|
|
11
13
|
|
|
12
14
|
class UnifiedPlayerModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
|
13
15
|
companion object {
|
|
@@ -195,4 +197,71 @@ class UnifiedPlayerModule(private val reactContext: ReactApplicationContext) : R
|
|
|
195
197
|
promise.reject("CAPTURE_ERROR", "Error in capture method: ${e.message}", e)
|
|
196
198
|
}
|
|
197
199
|
}
|
|
200
|
+
|
|
201
|
+
@ReactMethod
|
|
202
|
+
fun startRecording(viewTag: Int, outputPath: String?, promise: Promise) {
|
|
203
|
+
Log.d(TAG, "Native startRecording method called with viewTag: $viewTag, outputPath: $outputPath")
|
|
204
|
+
try {
|
|
205
|
+
val playerView = getPlayerViewByTag(viewTag)
|
|
206
|
+
if (playerView != null) {
|
|
207
|
+
UiThreadUtil.runOnUiThread {
|
|
208
|
+
try {
|
|
209
|
+
// Determine the output path
|
|
210
|
+
val finalOutputPath = if (outputPath.isNullOrEmpty()) {
|
|
211
|
+
// Use app-specific storage for Android 10+ (API level 29+)
|
|
212
|
+
val moviesDir = File(reactApplicationContext.getExternalFilesDir(Environment.DIRECTORY_MOVIES), "recordings")
|
|
213
|
+
if (!moviesDir.exists()) {
|
|
214
|
+
moviesDir.mkdirs()
|
|
215
|
+
}
|
|
216
|
+
val timestamp = System.currentTimeMillis()
|
|
217
|
+
File(moviesDir, "recording_$timestamp.mp4").absolutePath
|
|
218
|
+
} else {
|
|
219
|
+
outputPath
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Start recording
|
|
223
|
+
val result = playerView.startRecording(finalOutputPath)
|
|
224
|
+
Log.d(TAG, "Start recording command executed successfully, result: $result")
|
|
225
|
+
promise.resolve(result)
|
|
226
|
+
} catch (e: Exception) {
|
|
227
|
+
Log.e(TAG, "Error during startRecording: ${e.message}", e)
|
|
228
|
+
promise.reject("RECORDING_ERROR", "Error during startRecording: ${e.message}", e)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
Log.e(TAG, "Player view not found for tag: $viewTag")
|
|
233
|
+
promise.reject("VIEW_NOT_FOUND", "Player view not found for tag: $viewTag")
|
|
234
|
+
}
|
|
235
|
+
} catch (e: Exception) {
|
|
236
|
+
Log.e(TAG, "Error in startRecording method: ${e.message}", e)
|
|
237
|
+
promise.reject("RECORDING_ERROR", "Error in startRecording method: ${e.message}", e)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
@ReactMethod
|
|
242
|
+
fun stopRecording(viewTag: Int, promise: Promise) {
|
|
243
|
+
Log.d(TAG, "Native stopRecording method called with viewTag: $viewTag")
|
|
244
|
+
try {
|
|
245
|
+
val playerView = getPlayerViewByTag(viewTag)
|
|
246
|
+
if (playerView != null) {
|
|
247
|
+
UiThreadUtil.runOnUiThread {
|
|
248
|
+
try {
|
|
249
|
+
// Stop recording
|
|
250
|
+
val filePath = playerView.stopRecording()
|
|
251
|
+
Log.d(TAG, "Stop recording command executed successfully, filePath: $filePath")
|
|
252
|
+
promise.resolve(filePath)
|
|
253
|
+
} catch (e: Exception) {
|
|
254
|
+
Log.e(TAG, "Error during stopRecording: ${e.message}", e)
|
|
255
|
+
promise.reject("RECORDING_ERROR", "Error during stopRecording: ${e.message}", e)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
Log.e(TAG, "Player view not found for tag: $viewTag")
|
|
260
|
+
promise.reject("VIEW_NOT_FOUND", "Player view not found for tag: $viewTag")
|
|
261
|
+
}
|
|
262
|
+
} catch (e: Exception) {
|
|
263
|
+
Log.e(TAG, "Error in stopRecording method: ${e.message}", e)
|
|
264
|
+
promise.reject("RECORDING_ERROR", "Error in stopRecording method: ${e.message}", e)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
198
267
|
}
|
|
@@ -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
|
|
@@ -25,6 +28,15 @@ import com.google.android.exoplayer2.video.VideoSize
|
|
|
25
28
|
import com.facebook.react.bridge.WritableMap
|
|
26
29
|
import com.facebook.react.bridge.ReactContext
|
|
27
30
|
import com.facebook.react.uimanager.events.RCTEventEmitter
|
|
31
|
+
import java.io.File
|
|
32
|
+
import java.io.IOException
|
|
33
|
+
import android.media.MediaCodec
|
|
34
|
+
import android.media.MediaCodecInfo
|
|
35
|
+
import android.media.MediaFormat
|
|
36
|
+
import android.media.MediaMuxer
|
|
37
|
+
import android.view.Surface
|
|
38
|
+
import java.nio.ByteBuffer
|
|
39
|
+
import android.os.Environment
|
|
28
40
|
import com.unifiedplayer.UnifiedPlayerEventEmitter.Companion.EVENT_COMPLETE
|
|
29
41
|
import com.unifiedplayer.UnifiedPlayerEventEmitter.Companion.EVENT_ERROR
|
|
30
42
|
import com.unifiedplayer.UnifiedPlayerEventEmitter.Companion.EVENT_LOAD_START
|
|
@@ -36,14 +48,25 @@ import com.unifiedplayer.UnifiedPlayerEventEmitter.Companion.EVENT_RESUMED
|
|
|
36
48
|
import com.unifiedplayer.UnifiedPlayerEventEmitter.Companion.EVENT_STALLED
|
|
37
49
|
|
|
38
50
|
class UnifiedPlayerView(context: Context) : FrameLayout(context) {
|
|
51
|
+
// Recording related variables
|
|
52
|
+
private var mediaRecorder: MediaMuxer? = null
|
|
53
|
+
private var videoEncoder: MediaCodec? = null
|
|
54
|
+
private var recordingSurface: Surface? = null
|
|
55
|
+
private var isRecording = false
|
|
56
|
+
private var outputPath: String? = null
|
|
57
|
+
private var recordingThread: Thread? = null
|
|
58
|
+
private var videoTrackIndex = -1
|
|
59
|
+
private val bufferInfo = MediaCodec.BufferInfo()
|
|
39
60
|
companion object {
|
|
40
61
|
private const val TAG = "UnifiedPlayerView"
|
|
41
62
|
}
|
|
42
63
|
|
|
43
64
|
private var videoUrl: String? = null
|
|
65
|
+
private var thumbnailUrl: String? = null
|
|
44
66
|
private var autoplay: Boolean = true
|
|
45
67
|
private var loop: Boolean = false
|
|
46
68
|
private var textureView: android.view.TextureView
|
|
69
|
+
private var thumbnailImageView: ImageView? = null
|
|
47
70
|
internal var player: ExoPlayer? = null
|
|
48
71
|
private var currentProgress = 0
|
|
49
72
|
private var isPaused = false
|
|
@@ -82,16 +105,27 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
|
|
|
82
105
|
// Create ExoPlayer
|
|
83
106
|
player = ExoPlayer.Builder(context).build()
|
|
84
107
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
108
|
+
// Create TextureView for video rendering
|
|
109
|
+
textureView = android.view.TextureView(context).apply {
|
|
110
|
+
layoutParams = LayoutParams(
|
|
111
|
+
LayoutParams.MATCH_PARENT,
|
|
112
|
+
LayoutParams.MATCH_PARENT
|
|
113
|
+
)
|
|
114
|
+
}
|
|
92
115
|
|
|
93
|
-
|
|
94
|
-
|
|
116
|
+
// Create ImageView for thumbnail
|
|
117
|
+
thumbnailImageView = ImageView(context).apply {
|
|
118
|
+
layoutParams = LayoutParams(
|
|
119
|
+
LayoutParams.MATCH_PARENT,
|
|
120
|
+
LayoutParams.MATCH_PARENT
|
|
121
|
+
)
|
|
122
|
+
scaleType = ImageView.ScaleType.CENTER_CROP
|
|
123
|
+
visibility = View.GONE
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Add views to the layout (thumbnail on top of TextureView)
|
|
127
|
+
addView(textureView)
|
|
128
|
+
addView(thumbnailImageView)
|
|
95
129
|
|
|
96
130
|
// We'll set the video surface when the TextureView's surface is available
|
|
97
131
|
// in the onSurfaceTextureAvailable callback
|
|
@@ -127,6 +161,8 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
|
|
|
127
161
|
Log.d(TAG, "onIsPlayingChanged: $isPlaying") // Added log
|
|
128
162
|
if (isPlaying) {
|
|
129
163
|
Log.d(TAG, "ExoPlayer is now playing")
|
|
164
|
+
// Hide thumbnail when video starts playing
|
|
165
|
+
thumbnailImageView?.visibility = View.GONE
|
|
130
166
|
sendEvent(EVENT_RESUMED, Arguments.createMap())
|
|
131
167
|
sendEvent(EVENT_PLAYING, Arguments.createMap())
|
|
132
168
|
} else {
|
|
@@ -255,6 +291,33 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
|
|
|
255
291
|
player?.repeatMode = if (loop) Player.REPEAT_MODE_ALL else Player.REPEAT_MODE_OFF
|
|
256
292
|
}
|
|
257
293
|
|
|
294
|
+
fun setThumbnailUrl(url: String?) {
|
|
295
|
+
Log.d(TAG, "Setting thumbnail URL: $url")
|
|
296
|
+
|
|
297
|
+
thumbnailUrl = url
|
|
298
|
+
|
|
299
|
+
if (url != null && url.isNotEmpty()) {
|
|
300
|
+
// Show the thumbnail ImageView
|
|
301
|
+
thumbnailImageView?.visibility = View.VISIBLE
|
|
302
|
+
|
|
303
|
+
// Load the thumbnail image using Glide
|
|
304
|
+
try {
|
|
305
|
+
Glide.with(context)
|
|
306
|
+
.load(url)
|
|
307
|
+
.apply(RequestOptions().centerCrop())
|
|
308
|
+
.into(thumbnailImageView!!)
|
|
309
|
+
|
|
310
|
+
Log.d(TAG, "Thumbnail image loading started")
|
|
311
|
+
} catch (e: Exception) {
|
|
312
|
+
Log.e(TAG, "Error loading thumbnail image: ${e.message}", e)
|
|
313
|
+
thumbnailImageView?.visibility = View.GONE
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
// Hide the thumbnail if URL is null or empty
|
|
317
|
+
thumbnailImageView?.visibility = View.GONE
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
258
321
|
fun setIsPaused(isPaused: Boolean) {
|
|
259
322
|
Log.d(TAG, "setIsPaused called with value: $isPaused")
|
|
260
323
|
this.isPaused = isPaused
|
|
@@ -457,4 +520,221 @@ bitmap.let {
|
|
|
457
520
|
""
|
|
458
521
|
}
|
|
459
522
|
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Start recording the video to the specified output path
|
|
526
|
+
* @param outputPath Path where to save the recording
|
|
527
|
+
* @return true if recording started successfully
|
|
528
|
+
*/
|
|
529
|
+
fun startRecording(outputPath: String): Boolean {
|
|
530
|
+
Log.d(TAG, "startRecording called with outputPath: $outputPath")
|
|
531
|
+
|
|
532
|
+
if (isRecording) {
|
|
533
|
+
Log.w(TAG, "Recording is already in progress")
|
|
534
|
+
return false
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
try {
|
|
538
|
+
player?.let { exoPlayer ->
|
|
539
|
+
// Get the current media item's URI
|
|
540
|
+
val currentUri = exoPlayer.currentMediaItem?.localConfiguration?.uri?.toString()
|
|
541
|
+
if (currentUri == null) {
|
|
542
|
+
Log.e(TAG, "Current media URI is null")
|
|
543
|
+
return false
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
Log.d(TAG, "Current media URI: $currentUri")
|
|
547
|
+
|
|
548
|
+
// Store the output path
|
|
549
|
+
this.outputPath = if (outputPath.isNullOrEmpty()) {
|
|
550
|
+
// Use app-specific storage for Android 10+ (API level 29+)
|
|
551
|
+
val appContext = context.applicationContext
|
|
552
|
+
val moviesDir = File(appContext.getExternalFilesDir(Environment.DIRECTORY_MOVIES), "recordings")
|
|
553
|
+
if (!moviesDir.exists()) {
|
|
554
|
+
moviesDir.mkdirs()
|
|
555
|
+
}
|
|
556
|
+
File(moviesDir, "recording_${System.currentTimeMillis()}.mp4").absolutePath
|
|
557
|
+
} else {
|
|
558
|
+
outputPath
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Create parent directories if they don't exist
|
|
562
|
+
val outputFile = File(this.outputPath)
|
|
563
|
+
outputFile.parentFile?.mkdirs()
|
|
564
|
+
|
|
565
|
+
// Log the final output path
|
|
566
|
+
Log.d(TAG, "Recording will be saved to: ${this.outputPath}")
|
|
567
|
+
|
|
568
|
+
// Start a background thread to download the file
|
|
569
|
+
Thread {
|
|
570
|
+
try {
|
|
571
|
+
// Create a URL from the URI
|
|
572
|
+
val url = java.net.URL(currentUri)
|
|
573
|
+
|
|
574
|
+
// Open connection
|
|
575
|
+
val connection = url.openConnection() as java.net.HttpURLConnection
|
|
576
|
+
connection.requestMethod = "GET"
|
|
577
|
+
connection.connectTimeout = 15000
|
|
578
|
+
connection.readTimeout = 15000
|
|
579
|
+
connection.doInput = true
|
|
580
|
+
connection.connect()
|
|
581
|
+
|
|
582
|
+
// Check if the connection was successful
|
|
583
|
+
if (connection.responseCode != java.net.HttpURLConnection.HTTP_OK) {
|
|
584
|
+
Log.e(TAG, "HTTP error code: ${connection.responseCode}")
|
|
585
|
+
return@Thread
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Get the input stream
|
|
589
|
+
val inputStream = connection.inputStream
|
|
590
|
+
|
|
591
|
+
// Create the output file
|
|
592
|
+
val outputFile = File(this.outputPath!!)
|
|
593
|
+
|
|
594
|
+
// Create the output stream
|
|
595
|
+
val outputStream = outputFile.outputStream()
|
|
596
|
+
|
|
597
|
+
// Create a buffer
|
|
598
|
+
val buffer = ByteArray(1024)
|
|
599
|
+
var bytesRead: Int
|
|
600
|
+
var totalBytesRead: Long = 0
|
|
601
|
+
val fileSize = connection.contentLength.toLong()
|
|
602
|
+
|
|
603
|
+
// Read from the input stream and write to the output stream
|
|
604
|
+
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
|
|
605
|
+
outputStream.write(buffer, 0, bytesRead)
|
|
606
|
+
totalBytesRead += bytesRead
|
|
607
|
+
|
|
608
|
+
// Log progress
|
|
609
|
+
if (fileSize > 0) {
|
|
610
|
+
val progress = (totalBytesRead * 100 / fileSize).toInt()
|
|
611
|
+
Log.d(TAG, "Download progress: $progress%")
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Close the streams
|
|
616
|
+
outputStream.flush()
|
|
617
|
+
outputStream.close()
|
|
618
|
+
inputStream.close()
|
|
619
|
+
|
|
620
|
+
Log.d(TAG, "File downloaded successfully to ${this.outputPath}")
|
|
621
|
+
} catch (e: Exception) {
|
|
622
|
+
Log.e(TAG, "Error downloading file: ${e.message}", e)
|
|
623
|
+
}
|
|
624
|
+
}.start()
|
|
625
|
+
|
|
626
|
+
isRecording = true
|
|
627
|
+
Log.d(TAG, "Recording started successfully")
|
|
628
|
+
return true
|
|
629
|
+
} ?: run {
|
|
630
|
+
Log.e(TAG, "Cannot start recording: player is null")
|
|
631
|
+
return false
|
|
632
|
+
}
|
|
633
|
+
} catch (e: Exception) {
|
|
634
|
+
Log.e(TAG, "Error starting recording: ${e.message}", e)
|
|
635
|
+
return false
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Stop recording and save the video
|
|
641
|
+
* @return Path to the saved recording
|
|
642
|
+
*/
|
|
643
|
+
fun stopRecording(): String {
|
|
644
|
+
Log.d(TAG, "stopRecording called")
|
|
645
|
+
|
|
646
|
+
if (!isRecording) {
|
|
647
|
+
Log.w(TAG, "No recording in progress")
|
|
648
|
+
return ""
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Simply mark recording as stopped
|
|
652
|
+
isRecording = false
|
|
653
|
+
|
|
654
|
+
// Wait a moment to ensure any background operations complete
|
|
655
|
+
try {
|
|
656
|
+
Thread.sleep(500)
|
|
657
|
+
} catch (e: InterruptedException) {
|
|
658
|
+
Log.e(TAG, "Sleep interrupted: ${e.message}")
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Return the path where the recording was saved
|
|
662
|
+
val savedPath = outputPath ?: ""
|
|
663
|
+
Log.d(TAG, "Recording stopped successfully, saved to: $savedPath")
|
|
664
|
+
|
|
665
|
+
return savedPath
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
private fun cleanupRecording() {
|
|
669
|
+
try {
|
|
670
|
+
videoEncoder?.stop()
|
|
671
|
+
videoEncoder?.release()
|
|
672
|
+
videoEncoder = null
|
|
673
|
+
|
|
674
|
+
recordingSurface?.release()
|
|
675
|
+
recordingSurface = null
|
|
676
|
+
|
|
677
|
+
mediaRecorder?.stop()
|
|
678
|
+
mediaRecorder?.release()
|
|
679
|
+
mediaRecorder = null
|
|
680
|
+
|
|
681
|
+
videoTrackIndex = -1
|
|
682
|
+
isRecording = false
|
|
683
|
+
} catch (e: Exception) {
|
|
684
|
+
Log.e(TAG, "Error cleaning up recording resources: ${e.message}", e)
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
private inner class RecordingRunnable : Runnable {
|
|
689
|
+
override fun run() {
|
|
690
|
+
try {
|
|
691
|
+
// Add video track to muxer
|
|
692
|
+
val videoFormat = videoEncoder?.outputFormat
|
|
693
|
+
if (videoFormat != null && mediaRecorder != null) {
|
|
694
|
+
videoTrackIndex = mediaRecorder!!.addTrack(videoFormat)
|
|
695
|
+
} else {
|
|
696
|
+
Log.e(TAG, "Cannot add track: videoFormat or mediaRecorder is null")
|
|
697
|
+
videoTrackIndex = -1
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Start the muxer
|
|
701
|
+
mediaRecorder?.start()
|
|
702
|
+
|
|
703
|
+
// Process encoding
|
|
704
|
+
while (isRecording) {
|
|
705
|
+
val encoderStatus = videoEncoder?.dequeueOutputBuffer(bufferInfo, 10000) ?: -1
|
|
706
|
+
|
|
707
|
+
if (encoderStatus >= 0) {
|
|
708
|
+
val encodedData = videoEncoder?.getOutputBuffer(encoderStatus)
|
|
709
|
+
|
|
710
|
+
if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
|
|
711
|
+
// Ignore codec config data
|
|
712
|
+
bufferInfo.size = 0
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (bufferInfo.size > 0 && encodedData != null && mediaRecorder != null && videoTrackIndex >= 0) {
|
|
716
|
+
encodedData.position(bufferInfo.offset)
|
|
717
|
+
encodedData.limit(bufferInfo.offset + bufferInfo.size)
|
|
718
|
+
|
|
719
|
+
mediaRecorder!!.writeSampleData(videoTrackIndex, encodedData, bufferInfo)
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
videoEncoder?.releaseOutputBuffer(encoderStatus, false)
|
|
723
|
+
|
|
724
|
+
if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
|
|
725
|
+
break
|
|
726
|
+
}
|
|
727
|
+
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
|
728
|
+
// Handle format change if needed
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Signal end of stream to encoder
|
|
733
|
+
videoEncoder?.signalEndOfInputStream()
|
|
734
|
+
|
|
735
|
+
} catch (e: Exception) {
|
|
736
|
+
Log.e(TAG, "Error in recording thread: ${e.message}", e)
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
460
740
|
}
|
|
@@ -23,6 +23,11 @@ class UnifiedPlayerViewManager : SimpleViewManager<UnifiedPlayerView>() {
|
|
|
23
23
|
view.setVideoUrl(url)
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
@ReactProp(name = "thumbnailUrl")
|
|
27
|
+
fun setThumbnailUrl(view: UnifiedPlayerView, url: String?) {
|
|
28
|
+
view.setThumbnailUrl(url)
|
|
29
|
+
}
|
|
30
|
+
|
|
26
31
|
@ReactProp(name = "autoplay")
|
|
27
32
|
fun setAutoplay(view: UnifiedPlayerView, autoplay: Boolean) {
|
|
28
33
|
view.setAutoplay(autoplay)
|
|
@@ -57,4 +62,4 @@ class UnifiedPlayerViewManager : SimpleViewManager<UnifiedPlayerView>() {
|
|
|
57
62
|
.put("topLoadStart", MapBuilder.of("registrationName", "onLoadStart"))
|
|
58
63
|
.build()
|
|
59
64
|
}
|
|
60
|
-
}
|
|
65
|
+
}
|
|
@@ -216,4 +216,98 @@ RCT_EXPORT_METHOD(capture:(nonnull NSNumber *)reactTag
|
|
|
216
216
|
}];
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
RCT_EXPORT_METHOD(startRecording:(nonnull NSNumber *)reactTag
|
|
220
|
+
outputPath:(NSString *)outputPath
|
|
221
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
222
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
223
|
+
{
|
|
224
|
+
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
|
225
|
+
UIView *view = viewRegistry[reactTag];
|
|
226
|
+
if (![view isKindOfClass:[UnifiedPlayerUIView class]]) {
|
|
227
|
+
RCTLogError(@"Invalid view returned from registry, expecting UnifiedPlayerUIView, got: %@", view);
|
|
228
|
+
reject(@"E_INVALID_VIEW", @"Expected UnifiedPlayerUIView", nil);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
UnifiedPlayerUIView *playerView = (UnifiedPlayerUIView *)view;
|
|
233
|
+
|
|
234
|
+
// If no output path is provided, create a default one in the Documents directory
|
|
235
|
+
NSString *finalOutputPath = outputPath;
|
|
236
|
+
if (!finalOutputPath || [finalOutputPath isEqualToString:@""]) {
|
|
237
|
+
NSString *documentsDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
|
|
238
|
+
NSString *timestamp = [NSString stringWithFormat:@"%f", [[NSDate date] timeIntervalSince1970]];
|
|
239
|
+
finalOutputPath = [documentsDirectory stringByAppendingPathComponent:[NSString stringWithFormat:@"recording_%@.mp4", timestamp]];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
BOOL success = [playerView startRecordingToPath:finalOutputPath];
|
|
243
|
+
if (success) {
|
|
244
|
+
resolve(@YES);
|
|
245
|
+
} else {
|
|
246
|
+
reject(@"E_RECORDING_FAILED", @"Failed to start recording", nil);
|
|
247
|
+
}
|
|
248
|
+
}];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
RCT_EXPORT_METHOD(stopRecording:(nonnull NSNumber *)reactTag
|
|
252
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
253
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
254
|
+
{
|
|
255
|
+
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
|
256
|
+
UIView *view = viewRegistry[reactTag];
|
|
257
|
+
if (![view isKindOfClass:[UnifiedPlayerUIView class]]) {
|
|
258
|
+
RCTLogError(@"Invalid view returned from registry, expecting UnifiedPlayerUIView, got: %@", view);
|
|
259
|
+
reject(@"E_INVALID_VIEW", @"Expected UnifiedPlayerUIView", nil);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
UnifiedPlayerUIView *playerView = (UnifiedPlayerUIView *)view;
|
|
264
|
+
NSString *filePath = [playerView stopRecording];
|
|
265
|
+
|
|
266
|
+
if (filePath && ![filePath isEqualToString:@""]) {
|
|
267
|
+
resolve(filePath);
|
|
268
|
+
} else {
|
|
269
|
+
reject(@"E_RECORDING_FAILED", @"Failed to stop recording or no recording in progress", nil);
|
|
270
|
+
}
|
|
271
|
+
}];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
RCT_EXPORT_METHOD(saveVideo:(nonnull NSNumber *)reactTag
|
|
275
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
276
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
277
|
+
{
|
|
278
|
+
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
|
279
|
+
UIView *view = viewRegistry[reactTag];
|
|
280
|
+
if (![view isKindOfClass:[UnifiedPlayerUIView class]]) {
|
|
281
|
+
RCTLogError(@"Invalid view returned from registry, expecting UnifiedPlayerUIView, got: %@", view);
|
|
282
|
+
reject(@"E_INVALID_VIEW", @"Expected UnifiedPlayerUIView", nil);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
UnifiedPlayerUIView *playerView = (UnifiedPlayerUIView *)view;
|
|
287
|
+
|
|
288
|
+
// Create a file path in the Documents directory
|
|
289
|
+
NSString *documentsDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
|
|
290
|
+
NSString *timestamp = [NSString stringWithFormat:@"%f", [[NSDate date] timeIntervalSince1970]];
|
|
291
|
+
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:[NSString stringWithFormat:@"saved_video_%@.mp4", timestamp]];
|
|
292
|
+
|
|
293
|
+
// Start recording
|
|
294
|
+
BOOL success = [playerView startRecordingToPath:filePath];
|
|
295
|
+
if (!success) {
|
|
296
|
+
reject(@"E_RECORDING_FAILED", @"Failed to start recording", nil);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Record for 5 seconds (or adjust as needed)
|
|
301
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
302
|
+
NSString *savedFilePath = [playerView stopRecording];
|
|
303
|
+
|
|
304
|
+
if (savedFilePath && ![savedFilePath isEqualToString:@""]) {
|
|
305
|
+
resolve(savedFilePath);
|
|
306
|
+
} else {
|
|
307
|
+
reject(@"E_RECORDING_FAILED", @"Failed to stop recording or no recording in progress", nil);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
}];
|
|
311
|
+
}
|
|
312
|
+
|
|
219
313
|
@end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
#import <React/RCTView.h>
|
|
3
3
|
#import <React/RCTComponent.h>
|
|
4
4
|
#import <MobileVLCKit/MobileVLCKit.h>
|
|
5
|
+
#import <AVFoundation/AVFoundation.h>
|
|
6
|
+
#import <CoreVideo/CoreVideo.h>
|
|
5
7
|
|
|
6
8
|
NS_ASSUME_NONNULL_BEGIN
|
|
7
9
|
|
|
@@ -9,6 +11,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
9
11
|
|
|
10
12
|
@property (nonatomic, strong) VLCMediaPlayer *player;
|
|
11
13
|
@property (nonatomic, copy) NSString *videoUrlString;
|
|
14
|
+
@property (nonatomic, copy) NSString *thumbnailUrlString;
|
|
12
15
|
@property (nonatomic, assign) BOOL autoplay;
|
|
13
16
|
@property (nonatomic, assign) BOOL loop;
|
|
14
17
|
@property (nonatomic, assign) BOOL isPaused;
|
|
@@ -17,6 +20,13 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
17
20
|
@property (nonatomic, assign) VLCMediaPlayerState previousState;
|
|
18
21
|
@property (nonatomic, assign) BOOL hasRenderedVideo;
|
|
19
22
|
@property (nonatomic, assign) BOOL readyEventSent;
|
|
23
|
+
@property (nonatomic, assign) BOOL isRecording;
|
|
24
|
+
@property (nonatomic, strong) AVAssetWriter *assetWriter;
|
|
25
|
+
@property (nonatomic, strong) AVAssetWriterInput *assetWriterVideoInput;
|
|
26
|
+
@property (nonatomic, strong) AVAssetWriterInputPixelBufferAdaptor *assetWriterPixelBufferAdaptor;
|
|
27
|
+
@property (nonatomic, strong) NSString *recordingPath;
|
|
28
|
+
// We'll use associated objects instead of a property for CADisplayLink
|
|
29
|
+
@property (nonatomic, assign) NSInteger frameCount;
|
|
20
30
|
|
|
21
31
|
// Event callbacks
|
|
22
32
|
@property (nonatomic, copy) RCTDirectEventBlock onLoadStart;
|
|
@@ -31,12 +41,17 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
31
41
|
|
|
32
42
|
// Method declarations
|
|
33
43
|
- (void)setupWithVideoUrlString:(NSString *)videoUrlString;
|
|
44
|
+
- (void)setupThumbnailWithUrlString:(NSString *)thumbnailUrlString;
|
|
34
45
|
- (void)play;
|
|
35
46
|
- (void)pause;
|
|
36
47
|
- (void)seekToTime:(NSNumber *)timeNumber;
|
|
37
48
|
- (float)getCurrentTime;
|
|
38
49
|
- (float)getDuration;
|
|
39
50
|
- (void)captureFrameWithCompletion:(void (^)(NSString * _Nullable base64String, NSError * _Nullable error))completion;
|
|
51
|
+
- (void)captureFrameForRecording;
|
|
52
|
+
- (BOOL)startRecordingToPath:(NSString *)outputPath;
|
|
53
|
+
- (void)startFrameCapture;
|
|
54
|
+
- (NSString *)stopRecording;
|
|
40
55
|
|
|
41
56
|
@end
|
|
42
57
|
|
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
#import <React/RCTUIManagerUtils.h>
|
|
8
8
|
#import <React/RCTComponent.h>
|
|
9
9
|
#import <MobileVLCKit/MobileVLCKit.h>
|
|
10
|
+
#import <objc/runtime.h>
|
|
10
11
|
#import "UnifiedPlayerModule.h"
|
|
11
12
|
#import "UnifiedPlayerUIView.h"
|
|
12
13
|
|
|
13
14
|
// Main player view implementation
|
|
14
|
-
@implementation UnifiedPlayerUIView
|
|
15
|
+
@implementation UnifiedPlayerUIView {
|
|
16
|
+
UIImageView *_thumbnailImageView;
|
|
17
|
+
}
|
|
15
18
|
|
|
16
19
|
- (instancetype)init {
|
|
17
20
|
if ((self = [super init])) {
|
|
@@ -34,6 +37,13 @@
|
|
|
34
37
|
self.contentMode = UIViewContentModeScaleAspectFit;
|
|
35
38
|
self.clipsToBounds = YES;
|
|
36
39
|
|
|
40
|
+
// Create thumbnail image view
|
|
41
|
+
_thumbnailImageView = [[UIImageView alloc] initWithFrame:CGRectZero];
|
|
42
|
+
_thumbnailImageView.contentMode = UIViewContentModeScaleAspectFill;
|
|
43
|
+
_thumbnailImageView.clipsToBounds = YES;
|
|
44
|
+
_thumbnailImageView.hidden = YES;
|
|
45
|
+
[self addSubview:_thumbnailImageView];
|
|
46
|
+
|
|
37
47
|
// After the view is fully initialized, set it as the drawable
|
|
38
48
|
_player.drawable = self;
|
|
39
49
|
|
|
@@ -84,6 +94,66 @@
|
|
|
84
94
|
// Let VLC know the size has changed but don't force any redraws here
|
|
85
95
|
// This may be VLC-specific and not required for all implementations
|
|
86
96
|
}
|
|
97
|
+
|
|
98
|
+
// Update thumbnail image view frame
|
|
99
|
+
if (_thumbnailImageView) {
|
|
100
|
+
_thumbnailImageView.frame = bounds;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
- (void)setupThumbnailWithUrlString:(NSString *)thumbnailUrlString {
|
|
105
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] setupThumbnailWithUrlString: %@", thumbnailUrlString);
|
|
106
|
+
|
|
107
|
+
if (!thumbnailUrlString || [thumbnailUrlString length] == 0) {
|
|
108
|
+
// Hide thumbnail if URL is empty
|
|
109
|
+
_thumbnailImageView.hidden = YES;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Make sure thumbnail view is properly sized
|
|
114
|
+
_thumbnailImageView.frame = self.bounds;
|
|
115
|
+
|
|
116
|
+
// Show the thumbnail view
|
|
117
|
+
_thumbnailImageView.hidden = NO;
|
|
118
|
+
|
|
119
|
+
// Load the image from URL
|
|
120
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
121
|
+
NSURL *imageURL = [NSURL URLWithString:thumbnailUrlString];
|
|
122
|
+
if (!imageURL) {
|
|
123
|
+
// Try with encoding if the original URL doesn't work
|
|
124
|
+
NSString *escapedString = [thumbnailUrlString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
|
|
125
|
+
imageURL = [NSURL URLWithString:escapedString];
|
|
126
|
+
|
|
127
|
+
if (!imageURL) {
|
|
128
|
+
RCTLogError(@"[UnifiedPlayerViewManager] Invalid thumbnail URL format");
|
|
129
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
130
|
+
self->_thumbnailImageView.hidden = YES;
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
|
|
137
|
+
if (imageData) {
|
|
138
|
+
UIImage *image = [UIImage imageWithData:imageData];
|
|
139
|
+
if (image) {
|
|
140
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
141
|
+
self->_thumbnailImageView.image = image;
|
|
142
|
+
self->_thumbnailImageView.hidden = NO;
|
|
143
|
+
});
|
|
144
|
+
} else {
|
|
145
|
+
RCTLogError(@"[UnifiedPlayerViewManager] Failed to create image from data");
|
|
146
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
147
|
+
self->_thumbnailImageView.hidden = YES;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
RCTLogError(@"[UnifiedPlayerViewManager] Failed to load image data from URL");
|
|
152
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
153
|
+
self->_thumbnailImageView.hidden = YES;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
});
|
|
87
157
|
}
|
|
88
158
|
|
|
89
159
|
- (void)didMoveToSuperview {
|
|
@@ -388,6 +458,227 @@
|
|
|
388
458
|
}
|
|
389
459
|
}
|
|
390
460
|
|
|
461
|
+
- (BOOL)startRecordingToPath:(NSString *)outputPath {
|
|
462
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] startRecordingToPath: %@", outputPath);
|
|
463
|
+
|
|
464
|
+
if (_isRecording) {
|
|
465
|
+
RCTLogError(@"[UnifiedPlayerViewManager] Recording is already in progress");
|
|
466
|
+
return NO;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (!_player || !_player.isPlaying) {
|
|
470
|
+
RCTLogError(@"[UnifiedPlayerViewManager] Cannot start recording: Player is not playing");
|
|
471
|
+
return NO;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Store the recording path
|
|
475
|
+
_recordingPath = [outputPath copy];
|
|
476
|
+
|
|
477
|
+
// Create directory if it doesn't exist
|
|
478
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
479
|
+
NSString *directory = [outputPath stringByDeletingLastPathComponent];
|
|
480
|
+
if (![fileManager fileExistsAtPath:directory]) {
|
|
481
|
+
NSError *error = nil;
|
|
482
|
+
[fileManager createDirectoryAtPath:directory withIntermediateDirectories:YES attributes:nil error:&error];
|
|
483
|
+
if (error) {
|
|
484
|
+
RCTLogError(@"[UnifiedPlayerViewManager] Failed to create directory: %@", error);
|
|
485
|
+
return NO;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Set up AVAssetWriter
|
|
490
|
+
NSURL *outputURL = [NSURL fileURLWithPath:outputPath];
|
|
491
|
+
|
|
492
|
+
// Remove existing file if it exists
|
|
493
|
+
if ([fileManager fileExistsAtPath:outputPath]) {
|
|
494
|
+
NSError *error = nil;
|
|
495
|
+
[fileManager removeItemAtPath:outputPath error:&error];
|
|
496
|
+
if (error) {
|
|
497
|
+
RCTLogError(@"[UnifiedPlayerViewManager] Failed to remove existing file: %@", error);
|
|
498
|
+
return NO;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
NSError *error = nil;
|
|
503
|
+
_assetWriter = [[AVAssetWriter alloc] initWithURL:outputURL fileType:AVFileTypeMPEG4 error:&error];
|
|
504
|
+
if (error) {
|
|
505
|
+
RCTLogError(@"[UnifiedPlayerViewManager] Failed to create asset writer: %@", error);
|
|
506
|
+
return NO;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Get video dimensions
|
|
510
|
+
CGSize videoSize = _player.videoSize;
|
|
511
|
+
if (videoSize.width <= 0 || videoSize.height <= 0) {
|
|
512
|
+
// Use view size as fallback
|
|
513
|
+
videoSize = self.bounds.size;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Configure video settings
|
|
517
|
+
NSDictionary *videoSettings = @{
|
|
518
|
+
AVVideoCodecKey: AVVideoCodecTypeH264,
|
|
519
|
+
AVVideoWidthKey: @((int)videoSize.width),
|
|
520
|
+
AVVideoHeightKey: @((int)videoSize.height),
|
|
521
|
+
AVVideoCompressionPropertiesKey: @{
|
|
522
|
+
AVVideoAverageBitRateKey: @(2000000), // 2 Mbps
|
|
523
|
+
AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
// Create video input
|
|
528
|
+
_assetWriterVideoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
|
|
529
|
+
_assetWriterVideoInput.expectsMediaDataInRealTime = YES;
|
|
530
|
+
|
|
531
|
+
if ([_assetWriter canAddInput:_assetWriterVideoInput]) {
|
|
532
|
+
[_assetWriter addInput:_assetWriterVideoInput];
|
|
533
|
+
} else {
|
|
534
|
+
RCTLogError(@"[UnifiedPlayerViewManager] Cannot add video input to asset writer");
|
|
535
|
+
return NO;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Create a pixel buffer adaptor for writing pixel buffers
|
|
539
|
+
NSDictionary *pixelBufferAttributes = @{
|
|
540
|
+
(NSString *)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA),
|
|
541
|
+
(NSString *)kCVPixelBufferWidthKey: @((int)videoSize.width),
|
|
542
|
+
(NSString *)kCVPixelBufferHeightKey: @((int)videoSize.height),
|
|
543
|
+
(NSString *)kCVPixelBufferCGImageCompatibilityKey: @YES,
|
|
544
|
+
(NSString *)kCVPixelBufferCGBitmapContextCompatibilityKey: @YES
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
_assetWriterPixelBufferAdaptor = [AVAssetWriterInputPixelBufferAdaptor
|
|
548
|
+
assetWriterInputPixelBufferAdaptorWithAssetWriterInput:_assetWriterVideoInput
|
|
549
|
+
sourcePixelBufferAttributes:pixelBufferAttributes];
|
|
550
|
+
|
|
551
|
+
// Start recording session
|
|
552
|
+
if ([_assetWriter startWriting]) {
|
|
553
|
+
[_assetWriter startSessionAtSourceTime:kCMTimeZero];
|
|
554
|
+
_isRecording = YES;
|
|
555
|
+
|
|
556
|
+
// Start a timer to capture frames
|
|
557
|
+
[self startFrameCapture];
|
|
558
|
+
|
|
559
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] Recording started successfully");
|
|
560
|
+
return YES;
|
|
561
|
+
} else {
|
|
562
|
+
RCTLogError(@"[UnifiedPlayerViewManager] Failed to start writing: %@", _assetWriter.error);
|
|
563
|
+
return NO;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
- (void)startFrameCapture {
|
|
568
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] Frame capture started");
|
|
569
|
+
|
|
570
|
+
// Create a CADisplayLink to capture frames at the screen refresh rate
|
|
571
|
+
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(captureFrameForRecording)];
|
|
572
|
+
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
|
|
573
|
+
|
|
574
|
+
// Store the display link as an associated object
|
|
575
|
+
objc_setAssociatedObject(self, "displayLinkKey", displayLink, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
576
|
+
|
|
577
|
+
// Initialize frame count
|
|
578
|
+
_frameCount = 0;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
- (void)captureFrameForRecording {
|
|
582
|
+
if (!_isRecording || !_assetWriterVideoInput.isReadyForMoreMediaData) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Create a bitmap context to draw the current view
|
|
587
|
+
CGSize size = _player.videoSize;
|
|
588
|
+
if (size.width <= 0 || size.height <= 0) {
|
|
589
|
+
size = self.bounds.size;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Create a pixel buffer
|
|
593
|
+
CVPixelBufferRef pixelBuffer = NULL;
|
|
594
|
+
CVReturn status = CVPixelBufferPoolCreatePixelBuffer(NULL, _assetWriterPixelBufferAdaptor.pixelBufferPool, &pixelBuffer);
|
|
595
|
+
|
|
596
|
+
if (status != kCVReturnSuccess || pixelBuffer == NULL) {
|
|
597
|
+
RCTLogError(@"[UnifiedPlayerViewManager] Failed to create pixel buffer");
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Lock the pixel buffer
|
|
602
|
+
CVPixelBufferLockBaseAddress(pixelBuffer, 0);
|
|
603
|
+
|
|
604
|
+
// Get the pixel buffer address
|
|
605
|
+
void *pixelData = CVPixelBufferGetBaseAddress(pixelBuffer);
|
|
606
|
+
|
|
607
|
+
// Create a bitmap context
|
|
608
|
+
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
|
609
|
+
CGContextRef context = CGBitmapContextCreate(pixelData,
|
|
610
|
+
size.width,
|
|
611
|
+
size.height,
|
|
612
|
+
8,
|
|
613
|
+
CVPixelBufferGetBytesPerRow(pixelBuffer),
|
|
614
|
+
colorSpace,
|
|
615
|
+
kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little);
|
|
616
|
+
|
|
617
|
+
// Draw the current view into the context
|
|
618
|
+
UIGraphicsPushContext(context);
|
|
619
|
+
[self.layer renderInContext:context];
|
|
620
|
+
UIGraphicsPopContext();
|
|
621
|
+
|
|
622
|
+
// Clean up
|
|
623
|
+
CGContextRelease(context);
|
|
624
|
+
CGColorSpaceRelease(colorSpace);
|
|
625
|
+
|
|
626
|
+
// Unlock the pixel buffer
|
|
627
|
+
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
|
|
628
|
+
|
|
629
|
+
// Calculate the presentation time
|
|
630
|
+
CMTime presentationTime = CMTimeMake(_frameCount, 30); // 30 fps
|
|
631
|
+
|
|
632
|
+
// Append the pixel buffer to the asset writer
|
|
633
|
+
if (![_assetWriterPixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:presentationTime]) {
|
|
634
|
+
RCTLogError(@"[UnifiedPlayerViewManager] Failed to append pixel buffer: %@", _assetWriter.error);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Release the pixel buffer
|
|
638
|
+
CVPixelBufferRelease(pixelBuffer);
|
|
639
|
+
|
|
640
|
+
// Increment the frame count
|
|
641
|
+
_frameCount++;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
- (NSString *)stopRecording {
|
|
645
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] stopRecording called");
|
|
646
|
+
|
|
647
|
+
if (!_isRecording) {
|
|
648
|
+
RCTLogError(@"[UnifiedPlayerViewManager] No recording in progress");
|
|
649
|
+
return @"";
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Stop frame capture by stopping the display link
|
|
653
|
+
CADisplayLink *displayLink = objc_getAssociatedObject(self, "displayLinkKey");
|
|
654
|
+
if (displayLink) {
|
|
655
|
+
[displayLink invalidate];
|
|
656
|
+
objc_setAssociatedObject(self, "displayLinkKey", nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Finish writing
|
|
660
|
+
[_assetWriterVideoInput markAsFinished];
|
|
661
|
+
[_assetWriter finishWritingWithCompletionHandler:^{
|
|
662
|
+
if (self->_assetWriter.status == AVAssetWriterStatusCompleted) {
|
|
663
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] Recording completed successfully");
|
|
664
|
+
} else {
|
|
665
|
+
RCTLogError(@"[UnifiedPlayerViewManager] Recording failed: %@", self->_assetWriter.error);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Clean up
|
|
669
|
+
self->_assetWriter = nil;
|
|
670
|
+
self->_assetWriterVideoInput = nil;
|
|
671
|
+
self->_assetWriterPixelBufferAdaptor = nil;
|
|
672
|
+
self->_isRecording = NO;
|
|
673
|
+
self->_frameCount = 0;
|
|
674
|
+
}];
|
|
675
|
+
|
|
676
|
+
NSString *path = _recordingPath;
|
|
677
|
+
_recordingPath = nil;
|
|
678
|
+
|
|
679
|
+
return path;
|
|
680
|
+
}
|
|
681
|
+
|
|
391
682
|
- (void)setAutoplay:(BOOL)autoplay {
|
|
392
683
|
_autoplay = autoplay;
|
|
393
684
|
}
|
|
@@ -459,6 +750,11 @@
|
|
|
459
750
|
if (videoTracks.count > 0) {
|
|
460
751
|
RCTLogInfo(@"[UnifiedPlayerViewManager] Video tracks found: %lu", (unsigned long)videoTracks.count);
|
|
461
752
|
|
|
753
|
+
// Hide thumbnail when video starts playing
|
|
754
|
+
if (_thumbnailImageView) {
|
|
755
|
+
_thumbnailImageView.hidden = YES;
|
|
756
|
+
}
|
|
757
|
+
|
|
462
758
|
// Send playing event when we actually start playing
|
|
463
759
|
[self sendEvent:@"onPlaying" body:@{}];
|
|
464
760
|
|
|
@@ -556,6 +852,18 @@
|
|
|
556
852
|
// Remove all observers
|
|
557
853
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
558
854
|
|
|
855
|
+
// Stop recording if in progress
|
|
856
|
+
if (_isRecording) {
|
|
857
|
+
[self stopRecording];
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Clean up display link if it exists
|
|
861
|
+
CADisplayLink *displayLink = objc_getAssociatedObject(self, "displayLinkKey");
|
|
862
|
+
if (displayLink) {
|
|
863
|
+
[displayLink invalidate];
|
|
864
|
+
objc_setAssociatedObject(self, "displayLinkKey", nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
865
|
+
}
|
|
866
|
+
|
|
559
867
|
// Stop playback and release player
|
|
560
868
|
[_player stop];
|
|
561
869
|
_player.delegate = nil;
|
|
@@ -605,6 +913,13 @@ RCT_CUSTOM_VIEW_PROPERTY(videoUrl, NSString, UnifiedPlayerUIView)
|
|
|
605
913
|
[view setupWithVideoUrlString:json];
|
|
606
914
|
}
|
|
607
915
|
|
|
916
|
+
// Thumbnail URL property
|
|
917
|
+
RCT_CUSTOM_VIEW_PROPERTY(thumbnailUrl, NSString, UnifiedPlayerUIView)
|
|
918
|
+
{
|
|
919
|
+
view.thumbnailUrlString = json;
|
|
920
|
+
[view setupThumbnailWithUrlString:json];
|
|
921
|
+
}
|
|
922
|
+
|
|
608
923
|
// Autoplay property
|
|
609
924
|
RCT_CUSTOM_VIEW_PROPERTY(autoplay, BOOL, UnifiedPlayerUIView)
|
|
610
925
|
{
|
package/lib/module/index.js
CHANGED
|
@@ -163,6 +163,47 @@ export const UnifiedPlayer = {
|
|
|
163
163
|
console.log('Error calling capture:', error instanceof Error ? error.message : String(error));
|
|
164
164
|
return Promise.reject(error);
|
|
165
165
|
}
|
|
166
|
+
},
|
|
167
|
+
/**
|
|
168
|
+
* Start recording the video
|
|
169
|
+
* @param viewTag - The tag of the player view
|
|
170
|
+
* @param outputPath - Optional path where to save the recording (platform-specific)
|
|
171
|
+
* @returns Promise resolving to true if recording started successfully
|
|
172
|
+
*/
|
|
173
|
+
startRecording: (viewTag, outputPath) => {
|
|
174
|
+
try {
|
|
175
|
+
console.log('UnifiedPlayer.startRecording called with viewTag:', viewTag);
|
|
176
|
+
return UnifiedPlayerModule.startRecording(viewTag, outputPath).then(result => {
|
|
177
|
+
console.log('Native startRecording method called successfully');
|
|
178
|
+
return result;
|
|
179
|
+
}).catch(error => {
|
|
180
|
+
console.log('Error calling startRecording:', error instanceof Error ? error.message : String(error));
|
|
181
|
+
throw error;
|
|
182
|
+
});
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.log('Error calling startRecording:', error instanceof Error ? error.message : String(error));
|
|
185
|
+
return Promise.reject(error);
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
/**
|
|
189
|
+
* Stop recording the video
|
|
190
|
+
* @param viewTag - The tag of the player view
|
|
191
|
+
* @returns Promise resolving to the path of the saved recording
|
|
192
|
+
*/
|
|
193
|
+
stopRecording: viewTag => {
|
|
194
|
+
try {
|
|
195
|
+
console.log('UnifiedPlayer.stopRecording called with viewTag:', viewTag);
|
|
196
|
+
return UnifiedPlayerModule.stopRecording(viewTag).then(filePath => {
|
|
197
|
+
console.log('Native stopRecording method called successfully');
|
|
198
|
+
return filePath;
|
|
199
|
+
}).catch(error => {
|
|
200
|
+
console.log('Error calling stopRecording:', error instanceof Error ? error.message : String(error));
|
|
201
|
+
throw error;
|
|
202
|
+
});
|
|
203
|
+
} catch (error) {
|
|
204
|
+
console.log('Error calling stopRecording:', error instanceof Error ? error.message : String(error));
|
|
205
|
+
return Promise.reject(error);
|
|
206
|
+
}
|
|
166
207
|
}
|
|
167
208
|
};
|
|
168
209
|
//# sourceMappingURL=index.js.map
|
package/lib/module/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["forwardRef","requireNativeComponent","UIManager","NativeModules","Platform","jsx","_jsx","LINKING_ERROR","select","ios","default","getViewManagerConfig","UnifiedPlayer","Error","NativeUnifiedPlayerView","UnifiedPlayerModule","UnifiedPlayerEventTypes","LOAD_START","READY","ERROR","PROGRESS","COMPLETE","STALLED","RESUMED","PLAYING","PAUSED","UnifiedPlayerEvents","UnifiedPlayerView","props","ref","play","viewTag","console","log","then","result","catch","error","message","String","Promise","reject","pause","seekTo","time","getCurrentTime","getDuration","capture","base64String"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAA0BA,UAAU,QAAQ,OAAO,CAAC,CAAC;AACrD,SACEC,sBAAsB,EACtBC,SAAS,EACTC,aAAa,EACbC,QAAQ,QAEH,cAAc;;AAErB;AAAA,SAAAC,GAAA,IAAAC,IAAA;AACA,MAAMC,aAAa,GACjB,sFAAsF,GACtFH,QAAQ,CAACI,MAAM,CAAC;EAAEC,GAAG,EAAE,gCAAgC;EAAEC,OAAO,EAAE;AAAG,CAAC,CAAC,GACvE,sDAAsD,GACtD,+BAA+B;;AAEjC;AACA,IACE,CAACR,SAAS,CAACS,oBAAoB,CAAC,mBAAmB,CAAC,IACpD,CAACR,aAAa,CAACS,aAAa,EAC5B;EACA,MAAM,IAAIC,KAAK,CAACN,aAAa,CAAC;AAChC;;AAEA;;
|
|
1
|
+
{"version":3,"names":["forwardRef","requireNativeComponent","UIManager","NativeModules","Platform","jsx","_jsx","LINKING_ERROR","select","ios","default","getViewManagerConfig","UnifiedPlayer","Error","NativeUnifiedPlayerView","UnifiedPlayerModule","UnifiedPlayerEventTypes","LOAD_START","READY","ERROR","PROGRESS","COMPLETE","STALLED","RESUMED","PLAYING","PAUSED","UnifiedPlayerEvents","UnifiedPlayerView","props","ref","play","viewTag","console","log","then","result","catch","error","message","String","Promise","reject","pause","seekTo","time","getCurrentTime","getDuration","capture","base64String","startRecording","outputPath","stopRecording","filePath"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":"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","ignoreList":[]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ViewStyle } from 'react-native';
|
|
2
2
|
export type UnifiedPlayerProps = {
|
|
3
3
|
videoUrl: string;
|
|
4
|
+
thumbnailUrl?: string;
|
|
4
5
|
style: ViewStyle;
|
|
5
6
|
autoplay?: boolean;
|
|
6
7
|
loop?: boolean;
|
|
@@ -75,5 +76,18 @@ export declare const UnifiedPlayer: {
|
|
|
75
76
|
* @returns Promise resolving to the base64 encoded image string
|
|
76
77
|
*/
|
|
77
78
|
capture: (viewTag: number) => Promise<string>;
|
|
79
|
+
/**
|
|
80
|
+
* Start recording the video
|
|
81
|
+
* @param viewTag - The tag of the player view
|
|
82
|
+
* @param outputPath - Optional path where to save the recording (platform-specific)
|
|
83
|
+
* @returns Promise resolving to true if recording started successfully
|
|
84
|
+
*/
|
|
85
|
+
startRecording: (viewTag: number, outputPath?: string) => Promise<boolean>;
|
|
86
|
+
/**
|
|
87
|
+
* Stop recording the video
|
|
88
|
+
* @param viewTag - The tag of the player view
|
|
89
|
+
* @returns Promise resolving to the path of the saved recording
|
|
90
|
+
*/
|
|
91
|
+
stopRecording: (viewTag: number) => Promise<string>;
|
|
78
92
|
};
|
|
79
93
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AACA,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAkBtB,MAAM,MAAM,kBAAkB,GAAG;IAE/B,QAAQ,EAAE,MAAM,CAAC;IAGjB,KAAK,EAAE,SAAS,CAAC;IAGjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IAGnB,IAAI,CAAC,EAAE,OAAO,CAAC;IAGf,QAAQ,CAAC,EAAE,OAAO,CAAC;IAGnB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IAGzB,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAG3B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC;IAG/B,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAGhC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAGvE,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAC;IAG/B,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAC;IAG/B,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAGtB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;CACxB,CAAC;AAYF,eAAO,MAAM,uBAAuB;;;;;;;;;;CAUnC,CAAC;AAGF,eAAO,MAAM,mBAAmB,KAA8B,CAAC;AAE/D;;GAEG;AACH,eAAO,MAAM,iBAAiB,8LAK5B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa;IACxB;;;;OAIG;oBACa,MAAM,KAAG,OAAO,CAAC,OAAO,CAAC;IAwBzC;;;;OAIG;qBACc,MAAM,KAAG,OAAO,CAAC,OAAO,CAAC;IAwB1C;;;;;OAKG;sBACe,MAAM,QAAQ,MAAM,KAAG,OAAO,CAAC,OAAO,CAAC;IA6BzD;;;;OAIG;8BACuB,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC;IAalD;;;;OAIG;2BACoB,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC;IAa/C;;;;OAIG;uBACgB,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AACA,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAkBtB,MAAM,MAAM,kBAAkB,GAAG;IAE/B,QAAQ,EAAE,MAAM,CAAC;IAGjB,YAAY,CAAC,EAAE,MAAM,CAAC;IAGtB,KAAK,EAAE,SAAS,CAAC;IAGjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IAGnB,IAAI,CAAC,EAAE,OAAO,CAAC;IAGf,QAAQ,CAAC,EAAE,OAAO,CAAC;IAGnB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IAGzB,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAG3B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC;IAG/B,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAGhC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAGvE,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAC;IAG/B,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAC;IAG/B,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAGtB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;CACxB,CAAC;AAYF,eAAO,MAAM,uBAAuB;;;;;;;;;;CAUnC,CAAC;AAGF,eAAO,MAAM,mBAAmB,KAA8B,CAAC;AAE/D;;GAEG;AACH,eAAO,MAAM,iBAAiB,8LAK5B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa;IACxB;;;;OAIG;oBACa,MAAM,KAAG,OAAO,CAAC,OAAO,CAAC;IAwBzC;;;;OAIG;qBACc,MAAM,KAAG,OAAO,CAAC,OAAO,CAAC;IAwB1C;;;;;OAKG;sBACe,MAAM,QAAQ,MAAM,KAAG,OAAO,CAAC,OAAO,CAAC;IA6BzD;;;;OAIG;8BACuB,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC;IAalD;;;;OAIG;2BACoB,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC;IAa/C;;;;OAIG;uBACgB,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC;IAwB3C;;;;;OAKG;8BACuB,MAAM,eAAe,MAAM,KAAG,OAAO,CAAC,OAAO,CAAC;IAwBxE;;;;OAIG;6BACsB,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC;CAuBlD,CAAC"}
|
package/package.json
CHANGED
package/src/index.tsx
CHANGED
|
@@ -27,6 +27,9 @@ export type UnifiedPlayerProps = {
|
|
|
27
27
|
// Video source URL
|
|
28
28
|
videoUrl: string;
|
|
29
29
|
|
|
30
|
+
// Thumbnail image URL to display until video starts playing
|
|
31
|
+
thumbnailUrl?: string;
|
|
32
|
+
|
|
30
33
|
// Apply custom styling
|
|
31
34
|
style: ViewStyle;
|
|
32
35
|
|
|
@@ -263,4 +266,63 @@ export const UnifiedPlayer = {
|
|
|
263
266
|
return Promise.reject(error);
|
|
264
267
|
}
|
|
265
268
|
},
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Start recording the video
|
|
272
|
+
* @param viewTag - The tag of the player view
|
|
273
|
+
* @param outputPath - Optional path where to save the recording (platform-specific)
|
|
274
|
+
* @returns Promise resolving to true if recording started successfully
|
|
275
|
+
*/
|
|
276
|
+
startRecording: (viewTag: number, outputPath?: string): Promise<boolean> => {
|
|
277
|
+
try {
|
|
278
|
+
console.log('UnifiedPlayer.startRecording called with viewTag:', viewTag);
|
|
279
|
+
return UnifiedPlayerModule.startRecording(viewTag, outputPath)
|
|
280
|
+
.then((result: boolean) => {
|
|
281
|
+
console.log('Native startRecording method called successfully');
|
|
282
|
+
return result;
|
|
283
|
+
})
|
|
284
|
+
.catch((error: any) => {
|
|
285
|
+
console.log(
|
|
286
|
+
'Error calling startRecording:',
|
|
287
|
+
error instanceof Error ? error.message : String(error)
|
|
288
|
+
);
|
|
289
|
+
throw error;
|
|
290
|
+
});
|
|
291
|
+
} catch (error) {
|
|
292
|
+
console.log(
|
|
293
|
+
'Error calling startRecording:',
|
|
294
|
+
error instanceof Error ? error.message : String(error)
|
|
295
|
+
);
|
|
296
|
+
return Promise.reject(error);
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Stop recording the video
|
|
302
|
+
* @param viewTag - The tag of the player view
|
|
303
|
+
* @returns Promise resolving to the path of the saved recording
|
|
304
|
+
*/
|
|
305
|
+
stopRecording: (viewTag: number): Promise<string> => {
|
|
306
|
+
try {
|
|
307
|
+
console.log('UnifiedPlayer.stopRecording called with viewTag:', viewTag);
|
|
308
|
+
return UnifiedPlayerModule.stopRecording(viewTag)
|
|
309
|
+
.then((filePath: string) => {
|
|
310
|
+
console.log('Native stopRecording method called successfully');
|
|
311
|
+
return filePath;
|
|
312
|
+
})
|
|
313
|
+
.catch((error: any) => {
|
|
314
|
+
console.log(
|
|
315
|
+
'Error calling stopRecording:',
|
|
316
|
+
error instanceof Error ? error.message : String(error)
|
|
317
|
+
);
|
|
318
|
+
throw error;
|
|
319
|
+
});
|
|
320
|
+
} catch (error) {
|
|
321
|
+
console.log(
|
|
322
|
+
'Error calling stopRecording:',
|
|
323
|
+
error instanceof Error ? error.message : String(error)
|
|
324
|
+
);
|
|
325
|
+
return Promise.reject(error);
|
|
326
|
+
}
|
|
327
|
+
},
|
|
266
328
|
};
|