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.
@@ -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!!, Theme_Black_NoTitleBar_Fullscreen
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
- applySafeAreaToDialog(alertDialog!!, trimmerView!!)
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!!.setOnDismissListener {
168
- // This is called in same thread as the trimmer view -> UI thread
169
- if (trimmerView != null) {
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.currentActivity!!,R.color.black)
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
- if (trimmerView != null) {
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!!.setProgress(percentage, true)
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
- saveFileToExternalStorage(File(outputFile!!))
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(outputFile!!))
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 (!editorConfig?.getBoolean("enableCancelDialog")!!) {
339
+ if (editorConfig?.getBoolean("enableCancelDialog") != true) {
337
340
  sendEvent("onCancel", null)
338
341
  hideDialog(true)
339
342
  return
340
343
  }
341
344
 
342
- val builder = AlertDialog.Builder(reactApplicationContext.currentActivity!!)
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 (!editorConfig?.getBoolean("enableSaveDialog")!!) {
365
+ if (editorConfig?.getBoolean("enableSaveDialog") != true) {
362
366
  startTrim()
363
367
  return
364
368
  }
365
369
 
366
- val builder = AlertDialog.Builder(reactApplicationContext.currentActivity!!)
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
- mProgressBar!!.layoutParams = ViewGroup.LayoutParams(
418
- ViewGroup.LayoutParams.MATCH_PARENT,
419
- ViewGroup.LayoutParams.WRAP_CONTENT
420
- )
421
- mProgressBar!!.progressTintList = ColorStateList.valueOf("#2196F3".toColorInt())
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
- ) // or use your custom color
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
- if (trimmerView != null) {
455
- trimmerView!!.onCancelTrimClicked()
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!!.show()
471
+ cancelTrimmingConfirmDialog?.show()
468
472
  } else {
469
- if (trimmerView != null) {
470
- trimmerView!!.onCancelTrimClicked()
471
- }
473
+ trimmerView?.onCancelTrimClicked()
472
474
 
473
- if (mProgressDialog != null && mProgressDialog!!.isShowing) {
474
- mProgressDialog!!.dismiss()
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!!.setOnShowListener {
493
+ mProgressDialog?.setOnShowListener {
492
494
  sendEvent("onStartTrimming", null)
493
- if (trimmerView != null) {
494
- trimmerView!!.onSaveClicked()
495
- }
495
+ trimmerView?.onSaveClicked()
496
496
  }
497
497
 
498
- mProgressDialog!!.show()
498
+ mProgressDialog?.show()
499
499
  }
500
500
 
501
501
  private fun hideDialog(shouldCloseEditor: Boolean) {
502
- // handle the case when the cancel dialog is still showing but the trimming is finished
503
- if (cancelTrimmingConfirmDialog != null) {
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
- if (mProgressDialog != null) {
511
- if (mProgressDialog!!.isShowing) mProgressDialog!!.dismiss()
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
- if (alertDialog != null) {
518
- if (alertDialog!!.isShowing) {
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
- outputFile!!
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
- when {
612
- ReturnCode.isSuccess(returnCode) -> {
613
- // SUCCESS
614
- val duration = endTime - startTime
615
- val result = Arguments.createMap()
616
-
617
- result.putString("outputPath", outputFile)
618
- result.putDouble("startTime", startTime)
619
- result.putDouble("endTime", endTime)
620
- result.putDouble("duration", duration)
621
- result.putBoolean("success", true)
622
-
623
- if (options?.getBoolean("saveToPhoto") == true && options.getString("type") == "video") {
624
- Log.d(TAG, "Android trim: saveToPhoto is true, attempting to save to gallery")
625
- try {
626
- StorageUtil.saveVideoToGallery(reactApplicationContext, outputFile)
627
- Log.d(TAG, "Edited video saved to Photo Library successfully.")
628
- if (options.getBoolean("removeAfterSavedToPhoto")) {
629
- Log.d(TAG, "Removing file after successful save to photo")
630
- StorageUtil.deleteFile(outputFile)
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
- promise.resolve(result)
634
- } catch (e: IOException) {
635
- e.printStackTrace()
632
+ promise.resolve(result)
633
+ } catch (e: IOException) {
634
+ e.printStackTrace()
636
635
 
637
- if (options.getBoolean("removeAfterFailedToSavePhoto")) {
638
- Log.d(TAG, "Removing file after failed save to photo")
639
- StorageUtil.deleteFile(outputFile)
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.reject(
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
- ReturnCode.isCancel(returnCode) -> {
653
- // CANCEL
654
- println("FFmpeg command was cancelled")
655
- promise.reject(
656
- Exception("FFmpeg command was cancelled with code $returnCode")
657
- )
658
- }
659
- else -> {
660
- // FAILURE
661
- val errorMessage = String.format("Command failed with state %s and rc %s.%s", state, returnCode, session.getFailStackTrace());
662
- Log.d(TAG, errorMessage)
663
- promise.reject(
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"
@@ -6,5 +6,6 @@ enum class ErrorCode {
6
6
  FAIL_TO_INITIALIZE_AUDIO_PLAYER,
7
7
  FAIL_TO_LOAD_MEDIA,
8
8
  FAIL_TO_SAVE_TO_PHOTO,
9
- FAIL_TO_SAVE_TO_DOCUMENTS
9
+ FAIL_TO_SAVE_TO_DOCUMENTS,
10
+ UNKNOWN
10
11
  }
@@ -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
- callback.onResult(isValid, fileType, if (isValid) duration else -1L)
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
- return deleteFile(File(path!!))
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
- when {
86
- ReturnCode.isSuccess(session.returnCode) -> {
87
- callback.onFinishTrim(outputFile, startMs, endMs, videoDuration)
88
- }
89
- ReturnCode.isCancel(session.returnCode) -> {
90
- callback.onCancelTrim()
91
- }
92
- else -> {
93
- val errorMessage = "Command failed with state $state and rc $returnCode.${session.failStackTrace}"
94
- callback.onError(errorMessage, ErrorCode.TRIMMING_FAILED)
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
- callback.onLog(map)
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
- callback.onTrimmingProgress(completePercentage.coerceIn(0, 100))
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
- callback.onStatistics(map)
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!!.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
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 builder = AlertDialog.Builder(mContext.currentActivity!!)
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
- try {
473
- mediaMetadataRetriever?.release()
474
- } catch (e: Exception) {
475
- e.printStackTrace()
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 = mediaMetadataRetriever?.getFrameAtTime(clampedTimeUs, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
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) {
@@ -1,5 +1,5 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
2
  <paths xmlns:android="http://schemas.android.com/apk/res/android">
3
3
  <files-path name="internal_files" path="." />
4
- <external-path name="external_files" path="." />
4
+ <cache-path name="cache_files" path="." />
5
5
  </paths>
@@ -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.cancelDialogConfirmText, style: .destructive, handler: { (action) -> Void in
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.cancelDialogCancelText, style: .cancel)
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!.path
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": self.outputFile!.absoluteString, "startTime": (startTime * 1000).rounded(), "endTime": (endTime * 1000).rounded(), "duration": (videoDuration * 1000).rounded()]
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: self.outputFile!)
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: self.outputFile!)
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: self.outputFile!)
403
+ let _ = VideoTrim.deleteFile(url: outputFile)
394
404
  }
395
405
  }
396
406
  }
397
407
  }
398
408
  } else if self.openDocumentsOnFinish {
399
- self.saveFileToFilesApp(fileURL: self.outputFile!)
400
-
401
- // must return otherwise editor will close
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
- self.shareFile(fileURL: self.outputFile!)
405
-
406
- // must return otherwise editor will close
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
- if self.closeWhenFinish {
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
- if self.closeWhenFinish {
421
- self.closeEditor(delay: 500)
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: self.outputFile!)
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: self.outputFile!)
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: self.outputFile!)
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!.absoluteString), path: \(destPath!.path)")
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: self.vc!.asset!.duration.seconds, startTime: selectedRange.start.seconds, endTime: selectedRange.end.seconds, isVideoType: isVideoType)
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: vc.asset!.duration.seconds, startTime: selectedRange.start.seconds, endTime: selectedRange.end.seconds, isVideoType: isVideoType)
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
- // some how in case we trim a very short video the view controller is still visible after first .dismiss call
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 state = deleteFile(url: URL(string: uri)!)
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": loader.asset!.duration.seconds * 1000,
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
  }
@@ -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[seenIndex - 1].imageView
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
- // Clean up the observer
201
- player.removeObserver(self, forKeyPath: "status")
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
- // Add observer for player status
402
- player.addObserver(self, forKeyPath: "status", options: [.new, .initial], context: nil)
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
- override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
516
- if keyPath == "status" {
517
- if player.status == .readyToPlay {
518
- loadingIndicator.stopAnimating()
519
- btnStackView.removeArrangedSubview(loadingIndicator)
520
- loadingIndicator.removeFromSuperview()
521
- btnStackView.insertArrangedSubview(playBtn, at: 1)
522
-
523
- UIView.animate(withDuration: 0.25, animations: {
524
- self.playBtn.alpha = 1
525
- self.playBtn.isEnabled = true
526
- self.saveBtn.alpha = 1
527
- self.saveBtn.isEnabled = true
528
- })
529
-
530
- if jumpToPositionOnLoad > 0 {
531
- let duration = (asset?.duration.seconds ?? 0) * 1000
532
- let time = jumpToPositionOnLoad > duration ? duration : jumpToPositionOnLoad
533
- let cmtime = CMTime(value: CMTimeValue(time), timescale: 1000)
534
-
535
- self.seek(to: cmtime)
536
- self.trimmer.progress = cmtime
537
- self.currentTimeLabel.text = self.trimmer.progress.displayString
538
- }
539
-
540
- if autoplay {
541
- togglePlay(sender: playBtn)
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-video-trim",
3
- "version": "6.2.1",
3
+ "version": "6.2.3",
4
4
  "description": "Video trimmer for your React Native app",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",