@stefanmartin/expo-video-watermark 0.3.0 → 0.3.2

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.
@@ -3,6 +3,7 @@ package expo.modules.videowatermark
3
3
  import android.content.Context
4
4
  import android.graphics.Bitmap
5
5
  import android.graphics.BitmapFactory
6
+ import android.media.MediaCodecList
6
7
  import android.media.MediaMetadataRetriever
7
8
  import android.os.Build
8
9
  import android.os.Handler
@@ -10,6 +11,7 @@ import android.os.Looper
10
11
  import android.util.Log
11
12
  import androidx.annotation.OptIn
12
13
  import androidx.media3.common.MediaItem
14
+ import androidx.media3.common.MimeTypes
13
15
  import androidx.media3.common.util.UnstableApi
14
16
  import androidx.media3.effect.BitmapOverlay
15
17
  import androidx.media3.effect.OverlayEffect
@@ -35,6 +37,23 @@ class ExpoVideoWatermarkModule : Module() {
35
37
  companion object {
36
38
  private const val TAG = "ExpoVideoWatermark"
37
39
 
40
+ /**
41
+ * Check if the device has a hardware H.265/HEVC encoder
42
+ */
43
+ fun hasHevcEncoder(): Boolean {
44
+ return try {
45
+ val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
46
+ codecList.codecInfos.any { codecInfo ->
47
+ codecInfo.isEncoder && codecInfo.supportedTypes.any { type ->
48
+ type.equals(MimeTypes.VIDEO_H265, ignoreCase = true)
49
+ }
50
+ }
51
+ } catch (e: Exception) {
52
+ Log.w(TAG, "Failed to check HEVC encoder support: ${e.message}")
53
+ false
54
+ }
55
+ }
56
+
38
57
  /**
39
58
  * Get device information for debugging
40
59
  */
@@ -309,6 +328,8 @@ class ExpoVideoWatermarkModule : Module() {
309
328
  mainHandler.post {
310
329
  try {
311
330
  val transformer = Transformer.Builder(context)
331
+ // Force H.264 output for maximum compatibility
332
+ .setVideoMimeType(MimeTypes.VIDEO_H264)
312
333
  .addListener(object : Transformer.Listener {
313
334
  override fun onCompleted(composition: Composition, exportResult: ExportResult) {
314
335
  Log.d(TAG, "[Step 15] Transform completed successfully")
@@ -318,7 +339,88 @@ class ExpoVideoWatermarkModule : Module() {
318
339
  "averageVideoBitrate: ${exportResult.averageVideoBitrate}, " +
319
340
  "videoFrameCount: ${exportResult.videoFrameCount}")
320
341
  watermarkBitmap.recycle()
321
- promise.resolve("file://$cleanOutputPath")
342
+
343
+ // Step 16: Re-encode to H.265 if device supports HEVC encoder
344
+ val supportsHevc = hasHevcEncoder()
345
+ Log.d(TAG, "[Step 16] HEVC encoder support: $supportsHevc")
346
+
347
+ if (!supportsHevc) {
348
+ Log.d(TAG, "[Step 16] Skipping H.265 re-encode - no HEVC encoder available")
349
+ promise.resolve("file://$cleanOutputPath")
350
+ return
351
+ }
352
+
353
+ // Create temp path for the H.264 watermarked video, rename current output
354
+ val h264File = File(cleanOutputPath)
355
+ val h264TempPath = cleanOutputPath.replace(".mp4", "_h264_temp.mp4")
356
+ val h264TempFile = File(h264TempPath)
357
+
358
+ if (!h264File.renameTo(h264TempFile)) {
359
+ Log.e(TAG, "[Step 16] Failed to rename H.264 file for re-encoding, returning H.264 output")
360
+ promise.resolve("file://$cleanOutputPath")
361
+ return
362
+ }
363
+
364
+ Log.d(TAG, "[Step 16] Starting H.265 re-encode from: $h264TempPath to: $cleanOutputPath")
365
+
366
+ // Build transformer for H.265 re-encoding
367
+ val hevcTransformer = Transformer.Builder(context)
368
+ .setVideoMimeType(MimeTypes.VIDEO_H265)
369
+ .addListener(object : Transformer.Listener {
370
+ override fun onCompleted(composition: Composition, hevcExportResult: ExportResult) {
371
+ Log.d(TAG, "[Step 16] H.265 re-encode completed successfully")
372
+ Log.d(TAG, "[Step 16] Export result - durationMs: ${hevcExportResult.durationMs}, " +
373
+ "fileSizeBytes: ${hevcExportResult.fileSizeBytes}, " +
374
+ "averageAudioBitrate: ${hevcExportResult.averageAudioBitrate}, " +
375
+ "averageVideoBitrate: ${hevcExportResult.averageVideoBitrate}, " +
376
+ "videoFrameCount: ${hevcExportResult.videoFrameCount}")
377
+
378
+ // Calculate compression ratio
379
+ val h264Size = exportResult.fileSizeBytes
380
+ val h265Size = hevcExportResult.fileSizeBytes
381
+ if (h264Size > 0 && h265Size > 0) {
382
+ val savings = ((h264Size - h265Size) * 100.0 / h264Size)
383
+ Log.d(TAG, "[Step 16] Size reduction: H.264=${h264Size/1024}KB -> H.265=${h265Size/1024}KB (${String.format("%.1f", savings)}% smaller)")
384
+ }
385
+
386
+ // Clean up temp H.264 file
387
+ if (h264TempFile.exists()) {
388
+ h264TempFile.delete()
389
+ Log.d(TAG, "[Step 16] Cleaned up temp H.264 file")
390
+ }
391
+
392
+ promise.resolve("file://$cleanOutputPath")
393
+ }
394
+
395
+ override fun onError(
396
+ composition: Composition,
397
+ hevcExportResult: ExportResult,
398
+ hevcExportException: ExportException
399
+ ) {
400
+ val errorCodeName = getExportErrorCodeName(hevcExportException.errorCode)
401
+ Log.e(TAG, "[Step 16] H.265 re-encode failed: $errorCodeName - ${hevcExportException.message}")
402
+ Log.e(TAG, Log.getStackTraceString(hevcExportException))
403
+
404
+ // Restore H.264 file as output on failure
405
+ if (h264TempFile.exists()) {
406
+ val outputFile = File(cleanOutputPath)
407
+ if (outputFile.exists()) {
408
+ outputFile.delete()
409
+ }
410
+ h264TempFile.renameTo(outputFile)
411
+ Log.d(TAG, "[Step 16] Restored H.264 output after H.265 failure")
412
+ }
413
+
414
+ // Still resolve with the H.264 version rather than failing completely
415
+ Log.d(TAG, "[Step 16] Returning H.264 output instead")
416
+ promise.resolve("file://$cleanOutputPath")
417
+ }
418
+ })
419
+ .build()
420
+
421
+ val hevcMediaItem = MediaItem.fromUri("file://$h264TempPath")
422
+ val hevcEditedMediaItem = EditedMediaItem.Builder(hevcMediaItem).build()
423
+ hevcTransformer.start(hevcEditedMediaItem, cleanOutputPath)
322
424
  }
323
425
 
324
426
  override fun onError(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stefanmartin/expo-video-watermark",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Creating video watermarks on locally stored videos",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",