react-native-video-trim 2.1.0 → 2.2.1

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.
@@ -1,7 +1,6 @@
1
1
  package com.videotrim.widgets;
2
2
 
3
3
  import static com.facebook.react.bridge.UiThreadUtil.runOnUiThread;
4
- import static com.videotrim.utils.VideoTrimmerUtil.DEFAULT_AUDIO_EXTENSION;
5
4
  import static com.videotrim.utils.VideoTrimmerUtil.RECYCLER_VIEW_PADDING;
6
5
  import static com.videotrim.utils.VideoTrimmerUtil.VIDEO_FRAMES_WIDTH;
7
6
 
@@ -9,8 +8,6 @@ import android.content.Context;
9
8
  import android.content.pm.ActivityInfo;
10
9
  import android.content.res.Configuration;
11
10
  import android.graphics.Bitmap;
12
- import android.graphics.Color;
13
- import android.graphics.drawable.Drawable;
14
11
  import android.graphics.drawable.GradientDrawable;
15
12
  import android.media.MediaMetadataRetriever;
16
13
  import android.media.MediaPlayer;
@@ -21,10 +18,10 @@ import android.os.VibrationEffect;
21
18
  import android.os.Vibrator;
22
19
  import android.util.AttributeSet;
23
20
  import android.util.Log;
21
+ import android.util.TypedValue;
24
22
  import android.view.LayoutInflater;
25
23
  import android.view.MotionEvent;
26
24
  import android.view.View;
27
- import android.view.ViewGroup;
28
25
  import android.widget.FrameLayout;
29
26
  import android.widget.ImageView;
30
27
  import android.widget.LinearLayout;
@@ -33,9 +30,9 @@ import android.widget.RelativeLayout;
33
30
  import android.widget.TextView;
34
31
  import android.widget.VideoView;
35
32
 
36
- import androidx.core.content.ContextCompat;
37
- import androidx.core.content.res.ResourcesCompat;
33
+ import androidx.appcompat.app.AlertDialog;
38
34
 
35
+ import com.arthenica.ffmpegkit.FFmpegSession;
39
36
  import com.facebook.react.bridge.ReactApplicationContext;
40
37
  import com.facebook.react.bridge.ReadableMap;
41
38
  import com.videotrim.R;
@@ -64,7 +61,6 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
64
61
  private Uri mSourceUri;
65
62
  private VideoTrimListener mOnTrimVideoListener;
66
63
  private int mDuration = 0;
67
- private Boolean mIsPrepared = false;
68
64
  private long mMaxDuration = (long) Double.POSITIVE_INFINITY;
69
65
  private long mMinDuration = VideoTrimmerUtil.MIN_SHOOT_DURATION;
70
66
 
@@ -105,6 +101,15 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
105
101
 
106
102
  private String mOutputExt = "mp4";
107
103
  private boolean enableHapticFeedback = true;
104
+ private boolean autoplay = false;
105
+ private long jumpToPositionOnLoad = 0;
106
+ private FrameLayout headerView;
107
+ private TextView headerText;
108
+ private FFmpegSession ffmpegSession;
109
+ private boolean alertOnFailToLoad = true;
110
+ private String alertOnFailTitle = "Error";
111
+ private String alertOnFailMessage = "Fail to load media. Possibly invalid file or no network connection";
112
+ private String alertOnFailCloseText = "Close";
108
113
 
109
114
  public VideoTrimmerView(ReactApplicationContext context, ReadableMap config, AttributeSet attrs) {
110
115
  this(context, attrs, 0, config);
@@ -152,6 +157,9 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
152
157
  cancelBtn = findViewById(R.id.cancelBtn);
153
158
  audioBannerView = findViewById(R.id.audioBannerView);
154
159
  failToLoadBtn = findViewById(R.id.failToLoadBtn);
160
+
161
+ headerView = findViewById(R.id.headerView);
162
+ headerText = findViewById(R.id.headerText);
155
163
  }
156
164
 
157
165
  public void initByURI(final Uri videoURI) {
@@ -162,18 +170,11 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
162
170
  mVideoView.requestFocus();
163
171
 
164
172
  mVideoView.setOnPreparedListener(mp -> {
165
- if (!mIsPrepared) {
166
- mp.setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT);
167
- videoPrepared();
168
- mIsPrepared = true;
169
- }
173
+ mp.setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT);
174
+ mediaPrepared();
170
175
  });
