From 06e9df3f3b284bfb1654054253db9c3cf7117b67 Mon Sep 17 00:00:00 2001 From: hm21 Date: Tue, 28 Jul 2026 17:48:23 +0200 Subject: [PATCH 1/3] feat: chroma key (greenscreen) for the render pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes a solid-colored background with a soft edge and spill suppression, and fills it with a color, an image, or — in a VideoComposition — the layer below, so a video can sit behind the screen. Settable on VideoRenderData, VideoLayer and VideoSegment, resolved per clip as segment -> layer -> global. The whole key (matte alpha and despilled color) is a pure function of RGB, which is what lets Apple do it through a CIColorCube with varying, premultiplied alpha — no CIKernel and no Metal, neither of which the repo has today. Android needs a real shader, because SingleColorLut cannot produce alpha; ChromaKeyEffect is modelled on the existing VideoCompositionTransformation. Both platforms run the same BT.601 chroma-distance formula, pinned by an identical golden table in the Kotlin and Swift unit tests so a drift in either fails fast instead of surfacing as "it looks different on iOS". ChromaKey.autoDetect measures the key color and similarity off the footage rather than guessing. A constant sits beside the *recorded* screen — a studio screen painted SMPTE green recorded as 0xFF2A9D37, a chroma distance of 0.12 — and that offset has to be paid for with a wider similarity, which is the margin that protects the subject. Measured on the demo clip: against the constant the screen sat 0.10-0.20 away and needed similarity 0.20; against its own measured color it sat within 0.05 and 0.10 sufficed. Detection runs through the existing thumbnail pipeline, so it is pure Dart and touches no native code. greenScreen()/blueScreen() presets carry the measured values. Blue is not green with a different hue: it separates skin better (0.41-0.46 vs 0.38-0.42) but denim (0.19), blue eyes (0.20) and light blue shirts (0.21) all sit inside the green-tuned default, so the blue preset keys tighter and despills more gently. Two pre-existing bugs surfaced and are fixed here: - VideoCompositionTransformation blended each composition layer against an already-transparent framebuffer, squaring its alpha. A no-op while every layer was opaque; load-bearing once a keyed layer arrives with a soft edge. - EditorVideo.safeFilePath named its temp file from the millisecond alone, so sources resolved concurrently (as VideoRenderData.toAsyncMap does) could overwrite each other. Verified end to end on macOS and a Galaxy S25: 14 chroma-key integration tests plus the existing render, composition, image-layer, layer-animation and render-pixel suites. --- CHANGELOG.md | 7 + README.md | 45 ++ .../src/features/render/RenderVideo.kt | 39 +- .../features/render/helpers/ApplyChromaKey.kt | 56 ++ .../render/helpers/ApplyComposition.kt | 3 +- .../render/helpers/ChromaKeyEffect.kt | 249 ++++++++ .../features/render/helpers/ChromaKeyMath.kt | 90 +++ .../render/helpers/CompositionBuilder.kt | 1 + .../helpers/LayeredCompositionBuilder.kt | 26 +- .../helpers/VideoCompositionTransformation.kt | 20 +- .../render/helpers/VideoSequenceBuilder.kt | 27 + .../features/render/models/RenderConfig.kt | 132 +++- .../render/helpers/ChromaKeyMathTest.kt | 261 ++++++++ .../shared/features/render/RenderVideo.swift | 61 +- .../render/helpers/ApplyChromaKey.swift | 175 ++++++ .../render/helpers/ApplyComposition.swift | 8 +- .../render/helpers/BitrateCapPolicy.swift | 2 + .../render/helpers/CompositionBuilder.swift | 50 +- .../helpers/LayeredCompositionBuilder.swift | 23 +- .../render/helpers/VideoSequenceBuilder.swift | 3 + .../features/render/models/RenderConfig.swift | 103 +++- .../features/render/models/VideoClip.swift | 10 +- .../render/models/VideoCompositorConfig.swift | 9 + .../render/utils/VideoCompositor.swift | 193 +++++- example/assets/greenscreen.mp4 | Bin 0 -> 337789 bytes example/integration_test/chroma_key_test.dart | 575 ++++++++++++++++++ .../lib/core/constants/example_constants.dart | 4 + .../chroma_key/chroma_key_example_page.dart | 382 ++++++++++++ example/lib/main.dart | 6 + example/macos/RunnerTests/RunnerTests.swift | 214 +++++++ lib/core/models/video/chroma_key_model.dart | 467 ++++++++++++++ lib/core/models/video/editor_video_model.dart | 12 +- lib/core/models/video/video_layer_model.dart | 46 +- .../models/video/video_render_data_model.dart | 39 ++ .../models/video/video_segment_model.dart | 31 +- lib/core/utils/chroma_key_detector.dart | 221 +++++++ lib/pro_video_editor.dart | 2 + pubspec.yaml | 2 +- .../models/video/chroma_key_model_test.dart | 216 +++++++ .../video/video_composition_model_test.dart | 36 ++ .../video/video_render_data_model_test.dart | 86 +++ .../video/video_segment_model_test.dart | 49 ++ test/core/utils/chroma_key_detector_test.dart | 298 +++++++++ ...ideo_editor_method_channel_test.mocks.dart | 3 + 44 files changed, 4221 insertions(+), 61 deletions(-) create mode 100644 android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyChromaKey.kt create mode 100644 android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ChromaKeyEffect.kt create mode 100644 android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ChromaKeyMath.kt create mode 100644 android/src/test/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ChromaKeyMathTest.kt create mode 100644 darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/ApplyChromaKey.swift create mode 100644 example/assets/greenscreen.mp4 create mode 100644 example/integration_test/chroma_key_test.dart create mode 100644 example/lib/features/chroma_key/chroma_key_example_page.dart create mode 100644 lib/core/models/video/chroma_key_model.dart create mode 100644 lib/core/utils/chroma_key_detector.dart create mode 100644 test/core/models/video/chroma_key_model_test.dart create mode 100644 test/core/utils/chroma_key_detector_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 76e3f7fc..e50731ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 2.11.0 +- **FEAT**(android, iOS, macOS): Add `ChromaKey` — green-screen removal with a soft edge and spill suppression. The keyed area becomes a `backgroundColor`, a `backgroundImage`, or (in a `VideoComposition`) the layer below, so a video can sit behind the screen. Settable on `VideoRenderData`, `VideoLayer` and `VideoSegment`, resolved per clip as segment → layer → global. Note that H.264/HEVC carry no alpha: on the single-track `videoSegments` path a key without a background is flattened to black. +- **FEAT**: `ChromaKey.autoDetect(video)` measures the key color and `similarity` off the footage, for any saturated screen hue. A constant key sits beside the *recorded* screen and has to spend on `similarity` the margin that protects the subject. `ChromaKey.detect` returns the raw measurement. Costs one thumbnail decode, no render. +- **FEAT**: `ChromaKey.greenScreen()` and `ChromaKey.blueScreen()` presets. Blue separates skin better but crowds denim, blue eyes and light blue shirts, so it keys tighter and despills more gently. +- **FIX**(android): A `VideoComposition` layer no longer squares its own alpha. `VideoCompositionTransformation` blended against an already-transparent framebuffer, which thinned any partially transparent edge. +- **FIX**(android, iOS, macOS): Two sources resolved in the same millisecond no longer collide on one temp file in `EditorVideo.safeFilePath`. + ## 2.10.0 - **FEAT**(iOS, macOS): Add `VideoRenderData.trimToCommonTrackEnd` (default `false`). Ends each clip where both its video and audio track still have content, so an export no longer finishes on a stretch of missing audio — the seam a looping player replays every cycle. Leave it off when a clip is meant to outlast its own audio. diff --git a/README.md b/README.md index 890a75fd..7ad8fa74 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,7 @@ The ProVideoEditor is a Flutter widget designed for video editing within your ap - 🎞️ **Clip Transitions**: Add transitions between adjacent clips — `dissolve`, `fadeToBlack`, `fadeToWhite`, `slide`, `push`, and `wipe` — with configurable duration, easing curve, and direction. - 🧮 **Color Matrix**: Apply one or multiple 4x5 color matrices (e.g., for filters). - 💧 **Blur**: Add a blur effect to the video. +- 🟩 **Chroma Key**: Remove a green (or blue, or any saturated hue) screen with a soft edge and spill suppression, and fill it with a color, an image, or — in a `VideoComposition` — the layer below. `ChromaKey.autoDetect` measures the key straight off the footage; `greenScreen()`/`blueScreen()` presets are there when you already know. Configurable globally, per `VideoLayer`, or per `VideoSegment`. - 📡 **Bitrate**: Cap the video bitrate. Sources already below the cap are exported losslessly over the fast path; sources above it are re-encoded down to the cap. If constant bitrate (CBR) isn't supported, it will gracefully fall back to the next available mode. - 🌐 **Streaming Optimization**: Optimize video for progressive playback by placing metadata (moov atom) at the start of the file. @@ -160,6 +161,7 @@ The ProVideoEditor is a Flutter widget designed for video editing within your ap | `Multiple ColorMatrix 4x5` | ✅ | ✅ | ✅ | ❌ | ❌ | 🚫 | | `Cancel export task` | ✅ | ✅ | ✅ | ❌ | ❌ | 🚫 | | `Blur background` | 🧪 | 🧪 | 🧪 | ❌ | ❌ | 🚫 | +| `Chroma Key (Greenscreen)` | ✅ | ✅ | ✅ | ❌ | ❌ | 🚫 | | `Custom Audio Tracks` | ✅ | ✅ | ✅ | ❌ | ❌ | 🚫 | | `Merge Videos` | ✅ | ✅ | ✅ | ❌ | ❌ | 🚫 | | `Stop-Motion (Images→Video)`| ✅ | ✅ | ✅ | ❌ | ❌ | 🚫 | @@ -620,6 +622,14 @@ var task = VideoRenderData( ColorFilter(matrix: [ 1.0, 0.0, 0.0, 0.0, 50.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0 ]), ColorFilter(matrix: [ 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0 ]), ], + chromaKey: const ChromaKey( + // Defaults to SMPTE "chroma key green"; only the hue is used. + color: Color(0xFF00B140), + similarity: 0.20, // raise it if the screen survives the key + smoothness: 0.08, // width of the soft edge + spill: 0.5, // pull the green cast off the subject's edges + backgroundColor: Color(0xFF000000), + ), ); Uint8List result = await ProVideoEditor.instance.renderVideo(task); @@ -627,6 +637,41 @@ Uint8List result = await ProVideoEditor.instance.renderVideo(task); /// Note: Blur is an experimental feature (🧪 in platform matrix) /// The blur effect may render differently than in Flutter's preview. +/// Chroma key: the most reliable key is measured, not guessed. Paint, lighting +/// and the camera all shift the recorded screen away from any constant, and +/// that shift has to be paid for with a wider `similarity` — the very margin +/// that protects the subject. On a real studio clip the SMPTE constant sat +/// 0.10–0.20 away from the screen and needed `similarity: 0.20`; the measured +/// color sat within 0.05 and 0.10 was plenty. +final key = await ChromaKey.autoDetect( + greenScreenVideo, + backgroundColor: Colors.black, +); + +/// When you already know the screen, the presets carry the measured values. +/// Note that blue is not green with a different hue: it separates skin better, +/// but denim (0.19), blue eyes (0.20) and light blue shirts (0.21) all crowd +/// it, so `blueScreen()` keys tighter and despills more gently. +const greenKey = ChromaKey.greenScreen(backgroundColor: Color(0xFF000000)); +const blueKey = ChromaKey.blueScreen(backgroundColor: Color(0xFF000000)); + +/// Chroma key: to put a *video* behind the green screen, leave the key +/// transparent and place the keyed clip on a layer above another one. In the +/// single-track `videoSegments` path there is nothing underneath and the codec +/// carries no alpha, so a key there needs a `backgroundColor` or +/// `backgroundImage`. +final task = VideoRenderData( + composition: VideoComposition( + layers: [ + VideoLayer(clips: [VideoSegment(video: backgroundVideo)]), + VideoLayer( + clips: [VideoSegment(video: greenScreenVideo)], + chromaKey: const ChromaKey(), + ), + ], + ), +); + /// Listen progress StreamBuilder( stream: ProVideoEditor.instance.progressStreamById(task.id), diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/RenderVideo.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/RenderVideo.kt index 02f05aec..9d5dd987 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/RenderVideo.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/RenderVideo.kt @@ -26,6 +26,7 @@ import ch.waio.pro_video_editor.src.features.render.helpers.VideoReverser import ch.waio.pro_video_editor.src.features.render.helpers.ClipTransitionGeometry import ch.waio.pro_video_editor.src.features.render.helpers.ClipTransitionRenderer import ch.waio.pro_video_editor.src.features.render.helpers.MediaInfoExtractor +import ch.waio.pro_video_editor.src.features.render.models.ChromaKeyConfig import ch.waio.pro_video_editor.src.features.render.models.CodecResourceExhaustedException import ch.waio.pro_video_editor.src.features.render.models.RenderConfig import ch.waio.pro_video_editor.src.features.render.models.RenderJobHandle @@ -89,7 +90,17 @@ class RenderVideo(private val context: Context) { val hasBlur = config.blur != null && config.blur > 0.0 val hasColorFilters = config.colorFilters.isNotEmpty() - return hasImageLayers || hasBlur || hasColorFilters + // The chroma key is an ES 2.0 SDR shader, so a 10-bit HDR source has to + // be transcoded down first. Checked at every level it can be set: + // globally, per clip, and — for the layered path — per layer and per + // layer clip. + val hasChromaKey = config.chromaKey != null || + config.videoClips.any { it.chromaKey != null } || + config.composition?.layers?.any { layer -> + layer.chromaKey != null || layer.clips.any { it.chromaKey != null } + } == true + + return hasImageLayers || hasBlur || hasColorFilters || hasChromaKey } /** @@ -502,6 +513,7 @@ class RenderVideo(private val context: Context) { inputPath = rendered.outputPath, startUs = 0L, endUs = rendered.durationUs.takeIf { it > 0 }, + chromaKey = blendChromaKey(current, next, "$i"), ) ) // Trim the incoming head in place; it keeps its own speed/transition. @@ -592,6 +604,7 @@ class RenderVideo(private val context: Context) { inputPath = rendered.outputPath, startUs = 0L, endUs = rendered.durationUs.takeIf { it > 0 }, + chromaKey = blendChromaKey(last, first, "loop wrap"), ) ) } else { @@ -605,6 +618,30 @@ class RenderVideo(private val context: Context) { return result } + /** + * The chroma key to apply to a pre-rendered overlap blend. + * + * The blend is composed from the raw sources by [ClipTransitionRenderer], + * which knows nothing about keying, so the key has to be re-applied to its + * output. That only has a defined meaning when both sides key the same way: + * blending a keyed clip with an unkeyed one produces mixed colors that no + * single key can undo. Mismatched sides are therefore left unkeyed and + * reported, rather than silently keyed with one side's settings. + */ + private fun blendChromaKey( + outgoing: VideoClip, + incoming: VideoClip, + boundary: String, + ): ChromaKeyConfig? { + if (outgoing.chromaKey == incoming.chromaKey) return outgoing.chromaKey + Log.w( + RENDER_TAG, + "Chroma key: the two clips at boundary $boundary use different keys; " + + "the pre-rendered transition blend is emitted unkeyed" + ) + return null + } + /** * Internal render implementation after optional pre-transcoding. */ diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyChromaKey.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyChromaKey.kt new file mode 100644 index 00000000..f00a9508 --- /dev/null +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyChromaKey.kt @@ -0,0 +1,56 @@ +import androidx.media3.common.Effect +import androidx.media3.common.util.UnstableApi +import ch.waio.pro_video_editor.src.features.render.helpers.ChromaKeyEffect +import ch.waio.pro_video_editor.src.features.render.models.ChromaKeyConfig +import ch.waio.pro_video_editor.src.shared.logging.PluginLog as Log + +/** + * Adds a chroma key ("green screen" removal) to a clip's effect chain. + * + * Must be added **before** rotation, flip, the color LUT and blur, so the key + * sees the original decoded colors. Media3's LUT shader preserves alpha + * (`gl_FragColor.a = inputColor.a`), so a color filter after the key is safe. + * + * @param videoEffects List to add the effect to + * @param config The resolved key (clip ?: layer ?: global), or null for none + * @param flattenTransparency Whether a key without a background should be + * filled with opaque black instead of left transparent. True on the + * single-track path — see below. + */ +@UnstableApi +fun applyChromaKey( + videoEffects: MutableList, + config: ChromaKeyConfig?, + flattenTransparency: Boolean = false, +) { + if (config == null) return + + // H.264/HEVC carry no alpha channel, and the single-track path has no layer + // underneath to blend into, so "transparent" has no meaning there. Left + // alone the two platforms would disagree visibly: Apple's premultiplied + // color cube writes rgb*0 and the area comes out black, while this shader + // writes straight alpha and leaves the RGB untouched, so the screen would + // simply stay green — "nothing happened". Substituting opaque black makes + // both platforms produce the documented result. + val effective = if (flattenTransparency && config.isTransparent) { + config.copy(backgroundColor = OPAQUE_BLACK) + } else { + config + } + + val background = when { + effective.backgroundImageData != null -> "image" + effective.backgroundColor != null -> "color" + else -> "transparent" + } + Log.d( + RENDER_TAG, + "Applying chroma key: similarity=${effective.similarity} " + + "smoothness=${effective.smoothness} spill=${effective.spill} " + + "background=$background" + ) + + videoEffects += ChromaKeyEffect(effective) +} + +private const val OPAQUE_BLACK = 0xFF000000.toInt() diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyComposition.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyComposition.kt index 519d0cc8..25cf46b0 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyComposition.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyComposition.kt @@ -69,7 +69,8 @@ fun applyComposition( imageLayers = imageLayerConfigs, audioTracks = config.audioTracks, globalStartUs = config.startUs, - globalEndUs = config.endUs + globalEndUs = config.endUs, + globalChromaKey = config.chromaKey ) return CompositionResult( layeredBuilder.build(), diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ChromaKeyEffect.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ChromaKeyEffect.kt new file mode 100644 index 00000000..745120f6 --- /dev/null +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ChromaKeyEffect.kt @@ -0,0 +1,249 @@ +package ch.waio.pro_video_editor.src.features.render.helpers + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.opengl.GLES20 +import androidx.media3.common.VideoFrameProcessingException +import androidx.media3.common.util.GlProgram +import androidx.media3.common.util.GlUtil +import androidx.media3.common.util.Size +import androidx.media3.common.util.UnstableApi +import androidx.media3.effect.BaseGlShaderProgram +import androidx.media3.effect.GlEffect +import androidx.media3.effect.GlShaderProgram +import ch.waio.pro_video_editor.src.features.render.models.ChromaKeyConfig + +/** + * Removes a solid-colored background ("green screen") from every frame. + * + * Pixels whose chroma sits within [ChromaKeyConfig.similarity] of the key color + * are removed, with a [ChromaKeyConfig.smoothness]-wide soft edge, and the key's + * color cast is pulled back out of the pixels that remain + * ([ChromaKeyConfig.spill]). + * + * The removed area becomes, in this order of precedence: the background image, + * the background color, or transparency. Transparency only means something in + * the layered path, where Media3's compositor blends the sequences — see + * `applyChromaKey`, which substitutes opaque black on the single-track path so + * both platforms agree. + * + * The formula is specified once and implemented twice; see [ChromaKeyMath], + * which this shader mirrors and which the unit tests pin against the Swift + * implementation. + * + * Alpha is **straight**, not premultiplied — that is Media3's convention + * throughout (`AlphaScale`, the LUT shader, and `DefaultCompositorGlProgram`'s + * `glBlendFuncSeparate(SRC_ALPHA, ONE_MINUS_SRC_ALPHA, ONE, ONE_MINUS_SRC_ALPHA)`). + * Apple's color cube stores premultiplied entries instead, because that is what + * `CIColorCube` requires. Both are correct for their platform; do not align them. + */ +@UnstableApi +class ChromaKeyEffect(private val config: ChromaKeyConfig) : GlEffect { + + override fun toGlShaderProgram(context: Context, useHdr: Boolean): GlShaderProgram { + if (useHdr) { + // The shader is an ES 2.0 SDR program. HEVC 10-bit/HDR sources are + // pre-transcoded to 8-bit SDR by VideoTranscoder before they reach + // here (see RenderVideo.hasGpuEffects), so this is a guard against + // that gate regressing rather than an expected path. + throw VideoFrameProcessingException( + "Chroma key does not support HDR input" + ) + } + return ChromaKeyShaderProgram(useHdr, config) + } + + @UnstableApi + private class ChromaKeyShaderProgram( + useHdr: Boolean, + private val config: ChromaKeyConfig + ) : BaseGlShaderProgram(useHdr, /* texturePoolCapacity= */ 1) { + + private val glProgram: GlProgram + + /** Decoded once at construction, never per frame. */ + private val backgroundBitmap: Bitmap? = config.backgroundImageData?.let { bytes -> + BitmapFactory.decodeByteArray(bytes, 0, bytes.size) + } + + /** + * Background texture id. A 1x1 placeholder is uploaded when there is no + * background image, because Media3's [GlProgram] requires every sampler + * uniform to be bound before a draw. + */ + private var backgroundTexId: Int = -1 + + private val bgMode: Int = when { + backgroundBitmap != null -> BG_IMAGE + config.backgroundColor != null -> BG_COLOR + else -> BG_TRANSPARENT + } + + companion object { + private const val BG_TRANSPARENT = 0 + private const val BG_COLOR = 1 + private const val BG_IMAGE = 2 + + private const val VERTEX_SHADER_SOURCE = + "attribute vec4 aFramePosition;\n" + + "attribute vec4 aTexSamplingCoord;\n" + + "varying vec2 vTexSamplingCoord;\n" + + "void main() {\n" + + " gl_Position = aFramePosition;\n" + + " vTexSamplingCoord = aTexSamplingCoord.xy;\n" + + "}" + + // highp matters here: the Cb/Cr deltas the key discriminates are on + // the order of 1e-3, and mediump only guarantees ~10 mantissa bits. + private const val FRAGMENT_SHADER_SOURCE = + "#ifdef GL_FRAGMENT_PRECISION_HIGH\n" + + "precision highp float;\n" + + "#else\n" + + "precision mediump float;\n" + + "#endif\n" + + "uniform sampler2D uTexSampler;\n" + + "uniform sampler2D uBgSampler;\n" + + "uniform vec2 uKeyCbCr;\n" + + "uniform vec2 uKeyDir;\n" + + "uniform float uSimilarity;\n" + + "uniform float uSmoothness;\n" + + "uniform float uSpill;\n" + + "uniform vec3 uBgColor;\n" + + "uniform int uBgMode;\n" + + "varying vec2 vTexSamplingCoord;\n" + + "void main() {\n" + + " vec4 src = texture2D(uTexSampler, vTexSamplingCoord);\n" + + " vec3 rgb = src.rgb;\n" + + // BT.601 luma and chroma, matching ChromaKeyMath exactly. + " float y = dot(rgb, vec3(0.299, 0.587, 0.114));\n" + + " vec2 cbcr = vec2(\n" + + " dot(rgb, vec3(-0.168736, -0.331264, 0.5)),\n" + + " dot(rgb, vec3(0.5, -0.418688, -0.081312)));\n" + + // Matte: distance in the chroma plane, ramped by smoothstep. + " float d = distance(cbcr, uKeyCbCr);\n" + + " float a = smoothstep(uSimilarity," + + " uSimilarity + max(uSmoothness, 1e-4), d);\n" + + // Spill: remove the chroma component pointing at the key hue, + // keeping y so a despilled pixel never darkens. Only pixels + // leaning toward the key (projection > 0) are touched. + " float projection = dot(cbcr, uKeyDir);\n" + + " if (uSpill > 0.0 && projection > 0.0) {\n" + + " vec2 c = cbcr - uKeyDir * projection * uSpill;\n" + + " rgb = clamp(vec3(\n" + + " y + 1.402 * c.y,\n" + + " y - 0.344136 * c.x - 0.714136 * c.y,\n" + + " y + 1.772 * c.x), 0.0, 1.0);\n" + + " }\n" + + // Fold in the frame's own alpha, so an already-transparent + // pixel (a gap frame carrying AlphaScale(0)) stays transparent. + " float outA = src.a * a;\n" + + " if (uBgMode == 2) {\n" + + " vec3 bg = texture2D(uBgSampler, vTexSamplingCoord).rgb;\n" + + " gl_FragColor = vec4(mix(bg, rgb, outA), src.a);\n" + + " } else if (uBgMode == 1) {\n" + + " gl_FragColor = vec4(mix(uBgColor, rgb, outA), src.a);\n" + + " } else {\n" + + " gl_FragColor = vec4(rgb, outA);\n" + + " }\n" + + "}" + } + + init { + try { + glProgram = GlProgram(VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE) + } catch (e: Exception) { + throw VideoFrameProcessingException(e) + } + } + + /** Pass-through: keying does not change the frame geometry. */ + override fun configure(inputWidth: Int, inputHeight: Int): Size { + return Size(inputWidth, inputHeight) + } + + override fun drawFrame(inputTexId: Int, presentationTimeUs: Long) { + try { + glProgram.use() + + if (backgroundTexId == -1) { + backgroundTexId = GlUtil.createTexture( + backgroundBitmap ?: onePixelPlaceholder() + ) + } + + glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, 0) + glProgram.setSamplerTexIdUniform("uBgSampler", backgroundTexId, 1) + + glProgram.setFloatsUniform( + "uKeyCbCr", + floatArrayOf(config.keyCb.toFloat(), config.keyCr.toFloat()) + ) + glProgram.setFloatsUniform( + "uKeyDir", + floatArrayOf(config.keyDirCb.toFloat(), config.keyDirCr.toFloat()) + ) + glProgram.setFloatUniform("uSimilarity", config.similarity.toFloat()) + glProgram.setFloatUniform("uSmoothness", config.smoothness.toFloat()) + glProgram.setFloatUniform("uSpill", config.spill.toFloat()) + glProgram.setFloatsUniform("uBgColor", backgroundColorRgb()) + glProgram.setIntUniform("uBgMode", bgMode) + + val vertexData = GlUtil.getNormalizedCoordinateBounds() + glProgram.setBufferAttribute( + "aFramePosition", vertexData, if (vertexData.size == 8) 2 else 4 + ) + val texData = GlUtil.getTextureCoordinateBounds() + glProgram.setBufferAttribute( + "aTexSamplingCoord", texData, if (texData.size == 8) 2 else 4 + ) + + glProgram.bindAttributesAndUniforms() + + // No glClear and no blending: BaseGlShaderProgram already + // cleared the target to (0,0,0,0) and this draw covers it + // entirely, so the fragment output must simply replace it. + // Blending here would blend against transparent black and + // square the alpha. The one real blend is Media3's own + // compositor, which uses glBlendFuncSeparate. + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4) + GlUtil.checkGlError() + } catch (e: Exception) { + throw VideoFrameProcessingException(e, presentationTimeUs) + } + } + + override fun release() { + super.release() + try { + if (backgroundTexId != -1) { + GlUtil.deleteTexture(backgroundTexId) + backgroundTexId = -1 + } + glProgram.delete() + } catch (e: Exception) { + throw VideoFrameProcessingException(e) + } + backgroundBitmap?.recycle() + } + + /** The background color as linear-free RGB in 0..1, or black. */ + private fun backgroundColorRgb(): FloatArray { + val argb = config.backgroundColor ?: return floatArrayOf(0f, 0f, 0f) + return floatArrayOf( + ((argb shr 16) and 0xFF) / 255f, + ((argb shr 8) and 0xFF) / 255f, + (argb and 0xFF) / 255f + ) + } + + /** + * A 1x1 transparent texture, bound to `uBgSampler` when there is no + * background image so the sampler uniform is never left unbound. + */ + private fun onePixelPlaceholder(): Bitmap = + Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888).apply { + eraseColor(android.graphics.Color.TRANSPARENT) + } + } +} diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ChromaKeyMath.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ChromaKeyMath.kt new file mode 100644 index 00000000..8695eba8 --- /dev/null +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ChromaKeyMath.kt @@ -0,0 +1,90 @@ +package ch.waio.pro_video_editor.src.features.render.helpers + +import ch.waio.pro_video_editor.src.features.render.models.ChromaKeyConfig +import kotlin.math.max +import kotlin.math.min +import kotlin.math.sqrt + +/** + * The chroma-key formula, in plain Kotlin. + * + * This is the single spec both platforms implement. The GPU actually runs it + * inside [ChromaKeyEffect]'s fragment shader; Apple bakes the very same math + * into a Core Image color cube (`ApplyChromaKey.swift`). This class exists so + * the formula can be unit-tested without a GL context, and so the Kotlin and + * Swift tests can assert the *same* golden table — a drift in either + * implementation then fails a fast test instead of an emulator run. + * + * Keep this file, the fragment shader and `chromaKeyed(r:g:b:_:)` in Swift in + * lockstep. All values are gamma-encoded, non-color-managed RGB in `0..1`. + */ +object ChromaKeyMath { + + /** BT.601 luma of a gamma-encoded RGB triple. */ + fun luma(r: Double, g: Double, b: Double): Double = + 0.299 * r + 0.587 * g + 0.114 * b + + /** + * BT.601 chroma projection of a gamma-encoded RGB triple, as `(Cb, Cr)`. + * + * The keyer measures distance in this plane. Note that this is a position, + * not a pure hue: Cb and Cr scale with brightness, so a dimly lit patch of + * the screen sits closer to neutral and further from the key point. The + * default `similarity` of 0.20 covers roughly 40%..100% of the screen's + * reference brightness. Same behaviour as FFmpeg's `chromakey` and OBS. + */ + fun chroma(r: Double, g: Double, b: Double): DoubleArray = doubleArrayOf( + -0.168736 * r - 0.331264 * g + 0.5 * b, + 0.5 * r - 0.418688 * g - 0.081312 * b, + ) + + /** Hermite smoothstep, matching GLSL's `smoothstep`. */ + fun smoothstep(edge0: Double, edge1: Double, x: Double): Double { + if (edge1 <= edge0) return if (x < edge0) 0.0 else 1.0 + val t = ((x - edge0) / (edge1 - edge0)).coerceIn(0.0, 1.0) + return t * t * (3 - 2 * t) + } + + /** + * Evaluates the key for one pixel. + * + * @return `[r, g, b, alpha]` with **straight** (not premultiplied) alpha, + * which is the convention Media3 uses end to end. + */ + fun evaluate(r: Double, g: Double, b: Double, config: ChromaKeyConfig): DoubleArray { + val c = chroma(r, g, b) + val dCb = c[0] - config.keyCb + val dCr = c[1] - config.keyCr + val distance = sqrt(dCb * dCb + dCr * dCr) + + // max() keeps a zero-width ramp from dividing by zero, matching the + // shader's `max(uSmoothness, 1e-4)`. + val alpha = smoothstep( + config.similarity, + config.similarity + max(config.smoothness, 1e-4), + distance, + ) + + // Spill suppression. `projection` is how far the pixel leans toward the + // key hue; only pixels leaning toward it (> 0) are touched, so a + // complementary color is never desaturated. Y stays untouched, so a + // despilled pixel never darkens. + val projection = c[0] * config.keyDirCb + c[1] * config.keyDirCr + if (config.spill <= 0.0 || projection <= 0.0) { + return doubleArrayOf(r, g, b, alpha) + } + + val y = luma(r, g, b) + val cb = c[0] - config.keyDirCb * projection * config.spill + val cr = c[1] - config.keyDirCr * projection * config.spill + + return doubleArrayOf( + clamp01(y + 1.402 * cr), + clamp01(y - 0.344136 * cb - 0.714136 * cr), + clamp01(y + 1.772 * cb), + alpha, + ) + } + + private fun clamp01(v: Double): Double = min(max(v, 0.0), 1.0) +} diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/CompositionBuilder.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/CompositionBuilder.kt index 08d0b006..a563e8d4 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/CompositionBuilder.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/CompositionBuilder.kt @@ -77,6 +77,7 @@ class CompositionBuilder( .setRotation(rotationDegrees) .setFlip(config.flipX, config.flipY) .setScale(config.scaleX, config.scaleY) + .setChromaKey(config.chromaKey) .setOutputResolution(config.outputWidth, config.outputHeight) .setCrop(config.cropWidth, config.cropHeight, config.cropX, config.cropY) .setTimedImageLayers(config.imageLayers.map { imageLayer -> diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/LayeredCompositionBuilder.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/LayeredCompositionBuilder.kt index 17e1670a..3217cab9 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/LayeredCompositionBuilder.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/LayeredCompositionBuilder.kt @@ -1,6 +1,7 @@ package ch.waio.pro_video_editor.src.features.render.helpers import RENDER_TAG +import applyChromaKey import android.content.Context import android.graphics.Bitmap import android.graphics.Color @@ -17,6 +18,7 @@ import androidx.media3.transformer.EditedMediaItem import androidx.media3.transformer.EditedMediaItemSequence import androidx.media3.transformer.Effects import ch.waio.pro_video_editor.src.features.render.models.AudioTrackConfig +import ch.waio.pro_video_editor.src.features.render.models.ChromaKeyConfig import ch.waio.pro_video_editor.src.features.render.models.CompositionConfig import ch.waio.pro_video_editor.src.features.render.models.SegmentTransformConfig import ch.waio.pro_video_editor.src.features.render.models.VideoClip @@ -55,7 +57,12 @@ class LayeredCompositionBuilder( /** Global trim start across the whole composition, in microseconds. */ private val globalStartUs: Long? = null, /** Global trim end across the whole composition, in microseconds. */ - private val globalEndUs: Long? = null + private val globalEndUs: Long? = null, + /** + * Chroma key for clips that carry neither their own nor a layer key. + * Resolved per clip as `clip ?: layer ?: global`; never merged. + */ + private val globalChromaKey: ChromaKeyConfig? = null ) { /** Temp files (source duplicates) to delete after export. */ val temporaryFiles: MutableList = mutableListOf() @@ -196,7 +203,8 @@ class LayeredCompositionBuilder( seqBuilder.addItem( buildClipItem( clip, srcStartUs, srcEndUs, effectivePath, placement, - displayW, displayH, layer.opacity, canvasW, canvasH + displayW, displayH, layer.opacity, canvasW, canvasH, + layer.chromaKey ) ) outputCursorUs += outputDurationUs @@ -299,7 +307,8 @@ class LayeredCompositionBuilder( displayH: Int, opacity: Float, canvasW: Int, - canvasH: Int + canvasH: Int, + layerChromaKey: ChromaKeyConfig? ): EditedMediaItem { val mediaItemBuilder = MediaItem.Builder().setUri(Uri.fromFile(File(inputPath))) if (srcStartUs != null || srcEndUs != null) { @@ -310,6 +319,17 @@ class LayeredCompositionBuilder( } val effects = mutableListOf() + + // Chroma key first: each layer is keyed on its own source frame, before + // it is placed on the canvas. Keying the composed canvas afterwards + // would be meaningless, since it is already opaque. + // + // flattenTransparency stays off here — this is the whole point of the + // layered path. Media3's compositor blends the sequences with + // glBlendFuncSeparate, so a transparent key really does let the layer + // below show through. + applyChromaKey(effects, clip.chromaKey ?: layerChromaKey ?: globalChromaKey) + val draw = placement.draw val clipBox = placement.clip effects += VideoCompositionTransformation( diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/VideoCompositionTransformation.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/VideoCompositionTransformation.kt index 05385128..49b1536f 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/VideoCompositionTransformation.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/VideoCompositionTransformation.kt @@ -23,6 +23,10 @@ import kotlin.math.roundToInt * rest stays transparent, so the default Media3 compositor can alpha-blend the * layers on top of each other. * + * The draw itself does **not** blend — see the note in `drawFrame`. Partial + * alpha (a chroma-keyed edge, an [androidx.media3.effect.AlphaScale]d layer) + * passes through unchanged and is blended once, by the Media3 compositor. + * * When a clip box ([clipX], [clipY], [clipWidth], [clipHeight]) is provided, the * draw is scissored to that box (canvas pixels, top-left origin). This clips * `cover` overflow so a scaled-up clip cannot bleed past its target rectangle @@ -99,9 +103,18 @@ class VideoCompositionTransformation( GLES20.glClearColor(0f, 0f, 0f, 0f) GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT) - // Enable alpha blending to support transparent layers/overlays. - GLES20.glEnable(GLES20.GL_BLEND) - GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA) + // No blending: this draw replaces. The target was just cleared + // to (0,0,0,0) and exactly one quad is drawn, so there is + // nothing to blend against — and GL_SRC_ALPHA/ + // GL_ONE_MINUS_SRC_ALPHA against a transparent destination + // *squares* the alpha (dst.a = a*a + 0*(1-a)). That was + // harmless while this shader only ever saw opaque input, but a + // chroma-keyed layer arrives with a soft, partially transparent + // edge, which blending here would darken and thin out. + // + // The one real blend is Media3's own DefaultCompositorGlProgram, + // which uses glBlendFuncSeparate(SRC_ALPHA, ONE_MINUS_SRC_ALPHA, + // ONE, ONE_MINUS_SRC_ALPHA) — correct straight-alpha source-over. val glMatrix = FloatArray(16) Matrix.setIdentityM(glMatrix, 0) @@ -170,7 +183,6 @@ class VideoCompositionTransformation( if (applyScissor) { GLES20.glDisable(GLES20.GL_SCISSOR_TEST) } - GLES20.glDisable(GLES20.GL_BLEND) GlUtil.checkGlError() } catch (e: Exception) { throw VideoFrameProcessingException(e, presentationTimeUs) diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/VideoSequenceBuilder.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/VideoSequenceBuilder.kt index ba01cec2..d24da5fe 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/VideoSequenceBuilder.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/VideoSequenceBuilder.kt @@ -3,6 +3,7 @@ package ch.waio.pro_video_editor.src.features.render.helpers import RENDER_TAG import android.content.Context import android.net.Uri +import applyChromaKey import applyScale import androidx.media3.common.C import androidx.media3.common.Effect @@ -18,6 +19,7 @@ import androidx.media3.effect.SpeedChangeEffect import androidx.media3.transformer.EditedMediaItem import androidx.media3.transformer.EditedMediaItemSequence import androidx.media3.transformer.Effects +import ch.waio.pro_video_editor.src.features.render.models.ChromaKeyConfig import ch.waio.pro_video_editor.src.features.render.models.LayerAnimationConfig import ch.waio.pro_video_editor.src.features.render.models.VideoClip import ch.waio.pro_video_editor.src.features.render.utils.getRotatedVideoDimensions @@ -60,6 +62,7 @@ class VideoSequenceBuilder( private var scaleY: Float? = null private var outputWidth: Int? = null private var outputHeight: Int? = null + private var globalChromaKey: ChromaKeyConfig? = null private val rotatedDimensionsCache = mutableMapOf>() data class CropConfig( @@ -104,6 +107,16 @@ class VideoSequenceBuilder( return this } + /** + * Sets the chroma key applied to every clip that carries none of its own. + * + * A [VideoClip.chromaKey] overrides this per clip; the two are never merged. + */ + fun setChromaKey(chromaKey: ChromaKeyConfig?): VideoSequenceBuilder { + this.globalChromaKey = chromaKey + return this + } + /** * Sets the exact output canvas size. When set, each clip is scaled to fit * inside it (preserving aspect ratio), centered, and padded with black. @@ -595,6 +608,20 @@ class VideoSequenceBuilder( // Build video effects val clipVideoEffects = mutableListOf() + + // Chroma key first, so it sees the original decoded colors — before + // rotation, flip, the color LUT and blur. A clip's own key wins over + // the global one; they are never merged. + // + // flattenTransparency is on because this is the single-track path: + // there is no layer underneath, so a key without a background is filled + // with opaque black instead (see applyChromaKey for why). + applyChromaKey( + clipVideoEffects, + clip.chromaKey ?: globalChromaKey, + flattenTransparency = true, + ) + clipVideoEffects.addAll(videoEffects) // Calculate video dimensions for image layer positioning diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/models/RenderConfig.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/models/RenderConfig.kt index 9effc8d5..aacc7487 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/models/RenderConfig.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/models/RenderConfig.kt @@ -61,7 +61,12 @@ data class VideoClip( /** Start position on the layer timeline in microseconds (composition only). */ val timelineStartUs: Long? = null, /** Placement within the composition canvas (composition only). */ - val transform: SegmentTransformConfig? = null + val transform: SegmentTransformConfig? = null, + /** + * Removes a solid-colored background from this clip. Overrides the layer's + * and the global key; null falls back to those. + */ + val chromaKey: ChromaKeyConfig? = null ) { companion object { /** Parses a clip from a platform-channel map. */ @@ -70,6 +75,8 @@ data class VideoClip( val transitionRaw = clipMap["transition"] as? Map @Suppress("UNCHECKED_CAST") val transformRaw = clipMap["transform"] as? Map + @Suppress("UNCHECKED_CAST") + val chromaKeyRaw = clipMap["chromaKey"] as? Map return VideoClip( inputPath = clipMap["inputPath"] as String, startUs = (clipMap["startUs"] as? Number)?.toLong(), @@ -79,7 +86,8 @@ data class VideoClip( reverseVideo = clipMap["reverseVideo"] as? Boolean ?: false, transition = transitionRaw?.let { TransitionConfig.fromMap(it) }, timelineStartUs = (clipMap["timelineStartUs"] as? Number)?.toLong(), - transform = transformRaw?.let { SegmentTransformConfig.fromMap(it) } + transform = transformRaw?.let { SegmentTransformConfig.fromMap(it) }, + chromaKey = ChromaKeyConfig.fromMap(chromaKeyRaw) ) } } @@ -128,7 +136,12 @@ data class SegmentTransformConfig( data class LayerConfig( val clips: List, val opacity: Float, - val transform: SegmentTransformConfig? + val transform: SegmentTransformConfig?, + /** + * Default chroma key for the clips on this layer. A clip's own key wins; + * null falls back to the global key. + */ + val chromaKey: ChromaKeyConfig? = null ) { companion object { fun fromMap(map: Map): LayerConfig? { @@ -138,10 +151,13 @@ data class LayerConfig( if (clips.isEmpty()) return null @Suppress("UNCHECKED_CAST") val transformRaw = map["transform"] as? Map + @Suppress("UNCHECKED_CAST") + val chromaKeyRaw = map["chromaKey"] as? Map return LayerConfig( clips = clips, opacity = (map["opacity"] as? Number)?.toFloat() ?: 1.0f, - transform = transformRaw?.let { SegmentTransformConfig.fromMap(it) } + transform = transformRaw?.let { SegmentTransformConfig.fromMap(it) }, + chromaKey = ChromaKeyConfig.fromMap(chromaKeyRaw) ) } } @@ -205,6 +221,105 @@ data class ColorFilterConfig( } } +/** + * Removes a solid-colored background ("green screen"). + * + * Mirrors the Dart `ChromaKey` model and the Swift `ChromaKeyConfig`. The + * keying math lives in `ChromaKeyMath` and is the same formula Apple bakes into + * its color cube; the GPU runs it in `ChromaKeyEffect`'s fragment shader. + * + * @property keyR/keyG/keyB The screen color to remove, gamma-encoded, 0..1 + * @property similarity Chroma-plane radius within which a pixel is fully removed + * @property smoothness Width of the soft ramp just beyond [similarity] + * @property spill How strongly the key's color cast is pulled out of the rest + * @property backgroundColor Solid background ARGB, or null when none + * @property backgroundImageData Background image bytes, or null when none + */ +data class ChromaKeyConfig( + val keyR: Double, + val keyG: Double, + val keyB: Double, + val similarity: Double = 0.20, + val smoothness: Double = 0.08, + val spill: Double = 0.5, + val backgroundColor: Int? = null, + val backgroundImageData: ByteArray? = null, +) { + /** The key color projected onto the Cb/Cr chroma plane. */ + val keyCb: Double = -0.168736 * keyR - 0.331264 * keyG + 0.5 * keyB + val keyCr: Double = 0.5 * keyR - 0.418688 * keyG - 0.081312 * keyB + + /** + * Unit vector pointing from neutral toward the key hue, used to pull the + * key's cast back out during spill suppression. Zero for a neutral (gray) + * key color, which disables despill rather than dividing by zero. + */ + val keyDirCb: Double + val keyDirCr: Double + + init { + val length = kotlin.math.sqrt(keyCb * keyCb + keyCr * keyCr) + if (length > 1e-5) { + keyDirCb = keyCb / length + keyDirCr = keyCr / length + } else { + keyDirCb = 0.0 + keyDirCr = 0.0 + } + } + + /** Whether the keyed area is left transparent rather than filled. */ + val isTransparent: Boolean + get() = backgroundColor == null && backgroundImageData == null + + // Hand-written because of the ByteArray, like ImageLayer below. + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as ChromaKeyConfig + return keyR == other.keyR && + keyG == other.keyG && + keyB == other.keyB && + similarity == other.similarity && + smoothness == other.smoothness && + spill == other.spill && + backgroundColor == other.backgroundColor && + (backgroundImageData?.contentEquals(other.backgroundImageData) + ?: (other.backgroundImageData == null)) + } + + override fun hashCode(): Int { + var result = keyR.hashCode() + result = 31 * result + keyG.hashCode() + result = 31 * result + keyB.hashCode() + result = 31 * result + similarity.hashCode() + result = 31 * result + smoothness.hashCode() + result = 31 * result + spill.hashCode() + result = 31 * result + (backgroundColor?.hashCode() ?: 0) + result = 31 * result + (backgroundImageData?.contentHashCode() ?: 0) + return result + } + + companion object { + fun fromMap(map: Map?): ChromaKeyConfig? { + if (map == null) return null + val keyColor = (map["keyColor"] as? Number)?.toInt() ?: return null + val bgImage = (map["bgImageData"] as? ByteArray)?.takeIf { it.isNotEmpty() } + + return ChromaKeyConfig( + keyR = ((keyColor shr 16) and 0xFF) / 255.0, + keyG = ((keyColor shr 8) and 0xFF) / 255.0, + keyB = (keyColor and 0xFF) / 255.0, + similarity = (map["similarity"] as? Number)?.toDouble() ?: 0.20, + smoothness = (map["smoothness"] as? Number)?.toDouble() ?: 0.08, + spill = (map["spill"] as? Number)?.toDouble() ?: 0.5, + backgroundColor = (map["bgColor"] as? Number)?.toInt(), + backgroundImageData = bgImage, + ) + } + } +} + /** * Represents a custom audio track with timing and volume configuration. * @@ -360,6 +475,12 @@ data class RenderConfig( val colorFilters: List = emptyList(), val audioTracks: List = emptyList(), val blur: Double? = null, + /** + * Removes a solid-colored background ("green screen") from every clip that + * does not carry its own key. A [VideoClip.chromaKey] overrides this, and + * in the layered path a [LayerConfig.chromaKey] sits between the two. + */ + val chromaKey: ChromaKeyConfig? = null, /** Global start time in microseconds for trimming the final composition */ val startUs: Long? = null, /** Global end time in microseconds for trimming the final composition */ @@ -481,6 +602,9 @@ data class RenderConfig( colorFilters = colorFilters, audioTracks = audioTracks, blur = call.argument("blur")?.toDouble(), + chromaKey = ChromaKeyConfig.fromMap( + call.argument>("chromaKey") + ), startUs = call.argument("startUs")?.toLong(), endUs = call.argument("endUs")?.toLong(), shouldOptimizeForNetworkUse = call.argument("shouldOptimizeForNetworkUse") diff --git a/android/src/test/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ChromaKeyMathTest.kt b/android/src/test/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ChromaKeyMathTest.kt new file mode 100644 index 00000000..d8ae41a0 --- /dev/null +++ b/android/src/test/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ChromaKeyMathTest.kt @@ -0,0 +1,261 @@ +package ch.waio.pro_video_editor.src.features.render.helpers + +import ch.waio.pro_video_editor.src.features.render.models.ChromaKeyConfig +import kotlin.math.abs +import kotlin.math.hypot +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * Cross-platform parity guard for the chroma-key formula. + * + * The [golden] table below is duplicated verbatim in the Swift test + * (`example/macos/RunnerTests/RunnerTests.swift`, `ChromaKeyMathTests`). Both + * run it against their own implementation — Kotlin's [ChromaKeyMath], which the + * GLSL shader mirrors, and Swift's `chromaKeyed(r:g:b:_:)`, which is baked into + * the Core Image color cube. If either platform drifts, one of these two tests + * fails immediately instead of the difference surfacing later as "the key looks + * slightly different on iOS". + * + * **When you change the formula, regenerate both tables.** + */ +internal class ChromaKeyMathTest { + + /** + * The config the golden table was computed for: SMPTE green with + * similarity 0.15. Pinned explicitly rather than taken from the defaults, + * so raising the library default never silently invalidates the table. + */ + private val config = ChromaKeyConfig( + keyR = 0x00 / 255.0, + keyG = 0xB1 / 255.0, + keyB = 0x40 / 255.0, + similarity = 0.15, + smoothness = 0.08, + spill = 0.5, + ) + + private val tolerance = 1e-4 + + private data class Golden( + val name: String, + val r: Double, + val g: Double, + val b: Double, + val outR: Double, + val outG: Double, + val outB: Double, + val alpha: Double, + ) + + private val golden = listOf( + // The key color itself and its neighbourhood: removed completely. + Golden("key color", 0.0, 0.694118, 0.25098, 0.218029, 0.565088, 0.343519, 0.0), + Golden("near key", 0.05, 0.72, 0.28, 0.261122, 0.595058, 0.369608, 0.0), + Golden("bright screen", 0.35, 0.9, 0.5, 0.525426, 0.796183, 0.574457, 0.0), + // Half-lit screen: past the default similarity, so only mostly removed. + // This is the documented brightness sensitivity, pinned on purpose. + Golden("dim screen 50%", 0.0, 0.347059, 0.12549, 0.109015, 0.282544, 0.17176, 0.081671), + // Neutrals sit at the chroma origin, far from any saturated key. + Golden("black", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0), + Golden("mid gray", 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 1.0), + Golden("white", 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0), + // Skin tone — the calibration anchor quoted in the Dart docs. + Golden("skin tone", 0.86, 0.65, 0.53, 0.86, 0.65, 0.53, 1.0), + Golden("pure red", 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0), + Golden("pure blue", 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0), + // Kept, but despilled: the green cast is pulled out at full alpha. + Golden( + "green-spilled gray", 0.55, 0.75, 0.55, + 0.616767, 0.710487, 0.578338, 1.0, + ), + // Leans away from the key hue, so despill leaves it alone. + Golden("magenta", 0.8, 0.2, 0.8, 0.8, 0.2, 0.8, 1.0), + ) + + @Test + fun goldenTable_matchesTheSharedFormula() { + for (row in golden) { + val out = ChromaKeyMath.evaluate(row.r, row.g, row.b, config) + val expected = doubleArrayOf(row.outR, row.outG, row.outB, row.alpha) + val labels = listOf("r", "g", "b", "alpha") + + for (i in 0..3) { + assertTrue( + abs(out[i] - expected[i]) < tolerance, + "${row.name}: ${labels[i]} was ${out[i]}, expected ${expected[i]}", + ) + } + } + } + + @Test + fun keyColor_isRemovedCompletely() { + val out = ChromaKeyMath.evaluate(config.keyR, config.keyG, config.keyB, config) + assertEquals(0.0, out[3], tolerance) + } + + @Test + fun softEdge_producesPartialAlphaBetweenTheThresholds() { + // Walk the key color toward neutral gray and collect the alpha ramp. + // Between "fully keyed" and "fully opaque" there must be intermediate + // values, or the edge would be a hard cutout. + val ramp = (0..60).map { step -> + val t = step / 60.0 + ChromaKeyMath.evaluate( + config.keyR + (0.5 - config.keyR) * t, + config.keyG + (0.5 - config.keyG) * t, + config.keyB + (0.5 - config.keyB) * t, + config, + )[3] + } + + assertTrue(ramp.any { it == 0.0 }, "expected a fully keyed sample") + assertTrue(ramp.any { it == 1.0 }, "expected a fully opaque sample") + assertTrue( + ramp.any { it > 0.01 && it < 0.99 }, + "expected a soft edge, but alpha jumped straight from 0 to 1", + ) + } + + @Test + fun alpha_isMonotonicInChromaDistance() { + // Alpha must never dip as a pixel moves further from the key, or the + // matte would show rings. + var previous = -1.0 + for (step in 0..100) { + val t = step / 100.0 + val alpha = ChromaKeyMath.evaluate( + config.keyR + (0.5 - config.keyR) * t, + config.keyG + (0.5 - config.keyG) * t, + config.keyB + (0.5 - config.keyB) * t, + config, + )[3] + assertTrue(alpha >= previous - tolerance, "alpha dipped at t=$t") + previous = alpha + } + } + + @Test + fun matte_isBrightnessSensitive_asDocumented() { + // Cb/Cr scale with brightness, so a dimly lit patch of the screen sits + // closer to the neutral origin and further from the key point. At the + // 0.15 used here that is roughly 55%..100% of the reference brightness; + // the library default of 0.20 reaches down to ~40%, which is what a + // real studio screen needs (its darkest corners measured 0.18). Pinned + // because the Dart docs promise exactly this behaviour. + fun screenAt(fraction: Double) = ChromaKeyMath.evaluate( + config.keyR * fraction, + config.keyG * fraction, + config.keyB * fraction, + config, + )[3] + + assertEquals(0.0, screenAt(1.0), tolerance) + assertEquals(0.0, screenAt(0.7), tolerance) + assertEquals(0.0, screenAt(0.55), tolerance) + assertTrue(screenAt(0.5) > 0.0, "a half-lit screen should start to survive") + + // Widening similarity brings the dim end back under the key. + val wide = config.copy(similarity = 0.35) + assertEquals( + 0.0, + ChromaKeyMath.evaluate( + config.keyR * 0.4, config.keyG * 0.4, config.keyB * 0.4, wide, + )[3], + tolerance, + ) + } + + @Test + fun spill_pullsTheKeyCastOutWithoutDarkening() { + // A green-tinted gray, the classic bounce off a screen. + val r = 0.55 + val g = 0.75 + val b = 0.55 + + val without = ChromaKeyMath.evaluate(r, g, b, config.copy(spill = 0.0)) + val with = ChromaKeyMath.evaluate(r, g, b, config.copy(spill = 1.0)) + + val castBefore = without[1] - maxOf(without[0], without[2]) + val castAfter = with[1] - maxOf(with[0], with[2]) + assertTrue( + castAfter < castBefore, + "despill did not reduce the green cast ($castBefore -> $castAfter)", + ) + + // Luma is preserved, so despill never darkens the subject. + assertEquals( + ChromaKeyMath.luma(without[0], without[1], without[2]), + ChromaKeyMath.luma(with[0], with[1], with[2]), + 1e-3, + ) + } + + @Test + fun spill_leavesTheComplementaryHueAlone() { + // Magenta leans away from green, so despill must not touch it. + val without = ChromaKeyMath.evaluate(0.8, 0.2, 0.8, config.copy(spill = 0.0)) + val with = ChromaKeyMath.evaluate(0.8, 0.2, 0.8, config.copy(spill = 1.0)) + + assertEquals(without[0], with[0], tolerance) + assertEquals(without[1], with[1], tolerance) + assertEquals(without[2], with[2], tolerance) + } + + @Test + fun spillOff_leavesColorsUntouched() { + val out = ChromaKeyMath.evaluate(0.55, 0.75, 0.55, config.copy(spill = 0.0)) + + assertEquals(0.55, out[0], tolerance) + assertEquals(0.75, out[1], tolerance) + assertEquals(0.55, out[2], tolerance) + } + + @Test + fun neutralKeyColor_disablesDespillInsteadOfDividingByZero() { + // A gray key has no hue to pull out; the direction vector collapses to + // zero rather than producing NaN. + val gray = ChromaKeyConfig(keyR = 0.5, keyG = 0.5, keyB = 0.5, spill = 1.0) + assertEquals(0.0, gray.keyDirCb, tolerance) + assertEquals(0.0, gray.keyDirCr, tolerance) + + val out = ChromaKeyMath.evaluate(0.8, 0.2, 0.4, gray) + assertTrue(out.none { it.isNaN() }, "despill produced NaN: ${out.toList()}") + } + + @Test + fun zeroSmoothness_doesNotDivideByZero() { + val hard = config.copy(smoothness = 0.0) + + assertEquals(0.0, ChromaKeyMath.evaluate(0.0, 0.694118, 0.25098, hard)[3], tolerance) + assertEquals(1.0, ChromaKeyMath.evaluate(1.0, 1.0, 1.0, hard)[3], tolerance) + } + + @Test + fun higherSimilarity_keysMore() { + // A color that survives the default key must fall to the wider one. + val narrow = ChromaKeyMath.evaluate(0.35, 0.6, 0.4, config)[3] + val wide = ChromaKeyMath.evaluate(0.35, 0.6, 0.4, config.copy(similarity = 0.5))[3] + + assertTrue(wide < narrow, "widening similarity did not key more ($narrow -> $wide)") + } + + @Test + fun skinTone_keepsAWideMarginFromTheKey() { + // The margin quoted in the Dart docs: skin sits ~0.43 from SMPTE green, + // nearly 3x the default similarity, which is why the default is safe. + val skin = ChromaKeyMath.chroma(0.86, 0.65, 0.53) + val distance = hypot(skin[0] - config.keyCb, skin[1] - config.keyCr) + + assertEquals(0.4259, distance, 1e-3) + assertTrue(distance > config.similarity * 2.5) + } + + @Test + fun derivedKeyChroma_isAUnitDirection() { + val length = hypot(config.keyDirCb, config.keyDirCr) + assertEquals(1.0, length, tolerance) + } +} diff --git a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/RenderVideo.swift b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/RenderVideo.swift index 2b91f47a..8f36bbba 100644 --- a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/RenderVideo.swift +++ b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/RenderVideo.swift @@ -118,7 +118,8 @@ class RenderVideo { volume: clip.volume, playbackSpeed: clip.playbackSpeed, reverseVideo: clip.reverseVideo, - transition: clip.transition + transition: clip.transition, + chromaKey: clip.chromaKey ) } return clip @@ -193,12 +194,13 @@ class RenderVideo { let buildResult: ( AVMutableComposition, VideoCompositionData, CGSize, AVAudioMix?, CMPersistentTrackID, - [URL], [FadeWindow] + [URL], [FadeWindow], [ChromaKeyWindow] ) if let compositionConfig = workingConfig.composition { buildResult = try await LayeredCompositionBuilder(composition: compositionConfig) .setEnableAudio(workingConfig.enableAudio) .setAudioTracks(workingConfig.audioTracks) + .setChromaKey(workingConfig.chromaKey) .build() } else { buildResult = try await applyComposition( @@ -206,12 +208,13 @@ class RenderVideo { videoEffects: effectsConfig, enableAudio: workingConfig.enableAudio, audioTracks: workingConfig.audioTracks, - trimToCommonTrackEnd: workingConfig.trimToCommonTrackEnd + trimToCommonTrackEnd: workingConfig.trimToCommonTrackEnd, + chromaKey: workingConfig.chromaKey ) } let ( composition, videoCompData, renderSize, audioMix, sourceTrackID, audioTempURLs, - fadeWindows + fadeWindows, chromaKeyWindows ) = buildResult temporaryAudioURLs = audioTempURLs var videoCompConfig = videoCompData @@ -265,6 +268,11 @@ class RenderVideo { applyScale( config: &effectsConfig, scaleX: workingConfig.scaleX, scaleY: workingConfig.scaleY) + // Before applyColorMatrix, because the key must run on the original + // colors — the compositor folds both into one color cube, since a + // second CIColorCube would take its alpha from its own cube and + // silently un-key the frame. + applyChromaKey(config: &effectsConfig, windows: chromaKeyWindows) applyColorMatrix( config: &effectsConfig, filters: workingConfig.colorFilters) @@ -430,13 +438,6 @@ class RenderVideo { // MARK: - Helper Methods - private static func hasGpuIntensiveEffects(_ config: RenderConfig) -> Bool { - let hasImageOverlay = !config.imageLayers.isEmpty - let hasBlur = config.blur != nil && config.blur! > 0 - let hasColorFilter = !config.colorFilters.isEmpty - return hasImageOverlay || hasBlur || hasColorFilter - } - private static func makeVideoCompositorSubclass(with config: VideoCompositorConfig) -> AVVideoCompositing.Type { @@ -682,15 +683,17 @@ class RenderVideo { VideoClip( inputPath: current.inputPath, startUs: curStart, endUs: curEnd - tailSrc, volume: current.volume, playbackSpeed: current.playbackSpeed, - reverseVideo: false, transition: nil)) + reverseVideo: false, transition: nil, chromaKey: current.chromaKey)) result.append( VideoClip( - inputPath: rendered.outputURL.path, startUs: 0, endUs: rendered.durationUs)) + inputPath: rendered.outputURL.path, startUs: 0, endUs: rendered.durationUs, + chromaKey: blendChromaKey(current, next!, boundary: "\(i)"))) // Trim the incoming head in place; it keeps its own speed/transition. work[i + 1] = VideoClip( inputPath: next!.inputPath, startUs: nextStart + headSrc, endUs: nextEnd, volume: next!.volume, playbackSpeed: next!.playbackSpeed, - reverseVideo: next!.reverseVideo, transition: next!.transition) + reverseVideo: next!.reverseVideo, transition: next!.transition, + chromaKey: next!.chromaKey) } else { PluginLog.print("⚠️ Transition render failed at boundary \(i); hard cut") appendClip(clearedOverlap(current)) @@ -763,21 +766,23 @@ class RenderVideo { result[0] = VideoClip( inputPath: first.inputPath, startUs: firstStart + headSrc, endUs: lastEnd - tailSrc, volume: first.volume, - playbackSpeed: first.playbackSpeed, reverseVideo: false, transition: nil) + playbackSpeed: first.playbackSpeed, reverseVideo: false, transition: nil, + chromaKey: first.chromaKey) } else { result[0] = VideoClip( inputPath: first.inputPath, startUs: firstStart + headSrc, endUs: first.endUs, volume: first.volume, playbackSpeed: first.playbackSpeed, reverseVideo: false, - transition: first.transition) + transition: first.transition, chromaKey: first.chromaKey) result[lastIdx] = VideoClip( inputPath: last.inputPath, startUs: last.startUs, endUs: lastEnd - tailSrc, volume: last.volume, playbackSpeed: last.playbackSpeed, - reverseVideo: false, transition: nil) + reverseVideo: false, transition: nil, chromaKey: last.chromaKey) } result.append( VideoClip( - inputPath: rendered.outputURL.path, startUs: 0, endUs: rendered.durationUs)) + inputPath: rendered.outputURL.path, startUs: 0, endUs: rendered.durationUs, + chromaKey: blendChromaKey(last, first, boundary: "loop wrap"))) } else { PluginLog.print("⚠️ Loop wrap render failed; seamless loop skipped") } @@ -796,7 +801,25 @@ class RenderVideo { return VideoClip( inputPath: clip.inputPath, startUs: clip.startUs, endUs: clip.endUs, volume: clip.volume, playbackSpeed: clip.playbackSpeed, - reverseVideo: clip.reverseVideo, transition: nil) + reverseVideo: clip.reverseVideo, transition: nil, chromaKey: clip.chromaKey) + } + + /// The chroma key to apply to a pre-rendered overlap blend. + /// + /// The blend is composed from the raw sources by `ClipTransitionRenderer`, + /// which knows nothing about keying, so the key has to be re-applied to its + /// output. That only has a defined meaning when both sides key the same way: + /// blending a keyed clip with an unkeyed one produces mixed colors that no + /// single key can undo. Mismatched sides are therefore left unkeyed and + /// reported, rather than silently keyed with one side's settings. + private static func blendChromaKey( + _ outgoing: VideoClip, _ incoming: VideoClip, boundary: String + ) -> ChromaKeyConfig? { + if outgoing.chromaKey == incoming.chromaKey { return outgoing.chromaKey } + PluginLog.print( + "⚠️ Chroma key: the two clips at boundary \(boundary) use different keys; " + + "the pre-rendered transition blend is emitted unkeyed") + return nil } /// Loads a clip's total duration in microseconds. diff --git a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/ApplyChromaKey.swift b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/ApplyChromaKey.swift new file mode 100644 index 00000000..9145d306 --- /dev/null +++ b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/ApplyChromaKey.swift @@ -0,0 +1,175 @@ +import CoreImage +import Foundation + +// MARK: - The shared keying formula +// +// This is the single spec both platforms implement. Android runs it per pixel +// in `ChromaKeyEffect.kt`'s fragment shader; Apple bakes it into a color cube +// (see `generateChromaLUTData`) because Core Image has no custom kernel here. +// The Kotlin and Swift unit tests assert the same golden table against both, so +// a drift in either implementation fails fast. +// +// All values are gamma-encoded, non-color-managed RGB in 0...1 — the compositor +// runs with `workingColorSpace: NSNull()`, so these are exactly the numbers the +// decoder produced, which is what keeps the two platforms in step. +// +// Y = 0.299·r + 0.587·g + 0.114·b +// Cb = -0.168736·r - 0.331264·g + 0.5·b +// Cr = 0.5·r - 0.418688·g - 0.081312·b +// +// d = distance((Cb, Cr), (Cb_key, Cr_key)) +// alpha = smoothstep(similarity, similarity + smoothness, d) +// +// The distance lives in the Cb/Cr chroma plane, which is a position rather than +// a pure hue: Cb and Cr scale with brightness, so a dimly lit patch of the +// screen sits closer to neutral and further from the key point. The default +// `similarity` of 0.20 covers roughly 40%-100% of the screen's reference +// brightness. Same behaviour as FFmpeg's `chromakey` and OBS. +// +// Spill suppression removes the chroma component pointing toward the key hue, +// leaving Y untouched so a despilled pixel never darkens. + +/// BT.601 chroma projection of a gamma-encoded RGB triple. +func chromaOf(r: Double, g: Double, b: Double) -> (cb: Double, cr: Double) { + return ( + cb: -0.168736 * r - 0.331264 * g + 0.5 * b, + cr: 0.5 * r - 0.418688 * g - 0.081312 * b + ) +} + +/// BT.601 luma of a gamma-encoded RGB triple. +func lumaOf(r: Double, g: Double, b: Double) -> Double { + return 0.299 * r + 0.587 * g + 0.114 * b +} + +/// Hermite smoothstep, matching GLSL's `smoothstep`. +private func smoothstep(_ edge0: Double, _ edge1: Double, _ x: Double) -> Double { + guard edge1 > edge0 else { return x < edge0 ? 0 : 1 } + let t = min(max((x - edge0) / (edge1 - edge0), 0), 1) + return t * t * (3 - 2 * t) +} + +/// Evaluates the chroma key for one pixel. +/// +/// Returns the despilled color together with the matte alpha, both **straight** +/// (not premultiplied) — the cube builder premultiplies afterwards, since that +/// is what `CIColorCube` requires. +func chromaKeyed(r: Double, g: Double, b: Double, _ config: ChromaKeyConfig) + -> (r: Double, g: Double, b: Double, a: Double) +{ + let chroma = chromaOf(r: r, g: g, b: b) + let key = config.keyChroma + + let dCb = chroma.cb - key.cb + let dCr = chroma.cr - key.cr + let distance = (dCb * dCb + dCr * dCr).squareRoot() + + // max() keeps a zero-width ramp from dividing by zero; smoothstep already + // guards it, but this also matches the shader's `max(uSmoothness, 1e-4)`. + let alpha = smoothstep( + config.similarity, + config.similarity + max(config.smoothness, 1e-4), + distance + ) + + // Spill suppression. `projection` is how far the pixel leans toward the key + // hue; only pixels leaning toward it (> 0) are touched, so a complementary + // color is never desaturated. + let direction = config.keyDirection + let projection = chroma.cb * direction.cb + chroma.cr * direction.cr + guard config.spill > 0, projection > 0 else { + return (r, g, b, alpha) + } + + let y = lumaOf(r: r, g: g, b: b) + let cb = chroma.cb - direction.cb * projection * config.spill + let cr = chroma.cr - direction.cr * projection * config.spill + + return ( + r: min(max(y + 1.402 * cr, 0), 1), + g: min(max(y - 0.344136 * cb - 0.714136 * cr, 0), 1), + b: min(max(y + 1.772 * cb, 0), 1), + a: alpha + ) +} + +// MARK: - Compositor wiring + +/// A chroma-key window in composition time. +/// +/// The single-track path resolves the key per clip, so each clip contributes +/// one window covering its own span of the timeline — the same shape as the +/// existing `FadeWindow`. +public struct ChromaKeyWindow: Sendable { + /// Inclusive start of the window in composition microseconds. + let startUs: Int64 + /// Exclusive end of the window in composition microseconds. + let endUs: Int64 + let config: ChromaKeyConfig +} + +/// Hands the per-clip chroma-key windows to the custom video compositor. +/// +/// - Parameters: +/// - config: Video compositor configuration to modify. +/// - windows: One window per clip that carries a key; empty disables keying. +/// +/// - Note: The actual keying happens per frame in the compositor, which builds +/// a color cube from the key and composites its background. +func applyChromaKey( + config: inout VideoCompositorConfig, + windows: [ChromaKeyWindow] +) { + guard !windows.isEmpty else { return } + + config.chromaKeyWindows = windows + + PluginLog.print( + "[\(Tags.render)] 🟩 Applying chroma key: \(windows.count) window(s)" + ) +} + +// MARK: - Color cube + +/// Builds a premultiplied RGBA color cube for the chroma key. +/// +/// The whole key fits in a cube because both halves — the matte alpha and the +/// despilled color — are pure functions of the input RGB. This is what lets +/// Apple key without a custom `CIKernel` or any Metal. +/// +/// Entries are **premultiplied**, which `CIColorCube` requires: a fully removed +/// pixel is `(0, 0, 0, 0)`, not `(rgb, 0)`, or Core Image would composite the +/// screen color back in along the soft edge. Android's shader writes straight +/// alpha instead, per Media3's convention — do not align the two. +/// +/// - Parameters: +/// - chroma: The key to bake in. +/// - size: Cube dimension per axis. `CIColorCube` supports up to 64. +func generateChromaLUTData( + chroma: ChromaKeyConfig, + size: Int +) -> Data? { + let floatCount = size * size * size * 4 + var cubeData = [Float](repeating: 0, count: floatCount) + + var offset = 0 + for b in 0...size) +} diff --git a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/ApplyComposition.swift b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/ApplyComposition.swift index 064d5918..42183788 100644 --- a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/ApplyComposition.swift +++ b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/ApplyComposition.swift @@ -12,6 +12,7 @@ import Foundation /// - videoEffects: Configuration for visual effects (rotation, scale, color, blur, etc.). /// - enableAudio: If true, includes original audio from video clips. /// - audioTracks: Array of audio track configurations to mix over the video. +/// - chromaKey: Global chroma key for clips that carry none of their own. /// /// - Returns: A tuple containing: /// - AVMutableComposition: The concatenated video/audio composition @@ -21,6 +22,7 @@ import Foundation /// - CMPersistentTrackID: The track ID of the video composition track (for fallback on older iOS) /// - [URL]: Temporary file URLs (e.g. pre-rendered audio WAVs) the caller MUST delete after export /// - [FadeWindow]: Dip-to-color windows for fadeToBlack / fadeToWhite transitions +/// - [ChromaKeyWindow]: Per-clip chroma-key windows for the single-track path /// /// - Throws: NSError if video clips are empty, files don't exist, or tracks can't be loaded. func applyComposition( @@ -28,14 +30,16 @@ func applyComposition( videoEffects: VideoCompositorConfig, enableAudio: Bool, audioTracks: [AudioTrackConfig], - trimToCommonTrackEnd: Bool = false + trimToCommonTrackEnd: Bool = false, + chromaKey: ChromaKeyConfig? = nil ) async throws -> ( AVMutableComposition, VideoCompositionData, CGSize, AVAudioMix?, CMPersistentTrackID, [URL], - [FadeWindow] + [FadeWindow], [ChromaKeyWindow] ) { return try await CompositionBuilder(videoClips: videoClips, videoEffects: videoEffects) .setEnableAudio(enableAudio) .setTrimToCommonTrackEnd(trimToCommonTrackEnd) .setAudioTracks(audioTracks) + .setChromaKey(chromaKey) .build() } diff --git a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/BitrateCapPolicy.swift b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/BitrateCapPolicy.swift index 78822e05..6d8e29c1 100644 --- a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/BitrateCapPolicy.swift +++ b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/BitrateCapPolicy.swift @@ -62,6 +62,7 @@ internal enum BitrateCapPolicy { config.maxFrameRate == nil, config.playbackSpeed == nil || config.playbackSpeed == 1.0, config.blur == nil || config.blur == 0, + config.chromaKey == nil, config.startUs == nil, config.endUs == nil else { return false } @@ -71,5 +72,6 @@ internal enum BitrateCapPolicy { && (clip.volume == nil || clip.volume == 1.0) && (clip.playbackSpeed == nil || clip.playbackSpeed == 1.0) && !clip.reverseVideo + && clip.chromaKey == nil } } diff --git a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/CompositionBuilder.swift b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/CompositionBuilder.swift index f7cb897a..72df81ab 100644 --- a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/CompositionBuilder.swift +++ b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/CompositionBuilder.swift @@ -14,6 +14,7 @@ internal class CompositionBuilder { private var enableAudio: Bool = true private var trimToCommonTrackEnd: Bool = false private var audioTracks: [AudioTrackConfig] = [] + private var globalChromaKey: ChromaKeyConfig? /// Initializes builder with configuration. /// @@ -53,13 +54,22 @@ internal class CompositionBuilder { return self } + /// Sets the chroma key applied to every clip that carries none of its own. + /// + /// - Parameter key: The global key, or nil for none + /// - Returns: Self for chaining + func setChromaKey(_ key: ChromaKeyConfig?) -> CompositionBuilder { + self.globalChromaKey = key + return self + } + /// Builds the complete composition. /// /// - Returns: Tuple containing composition, video composition, render size, audio mix, source track ID, and temporary file URLs to clean up after export /// - Throws: Error if composition creation fails func build() async throws -> ( AVMutableComposition, VideoCompositionData, CGSize, AVAudioMix?, CMPersistentTrackID, [URL], - [FadeWindow] + [FadeWindow], [ChromaKeyWindow] ) { guard !videoClips.isEmpty else { throw NSError( @@ -196,12 +206,48 @@ internal class CompositionBuilder { // Compute dip-to-color windows for fadeToBlack / fadeToWhite transitions. let fadeWindows = computeFadeWindows(clipInstructions: videoResult.clipInstructions) + // Resolve the chroma key per clip onto its span of the composition timeline. + let chromaKeyWindows = computeChromaKeyWindows( + clipInstructions: videoResult.clipInstructions) + return ( composition, videoCompositionData, videoResult.renderSize, audioMix, sourceTrackID, - temporaryAudioURLs, fadeWindows + temporaryAudioURLs, fadeWindows, chromaKeyWindows ) } + /// Builds the chroma-key windows from the per-clip instruction time ranges. + /// + /// Every composition frame belongs to exactly one clip instruction, so one + /// window per keyed clip covers the whole timeline — the compositor needs no + /// separate global fallback. A clip's own key wins over the global one. + /// + /// H.264/HEVC carry no alpha and the single-track path has nothing underneath, + /// so a key without a background is substituted with opaque black here. The + /// Android shader does the same (`applyChromaKey(flattenTransparency:)`), which + /// is what keeps the two platforms from diverging into "black" versus + /// "unchanged green". + private func computeChromaKeyWindows(clipInstructions: [ClipInstruction]) + -> [ChromaKeyWindow] + { + var windows: [ChromaKeyWindow] = [] + for (index, clip) in videoClips.enumerated() { + guard index < clipInstructions.count, + let key = clip.chromaKey ?? globalChromaKey + else { continue } + + let effective = key.isTransparent ? key.withOpaqueBlackBackground() : key + let range = clipInstructions[index].timeRange + windows.append( + ChromaKeyWindow( + startUs: Int64(CMTimeGetSeconds(range.start) * 1_000_000), + endUs: Int64(CMTimeGetSeconds(CMTimeRangeGetEnd(range)) * 1_000_000), + config: effective + )) + } + return windows + } + /// Builds the dip-to-color windows for `fadeToBlack` / `fadeToWhite` /// transitions from the per-clip instruction time ranges. /// diff --git a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/LayeredCompositionBuilder.swift b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/LayeredCompositionBuilder.swift index 1764fb62..7a302b0e 100644 --- a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/LayeredCompositionBuilder.swift +++ b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/LayeredCompositionBuilder.swift @@ -16,6 +16,7 @@ internal class LayeredCompositionBuilder { private let config: CompositionConfig private var enableAudio: Bool = true private var audioTracks: [AudioTrackConfig] = [] + private var globalChromaKey: ChromaKeyConfig? init(composition: CompositionConfig) { self.config = composition @@ -31,6 +32,14 @@ internal class LayeredCompositionBuilder { return self } + /// Sets the chroma key for clips carrying neither their own nor a layer key. + /// + /// Resolved per clip as `clip ?? layer ?? global`; never merged. + func setChromaKey(_ key: ChromaKeyConfig?) -> LayeredCompositionBuilder { + self.globalChromaKey = key + return self + } + /// A clip after it has been inserted onto its layer's track, with the data /// needed to build instructions and the audio mix. private struct PlacedClip { @@ -42,6 +51,7 @@ internal class LayeredCompositionBuilder { let transformConfig: SegmentTransformConfig? let preferredTransform: CGAffineTransform let displaySize: CGSize + let chromaKey: ChromaKeyConfig? } private struct AudioWindow { @@ -52,7 +62,7 @@ internal class LayeredCompositionBuilder { func build() async throws -> ( AVMutableComposition, VideoCompositionData, CGSize, AVAudioMix?, CMPersistentTrackID, [URL], - [FadeWindow] + [FadeWindow], [ChromaKeyWindow] ) { guard !config.layers.isEmpty else { throw NSError( @@ -156,7 +166,8 @@ internal class LayeredCompositionBuilder { opacity: layer.opacity, transformConfig: clip.transform ?? layer.transform, preferredTransform: pt, - displaySize: displaySize)) + displaySize: displaySize, + chromaKey: clip.chromaKey ?? layer.chromaKey ?? globalChromaKey)) } } @@ -209,9 +220,12 @@ internal class LayeredCompositionBuilder { + "\(audioTracks.count) audio tracks, " + "canvas \(Int(canvasSize.width))x\(Int(canvasSize.height))") + // No chroma-key windows: the layered path keys each layer on its own + // source frame via LayerPlacement, before it reaches the canvas. Keying the + // composed (opaque) canvas afterwards would be meaningless. return ( composition, videoCompositionData, canvasSize, audioMix, firstVideoTrackID, - temporaryAudioURLs, [] + temporaryAudioURLs, [], [] ) } @@ -250,7 +264,8 @@ internal class LayeredCompositionBuilder { targetRect: resolveRect(clip.transformConfig, displaySize: clip.displaySize), fit: clip.transformConfig?.fit ?? "fill", preferredTransform: clip.preferredTransform, - displaySize: clip.displaySize) + displaySize: clip.displaySize, + chromaKey: clip.chromaKey) } let timeRange = CMTimeRange( diff --git a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/VideoSequenceBuilder.swift b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/VideoSequenceBuilder.swift index b47124bc..66e2a312 100644 --- a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/VideoSequenceBuilder.swift +++ b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/VideoSequenceBuilder.swift @@ -487,6 +487,9 @@ internal struct LayerPlacement: Sendable { let preferredTransform: CGAffineTransform /// The source display size after applying `preferredTransform`. let displaySize: CGSize + /// Chroma key for this layer, resolved as `clip ?? layer ?? global`. + /// Applied to the layer's own source frame, before it reaches the canvas. + let chromaKey: ChromaKeyConfig? } /// Custom video composition instruction that explicitly provides source track IDs. diff --git a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/models/RenderConfig.swift b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/models/RenderConfig.swift index 6df6d979..47ee4078 100644 --- a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/models/RenderConfig.swift +++ b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/models/RenderConfig.swift @@ -162,6 +162,96 @@ public struct ColorFilterConfig: Sendable { } } +/// Configuration for removing a solid-colored background ("green screen"). +/// +/// Mirrors the Dart `ChromaKey` model and the Kotlin `ChromaKeyConfig`. The +/// keying math itself lives in `ApplyChromaKey.swift` and is byte-for-byte the +/// same formula the Android fragment shader runs. +public struct ChromaKeyConfig: Sendable, Equatable { + /// The screen color to remove, as gamma-encoded RGB in 0...1. + let keyR: Double + let keyG: Double + let keyB: Double + /// Chroma-plane radius within which a pixel is removed completely. + let similarity: Double + /// Width of the soft ramp just beyond `similarity`. + let smoothness: Double + /// How strongly the key's color cast is pulled out of the remaining pixels. + let spill: Double + /// Solid background ARGB, or -1 when none (the sentinel convention used by + /// `ColorFilterConfig.startUs`/`endUs`). + let backgroundColor: Int64 + /// Background image bytes, or nil when none. + let backgroundImageData: Data? + + /// Whether the keyed area is left transparent rather than filled. + var isTransparent: Bool { backgroundColor == -1 && backgroundImageData == nil } + + /// The same key, but filling the removed area with opaque black. + /// + /// Used on the single-track path, where nothing sits underneath and the codec + /// carries no alpha, so "transparent" has no meaning. Android does the same + /// substitution in `applyChromaKey(flattenTransparency:)`. + func withOpaqueBlackBackground() -> ChromaKeyConfig { + ChromaKeyConfig( + keyR: keyR, keyG: keyG, keyB: keyB, + similarity: similarity, smoothness: smoothness, spill: spill, + backgroundColor: Int64(0xFF00_0000), backgroundImageData: nil) + } + + /// The key color projected onto the Cb/Cr chroma plane. + var keyChroma: (cb: Double, cr: Double) { + chromaOf(r: keyR, g: keyG, b: keyB) + } + + /// The unit vector pointing from neutral toward the key hue, used to pull + /// the key's cast back out during spill suppression. Zero for a neutral + /// (gray) key color, which disables despill rather than dividing by zero. + var keyDirection: (cb: Double, cr: Double) { + let c = keyChroma + let length = (c.cb * c.cb + c.cr * c.cr).squareRoot() + guard length > 1e-5 else { return (0, 0) } + return (c.cb / length, c.cr / length) + } + + /// Stable identity for the compositor's color-cube cache. + /// + /// Deliberately not `hashValue`: Swift seeds its hashing per process, so a + /// hash is not a safe cache key across the lifetime of a render, and the + /// background image data is far too large to hash per frame. + var cacheKey: String { + let bgLength = backgroundImageData?.count ?? 0 + return "\(keyR),\(keyG),\(keyB),\(similarity),\(smoothness),\(spill)," + + "\(backgroundColor),\(bgLength)" + } + + static func fromArguments(_ args: [String: Any]?) -> ChromaKeyConfig? { + guard let args = args, + let keyColor = (args["keyColor"] as? NSNumber)?.int64Value + else { return nil } + + let backgroundImageData: Data? + if let flutterData = args["bgImageData"] as? FlutterStandardTypedData { + backgroundImageData = flutterData.data.isEmpty ? nil : flutterData.data + } else if let data = args["bgImageData"] as? Data, !data.isEmpty { + backgroundImageData = data + } else { + backgroundImageData = nil + } + + return ChromaKeyConfig( + keyR: Double((keyColor >> 16) & 0xFF) / 255.0, + keyG: Double((keyColor >> 8) & 0xFF) / 255.0, + keyB: Double(keyColor & 0xFF) / 255.0, + similarity: (args["similarity"] as? NSNumber)?.doubleValue ?? 0.20, + smoothness: (args["smoothness"] as? NSNumber)?.doubleValue ?? 0.08, + spill: (args["spill"] as? NSNumber)?.doubleValue ?? 0.5, + backgroundColor: (args["bgColor"] as? NSNumber)?.int64Value ?? -1, + backgroundImageData: backgroundImageData + ) + } +} + /// Configuration for a custom audio track with timing and volume. struct AudioTrackConfig { let path: String @@ -227,6 +317,9 @@ struct LayerConfig: Sendable { let opacity: Float /// Default placement for clips without their own transform. let transform: SegmentTransformConfig? + /// Default chroma key for the clips on this layer. A clip's own key wins; + /// `nil` falls back to the global key. + let chromaKey: ChromaKeyConfig? static func fromArguments(_ args: [String: Any]?) -> LayerConfig? { guard let args = args, @@ -237,7 +330,8 @@ struct LayerConfig: Sendable { return LayerConfig( clips: clips, opacity: (args["opacity"] as? NSNumber)?.floatValue ?? 1.0, - transform: SegmentTransformConfig.fromArguments(args["transform"] as? [String: Any]) + transform: SegmentTransformConfig.fromArguments(args["transform"] as? [String: Any]), + chromaKey: ChromaKeyConfig.fromArguments(args["chromaKey"] as? [String: Any]) ) } } @@ -359,6 +453,11 @@ struct RenderConfig: Sendable { /// Blur radius (nil = no blur, experimental feature) let blur: Double? + /// Removes a solid-colored background ("green screen") from every clip that + /// does not carry its own key. A `VideoClip.chromaKey` overrides this, and in + /// the layered path a `LayerConfig.chromaKey` sits between the two. + let chromaKey: ChromaKeyConfig? + /// Global start time in microseconds for trimming the final composition let startUs: Int64? @@ -404,6 +503,7 @@ struct RenderConfig: Sendable { colorFilters: self.colorFilters, audioTracks: self.audioTracks, blur: self.blur, + chromaKey: self.chromaKey, startUs: self.startUs, endUs: self.endUs, shouldOptimizeForNetworkUse: self.shouldOptimizeForNetworkUse, @@ -474,6 +574,7 @@ struct RenderConfig: Sendable { colorFilters: colorFilters, audioTracks: audioTracks, blur: (args["blur"] as? NSNumber)?.doubleValue, + chromaKey: ChromaKeyConfig.fromArguments(args["chromaKey"] as? [String: Any]), startUs: (args["startUs"] as? NSNumber)?.int64Value, endUs: (args["endUs"] as? NSNumber)?.int64Value, shouldOptimizeForNetworkUse: args["shouldOptimizeForNetworkUse"] as? Bool ?? true, diff --git a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/models/VideoClip.swift b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/models/VideoClip.swift index 3b23c1be..83e81bfb 100644 --- a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/models/VideoClip.swift +++ b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/models/VideoClip.swift @@ -18,6 +18,9 @@ internal struct VideoClip: Sendable { /// Placement of this clip within the composition canvas (composition only). /// Overrides the layer transform. `nil` = use the layer transform. let transform: SegmentTransformConfig? + /// Removes a solid-colored background from this clip. Overrides the layer's + /// and the global key. `nil` = fall back to those. + let chromaKey: ChromaKeyConfig? init( inputPath: String, @@ -28,7 +31,8 @@ internal struct VideoClip: Sendable { reverseVideo: Bool = false, transition: ClipTransitionConfig? = nil, timelineStartUs: Int64? = nil, - transform: SegmentTransformConfig? = nil + transform: SegmentTransformConfig? = nil, + chromaKey: ChromaKeyConfig? = nil ) { self.inputPath = inputPath self.startUs = startUs @@ -39,6 +43,7 @@ internal struct VideoClip: Sendable { self.transition = transition self.timelineStartUs = timelineStartUs self.transform = transform + self.chromaKey = chromaKey } /// Parses a clip from a platform-channel map. Used by both the single-track @@ -58,6 +63,9 @@ internal struct VideoClip: Sendable { timelineStartUs: (clipMap["timelineStartUs"] as? NSNumber)?.int64Value, transform: SegmentTransformConfig.fromArguments( clipMap["transform"] as? [String: Any] + ), + chromaKey: ChromaKeyConfig.fromArguments( + clipMap["chromaKey"] as? [String: Any] ) ) } diff --git a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/models/VideoCompositorConfig.swift b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/models/VideoCompositorConfig.swift index 9390903f..ea168e94 100644 --- a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/models/VideoCompositorConfig.swift +++ b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/models/VideoCompositorConfig.swift @@ -47,6 +47,15 @@ public struct VideoCompositorConfig { /// Dip-to-color windows for `fadeToBlack` / `fadeToWhite` clip transitions. var fadeWindows: [FadeWindow] = [] + /// Chroma-key windows for the **single-track** path, one per clip that + /// carries a key. + /// + /// Always empty on the layered path, where the key lives on each + /// `LayerPlacement` instead: there, every layer is keyed on its own source + /// frame before it reaches the canvas, and keying the composed (opaque) + /// canvas afterwards would be meaningless. + var chromaKeyWindows: [ChromaKeyWindow] = [] + var videoRotationDegrees: Double = 0.0 var shouldApplyOrientationCorrection: Bool = false diff --git a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/utils/VideoCompositor.swift b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/utils/VideoCompositor.swift index 82c0348d..aa1e924d 100644 --- a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/utils/VideoCompositor.swift +++ b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/utils/VideoCompositor.swift @@ -137,10 +137,22 @@ class VideoCompositor: NSObject, AVVideoCompositing { /// Dip-to-color windows for fadeToBlack / fadeToWhite clip transitions private var fadeWindows: [FadeWindow] = [] - /// Cache for computed LUTs keyed by active filter indices + /// Per-clip chroma-key windows (single-track path). + private var chromaKeyWindows: [ChromaKeyWindow] = [] + + /// Decoded chroma-key background images, keyed by config, so a background is + /// decoded once per render rather than once per frame. + private var chromaBackgroundCache: [String: CIImage] = [:] + + /// Cache for computed LUTs keyed by the active chroma key and filter indices private let lutCacheQueue = DispatchQueue(label: "lut.cache.queue") private var lutCache: [String: (data: Data, size: Int)] = [:] + /// Insertion order of `lutCache`, so the oldest entry can be evicted. + private var lutCacheOrder: [String] = [] private let defaultLutSize = 33 + /// A 33³ cube is 574 KB; a long timeline with many distinct keys and filters + /// would otherwise accumulate one per combination. + private let lutCacheLimit = 8 static var config = VideoCompositorConfig() @@ -176,6 +188,23 @@ class VideoCompositor: NSObject, AVVideoCompositing { self.setOverlayImageLayers(from: config.imageLayerConfigs) self.colorFilterConfigs = config.colorFilterConfigs self.fadeWindows = config.fadeWindows + self.chromaKeyWindows = config.chromaKeyWindows + } + + /// The chroma key active at the given composition time, if any. + /// + /// Windows are per clip and never overlap, so the first match wins. + private func activeChromaKey(at compositionTime: CMTime) -> ChromaKeyConfig? { + guard !chromaKeyWindows.isEmpty else { return nil } + let tUs = Int64(CMTimeGetSeconds(compositionTime) * 1_000_000) + for window in chromaKeyWindows + where window.endUs > window.startUs && tUs >= window.startUs && tUs < window.endUs { + return window.config + } + // The last frame of the timeline lands exactly on the final window's end, + // so clamp to it rather than emitting one unkeyed frame. + if let last = chromaKeyWindows.last, tUs >= last.endUs { return last.config } + return nil } /// Applies a dip-to-color (fade-to-black / fade-to-white) at the given @@ -261,8 +290,9 @@ class VideoCompositor: NSObject, AVVideoCompositing { } } - /// Computes the LUT for a given set of active color filter indices. - /// Results are cached so that each unique combination is only computed once. + /// Computes the color-filter cube active at the given composition time. + /// + /// Results are cached so each unique combination is built only once. private func getLUTForActiveFilters(at compositionTime: CMTime) -> (data: Data, size: Int)? { guard !colorFilterConfigs.isEmpty else { return nil } @@ -279,7 +309,7 @@ class VideoCompositor: NSObject, AVVideoCompositing { } guard !activeIndices.isEmpty else { return nil } - let cacheKey = activeIndices.map { String($0) }.joined(separator: ",") + let cacheKey = "cf:" + activeIndices.map { String($0) }.joined(separator: ",") // Check cache var cached: (data: Data, size: Int)? @@ -293,16 +323,24 @@ class VideoCompositor: NSObject, AVVideoCompositing { let activeMatrices = activeIndices.map { colorFilterConfigs[$0].matrix } let combined = combineColorMatrices(activeMatrices) guard combined.count == 20 else { return nil } - guard let data = generateLUTData(from: combined, size: defaultLutSize) else { return nil } + guard let data = generateLUTData(from: combined, size: defaultLutSize) else { + return nil + } let result = (data: data, size: defaultLutSize) lutCacheQueue.sync { - lutCache[cacheKey] = result + if lutCache[cacheKey] == nil { + lutCache[cacheKey] = result + lutCacheOrder.append(cacheKey) + while lutCacheOrder.count > lutCacheLimit { + lutCache.removeValue(forKey: lutCacheOrder.removeFirst()) + } + } } return result } - /// Applies the LUT for active color filters at the given composition time. + /// Applies the active color cube at the given composition time. private func applyColorFilter(to image: CIImage, at compositionTime: CMTime) -> CIImage { guard let lut = getLUTForActiveFilters(at: compositionTime), let lutFilter = CIFilter(name: "CIColorCube") @@ -315,6 +353,133 @@ class VideoCompositor: NSObject, AVVideoCompositing { return lutFilter.outputImage ?? image } + /// Applies a chroma key as its own color cube. + /// + /// The cube encodes the whole key — matte alpha *and* despilled color — since + /// both are pure functions of the input RGB. Entries are premultiplied, as + /// `CIColorCube` requires. Cached like the color-filter cube. + private func applyChromaKeyCube(to image: CIImage, _ config: ChromaKeyConfig) -> CIImage { + let cacheKey = "ck:\(config.cacheKey)" + + var cached: (data: Data, size: Int)? + lutCacheQueue.sync { cached = lutCache[cacheKey] } + + let lut: (data: Data, size: Int) + if let cached = cached { + lut = cached + } else { + guard + let data = generateChromaLUTData(chroma: config, size: defaultLutSize) + else { return image } + lut = (data: data, size: defaultLutSize) + lutCacheQueue.sync { + if lutCache[cacheKey] == nil { + lutCache[cacheKey] = lut + lutCacheOrder.append(cacheKey) + while lutCacheOrder.count > lutCacheLimit { + lutCache.removeValue(forKey: lutCacheOrder.removeFirst()) + } + } + } + } + + guard let filter = CIFilter(name: "CIColorCube") else { return image } + filter.setValue(lut.size, forKey: "inputCubeDimension") + filter.setValue(lut.data, forKey: "inputCubeData") + filter.setValue(image, forKey: kCIInputImageKey) + return filter.outputImage ?? image + } + + /// Removes the chroma key from a source frame and fills the keyed area. + /// + /// Runs on the **raw source frame**, before crop, rotation, flip and scale. + /// Three reasons: + /// + /// 1. It is a single insertion point that covers both branches of the + /// `imageBytesWithCropping` split, where the color filter is duplicated. + /// Applying it inside `applyColorFilter` instead would place it before the + /// crop in one branch and after it in the other, so a background *image* + /// would be cropped along with the video in one case and not the other. + /// 2. It matches the Android ordering exactly: there the chroma shader is + /// first in the per-clip chain and `SingleColorLut` grades whatever it + /// produced — so a color filter grades the substituted background too. + /// Folding the filter into the key's own cube instead would grade only the + /// video and leave the background at its raw color. + /// 3. It keys unresampled pixels, which keeps the soft edge crisp. + /// + /// Chaining the color filter's own `CIColorCube` afterwards is safe precisely + /// because the background has already been composited: the frame is opaque + /// again, so the second cube resetting alpha from its own data changes + /// nothing. Without a background it would un-key the frame — which is why the + /// single-track path substitutes opaque black for a transparent key, and why + /// the layered path keys inside `composeLayered` instead. + private func applyChromaKeyStage(to image: CIImage, at compositionTime: CMTime) + -> CIImage + { + guard let chroma = activeChromaKey(at: compositionTime) else { return image } + return compositeChromaBackground(applyChromaKeyCube(to: image, chroma), chroma) + } + + /// Fills the keyed-out area with the key's background. + /// + /// A nil background leaves the image transparent, which only carries meaning + /// on the layered path — the single-track path substitutes opaque black long + /// before this, in `CompositionBuilder.computeChromaKeyWindows`. + private func compositeChromaBackground(_ image: CIImage, _ config: ChromaKeyConfig) + -> CIImage + { + let extent = image.extent + guard extent.width > 0, extent.height > 0 else { return image } + + if let background = chromaBackgroundImage(for: config) { + return image.composited(over: scaleToFill(background, extent)) + } + + guard config.backgroundColor != -1 else { return image } + let argb = config.backgroundColor + let color = CIColor( + red: CGFloat((argb >> 16) & 0xFF) / 255.0, + green: CGFloat((argb >> 8) & 0xFF) / 255.0, + blue: CGFloat(argb & 0xFF) / 255.0, + alpha: CGFloat((argb >> 24) & 0xFF) / 255.0) + return image.composited(over: CIImage(color: color).cropped(to: extent)) + } + + /// Stretches [image] to exactly cover [extent]. + /// + /// Matches `ImageLayer`'s "no offset means stretch to the frame" semantics and + /// the Android shader, which samples the background with the frame's own + /// texture coordinates. + private func scaleToFill(_ image: CIImage, _ extent: CGRect) -> CIImage { + let source = image.extent + guard source.width > 0, source.height > 0 else { return image } + let scaled = image.transformed( + by: CGAffineTransform( + scaleX: extent.width / source.width, y: extent.height / source.height)) + return scaled.transformed( + by: CGAffineTransform( + translationX: extent.minX - scaled.extent.minX, + y: extent.minY - scaled.extent.minY)) + } + + /// The decoded background image for [config], decoded once and cached. + private func chromaBackgroundImage(for config: ChromaKeyConfig) -> CIImage? { + guard let data = config.backgroundImageData else { return nil } + if let cached = chromaBackgroundCache[config.cacheKey] { return cached } + + #if os(iOS) + guard let image = UIImage(data: data), let cgImage = image.cgImage else { return nil } + #elseif os(macOS) + guard let image = NSImage(data: data), + let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) + else { return nil } + #endif + + let ciImage = CIImage(cgImage: cgImage) + chromaBackgroundCache[config.cacheKey] = ciImage + return ciImage + } + private let context = CIContext(options: [ .workingColorSpace: NSNull(), .outputColorSpace: CGColorSpace(name: CGColorSpace.sRGB)!, @@ -354,6 +519,15 @@ class VideoCompositor: NSObject, AVVideoCompositing { guard let buffer = request.sourceFrame(byTrackID: placement.trackID) else { continue } var img = CIImage(cvPixelBuffer: buffer) + // 0. Remove this layer's chroma key on its raw source frame, before any + // orientation or scaling. A key without a background stays transparent + // here — unlike the single-track path, there really is something + // underneath, and step 5's composite lets it show through. + if let key = placement.chromaKey { + img = applyChromaKeyCube(to: img, key) + img = compositeChromaBackground(img, key) + } + // 1. Orient using the source preferred transform (rotation + mirror // metadata), then normalize the extent back to the origin. let preferred = placement.preferredTransform @@ -568,6 +742,11 @@ class VideoCompositor: NSObject, AVVideoCompositing { } } + // Remove the chroma key on the raw source frame, before any geometry, and + // fill the keyed area with its background. One insertion point for both + // branches of the imageBytesWithCropping split below. + outputImage = applyChromaKeyStage(to: outputImage, at: request.compositionTime) + var center = CGPoint(x: outputImage.extent.midX, y: outputImage.extent.midY) // Apply user-defined effects (crop, rotation, flip, scale) diff --git a/example/assets/greenscreen.mp4 b/example/assets/greenscreen.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..2fd05c1994d72a85966b260c5ce5cfc92ce1f28e GIT binary patch literal 337789 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
WAJfI%iwNB0-DmJ29Zzez8|oft727K0{;E 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 paintPng(Size size, void Function(Canvas) paint) async { + final recorder = PictureRecorder(); + final canvas = Canvas(recorder); + paint(canvas); + final picture = recorder.endRecording(); + final image = await picture.toImage( + size.width.toInt(), + size.height.toInt(), + ); + final data = await image.toByteData(format: ImageByteFormat.png); + return data!.buffer.asUint8List(); + } + + /// A PNG whose left half is [left] and right half is [right]. + Future splitPng(Color left, Color right) { + return paintPng(canvas, (c) { + final w = canvas.width; + final h = canvas.height; + c + ..drawRect(Rect.fromLTWH(0, 0, w / 2, h), Paint()..color = left) + ..drawRect(Rect.fromLTWH(w / 2, 0, w / 2, h), Paint()..color = right); + }); + } + + /// A solid-color PNG. + Future solidPng(Color color) { + return paintPng( + canvas, + (c) => c.drawRect( + Rect.fromLTWH(0, 0, canvas.width, canvas.height), + Paint()..color = color, + ), + ); + } + + /// Turns a still image into a video the key can work on. + /// + /// Built by painting [png] over a real clip rather than by + /// `renderStopMotion`: a stop-motion clip is a single held still, and two of + /// those do not concatenate (verified: 2s + 2s comes out as 2.03s), which the + /// multi-segment test below needs. Overlaying a real clip keeps a normal + /// frame/GOP structure while making every pixel an exactly known color. + Future videoFromImage( + Uint8List png, { + Duration duration = const Duration(seconds: 2), + }) async { + final bytes = await pve.renderVideo( + VideoRenderData( + videoSegments: [ + VideoSegment( + video: EditorVideo.asset('assets/tests/test_d.mp4'), + endTime: duration, + ), + ], + // No offset/size: the layer is stretched over the whole frame. + imageLayers: [ImageLayer(image: EditorLayerImage.memory(png))], + transform: ExportTransform( + width: canvas.width.toInt(), + height: canvas.height.toInt(), + ), + ), + ); + return EditorVideo.memory(bytes); + } + + /// Decodes a frame of [video] and returns the RGBA at the relative position + /// ([fx], [fy]) in `0..1`. + Future> samplePixel( + Uint8List video, + double fx, + double fy, { + Duration at = frameAt, + }) async { + final frames = await pve.getThumbnails( + ThumbnailConfigs( + video: EditorVideo.memory(video), + outputFormat: ThumbnailFormat.png, + timestamps: [at], + outputSize: canvas, + boxFit: ThumbnailBoxFit.cover, + ), + ); + expect(frames, isNotEmpty, reason: 'No frame extracted from the output'); + final codec = await instantiateImageCodec(frames.first); + final image = (await codec.getNextFrame()).image; + final data = await image.toByteData(); + final px = (fx * (image.width - 1)).round().clamp(0, image.width - 1); + final py = (fy * (image.height - 1)).round().clamp(0, image.height - 1); + final i = (py * image.width + px) * 4; + return [ + data!.getUint8(i), + data.getUint8(i + 1), + data.getUint8(i + 2), + data.getUint8(i + 3), + ]; + } + + /// Sum of absolute per-channel (RGB) differences between two pixels. + int colorDist(List a, List b) => + (a[0] - b[0]).abs() + (a[1] - b[1]).abs() + (a[2] - b[2]).abs(); + + bool isRed(List c) => c[0] > 150 && c[1] < 100 && c[2] < 100; + bool isBlue(List c) => c[2] > 130 && c[0] < 120 && c[1] < 130; + bool isGreen(List c) => c[1] > 110 && c[0] < 110; + + // Sample points well inside each half, away from the seam. + const leftPoints = [ + Offset(0.15, 0.3), + Offset(0.25, 0.5), + Offset(0.15, 0.7), + ]; + const rightPoints = [ + Offset(0.75, 0.3), + Offset(0.85, 0.5), + Offset(0.75, 0.7), + ]; + + /// The green/blue split source, built once and shared by the tests. + late EditorVideo greenScreen; + + setUpAll(() async { + greenScreen = await videoFromImage( + await splitPng(screenGreen, subjectBlue), + ); + + // Guard the fixture itself: if the synthesized source is not actually + // green on the left and blue on the right, every assertion below would be + // meaningless. + final source = await greenScreen.safeByteArray(); + for (final p in leftPoints) { + expect( + isGreen(await samplePixel(source, p.dx, p.dy)), + isTrue, + reason: 'Fixture is not green at $p', + ); + } + for (final p in rightPoints) { + expect( + isBlue(await samplePixel(source, p.dx, p.dy)), + isTrue, + reason: 'Fixture is not blue at $p', + ); + } + }); + + group('ChromaKey - single track', () { + testWidgets('a color background replaces the screen', (_) async { + final out = await pve.renderVideo( + VideoRenderData( + videoSegments: [VideoSegment(video: greenScreen)], + chromaKey: const ChromaKey(backgroundColor: backgroundRed), + ), + ); + + for (final p in leftPoints) { + final c = await samplePixel(out, p.dx, p.dy); + expect(isRed(c), isTrue, reason: 'Screen not replaced at $p, got $c'); + } + }); + + testWidgets('the subject is left alone', (_) async { + final out = await pve.renderVideo( + VideoRenderData( + videoSegments: [VideoSegment(video: greenScreen)], + chromaKey: const ChromaKey(backgroundColor: backgroundRed), + ), + ); + + for (final p in rightPoints) { + final c = await samplePixel(out, p.dx, p.dy); + expect(isBlue(c), isTrue, reason: 'Subject was keyed at $p, got $c'); + } + }); + + testWidgets('a tiny similarity keys nothing', (_) async { + // The regression guard: with the threshold below the encoder's own + // rounding of the key color, the output must still be the source. + final out = await pve.renderVideo( + VideoRenderData( + videoSegments: [VideoSegment(video: greenScreen)], + chromaKey: const ChromaKey( + similarity: 0.001, + smoothness: 0, + spill: 0, + backgroundColor: backgroundRed, + ), + ), + ); + + for (final p in leftPoints) { + final c = await samplePixel(out, p.dx, p.dy); + expect(isRed(c), isFalse, reason: 'Keyed despite similarity≈0 at $p'); + expect(isGreen(c), isTrue, reason: 'Screen not preserved at $p'); + } + }); + + testWidgets('an image background fills the screen', (_) async { + final out = await pve.renderVideo( + VideoRenderData( + videoSegments: [VideoSegment(video: greenScreen)], + chromaKey: ChromaKey( + backgroundImage: EditorLayerImage.memory( + await solidPng(backgroundRed), + ), + ), + ), + ); + + for (final p in leftPoints) { + final c = await samplePixel(out, p.dx, p.dy); + expect(isRed(c), isTrue, reason: 'Background image missing at $p'); + } + for (final p in rightPoints) { + final c = await samplePixel(out, p.dx, p.dy); + expect(isBlue(c), isTrue, reason: 'Subject was keyed at $p, got $c'); + } + }); + + testWidgets('a color filter does not un-key the frame', (_) async { + // Apple folds the key and the filter into one CIColorCube because a + // second cube would take its alpha from its own data and resurrect the + // screen. This is the regression guard for that. + final out = await pve.renderVideo( + VideoRenderData( + videoSegments: [VideoSegment(video: greenScreen)], + chromaKey: const ChromaKey(backgroundColor: backgroundRed), + colorFilters: const [ + ColorFilter( + matrix: [ + 0.299, 0.587, 0.114, 0, 0, // + 0.299, 0.587, 0.114, 0, 0, // + 0.299, 0.587, 0.114, 0, 0, // + 0, 0, 0, 1, 0, // + ], + ), + ], + ), + ); + + for (final p in leftPoints) { + final c = await samplePixel(out, p.dx, p.dy); + expect( + isGreen(c), + isFalse, + reason: 'The color filter resurrected the screen at $p, got $c', + ); + // Grayscaled red: R==G==B, and clearly not the original green. + expect( + (c[0] - c[1]).abs() < 30 && (c[1] - c[2]).abs() < 30, + isTrue, + reason: 'Output is not grayscale at $p, got $c', + ); + } + }); + + testWidgets('a bitrate cap does not skip the key via passthrough', ( + _, + ) async { + // A single untrimmed clip within the cap would otherwise take the + // lossless fast path (Apple passthrough / Android transmux) and the key + // would silently do nothing. + final out = await pve.renderVideo( + VideoRenderData( + videoSegments: [VideoSegment(video: greenScreen)], + chromaKey: const ChromaKey(backgroundColor: backgroundRed), + bitrate: 8000000, + ), + ); + + for (final p in leftPoints) { + final c = await samplePixel(out, p.dx, p.dy); + expect(isRed(c), isTrue, reason: 'Passthrough skipped the key at $p'); + } + }); + + testWidgets('imageBytesWithCropping keys in both branches', (_) async { + // The compositor duplicates its effect chain across this flag, so both + // sides have to be exercised or one can silently lose the key. + for (final withCropping in [false, true]) { + final out = await pve.renderVideo( + VideoRenderData( + videoSegments: [VideoSegment(video: greenScreen)], + chromaKey: const ChromaKey(backgroundColor: backgroundRed), + imageBytesWithCropping: withCropping, + ), + ); + + for (final p in leftPoints) { + final c = await samplePixel(out, p.dx, p.dy); + expect( + isRed(c), + isTrue, + reason: 'Key lost with imageBytesWithCropping=$withCropping at $p', + ); + } + } + }); + + testWidgets('a per-segment key overrides the global one', (_) async { + // Two sources, each re-encoded once through the normal render path: a + // stop-motion clip holds a single still for its whole duration, and two + // of those concatenate down to one segment. Re-encoding gives them an + // ordinary frame/GOP structure. + const half = Duration(seconds: 2); + Future concatenatable(Color right) async { + final still = await videoFromImage( + await splitPng(screenGreen, right), + duration: half, + ); + return EditorVideo.memory( + await pve.renderVideo( + VideoRenderData(videoSegments: [VideoSegment(video: still)]), + ), + ); + } + + final firstSource = await concatenatable(subjectBlue); + final secondSource = await concatenatable(const Color(0xFF804000)); + + final out = await pve.renderVideo( + VideoRenderData( + videoSegments: [ + VideoSegment(video: firstSource), + VideoSegment( + video: secondSource, + // Overrides the global red with a distinctly different fill. + chromaKey: const ChromaKey( + backgroundColor: Color(0xFFFFFFFF), + ), + ), + ], + chromaKey: const ChromaKey(backgroundColor: backgroundRed), + ), + ); + + // Sample a quarter and three quarters into the real output rather than at + // fixed timestamps: the concatenated length depends on how the encoder + // rounds each segment, and a timestamp past the end yields no frame. + final duration = (await pve.getMetadata( + EditorVideo.memory(out), + )).duration; + expect( + duration.inMilliseconds, + closeTo(4000, 700), + reason: 'Both segments should be in the output, got $duration', + ); + final inFirst = duration * 0.25; + final inSecond = duration * 0.75; + + // First segment: the global key's red. + final first = await samplePixel(out, 0.2, 0.5, at: inFirst); + expect( + isRed(first), + isTrue, + reason: 'Global key not applied, got $first', + ); + + // Second segment: its own key's white. + final second = await samplePixel(out, 0.2, 0.5, at: inSecond); + expect( + second[0] > 180 && second[1] > 180 && second[2] > 180, + isTrue, + reason: 'Per-segment key did not override the global one, got $second', + ); + }); + }); + + group('ChromaKey - autoDetect', () { + testWidgets('measures the real studio screen', (_) async { + final detection = await ChromaKey.detect( + EditorVideo.asset('assets/greenscreen.mp4'), + ); + + // The recorded screen, not the paint it was mixed from: SMPTE green is + // 0xFF00B140, the camera saw roughly 0xFF2A9D37. + expect(detection.coverage, greaterThan(0.9)); + expect( + detection.color.g, + greaterThan(detection.color.r), + reason: 'a green screen should detect as green, got ${detection.color}', + ); + expect( + detection.color.g, + greaterThan(detection.color.b), + reason: 'a green screen should detect as green, got ${detection.color}', + ); + + // The whole point: a measured key sits much closer to the screen than + // the SMPTE constant, so it needs far less similarity to cover it. + expect( + detection.similarity, + lessThan(const ChromaKey().similarity), + reason: 'measured ${detection.similarity} should beat the constant', + ); + }); + + testWidgets('the detected key removes the screen', (_) async { + final gs = EditorVideo.asset('assets/greenscreen.mp4'); + final key = await ChromaKey.autoDetect( + gs, + backgroundColor: backgroundRed, + ); + + final out = await pve.renderVideo( + VideoRenderData( + videoSegments: [VideoSegment(video: gs)], + chromaKey: key, + ), + ); + + // Corners: screen everywhere, including the dimly lit bottom ones that + // survive a too-tight constant key. + for (final p in const [ + Offset(0.05, 0.05), + Offset(0.95, 0.05), + Offset(0.05, 0.95), + Offset(0.95, 0.95), + Offset(0.15, 0.5), + ]) { + final c = await samplePixel(out, p.dx, p.dy); + expect(isRed(c), isTrue, reason: 'Screen survived at $p, got $c'); + } + }); + + testWidgets('the detected key keeps the subject', (_) async { + final gs = EditorVideo.asset('assets/greenscreen.mp4'); + final key = await ChromaKey.autoDetect( + gs, + backgroundColor: backgroundRed, + ); + + final out = await pve.renderVideo( + VideoRenderData( + videoSegments: [VideoSegment(video: gs)], + chromaKey: key, + ), + ); + + // Center of the subject: pink outfit, must not be keyed. + final c = await samplePixel(out, 0.66, 0.45); + expect( + isRed(c), + isFalse, + reason: 'The subject was keyed away, got $c', + ); + }); + + testWidgets('rejects a frame that is not a screen', (_) async { + // The plain demo video has no screen at all. + expect( + () => ChromaKey.detect( + EditorVideo.asset(kVideoEditorExampleH264Path), + ), + throwsA(isA()), + ); + }); + }); + + group('ChromaKey - composition', () { + testWidgets('a transparent key lets the layer below show through', ( + _, + ) async { + // The one case that exercises real transparency end to end: it would + // fail on a premultiplied/straight alpha mix-up, on a Media3 blend + // regression, and on the VideoCompositionTransformation alpha-squaring. + final backdrop = await videoFromImage(await solidPng(backgroundRed)); + + final out = await pve.renderVideo( + VideoRenderData( + composition: VideoComposition( + canvasSize: canvas, + layers: [ + VideoLayer(clips: [VideoSegment(video: backdrop)]), + VideoLayer( + clips: [VideoSegment(video: greenScreen)], + chromaKey: const ChromaKey(), + ), + ], + ), + ), + ); + + for (final p in leftPoints) { + final c = await samplePixel(out, p.dx, p.dy); + expect( + isRed(c), + isTrue, + reason: 'The layer below does not show through at $p, got $c', + ); + // Specifically not black: that is what a flattened alpha looks like. + expect( + colorDist(c, [0, 0, 0, 255]) > 90, + isTrue, + reason: 'Keyed area came out black at $p, got $c', + ); + } + + for (final p in rightPoints) { + final c = await samplePixel(out, p.dx, p.dy); + expect(isBlue(c), isTrue, reason: 'Subject was keyed at $p, got $c'); + } + }); + + testWidgets('a per-clip key overrides the layer key', (_) async { + final backdrop = await videoFromImage(await solidPng(backgroundRed)); + + final out = await pve.renderVideo( + VideoRenderData( + composition: VideoComposition( + canvasSize: canvas, + layers: [ + VideoLayer(clips: [VideoSegment(video: backdrop)]), + VideoLayer( + clips: [ + VideoSegment( + video: greenScreen, + // Keys almost nothing, overriding the layer's default key. + chromaKey: const ChromaKey( + similarity: 0.001, + smoothness: 0, + spill: 0, + ), + ), + ], + chromaKey: const ChromaKey(), + ), + ], + ), + ), + ); + + for (final p in leftPoints) { + final c = await samplePixel(out, p.dx, p.dy); + expect( + isGreen(c), + isTrue, + reason: 'The layer key won over the clip key at $p, got $c', + ); + } + }); + }); +} diff --git a/example/lib/core/constants/example_constants.dart b/example/lib/core/constants/example_constants.dart index 9b7c2f88..b36b394c 100644 --- a/example/lib/core/constants/example_constants.dart +++ b/example/lib/core/constants/example_constants.dart @@ -21,6 +21,10 @@ const String kVideoEditorExampleDivinePath = /// Darwin AVAssetWriter path downmixes them to stereo). const String kVideoEditorExampleSurround51Path = 'assets/surround_5_1.mp4'; +/// A local path to a real green-screen clip: a person in front of a lit studio +/// screen. Cropped to the screen area, so nothing outside it survives the key. +const String kVideoEditorExampleGreenScreenPath = 'assets/greenscreen.mp4'; + /// A local path to the first example audio track used in video editor demos. const String kVideoEditorExampleAudio1Path = 'assets/audio1.mp3'; diff --git a/example/lib/features/chroma_key/chroma_key_example_page.dart b/example/lib/features/chroma_key/chroma_key_example_page.dart new file mode 100644 index 00000000..7b210602 --- /dev/null +++ b/example/lib/features/chroma_key/chroma_key_example_page.dart @@ -0,0 +1,382 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:chewie/chewie.dart'; +import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:pro_video_editor/pro_video_editor.dart'; +import 'package:pro_video_editor_example/shared/utils/render_cancel_capability.dart'; +import 'package:pro_video_editor_example/shared/widgets/video_renderer_progress.dart'; +import 'package:video_player/video_player.dart'; + +import '/core/constants/example_constants.dart'; +import '/shared/utils/bytes_formatter.dart'; +import '/shared/widgets/native_log_console.dart'; + +/// What the removed screen is filled with. +enum _Background { + /// A solid color. + color, + + /// A still image. + image, + + /// Nothing — the layer below in a [VideoComposition] shows through. + video, +} + +/// A page demonstrating the chroma key: removing a green screen and putting +/// something else behind the subject. +class ChromaKeyExamplePage extends StatefulWidget { + /// Creates a [ChromaKeyExamplePage]. + const ChromaKeyExamplePage({super.key}); + + @override + State createState() => _ChromaKeyExamplePageState(); +} + +class _ChromaKeyExamplePageState extends State { + final _pve = ProVideoEditor.instance; + + VideoPlayerController? _controllerPreview; + ChewieController? _chewieControllerPreview; + bool _isPreviewInitialized = false; + + bool _isExporting = false; + Uint8List? _videoBytes; + Duration _generationTime = Duration.zero; + String _taskId = DateTime.now().microsecondsSinceEpoch.toString(); + + _Background _background = _Background.color; + Color _keyColor = const ChromaKey().color; + double _similarity = const ChromaKey().similarity; + double _smoothness = 0.08; + double _spill = 0.5; + + /// The last auto-detection, shown so the measurement is visible rather than + /// just silently applied. + ChromaKeyDetection? _detection; + bool _isDetecting = false; + + bool get _supportsCancel => canCancelOnCurrentPlatform(); + + static const _fillRed = Color(0xFFE00000); + + /// The green-screen asset's own size, used as the composition canvas so the + /// keyed clip and the backdrop line up without scaling. + static const _canvas = Size(756, 732); + + EditorVideo get _greenScreen => + EditorVideo.asset(kVideoEditorExampleGreenScreenPath); + + @override + void dispose() { + if (_isPreviewInitialized) _controllerPreview?.dispose(); + _chewieControllerPreview?.dispose(); + super.dispose(); + } + + /// Builds the render for the selected background. + VideoRenderData _buildRenderData(EditorVideo source) { + switch (_background) { + case _Background.color: + return VideoRenderData( + id: _taskId, + videoSegments: [VideoSegment(video: source)], + chromaKey: ChromaKey( + color: _keyColor, + similarity: _similarity, + smoothness: _smoothness, + spill: _spill, + backgroundColor: _fillRed, + ), + ); + + case _Background.image: + return VideoRenderData( + id: _taskId, + videoSegments: [VideoSegment(video: source)], + chromaKey: ChromaKey( + color: _keyColor, + similarity: _similarity, + smoothness: _smoothness, + spill: _spill, + backgroundImage: EditorLayerImage.asset('assets/sticker.png'), + ), + ); + + case _Background.video: + // The key is left transparent, so the layer below — a second video — + // shows through. This is the only way to put a *video* behind the + // screen: the single-track path has nothing underneath, and neither + // H.264 nor HEVC carries an alpha channel. + return VideoRenderData( + id: _taskId, + composition: VideoComposition( + canvasSize: _canvas, + layers: [ + VideoLayer( + clips: [ + VideoSegment( + video: EditorVideo.asset(kVideoEditorExampleAssetWorldPath), + endTime: const Duration(seconds: 3), + ), + ], + ), + VideoLayer( + clips: [VideoSegment(video: source)], + chromaKey: ChromaKey( + color: _keyColor, + similarity: _similarity, + smoothness: _smoothness, + spill: _spill, + ), + ), + ], + ), + ); + } + } + + /// Measures the screen in the source and adopts the result, so the sliders + /// show what was found instead of a guess. + Future _autoDetect() async { + setState(() => _isDetecting = true); + try { + final detection = await ChromaKey.detect(_greenScreen); + if (!mounted) return; + setState(() { + _detection = detection; + _keyColor = detection.color; + _similarity = detection.similarity; + }); + } on ChromaKeyDetectionException catch (e) { + if (!mounted) return; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(e.message))); + } finally { + if (mounted) setState(() => _isDetecting = false); + } + } + + Future _render() async { + if (_isExporting) return; + + _taskId = DateTime.now().microsecondsSinceEpoch.toString(); + setState(() { + _isExporting = true; + _isPreviewInitialized = false; + }); + + await _disposePreview(); + + final data = _buildRenderData(_greenScreen); + + final directory = await getTemporaryDirectory(); + final now = DateTime.now().millisecondsSinceEpoch; + final outputPath = '${directory.path}/chroma_key_$now.mp4'; + + final sw = Stopwatch()..start(); + try { + await _pve.renderVideoToFile( + outputPath, + data, + nativeLogLevel: NativeLogLevel.debug, + ); + } on RenderCanceledException { + setState(() => _isExporting = false); + return; + } + _generationTime = sw.elapsed; + + _videoBytes = File(outputPath).readAsBytesSync(); + _isExporting = false; + + _controllerPreview = VideoPlayerController.file(File(outputPath)); + await _controllerPreview!.initialize(); + _chewieControllerPreview = ChewieController( + videoPlayerController: _controllerPreview!, + autoPlay: true, + looping: true, + placeholder: Container(color: Colors.black), + ); + + if (!mounted) return; + setState(() => _isPreviewInitialized = true); + } + + Future _cancel() async { + if (!_supportsCancel) return; + await _pve.cancel(_taskId); + } + + Future _disposePreview() async { + await _chewieControllerPreview?.pause(); + _chewieControllerPreview?.dispose(); + _chewieControllerPreview = null; + final controller = _controllerPreview; + _controllerPreview = null; + await controller?.dispose(); + } + + @override + Widget build(BuildContext context) { + final bottom = MediaQuery.viewPaddingOf(context).bottom; + return Scaffold( + appBar: AppBar(title: const Text('Chroma Key')), + body: SingleChildScrollView( + padding: EdgeInsets.fromLTRB(16, 16, 16, 16 + bottom), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 20, + children: [ + if (_videoBytes != null) _buildPreview(), + if (_isExporting) + VideoRendererProgressPanel( + progressStream: _pve.progressStreamById(_taskId), + supportsCancel: _supportsCancel, + onCancel: _supportsCancel ? _cancel : null, + ) + else + _buildControls(), + NativeLogConsole(logStream: _pve.logStream), + ], + ), + ), + ); + } + + Widget _buildControls() { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 12, + children: [ + const Text( + 'Source: a person in front of a lit studio green screen. The screen ' + 'is not evenly lit — its darkest corners sit close to the default ' + 'similarity, so lowering that slider makes them survive the key.', + style: TextStyle(fontWeight: FontWeight.bold), + ), + OutlinedButton.icon( + onPressed: _isDetecting ? null : _autoDetect, + icon: _isDetecting + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.colorize_outlined), + label: const Text('Measure the screen from the clip'), + ), + if (_detection != null) _buildDetectionSummary(_detection!), + const Text('Fill the removed screen with:'), + SegmentedButton<_Background>( + selected: {_background}, + onSelectionChanged: (v) => setState(() => _background = v.first), + segments: const [ + ButtonSegment( + value: _Background.color, + icon: Icon(Icons.format_color_fill_outlined), + label: Text('Color'), + ), + ButtonSegment( + value: _Background.image, + icon: Icon(Icons.image_outlined), + label: Text('Image'), + ), + ButtonSegment( + value: _Background.video, + icon: Icon(Icons.layers_outlined), + label: Text('Video'), + ), + ], + ), + if (_background == _Background.video) + const Text( + 'Transparent key on the upper layer of a VideoComposition, so the ' + 'video below shows through.', + style: TextStyle(fontSize: 12), + ), + _buildSlider( + label: 'similarity', + value: _similarity, + min: 0.01, + max: 0.6, + hint: 'Raise it if the screen survives, lower it if the subject goes', + onChanged: (v) => setState(() => _similarity = v), + ), + _buildSlider( + label: 'smoothness', + value: _smoothness, + min: 0, + max: 0.4, + hint: 'Width of the soft edge', + onChanged: (v) => setState(() => _smoothness = v), + ), + _buildSlider( + label: 'spill', + value: _spill, + min: 0, + max: 1, + hint: 'How much of the green cast is pulled off the subject', + onChanged: (v) => setState(() => _spill = v), + ), + FilledButton.icon( + onPressed: _render, + icon: const Icon(Icons.movie_creation_outlined), + label: const Text('Render with this key'), + ), + ], + ); + } + + Widget _buildDetectionSummary(ChromaKeyDetection d) { + final hex = d.color.toARGB32().toRadixString(16).toUpperCase(); + return Text( + 'Detected 0x$hex · spread ${d.spread.toStringAsFixed(3)} · ' + '${(d.coverage * 100).toStringAsFixed(0)}% of the border. ' + 'The SMPTE constant sits about 0.12 away from this — margin the key ' + 'would have to spend instead of keeping the subject safe.', + style: const TextStyle(fontSize: 12), + ); + } + + Widget _buildSlider({ + required String label, + required double value, + required double min, + required double max, + required String hint, + required ValueChanged onChanged, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('$label: ${value.toStringAsFixed(2)}'), + Text(hint, style: const TextStyle(fontSize: 12)), + Slider(value: value, min: min, max: max, onChanged: onChanged), + ], + ); + } + + Widget _buildPreview() { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 5, + children: [ + const Text('Output-Video'), + AspectRatio( + aspectRatio: _canvas.width / _canvas.height, + child: _isPreviewInitialized + ? Chewie(controller: _chewieControllerPreview!) + : const Center(child: CircularProgressIndicator()), + ), + Text( + 'Result: ${formatBytes(_videoBytes!.lengthInBytes)} bytes ' + 'in ${_generationTime.inMilliseconds}ms', + ), + ], + ); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index bd5944c9..c652e085 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -7,6 +7,7 @@ import 'package:pro_video_editor_example/features/editor/pages/video_editor_grou import 'features/audio/audio_extract_example_page.dart'; import 'features/audio/audio_merge_example_page.dart'; +import 'features/chroma_key/chroma_key_example_page.dart'; import 'features/metadata/video_metadata_example_page.dart'; import 'features/render/video_renderer_page.dart'; import 'features/split/split_example_page.dart'; @@ -84,6 +85,11 @@ class _HomePageState extends State { title: 'Video-Renderer', pageBuilder: () => const VideoRendererPage(), ), + _ExampleListItem( + icon: Icons.filter_center_focus, + title: 'Chroma Key (Greenscreen)', + pageBuilder: () => const ChromaKeyExamplePage(), + ), _ExampleListItem( icon: Icons.burst_mode_outlined, title: 'Stop-Motion', diff --git a/example/macos/RunnerTests/RunnerTests.swift b/example/macos/RunnerTests/RunnerTests.swift index 2350453c..e052f782 100644 --- a/example/macos/RunnerTests/RunnerTests.swift +++ b/example/macos/RunnerTests/RunnerTests.swift @@ -542,3 +542,217 @@ enum ThumbnailTimestampFixture { return bestIndex } } + +// MARK: - Chroma key + +/// Cross-platform parity guard for the chroma-key formula. +/// +/// The golden table below is duplicated verbatim in the Kotlin test +/// (`android/src/test/.../ChromaKeyMathTest.kt`). Both run it against their own +/// implementation — Swift's `chromaKeyed(r:g:b:_:)`, which is baked into the +/// Core Image color cube, and Kotlin's `ChromaKeyMath`, which the GLSL shader +/// mirrors. If either platform drifts, one of these two tests fails immediately +/// instead of the difference surfacing later as "the key looks slightly +/// different on Android". +/// +/// **When you change the formula, regenerate both tables.** +class ChromaKeyMathTests: XCTestCase { + + /// The config the golden table was computed for: SMPTE green, defaults. + private let config = ChromaKeyConfig( + keyR: Double(0x00) / 255.0, + keyG: Double(0xB1) / 255.0, + keyB: Double(0x40) / 255.0, + similarity: 0.15, + smoothness: 0.08, + spill: 0.5, + backgroundColor: -1, + backgroundImageData: nil + ) + + private let tolerance = 1e-4 + + /// The same key with a different despill strength. + private func withSpill(_ spill: Double) -> ChromaKeyConfig { + ChromaKeyConfig( + keyR: config.keyR, keyG: config.keyG, keyB: config.keyB, + similarity: config.similarity, smoothness: config.smoothness, + spill: spill, backgroundColor: -1, backgroundImageData: nil) + } + + private struct Golden { + let name: String + let r, g, b: Double + let outR, outG, outB, alpha: Double + } + + private let golden: [Golden] = [ + // The key color itself and its neighbourhood: removed completely. + Golden(name: "key color", r: 0.0, g: 0.694118, b: 0.25098, + outR: 0.218029, outG: 0.565088, outB: 0.343519, alpha: 0.0), + Golden(name: "near key", r: 0.05, g: 0.72, b: 0.28, + outR: 0.261122, outG: 0.595058, outB: 0.369608, alpha: 0.0), + Golden(name: "bright screen", r: 0.35, g: 0.9, b: 0.5, + outR: 0.525426, outG: 0.796183, outB: 0.574457, alpha: 0.0), + // Half-lit screen: past the default similarity, so only mostly removed. + // This is the documented brightness sensitivity, pinned on purpose. + Golden(name: "dim screen 50%", r: 0.0, g: 0.347059, b: 0.12549, + outR: 0.109015, outG: 0.282544, outB: 0.17176, alpha: 0.081671), + // Neutrals sit at the chroma origin, far from any saturated key. + Golden(name: "black", r: 0.0, g: 0.0, b: 0.0, + outR: 0.0, outG: 0.0, outB: 0.0, alpha: 1.0), + Golden(name: "mid gray", r: 0.5, g: 0.5, b: 0.5, + outR: 0.5, outG: 0.5, outB: 0.5, alpha: 1.0), + Golden(name: "white", r: 1.0, g: 1.0, b: 1.0, + outR: 1.0, outG: 1.0, outB: 1.0, alpha: 1.0), + // Skin tone — the calibration anchor quoted in the Dart docs. + Golden(name: "skin tone", r: 0.86, g: 0.65, b: 0.53, + outR: 0.86, outG: 0.65, outB: 0.53, alpha: 1.0), + Golden(name: "pure red", r: 1.0, g: 0.0, b: 0.0, + outR: 1.0, outG: 0.0, outB: 0.0, alpha: 1.0), + Golden(name: "pure blue", r: 0.0, g: 0.0, b: 1.0, + outR: 0.0, outG: 0.0, outB: 1.0, alpha: 1.0), + // Kept, but despilled: the green cast is pulled out at full alpha. + Golden(name: "green-spilled gray", r: 0.55, g: 0.75, b: 0.55, + outR: 0.616767, outG: 0.710487, outB: 0.578338, alpha: 1.0), + // Leans away from the key hue, so despill leaves it alone. + Golden(name: "magenta", r: 0.8, g: 0.2, b: 0.8, + outR: 0.8, outG: 0.2, outB: 0.8, alpha: 1.0), + ] + + func testGoldenTableMatchesTheSharedFormula() { + for row in golden { + let out = chromaKeyed(r: row.r, g: row.g, b: row.b, config) + XCTAssertEqual(out.r, row.outR, accuracy: tolerance, "\(row.name): r") + XCTAssertEqual(out.g, row.outG, accuracy: tolerance, "\(row.name): g") + XCTAssertEqual(out.b, row.outB, accuracy: tolerance, "\(row.name): b") + XCTAssertEqual(out.a, row.alpha, accuracy: tolerance, "\(row.name): alpha") + } + } + + func testKeyColorIsRemovedCompletely() { + let out = chromaKeyed(r: config.keyR, g: config.keyG, b: config.keyB, config) + XCTAssertEqual(out.a, 0.0, accuracy: tolerance) + } + + func testSoftEdgeProducesPartialAlpha() { + // Walk the key color toward neutral gray and collect the alpha ramp. + let ramp: [Double] = (0...60).map { step in + let t = Double(step) / 60.0 + return chromaKeyed( + r: config.keyR + (0.5 - config.keyR) * t, + g: config.keyG + (0.5 - config.keyG) * t, + b: config.keyB + (0.5 - config.keyB) * t, + config + ).a + } + + XCTAssertTrue(ramp.contains(0.0), "expected a fully keyed sample") + XCTAssertTrue(ramp.contains(1.0), "expected a fully opaque sample") + XCTAssertTrue( + ramp.contains { $0 > 0.01 && $0 < 0.99 }, + "expected a soft edge, but alpha jumped straight from 0 to 1") + } + + func testSpillPullsTheKeyCastOutWithoutDarkening() { + let noSpill = withSpill(0.0) + let fullSpill = withSpill(1.0) + + let without = chromaKeyed(r: 0.55, g: 0.75, b: 0.55, noSpill) + let with = chromaKeyed(r: 0.55, g: 0.75, b: 0.55, fullSpill) + + let castBefore = without.g - max(without.r, without.b) + let castAfter = with.g - max(with.r, with.b) + XCTAssertLessThan(castAfter, castBefore, "despill did not reduce the green cast") + + // Luma is preserved, so despill never darkens the subject. + XCTAssertEqual( + lumaOf(r: without.r, g: without.g, b: without.b), + lumaOf(r: with.r, g: with.g, b: with.b), + accuracy: 1e-3) + } + + func testMatteIsBrightnessSensitiveAsDocumented() { + // Cb/Cr scale with brightness, so a dimly lit patch of the screen sits + // closer to neutral and further from the key point. The default similarity + // covers roughly 55%..100% of the reference brightness. Pinned because the + // Dart docs promise exactly this. + func screenAt(_ fraction: Double) -> Double { + chromaKeyed( + r: config.keyR * fraction, g: config.keyG * fraction, + b: config.keyB * fraction, config + ).a + } + + XCTAssertEqual(screenAt(1.0), 0.0, accuracy: tolerance) + XCTAssertEqual(screenAt(0.55), 0.0, accuracy: tolerance) + XCTAssertGreaterThan(screenAt(0.5), 0.0, "a half-lit screen should start to survive") + } + + func testNeutralKeyColorDisablesDespillInsteadOfDividingByZero() { + let gray = ChromaKeyConfig( + keyR: 0.5, keyG: 0.5, keyB: 0.5, similarity: 0.15, smoothness: 0.08, + spill: 1.0, backgroundColor: -1, backgroundImageData: nil) + + XCTAssertEqual(gray.keyDirection.cb, 0.0, accuracy: tolerance) + XCTAssertEqual(gray.keyDirection.cr, 0.0, accuracy: tolerance) + + let out = chromaKeyed(r: 0.8, g: 0.2, b: 0.4, gray) + XCTAssertFalse(out.r.isNaN || out.g.isNaN || out.b.isNaN || out.a.isNaN) + } + + /// The cube is what actually runs on Apple, so verify it carries the same + /// numbers **and** that its entries are premultiplied, which `CIColorCube` + /// requires and which is the one place Apple and Android must differ. + func testCubeIsPremultipliedAndMatchesTheFormula() throws { + let size = 33 + let data = try XCTUnwrap( + generateChromaLUTData(chroma: config, size: size)) + + XCTAssertEqual(data.count, size * size * size * 4 * MemoryLayout.size) + + let floats: [Float] = data.withUnsafeBytes { Array($0.bindMemory(to: Float.self)) } + + // Walk the cube's own sample grid, so no interpolation is involved. + for bi in stride(from: 0, to: size, by: 8) { + for gi in stride(from: 0, to: size, by: 8) { + for ri in stride(from: 0, to: size, by: 8) { + let rf = Double(ri) / Double(size - 1) + let gf = Double(gi) / Double(size - 1) + let bf = Double(bi) / Double(size - 1) + let expected = chromaKeyed(r: rf, g: gf, b: bf, config) + + let offset = (bi * size * size + gi * size + ri) * 4 + XCTAssertEqual( + Double(floats[offset]), expected.r * expected.a, accuracy: 1e-5) + XCTAssertEqual( + Double(floats[offset + 1]), expected.g * expected.a, accuracy: 1e-5) + XCTAssertEqual( + Double(floats[offset + 2]), expected.b * expected.a, accuracy: 1e-5) + XCTAssertEqual(Double(floats[offset + 3]), expected.a, accuracy: 1e-5) + } + } + } + } + + /// A fully keyed entry must be `(0, 0, 0, 0)`, not `(rgb, 0)` — Core Image + /// would otherwise composite the key color back in at the edges. + func testFullyKeyedCubeEntriesAreZero() throws { + let size = 33 + let data = try XCTUnwrap( + generateChromaLUTData(chroma: config, size: size)) + let floats: [Float] = data.withUnsafeBytes { Array($0.bindMemory(to: Float.self)) } + + // Nearest grid point to the key color. + let ri = Int((config.keyR * Double(size - 1)).rounded()) + let gi = Int((config.keyG * Double(size - 1)).rounded()) + let bi = Int((config.keyB * Double(size - 1)).rounded()) + let offset = (bi * size * size + gi * size + ri) * 4 + + XCTAssertEqual(floats[offset + 3], 0.0, accuracy: 1e-5, "key should be fully removed") + XCTAssertEqual(floats[offset], 0.0, accuracy: 1e-5, "premultiplied red must be 0") + XCTAssertEqual(floats[offset + 1], 0.0, accuracy: 1e-5, "premultiplied green must be 0") + XCTAssertEqual(floats[offset + 2], 0.0, accuracy: 1e-5, "premultiplied blue must be 0") + } + +} diff --git a/lib/core/models/video/chroma_key_model.dart b/lib/core/models/video/chroma_key_model.dart new file mode 100644 index 00000000..5f53d0d2 --- /dev/null +++ b/lib/core/models/video/chroma_key_model.dart @@ -0,0 +1,467 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:convert'; +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:pro_video_editor/pro_video_editor.dart'; +import 'package:pro_video_editor/shared/utils/parser/double_parser.dart'; +import 'package:pro_video_editor/shared/utils/parser/int_parser.dart'; + +/// Removes a solid-colored background (a "green screen") from the video. +/// +/// Pixels whose hue is close to [color] are made transparent, with a soft edge +/// so the matte does not alias, and the key's color cast is pulled back out of +/// the pixels that remain ([spill]). +/// +/// ## The algorithm +/// +/// Both platforms evaluate the exact same formula, so a key tuned on one +/// renders the same on the other. Every value is gamma-encoded RGB in `0..1`: +/// +/// ```text +/// Y = 0.299·r + 0.587·g + 0.114·b +/// Cb = -0.168736·r - 0.331264·g + 0.5·b +/// Cr = 0.5·r - 0.418688·g - 0.081312·b +/// +/// d = distance((Cb, Cr), (Cb_key, Cr_key)) +/// alpha = smoothstep(similarity, similarity + smoothness, d) +/// ``` +/// +/// The distance is measured in the **Cb/Cr chroma plane**, which is a position, +/// not a pure hue: Cb and Cr scale with brightness, so a dimly lit patch of the +/// screen sits closer to neutral and therefore further from the key point. The +/// default [similarity] covers roughly 40%–100% of the screen's reference +/// brightness; a badly lit screen needs a wider one. This is the same behaviour +/// as FFmpeg's `chromakey` and OBS. +/// +/// ## What the keyed area becomes +/// +/// - [backgroundImage] — the image fills it (stretched to the frame). +/// - [backgroundColor] — that color fills it. +/// - neither — the area is **transparent**, so a lower [VideoLayer] of a +/// [VideoComposition] shows through. This is how you put a video behind a +/// green screen. +/// +/// **Important:** H.264 and HEVC carry no alpha channel. In the single-track +/// [VideoRenderData.videoSegments] path there is no layer underneath, so a key +/// without a background is flattened to opaque **black**. Set a background, or +/// use [VideoRenderData.composition] and place the keyed clip on a layer above +/// another one. +/// +/// ## Where it can be set +/// +/// On [VideoRenderData], [VideoLayer] and [VideoSegment]. The most specific one +/// wins for a given clip — `segment → layer → global` — they are not merged. +/// +/// ## Limits +/// +/// - No time ranges (unlike [ColorFilter]). Split the clip into two +/// [VideoSegment]s when the key has to change mid-clip. +/// - Ignored inside an overlap [ClipTransition] (dissolve/slide/push/wipe) when +/// the two clips at that boundary do not carry the same key — the blend is +/// pre-rendered from the raw sources and is emitted unkeyed with a warning. +/// - No matte choke/erode and no light wrap. +/// - Combining a transparent key with [VideoRenderData.blur] is not +/// recommended: the blur bleeds color out of the transparent pixels. +/// - Ignored on Web, Windows and Linux, which have no render pipeline. +class ChromaKey { + /// Creates a [ChromaKey]. + const ChromaKey({ + this.color = const Color(0xFF00B140), + this.similarity = 0.20, + this.smoothness = 0.08, + this.spill = 0.5, + this.backgroundColor, + this.backgroundImage, + }) : assert( + similarity > 0 && similarity <= 1, + '[similarity] must be greater than 0 and at most 1', + ), + assert( + smoothness >= 0 && smoothness <= 1, + '[smoothness] must be between 0 and 1', + ), + assert(spill >= 0 && spill <= 1, '[spill] must be between 0 and 1'), + assert( + backgroundColor == null || backgroundImage == null, + 'Provide at most one of [backgroundColor] or [backgroundImage]', + ); + + /// A key tuned for a **green** screen. + /// + /// Identical to the default constructor; it exists so the counterpart + /// [ChromaKey.blueScreen] reads as a deliberate choice rather than an + /// afterthought. + /// + /// Prefer [autoDetect] when you have the footage: measuring the screen beats + /// any constant, because it removes the offset between the paint and what the + /// camera recorded. + const ChromaKey.greenScreen({ + this.similarity = 0.20, + this.smoothness = 0.08, + this.spill = 0.5, + this.backgroundColor, + this.backgroundImage, + }) : color = const Color(0xFF00B140), + assert( + similarity > 0 && similarity <= 1, + '[similarity] must be greater than 0 and at most 1', + ), + assert( + smoothness >= 0 && smoothness <= 1, + '[smoothness] must be between 0 and 1', + ), + assert(spill >= 0 && spill <= 1, '[spill] must be between 0 and 1'), + assert( + backgroundColor == null || backgroundImage == null, + 'Provide at most one of [backgroundColor] or [backgroundImage]', + ); + + /// A key tuned for a **blue** screen. + /// + /// Blue is not green with a different hue — the two fail on opposite things. + /// Blue separates skin better (`0.41`–`0.46` versus green's `0.38`–`0.42`), + /// which is why it was the film standard, but denim (`0.19`), blue eyes + /// (`0.20`) and a light blue shirt (`0.21`) all crowd it. So this preset + /// keys **tighter** than the green one and despills more gently, or a pair of + /// jeans disappears along with the background. + /// + /// The price of the tighter [similarity] is that the screen has to be lit + /// more evenly: `0.12` reaches down to about 64% of its brightest patch, + /// where green's `0.20` reaches 40%. If parts of the screen survive, raise + /// [similarity] — but check the subject's wardrobe when you do. + const ChromaKey.blueScreen({ + this.similarity = 0.12, + this.smoothness = 0.08, + this.spill = 0.25, + this.backgroundColor, + this.backgroundImage, + }) : color = const Color(0xFF0047BB), + assert( + similarity > 0 && similarity <= 1, + '[similarity] must be greater than 0 and at most 1', + ), + assert( + smoothness >= 0 && smoothness <= 1, + '[smoothness] must be between 0 and 1', + ), + assert(spill >= 0 && spill <= 1, '[spill] must be between 0 and 1'), + assert( + backgroundColor == null || backgroundImage == null, + 'Provide at most one of [backgroundColor] or [backgroundImage]', + ); + + /// Measures the screen in [video] and returns a key tuned to it. + /// + /// This is the most reliable way to build a key, and it is hue-agnostic — + /// green, blue or anything else, as long as it is saturated. + /// + /// A constant [color] is always a compromise. Paint, fabric, lighting and the + /// camera's color science all shift the recorded screen away from it, and + /// every bit of that shift has to be absorbed by a wider [similarity] — which + /// is exactly the margin that protects the subject. Measured on a real studio + /// clip: against the SMPTE constant the screen sat `0.10`–`0.20` away and + /// needed `similarity: 0.20`; against its own measured color it sat within + /// `0.05` and `0.10` was plenty. Same footage, twice the headroom. + /// + /// Frames are sampled through the existing thumbnail pipeline, so this costs + /// one decode and no render. + /// + /// **The screen has to reach the frame border**, because that is what lets + /// the subject be ignored — only a ring around the edge is sampled. A frame + /// that also shows the studio around the screen will throw a + /// [ChromaKeyDetectionException]; crop it first, or set the key by hand. + /// + /// ```dart + /// final key = await ChromaKey.autoDetect( + /// myClip, + /// backgroundColor: Colors.black, + /// ); + /// ``` + /// + /// Throws [ChromaKeyDetectionException] when no usable screen is found. + static Future autoDetect( + EditorVideo video, { + List? timestamps, + double smoothness = 0.08, + double spill = 0.5, + Color? backgroundColor, + EditorLayerImage? backgroundImage, + }) async { + final detection = await detect(video, timestamps: timestamps); + return ChromaKey( + color: detection.color, + similarity: detection.similarity, + smoothness: smoothness, + spill: spill, + backgroundColor: backgroundColor, + backgroundImage: backgroundImage, + ); + } + + /// Measures the screen in [video] without building a key. + /// + /// Use this when you want to show the measurement to the user — the detected + /// color, how evenly the screen is lit ([ChromaKeyDetection.spread]) and how + /// much of the border it covers — before committing to it. + static Future detect( + EditorVideo video, { + List? timestamps, + }) async { + final editor = ProVideoEditor.instance; + final metadata = await editor.getMetadata(video); + + // Sample across the clip so a subject that briefly touches an edge cannot + // decide the result on its own. + final duration = metadata.duration; + final at = + timestamps ?? + [ + duration * 0.25, + duration * 0.5, + duration * 0.75, + ]; + + // Small, and at the source aspect ratio: `cover` then neither crops nor + // pads, so the sampled ring really is the frame's own border. + const sampleHeight = 160.0; + final aspect = metadata.resolution.aspectRatio; + final size = Size((sampleHeight * aspect).roundToDouble(), sampleHeight); + + final frames = await editor.getThumbnails( + ThumbnailConfigs( + video: video, + outputFormat: ThumbnailFormat.png, + timestamps: at, + outputSize: size, + boxFit: ThumbnailBoxFit.cover, + ), + ); + + if (frames.isEmpty) { + throw const ChromaKeyDetectionException( + 'Could not extract a frame from the video', + ); + } + + final buffers = []; + int? width; + int? height; + for (final frame in frames) { + final codec = await instantiateImageCodec(frame); + final image = (await codec.getNextFrame()).image; + final data = await image.toByteData(); + if (data == null) continue; + width ??= image.width; + height ??= image.height; + if (image.width != width || image.height != height) continue; + buffers.add(data.buffer.asUint8List()); + } + + if (buffers.isEmpty || width == null || height == null) { + throw const ChromaKeyDetectionException('Could not decode any frame'); + } + + return ChromaKeyDetector.fromFrames( + buffers, + width: width, + height: height, + ); + } + + /// The screen color to remove. + /// + /// **Default**: `0xFF00B140`, the SMPTE "chroma key green" most physical + /// screens are painted in. Only the hue is used — the brightness of this + /// color does not matter. + /// + /// ## Blue screens + /// + /// Any saturated hue works; nothing here is specific to green. A blue screen + /// (`0xFF0047BB`) keys just as cleanly and is in fact *safer for skin*, which + /// sits `0.41`–`0.46` from it versus `0.38`–`0.42` from green. + /// + /// The trade is what the subject wears. Measured against SMPTE blue: + /// + /// | | distance | + /// |---|---| + /// | denim / jeans | `0.19` | + /// | blue eyes | `0.20` | + /// | light blue shirt | `0.21` | + /// + /// All three sit at or below the default [similarity] of `0.20`, which is + /// tuned for green — so on a blue screen denim and a light blue shirt are + /// keyed away along with the background. Drop [similarity] to about `0.12` + /// for blue, and lower [spill] as well when the subject wears blue, or the + /// despill pulls the blue out of the garment too (denim measured + /// `(59,91,140)` → `(92,79,98)` at the default `spill`). Skin is unaffected + /// by despill on a blue screen, since it leans away from that hue. + /// + /// A lower [similarity] needs a more evenly lit screen: `0.12` only covers + /// down to about 65% of the screen's reference brightness. + /// + /// Avoid the darker "digital blue" (`0xFF1A46A8`) — its chroma magnitude is + /// only `0.25` versus `0.33`, so every subject color crowds it, a white shirt + /// included. + final Color color; + + /// How far from [color] a pixel may sit and still be removed completely. + /// + /// Measured as a distance in the Cb/Cr chroma plane. Some anchors for + /// calibration, all against the default SMPTE green (whose own chroma + /// magnitude is `0.33`): + /// + /// - skin tones sit `0.38`–`0.42` away, so the default keeps faces safe by a + /// wide margin; + /// - the same screen lit at 40% of its brightest patch sits `0.20` away — + /// exactly at the default threshold. A real, evenly lit studio screen + /// measured `0.18` in its darkest corners, which is why the default is not + /// tighter; + /// - a green-ish garment (an olive shirt) sits around `0.26`, so that is the + /// case where raising this too far starts eating the subject. + /// + /// So: raise it when shadowed or unevenly lit parts of the screen survive the + /// key, and lower it when the subject starts disappearing. + /// + /// **Default**: `0.20` + final double similarity; + + /// The width of the soft ramp just beyond [similarity]. + /// + /// Pixels between `similarity` and `similarity + smoothness` fade from fully + /// removed to fully opaque, which is what keeps hair and motion blur from + /// turning into a jagged cutout. + /// + /// `0` asks for a hard edge. Note that Apple evaluates the key through a + /// 33³ color cube with trilinear interpolation, so a perfectly hard edge is + /// not reachable there — about one cube cell of ramp always remains, which in + /// practice just anti-aliases the matte. Values from `0.05` up span several + /// cells and are smooth on both platforms. + /// + /// **Default**: `0.08` + final double smoothness; + + /// How strongly the key's color cast is removed from the pixels that stay. + /// + /// A green screen bounces green light onto everything near it, leaving a tint + /// along the subject's edges. This pulls the chroma component that points + /// toward [color] back out, without changing the pixel's brightness. + /// + /// - `0.0`: off + /// - `1.0`: neutralize the key hue completely + /// + /// **Default**: `0.5` + final double spill; + + /// A solid color to put behind the subject. + /// + /// Mutually exclusive with [backgroundImage]. When both are `null` the keyed + /// area is transparent — see the class docs for what that means per path. + final Color? backgroundColor; + + /// An image to put behind the subject, stretched to fill the frame. + /// + /// Mutually exclusive with [backgroundColor]. + final EditorLayerImage? backgroundImage; + + /// Whether the keyed area is left transparent rather than filled. + bool get isTransparent => backgroundColor == null && backgroundImage == null; + + /// Converts this key to a map for platform channel communication. + /// + /// Resolves [backgroundImage] to bytes, so this is asynchronous. + Future> toAsyncMap() async { + return { + 'keyColor': color.toARGB32(), + 'similarity': similarity, + 'smoothness': smoothness, + 'spill': spill, + 'bgColor': backgroundColor?.toARGB32(), + 'bgImageData': await backgroundImage?.safeByteArray(), + }; + } + + /// Creates a copy with updated values. + ChromaKey copyWith({ + Color? color, + double? similarity, + double? smoothness, + double? spill, + Color? backgroundColor, + EditorLayerImage? backgroundImage, + }) { + return ChromaKey( + color: color ?? this.color, + similarity: similarity ?? this.similarity, + smoothness: smoothness ?? this.smoothness, + spill: spill ?? this.spill, + backgroundColor: backgroundColor ?? this.backgroundColor, + backgroundImage: backgroundImage ?? this.backgroundImage, + ); + } + + Map toMap() { + return { + 'color': color.toARGB32(), + 'similarity': similarity, + 'smoothness': smoothness, + 'spill': spill, + 'backgroundColor': backgroundColor?.toARGB32(), + 'backgroundImage': backgroundImage?.toMap(), + }; + } + + factory ChromaKey.fromMap(Map map) { + return ChromaKey( + color: Color(safeParseInt(map['color'], fallback: 0xFF00B140)), + similarity: safeParseDouble(map['similarity'], fallback: 0.20), + smoothness: safeParseDouble(map['smoothness'], fallback: 0.08), + spill: safeParseDouble(map['spill'], fallback: 0.5), + backgroundColor: map['backgroundColor'] != null + ? Color(safeParseInt(map['backgroundColor'])) + : null, + backgroundImage: map['backgroundImage'] != null + ? EditorLayerImage.fromMap( + map['backgroundImage'] as Map, + ) + : null, + ); + } + + String toJson() => json.encode(toMap()); + + factory ChromaKey.fromJson(String source) => + ChromaKey.fromMap(json.decode(source) as Map); + + @override + String toString() { + return 'ChromaKey(color: $color, ' + 'similarity: $similarity, ' + 'smoothness: $smoothness, ' + 'spill: $spill, ' + 'backgroundColor: $backgroundColor, ' + 'backgroundImage: $backgroundImage)'; + } + + @override + bool operator ==(covariant ChromaKey other) { + if (identical(this, other)) return true; + + return other.color == color && + other.similarity == similarity && + other.smoothness == smoothness && + other.spill == spill && + other.backgroundColor == backgroundColor && + other.backgroundImage == backgroundImage; + } + + @override + int get hashCode { + return color.hashCode ^ + similarity.hashCode ^ + smoothness.hashCode ^ + spill.hashCode ^ + backgroundColor.hashCode ^ + backgroundImage.hashCode; + } +} diff --git a/lib/core/models/video/editor_video_model.dart b/lib/core/models/video/editor_video_model.dart index 4d59b561..2e413e69 100644 --- a/lib/core/models/video/editor_video_model.dart +++ b/lib/core/models/video/editor_video_model.dart @@ -5,6 +5,10 @@ import '/core/platform/path/path_provider_helper.dart'; import '/shared/utils/converters.dart'; import '/shared/utils/file_constructor_utils.dart'; +/// Distinguishes temp files written within the same millisecond, so several +/// sources resolved concurrently cannot collide on one path. +int _tempFileCounter = 0; + /// A model that encapsulates various ways to load and represent a video. /// /// This class supports videos from in-memory bytes, file system, network, @@ -165,9 +169,15 @@ class EditorVideo { final directory = await getTemporaryDirectory(); final now = DateTime.now().millisecondsSinceEpoch; + // A per-call counter on top of the timestamp. Several sources are + // routinely resolved concurrently (VideoRenderData.toAsyncMap awaits all + // of its segments together), and a millisecond is not fine enough to tell + // them apart — two of them would pick the same path, write over each + // other and end up as one clip. + final unique = _tempFileCounter++; // Preserve original file extension for proper format detection final extension = _getFileExtension(); - filePath = '${directory.path}/media_$now.$extension'; + filePath = '${directory.path}/media_${now}_$unique.$extension'; } switch (typePreferredFile) { diff --git a/lib/core/models/video/video_layer_model.dart b/lib/core/models/video/video_layer_model.dart index 65e38f58..f688ca4b 100644 --- a/lib/core/models/video/video_layer_model.dart +++ b/lib/core/models/video/video_layer_model.dart @@ -18,9 +18,16 @@ import 'package:pro_video_editor/shared/utils/parser/double_parser.dart'; /// clip provides its own [VideoSegment.transform], placed using [transform]. class VideoLayer { /// Creates a [VideoLayer] from a list of [clips]. - const VideoLayer({required this.clips, this.opacity = 1.0, this.transform}) - : assert(clips.length > 0, 'A layer must contain at least one clip'), - assert(opacity >= 0 && opacity <= 1, '[opacity] must be between 0 and 1'); + const VideoLayer({ + required this.clips, + this.opacity = 1.0, + this.transform, + this.chromaKey, + }) : assert(clips.length > 0, 'A layer must contain at least one clip'), + assert( + opacity >= 0 && opacity <= 1, + '[opacity] must be between 0 and 1', + ); /// The time-ordered sequence of clips on this layer. final List clips; @@ -37,9 +44,21 @@ class VideoLayer { /// clips fill the entire canvas unless they define their own transform. final SegmentTransform? transform; + /// Default chroma key for the clips on this layer. + /// + /// This is a **default for the clips**, not a post-composite effect: each + /// clip is keyed on its own source frame, before it is placed on the canvas. + /// A clip with its own [VideoSegment.chromaKey] overrides this; when neither + /// is set, the clip falls back to [VideoRenderData.chromaKey]. + /// + /// Since the keyed area is transparent by default, the layer below shows + /// through — which is how you put a video behind a green screen. + final ChromaKey? chromaKey; + /// Converts this layer to a map for platform channel communication. /// - /// Resolves each clip's input path, so this is asynchronous. + /// Resolves each clip's input path and any chroma-key background image, so + /// this is asynchronous. Future> toAsyncMap() async { assert( clips.every((c) => c.transition == null), @@ -57,6 +76,7 @@ class VideoLayer { 'clips': await Future.wait(clips.map((clip) => clip.toAsyncMap())), 'opacity': opacity, 'transform': transform?.toMap(), + 'chromaKey': await chromaKey?.toAsyncMap(), }; } @@ -65,11 +85,13 @@ class VideoLayer { List? clips, double? opacity, SegmentTransform? transform, + ChromaKey? chromaKey, }) { return VideoLayer( clips: clips ?? this.clips, opacity: opacity ?? this.opacity, transform: transform ?? this.transform, + chromaKey: chromaKey ?? this.chromaKey, ); } @@ -78,6 +100,7 @@ class VideoLayer { 'clips': clips.map((x) => x.toMap()).toList(), 'opacity': opacity, 'transform': transform?.toMap(), + 'chromaKey': chromaKey?.toMap(), }; } @@ -92,6 +115,9 @@ class VideoLayer { transform: map['transform'] != null ? SegmentTransform.fromMap(map['transform'] as Map) : null, + chromaKey: map['chromaKey'] != null + ? ChromaKey.fromMap(map['chromaKey'] as Map) + : null, ); } @@ -102,7 +128,8 @@ class VideoLayer { @override String toString() => - 'VideoLayer(clips: $clips, opacity: $opacity, transform: $transform)'; + 'VideoLayer(clips: $clips, opacity: $opacity, transform: $transform, ' + 'chromaKey: $chromaKey)'; @override bool operator ==(covariant VideoLayer other) { @@ -110,9 +137,14 @@ class VideoLayer { return listEquals(other.clips, clips) && other.opacity == opacity && - other.transform == transform; + other.transform == transform && + other.chromaKey == chromaKey; } @override - int get hashCode => clips.hashCode ^ opacity.hashCode ^ transform.hashCode; + int get hashCode => + clips.hashCode ^ + opacity.hashCode ^ + transform.hashCode ^ + chromaKey.hashCode; } diff --git a/lib/core/models/video/video_render_data_model.dart b/lib/core/models/video/video_render_data_model.dart index 4d174d33..43663048 100644 --- a/lib/core/models/video/video_render_data_model.dart +++ b/lib/core/models/video/video_render_data_model.dart @@ -36,6 +36,7 @@ class VideoRenderData { this.colorFilters = const [], this.audioTracks = const [], this.blur, + this.chromaKey, this.bitrate, this.maxFrameRate, this.shouldOptimizeForNetworkUse = false, @@ -64,6 +65,16 @@ class VideoRenderData { assert( maxFrameRate == null || maxFrameRate > 0, '[maxFrameRate] must be greater than 0', + ), + assert( + composition != null || chromaKey == null || !chromaKey.isTransparent, + 'A [chromaKey] without a background needs something underneath to ' + 'show through, and videoSegments is a single track with nothing ' + 'below it — H.264/HEVC carry no alpha, so the keyed area would be ' + 'flattened to black. Set chromaKey.backgroundColor or ' + 'chromaKey.backgroundImage, or use composition and put the keyed ' + 'clip on a layer above another one. Pass ' + 'backgroundColor: Color(0xFF000000) if you really do want black.', ); /// Creates a [VideoRenderData] with a predefined quality preset. @@ -96,6 +107,7 @@ class VideoRenderData { Duration? startTime, Duration? endTime, double? blur, + ChromaKey? chromaKey, int? bitrateOverride, int? maxFrameRate, List colorFilters = const [], @@ -118,6 +130,7 @@ class VideoRenderData { startTime: startTime, endTime: endTime, blur: blur, + chromaKey: chromaKey, bitrate: bitrateOverride ?? qualityConfig.bitrate, maxFrameRate: maxFrameRate, colorFilters: colorFilters, @@ -234,6 +247,22 @@ class VideoRenderData { /// Higher values result in a stronger blur effect. final double? blur; + /// Removes a solid-colored background ("green screen") from the video. + /// + /// Applies to every clip that does not carry its own key: a + /// [VideoSegment.chromaKey] overrides this, and inside a [composition] a + /// [VideoLayer.chromaKey] sits between the two. They are never merged. + /// + /// Keying runs on the original source colors, so it happens before + /// [colorFilters] and [blur]. + /// + /// See [ChromaKey] for how the key is tuned and what the removed area + /// becomes. Note that a key without a background is only meaningful with + /// [composition] — see the assert on this constructor. + /// + /// **Note:** Ignored on Web, Windows and Linux. + final ChromaKey? chromaKey; + /// The maximum bitrate of the video in bits per second. /// /// This is an upper limit, not a target: when a simple export (a single @@ -420,6 +449,7 @@ class VideoRenderData { 'trimToCommonTrackEnd': trimToCommonTrackEnd, 'outputFormat': outputFormat.name, 'blur': blur, + 'chromaKey': await chromaKey?.toAsyncMap(), // Fall back to the quality config's bitrate when no explicit bitrate is // set, so a `qualityConfig` used on its own is still applied. 'bitrate': bitrate ?? qualityConfig?.bitrate, @@ -453,6 +483,7 @@ class VideoRenderData { List? colorFilters, List? audioTracks, double? blur, + ChromaKey? chromaKey, int? bitrate, int? maxFrameRate, bool? shouldOptimizeForNetworkUse, @@ -473,6 +504,7 @@ class VideoRenderData { colorFilters: colorFilters ?? this.colorFilters, audioTracks: audioTracks ?? this.audioTracks, blur: blur ?? this.blur, + chromaKey: chromaKey ?? this.chromaKey, bitrate: bitrate ?? this.bitrate, maxFrameRate: maxFrameRate ?? this.maxFrameRate, shouldOptimizeForNetworkUse: @@ -498,6 +530,7 @@ class VideoRenderData { 'colorFilters': colorFilters.map((x) => x.toMap()).toList(), 'audioTracks': audioTracks.map((x) => x.toMap()).toList(), 'blur': blur, + 'chromaKey': chromaKey?.toMap(), 'bitrate': bitrate, 'maxFrameRate': maxFrameRate, 'shouldOptimizeForNetworkUse': shouldOptimizeForNetworkUse, @@ -555,6 +588,9 @@ class VideoRenderData { ), ), blur: tryParseDouble(map['blur']), + chromaKey: map['chromaKey'] != null + ? ChromaKey.fromMap(map['chromaKey'] as Map) + : null, bitrate: map['bitrate'] != null ? safeParseInt(map['bitrate']) : null, maxFrameRate: map['maxFrameRate'] != null ? safeParseInt(map['maxFrameRate']) @@ -585,6 +621,7 @@ class VideoRenderData { 'colorFilters: $colorFilters, ' 'audioTracks: $audioTracks, ' 'blur: $blur, ' + 'chromaKey: $chromaKey, ' 'bitrate: $bitrate, ' 'maxFrameRate: $maxFrameRate, ' 'shouldOptimizeForNetworkUse: $shouldOptimizeForNetworkUse, ' @@ -609,6 +646,7 @@ class VideoRenderData { listEquals(other.colorFilters, colorFilters) && listEquals(other.audioTracks, audioTracks) && other.blur == blur && + other.chromaKey == chromaKey && other.bitrate == bitrate && other.maxFrameRate == maxFrameRate && other.shouldOptimizeForNetworkUse == shouldOptimizeForNetworkUse && @@ -631,6 +669,7 @@ class VideoRenderData { colorFilters.hashCode ^ audioTracks.hashCode ^ blur.hashCode ^ + chromaKey.hashCode ^ bitrate.hashCode ^ maxFrameRate.hashCode ^ shouldOptimizeForNetworkUse.hashCode ^ diff --git a/lib/core/models/video/video_segment_model.dart b/lib/core/models/video/video_segment_model.dart index 2af94aa6..135ef19f 100644 --- a/lib/core/models/video/video_segment_model.dart +++ b/lib/core/models/video/video_segment_model.dart @@ -21,6 +21,7 @@ class VideoSegment { this.transition, this.timelineStart, this.transform, + this.chromaKey, }) : assert( startTime == null || endTime == null || startTime < endTime, 'startTime must be before endTime', @@ -122,7 +123,21 @@ class VideoSegment { /// or fills the entire canvas if neither is set. final SegmentTransform? transform; + /// Removes a solid-colored background from this clip only. + /// + /// Overrides [VideoLayer.chromaKey] and [VideoRenderData.chromaKey] for this + /// clip; the three are never merged. When `null`, the clip falls back to its + /// layer's key, then to the global one. + /// + /// **Ignored inside an overlap [transition]** (dissolve/slide/push/wipe) when + /// the two clips at that boundary carry different keys — that blend is + /// pre-rendered from the raw sources and is emitted unkeyed with a warning. + final ChromaKey? chromaKey; + /// Converts this clip to a map for platform channel communication. + /// + /// Resolves the input path and any chroma-key background image, so this is + /// asynchronous. Future> toAsyncMap() async { final inputPath = await video.safeFilePath(); @@ -136,6 +151,7 @@ class VideoSegment { 'transition': transition?.toMap(), 'timelineStartUs': timelineStart?.inMicroseconds, 'transform': transform?.toMap(), + 'chromaKey': await chromaKey?.toAsyncMap(), }; } @@ -150,6 +166,7 @@ class VideoSegment { ClipTransition? transition, Duration? timelineStart, SegmentTransform? transform, + ChromaKey? chromaKey, }) { return VideoSegment( video: video ?? this.video, @@ -161,6 +178,7 @@ class VideoSegment { transition: transition ?? this.transition, timelineStart: timelineStart ?? this.timelineStart, transform: transform ?? this.transform, + chromaKey: chromaKey ?? this.chromaKey, ); } @@ -176,7 +194,8 @@ class VideoSegment { other.reverseVideo == reverseVideo && other.transition == transition && other.timelineStart == timelineStart && - other.transform == transform; + other.transform == transform && + other.chromaKey == chromaKey; } @override @@ -189,7 +208,8 @@ class VideoSegment { reverseVideo.hashCode ^ transition.hashCode ^ timelineStart.hashCode ^ - transform.hashCode; + transform.hashCode ^ + chromaKey.hashCode; } @override @@ -202,7 +222,8 @@ class VideoSegment { 'reverseVideo: $reverseVideo, ' 'transition: $transition, ' 'timelineStart: $timelineStart, ' - 'transform: $transform)'; + 'transform: $transform, ' + 'chromaKey: $chromaKey)'; } Map toMap() { @@ -216,6 +237,7 @@ class VideoSegment { 'transition': transition?.toMap(), 'timelineStart': timelineStart?.inMicroseconds, 'transform': transform?.toMap(), + 'chromaKey': chromaKey?.toMap(), }; } @@ -240,6 +262,9 @@ class VideoSegment { transform: map['transform'] != null ? SegmentTransform.fromMap(map['transform'] as Map) : null, + chromaKey: map['chromaKey'] != null + ? ChromaKey.fromMap(map['chromaKey'] as Map) + : null, ); } diff --git a/lib/core/utils/chroma_key_detector.dart b/lib/core/utils/chroma_key_detector.dart new file mode 100644 index 00000000..f4f33688 --- /dev/null +++ b/lib/core/utils/chroma_key_detector.dart @@ -0,0 +1,221 @@ +import 'dart:math'; +import 'dart:typed_data'; +import 'dart:ui'; + +/// What [ChromaKeyDetector] read out of a frame. +class ChromaKeyDetection { + /// Creates a [ChromaKeyDetection]. + const ChromaKeyDetection({ + required this.color, + required this.similarity, + required this.coverage, + required this.spread, + }); + + /// The screen color as it was actually recorded — not the paint it was + /// mixed from. These differ by more than you would expect: a studio screen + /// painted SMPTE green (`0xFF00B140`) measured `0xFF2A9D37` through the + /// camera, which is a chroma distance of `0.12` — most of the budget a + /// hand-set key has to spend before it touches the subject. + final Color color; + + /// A [similarity] that covers the measured spread of the screen with room to + /// spare, without reaching further than it has to. + final double similarity; + + /// The fraction of sampled border pixels that belong to the detected screen, + /// in `0..1`. + /// + /// Low coverage means the border was not one solid color — the frame likely + /// shows more than just the screen (studio walls, lights, a subject standing + /// at the edge), and the result should not be trusted. + final double coverage; + + /// The 99th-percentile chroma distance of the screen from [color]. + /// + /// This is the raw measurement [similarity] is derived from; a large value + /// means an unevenly lit screen. + final double spread; + + @override + String toString() => + 'ChromaKeyDetection(color: $color, similarity: ' + '${similarity.toStringAsFixed(3)}, coverage: ' + '${(coverage * 100).toStringAsFixed(0)}%, spread: ' + '${spread.toStringAsFixed(3)})'; +} + +/// Thrown when a frame does not contain a usable screen. +class ChromaKeyDetectionException implements Exception { + /// Creates a [ChromaKeyDetectionException] with a [message] explaining what + /// the frame looked like instead. + const ChromaKeyDetectionException(this.message); + + /// What was measured, and why it is not a screen. + final String message; + + @override + String toString() => 'ChromaKeyDetectionException: $message'; +} + +/// Reads the screen color and its spread out of a frame. +/// +/// A fixed key color is always a compromise: paint, fabric, lighting and the +/// camera's own color science all move the recorded green away from whatever +/// constant the caller picked, and every bit of that offset has to be absorbed +/// by a wider [ChromaKey.similarity] — which is budget taken away from the +/// margin that protects the subject. Measuring the screen instead removes the +/// offset entirely, and works the same for a green, blue or any other screen. +/// +/// The screen is assumed to reach the frame border, which is what lets the +/// subject be ignored: only a ring around the edge is sampled. +abstract final class ChromaKeyDetector { + /// Fraction of the frame, per side, sampled as the border ring. + static const _ringFraction = 0.12; + + /// A screen has to be at least this saturated. Below it, the border is grey + /// or black and there is nothing to key. + static const _minChromaMagnitude = 0.08; + + /// At least this share of the border must belong to one color. + static const _minCoverage = 0.6; + + /// Multiplier on the measured spread, so the soft edge and a little + /// frame-to-frame noise stay inside the key. + static const _spreadMargin = 2.0; + + /// Never key tighter than this, or compression noise on a flat screen starts + /// poking through. + static const _minSimilarity = 0.08; + + /// Never key wider than this from a measurement alone — beyond it the risk of + /// eating the subject outweighs whatever the border suggested. + static const _maxSimilarity = 0.35; + + /// BT.601 chroma of a gamma-encoded RGB triple in `0..1`, matching the + /// keying formula the renderers use. + static ({double cb, double cr}) chromaOf(double r, double g, double b) => ( + cb: -0.168736 * r - 0.331264 * g + 0.5 * b, + cr: 0.5 * r - 0.418688 * g - 0.081312 * b, + ); + + /// Detects the screen in one or more RGBA frames. + /// + /// [frames] are raw RGBA buffers, all [width] x [height]. Passing several + /// frames pools their border pixels, which averages out sensor noise and a + /// subject that briefly touches the edge. + /// + /// Throws a [ChromaKeyDetectionException] when the border is not one + /// saturated color. + static ChromaKeyDetection fromFrames( + List frames, { + required int width, + required int height, + }) { + if (frames.isEmpty) { + throw const ChromaKeyDetectionException('No frames to analyze'); + } + + final ringR = []; + final ringG = []; + final ringB = []; + + final marginX = max(1, (width * _ringFraction).round()); + final marginY = max(1, (height * _ringFraction).round()); + + for (final frame in frames) { + if (frame.length < width * height * 4) { + throw ChromaKeyDetectionException( + 'Frame buffer is ${frame.length} bytes, expected at least ' + '${width * height * 4} for ${width}x$height RGBA', + ); + } + for (var y = 0; y < height; y++) { + final onHorizontalEdge = y < marginY || y >= height - marginY; + for (var x = 0; x < width; x++) { + if (!onHorizontalEdge && x >= marginX && x < width - marginX) { + // Interior: skip ahead to the right-hand band in one step. + x = width - marginX - 1; + continue; + } + final i = (y * width + x) * 4; + ringR.add(frame[i] / 255); + ringG.add(frame[i + 1] / 255); + ringB.add(frame[i + 2] / 255); + } + } + } + + if (ringR.isEmpty) { + throw const ChromaKeyDetectionException('Frame is too small to sample'); + } + + // The median rejects the subject and any studio clutter that reaches the + // edge, as long as they are the minority there — which an average would + // not, since a few bright pixels drag it a long way. + final r = _median(ringR); + final g = _median(ringG); + final b = _median(ringB); + final key = chromaOf(r, g, b); + final magnitude = sqrt(key.cb * key.cb + key.cr * key.cr); + + if (magnitude < _minChromaMagnitude) { + throw ChromaKeyDetectionException( + 'The frame border averages to a near-neutral color ' + '(chroma magnitude ${magnitude.toStringAsFixed(3)}), so there is no ' + 'screen to key. Is the screen actually reaching the frame edge?', + ); + } + + // How far each border pixel sits from that color. + final distances = List.generate(ringR.length, (i) { + final c = chromaOf(ringR[i], ringG[i], ringB[i]); + final dcb = c.cb - key.cb; + final dcr = c.cr - key.cr; + return sqrt(dcb * dcb + dcr * dcr); + })..sort(); + + final spread = distances[(distances.length * 0.99).floor().clamp( + 0, + distances.length - 1, + )]; + + // Coverage uses a generous band around the median, so a soft edge counts + // as screen while a wall or a sleeve does not. + final band = max(spread, _minSimilarity); + final covered = distances.where((d) => d <= band).length; + final coverage = covered / distances.length; + + if (coverage < _minCoverage) { + throw ChromaKeyDetectionException( + 'Only ${(coverage * 100).toStringAsFixed(0)}% of the frame border is ' + 'one color, so it is not a clean screen. Crop the frame to the screen ' + 'area, or set the key manually.', + ); + } + + final similarity = (spread * _spreadMargin).clamp( + _minSimilarity, + _maxSimilarity, + ); + + return ChromaKeyDetection( + color: Color.fromARGB( + 255, + (r * 255).round().clamp(0, 255), + (g * 255).round().clamp(0, 255), + (b * 255).round().clamp(0, 255), + ), + similarity: similarity, + coverage: coverage, + spread: spread, + ); + } + + static double _median(List values) { + final sorted = List.from(values)..sort(); + final mid = sorted.length ~/ 2; + if (sorted.length.isOdd) return sorted[mid]; + return (sorted[mid - 1] + sorted[mid]) / 2; + } +} diff --git a/lib/pro_video_editor.dart b/lib/pro_video_editor.dart index 045c309c..251f048b 100644 --- a/lib/pro_video_editor.dart +++ b/lib/pro_video_editor.dart @@ -15,6 +15,8 @@ export 'features/audio/models/waveform_style.dart'; export 'core/models/image/editor_layer_image_model.dart'; export 'core/models/image/image_layer_model.dart'; export 'core/models/image/layer_animation_model.dart'; +export 'core/models/video/chroma_key_model.dart'; +export 'core/utils/chroma_key_detector.dart'; export 'core/models/video/color_filter_model.dart'; export 'core/models/video/progress_model.dart'; export 'core/models/video/editor_video_model.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index d0b18d01..9fa7e827 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: pro_video_editor description: "A Flutter video editor: Seamlessly enhance your videos with user-friendly editing features." -version: 2.10.0 +version: 2.11.0 homepage: https://github.com/hm21/pro_video_editor/ repository: https://github.com/hm21/pro_video_editor/ documentation: https://github.com/hm21/pro_video_editor/ diff --git a/test/core/models/video/chroma_key_model_test.dart b/test/core/models/video/chroma_key_model_test.dart new file mode 100644 index 00000000..ff6d0526 --- /dev/null +++ b/test/core/models/video/chroma_key_model_test.dart @@ -0,0 +1,216 @@ +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:pro_video_editor/pro_video_editor.dart'; + +void main() { + group('ChromaKey', () { + const key = ChromaKey( + color: Color(0xFF00FF00), + similarity: 0.2, + smoothness: 0.1, + spill: 0.75, + backgroundColor: Color(0xFFFF0000), + ); + + group('defaults', () { + test('match the documented SMPTE green preset', () { + const defaults = ChromaKey(); + + expect(defaults.color, const Color(0xFF00B140)); + expect(defaults.similarity, 0.20); + expect(defaults.smoothness, 0.08); + expect(defaults.spill, 0.5); + expect(defaults.backgroundColor, isNull); + expect(defaults.backgroundImage, isNull); + }); + + test('are transparent, since no background is set', () { + expect(const ChromaKey().isTransparent, isTrue); + }); + }); + + group('assertions', () { + test('similarity must be greater than 0 and at most 1', () { + expect(() => ChromaKey(similarity: 0), throwsA(isA())); + expect( + () => ChromaKey(similarity: -0.1), + throwsA(isA()), + ); + expect( + () => ChromaKey(similarity: 1.1), + throwsA(isA()), + ); + expect(const ChromaKey(similarity: 1).similarity, 1); + }); + + test('smoothness must be between 0 and 1', () { + expect(() => ChromaKey(smoothness: -1), throwsA(isA())); + expect(() => ChromaKey(smoothness: 2), throwsA(isA())); + expect(const ChromaKey(smoothness: 0).smoothness, 0); + }); + + test('spill must be between 0 and 1', () { + expect(() => ChromaKey(spill: -1), throwsA(isA())); + expect(() => ChromaKey(spill: 2), throwsA(isA())); + expect(const ChromaKey(spill: 0).spill, 0); + }); + + test('rejects two backgrounds at once', () { + expect( + () => ChromaKey( + backgroundColor: const Color(0xFFFF0000), + backgroundImage: EditorLayerImage.asset('assets/bg.png'), + ), + throwsA(isA()), + ); + }); + }); + + group('isTransparent', () { + test('is false once a background is set', () { + expect( + const ChromaKey( + backgroundColor: Color(0xFF123456), + ).isTransparent, + isFalse, + ); + expect( + ChromaKey( + backgroundImage: EditorLayerImage.asset('assets/bg.png'), + ).isTransparent, + isFalse, + ); + }); + }); + + group('toAsyncMap', () { + test('serializes colors as ARGB ints for the platform channel', () async { + final map = await key.toAsyncMap(); + + expect(map['keyColor'], 0xFF00FF00); + expect(map['similarity'], 0.2); + expect(map['smoothness'], 0.1); + expect(map['spill'], 0.75); + expect(map['bgColor'], 0xFFFF0000); + expect(map['bgImageData'], isNull); + }); + + test('sends null for both backgrounds when transparent', () async { + final map = await const ChromaKey().toAsyncMap(); + + expect(map['bgColor'], isNull); + expect(map['bgImageData'], isNull); + }); + + test('resolves the background image to bytes', () async { + final bytes = Uint8List.fromList([1, 2, 3, 4]); + final map = await ChromaKey( + backgroundImage: EditorLayerImage.memory(bytes), + ).toAsyncMap(); + + expect(map['bgImageData'], bytes); + }); + }); + + group('toMap / fromMap', () { + test('serializes all fields correctly', () { + final map = key.toMap(); + + expect(map['color'], 0xFF00FF00); + expect(map['similarity'], 0.2); + expect(map['smoothness'], 0.1); + expect(map['spill'], 0.75); + expect(map['backgroundColor'], 0xFFFF0000); + expect(map['backgroundImage'], isNull); + }); + + test('roundtrip preserves every field', () { + final restored = ChromaKey.fromMap(key.toMap()); + + expect(restored, key); + }); + + test('roundtrip preserves a transparent key', () { + const transparent = ChromaKey(); + final restored = ChromaKey.fromMap(transparent.toMap()); + + expect(restored, transparent); + expect(restored.isTransparent, isTrue); + }); + + test('roundtrip preserves an asset background image', () { + final withImage = ChromaKey( + backgroundImage: EditorLayerImage.asset('assets/bg.png'), + ); + final restored = ChromaKey.fromMap(withImage.toMap()); + + expect(restored.backgroundImage?.assetPath, 'assets/bg.png'); + }); + + test('falls back to the defaults for missing fields', () { + final restored = ChromaKey.fromMap(const {}); + + expect(restored, const ChromaKey()); + }); + + test('parses numeric strings safely', () { + final map = key.toMap() + ..['similarity'] = '0.3' + ..['spill'] = '1'; + + final restored = ChromaKey.fromMap(map); + expect(restored.similarity, 0.3); + expect(restored.spill, 1.0); + }); + + test('json roundtrip preserves every field', () { + expect(ChromaKey.fromJson(key.toJson()), key); + }); + }); + + group('copyWith', () { + test('overrides only what is given', () { + final copy = key.copyWith(similarity: 0.5); + + expect(copy.similarity, 0.5); + expect(copy.color, key.color); + expect(copy.smoothness, key.smoothness); + expect(copy.spill, key.spill); + expect(copy.backgroundColor, key.backgroundColor); + }); + + test('keeps every value when called empty', () { + expect(key.copyWith(), key); + }); + }); + + group('equality', () { + test('two identical keys are equal and share a hashCode', () { + const a = ChromaKey(similarity: 0.3); + const b = ChromaKey(similarity: 0.3); + + expect(a, b); + expect(a.hashCode, b.hashCode); + }); + + test('a differing parameter breaks equality', () { + expect(const ChromaKey(similarity: 0.3), isNot(const ChromaKey())); + expect(const ChromaKey(spill: 0.1), isNot(const ChromaKey())); + expect( + const ChromaKey(color: Color(0xFF0000FF)), + isNot(const ChromaKey()), + ); + }); + }); + + test('toString includes every field', () { + final text = key.toString(); + + expect(text, contains('similarity: 0.2')); + expect(text, contains('smoothness: 0.1')); + expect(text, contains('spill: 0.75')); + }); + }); +} diff --git a/test/core/models/video/video_composition_model_test.dart b/test/core/models/video/video_composition_model_test.dart index 066ca410..c0d4ade0 100644 --- a/test/core/models/video/video_composition_model_test.dart +++ b/test/core/models/video/video_composition_model_test.dart @@ -105,6 +105,42 @@ void main() { throwsAssertionError, ); }); + + group('chromaKey', () { + const key = ChromaKey(similarity: 0.25); + + test('defaults to null so clips inherit the global key', () { + expect(layer.chromaKey, isNull); + }); + + // toAsyncMap resolves each clip's source to a local path, so these use a + // file video — an asset would need the platform channel. + final local = VideoLayer( + clips: [VideoSegment(video: EditorVideo.file('test.mp4'))], + ); + + test('toAsyncMap sends the key over the platform channel', () async { + final map = await local.copyWith(chromaKey: key).toAsyncMap(); + + expect(map['chromaKey'], isA>()); + expect((map['chromaKey'] as Map)['similarity'], 0.25); + }); + + test('toAsyncMap omits the key when unset', () async { + expect((await local.toAsyncMap())['chromaKey'], isNull); + }); + + test('toJson / fromJson roundtrip preserves the key', () { + final withKey = layer.copyWith(chromaKey: key); + + expect(VideoLayer.fromJson(withKey.toJson()), withKey); + expect(VideoLayer.fromJson(layer.toJson()).chromaKey, isNull); + }); + + test('a differing key breaks equality', () { + expect(layer.copyWith(chromaKey: key), isNot(layer)); + }); + }); }); group('VideoComposition', () { diff --git a/test/core/models/video/video_render_data_model_test.dart b/test/core/models/video/video_render_data_model_test.dart index 315d5ad8..8a95ae56 100644 --- a/test/core/models/video/video_render_data_model_test.dart +++ b/test/core/models/video/video_render_data_model_test.dart @@ -1,7 +1,10 @@ import 'dart:ui'; import 'package:flutter_test/flutter_test.dart'; +import 'package:pro_video_editor/core/models/video/chroma_key_model.dart'; import 'package:pro_video_editor/core/models/video/editor_video_model.dart'; +import 'package:pro_video_editor/core/models/video/video_composition_model.dart'; +import 'package:pro_video_editor/core/models/video/video_layer_model.dart'; import 'package:pro_video_editor/core/models/video/video_quality_config.dart'; import 'package:pro_video_editor/core/models/video/video_quality_preset.dart'; import 'package:pro_video_editor/core/models/video/video_render_data_model.dart'; @@ -159,4 +162,87 @@ void main() { ); }); }); + + group('VideoRenderData chromaKey', () { + const key = ChromaKey(backgroundColor: Color(0xFFFF0000)); + + VideoRenderData buildData({ChromaKey? chromaKey}) { + return VideoRenderData( + id: 'test', + videoSegments: [VideoSegment(video: EditorVideo.file('test.mp4'))], + chromaKey: chromaKey, + ); + } + + test('defaults to null', () { + expect(buildData().chromaKey, isNull); + }); + + test('rejects a transparent key on the single-track path', () { + // videoSegments has nothing underneath and the codec carries no alpha, + // so a background-less key would silently flatten to black. + expect( + () => buildData(chromaKey: const ChromaKey()), + throwsA(isA()), + ); + }); + + test('allows a transparent key inside a composition', () { + expect( + VideoRenderData( + id: 'test', + composition: VideoComposition( + layers: [ + VideoLayer( + clips: [VideoSegment(video: EditorVideo.file('test.mp4'))], + ), + ], + ), + chromaKey: const ChromaKey(), + ).chromaKey, + const ChromaKey(), + ); + }); + + test('toAsyncMap sends the key over the platform channel', () async { + // toMap() is the Dart-side JSON form; toAsyncMap() is what actually + // reaches the native RenderConfig, so the wire keys are asserted here. + final map = (await buildData(chromaKey: key).toAsyncMap())['chromaKey']; + + expect(map, isA>()); + expect((map as Map)['keyColor'], 0xFF00B140); + expect(map['bgColor'], 0xFFFF0000); + }); + + test('toAsyncMap omits the key when unset', () async { + expect((await buildData().toAsyncMap())['chromaKey'], isNull); + }); + + test('toMap / fromMap roundtrip preserves the key', () { + final restored = VideoRenderData.fromMap( + buildData(chromaKey: key).toMap(), + ); + + expect(restored.chromaKey, key); + expect(VideoRenderData.fromMap(buildData().toMap()).chromaKey, isNull); + }); + + test('copyWith overrides and otherwise keeps the key', () { + const other = ChromaKey(backgroundColor: Color(0xFF0000FF)); + + expect(buildData(chromaKey: key).copyWith(chromaKey: other).chromaKey, + other); + expect(buildData(chromaKey: key).copyWith().chromaKey, key); + }); + + test('withQualityPreset forwards the key', () { + final data = VideoRenderData.withQualityPreset( + videoSegments: [VideoSegment(video: EditorVideo.file('test.mp4'))], + qualityPreset: VideoQualityPreset.p720, + chromaKey: key, + ); + + expect(data.chromaKey, key); + }); + }); } diff --git a/test/core/models/video/video_segment_model_test.dart b/test/core/models/video/video_segment_model_test.dart index 5954df36..57cf6e2f 100644 --- a/test/core/models/video/video_segment_model_test.dart +++ b/test/core/models/video/video_segment_model_test.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:flutter_test/flutter_test.dart'; import 'package:pro_video_editor/pro_video_editor.dart'; @@ -125,5 +127,52 @@ void main() { test('toString contains class name', () { expect(segment.toString(), contains('VideoSegment')); }); + + group('chromaKey', () { + const key = ChromaKey( + similarity: 0.3, + backgroundColor: Color(0xFF0000FF), + ); + + test('defaults to null so the clip inherits the layer/global key', () { + expect(segment.chromaKey, isNull); + }); + + // toAsyncMap resolves the source to a local path, so these use a file + // video — an asset would need the platform channel. + final local = VideoSegment(video: EditorVideo.file('test.mp4')); + + test('toAsyncMap sends the key over the platform channel', () async { + final map = await local.copyWith(chromaKey: key).toAsyncMap(); + + expect(map['chromaKey'], isA>()); + expect((map['chromaKey'] as Map)['similarity'], 0.3); + }); + + test('toAsyncMap omits the key when unset', () async { + expect((await local.toAsyncMap())['chromaKey'], isNull); + }); + + test('toMap / fromMap roundtrip preserves the key', () { + final withKey = segment.copyWith(chromaKey: key); + + expect(VideoSegment.fromMap(withKey.toMap()).chromaKey, key); + expect(VideoSegment.fromMap(segment.toMap()).chromaKey, isNull); + }); + + test('copyWith overrides and otherwise keeps the key', () { + final withKey = segment.copyWith(chromaKey: key); + + expect(withKey.copyWith().chromaKey, key); + expect( + withKey.copyWith(chromaKey: const ChromaKey()).chromaKey, + const ChromaKey(), + ); + }); + + test('a differing key breaks equality', () { + expect(segment.copyWith(chromaKey: key), isNot(segment)); + }); + }); }); } diff --git a/test/core/utils/chroma_key_detector_test.dart b/test/core/utils/chroma_key_detector_test.dart new file mode 100644 index 00000000..248dad6a --- /dev/null +++ b/test/core/utils/chroma_key_detector_test.dart @@ -0,0 +1,298 @@ +import 'dart:math'; +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:pro_video_editor/pro_video_editor.dart'; + +void main() { + const width = 80; + const height = 60; + + /// Builds an RGBA frame: [screen] everywhere, with a [subject]-colored block + /// covering the middle [subjectFraction] of the frame. + Uint8List frame({ + required Color screen, + Color? subject, + double subjectFraction = 0.5, + double lightingFalloff = 0, + }) { + final buffer = Uint8List(width * height * 4); + final inset = (1 - subjectFraction) / 2; + for (var y = 0; y < height; y++) { + // Vertical brightness gradient, like a screen lit from the top. + final dim = 1 - (y / height) * lightingFalloff; + for (var x = 0; x < width; x++) { + final inSubject = + subject != null && + x >= width * inset && + x < width * (1 - inset) && + y >= height * inset && + y < height * (1 - inset); + final c = inSubject ? subject : screen; + final scale = inSubject ? 1.0 : dim; + final i = (y * width + x) * 4; + buffer[i] = ((c.r * 255) * scale).round().clamp(0, 255); + buffer[i + 1] = ((c.g * 255) * scale).round().clamp(0, 255); + buffer[i + 2] = ((c.b * 255) * scale).round().clamp(0, 255); + buffer[i + 3] = 255; + } + } + return buffer; + } + + double chromaDistance(Color a, Color b) { + final ca = ChromaKeyDetector.chromaOf(a.r, a.g, a.b); + final cb = ChromaKeyDetector.chromaOf(b.r, b.g, b.b); + return sqrt(pow(ca.cb - cb.cb, 2) + pow(ca.cr - cb.cr, 2)); + } + + group('ChromaKeyDetector', () { + const green = Color(0xFF00B140); + const blue = Color(0xFF0047BB); + const skin = Color(0xFFC09070); + + test('finds a flat green screen behind a subject', () { + final result = ChromaKeyDetector.fromFrames( + [frame(screen: green, subject: skin)], + width: width, + height: height, + ); + + expect(chromaDistance(result.color, green), lessThan(0.01)); + expect(result.coverage, greaterThan(0.99)); + expect(result.spread, lessThan(0.01)); + }); + + test('is hue-agnostic — a blue screen works the same', () { + final result = ChromaKeyDetector.fromFrames( + [frame(screen: blue, subject: skin)], + width: width, + height: height, + ); + + expect(chromaDistance(result.color, blue), lessThan(0.01)); + expect(result.coverage, greaterThan(0.99)); + }); + + test('a flat screen still gets a workable soft margin', () { + // A perfectly uniform synthetic screen has zero spread; the floor keeps + // the key from being so tight that codec noise pokes through. + final result = ChromaKeyDetector.fromFrames( + [frame(screen: green, subject: skin)], + width: width, + height: height, + ); + + expect(result.similarity, greaterThanOrEqualTo(0.08)); + }); + + test('the subject never decides the key', () { + // The block covers most of the frame, but not the border ring. + final result = ChromaKeyDetector.fromFrames( + [frame(screen: green, subject: skin, subjectFraction: 0.7)], + width: width, + height: height, + ); + + expect(chromaDistance(result.color, green), lessThan(0.01)); + expect(chromaDistance(result.color, skin), greaterThan(0.3)); + }); + + test('an unevenly lit screen widens similarity, not the color', () { + final even = ChromaKeyDetector.fromFrames( + [frame(screen: green, subject: skin)], + width: width, + height: height, + ); + final uneven = ChromaKeyDetector.fromFrames( + [frame(screen: green, subject: skin, lightingFalloff: 0.5)], + width: width, + height: height, + ); + + expect( + uneven.spread, + greaterThan(even.spread), + reason: 'a falloff should show up as spread', + ); + expect( + uneven.similarity, + greaterThan(even.similarity), + reason: 'and similarity should widen to cover it', + ); + // The hue did not change, only the brightness, so the detected color + // stays on the same chroma direction. + expect(chromaDistance(uneven.color, green), lessThan(0.08)); + }); + + test('similarity covers the measured spread with margin', () { + final result = ChromaKeyDetector.fromFrames( + [frame(screen: green, subject: skin, lightingFalloff: 0.4)], + width: width, + height: height, + ); + + expect(result.similarity, greaterThanOrEqualTo(result.spread)); + }); + + test('similarity is capped so a bad read cannot eat the subject', () { + // A border that is half green and half a very different green-ish tone + // produces a large spread; the cap keeps it from opening up wildly. + final result = ChromaKeyDetector.fromFrames( + [frame(screen: green, subject: skin, lightingFalloff: 0.9)], + width: width, + height: height, + ); + + expect(result.similarity, lessThanOrEqualTo(0.35)); + }); + + test('pools several frames', () { + final result = ChromaKeyDetector.fromFrames( + [ + frame(screen: green, subject: skin), + frame(screen: green, subject: skin), + frame(screen: green, subject: skin), + ], + width: width, + height: height, + ); + + expect(chromaDistance(result.color, green), lessThan(0.01)); + }); + + group('rejects what is not a screen', () { + test('a neutral border', () { + expect( + () => ChromaKeyDetector.fromFrames( + [frame(screen: const Color(0xFF808080))], + width: width, + height: height, + ), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('near-neutral'), + ), + ), + ); + }); + + test('a black border', () { + expect( + () => ChromaKeyDetector.fromFrames( + [frame(screen: const Color(0xFF000000))], + width: width, + height: height, + ), + throwsA(isA()), + ); + }); + + test('no frames at all', () { + expect( + () => ChromaKeyDetector.fromFrames( + [], + width: width, + height: height, + ), + throwsA(isA()), + ); + }); + + test('a buffer that is too small for the stated size', () { + expect( + () => ChromaKeyDetector.fromFrames( + [Uint8List(10)], + width: width, + height: height, + ), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('expected at least'), + ), + ), + ); + }); + }); + + test('chromaOf matches the renderers\' BT.601 formula', () { + // Pinned against the same constants the shader and the color cube use. + final c = ChromaKeyDetector.chromaOf(0, 0xB1 / 255, 0x40 / 255); + expect(c.cb, closeTo(-0.1044, 1e-4)); + expect(c.cr, closeTo(-0.3110, 1e-4)); + }); + + test('a neutral color sits at the chroma origin', () { + final c = ChromaKeyDetector.chromaOf(0.5, 0.5, 0.5); + expect(c.cb, closeTo(0, 1e-9)); + expect(c.cr, closeTo(0, 1e-9)); + }); + }); + + group('ChromaKey presets', () { + test('greenScreen matches the default constructor', () { + const preset = ChromaKey.greenScreen(); + const plain = ChromaKey(); + + expect(preset.color, plain.color); + expect(preset.similarity, plain.similarity); + expect(preset.spill, plain.spill); + }); + + test('blueScreen keys tighter and despills more gently', () { + const green = ChromaKey.greenScreen(); + const blue = ChromaKey.blueScreen(); + + expect(blue.color, const Color(0xFF0047BB)); + expect( + blue.similarity, + lessThan(green.similarity), + reason: 'denim sits at 0.19 from blue, inside the green default', + ); + expect(blue.spill, lessThan(green.spill)); + }); + + test('the blue preset does not key denim', () { + const blue = ChromaKey.blueScreen(); + const denim = Color(0xFF3B5B8C); + + final d = chromaDistance(blue.color, denim); + expect( + d, + greaterThan(blue.similarity), + reason: 'denim ($d) must sit outside similarity (${blue.similarity})', + ); + }); + + test('the green default would key denim on a blue screen', () { + // The reason blueScreen exists as its own preset. + const denim = Color(0xFF3B5B8C); + final d = chromaDistance(const Color(0xFF0047BB), denim); + + expect(d, lessThan(const ChromaKey.greenScreen().similarity)); + }); + + test('presets carry a background through', () { + const key = ChromaKey.blueScreen(backgroundColor: Color(0xFFFF0000)); + + expect(key.isTransparent, isFalse); + expect(key.backgroundColor, const Color(0xFFFF0000)); + }); + + test('presets keep the shared assertions', () { + expect( + () => ChromaKey.blueScreen(similarity: 0), + throwsA(isA()), + ); + expect( + () => ChromaKey.greenScreen(spill: 2), + throwsA(isA()), + ); + }); + }); +} diff --git a/test/pro_video_editor_method_channel_test.mocks.dart b/test/pro_video_editor_method_channel_test.mocks.dart index 9a3ce0be..b51dc851 100644 --- a/test/pro_video_editor_method_channel_test.mocks.dart +++ b/test/pro_video_editor_method_channel_test.mocks.dart @@ -418,6 +418,7 @@ class MockVideoRenderData extends _i1.Mock implements _i4.VideoRenderData { List<_i4.ColorFilter>? colorFilters, List<_i4.VideoAudioTrack>? audioTracks, double? blur, + _i4.ChromaKey? chromaKey, int? bitrate, int? maxFrameRate, bool? shouldOptimizeForNetworkUse, @@ -439,6 +440,7 @@ class MockVideoRenderData extends _i1.Mock implements _i4.VideoRenderData { #colorFilters: colorFilters, #audioTracks: audioTracks, #blur: blur, + #chromaKey: chromaKey, #bitrate: bitrate, #maxFrameRate: maxFrameRate, #shouldOptimizeForNetworkUse: shouldOptimizeForNetworkUse, @@ -461,6 +463,7 @@ class MockVideoRenderData extends _i1.Mock implements _i4.VideoRenderData { #colorFilters: colorFilters, #audioTracks: audioTracks, #blur: blur, + #chromaKey: chromaKey, #bitrate: bitrate, #maxFrameRate: maxFrameRate, #shouldOptimizeForNetworkUse: shouldOptimizeForNetworkUse, From a9be8d0d3f3a162d597c6439f66b06dff10760d2 Mon Sep 17 00:00:00 2001 From: hm21 Date: Tue, 28 Jul 2026 17:51:48 +0200 Subject: [PATCH 2/3] chore: dart format --- example/integration_test/chroma_key_test.dart | 14 +++----------- lib/core/models/video/chroma_key_model.dart | 14 ++------------ lib/core/utils/chroma_key_detector.dart | 9 +++++---- test/core/models/video/chroma_key_model_test.dart | 4 +--- .../models/video/video_render_data_model_test.dart | 6 ++++-- test/core/utils/chroma_key_detector_test.dart | 6 +----- 6 files changed, 16 insertions(+), 37 deletions(-) diff --git a/example/integration_test/chroma_key_test.dart b/example/integration_test/chroma_key_test.dart index f8145e6f..ab76aa6a 100644 --- a/example/integration_test/chroma_key_test.dart +++ b/example/integration_test/chroma_key_test.dart @@ -357,9 +357,7 @@ void main() { VideoSegment( video: secondSource, // Overrides the global red with a distinctly different fill. - chromaKey: const ChromaKey( - backgroundColor: Color(0xFFFFFFFF), - ), + chromaKey: const ChromaKey(backgroundColor: Color(0xFFFFFFFF)), ), ], chromaKey: const ChromaKey(backgroundColor: backgroundRed), @@ -471,19 +469,13 @@ void main() { // Center of the subject: pink outfit, must not be keyed. final c = await samplePixel(out, 0.66, 0.45); - expect( - isRed(c), - isFalse, - reason: 'The subject was keyed away, got $c', - ); + expect(isRed(c), isFalse, reason: 'The subject was keyed away, got $c'); }); testWidgets('rejects a frame that is not a screen', (_) async { // The plain demo video has no screen at all. expect( - () => ChromaKey.detect( - EditorVideo.asset(kVideoEditorExampleH264Path), - ), + () => ChromaKey.detect(EditorVideo.asset(kVideoEditorExampleH264Path)), throwsA(isA()), ); }); diff --git a/lib/core/models/video/chroma_key_model.dart b/lib/core/models/video/chroma_key_model.dart index 5f53d0d2..92e8699a 100644 --- a/lib/core/models/video/chroma_key_model.dart +++ b/lib/core/models/video/chroma_key_model.dart @@ -214,13 +214,7 @@ class ChromaKey { // Sample across the clip so a subject that briefly touches an edge cannot // decide the result on its own. final duration = metadata.duration; - final at = - timestamps ?? - [ - duration * 0.25, - duration * 0.5, - duration * 0.75, - ]; + final at = timestamps ?? [duration * 0.25, duration * 0.5, duration * 0.75]; // Small, and at the source aspect ratio: `cover` then neither crops nor // pads, so the sampled ring really is the frame's own border. @@ -262,11 +256,7 @@ class ChromaKey { throw const ChromaKeyDetectionException('Could not decode any frame'); } - return ChromaKeyDetector.fromFrames( - buffers, - width: width, - height: height, - ); + return ChromaKeyDetector.fromFrames(buffers, width: width, height: height); } /// The screen color to remove. diff --git a/lib/core/utils/chroma_key_detector.dart b/lib/core/utils/chroma_key_detector.dart index f4f33688..2bd1a317 100644 --- a/lib/core/utils/chroma_key_detector.dart +++ b/lib/core/utils/chroma_key_detector.dart @@ -175,10 +175,11 @@ abstract final class ChromaKeyDetector { return sqrt(dcb * dcb + dcr * dcr); })..sort(); - final spread = distances[(distances.length * 0.99).floor().clamp( - 0, - distances.length - 1, - )]; + final spread = + distances[(distances.length * 0.99).floor().clamp( + 0, + distances.length - 1, + )]; // Coverage uses a generous band around the median, so a soft edge counts // as screen while a wall or a sleeve does not. diff --git a/test/core/models/video/chroma_key_model_test.dart b/test/core/models/video/chroma_key_model_test.dart index ff6d0526..7b6e8022 100644 --- a/test/core/models/video/chroma_key_model_test.dart +++ b/test/core/models/video/chroma_key_model_test.dart @@ -71,9 +71,7 @@ void main() { group('isTransparent', () { test('is false once a background is set', () { expect( - const ChromaKey( - backgroundColor: Color(0xFF123456), - ).isTransparent, + const ChromaKey(backgroundColor: Color(0xFF123456)).isTransparent, isFalse, ); expect( diff --git a/test/core/models/video/video_render_data_model_test.dart b/test/core/models/video/video_render_data_model_test.dart index 8a95ae56..77a58b8a 100644 --- a/test/core/models/video/video_render_data_model_test.dart +++ b/test/core/models/video/video_render_data_model_test.dart @@ -230,8 +230,10 @@ void main() { test('copyWith overrides and otherwise keeps the key', () { const other = ChromaKey(backgroundColor: Color(0xFF0000FF)); - expect(buildData(chromaKey: key).copyWith(chromaKey: other).chromaKey, - other); + expect( + buildData(chromaKey: key).copyWith(chromaKey: other).chromaKey, + other, + ); expect(buildData(chromaKey: key).copyWith().chromaKey, key); }); diff --git a/test/core/utils/chroma_key_detector_test.dart b/test/core/utils/chroma_key_detector_test.dart index 248dad6a..bcdcb6fb 100644 --- a/test/core/utils/chroma_key_detector_test.dart +++ b/test/core/utils/chroma_key_detector_test.dart @@ -193,11 +193,7 @@ void main() { test('no frames at all', () { expect( - () => ChromaKeyDetector.fromFrames( - [], - width: width, - height: height, - ), + () => ChromaKeyDetector.fromFrames([], width: width, height: height), throwsA(isA()), ); }); From 04eebd4ca52396897017179678762d75781a2c1f Mon Sep 17 00:00:00 2001 From: hm21 Date: Tue, 28 Jul 2026 19:29:19 +0200 Subject: [PATCH 3/3] fix: chroma key review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Detector: the coverage guard was unreachable. `coverage` was measured against a band derived from the distances' own 99th percentile, so it came out ~0.99 for every possible frame and `detect` never rejected a border that was not a clean screen. Use a constant band, and measure `spread` over the covered pixels only so a subject at the border cannot widen the key. - Darwin: the chroma-key window end-clamp keyed every frame after the last keyed clip. Windows are emitted per keyed clip, so the array is sparse; bound the clamp to one frame. - Android: the HDR pre-transcode only rewrote `videoClips`, so an HDR clip on a composition layer reached the ES 2.0 shader and hard-failed the export. Retarget the composition layers' clips too. - Both: a transition blend with mismatched keys was logged as "emitted unkeyed" but `null` means inherit, so the global key was applied to it anyway. Add `suppressChromaKey` and compare the resolved keys, which also drops the spurious warning when both sides resolve to the same key. - Android: the background image was sampled with the frame's texture coordinate, where t=0 is the bottom, while GLUtils.texImage2D uploads row 0 at t=0 — so it rendered upside down. Flip the background lookup. - Both: `backgroundColor` alpha was honored on Darwin and dropped on Android. Ignore it on both and assert the color is opaque. - Both: a background image that will not decode fell back to transparent on Android and black on Darwin. Fill with opaque black on both. - Android: downscale a background bitmap to GL_MAX_TEXTURE_SIZE instead of throwing, recycle the placeholder, and release the program even when deleting the texture throws. - Darwin: identify a cached background by content, not byte length, and run the key before the layer transform so it sees unresampled pixels. - `ChromaKey.copyWith` could not switch between the two background types and could not clear them; add `removeBackground`. - `ChromaKey.detect` never disposed its codecs or images. - The H.264-alpha assert only inspected the global key, not per-segment. - Correct the docs claiming the key color's brightness is ignored. --- CHANGELOG.md | 8 +- README.md | 5 +- .../src/features/render/RenderVideo.kt | 60 +++++++-- .../render/helpers/ChromaKeyEffect.kt | 124 ++++++++++++++++-- .../helpers/LayeredCompositionBuilder.kt | 9 +- .../render/helpers/VideoSequenceBuilder.kt | 2 +- .../features/render/models/RenderConfig.kt | 11 +- .../shared/features/render/RenderVideo.swift | 44 +++++-- .../render/helpers/CompositionBuilder.swift | 1 + .../features/render/models/RenderConfig.swift | 24 +++- .../features/render/models/VideoClip.swift | 11 +- .../render/utils/VideoCompositor.swift | 50 +++++-- example/integration_test/chroma_key_test.dart | 47 +++++++ .../chroma_key/chroma_key_example_page.dart | 14 +- example/macos/RunnerTests/RunnerTests.swift | 27 ++++ lib/core/models/video/chroma_key_model.dart | 82 ++++++++++-- .../models/video/video_render_data_model.dart | 22 +++- lib/core/utils/chroma_key_detector.dart | 33 +++-- .../models/video/chroma_key_model_test.dart | 66 ++++++++++ .../video/video_render_data_model_test.dart | 55 ++++++++ test/core/utils/chroma_key_detector_test.dart | 67 ++++++++++ 21 files changed, 686 insertions(+), 76 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e50731ad..f2962d28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ ## 2.11.0 -- **FEAT**(android, iOS, macOS): Add `ChromaKey` — green-screen removal with a soft edge and spill suppression. The keyed area becomes a `backgroundColor`, a `backgroundImage`, or (in a `VideoComposition`) the layer below, so a video can sit behind the screen. Settable on `VideoRenderData`, `VideoLayer` and `VideoSegment`, resolved per clip as segment → layer → global. Note that H.264/HEVC carry no alpha: on the single-track `videoSegments` path a key without a background is flattened to black. -- **FEAT**: `ChromaKey.autoDetect(video)` measures the key color and `similarity` off the footage, for any saturated screen hue. A constant key sits beside the *recorded* screen and has to spend on `similarity` the margin that protects the subject. `ChromaKey.detect` returns the raw measurement. Costs one thumbnail decode, no render. -- **FEAT**: `ChromaKey.greenScreen()` and `ChromaKey.blueScreen()` presets. Blue separates skin better but crowds denim, blue eyes and light blue shirts, so it keys tighter and despills more gently. -- **FIX**(android): A `VideoComposition` layer no longer squares its own alpha. `VideoCompositionTransformation` blended against an already-transparent framebuffer, which thinned any partially transparent edge. +- **FEAT**(android, iOS, macOS): Add `ChromaKey` — green-screen removal with a soft edge and spill suppression. The keyed area becomes a `backgroundColor`, a `backgroundImage`, or (in a `VideoComposition`) the layer below. Settable on `VideoRenderData`, `VideoLayer` and `VideoSegment`, resolved per clip as segment → layer → global. H.264/HEVC carry no alpha, so on the single-track `videoSegments` path a key without a background is flattened to black. +- **FEAT**: `ChromaKey.autoDetect(video)` measures the key color and `similarity` off the footage, for any saturated screen hue. `ChromaKey.detect` returns the raw measurement. Costs one thumbnail decode, no render. +- **FEAT**: `ChromaKey.greenScreen()` and `ChromaKey.blueScreen()` presets. Blue separates skin better but crowds denim and blue eyes, so it keys tighter and despills more gently. +- **FIX**(android): A `VideoComposition` layer no longer squares its own alpha. `VideoCompositionTransformation` blended against an already-transparent framebuffer, thinning any partially transparent edge. - **FIX**(android, iOS, macOS): Two sources resolved in the same millisecond no longer collide on one temp file in `EditorVideo.safeFilePath`. ## 2.10.0 diff --git a/README.md b/README.md index 7ad8fa74..60d0f75c 100644 --- a/README.md +++ b/README.md @@ -623,7 +623,8 @@ var task = VideoRenderData( ColorFilter(matrix: [ 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0 ]), ], chromaKey: const ChromaKey( - // Defaults to SMPTE "chroma key green"; only the hue is used. + // Defaults to SMPTE "chroma key green". Brightness matters as well as + // hue, so measure it with `ChromaKey.autoDetect` when you can. color: Color(0xFF00B140), similarity: 0.20, // raise it if the screen survives the key smoothness: 0.08, // width of the soft edge @@ -660,7 +661,7 @@ const blueKey = ChromaKey.blueScreen(backgroundColor: Color(0xFF000000)); /// single-track `videoSegments` path there is nothing underneath and the codec /// carries no alpha, so a key there needs a `backgroundColor` or /// `backgroundImage`. -final task = VideoRenderData( +final keyedTask = VideoRenderData( composition: VideoComposition( layers: [ VideoLayer(clips: [VideoSegment(video: backgroundVideo)]), diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/RenderVideo.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/RenderVideo.kt index 9d5dd987..2389a628 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/RenderVideo.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/RenderVideo.kt @@ -222,7 +222,16 @@ class RenderVideo(private val context: Context) { // 1) Pre-transcode HEVC 10-bit clips (if needed). if (needsPreTranscode) { Log.d(RENDER_TAG, "Pre-transcoding HEVC 10-bit videos...") - val inputPaths = workingConfig.videoClips.map { it.inputPath } + // Both paths, not just the single-track one: a GPU + // effect on a composition layer needs its clips + // transcoded too. ChromaKeyEffect is an ES 2.0 SDR + // shader and hard-fails on an HDR input, so leaving the + // layered clips untouched here aborted the export. + val inputPaths = ( + workingConfig.videoClips.map { it.inputPath } + + (workingConfig.composition?.layers ?: emptyList()) + .flatMap { layer -> layer.clips.map { it.inputPath } } + ).distinct() val transcodeMap = VideoTranscoder.transcodeClipsIfNeeded( context, inputPaths ) @@ -234,13 +243,22 @@ class RenderVideo(private val context: Context) { "Pre-transcoded ${transcodedFiles.size} HEVC 10-bit videos to H.264" ) } - val updatedClips = workingConfig.videoClips.map { clip -> + fun retarget(clip: VideoClip): VideoClip { val newPath = transcodeMap[clip.inputPath] ?: clip.inputPath - if (newPath != clip.inputPath) { + return if (newPath != clip.inputPath) { clip.copy(inputPath = newPath) } else clip } - workingConfig = workingConfig.copy(videoClips = updatedClips) + workingConfig = workingConfig.copy( + videoClips = workingConfig.videoClips.map(::retarget), + composition = workingConfig.composition?.let { composition -> + composition.copy( + layers = composition.layers.map { layer -> + layer.copy(clips = layer.clips.map(::retarget)) + } + ) + } + ) } // 2) Pre-render reversed clips into single forward MP4 temp files. @@ -273,6 +291,7 @@ class RenderVideo(private val context: Context) { val transitionedClips = preRenderTransitions( workingConfig.videoClips, enableAudio = workingConfig.enableAudio, + globalChromaKey = workingConfig.chromaKey, shouldStop = shouldStopPolling, collectPath = { paths.add(it) }, onProgress = { f -> transitionProgress(f.toDouble()) }, @@ -410,6 +429,7 @@ class RenderVideo(private val context: Context) { private fun preRenderTransitions( clips: List, enableAudio: Boolean, + globalChromaKey: ChromaKeyConfig?, shouldStop: AtomicBoolean, collectPath: (String) -> Unit, onProgress: (Float) -> Unit, @@ -508,12 +528,14 @@ class RenderVideo(private val context: Context) { // Keep the outgoing clip's speed; it now ends `tailSrc` of source // earlier (those frames moved into the speed-adjusted blend). addClip(current.copy(endUs = curEnd - tailSrc, transition = null)) + val blendKey = blendChromaKey(current, next, globalChromaKey, "$i") result.add( VideoClip( inputPath = rendered.outputPath, startUs = 0L, endUs = rendered.durationUs.takeIf { it > 0 }, - chromaKey = blendChromaKey(current, next, "$i"), + chromaKey = blendKey.config, + suppressChromaKey = blendKey.suppressed, ) ) // Trim the incoming head in place; it keeps its own speed/transition. @@ -599,12 +621,15 @@ class RenderVideo(private val context: Context) { result[0] = first.copy(startUs = firstStart + headSrc) result[lastIdx] = last.copy(endUs = lastEnd - tailSrc) } + val wrapKey = + blendChromaKey(last, first, globalChromaKey, "loop wrap") result.add( VideoClip( inputPath = rendered.outputPath, startUs = 0L, endUs = rendered.durationUs.takeIf { it > 0 }, - chromaKey = blendChromaKey(last, first, "loop wrap"), + chromaKey = wrapKey.config, + suppressChromaKey = wrapKey.suppressed, ) ) } else { @@ -631,17 +656,34 @@ class RenderVideo(private val context: Context) { private fun blendChromaKey( outgoing: VideoClip, incoming: VideoClip, + global: ChromaKeyConfig?, boundary: String, - ): ChromaKeyConfig? { - if (outgoing.chromaKey == incoming.chromaKey) return outgoing.chromaKey + ): BlendChromaKey { + // Compare the *effective* keys. A clip that leaves its own key null + // still inherits the global one, so comparing the raw per-clip fields + // reported a mismatch for two clips that in fact key identically. + val outgoingKey = outgoing.chromaKey ?: global + val incomingKey = incoming.chromaKey ?: global + if (outgoingKey == incomingKey) return BlendChromaKey(outgoingKey, false) + Log.w( RENDER_TAG, "Chroma key: the two clips at boundary $boundary use different keys; " + "the pre-rendered transition blend is emitted unkeyed" ) - return null + // A null key alone would mean "inherit", and `VideoSequenceBuilder` + // would then fall back to the global key — applying to the blend the + // very thing this warning promises it will not. The suppress flag is + // what makes "unkeyed" actually mean unkeyed. + return BlendChromaKey(null, true) } + /** The key for a pre-rendered blend, and whether it opts out of the global one. */ + private data class BlendChromaKey( + val config: ChromaKeyConfig?, + val suppressed: Boolean, + ) + /** * Internal render implementation after optional pre-transcoding. */ diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ChromaKeyEffect.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ChromaKeyEffect.kt index 745120f6..3c24e3fb 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ChromaKeyEffect.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ChromaKeyEffect.kt @@ -1,5 +1,6 @@ package ch.waio.pro_video_editor.src.features.render.helpers +import RENDER_TAG import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory @@ -13,6 +14,7 @@ import androidx.media3.effect.BaseGlShaderProgram import androidx.media3.effect.GlEffect import androidx.media3.effect.GlShaderProgram import ch.waio.pro_video_editor.src.features.render.models.ChromaKeyConfig +import ch.waio.pro_video_editor.src.shared.logging.PluginLog as Log /** * Removes a solid-colored background ("green screen") from every frame. @@ -45,8 +47,10 @@ class ChromaKeyEffect(private val config: ChromaKeyConfig) : GlEffect { if (useHdr) { // The shader is an ES 2.0 SDR program. HEVC 10-bit/HDR sources are // pre-transcoded to 8-bit SDR by VideoTranscoder before they reach - // here (see RenderVideo.hasGpuEffects), so this is a guard against - // that gate regressing rather than an expected path. + // here — `RenderVideo.hasGpuEffects` detects a key at every level it + // can be set, and the transcode step rewrites both `videoClips` and + // every composition layer's clips. So this is a guard against that + // gate regressing rather than an expected path. throw VideoFrameProcessingException( "Chroma key does not support HDR input" ) @@ -62,10 +66,21 @@ class ChromaKeyEffect(private val config: ChromaKeyConfig) : GlEffect { private val glProgram: GlProgram - /** Decoded once at construction, never per frame. */ - private val backgroundBitmap: Bitmap? = config.backgroundImageData?.let { bytes -> - BitmapFactory.decodeByteArray(bytes, 0, bytes.size) - } + /** + * Bounds-only probe of the background image. + * + * Decoding just the header tells us whether the bytes are usable — and + * how large they are — without holding a full-size bitmap from + * construction until the first draw. The real decode happens in + * [drawFrame], where there is a GL context to size it against. + */ + private val backgroundBounds: BitmapFactory.Options? = + config.backgroundImageData?.let { bytes -> + BitmapFactory.Options().apply { + inJustDecodeBounds = true + BitmapFactory.decodeByteArray(bytes, 0, bytes.size, this) + }.takeIf { it.outWidth > 0 && it.outHeight > 0 } + } /** * Background texture id. A 1x1 placeholder is uploaded when there is no @@ -75,8 +90,15 @@ class ChromaKeyEffect(private val config: ChromaKeyConfig) : GlEffect { private var backgroundTexId: Int = -1 private val bgMode: Int = when { - backgroundBitmap != null -> BG_IMAGE + backgroundBounds != null -> BG_IMAGE config.backgroundColor != null -> BG_COLOR + // An image was asked for but its bytes will not decode. Falling + // through to BG_TRANSPARENT would quietly un-key the clip on the + // single-track path, where `applyChromaKey` has already ruled that + // the area must be filled — the export would come out looking like + // the key never ran. Fill with opaque black instead, which is what + // Apple produces in the same situation. + config.backgroundImageData != null -> BG_COLOR else -> BG_TRANSPARENT } @@ -139,7 +161,15 @@ class ChromaKeyEffect(private val config: ChromaKeyConfig) : GlEffect { // pixel (a gap frame carrying AlphaScale(0)) stays transparent. " float outA = src.a * a;\n" + " if (uBgMode == 2) {\n" + - " vec3 bg = texture2D(uBgSampler, vTexSamplingCoord).rgb;\n" + + // The background comes from a Bitmap, and GLUtils.texImage2D + // uploads its first row at t=0, while a frame texture puts t=0 + // at the *bottom* of the frame. Sampling the bitmap with the + // frame's own coordinate would show it upside down, so flip t + // here — the same correction Media3's BitmapOverlay applies via + // its flipVerticallyMatrix. + " vec2 bgCoord = vec2(vTexSamplingCoord.x," + + " 1.0 - vTexSamplingCoord.y);\n" + + " vec3 bg = texture2D(uBgSampler, bgCoord).rgb;\n" + " gl_FragColor = vec4(mix(bg, rgb, outA), src.a);\n" + " } else if (uBgMode == 1) {\n" + " gl_FragColor = vec4(mix(uBgColor, rgb, outA), src.a);\n" + @@ -150,6 +180,13 @@ class ChromaKeyEffect(private val config: ChromaKeyConfig) : GlEffect { } init { + if (backgroundBounds == null && config.backgroundImageData != null) { + Log.w( + RENDER_TAG, + "Chroma key: the background image could not be decoded; " + + "filling the keyed area with opaque black instead" + ) + } try { glProgram = GlProgram(VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE) } catch (e: Exception) { @@ -167,9 +204,13 @@ class ChromaKeyEffect(private val config: ChromaKeyConfig) : GlEffect { glProgram.use() if (backgroundTexId == -1) { - backgroundTexId = GlUtil.createTexture( - backgroundBitmap ?: onePixelPlaceholder() - ) + // createTexture uploads the pixels, so nothing needs the + // bitmap afterwards — including the placeholder, which used + // to be allocated and then dropped on the floor. + val bitmap = decodeBackgroundWithinTextureLimit() + ?: onePixelPlaceholder() + backgroundTexId = GlUtil.createTexture(bitmap) + bitmap.recycle() } glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, 0) @@ -220,11 +261,68 @@ class ChromaKeyEffect(private val config: ChromaKeyConfig) : GlEffect { GlUtil.deleteTexture(backgroundTexId) backgroundTexId = -1 } - glProgram.delete() } catch (e: Exception) { throw VideoFrameProcessingException(e) + } finally { + // Deleting the texture can throw, and the program still has to + // go — otherwise a failure on the way out leaks it. + try { + glProgram.delete() + } catch (e: Exception) { + throw VideoFrameProcessingException(e) + } } - backgroundBitmap?.recycle() + } + + /** + * Decodes the background image, downscaled to fit `GL_MAX_TEXTURE_SIZE`. + * + * A camera photo easily exceeds the 4096 (or 2048) px limit a mid-range + * GPU reports, and `GlUtil.createTexture` throws above it — which inside + * [drawFrame] kills the whole export on its first frame. The shader + * stretches the texture to the frame anyway, so the extra resolution was + * never going to survive; dropping it here costs nothing. + * + * Must be called on the GL thread. + */ + private fun decodeBackgroundWithinTextureLimit(): Bitmap? { + val bytes = config.backgroundImageData ?: return null + val bounds = backgroundBounds ?: return null + + val limit = maxTextureSize() + var sampleSize = 1 + while (bounds.outWidth / sampleSize > limit || + bounds.outHeight / sampleSize > limit + ) { + sampleSize *= 2 + } + + val decoded = BitmapFactory.decodeByteArray( + bytes, 0, bytes.size, + BitmapFactory.Options().apply { inSampleSize = sampleSize } + ) ?: return null + + // inSampleSize only halves, so one more exact pass may be needed. + if (decoded.width <= limit && decoded.height <= limit) return decoded + val scale = minOf( + limit.toFloat() / decoded.width, + limit.toFloat() / decoded.height + ) + val scaled = Bitmap.createScaledBitmap( + decoded, + (decoded.width * scale).toInt().coerceAtLeast(1), + (decoded.height * scale).toInt().coerceAtLeast(1), + /* filter= */ true + ) + if (scaled !== decoded) decoded.recycle() + return scaled + } + + /** The GPU's maximum texture dimension, or a conservative fallback. */ + private fun maxTextureSize(): Int { + val value = IntArray(1) + GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_SIZE, value, 0) + return if (value[0] > 0) value[0] else 2048 } /** The background color as linear-free RGB in 0..1, or black. */ diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/LayeredCompositionBuilder.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/LayeredCompositionBuilder.kt index 3217cab9..368f2963 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/LayeredCompositionBuilder.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/LayeredCompositionBuilder.kt @@ -328,7 +328,14 @@ class LayeredCompositionBuilder( // layered path. Media3's compositor blends the sequences with // glBlendFuncSeparate, so a transparent key really does let the layer // below show through. - applyChromaKey(effects, clip.chromaKey ?: layerChromaKey ?: globalChromaKey) + applyChromaKey( + effects, + if (clip.suppressChromaKey) { + null + } else { + clip.chromaKey ?: layerChromaKey ?: globalChromaKey + } + ) val draw = placement.draw val clipBox = placement.clip diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/VideoSequenceBuilder.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/VideoSequenceBuilder.kt index d24da5fe..a2dd8a6c 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/VideoSequenceBuilder.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/VideoSequenceBuilder.kt @@ -618,7 +618,7 @@ class VideoSequenceBuilder( // with opaque black instead (see applyChromaKey for why). applyChromaKey( clipVideoEffects, - clip.chromaKey ?: globalChromaKey, + if (clip.suppressChromaKey) null else clip.chromaKey ?: globalChromaKey, flattenTransparency = true, ) diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/models/RenderConfig.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/models/RenderConfig.kt index aacc7487..97cf3e35 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/models/RenderConfig.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/models/RenderConfig.kt @@ -66,7 +66,16 @@ data class VideoClip( * Removes a solid-colored background from this clip. Overrides the layer's * and the global key; null falls back to those. */ - val chromaKey: ChromaKeyConfig? = null + val chromaKey: ChromaKeyConfig? = null, + /** + * Opts this clip out of the layer/global key entirely. + * + * Internal, never parsed from the platform channel. `chromaKey = null` + * means "inherit", so it cannot express "deliberately unkeyed" — which is + * exactly what a pre-rendered overlap blend needs when the two clips it was + * composed from carry different keys. See `RenderVideo.blendChromaKey`. + */ + val suppressChromaKey: Boolean = false ) { companion object { /** Parses a clip from a platform-channel map. */ diff --git a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/RenderVideo.swift b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/RenderVideo.swift index 8f36bbba..5671a9ae 100644 --- a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/RenderVideo.swift +++ b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/RenderVideo.swift @@ -166,7 +166,8 @@ class RenderVideo { let (newClips, urls) = await preRenderTransitions( clips: workingConfig.videoClips, enableAudio: workingConfig.enableAudio, - outputFormat: workingConfig.outputFormat) + outputFormat: workingConfig.outputFormat, + globalChromaKey: workingConfig.chromaKey) workingConfig = workingConfig.copyWith(videoClips: newClips) transitionURLs = urls } @@ -586,7 +587,8 @@ class RenderVideo { /// (e.g. a neighbour is reversed or has too little content) — those cases are /// handled live by the main pipeline. private static func preRenderTransitions( - clips: [VideoClip], enableAudio: Bool, outputFormat: String + clips: [VideoClip], enableAudio: Bool, outputFormat: String, + globalChromaKey: ChromaKeyConfig? ) async -> ([VideoClip], [URL]) { // An overlap transition on the last/only clip loops back into the first clip // (seamless loop). Captured before the between-clip pass clears it. @@ -684,10 +686,12 @@ class RenderVideo { inputPath: current.inputPath, startUs: curStart, endUs: curEnd - tailSrc, volume: current.volume, playbackSpeed: current.playbackSpeed, reverseVideo: false, transition: nil, chromaKey: current.chromaKey)) + let blend = blendChromaKey( + current, next!, global: globalChromaKey, boundary: "\(i)") result.append( VideoClip( inputPath: rendered.outputURL.path, startUs: 0, endUs: rendered.durationUs, - chromaKey: blendChromaKey(current, next!, boundary: "\(i)"))) + chromaKey: blend.config, suppressChromaKey: blend.suppressed)) // Trim the incoming head in place; it keeps its own speed/transition. work[i + 1] = VideoClip( inputPath: next!.inputPath, startUs: nextStart + headSrc, endUs: nextEnd, @@ -779,10 +783,12 @@ class RenderVideo { volume: last.volume, playbackSpeed: last.playbackSpeed, reverseVideo: false, transition: nil, chromaKey: last.chromaKey) } + let wrapBlend = blendChromaKey( + last, first, global: globalChromaKey, boundary: "loop wrap") result.append( VideoClip( inputPath: rendered.outputURL.path, startUs: 0, endUs: rendered.durationUs, - chromaKey: blendChromaKey(last, first, boundary: "loop wrap"))) + chromaKey: wrapBlend.config, suppressChromaKey: wrapBlend.suppressed)) } else { PluginLog.print("⚠️ Loop wrap render failed; seamless loop skipped") } @@ -813,13 +819,35 @@ class RenderVideo { /// single key can undo. Mismatched sides are therefore left unkeyed and /// reported, rather than silently keyed with one side's settings. private static func blendChromaKey( - _ outgoing: VideoClip, _ incoming: VideoClip, boundary: String - ) -> ChromaKeyConfig? { - if outgoing.chromaKey == incoming.chromaKey { return outgoing.chromaKey } + _ outgoing: VideoClip, _ incoming: VideoClip, global: ChromaKeyConfig?, + boundary: String + ) -> BlendChromaKey { + // Compare the *effective* keys. A clip that leaves its own key nil still + // inherits the global one, so comparing the raw per-clip fields reported a + // mismatch for two clips that in fact key identically. + let outgoingKey = outgoing.chromaKey ?? global + let incomingKey = incoming.chromaKey ?? global + if outgoingKey == incomingKey { return BlendChromaKey(outgoingKey, false) } + PluginLog.print( "⚠️ Chroma key: the two clips at boundary \(boundary) use different keys; " + "the pre-rendered transition blend is emitted unkeyed") - return nil + // A nil key alone would mean "inherit", and `computeChromaKeyWindows` would + // then fall back to the global key — applying to the blend the very thing + // this warning promises it will not. The suppress flag is what makes + // "unkeyed" actually mean unkeyed. + return BlendChromaKey(nil, true) + } + + /// The key for a pre-rendered blend, and whether it opts out of the global one. + private struct BlendChromaKey { + let config: ChromaKeyConfig? + let suppressed: Bool + + init(_ config: ChromaKeyConfig?, _ suppressed: Bool) { + self.config = config + self.suppressed = suppressed + } } /// Loads a clip's total duration in microseconds. diff --git a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/CompositionBuilder.swift b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/CompositionBuilder.swift index 72df81ab..84e71b88 100644 --- a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/CompositionBuilder.swift +++ b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/helpers/CompositionBuilder.swift @@ -233,6 +233,7 @@ internal class CompositionBuilder { var windows: [ChromaKeyWindow] = [] for (index, clip) in videoClips.enumerated() { guard index < clipInstructions.count, + !clip.suppressChromaKey, let key = clip.chromaKey ?? globalChromaKey else { continue } diff --git a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/models/RenderConfig.swift b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/models/RenderConfig.swift index 47ee4078..e2a165bd 100644 --- a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/models/RenderConfig.swift +++ b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/models/RenderConfig.swift @@ -220,9 +220,29 @@ public struct ChromaKeyConfig: Sendable, Equatable { /// hash is not a safe cache key across the lifetime of a render, and the /// background image data is far too large to hash per frame. var cacheKey: String { - let bgLength = backgroundImageData?.count ?? 0 return "\(keyR),\(keyG),\(keyB),\(similarity),\(smoothness),\(spill)," - + "\(backgroundColor),\(bgLength)" + + "\(backgroundColor),\(backgroundImageFingerprint)" + } + + /// Cheap content fingerprint of the background image. + /// + /// The byte count alone is not an identity — two different backgrounds that + /// happen to be the same size would share a cache entry, and the second clip + /// would render the first one's image. Mixing in the head and tail bytes + /// separates them while staying O(1), which matters because `cacheKey` is + /// evaluated per frame. + private var backgroundImageFingerprint: String { + guard let data = backgroundImageData, !data.isEmpty else { return "0" } + let sampleSize = min(32, data.count) + let head = data.prefix(sampleSize).reduce(into: UInt64(1_469_598_103_934_665_603)) { + accumulator, byte in + accumulator = (accumulator ^ UInt64(byte)) &* 1_099_511_628_211 + } + let tail = data.suffix(sampleSize).reduce(into: UInt64(1_469_598_103_934_665_603)) { + accumulator, byte in + accumulator = (accumulator ^ UInt64(byte)) &* 1_099_511_628_211 + } + return "\(data.count)-\(head)-\(tail)" } static func fromArguments(_ args: [String: Any]?) -> ChromaKeyConfig? { diff --git a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/models/VideoClip.swift b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/models/VideoClip.swift index 83e81bfb..e62396dc 100644 --- a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/models/VideoClip.swift +++ b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/models/VideoClip.swift @@ -21,6 +21,13 @@ internal struct VideoClip: Sendable { /// Removes a solid-colored background from this clip. Overrides the layer's /// and the global key. `nil` = fall back to those. let chromaKey: ChromaKeyConfig? + /// Opts this clip out of the layer/global key entirely. + /// + /// Internal, never parsed from the platform channel. `chromaKey == nil` means + /// "inherit", so it cannot express "deliberately unkeyed" — which is exactly + /// what a pre-rendered overlap blend needs when the two clips it was composed + /// from carry different keys. See `RenderVideo.blendChromaKey`. + let suppressChromaKey: Bool init( inputPath: String, @@ -32,7 +39,8 @@ internal struct VideoClip: Sendable { transition: ClipTransitionConfig? = nil, timelineStartUs: Int64? = nil, transform: SegmentTransformConfig? = nil, - chromaKey: ChromaKeyConfig? = nil + chromaKey: ChromaKeyConfig? = nil, + suppressChromaKey: Bool = false ) { self.inputPath = inputPath self.startUs = startUs @@ -44,6 +52,7 @@ internal struct VideoClip: Sendable { self.timelineStartUs = timelineStartUs self.transform = transform self.chromaKey = chromaKey + self.suppressChromaKey = suppressChromaKey } /// Parses a clip from a platform-channel map. Used by both the single-track diff --git a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/utils/VideoCompositor.swift b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/utils/VideoCompositor.swift index aa1e924d..2d883967 100644 --- a/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/utils/VideoCompositor.swift +++ b/darwin/pro_video_editor/Sources/pro_video_editor/src/shared/features/render/utils/VideoCompositor.swift @@ -201,12 +201,27 @@ class VideoCompositor: NSObject, AVVideoCompositing { where window.endUs > window.startUs && tUs >= window.startUs && tUs < window.endUs { return window.config } - // The last frame of the timeline lands exactly on the final window's end, - // so clamp to it rather than emitting one unkeyed frame. - if let last = chromaKeyWindows.last, tUs >= last.endUs { return last.config } + // The very last frame of the timeline can land exactly on the final + // window's end, so clamp to it rather than emitting one unkeyed frame. + // + // Only ever by a frame, and only for the window that actually ends the + // timeline. Windows are emitted per *keyed* clip, so the array is sparse: + // an unconditional clamp applied the last keyed clip's key to every clip + // after it, keying clips that carry no key at all. + if let last = chromaKeyWindows.last, tUs >= last.endUs, + tUs - last.endUs <= Self.chromaKeyEndClampToleranceUs + { + return last.config + } return nil } + /// How far past a window's end still counts as its final frame. + /// + /// One frame at 24 fps, the slowest rate worth accommodating; anything beyond + /// that is a different clip, not a rounding edge. + private static let chromaKeyEndClampToleranceUs: Int64 = 41_667 + /// Applies a dip-to-color (fade-to-black / fade-to-white) at the given /// composition time, if any fade window is active. The video is mixed toward /// the dip color by `dipAmount` (0 = full video, 1 = full color). @@ -435,13 +450,26 @@ class VideoCompositor: NSObject, AVVideoCompositing { return image.composited(over: scaleToFill(background, extent)) } + // A background image that will not decode must not quietly leave the keyed + // area transparent: on the layered path that shows the layer below instead + // of the requested backdrop, and Android would disagree. Fill with opaque + // black, matching `ChromaKeyEffect`'s fallback for the same case. + if config.backgroundImageData != nil { + return image.composited(over: CIImage(color: .black).cropped(to: extent)) + } + guard config.backgroundColor != -1 else { return image } let argb = config.backgroundColor + // Alpha is deliberately ignored: the fill replaces the removed screen + // rather than tinting it, and Android's shader has no alpha uniform for it. + // `ChromaKey.toAsyncMap` asserts the color is opaque, so this only bites in + // release. For a see-through result the key is left backgroundless and the + // backdrop goes on a lower layer. let color = CIColor( red: CGFloat((argb >> 16) & 0xFF) / 255.0, green: CGFloat((argb >> 8) & 0xFF) / 255.0, blue: CGFloat(argb & 0xFF) / 255.0, - alpha: CGFloat((argb >> 24) & 0xFF) / 255.0) + alpha: 1.0) return image.composited(over: CIImage(color: color).cropped(to: extent)) } @@ -670,6 +698,15 @@ class VideoCompositor: NSObject, AVVideoCompositing { outputImage = CIImage(cvPixelBuffer: sourceBuffer) + // Remove the chroma key on the raw decoded frame and fill the keyed area + // with its background — before the layer-instruction transform below, + // which on a mixed-resolution timeline is a real bilinear resample. Keying + // interpolated pixels widens the matte edge by an invented pixel and runs + // the ramp over colors the decoder never produced. Android keys first for + // the same reason (`applyChromaKey` is added ahead of + // `VideoCompositionTransformation` in the effect chain). + outputImage = applyChromaKeyStage(to: outputImage, at: request.compositionTime) + // Apply layer instruction transform first (video scaling/centering/rotation) // This ensures all videos are properly sized and oriented before applying user effects. // The layerInstruction contains the preferredTransform which already handles video rotation @@ -742,11 +779,6 @@ class VideoCompositor: NSObject, AVVideoCompositing { } } - // Remove the chroma key on the raw source frame, before any geometry, and - // fill the keyed area with its background. One insertion point for both - // branches of the imageBytesWithCropping split below. - outputImage = applyChromaKeyStage(to: outputImage, at: request.compositionTime) - var center = CGPoint(x: outputImage.extent.midX, y: outputImage.extent.midY) // Apply user-defined effects (crop, rotation, flip, scale) diff --git a/example/integration_test/chroma_key_test.dart b/example/integration_test/chroma_key_test.dart index ab76aa6a..31fe33ab 100644 --- a/example/integration_test/chroma_key_test.dart +++ b/example/integration_test/chroma_key_test.dart @@ -55,6 +55,20 @@ void main() { }); } + /// A PNG whose top half is [top] and bottom half is [bottom]. + /// + /// A solid background cannot tell a vertically flipped texture from a correct + /// one, and neither can a left/right split. This can. + Future stackedPng(Color top, Color bottom) { + return paintPng(canvas, (c) { + final w = canvas.width; + final h = canvas.height; + c + ..drawRect(Rect.fromLTWH(0, 0, w, h / 2), Paint()..color = top) + ..drawRect(Rect.fromLTWH(0, h / 2, w, h / 2), Paint()..color = bottom); + }); + } + /// A solid-color PNG. Future solidPng(Color color) { return paintPng( @@ -249,6 +263,39 @@ void main() { } }); + testWidgets('the background image is not vertically flipped', (_) async { + // Android uploads the bitmap with GLUtils.texImage2D, which puts row 0 + // at t=0, but samples it with the frame's own coordinate, where t=0 is + // the *bottom*. Media3 flips bitmap textures for exactly this reason. + // Only an asymmetric background can see the difference. + final out = await pve.renderVideo( + VideoRenderData( + videoSegments: [VideoSegment(video: greenScreen)], + chromaKey: ChromaKey( + backgroundImage: EditorLayerImage.memory( + await stackedPng(backgroundRed, subjectBlue), + ), + ), + ), + ); + + // Sampled inside the keyed (left) half, so only the background shows. + final top = await samplePixel(out, 0.15, 0.2); + final bottom = await samplePixel(out, 0.15, 0.8); + + expect( + isRed(top), + isTrue, + reason: 'Background image top should be red, got $top — flipped?', + ); + expect( + isBlue(bottom), + isTrue, + reason: + 'Background image bottom should be blue, got $bottom — flipped?', + ); + }); + testWidgets('a color filter does not un-key the frame', (_) async { // Apple folds the key and the filter into one CIColorCube because a // second cube would take its alpha from its own data and resurrect the diff --git a/example/lib/features/chroma_key/chroma_key_example_page.dart b/example/lib/features/chroma_key/chroma_key_example_page.dart index 7b210602..9d0adb2f 100644 --- a/example/lib/features/chroma_key/chroma_key_example_page.dart +++ b/example/lib/features/chroma_key/chroma_key_example_page.dart @@ -71,8 +71,11 @@ class _ChromaKeyExamplePageState extends State { @override void dispose() { - if (_isPreviewInitialized) _controllerPreview?.dispose(); + // Unconditional: the controller can be fully initialized and playing while + // `_isPreviewInitialized` is still false, when the page is popped between + // `initialize()` and the `setState` that flips the flag. _chewieControllerPreview?.dispose(); + _controllerPreview?.dispose(); super.dispose(); } @@ -195,6 +198,14 @@ class _ChromaKeyExamplePageState extends State { _controllerPreview = VideoPlayerController.file(File(outputPath)); await _controllerPreview!.initialize(); + + // Checked before the Chewie controller exists, so a page popped during + // initialization never leaves an autoplaying player behind. + if (!mounted) { + await _disposePreview(); + return; + } + _chewieControllerPreview = ChewieController( videoPlayerController: _controllerPreview!, autoPlay: true, @@ -202,7 +213,6 @@ class _ChromaKeyExamplePageState extends State { placeholder: Container(color: Colors.black), ); - if (!mounted) return; setState(() => _isPreviewInitialized = true); } diff --git a/example/macos/RunnerTests/RunnerTests.swift b/example/macos/RunnerTests/RunnerTests.swift index e052f782..7f930767 100644 --- a/example/macos/RunnerTests/RunnerTests.swift +++ b/example/macos/RunnerTests/RunnerTests.swift @@ -755,4 +755,31 @@ class ChromaKeyMathTests: XCTestCase { XCTAssertEqual(floats[offset + 2], 0.0, accuracy: 1e-5, "premultiplied blue must be 0") } + /// Two different background images of the same byte length must not share a + /// cache entry, or the second clip renders the first one's backdrop. + func testCacheKeySeparatesEqualLengthBackgrounds() { + func withBackground(_ bytes: [UInt8]) -> ChromaKeyConfig { + ChromaKeyConfig( + keyR: config.keyR, keyG: config.keyG, keyB: config.keyB, + similarity: config.similarity, smoothness: config.smoothness, + spill: config.spill, backgroundColor: -1, + backgroundImageData: Data(bytes)) + } + + let a = withBackground(Array(repeating: 0x11, count: 128)) + var bBytes = Array(repeating: UInt8(0x11), count: 128) + bBytes[0] = 0x22 + let b = withBackground(bBytes) + + XCTAssertEqual(a.backgroundImageData?.count, b.backgroundImageData?.count) + XCTAssertNotEqual(a.cacheKey, b.cacheKey) + XCTAssertEqual(a.cacheKey, withBackground(Array(repeating: 0x11, count: 128)).cacheKey) + } + + /// A key with no background image at all keeps a stable, distinct key. + func testCacheKeyIsStableWithoutABackgroundImage() { + XCTAssertEqual(config.cacheKey, config.cacheKey) + XCTAssertNotEqual(config.cacheKey, withSpill(0.9).cacheKey) + } + } diff --git a/lib/core/models/video/chroma_key_model.dart b/lib/core/models/video/chroma_key_model.dart index 92e8699a..499a5ea3 100644 --- a/lib/core/models/video/chroma_key_model.dart +++ b/lib/core/models/video/chroma_key_model.dart @@ -243,13 +243,21 @@ class ChromaKey { int? height; for (final frame in frames) { final codec = await instantiateImageCodec(frame); - final image = (await codec.getNextFrame()).image; - final data = await image.toByteData(); - if (data == null) continue; - width ??= image.width; - height ??= image.height; - if (image.width != width || image.height != height) continue; - buffers.add(data.buffer.asUint8List()); + try { + final image = (await codec.getNextFrame()).image; + try { + final data = await image.toByteData(); + if (data == null) continue; + width ??= image.width; + height ??= image.height; + if (image.width != width || image.height != height) continue; + buffers.add(data.buffer.asUint8List()); + } finally { + image.dispose(); + } + } finally { + codec.dispose(); + } } if (buffers.isEmpty || width == null || height == null) { @@ -262,8 +270,13 @@ class ChromaKey { /// The screen color to remove. /// /// **Default**: `0xFF00B140`, the SMPTE "chroma key green" most physical - /// screens are painted in. Only the hue is used — the brightness of this - /// color does not matter. + /// screens are painted in. + /// + /// Both the hue **and** the brightness matter. The key point is this color's + /// un-normalized Cb/Cr projection, and Cb/Cr scale with brightness, so a key + /// color much darker than the recorded screen sits closer to neutral and can + /// leave the whole screen outside [similarity]. Prefer [autoDetect] over + /// picking a swatch by eye. /// /// ## Blue screens /// @@ -347,6 +360,13 @@ class ChromaKey { /// /// Mutually exclusive with [backgroundImage]. When both are `null` the keyed /// area is transparent — see the class docs for what that means per path. + /// + /// **Must be fully opaque.** Only the RGB channels are used: the fill is a + /// replacement for the removed screen, not a tint over it. A translucent + /// background is not expressible identically on both renderers, so it is + /// rejected rather than silently rendered two different ways. For a + /// see-through result leave this `null` and put the backdrop on a lower + /// [VideoLayer]. final Color? backgroundColor; /// An image to put behind the subject, stretched to fill the frame. @@ -361,6 +381,13 @@ class ChromaKey { /// /// Resolves [backgroundImage] to bytes, so this is asynchronous. Future> toAsyncMap() async { + assert( + backgroundColor == null || backgroundColor!.a == 1.0, + '[backgroundColor] must be fully opaque — only its RGB channels are ' + 'used, and both renderers fill the keyed area solidly. For a ' + 'see-through result leave it null and put the backdrop on a lower ' + 'VideoLayer of a VideoComposition.', + ); return { 'keyColor': color.toARGB32(), 'similarity': similarity, @@ -372,6 +399,12 @@ class ChromaKey { } /// Creates a copy with updated values. + /// + /// [backgroundColor] and [backgroundImage] are mutually exclusive, so passing + /// one **drops** the other — that is what lets a key switch from an image + /// background to a color one. Passing neither keeps whichever is already set. + /// Pass [removeBackground] to clear both and leave the keyed area + /// transparent, which no combination of the other arguments can express. ChromaKey copyWith({ Color? color, double? similarity, @@ -379,14 +412,41 @@ class ChromaKey { double? spill, Color? backgroundColor, EditorLayerImage? backgroundImage, + bool removeBackground = false, }) { + assert( + backgroundColor == null || backgroundImage == null, + 'Provide at most one of [backgroundColor] or [backgroundImage]', + ); + assert( + !removeBackground || (backgroundColor == null && backgroundImage == null), + '[removeBackground] clears the background, so it cannot be combined ' + 'with [backgroundColor] or [backgroundImage]', + ); + + final Color? nextColor; + final EditorLayerImage? nextImage; + if (removeBackground) { + nextColor = null; + nextImage = null; + } else if (backgroundColor != null) { + nextColor = backgroundColor; + nextImage = null; + } else if (backgroundImage != null) { + nextColor = null; + nextImage = backgroundImage; + } else { + nextColor = this.backgroundColor; + nextImage = this.backgroundImage; + } + return ChromaKey( color: color ?? this.color, similarity: similarity ?? this.similarity, smoothness: smoothness ?? this.smoothness, spill: spill ?? this.spill, - backgroundColor: backgroundColor ?? this.backgroundColor, - backgroundImage: backgroundImage ?? this.backgroundImage, + backgroundColor: nextColor, + backgroundImage: nextImage, ); } diff --git a/lib/core/models/video/video_render_data_model.dart b/lib/core/models/video/video_render_data_model.dart index 43663048..d9c0163e 100644 --- a/lib/core/models/video/video_render_data_model.dart +++ b/lib/core/models/video/video_render_data_model.dart @@ -67,7 +67,7 @@ class VideoRenderData { '[maxFrameRate] must be greater than 0', ), assert( - composition != null || chromaKey == null || !chromaKey.isTransparent, + _everyKeyHasABackground(composition, chromaKey, videoSegments), 'A [chromaKey] without a background needs something underneath to ' 'show through, and videoSegments is a single track with nothing ' 'below it — H.264/HEVC carry no alpha, so the keyed area would be ' @@ -77,6 +77,26 @@ class VideoRenderData { 'backgroundColor: Color(0xFF000000) if you really do want black.', ); + /// Whether every key that can reach the single-track path has a background. + /// + /// Checks the per-segment keys as well as the global one: they are resolved + /// as `segment → global`, so a transparent key set on a single [VideoSegment] + /// is flattened to black exactly like a transparent global one. The layered + /// path is exempt — there a lower [VideoLayer] is what shows through. + static bool _everyKeyHasABackground( + VideoComposition? composition, + ChromaKey? chromaKey, + List? videoSegments, + ) { + if (composition != null) return true; + if (chromaKey != null && chromaKey.isTransparent) return false; + return videoSegments?.every((segment) { + final key = segment.chromaKey; + return key == null || !key.isTransparent; + }) ?? + true; + } + /// Creates a [VideoRenderData] with a predefined quality preset. /// /// This factory constructor simplifies video export by providing common diff --git a/lib/core/utils/chroma_key_detector.dart b/lib/core/utils/chroma_key_detector.dart index 2bd1a317..557cc47f 100644 --- a/lib/core/utils/chroma_key_detector.dart +++ b/lib/core/utils/chroma_key_detector.dart @@ -80,6 +80,17 @@ abstract final class ChromaKeyDetector { /// At least this share of the border must belong to one color. static const _minCoverage = 0.6; + /// How far from the median a border pixel may sit and still count as screen. + /// + /// Deliberately a **constant**, not a percentile of the measured + /// distribution: a band derived from the distances themselves would contain + /// a fixed share of them by construction, and the coverage test below would + /// pass for every possible frame. Sized off the default `similarity` — wide + /// enough that an unevenly lit screen still counts (the falloff shows up as + /// spread, not as missing coverage), tight enough that skin (`0.38`–`0.42` + /// from green) and an olive shirt (`0.26`) do not. + static const _coverageBand = 0.20; + /// Multiplier on the measured spread, so the soft edge and a little /// frame-to-frame noise stay inside the key. static const _spreadMargin = 2.0; @@ -175,17 +186,12 @@ abstract final class ChromaKeyDetector { return sqrt(dcb * dcb + dcr * dcr); })..sort(); - final spread = - distances[(distances.length * 0.99).floor().clamp( - 0, - distances.length - 1, - )]; - - // Coverage uses a generous band around the median, so a soft edge counts - // as screen while a wall or a sleeve does not. - final band = max(spread, _minSimilarity); - final covered = distances.where((d) => d <= band).length; - final coverage = covered / distances.length; + // Which border pixels belong to the screen. The band is a constant, so + // this is a real measurement — a wall or a sleeve falls outside it and + // drags coverage down, while an unevenly lit screen stays inside and shows + // up as spread instead. + final screen = distances.takeWhile((d) => d <= _coverageBand).toList(); + final coverage = screen.length / distances.length; if (coverage < _minCoverage) { throw ChromaKeyDetectionException( @@ -195,6 +201,11 @@ abstract final class ChromaKeyDetector { ); } + // Measured over the screen pixels alone, so anything that reached the + // border without being part of the screen cannot widen the key. + final spread = + screen[(screen.length * 0.99).floor().clamp(0, screen.length - 1)]; + final similarity = (spread * _spreadMargin).clamp( _minSimilarity, _maxSimilarity, diff --git a/test/core/models/video/chroma_key_model_test.dart b/test/core/models/video/chroma_key_model_test.dart index 7b6e8022..ad4f6d94 100644 --- a/test/core/models/video/chroma_key_model_test.dart +++ b/test/core/models/video/chroma_key_model_test.dart @@ -182,6 +182,72 @@ void main() { test('keeps every value when called empty', () { expect(key.copyWith(), key); }); + + test('switching from an image background to a color drops the image', () { + final withImage = key.copyWith( + backgroundImage: EditorLayerImage.asset('assets/bg.png'), + ); + expect(withImage.backgroundImage, isNotNull); + expect(withImage.backgroundColor, isNull); + + final withColor = withImage.copyWith( + backgroundColor: const Color(0xFF0000FF), + ); + + expect(withColor.backgroundColor, const Color(0xFF0000FF)); + expect( + withColor.backgroundImage, + isNull, + reason: + 'the two are mutually exclusive, so setting one clears the ' + 'other instead of tripping the assert', + ); + }); + + test('switching from a color background to an image drops the color', () { + final copy = key.copyWith( + backgroundImage: EditorLayerImage.asset('assets/bg.png'), + ); + + expect(copy.backgroundImage, isNotNull); + expect(copy.backgroundColor, isNull); + }); + + test('removeBackground clears both and leaves the key transparent', () { + final copy = key.copyWith(removeBackground: true); + + expect(copy.isTransparent, isTrue); + expect(copy.backgroundColor, isNull); + expect(copy.backgroundImage, isNull); + expect(copy.color, key.color, reason: 'only the background is cleared'); + expect(copy.similarity, key.similarity); + }); + + test('removeBackground cannot be combined with a background', () { + expect( + () => key.copyWith( + removeBackground: true, + backgroundColor: const Color(0xFF00FF00), + ), + throwsA(isA()), + ); + }); + }); + + group('backgroundColor opacity', () { + test('a translucent background is rejected when serialized', () { + // Only RGB reaches either renderer, so a translucent fill would render + // two different ways. Caught at the boundary rather than silently. + const translucent = ChromaKey(backgroundColor: Color(0x80FF0000)); + + expect(translucent.toAsyncMap(), throwsA(isA())); + }); + + test('an opaque background serializes fine', () async { + const opaque = ChromaKey(backgroundColor: Color(0xFFFF0000)); + + await expectLater(opaque.toAsyncMap(), completes); + }); }); group('equality', () { diff --git a/test/core/models/video/video_render_data_model_test.dart b/test/core/models/video/video_render_data_model_test.dart index 77a58b8a..f5a7fa07 100644 --- a/test/core/models/video/video_render_data_model_test.dart +++ b/test/core/models/video/video_render_data_model_test.dart @@ -187,6 +187,61 @@ void main() { ); }); + test('rejects a transparent key set on a single segment', () { + // Keys resolve segment → global, so a transparent per-segment key is + // flattened to black exactly like a transparent global one. The guard + // used to look only at the global key and let this through silently. + expect( + () => VideoRenderData( + id: 'test', + videoSegments: [ + VideoSegment(video: EditorVideo.file('a.mp4')), + VideoSegment( + video: EditorVideo.file('b.mp4'), + chromaKey: const ChromaKey(), + ), + ], + ), + throwsA(isA()), + ); + }); + + test('allows a backed key on a segment', () { + expect( + VideoRenderData( + id: 'test', + videoSegments: [ + VideoSegment( + video: EditorVideo.file('a.mp4'), + chromaKey: const ChromaKey(backgroundColor: Color(0xFF000000)), + ), + ], + ).videoSegments!.first.chromaKey, + isNotNull, + ); + }); + + test('allows a transparent segment key inside a composition', () { + expect( + VideoRenderData( + id: 'test', + composition: VideoComposition( + layers: [ + VideoLayer( + clips: [ + VideoSegment( + video: EditorVideo.file('test.mp4'), + chromaKey: const ChromaKey(), + ), + ], + ), + ], + ), + ).composition, + isNotNull, + ); + }); + test('allows a transparent key inside a composition', () { expect( VideoRenderData( diff --git a/test/core/utils/chroma_key_detector_test.dart b/test/core/utils/chroma_key_detector_test.dart index bcdcb6fb..5e136dd3 100644 --- a/test/core/utils/chroma_key_detector_test.dart +++ b/test/core/utils/chroma_key_detector_test.dart @@ -41,6 +41,28 @@ void main() { return buffer; } + /// Builds an RGBA frame split vertically: [left] over the leftmost + /// [leftFraction] of the width, [right] over the rest. Both reach the frame + /// border, which is what the coverage test is there to catch. + Uint8List splitFrame({ + required Color left, + required Color right, + double leftFraction = 0.5, + }) { + final buffer = Uint8List(width * height * 4); + for (var y = 0; y < height; y++) { + for (var x = 0; x < width; x++) { + final c = x < width * leftFraction ? left : right; + final i = (y * width + x) * 4; + buffer[i] = (c.r * 255).round(); + buffer[i + 1] = (c.g * 255).round(); + buffer[i + 2] = (c.b * 255).round(); + buffer[i + 3] = 255; + } + } + return buffer; + } + double chromaDistance(Color a, Color b) { final ca = ChromaKeyDetector.chromaOf(a.r, a.g, a.b); final cb = ChromaKeyDetector.chromaOf(b.r, b.g, b.b); @@ -198,6 +220,51 @@ void main() { ); }); + test('a border that is half screen and half studio wall', () { + // The regression this guards: coverage used to be derived from a + // percentile of the very distances it was measuring, so it came out + // at ~99% for any frame at all and this never threw. + expect( + () => ChromaKeyDetector.fromFrames( + [splitFrame(left: green, right: const Color(0xFF8B5A2B))], + width: width, + height: height, + ), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('one color'), + ), + ), + ); + }); + + test('a border that is half screen and half red curtain', () { + expect( + () => ChromaKeyDetector.fromFrames( + [splitFrame(left: green, right: const Color(0xFFCC2222))], + width: width, + height: height, + ), + throwsA(isA()), + ); + }); + + test('coverage is a real measurement, not a constant', () { + // A minority of off-screen pixels is tolerated, but it has to show up + // in `coverage` — the old implementation reported ~1.0 regardless. + final result = ChromaKeyDetector.fromFrames( + [splitFrame(left: green, right: skin, leftFraction: 0.8)], + width: width, + height: height, + ); + + expect(result.coverage, lessThan(0.95)); + expect(result.coverage, greaterThan(0.6)); + expect(chromaDistance(result.color, green), lessThan(0.01)); + }); + test('a buffer that is too small for the stated size', () { expect( () => ChromaKeyDetector.fromFrames(