Address grill feedback: BT.709 tagging, per-job cancel, frameRateCap clamp

B1: Android encoder format now sets KEY_COLOR_STANDARD = BT709,
KEY_COLOR_TRANSFER = SDR_VIDEO, KEY_COLOR_RANGE = LIMITED (API 24+).
Previously the encoder inherited or emitted default color metadata,
which meant HDR sources produced incorrectly-tagged output on Android.
iOS already had AVVideoColorPropertiesKey set correctly.

B2: Replace single currentCompressor reference with a per-job map
keyed by jobId. The cancel function now takes a jobId and only cancels
that specific job. Prevents the previous race where a second compress
call overwrote the reference and made the first job uncancellable.

B3: Clamp frameRateCap to >= 1 at the native module boundary. Previously
a value of 0 from JS would cause divide-by-zero (Android Long division
ArithmeticException, iOS CMTime Infinity / Int32(0) frameDuration trap).
This commit is contained in:
vineyardbovines
2026-06-18 17:54:08 -04:00
parent 3d28b11ae3
commit 434d56ad00
5 changed files with 40 additions and 18 deletions
@@ -2,9 +2,10 @@ package expo.modules.blueskyvideocompress
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import java.util.concurrent.ConcurrentHashMap
class ExpoBlueskyVideoCompressModule : Module() {
private var currentCompressor: VideoCompressor? = null
private val activeCompressors = ConcurrentHashMap<Int, VideoCompressor>()
override fun definition() = ModuleDefinition {
Name("ExpoBlueskyVideoCompress")
@@ -23,7 +24,7 @@ class ExpoBlueskyVideoCompressModule : Module() {
val targetBitrate = (options["targetBitrate"] as? Number)?.toInt() ?: 0
val maxSize = (options["maxSize"] as? Number)?.toInt() ?: 1920
val codecPref = (options["codec"] as? String) ?: "auto"
val frameRateCap = (options["frameRateCap"] as? Number)?.toInt() ?: 30
val frameRateCap = ((options["frameRateCap"] as? Number)?.toInt() ?: 30).coerceAtLeast(1)
val jobId = (options["jobId"] as? Number)?.toInt() ?: 0
val compressor = VideoCompressor(
@@ -42,21 +43,20 @@ class ExpoBlueskyVideoCompressModule : Module() {
}
)
currentCompressor = compressor
activeCompressors[jobId] = compressor
try {
val result = compressor.compress()
currentCompressor = null
activeCompressors.remove(jobId)
return@AsyncFunction result
} catch (e: Exception) {
currentCompressor = null
activeCompressors.remove(jobId)
throw e
}
}
Function("cancel") {
currentCompressor?.cancel()
currentCompressor = null
Function("cancel") { jobId: Int ->
activeCompressors.remove(jobId)?.cancel()
}
}
}
@@ -148,6 +148,11 @@ class VideoCompressor(
)
setInteger(MediaFormat.KEY_FRAME_RATE, frameRateCap)
setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, I_FRAME_INTERVAL)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
setInteger(MediaFormat.KEY_COLOR_STANDARD, MediaFormat.COLOR_STANDARD_BT709)
setInteger(MediaFormat.KEY_COLOR_TRANSFER, MediaFormat.COLOR_TRANSFER_SDR_VIDEO)
setInteger(MediaFormat.KEY_COLOR_RANGE, MediaFormat.COLOR_RANGE_LIMITED)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
setInteger(MediaFormat.KEY_PRIORITY, 0)
setInteger(MediaFormat.KEY_OPERATING_RATE, frameRateCap)