cea708overlay: support changing the safe title area

By default it is 80% of the output size as recommended by CEA-708/608.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1985>
This commit is contained in:
Matthew Waters 2024-12-13 17:04:24 +11:00 committed by GStreamer Marge Bot
parent b367b38633
commit 497b1e58bd
5 changed files with 220 additions and 47 deletions

View file

@ -7984,6 +7984,34 @@
"type": "gint", "type": "gint",
"writable": true "writable": true
}, },
"safe-title-height": {
"blurb": "Ratio of the video height to use as the safe area for caption display",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "0.8",
"max": "1",
"min": "0",
"mutable": "playing",
"readable": true,
"type": "gfloat",
"writable": true
},
"safe-title-width": {
"blurb": "Ratio of the video width to use as the safe area for caption display",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "0.8",
"max": "1",
"min": "0",
"mutable": "playing",
"readable": true,
"type": "gfloat",
"writable": true
},
"service": { "service": {
"blurb": "The service to render the caption for when available, (-1=automatic, 0=disabled)", "blurb": "The service to render the caption for when available, (-1=automatic, 0=disabled)",
"conditionally-available": false, "conditionally-available": false,

View file

@ -157,8 +157,6 @@ pub(crate) fn recalculate_pango_layout(
video_height: u32, video_height: u32,
) -> (i32, i32) { ) -> (i32, i32) {
let mut font_desc = pango::FontDescription::from_string("monospace"); let mut font_desc = pango::FontDescription::from_string("monospace");
let video_width = video_width * 80 / 100;
let video_height = video_height * 80 / 100;
let mut font_size = 1; let mut font_size = 1;
loop { loop {

View file

@ -494,6 +494,11 @@ pub struct Cea608Renderer {
video_height: u32, video_height: u32,
left_alignment: i32, left_alignment: i32,
black_background: bool, black_background: bool,
safe_width: f32,
safe_height: f32,
max_layout_width: i32,
max_layout_height: i32,
} }
impl Cea608Renderer { impl Cea608Renderer {
@ -506,7 +511,8 @@ impl Cea608Renderer {
context.set_base_dir(pango::Direction::Ltr); context.set_base_dir(pango::Direction::Ltr);
let layout = pango::Layout::new(&context); let layout = pango::Layout::new(&context);
layout.set_alignment(pango::Alignment::Left); layout.set_alignment(pango::Alignment::Left);
recalculate_pango_layout(&layout, video_width, video_height); let (max_layout_width, max_layout_height) =
recalculate_pango_layout(&layout, video_width, video_height);
Self { Self {
frame: Cea608Frame::new(), frame: Cea608Frame::new(),
state: Cea608State::default(), state: Cea608State::default(),
@ -517,6 +523,10 @@ impl Cea608Renderer {
video_height, video_height,
left_alignment: 0, left_alignment: 0,
black_background: false, black_background: false,
safe_width: 0.8,
safe_height: 0.8,
max_layout_width,
max_layout_height,
} }
} }
@ -546,15 +556,31 @@ impl Cea608Renderer {
if width != self.video_width || height != self.video_height { if width != self.video_width || height != self.video_height {
self.video_width = width; self.video_width = width;
self.video_height = height; self.video_height = height;
self.layout = pango::Layout::new(&self.context); self.recalculate_window_position();
self.layout.set_alignment(pango::Alignment::Left);
let (max_layout_width, _max_layout_height) =
recalculate_pango_layout(&self.layout, width, height);
self.left_alignment = (width as i32 - max_layout_width) / 2;
self.rectangle.take();
} }
} }
pub fn set_safe_title_area(&mut self, safe_width: f32, safe_height: f32) {
if safe_width != self.safe_width || safe_height != self.safe_height {
self.safe_width = safe_width;
self.safe_height = safe_height;
self.recalculate_window_position();
}
}
fn recalculate_window_position(&mut self) {
self.layout = pango::Layout::new(&self.context);
self.layout.set_alignment(pango::Alignment::Left);
let width = (self.video_width as f32 * self.safe_width) as u32;
let height = (self.video_height as f32 * self.safe_height) as u32;
let (max_layout_width, max_layout_height) =
recalculate_pango_layout(&self.layout, width, height);
self.left_alignment = (self.video_width as i32 - max_layout_width) / 2;
self.max_layout_width = max_layout_width;
self.max_layout_height = max_layout_height;
self.rectangle.take();
}
pub fn set_black_background(&mut self, bg: bool) { pub fn set_black_background(&mut self, bg: bool) {
self.black_background = bg; self.black_background = bg;
self.rectangle.take(); self.rectangle.take();
@ -670,7 +696,6 @@ impl Cea608Renderer {
let (_ink_rect, logical_rect) = self.layout.extents(); let (_ink_rect, logical_rect) = self.layout.extents();
let height = logical_rect.height() / pango::SCALE; let height = logical_rect.height() / pango::SCALE;
let width = logical_rect.width() / pango::SCALE; let width = logical_rect.width() / pango::SCALE;
gst::debug!(CAT, "overlaying size {width}x{height}, text {text}");
// No text actually needs rendering // No text actually needs rendering
if width == 0 || height == 0 { if width == 0 || height == 0 {
@ -756,16 +781,24 @@ impl Cea608Renderer {
} }
}; };
let vertical_padding = self.video_height / 10; let safe_height = (self.video_height as f32 * self.safe_height) as i32;
let safe_height = self.video_height.mul_div_floor(80, 100).unwrap(); let vertical_padding =
let first_row_position = (safe_height as i32) (2 * self.video_height as i32 - safe_height - self.max_layout_height) / 2;
let first_row_position = self
.max_layout_height
.mul_div_round(first_row as i32, MAX_ROW as i32 + 1) .mul_div_round(first_row as i32, MAX_ROW as i32 + 1)
.unwrap(); .unwrap();
gst::debug!(
CAT,
"overlay size {width}x{height} at {}x{}, text {text}",
self.left_alignment,
first_row_position + vertical_padding
);
let rect = gst_video::VideoOverlayRectangle::new_raw( let rect = gst_video::VideoOverlayRectangle::new_raw(
&buffer, &buffer,
self.left_alignment, self.left_alignment,
first_row_position + vertical_padding as i32, first_row_position + vertical_padding,
width as u32, width as u32,
height as u32, height as u32,
gst_video::VideoOverlayFormatFlags::PREMULTIPLIED_ALPHA, gst_video::VideoOverlayFormatFlags::PREMULTIPLIED_ALPHA,

View file

@ -28,6 +28,8 @@ static CAT: LazyLock<gst::DebugCategory> = LazyLock::new(|| {
const DEFAULT_CEA608_CHANNEL: i32 = -1; const DEFAULT_CEA608_CHANNEL: i32 = -1;
const DEFAULT_SERVICE: i32 = 1; const DEFAULT_SERVICE: i32 = 1;
const DEFAULT_SAFE_WIDTH: f32 = 0.8;
const DEFAULT_SAFE_HEIGHT: f32 = 0.8;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct Settings { struct Settings {
@ -35,6 +37,8 @@ struct Settings {
cea608_channel: i32, cea608_channel: i32,
service: i32, service: i32,
timeout: Option<gst::ClockTime>, timeout: Option<gst::ClockTime>,
safe_width: f32,
safe_height: f32,
} }
impl Default for Settings { impl Default for Settings {
@ -44,6 +48,8 @@ impl Default for Settings {
cea608_channel: DEFAULT_CEA608_CHANNEL, cea608_channel: DEFAULT_CEA608_CHANNEL,
service: DEFAULT_SERVICE, service: DEFAULT_SERVICE,
timeout: gst::ClockTime::NONE, timeout: gst::ClockTime::NONE,
safe_width: DEFAULT_SAFE_WIDTH,
safe_height: DEFAULT_SAFE_HEIGHT,
} }
} }
} }
@ -131,6 +137,9 @@ impl Cea708Overlay {
); );
state.cea708_renderer.set_service_channel(state.selected); state.cea708_renderer.set_service_channel(state.selected);
state
.cea708_renderer
.set_safe_title_area(settings.safe_width, settings.safe_height);
settings.changed = false; settings.changed = false;
} }
@ -437,9 +446,12 @@ impl Cea708Overlay {
true true
} }
EventView::FlushStop(..) => { EventView::FlushStop(..) => {
let settings = self.settings.lock().unwrap().clone();
let mut state = self.state.lock().unwrap(); let mut state = self.state.lock().unwrap();
state.cea708_renderer = Cea708Renderer::new(); state.cea708_renderer = Cea708Renderer::new();
//state.cea608_renderer.set_black_background(settings.black_background); state
.cea708_renderer
.set_safe_title_area(settings.safe_width, settings.safe_height);
drop(state); drop(state);
gst::Pad::event_default(pad, Some(&*self.obj()), event) gst::Pad::event_default(pad, Some(&*self.obj()), event)
@ -518,6 +530,22 @@ impl ObjectImpl for Cea708Overlay {
.default_value(u64::MAX) .default_value(u64::MAX)
.mutable_playing() .mutable_playing()
.build(), .build(),
glib::ParamSpecFloat::builder("safe-title-height")
.nick("Safe Title Height")
.blurb("Ratio of the video height to use as the safe area for caption display")
.minimum(0.0)
.maximum(1.0)
.default_value(0.8)
.mutable_playing()
.build(),
glib::ParamSpecFloat::builder("safe-title-width")
.nick("Safe Title Width")
.blurb("Ratio of the video width to use as the safe area for caption display")
.minimum(0.0)
.maximum(1.0)
.default_value(0.8)
.mutable_playing()
.build(),
] ]
}); });
@ -554,28 +582,42 @@ impl ObjectImpl for Cea708Overlay {
_ => Some(timeout.nseconds()), _ => Some(timeout.nseconds()),
}; };
} }
"safe-title-width" => {
let mut settings = self.settings.lock().unwrap();
let new = value.get().expect("type checked upstream");
if new != settings.safe_width {
settings.safe_width = new;
settings.changed = true;
}
}
"safe-title-height" => {
let mut settings = self.settings.lock().unwrap();
let new = value.get().expect("type checked upstream");
if new != settings.safe_height {
settings.safe_height = new;
settings.changed = true;
}
}
_ => unimplemented!(), _ => unimplemented!(),
} }
} }
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let settings = self.settings.lock().unwrap();
match pspec.name() { match pspec.name() {
"cea608-channel" => { "cea608-channel" => settings.cea608_channel.to_value(),
let settings = self.settings.lock().unwrap(); "service" => settings.service.to_value(),
settings.cea608_channel.to_value()
}
"service" => {
let settings = self.settings.lock().unwrap();
settings.service.to_value()
}
"timeout" => { "timeout" => {
let settings = self.settings.lock().unwrap();
if let Some(timeout) = settings.timeout { if let Some(timeout) = settings.timeout {
timeout.nseconds().to_value() timeout.nseconds().to_value()
} else { } else {
u64::MAX.to_value() u64::MAX.to_value()
} }
} }
"safe-title-width" => settings.safe_width.to_value(),
"safe-title-height" => settings.safe_height.to_value(),
_ => unimplemented!(), _ => unimplemented!(),
} }
} }

