@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.
@@ -9,6 +9,7 @@ version = '0.2.1'
9
9
  android {
10
10
  namespace "expo.modules.videowatermark"
11
11
  defaultConfig {
12
+ minSdk 33
12
13
  versionCode 2
13
14
  versionName "0.2.1"
14
15
  }
@@ -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
- "[Step 15] Video transform failed (video: ${videoWidth.toInt()}x${videoHeight.toInt()}, rotation: $rotation, watermark: ${watermarkWidth.toInt()}x${watermarkHeight.toInt()}, scale: $scale): ${exportException.message ?: "Unknown error"}",
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("STEP15_TRANSFORMER_BUILD_ERROR", "[Step 15] Failed to build/start transformer: ${e.message}", e)
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stefanmartin/expo-video-watermark",
3
- "version": "0.2.7",
3
+ "version": "0.3.0",
4
4
  "description": "Creating video watermarks on locally stored videos",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",