@stefanmartin/expo-video-watermark 0.3.0 → 0.3.1

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,10 @@ 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)
333
+ // Enable HDR to SDR tone mapping for videos with HDR content
334
+ .setHdrMode(Transformer.HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL)
312
335
  .addListener(object : Transformer.Listener {
313
336
  override fun onCompleted(composition: Composition, exportResult: ExportResult) {
314
337
  Log.d(TAG, "[Step 15] Transform completed successfully")
@@ -318,7 +341,88 @@ class ExpoVideoWatermarkModule : Module() {
318
341
  "averageVideoBitrate: ${exportResult.averageVideoBitrate}, " +
319
342
  "videoFrameCount: ${exportResult.videoFrameCount}")
320
343
  watermarkBitmap.recycle()
321
- promise.resolve("file://$cleanOutputPath")
344
+
345
+ // Step 16: Re-encode to H.265 if device supports HEVC encoder
346
+ val supportsHevc = hasHevcEncoder()
347
+ Log.d(TAG, "[Step 16] HEVC encoder support: $supportsHevc")
348
+
349
+ if (!supportsHevc) {
350
+ Log.d(TAG, "[Step 16] Skipping H.265 re-encode - no HEVC encoder available")
351
+ promise.resolve("file://$cleanOutputPath")
352
+ return
353
+ }
354
+
355
+ // Create temp path for the H.264 watermarked video, rename current output
356
+ val h264File = File(cleanOutputPath)
357
+ val h264TempPath = cleanOutputPath.replace(".mp4", "_h264_temp.mp4")
358
+ val h264TempFile = File(h264TempPath)
359
+
360
+ if (!h264File.renameTo(h264TempFile)) {
361
+ Log.e(TAG, "[Step 16] Failed to rename H.264 file for re-encoding, returning H.264 output")
362
+ promise.resolve("file://$cleanOutputPath")
363
+ return
364
+ }
365
+
366
+ Log.d(TAG, "[Step 16] Starting H.265 re-encode from: $h264TempPath to: $cleanOutputPath")
367
+
368
+ // Build transformer for H.265 re-encoding
369
+ val hevcTransformer = Transformer.Builder(context)
370
+ .setVideoMimeType(MimeTypes.VIDEO_H265)
371
+ .addListener(object : Transformer.Listener {
372
+ override fun onCompleted(composition: Composition, hevcExportResult: ExportResult) {
373
+ Log.d(TAG, "[Step 16] H.265 re-encode completed successfully")
374
+ Log.d(TAG, "[Step 16] Export result - durationMs: ${hevcExportResult.durationMs}, " +
375
+ "fileSizeBytes: ${hevcExportResult.fileSizeBytes}, " +
376
+ "averageAudioBitrate: ${hevcExportResult.averageAudioBitrate}, " +
377
+ "averageVideoBitrate: ${hevcExportResult.averageVideoBitrate}, " +
378
+ "videoFrameCount: ${hevcExportResult.videoFrameCount}")
379
+
380
+ // Calculate compression ratio
381
+ val h264Size = exportResult.fileSizeBytes
382
+ val h265Size = hevcExportResult.fileSizeBytes
383
+ if (h264Size > 0 && h265Size > 0) {
384
+ val savings = ((h264Size - h265Size) * 100.0 / h264Size)
385
+ Log.d(TAG, "[Step 16] Size reduction: H.264=${h264Size/1024}KB -> H.265=${h265Size/1024}KB (${String.format("%.1f", savings)}% smaller)")
386
+ }
387
+
388
+ // Clean up temp H.264 file
389
+ if (h264TempFile.exists()) {
390
+ h264TempFile.delete()
391
+ Log.d(TAG, "[Step 16] Cleaned up temp H.264 file")
392
+ }
393
+
394
+ promise.resolve("file://$cleanOutputPath")
395
+ }
396
+
397
+ override fun onError(
398
+ composition: Composition,
399
+ hevcExportResult: ExportResult,
400
+ hevcExportException: ExportException
401
+ ) {
402
+ val errorCodeName = getExportErrorCodeName(hevcExportException.errorCode)
403
+ Log.e(TAG, "[Step 16] H.265 re-encode failed: $errorCodeName - ${hevcExportException.message}")
404
+ Log.e(TAG, Log.getStackTraceString(hevcExportException))
405
+
406
+ // Restore H.264 file as output on failure
407
+ if (h264TempFile.exists()) {
408
+ val outputFile = File(cleanOutputPath)
409
+ if (outputFile.exists()) {
410
+ outputFile.delete()
411
+ }
412
+ h264TempFile.renameTo(outputFile)
413
+ Log.d(TAG, "[Step 16] Restored H.264 output after H.265 failure")
414
+ }
415
+
416
+ // Still resolve with the H.264 version rather than failing completely
417
+ Log.d(TAG, "[Step 16] Returning H.264 output instead")
418
+ promise.resolve("file://$cleanOutputPath")
419
+ }
420
+ })
421
+ .build()
422
+
423
+ val hevcMediaItem = MediaItem.fromUri("file://$h264TempPath")
424
+ val hevcEditedMediaItem = EditedMediaItem.Builder(hevcMediaItem).build()
425
+ hevcTransformer.start(hevcEditedMediaItem, cleanOutputPath)
322
426
  }
323
427
 
324
428
  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.1",
4
4
  "description": "Creating video watermarks on locally stored videos",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",