@stefanmartin/expo-video-watermark 0.2.7 → 0.3.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/android/build.gradle
CHANGED
|
@@ -4,8 +4,10 @@ import android.content.Context
|
|
|
4
4
|
import android.graphics.Bitmap
|
|
5
5
|
import android.graphics.BitmapFactory
|
|
6
6
|
import android.media.MediaMetadataRetriever
|
|
7
|
+
import android.os.Build
|
|
7
8
|
import android.os.Handler
|
|
8
9
|
import android.os.Looper
|
|
10
|
+
import android.util.Log
|
|
9
11
|
import androidx.annotation.OptIn
|
|
10
12
|
import androidx.media3.common.MediaItem
|
|
11
13
|
import androidx.media3.common.util.UnstableApi
|
|
@@ -25,8 +27,86 @@ import expo.modules.kotlin.exception.Exceptions
|
|
|
25
27
|
import expo.modules.kotlin.modules.Module
|
|
26
28
|
import expo.modules.kotlin.modules.ModuleDefinition
|
|
27
29
|
import java.io.File
|
|
30
|
+
import javax.microedition.khronos.egl.EGL10
|
|
31
|
+
import javax.microedition.khronos.egl.EGLConfig
|
|
32
|
+
import javax.microedition.khronos.egl.EGLContext
|
|
28
33
|
|
|
29
34
|
class ExpoVideoWatermarkModule : Module() {
|
|
35
|
+
companion object {
|
|
36
|
+
private const val TAG = "ExpoVideoWatermark"
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get device information for debugging
|
|
40
|
+
*/
|
|
41
|
+
fun getDeviceInfo(): String {
|
|
42
|
+
return buildString {
|
|
43
|
+
append("Device: ${Build.MANUFACTURER} ${Build.MODEL}")
|
|
44
|
+
append(", Android ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})")
|
|
45
|
+
append(", Board: ${Build.BOARD}")
|
|
46
|
+
append(", Hardware: ${Build.HARDWARE}")
|
|
47
|
+
append(", SOC: ${Build.SOC_MANUFACTURER} ${Build.SOC_MODEL}")
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get OpenGL ES info (call on GL thread or main thread)
|
|
53
|
+
*/
|
|
54
|
+
fun getGLInfo(): String {
|
|
55
|
+
return try {
|
|
56
|
+
val egl = EGLContext.getEGL() as? EGL10
|
|
57
|
+
if (egl != null) {
|
|
58
|
+
val display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY)
|
|
59
|
+
egl.eglInitialize(display, IntArray(2))
|
|
60
|
+
|
|
61
|
+
val configAttribs = intArrayOf(
|
|
62
|
+
EGL10.EGL_RENDERABLE_TYPE, 4, // EGL_OPENGL_ES2_BIT
|
|
63
|
+
EGL10.EGL_NONE
|
|
64
|
+
)
|
|
65
|
+
val configs = arrayOfNulls<EGLConfig>(1)
|
|
66
|
+
val numConfigs = IntArray(1)
|
|
67
|
+
egl.eglChooseConfig(display, configAttribs, configs, 1, numConfigs)
|
|
68
|
+
|
|
69
|
+
val vendor = egl.eglQueryString(display, EGL10.EGL_VENDOR) ?: "unknown"
|
|
70
|
+
val version = egl.eglQueryString(display, EGL10.EGL_VERSION) ?: "unknown"
|
|
71
|
+
val extensions = egl.eglQueryString(display, EGL10.EGL_EXTENSIONS) ?: ""
|
|
72
|
+
|
|
73
|
+
egl.eglTerminate(display)
|
|
74
|
+
|
|
75
|
+
"EGL Vendor: $vendor, Version: $version, Has OES_EGL_image_external: ${extensions.contains("EGL_KHR_image_base")}"
|
|
76
|
+
} else {
|
|
77
|
+
"EGL not available"
|
|
78
|
+
}
|
|
79
|
+
} catch (e: Exception) {
|
|
80
|
+
"GL info error: ${e.message}"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Map ExportException error code to human-readable string
|
|
86
|
+
*/
|
|
87
|
+
@OptIn(UnstableApi::class)
|
|
88
|
+
fun getExportErrorCodeName(errorCode: Int): String {
|
|
89
|
+
return when (errorCode) {
|
|
90
|
+
ExportException.ERROR_CODE_UNSPECIFIED -> "ERROR_CODE_UNSPECIFIED"
|
|
91
|
+
ExportException.ERROR_CODE_IO_UNSPECIFIED -> "ERROR_CODE_IO_UNSPECIFIED"
|
|
92
|
+
ExportException.ERROR_CODE_IO_FILE_NOT_FOUND -> "ERROR_CODE_IO_FILE_NOT_FOUND"
|
|
93
|
+
ExportException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED -> "ERROR_CODE_IO_NETWORK_CONNECTION_FAILED"
|
|
94
|
+
ExportException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT -> "ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT"
|
|
95
|
+
ExportException.ERROR_CODE_DECODER_INIT_FAILED -> "ERROR_CODE_DECODER_INIT_FAILED"
|
|
96
|
+
ExportException.ERROR_CODE_DECODING_FAILED -> "ERROR_CODE_DECODING_FAILED"
|
|
97
|
+
ExportException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED -> "ERROR_CODE_DECODING_FORMAT_UNSUPPORTED"
|
|
98
|
+
ExportException.ERROR_CODE_ENCODER_INIT_FAILED -> "ERROR_CODE_ENCODER_INIT_FAILED"
|
|
99
|
+
ExportException.ERROR_CODE_ENCODING_FAILED -> "ERROR_CODE_ENCODING_FAILED"
|
|
100
|
+
ExportException.ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED -> "ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED"
|
|
101
|
+
ExportException.ERROR_CODE_VIDEO_FRAME_PROCESSING_FAILED -> "ERROR_CODE_VIDEO_FRAME_PROCESSING_FAILED"
|
|
102
|
+
ExportException.ERROR_CODE_AUDIO_PROCESSING_FAILED -> "ERROR_CODE_AUDIO_PROCESSING_FAILED"
|
|
103
|
+
ExportException.ERROR_CODE_MUXING_FAILED -> "ERROR_CODE_MUXING_FAILED"
|
|
104
|
+
ExportException.ERROR_CODE_MUXING_TIMEOUT -> "ERROR_CODE_MUXING_TIMEOUT"
|
|
105
|
+
else -> "UNKNOWN_ERROR_CODE($errorCode)"
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
30
110
|
private val context: Context
|
|
31
111
|
get() = appContext.reactContext ?: throw Exceptions.AppContextLost()
|
|
32
112
|
|
|
@@ -77,6 +157,7 @@ class ExpoVideoWatermarkModule : Module() {
|
|
|
77
157
|
|
|
78
158
|
// Step 4: Ensure bitmap is in ARGB_8888 format (required for Media3 GPU processing)
|
|
79
159
|
val watermarkBitmap: Bitmap = if (decodedBitmap.config != Bitmap.Config.ARGB_8888) {
|
|
160
|
+
Log.d(TAG, "[Step 4] Converting bitmap from ${decodedBitmap.config} to ARGB_8888")
|
|
80
161
|
val converted = decodedBitmap.copy(Bitmap.Config.ARGB_8888, false)
|
|
81
162
|
decodedBitmap.recycle()
|
|
82
163
|
if (converted == null) {
|
|
@@ -88,6 +169,16 @@ class ExpoVideoWatermarkModule : Module() {
|
|
|
88
169
|
decodedBitmap
|
|
89
170
|
}
|
|
90
171
|
|
|
172
|
+
// Log bitmap details for debugging
|
|
173
|
+
val bitmapInfo = buildString {
|
|
174
|
+
append("size=${watermarkBitmap.width}x${watermarkBitmap.height}, ")
|
|
175
|
+
append("config=${watermarkBitmap.config}, ")
|
|
176
|
+
append("byteCount=${watermarkBitmap.byteCount / 1024}KB, ")
|
|
177
|
+
append("hasAlpha=${watermarkBitmap.hasAlpha()}, ")
|
|
178
|
+
append("isPremultiplied=${watermarkBitmap.isPremultiplied}")
|
|
179
|
+
}
|
|
180
|
+
Log.d(TAG, "[Step 4] Watermark bitmap: $bitmapInfo")
|
|
181
|
+
|
|
91
182
|
// Step 5: Ensure output directory exists
|
|
92
183
|
val outputFile = File(cleanOutputPath)
|
|
93
184
|
outputFile.parentFile?.mkdirs()
|
|
@@ -97,7 +188,7 @@ class ExpoVideoWatermarkModule : Module() {
|
|
|
97
188
|
outputFile.delete()
|
|
98
189
|
}
|
|
99
190
|
|
|
100
|
-
// Step 6: Get video dimensions to calculate scale
|
|
191
|
+
// Step 6: Get video dimensions and metadata to calculate scale
|
|
101
192
|
val retriever = MediaMetadataRetriever()
|
|
102
193
|
try {
|
|
103
194
|
retriever.setDataSource(cleanVideoPath)
|
|
@@ -110,8 +201,27 @@ class ExpoVideoWatermarkModule : Module() {
|
|
|
110
201
|
val rawVideoWidth = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toFloatOrNull() ?: 0f
|
|
111
202
|
val rawVideoHeight = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toFloatOrNull() ?: 0f
|
|
112
203
|
val rotation = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toIntOrNull() ?: 0
|
|
204
|
+
|
|
205
|
+
// Capture additional video metadata for debugging
|
|
206
|
+
val mimeType = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE) ?: "unknown"
|
|
207
|
+
val bitrate = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE)?.toLongOrNull() ?: 0L
|
|
208
|
+
val duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLongOrNull() ?: 0L
|
|
209
|
+
val frameRate = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE)?.toFloatOrNull() ?: 0f
|
|
210
|
+
val colorStandard = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_COLOR_STANDARD) ?: "unknown"
|
|
211
|
+
val colorTransfer = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_COLOR_TRANSFER) ?: "unknown"
|
|
113
212
|
retriever.release()
|
|
114
213
|
|
|
214
|
+
// Build comprehensive video info string for debugging
|
|
215
|
+
val videoInfo = buildString {
|
|
216
|
+
append("mime=$mimeType, ")
|
|
217
|
+
append("bitrate=${bitrate / 1000}kbps, ")
|
|
218
|
+
append("duration=${duration}ms, ")
|
|
219
|
+
append("frameRate=$frameRate, ")
|
|
220
|
+
append("colorStandard=$colorStandard, ")
|
|
221
|
+
append("colorTransfer=$colorTransfer")
|
|
222
|
+
}
|
|
223
|
+
Log.d(TAG, "[Step 6] Video metadata: $videoInfo")
|
|
224
|
+
|
|
115
225
|
if (rawVideoWidth <= 0 || rawVideoHeight <= 0) {
|
|
116
226
|
watermarkBitmap.recycle()
|
|
117
227
|
promise.reject("STEP6_VIDEO_DIMENSIONS_ERROR", "[Step 6] Failed to get video dimensions (width=$rawVideoWidth, height=$rawVideoHeight)", null)
|
|
@@ -189,12 +299,24 @@ class ExpoVideoWatermarkModule : Module() {
|
|
|
189
299
|
// Handler for main thread callbacks
|
|
190
300
|
val mainHandler = Handler(Looper.getMainLooper())
|
|
191
301
|
|
|
302
|
+
// Gather device and GL info for debugging (do this before posting to main thread)
|
|
303
|
+
val deviceInfo = getDeviceInfo()
|
|
304
|
+
val glInfo = getGLInfo()
|
|
305
|
+
Log.d(TAG, "[Step 15] Starting transform on device: $deviceInfo")
|
|
306
|
+
Log.d(TAG, "[Step 15] GL info: $glInfo")
|
|
307
|
+
|
|
192
308
|
// Step 15: Build and start transformer
|
|
193
309
|
mainHandler.post {
|
|
194
310
|
try {
|
|
195
311
|
val transformer = Transformer.Builder(context)
|
|
196
312
|
.addListener(object : Transformer.Listener {
|
|
197
313
|
override fun onCompleted(composition: Composition, exportResult: ExportResult) {
|
|
314
|
+
Log.d(TAG, "[Step 15] Transform completed successfully")
|
|
315
|
+
Log.d(TAG, "[Step 15] Export result - durationMs: ${exportResult.durationMs}, " +
|
|
316
|
+
"fileSizeBytes: ${exportResult.fileSizeBytes}, " +
|
|
317
|
+
"averageAudioBitrate: ${exportResult.averageAudioBitrate}, " +
|
|
318
|
+
"averageVideoBitrate: ${exportResult.averageVideoBitrate}, " +
|
|
319
|
+
"videoFrameCount: ${exportResult.videoFrameCount}")
|
|
198
320
|
watermarkBitmap.recycle()
|
|
199
321
|
promise.resolve("file://$cleanOutputPath")
|
|
200
322
|
}
|
|
@@ -204,20 +326,89 @@ class ExpoVideoWatermarkModule : Module() {
|
|
|
204
326
|
exportResult: ExportResult,
|
|
205
327
|
exportException: ExportException
|
|
206
328
|
) {
|
|
329
|
+
// Build comprehensive error message with all diagnostic info
|
|
330
|
+
val errorCodeName = getExportErrorCodeName(exportException.errorCode)
|
|
331
|
+
|
|
332
|
+
val diagnosticInfo = buildString {
|
|
333
|
+
appendLine("=== STEP 15 TRANSFORM ERROR ===")
|
|
334
|
+
appendLine("Error code: ${exportException.errorCode} ($errorCodeName)")
|
|
335
|
+
appendLine("Error message: ${exportException.message ?: "null"}")
|
|
336
|
+
appendLine("Cause: ${exportException.cause?.message ?: "null"}")
|
|
337
|
+
appendLine("Cause class: ${exportException.cause?.javaClass?.name ?: "null"}")
|
|
338
|
+
appendLine()
|
|
339
|
+
appendLine("--- Device Info ---")
|
|
340
|
+
appendLine(deviceInfo)
|
|
341
|
+
appendLine()
|
|
342
|
+
appendLine("--- GL Info ---")
|
|
343
|
+
appendLine(glInfo)
|
|
344
|
+
appendLine()
|
|
345
|
+
appendLine("--- Video Info ---")
|
|
346
|
+
appendLine("Dimensions (raw): ${rawVideoWidth.toInt()}x${rawVideoHeight.toInt()}")
|
|
347
|
+
appendLine("Dimensions (adjusted): ${videoWidth.toInt()}x${videoHeight.toInt()}")
|
|
348
|
+
appendLine("Rotation: $rotation")
|
|
349
|
+
appendLine("Metadata: $videoInfo")
|
|
350
|
+
appendLine("Input path: $cleanVideoPath")
|
|
351
|
+
appendLine()
|
|
352
|
+
appendLine("--- Watermark Info ---")
|
|
353
|
+
appendLine("Dimensions: ${watermarkWidth.toInt()}x${watermarkHeight.toInt()}")
|
|
354
|
+
appendLine("Bitmap info: $bitmapInfo")
|
|
355
|
+
appendLine("Scale factor: $scale")
|
|
356
|
+
appendLine()
|
|
357
|
+
appendLine("--- Output Info ---")
|
|
358
|
+
appendLine("Output path: $cleanOutputPath")
|
|
359
|
+
appendLine("Partial export result - durationMs: ${exportResult.durationMs}, " +
|
|
360
|
+
"fileSizeBytes: ${exportResult.fileSizeBytes}")
|
|
361
|
+
appendLine()
|
|
362
|
+
appendLine("--- Full Stack Trace ---")
|
|
363
|
+
append(Log.getStackTraceString(exportException))
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Log full diagnostics
|
|
367
|
+
Log.e(TAG, diagnosticInfo)
|
|
368
|
+
|
|
369
|
+
// Also log any nested causes
|
|
370
|
+
var cause: Throwable? = exportException.cause
|
|
371
|
+
var causeLevel = 1
|
|
372
|
+
while (cause != null) {
|
|
373
|
+
Log.e(TAG, "[Step 15] Cause level $causeLevel: ${cause.javaClass.name}: ${cause.message}")
|
|
374
|
+
Log.e(TAG, Log.getStackTraceString(cause))
|
|
375
|
+
cause = cause.cause
|
|
376
|
+
causeLevel++
|
|
377
|
+
}
|
|
378
|
+
|
|
207
379
|
watermarkBitmap.recycle()
|
|
380
|
+
|
|
381
|
+
// Reject with comprehensive error message
|
|
382
|
+
val errorMessage = "[Step 15] Transform failed - " +
|
|
383
|
+
"ErrorCode: $errorCodeName (${exportException.errorCode}), " +
|
|
384
|
+
"Device: ${Build.MANUFACTURER} ${Build.MODEL} (API ${Build.VERSION.SDK_INT}), " +
|
|
385
|
+
"Video: ${videoWidth.toInt()}x${videoHeight.toInt()} $mimeType, " +
|
|
386
|
+
"Watermark: ${watermarkWidth.toInt()}x${watermarkHeight.toInt()}, " +
|
|
387
|
+
"Scale: $scale, " +
|
|
388
|
+
"Message: ${exportException.message ?: "Unknown error"}"
|
|
389
|
+
|
|
208
390
|
promise.reject(
|
|
209
391
|
"STEP15_TRANSFORM_ERROR",
|
|
210
|
-
|
|
392
|
+
errorMessage,
|
|
211
393
|
exportException
|
|
212
394
|
)
|
|
213
395
|
}
|
|
214
396
|
})
|
|
215
397
|
.build()
|
|
216
398
|
|
|
399
|
+
Log.d(TAG, "[Step 15] Transformer built, starting export...")
|
|
217
400
|
transformer.start(editedMediaItem, cleanOutputPath)
|
|
401
|
+
Log.d(TAG, "[Step 15] Transformer.start() called, waiting for completion...")
|
|
218
402
|
} catch (e: Exception) {
|
|
403
|
+
Log.e(TAG, "[Step 15] Exception building/starting transformer", e)
|
|
404
|
+
Log.e(TAG, "[Step 15] Device info: $deviceInfo")
|
|
405
|
+
Log.e(TAG, "[Step 15] GL info: $glInfo")
|
|
219
406
|
watermarkBitmap.recycle()
|
|
220
|
-
promise.reject(
|
|
407
|
+
promise.reject(
|
|
408
|
+
"STEP15_TRANSFORMER_BUILD_ERROR",
|
|
409
|
+
"[Step 15] Failed to build/start transformer on ${Build.MANUFACTURER} ${Build.MODEL}: ${e.message}",
|
|
410
|
+
e
|
|
411
|
+
)
|
|
221
412
|
}
|
|
222
413
|
}
|
|
223
414
|
}
|