react-native-unified-player 0.3.6 → 0.3.8
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 +2 -2
- 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 +309 -62
- package/ios/UnifiedPlayerModule.m +94 -0
- package/ios/UnifiedPlayerUIView.h +13 -0
- package/ios/UnifiedPlayerViewManager.m +234 -0
- package/lib/module/index.js +41 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/index.d.ts +13 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.tsx +59 -0
package/android/build.gradle
CHANGED
|
@@ -77,6 +77,6 @@ dependencies {
|
|
|
77
77
|
implementation "com.facebook.react:react-android"
|
|
78
78
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
implementation "com.google.android.exoplayer:exoplayer-core:2.19.0"
|
|
81
|
+
implementation "com.google.android.exoplayer:exoplayer-ui:2.19.0"
|
|
82
82
|
}
|
|
@@ -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
|
}
|
|
@@ -25,6 +25,15 @@ import com.google.android.exoplayer2.video.VideoSize
|
|
|
25
25
|
import com.facebook.react.bridge.WritableMap
|
|
26
26
|
import com.facebook.react.bridge.ReactContext
|
|
27
27
|
import com.facebook.react.uimanager.events.RCTEventEmitter
|
|
28
|
+
import java.io.File
|
|
29
|
+
import java.io.IOException
|
|
30
|
+
import android.media.MediaCodec
|
|
31
|
+
import android.media.MediaCodecInfo
|
|
32
|
+
import android.media.MediaFormat
|
|
33
|
+
import android.media.MediaMuxer
|
|
34
|
+
import android.view.Surface
|
|
35
|
+
import java.nio.ByteBuffer
|
|
36
|
+
import android.os.Environment
|
|
28
37
|
import com.unifiedplayer.UnifiedPlayerEventEmitter.Companion.EVENT_COMPLETE
|
|
29
38
|
import com.unifiedplayer.UnifiedPlayerEventEmitter.Companion.EVENT_ERROR
|
|
30
39
|
import com.unifiedplayer.UnifiedPlayerEventEmitter.Companion.EVENT_LOAD_START
|
|
@@ -36,6 +45,15 @@ import com.unifiedplayer.UnifiedPlayerEventEmitter.Companion.EVENT_RESUMED
|
|
|
36
45
|
import com.unifiedplayer.UnifiedPlayerEventEmitter.Companion.EVENT_STALLED
|
|
37
46
|
|
|
38
47
|
class UnifiedPlayerView(context: Context) : FrameLayout(context) {
|
|
48
|
+
// Recording related variables
|
|
49
|
+
private var mediaRecorder: MediaMuxer? = null
|
|
50
|
+
private var videoEncoder: MediaCodec? = null
|
|
51
|
+
private var recordingSurface: Surface? = null
|
|
52
|
+
private var isRecording = false
|
|
53
|
+
private var outputPath: String? = null
|
|
54
|
+
private var recordingThread: Thread? = null
|
|
55
|
+
private var videoTrackIndex = -1
|
|
56
|
+
private val bufferInfo = MediaCodec.BufferInfo()
|
|
39
57
|
companion object {
|
|
40
58
|
private const val TAG = "UnifiedPlayerView"
|
|
41
59
|
}
|
|
@@ -43,9 +61,8 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
|
|
|
43
61
|
private var videoUrl: String? = null
|
|
44
62
|
private var autoplay: Boolean = true
|
|
45
63
|
private var loop: Boolean = false
|
|
46
|
-
private var
|
|
47
|
-
|
|
48
|
-
private var player: ExoPlayer? = null
|
|
64
|
+
private var textureView: android.view.TextureView
|
|
65
|
+
internal var player: ExoPlayer? = null
|
|
49
66
|
private var currentProgress = 0
|
|
50
67
|
private var isPaused = false
|
|
51
68
|
|
|
@@ -83,33 +100,23 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
|
|
|
83
100
|
// Create ExoPlayer
|
|
84
101
|
player = ExoPlayer.Builder(context).build()
|
|
85
102
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
// Create PlayerView without SurfaceView
|
|
95
|
-
playerView = PlayerView(context).apply {
|
|
96
|
-
layoutParams = LayoutParams(
|
|
97
|
-
LayoutParams.MATCH_PARENT,
|
|
98
|
-
LayoutParams.MATCH_PARENT
|
|
99
|
-
)
|
|
100
|
-
setPlayer(player)
|
|
101
|
-
useController = false // Disable default controls
|
|
102
|
-
}
|
|
103
|
+
// Create TextureView for video rendering
|
|
104
|
+
textureView = android.view.TextureView(context).apply {
|
|
105
|
+
layoutParams = LayoutParams(
|
|
106
|
+
LayoutParams.MATCH_PARENT,
|
|
107
|
+
LayoutParams.MATCH_PARENT
|
|
108
|
+
)
|
|
109
|
+
}
|
|
103
110
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
addView(playerView)
|
|
111
|
+
// Add TextureView to the view hierarchy
|
|
112
|
+
addView(textureView)
|
|
107
113
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
114
|
+
// We'll set the video surface when the TextureView's surface is available
|
|
115
|
+
// in the onSurfaceTextureAvailable callback
|
|
116
|
+
Log.d(TAG, "TextureView added to view hierarchy")
|
|
117
|
+
// Log dimensions of the view
|
|
118
|
+
post {
|
|
119
|
+
Log.d(TAG, "UnifiedPlayerView dimensions after init: width=${width}, height=${height}")
|
|
113
120
|
}
|
|
114
121
|
|
|
115
122
|
player?.addListener(object : Player.Listener {
|
|
@@ -195,24 +202,7 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
|
|
|
195
202
|
}
|
|
196
203
|
|
|
197
204
|
override fun onVideoSizeChanged(videoSize: VideoSize) {
|
|
198
|
-
//
|
|
199
|
-
Log.d(TAG, "ExoPlayer onVideoSizeChanged: videoSize=$videoSize")
|
|
200
|
-
Log.d(TAG, "Video size changed: width=${videoSize.width}, height=${videoSize.height}")
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
override fun onSurfaceSizeChanged(width: Int, height: Int) {
|
|
204
|
-
// Called when the size of the surface changes.
|
|
205
|
-
Log.d(TAG, "ExoPlayer onSurfaceSizeChanged: width=$width, height=$height")
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
override fun onRenderedFirstFrame() {
|
|
209
|
-
// Called when the first frame is rendered.
|
|
210
|
-
Log.d(TAG, "ExoPlayer onRenderedFirstFrame")
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
override fun onSkipSilenceEnabledChanged(skipSilenceEnabled: Boolean) {
|
|
214
|
-
// Called when skip silence is enabled or disabled.
|
|
215
|
-
Log.d(TAG, "ExoPlayer onSkipSilenceEnabledChanged: skipSilenceEnabled=$skipSilenceEnabled")
|
|
205
|
+
// Handle video size changes if needed
|
|
216
206
|
}
|
|
217
207
|
})
|
|
218
208
|
}
|
|
@@ -273,14 +263,9 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
|
|
|
273
263
|
}
|
|
274
264
|
}
|
|
275
265
|
|
|
276
|
-
fun setAuthToken(token: String?) {
|
|
277
|
-
// Removed as per request
|
|
278
|
-
Log.d(TAG, "Auth token handling removed")
|
|
279
|
-
}
|
|
280
|
-
|
|
281
266
|
fun setAutoplay(value: Boolean) {
|
|
282
267
|
autoplay = value
|
|
283
|
-
player?.playWhenReady = value
|
|
268
|
+
player?.playWhenReady = value
|
|
284
269
|
}
|
|
285
270
|
|
|
286
271
|
fun setLoop(value: Boolean) {
|
|
@@ -316,7 +301,7 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
|
|
|
316
301
|
it.seekTo(milliseconds)
|
|
317
302
|
|
|
318
303
|
// Force a progress update after seeking
|
|
319
|
-
|
|
304
|
+
progressRunnable.run()
|
|
320
305
|
} ?: Log.e(TAG, "Cannot seek: player is null")
|
|
321
306
|
}
|
|
322
307
|
|
|
@@ -390,14 +375,39 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
|
|
|
390
375
|
val width = right - left
|
|
391
376
|
val height = bottom - top
|
|
392
377
|
Log.d(TAG, "UnifiedPlayerView onLayout: width=$width, height=$height")
|
|
393
|
-
// Ensure
|
|
394
|
-
|
|
378
|
+
// Ensure textureView gets laid out properly
|
|
379
|
+
textureView.layout(0, 0, width, height)
|
|
395
380
|
}
|
|
396
381
|
|
|
397
382
|
override fun onAttachedToWindow() {
|
|
398
383
|
super.onAttachedToWindow()
|
|
399
384
|
Log.d(TAG, "UnifiedPlayerView onAttachedToWindow")
|
|
400
|
-
|
|
385
|
+
textureView.surfaceTextureListener = object : android.view.TextureView.SurfaceTextureListener {
|
|
386
|
+
override fun onSurfaceTextureAvailable(surface: android.graphics.SurfaceTexture, width: Int, height: Int) {
|
|
387
|
+
Log.d(TAG, "TextureView onSurfaceTextureAvailable: width=$width, height=$height")
|
|
388
|
+
// Create a Surface from the SurfaceTexture and set it on the player
|
|
389
|
+
val videoSurface = android.view.Surface(surface)
|
|
390
|
+
player?.setVideoSurface(videoSurface)
|
|
391
|
+
Log.d(TAG, "Set video surface from TextureView's SurfaceTexture")
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
override fun onSurfaceTextureSizeChanged(surface: android.graphics.SurfaceTexture, width: Int, height: Int) {
|
|
395
|
+
Log.d(TAG, "TextureView onSurfaceTextureSizeChanged: width=$width, height=$height")
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
override fun onSurfaceTextureDestroyed(surface: android.graphics.SurfaceTexture): Boolean {
|
|
399
|
+
Log.d(TAG, "TextureView onSurfaceTextureDestroyed")
|
|
400
|
+
// Set the player's surface to null to release it
|
|
401
|
+
player?.setVideoSurface(null)
|
|
402
|
+
Log.d(TAG, "Cleared video surface from player")
|
|
403
|
+
return true
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
override fun onSurfaceTextureUpdated(surface: android.graphics.SurfaceTexture) {
|
|
407
|
+
// This is called very frequently, so we'll comment out this log
|
|
408
|
+
// Log.d(TAG, "TextureView onSurfaceTextureUpdated")
|
|
409
|
+
}
|
|
410
|
+
}
|
|
401
411
|
startProgressUpdates() // Use the new method to start progress updates
|
|
402
412
|
}
|
|
403
413
|
|
|
@@ -420,11 +430,31 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
|
|
|
420
430
|
}
|
|
421
431
|
|
|
422
432
|
// Get bitmap directly from TextureView
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
433
|
+
val bitmap = textureView.bitmap ?: run {
|
|
434
|
+
Log.e(TAG, "Failed to get bitmap from TextureView")
|
|
435
|
+
return ""
|
|
436
|
+
}
|
|
427
437
|
|
|
438
|
+
// Debugging: Log the dimensions of the bitmap
|
|
439
|
+
bitmap.let {
|
|
440
|
+
Log.d(TAG, "Bitmap dimensions: width=${it.width}, height=${it.height}")
|
|
441
|
+
|
|
442
|
+
// Check if bitmap is empty (all black)
|
|
443
|
+
var hasNonBlackPixel = false
|
|
444
|
+
for (x in 0 until it.width) {
|
|
445
|
+
for (y in 0 until it.height) {
|
|
446
|
+
if (it.getPixel(x, y) != Color.BLACK) {
|
|
447
|
+
hasNonBlackPixel = true
|
|
448
|
+
break
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (hasNonBlackPixel) break
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (!hasNonBlackPixel) {
|
|
455
|
+
Log.w(TAG, "Bitmap appears to be all black")
|
|
456
|
+
}
|
|
457
|
+
}
|
|
428
458
|
// Compress and encode the bitmap
|
|
429
459
|
val byteArrayOutputStream = ByteArrayOutputStream()
|
|
430
460
|
if (bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)) {
|
|
@@ -439,10 +469,227 @@ class UnifiedPlayerView(context: Context) : FrameLayout(context) {
|
|
|
439
469
|
} ?: run {
|
|
440
470
|
Log.e(TAG, "Cannot capture: player is null")
|
|
441
471
|
""
|
|
442
|
-
|
|
443
|
-
|
|
472
|
+
}
|
|
473
|
+
} catch (e: Exception) {
|
|
444
474
|
Log.e(TAG, "Error during capture: ${e.message}", e)
|
|
445
475
|
""
|
|
446
476
|
}
|
|
447
477
|
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Start recording the video to the specified output path
|
|
481
|
+
* @param outputPath Path where to save the recording
|
|
482
|
+
* @return true if recording started successfully
|
|
483
|
+
*/
|
|
484
|
+
fun startRecording(outputPath: String): Boolean {
|
|
485
|
+
Log.d(TAG, "startRecording called with outputPath: $outputPath")
|
|
486
|
+
|
|
487
|
+
if (isRecording) {
|
|
488
|
+
Log.w(TAG, "Recording is already in progress")
|
|
489
|
+
return false
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
player?.let { exoPlayer ->
|
|
494
|
+
// Get the current media item's URI
|
|
495
|
+
val currentUri = exoPlayer.currentMediaItem?.localConfiguration?.uri?.toString()
|
|
496
|
+
if (currentUri == null) {
|
|
497
|
+
Log.e(TAG, "Current media URI is null")
|
|
498
|
+
return false
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
Log.d(TAG, "Current media URI: $currentUri")
|
|
502
|
+
|
|
503
|
+
// Store the output path
|
|
504
|
+
this.outputPath = if (outputPath.isNullOrEmpty()) {
|
|
505
|
+
// Use app-specific storage for Android 10+ (API level 29+)
|
|
506
|
+
val appContext = context.applicationContext
|
|
507
|
+
val moviesDir = File(appContext.getExternalFilesDir(Environment.DIRECTORY_MOVIES), "recordings")
|
|
508
|
+
if (!moviesDir.exists()) {
|
|
509
|
+
moviesDir.mkdirs()
|
|
510
|
+
}
|
|
511
|
+
File(moviesDir, "recording_${System.currentTimeMillis()}.mp4").absolutePath
|
|
512
|
+
} else {
|
|
513
|
+
outputPath
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Create parent directories if they don't exist
|
|
517
|
+
val outputFile = File(this.outputPath)
|
|
518
|
+
outputFile.parentFile?.mkdirs()
|
|
519
|
+
|
|
520
|
+
// Log the final output path
|
|
521
|
+
Log.d(TAG, "Recording will be saved to: ${this.outputPath}")
|
|
522
|
+
|
|
523
|
+
// Start a background thread to download the file
|
|
524
|
+
Thread {
|
|
525
|
+
try {
|
|
526
|
+
// Create a URL from the URI
|
|
527
|
+
val url = java.net.URL(currentUri)
|
|
528
|
+
|
|
529
|
+
// Open connection
|
|
530
|
+
val connection = url.openConnection() as java.net.HttpURLConnection
|
|
531
|
+
connection.requestMethod = "GET"
|
|
532
|
+
connection.connectTimeout = 15000
|
|
533
|
+
connection.readTimeout = 15000
|
|
534
|
+
connection.doInput = true
|
|
535
|
+
connection.connect()
|
|
536
|
+
|
|
537
|
+
// Check if the connection was successful
|
|
538
|
+
if (connection.responseCode != java.net.HttpURLConnection.HTTP_OK) {
|
|
539
|
+
Log.e(TAG, "HTTP error code: ${connection.responseCode}")
|
|
540
|
+
return@Thread
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Get the input stream
|
|
544
|
+
val inputStream = connection.inputStream
|
|
545
|
+
|
|
546
|
+
// Create the output file
|
|
547
|
+
val outputFile = File(this.outputPath!!)
|
|
548
|
+
|
|
549
|
+
// Create the output stream
|
|
550
|
+
val outputStream = outputFile.outputStream()
|
|
551
|
+
|
|
552
|
+
// Create a buffer
|
|
553
|
+
val buffer = ByteArray(1024)
|
|
554
|
+
var bytesRead: Int
|
|
555
|
+
var totalBytesRead: Long = 0
|
|
556
|
+
val fileSize = connection.contentLength.toLong()
|
|
557
|
+
|
|
558
|
+
// Read from the input stream and write to the output stream
|
|
559
|
+
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
|
|
560
|
+
outputStream.write(buffer, 0, bytesRead)
|
|
561
|
+
totalBytesRead += bytesRead
|
|
562
|
+
|
|
563
|
+
// Log progress
|
|
564
|
+
if (fileSize > 0) {
|
|
565
|
+
val progress = (totalBytesRead * 100 / fileSize).toInt()
|
|
566
|
+
Log.d(TAG, "Download progress: $progress%")
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Close the streams
|
|
571
|
+
outputStream.flush()
|
|
572
|
+
outputStream.close()
|
|
573
|
+
inputStream.close()
|
|
574
|
+
|
|
575
|
+
Log.d(TAG, "File downloaded successfully to ${this.outputPath}")
|
|
576
|
+
} catch (e: Exception) {
|
|
577
|
+
Log.e(TAG, "Error downloading file: ${e.message}", e)
|
|
578
|
+
}
|
|
579
|
+
}.start()
|
|
580
|
+
|
|
581
|
+
isRecording = true
|
|
582
|
+
Log.d(TAG, "Recording started successfully")
|
|
583
|
+
return true
|
|
584
|
+
} ?: run {
|
|
585
|
+
Log.e(TAG, "Cannot start recording: player is null")
|
|
586
|
+
return false
|
|
587
|
+
}
|
|
588
|
+
} catch (e: Exception) {
|
|
589
|
+
Log.e(TAG, "Error starting recording: ${e.message}", e)
|
|
590
|
+
return false
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Stop recording and save the video
|
|
596
|
+
* @return Path to the saved recording
|
|
597
|
+
*/
|
|
598
|
+
fun stopRecording(): String {
|
|
599
|
+
Log.d(TAG, "stopRecording called")
|
|
600
|
+
|
|
601
|
+
if (!isRecording) {
|
|
602
|
+
Log.w(TAG, "No recording in progress")
|
|
603
|
+
return ""
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Simply mark recording as stopped
|
|
607
|
+
isRecording = false
|
|
608
|
+
|
|
609
|
+
// Wait a moment to ensure any background operations complete
|
|
610
|
+
try {
|
|
611
|
+
Thread.sleep(500)
|
|
612
|
+
} catch (e: InterruptedException) {
|
|
613
|
+
Log.e(TAG, "Sleep interrupted: ${e.message}")
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Return the path where the recording was saved
|
|
617
|
+
val savedPath = outputPath ?: ""
|
|
618
|
+
Log.d(TAG, "Recording stopped successfully, saved to: $savedPath")
|
|
619
|
+
|
|
620
|
+
return savedPath
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private fun cleanupRecording() {
|
|
624
|
+
try {
|
|
625
|
+
videoEncoder?.stop()
|
|
626
|
+
videoEncoder?.release()
|
|
627
|
+
videoEncoder = null
|
|
628
|
+
|
|
629
|
+
recordingSurface?.release()
|
|
630
|
+
recordingSurface = null
|
|
631
|
+
|
|
632
|
+
mediaRecorder?.stop()
|
|
633
|
+
mediaRecorder?.release()
|
|
634
|
+
mediaRecorder = null
|
|
635
|
+
|
|
636
|
+
videoTrackIndex = -1
|
|
637
|
+
isRecording = false
|
|
638
|
+
} catch (e: Exception) {
|
|
639
|
+
Log.e(TAG, "Error cleaning up recording resources: ${e.message}", e)
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
private inner class RecordingRunnable : Runnable {
|
|
644
|
+
override fun run() {
|
|
645
|
+
try {
|
|
646
|
+
// Add video track to muxer
|
|
647
|
+
val videoFormat = videoEncoder?.outputFormat
|
|
648
|
+
if (videoFormat != null && mediaRecorder != null) {
|
|
649
|
+
videoTrackIndex = mediaRecorder!!.addTrack(videoFormat)
|
|
650
|
+
} else {
|
|
651
|
+
Log.e(TAG, "Cannot add track: videoFormat or mediaRecorder is null")
|
|
652
|
+
videoTrackIndex = -1
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Start the muxer
|
|
656
|
+
mediaRecorder?.start()
|
|
657
|
+
|
|
658
|
+
// Process encoding
|
|
659
|
+
while (isRecording) {
|
|
660
|
+
val encoderStatus = videoEncoder?.dequeueOutputBuffer(bufferInfo, 10000) ?: -1
|
|
661
|
+
|
|
662
|
+
if (encoderStatus >= 0) {
|
|
663
|
+
val encodedData = videoEncoder?.getOutputBuffer(encoderStatus)
|
|
664
|
+
|
|
665
|
+
if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
|
|
666
|
+
// Ignore codec config data
|
|
667
|
+
bufferInfo.size = 0
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (bufferInfo.size > 0 && encodedData != null && mediaRecorder != null && videoTrackIndex >= 0) {
|
|
671
|
+
encodedData.position(bufferInfo.offset)
|
|
672
|
+
encodedData.limit(bufferInfo.offset + bufferInfo.size)
|
|
673
|
+
|
|
674
|
+
mediaRecorder!!.writeSampleData(videoTrackIndex, encodedData, bufferInfo)
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
videoEncoder?.releaseOutputBuffer(encoderStatus, false)
|
|
678
|
+
|
|
679
|
+
if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
|
|
680
|
+
break
|
|
681
|
+
}
|
|
682
|
+
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
|
683
|
+
// Handle format change if needed
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Signal end of stream to encoder
|
|
688
|
+
videoEncoder?.signalEndOfInputStream()
|
|
689
|
+
|
|
690
|
+
} catch (e: Exception) {
|
|
691
|
+
Log.e(TAG, "Error in recording thread: ${e.message}", e)
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
448
695
|
}
|
|
@@ -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
|
|
|
@@ -17,6 +19,13 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
17
19
|
@property (nonatomic, assign) VLCMediaPlayerState previousState;
|
|
18
20
|
@property (nonatomic, assign) BOOL hasRenderedVideo;
|
|
19
21
|
@property (nonatomic, assign) BOOL readyEventSent;
|
|
22
|
+
@property (nonatomic, assign) BOOL isRecording;
|
|
23
|
+
@property (nonatomic, strong) AVAssetWriter *assetWriter;
|
|
24
|
+
@property (nonatomic, strong) AVAssetWriterInput *assetWriterVideoInput;
|
|
25
|
+
@property (nonatomic, strong) AVAssetWriterInputPixelBufferAdaptor *assetWriterPixelBufferAdaptor;
|
|
26
|
+
@property (nonatomic, strong) NSString *recordingPath;
|
|
27
|
+
// We'll use associated objects instead of a property for CADisplayLink
|
|
28
|
+
@property (nonatomic, assign) NSInteger frameCount;
|
|
20
29
|
|
|
21
30
|
// Event callbacks
|
|
22
31
|
@property (nonatomic, copy) RCTDirectEventBlock onLoadStart;
|
|
@@ -37,6 +46,10 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
37
46
|
- (float)getCurrentTime;
|
|
38
47
|
- (float)getDuration;
|
|
39
48
|
- (void)captureFrameWithCompletion:(void (^)(NSString * _Nullable base64String, NSError * _Nullable error))completion;
|
|
49
|
+
- (void)captureFrameForRecording;
|
|
50
|
+
- (BOOL)startRecordingToPath:(NSString *)outputPath;
|
|
51
|
+
- (void)startFrameCapture;
|
|
52
|
+
- (NSString *)stopRecording;
|
|
40
53
|
|
|
41
54
|
@end
|
|
42
55
|
|
|
@@ -7,6 +7,7 @@
|
|
|
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
|
|
|
@@ -388,6 +389,227 @@
|
|
|
388
389
|
}
|
|
389
390
|
}
|
|
390
391
|
|
|
392
|
+
- (BOOL)startRecordingToPath:(NSString *)outputPath {
|
|
393
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] startRecordingToPath: %@", outputPath);
|
|
394
|
+
|
|
395
|
+
if (_isRecording) {
|
|
396
|
+
RCTLogError(@"[UnifiedPlayerViewManager] Recording is already in progress");
|
|
397
|
+
return NO;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (!_player || !_player.isPlaying) {
|
|
401
|
+
RCTLogError(@"[UnifiedPlayerViewManager] Cannot start recording: Player is not playing");
|
|
402
|
+
return NO;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Store the recording path
|
|
406
|
+
_recordingPath = [outputPath copy];
|
|
407
|
+
|
|
408
|
+
// Create directory if it doesn't exist
|
|
409
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
410
|
+
NSString *directory = [outputPath stringByDeletingLastPathComponent];
|
|
411
|
+
if (![fileManager fileExistsAtPath:directory]) {
|
|
412
|
+
NSError *error = nil;
|
|
413
|
+
[fileManager createDirectoryAtPath:directory withIntermediateDirectories:YES attributes:nil error:&error];
|
|
414
|
+
if (error) {
|
|
415
|
+
RCTLogError(@"[UnifiedPlayerViewManager] Failed to create directory: %@", error);
|
|
416
|
+
return NO;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Set up AVAssetWriter
|
|
421
|
+
NSURL *outputURL = [NSURL fileURLWithPath:outputPath];
|
|
422
|
+
|
|
423
|
+
// Remove existing file if it exists
|
|
424
|
+
if ([fileManager fileExistsAtPath:outputPath]) {
|
|
425
|
+
NSError *error = nil;
|
|
426
|
+
[fileManager removeItemAtPath:outputPath error:&error];
|
|
427
|
+
if (error) {
|
|
428
|
+
RCTLogError(@"[UnifiedPlayerViewManager] Failed to remove existing file: %@", error);
|
|
429
|
+
return NO;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
NSError *error = nil;
|
|
434
|
+
_assetWriter = [[AVAssetWriter alloc] initWithURL:outputURL fileType:AVFileTypeMPEG4 error:&error];
|
|
435
|
+
if (error) {
|
|
436
|
+
RCTLogError(@"[UnifiedPlayerViewManager] Failed to create asset writer: %@", error);
|
|
437
|
+
return NO;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Get video dimensions
|
|
441
|
+
CGSize videoSize = _player.videoSize;
|
|
442
|
+
if (videoSize.width <= 0 || videoSize.height <= 0) {
|
|
443
|
+
// Use view size as fallback
|
|
444
|
+
videoSize = self.bounds.size;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Configure video settings
|
|
448
|
+
NSDictionary *videoSettings = @{
|
|
449
|
+
AVVideoCodecKey: AVVideoCodecTypeH264,
|
|
450
|
+
AVVideoWidthKey: @((int)videoSize.width),
|
|
451
|
+
AVVideoHeightKey: @((int)videoSize.height),
|
|
452
|
+
AVVideoCompressionPropertiesKey: @{
|
|
453
|
+
AVVideoAverageBitRateKey: @(2000000), // 2 Mbps
|
|
454
|
+
AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
// Create video input
|
|
459
|
+
_assetWriterVideoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
|
|
460
|
+
_assetWriterVideoInput.expectsMediaDataInRealTime = YES;
|
|
461
|
+
|
|
462
|
+
if ([_assetWriter canAddInput:_assetWriterVideoInput]) {
|
|
463
|
+
[_assetWriter addInput:_assetWriterVideoInput];
|
|
464
|
+
} else {
|
|
465
|
+
RCTLogError(@"[UnifiedPlayerViewManager] Cannot add video input to asset writer");
|
|
466
|
+
return NO;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Create a pixel buffer adaptor for writing pixel buffers
|
|
470
|
+
NSDictionary *pixelBufferAttributes = @{
|
|
471
|
+
(NSString *)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA),
|
|
472
|
+
(NSString *)kCVPixelBufferWidthKey: @((int)videoSize.width),
|
|
473
|
+
(NSString *)kCVPixelBufferHeightKey: @((int)videoSize.height),
|
|
474
|
+
(NSString *)kCVPixelBufferCGImageCompatibilityKey: @YES,
|
|
475
|
+
(NSString *)kCVPixelBufferCGBitmapContextCompatibilityKey: @YES
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
_assetWriterPixelBufferAdaptor = [AVAssetWriterInputPixelBufferAdaptor
|
|
479
|
+
assetWriterInputPixelBufferAdaptorWithAssetWriterInput:_assetWriterVideoInput
|
|
480
|
+
sourcePixelBufferAttributes:pixelBufferAttributes];
|
|
481
|
+
|
|
482
|
+
// Start recording session
|
|
483
|
+
if ([_assetWriter startWriting]) {
|
|
484
|
+
[_assetWriter startSessionAtSourceTime:kCMTimeZero];
|
|
485
|
+
_isRecording = YES;
|
|
486
|
+
|
|
487
|
+
// Start a timer to capture frames
|
|
488
|
+
[self startFrameCapture];
|
|
489
|
+
|
|
490
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] Recording started successfully");
|
|
491
|
+
return YES;
|
|
492
|
+
} else {
|
|
493
|
+
RCTLogError(@"[UnifiedPlayerViewManager] Failed to start writing: %@", _assetWriter.error);
|
|
494
|
+
return NO;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
- (void)startFrameCapture {
|
|
499
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] Frame capture started");
|
|
500
|
+
|
|
501
|
+
// Create a CADisplayLink to capture frames at the screen refresh rate
|
|
502
|
+
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(captureFrameForRecording)];
|
|
503
|
+
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
|
|
504
|
+
|
|
505
|
+
// Store the display link as an associated object
|
|
506
|
+
objc_setAssociatedObject(self, "displayLinkKey", displayLink, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
507
|
+
|
|
508
|
+
// Initialize frame count
|
|
509
|
+
_frameCount = 0;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
- (void)captureFrameForRecording {
|
|
513
|
+
if (!_isRecording || !_assetWriterVideoInput.isReadyForMoreMediaData) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Create a bitmap context to draw the current view
|
|
518
|
+
CGSize size = _player.videoSize;
|
|
519
|
+
if (size.width <= 0 || size.height <= 0) {
|
|
520
|
+
size = self.bounds.size;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Create a pixel buffer
|
|
524
|
+
CVPixelBufferRef pixelBuffer = NULL;
|
|
525
|
+
CVReturn status = CVPixelBufferPoolCreatePixelBuffer(NULL, _assetWriterPixelBufferAdaptor.pixelBufferPool, &pixelBuffer);
|
|
526
|
+
|
|
527
|
+
if (status != kCVReturnSuccess || pixelBuffer == NULL) {
|
|
528
|
+
RCTLogError(@"[UnifiedPlayerViewManager] Failed to create pixel buffer");
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Lock the pixel buffer
|
|
533
|
+
CVPixelBufferLockBaseAddress(pixelBuffer, 0);
|
|
534
|
+
|
|
535
|
+
// Get the pixel buffer address
|
|
536
|
+
void *pixelData = CVPixelBufferGetBaseAddress(pixelBuffer);
|
|
537
|
+
|
|
538
|
+
// Create a bitmap context
|
|
539
|
+
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
|
540
|
+
CGContextRef context = CGBitmapContextCreate(pixelData,
|
|
541
|
+
size.width,
|
|
542
|
+
size.height,
|
|
543
|
+
8,
|
|
544
|
+
CVPixelBufferGetBytesPerRow(pixelBuffer),
|
|
545
|
+
colorSpace,
|
|
546
|
+
kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little);
|
|
547
|
+
|
|
548
|
+
// Draw the current view into the context
|
|
549
|
+
UIGraphicsPushContext(context);
|
|
550
|
+
[self.layer renderInContext:context];
|
|
551
|
+
UIGraphicsPopContext();
|
|
552
|
+
|
|
553
|
+
// Clean up
|
|
554
|
+
CGContextRelease(context);
|
|
555
|
+
CGColorSpaceRelease(colorSpace);
|
|
556
|
+
|
|
557
|
+
// Unlock the pixel buffer
|
|
558
|
+
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
|
|
559
|
+
|
|
560
|
+
// Calculate the presentation time
|
|
561
|
+
CMTime presentationTime = CMTimeMake(_frameCount, 30); // 30 fps
|
|
562
|
+
|
|
563
|
+
// Append the pixel buffer to the asset writer
|
|
564
|
+
if (![_assetWriterPixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:presentationTime]) {
|
|
565
|
+
RCTLogError(@"[UnifiedPlayerViewManager] Failed to append pixel buffer: %@", _assetWriter.error);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Release the pixel buffer
|
|
569
|
+
CVPixelBufferRelease(pixelBuffer);
|
|
570
|
+
|
|
571
|
+
// Increment the frame count
|
|
572
|
+
_frameCount++;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
- (NSString *)stopRecording {
|
|
576
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] stopRecording called");
|
|
577
|
+
|
|
578
|
+
if (!_isRecording) {
|
|
579
|
+
RCTLogError(@"[UnifiedPlayerViewManager] No recording in progress");
|
|
580
|
+
return @"";
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Stop frame capture by stopping the display link
|
|
584
|
+
CADisplayLink *displayLink = objc_getAssociatedObject(self, "displayLinkKey");
|
|
585
|
+
if (displayLink) {
|
|
586
|
+
[displayLink invalidate];
|
|
587
|
+
objc_setAssociatedObject(self, "displayLinkKey", nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Finish writing
|
|
591
|
+
[_assetWriterVideoInput markAsFinished];
|
|
592
|
+
[_assetWriter finishWritingWithCompletionHandler:^{
|
|
593
|
+
if (self->_assetWriter.status == AVAssetWriterStatusCompleted) {
|
|
594
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] Recording completed successfully");
|
|
595
|
+
} else {
|
|
596
|
+
RCTLogError(@"[UnifiedPlayerViewManager] Recording failed: %@", self->_assetWriter.error);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Clean up
|
|
600
|
+
self->_assetWriter = nil;
|
|
601
|
+
self->_assetWriterVideoInput = nil;
|
|
602
|
+
self->_assetWriterPixelBufferAdaptor = nil;
|
|
603
|
+
self->_isRecording = NO;
|
|
604
|
+
self->_frameCount = 0;
|
|
605
|
+
}];
|
|
606
|
+
|
|
607
|
+
NSString *path = _recordingPath;
|
|
608
|
+
_recordingPath = nil;
|
|
609
|
+
|
|
610
|
+
return path;
|
|
611
|
+
}
|
|
612
|
+
|
|
391
613
|
- (void)setAutoplay:(BOOL)autoplay {
|
|
392
614
|
_autoplay = autoplay;
|
|
393
615
|
}
|
|
@@ -556,6 +778,18 @@
|
|
|
556
778
|
// Remove all observers
|
|
557
779
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
558
780
|
|
|
781
|
+
// Stop recording if in progress
|
|
782
|
+
if (_isRecording) {
|
|
783
|
+
[self stopRecording];
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Clean up display link if it exists
|
|
787
|
+
CADisplayLink *displayLink = objc_getAssociatedObject(self, "displayLinkKey");
|
|
788
|
+
if (displayLink) {
|
|
789
|
+
[displayLink invalidate];
|
|
790
|
+
objc_setAssociatedObject(self, "displayLinkKey", nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
791
|
+
}
|
|
792
|
+
|
|
559
793
|
// Stop playback and release player
|
|
560
794
|
[_player stop];
|
|
561
795
|
_player.delegate = nil;
|
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":"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","ignoreList":[]}
|
|
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":[]}
|
|
@@ -75,5 +75,18 @@ export declare const UnifiedPlayer: {
|
|
|
75
75
|
* @returns Promise resolving to the base64 encoded image string
|
|
76
76
|
*/
|
|
77
77
|
capture: (viewTag: number) => Promise<string>;
|
|
78
|
+
/**
|
|
79
|
+
* Start recording the video
|
|
80
|
+
* @param viewTag - The tag of the player view
|
|
81
|
+
* @param outputPath - Optional path where to save the recording (platform-specific)
|
|
82
|
+
* @returns Promise resolving to true if recording started successfully
|
|
83
|
+
*/
|
|
84
|
+
startRecording: (viewTag: number, outputPath?: string) => Promise<boolean>;
|
|
85
|
+
/**
|
|
86
|
+
* Stop recording the video
|
|
87
|
+
* @param viewTag - The tag of the player view
|
|
88
|
+
* @returns Promise resolving to the path of the saved recording
|
|
89
|
+
*/
|
|
90
|
+
stopRecording: (viewTag: number) => Promise<string>;
|
|
78
91
|
};
|
|
79
92
|
//# 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,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
|
@@ -263,4 +263,63 @@ export const UnifiedPlayer = {
|
|
|
263
263
|
return Promise.reject(error);
|
|
264
264
|
}
|
|
265
265
|
},
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Start recording the video
|
|
269
|
+
* @param viewTag - The tag of the player view
|
|
270
|
+
* @param outputPath - Optional path where to save the recording (platform-specific)
|
|
271
|
+
* @returns Promise resolving to true if recording started successfully
|
|
272
|
+
*/
|
|
273
|
+
startRecording: (viewTag: number, outputPath?: string): Promise<boolean> => {
|
|
274
|
+
try {
|
|
275
|
+
console.log('UnifiedPlayer.startRecording called with viewTag:', viewTag);
|
|
276
|
+
return UnifiedPlayerModule.startRecording(viewTag, outputPath)
|
|
277
|
+
.then((result: boolean) => {
|
|
278
|
+
console.log('Native startRecording method called successfully');
|
|
279
|
+
return result;
|
|
280
|
+
})
|
|
281
|
+
.catch((error: any) => {
|
|
282
|
+
console.log(
|
|
283
|
+
'Error calling startRecording:',
|
|
284
|
+
error instanceof Error ? error.message : String(error)
|
|
285
|
+
);
|
|
286
|
+
throw error;
|
|
287
|
+
});
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.log(
|
|
290
|
+
'Error calling startRecording:',
|
|
291
|
+
error instanceof Error ? error.message : String(error)
|
|
292
|
+
);
|
|
293
|
+
return Promise.reject(error);
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Stop recording the video
|
|
299
|
+
* @param viewTag - The tag of the player view
|
|
300
|
+
* @returns Promise resolving to the path of the saved recording
|
|
301
|
+
*/
|
|
302
|
+
stopRecording: (viewTag: number): Promise<string> => {
|
|
303
|
+
try {
|
|
304
|
+
console.log('UnifiedPlayer.stopRecording called with viewTag:', viewTag);
|
|
305
|
+
return UnifiedPlayerModule.stopRecording(viewTag)
|
|
306
|
+
.then((filePath: string) => {
|
|
307
|
+
console.log('Native stopRecording method called successfully');
|
|
308
|
+
return filePath;
|
|
309
|
+
})
|
|
310
|
+
.catch((error: any) => {
|
|
311
|
+
console.log(
|
|
312
|
+
'Error calling stopRecording:',
|
|
313
|
+
error instanceof Error ? error.message : String(error)
|
|
314
|
+
);
|
|
315
|
+
throw error;
|
|
316
|
+
});
|
|
317
|
+
} catch (error) {
|
|
318
|
+
console.log(
|
|
319
|
+
'Error calling stopRecording:',
|
|
320
|
+
error instanceof Error ? error.message : String(error)
|
|
321
|
+
);
|
|
322
|
+
return Promise.reject(error);
|
|
323
|
+
}
|
|
324
|
+
},
|
|
266
325
|
};
|