@stefanmartin/expo-video-watermark 0.3.2 → 0.3.3

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.
@@ -254,34 +254,55 @@ class ExpoVideoWatermarkModule : Module() {
254
254
  rawVideoWidth to rawVideoHeight
255
255
  }
256
256
 
257
- // Step 8: Calculate scale to make watermark span full video width, maintaining aspect ratio
258
- val watermarkWidth = watermarkBitmap.width.toFloat()
259
- val watermarkHeight = watermarkBitmap.height.toFloat()
260
- val scale = videoWidth / watermarkWidth
257
+ // Step 8: Pre-scale watermark bitmap to match video width if needed
258
+ val originalWidth = watermarkBitmap.width
259
+ val originalHeight = watermarkBitmap.height
260
+ val targetWidth = videoWidth.toInt()
261
+ val scale = targetWidth.toFloat() / originalWidth.toFloat()
262
+ val targetHeight = (originalHeight * scale).toInt()
263
+
264
+ // Skip scaling if watermark already matches video width
265
+ val scaledWatermark: Bitmap = if (originalWidth == targetWidth) {
266
+ Log.d(TAG, "[Step 8] Watermark already matches video width (${originalWidth}x${originalHeight}), skipping scale")
267
+ watermarkBitmap
268
+ } else {
269
+ Log.d(TAG, "[Step 8] Pre-scaling watermark: ${originalWidth}x${originalHeight} -> ${targetWidth}x${targetHeight} (scale: $scale)")
270
+ try {
271
+ val scaled = Bitmap.createScaledBitmap(watermarkBitmap, targetWidth, targetHeight, true)
272
+ // Recycle original if we created a new scaled version
273
+ if (scaled !== watermarkBitmap) {
274
+ watermarkBitmap.recycle()
275
+ }
276
+ scaled
277
+ } catch (e: Exception) {
278
+ watermarkBitmap.recycle()
279
+ promise.reject("STEP8_SCALE_ERROR", "[Step 8] Failed to scale watermark bitmap: ${e.message}", e)
280
+ return
281
+ }
282
+ }
261
283
 
262
- // Step 9: Create overlay settings for full-width bottom positioning
284
+ // Step 9: Create overlay settings for bottom positioning (no GPU scaling needed)
263
285
  // In Media3, coordinates are normalized: (0,0) is center
264
286
  // x range [-1, 1] (left to right), y range [-1, 1] (bottom to top)
265
287
  val overlaySettings = try {
266
288
  StaticOverlaySettings.Builder()
267
- .setScale(scale, scale) // Scale uniformly to match video width
268
289
  .setOverlayFrameAnchor(0f, -1f) // Anchor at bottom-center of watermark
269
290
  .setBackgroundFrameAnchor(0f, -1f) // Position at very bottom of video
270
291
  .build()
271
292
  } catch (e: Exception) {
272
- watermarkBitmap.recycle()
293
+ scaledWatermark.recycle()
273
294
  promise.reject("STEP9_OVERLAY_SETTINGS_ERROR", "[Step 9] Failed to create overlay settings: ${e.message}", e)
274
295
  return
275
296
  }
276
297
 
277
- // Step 10: Create the bitmap overlay with settings
298
+ // Step 10: Create the bitmap overlay with pre-scaled bitmap
278
299
  val bitmapOverlay = try {
279
300
  BitmapOverlay.createStaticBitmapOverlay(
280
- watermarkBitmap,
301
+ scaledWatermark,
281
302
  overlaySettings
282
303
  )
283
304
  } catch (e: Exception) {
284
- watermarkBitmap.recycle()
305
+ scaledWatermark.recycle()
285
306
  promise.reject("STEP10_BITMAP_OVERLAY_ERROR", "[Step 10] Failed to create bitmap overlay: ${e.message}", e)
286
307
  return
287
308
  }
