diff --git a/docs/plugins/gst_plugins_cache.json b/docs/plugins/gst_plugins_cache.json index b64a8401..9d409956 100644 --- a/docs/plugins/gst_plugins_cache.json +++ b/docs/plugins/gst_plugins_cache.json @@ -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, diff --git a/video/closedcaption/src/ccutils.rs b/video/closedcaption/src/ccutils.rs index 14597d4d..1ad2836f 100644 --- a/video/closedcaption/src/ccutils.rs +++ b/video/closedcaption/src/ccutils.rs @@ -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 { diff --git a/video/closedcaption/src/cea608utils.rs b/video/closedcaption/src/cea608utils.rs index a256bd99..d866f759 100644 --- a/video/closedcaption/src/cea608utils.rs +++ b/video/closedcaption/src/cea608utils.rs @@ -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,7 +511,8 @@ impl Cea608Renderer { context.set_base_dir(pango::Direction::Ltr); let layout = pango::Layout::new(&context); 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 { frame: Cea608Frame::new(), state: Cea608State::default(), @@ -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,15 +556,31 @@ impl Cea608Renderer { if width != self.video_width || height != self.video_height { self.video_width = width; self.video_height = height; - self.layout = pango::Layout::new(&self.context); - 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(); + 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 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) { self.black_background = bg; self.rectangle.take(); @@ -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, diff --git a/video/closedcaption/src/cea708overlay/imp.rs b/video/closedcaption/src/cea708overlay/imp.rs index a6cefbe9..36b71b34 100644 --- a/video/closedcaption/src/cea708overlay/imp.rs +++ b/video/closedcaption/src/cea708overlay/imp.rs @@ -28,6 +28,8 @@ static CAT: LazyLock = 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, + 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!(), } } diff --git a/video/closedcaption/src/cea708utils.rs b/video/closedcaption/src/cea708utils.rs index 73547629..fbb935a2 100644 --- a/video/closedcaption/src/cea708utils.rs +++ b/video/closedcaption/src/cea708utils.rs @@ -309,6 +309,9 @@ pub struct Cea708Renderer { video_width: u32, video_height: u32, composition: Option, + + 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) { 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, + 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 { if !self.visible { return None;