171
176
 
172
- mVideoView.setOnErrorListener((mp, what, extra) -> {
173
- mediaFailed();
174
- mOnTrimVideoListener.onError("Error loading video file. Please try again.", ErrorCode.FAIL_TO_LOAD_VIDEO);
175
- return true;
176
- });
177
+ mVideoView.setOnErrorListener(this::onFailToLoadMedia);
177
178
 
178
179
  mVideoView.setOnCompletionListener(mp -> mediaCompleted());
179
180
  } else {
@@ -186,17 +187,10 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
186
187
  try {
187
188
  audioPlayer.setDataSource(videoURI.toString());
188
189
  audioPlayer.setOnPreparedListener(mp -> {
189
- if (!mIsPrepared) {
190
- audioPrepared();
191
- mIsPrepared = true;
192
- }
190
+ mediaPrepared();
193
191
  });
194
192
  audioPlayer.setOnCompletionListener(mp -> mediaCompleted());
195
- audioPlayer.setOnErrorListener((mp, what, extra) -> {
196
- mediaFailed();
197
- mOnTrimVideoListener.onError("Error loading audio file. Please try again.", ErrorCode.FAIL_TO_LOAD_AUDIO);
198
- return true;
199
- });
193
+ audioPlayer.setOnErrorListener(this::onFailToLoadMedia);
200
194
 
201
195
  audioPlayer.prepareAsync(); // use prepareAsync to avoid blocking the main thread
202
196
  } catch (IOException e) {
@@ -207,6 +201,25 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
207
201
  }
208
202
  }
209
203
 
204
+ private boolean onFailToLoadMedia(MediaPlayer mp, int what, int extra) {
205
+ mediaFailed();
206
+ mOnTrimVideoListener.onError("Error loading media file. Please try again.", ErrorCode.FAIL_TO_LOAD_MEDIA);
207
+ if (alertOnFailToLoad) {
208
+ AlertDialog.Builder builder = new AlertDialog.Builder(mContext.getCurrentActivity());
209
+ builder.setMessage(alertOnFailMessage);
210
+ builder.setTitle(alertOnFailTitle);
211
+ builder.setCancelable(false);
212
+ builder.setPositiveButton(alertOnFailCloseText, (dialog, which) -> {
213
+ dialog.cancel();
214
+ });
215
+
216
+ AlertDialog alertDialog = builder.create();
217
+ alertDialog.show();
218
+ }
219
+
220
+ return true;
221
+ }
222
+
210
223
  private void startShootVideoThumbs(final Context context, int totalThumbsCount, long startPosition, long endPosition) {
211
224
  mThumbnailContainer.removeAllViews();
212
225
  VideoTrimmerUtil.shootVideoThumbInBackground(mediaMetadataRetriever, totalThumbsCount, startPosition, endPosition,
@@ -225,28 +238,32 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
225
238
  });
226
239
  }
227
240
 
