shakebug-react-native 2.4.8 → 2.4.10
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/README.md +0 -12
- package/VideoPlayer/PlayVideo.tsx +4 -46
- package/VideoPlayer/index.js +6 -43
- package/android/src/main/java/com/shakebug/BaseUtils.java +7 -19
- package/android/src/main/java/com/shakebug/DeviceUtil.java +34 -21
- package/android/src/main/java/com/shakebug/DrawingView.java +8 -3
- package/android/src/main/java/com/shakebug/RecordScreenModule.java +1 -2
- package/android/src/main/java/com/shakebug/ShakebugPackage.java +3 -3
- package/android/src/main/java/com/shakebug/UnitConverter.java +8 -8
- package/android/src/main/java/com/shakebug/VideoPlayerModule.java +81 -0
- package/android/src/main/java/com/shakebug/VideoTrimModule.java +387 -0
- package/android/src/main/java/com/shakebug/VideoTrimUtil.java +133 -0
- package/android/src/main/java/com/shakebug/ViewShot.java +5 -2
- package/android/src/main/java/com/shakebug/adapters/VideoTrimmerAdapter.java +53 -0
- package/android/src/main/java/com/shakebug/callback/SingleCallback.java +0 -6
- package/android/src/main/java/com/shakebug/thread/BackgroundExecutor.java +1 -93
- package/android/src/main/java/com/shakebug/thread/UiThreadExecutor.java +0 -37
- package/android/src/main/java/com/shakebug/utils/StorageUtil.java +135 -0
- package/android/src/main/java/com/shakebug/utils/VideoTrimmerUtil.java +70 -0
- package/android/src/main/java/com/shakebug/widgets/RangeSeekBarView.java +534 -0
- package/android/src/main/java/com/shakebug/widgets/SpacesItemDecoration2.java +1 -2
- package/android/src/main/java/com/shakebug/widgets/VideoPlayerView.java +317 -0
- package/android/src/main/java/com/shakebug/widgets/VideoTrimmerView.java +520 -0
- package/android/src/main/res/drawable/ic_volume_off.xml +10 -0
- package/android/src/main/res/drawable/ic_volume_on.xml +10 -0
- package/android/src/main/res/layout/{video_player_view.xml → shakebug_video_player_view.xml} +29 -9
- package/ios/TrimVideo/ShakebugSDKVideoTrim.swift +389 -0
- package/ios/TrimVideo/VideoTrim-Bridging-Header.h +10 -0
- package/ios/TrimVideo/VideoTrim.m +23 -0
- package/ios/TrimVideo/VideoTrimmer.swift +810 -0
- package/ios/TrimVideo/VideoTrimmerThumb.swift +119 -0
- package/ios/TrimVideo/VideoTrimmerViewController.swift +322 -0
- package/ios/VideoPlayer/VideoPlayerModule.m +23 -7
- package/package.json +1 -1
- package/src/APIs/AddIssue.js +11 -1
- package/src/Componanats/CustomAlert.js +2 -6
- package/src/Componanats/ErrorHandler.js +10 -1
- package/src/Componanats/LoadingModal.js +59 -0
- package/src/Componanats/ShakeFunction.js +2 -10
- package/src/Screens/AddDetailsScreen.js +29 -33
- package/src/Screens/FirstScreen.js +15 -19
- package/src/Screens/Shakebug.js +18 -26
- package/src/Screens/TrimVideoScreen.js +37 -22
- package/src/index.tsx +4 -4
- package/trimVideo/src/index.tsx +12 -48
- /package/android/src/main/res/layout/{video_thumb_item_layout.xml → shakebug_video_thumb_item_layout.xml} +0 -0
- /package/android/src/main/res/layout/{video_trimmer_view.xml → shakebug_video_trimmer_view.xml} +0 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
package com.shakebug;
|
|
2
|
+
|
|
3
|
+
import static com.facebook.react.bridge.UiThreadUtil.runOnUiThread;
|
|
4
|
+
|
|
5
|
+
import android.app.Activity;
|
|
6
|
+
import android.content.res.ColorStateList;
|
|
7
|
+
import android.graphics.Color;
|
|
8
|
+
import android.media.MediaMetadataRetriever;
|
|
9
|
+
import android.net.Uri;
|
|
10
|
+
import android.os.Build;
|
|
11
|
+
import android.view.Gravity;
|
|
12
|
+
import android.view.ViewGroup;
|
|
13
|
+
import android.widget.LinearLayout;
|
|
14
|
+
import android.widget.ProgressBar;
|
|
15
|
+
import android.widget.TextView;
|
|
16
|
+
|
|
17
|
+
import androidx.annotation.NonNull;
|
|
18
|
+
import androidx.annotation.Nullable;
|
|
19
|
+
import androidx.appcompat.app.AlertDialog;
|
|
20
|
+
|
|
21
|
+
import com.facebook.react.bridge.Arguments;
|
|
22
|
+
import com.facebook.react.bridge.LifecycleEventListener;
|
|
23
|
+
import com.facebook.react.bridge.Promise;
|
|
24
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
25
|
+
import com.facebook.react.bridge.ReactContext;
|
|
26
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
27
|
+
import com.facebook.react.bridge.ReactMethod;
|
|
28
|
+
import com.facebook.react.bridge.ReadableMap;
|
|
29
|
+
import com.facebook.react.bridge.WritableMap;
|
|
30
|
+
import com.facebook.react.module.annotations.ReactModule;
|
|
31
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
|
32
|
+
import com.shakebug.interfaces.VideoTrimListener;
|
|
33
|
+
import com.shakebug.utils.StorageUtil;
|
|
34
|
+
import com.shakebug.widgets.VideoTrimmerView;
|
|
35
|
+
|
|
36
|
+
import java.io.IOException;
|
|
37
|
+
|
|
38
|
+
@ReactModule(name = VideoTrimModule.NAME)
|
|
39
|
+
public class VideoTrimModule extends ReactContextBaseJavaModule implements VideoTrimListener, LifecycleEventListener {
|
|
40
|
+
public static final String NAME = "ShakebugSDKVideoTrim";
|
|
41
|
+
private static Boolean isInit = false;
|
|
42
|
+
private VideoTrimmerView trimmerView;
|
|
43
|
+
private AlertDialog alertDialog;
|
|
44
|
+
private AlertDialog mProgressDialog;
|
|
45
|
+
private ProgressBar mProgressBar;
|
|
46
|
+
private int listenerCount = 0;
|
|
47
|
+
|
|
48
|
+
private Promise showEditorPromise;
|
|
49
|
+
|
|
50
|
+
private boolean enableCancelDialog = true;
|
|
51
|
+
private String cancelDialogTitle = "Warning!";
|
|
52
|
+
private String cancelDialogMessage = "Are you sure want to cancel?";
|
|
53
|
+
private String cancelDialogCancelText = "Close";
|
|
54
|
+
private String cancelDialogConfirmText = "Proceed";
|
|
55
|
+
private boolean enableSaveDialog = true;
|
|
56
|
+
private String saveDialogTitle = "Confirmation!";
|
|
57
|
+
private String saveDialogMessage = "Are you sure want to save?";
|
|
58
|
+
private String saveDialogCancelText = "Close";
|
|
59
|
+
private String saveDialogConfirmText = "Proceed";
|
|
60
|
+
private String trimmingText = "Trimming video...";
|
|
61
|
+
|
|
62
|
+
public VideoTrimModule(ReactApplicationContext reactContext) {
|
|
63
|
+
super(reactContext);
|
|
64
|
+
BaseUtils.init(reactContext);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@Override
|
|
68
|
+
@NonNull
|
|
69
|
+
public String getName() {
|
|
70
|
+
return NAME;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@ReactMethod
|
|
74
|
+
public void showEditor(String videoPath, ReadableMap config, Promise promise) {
|
|
75
|
+
showEditorPromise = promise;
|
|
76
|
+
if (trimmerView != null || alertDialog != null) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!isValidVideo(videoPath)) {
|
|
81
|
+
WritableMap map = Arguments.createMap();
|
|
82
|
+
map.putString("message", "File is not a valid video");
|
|
83
|
+
sendEvent(getReactApplicationContext(), "onError", map);
|
|
84
|
+
if (promise != null) {
|
|
85
|
+
promise.reject("INVALID_VIDEO", "File is not a valid video");
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
enableCancelDialog = config.hasKey("enableCancelDialog") ? config.getBoolean("enableCancelDialog") : true;
|
|
91
|
+
cancelDialogTitle = config.hasKey("cancelDialogTitle") ? config.getString("cancelDialogTitle") : "Warning!";
|
|
92
|
+
cancelDialogMessage = config.hasKey("cancelDialogMessage") ? config.getString("cancelDialogMessage") : "Are you sure want to cancel?";
|
|
93
|
+
cancelDialogCancelText = config.hasKey("cancelDialogCancelText") ? config.getString("cancelDialogCancelText") : "Close";
|
|
94
|
+
cancelDialogConfirmText = config.hasKey("cancelDialogConfirmText") ? config.getString("cancelDialogConfirmText") : "Proceed";
|
|
95
|
+
|
|
96
|
+
enableSaveDialog = config.hasKey("enableSaveDialog") ? config.getBoolean("enableSaveDialog") : true;
|
|
97
|
+
saveDialogTitle = config.hasKey("saveDialogTitle") ? config.getString("saveDialogTitle") : "Confirmation!";
|
|
98
|
+
saveDialogMessage = config.hasKey("saveDialogMessage") ? config.getString("saveDialogMessage") : "Are you sure want to save?";
|
|
99
|
+
saveDialogCancelText = config.hasKey("saveDialogCancelText") ? config.getString("saveDialogCancelText") : "Close";
|
|
100
|
+
saveDialogConfirmText = config.hasKey("saveDialogConfirmText") ? config.getString("saveDialogConfirmText") : "Proceed";
|
|
101
|
+
trimmingText = config.hasKey("trimmingText") ? config.getString("trimmingText") : "Trimming video...";
|
|
102
|
+
|
|
103
|
+
BaseUtils.init(getReactApplicationContext());
|
|
104
|
+
|
|
105
|
+
runOnUiThread(() -> {
|
|
106
|
+
try {
|
|
107
|
+
Activity currentAct = getReactApplicationContext().getCurrentActivity();
|
|
108
|
+
if (currentAct == null || currentAct.isFinishing()) {
|
|
109
|
+
if (showEditorPromise != null) {
|
|
110
|
+
showEditorPromise.reject("NO_ACTIVITY", "Current activity is not available");
|
|
111
|
+
showEditorPromise = null;
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
trimmerView = new VideoTrimmerView(currentAct, config, null);
|
|
117
|
+
trimmerView.setOnTrimVideoListener(this);
|
|
118
|
+
|
|
119
|
+
String cleanPath = videoPath.replace("file://", "");
|
|
120
|
+
Uri uri = cleanPath.startsWith("/") ? Uri.fromFile(new java.io.File(cleanPath)) : Uri.parse(videoPath);
|
|
121
|
+
trimmerView.initVideoByURI(uri);
|
|
122
|
+
|
|
123
|
+
AlertDialog.Builder builder = new AlertDialog.Builder(currentAct, android.R.style.Theme_Black_NoTitleBar_Fullscreen);
|
|
124
|
+
builder.setCancelable(false);
|
|
125
|
+
alertDialog = builder.create();
|
|
126
|
+
alertDialog.setView(trimmerView);
|
|
127
|
+
alertDialog.show();
|
|
128
|
+
|
|
129
|
+
alertDialog.setOnDismissListener(dialog -> {
|
|
130
|
+
if (trimmerView != null) {
|
|
131
|
+
trimmerView.onDestroy();
|
|
132
|
+
trimmerView = null;
|
|
133
|
+
}
|
|
134
|
+
hideDialog();
|
|
135
|
+
sendEvent(getReactApplicationContext(), "onHide", null);
|
|
136
|
+
});
|
|
137
|
+
sendEvent(getReactApplicationContext(), "onShow", null);
|
|
138
|
+
} catch (Exception e) {
|
|
139
|
+
e.printStackTrace();
|
|
140
|
+
onError(e.getMessage() != null ? e.getMessage() : "Error displaying video trimmer");
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@Override
|
|
146
|
+
public void onHostResume() {
|
|
147
|
+
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
@Override
|
|
151
|
+
public void onHostPause() {
|
|
152
|
+
if (trimmerView != null) {
|
|
153
|
+
trimmerView.onVideoPause();
|
|
154
|
+
trimmerView.setRestoreState(true);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@Override
|
|
159
|
+
public void onHostDestroy() {
|
|
160
|
+
hideDialog();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@Override public void onStartTrim() {
|
|
164
|
+
sendEvent(getReactApplicationContext(), "onStartTrimming", null);
|
|
165
|
+
runOnUiThread(() -> {
|
|
166
|
+
buildDialog();
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@Override public void onTrimmingProgress(int percentage) {
|
|
171
|
+
if (mProgressBar != null) {
|
|
172
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
173
|
+
mProgressBar.setProgress(percentage, true);
|
|
174
|
+
} else {
|
|
175
|
+
mProgressBar.setProgress(percentage);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@Override public void onFinishTrim(String in) {
|
|
181
|
+
runOnUiThread(() -> {
|
|
182
|
+
hideDialog();
|
|
183
|
+
WritableMap map = Arguments.createMap();
|
|
184
|
+
map.putString("outputPath", in);
|
|
185
|
+
sendEvent(getReactApplicationContext(), "onFinishTrimming", map);
|
|
186
|
+
if (showEditorPromise != null) {
|
|
187
|
+
showEditorPromise.resolve(in);
|
|
188
|
+
showEditorPromise = null;
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@Override public void onError(String errorMessage) {
|
|
194
|
+
runOnUiThread(() -> {
|
|
195
|
+
this.hideDialog();
|
|
196
|
+
WritableMap map = Arguments.createMap();
|
|
197
|
+
map.putString("message", errorMessage);
|
|
198
|
+
sendEvent(getReactApplicationContext(), "onError", map);
|
|
199
|
+
if (showEditorPromise != null) {
|
|
200
|
+
showEditorPromise.reject("TRIM_ERROR", errorMessage);
|
|
201
|
+
showEditorPromise = null;
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@Override public void onCancel() {
|
|
207
|
+
if (!enableCancelDialog) {
|
|
208
|
+
sendEvent(getReactApplicationContext(), "onCancel", null);
|
|
209
|
+
if (showEditorPromise != null) {
|
|
210
|
+
showEditorPromise.resolve(null);
|
|
211
|
+
showEditorPromise = null;
|
|
212
|
+
}
|
|
213
|
+
hideDialog();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
Activity activity = getReactApplicationContext().getCurrentActivity();
|
|
218
|
+
if (activity == null) return;
|
|
219
|
+
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
|
220
|
+
builder.setMessage(cancelDialogMessage);
|
|
221
|
+
builder.setTitle(cancelDialogTitle);
|
|
222
|
+
builder.setCancelable(false);
|
|
223
|
+
builder.setPositiveButton(cancelDialogConfirmText, (dialog, which) -> {
|
|
224
|
+
dialog.cancel();
|
|
225
|
+
sendEvent(getReactApplicationContext(), "onCancel", null);
|
|
226
|
+
if (showEditorPromise != null) {
|
|
227
|
+
showEditorPromise.resolve(null);
|
|
228
|
+
showEditorPromise = null;
|
|
229
|
+
}
|
|
230
|
+
hideDialog();
|
|
231
|
+
});
|
|
232
|
+
builder.setNegativeButton(cancelDialogCancelText, (dialog, which) -> {
|
|
233
|
+
dialog.cancel();
|
|
234
|
+
});
|
|
235
|
+
AlertDialog d = builder.create();
|
|
236
|
+
d.show();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
@Override public void onSave() {
|
|
240
|
+
if (!enableSaveDialog) {
|
|
241
|
+
if (trimmerView != null) trimmerView.onSaveClicked();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
Activity activity = getReactApplicationContext().getCurrentActivity();
|
|
246
|
+
if (activity == null) return;
|
|
247
|
+
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
|
248
|
+
builder.setMessage(saveDialogMessage);
|
|
249
|
+
builder.setTitle(saveDialogTitle);
|
|
250
|
+
builder.setCancelable(false);
|
|
251
|
+
builder.setPositiveButton(saveDialogConfirmText, (dialog, which) -> {
|
|
252
|
+
dialog.cancel();
|
|
253
|
+
if (trimmerView != null) trimmerView.onSaveClicked();
|
|
254
|
+
});
|
|
255
|
+
builder.setNegativeButton(saveDialogCancelText, (dialog, which) -> {
|
|
256
|
+
dialog.cancel();
|
|
257
|
+
});
|
|
258
|
+
AlertDialog d = builder.create();
|
|
259
|
+
d.show();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
@ReactMethod
|
|
263
|
+
public void hideDialog() {
|
|
264
|
+
if (mProgressDialog != null) {
|
|
265
|
+
if (mProgressDialog.isShowing()) mProgressDialog.dismiss();
|
|
266
|
+
mProgressBar = null;
|
|
267
|
+
mProgressDialog = null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (alertDialog != null) {
|
|
271
|
+
if (alertDialog.isShowing()) {
|
|
272
|
+
alertDialog.dismiss();
|
|
273
|
+
}
|
|
274
|
+
alertDialog = null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private void buildDialog() {
|
|
279
|
+
Activity activity = getReactApplicationContext().getCurrentActivity();
|
|
280
|
+
if (activity == null) return;
|
|
281
|
+
LinearLayout layout = new LinearLayout(activity);
|
|
282
|
+
layout.setLayoutParams(new ViewGroup.LayoutParams(
|
|
283
|
+
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
284
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
285
|
+
));
|
|
286
|
+
layout.setOrientation(LinearLayout.VERTICAL);
|
|
287
|
+
layout.setGravity(Gravity.CENTER_HORIZONTAL);
|
|
288
|
+
layout.setPadding(16, 32, 16, 32);
|
|
289
|
+
|
|
290
|
+
TextView textView = new TextView(activity);
|
|
291
|
+
textView.setLayoutParams(new ViewGroup.LayoutParams(
|
|
292
|
+
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
293
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
294
|
+
));
|
|
295
|
+
textView.setText(trimmingText);
|
|
296
|
+
textView.setTextSize(18);
|
|
297
|
+
layout.addView(textView);
|
|
298
|
+
|
|
299
|
+
mProgressBar = new ProgressBar(activity, null, android.R.attr.progressBarStyleHorizontal);
|
|
300
|
+
mProgressBar.setLayoutParams(new ViewGroup.LayoutParams(
|
|
301
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
302
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
303
|
+
));
|
|
304
|
+
mProgressBar.setProgressTintList(ColorStateList.valueOf(Color.parseColor("#2196F3")));
|
|
305
|
+
layout.addView(mProgressBar);
|
|
306
|
+
|
|
307
|
+
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
|
308
|
+
builder.setCancelable(false);
|
|
309
|
+
builder.setView(layout);
|
|
310
|
+
|
|
311
|
+
mProgressDialog = builder.create();
|
|
312
|
+
mProgressDialog.show();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
@ReactMethod
|
|
316
|
+
public void addListener(String eventName) {
|
|
317
|
+
listenerCount += 1;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
@ReactMethod
|
|
321
|
+
public void removeListeners(Integer count) {
|
|
322
|
+
listenerCount -= count;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private void sendEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params) {
|
|
326
|
+
WritableMap map = params != null ? params : Arguments.createMap();
|
|
327
|
+
map.putString("name", eventName);
|
|
328
|
+
reactContext
|
|
329
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
|
330
|
+
.emit("VideoTrim", map);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
public boolean isValidVideo(String filePath) {
|
|
334
|
+
if (filePath == null || filePath.isEmpty()) return false;
|
|
335
|
+
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
|
336
|
+
try {
|
|
337
|
+
retriever.setDataSource(getReactApplicationContext(), Uri.parse(filePath));
|
|
338
|
+
} catch (Exception e) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
String hasVideo = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO);
|
|
342
|
+
return "yes".equals(hasVideo);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
@ReactMethod
|
|
346
|
+
public void isValidVideo(String filePath, Promise promise) {
|
|
347
|
+
promise.resolve(isValidVideo(filePath));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
@ReactMethod
|
|
351
|
+
public void saveVideo(String filePath, Promise promise) {
|
|
352
|
+
try {
|
|
353
|
+
StorageUtil.saveVideoToGallery(getReactApplicationContext(), filePath);
|
|
354
|
+
} catch (IOException e) {
|
|
355
|
+
WritableMap mapE = Arguments.createMap();
|
|
356
|
+
mapE.putString("message", "Fail while copying file to Gallery");
|
|
357
|
+
sendEvent(getReactApplicationContext(), "onError", mapE);
|
|
358
|
+
}
|
|
359
|
+
this.hideDialog();
|
|
360
|
+
promise.resolve(null);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
@ReactMethod
|
|
364
|
+
public void listFiles(Promise promise) {
|
|
365
|
+
String[] files = StorageUtil.listFiles(getReactApplicationContext());
|
|
366
|
+
promise.resolve(Arguments.fromArray(files));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
@ReactMethod
|
|
370
|
+
public void cleanFiles(Promise promise) {
|
|
371
|
+
String[] files = StorageUtil.listFiles(getReactApplicationContext());
|
|
372
|
+
int successCount = 0;
|
|
373
|
+
for (String file : files) {
|
|
374
|
+
boolean state = StorageUtil.deleteFile(file);
|
|
375
|
+
if (state) {
|
|
376
|
+
successCount++;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
promise.resolve(successCount);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
@ReactMethod
|
|
383
|
+
public void deleteFile(String filePath, Promise promise) {
|
|
384
|
+
boolean state = StorageUtil.deleteFile(filePath);
|
|
385
|
+
promise.resolve(state);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
package com.shakebug;
|
|
2
|
+
|
|
3
|
+
import android.media.MediaCodec;
|
|
4
|
+
import android.media.MediaExtractor;
|
|
5
|
+
import android.media.MediaFormat;
|
|
6
|
+
import android.media.MediaMetadataRetriever;
|
|
7
|
+
import android.media.MediaMuxer;
|
|
8
|
+
import android.util.Log;
|
|
9
|
+
|
|
10
|
+
import java.io.IOException;
|
|
11
|
+
import java.nio.ByteBuffer;
|
|
12
|
+
import java.util.HashMap;
|
|
13
|
+
|
|
14
|
+
public class VideoTrimUtil {
|
|
15
|
+
private static final String TAG = "VideoTrimUtil";
|
|
16
|
+
private static final int DEFAULT_BUFFER_SIZE = 1024 * 1024;
|
|
17
|
+
|
|
18
|
+
public static long getVideoDurationMs(String sourcePath) {
|
|
19
|
+
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
|
20
|
+
try {
|
|
21
|
+
retriever.setDataSource(sourcePath);
|
|
22
|
+
String duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
|
|
23
|
+
return duration != null ? Long.parseLong(duration) : 0;
|
|
24
|
+
} catch (Exception e) {
|
|
25
|
+
Log.e(TAG, "Error reading video duration", e);
|
|
26
|
+
return 0;
|
|
27
|
+
} finally {
|
|
28
|
+
try {
|
|
29
|
+
retriever.release();
|
|
30
|
+
} catch (Exception ignored) {}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public static void trimVideo(String srcPath, String destPath, long startMs, long endMs) throws IOException {
|
|
35
|
+
MediaExtractor extractor = new MediaExtractor();
|
|
36
|
+
MediaMuxer muxer = null;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
extractor.setDataSource(srcPath);
|
|
40
|
+
int trackCount = extractor.getTrackCount();
|
|
41
|
+
|
|
42
|
+
muxer = new MediaMuxer(destPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
|
|
43
|
+
HashMap<Integer, Integer> indexMap = new HashMap<>(trackCount);
|
|
44
|
+
|
|
45
|
+
// Preserve video orientation if available
|
|
46
|
+
try {
|
|
47
|
+
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
|
48
|
+
retriever.setDataSource(srcPath);
|
|
49
|
+
String rotationStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
|
|
50
|
+
if (rotationStr != null) {
|
|
51
|
+
int rotation = Integer.parseInt(rotationStr);
|
|
52
|
+
muxer.setOrientationHint(rotation);
|
|
53
|
+
}
|
|
54
|
+
retriever.release();
|
|
55
|
+
} catch (Exception ignored) {}
|
|
56
|
+
|
|
57
|
+
int bufferSize = -1;
|
|
58
|
+
for (int i = 0; i < trackCount; i++) {
|
|
59
|
+
MediaFormat format = extractor.getTrackFormat(i);
|
|
60
|
+
String mime = format.getString(MediaFormat.KEY_MIME);
|
|
61
|
+
if (mime != null && (mime.startsWith("video/") || mime.startsWith("audio/"))) {
|
|
62
|
+
int dstIndex = muxer.addTrack(format);
|
|
63
|
+
indexMap.put(i, dstIndex);
|
|
64
|
+
if (format.containsKey(MediaFormat.KEY_MAX_INPUT_SIZE)) {
|
|
65
|
+
int newSize = format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);
|
|
66
|
+
bufferSize = Math.max(newSize, bufferSize);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (bufferSize < 0) {
|
|
72
|
+
bufferSize = DEFAULT_BUFFER_SIZE;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
muxer.start();
|
|
76
|
+
|
|
77
|
+
long startUs = startMs * 1000L;
|
|
78
|
+
long endUs = endMs * 1000L;
|
|
79
|
+
|
|
80
|
+
ByteBuffer dstBuf = ByteBuffer.allocate(bufferSize);
|
|
81
|
+
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
|
82
|
+
|
|
83
|
+
// Extract each track independently
|
|
84
|
+
for (int srcTrackIndex : indexMap.keySet()) {
|
|
85
|
+
// Select only the current track
|
|
86
|
+
for (int i = 0; i < trackCount; i++) {
|
|
87
|
+
extractor.unselectTrack(i);
|
|
88
|
+
}
|
|
89
|
+
extractor.selectTrack(srcTrackIndex);
|
|
90
|
+
extractor.seekTo(startUs, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
|
|
91
|
+
|
|
92
|
+
int dstTrackIndex = indexMap.get(srcTrackIndex);
|
|
93
|
+
|
|
94
|
+
while (true) {
|
|
95
|
+
bufferInfo.offset = 0;
|
|
96
|
+
bufferInfo.size = extractor.readSampleData(dstBuf, 0);
|
|
97
|
+
|
|
98
|
+
if (bufferInfo.size < 0) {
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
long presentationTimeUs = extractor.getSampleTime();
|
|
103
|
+
if (presentationTimeUs > endUs) {
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (presentationTimeUs >= startUs) {
|
|
108
|
+
bufferInfo.presentationTimeUs = presentationTimeUs - startUs;
|
|
109
|
+
bufferInfo.flags = extractor.getSampleFlags();
|
|
110
|
+
muxer.writeSampleData(dstTrackIndex, dstBuf, bufferInfo);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!extractor.advance()) {
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
} finally {
|
|
120
|
+
try {
|
|
121
|
+
if (muxer != null) {
|
|
122
|
+
muxer.stop();
|
|
123
|
+
muxer.release();
|
|
124
|
+
}
|
|
125
|
+
} catch (Exception e) {
|
|
126
|
+
Log.e(TAG, "Error releasing muxer", e);
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
extractor.release();
|
|
130
|
+
} catch (Exception ignored) {}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -351,10 +351,13 @@ public class ViewShot implements UIBlock {
|
|
|
351
351
|
|
|
352
352
|
// evaluate real height
|
|
353
353
|
if (snapshotContentContainer) {
|
|
354
|
-
|
|
354
|
+
int evaluatedHeight = 0;
|
|
355
355
|
ScrollView scrollView = (ScrollView) view;
|
|
356
356
|
for (int i = 0; i < scrollView.getChildCount(); i++) {
|
|
357
|
-
|
|
357
|
+
evaluatedHeight += scrollView.getChildAt(i).getHeight();
|
|
358
|
+
}
|
|
359
|
+
if (evaluatedHeight > 0) {
|
|
360
|
+
h = evaluatedHeight;
|
|
358
361
|
}
|
|
359
362
|
}
|
|
360
363
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
package com.shakebug.adapters;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.graphics.Bitmap;
|
|
5
|
+
import android.view.LayoutInflater;
|
|
6
|
+
import android.view.View;
|
|
7
|
+
import android.view.ViewGroup;
|
|
8
|
+
import android.widget.ImageView;
|
|
9
|
+
import android.widget.LinearLayout;
|
|
10
|
+
import androidx.annotation.NonNull;
|
|
11
|
+
import androidx.recyclerview.widget.RecyclerView;
|
|
12
|
+
import com.shakebug.R;
|
|
13
|
+
import com.shakebug.utils.VideoTrimmerUtil;
|
|
14
|
+
import java.util.ArrayList;
|
|
15
|
+
import java.util.List;
|
|
16
|
+
|
|
17
|
+
public class VideoTrimmerAdapter extends RecyclerView.Adapter {
|
|
18
|
+
private final List<Bitmap> mBitmaps = new ArrayList<>();
|
|
19
|
+
private final LayoutInflater mInflater;
|
|
20
|
+
|
|
21
|
+
public VideoTrimmerAdapter(Context context) {
|
|
22
|
+
this.mInflater = LayoutInflater.from(context);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
26
|
+
return new TrimmerViewHolder(mInflater.inflate(R.layout.shakebug_video_thumb_item_layout, parent, false));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
|
30
|
+
((TrimmerViewHolder) holder).thumbImageView.setImageBitmap(mBitmaps.get(position));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@Override public int getItemCount() {
|
|
34
|
+
return mBitmaps.size();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public void addBitmaps(Bitmap bitmap) {
|
|
38
|
+
mBitmaps.add(bitmap);
|
|
39
|
+
notifyItemInserted(mBitmaps.size() - 1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private static final class TrimmerViewHolder extends RecyclerView.ViewHolder {
|
|
43
|
+
ImageView thumbImageView;
|
|
44
|
+
|
|
45
|
+
TrimmerViewHolder(View itemView) {
|
|
46
|
+
super(itemView);
|
|
47
|
+
thumbImageView = itemView.findViewById(R.id.thumb);
|
|
48
|
+
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) thumbImageView.getLayoutParams();
|
|
49
|
+
layoutParams.width = VideoTrimmerUtil.VIDEO_FRAMES_WIDTH / VideoTrimmerUtil.MAX_COUNT_RANGE;
|
|
50
|
+
thumbImageView.setLayoutParams(layoutParams);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|