@@ -290,7 +311,7 @@ class ExpoVideoWatermarkModule : Module() {
290
311
  val overlayEffect = try {
291
312
  OverlayEffect(ImmutableList.of<TextureOverlay>(bitmapOverlay))
292
313
  } catch (e: Exception) {
293
- watermarkBitmap.recycle()
314
+ scaledWatermark.recycle()
294
315
  promise.reject("STEP11_OVERLAY_EFFECT_ERROR", "[Step 11] Failed to create overlay effect: ${e.message}", e)
295
316
  return
296
317
  }
@@ -310,11 +331,24 @@ class ExpoVideoWatermarkModule : Module() {
310
331
  .setEffects(effects)
311
332
  .build()
312
333
  } catch (e: Exception) {
313
- watermarkBitmap.recycle()
334
+ scaledWatermark.recycle()
314
335
  promise.reject("STEP14_EDITED_MEDIA_ERROR", "[Step 14] Failed to create edited media item: ${e.message}", e)
315
336
  return
316
337
  }
317
338
 
339
+ // Step 14b: Create composition with HDR to SDR tone mapping
340
+ // Use MediaCodec-based tone mapping (avoids OpenGL frame processing issues on some devices)
341
+ val composition = try {
342
+ Composition.Builder(listOf(editedMediaItem))
343
+ .setHdrMode(Composition.HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_MEDIACODEC)
344
+ .build()
345
+ } catch (e: Exception) {
346
+ scaledWatermark.recycle()
347
+ promise.reject("STEP14B_COMPOSITION_ERROR", "[Step 14b] Failed to create composition: ${e.message}", e)
348
+ return
349
+ }
350
+ Log.d(TAG, "[Step 14b] Created composition with HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_MEDIACODEC")
351
+
318
352
  // Handler for main thread callbacks
319
353
  val mainHandler = Handler(Looper.getMainLooper())
320
354
 
@@ -338,7 +372,7 @@ class ExpoVideoWatermarkModule : Module() {
338
372
  "averageAudioBitrate: ${exportResult.averageAudioBitrate}, " +
339
373
  "averageVideoBitrate: ${exportResult.averageVideoBitrate}, " +
340
374
  "videoFrameCount: ${exportResult.videoFrameCount}")
341
- watermarkBitmap.recycle()
375
+ scaledWatermark.recycle()
342
376
 
343
377
  // Step 16: Re-encode to H.265 if device supports HEVC encoder
344
378
  val supportsHevc = hasHevcEncoder()
@@ -452,7 +486,8 @@ class ExpoVideoWatermarkModule : Module() {
452
486
  appendLine("Input path: $cleanVideoPath")
453
487
  appendLine()
454
488
  appendLine("--- Watermark Info ---")
455
- appendLine("Dimensions: ${watermarkWidth.toInt()}x${watermarkHeight.toInt()}")
489
+ appendLine("Original dimensions: ${originalWidth}x${originalHeight}")
490
+ appendLine("Scaled dimensions: ${targetWidth}x${targetHeight}")
456
491
  appendLine("Bitmap info: $bitmapInfo")
457
492
  appendLine("Scale factor: $scale")
458
493
  appendLine()
@@ -478,14 +513,14 @@ class ExpoVideoWatermarkModule : Module() {
478
513
  causeLevel++
479
514
  }
480
515
 
481
- watermarkBitmap.recycle()
516
+ scaledWatermark.recycle()
482
517
 
483
518
  // Reject with comprehensive error message
484
519
  val errorMessage = "[Step 15] Transform failed - " +
485
520
  "ErrorCode: $errorCodeName (${exportException.errorCode}), " +
486
521
  "Device: ${Build.MANUFACTURER} ${Build.MODEL} (API ${Build.VERSION.SDK_INT}), " +
487
522
  "Video: ${videoWidth.toInt()}x${videoHeight.toInt()} $mimeType, " +
488
- "Watermark: ${watermarkWidth.toInt()}x${watermarkHeight.toInt()}, " +
523
+ "Watermark: ${originalWidth}x${originalHeight} -> ${targetWidth}x${targetHeight}, " +
489
524
  "Scale: $scale, " +
490
525
  "Message: ${exportException.message ?: "Unknown error"}"
491
526
 
@@ -499,13 +534,13 @@ class ExpoVideoWatermarkModule : Module() {
499
534
  .build()
500
535
 
501
536
  Log.d(TAG, "[Step 15] Transformer built, starting export...")
502
- transformer.start(editedMediaItem, cleanOutputPath)
537
+ transformer.start(composition, cleanOutputPath)
503
538
  Log.d(TAG, "[Step 15] Transformer.start() called, waiting for completion...")
504
539
  } catch (e: Exception) {
505
540
  Log.e(TAG, "[Step 15] Exception building/starting transformer", e)
506
541
  Log.e(TAG, "[Step 15] Device info: $deviceInfo")
507
542
  Log.e(TAG, "[Step 15] GL info: $glInfo")
508
- watermarkBitmap.recycle()
543
+ scaledWatermark.recycle()
509
544
  promise.reject(
510
545
  "STEP15_TRANSFORMER_BUILD_ERROR",
511
546
  "[Step 15] Failed to build/start transformer on ${Build.MANUFACTURER} ${Build.MODEL}: ${e.message}",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stefanmartin/expo-video-watermark",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Creating video watermarks on locally stored videos",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",