228
- private void videoPrepared() {
229
- mDuration = mVideoView.getDuration();
241
+ private void mediaPrepared() {
242
+ mDuration = isVideoType ? mVideoView.getDuration() : audioPlayer.getDuration();
230
243
  mMaxDuration = Math.min(mMaxDuration, mDuration);
231
- mediaMetadataRetriever = MediaMetadataUtil.getMediaMetadataRetriever(mSourceUri.toString());
232
244
 
233
- if (mediaMetadataRetriever == null) {
234
- mOnTrimVideoListener.onError("Error when retrieving video info. Please try again.", ErrorCode.FAIL_TO_GET_VIDEO_INFO);
235
- return;
236
- }
245
+ if (isVideoType) {
246
+ mediaMetadataRetriever = MediaMetadataUtil.getMediaMetadataRetriever(mSourceUri.toString());
247
+ if (mediaMetadataRetriever == null) {
248
+ mOnTrimVideoListener.onError("Error when retrieving video info. Please try again.", ErrorCode.FAIL_TO_GET_VIDEO_INFO);
249
+ return;
250
+ }
237
251
 
238
- // take first frame
239
- Bitmap bitmap = mediaMetadataRetriever.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
252
+ // take first frame
253
+ Bitmap bitmap = mediaMetadataRetriever.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
240
254
 
241
- if (bitmap != null) {
242
- VideoTrimmerUtil.mThumbWidth = VideoTrimmerUtil.THUMB_HEIGHT * bitmap.getWidth() / bitmap.getHeight();
243
- }
255
+ if (bitmap != null) {
256
+ VideoTrimmerUtil.mThumbWidth = VideoTrimmerUtil.THUMB_HEIGHT * bitmap.getWidth() / bitmap.getHeight();
257
+ }
244
258
 
245
- VideoTrimmerUtil.SCREEN_WIDTH_FULL = this.getScreenWidthInPortraitMode();
246
- VideoTrimmerUtil.VIDEO_FRAMES_WIDTH = VideoTrimmerUtil.SCREEN_WIDTH_FULL - RECYCLER_VIEW_PADDING * 2;
247
- VideoTrimmerUtil.MAX_COUNT_RANGE = Math.max((VIDEO_FRAMES_WIDTH / VideoTrimmerUtil.mThumbWidth), VideoTrimmerUtil.MAX_COUNT_RANGE);
259
+ VideoTrimmerUtil.SCREEN_WIDTH_FULL = this.getScreenWidthInPortraitMode();
260
+ VideoTrimmerUtil.VIDEO_FRAMES_WIDTH = VideoTrimmerUtil.SCREEN_WIDTH_FULL - RECYCLER_VIEW_PADDING * 2;
261
+ VideoTrimmerUtil.MAX_COUNT_RANGE = Math.max((VIDEO_FRAMES_WIDTH / VideoTrimmerUtil.mThumbWidth), VideoTrimmerUtil.MAX_COUNT_RANGE);
248
262
 
249
- startShootVideoThumbs(mContext, VideoTrimmerUtil.MAX_COUNT_RANGE, 0, mDuration);
263
+ startShootVideoThumbs(mContext, VideoTrimmerUtil.MAX_COUNT_RANGE, 0, mDuration);
264
+ } else {
265
+
266
+ }
250
267
 
251
268
  // Set initial handle positions if mMaxDuration < video duration
252
269
  if (mMaxDuration < mDuration) {
@@ -259,24 +276,16 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
259
276
  loadingIndicator.setVisibility(View.GONE);
260
277
  mPlayView.setVisibility(View.VISIBLE);
261
278
  saveBtn.setVisibility(View.VISIBLE);
262
- }
263
279
 
264
- private void audioPrepared() {
265
- mDuration = audioPlayer.getDuration();
266
- mMaxDuration = Math.min(mMaxDuration, mDuration);
280
+ if (jumpToPositionOnLoad > 0) {
281
+ seekTo(jumpToPositionOnLoad > mDuration ? mDuration : jumpToPositionOnLoad, true);
282
+ }
267
283
 
268
- // Set initial handle positions if mMaxDuration < video duration
269
- if (mMaxDuration < mDuration) {
270
- endTime = mMaxDuration;
271
- } else {
272
- endTime = mDuration;
284
+ if (autoplay) {
285
+ playOrPause();
273
286
  }
274
287
 
275
- updateHandlePositions();
276
- loadingIndicator.setVisibility(View.GONE);
277
- mPlayView.setVisibility(View.VISIBLE);
278
- saveBtn.setVisibility(View.VISIBLE);
279
- // mThumbnailContainer.animate().alpha(1f).setDuration(250).start();
288
+ mOnTrimVideoListener.onLoad(mDuration);
280
289
  }
281
290
 
282
291
  private void updateGradientColors(int startColor, int endColor) {
@@ -375,8 +384,8 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
375
384
 
376
385
  public void onSaveClicked() {
377
386
  onMediaPause();
378
- VideoTrimmerUtil.trim(
379
- isVideoType ? mSourceUri.getPath() : mSourceUri.toString(),
387
+ ffmpegSession = VideoTrimmerUtil.trim(
388
+ mSourceUri.toString(),
380
389
  StorageUtil.getOutputPath(mContext, mOutputExt),
381
390
  mDuration,
382
391
  startTime,
@@ -384,6 +393,14 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
384
393
  mOnTrimVideoListener);
385
394
  }
386
395
 
396
+ public void onCancelTrimClicked() {
397
+ if (ffmpegSession != null) {
398
+ ffmpegSession.cancel();
399
+ } else {
400
+ mOnTrimVideoListener.onCancelTrim();
401
+ }
402
+ }
403
+
387
404
  private void seekTo(long msec, boolean needUpdateProgress) {
388
405
  if (isVideoType) {
389
406
  mVideoView.seekTo((int) msec);
@@ -462,11 +479,54 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
462
479
 
463
480
  if (config.hasKey("outputExt")) {
464
481
  mOutputExt = config.getString("outputExt");
482
+ } else if (!isVideoType) {
483
+ mOutputExt = "wav";
465
484
  }
466
485
 
467
486
  if (config.hasKey("enableHapticFeedback")) {
468
487
  enableHapticFeedback = config.getBoolean("enableHapticFeedback");
469
488
  }
489
+
490
+ if (config.hasKey("autoplay")) {
491
+ autoplay = config.getBoolean("autoplay");
492
+ }
493
+
494
+ if (config.hasKey("jumpToPositionOnLoad")) {
495
+ jumpToPositionOnLoad = config.getInt("jumpToPositionOnLoad");
496
+ }
497
+ // check if config.getString("headerText") is not empty
498
+
499
+ if (config.hasKey("headerText") && !config.getString("headerText").isEmpty()){
500
+ headerText.setText(config.getString("headerText"));
501
+
502
+ if (config.hasKey("headerTextSize")) {
503
+ int textSize = config.getInt("headerTextSize");
504
+ if (textSize < 0) {
505
+ textSize = 16;
506
+ }
507
+ headerText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize);
508
+ }
509
+
510
+ if (config.hasKey("headerTextColor")) {
511
+ headerText.setTextColor(config.getInt("headerTextColor"));
512
+ }
513
+
514
+ headerView.setVisibility(View.VISIBLE);
515
+ }
516
+
517
+ alertOnFailToLoad = !config.hasKey("alertOnFailToLoad") || config.getBoolean("alertOnFailToLoad");
518
+
519
+ if (config.hasKey("alertOnFailTitle")) {
520
+ alertOnFailTitle = config.getString("alertOnFailTitle");
521
+ }
522
+
523
+ if (config.hasKey("alertOnFailMessage")) {
524
+ alertOnFailMessage = config.getString("alertOnFailMessage");
525
+ }
526
+
527
+ if (config.hasKey("alertOnFailCloseText")) {
528
+ alertOnFailCloseText = config.getString("alertOnFailCloseText");
529
+ }
470
530
  }
471
531
 
472
532
  private void startTimingRunnable() {
@@ -3,11 +3,31 @@
3
3
  android:layout_width="match_parent"
4
4
  android:layout_height="match_parent"
5
5
  android:orientation="vertical">
6
+ <FrameLayout
7
+ android:id="@+id/headerView"
8
+ android:layout_width="match_parent"
9
+ android:layout_height="wrap_content"
10
+ android:minHeight="50dp"
11
+ android:background="@android:color/black"
12
+ android:visibility="gone"
13
+ >
14
+ <TextView
15
+ android:id="@+id/headerText"
16
+ android:layout_width="wrap_content"
17
+ android:layout_height="match_parent"
18
+ android:layout_gravity="center"
19
+ android:clickable="true"
20
+ android:focusable="true"
21
+ android:text="Header text"
22
+ android:textColor="@android:color/white"
23
+ android:textSize="16dp" />
24
+ </FrameLayout>
6
25
 
7
26
  <RelativeLayout
8
27
  android:layout_width="match_parent"
9
28
  android:layout_height="match_parent"
10
29
  android:layout_above="@+id/layout"
30
+ android:layout_below="@+id/headerView"
11
31
  android:background="@android:color/black"
12
32
  android:gravity="center">
13
33
 
@@ -2,7 +2,7 @@
2
2
  // AssetLoader.swift
3
3
  // react-native-video-trim
4
4
  //
5
- // Created by ByteDance on 7/27/24.
5
+ // Created by Duc Trung Mai on 7/27/24.
6
6
  //
7
7
 
8
8
  import AVFoundation
@@ -9,7 +9,7 @@ import Foundation
9
9
 
10
10
  enum ErrorCode: String {
11
11
  case trimmingFailed = "TRIMMING_FAILED"
12
- case failToLoadVideo = "FAIL_TO_LOAD_VIDEO"
12
+ case failToLoadMedia = "FAIL_TO_LOAD_MEDIA"
13
13
  case failToSaveToPhoto = "FAIL_TO_SAVE_TO_PHOTO"
14
14
  case failToShare = "FAIL_TO_SHARE"
15
15
  case noPhotoPermission = "NO_PHOTO_PERMISSION"
@@ -0,0 +1,100 @@
1
+ //
2
+ // ProgressAlertController.swift
3
+ // react-native-video-trim
4
+ //
5
+ // Created by Duc Trung Mai on 8/18/24.
6
+ //
7
+
8
+ import UIKit
9
+
10
+ class ProgressAlertController: UIViewController {
11
+ var onDismiss: (() -> Void)?
12
+
13
+ private let titleLabel = UILabel()
14
+ private let progressBar = UIProgressView(progressViewStyle: .default)
15
+ private let actionButton = UIButton(type: .system)
16
+
17
+ override func viewDidLoad() {
18
+ super.viewDidLoad()
19
+
20
+ setupBackground()
21
+ setupAlertView()
22
+ }
23
+
24
+ private func setupBackground() {
25
+ view.backgroundColor = UIColor.black.withAlphaComponent(0.4)
26
+ }
27
+
28
+ private func setupAlertView() {
29
+ let alertView = UIView()
30
+ alertView.backgroundColor = UIColor(red: 28/255, green: 28/255, blue: 30/255, alpha: 1.0)
31
+ alertView.layer.cornerRadius = 12
32
+ alertView.translatesAutoresizingMaskIntoConstraints = false
33
+ view.addSubview(alertView)
34
+
35
+ // AlertView Constraints
36
+ NSLayoutConstraint.activate([
37
+ alertView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
38
+ alertView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
39
+ alertView.widthAnchor.constraint(equalToConstant: 270)
40
+ ])
41
+
42
+ // Title Label
43
+ titleLabel.translatesAutoresizingMaskIntoConstraints = false
44
+ titleLabel.textAlignment = .center
45
+ titleLabel.font = UIFont.systemFont(ofSize: 18)
46
+ titleLabel.numberOfLines = 0
47
+ titleLabel.textColor = .white
48
+ alertView.addSubview(titleLabel)
49
+
50
+ // Progress Bar
51
+ progressBar.translatesAutoresizingMaskIntoConstraints = false
52
+ alertView.addSubview(progressBar)
53
+
54
+ // Action Button
55
+ actionButton.setTitle("Cancel", for: .normal)
56
+ actionButton.setTitleColor(.systemPink, for: .normal)
57
+ actionButton.titleLabel?.font = UIFont.systemFont(ofSize: 16)
58
+ actionButton.addTarget(self, action: #selector(dismissAlert), for: .touchUpInside)
59
+ actionButton.translatesAutoresizingMaskIntoConstraints = false
60
+ actionButton.isHidden = true
61
+ alertView.addSubview(actionButton)
62
+
63
+ // Constraints for titleLabel, progressBar, and actionButton
64
+ NSLayoutConstraint.activate([
65
+ titleLabel.topAnchor.constraint(equalTo: alertView.topAnchor, constant: 16),
66
+ titleLabel.leadingAnchor.constraint(equalTo: alertView.leadingAnchor, constant: 16),
67
+ titleLabel.trailingAnchor.constraint(equalTo: alertView.trailingAnchor, constant: -16),
68
+
69
+ progressBar.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 16),
70
+ progressBar.leadingAnchor.constraint(equalTo: alertView.leadingAnchor, constant: 16),
71
+ progressBar.trailingAnchor.constraint(equalTo: alertView.trailingAnchor, constant: -16),
72
+
73
+ actionButton.topAnchor.constraint(equalTo: progressBar.bottomAnchor, constant: 16),
74
+ actionButton.bottomAnchor.constraint(equalTo: alertView.bottomAnchor, constant: -16),
75
+ actionButton.centerXAnchor.constraint(equalTo: alertView.centerXAnchor)
76
+ ])
77
+ }
78
+
79
+ @objc private func dismissAlert() {
80
+ self.onDismiss?()
81
+ }
82
+
83
+ func setTitle(_ text: String) {
84
+ titleLabel.text = text
85
+ }
86
+
87
+ func setCancelTitle(_ text: String) {
88
+ actionButton.setTitle(text, for: .normal)
89
+ }
90
+
91
+ func setProgress(_ progress: Float) {
92
+ progressBar.setProgress(progress, animated: true)
93
+ }
94
+
95
+ func showCancelBtn() {
96
+ // Ensure that the button is properly added to the view hierarchy and that the layout has been updated before hiding the button
97
+ view.layoutIfNeeded()
98
+ actionButton.isHidden = false
99
+ }
100
+ }