View file

@ -309,6 +309,9 @@ pub struct Cea708Renderer {
video_width: u32, video_width: u32,
video_height: u32, video_height: u32,
composition: Option<gst_video::VideoOverlayComposition>, composition: Option<gst_video::VideoOverlayComposition>,
safe_width: f32,
safe_height: f32,
} }
impl Cea708Renderer { impl Cea708Renderer {
@ -322,6 +325,8 @@ impl Cea708Renderer {
video_width: 0, video_width: 0,
video_height: 0, video_height: 0,
composition: None, composition: None,
safe_width: 0.8,
safe_height: 0.8,
} }
} }
@ -337,6 +342,18 @@ impl Cea708Renderer {
} }
} }
pub fn set_safe_title_area(&mut self, safe_width: f32, safe_height: f32) {
if safe_width != self.safe_width || safe_height != self.safe_height {
self.safe_width = safe_width;
self.safe_height = safe_height;
self.cea608.set_safe_title_area(safe_width, safe_height);
if let Some(service) = self.service.as_mut() {
service.set_safe_title_area(safe_width, safe_height);
}
self.composition.take();
}
}
pub fn set_service_channel(&mut self, service_channel: Option<ServiceOrChannel>) { pub fn set_service_channel(&mut self, service_channel: Option<ServiceOrChannel>) {
if self.selected != service_channel { if self.selected != service_channel {
self.selected = service_channel; self.selected = service_channel;
@ -353,6 +370,7 @@ impl Cea708Renderer {
let overlay_service = self.service.get_or_insert_with(|| { let overlay_service = self.service.get_or_insert_with(|| {
let mut service = ServiceState::new(); let mut service = ServiceState::new();
service.set_video_size(self.video_width, self.video_height); service.set_video_size(self.video_width, self.video_height);
service.set_safe_title_area(self.safe_width, self.safe_height);
service service
}); });
overlay_service.handle_code(code); overlay_service.handle_code(code);
@ -446,6 +464,8 @@ struct ServiceState {
pango_context: pango::Context, pango_context: pango::Context,
video_width: u32, video_width: u32,
video_height: u32, video_height: u32,
width_ratio: f32,
height_ratio: f32,
} }
impl ServiceState { impl ServiceState {
@ -462,6 +482,8 @@ impl ServiceState {
pango_context: context, pango_context: context,
video_width: 0, video_width: 0,
video_height: 0, video_height: 0,
width_ratio: 0.8,
height_ratio: 0.8,
} }
} }
@ -486,23 +508,20 @@ impl ServiceState {
let layout = pango::Layout::new(&self.pango_context); let layout = pango::Layout::new(&self.pango_context);
// XXX: May need a different alignment // XXX: May need a different alignment
layout.set_alignment(pango::Alignment::Left); layout.set_alignment(pango::Alignment::Left);
let mut window = Window { self.windows.push_back(Window::new(
visible: args.visible, args.visible,
attrs: args.window_attributes(), *args,
pen_attrs: args.pen_attributes(), args.window_attributes(),
pen_color: args.pen_color(), args.pen_attributes(),
define: *args, args.pen_color(),
pen_location: SetPenLocationArgs::default(),
lines: VecDeque::new(),
rectangle: None,
layout, layout,
video_dims: Dimensions::default(), Dimensions {
window_position: Dimensions::default(), w: self.video_width,
window_dims: Dimensions::default(), h: self.video_height,
max_layout_dims: Dimensions::default(), },
}; self.width_ratio,
window.set_video_size(self.video_width, self.video_height); self.height_ratio,
self.windows.push_back(window); ));
}; };
self.current_window = args.window_id as usize; self.current_window = args.window_id as usize;
} }
@ -667,6 +686,14 @@ impl ServiceState {
self.video_width = video_width; self.video_width = video_width;
self.video_height = video_height; self.video_height = video_height;
} }
fn set_safe_title_area(&mut self, width_ratio: f32, height_ratio: f32) {
for window in self.windows.iter_mut() {
window.set_safe_title_area(width_ratio, height_ratio);
}
self.width_ratio = width_ratio;
self.height_ratio = height_ratio;
}
} }
fn color_value_as_u16(val: ColorValue) -> u16 { fn color_value_as_u16(val: ColorValue) -> u16 {
@ -753,6 +780,8 @@ struct Window {
pen_location: SetPenLocationArgs, pen_location: SetPenLocationArgs,
lines: VecDeque<WindowLine>, lines: VecDeque<WindowLine>,
safe_width: f32,
safe_height: f32,
window_position: Dimensions, window_position: Dimensions,
video_dims: Dimensions, video_dims: Dimensions,
window_dims: Dimensions, window_dims: Dimensions,
@ -762,6 +791,39 @@ struct Window {
} }
impl Window { impl Window {
#[allow(clippy::too_many_arguments)]
fn new(
visible: bool,
define: DefineWindowArgs,
attrs: SetWindowAttributesArgs,
pen_attrs: SetPenAttributesArgs,
pen_color: SetPenColorArgs,
layout: pango::Layout,
video_dims: Dimensions,
width_ratio: f32,
height_ratio: f32,
) -> Self {
let mut ret = Self {
visible,
define,
attrs,
pen_attrs,
pen_color,
pen_location: SetPenLocationArgs::default(),
lines: VecDeque::new(),
rectangle: None,
layout,
video_dims: Dimensions::default(),
window_position: Dimensions::default(),
window_dims: Dimensions::default(),
max_layout_dims: Dimensions::default(),
safe_width: width_ratio,
safe_height: height_ratio,
};
ret.set_video_size(video_dims.w, video_dims.h);
ret
}
fn dump(&self) { fn dump(&self) {
for line in self.lines.iter() { for line in self.lines.iter() {
let mut string = line.no.to_string(); let mut string = line.no.to_string();
@ -1050,8 +1112,12 @@ impl Window {
// XXX: may need a better implementation for 'skinny' (horizontal or vertical) output // XXX: may need a better implementation for 'skinny' (horizontal or vertical) output
// sizes. // sizes.
let safe_area = Dimensions {
w: (self.video_dims.w as f32 * self.safe_width) as u32,
h: (self.video_dims.h as f32 * self.safe_height) as u32,
};
let (max_layout_width, max_layout_height) = let (max_layout_width, max_layout_height) =
recalculate_pango_layout(&self.layout, self.video_dims.w, self.video_dims.h); recalculate_pango_layout(&self.layout, safe_area.w, safe_area.h);
self.max_layout_dims = Dimensions { self.max_layout_dims = Dimensions {
w: max_layout_width as u32, w: max_layout_width as u32,
h: max_layout_height as u32, h: max_layout_height as u32,
@ -1067,12 +1133,8 @@ impl Window {
}; };
let padding = Dimensions { let padding = Dimensions {
w: self.video_dims.w / 10, w: (self.video_dims.w - safe_area.w) / 2,
h: self.video_dims.h / 10, h: (self.video_dims.h - safe_area.h) / 2,
};
let safe_area = Dimensions {
w: self.video_dims.w - self.video_dims.w / 5,
h: self.video_dims.h - self.video_dims.h / 5,
}; };
self.window_position = if self.define.relative_positioning { self.window_position = if self.define.relative_positioning {
@ -1131,6 +1193,16 @@ impl Window {
self.recalculate_window_position(); self.recalculate_window_position();
} }
fn set_safe_title_area(&mut self, width_ratio: f32, height_ratio: f32) {
if self.safe_width == width_ratio && self.safe_height == height_ratio {
return;
}
self.safe_width = width_ratio;
self.safe_height = height_ratio;
self.recalculate_window_position();
}
fn generate_rectangle(&mut self) -> Option<gst_video::VideoOverlayRectangle> { fn generate_rectangle(&mut self) -> Option<gst_video::VideoOverlayRectangle> {
if !self.visible { if !self.visible {
return None; return None;