react-native-video-trim 4.0.0 → 5.0.0
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/LICENSE +1 -1
- package/README.md +124 -83
- package/VideoTrim.podspec +4 -4
- package/android/build.gradle +15 -54
- package/android/gradle.properties +2 -0
- package/android/src/main/AndroidManifest.xml +1 -1
- package/android/src/main/java/com/videotrim/VideoTrimModule.kt +660 -0
- package/android/src/main/java/com/videotrim/VideoTrimPackage.kt +33 -0
- package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/enums/ErrorCode.java +1 -1
- package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/interfaces/IVideoTrimmerView.java +1 -1
- package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/interfaces/VideoTrimListener.java +5 -4
- package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/utils/MediaMetadataUtil.java +1 -1
- package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/utils/StorageUtil.java +1 -1
- package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/utils/VideoTrimmerUtil.java +49 -39
- package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/widgets/VideoTrimmerView.java +49 -46
- package/ios/AssetLoader.h +19 -0
- package/ios/AssetLoader.mm +87 -0
- package/ios/ErrorCode.h +9 -0
- package/ios/ProgressAlertController.h +15 -0
- package/ios/ProgressAlertController.mm +78 -0
- package/ios/VideoTrim.h +31 -0
- package/ios/VideoTrim.mm +663 -0
- package/ios/VideoTrimmer.h +67 -0
- package/ios/VideoTrimmer.mm +863 -0
- package/ios/VideoTrimmerThumb.h +23 -0
- package/ios/VideoTrimmerThumb.mm +175 -0
- package/ios/VideoTrimmerViewController.h +52 -0
- package/ios/VideoTrimmerViewController.mm +533 -0
- package/lib/module/NativeVideoTrim.js +5 -0
- package/lib/module/NativeVideoTrim.js.map +1 -0
- package/lib/module/index.js +59 -34
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/NativeVideoTrim.d.ts +107 -0
- package/lib/typescript/src/NativeVideoTrim.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +21 -10
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +15 -18
- package/src/NativeVideoTrim.ts +113 -0
- package/src/index.tsx +68 -39
- package/android/CMakeLists.txt +0 -24
- package/android/src/main/cpp/cpp-adapter.cpp +0 -6
- package/android/src/main/java/com/margelo/nitro/videotrim/VideoTrim.kt +0 -629
- package/android/src/main/java/com/margelo/nitro/videotrim/VideoTrimPackage.kt +0 -22
- package/ios/AssetLoader.swift +0 -99
- package/ios/ErrorCode.swift +0 -17
- package/ios/ProgressAlertController.swift +0 -100
- package/ios/VideoTrim.swift +0 -60
- package/ios/VideoTrimImpl.swift +0 -860
- package/ios/VideoTrimmer.swift +0 -872
- package/ios/VideoTrimmerThumb.swift +0 -175
- package/ios/VideoTrimmerViewController.swift +0 -557
- package/lib/module/VideoTrim.nitro.js +0 -4
- package/lib/module/VideoTrim.nitro.js.map +0 -1
- package/lib/typescript/src/VideoTrim.nitro.d.ts +0 -240
- package/lib/typescript/src/VideoTrim.nitro.d.ts.map +0 -1
- package/nitrogen/generated/android/c++/JEditorConfig.hpp +0 -229
- package/nitrogen/generated/android/c++/JFileValidationResult.hpp +0 -61
- package/nitrogen/generated/android/c++/JFunc_void.hpp +0 -74
- package/nitrogen/generated/android/c++/JFunc_void_std__string_std__unordered_map_std__string__std__string_.hpp +0 -89
- package/nitrogen/generated/android/c++/JHybridVideoTrimSpec.cpp +0 -131
- package/nitrogen/generated/android/c++/JHybridVideoTrimSpec.hpp +0 -67
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/EditorConfig.kt +0 -70
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/FileValidationResult.kt +0 -28
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/Func_void.kt +0 -80
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/Func_void_std__string_std__unordered_map_std__string__std__string_.kt +0 -80
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/HybridVideoTrimSpec.kt +0 -82
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/videotrimOnLoad.kt +0 -35
- package/nitrogen/generated/android/videotrim+autolinking.cmake +0 -78
- package/nitrogen/generated/android/videotrim+autolinking.gradle +0 -27
- package/nitrogen/generated/android/videotrimOnLoad.cpp +0 -50
- package/nitrogen/generated/android/videotrimOnLoad.hpp +0 -25
- package/nitrogen/generated/ios/VideoTrim+autolinking.rb +0 -60
- package/nitrogen/generated/ios/VideoTrim-Swift-Cxx-Bridge.cpp +0 -88
- package/nitrogen/generated/ios/VideoTrim-Swift-Cxx-Bridge.hpp +0 -331
- package/nitrogen/generated/ios/VideoTrim-Swift-Cxx-Umbrella.hpp +0 -53
- package/nitrogen/generated/ios/VideoTrimAutolinking.mm +0 -33
- package/nitrogen/generated/ios/VideoTrimAutolinking.swift +0 -25
- package/nitrogen/generated/ios/c++/HybridVideoTrimSpecSwift.cpp +0 -11
- package/nitrogen/generated/ios/c++/HybridVideoTrimSpecSwift.hpp +0 -116
- package/nitrogen/generated/ios/swift/EditorConfig.swift +0 -519
- package/nitrogen/generated/ios/swift/FileValidationResult.swift +0 -57
- package/nitrogen/generated/ios/swift/Func_void.swift +0 -46
- package/nitrogen/generated/ios/swift/Func_void_FileValidationResult.swift +0 -46
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +0 -46
- package/nitrogen/generated/ios/swift/Func_void_double.swift +0 -46
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +0 -46
- package/nitrogen/generated/ios/swift/Func_void_std__string_std__unordered_map_std__string__std__string_.swift +0 -54
- package/nitrogen/generated/ios/swift/Func_void_std__vector_std__string_.swift +0 -46
- package/nitrogen/generated/ios/swift/HybridVideoTrimSpec.swift +0 -53
- package/nitrogen/generated/ios/swift/HybridVideoTrimSpec_cxx.swift +0 -222
- package/nitrogen/generated/shared/c++/EditorConfig.hpp +0 -245
- package/nitrogen/generated/shared/c++/FileValidationResult.hpp +0 -77
- package/nitrogen/generated/shared/c++/HybridVideoTrimSpec.cpp +0 -26
- package/nitrogen/generated/shared/c++/HybridVideoTrimSpec.hpp +0 -76
- package/src/VideoTrim.nitro.ts +0 -244
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
package com.videotrim
|
|
2
|
+
|
|
3
|
+
import android.R.attr.progressBarStyleHorizontal
|
|
4
|
+
import android.R.attr.selectableItemBackground
|
|
5
|
+
import android.R.color.holo_red_light
|
|
6
|
+
import android.R.style.Theme_Black_NoTitleBar_Fullscreen
|
|
7
|
+
import android.app.Activity
|
|
8
|
+
import android.content.Context
|
|
9
|
+
import android.content.DialogInterface
|
|
10
|
+
import android.content.Intent
|
|
11
|
+
import android.content.pm.PackageManager
|
|
12
|
+
import android.content.res.ColorStateList
|
|
13
|
+
import android.os.Build
|
|
14
|
+
import android.util.Log
|
|
15
|
+
import android.util.TypedValue
|
|
16
|
+
import android.view.Gravity
|
|
17
|
+
import android.view.View
|
|
18
|
+
import android.view.ViewGroup
|
|
19
|
+
import android.widget.Button
|
|
20
|
+
import android.widget.LinearLayout
|
|
21
|
+
import android.widget.ProgressBar
|
|
22
|
+
import android.widget.TextView
|
|
23
|
+
import androidx.appcompat.app.AlertDialog
|
|
24
|
+
import androidx.core.content.ContextCompat
|
|
25
|
+
import androidx.core.content.FileProvider
|
|
26
|
+
import androidx.core.graphics.toColorInt
|
|
27
|
+
import androidx.core.net.toUri
|
|
28
|
+
import androidx.core.view.ViewCompat
|
|
29
|
+
import androidx.core.view.WindowInsetsCompat
|
|
30
|
+
import com.arthenica.ffmpegkit.FFmpegKit
|
|
31
|
+
import com.arthenica.ffmpegkit.ReturnCode
|
|
32
|
+
import com.facebook.react.bridge.*
|
|
33
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
34
|
+
import com.videotrim.enums.ErrorCode
|
|
35
|
+
import com.videotrim.interfaces.VideoTrimListener
|
|
36
|
+
import com.videotrim.utils.MediaMetadataUtil
|
|
37
|
+
import com.videotrim.utils.StorageUtil
|
|
38
|
+
import com.videotrim.widgets.VideoTrimmerView
|
|
39
|
+
import iknow.android.utils.BaseUtils
|
|
40
|
+
import java.io.File
|
|
41
|
+
import java.io.FileInputStream
|
|
42
|
+
import java.io.IOException
|
|
43
|
+
import java.text.SimpleDateFormat
|
|
44
|
+
import java.util.Date
|
|
45
|
+
import java.util.TimeZone
|
|
46
|
+
|
|
47
|
+
@ReactModule(name = VideoTrimModule.NAME)
|
|
48
|
+
class VideoTrimModule(reactContext: ReactApplicationContext) :
|
|
49
|
+
NativeVideoTrimSpec(reactContext), VideoTrimListener, LifecycleEventListener {
|
|
50
|
+
|
|
51
|
+
private var isInit: Boolean = false
|
|
52
|
+
private var trimmerView: VideoTrimmerView? = null
|
|
53
|
+
private var alertDialog: AlertDialog? = null
|
|
54
|
+
private var mProgressDialog: AlertDialog? = null
|
|
55
|
+
private var cancelTrimmingConfirmDialog: AlertDialog? = null
|
|
56
|
+
private var mProgressBar: ProgressBar? = null
|
|
57
|
+
private var outputFile: String? = null
|
|
58
|
+
private var isVideoType = true
|
|
59
|
+
private var editorConfig: ReadableMap? = null
|
|
60
|
+
private var trimOptions: ReadableMap? = null
|
|
61
|
+
|
|
62
|
+
init {
|
|
63
|
+
val mActivityEventListener = object : BaseActivityEventListener() {
|
|
64
|
+
override fun onActivityResult(
|
|
65
|
+
activity: Activity,
|
|
66
|
+
requestCode: Int,
|
|
67
|
+
resultCode: Int,
|
|
68
|
+
intent: Intent?
|
|
69
|
+
) {
|
|
70
|
+
if (requestCode == REQUEST_CODE_SAVE_FILE && resultCode == Activity.RESULT_OK) {
|
|
71
|
+
|
|
72
|
+
val uri = intent?.data ?: return
|
|
73
|
+
try {
|
|
74
|
+
reactApplicationContext.contentResolver?.openOutputStream(uri)
|
|
75
|
+
?.use { outputStream ->
|
|
76
|
+
FileInputStream(outputFile).use { fileInputStream ->
|
|
77
|
+
val buffer = ByteArray(1024)
|
|
78
|
+
var length: Int
|
|
79
|
+
while (fileInputStream.read(buffer).also { length = it } > 0) {
|
|
80
|
+
outputStream.write(buffer, 0, length)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} ?: return
|
|
84
|
+
// File saved successfully
|
|
85
|
+
Log.d(TAG, "File saved successfully to $uri")
|
|
86
|
+
|
|
87
|
+
if (
|
|
88
|
+
editorConfig?.getBoolean("removeAfterSavedToDocuments") == true ||
|
|
89
|
+
trimOptions?.getBoolean("removeAfterFailedToSaveDocuments") == true
|
|
90
|
+
) {
|
|
91
|
+
StorageUtil.deleteFile(outputFile)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
} catch (e: Exception) {
|
|
95
|
+
e.printStackTrace()
|
|
96
|
+
// Handle the error
|
|
97
|
+
onError(
|
|
98
|
+
"Failed to save edited video to Documents: ${e.localizedMessage}",
|
|
99
|
+
ErrorCode.FAIL_TO_SAVE_TO_DOCUMENTS
|
|
100
|
+
)
|
|
101
|
+
if (editorConfig?.getBoolean("removeAfterFailedToSaveDocuments") == true || trimOptions?.getBoolean("removeAfterFailedToSaveDocuments") == true) {
|
|
102
|
+
StorageUtil.deleteFile(outputFile)
|
|
103
|
+
}
|
|
104
|
+
} finally {
|
|
105
|
+
hideDialog(true)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
reactApplicationContext.addActivityEventListener(mActivityEventListener)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
override fun showEditor(
|
|
114
|
+
filePath: String,
|
|
115
|
+
config: ReadableMap,
|
|
116
|
+
) {
|
|
117
|
+
if (trimmerView != null || alertDialog != null) {
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.editorConfig = config
|
|
122
|
+
|
|
123
|
+
this.isVideoType = config.hasKey("type") && config.getString("type") == "video"
|
|
124
|
+
|
|
125
|
+
val activity = reactApplicationContext.currentActivity
|
|
126
|
+
if (!isInit) {
|
|
127
|
+
init()
|
|
128
|
+
isInit = true
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// here is NOT main thread, we need to create VideoTrimmerView on UI thread, so that later we can update it using same thread
|
|
132
|
+
UiThreadUtil.runOnUiThread {
|
|
133
|
+
trimmerView = VideoTrimmerView(reactApplicationContext, editorConfig, null)
|
|
134
|
+
trimmerView?.setOnTrimVideoListener(this)
|
|
135
|
+
trimmerView?.initByURI(filePath.toUri())
|
|
136
|
+
|
|
137
|
+
val builder = AlertDialog.Builder(
|
|
138
|
+
activity!!, Theme_Black_NoTitleBar_Fullscreen
|
|
139
|
+
)
|
|
140
|
+
builder.setCancelable(false)
|
|
141
|
+
alertDialog = builder.create()
|
|
142
|
+
alertDialog?.setView(trimmerView)
|
|
143
|
+
|
|
144
|
+
// Apply safe area handling after the dialog is shown
|
|
145
|
+
alertDialog?.setOnShowListener {
|
|
146
|
+
applySafeAreaToDialog(alertDialog!!, trimmerView!!)
|
|
147
|
+
|
|
148
|
+
emitOnShow()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 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)
|
|
152
|
+
alertDialog!!.setOnDismissListener {
|
|
153
|
+
// This is called in same thread as the trimmer view -> UI thread
|
|
154
|
+
if (trimmerView != null) {
|
|
155
|
+
trimmerView!!.onDestroy()
|
|
156
|
+
trimmerView = null
|
|
157
|
+
}
|
|
158
|
+
hideDialog(true)
|
|
159
|
+
emitOnHide()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
alertDialog?.show()
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// because trimmerView is rendered within the dialog, we need to apply safe area insets to the dialog
|
|
167
|
+
// else setOnApplyWindowInsetsListener will not fire
|
|
168
|
+
private fun applySafeAreaToDialog(dialog: AlertDialog, trimmerView: VideoTrimmerView) {
|
|
169
|
+
val window = dialog.window
|
|
170
|
+
if (window != null) {
|
|
171
|
+
// Enable edge-to-edge for the dialog window
|
|
172
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
173
|
+
window.setDecorFitsSystemWindows(false)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Get the dialog's decorView and apply insets listener
|
|
177
|
+
val decorView = window.decorView
|
|
178
|
+
ViewCompat.setOnApplyWindowInsetsListener(decorView) { _, windowInsets ->
|
|
179
|
+
val insets = windowInsets.getInsets(
|
|
180
|
+
WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
|
|
181
|
+
)
|
|
182
|
+
Log.d(TAG, "Dialog insets: top=${insets.top}, left=${insets.left}, bottom=${insets.bottom}, right=${insets.right}")
|
|
183
|
+
|
|
184
|
+
// Apply padding to the trimmer view
|
|
185
|
+
trimmerView.setPadding(insets.left, insets.top, insets.right, insets.bottom)
|
|
186
|
+
|
|
187
|
+
WindowInsetsCompat.CONSUMED
|
|
188
|
+
}
|
|
189
|
+
ViewCompat.requestApplyInsets(decorView)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private fun init() {
|
|
194
|
+
isInit = true
|
|
195
|
+
// we have to init this before create videoTrimmerView
|
|
196
|
+
BaseUtils.init(reactApplicationContext)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
override fun onHostResume() {
|
|
200
|
+
Log.d(TAG, "onHostResume: ")
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
override fun onHostPause() {
|
|
204
|
+
Log.d(TAG, "onHostPause: ")
|
|
205
|
+
if (trimmerView != null) {
|
|
206
|
+
trimmerView!!.onMediaPause()
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
override fun onHostDestroy() {
|
|
211
|
+
hideDialog(true)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
override fun invalidate() {
|
|
215
|
+
super.invalidate()
|
|
216
|
+
hideDialog(true)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
override fun onLoad(duration: Int) {
|
|
220
|
+
val map = Arguments.createMap()
|
|
221
|
+
map.putInt("duration", duration)
|
|
222
|
+
emitOnLoad(map)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
override fun onTrimmingProgress(percentage: Int) {
|
|
226
|
+
// prevent onTrimmingProgress is called after onFinishTrim (some rare cases)
|
|
227
|
+
if (mProgressBar == null) {
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
mProgressBar!!.setProgress(percentage, true)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
override fun onFinishTrim(out: String, startTime: Long, endTime: Long, duration: Int) {
|
|
236
|
+
// save output file to use in other places
|
|
237
|
+
outputFile = out
|
|
238
|
+
|
|
239
|
+
val map = Arguments.createMap()
|
|
240
|
+
map.putString("outputPath", outputFile)
|
|
241
|
+
map.putInt("duration", duration)
|
|
242
|
+
map.putDouble("startTime", startTime.toDouble())
|
|
243
|
+
map.putDouble("endTime", endTime.toDouble())
|
|
244
|
+
emitOnFinishTrimming(map)
|
|
245
|
+
|
|
246
|
+
if (editorConfig?.getBoolean("saveToPhoto") == true && isVideoType) {
|
|
247
|
+
try {
|
|
248
|
+
StorageUtil.saveVideoToGallery(reactApplicationContext, outputFile)
|
|
249
|
+
Log.d(TAG, "Edited video saved to Photo Library successfully.")
|
|
250
|
+
if (editorConfig?.getBoolean("removeAfterSavedToPhoto") == true) {
|
|
251
|
+
StorageUtil.deleteFile(outputFile)
|
|
252
|
+
}
|
|
253
|
+
} catch (e: IOException) {
|
|
254
|
+
e.printStackTrace()
|
|
255
|
+
onError(
|
|
256
|
+
"Failed to save edited video to Photo Library: " + e.localizedMessage,
|
|
257
|
+
ErrorCode.FAIL_TO_SAVE_TO_PHOTO
|
|
258
|
+
)
|
|
259
|
+
if (editorConfig?.getBoolean("removeAfterFailedToSavePhoto") == true) {
|
|
260
|
+
StorageUtil.deleteFile(outputFile)
|
|
261
|
+
}
|
|
262
|
+
} finally {
|
|
263
|
+
hideDialog(editorConfig?.getBoolean("closeWhenFinish") ?: true)
|
|
264
|
+
}
|
|
265
|
+
} else if (editorConfig?.getBoolean("openDocumentsOnFinish") == true) {
|
|
266
|
+
saveFileToExternalStorage(File(outputFile!!))
|
|
267
|
+
} else if (editorConfig?.getBoolean("openShareSheetOnFinish") == true) {
|
|
268
|
+
hideDialog(editorConfig?.getBoolean("closeWhenFinish") ?: true)
|
|
269
|
+
shareFile(reactApplicationContext, File(outputFile!!))
|
|
270
|
+
} else {
|
|
271
|
+
hideDialog(editorConfig?.getBoolean("closeWhenFinish") ?: true)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
override fun onCancelTrim() {
|
|
276
|
+
emitOnCancelTrimming()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
override fun onError(errorMessage: String?, errorCode: ErrorCode) {
|
|
280
|
+
val map = Arguments.createMap()
|
|
281
|
+
map.putString("message", errorMessage)
|
|
282
|
+
map.putString("errorCode", errorCode.name)
|
|
283
|
+
emitOnError(map)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
override fun onCancel() {
|
|
287
|
+
if (!editorConfig?.getBoolean("enableCancelDialog")!!) {
|
|
288
|
+
emitOnCancel()
|
|
289
|
+
hideDialog(true)
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
val builder = AlertDialog.Builder(reactApplicationContext.currentActivity!!)
|
|
294
|
+
builder.setMessage(editorConfig?.getString("cancelDialogMessage"))
|
|
295
|
+
builder.setTitle(editorConfig?.getString("cancelDialogTitle"))
|
|
296
|
+
builder.setCancelable(false)
|
|
297
|
+
builder.setPositiveButton(editorConfig?.getString("cancelDialogConfirmText")) { dialog: DialogInterface, _: Int ->
|
|
298
|
+
dialog.cancel()
|
|
299
|
+
emitOnCancel()
|
|
300
|
+
hideDialog(true)
|
|
301
|
+
}
|
|
302
|
+
builder.setNegativeButton(
|
|
303
|
+
editorConfig?.getString("cancelDialogCancelText") ?: "Cancel"
|
|
304
|
+
) { dialog: DialogInterface, _: Int ->
|
|
305
|
+
dialog.cancel()
|
|
306
|
+
}
|
|
307
|
+
val alertDialog = builder.create()
|
|
308
|
+
alertDialog.show()
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
override fun onSave() {
|
|
312
|
+
if (!editorConfig?.getBoolean("enableSaveDialog")!!) {
|
|
313
|
+
startTrim()
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
val builder = AlertDialog.Builder(reactApplicationContext.currentActivity!!)
|
|
318
|
+
builder.setMessage(editorConfig?.getString("saveDialogMessage"))
|
|
319
|
+
builder.setTitle(editorConfig?.getString("saveDialogTitle"))
|
|
320
|
+
builder.setCancelable(false)
|
|
321
|
+
builder.setPositiveButton(editorConfig?.getString("saveDialogConfirmText")) { dialog: DialogInterface, _: Int ->
|
|
322
|
+
dialog.cancel()
|
|
323
|
+
startTrim()
|
|
324
|
+
}
|
|
325
|
+
builder.setNegativeButton(
|
|
326
|
+
editorConfig?.getString("saveDialogCancelText") ?: "Cancel"
|
|
327
|
+
) { dialog: DialogInterface, _: Int ->
|
|
328
|
+
dialog.cancel()
|
|
329
|
+
}
|
|
330
|
+
val alertDialog = builder.create()
|
|
331
|
+
alertDialog.show()
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
override fun onLog(log: ReadableMap) {
|
|
335
|
+
emitOnLog( log)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
override fun onStatistics(statistics: ReadableMap) {
|
|
339
|
+
emitOnStatistics(statistics)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private fun startTrim() {
|
|
343
|
+
val activity = reactApplicationContext.currentActivity
|
|
344
|
+
// Create the parent layout for the dialog
|
|
345
|
+
val layout = LinearLayout(activity)
|
|
346
|
+
layout.layoutParams = ViewGroup.LayoutParams(
|
|
347
|
+
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
348
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
349
|
+
)
|
|
350
|
+
layout.orientation = LinearLayout.VERTICAL
|
|
351
|
+
layout.gravity = Gravity.CENTER_HORIZONTAL
|
|
352
|
+
layout.setPadding(16, 32, 16, 32)
|
|
353
|
+
|
|
354
|
+
// Create and add the TextView
|
|
355
|
+
val textView = TextView(activity)
|
|
356
|
+
textView.layoutParams = ViewGroup.LayoutParams(
|
|
357
|
+
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
358
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
359
|
+
)
|
|
360
|
+
textView.text = editorConfig?.getString("trimmingText")
|
|
361
|
+
?: "Trimming in progress..."
|
|
362
|
+
textView.gravity = Gravity.CENTER
|
|
363
|
+
textView.textSize = 18f
|
|
364
|
+
layout.addView(textView)
|
|
365
|
+
|
|
366
|
+
// Create and add the ProgressBar
|
|
367
|
+
mProgressBar = ProgressBar(activity, null, progressBarStyleHorizontal)
|
|
368
|
+
mProgressBar!!.layoutParams = ViewGroup.LayoutParams(
|
|
369
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
370
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
371
|
+
)
|
|
372
|
+
mProgressBar!!.progressTintList = ColorStateList.valueOf("#2196F3".toColorInt())
|
|
373
|
+
layout.addView(mProgressBar)
|
|
374
|
+
|
|
375
|
+
// Create button
|
|
376
|
+
if (editorConfig?.getBoolean("enableCancelTrimming") == true) {
|
|
377
|
+
val button = Button(activity)
|
|
378
|
+
button.layoutParams = ViewGroup.LayoutParams(
|
|
379
|
+
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
380
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
381
|
+
)
|
|
382
|
+
// Set the text and style it like a text button
|
|
383
|
+
button.text = editorConfig?.getString("cancelTrimmingText")
|
|
384
|
+
?: "Cancel Trimming"
|
|
385
|
+
button.setTextColor(
|
|
386
|
+
ContextCompat.getColor(
|
|
387
|
+
activity!!,
|
|
388
|
+
holo_red_light
|
|
389
|
+
)
|
|
390
|
+
) // or use your custom color
|
|
391
|
+
|
|
392
|
+
// Apply ripple effect while keeping the button background transparent
|
|
393
|
+
val outValue = TypedValue()
|
|
394
|
+
activity.theme.resolveAttribute(selectableItemBackground, outValue, true)
|
|
395
|
+
button.setBackgroundResource(outValue.resourceId)
|
|
396
|
+
button.setOnClickListener { _: View? ->
|
|
397
|
+
if (editorConfig?.getBoolean("enableCancelTrimmingDialog") == true) {
|
|
398
|
+
val builder = AlertDialog.Builder(
|
|
399
|
+
activity
|
|
400
|
+
)
|
|
401
|
+
builder.setMessage(editorConfig?.getString("cancelTrimmingDialogMessage"))
|
|
402
|
+
builder.setTitle(editorConfig?.getString("cancelTrimmingDialogTitle"))
|
|
403
|
+
builder.setCancelable(false)
|
|
404
|
+
builder.setPositiveButton(editorConfig?.getString("cancelTrimmingDialogConfirmText")) { _: DialogInterface?, _: Int ->
|
|
405
|
+
if (trimmerView != null) {
|
|
406
|
+
trimmerView!!.onCancelTrimClicked()
|
|
407
|
+
}
|
|
408
|
+
if (mProgressDialog != null && mProgressDialog!!.isShowing) {
|
|
409
|
+
mProgressDialog!!.dismiss()
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
builder.setNegativeButton(
|
|
413
|
+
editorConfig?.getString("cancelTrimmingDialogCancelText") ?: "Close"
|
|
414
|
+
) { dialog: DialogInterface, _: Int ->
|
|
415
|
+
dialog.cancel()
|
|
416
|
+
}
|
|
417
|
+
cancelTrimmingConfirmDialog = builder.create()
|
|
418
|
+
cancelTrimmingConfirmDialog!!.show()
|
|
419
|
+
} else {
|
|
420
|
+
if (trimmerView != null) {
|
|
421
|
+
trimmerView!!.onCancelTrimClicked()
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (mProgressDialog != null && mProgressDialog!!.isShowing) {
|
|
425
|
+
mProgressDialog!!.dismiss()
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
layout.addView(button)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Create the AlertDialog
|
|
433
|
+
val builder = AlertDialog.Builder(
|
|
434
|
+
activity!!
|
|
435
|
+
)
|
|
436
|
+
builder.setCancelable(false)
|
|
437
|
+
builder.setView(layout)
|
|
438
|
+
|
|
439
|
+
// Show the dialog
|
|
440
|
+
mProgressDialog = builder.create()
|
|
441
|
+
|
|
442
|
+
mProgressDialog!!.setOnShowListener {
|
|
443
|
+
emitOnStartTrimming()
|
|
444
|
+
if (trimmerView != null) {
|
|
445
|
+
trimmerView!!.onSaveClicked()
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
mProgressDialog!!.show()
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private fun hideDialog(shouldCloseEditor: Boolean) {
|
|
453
|
+
// handle the case when the cancel dialog is still showing but the trimming is finished
|
|
454
|
+
if (cancelTrimmingConfirmDialog != null) {
|
|
455
|
+
if (cancelTrimmingConfirmDialog!!.isShowing) {
|
|
456
|
+
cancelTrimmingConfirmDialog!!.dismiss()
|
|
457
|
+
}
|
|
458
|
+
cancelTrimmingConfirmDialog = null
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (mProgressDialog != null) {
|
|
462
|
+
if (mProgressDialog!!.isShowing) mProgressDialog!!.dismiss()
|
|
463
|
+
mProgressBar = null
|
|
464
|
+
mProgressDialog = null
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (shouldCloseEditor) {
|
|
468
|
+
if (alertDialog != null) {
|
|
469
|
+
if (alertDialog!!.isShowing) {
|
|
470
|
+
alertDialog!!.dismiss()
|
|
471
|
+
}
|
|
472
|
+
alertDialog = null
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// private fun sendEvent(
|
|
478
|
+
// eventName: String,
|
|
479
|
+
// params: Map<String, String>
|
|
480
|
+
// ) {
|
|
481
|
+
// onEvent?.let { it(eventName, params) }
|
|
482
|
+
//
|
|
483
|
+
// if (eventName == "onHide" && onComplete != null) {
|
|
484
|
+
// onComplete?.let { it() }
|
|
485
|
+
// onComplete = null // Clear the callback after invoking it
|
|
486
|
+
// }
|
|
487
|
+
// }
|
|
488
|
+
|
|
489
|
+
override fun listFiles(promise: Promise) {
|
|
490
|
+
promise.resolve(StorageUtil.listFiles(reactApplicationContext))
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
override fun cleanFiles(promise: Promise) {
|
|
494
|
+
val files = StorageUtil.listFiles(reactApplicationContext)
|
|
495
|
+
var successCount = 0
|
|
496
|
+
for (file in files) {
|
|
497
|
+
val state = StorageUtil.deleteFile(file)
|
|
498
|
+
if (state) {
|
|
499
|
+
successCount++
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
promise.resolve(successCount.toDouble())
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
override fun deleteFile(filePath: String?, promise: Promise) {
|
|
507
|
+
promise.resolve(StorageUtil.deleteFile(filePath))
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
override fun closeEditor() {
|
|
511
|
+
hideDialog(true)
|
|
512
|
+
emitOnHide()
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
override fun isValidFile(url: String, promise: Promise) {
|
|
516
|
+
MediaMetadataUtil.checkFileValidity(url) { isValid: Boolean, fileType: String, duration: Long ->
|
|
517
|
+
if (isValid) {
|
|
518
|
+
Log.d(TAG, "Valid $fileType file with duration: $duration milliseconds")
|
|
519
|
+
} else {
|
|
520
|
+
Log.d(TAG, "Invalid file")
|
|
521
|
+
}
|
|
522
|
+
// Create a FileValidationResult object
|
|
523
|
+
|
|
524
|
+
val result = Arguments.createMap()
|
|
525
|
+
result.putBoolean("isValid", isValid)
|
|
526
|
+
result.putString("fileType", fileType)
|
|
527
|
+
result.putDouble("duration", duration.toDouble())
|
|
528
|
+
|
|
529
|
+
promise.resolve(result)
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
override fun trim(url: String, options: ReadableMap?, promise: Promise) {
|
|
534
|
+
trimOptions = options
|
|
535
|
+
|
|
536
|
+
val currentDate = Date()
|
|
537
|
+
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
|
|
538
|
+
|
|
539
|
+
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
|
540
|
+
val formattedDateTime = dateFormat.format(currentDate)
|
|
541
|
+
|
|
542
|
+
var cmds = arrayOf(
|
|
543
|
+
"-ss",
|
|
544
|
+
"${options?.getDouble("startTime") ?: 0 }ms",
|
|
545
|
+
"-to",
|
|
546
|
+
"${options?.getDouble("endTime") ?: 1000}ms",
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
if (options?.getBoolean("enableRotation") == true) {
|
|
550
|
+
cmds += arrayOf("-display_rotation", "${options.getDouble("rotationAngle")}")
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
outputFile = StorageUtil.getOutputPath(reactApplicationContext, options?.getString("outputExt") ?: "mp4")
|
|
554
|
+
|
|
555
|
+
cmds += arrayOf(
|
|
556
|
+
"-i",
|
|
557
|
+
url,
|
|
558
|
+
"-c",
|
|
559
|
+
"copy",
|
|
560
|
+
"-metadata",
|
|
561
|
+
"creation_time=$formattedDateTime",
|
|
562
|
+
outputFile!!
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
Log.d(TAG, "Command: ${cmds.joinToString(",")}")
|
|
566
|
+
|
|
567
|
+
FFmpegKit.executeWithArgumentsAsync(cmds, { session ->
|
|
568
|
+
val state = session.state
|
|
569
|
+
val returnCode = session.returnCode
|
|
570
|
+
when {
|
|
571
|
+
ReturnCode.isSuccess(returnCode) -> {
|
|
572
|
+
// SUCCESS
|
|
573
|
+
if (options?.getBoolean("saveToPhoto") == true && options.getString("type") == "video") {
|
|
574
|
+
try {
|
|
575
|
+
StorageUtil.saveVideoToGallery(reactApplicationContext, outputFile)
|
|
576
|
+
Log.d(TAG, "Edited video saved to Photo Library successfully.")
|
|
577
|
+
if (options.getBoolean("removeAfterSavedToPhoto")) {
|
|
578
|
+
StorageUtil.deleteFile(outputFile)
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
promise.resolve(outputFile)
|
|
582
|
+
} catch (e: IOException) {
|
|
583
|
+
e.printStackTrace()
|
|
584
|
+
|
|
585
|
+
if (options.getBoolean("removeAfterFailedToSavePhoto")) {
|
|
586
|
+
StorageUtil.deleteFile(outputFile)
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
promise.reject(
|
|
590
|
+
Exception("Failed to save edited video to Photo Library: " + e.localizedMessage)
|
|
591
|
+
)
|
|
592
|
+
}
|
|
593
|
+
} else {
|
|
594
|
+
if (options?.getBoolean("openDocumentsOnFinish") == true) {
|
|
595
|
+
saveFileToExternalStorage(File(outputFile!!))
|
|
596
|
+
} else if (options?.getBoolean("openShareSheetOnFinish") == true) {
|
|
597
|
+
shareFile(reactApplicationContext, File(outputFile!!))
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
promise.resolve(outputFile)
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
ReturnCode.isCancel(returnCode) -> {
|
|
604
|
+
// CANCEL
|
|
605
|
+
println("FFmpeg command was cancelled")
|
|
606
|
+
promise.reject(
|
|
607
|
+
Exception("FFmpeg command was cancelled")
|
|
608
|
+
)
|
|
609
|
+
}
|
|
610
|
+
else -> {
|
|
611
|
+
// FAILURE
|
|
612
|
+
val errorMessage = String.format("Command failed with state %s and rc %s.%s", state, returnCode, session.getFailStackTrace());
|
|
613
|
+
println(errorMessage)
|
|
614
|
+
promise.reject(
|
|
615
|
+
Exception(errorMessage)
|
|
616
|
+
)
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}, { log ->
|
|
620
|
+
Log.d(TAG, "FFmpeg process started with log ${log.message}")
|
|
621
|
+
}, { statistics ->
|
|
622
|
+
// Handle statistics if needed
|
|
623
|
+
})
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
private fun saveFileToExternalStorage(file: File) {
|
|
627
|
+
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
|
628
|
+
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
|
629
|
+
intent.setType("*/*") // Change MIME type as needed
|
|
630
|
+
intent.putExtra(Intent.EXTRA_TITLE, file.name)
|
|
631
|
+
reactApplicationContext.currentActivity?.startActivityForResult(intent, REQUEST_CODE_SAVE_FILE)
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
private fun shareFile(context: Context, file: File) {
|
|
635
|
+
val fileUri = FileProvider.getUriForFile(context, context.packageName + ".provider", file)
|
|
636
|
+
|
|
637
|
+
val shareIntent = Intent(Intent.ACTION_SEND)
|
|
638
|
+
shareIntent.setType("*/*")
|
|
639
|
+
shareIntent.putExtra(Intent.EXTRA_STREAM, fileUri)
|
|
640
|
+
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
641
|
+
|
|
642
|
+
// Grant permissions to all applications that can handle the intent
|
|
643
|
+
for (resolveInfo in context.packageManager.queryIntentActivities(
|
|
644
|
+
shareIntent,
|
|
645
|
+
PackageManager.MATCH_DEFAULT_ONLY
|
|
646
|
+
)) {
|
|
647
|
+
val packageName = resolveInfo.activityInfo.packageName
|
|
648
|
+
context.grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// directly use context.startActivity(shareIntent) will cause crash
|
|
652
|
+
reactApplicationContext.currentActivity?.startActivity(Intent.createChooser(shareIntent, "Share file"))
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
companion object {
|
|
656
|
+
const val NAME = "VideoTrim"
|
|
657
|
+
const val TAG = "VideoTrimModule"
|
|
658
|
+
const val REQUEST_CODE_SAVE_FILE = 1
|
|
659
|
+
}
|
|
660
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package com.videotrim
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
import java.util.HashMap
|
|
9
|
+
|
|
10
|
+
class VideoTrimPackage : BaseReactPackage() {
|
|
11
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
12
|
+
return if (name == VideoTrimModule.NAME) {
|
|
13
|
+
VideoTrimModule(reactContext)
|
|
14
|
+
} else {
|
|
15
|
+
null
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
|
20
|
+
return ReactModuleInfoProvider {
|
|
21
|
+
val moduleInfos: MutableMap<String, ReactModuleInfo> = HashMap()
|
|
22
|
+
moduleInfos[VideoTrimModule.NAME] = ReactModuleInfo(
|
|
23
|
+
VideoTrimModule.NAME,
|
|
24
|
+
VideoTrimModule.NAME,
|
|
25
|
+
false, // canOverrideExistingModule
|
|
26
|
+
false, // needsEagerInit
|
|
27
|
+
false, // isCxxModule
|
|
28
|
+
true // isTurboModule
|
|
29
|
+
)
|
|
30
|
+
moduleInfos
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|