react-native-video-trim 6.2.1 → 6.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/videotrim/BaseVideoTrimModule.kt +110 -108
- package/android/src/main/java/com/videotrim/enums/ErrorCode.kt +2 -1
- package/android/src/main/java/com/videotrim/utils/MediaMetadataUtil.kt +6 -2
- package/android/src/main/java/com/videotrim/utils/StorageUtil.kt +5 -1
- package/android/src/main/java/com/videotrim/utils/VideoTrimmerUtil.kt +22 -13
- package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.kt +16 -7
- package/android/src/main/res/xml/file_paths.xml +1 -1
- package/ios/VideoTrim.swift +78 -51
- package/ios/VideoTrimmer.swift +2 -4
- package/ios/VideoTrimmerViewController.swift +37 -34
- package/package.json +1 -1
|
@@ -75,6 +75,8 @@ open class BaseVideoTrimModule internal constructor(
|
|
|
75
75
|
get() = editorConfig?.hasKey("changeStatusBarColorOnOpen") == true && editorConfig?.getBoolean("changeStatusBarColorOnOpen") == true
|
|
76
76
|
|
|
77
77
|
init {
|
|
78
|
+
reactApplicationContext.addLifecycleEventListener(this)
|
|
79
|
+
|
|
78
80
|
val mActivityEventListener = object : BaseActivityEventListener() {
|
|
79
81
|
override fun onActivityResult(
|
|
80
82
|
activity: Activity,
|
|
@@ -137,7 +139,10 @@ open class BaseVideoTrimModule internal constructor(
|
|
|
137
139
|
|
|
138
140
|
this.isVideoType = config.hasKey("type") && config.getString("type") == "video"
|
|
139
141
|
|
|
140
|
-
val activity = reactApplicationContext.currentActivity
|
|
142
|
+
val activity = reactApplicationContext.currentActivity ?: run {
|
|
143
|
+
onError("Activity is not available", ErrorCode.UNKNOWN)
|
|
144
|
+
return
|
|
145
|
+
}
|
|
141
146
|
if (!isInit) {
|
|
142
147
|
init()
|
|
143
148
|
isInit = true
|
|
@@ -150,7 +155,7 @@ open class BaseVideoTrimModule internal constructor(
|
|
|
150
155
|
trimmerView?.initByURI(filePath.toUri())
|
|
151
156
|
|
|
152
157
|
val builder = AlertDialog.Builder(
|
|
153
|
-
activity
|
|
158
|
+
activity, Theme_Black_NoTitleBar_Fullscreen
|
|
154
159
|
)
|
|
155
160
|
builder.setCancelable(false)
|
|
156
161
|
alertDialog = builder.create()
|
|
@@ -158,18 +163,17 @@ open class BaseVideoTrimModule internal constructor(
|
|
|
158
163
|
|
|
159
164
|
// Apply safe area handling after the dialog is shown
|
|
160
165
|
alertDialog?.setOnShowListener {
|
|
161
|
-
|
|
166
|
+
val dialog = alertDialog ?: return@setOnShowListener
|
|
167
|
+
val view = trimmerView ?: return@setOnShowListener
|
|
168
|
+
applySafeAreaToDialog(dialog, view)
|
|
162
169
|
|
|
163
170
|
sendEvent("onShow", null)
|
|
164
171
|
}
|
|
165
172
|
|
|
166
173
|
// this is to ensure to release resource if dialog is dismissed in unexpected way (Eg. open control/notification center by dragging from top of screen)
|
|
167
|
-
alertDialog
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
trimmerView!!.onDestroy()
|
|
171
|
-
trimmerView = null
|
|
172
|
-
}
|
|
174
|
+
alertDialog?.setOnDismissListener {
|
|
175
|
+
trimmerView?.onDestroy()
|
|
176
|
+
trimmerView = null
|
|
173
177
|
hideDialog(true)
|
|
174
178
|
sendEvent("onHide", null)
|
|
175
179
|
}
|
|
@@ -195,7 +199,7 @@ open class BaseVideoTrimModule internal constructor(
|
|
|
195
199
|
it.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
|
|
196
200
|
|
|
197
201
|
// finally change the color
|
|
198
|
-
it.statusBarColor = ContextCompat.getColor(reactApplicationContext
|
|
202
|
+
it.statusBarColor = ContextCompat.getColor(reactApplicationContext, R.color.black)
|
|
199
203
|
}
|
|
200
204
|
}
|
|
201
205
|
|
|
@@ -251,9 +255,7 @@ open class BaseVideoTrimModule internal constructor(
|
|
|
251
255
|
|
|
252
256
|
override fun onHostPause() {
|
|
253
257
|
Log.d(TAG, "onHostPause: ")
|
|
254
|
-
|
|
255
|
-
trimmerView!!.onMediaPause()
|
|
256
|
-
}
|
|
258
|
+
trimmerView?.onMediaPause()
|
|
257
259
|
}
|
|
258
260
|
|
|
259
261
|
override fun onHostDestroy() {
|
|
@@ -277,7 +279,7 @@ open class BaseVideoTrimModule internal constructor(
|
|
|
277
279
|
return
|
|
278
280
|
}
|
|
279
281
|
|
|
280
|
-
mProgressBar
|
|
282
|
+
mProgressBar?.setProgress(percentage, true)
|
|
281
283
|
}
|
|
282
284
|
|
|
283
285
|
|
|
@@ -312,10 +314,11 @@ open class BaseVideoTrimModule internal constructor(
|
|
|
312
314
|
hideDialog(editorConfig?.getBoolean("closeWhenFinish") ?: true)
|
|
313
315
|
}
|
|
314
316
|
} else if (editorConfig?.getBoolean("openDocumentsOnFinish") == true) {
|
|
315
|
-
|
|
317
|
+
val output = outputFile ?: return
|
|
318
|
+
saveFileToExternalStorage(File(output))
|
|
316
319
|
} else if (editorConfig?.getBoolean("openShareSheetOnFinish") == true) {
|
|
317
320
|
hideDialog(editorConfig?.getBoolean("closeWhenFinish") ?: true)
|
|
318
|
-
shareFile(reactApplicationContext, File(
|
|
321
|
+
outputFile?.let { shareFile(reactApplicationContext, File(it)) }
|
|
319
322
|
} else {
|
|
320
323
|
hideDialog(editorConfig?.getBoolean("closeWhenFinish") ?: true)
|
|
321
324
|
}
|
|
@@ -333,13 +336,14 @@ open class BaseVideoTrimModule internal constructor(
|
|
|
333
336
|
}
|
|
334
337
|
|
|
335
338
|
override fun onCancel() {
|
|
336
|
-
if (
|
|
339
|
+
if (editorConfig?.getBoolean("enableCancelDialog") != true) {
|
|
337
340
|
sendEvent("onCancel", null)
|
|
338
341
|
hideDialog(true)
|
|
339
342
|
return
|
|
340
343
|
}
|
|
341
344
|
|
|
342
|
-
val
|
|
345
|
+
val activity = reactApplicationContext.currentActivity ?: return
|
|
346
|
+
val builder = AlertDialog.Builder(activity)
|
|
343
347
|
builder.setMessage(editorConfig?.getString("cancelDialogMessage"))
|
|
344
348
|
builder.setTitle(editorConfig?.getString("cancelDialogTitle"))
|
|
345
349
|
builder.setCancelable(false)
|
|
@@ -358,12 +362,13 @@ open class BaseVideoTrimModule internal constructor(
|
|
|
358
362
|
}
|
|
359
363
|
|
|
360
364
|
override fun onSave() {
|
|
361
|
-
if (
|
|
365
|
+
if (editorConfig?.getBoolean("enableSaveDialog") != true) {
|
|
362
366
|
startTrim()
|
|
363
367
|
return
|
|
364
368
|
}
|
|
365
369
|
|
|
366
|
-
val
|
|
370
|
+
val activity = reactApplicationContext.currentActivity ?: return
|
|
371
|
+
val builder = AlertDialog.Builder(activity)
|
|
367
372
|
builder.setMessage(editorConfig?.getString("saveDialogMessage"))
|
|
368
373
|
builder.setTitle(editorConfig?.getString("saveDialogTitle"))
|
|
369
374
|
builder.setCancelable(false)
|
|
@@ -389,7 +394,7 @@ open class BaseVideoTrimModule internal constructor(
|
|
|
389
394
|
}
|
|
390
395
|
|
|
391
396
|
private fun startTrim() {
|
|
392
|
-
val activity = reactApplicationContext.currentActivity
|
|
397
|
+
val activity = reactApplicationContext.currentActivity ?: return
|
|
393
398
|
// Create the parent layout for the dialog
|
|
394
399
|
val layout = LinearLayout(activity)
|
|
395
400
|
layout.layoutParams = ViewGroup.LayoutParams(
|
|
@@ -413,12 +418,13 @@ open class BaseVideoTrimModule internal constructor(
|
|
|
413
418
|
layout.addView(textView)
|
|
414
419
|
|
|
415
420
|
// Create and add the ProgressBar
|
|
416
|
-
mProgressBar = ProgressBar(activity, null, progressBarStyleHorizontal)
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
421
|
+
mProgressBar = ProgressBar(activity, null, progressBarStyleHorizontal).also {
|
|
422
|
+
it.layoutParams = ViewGroup.LayoutParams(
|
|
423
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
424
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
425
|
+
)
|
|
426
|
+
it.progressTintList = ColorStateList.valueOf("#2196F3".toColorInt())
|
|
427
|
+
}
|
|
422
428
|
layout.addView(mProgressBar)
|
|
423
429
|
|
|
424
430
|
// Create button
|
|
@@ -433,10 +439,10 @@ open class BaseVideoTrimModule internal constructor(
|
|
|
433
439
|
?: "Cancel Trimming"
|
|
434
440
|
button.setTextColor(
|
|
435
441
|
ContextCompat.getColor(
|
|
436
|
-
activity
|
|
442
|
+
activity,
|
|
437
443
|
holo_red_light
|
|
438
444
|
)
|
|
439
|
-
)
|
|
445
|
+
)
|
|
440
446
|
|
|
441
447
|
// Apply ripple effect while keeping the button background transparent
|
|
442
448
|
val outValue = TypedValue()
|
|
@@ -451,11 +457,9 @@ open class BaseVideoTrimModule internal constructor(
|
|
|
451
457
|
builder.setTitle(editorConfig?.getString("cancelTrimmingDialogTitle"))
|
|
452
458
|
builder.setCancelable(false)
|
|
453
459
|
builder.setPositiveButton(editorConfig?.getString("cancelTrimmingDialogConfirmText")) { _: DialogInterface?, _: Int ->
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
if (mProgressDialog != null && mProgressDialog!!.isShowing) {
|
|
458
|
-
mProgressDialog!!.dismiss()
|
|
460
|
+
trimmerView?.onCancelTrimClicked()
|
|
461
|
+
if (mProgressDialog?.isShowing == true) {
|
|
462
|
+
mProgressDialog?.dismiss()
|
|
459
463
|
}
|
|
460
464
|
}
|
|
461
465
|
builder.setNegativeButton(
|
|
@@ -464,14 +468,12 @@ open class BaseVideoTrimModule internal constructor(
|
|
|
464
468
|
dialog.cancel()
|
|
465
469
|
}
|
|
466
470
|
cancelTrimmingConfirmDialog = builder.create()
|
|
467
|
-
cancelTrimmingConfirmDialog
|
|
471
|
+
cancelTrimmingConfirmDialog?.show()
|
|
468
472
|
} else {
|
|
469
|
-
|
|
470
|
-
trimmerView!!.onCancelTrimClicked()
|
|
471
|
-
}
|
|
473
|
+
trimmerView?.onCancelTrimClicked()
|
|
472
474
|
|
|
473
|
-
if (mProgressDialog
|
|
474
|
-
mProgressDialog
|
|
475
|
+
if (mProgressDialog?.isShowing == true) {
|
|
476
|
+
mProgressDialog?.dismiss()
|
|
475
477
|
}
|
|
476
478
|
}
|
|
477
479
|
}
|
|
@@ -480,7 +482,7 @@ open class BaseVideoTrimModule internal constructor(
|
|
|
480
482
|
|
|
481
483
|
// Create the AlertDialog
|
|
482
484
|
val builder = AlertDialog.Builder(
|
|
483
|
-
activity
|
|
485
|
+
activity
|
|
484
486
|
)
|
|
485
487
|
builder.setCancelable(false)
|
|
486
488
|
builder.setView(layout)
|
|
@@ -488,36 +490,29 @@ open class BaseVideoTrimModule internal constructor(
|
|
|
488
490
|
// Show the dialog
|
|
489
491
|
mProgressDialog = builder.create()
|
|
490
492
|
|
|
491
|
-
mProgressDialog
|
|
493
|
+
mProgressDialog?.setOnShowListener {
|
|
492
494
|
sendEvent("onStartTrimming", null)
|
|
493
|
-
|
|
494
|
-
trimmerView!!.onSaveClicked()
|
|
495
|
-
}
|
|
495
|
+
trimmerView?.onSaveClicked()
|
|
496
496
|
}
|
|
497
497
|
|
|
498
|
-
mProgressDialog
|
|
498
|
+
mProgressDialog?.show()
|
|
499
499
|
}
|
|
500
500
|
|
|
501
501
|
private fun hideDialog(shouldCloseEditor: Boolean) {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
if (cancelTrimmingConfirmDialog!!.isShowing) {
|
|
505
|
-
cancelTrimmingConfirmDialog!!.dismiss()
|
|
506
|
-
}
|
|
502
|
+
cancelTrimmingConfirmDialog?.let {
|
|
503
|
+
if (it.isShowing) it.dismiss()
|
|
507
504
|
cancelTrimmingConfirmDialog = null
|
|
508
505
|
}
|
|
509
506
|
|
|
510
|
-
|
|
511
|
-
if (
|
|
507
|
+
mProgressDialog?.let {
|
|
508
|
+
if (it.isShowing) it.dismiss()
|
|
512
509
|
mProgressBar = null
|
|
513
510
|
mProgressDialog = null
|
|
514
511
|
}
|
|
515
512
|
|
|
516
513
|
if (shouldCloseEditor) {
|
|
517
|
-
|
|
518
|
-
if (
|
|
519
|
-
alertDialog!!.dismiss()
|
|
520
|
-
}
|
|
514
|
+
alertDialog?.let {
|
|
515
|
+
if (it.isShowing) it.dismiss()
|
|
521
516
|
alertDialog = null
|
|
522
517
|
}
|
|
523
518
|
}
|
|
@@ -550,7 +545,6 @@ open class BaseVideoTrimModule internal constructor(
|
|
|
550
545
|
|
|
551
546
|
fun closeEditor() {
|
|
552
547
|
hideDialog(true)
|
|
553
|
-
sendEvent("onHide", null)
|
|
554
548
|
}
|
|
555
549
|
|
|
556
550
|
fun isValidFile(url: String, promise: Promise) {
|
|
@@ -593,6 +587,11 @@ open class BaseVideoTrimModule internal constructor(
|
|
|
593
587
|
|
|
594
588
|
val outputFile = StorageUtil.getOutputPath(reactApplicationContext, options?.getString("outputExt") ?: "mp4")
|
|
595
589
|
|
|
590
|
+
val resolvedOutputFile = outputFile ?: run {
|
|
591
|
+
promise.reject(Exception("Failed to create output file path"))
|
|
592
|
+
return
|
|
593
|
+
}
|
|
594
|
+
|
|
596
595
|
cmds += arrayOf(
|
|
597
596
|
"-i",
|
|
598
597
|
url,
|
|
@@ -600,7 +599,7 @@ open class BaseVideoTrimModule internal constructor(
|
|
|
600
599
|
"copy",
|
|
601
600
|
"-metadata",
|
|
602
601
|
"creation_time=$formattedDateTime",
|
|
603
|
-
|
|
602
|
+
resolvedOutputFile
|
|
604
603
|
)
|
|
605
604
|
|
|
606
605
|
Log.d(TAG, "Command: ${cmds.joinToString(",")}")
|
|
@@ -608,61 +607,60 @@ open class BaseVideoTrimModule internal constructor(
|
|
|
608
607
|
FFmpegKit.executeWithArgumentsAsync(cmds, { session ->
|
|
609
608
|
val state = session.state
|
|
610
609
|
val returnCode = session.returnCode
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
610
|
+
UiThreadUtil.runOnUiThread {
|
|
611
|
+
when {
|
|
612
|
+
ReturnCode.isSuccess(returnCode) -> {
|
|
613
|
+
val duration = endTime - startTime
|
|
614
|
+
val result = Arguments.createMap()
|
|
615
|
+
|
|
616
|
+
result.putString("outputPath", resolvedOutputFile)
|
|
617
|
+
result.putDouble("startTime", startTime)
|
|
618
|
+
result.putDouble("endTime", endTime)
|
|
619
|
+
result.putDouble("duration", duration)
|
|
620
|
+
result.putBoolean("success", true)
|
|
621
|
+
|
|
622
|
+
if (options?.getBoolean("saveToPhoto") == true && options.getString("type") == "video") {
|
|
623
|
+
Log.d(TAG, "Android trim: saveToPhoto is true, attempting to save to gallery")
|
|
624
|
+
try {
|
|
625
|
+
StorageUtil.saveVideoToGallery(reactApplicationContext, resolvedOutputFile)
|
|
626
|
+
Log.d(TAG, "Edited video saved to Photo Library successfully.")
|
|
627
|
+
if (options.getBoolean("removeAfterSavedToPhoto")) {
|
|
628
|
+
Log.d(TAG, "Removing file after successful save to photo")
|
|
629
|
+
StorageUtil.deleteFile(resolvedOutputFile)
|
|
630
|
+
}
|
|
632
631
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
632
|
+
promise.resolve(result)
|
|
633
|
+
} catch (e: IOException) {
|
|
634
|
+
e.printStackTrace()
|
|
636
635
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
636
|
+
if (options.getBoolean("removeAfterFailedToSavePhoto")) {
|
|
637
|
+
Log.d(TAG, "Removing file after failed save to photo")
|
|
638
|
+
StorageUtil.deleteFile(resolvedOutputFile)
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
promise.reject(
|
|
642
|
+
Exception("Failed to save edited video to Photo Library: " + e.localizedMessage)
|
|
643
|
+
)
|
|
640
644
|
}
|
|
645
|
+
} else {
|
|
646
|
+
Log.d(TAG, "Android trim: saveToPhoto is false or not video type, resolving with structured result")
|
|
641
647
|
|
|
642
|
-
promise.
|
|
643
|
-
Exception("Failed to save edited video to Photo Library: " + e.localizedMessage)
|
|
644
|
-
)
|
|
648
|
+
promise.resolve(result)
|
|
645
649
|
}
|
|
646
|
-
} else {
|
|
647
|
-
Log.d(TAG, "Android trim: saveToPhoto is false or not video type, resolving with structured result")
|
|
648
|
-
|
|
649
|
-
promise.resolve(result)
|
|
650
650
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
Exception(errorMessage)
|
|
665
|
-
)
|
|
651
|
+
ReturnCode.isCancel(returnCode) -> {
|
|
652
|
+
println("FFmpeg command was cancelled")
|
|
653
|
+
promise.reject(
|
|
654
|
+
Exception("FFmpeg command was cancelled with code $returnCode")
|
|
655
|
+
)
|
|
656
|
+
}
|
|
657
|
+
else -> {
|
|
658
|
+
val errorMessage = String.format("Command failed with state %s and rc %s.%s", state, returnCode, session.getFailStackTrace())
|
|
659
|
+
Log.d(TAG, errorMessage)
|
|
660
|
+
promise.reject(
|
|
661
|
+
Exception(errorMessage)
|
|
662
|
+
)
|
|
663
|
+
}
|
|
666
664
|
}
|
|
667
665
|
}
|
|
668
666
|
}, { log ->
|
|
@@ -701,6 +699,10 @@ open class BaseVideoTrimModule internal constructor(
|
|
|
701
699
|
reactApplicationContext.currentActivity?.startActivity(Intent.createChooser(shareIntent, "Share file"))
|
|
702
700
|
}
|
|
703
701
|
|
|
702
|
+
fun cleanup() {
|
|
703
|
+
reactApplicationContext.removeLifecycleEventListener(this)
|
|
704
|
+
}
|
|
705
|
+
|
|
704
706
|
companion object {
|
|
705
707
|
const val NAME = "VideoTrim"
|
|
706
708
|
const val TAG = "VideoTrimModule"
|
|
@@ -3,6 +3,7 @@ package com.videotrim.utils
|
|
|
3
3
|
import android.media.MediaMetadataRetriever
|
|
4
4
|
import android.net.Uri
|
|
5
5
|
import android.util.Log
|
|
6
|
+
import com.facebook.react.bridge.UiThreadUtil
|
|
6
7
|
import java.io.IOException
|
|
7
8
|
|
|
8
9
|
object MediaMetadataUtil {
|
|
@@ -47,7 +48,7 @@ object MediaMetadataUtil {
|
|
|
47
48
|
Thread {
|
|
48
49
|
val retriever = getMediaMetadataRetriever(urlString)
|
|
49
50
|
if (retriever == null) {
|
|
50
|
-
callback.onResult(false, "unknown", -1L)
|
|
51
|
+
UiThreadUtil.runOnUiThread { callback.onResult(false, "unknown", -1L) }
|
|
51
52
|
return@Thread
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -74,7 +75,10 @@ object MediaMetadataUtil {
|
|
|
74
75
|
} catch (e: IOException) {
|
|
75
76
|
Log.e(TAG, "Error releasing retriever", e)
|
|
76
77
|
}
|
|
77
|
-
|
|
78
|
+
val resultIsValid = isValid
|
|
79
|
+
val resultFileType = fileType
|
|
80
|
+
val resultDuration = if (isValid) duration else -1L
|
|
81
|
+
UiThreadUtil.runOnUiThread { callback.onResult(resultIsValid, resultFileType, resultDuration) }
|
|
78
82
|
}.start()
|
|
79
83
|
}
|
|
80
84
|
|
|
@@ -9,6 +9,7 @@ import android.os.Environment
|
|
|
9
9
|
import android.provider.MediaStore
|
|
10
10
|
import android.text.TextUtils
|
|
11
11
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
12
|
+
import iknow.android.utils.BaseUtils
|
|
12
13
|
import java.io.File
|
|
13
14
|
import java.io.FileInputStream
|
|
14
15
|
import java.io.FileOutputStream
|
|
@@ -38,7 +39,10 @@ object StorageUtil {
|
|
|
38
39
|
|
|
39
40
|
fun deleteFile(path: String?): Boolean {
|
|
40
41
|
if (TextUtils.isEmpty(path)) return true
|
|
41
|
-
|
|
42
|
+
val file = File(path!!).canonicalFile
|
|
43
|
+
val allowedDir = BaseUtils.getContext().filesDir.canonicalFile
|
|
44
|
+
if (!file.path.startsWith(allowedDir.path)) return false
|
|
45
|
+
return deleteFile(file)
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
fun deleteFile(file: File?): Boolean {
|
|
@@ -8,6 +8,7 @@ import com.arthenica.ffmpegkit.FFmpegKit
|
|
|
8
8
|
import com.arthenica.ffmpegkit.FFmpegSession
|
|
9
9
|
import com.arthenica.ffmpegkit.ReturnCode
|
|
10
10
|
import com.facebook.react.bridge.Arguments
|
|
11
|
+
import com.facebook.react.bridge.UiThreadUtil
|
|
11
12
|
import com.videotrim.enums.ErrorCode
|
|
12
13
|
import com.videotrim.interfaces.VideoTrimListener
|
|
13
14
|
import iknow.android.utils.DeviceUtil
|
|
@@ -82,16 +83,18 @@ object VideoTrimmerUtil {
|
|
|
82
83
|
return FFmpegKit.executeWithArgumentsAsync(command, { session ->
|
|
83
84
|
val state = session.state
|
|
84
85
|
val returnCode = session.returnCode
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
86
|
+
UiThreadUtil.runOnUiThread {
|
|
87
|
+
when {
|
|
88
|
+
ReturnCode.isSuccess(returnCode) -> {
|
|
89
|
+
callback.onFinishTrim(outputFile, startMs, endMs, videoDuration)
|
|
90
|
+
}
|
|
91
|
+
ReturnCode.isCancel(returnCode) -> {
|
|
92
|
+
callback.onCancelTrim()
|
|
93
|
+
}
|
|
94
|
+
else -> {
|
|
95
|
+
val errorMessage = "Command failed with state $state and rc $returnCode.${session.failStackTrace}"
|
|
96
|
+
callback.onError(errorMessage, ErrorCode.TRIMMING_FAILED)
|
|
97
|
+
}
|
|
95
98
|
}
|
|
96
99
|
}
|
|
97
100
|
}, { log ->
|
|
@@ -102,12 +105,16 @@ object VideoTrimmerUtil {
|
|
|
102
105
|
map.putString("message", log.message)
|
|
103
106
|
map.putDouble("sessionId", log.sessionId.toDouble())
|
|
104
107
|
map.putString("logStr", log.toString())
|
|
105
|
-
|
|
108
|
+
UiThreadUtil.runOnUiThread {
|
|
109
|
+
callback.onLog(map)
|
|
110
|
+
}
|
|
106
111
|
}, { statistics ->
|
|
107
112
|
val timeInMilliseconds = statistics.time.toInt()
|
|
108
113
|
if (timeInMilliseconds > 0) {
|
|
109
114
|
val completePercentage = (timeInMilliseconds * 100) / videoDuration
|
|
110
|
-
|
|
115
|
+
UiThreadUtil.runOnUiThread {
|
|
116
|
+
callback.onTrimmingProgress(completePercentage.coerceIn(0, 100))
|
|
117
|
+
}
|
|
111
118
|
}
|
|
112
119
|
|
|
113
120
|
val map = Arguments.createMap()
|
|
@@ -120,7 +127,9 @@ object VideoTrimmerUtil {
|
|
|
120
127
|
map.putDouble("bitrate", statistics.bitrate.toDouble())
|
|
121
128
|
map.putDouble("speed", statistics.speed.toDouble())
|
|
122
129
|
map.putString("statisticsStr", statistics.toString())
|
|
123
|
-
|
|
130
|
+
UiThreadUtil.runOnUiThread {
|
|
131
|
+
callback.onStatistics(map)
|
|
132
|
+
}
|
|
124
133
|
})
|
|
125
134
|
}
|
|
126
135
|
|
|
@@ -124,6 +124,8 @@ class VideoTrimmerView(
|
|
|
124
124
|
private var isGeneratingThumbnails = false
|
|
125
125
|
|
|
126
126
|
private var mediaMetadataRetriever: MediaMetadataRetriever? = null
|
|
127
|
+
private val retrieverLock = Object()
|
|
128
|
+
@Volatile private var retrieverReleased = false
|
|
127
129
|
private lateinit var loadingIndicator: ProgressBar
|
|
128
130
|
private lateinit var saveBtn: TextView
|
|
129
131
|
private lateinit var cancelBtn: TextView
|
|
@@ -158,7 +160,7 @@ class VideoTrimmerView(
|
|
|
158
160
|
private fun init(context: ReactApplicationContext, config: ReadableMap?) {
|
|
159
161
|
mContext = context
|
|
160
162
|
|
|
161
|
-
context.currentActivity
|
|
163
|
+
context.currentActivity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
|
162
164
|
LayoutInflater.from(context).inflate(R.layout.video_trimmer_view, this, true)
|
|
163
165
|
vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
|
|
164
166
|
|
|
@@ -256,7 +258,8 @@ class VideoTrimmerView(
|
|
|
256
258
|
mediaFailed()
|
|
257
259
|
mOnTrimVideoListener.onError("Error loading media file. Please try again.", ErrorCode.FAIL_TO_LOAD_MEDIA)
|
|
258
260
|
if (alertOnFailToLoad) {
|
|
259
|
-
val
|
|
261
|
+
val activity = mContext.currentActivity ?: return true
|
|
262
|
+
val builder = AlertDialog.Builder(activity)
|
|
260
263
|
builder.setMessage(alertOnFailMessage)
|
|
261
264
|
builder.setTitle(alertOnFailTitle)
|
|
262
265
|
builder.setCancelable(false)
|
|
@@ -462,6 +465,7 @@ class VideoTrimmerView(
|
|
|
462
465
|
override fun onDestroy() {
|
|
463
466
|
isGeneratingThumbnails = false
|
|
464
467
|
BackgroundExecutor.cancelAll("", true)
|
|
468
|
+
BackgroundExecutor.cancelAll("progressive_thumbs", true)
|
|
465
469
|
UiThreadExecutor.cancelAll("")
|
|
466
470
|
mContext.currentActivity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
|
467
471
|
mTimingRunnable?.let { mTimingHandler.removeCallbacks(it) }
|
|
@@ -469,10 +473,13 @@ class VideoTrimmerView(
|
|
|
469
473
|
|
|
470
474
|
cachedFullViewThumbnails.clear()
|
|
471
475
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
+
synchronized(retrieverLock) {
|
|
477
|
+
retrieverReleased = true
|
|
478
|
+
try {
|
|
479
|
+
mediaMetadataRetriever?.release()
|
|
480
|
+
} catch (e: Exception) {
|
|
481
|
+
e.printStackTrace()
|
|
482
|
+
}
|
|
476
483
|
}
|
|
477
484
|
|
|
478
485
|
try {
|
|
@@ -1069,7 +1076,9 @@ class VideoTrimmerView(
|
|
|
1069
1076
|
val clampedTimeUs = maxOf(0L, minOf(timeUs, mDuration * 1000L))
|
|
1070
1077
|
|
|
1071
1078
|
try {
|
|
1072
|
-
val bitmap =
|
|
1079
|
+
val bitmap = synchronized(retrieverLock) {
|
|
1080
|
+
if (retrieverReleased) null else mediaMetadataRetriever?.getFrameAtTime(clampedTimeUs, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
|
|
1081
|
+
}
|
|
1073
1082
|
if (bitmap != null && isGeneratingThumbnails && isZoomedIn) {
|
|
1074
1083
|
UiThreadExecutor.runTask("", {
|
|
1075
1084
|
if (isZoomedIn && index < mThumbnailContainer.childCount) {
|
package/ios/VideoTrim.swift
CHANGED
|
@@ -8,6 +8,7 @@ let FILE_PREFIX = "trimmedVideo"
|
|
|
8
8
|
public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDelegate {
|
|
9
9
|
// MARK: instance private props
|
|
10
10
|
private var isShowing = false
|
|
11
|
+
private var isTrimming = false
|
|
11
12
|
private var vc: VideoTrimmerViewController?
|
|
12
13
|
private var outputFile: URL?
|
|
13
14
|
private var editorConfig: NSDictionary?
|
|
@@ -260,6 +261,9 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
260
261
|
}
|
|
261
262
|
|
|
262
263
|
private func trim(viewController: VideoTrimmerViewController, inputFile: URL, videoDuration: Double, startTime: Double, endTime: Double, isVideoType: Bool) {
|
|
264
|
+
guard !isTrimming else { return }
|
|
265
|
+
isTrimming = true
|
|
266
|
+
|
|
263
267
|
vc?.pausePlayer()
|
|
264
268
|
|
|
265
269
|
let timestamp = Int(Date().timeIntervalSince1970)
|
|
@@ -289,7 +293,7 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
289
293
|
dialogMessage.overrideUserInterfaceStyle = .dark
|
|
290
294
|
|
|
291
295
|
// Create OK button with action handler
|
|
292
|
-
let ok = UIAlertAction(title: self.
|
|
296
|
+
let ok = UIAlertAction(title: self.cancelTrimmingDialogConfirmText, style: .destructive, handler: { (action) -> Void in
|
|
293
297
|
|
|
294
298
|
if let ffmpegSession = ffmpegSession {
|
|
295
299
|
ffmpegSession.cancel()
|
|
@@ -297,11 +301,13 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
297
301
|
self.emitEventToJS("onCancelTrimming", eventData: nil)
|
|
298
302
|
}
|
|
299
303
|
|
|
300
|
-
progressAlert.dismiss(animated: true)
|
|
304
|
+
progressAlert.dismiss(animated: true) {
|
|
305
|
+
self.isTrimming = false
|
|
306
|
+
}
|
|
301
307
|
})
|
|
302
308
|
|
|
303
309
|
// Create Cancel button with action handlder
|
|
304
|
-
let cancel = UIAlertAction(title: self.
|
|
310
|
+
let cancel = UIAlertAction(title: self.cancelTrimmingDialogCancelText, style: .cancel)
|
|
305
311
|
|
|
306
312
|
//Add OK and Cancel button to an Alert object
|
|
307
313
|
dialogMessage.addAction(ok)
|
|
@@ -318,7 +324,9 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
318
324
|
self.emitEventToJS("onCancelTrimming", eventData: nil)
|
|
319
325
|
}
|
|
320
326
|
|
|
321
|
-
progressAlert.dismiss(animated: true)
|
|
327
|
+
progressAlert.dismiss(animated: true) {
|
|
328
|
+
self.isTrimming = false
|
|
329
|
+
}
|
|
322
330
|
}
|
|
323
331
|
|
|
324
332
|
}
|
|
@@ -339,6 +347,11 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
339
347
|
cmds.append(contentsOf: ["-display_rotation", "\(rotationAngle)"])
|
|
340
348
|
}
|
|
341
349
|
|
|
350
|
+
guard let outputFile = outputFile else {
|
|
351
|
+
self.onError(message: "Output file path is nil", code: .trimmingFailed)
|
|
352
|
+
return
|
|
353
|
+
}
|
|
354
|
+
|
|
342
355
|
cmds.append(contentsOf: [
|
|
343
356
|
"-i",
|
|
344
357
|
inputFile.path,
|
|
@@ -346,7 +359,7 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
346
359
|
"copy",
|
|
347
360
|
"-metadata",
|
|
348
361
|
"creation_time=\(dateTime)",
|
|
349
|
-
outputFile
|
|
362
|
+
outputFile.path
|
|
350
363
|
])
|
|
351
364
|
|
|
352
365
|
print("Command: ", cmds.joined(separator: " "))
|
|
@@ -358,16 +371,13 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
358
371
|
|
|
359
372
|
ffmpegSession = FFmpegKit.execute(withArgumentsAsync: cmds, withCompleteCallback: { session in
|
|
360
373
|
|
|
361
|
-
// always hide progressAlert
|
|
362
|
-
DispatchQueue.main.async {
|
|
363
|
-
progressAlert.dismiss(animated: true)
|
|
364
|
-
}
|
|
365
|
-
|
|
366
374
|
let state = session?.getState()
|
|
367
375
|
let returnCode = session?.getReturnCode()
|
|
368
376
|
|
|
377
|
+
var shouldCloseEditor = false
|
|
378
|
+
|
|
369
379
|
if ReturnCode.isSuccess(returnCode) {
|
|
370
|
-
let eventPayload: [String: Any] = ["outputPath":
|
|
380
|
+
let eventPayload: [String: Any] = ["outputPath": outputFile.absoluteString, "startTime": (startTime * 1000).rounded(), "endTime": (endTime * 1000).rounded(), "duration": (videoDuration * 1000).rounded()]
|
|
371
381
|
self.emitEventToJS("onFinishTrimming", eventData: eventPayload)
|
|
372
382
|
|
|
373
383
|
if (self.saveToPhoto && isVideoType) {
|
|
@@ -378,38 +388,42 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
378
388
|
}
|
|
379
389
|
|
|
380
390
|
PHPhotoLibrary.shared().performChanges({
|
|
381
|
-
let request = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL:
|
|
391
|
+
let request = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: outputFile)
|
|
382
392
|
request?.creationDate = Date()
|
|
383
393
|
}) { success, error in
|
|
384
394
|
if success {
|
|
385
395
|
print("Edited video saved to Photo Library successfully.")
|
|
386
396
|
|
|
387
397
|
if self.removeAfterSavedToPhoto {
|
|
388
|
-
let _ = VideoTrim.deleteFile(url:
|
|
398
|
+
let _ = VideoTrim.deleteFile(url: outputFile)
|
|
389
399
|
}
|
|
390
400
|
} else {
|
|
391
401
|
self.onError(message: "Failed to save edited video to Photo Library: \(error?.localizedDescription ?? "Unknown error")", code: .failToSaveToPhoto)
|
|
392
402
|
if self.removeAfterFailedToSavePhoto {
|
|
393
|
-
let _ = VideoTrim.deleteFile(url:
|
|
403
|
+
let _ = VideoTrim.deleteFile(url: outputFile)
|
|
394
404
|
}
|
|
395
405
|
}
|
|
396
406
|
}
|
|
397
407
|
}
|
|
398
408
|
} else if self.openDocumentsOnFinish {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
409
|
+
DispatchQueue.main.async {
|
|
410
|
+
progressAlert.dismiss(animated: true) {
|
|
411
|
+
self.isTrimming = false
|
|
412
|
+
self.saveFileToFilesApp(fileURL: outputFile)
|
|
413
|
+
}
|
|
414
|
+
}
|
|
402
415
|
return
|
|
403
416
|
} else if self.openShareSheetOnFinish {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
417
|
+
DispatchQueue.main.async {
|
|
418
|
+
progressAlert.dismiss(animated: true) {
|
|
419
|
+
self.isTrimming = false
|
|
420
|
+
self.shareFile(fileURL: outputFile)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
407
423
|
return
|
|
408
424
|
}
|
|
409
425
|
|
|
410
|
-
|
|
411
|
-
self.closeEditor(delay: 500)
|
|
412
|
-
}
|
|
426
|
+
shouldCloseEditor = self.closeWhenFinish
|
|
413
427
|
|
|
414
428
|
} else if ReturnCode.isCancel(returnCode) {
|
|
415
429
|
// CANCEL
|
|
@@ -417,8 +431,15 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
417
431
|
} else {
|
|
418
432
|
// FAILURE
|
|
419
433
|
self.onError(message: "Command failed with state \(String(describing: FFmpegKitConfig.sessionState(toString: state ?? .failed))) and rc \(String(describing: returnCode)).\(String(describing: session?.getFailStackTrace()))", code: .trimmingFailed)
|
|
420
|
-
|
|
421
|
-
|
|
434
|
+
shouldCloseEditor = self.closeWhenFinish
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
DispatchQueue.main.async {
|
|
438
|
+
progressAlert.dismiss(animated: true) {
|
|
439
|
+
self.isTrimming = false
|
|
440
|
+
if shouldCloseEditor {
|
|
441
|
+
self.closeEditor()
|
|
442
|
+
}
|
|
422
443
|
}
|
|
423
444
|
}
|
|
424
445
|
|
|
@@ -463,9 +484,7 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
463
484
|
// New Arch
|
|
464
485
|
@objc(trim:url:config:)
|
|
465
486
|
public func _trim(inputFile: String, config: NSDictionary, completion: @escaping ([String: Any]) -> Void) {
|
|
466
|
-
let destPath = URL(string: inputFile)
|
|
467
|
-
|
|
468
|
-
guard let destPath = destPath else {
|
|
487
|
+
guard let destPath = URL(string: inputFile) ?? URL(fileURLWithPath: inputFile) as URL? else {
|
|
469
488
|
let result = [
|
|
470
489
|
"success": false,
|
|
471
490
|
"message": "Invalid input file path",
|
|
@@ -661,21 +680,21 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
661
680
|
print(message)
|
|
662
681
|
self.onError(message: message, code: .failToShare)
|
|
663
682
|
|
|
664
|
-
if self.removeAfterFailedToShare {
|
|
665
|
-
let _ = VideoTrim.deleteFile(url:
|
|
683
|
+
if self.removeAfterFailedToShare, let outputFile = self.outputFile {
|
|
684
|
+
let _ = VideoTrim.deleteFile(url: outputFile)
|
|
666
685
|
}
|
|
667
686
|
return
|
|
668
687
|
}
|
|
669
688
|
|
|
670
689
|
if completed {
|
|
671
690
|
print("User completed the sharing activity")
|
|
672
|
-
if self.removeAfterShared {
|
|
673
|
-
let _ = VideoTrim.deleteFile(url:
|
|
691
|
+
if self.removeAfterShared, let outputFile = self.outputFile {
|
|
692
|
+
let _ = VideoTrim.deleteFile(url: outputFile)
|
|
674
693
|
}
|
|
675
694
|
} else {
|
|
676
695
|
print("User cancelled or failed to complete the sharing activity")
|
|
677
|
-
if self.removeAfterFailedToShare {
|
|
678
|
-
let _ = VideoTrim.deleteFile(url:
|
|
696
|
+
if self.removeAfterFailedToShare, let outputFile = self.outputFile {
|
|
697
|
+
let _ = VideoTrim.deleteFile(url: outputFile)
|
|
679
698
|
}
|
|
680
699
|
}
|
|
681
700
|
|
|
@@ -711,10 +730,8 @@ extension VideoTrim {
|
|
|
711
730
|
editorConfig = config
|
|
712
731
|
print("Show editor called with URI: \(uri)")
|
|
713
732
|
|
|
714
|
-
let destPath = URL(string: uri)
|
|
715
|
-
print("Destination Path: \(destPath
|
|
716
|
-
|
|
717
|
-
guard let destPath = destPath else { return }
|
|
733
|
+
guard let destPath = URL(string: uri) ?? URL(fileURLWithPath: uri) as URL? else { return }
|
|
734
|
+
print("Destination Path: \(destPath.absoluteString), path: \(destPath.path)")
|
|
718
735
|
|
|
719
736
|
DispatchQueue.main.async {
|
|
720
737
|
self.vc = VideoTrimmerViewController()
|
|
@@ -757,8 +774,9 @@ extension VideoTrim {
|
|
|
757
774
|
let isVideoType = (config["type"] as? String ?? "video") == "video"
|
|
758
775
|
|
|
759
776
|
vc.saveBtnClicked = {(selectedRange: CMTimeRange) in
|
|
777
|
+
guard let asset = vc.asset else { return }
|
|
760
778
|
if !self.enableSaveDialog {
|
|
761
|
-
self.trim(viewController: vc,inputFile: destPath, videoDuration:
|
|
779
|
+
self.trim(viewController: vc,inputFile: destPath, videoDuration: asset.duration.seconds, startTime: selectedRange.start.seconds, endTime: selectedRange.end.seconds, isVideoType: isVideoType)
|
|
762
780
|
return
|
|
763
781
|
}
|
|
764
782
|
|
|
@@ -768,7 +786,7 @@ extension VideoTrim {
|
|
|
768
786
|
|
|
769
787
|
// Create OK button with action handler
|
|
770
788
|
let ok = UIAlertAction(title: self.saveDialogConfirmText, style: .default, handler: { (action) -> Void in
|
|
771
|
-
self.trim(viewController: vc,inputFile: destPath, videoDuration:
|
|
789
|
+
self.trim(viewController: vc,inputFile: destPath, videoDuration: asset.duration.seconds, startTime: selectedRange.start.seconds, endTime: selectedRange.end.seconds, isVideoType: isVideoType)
|
|
772
790
|
})
|
|
773
791
|
|
|
774
792
|
// Create Cancel button with action handlder
|
|
@@ -809,15 +827,19 @@ extension VideoTrim {
|
|
|
809
827
|
@objc(closeEditor:)
|
|
810
828
|
public func closeEditor(delay: Int = 0) {
|
|
811
829
|
guard let vc = vc else { return }
|
|
812
|
-
|
|
813
|
-
// even the file is successfully saved
|
|
814
|
-
// that's why we need a small delay here to ensure vc will be dismissed
|
|
815
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(delay)) {
|
|
830
|
+
let dismissBlock = {
|
|
816
831
|
vc.dismiss(animated: true, completion: {
|
|
817
832
|
self.emitEventToJS("onHide", eventData: nil)
|
|
818
833
|
self.isShowing = false
|
|
834
|
+
self.vc = nil
|
|
819
835
|
})
|
|
820
836
|
}
|
|
837
|
+
|
|
838
|
+
if delay > 0 {
|
|
839
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(delay), execute: dismissBlock)
|
|
840
|
+
} else {
|
|
841
|
+
DispatchQueue.main.async(execute: dismissBlock)
|
|
842
|
+
}
|
|
821
843
|
}
|
|
822
844
|
|
|
823
845
|
// Old Arch
|
|
@@ -866,7 +888,8 @@ extension VideoTrim {
|
|
|
866
888
|
// New Arch
|
|
867
889
|
@objc(deleteFile:)
|
|
868
890
|
public static func deleteFile(uri: String) -> Bool {
|
|
869
|
-
let
|
|
891
|
+
guard let url = URL(string: uri) else { return false }
|
|
892
|
+
let state = deleteFile(url: url)
|
|
870
893
|
return state == 0
|
|
871
894
|
}
|
|
872
895
|
|
|
@@ -899,7 +922,10 @@ extension VideoTrim {
|
|
|
899
922
|
// New Arch
|
|
900
923
|
@objc(isValidFile:url:)
|
|
901
924
|
public static func isValidFile(url: String, completion: @escaping ([String: Any]) -> Void) -> Void {
|
|
902
|
-
let fileURL = URL(string: url)
|
|
925
|
+
guard let fileURL = URL(string: url) ?? URL(fileURLWithPath: url) as URL? else {
|
|
926
|
+
completion(["isValid": false, "fileType": "unknown", "duration": -1])
|
|
927
|
+
return
|
|
928
|
+
}
|
|
903
929
|
checkFileValidity(url: fileURL) { isValid, fileType, duration in
|
|
904
930
|
if isValid {
|
|
905
931
|
print("Valid \(fileType) file with duration: \(duration) milliseconds")
|
|
@@ -999,8 +1025,9 @@ extension VideoTrim {
|
|
|
999
1025
|
|
|
1000
1026
|
vc?.asset = loader.asset
|
|
1001
1027
|
|
|
1028
|
+
let duration = loader.asset?.duration.seconds ?? 0
|
|
1002
1029
|
let eventPayload: [String: Any] = [
|
|
1003
|
-
"duration":
|
|
1030
|
+
"duration": duration * 1000,
|
|
1004
1031
|
]
|
|
1005
1032
|
self.emitEventToJS("onLoad", eventData: eventPayload)
|
|
1006
1033
|
}
|
|
@@ -1010,15 +1037,15 @@ extension VideoTrim {
|
|
|
1010
1037
|
// MARK: DocumentPicker delegate
|
|
1011
1038
|
extension VideoTrim {
|
|
1012
1039
|
public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
|
1013
|
-
if removeAfterSavedToDocuments {
|
|
1014
|
-
let _ = VideoTrim.deleteFile(url: outputFile
|
|
1040
|
+
if removeAfterSavedToDocuments, let outputFile = self.outputFile {
|
|
1041
|
+
let _ = VideoTrim.deleteFile(url: outputFile)
|
|
1015
1042
|
}
|
|
1016
1043
|
closeEditor()
|
|
1017
1044
|
}
|
|
1018
1045
|
|
|
1019
1046
|
public func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
|
1020
|
-
if removeAfterFailedToSaveDocuments {
|
|
1021
|
-
let _ = VideoTrim.deleteFile(url: outputFile
|
|
1047
|
+
if removeAfterFailedToSaveDocuments, let outputFile = self.outputFile {
|
|
1048
|
+
let _ = VideoTrim.deleteFile(url: outputFile)
|
|
1022
1049
|
}
|
|
1023
1050
|
closeEditor()
|
|
1024
1051
|
}
|
package/ios/VideoTrimmer.swift
CHANGED
|
@@ -446,17 +446,15 @@ import AVFoundation
|
|
|
446
446
|
self.thumbnails.removeAll(where: {uuidsToRemove.contains($0.uuid)})
|
|
447
447
|
})
|
|
448
448
|
|
|
449
|
-
var seenIndex = 0
|
|
450
449
|
generator.requestedTimeToleranceBefore = .zero
|
|
451
450
|
generator.requestedTimeToleranceAfter = .zero
|
|
452
451
|
generator.generateCGImagesAsynchronously(forTimes: times) { requestedTime, cgImage, actualTime, result, error in
|
|
453
452
|
DispatchQueue.main.async {
|
|
454
|
-
seenIndex += 1
|
|
455
|
-
|
|
456
453
|
guard let cgImage = cgImage else {return}
|
|
454
|
+
guard let index = newThumbnails.firstIndex(where: { CMTimeCompare($0.time, requestedTime) == 0 }) else {return}
|
|
457
455
|
let image = UIImage(cgImage: cgImage)
|
|
458
456
|
|
|
459
|
-
let imageView = newThumbnails[
|
|
457
|
+
let imageView = newThumbnails[index].imageView
|
|
460
458
|
UIView.transition(with: imageView, duration: 0.25, options: [.transitionCrossDissolve], animations: {
|
|
461
459
|
imageView.image = image
|
|
462
460
|
})
|
|
@@ -69,6 +69,7 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
69
69
|
private let audioBannerView = UIImage(systemName: "airpodsmax")
|
|
70
70
|
private var player: AVPlayer! { playerController.player }
|
|
71
71
|
private var timeObserverToken: Any?
|
|
72
|
+
private var statusObservation: NSKeyValueObservation?
|
|
72
73
|
private var autoplay = false
|
|
73
74
|
private var jumpToPositionOnLoad: Double = 0;
|
|
74
75
|
private var headerText: String?
|
|
@@ -197,8 +198,8 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
197
198
|
guard let _ = asset else { return }
|
|
198
199
|
player.pause()
|
|
199
200
|
|
|
200
|
-
|
|
201
|
-
|
|
201
|
+
statusObservation?.invalidate()
|
|
202
|
+
statusObservation = nil
|
|
202
203
|
|
|
203
204
|
if let token = timeObserverToken {
|
|
204
205
|
player.removeTimeObserver(token)
|
|
@@ -391,15 +392,19 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
391
392
|
}
|
|
392
393
|
|
|
393
394
|
private func setupPlayerController() {
|
|
395
|
+
guard let asset = asset else { return }
|
|
394
396
|
playerController.showsPlaybackControls = false
|
|
395
397
|
if #available(iOS 16.0, *) {
|
|
396
398
|
playerController.allowsVideoFrameAnalysis = false
|
|
397
399
|
}
|
|
398
400
|
playerController.player = AVPlayer()
|
|
399
|
-
player.replaceCurrentItem(with: AVPlayerItem(asset: asset
|
|
401
|
+
player.replaceCurrentItem(with: AVPlayerItem(asset: asset))
|
|
400
402
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
+
statusObservation = player.observe(\.status, options: [.new, .initial]) { [weak self] player, _ in
|
|
404
|
+
DispatchQueue.main.async {
|
|
405
|
+
self?.onPlayerReady()
|
|
406
|
+
}
|
|
407
|
+
}
|
|
403
408
|
|
|
404
409
|
try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [])
|
|
405
410
|
addChild(playerController)
|
|
@@ -512,35 +517,33 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
512
517
|
}
|
|
513
518
|
}
|
|
514
519
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
}
|
|
543
|
-
}
|
|
520
|
+
private func onPlayerReady() {
|
|
521
|
+
guard player.status == .readyToPlay else { return }
|
|
522
|
+
|
|
523
|
+
loadingIndicator.stopAnimating()
|
|
524
|
+
btnStackView.removeArrangedSubview(loadingIndicator)
|
|
525
|
+
loadingIndicator.removeFromSuperview()
|
|
526
|
+
btnStackView.insertArrangedSubview(playBtn, at: 1)
|
|
527
|
+
|
|
528
|
+
UIView.animate(withDuration: 0.25, animations: {
|
|
529
|
+
self.playBtn.alpha = 1
|
|
530
|
+
self.playBtn.isEnabled = true
|
|
531
|
+
self.saveBtn.alpha = 1
|
|
532
|
+
self.saveBtn.isEnabled = true
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
if jumpToPositionOnLoad > 0 {
|
|
536
|
+
let duration = (asset?.duration.seconds ?? 0) * 1000
|
|
537
|
+
let time = jumpToPositionOnLoad > duration ? duration : jumpToPositionOnLoad
|
|
538
|
+
let cmtime = CMTime(value: CMTimeValue(time), timescale: 1000)
|
|
539
|
+
|
|
540
|
+
self.seek(to: cmtime)
|
|
541
|
+
self.trimmer.progress = cmtime
|
|
542
|
+
self.currentTimeLabel.text = self.trimmer.progress.displayString
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if autoplay {
|
|
546
|
+
togglePlay(sender: playBtn)
|
|
544
547
|
}
|
|
545
548
|
}
|
|
546
549
|
}
|