Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions backend/src/blocks/builtin/vision_mixer/builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ impl BlockBuilder for VisionMixerBuilder {
);

let num_dsk_inputs = properties::parse_num_dsk_inputs(props);
let dsk_alpha_modes = properties::parse_dsk_alpha_modes(props, num_dsk_inputs);

let pip_bg_inputs: Vec<Option<usize>> = (0..num_pips)
.map(|i| properties::parse_pip_bg(props, i, num_inputs))
Expand Down Expand Up @@ -174,17 +175,18 @@ impl BlockBuilder for VisionMixerBuilder {
let backend = super::elements::select_backend(pref)?;

info!(
"Building vision mixer: {} inputs, PGM={}x{}@{}/{}, MV={}x{}@{}/{}, backend={:?}, pgm={}, pvw={}",
"Building vision mixer: {} inputs, PGM={}x{}@{}/{}, MV={}x{}@{}/{}, backend={:?}, pgm={}, pvw={}, dsk_alpha={:?}",
num_inputs, pgm_w, pgm_h, pgm_framerate.0, pgm_framerate.1,
mv_w, mv_h, mv_framerate.0, mv_framerate.1,
backend, pgm_input, pvw_input
backend, pgm_input, pvw_input, dsk_alpha_modes
);

let p = PipelineParams {
instance_id,
flow_id: properties::parse_flow_id(props),
num_inputs,
num_dsk_inputs,
dsk_alpha_modes: &dsk_alpha_modes,
num_pips,
pgm_input,
pvw_input,
Expand Down Expand Up @@ -225,6 +227,8 @@ pub(super) struct PipelineParams<'a> {
pub(super) flow_id: strom_types::FlowId,
pub(super) num_inputs: usize,
pub(super) num_dsk_inputs: usize,
/// Alpha encoding of each DSK input, `num_dsk_inputs` long.
pub(super) dsk_alpha_modes: &'a [vision_mixer::AlphaMode],
pub(super) num_pips: usize,
pub(super) pgm_input: usize,
pub(super) pvw_input: usize,
Expand Down Expand Up @@ -266,6 +270,11 @@ impl<'a> PipelineParams<'a> {
format!("{}:{}", self.instance_id, name)
}

/// Whether DSK input `i` was declared premultiplied.
pub(super) fn dsk_premultiplied(&self, i: usize) -> bool {
self.dsk_alpha_modes.get(i) == Some(&vision_mixer::AlphaMode::Premultiplied)
}

/// Raw video caps with resolution, framerate and an optional pixel format.
fn raw_caps(&self, w: u32, h: u32, framerate: (i32, i32), format: Option<&str>) -> gst::Caps {
let mut builder = gst::Caps::builder("video/x-raw")
Expand Down
12 changes: 12 additions & 0 deletions backend/src/blocks/builtin/vision_mixer/builder/pad_layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::collections::HashMap;

use strom_types::{vision_mixer, PropertyValue};

use super::super::elements::CompositorBackend;
use super::super::layout;
use super::PipelineParams;

Expand Down Expand Up @@ -165,6 +166,17 @@ pub(super) fn build_pad_properties(
"sizing-policy".to_string(),
PropertyValue::String("keep-aspect-ratio".to_string()),
);
if p.backend == CompositorBackend::OpenGL && p.dsk_premultiplied(i) {
// Premultiplied colour must not be scaled by its own alpha again,
// but still has to fade with the pad: blending with the constant
// gives src * pad_alpha + dst * (1 - src_alpha * pad_alpha).
// `one` would be right only at full pad alpha. The constant
// follows `alpha` through a binding set up in `pipeline_gpu`.
props.insert(
"blend-function-src-rgb".to_string(),
PropertyValue::String("constant-alpha".to_string()),
);
}
}

// --- Stinger pads: graphic, and on the GPU the matte. Both hidden, full
Expand Down
45 changes: 41 additions & 4 deletions backend/src/blocks/builtin/vision_mixer/builder/pipeline_cpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use super::super::{elements, layout};
use super::{audio_meter, pad_layout, setup_overlay_renderer, PipelineParams};
use crate::blocks::{BlockBuildContext, BlockBuildError, BlockBuildResult};
use crate::gpu;
use crate::gst::unpremultiply;

pub(super) fn build_cpu_pipeline(
p: &PipelineParams,
Expand Down Expand Up @@ -162,10 +163,46 @@ pub(super) fn build_cpu_pipeline(
elems.push((q_id.clone(), queue));
elems.push((vc_id_dsk.clone(), videoconvert));

links.push((
ElementPadRef::pad(&q_id, "src"),
ElementPadRef::pad(&vc_id_dsk, "sink"),
));
if p.dsk_premultiplied(i) {
// `compositor` blends straight alpha only, so a premultiplied
// source is unpremultiplied first. The element takes packed RGB
// with alpha; the converter in front brings other formats (such as
// a decoded A420 clip) to that, and passes RGBA-family input
// straight through.
if !unpremultiply::register() {
return Err(BlockBuildError::ElementCreation(format!(
"DSK{} is premultiplied but {} could not be registered",
i + 1,
unpremultiply::ELEMENT_NAME
)));
}
let vc_pre_id = p.id(&format!("videoconvert_unpremultiply_dsk_{}", i));
let unpre_id = p.id(&format!("unpremultiply_dsk_{}", i));
let videoconvert_pre = elements::make_element(vc_factory, &vc_pre_id)?;
gpu::configure_video_convert(&videoconvert_pre);
elems.push((vc_pre_id.clone(), videoconvert_pre));
elems.push((
unpre_id.clone(),
elements::make_element(unpremultiply::ELEMENT_NAME, &unpre_id)?,
));
links.push((
ElementPadRef::pad(&q_id, "src"),
ElementPadRef::pad(&vc_pre_id, "sink"),
));
links.push((
ElementPadRef::pad(&vc_pre_id, "src"),
ElementPadRef::pad(&unpre_id, "sink"),
));
links.push((
ElementPadRef::pad(&unpre_id, "src"),
ElementPadRef::pad(&vc_id_dsk, "sink"),
));
} else {
links.push((
ElementPadRef::pad(&q_id, "src"),
ElementPadRef::pad(&vc_id_dsk, "sink"),
));
}

// No output_format capsfilter here. A DSK is a keyer: forcing an
// alpha-less format (I420/NV12) ahead of the compositor silently
Expand Down
27 changes: 27 additions & 0 deletions backend/src/blocks/builtin/vision_mixer/builder/pipeline_gpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,7 @@ pub(super) fn build_gpu_pipeline(
let num_inputs = p.num_inputs;
let num_pips = p.num_pips;
let underlay_state = std::sync::Arc::clone(&overlay_state);
let dist_weak = dist_weak.clone();
ctx.register_element_setup(Box::new(move |_flow_id, _events| {
let (Some(mixer), Some(mv_comp)) = (dist_weak.upgrade(), mv_weak.upgrade()) else {
return;
Expand All @@ -680,6 +681,32 @@ pub(super) fn build_gpu_pipeline(
}));
}

// --- Premultiplied DSK pads: blend constant follows pad alpha ---
// `pad_layout` blends these pads with `constant-alpha`, which is only
// correct while the constant equals the pad's alpha. A property binding
// keeps them equal for every writer: DSK toggles, takes, fade to black
// (whose control binding sets `alpha` per frame), and the property API.
let premultiplied_dsk_pads: Vec<String> = (0..p.num_dsk_inputs)
.filter(|&i| p.dsk_premultiplied(i))
.map(|i| format!("sink_{}", p.num_inputs + i))
.collect();
if !premultiplied_dsk_pads.is_empty() {
ctx.register_element_setup(Box::new(move |_flow_id, _events| {
let Some(mixer) = dist_weak.upgrade() else {
return;
};
for pad in mixer
.sink_pads()
.into_iter()
.filter(|pad| premultiplied_dsk_pads.iter().any(|n| pad.name() == *n))
{
pad.bind_property("alpha", &pad, "blend-constant-color-alpha")
.sync_create()
.build();
}
}));
}

// --- Bus handler that pipes `level` messages into the overlay state ---
let bus_message_handler = Some(audio_meter::build_meter_bus_handler(
p.instance_id,
Expand Down
40 changes: 40 additions & 0 deletions backend/src/blocks/builtin/vision_mixer/definition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,46 @@ fn vision_mixer_definition() -> BlockDefinition {
});
}

// Per-DSK alpha mode, one property per possible DSK slot; the frontend
// hides slots beyond `num_dsk_inputs` (see `vision_mixer_skip_set`).
for i in 0..MAX_DSK_INPUTS {
let name = dsk_alpha_mode_property(i);
exposed_properties.push(ExposedProperty {
name: name.clone(),
label: format!("DSK{} Alpha", i + 1),
description: format!(
"How DSK{}'s source encodes alpha. HTML sources (cefsrc) emit premultiplied, \
which composites too dark as straight. Leave straight if the source already \
outputs straight alpha; converting twice comes out too bright. On the CPU \
mixer, feed a premultiplied source at program resolution (e.g. through a \
Video Format block), or its edges come out slightly dark",
i + 1
),
property_type: PropertyType::Enum {
values: vec![
EnumValue {
value: AlphaMode::Straight.as_str().to_string(),
label: Some("Straight".to_string()),
},
EnumValue {
value: AlphaMode::Premultiplied.as_str().to_string(),
label: Some("Premultiplied".to_string()),
},
],
},
default_value: Some(PropertyValue::String(
AlphaMode::default().as_str().to_string(),
)),
mapping: PropertyMapping {
element_id: "_block".to_string(),
property_name: name,
transform: None,
},
live: false,
persist: None,
});
}

// Number of PiP tiles
{
let mut pip_values = vec![EnumValue {
Expand Down
51 changes: 49 additions & 2 deletions backend/src/blocks/builtin/vision_mixer/properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

use std::collections::HashMap;
use strom_types::vision_mixer::{
Source, DEFAULT_DSK_INPUTS, DEFAULT_NUM_INPUTS, DEFAULT_NUM_PIPS, DEFAULT_SHOW_VU_METERS,
MAX_DSK_INPUTS, MAX_NUM_INPUTS, MAX_NUM_PIPS, MIN_NUM_INPUTS,
dsk_alpha_mode_property, AlphaMode, Source, DEFAULT_DSK_INPUTS, DEFAULT_NUM_INPUTS,
DEFAULT_NUM_PIPS, DEFAULT_SHOW_VU_METERS, MAX_DSK_INPUTS, MAX_NUM_INPUTS, MAX_NUM_PIPS,
MIN_NUM_INPUTS,
};
use strom_types::FlowId;
use strom_types::PropertyValue;
use tracing::warn;

/// Parse the owning flow id, injected as `_flow_id` by block expansion.
///
Expand Down Expand Up @@ -37,6 +39,29 @@ pub fn parse_num_dsk_inputs(properties: &HashMap<String, PropertyValue>) -> usiz
.min(MAX_DSK_INPUTS)
}

/// Parse each DSK input's alpha mode. Missing or unrecognised values are
/// straight, which is what the compositors assume; unrecognised ones are
/// logged, since the only symptom on air is a graphic that is too dark.
pub fn parse_dsk_alpha_modes(
properties: &HashMap<String, PropertyValue>,
num_dsk_inputs: usize,
) -> Vec<AlphaMode> {
(0..num_dsk_inputs)
.map(|i| {
let name = dsk_alpha_mode_property(i);
let parsed = match properties.get(&name) {
None => return AlphaMode::default(),
Some(PropertyValue::String(s)) => s.parse().map_err(|e| format!("{s:?}: {e}")),
Some(other) => Err(format!("{other:?} is not a string")),
};
parsed.unwrap_or_else(|e| {
warn!("{name} = {e}; using straight");
AlphaMode::default()
})
})
.collect()
}

/// Parse the number of PiP tiles from block properties.
pub fn parse_num_pips(properties: &HashMap<String, PropertyValue>) -> usize {
properties
Expand Down Expand Up @@ -306,6 +331,28 @@ pub fn parse_u64(properties: &HashMap<String, PropertyValue>, key: &str, default
mod tests {
use super::*;

#[test]
fn dsk_alpha_modes_default_to_straight() {
let mut props = HashMap::new();
props.insert(
"dsk_1_alpha_mode".to_string(),
PropertyValue::String("premultiplied".to_string()),
);
props.insert(
"dsk_2_alpha_mode".to_string(),
PropertyValue::String("nonsense".to_string()),
);
assert_eq!(
parse_dsk_alpha_modes(&props, 4),
vec![
AlphaMode::Straight,
AlphaMode::Premultiplied,
AlphaMode::Straight,
AlphaMode::Straight,
]
);
}

#[test]
fn parse_framerate_string_valid_fractions() {
assert_eq!(parse_framerate_string("25/1"), Some((25, 1)));
Expand Down
1 change: 1 addition & 0 deletions backend/src/gst/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub mod thumbnail;
pub mod thumbnail_tap;
pub mod transitions;
pub(crate) mod underlay;
pub mod unpremultiply;
pub mod video_adapt;
pub mod video_frame;
pub mod video_input_bridge;
Expand Down
8 changes: 8 additions & 0 deletions backend/src/gst/pipeline/effects/mixer_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,14 @@ impl PipelineManager {
let pad_name = format!("sink_{}", num_inputs + dsk_index);
if let Some(pad) = find_pad(mixer, &pad_name) {
let alpha = if enabled { 1.0f64 } else { 0.0f64 };
// A premultiplied DSK pad on the GL mixer blends with a constant
// that a binding copies from `alpha` after the write. Enabling
// sets it first, so no frame is drawn at alpha 1 with the constant
// still 0, which puts the graphic's silhouette on air in black.
// Disabling needs no help: the mixer skips pads at alpha 0.
if enabled && pad.has_property("blend-constant-color-alpha") {
pad.set_property("blend-constant-color-alpha", alpha);
}
pad.set_property("alpha", alpha);
// Update overlay state for DSK tracking
if let Some(state) = crate::blocks::builtin::vision_mixer::overlay::get_overlay_state(
Expand Down
Loading
Loading