react-native-video-trim 6.2.2 → 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 +39 -31
- 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
|
@@ -293,7 +293,7 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
293
293
|
dialogMessage.overrideUserInterfaceStyle = .dark
|
|
294
294
|
|
|
295
295
|
// Create OK button with action handler
|
|
296
|
-
let ok = UIAlertAction(title: self.
|
|
296
|
+
let ok = UIAlertAction(title: self.cancelTrimmingDialogConfirmText, style: .destructive, handler: { (action) -> Void in
|
|
297
297
|
|
|
298
298
|
if let ffmpegSession = ffmpegSession {
|
|
299
299
|
ffmpegSession.cancel()
|
|
@@ -307,7 +307,7 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
307
307
|
})
|
|
308
308
|
|
|
309
309
|
// Create Cancel button with action handlder
|
|
310
|
-
let cancel = UIAlertAction(title: self.
|
|
310
|
+
let cancel = UIAlertAction(title: self.cancelTrimmingDialogCancelText, style: .cancel)
|
|
311
311
|
|
|
312
312
|
//Add OK and Cancel button to an Alert object
|
|
313
313
|
dialogMessage.addAction(ok)
|
|
@@ -347,6 +347,11 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
347
347
|
cmds.append(contentsOf: ["-display_rotation", "\(rotationAngle)"])
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
+
guard let outputFile = outputFile else {
|
|
351
|
+
self.onError(message: "Output file path is nil", code: .trimmingFailed)
|
|
352
|
+
return
|
|
353
|
+
}
|
|
354
|
+
|
|
350
355
|
cmds.append(contentsOf: [
|
|
351
356
|
"-i",
|
|
352
357
|
inputFile.path,
|
|
@@ -354,7 +359,7 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
354
359
|
"copy",
|
|
355
360
|
"-metadata",
|
|
356
361
|
"creation_time=\(dateTime)",
|
|
357
|
-
outputFile
|
|
362
|
+
outputFile.path
|
|
358
363
|
])
|
|
359
364
|
|
|
360
365
|
print("Command: ", cmds.joined(separator: " "))
|
|
@@ -372,7 +377,7 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
372
377
|
var shouldCloseEditor = false
|
|
373
378
|
|
|
374
379
|
if ReturnCode.isSuccess(returnCode) {
|
|
375
|
-
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()]
|
|
376
381
|
self.emitEventToJS("onFinishTrimming", eventData: eventPayload)
|
|
377
382
|
|
|
378
383
|
if (self.saveToPhoto && isVideoType) {
|
|
@@ -383,19 +388,19 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
383
388
|
}
|
|
384
389
|
|
|
385
390
|
PHPhotoLibrary.shared().performChanges({
|
|
386
|
-
let request = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL:
|
|
391
|
+
let request = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: outputFile)
|
|
387
392
|
request?.creationDate = Date()
|
|
388
393
|
}) { success, error in
|
|
389
394
|
if success {
|
|
390
395
|
print("Edited video saved to Photo Library successfully.")
|
|
391
396
|
|
|
392
397
|
if self.removeAfterSavedToPhoto {
|
|
393
|
-
let _ = VideoTrim.deleteFile(url:
|
|
398
|
+
let _ = VideoTrim.deleteFile(url: outputFile)
|
|
394
399
|
}
|
|
395
400
|
} else {
|
|
396
401
|
self.onError(message: "Failed to save edited video to Photo Library: \(error?.localizedDescription ?? "Unknown error")", code: .failToSaveToPhoto)
|
|
397
402
|
if self.removeAfterFailedToSavePhoto {
|
|
398
|
-
let _ = VideoTrim.deleteFile(url:
|
|
403
|
+
let _ = VideoTrim.deleteFile(url: outputFile)
|
|
399
404
|
}
|
|
400
405
|
}
|
|
401
406
|
}
|
|
@@ -404,7 +409,7 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
404
409
|
DispatchQueue.main.async {
|
|
405
410
|
progressAlert.dismiss(animated: true) {
|
|
406
411
|
self.isTrimming = false
|
|
407
|
-
self.saveFileToFilesApp(fileURL:
|
|
412
|
+
self.saveFileToFilesApp(fileURL: outputFile)
|
|
408
413
|
}
|
|
409
414
|
}
|
|
410
415
|
return
|
|
@@ -412,7 +417,7 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
412
417
|
DispatchQueue.main.async {
|
|
413
418
|
progressAlert.dismiss(animated: true) {
|
|
414
419
|
self.isTrimming = false
|
|
415
|
-
self.shareFile(fileURL:
|
|
420
|
+
self.shareFile(fileURL: outputFile)
|
|
416
421
|
}
|
|
417
422
|
}
|
|
418
423
|
return
|
|
@@ -479,9 +484,7 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
479
484
|
// New Arch
|
|
480
485
|
@objc(trim:url:config:)
|
|
481
486
|
public func _trim(inputFile: String, config: NSDictionary, completion: @escaping ([String: Any]) -> Void) {
|
|
482
|
-
let destPath = URL(string: inputFile)
|
|
483
|
-
|
|
484
|
-
guard let destPath = destPath else {
|
|
487
|
+
guard let destPath = URL(string: inputFile) ?? URL(fileURLWithPath: inputFile) as URL? else {
|
|
485
488
|
let result = [
|
|
486
489
|
"success": false,
|
|
487
490
|
"message": "Invalid input file path",
|
|
@@ -677,21 +680,21 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
|
|
|
677
680
|
print(message)
|
|
678
681
|
self.onError(message: message, code: .failToShare)
|
|
679
682
|
|
|
680
|
-
if self.removeAfterFailedToShare {
|
|
681
|
-
let _ = VideoTrim.deleteFile(url:
|
|
683
|
+
if self.removeAfterFailedToShare, let outputFile = self.outputFile {
|
|
684
|
+
let _ = VideoTrim.deleteFile(url: outputFile)
|
|
682
685
|
}
|
|
683
686
|
return
|
|
684
687
|
}
|
|
685
688
|
|
|
686
689
|
if completed {
|
|
687
690
|
print("User completed the sharing activity")
|
|
688
|
-
if self.removeAfterShared {
|
|
689
|
-
let _ = VideoTrim.deleteFile(url:
|
|
691
|
+
if self.removeAfterShared, let outputFile = self.outputFile {
|
|
692
|
+
let _ = VideoTrim.deleteFile(url: outputFile)
|
|
690
693
|
}
|
|
691
694
|
} else {
|
|
692
695
|
print("User cancelled or failed to complete the sharing activity")
|
|
693
|
-
if self.removeAfterFailedToShare {
|
|
694
|
-
let _ = VideoTrim.deleteFile(url:
|
|
696
|
+
if self.removeAfterFailedToShare, let outputFile = self.outputFile {
|
|
697
|
+
let _ = VideoTrim.deleteFile(url: outputFile)
|
|
695
698
|
}
|
|
696
699
|
}
|
|
697
700
|
|
|
@@ -727,10 +730,8 @@ extension VideoTrim {
|
|
|
727
730
|
editorConfig = config
|
|
728
731
|
print("Show editor called with URI: \(uri)")
|
|
729
732
|
|
|
730
|
-
let destPath = URL(string: uri)
|
|
731
|
-
print("Destination Path: \(destPath
|
|
732
|
-
|
|
733
|
-
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)")
|
|
734
735
|
|
|
735
736
|
DispatchQueue.main.async {
|
|
736
737
|
self.vc = VideoTrimmerViewController()
|
|
@@ -773,8 +774,9 @@ extension VideoTrim {
|
|
|
773
774
|
let isVideoType = (config["type"] as? String ?? "video") == "video"
|
|
774
775
|
|
|
775
776
|
vc.saveBtnClicked = {(selectedRange: CMTimeRange) in
|
|
777
|
+
guard let asset = vc.asset else { return }
|
|
776
778
|
if !self.enableSaveDialog {
|
|
777
|
-
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)
|
|
778
780
|
return
|
|
779
781
|
}
|
|
780
782
|
|
|
@@ -784,7 +786,7 @@ extension VideoTrim {
|
|
|
784
786
|
|
|
785
787
|
// Create OK button with action handler
|
|
786
788
|
let ok = UIAlertAction(title: self.saveDialogConfirmText, style: .default, handler: { (action) -> Void in
|
|
787
|
-
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)
|
|
788
790
|
})
|
|
789
791
|
|
|
790
792
|
// Create Cancel button with action handlder
|
|
@@ -829,6 +831,7 @@ extension VideoTrim {
|
|
|
829
831
|
vc.dismiss(animated: true, completion: {
|
|
830
832
|
self.emitEventToJS("onHide", eventData: nil)
|
|
831
833
|
self.isShowing = false
|
|
834
|
+
self.vc = nil
|
|
832
835
|
})
|
|
833
836
|
}
|
|
834
837
|
|
|
@@ -885,7 +888,8 @@ extension VideoTrim {
|
|
|
885
888
|
// New Arch
|
|
886
889
|
@objc(deleteFile:)
|
|
887
890
|
public static func deleteFile(uri: String) -> Bool {
|
|
888
|
-
let
|
|
891
|
+
guard let url = URL(string: uri) else { return false }
|
|
892
|
+
let state = deleteFile(url: url)
|
|
889
893
|
return state == 0
|
|
890
894
|
}
|
|
891
895
|
|
|
@@ -918,7 +922,10 @@ extension VideoTrim {
|
|
|
918
922
|
// New Arch
|
|
919
923
|
@objc(isValidFile:url:)
|
|
920
924
|
public static func isValidFile(url: String, completion: @escaping ([String: Any]) -> Void) -> Void {
|
|
921
|
-
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
|
+
}
|
|
922
929
|
checkFileValidity(url: fileURL) { isValid, fileType, duration in
|
|
923
930
|
if isValid {
|
|
924
931
|
print("Valid \(fileType) file with duration: \(duration) milliseconds")
|
|
@@ -1018,8 +1025,9 @@ extension VideoTrim {
|
|
|
1018
1025
|
|
|
1019
1026
|
vc?.asset = loader.asset
|
|
1020
1027
|
|
|
1028
|
+
let duration = loader.asset?.duration.seconds ?? 0
|
|
1021
1029
|
let eventPayload: [String: Any] = [
|
|
1022
|
-
"duration":
|
|
1030
|
+
"duration": duration * 1000,
|
|
1023
1031
|
]
|
|
1024
1032
|
self.emitEventToJS("onLoad", eventData: eventPayload)
|
|
1025
1033
|
}
|
|
@@ -1029,15 +1037,15 @@ extension VideoTrim {
|
|
|
1029
1037
|
// MARK: DocumentPicker delegate
|
|
1030
1038
|
extension VideoTrim {
|
|
1031
1039
|
public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
|
1032
|
-
if removeAfterSavedToDocuments {
|
|
1033
|
-
let _ = VideoTrim.deleteFile(url: outputFile
|
|
1040
|
+
if removeAfterSavedToDocuments, let outputFile = self.outputFile {
|
|
1041
|
+
let _ = VideoTrim.deleteFile(url: outputFile)
|
|
1034
1042
|
}
|
|
1035
1043
|
closeEditor()
|
|
1036
1044
|
}
|
|
1037
1045
|
|
|
1038
1046
|
public func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
|
1039
|
-
if removeAfterFailedToSaveDocuments {
|
|
1040
|
-
let _ = VideoTrim.deleteFile(url: outputFile
|
|
1047
|
+
if removeAfterFailedToSaveDocuments, let outputFile = self.outputFile {
|
|
1048
|
+
let _ = VideoTrim.deleteFile(url: outputFile)
|
|
1041
1049
|
}
|
|
1042
1050
|
closeEditor()
|
|
1043
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
|
}
|