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",
"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": {
"blurb": "The service to render the caption for when available, (-1=automatic, 0=disabled)",
"conditionally-available": false,

View file

@ -157,8 +157,6 @@ pub(crate) fn recalculate_pango_layout(
video_height: u32,
) -> (i32, i32) {
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;
loop {

View file

@ -494,6 +494,11 @@ pub struct Cea608Renderer {
video_height: u32,
left_alignment: i32,
black_background: bool,
safe_width: f32,
safe_height: f32,
max_layout_width: i32,
max_layout_height: i32,
}
impl Cea608Renderer {
@ -506,6 +511,7 @@ impl Cea608Renderer {
context.set_base_dir(pango::Direction::Ltr);
let layout = pango::Layout::new(&context);
layout.set_alignment(pango::Alignment::Left);
let (max_layout_width, max_layout_height) =
recalculate_pango_layout(&layout, video_width, video_height);
Self {
frame: Cea608Frame::new(),
@ -517,6 +523,10 @@ impl Cea608Renderer {
video_height,
left_alignment: 0,
black_background: false,
safe_width: 0.8,
safe_height: 0.8,
max_layout_width,
max_layout_height,
}
}
@ -546,14 +556,30 @@ impl Cea608Renderer {
if width != self.video_width || height != self.video_height {
self.video_width = width;
self.video_height = height;
self.recalculate_window_position();
}
}
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 (max_layout_width, _max_layout_height) =
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 = (width as i32 - max_layout_width) / 2;
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) {
self.black_background = bg;
@ -670,7 +696,6 @@ impl Cea608Renderer {
let (_ink_rect, logical_rect) = self.layout.extents();
let height = logical_rect.height() / pango::SCALE;
let width = logical_rect.width() / pango::SCALE;
gst::debug!(CAT, "overlaying size {width}x{height}, text {text}");
// No text actually needs rendering
if width == 0 || height == 0 {
@ -756,16 +781,24 @@ impl Cea608Renderer {
}
};
let vertical_padding = self.video_height / 10;
let safe_height = self.video_height.mul_div_floor(80, 100).unwrap();
let first_row_position = (safe_height as i32)
let safe_height = (self.video_height as f32 * self.safe_height) as i32;
let vertical_padding =
(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)
.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(
&buffer,
self.left_alignment,
first_row_position + vertical_padding as i32,
first_row_position + vertical_padding,
width as u32,
height as u32,
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_SERVICE: i32 = 1;
const DEFAULT_SAFE_WIDTH: f32 = 0.8;
const DEFAULT_SAFE_HEIGHT: f32 = 0.8;
#[derive(Debug, Clone)]
struct Settings {
@ -35,6 +37,8 @@ struct Settings {
cea608_channel: i32,
service: i32,
timeout: Option<gst::ClockTime>,
safe_width: f32,
safe_height: f32,
}
impl Default for Settings {
@ -44,6 +48,8 @@ impl Default for Settings {
cea608_channel: DEFAULT_CEA608_CHANNEL,
service: DEFAULT_SERVICE,
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_safe_title_area(settings.safe_width, settings.safe_height);
settings.changed = false;
}
@ -437,9 +446,12 @@ impl Cea708Overlay {
true
}
EventView::FlushStop(..) => {
let settings = self.settings.lock().unwrap().clone();
let mut state = self.state.lock().unwrap();
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);
gst::Pad::event_default(pad, Some(&*self.obj()), event)
@ -518,6 +530,22 @@ impl ObjectImpl for Cea708Overlay {
.default_value(u64::MAX)
.mutable_playing()
.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()),
};
}
"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!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let settings = self.settings.lock().unwrap();
match pspec.name() {
"cea608-channel" => {
let settings = self.settings.lock().unwrap();
settings.cea608_channel.to_value()
}
"service" => {
let settings = self.settings.lock().unwrap();
settings.service.to_value()
}
"cea608-channel" => settings.cea608_channel.to_value(),
"service" => settings.service.to_value(),
"timeout" => {
let settings = self.settings.lock().unwrap();
if let Some(timeout) = settings.timeout {
timeout.nseconds().to_value()
} else {
u64::MAX.to_value()
}
}
"safe-title-width" => settings.safe_width.to_value(),
"safe-title-height" => settings.safe_height.to_value(),
_ => unimplemented!(),
}
}

View file

@ -309,6 +309,9 @@ pub struct Cea708Renderer {
video_width: u32,
video_height: u32,
composition: Option<gst_video::VideoOverlayComposition>,
safe_width: f32,
safe_height: f32,
}
impl Cea708Renderer {
@ -322,6 +325,8 @@ impl Cea708Renderer {
video_width: 0,
video_height: 0,
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>) {
if self.selected != service_channel {
self.selected = service_channel;
@ -353,6 +370,7 @@ impl Cea708Renderer {
let overlay_service = self.service.get_or_insert_with(|| {
let mut service = ServiceState::new();
service.set_video_size(self.video_width, self.video_height);
service.set_safe_title_area(self.safe_width, self.safe_height);
service
});
overlay_service.handle_code(code);
@ -446,6 +464,8 @@ struct ServiceState {
pango_context: pango::Context,
video_width: u32,
video_height: u32,
width_ratio: f32,
height_ratio: f32,
}
impl ServiceState {
@ -462,6 +482,8 @@ impl ServiceState {
pango_context: context,
video_width: 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);
// XXX: May need a different alignment
layout.set_alignment(pango::Alignment::Left);
let mut window = Window {
visible: args.visible,
attrs: args.window_attributes(),
pen_attrs: args.pen_attributes(),
pen_color: args.pen_color(),
define: *args,
pen_location: SetPenLocationArgs::default(),
lines: VecDeque::new(),
rectangle: None,
self.windows.push_back(Window::new(
args.visible,
*args,
args.window_attributes(),
args.pen_attributes(),
args.pen_color(),
layout,
video_dims: Dimensions::default(),
window_position: Dimensions::default(),
window_dims: Dimensions::default(),
max_layout_dims: Dimensions::default(),
};
window.set_video_size(self.video_width, self.video_height);
self.windows.push_back(window);
Dimensions {
w: self.video_width,
h: self.video_height,
},
self.width_ratio,
self.height_ratio,
));
};
self.current_window = args.window_id as usize;
}
@ -667,6 +686,14 @@ impl ServiceState {
self.video_width = video_width;
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 {
@ -753,6 +780,8 @@ struct Window {
pen_location: SetPenLocationArgs,
lines: VecDeque<WindowLine>,
safe_width: f32,
safe_height: f32,
window_position: Dimensions,
video_dims: Dimensions,
window_dims: Dimensions,
@ -762,6 +791,39 @@ struct 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) {
for line in self.lines.iter() {
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
// 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) =
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 {
w: max_layout_width as u32,
h: max_layout_height as u32,
@ -1067,12 +1133,8 @@ impl Window {
};
let padding = Dimensions {
w: self.video_dims.w / 10,
h: self.video_dims.h / 10,
};
let safe_area = Dimensions {
w: self.video_dims.w - self.video_dims.w / 5,
h: self.video_dims.h - self.video_dims.h / 5,
w: (self.video_dims.w - safe_area.w) / 2,
h: (self.video_dims.h - safe_area.h) / 2,
};
self.window_position = if self.define.relative_positioning {
@ -1131,6 +1193,16 @@ impl Window {
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> {
if !self.visible {
return None;