mirror of
https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git
synced 2024-05-18 16:28:25 +00:00
fbce73f6fc
Can overlay any single CEA-708 service or any single CEA-608 channel. Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1519>
1370 lines
46 KiB
Rust
1370 lines
46 KiB
Rust
// Copyright (C) 2023 Matthew Waters <matthew@centricular.com>
|
|
//
|
|
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
|
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
|
// <https://mozilla.org/MPL/2.0/>.
|
|
//
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
use cea708_types::{tables::*, Service};
|
|
|
|
use std::collections::VecDeque;
|
|
|
|
use gst::glib;
|
|
use gst::prelude::MulDiv;
|
|
use once_cell::sync::Lazy;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use pango::prelude::*;
|
|
|
|
use crate::ccutils::recalculate_pango_layout;
|
|
use crate::cea608utils::{Cea608Renderer, TextStyle};
|
|
|
|
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
|
|
gst::DebugCategory::new(
|
|
"cea708utils",
|
|
gst::DebugColorFlags::empty(),
|
|
Some("CEA-708 Utilities"),
|
|
)
|
|
});
|
|
|
|
#[derive(
|
|
Serialize, Deserialize, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy, glib::Enum,
|
|
)]
|
|
#[repr(u32)]
|
|
#[enum_type(name = "GstTtToCea708Mode")]
|
|
pub enum Cea708Mode {
|
|
PopOn,
|
|
PaintOn,
|
|
RollUp,
|
|
}
|
|
|
|
pub fn textstyle_foreground_color(style: TextStyle) -> cea708_types::tables::Color {
|
|
let color = match style {
|
|
TextStyle::Red => cea608_types::tables::Color::Red,
|
|
TextStyle::Blue => cea608_types::tables::Color::Blue,
|
|
TextStyle::Cyan => cea608_types::tables::Color::Cyan,
|
|
TextStyle::White => cea608_types::tables::Color::White,
|
|
TextStyle::Green => cea608_types::tables::Color::Green,
|
|
TextStyle::Yellow => cea608_types::tables::Color::Yellow,
|
|
TextStyle::Magenta => cea608_types::tables::Color::Magenta,
|
|
TextStyle::ItalicWhite => cea608_types::tables::Color::White,
|
|
};
|
|
cea608_color_to_foreground_color(color)
|
|
}
|
|
|
|
pub fn cea608_color_to_foreground_color(
|
|
color: cea608_types::tables::Color,
|
|
) -> cea708_types::tables::Color {
|
|
match color {
|
|
cea608_types::tables::Color::Red => cea708_types::tables::Color {
|
|
r: ColorValue::Full,
|
|
g: ColorValue::None,
|
|
b: ColorValue::None,
|
|
},
|
|
cea608_types::tables::Color::Green => cea708_types::tables::Color {
|
|
r: ColorValue::None,
|
|
g: ColorValue::Full,
|
|
b: ColorValue::None,
|
|
},
|
|
cea608_types::tables::Color::Blue => cea708_types::tables::Color {
|
|
r: ColorValue::None,
|
|
g: ColorValue::None,
|
|
b: ColorValue::Full,
|
|
},
|
|
cea608_types::tables::Color::Cyan => cea708_types::tables::Color {
|
|
r: ColorValue::None,
|
|
g: ColorValue::Full,
|
|
b: ColorValue::Full,
|
|
},
|
|
cea608_types::tables::Color::Yellow => cea708_types::tables::Color {
|
|
r: ColorValue::Full,
|
|
g: ColorValue::Full,
|
|
b: ColorValue::None,
|
|
},
|
|
cea608_types::tables::Color::Magenta => cea708_types::tables::Color {
|
|
r: ColorValue::Full,
|
|
g: ColorValue::None,
|
|
b: ColorValue::Full,
|
|
},
|
|
cea608_types::tables::Color::White => cea708_types::tables::Color {
|
|
r: ColorValue::Full,
|
|
g: ColorValue::Full,
|
|
b: ColorValue::Full,
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn textstyle_to_pen_color(style: TextStyle) -> SetPenColorArgs {
|
|
let black = Color {
|
|
r: ColorValue::None,
|
|
g: ColorValue::None,
|
|
b: ColorValue::None,
|
|
};
|
|
SetPenColorArgs {
|
|
foreground_color: textstyle_foreground_color(style),
|
|
foreground_opacity: Opacity::Solid,
|
|
background_color: black,
|
|
background_opacity: Opacity::Solid,
|
|
edge_color: black,
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct Cea708ServiceWriter {
|
|
codes: Vec<Code>,
|
|
service_no: u8,
|
|
active_window: WindowBits,
|
|
hidden_window: WindowBits,
|
|
}
|
|
|
|
impl Cea708ServiceWriter {
|
|
pub fn new(service_no: u8) -> Self {
|
|
Self {
|
|
codes: vec![],
|
|
service_no,
|
|
active_window: WindowBits::ZERO,
|
|
hidden_window: WindowBits::ONE,
|
|
}
|
|
}
|
|
|
|
pub fn take_service(&mut self, available_bytes: usize) -> Option<Service> {
|
|
if self.codes.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
gst::trace!(CAT, "New service block {}", self.service_no);
|
|
let mut service = Service::new(self.service_no);
|
|
let mut i = 0;
|
|
for code in self.codes.iter() {
|
|
if code.byte_len() > service.free_space() {
|
|
gst::trace!(CAT, "service is full");
|
|
break;
|
|
}
|
|
if service.len() + code.byte_len() > available_bytes {
|
|
gst::trace!(CAT, "packet is full");
|
|
break;
|
|
}
|
|
gst::trace!(CAT, "Adding code {code:?} to service");
|
|
match service.push_code(code) {
|
|
Ok(_) => i += 1,
|
|
Err(_) => break,
|
|
}
|
|
}
|
|
if i == 0 {
|
|
return None;
|
|
}
|
|
self.codes = self.codes.split_off(i);
|
|
Some(service)
|
|
}
|
|
|
|
pub fn popon_preamble(&mut self) {
|
|
gst::trace!(CAT, "popon_preamble");
|
|
let window = match self.hidden_window {
|
|
// switch up the newly defined window
|
|
WindowBits::ZERO => 0,
|
|
WindowBits::ONE => 1,
|
|
_ => unreachable!(),
|
|
};
|
|
let args = DefineWindowArgs::new(
|
|
window,
|
|
0,
|
|
Anchor::BottomMiddle,
|
|
true,
|
|
100,
|
|
50,
|
|
14,
|
|
31,
|
|
true,
|
|
true,
|
|
false,
|
|
1,
|
|
1,
|
|
);
|
|
gst::trace!(CAT, "active window {:?}", self.active_window);
|
|
let codes = [
|
|
Code::DeleteWindows(!self.active_window),
|
|
Code::DefineWindow(args),
|
|
];
|
|
self.push_codes(&codes)
|
|
}
|
|
|
|
pub fn clear_current_window(&mut self) {
|
|
gst::trace!(CAT, "clear_current_window {:?}", self.active_window);
|
|
self.push_codes(&[Code::ClearWindows(self.active_window)])
|
|
}
|
|
|
|
pub fn clear_hidden_window(&mut self) {
|
|
gst::trace!(CAT, "clear_hidden_window");
|
|
self.push_codes(&[Code::ClearWindows(self.hidden_window)])
|
|
}
|
|
|
|
pub fn end_of_caption(&mut self) {
|
|
gst::trace!(CAT, "end_of_caption");
|
|
self.push_codes(&[Code::ToggleWindows(self.active_window | self.hidden_window)]);
|
|
std::mem::swap(&mut self.active_window, &mut self.hidden_window);
|
|
gst::trace!(CAT, "active window {:?}", self.active_window);
|
|
}
|
|
|
|
pub fn paint_on_preamble(&mut self) {
|
|
gst::trace!(CAT, "paint_on_preamble");
|
|
let window = match self.active_window {
|
|
WindowBits::ZERO => 0,
|
|
WindowBits::ONE => 1,
|
|
_ => unreachable!(),
|
|
};
|
|
self.push_codes(&[
|
|
// FIXME: assumes positioning in a 16:9 ratio
|
|
Code::DefineWindow(DefineWindowArgs::new(
|
|
window,
|
|
0,
|
|
Anchor::BottomMiddle,
|
|
true,
|
|
100,
|
|
50,
|
|
14,
|
|
31,
|
|
true,
|
|
true,
|
|
true,
|
|
1,
|
|
1,
|
|
)),
|
|
])
|
|
}
|
|
|
|
pub fn rollup_preamble(&mut self, rollup_count: u8, base_row: u8) {
|
|
let base_row = std::cmp::max(rollup_count, base_row);
|
|
let anchor_vertical = (base_row as u32 * 100 / 14) as u8;
|
|
gst::trace!(
|
|
CAT,
|
|
"rollup_preamble base {base_row} count {rollup_count}, anchor-v {anchor_vertical}"
|
|
);
|
|
let codes = [
|
|
Code::DeleteWindows(!WindowBits::ZERO),
|
|
Code::DefineWindow(DefineWindowArgs::new(
|
|
0,
|
|
0,
|
|
Anchor::BottomMiddle,
|
|
true,
|
|
anchor_vertical,
|
|
50,
|
|
rollup_count - 1,
|
|
31,
|
|
true,
|
|
true,
|
|
true,
|
|
1,
|
|
1,
|
|
)),
|
|
Code::SetPenLocation(SetPenLocationArgs::new(rollup_count - 1, 0)),
|
|
];
|
|
self.active_window = WindowBits::ZERO;
|
|
self.hidden_window = WindowBits::ONE;
|
|
self.push_codes(&codes)
|
|
}
|
|
|
|
pub fn write_char(&mut self, c: char) {
|
|
if let Some(code) = Code::from_char(c) {
|
|
self.push_codes(&[code])
|
|
}
|
|
}
|
|
|
|
pub fn push_codes(&mut self, codes: &[Code]) {
|
|
gst::log!(CAT, "pushing codes: {codes:?}");
|
|
self.codes.extend(codes.iter().cloned());
|
|
}
|
|
|
|
pub fn etx(&mut self) {
|
|
self.push_codes(&[Code::ETX])
|
|
}
|
|
|
|
pub fn carriage_return(&mut self) {
|
|
self.push_codes(&[Code::CR])
|
|
}
|
|
|
|
pub fn set_pen_attributes(&mut self, args: SetPenAttributesArgs) {
|
|
self.push_codes(&[Code::SetPenAttributes(args)])
|
|
}
|
|
|
|
pub fn set_pen_location(&mut self, args: SetPenLocationArgs) {
|
|
self.push_codes(&[Code::SetPenLocation(args)])
|
|
}
|
|
|
|
pub fn set_pen_color(&mut self, args: SetPenColorArgs) {
|
|
self.push_codes(&[Code::SetPenColor(args)])
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
|
pub enum ServiceOrChannel {
|
|
Service(u8),
|
|
Cea608Channel(cea608_types::Id),
|
|
}
|
|
|
|
pub struct Cea708Renderer {
|
|
selected: Option<ServiceOrChannel>,
|
|
cea608: Cea608Renderer,
|
|
service: Option<ServiceState>,
|
|
video_width: u32,
|
|
video_height: u32,
|
|
composition: Option<gst_video::VideoOverlayComposition>,
|
|
}
|
|
|
|
impl Cea708Renderer {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
selected: None,
|
|
cea608: Cea608Renderer::new(),
|
|
service: None,
|
|
video_width: 0,
|
|
video_height: 0,
|
|
composition: None,
|
|
}
|
|
}
|
|
|
|
pub fn set_video_size(&mut self, width: u32, height: u32) {
|
|
if width != self.video_width || height != self.video_height {
|
|
self.video_width = width;
|
|
self.video_height = height;
|
|
self.cea608.set_video_size(width, height);
|
|
if let Some(service) = self.service.as_mut() {
|
|
service.set_video_size(width, height);
|
|
}
|
|
self.composition.take();
|
|
}
|
|
}
|
|
|
|
pub fn set_service_channel(&mut self, service_channel: Option<ServiceOrChannel>) {
|
|
if self.selected != service_channel {
|
|
self.selected = service_channel;
|
|
match service_channel {
|
|
Some(ServiceOrChannel::Cea608Channel(id)) => self.cea608.set_channel(id.channel()),
|
|
None => self.cea608.set_channel(cea608_types::tables::Channel::ONE),
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn push_service(&mut self, service: &Service) {
|
|
for code in service.codes() {
|
|
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
|
|
});
|
|
overlay_service.handle_code(code);
|
|
}
|
|
}
|
|
|
|
pub fn push_cea608(
|
|
&mut self,
|
|
field: cea608_types::tables::Field,
|
|
pair: [u8; 2],
|
|
) -> Result<bool, cea608_types::ParserError> {
|
|
match self.selected {
|
|
Some(ServiceOrChannel::Cea608Channel(channel)) => {
|
|
if channel.field() != field {
|
|
// data is not for the configured field, ignore
|
|
return Ok(false);
|
|
} else {
|
|
channel
|
|
}
|
|
}
|
|
None => cea608_types::Id::from_caption_field_channel(
|
|
field,
|
|
cea608_types::tables::Channel::ONE,
|
|
),
|
|
// data is not for the configured service, ignore
|
|
_ => return Ok(false),
|
|
};
|
|
|
|
let ret = self.cea608.push_pair(pair);
|
|
if let Ok(changed) = ret {
|
|
if self.selected.is_none() {
|
|
if let Some(chan) = self.cea608.channel() {
|
|
self.selected = Some(ServiceOrChannel::Cea608Channel(
|
|
cea608_types::Id::from_caption_field_channel(field, chan),
|
|
));
|
|
}
|
|
}
|
|
if changed {
|
|
self.composition.take();
|
|
}
|
|
}
|
|
ret
|
|
}
|
|
|
|
pub fn clear_composition(&mut self) {
|
|
self.composition.take();
|
|
}
|
|
|
|
pub fn generate_composition(&mut self) -> Option<gst_video::VideoOverlayComposition> {
|
|
let Some(selected) = self.selected else {
|
|
self.composition.take();
|
|
return None;
|
|
};
|
|
|
|
if matches!(selected, ServiceOrChannel::Service(_)) {
|
|
let service = self.service.as_mut()?;
|
|
|
|
let mut composition: Option<gst_video::VideoOverlayComposition> = None;
|
|
for window in service.windows.iter_mut() {
|
|
if let Some(rectangle) = window.generate_rectangle() {
|
|
if let Some(composition) = composition.as_mut() {
|
|
composition.get_mut().unwrap().add_rectangle(&rectangle);
|
|
} else {
|
|
composition =
|
|
gst_video::VideoOverlayComposition::new(Some(&rectangle)).ok();
|
|
}
|
|
}
|
|
}
|
|
|
|
self.composition = composition;
|
|
} else if let Some(rectangle) = self.cea608.generate_rectangle() {
|
|
self.composition = gst_video::VideoOverlayComposition::new(Some(&rectangle)).ok();
|
|
}
|
|
self.composition.clone()
|
|
}
|
|
}
|
|
|
|
// SAFETY: Required because `pango::Layout` / `pango::Context` are not `Send` but the whole
|
|
// `ServiceState` needs to be.
|
|
// We ensure that no additional references to the layout are ever created, which makes it safe
|
|
// to send it to other threads as long as only a single thread uses it concurrently.
|
|
unsafe impl Send for ServiceState {}
|
|
|
|
struct ServiceState {
|
|
windows: VecDeque<Window>,
|
|
current_window: usize,
|
|
pango_context: pango::Context,
|
|
video_width: u32,
|
|
video_height: u32,
|
|
}
|
|
|
|
impl ServiceState {
|
|
fn new() -> Self {
|
|
let fontmap = pangocairo::FontMap::new();
|
|
let context = fontmap.create_context();
|
|
// XXX: may need a different language sometimes
|
|
context.set_language(Some(&pango::Language::from_string("en_US")));
|
|
// XXX: May need a different direction
|
|
context.set_base_dir(pango::Direction::Ltr);
|
|
Self {
|
|
windows: VecDeque::new(),
|
|
current_window: usize::MAX,
|
|
pango_context: context,
|
|
video_width: 0,
|
|
video_height: 0,
|
|
}
|
|
}
|
|
|
|
fn window_mut(&mut self, id: usize) -> Option<&mut Window> {
|
|
self.windows
|
|
.iter_mut()
|
|
.find(|window| window.define.window_id as usize == id)
|
|
}
|
|
|
|
fn define_window(&mut self, args: &DefineWindowArgs) {
|
|
if let Some(window) = self.window_mut(args.window_id as usize) {
|
|
if &window.define != args {
|
|
// we only change these if they are different from the previous define_window
|
|
// command
|
|
window.attrs = args.window_attributes();
|
|
window.pen_attrs = args.pen_attributes();
|
|
window.pen_color = args.pen_color();
|
|
}
|
|
window.define = *args;
|
|
window.recalculate_window_position();
|
|
} else {
|
|
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,
|
|
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);
|
|
};
|
|
self.current_window = args.window_id as usize;
|
|
}
|
|
|
|
fn set_current_window(&mut self, window_id: u8) {
|
|
self.current_window = window_id as usize;
|
|
}
|
|
|
|
fn clear_windows(&mut self, args: &WindowBits) {
|
|
for window in self.windows.iter_mut() {
|
|
if (WindowBits::from_window_id(window.define.window_id) & *args) != WindowBits::NONE {
|
|
window.pen_location = SetPenLocationArgs::default();
|
|
window.lines.clear();
|
|
window.rectangle = None;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn delete_windows(&mut self, args: &WindowBits) {
|
|
self.windows.retain(|window| {
|
|
(WindowBits::from_window_id(window.define.window_id) & *args) == WindowBits::NONE
|
|
});
|
|
}
|
|
|
|
fn display_windows(&mut self, args: &WindowBits) {
|
|
for window in self.windows.iter_mut() {
|
|
if (WindowBits::from_window_id(window.define.window_id) & *args) != WindowBits::NONE {
|
|
window.visible = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn hide_windows(&mut self, args: &WindowBits) {
|
|
for window in self.windows.iter_mut() {
|
|
if (WindowBits::from_window_id(window.define.window_id) & *args) != WindowBits::NONE {
|
|
window.visible = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn toggle_windows(&mut self, args: &WindowBits) {
|
|
for window in self.windows.iter_mut() {
|
|
if (WindowBits::from_window_id(window.define.window_id) & *args) != WindowBits::NONE {
|
|
window.visible = !window.visible;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn set_window_attributes(&mut self, attrs: &SetWindowAttributesArgs) {
|
|
let Some(window) = self.window_mut(self.current_window) else {
|
|
return;
|
|
};
|
|
if &window.attrs != attrs {
|
|
window.lines.clear();
|
|
window.attrs = *attrs;
|
|
window.rectangle = None;
|
|
}
|
|
}
|
|
|
|
fn set_pen_attributes(&mut self, attrs: &SetPenAttributesArgs) {
|
|
let Some(window) = self.window_mut(self.current_window) else {
|
|
return;
|
|
};
|
|
window.pen_attrs = *attrs;
|
|
}
|
|
|
|
fn set_pen_color(&mut self, color: &SetPenColorArgs) {
|
|
let Some(window) = self.window_mut(self.current_window) else {
|
|
return;
|
|
};
|
|
window.pen_color = *color;
|
|
}
|
|
|
|
fn set_pen_location(&mut self, location: &SetPenLocationArgs) {
|
|
let Some(window) = self.window_mut(self.current_window) else {
|
|
return;
|
|
};
|
|
window.pen_location = *location;
|
|
}
|
|
|
|
fn reset(&mut self) {
|
|
*self = Self::new();
|
|
}
|
|
|
|
fn handle_code(&mut self, code: &Code) {
|
|
match code {
|
|
Code::DefineWindow(args) => self.define_window(args),
|
|
Code::SetCurrentWindow0 => self.set_current_window(0),
|
|
Code::SetCurrentWindow1 => self.set_current_window(1),
|
|
Code::SetCurrentWindow2 => self.set_current_window(2),
|
|
Code::SetCurrentWindow3 => self.set_current_window(3),
|
|
Code::SetCurrentWindow4 => self.set_current_window(4),
|
|
Code::SetCurrentWindow5 => self.set_current_window(5),
|
|
Code::SetCurrentWindow6 => self.set_current_window(6),
|
|
Code::SetCurrentWindow7 => self.set_current_window(7),
|
|
Code::ClearWindows(args) => self.clear_windows(args),
|
|
Code::DeleteWindows(args) => self.delete_windows(args),
|
|
Code::DisplayWindows(args) => self.display_windows(args),
|
|
Code::HideWindows(args) => self.hide_windows(args),
|
|
Code::ToggleWindows(args) => self.toggle_windows(args),
|
|
Code::SetWindowAttributes(args) => self.set_window_attributes(args),
|
|
Code::SetPenAttributes(args) => self.set_pen_attributes(args),
|
|
Code::SetPenColor(args) => self.set_pen_color(args),
|
|
Code::SetPenLocation(args) => self.set_pen_location(args),
|
|
Code::BS => self.backspace(),
|
|
Code::CR => self.carriage_return(),
|
|
Code::FF => {
|
|
self.clear_windows(&WindowBits::from_window_id(self.current_window as u8));
|
|
self.set_pen_location(&SetPenLocationArgs { row: 0, column: 0 });
|
|
}
|
|
Code::ETX => (),
|
|
Code::HCR => self.horizontal_carriage_return(),
|
|
Code::Reset => self.reset(),
|
|
_ => {
|
|
if let Some(ch) = code.char() {
|
|
self.push_char(ch);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn push_char(&mut self, ch: char) {
|
|
let Some(window) = self.window_mut(self.current_window) else {
|
|
return;
|
|
};
|
|
window.push_char(ch);
|
|
}
|
|
|
|
fn backspace(&mut self) {
|
|
let Some(window) = self.window_mut(self.current_window) else {
|
|
return;
|
|
};
|
|
window.backspace();
|
|
}
|
|
|
|
fn carriage_return(&mut self) {
|
|
let Some(window) = self.window_mut(self.current_window) else {
|
|
return;
|
|
};
|
|
window.carriage_return();
|
|
}
|
|
|
|
fn horizontal_carriage_return(&mut self) {
|
|
let Some(window) = self.window_mut(self.current_window) else {
|
|
return;
|
|
};
|
|
window.horizontal_carriage_return();
|
|
}
|
|
|
|
fn set_video_size(&mut self, video_width: u32, video_height: u32) {
|
|
for window in self.windows.iter_mut() {
|
|
window.set_video_size(video_width, video_height);
|
|
}
|
|
self.video_width = video_width;
|
|
self.video_height = video_height;
|
|
}
|
|
}
|
|
|
|
fn color_value_as_u16(val: ColorValue) -> u16 {
|
|
match val {
|
|
ColorValue::None => 0,
|
|
ColorValue::OneThird => u16::MAX / 3,
|
|
ColorValue::TwoThirds => u16::MAX / 3 * 2,
|
|
ColorValue::Full => u16::MAX,
|
|
}
|
|
}
|
|
|
|
fn opacity_as_u16(val: Opacity) -> u16 {
|
|
match val {
|
|
Opacity::Transparent => 0,
|
|
Opacity::Translucent => u16::MAX / 3,
|
|
// FIXME
|
|
Opacity::Flash => u16::MAX / 3 * 2,
|
|
Opacity::Solid => u16::MAX,
|
|
}
|
|
}
|
|
|
|
fn pango_foreground_color_from_708(args: &SetPenColorArgs) -> pango::AttrColor {
|
|
pango::AttrColor::new_foreground(
|
|
color_value_as_u16(args.foreground_color.r),
|
|
color_value_as_u16(args.foreground_color.g),
|
|
color_value_as_u16(args.foreground_color.b),
|
|
)
|
|
}
|
|
|
|
fn pango_foreground_opacity_from_708(args: &SetPenColorArgs) -> pango::AttrInt {
|
|
pango::AttrInt::new_foreground_alpha(opacity_as_u16(args.foreground_opacity))
|
|
}
|
|
|
|
fn pango_background_color_from_708(args: &SetPenColorArgs) -> pango::AttrColor {
|
|
pango::AttrColor::new_foreground(
|
|
color_value_as_u16(args.background_color.r),
|
|
color_value_as_u16(args.background_color.g),
|
|
color_value_as_u16(args.background_color.b),
|
|
)
|
|
}
|
|
|
|
fn pango_background_opacity_from_708(args: &SetPenColorArgs) -> pango::AttrInt {
|
|
pango::AttrInt::new_background_alpha(opacity_as_u16(args.background_opacity))
|
|
}
|
|
|
|
#[derive(Debug, Default, PartialEq, Eq)]
|
|
struct Dimensions {
|
|
w: u32,
|
|
h: u32,
|
|
}
|
|
|
|
pub enum Align {
|
|
First,
|
|
Center,
|
|
Last,
|
|
}
|
|
|
|
impl Align {
|
|
fn horizontal_from_anchor(anchor: Anchor) -> Self {
|
|
match anchor {
|
|
Anchor::TopLeft | Anchor::CenterLeft | Anchor::BottomLeft => Self::First,
|
|
Anchor::TopMiddle | Anchor::CenterMiddle | Anchor::BottomMiddle => Self::Center,
|
|
Anchor::TopRight | Anchor::CenterRight | Anchor::BottomRight => Self::Last,
|
|
_ => Self::Center,
|
|
}
|
|
}
|
|
|
|
fn vertical_from_anchor(anchor: Anchor) -> Self {
|
|
match anchor {
|
|
Anchor::TopLeft | Anchor::TopMiddle | Anchor::TopRight => Self::First,
|
|
Anchor::CenterLeft | Anchor::CenterMiddle | Anchor::CenterRight => Self::Center,
|
|
Anchor::BottomLeft | Anchor::BottomMiddle | Anchor::BottomRight => Self::Last,
|
|
_ => Self::Last,
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Window {
|
|
visible: bool,
|
|
define: DefineWindowArgs,
|
|
attrs: SetWindowAttributesArgs,
|
|
pen_attrs: SetPenAttributesArgs,
|
|
pen_color: SetPenColorArgs,
|
|
pen_location: SetPenLocationArgs,
|
|
lines: VecDeque<WindowLine>,
|
|
|
|
window_position: Dimensions,
|
|
video_dims: Dimensions,
|
|
window_dims: Dimensions,
|
|
max_layout_dims: Dimensions,
|
|
rectangle: Option<gst_video::VideoOverlayRectangle>,
|
|
layout: pango::Layout,
|
|
}
|
|
|
|
impl Window {
|
|
fn dump(&self) {
|
|
for line in self.lines.iter() {
|
|
let mut string = line.no.to_string();
|
|
string.push(' ');
|
|
for cell in line.line.iter() {
|
|
string.push(cell.character.unwrap_or(' '));
|
|
}
|
|
string.push('|');
|
|
gst::trace!(CAT, "dump: {string}");
|
|
}
|
|
}
|
|
|
|
fn ensure_cell(&mut self, row: usize, column: usize) {
|
|
let line = if let Some(line) = self.lines.iter_mut().find(|line| line.no == row) {
|
|
line
|
|
} else {
|
|
self.lines.push_back(WindowLine {
|
|
no: row,
|
|
line: VecDeque::new(),
|
|
});
|
|
self.lines.back_mut().unwrap()
|
|
};
|
|
while line.line.len() <= column {
|
|
line.line
|
|
.push_back(Cell::new_empty(self.pen_attrs, self.pen_color));
|
|
}
|
|
}
|
|
|
|
fn cell_mut(&mut self, row: usize, column: usize) -> Option<&mut Cell> {
|
|
self.lines
|
|
.iter_mut()
|
|
.find(|line| line.no == row)
|
|
.and_then(|line| line.line.get_mut(column))
|
|
}
|
|
|
|
fn backspace(&mut self) {
|
|
match self.attrs.print_direction {
|
|
Direction::LeftToRight => {
|
|
self.pen_location.column = self.pen_location.column.max(1) - 1;
|
|
}
|
|
Direction::RightToLeft => {
|
|
self.pen_location.column = (self.pen_location.column + 1).min(self.column_count());
|
|
}
|
|
Direction::TopToBottom => {
|
|
self.pen_location.row = self.pen_location.row.max(1) - 1;
|
|
}
|
|
Direction::BottomToTop => {
|
|
self.pen_location.row = (self.pen_location.row + 1).min(self.row_count());
|
|
}
|
|
}
|
|
self.ensure_cell(
|
|
self.pen_location.row as usize,
|
|
self.pen_location.column as usize,
|
|
);
|
|
let cell = self
|
|
.cell_mut(
|
|
self.pen_location.row as usize,
|
|
self.pen_location.column as usize,
|
|
)
|
|
.unwrap();
|
|
if cell.character.take().is_some() {
|
|
self.rectangle.take();
|
|
}
|
|
}
|
|
|
|
fn row_count(&self) -> u8 {
|
|
self.define.row_count + 1
|
|
}
|
|
|
|
fn column_count(&self) -> u8 {
|
|
self.define.column_count + 1
|
|
}
|
|
|
|
fn move_to_line_beginning(&mut self) {
|
|
match self.attrs.print_direction {
|
|
Direction::LeftToRight => {
|
|
self.pen_location.column = 0;
|
|
}
|
|
Direction::RightToLeft => {
|
|
self.pen_location.column = self.define.column_count;
|
|
}
|
|
Direction::TopToBottom => {
|
|
self.pen_location.row = 0;
|
|
}
|
|
Direction::BottomToTop => {
|
|
self.pen_location.row = self.row_count();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn scroll_top_to_bottom(&mut self) {
|
|
if self.pen_location.row == 0 {
|
|
let row_count = self.row_count() as usize;
|
|
self.lines
|
|
.retain(|line| (0..=row_count - 1).contains(&line.no));
|
|
for line in self.lines.iter_mut() {
|
|
line.no += 1;
|
|
}
|
|
} else {
|
|
self.pen_location.row -= 1;
|
|
}
|
|
}
|
|
|
|
fn scroll_bottom_to_top(&mut self) {
|
|
if self.pen_location.row >= self.define.row_count {
|
|
let row_count = self.row_count() as usize;
|
|
self.lines.retain(|line| (1..=row_count).contains(&line.no));
|
|
for line in self.lines.iter_mut() {
|
|
line.no -= 1
|
|
}
|
|
} else {
|
|
self.pen_location.row += 1;
|
|
}
|
|
}
|
|
|
|
fn scroll_left_to_right(&mut self) {
|
|
if self.pen_location.column == 0 {
|
|
gst::warning!(CAT, "Unsupported scroll direction left-to-right");
|
|
} else {
|
|
self.pen_location.column -= 1;
|
|
}
|
|
}
|
|
|
|
fn scroll_right_to_left(&mut self) {
|
|
if self.pen_location.column >= self.column_count() {
|
|
gst::warning!(CAT, "Unsupported scroll direction right-to-left");
|
|
} else {
|
|
self.pen_location.column += 1;
|
|
}
|
|
}
|
|
|
|
fn carriage_return(&mut self) {
|
|
match (self.attrs.print_direction, self.attrs.scroll_direction) {
|
|
(Direction::LeftToRight, Direction::TopToBottom) => {
|
|
self.scroll_top_to_bottom();
|
|
self.move_to_line_beginning();
|
|
}
|
|
(Direction::LeftToRight, Direction::BottomToTop) => {
|
|
self.scroll_bottom_to_top();
|
|
self.move_to_line_beginning();
|
|
}
|
|
(Direction::RightToLeft, Direction::TopToBottom) => {
|
|
self.scroll_top_to_bottom();
|
|
self.move_to_line_beginning();
|
|
}
|
|
(Direction::RightToLeft, Direction::BottomToTop) => {
|
|
self.scroll_bottom_to_top();
|
|
self.move_to_line_beginning();
|
|
}
|
|
(Direction::TopToBottom, Direction::LeftToRight) => {
|
|
self.scroll_left_to_right();
|
|
self.move_to_line_beginning();
|
|
}
|
|
(Direction::TopToBottom, Direction::RightToLeft) => {
|
|
self.scroll_right_to_left();
|
|
self.move_to_line_beginning();
|
|
}
|
|
(Direction::BottomToTop, Direction::LeftToRight) => {
|
|
self.scroll_left_to_right();
|
|
self.move_to_line_beginning();
|
|
}
|
|
(Direction::BottomToTop, Direction::RightToLeft) => {
|
|
self.scroll_right_to_left();
|
|
self.move_to_line_beginning();
|
|
}
|
|
// all other variants invalid
|
|
(print, scroll) => {
|
|
gst::warning!(
|
|
CAT,
|
|
"Unspecified print direction ({print:?}) and scroll direction ({scroll:?})"
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
gst::trace!(
|
|
CAT,
|
|
"carriage return after position {},{}",
|
|
self.pen_location.row,
|
|
self.pen_location.column
|
|
);
|
|
self.rectangle.take();
|
|
}
|
|
|
|
fn horizontal_carriage_return(&mut self) {
|
|
let min_row;
|
|
let max_row;
|
|
let min_column;
|
|
let max_column;
|
|
match self.attrs.print_direction {
|
|
Direction::LeftToRight => {
|
|
min_row = self.pen_location.row;
|
|
max_row = self.pen_location.row;
|
|
max_column = self.pen_location.column;
|
|
min_column = 0;
|
|
self.pen_location.column = 0;
|
|
}
|
|
Direction::RightToLeft => {
|
|
min_row = self.pen_location.row;
|
|
max_row = self.pen_location.row;
|
|
min_column = self.pen_location.column;
|
|
max_column = self.row_count();
|
|
self.pen_location.column = self.row_count();
|
|
}
|
|
Direction::TopToBottom => {
|
|
min_column = self.pen_location.column;
|
|
max_column = self.pen_location.column;
|
|
min_row = self.pen_location.row;
|
|
max_row = 0;
|
|
self.pen_location.row = 0;
|
|
}
|
|
Direction::BottomToTop => {
|
|
min_column = self.pen_location.column;
|
|
max_column = self.pen_location.column;
|
|
min_row = self.pen_location.row;
|
|
max_row = self.column_count();
|
|
self.pen_location.row = self.column_count();
|
|
}
|
|
}
|
|
for row in min_row..=max_row {
|
|
for column in min_column..=max_column {
|
|
self.ensure_cell(row as usize, column as usize);
|
|
let cell = self.cell_mut(row as usize, column as usize).unwrap();
|
|
cell.character = None;
|
|
}
|
|
}
|
|
self.rectangle.take();
|
|
}
|
|
|
|
fn push_char(&mut self, ch: char) {
|
|
if self.pen_location.row > self.row_count() {
|
|
gst::warning!(
|
|
CAT,
|
|
"row {} outside configured window row count {}",
|
|
self.pen_location.row,
|
|
self.row_count()
|
|
);
|
|
return;
|
|
}
|
|
if self.pen_location.column > self.column_count() {
|
|
gst::warning!(
|
|
CAT,
|
|
"column {} outside configured window column count {}",
|
|
self.pen_location.column,
|
|
self.column_count()
|
|
);
|
|
return;
|
|
}
|
|
gst::trace!(
|
|
CAT,
|
|
"push char \'{ch}\' at row {} column {}",
|
|
self.pen_location.row,
|
|
self.pen_location.column
|
|
);
|
|
self.ensure_cell(
|
|
self.pen_location.row as usize,
|
|
self.pen_location.column as usize,
|
|
);
|
|
let cell = self
|
|
.cell_mut(
|
|
self.pen_location.row as usize,
|
|
self.pen_location.column as usize,
|
|
)
|
|
.unwrap();
|
|
cell.character = Some(ch);
|
|
self.rectangle.take();
|
|
|
|
match self.attrs.print_direction {
|
|
Direction::LeftToRight => {
|
|
self.pen_location.column = (self.pen_location.column + 1).min(self.column_count());
|
|
}
|
|
Direction::RightToLeft => {
|
|
self.pen_location.column = self.pen_location.column.max(1) - 1;
|
|
}
|
|
Direction::TopToBottom => {
|
|
self.pen_location.row = (self.pen_location.row + 1).min(self.row_count());
|
|
}
|
|
Direction::BottomToTop => {
|
|
self.pen_location.row = self.pen_location.row.max(1) - 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn recalculate_window_position(&mut self) {
|
|
self.rectangle.take();
|
|
|
|
// XXX: may need a better implementation for 'skinny' (horizontal or vertical) output
|
|
// sizes.
|
|
|
|
let (max_layout_width, max_layout_height) =
|
|
recalculate_pango_layout(&self.layout, self.video_dims.w, self.video_dims.h);
|
|
self.max_layout_dims = Dimensions {
|
|
w: max_layout_width as u32,
|
|
h: max_layout_height as u32,
|
|
};
|
|
|
|
let char_width = max_layout_width as u32 / 32;
|
|
let char_height = max_layout_height as u32 / 15;
|
|
let height = self.row_count() as u32 * char_height;
|
|
let width = self.column_count() as u32 * char_width;
|
|
self.window_dims = Dimensions {
|
|
w: width,
|
|
h: height,
|
|
};
|
|
|
|
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,
|
|
};
|
|
|
|
self.window_position = if self.define.relative_positioning {
|
|
let halign = Align::horizontal_from_anchor(self.define.anchor_point);
|
|
let valign = Align::vertical_from_anchor(self.define.anchor_point);
|
|
let x = safe_area
|
|
.w
|
|
.mul_div_round(self.define.anchor_horizontal.min(100) as u32, 100)
|
|
.unwrap();
|
|
let x = padding.w
|
|
+ match halign {
|
|
Align::First => x,
|
|
Align::Center => x.max(self.max_layout_dims.w / 2) - self.max_layout_dims.w / 2,
|
|
Align::Last => x.max(self.window_dims.w) - self.window_dims.w,
|
|
};
|
|
let y = safe_area
|
|
.h
|
|
.mul_div_round(self.define.anchor_vertical.min(100) as u32, 100)
|
|
.unwrap();
|
|
let y = padding.h
|
|
+ match valign {
|
|
Align::First => y,
|
|
Align::Center => y.max(self.max_layout_dims.h / 2),
|
|
Align::Last => y.max(self.window_dims.h) - self.window_dims.h,
|
|
};
|
|
Dimensions { w: x, h: y }
|
|
} else {
|
|
// FIXME
|
|
gst::fixme!(CAT, "Handle non-relative-positioning");
|
|
padding
|
|
};
|
|
|
|
gst::trace!(
|
|
CAT,
|
|
"char sizes {char_width}x{char_height}, row/columns {}x{}, safe area {:?} window dimensions: {:?}, window position: {:?}, max layout {:?}, define {:?}",
|
|
self.row_count(),
|
|
self.column_count(),
|
|
safe_area,
|
|
self.window_dims,
|
|
self.window_position,
|
|
self.max_layout_dims,
|
|
self.define,
|
|
);
|
|
}
|
|
|
|
fn set_video_size(&mut self, video_width: u32, video_height: u32) {
|
|
let new_dims = Dimensions {
|
|
w: video_width,
|
|
h: video_height,
|
|
};
|
|
if new_dims == self.video_dims {
|
|
return;
|
|
}
|
|
self.video_dims = new_dims;
|
|
|
|
self.recalculate_window_position();
|
|
}
|
|
|
|
fn generate_rectangle(&mut self) -> Option<gst_video::VideoOverlayRectangle> {
|
|
if !self.visible {
|
|
return None;
|
|
}
|
|
|
|
if self.rectangle.is_some() {
|
|
return self.rectangle.clone();
|
|
}
|
|
self.dump();
|
|
|
|
// 1. generate the pango layout for the text
|
|
let mut text = String::new();
|
|
let attrs = pango::AttrList::new();
|
|
let mut last_color = None;
|
|
let mut last_attrs = None;
|
|
let mut background_color_attr = pango::AttrColor::new_background(0, 0, 0);
|
|
let mut background_opacity_attr = pango::AttrInt::new_background_alpha(0);
|
|
let mut foreground_color_attr =
|
|
pango::AttrColor::new_background(u16::MAX, u16::MAX, u16::MAX);
|
|
let mut foreground_opacity_attr = pango::AttrInt::new_background_alpha(u16::MAX);
|
|
let mut underline_attr = pango::AttrInt::new_underline(pango::Underline::None);
|
|
let mut italic_attr = None::<pango::AttrInt>;
|
|
let mut last_row = 0;
|
|
for line in self.lines.iter() {
|
|
for _ in 0..line.no - last_row {
|
|
text.push('\n');
|
|
}
|
|
last_row = line.no;
|
|
for c in line.line.iter() {
|
|
// XXX: Need to double check these indices with more complicated text characters
|
|
let start_idx = text.len();
|
|
if last_color.map(|col| col != c.pen_color).unwrap_or(true) {
|
|
background_color_attr.set_end_index(start_idx as u32);
|
|
attrs.insert(background_color_attr.clone());
|
|
background_color_attr = pango_background_color_from_708(&c.pen_color);
|
|
background_color_attr.set_start_index(start_idx as u32);
|
|
|
|
background_opacity_attr.set_end_index(start_idx as u32);
|
|
attrs.insert(background_opacity_attr.clone());
|
|
background_opacity_attr = pango_background_opacity_from_708(&c.pen_color);
|
|
background_opacity_attr.set_start_index(start_idx as u32);
|
|
|
|
foreground_color_attr.set_end_index(start_idx as u32);
|
|
attrs.insert(foreground_color_attr.clone());
|
|
foreground_color_attr = pango_foreground_color_from_708(&c.pen_color);
|
|
foreground_color_attr.set_start_index(start_idx as u32);
|
|
|
|
foreground_opacity_attr.set_end_index(start_idx as u32);
|
|
attrs.insert(foreground_opacity_attr.clone());
|
|
foreground_opacity_attr = pango_foreground_opacity_from_708(&c.pen_color);
|
|
foreground_opacity_attr.set_start_index(start_idx as u32);
|
|
|
|
last_color = Some(c.pen_color);
|
|
}
|
|
if last_attrs.map(|attrs| attrs != c.pen_attrs).unwrap_or(true) {
|
|
underline_attr.set_end_index(start_idx as u32);
|
|
attrs.insert(underline_attr.clone());
|
|
let underline_type = if c.pen_attrs.underline {
|
|
pango::Underline::Single
|
|
} else {
|
|
pango::Underline::None
|
|
};
|
|
underline_attr = pango::AttrInt::new_underline(underline_type);
|
|
underline_attr.set_start_index(start_idx as u32);
|
|
|
|
if !c.pen_attrs.italics {
|
|
if let Some(mut italic) = italic_attr.take() {
|
|
italic.set_end_index(start_idx as u32);
|
|
attrs.insert(italic.clone());
|
|
}
|
|
} else if c.pen_attrs.italics && italic_attr.is_none() {
|
|
let mut attr = pango::AttrInt::new_style(pango::Style::Italic);
|
|
attr.set_start_index(start_idx as u32);
|
|
italic_attr = Some(attr);
|
|
}
|
|
|
|
last_attrs = Some(c.pen_attrs);
|
|
}
|
|
|
|
let Some(character) = c.character else {
|
|
text.push(' ');
|
|
continue;
|
|
};
|
|
text.push(character);
|
|
}
|
|
}
|
|
let start_idx = text.len();
|
|
background_color_attr.set_end_index(start_idx as u32);
|
|
attrs.insert(background_color_attr.clone());
|
|
background_opacity_attr.set_end_index(start_idx as u32);
|
|
attrs.insert(background_opacity_attr.clone());
|
|
foreground_color_attr.set_end_index(start_idx as u32);
|
|
attrs.insert(foreground_color_attr.clone());
|
|
foreground_opacity_attr.set_end_index(start_idx as u32);
|
|
attrs.insert(foreground_opacity_attr.clone());
|
|
underline_attr.set_end_index(start_idx as u32);
|
|
attrs.insert(underline_attr.clone());
|
|
if let Some(mut italic) = italic_attr {
|
|
italic.set_end_index(start_idx as u32);
|
|
attrs.insert(italic);
|
|
}
|
|
|
|
self.layout.set_text(&text);
|
|
self.layout.set_attributes(Some(&attrs));
|
|
let (_ink_rect, logical_rect) = self.layout.extents();
|
|
let height = logical_rect.height() / pango::SCALE;
|
|
let width = logical_rect.width() / pango::SCALE;
|
|
|
|
// 2. render text and window
|
|
let render_buffer = || -> Result<gst::Buffer, anyhow::Error> {
|
|
let mut buffer = gst::Buffer::with_size((width * height) as usize * 4)?;
|
|
|
|
gst_video::VideoMeta::add(
|
|
buffer.get_mut().unwrap(),
|
|
gst_video::VideoFrameFlags::empty(),
|
|
#[cfg(target_endian = "little")]
|
|
gst_video::VideoFormat::Bgra,
|
|
#[cfg(target_endian = "big")]
|
|
gst_video::VideoFormat::Argb,
|
|
width as u32,
|
|
height as u32,
|
|
)?;
|
|
let buffer = buffer.into_mapped_buffer_writable().unwrap();
|
|
|
|
// Pass ownership of the buffer to the cairo surface but keep around
|
|
// a raw pointer so we can later retrieve it again when the surface
|
|
// is done
|
|
let buffer_ptr = buffer.buffer().as_ptr();
|
|
let surface = cairo::ImageSurface::create_for_data(
|
|
buffer,
|
|
cairo::Format::ARgb32,
|
|
width,
|
|
height,
|
|
width * 4,
|
|
)?;
|
|
|
|
let cr = cairo::Context::new(&surface)?;
|
|
|
|
// Clear background
|
|
cr.set_operator(cairo::Operator::Source);
|
|
cr.set_source_rgba(0.0, 0.0, 0.0, 0.0);
|
|
cr.paint()?;
|
|
|
|
// Render text outline
|
|
cr.save()?;
|
|
cr.set_operator(cairo::Operator::Over);
|
|
|
|
cr.set_source_rgba(0.0, 0.0, 0.0, 1.0);
|
|
|
|
pangocairo::functions::layout_path(&cr, &self.layout);
|
|
cr.stroke()?;
|
|
cr.restore()?;
|
|
|
|
// Render text
|
|
cr.save()?;
|
|
cr.set_source_rgba(255.0, 255.0, 255.0, 1.0);
|
|
|
|
pangocairo::functions::show_layout(&cr, &self.layout);
|
|
|
|
cr.restore()?;
|
|
drop(cr);
|
|
|
|
// Safety: The surface still owns a mutable reference to the buffer but our reference
|
|
// to the surface here is the last one. After dropping the surface the buffer would be
|
|
// freed, so we keep an additional strong reference here before dropping the surface,
|
|
// which is then returned. As such it's guaranteed that nothing is using the buffer
|
|
// anymore mutably.
|
|
unsafe {
|
|
assert_eq!(
|
|
cairo::ffi::cairo_surface_get_reference_count(surface.to_raw_none()),
|
|
1
|
|
);
|
|
let buffer = glib::translate::from_glib_none(buffer_ptr);
|
|
drop(surface);
|
|
Ok(buffer)
|
|
}
|
|
};
|
|
|
|
let buffer = match render_buffer() {
|
|
Ok(buffer) => buffer,
|
|
Err(e) => {
|
|
self.dump();
|
|
gst::error!(CAT, "Failed to render buffer: \"{e}\"");
|
|
return None;
|
|
}
|
|
};
|
|
gst::trace!(
|
|
CAT,
|
|
"sizes: video {:?}, window {:?} overlay {}x{}",
|
|
self.video_dims,
|
|
self.window_dims,
|
|
width,
|
|
height
|
|
);
|
|
|
|
// 3. generate overlay rectangle
|
|
// FIXME: use the window location values to place the overlay
|
|
let ret = Some(gst_video::VideoOverlayRectangle::new_raw(
|
|
&buffer,
|
|
self.window_position.w as i32,
|
|
self.window_position.h as i32,
|
|
width as u32,
|
|
height as u32,
|
|
gst_video::VideoOverlayFormatFlags::PREMULTIPLIED_ALPHA,
|
|
));
|
|
self.rectangle = ret.clone();
|
|
ret
|
|
}
|
|
}
|
|
|
|
struct WindowLine {
|
|
no: usize,
|
|
line: VecDeque<Cell>,
|
|
}
|
|
|
|
impl PartialOrd for WindowLine {
|
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
|
Some(self.cmp(other))
|
|
}
|
|
}
|
|
|
|
impl Ord for WindowLine {
|
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
|
self.no.cmp(&other.no)
|
|
}
|
|
}
|
|
|
|
impl PartialEq for WindowLine {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.no == other.no
|
|
}
|
|
}
|
|
|
|
impl Eq for WindowLine {}
|
|
|
|
struct Cell {
|
|
character: Option<char>,
|
|
pen_attrs: SetPenAttributesArgs,
|
|
pen_color: SetPenColorArgs,
|
|
}
|
|
|
|
impl Cell {
|
|
fn new_empty(attrs: SetPenAttributesArgs, color: SetPenColorArgs) -> Self {
|
|
Self {
|
|
character: None,
|
|
pen_attrs: attrs,
|
|
pen_color: color,
|
|
}
|
|
}
|
|
}
|