From 1de32229ba77c975ecfc0d008bac62079c863ec0 Mon Sep 17 00:00:00 2001 From: SeaDve Date: Tue, 23 Aug 2022 17:25:05 +0800 Subject: [PATCH 01/46] build: Enable gst-plugins-ugly checks --- build-aux/io.github.seadve.Kooha.Devel.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/build-aux/io.github.seadve.Kooha.Devel.json b/build-aux/io.github.seadve.Kooha.Devel.json index f635424cb..7de85274e 100644 --- a/build-aux/io.github.seadve.Kooha.Devel.json +++ b/build-aux/io.github.seadve.Kooha.Devel.json @@ -54,10 +54,7 @@ "-Ddoc=disabled", "-Dorc=disabled", "-Dnls=disabled", - "-Dtests=disabled", - "-Dgobject-cast-checks=disabled", - "-Dglib-asserts=disabled", - "-Dglib-checks=disabled" + "-Dtests=disabled" ], "sources": [ { From c3259552c24419b244b4dc89e3e688e5d1c9d707 Mon Sep 17 00:00:00 2001 From: SeaDve Date: Tue, 23 Aug 2022 17:34:13 +0800 Subject: [PATCH 02/46] refactor: Use encodebin --- Cargo.lock | 45 ++++ Cargo.toml | 2 + meson.build | 2 +- src/pipeline_builder.rs | 530 +++++++++++++++++++++++----------------- src/recording.rs | 9 +- 5 files changed, 351 insertions(+), 237 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bfc48a6eb..c67b24227 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -536,6 +536,20 @@ dependencies = [ "thiserror", ] +[[package]] +name = "gstreamer-audio-sys" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a34258fb53c558c0f41dad194037cbeaabf49d347570df11b8bd1c4897cf7d7c" +dependencies = [ + "glib-sys", + "gobject-sys", + "gstreamer-base-sys", + "gstreamer-sys", + "libc", + "system-deps", +] + [[package]] name = "gstreamer-base" version = "0.18.0" @@ -563,6 +577,35 @@ dependencies = [ "system-deps", ] +[[package]] +name = "gstreamer-pbutils" +version = "0.18.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330684c49f79775d7acce8bef5a7a7475f02374c9c6cead39ced3ad423fc8ea9" +dependencies = [ + "bitflags", + "glib", + "gstreamer", + "gstreamer-pbutils-sys", + "libc", + "thiserror", +] + +[[package]] +name = "gstreamer-pbutils-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f79839066fbcc6d1a8690b2f85d5cc5cdc0984f36d4054f5cc67a7ad3ab72d" +dependencies = [ + "glib-sys", + "gobject-sys", + "gstreamer-audio-sys", + "gstreamer-sys", + "gstreamer-video-sys", + "libc", + "system-deps", +] + [[package]] name = "gstreamer-sys" version = "0.18.0" @@ -704,6 +747,8 @@ dependencies = [ "gsettings-macro", "gst-plugin-gif", "gstreamer", + "gstreamer-pbutils", + "gstreamer-video", "gtk4", "libadwaita", "libpulse-binding", diff --git a/Cargo.toml b/Cargo.toml index 3bc8cc266..272870a40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,8 @@ gdk-wayland = { package = "gdk4-wayland", version = "0.4.5" } gdk-x11 = { package = "gdk4-x11", version = "0.4.5" } adw = { package = "libadwaita", version = "0.2.0-alpha.2", features = ["v1_2"] } gst = { package = "gstreamer", version = "0.18.2" } +gst_video = { package = "gstreamer-video", version = "0.18.2" } +gst_pbutils = { package = "gstreamer-pbutils", version = "0.18.2" } gst-plugin-gif = "0.8.0" futures-channel = "0.3.19" futures-util = { version = "0.3", default-features = false } diff --git a/meson.build b/meson.build index 8f953250c..d3355e14f 100644 --- a/meson.build +++ b/meson.build @@ -16,7 +16,7 @@ dependency('gio-2.0', version: '>= 2.66') dependency('gtk4', version: '>= 4.4.0') dependency('libadwaita-1', version: '>= 1.2') dependency('gstreamer-1.0', version: '>= 1.18') -dependency('gstreamer-base-1.0', version: '>= 1.18') +dependency('gstreamer-pbutils-1.0', version: '>= 1.18') dependency('gstreamer-plugins-base-1.0', version: '>= 1.18') dependency('libpulse-mainloop-glib', version: '>= 15.0') dependency('libpulse', version: '>= 15.0') diff --git a/src/pipeline_builder.rs b/src/pipeline_builder.rs index 15e71f5bf..e09b599db 100644 --- a/src/pipeline_builder.rs +++ b/src/pipeline_builder.rs @@ -1,12 +1,13 @@ -use anyhow::{anyhow, Context, Result}; +use anyhow::{bail, Context, Result}; +use gst::prelude::*; +use gst_pbutils::prelude::*; use gtk::{ glib, graphene::{Rect, Size}, - prelude::*, }; use std::{ - cmp, env, + cmp, os::unix::io::RawFd, path::{Path, PathBuf}, }; @@ -14,7 +15,13 @@ use std::{ use crate::{screencast_session::Stream, settings::VideoFormat}; const MAX_THREAD_COUNT: u32 = 64; -const GIF_DEFAULT_FRAMERATE: u32 = 15; +const DEFAULT_GIF_FRAMERATE: u32 = 15; + +#[derive(Debug)] +struct SelectAreaContext { + pub coords: Rect, + pub screen_size: Size, +} #[derive(Debug)] #[must_use] @@ -26,8 +33,7 @@ pub struct PipelineBuilder { streams: Vec, speaker_source: Option, mic_source: Option, - coordinates: Option, - actual_screen: Option, + select_area_context: Option, } impl PipelineBuilder { @@ -46,8 +52,7 @@ impl PipelineBuilder { streams, speaker_source: None, mic_source: None, - coordinates: None, - actual_screen: None, + select_area_context: None, } } @@ -61,267 +66,332 @@ impl PipelineBuilder { self } - pub fn coordinates(&mut self, coordinates: Rect) -> &mut Self { - self.coordinates = Some(coordinates); - self - } - - pub fn actual_screen(&mut self, actual_screen: Size) -> &mut Self { - self.actual_screen = Some(actual_screen); + pub fn select_area_context(&mut self, coords: Rect, screen_size: Size) -> &mut Self { + self.select_area_context = Some(SelectAreaContext { + coords, + screen_size, + }); self } - pub fn build(self) -> Result { - let pipeline_string = PipelineAssembler::from_builder(self).assemble()?; - tracing::debug!(?pipeline_string); - - gst::parse_launch_full(&pipeline_string, None, gst::ParseFlags::FATAL_ERRORS) - .map(|element| element.downcast().unwrap()) - .with_context(|| { - format!( - "Failed to parse string into pipeline. string: {}", - pipeline_string - ) - }) - } -} - -struct PipelineAssembler { - builder: PipelineBuilder, -} - -impl PipelineAssembler { - pub fn from_builder(builder: PipelineBuilder) -> Self { - Self { builder } - } + pub fn build(&self) -> Result { + let pipeline = gst::Pipeline::new(None); + + let encodebin = element_factory_make("encodebin")?; + encodebin.set_property("profile", &create_profile(self.format)); + encodebin.set_property("avoid-reencoding", true); + let queue = element_factory_make("queue")?; + let filesink = element_factory_make("filesink")?; + filesink.set_property( + "location", + self.file_path + .to_str() + .context("Could not convert file path to string")?, + ); + + pipeline.add_many(&[&encodebin, &queue, &filesink])?; + gst::Element::link_many(&[&encodebin, &queue, &filesink])?; + + let framerate = match self.format { + VideoFormat::Gif => DEFAULT_GIF_FRAMERATE, + _ => self.framerate, + }; - pub fn assemble(&self) -> Result { - let file_path = self - .builder - .file_path - .to_str() - .ok_or_else(|| anyhow!("Could not convert file_path to string."))?; - - let pipeline_elements = vec![ - self.compositor(), - Some("queue name=queue0".to_string()), - Some("videorate".to_string()), - Some(format!("video/x-raw, framerate={}/1", self.framerate())), - self.videoscale(), - self.videocrop(), - Some("videoconvert chroma-mode=GST_VIDEO_CHROMA_MODE_NONE dither=GST_VIDEO_DITHER_NONE matrix-mode=GST_VIDEO_MATRIX_MODE_OUTPUT_ONLY n-threads=%T".to_string()), - Some("queue".to_string()), - Some(self.videoenc()), - Some("queue".to_string()), - self.muxer(), - Some(format!("filesink name=filesink location=\"{}\"", file_path)), - ]; - - let pipeline_string = pipeline_elements - .into_iter() - .flatten() - .collect::>() - .join(" ! "); - - Ok([ - pipeline_string, - self.pipewiresrc(), - self.pulsesrc().unwrap_or_default(), - ] - .join(" ") - .replace("%T", &ideal_thread_count().to_string())) - } + tracing::debug!(stream_len = ?self.streams.len()); + + let videosrc_bin = match self.streams.len() { + 0 => bail!("Found no streams"), + 1 => single_stream_pipewiresrc_bin( + self.fd, + self.streams.get(0).unwrap(), + framerate, + self.select_area_context.as_ref(), + )?, + _ => { + if self.select_area_context.is_some() { + bail!("Select area is not supported for multiple streams"); + } + + multi_stream_pipewiresrc_bin(self.fd, &self.streams, framerate)? + } + }; - fn compositor(&self) -> Option { - if self.has_single_stream() { - return None; + pipeline.add(&videosrc_bin)?; + videosrc_bin.static_pad("src").unwrap().link( + &encodebin + .request_pad_simple("video_%u") + .context("Failed to request video_%u pad from encodebin")?, + )?; + + let mut audiosrc_bins = Vec::new(); + if let Some(ref device_name) = self.speaker_source { + let pulsesrc_bin = pulsesrc_bin(device_name)?; + audiosrc_bins.push(pulsesrc_bin); + } + if let Some(ref device_name) = self.mic_source { + let pulsesrc_bin = pulsesrc_bin(device_name)?; + audiosrc_bins.push(pulsesrc_bin); } - // This allows us to place the videos side by side with each other, without overlaps. - let mut current_pos = 0; - let compositor_pads: Vec = self - .streams() - .iter() - .enumerate() - .map(|(sink_num, stream)| { - let pad = format!("sink_{}::xpos={}", sink_num, current_pos); - let stream_width = stream.size().unwrap().0; - current_pos += stream_width; - pad - }) - .collect(); - - Some(format!( - "compositor name=comp {}", - compositor_pads.join(" ") - )) - } - - fn pipewiresrc(&self) -> String { - if self.has_single_stream() { - // If there is a single stream, connect pipewiresrc directly to queue0. - let node_id = self.streams()[0].node_id(); - return format!("pipewiresrc fd={} path={} do-timestamp=true keepalive-time=1000 resend-last=true ! video/x-raw ! queue0.", self.fd(), node_id); + for bin in audiosrc_bins { + pipeline.add(&bin)?; + bin.static_pad("src").unwrap().link( + &encodebin + .request_pad_simple("audio_%u") + .context("Failed to request audio_%u pad from encodebin")?, + )?; } - let pipewiresrc_list: Vec = self.streams().iter().map(|stream| { - let node_id = stream.node_id(); - format!("pipewiresrc fd={} path={} do-timestamp=true keepalive-time=1000 resend-last=true ! video/x-raw ! comp.", self.fd(), node_id) - }).collect(); + Ok(pipeline) + } +} - pipewiresrc_list.join(" ") +fn create_profile(video_format: VideoFormat) -> gst_pbutils::EncodingContainerProfile { + // FIXME broken gif and mp4 + + if video_format == VideoFormat::Gif { + let caps = gst::Caps::builder("image/gif").build(); + let video_profile = gst_pbutils::EncodingVideoProfile::builder(&caps) + .presence(0) + .build(); + return gst_pbutils::EncodingContainerProfile::builder(&caps) + .presence(0) + .add_profile(&video_profile) + .build(); } - fn pulsesrc(&self) -> Option { - let audioenc = match self.video_format() { - VideoFormat::Webm | VideoFormat::Mkv | VideoFormat::Mp4 => "opusenc", - VideoFormat::Gif => return None, + // TODO option to force vaapi + // TODO modify element_properties + let video_profile = { + let caps = match video_format { + VideoFormat::Webm => gst::Caps::builder("video/x-vp8").build(), + VideoFormat::Mkv => gst::Caps::builder("video/x-vp8").build(), + VideoFormat::Mp4 => gst::Caps::builder("video/x-h264") + .field("alignment", "au") + .field("stream-format", "avc") + .build(), + VideoFormat::Gif => unreachable!(), + }; + gst_pbutils::EncodingVideoProfile::builder(&caps) + .variable_framerate(false) + .presence(0) + .build() + }; + + let audio_profile = { + let caps = match video_format { + VideoFormat::Webm => gst::Caps::builder("audio/x-opus").build(), + VideoFormat::Mkv => gst::Caps::builder("audio/x-opus").build(), + VideoFormat::Mp4 => gst::Caps::builder("audio/mpeg") + .field("mpegversion", 1) + .field("layer", 3) + .build(), + VideoFormat::Gif => unreachable!(), }; + gst_pbutils::EncodingAudioProfile::builder(&caps) + .presence(0) + .build() + }; + + let container_profile = { + let caps = match video_format { + VideoFormat::Webm => gst::Caps::builder("video/webm").build(), + VideoFormat::Mkv => gst::Caps::builder("video/x-matroska").build(), + VideoFormat::Mp4 => gst::Caps::builder("video/quicktime") + .field("variant", "iso") + .build(), + VideoFormat::Gif => unreachable!(), + }; + gst_pbutils::EncodingContainerProfile::builder(&caps) + .add_profile(&video_profile) + .add_profile(&audio_profile) + .build() + }; - match (self.speaker_source(), self.mic_source()) { - (Some(speaker_source), Some(mic_source)) => { - Some(format!("pulsesrc device=\"{}\" ! queue ! audiomixer name=mix ! {} ! queue ! mux. pulsesrc device=\"{}\" ! queue ! mix.", - speaker_source, - audioenc, - mic_source, - )) - } - (Some(speaker_source), None) => { - Some(format!( - "pulsesrc device=\"{}\" ! {} ! queue ! mux.", - speaker_source, audioenc - )) - } - (None, Some(mic_source)) => { - Some(format!( - "pulsesrc device=\"{}\" ! {} ! queue ! mux.", - mic_source, audioenc - )) - } - (None, None) => None, - } - } + tracing::debug!(suggested_file_extension = ?container_profile.file_extension()); - fn videoscale(&self) -> Option { - if self.builder.coordinates.is_some() { - // We could freely get the first stream because screencast portal won't allow multiple - // sources selection if it is selection mode. Thus, there will be always single stream - // present when we have coordinates. (The same applies with videocrop). - let (width, height) = self.streams()[0].size().unwrap(); - - Some(format!( - "videoscale ! video/x-raw, width={}, height={}", - round_to_even(width), - round_to_even(height) - )) - } else { - None - } - } + container_profile +} - fn videocrop(&self) -> Option { - self.builder.coordinates.map(|ref coords| { - let stream = &self.streams()[0]; - - let actual_screen = self.builder.actual_screen.as_ref().unwrap(); - let (stream_width, stream_height) = stream.size().unwrap(); - - let scale_factor = stream_width as f32 / actual_screen.width(); - let coords = coords.scale(scale_factor, scale_factor); - - let top_crop = coords.y(); - let left_crop = coords.x(); - let right_crop = stream_width as f32 - (coords.width() + coords.x()); - let bottom_crop = stream_height as f32 - (coords.height() + coords.y()); - - // It is a requirement for x264enc to have even resolution. - format!( - "videocrop top={} left={} right={} bottom={}", - round_to_even_f32(top_crop), - round_to_even_f32(left_crop), - round_to_even_f32(right_crop), - round_to_even_f32(bottom_crop) - ) - }) - } +fn element_factory_make(element_name: &str) -> Result { + gst::ElementFactory::make(element_name, None) + .with_context(|| format!("Failed to make element `{}`", element_name)) +} - fn videoenc(&self) -> String { - // TODO consider using encodebin +fn pipewiresrc_with_default(fd: RawFd, path: &str) -> Result { + let src = element_factory_make("pipewiresrc")?; + src.set_property("fd", &fd); + src.set_property("path", path); + src.set_property("do-timestamp", true); + src.set_property("keepalive-time", 1000); + src.set_property("resend-last", true); + Ok(src) +} - let value = env::var("KOOHA_VAAPI").unwrap_or_default(); - let is_use_vaapi = value == "1"; +fn videoconvert_with_default() -> Result { + let conv = element_factory_make("videoconvert")?; + conv.set_property("chroma-mode", gst_video::VideoChromaMode::None); + conv.set_property("dither", gst_video::VideoDitherMethod::None); + conv.set_property("matrix-mode", gst_video::VideoMatrixMode::OutputOnly); + conv.set_property("n-threads", ideal_thread_count()); + Ok(conv) +} - tracing::debug!(?is_use_vaapi); +fn videocrop_compute( + stream_width: i32, + stream_height: i32, + context: &SelectAreaContext, +) -> Result { + let actual_screen = context.screen_size; + + let scale_factor = stream_width as f32 / actual_screen.width(); + let coords = context.coords.scale(scale_factor, scale_factor); + + let top_crop = coords.y(); + let left_crop = coords.x(); + let right_crop = stream_width as f32 - (coords.width() + coords.x()); + let bottom_crop = stream_height as f32 - (coords.height() + coords.y()); + + // x264enc requires even resolution. + let crop = element_factory_make("videocrop")?; + crop.set_property("top", round_to_even_f32(top_crop)); + crop.set_property("left", round_to_even_f32(left_crop)); + crop.set_property("right", round_to_even_f32(right_crop)); + crop.set_property("bottom", round_to_even_f32(bottom_crop)); + Ok(crop) +} - if is_use_vaapi { - match self.video_format() { - VideoFormat::Webm | VideoFormat::Mkv => "vaapivp8enc", // FIXME Improve pipelines - VideoFormat::Mp4 => "vaapih264enc max-qp=17 ! h264parse", - VideoFormat::Gif => "gifenc repeat=-1 speed=30", // FIXME This doesn't really use vaapi - } - } else { - match self.video_format() { - VideoFormat::Webm | VideoFormat::Mkv => "vp8enc max_quantizer=17 cpu-used=16 cq_level=13 deadline=1 static-threshold=100 keyframe-mode=disabled buffer-size=20000 threads=%T", - VideoFormat::Mp4 => "x264enc qp-max=17 speed-preset=superfast threads=%T ! video/x-h264, profile=baseline", - VideoFormat::Gif => "gifenc repeat=-1 speed=30", - } - }.to_string() +/// Creates a bin with a src pad for multiple pipewire streams. +/// +/// pipewiresrc1 -> videorate -> | +/// | -> compositor -> videoconvert -> queue +/// pipewiresrc2 -> videorate -> | +fn multi_stream_pipewiresrc_bin(fd: i32, streams: &[Stream], framerate: u32) -> Result { + let bin = gst::Bin::new(None); + + let compositor = element_factory_make("compositor")?; + let videoconvert = videoconvert_with_default()?; + let queue = element_factory_make("queue")?; + + bin.add_many(&[&compositor, &videoconvert, &queue])?; + gst::Element::link_many(&[&compositor, &videoconvert, &queue])?; + + let videorate_filter = gst::Caps::builder("video/x-raw") + .field("framerate", gst::Fraction::new(framerate as i32, 1)) + .build(); + + let mut last_pos = 0; + for stream in streams { + // TODO maybe put another videoconvert here + + let pipewiresrc = pipewiresrc_with_default(fd, &stream.node_id().to_string())?; + let videorate = element_factory_make("videorate")?; + let videorate_capsfilter = element_factory_make("capsfilter")?; + videorate_capsfilter.set_property("caps", &videorate_filter); + + bin.add_many(&[&pipewiresrc, &videorate, &videorate_capsfilter])?; + gst::Element::link_many(&[&pipewiresrc, &videorate, &videorate_capsfilter])?; + + let compositor_sink_pad = compositor + .request_pad_simple("sink_%u") + .context("Failed to request sink_%u pad from compositor")?; + compositor_sink_pad.set_property("xpos", last_pos); + videorate_capsfilter + .static_pad("src") + .unwrap() + .link(&compositor_sink_pad)?; + + let stream_width = stream.size().unwrap().0; + last_pos += stream_width; } - fn muxer(&self) -> Option { - let video_format = self.video_format(); + let queue_pad = queue.static_pad("src").unwrap(); + bin.add_pad(&gst::GhostPad::with_target(Some("src"), &queue_pad)?)?; - let muxer = match video_format { - VideoFormat::Webm => "webmmux", - VideoFormat::Mkv => "matroskamux", - VideoFormat::Mp4 => "mp4mux", - VideoFormat::Gif => return None, - }; + Ok(bin) +} - Some(format!("{} name=mux", muxer)) +/// Creates a bin with a src pad for a single pipewire stream. +/// +/// No selection: +/// pipewiresrc -> videconvert -> videorate -> queue +/// +/// Has selection: +/// pipewiresrc -> videconvert -> videorate -> videoscale -> videocrop -> queue +fn single_stream_pipewiresrc_bin( + fd: RawFd, + stream: &Stream, + framerate: u32, + select_area_context: Option<&SelectAreaContext>, +) -> Result { + let bin = gst::Bin::new(None); + + let pipewiresrc = pipewiresrc_with_default(fd, &stream.node_id().to_string())?; + let videoconvert = videoconvert_with_default()?; + let videorate = element_factory_make("videorate")?; + let queue = element_factory_make("queue")?; + + bin.add_many(&[&pipewiresrc, &videoconvert, &videorate, &queue])?; + gst::Element::link_many(&[&pipewiresrc, &videoconvert, &videorate])?; + + let videorate_filter = gst::Caps::builder("video/x-raw") + .field("framerate", gst::Fraction::new(framerate as i32, 1)) + .build(); + + if let Some(context) = select_area_context { + let (stream_width, stream_height) = stream.size().context("Stream has no size")?; + + let videoscale = element_factory_make("videoscale")?; + let videocrop = videocrop_compute(stream_width, stream_height, context)?; + + // x264enc requires even resolution. + let videoscale_filter = gst::Caps::builder("video/x-raw") + .field("width", round_to_even(stream_width)) + .field("height", round_to_even(stream_height)) + .build(); + + bin.add_many(&[&videoscale, &videocrop])?; + videorate.link_filtered(&videoscale, &videorate_filter)?; + videoscale.link_filtered(&videocrop, &videoscale_filter)?; + gst::Element::link_many(&[&videocrop, &queue])?; + } else { + videorate.link_filtered(&queue, &videorate_filter)?; } - fn video_format(&self) -> VideoFormat { - self.builder.format - } + let queue_pad = queue.static_pad("src").unwrap(); + bin.add_pad(&gst::GhostPad::with_target(Some("src"), &queue_pad)?)?; - fn framerate(&self) -> u32 { - if self.video_format() == VideoFormat::Gif { - return GIF_DEFAULT_FRAMERATE; - } + Ok(bin) +} - self.builder.framerate - } +/// Creates a bin with a src pad for a pulse audio device +/// +/// pulsesrc -> audioconvert -> queue +fn pulsesrc_bin(device_name: &str) -> Result { + let bin = gst::Bin::new(None); - fn speaker_source(&self) -> Option<&str> { - self.builder.speaker_source.as_deref() - } + let pulsesrc = element_factory_make("pulsesrc")?; + pulsesrc.set_property("device", device_name); + let audioconvert = element_factory_make("audioconvert")?; + let queue = element_factory_make("queue")?; - fn mic_source(&self) -> Option<&str> { - self.builder.mic_source.as_deref() - } + bin.add_many(&[&pulsesrc, &audioconvert, &queue])?; + gst::Element::link_many(&[&pulsesrc, &audioconvert, &queue])?; - fn fd(&self) -> i32 { - self.builder.fd - } + let queue_pad = queue.static_pad("src").unwrap(); + bin.add_pad(&gst::GhostPad::with_target(Some("src"), &queue_pad)?)?; - fn streams(&self) -> &Vec { - &self.builder.streams - } + Ok(bin) +} - fn has_single_stream(&self) -> bool { - self.streams().len() == 1 - } +fn round_to_even(number: i32) -> i32 { + number / 2 * 2 } fn round_to_even_f32(number: f32) -> i32 { number as i32 / 2 * 2 } -fn round_to_even(number: i32) -> i32 { - number / 2 * 2 -} - fn ideal_thread_count() -> u32 { let num_processors = glib::num_processors(); cmp::min(num_processors, MAX_THREAD_COUNT) diff --git a/src/recording.rs b/src/recording.rs index d821c752f..a97c3aea0 100644 --- a/src/recording.rs +++ b/src/recording.rs @@ -200,11 +200,8 @@ impl Recording { // select area if settings.capture_mode() == CaptureMode::Selection { - let (selection, screen) = AreaSelector::select_area().await?; - - pipeline_builder - .coordinates(selection) - .actual_screen(screen); + let (coords, screen) = AreaSelector::select_area().await?; + pipeline_builder.select_area_context(coords, screen); } // setup timer @@ -505,7 +502,7 @@ impl Recording { debug_assert_eq!( self.pipeline() - .by_name("filesink") + .by_name("filesink0") .map(|fs| fs.property::("location")) .map(|path| PathBuf::from(&path)), Some(file.path().unwrap()) From b0b9cd5c3da45e3accc7381b64ed4e30767c45d3 Mon Sep 17 00:00:00 2001 From: SeaDve Date: Tue, 23 Aug 2022 18:31:13 +0800 Subject: [PATCH 03/46] style: Add more debug logs --- src/pipeline_builder.rs | 47 ++++++++++++++++++++++++++++++++++++----- src/recording.rs | 2 +- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/pipeline_builder.rs b/src/pipeline_builder.rs index e09b599db..4dde883f4 100644 --- a/src/pipeline_builder.rs +++ b/src/pipeline_builder.rs @@ -81,7 +81,7 @@ impl PipelineBuilder { encodebin.set_property("profile", &create_profile(self.format)); encodebin.set_property("avoid-reencoding", true); let queue = element_factory_make("queue")?; - let filesink = element_factory_make("filesink")?; + let filesink = element_factory_make_named("filesink", Some("filesink"))?; filesink.set_property( "location", self.file_path @@ -97,7 +97,15 @@ impl PipelineBuilder { _ => self.framerate, }; - tracing::debug!(stream_len = ?self.streams.len()); + tracing::debug!( + file_path = ?self.file_path, + format = ?self.format, + framerate, + stream_len = self.streams.len(), + streams = ?self.streams, + speaker_source = ?self.speaker_source, + mic_source = ?self.mic_source, + ); let videosrc_bin = match self.streams.len() { 0 => bail!("Found no streams"), @@ -142,6 +150,26 @@ impl PipelineBuilder { )?; } + if tracing::enabled!(tracing::Level::DEBUG) { + let codec_elements = pipeline + .iterate_recurse() + .into_iter() + .filter_map(|element| { + let element = element.unwrap(); + element + .metadata(&gst::ELEMENT_METADATA_KLASS) + .unwrap() + .contains("Codec") + .then(|| { + element + .factory() + .map_or_else(|| element.name(), |f| f.name()) + }) + }) + .collect::>(); + tracing::debug!(?codec_elements); + } + Ok(pipeline) } } @@ -213,9 +241,16 @@ fn create_profile(video_format: VideoFormat) -> gst_pbutils::EncodingContainerPr container_profile } -fn element_factory_make(element_name: &str) -> Result { - gst::ElementFactory::make(element_name, None) - .with_context(|| format!("Failed to make element `{}`", element_name)) +fn element_factory_make(factory_name: &str) -> Result { + element_factory_make_named(factory_name, None) +} + +fn element_factory_make_named( + factory_name: &str, + element_name: Option<&str>, +) -> Result { + gst::ElementFactory::make(factory_name, element_name) + .with_context(|| format!("Failed to make element `{}`", factory_name)) } fn pipewiresrc_with_default(fd: RawFd, path: &str) -> Result { @@ -252,6 +287,8 @@ fn videocrop_compute( let right_crop = stream_width as f32 - (coords.width() + coords.x()); let bottom_crop = stream_height as f32 - (coords.height() + coords.y()); + tracing::debug!(top_crop, left_crop, right_crop, bottom_crop); + // x264enc requires even resolution. let crop = element_factory_make("videocrop")?; crop.set_property("top", round_to_even_f32(top_crop)); diff --git a/src/recording.rs b/src/recording.rs index a97c3aea0..0df0eb571 100644 --- a/src/recording.rs +++ b/src/recording.rs @@ -502,7 +502,7 @@ impl Recording { debug_assert_eq!( self.pipeline() - .by_name("filesink0") + .by_name("filesink") .map(|fs| fs.property::("location")) .map(|path| PathBuf::from(&path)), Some(file.path().unwrap()) From 88cd377558a81d2ce279ffd9f36bdfc5c6615f57 Mon Sep 17 00:00:00 2001 From: SeaDve Date: Tue, 23 Aug 2022 18:33:14 +0800 Subject: [PATCH 04/46] chore: Update todo --- src/pipeline_builder.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pipeline_builder.rs b/src/pipeline_builder.rs index 4dde883f4..39099a749 100644 --- a/src/pipeline_builder.rs +++ b/src/pipeline_builder.rs @@ -175,9 +175,8 @@ impl PipelineBuilder { } fn create_profile(video_format: VideoFormat) -> gst_pbutils::EncodingContainerProfile { - // FIXME broken gif and mp4 - if video_format == VideoFormat::Gif { + // TODO broken gif let caps = gst::Caps::builder("image/gif").build(); let video_profile = gst_pbutils::EncodingVideoProfile::builder(&caps) .presence(0) @@ -188,7 +187,7 @@ fn create_profile(video_format: VideoFormat) -> gst_pbutils::EncodingContainerPr .build(); } - // TODO option to force vaapi + // TODO option to force vaapi and block vaapi (fixes broken mp4) // TODO modify element_properties let video_profile = { let caps = match video_format { From 1a6596d6da0d5c2947bb461abf506c12dd74e4cd Mon Sep 17 00:00:00 2001 From: SeaDve Date: Tue, 23 Aug 2022 19:01:42 +0800 Subject: [PATCH 05/46] Don't set avoid-reencoding encodebin property --- src/pipeline_builder.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pipeline_builder.rs b/src/pipeline_builder.rs index 39099a749..52db42b2a 100644 --- a/src/pipeline_builder.rs +++ b/src/pipeline_builder.rs @@ -79,7 +79,6 @@ impl PipelineBuilder { let encodebin = element_factory_make("encodebin")?; encodebin.set_property("profile", &create_profile(self.format)); - encodebin.set_property("avoid-reencoding", true); let queue = element_factory_make("queue")?; let filesink = element_factory_make_named("filesink", Some("filesink"))?; filesink.set_property( From f29315a545c7bae4242b2d30c2c3e46c306322a4 Mon Sep 17 00:00:00 2001 From: SeaDve Date: Wed, 24 Aug 2022 08:51:06 +0800 Subject: [PATCH 06/46] Include all encodebin elements --- src/pipeline_builder.rs | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/pipeline_builder.rs b/src/pipeline_builder.rs index 52db42b2a..9dfad3a83 100644 --- a/src/pipeline_builder.rs +++ b/src/pipeline_builder.rs @@ -150,23 +150,24 @@ impl PipelineBuilder { } if tracing::enabled!(tracing::Level::DEBUG) { - let codec_elements = pipeline + let encodebin_elements = encodebin + .downcast::() + .unwrap() .iterate_recurse() .into_iter() - .filter_map(|element| { + .map(|element| { let element = element.unwrap(); - element - .metadata(&gst::ELEMENT_METADATA_KLASS) - .unwrap() - .contains("Codec") - .then(|| { - element - .factory() - .map_or_else(|| element.name(), |f| f.name()) - }) + let name = element + .factory() + .map_or_else(|| element.name(), |f| f.name()); + if name == "capsfilter" { + element.property::("caps").to_string() + } else { + name.to_string() + } }) .collect::>(); - tracing::debug!(?codec_elements); + tracing::debug!(?encodebin_elements); } Ok(pipeline) From e4c357fc970635fb4d53828e02d5864c77551a63 Mon Sep 17 00:00:00 2001 From: SeaDve Date: Wed, 24 Aug 2022 08:56:34 +0800 Subject: [PATCH 07/46] Rename DEFAULT_GIF_FRAMERATE -> GIF_FRAMERATE_OVERRIDE --- src/pipeline_builder.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pipeline_builder.rs b/src/pipeline_builder.rs index 9dfad3a83..a18a26771 100644 --- a/src/pipeline_builder.rs +++ b/src/pipeline_builder.rs @@ -15,7 +15,7 @@ use std::{ use crate::{screencast_session::Stream, settings::VideoFormat}; const MAX_THREAD_COUNT: u32 = 64; -const DEFAULT_GIF_FRAMERATE: u32 = 15; +const GIF_FRAMERATE_OVERRIDE: u32 = 15; #[derive(Debug)] struct SelectAreaContext { @@ -92,7 +92,7 @@ impl PipelineBuilder { gst::Element::link_many(&[&encodebin, &queue, &filesink])?; let framerate = match self.format { - VideoFormat::Gif => DEFAULT_GIF_FRAMERATE, + VideoFormat::Gif => GIF_FRAMERATE_OVERRIDE, _ => self.framerate, }; @@ -318,8 +318,6 @@ fn multi_stream_pipewiresrc_bin(fd: i32, streams: &[Stream], framerate: u32) -> let mut last_pos = 0; for stream in streams { - // TODO maybe put another videoconvert here - let pipewiresrc = pipewiresrc_with_default(fd, &stream.node_id().to_string())?; let videorate = element_factory_make("videorate")?; let videorate_capsfilter = element_factory_make("capsfilter")?; From cf4070e17c7cc83c7b45f9ce660daa12f2184fe9 Mon Sep 17 00:00:00 2001 From: SeaDve Date: Wed, 24 Aug 2022 09:09:20 +0800 Subject: [PATCH 08/46] Log more message on recording bus --- src/recording.rs | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/recording.rs b/src/recording.rs index 0df0eb571..4bb1c58d8 100644 --- a/src/recording.rs +++ b/src/recording.rs @@ -433,8 +433,6 @@ impl Recording { fn handle_bus_message(&self, message: &gst::Message) -> glib::Continue { use gst::MessageView; - tracing::trace!("Received bus message {:?}", message); - let imp = self.imp(); match message.view() { @@ -513,19 +511,27 @@ impl Recording { Continue(false) } MessageView::StateChanged(sc) => { + let new_state = sc.current(); + if message.src().as_ref() != imp .pipeline .get() .map(|pipeline| pipeline.upcast_ref::()) { + tracing::trace!( + "`{}` changed state from `{:?}` -> `{:?}`", + message + .src() + .map_or_else(|| "".into(), |e| e.name()), + sc.old(), + new_state, + ); return Continue(true); } - let new_state = sc.current(); - tracing::debug!( - "Pipeline state changed from `{:?}` -> `{:?}`", + "Pipeline changed state from `{:?}` -> `{:?}`", sc.old(), new_state, ); @@ -539,7 +545,18 @@ impl Recording { Continue(true) } - _ => Continue(true), + MessageView::Warning(w) => { + tracing::warn!("Received warning message on bus: {:?}", w); + Continue(true) + } + MessageView::Info(i) => { + tracing::debug!("Received info message on bus: {:?}", i); + Continue(true) + } + other => { + tracing::trace!("Received other message on bus: {:?}", other); + Continue(true) + } } } } From 295de4ccaa9835fc45dca85e81ea57047eb9e5d6 Mon Sep 17 00:00:00 2001 From: SeaDve Date: Wed, 24 Aug 2022 09:18:41 +0800 Subject: [PATCH 09/46] Don't bundle gstreamer-vaapi --- README.md | 3 +++ build-aux/io.github.seadve.Kooha.Devel.json | 18 ------------------ 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 84df62954..591f95d51 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,9 @@ more efficient or perhaps faster encoding. It is not guaranteed to work on all devices, so it may give errors such as `no element vaapivp8enc` depending on the features and capability of your hardware. +First, you have to install `gstreamer-vaapi` on your system. If Kooha is installed +through Flatpak, it is as simple as running `flatpak install org.freedesktop.Platform.GStreamer.gstreamer-vaapi`. + To enable all the supported drivers and force Kooha to use VAAPI elements, set `GST_VAAPI_ALL_DRIVERS` and `KOOHA_VAAPI` both to 1 respectively. These environment variables are needed for hardware accelerated encoding. diff --git a/build-aux/io.github.seadve.Kooha.Devel.json b/build-aux/io.github.seadve.Kooha.Devel.json index 7de85274e..3d9519899 100644 --- a/build-aux/io.github.seadve.Kooha.Devel.json +++ b/build-aux/io.github.seadve.Kooha.Devel.json @@ -64,24 +64,6 @@ } ] }, - { - "name": "gstreamer-vaapi", - "buildsystem": "meson", - "builddir": true, - "config-opts": [ - "-Ddoc=disabled", - "-Dtests=disabled", - "-Dexamples=disabled", - "-Dwith_encoders=yes" - ], - "sources": [ - { - "type": "archive", - "url": "https://gstreamer.freedesktop.org/src/gstreamer-vaapi/gstreamer-vaapi-1.18.6.tar.xz", - "sha256": "ab6270f1e5e4546fbe6f5ea246d86ca3d196282eb863d46e6cdcc96f867449e0" - } - ] - }, { "name": "libadwaita", "buildsystem": "meson", From fd3279b1f3e23c2436ed5634bba87a41cc48416b Mon Sep 17 00:00:00 2001 From: SeaDve Date: Wed, 24 Aug 2022 09:29:22 +0800 Subject: [PATCH 10/46] Enable GST_DEBUG to 3 --- build-aux/io.github.seadve.Kooha.Devel.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build-aux/io.github.seadve.Kooha.Devel.json b/build-aux/io.github.seadve.Kooha.Devel.json index 3d9519899..966b3fb03 100644 --- a/build-aux/io.github.seadve.Kooha.Devel.json +++ b/build-aux/io.github.seadve.Kooha.Devel.json @@ -18,7 +18,8 @@ "--talk-name=org.freedesktop.FileManager1", "--env=RUST_BACKTRACE=1", "--env=RUST_LOG=kooha=debug", - "--env=G_MESSAGES_DEBUG=none" + "--env=G_MESSAGES_DEBUG=none", + "--env=GST_DEBUG=3" ], "build-options": { "append-path": "/usr/lib/sdk/llvm14/bin:/usr/lib/sdk/rust-stable/bin", From 5effe4f005d1f30ab347fd5ed34116de6dd26739 Mon Sep 17 00:00:00 2001 From: SeaDve Date: Wed, 24 Aug 2022 09:53:34 +0800 Subject: [PATCH 11/46] Don't notify duration if equal to last --- src/recording.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/recording.rs b/src/recording.rs index 4bb1c58d8..077f25eca 100644 --- a/src/recording.rs +++ b/src/recording.rs @@ -426,6 +426,10 @@ impl Recording { .and_then(|pipeline| pipeline.query_position::()) .unwrap_or(gst::ClockTime::ZERO); + if clock_time == self.duration() { + return; + } + self.imp().duration.set(clock_time); self.notify("duration"); } From f61566b3a18e3d9e370ed8cf88446ca0e5a68326 Mon Sep 17 00:00:00 2001 From: SeaDve Date: Wed, 24 Aug 2022 10:38:05 +0800 Subject: [PATCH 12/46] refactor: Use iterator to avoid mut --- src/pipeline_builder.rs | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/src/pipeline_builder.rs b/src/pipeline_builder.rs index a18a26771..8543bcc7d 100644 --- a/src/pipeline_builder.rs +++ b/src/pipeline_builder.rs @@ -1,4 +1,4 @@ -use anyhow::{bail, Context, Result}; +use anyhow::{bail, Context, Ok, Result}; use gst::prelude::*; use gst_pbutils::prelude::*; use gtk::{ @@ -130,24 +130,19 @@ impl PipelineBuilder { .context("Failed to request video_%u pad from encodebin")?, )?; - let mut audiosrc_bins = Vec::new(); - if let Some(ref device_name) = self.speaker_source { - let pulsesrc_bin = pulsesrc_bin(device_name)?; - audiosrc_bins.push(pulsesrc_bin); - } - if let Some(ref device_name) = self.mic_source { - let pulsesrc_bin = pulsesrc_bin(device_name)?; - audiosrc_bins.push(pulsesrc_bin); - } - - for bin in audiosrc_bins { - pipeline.add(&bin)?; - bin.static_pad("src").unwrap().link( - &encodebin - .request_pad_simple("audio_%u") - .context("Failed to request audio_%u pad from encodebin")?, - )?; - } + [&self.speaker_source, &self.mic_source] + .iter() + .filter_map(|d| d.as_ref()) // Filter out None + .try_for_each(|device_name| { + let pulsesrc_bin = pulsesrc_bin(device_name)?; + pipeline.add(&pulsesrc_bin)?; + pulsesrc_bin.static_pad("src").unwrap().link( + &encodebin + .request_pad_simple("audio_%u") + .context("Failed to request audio_%u pad from encodebin")?, + )?; + Ok(()) + })?; if tracing::enabled!(tracing::Level::DEBUG) { let encodebin_elements = encodebin From bee060265e797fa7de1904e6ed18057a0c9eb80b Mon Sep 17 00:00:00 2001 From: SeaDve Date: Wed, 24 Aug 2022 21:03:34 +0800 Subject: [PATCH 13/46] Properly set properties on encoders --- Cargo.toml | 4 +- build-aux/io.github.seadve.Kooha.Devel.json | 23 ++ src/pipeline_builder.rs | 344 ++++++++++++++++---- 3 files changed, 312 insertions(+), 59 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 272870a40..56a5b5e25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,9 @@ gdk-x11 = { package = "gdk4-x11", version = "0.4.5" } adw = { package = "libadwaita", version = "0.2.0-alpha.2", features = ["v1_2"] } gst = { package = "gstreamer", version = "0.18.2" } gst_video = { package = "gstreamer-video", version = "0.18.2" } -gst_pbutils = { package = "gstreamer-pbutils", version = "0.18.2" } +gst_pbutils = { package = "gstreamer-pbutils", version = "0.18.2", features = [ + "v1_20", +] } gst-plugin-gif = "0.8.0" futures-channel = "0.3.19" futures-util = { version = "0.3", default-features = false } diff --git a/build-aux/io.github.seadve.Kooha.Devel.json b/build-aux/io.github.seadve.Kooha.Devel.json index 966b3fb03..92788d492 100644 --- a/build-aux/io.github.seadve.Kooha.Devel.json +++ b/build-aux/io.github.seadve.Kooha.Devel.json @@ -47,6 +47,29 @@ } ] }, + { + "name": "gstreamer", + "buildsystem": "meson", + "config-opts": [ + "-Dexamples=disabled", + "-Dtests=disabled", + "-Dbenchmarks=disabled", + "-Dtools=disabled", + "-Dintrospection=disabled", + "-Dnls=disabled", + "-Ddoc=disabled", + "-Dugly=enabled", + "--libdir=lib" + ], + "sources": [ + { + "type": "git", + "tag": "1.20.3", + "url": "https://gitlab.freedesktop.org/gstreamer/gstreamer.git", + "commit": "ccf22e315cedf81e0075ab179ffb1b733da5206e" + } + ] + }, { "name": "gst-plugins-ugly", "buildsystem": "meson", diff --git a/src/pipeline_builder.rs b/src/pipeline_builder.rs index 8543bcc7d..645f4736a 100644 --- a/src/pipeline_builder.rs +++ b/src/pipeline_builder.rs @@ -169,65 +169,61 @@ impl PipelineBuilder { } } +/// Create an encoding profile based on video format fn create_profile(video_format: VideoFormat) -> gst_pbutils::EncodingContainerProfile { - if video_format == VideoFormat::Gif { - // TODO broken gif - let caps = gst::Caps::builder("image/gif").build(); - let video_profile = gst_pbutils::EncodingVideoProfile::builder(&caps) - .presence(0) - .build(); - return gst_pbutils::EncodingContainerProfile::builder(&caps) - .presence(0) - .add_profile(&video_profile) - .build(); - } - - // TODO option to force vaapi and block vaapi (fixes broken mp4) - // TODO modify element_properties - let video_profile = { - let caps = match video_format { - VideoFormat::Webm => gst::Caps::builder("video/x-vp8").build(), - VideoFormat::Mkv => gst::Caps::builder("video/x-vp8").build(), - VideoFormat::Mp4 => gst::Caps::builder("video/x-h264") - .field("alignment", "au") - .field("stream-format", "avc") - .build(), - VideoFormat::Gif => unreachable!(), - }; - gst_pbutils::EncodingVideoProfile::builder(&caps) - .variable_framerate(false) - .presence(0) - .build() - }; - - let audio_profile = { - let caps = match video_format { - VideoFormat::Webm => gst::Caps::builder("audio/x-opus").build(), - VideoFormat::Mkv => gst::Caps::builder("audio/x-opus").build(), - VideoFormat::Mp4 => gst::Caps::builder("audio/mpeg") - .field("mpegversion", 1) - .field("layer", 3) + use profile::{Builder as ProfileBuilder, ElementPropertiesBuilder}; + + // TODO Option for vaapi + + let thread_count = ideal_thread_count(); + + let container_profile = match video_format { + VideoFormat::Webm => { + ProfileBuilder::new_simple("video/webm", "video/x-vp8", "audio/x-opus") + .video_preset("vp8enc") + .video_element_properties( + ElementPropertiesBuilder::new("vp8enc") + .field("max-quantizer", 17) + .field("cpu-used", 16) + .field("cq-level", 13) + .field("deadline", 1) + .field("static-threshold", 100) + .field_from_str("keyframe-mode", "disabled") + .field("buffer-size", 20000) + .field("threads", thread_count) + .build(), + ) + .build() + } + VideoFormat::Mkv => { + ProfileBuilder::new_simple("video/x-matroska", "video/x-h264", "audio/x-opus") + .video_preset("x264enc") + .video_element_properties( + ElementPropertiesBuilder::new("x264enc") + .field("qp-max", 17) + .field_from_str("speed-preset", "superfast") + .field("threads", thread_count) + .build(), + ) + .build() + } + VideoFormat::Mp4 => ProfileBuilder::new( + caps("video/quicktime"), + gst::Caps::builder("video/x-h264") + .field("profile", "baseline") .build(), - VideoFormat::Gif => unreachable!(), - }; - gst_pbutils::EncodingAudioProfile::builder(&caps) - .presence(0) - .build() - }; - - let container_profile = { - let caps = match video_format { - VideoFormat::Webm => gst::Caps::builder("video/webm").build(), - VideoFormat::Mkv => gst::Caps::builder("video/x-matroska").build(), - VideoFormat::Mp4 => gst::Caps::builder("video/quicktime") - .field("variant", "iso") + caps("audio/mpeg"), + ) + .video_preset("x264enc") + .video_element_properties( + ElementPropertiesBuilder::new("x264enc") + .field("qp-max", 17) + .field_from_str("speed-preset", "superfast") + .field("threads", thread_count) .build(), - VideoFormat::Gif => unreachable!(), - }; - gst_pbutils::EncodingContainerProfile::builder(&caps) - .add_profile(&video_profile) - .add_profile(&audio_profile) - .build() + ) + .build(), + VideoFormat::Gif => todo!("Unsupported video format"), // FIXME }; tracing::debug!(suggested_file_extension = ?container_profile.file_extension()); @@ -235,10 +231,19 @@ fn create_profile(video_format: VideoFormat) -> gst_pbutils::EncodingContainerPr container_profile } +/// Helper function to create a caps with just a name. +fn caps(name: &str) -> gst::Caps { + gst::Caps::new_simple(name, &[]) +} + +/// Helper function for more helpful error messages when failing +/// to make an element. fn element_factory_make(factory_name: &str) -> Result { element_factory_make_named(factory_name, None) } +/// Helper function for more helpful error messages when failing +/// to make an element. fn element_factory_make_named( factory_name: &str, element_name: Option<&str>, @@ -266,6 +271,8 @@ fn videoconvert_with_default() -> Result { Ok(conv) } +/// Create a videocrop element that computes the crop from the given coordinates +/// and size. fn videocrop_compute( stream_width: i32, stream_height: i32, @@ -422,8 +429,229 @@ fn round_to_even_f32(number: f32) -> i32 { } fn ideal_thread_count() -> u32 { - let num_processors = glib::num_processors(); - cmp::min(num_processors, MAX_THREAD_COUNT) + cmp::min(glib::num_processors(), MAX_THREAD_COUNT) +} + +mod profile { + use anyhow::{anyhow, Result}; + use gst_pbutils::prelude::*; + use gtk::glib::{ + self, + translate::{ToGlibPtr, UnsafeFrom}, + }; + + use super::{caps, element_factory_make}; + + pub struct ElementPropertiesBuilder { + structure: gst::Structure, + } + + impl ElementPropertiesBuilder { + pub fn new(element_name: &str) -> Self { + Self { + structure: gst::Structure::new_empty(element_name), + } + } + + pub fn field(mut self, name: &str, value: V) -> Self { + self.structure.set(name, value); + self + } + + /// Parse the value into the type of the element's property. + /// + /// The element is based on the given name on `Self::new` and + /// the element's property is based on the recently given name. + pub fn field_from_str(self, name: &str, value: &str) -> Self { + self.try_field_from_str(name, value).unwrap() + } + + pub fn try_field_from_str(mut self, name: &str, value: &str) -> Result { + let element = element_factory_make(self.structure.name())?; + let pspec = element.find_property(name).ok_or_else(|| { + anyhow!( + "Property `{}` not found on type `{}`", + name, + element.type_() + ) + })?; + let value = unsafe { + glib::SendValue::unsafe_from( + glib::Value::deserialize_with_pspec(value, &pspec)?.into_raw(), + ) + }; + + self.structure.set_value(name, value); + Ok(self) + } + + pub fn build(self) -> gst::Structure { + self.structure + } + } + + pub struct Builder { + container_caps: gst::Caps, + container_preset_name: Option, + container_element_properties: Vec, + + video_caps: gst::Caps, + video_preset_name: Option, + video_element_properties: Vec, + + audio_caps: gst::Caps, + audio_preset_name: Option, + audio_element_properties: Vec, + } + + #[allow(dead_code)] + impl Builder { + pub fn new( + container_caps: gst::Caps, + video_caps: gst::Caps, + audio_caps: gst::Caps, + ) -> Self { + Self { + container_caps, + container_preset_name: None, + container_element_properties: Vec::new(), + video_caps, + video_preset_name: None, + video_element_properties: Vec::new(), + audio_caps, + audio_preset_name: None, + audio_element_properties: Vec::new(), + } + } + + pub fn new_simple( + container_caps_name: &str, + video_caps_name: &str, + audio_caps_name: &str, + ) -> Self { + Self::new( + caps(container_caps_name), + caps(video_caps_name), + caps(audio_caps_name), + ) + } + + pub fn container_preset(mut self, preset_name: &str) -> Self { + self.container_preset_name = Some(preset_name.to_string()); + self + } + + pub fn video_preset(mut self, preset_name: &str) -> Self { + self.video_preset_name = Some(preset_name.to_string()); + self + } + + pub fn audio_preset(mut self, preset_name: &str) -> Self { + self.audio_preset_name = Some(preset_name.to_string()); + self + } + + /// Appends to the container element properties. + pub fn container_element_properties(mut self, element_properties: gst::Structure) -> Self { + self.container_element_properties.push(element_properties); + self + } + + /// Appends to the video element properties. + pub fn video_element_properties(mut self, element_properties: gst::Structure) -> Self { + self.video_element_properties.push(element_properties); + self + } + + /// Appends to the audio element properties. + pub fn audio_element_properties(mut self, element_properties: gst::Structure) -> Self { + self.audio_element_properties.push(element_properties); + self + } + + pub fn build(self) -> gst_pbutils::EncodingContainerProfile { + let video_profile = { + let mut builder = + gst_pbutils::EncodingVideoProfile::builder(&self.video_caps).presence(0); + + if let Some(ref preset_name) = self.video_preset_name { + builder = builder.preset_name(preset_name); + } + + let profile = builder.build(); + + if !self.video_element_properties.is_empty() { + profile.set_element_properties(&self.video_element_properties); + } + + profile + }; + + let audio_profile = { + let mut builder = + gst_pbutils::EncodingAudioProfile::builder(&self.audio_caps).presence(0); + + if let Some(ref preset_name) = self.audio_preset_name { + builder = builder.preset_name(preset_name); + } + + let profile = builder.build(); + + if !self.audio_element_properties.is_empty() { + profile.set_element_properties(&self.audio_element_properties); + } + + profile + }; + + let container_profile = { + let mut builder = + gst_pbutils::EncodingContainerProfile::builder(&self.container_caps) + .add_profile(&video_profile) + .add_profile(&audio_profile) + .presence(0); + + if let Some(ref preset_name) = self.container_preset_name { + builder = builder.preset_name(preset_name); + } + + let profile = builder.build(); + + if !self.container_element_properties.is_empty() { + profile.set_element_properties(&self.container_element_properties); + } + + profile + }; + + container_profile + } + } + + trait EncodingProfileExt { + fn set_element_properties(&self, element_properties: &[gst::Structure]); + } + + impl> EncodingProfileExt for T { + fn set_element_properties(&self, element_properties: &[gst::Structure]) { + let actual_element_properties = gst::Structure::builder("element-properties-map") + .field( + "map", + element_properties + .iter() + .map(|ep| ep.to_send_value()) + .collect::(), + ) + .build(); + + unsafe { + gst_pbutils::ffi::gst_encoding_profile_set_element_properties( + self.as_ref().to_glib_none().0, + actual_element_properties.to_glib_full(), + ); + } + } + } } #[cfg(test)] From 59a967fff32ce20e6154f68757df3cb94ff7fec7 Mon Sep 17 00:00:00 2001 From: SeaDve Date: Wed, 24 Aug 2022 21:13:43 +0800 Subject: [PATCH 14/46] Improve naming --- src/pipeline_builder.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pipeline_builder.rs b/src/pipeline_builder.rs index 645f4736a..38047d2c7 100644 --- a/src/pipeline_builder.rs +++ b/src/pipeline_builder.rs @@ -462,11 +462,11 @@ mod profile { /// /// The element is based on the given name on `Self::new` and /// the element's property is based on the recently given name. - pub fn field_from_str(self, name: &str, value: &str) -> Self { - self.try_field_from_str(name, value).unwrap() + pub fn field_from_str(self, name: &str, string: &str) -> Self { + self.try_field_from_str(name, string).unwrap() } - pub fn try_field_from_str(mut self, name: &str, value: &str) -> Result { + pub fn try_field_from_str(mut self, name: &str, string: &str) -> Result { let element = element_factory_make(self.structure.name())?; let pspec = element.find_property(name).ok_or_else(|| { anyhow!( @@ -477,7 +477,7 @@ mod profile { })?; let value = unsafe { glib::SendValue::unsafe_from( - glib::Value::deserialize_with_pspec(value, &pspec)?.into_raw(), + glib::Value::deserialize_with_pspec(string, &pspec)?.into_raw(), ) }; From 7e2ab9bfaa2d2639038ed5a6da892056dabc654b Mon Sep 17 00:00:00 2001 From: SeaDve Date: Thu, 25 Aug 2022 12:23:51 +0800 Subject: [PATCH 15/46] Add baseline on MKV profile --- src/pipeline_builder.rs | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/pipeline_builder.rs b/src/pipeline_builder.rs index 38047d2c7..1895af7ed 100644 --- a/src/pipeline_builder.rs +++ b/src/pipeline_builder.rs @@ -195,18 +195,22 @@ fn create_profile(video_format: VideoFormat) -> gst_pbutils::EncodingContainerPr ) .build() } - VideoFormat::Mkv => { - ProfileBuilder::new_simple("video/x-matroska", "video/x-h264", "audio/x-opus") - .video_preset("x264enc") - .video_element_properties( - ElementPropertiesBuilder::new("x264enc") - .field("qp-max", 17) - .field_from_str("speed-preset", "superfast") - .field("threads", thread_count) - .build(), - ) - .build() - } + VideoFormat::Mkv => ProfileBuilder::new( + caps("video/x-matroska"), + gst::Caps::builder("video/x-h264") + .field("profile", "baseline") + .build(), + caps("audio/x-opus"), + ) + .video_preset("x264enc") + .video_element_properties( + ElementPropertiesBuilder::new("x264enc") + .field("qp-max", 17) + .field_from_str("speed-preset", "superfast") + .field("threads", thread_count) + .build(), + ) + .build(), VideoFormat::Mp4 => ProfileBuilder::new( caps("video/quicktime"), gst::Caps::builder("video/x-h264") From fad4a89bd410eefb5b113cb4839b1adb4b2acc6b Mon Sep 17 00:00:00 2001 From: SeaDve Date: Thu, 25 Aug 2022 14:10:26 +0800 Subject: [PATCH 16/46] Separate into different files and improve ElementProperties API --- src/main.rs | 2 +- src/pipeline/element_properties.rs | 285 +++++++++++++++++++ src/{pipeline_builder.rs => pipeline/mod.rs} | 274 +++--------------- src/pipeline/profile.rs | 139 +++++++++ src/recording.rs | 2 +- 5 files changed, 459 insertions(+), 243 deletions(-) create mode 100644 src/pipeline/element_properties.rs rename src/{pipeline_builder.rs => pipeline/mod.rs} (64%) create mode 100644 src/pipeline/profile.rs diff --git a/src/main.rs b/src/main.rs index 683dea10f..b6f285d34 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,7 +27,7 @@ mod audio_device; mod cancelled; mod config; mod help; -mod pipeline_builder; +mod pipeline; mod recording; mod screencast_session; mod settings; diff --git a/src/pipeline/element_properties.rs b/src/pipeline/element_properties.rs new file mode 100644 index 000000000..3cac1eb3e --- /dev/null +++ b/src/pipeline/element_properties.rs @@ -0,0 +1,285 @@ +use gst::prelude::*; +use gtk::glib::{ + self, + translate::{ToGlibPtr, UnsafeFrom}, +}; + +use std::ops::Deref; + +pub trait ElementPropertiesEncodingProfileExt { + fn set_element_properties(&self, element_properties: ElementProperties); +} + +impl> ElementPropertiesEncodingProfileExt for P { + fn set_element_properties(&self, element_properties: ElementProperties) { + unsafe { + gst_pbutils::ffi::gst_encoding_profile_set_element_properties( + self.as_ref().to_glib_none().0, + element_properties.into_inner().to_glib_full(), + ); + } + } +} + +/// Wrapper around `gst::Structure` for `element-properties` +/// property of `EncodingProfile`. +/// +/// # Examples +/// +/// ```rust +/// ElementProperties::builder_general() +/// .field("threads", 16) +/// .build(); +/// ``` +/// +/// ```rust +/// ElementProperties::builder_map() +/// .field( +/// ElementFactoryPropertiesMap::new("vp8enc") +/// .field("max-quantizer", 17) +/// .field_from_str("keyframe-mode", "disabled") +/// .field("buffer-size", 20000) +/// .field("threads", 16), +/// ) +/// .build() +/// ``` +#[derive(Debug, Clone)] +pub struct ElementProperties(gst::Structure); + +impl Deref for ElementProperties { + type Target = gst::StructureRef; + + fn deref(&self) -> &gst::StructureRef { + self.0.as_ref() + } +} + +impl From for gst::Structure { + fn from(e: ElementProperties) -> Self { + e.into_inner() + } +} + +impl ElementProperties { + /// Creates an `ElementProperties` builder that build into + /// something similar to the following: + /// + /// element-properties-map, map = { + /// [openh264enc, gop-size=32, ], + /// [x264enc, key-int-max=32, tune=zerolatency], + /// } + pub fn builder_map() -> ElementPropertiesMapBuilder { + ElementPropertiesMapBuilder::new() + } + + /// Creates an `ElementProperties` builder that build into + /// something similar to the following: + /// + /// [element-properties, boolean-prop=true, string-prop="hi"] + pub fn builder_general() -> ElementPropertiesGeneralBuilder { + ElementPropertiesGeneralBuilder::new() + } + + pub fn into_inner(self) -> gst::Structure { + self.0 + } +} + +#[must_use = "The builder must be built to be used"] +#[derive(Debug, Clone)] +pub struct ElementPropertiesGeneralBuilder { + structure: gst::Structure, +} + +impl Default for ElementPropertiesGeneralBuilder { + fn default() -> Self { + Self::new() + } +} + +impl ElementPropertiesGeneralBuilder { + pub fn new() -> Self { + Self { + structure: gst::Structure::new_empty("element-properties"), + } + } + + pub fn field(mut self, property_name: &str, value: T) -> Self + where + T: ToSendValue + Sync, + { + self.structure.set(property_name, value); + self + } + + pub fn build(self) -> ElementProperties { + ElementProperties(self.structure) + } +} + +#[must_use = "The builder must be built to be used"] +#[derive(Debug, Clone)] +pub struct ElementPropertiesMapBuilder { + map: Vec, +} + +impl Default for ElementPropertiesMapBuilder { + fn default() -> Self { + Self::new() + } +} + +impl ElementPropertiesMapBuilder { + pub fn new() -> Self { + Self { map: Vec::new() } + } + + /// Insert a new `element-properties-map` map entry. + pub fn item(mut self, structure: ElementFactoryPropertiesMap) -> Self { + self.map.push(structure.into_inner().to_send_value()); + self + } + + pub fn build(self) -> ElementProperties { + ElementProperties( + gst::Structure::builder("element-properties-map") + .field("map", gst::List::from(self.map)) + .build(), + ) + } +} + +/// Wrapper around `gst::Structure` for an item +/// on a `ElementPropertiesMapBuilder`. +/// +/// # Example +/// +/// ```rust +/// ElementFactoryPropertiesMap::new("vp8enc") +/// .field("max-quantizer", 17) +/// .field_from_str("keyframe-mode", "disabled") +/// .field("buffer-size", 20000) +/// .field("threads", 16), +/// ``` +#[must_use = "The builder must be built to be used"] +#[derive(Debug, Clone)] +pub struct ElementFactoryPropertiesMap(gst::Structure); + +impl From for gst::Structure { + fn from(e: ElementFactoryPropertiesMap) -> Self { + e.into_inner() + } +} + +impl ElementFactoryPropertiesMap { + pub fn new(factory_name: &str) -> Self { + Self(gst::Structure::new_empty(factory_name)) + } + + pub fn field(mut self, property_name: &str, value: T) -> Self + where + T: ToSendValue + Sync, + { + self.0.set(property_name, value); + self + } + + /// Parses the given string into a property of element from the + /// given `factory_name` with type based on the property's param spec. + /// + /// This works similar to `GObjectExtManualGst::try_set_property_from_str`. + pub fn field_try_from_str( + mut self, + property_name: &str, + string: &str, + ) -> Result { + let factory_name = self.0.name(); + let element = gst::ElementFactory::make(factory_name, None).map_err(|_| { + glib::bool_error!( + "Failed to create element from factory name `{}`", + factory_name + ) + })?; + let pspec = element.find_property(property_name).ok_or_else(|| { + glib::bool_error!( + "Property `{}` not found on type `{}`", + property_name, + element.type_() + ) + })?; + let value = unsafe { + glib::SendValue::unsafe_from( + glib::Value::deserialize_with_pspec(string, &pspec)?.into_raw(), + ) + }; + self.0.set_value(property_name, value); + Ok(self) + } + + /// Parses the given string into a property of element from the + /// given `factory_name` with type based on the property's param spec. + /// + /// This works similar to `GObjectExtManualGst::set_property_from_str`. + pub fn field_from_str(self, property_name: &str, string: &str) -> Self { + self.field_try_from_str(property_name, string).unwrap() + } + + pub fn into_inner(self) -> gst::Structure { + self.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn element_properties_general_builder() { + let elem_props = ElementProperties::builder_general() + .field("string-prop", "hi") + .field("boolean-prop", true) + .build(); + assert_eq!(elem_props.n_fields(), 2); + assert_eq!(elem_props.name(), "element-properties"); + assert_eq!(elem_props.get::("string-prop").unwrap(), "hi"); + assert!(elem_props.get::("boolean-prop").unwrap()); + } + + #[test] + fn element_properties_map_builder() { + let props_map = ElementFactoryPropertiesMap::new("vp8enc") + .field("cq-level", 13) + .field("resize-allowed", false); + let props_map_s = props_map.clone().into_inner(); + assert_eq!(props_map_s.n_fields(), 2); + assert_eq!(props_map_s.name(), "vp8enc"); + assert_eq!(props_map_s.get::("cq-level").unwrap(), 13); + assert!(!props_map_s.get::("resize-allowed").unwrap()); + + let elem_props = ElementProperties::builder_map() + .item(props_map.clone()) + .build(); + assert_eq!(elem_props.n_fields(), 1); + + let list = elem_props.get::("map").unwrap(); + assert_eq!(list.len(), 1); + assert_eq!( + list.get(0).unwrap().get::().unwrap(), + gst::Structure::from(props_map) + ); + } + + #[test] + fn element_factory_properties_map_field_from_str() { + let prop_map_s = ElementFactoryPropertiesMap::new("vp8enc") + .field("threads", 16) + .field_from_str("keyframe-mode", "disabled") + .into_inner(); + assert_eq!(prop_map_s.n_fields(), 2); + assert_eq!(prop_map_s.name(), "vp8enc"); + assert_eq!(prop_map_s.get::("threads").unwrap(), 16); + + let keyframe_mode_value = prop_map_s.value("keyframe-mode").unwrap(); + assert!(format!("{:?}", keyframe_mode_value).starts_with("(GstVPXEncKfMode)")); + } +} diff --git a/src/pipeline_builder.rs b/src/pipeline/mod.rs similarity index 64% rename from src/pipeline_builder.rs rename to src/pipeline/mod.rs index 1895af7ed..23a1649a2 100644 --- a/src/pipeline_builder.rs +++ b/src/pipeline/mod.rs @@ -1,3 +1,6 @@ +mod element_properties; +mod profile; + use anyhow::{bail, Context, Ok, Result}; use gst::prelude::*; use gst_pbutils::prelude::*; @@ -12,6 +15,10 @@ use std::{ path::{Path, PathBuf}, }; +use self::{ + element_properties::{ElementFactoryPropertiesMap, ElementProperties}, + profile::Builder as ProfileBuilder, +}; use crate::{screencast_session::Stream, settings::VideoFormat}; const MAX_THREAD_COUNT: u32 = 64; @@ -171,8 +178,6 @@ impl PipelineBuilder { /// Create an encoding profile based on video format fn create_profile(video_format: VideoFormat) -> gst_pbutils::EncodingContainerProfile { - use profile::{Builder as ProfileBuilder, ElementPropertiesBuilder}; - // TODO Option for vaapi let thread_count = ideal_thread_count(); @@ -182,15 +187,18 @@ fn create_profile(video_format: VideoFormat) -> gst_pbutils::EncodingContainerPr ProfileBuilder::new_simple("video/webm", "video/x-vp8", "audio/x-opus") .video_preset("vp8enc") .video_element_properties( - ElementPropertiesBuilder::new("vp8enc") - .field("max-quantizer", 17) - .field("cpu-used", 16) - .field("cq-level", 13) - .field("deadline", 1) - .field("static-threshold", 100) - .field_from_str("keyframe-mode", "disabled") - .field("buffer-size", 20000) - .field("threads", thread_count) + ElementProperties::builder_map() + .item( + ElementFactoryPropertiesMap::new("vp8enc") + .field("max-quantizer", 17) + .field("cpu-used", 16) + .field("cq-level", 13) + .field("deadline", 1) + .field("static-threshold", 100) + .field_from_str("keyframe-mode", "disabled") + .field("buffer-size", 20000) + .field("threads", thread_count), + ) .build(), ) .build() @@ -204,10 +212,13 @@ fn create_profile(video_format: VideoFormat) -> gst_pbutils::EncodingContainerPr ) .video_preset("x264enc") .video_element_properties( - ElementPropertiesBuilder::new("x264enc") - .field("qp-max", 17) - .field_from_str("speed-preset", "superfast") - .field("threads", thread_count) + ElementProperties::builder_map() + .item( + ElementFactoryPropertiesMap::new("x264enc") + .field("qp-max", 17) + .field_from_str("speed-preset", "superfast") + .field("threads", thread_count), + ) .build(), ) .build(), @@ -220,10 +231,13 @@ fn create_profile(video_format: VideoFormat) -> gst_pbutils::EncodingContainerPr ) .video_preset("x264enc") .video_element_properties( - ElementPropertiesBuilder::new("x264enc") - .field("qp-max", 17) - .field_from_str("speed-preset", "superfast") - .field("threads", thread_count) + ElementProperties::builder_map() + .item( + ElementFactoryPropertiesMap::new("x264enc") + .field("qp-max", 17) + .field_from_str("speed-preset", "superfast") + .field("threads", thread_count), + ) .build(), ) .build(), @@ -436,228 +450,6 @@ fn ideal_thread_count() -> u32 { cmp::min(glib::num_processors(), MAX_THREAD_COUNT) } -mod profile { - use anyhow::{anyhow, Result}; - use gst_pbutils::prelude::*; - use gtk::glib::{ - self, - translate::{ToGlibPtr, UnsafeFrom}, - }; - - use super::{caps, element_factory_make}; - - pub struct ElementPropertiesBuilder { - structure: gst::Structure, - } - - impl ElementPropertiesBuilder { - pub fn new(element_name: &str) -> Self { - Self { - structure: gst::Structure::new_empty(element_name), - } - } - - pub fn field(mut self, name: &str, value: V) -> Self { - self.structure.set(name, value); - self - } - - /// Parse the value into the type of the element's property. - /// - /// The element is based on the given name on `Self::new` and - /// the element's property is based on the recently given name. - pub fn field_from_str(self, name: &str, string: &str) -> Self { - self.try_field_from_str(name, string).unwrap() - } - - pub fn try_field_from_str(mut self, name: &str, string: &str) -> Result { - let element = element_factory_make(self.structure.name())?; - let pspec = element.find_property(name).ok_or_else(|| { - anyhow!( - "Property `{}` not found on type `{}`", - name, - element.type_() - ) - })?; - let value = unsafe { - glib::SendValue::unsafe_from( - glib::Value::deserialize_with_pspec(string, &pspec)?.into_raw(), - ) - }; - - self.structure.set_value(name, value); - Ok(self) - } - - pub fn build(self) -> gst::Structure { - self.structure - } - } - - pub struct Builder { - container_caps: gst::Caps, - container_preset_name: Option, - container_element_properties: Vec, - - video_caps: gst::Caps, - video_preset_name: Option, - video_element_properties: Vec, - - audio_caps: gst::Caps, - audio_preset_name: Option, - audio_element_properties: Vec, - } - - #[allow(dead_code)] - impl Builder { - pub fn new( - container_caps: gst::Caps, - video_caps: gst::Caps, - audio_caps: gst::Caps, - ) -> Self { - Self { - container_caps, - container_preset_name: None, - container_element_properties: Vec::new(), - video_caps, - video_preset_name: None, - video_element_properties: Vec::new(), - audio_caps, - audio_preset_name: None, - audio_element_properties: Vec::new(), - } - } - - pub fn new_simple( - container_caps_name: &str, - video_caps_name: &str, - audio_caps_name: &str, - ) -> Self { - Self::new( - caps(container_caps_name), - caps(video_caps_name), - caps(audio_caps_name), - ) - } - - pub fn container_preset(mut self, preset_name: &str) -> Self { - self.container_preset_name = Some(preset_name.to_string()); - self - } - - pub fn video_preset(mut self, preset_name: &str) -> Self { - self.video_preset_name = Some(preset_name.to_string()); - self - } - - pub fn audio_preset(mut self, preset_name: &str) -> Self { - self.audio_preset_name = Some(preset_name.to_string()); - self - } - - /// Appends to the container element properties. - pub fn container_element_properties(mut self, element_properties: gst::Structure) -> Self { - self.container_element_properties.push(element_properties); - self - } - - /// Appends to the video element properties. - pub fn video_element_properties(mut self, element_properties: gst::Structure) -> Self { - self.video_element_properties.push(element_properties); - self - } - - /// Appends to the audio element properties. - pub fn audio_element_properties(mut self, element_properties: gst::Structure) -> Self { - self.audio_element_properties.push(element_properties); - self - } - - pub fn build(self) -> gst_pbutils::EncodingContainerProfile { - let video_profile = { - let mut builder = - gst_pbutils::EncodingVideoProfile::builder(&self.video_caps).presence(0); - - if let Some(ref preset_name) = self.video_preset_name { - builder = builder.preset_name(preset_name); - } - - let profile = builder.build(); - - if !self.video_element_properties.is_empty() { - profile.set_element_properties(&self.video_element_properties); - } - - profile - }; - - let audio_profile = { - let mut builder = - gst_pbutils::EncodingAudioProfile::builder(&self.audio_caps).presence(0); - - if let Some(ref preset_name) = self.audio_preset_name { - builder = builder.preset_name(preset_name); - } - - let profile = builder.build(); - - if !self.audio_element_properties.is_empty() { - profile.set_element_properties(&self.audio_element_properties); - } - - profile - }; - - let container_profile = { - let mut builder = - gst_pbutils::EncodingContainerProfile::builder(&self.container_caps) - .add_profile(&video_profile) - .add_profile(&audio_profile) - .presence(0); - - if let Some(ref preset_name) = self.container_preset_name { - builder = builder.preset_name(preset_name); - } - - let profile = builder.build(); - - if !self.container_element_properties.is_empty() { - profile.set_element_properties(&self.container_element_properties); - } - - profile - }; - - container_profile - } - } - - trait EncodingProfileExt { - fn set_element_properties(&self, element_properties: &[gst::Structure]); - } - - impl> EncodingProfileExt for T { - fn set_element_properties(&self, element_properties: &[gst::Structure]) { - let actual_element_properties = gst::Structure::builder("element-properties-map") - .field( - "map", - element_properties - .iter() - .map(|ep| ep.to_send_value()) - .collect::(), - ) - .build(); - - unsafe { - gst_pbutils::ffi::gst_encoding_profile_set_element_properties( - self.as_ref().to_glib_none().0, - actual_element_properties.to_glib_full(), - ); - } - } - } -} - #[cfg(test)] mod test { use super::*; diff --git a/src/pipeline/profile.rs b/src/pipeline/profile.rs new file mode 100644 index 000000000..5095b20fb --- /dev/null +++ b/src/pipeline/profile.rs @@ -0,0 +1,139 @@ +use gst_pbutils::prelude::*; + +use super::{ + caps, + element_properties::{ElementProperties, ElementPropertiesEncodingProfileExt}, +}; + +pub struct Builder { + container_caps: gst::Caps, + container_preset_name: Option, + container_element_properties: Option, + + video_caps: gst::Caps, + video_preset_name: Option, + video_element_properties: Option, + + audio_caps: gst::Caps, + audio_preset_name: Option, + audio_element_properties: Option, +} + +#[allow(dead_code)] +impl Builder { + pub fn new(container_caps: gst::Caps, video_caps: gst::Caps, audio_caps: gst::Caps) -> Self { + Self { + container_caps, + container_preset_name: None, + container_element_properties: None, + video_caps, + video_preset_name: None, + video_element_properties: None, + audio_caps, + audio_preset_name: None, + audio_element_properties: None, + } + } + + pub fn new_simple( + container_caps_name: &str, + video_caps_name: &str, + audio_caps_name: &str, + ) -> Self { + Self::new( + caps(container_caps_name), + caps(video_caps_name), + caps(audio_caps_name), + ) + } + + pub fn container_preset(mut self, preset_name: &str) -> Self { + self.container_preset_name = Some(preset_name.to_string()); + self + } + + pub fn video_preset(mut self, preset_name: &str) -> Self { + self.video_preset_name = Some(preset_name.to_string()); + self + } + + pub fn audio_preset(mut self, preset_name: &str) -> Self { + self.audio_preset_name = Some(preset_name.to_string()); + self + } + + /// Appends to the container element properties. + pub fn container_element_properties(mut self, element_properties: ElementProperties) -> Self { + self.container_element_properties = Some(element_properties); + self + } + + /// Appends to the video element properties. + pub fn video_element_properties(mut self, element_properties: ElementProperties) -> Self { + self.video_element_properties = Some(element_properties); + self + } + + /// Appends to the audio element properties. + pub fn audio_element_properties(mut self, element_properties: ElementProperties) -> Self { + self.audio_element_properties = Some(element_properties); + self + } + + pub fn build(self) -> gst_pbutils::EncodingContainerProfile { + let video_profile = { + let mut builder = + gst_pbutils::EncodingVideoProfile::builder(&self.video_caps).presence(0); + + if let Some(ref preset_name) = self.video_preset_name { + builder = builder.preset_name(preset_name); + } + + let profile = builder.build(); + + if let Some(element_properties) = self.video_element_properties { + profile.set_element_properties(element_properties); + } + + profile + }; + + let audio_profile = { + let mut builder = + gst_pbutils::EncodingAudioProfile::builder(&self.audio_caps).presence(0); + + if let Some(ref preset_name) = self.audio_preset_name { + builder = builder.preset_name(preset_name); + } + + let profile = builder.build(); + + if let Some(element_properties) = self.audio_element_properties { + profile.set_element_properties(element_properties); + } + + profile + }; + + let container_profile = { + let mut builder = gst_pbutils::EncodingContainerProfile::builder(&self.container_caps) + .add_profile(&video_profile) + .add_profile(&audio_profile) + .presence(0); + + if let Some(ref preset_name) = self.container_preset_name { + builder = builder.preset_name(preset_name); + } + + let profile = builder.build(); + + if let Some(element_properties) = self.container_element_properties { + profile.set_element_properties(element_properties); + } + + profile + }; + + container_profile + } +} diff --git a/src/recording.rs b/src/recording.rs index 077f25eca..4ae85ac75 100644 --- a/src/recording.rs +++ b/src/recording.rs @@ -20,7 +20,7 @@ use crate::{ audio_device::{self, Class as AudioDeviceClass}, cancelled::Cancelled, help::{ErrorExt, ResultExt}, - pipeline_builder::PipelineBuilder, + pipeline::PipelineBuilder, screencast_session::{CursorMode, PersistMode, ScreencastSession, SourceType, Stream}, settings::{CaptureMode, Settings, VideoFormat}, timer::Timer, From c1a542a0ddeaf488e1600729372272a38330a07c Mon Sep 17 00:00:00 2001 From: SeaDve Date: Thu, 25 Aug 2022 14:21:06 +0800 Subject: [PATCH 17/46] Init gst --- src/pipeline/element_properties.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/pipeline/element_properties.rs b/src/pipeline/element_properties.rs index 3cac1eb3e..f9b8acc41 100644 --- a/src/pipeline/element_properties.rs +++ b/src/pipeline/element_properties.rs @@ -235,6 +235,8 @@ mod tests { #[test] fn element_properties_general_builder() { + gst::init().unwrap(); + let elem_props = ElementProperties::builder_general() .field("string-prop", "hi") .field("boolean-prop", true) @@ -247,6 +249,8 @@ mod tests { #[test] fn element_properties_map_builder() { + gst::init().unwrap(); + let props_map = ElementFactoryPropertiesMap::new("vp8enc") .field("cq-level", 13) .field("resize-allowed", false); @@ -271,6 +275,8 @@ mod tests { #[test] fn element_factory_properties_map_field_from_str() { + gst::init().unwrap(); + let prop_map_s = ElementFactoryPropertiesMap::new("vp8enc") .field("threads", 16) .field_from_str("keyframe-mode", "disabled") From 86f9d5ffd0254820724525b644014bca9364c60c Mon Sep 17 00:00:00 2001 From: SeaDve Date: Thu, 25 Aug 2022 14:28:10 +0800 Subject: [PATCH 18/46] Drop misleading docs --- src/pipeline/profile.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pipeline/profile.rs b/src/pipeline/profile.rs index 5095b20fb..3c82e935c 100644 --- a/src/pipeline/profile.rs +++ b/src/pipeline/profile.rs @@ -62,19 +62,16 @@ impl Builder { self } - /// Appends to the container element properties. pub fn container_element_properties(mut self, element_properties: ElementProperties) -> Self { self.container_element_properties = Some(element_properties); self } - /// Appends to the video element properties. pub fn video_element_properties(mut self, element_properties: ElementProperties) -> Self { self.video_element_properties = Some(element_properties); self } - /// Appends to the audio element properties. pub fn audio_element_properties(mut self, element_properties: ElementProperties) -> Self { self.audio_element_properties = Some(element_properties); self From 8f084efb1b97e9edffeb76df5bb9cc663c7933bf Mon Sep 17 00:00:00 2001 From: SeaDve Date: Thu, 25 Aug 2022 14:32:10 +0800 Subject: [PATCH 19/46] Drop useless ProfileBuilder constructor --- src/pipeline/mod.rs | 40 +++++++++++++++++++++------------------- src/pipeline/profile.rs | 17 +---------------- 2 files changed, 22 insertions(+), 35 deletions(-) diff --git a/src/pipeline/mod.rs b/src/pipeline/mod.rs index 23a1649a2..8913e4701 100644 --- a/src/pipeline/mod.rs +++ b/src/pipeline/mod.rs @@ -183,26 +183,28 @@ fn create_profile(video_format: VideoFormat) -> gst_pbutils::EncodingContainerPr let thread_count = ideal_thread_count(); let container_profile = match video_format { - VideoFormat::Webm => { - ProfileBuilder::new_simple("video/webm", "video/x-vp8", "audio/x-opus") - .video_preset("vp8enc") - .video_element_properties( - ElementProperties::builder_map() - .item( - ElementFactoryPropertiesMap::new("vp8enc") - .field("max-quantizer", 17) - .field("cpu-used", 16) - .field("cq-level", 13) - .field("deadline", 1) - .field("static-threshold", 100) - .field_from_str("keyframe-mode", "disabled") - .field("buffer-size", 20000) - .field("threads", thread_count), - ) - .build(), + VideoFormat::Webm => ProfileBuilder::new( + caps("video/webm"), + caps("video/x-vp8"), + caps("audio/x-opus"), + ) + .video_preset("vp8enc") + .video_element_properties( + ElementProperties::builder_map() + .item( + ElementFactoryPropertiesMap::new("vp8enc") + .field("max-quantizer", 17) + .field("cpu-used", 16) + .field("cq-level", 13) + .field("deadline", 1) + .field("static-threshold", 100) + .field_from_str("keyframe-mode", "disabled") + .field("buffer-size", 20000) + .field("threads", thread_count), ) - .build() - } + .build(), + ) + .build(), VideoFormat::Mkv => ProfileBuilder::new( caps("video/x-matroska"), gst::Caps::builder("video/x-h264") diff --git a/src/pipeline/profile.rs b/src/pipeline/profile.rs index 3c82e935c..fd18cd067 100644 --- a/src/pipeline/profile.rs +++ b/src/pipeline/profile.rs @@ -1,9 +1,6 @@ use gst_pbutils::prelude::*; -use super::{ - caps, - element_properties::{ElementProperties, ElementPropertiesEncodingProfileExt}, -}; +use super::element_properties::{ElementProperties, ElementPropertiesEncodingProfileExt}; pub struct Builder { container_caps: gst::Caps, @@ -35,18 +32,6 @@ impl Builder { } } - pub fn new_simple( - container_caps_name: &str, - video_caps_name: &str, - audio_caps_name: &str, - ) -> Self { - Self::new( - caps(container_caps_name), - caps(video_caps_name), - caps(audio_caps_name), - ) - } - pub fn container_preset(mut self, preset_name: &str) -> Self { self.container_preset_name = Some(preset_name.to_string()); self From fb2a004e7e7d1f80b043e70a6d5341a77d1b4a1a Mon Sep 17 00:00:00 2001 From: SeaDve Date: Thu, 25 Aug 2022 14:42:19 +0800 Subject: [PATCH 20/46] Update todo --- src/pipeline/mod.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/pipeline/mod.rs b/src/pipeline/mod.rs index 8913e4701..3af120203 100644 --- a/src/pipeline/mod.rs +++ b/src/pipeline/mod.rs @@ -21,6 +21,16 @@ use self::{ }; use crate::{screencast_session::Stream, settings::VideoFormat}; +// TODO +// Plugin preferences ui (Show summary on drop down): +// * Bring back GIF support `gifenc repeat=-1 speed=30` +// * Handle missing plugins (Hide profile if missing) +// * Option for vaapi profiles +// +// * Do we need restrictions? +// * Can we drop filter elements (videorate, videoconvert, videoscale, audioconvert) and let encodebin handle it? +// * Add tests + const MAX_THREAD_COUNT: u32 = 64; const GIF_FRAMERATE_OVERRIDE: u32 = 15; @@ -178,8 +188,6 @@ impl PipelineBuilder { /// Create an encoding profile based on video format fn create_profile(video_format: VideoFormat) -> gst_pbutils::EncodingContainerProfile { - // TODO Option for vaapi - let thread_count = ideal_thread_count(); let container_profile = match video_format { @@ -243,7 +251,7 @@ fn create_profile(video_format: VideoFormat) -> gst_pbutils::EncodingContainerPr .build(), ) .build(), - VideoFormat::Gif => todo!("Unsupported video format"), // FIXME + VideoFormat::Gif => panic!("Unsupported video format"), }; tracing::debug!(suggested_file_extension = ?container_profile.file_extension()); From da029a8c981d50c4c2feb4bf755128f047ec5743 Mon Sep 17 00:00:00 2001 From: SeaDve Date: Thu, 25 Aug 2022 14:56:58 +0800 Subject: [PATCH 21/46] Don't panic when failed to set field from str --- src/pipeline/element_properties.rs | 46 +++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/src/pipeline/element_properties.rs b/src/pipeline/element_properties.rs index f9b8acc41..8a6fc284b 100644 --- a/src/pipeline/element_properties.rs +++ b/src/pipeline/element_properties.rs @@ -193,6 +193,38 @@ impl ElementFactoryPropertiesMap { property_name: &str, string: &str, ) -> Result { + self.set_field_from_str(property_name, string)?; + Ok(self) + } + + /// Parses the given string into a property of element from the + /// given `factory_name` with type based on the property's param spec. + /// + /// This works similar to `GObjectExtManualGst::set_property_from_str`. + /// + /// Note: The property will not be set if any of `factory_name`, `property_name` + /// or `string` is invalid. + pub fn field_from_str(mut self, property_name: &str, string: &str) -> Self { + if let Err(err) = self.set_field_from_str(property_name, string) { + tracing::error!( + "Failed to set property `{}` to `{}`: {:?}", + property_name, + string, + err + ); + } + self + } + + pub fn into_inner(self) -> gst::Structure { + self.0 + } + + fn set_field_from_str( + &mut self, + property_name: &str, + string: &str, + ) -> Result<(), glib::BoolError> { let factory_name = self.0.name(); let element = gst::ElementFactory::make(factory_name, None).map_err(|_| { glib::bool_error!( @@ -213,19 +245,7 @@ impl ElementFactoryPropertiesMap { ) }; self.0.set_value(property_name, value); - Ok(self) - } - - /// Parses the given string into a property of element from the - /// given `factory_name` with type based on the property's param spec. - /// - /// This works similar to `GObjectExtManualGst::set_property_from_str`. - pub fn field_from_str(self, property_name: &str, string: &str) -> Self { - self.field_try_from_str(property_name, string).unwrap() - } - - pub fn into_inner(self) -> gst::Structure { - self.0 + Ok(()) } } From 643858568bbd3561d01ca769fe908d282e864780 Mon Sep 17 00:00:00 2001 From: SeaDve Date: Thu, 25 Aug 2022 20:51:22 +0800 Subject: [PATCH 22/46] Refactor ElementFactoryPropertiesMap * Don't create the element * Separate the builder from the actual type --- src/pipeline/element_properties.rs | 224 ++++++++--------------------- src/pipeline/mod.rs | 21 +-- 2 files changed, 73 insertions(+), 172 deletions(-) diff --git a/src/pipeline/element_properties.rs b/src/pipeline/element_properties.rs index 8a6fc284b..448a89fe9 100644 --- a/src/pipeline/element_properties.rs +++ b/src/pipeline/element_properties.rs @@ -1,11 +1,10 @@ +use anyhow::{anyhow, Context, Result}; use gst::prelude::*; use gtk::glib::{ self, translate::{ToGlibPtr, UnsafeFrom}, }; -use std::ops::Deref; - pub trait ElementPropertiesEncodingProfileExt { fn set_element_properties(&self, element_properties: ElementProperties); } @@ -21,45 +20,9 @@ impl> ElementPropertiesEncodingProfileExt f } } -/// Wrapper around `gst::Structure` for `element-properties` -/// property of `EncodingProfile`. -/// -/// # Examples -/// -/// ```rust -/// ElementProperties::builder_general() -/// .field("threads", 16) -/// .build(); -/// ``` -/// -/// ```rust -/// ElementProperties::builder_map() -/// .field( -/// ElementFactoryPropertiesMap::new("vp8enc") -/// .field("max-quantizer", 17) -/// .field_from_str("keyframe-mode", "disabled") -/// .field("buffer-size", 20000) -/// .field("threads", 16), -/// ) -/// .build() -/// ``` #[derive(Debug, Clone)] pub struct ElementProperties(gst::Structure); -impl Deref for ElementProperties { - type Target = gst::StructureRef; - - fn deref(&self) -> &gst::StructureRef { - self.0.as_ref() - } -} - -impl From for gst::Structure { - fn from(e: ElementProperties) -> Self { - e.into_inner() - } -} - impl ElementProperties { /// Creates an `ElementProperties` builder that build into /// something similar to the following: @@ -68,16 +31,8 @@ impl ElementProperties { /// [openh264enc, gop-size=32, ], /// [x264enc, key-int-max=32, tune=zerolatency], /// } - pub fn builder_map() -> ElementPropertiesMapBuilder { - ElementPropertiesMapBuilder::new() - } - - /// Creates an `ElementProperties` builder that build into - /// something similar to the following: - /// - /// [element-properties, boolean-prop=true, string-prop="hi"] - pub fn builder_general() -> ElementPropertiesGeneralBuilder { - ElementPropertiesGeneralBuilder::new() + pub fn builder() -> ElementPropertiesBuilder { + ElementPropertiesBuilder::new() } pub fn into_inner(self) -> gst::Structure { @@ -87,54 +42,16 @@ impl ElementProperties { #[must_use = "The builder must be built to be used"] #[derive(Debug, Clone)] -pub struct ElementPropertiesGeneralBuilder { - structure: gst::Structure, -} - -impl Default for ElementPropertiesGeneralBuilder { - fn default() -> Self { - Self::new() - } -} - -impl ElementPropertiesGeneralBuilder { - pub fn new() -> Self { - Self { - structure: gst::Structure::new_empty("element-properties"), - } - } - - pub fn field(mut self, property_name: &str, value: T) -> Self - where - T: ToSendValue + Sync, - { - self.structure.set(property_name, value); - self - } - - pub fn build(self) -> ElementProperties { - ElementProperties(self.structure) - } -} - -#[must_use = "The builder must be built to be used"] -#[derive(Debug, Clone)] -pub struct ElementPropertiesMapBuilder { +pub struct ElementPropertiesBuilder { map: Vec, } -impl Default for ElementPropertiesMapBuilder { - fn default() -> Self { - Self::new() - } -} - -impl ElementPropertiesMapBuilder { +impl ElementPropertiesBuilder { pub fn new() -> Self { Self { map: Vec::new() } } - /// Insert a new `element-properties-map` map entry. + /// Insert a new `element-properties-map` map item. pub fn item(mut self, structure: ElementFactoryPropertiesMap) -> Self { self.map.push(structure.into_inner().to_send_value()); self @@ -165,38 +82,61 @@ impl ElementPropertiesMapBuilder { #[derive(Debug, Clone)] pub struct ElementFactoryPropertiesMap(gst::Structure); -impl From for gst::Structure { - fn from(e: ElementFactoryPropertiesMap) -> Self { - e.into_inner() +impl ElementFactoryPropertiesMap { + pub fn builder(factory_name: &str) -> ElementFactoryPropertiesMapBuilder { + ElementFactoryPropertiesMapBuilder::new(factory_name) + } + + pub fn into_inner(self) -> gst::Structure { + self.0 + } + + fn set_field_from_str(&mut self, property_name: &str, string: &str) -> Result<()> { + let factory_name = self.0.name(); + let element_type = gst::ElementFactory::find(factory_name) + .ok_or_else(|| anyhow!("Failed to find factory with name `{}`", factory_name))? + .load() + .with_context(|| anyhow!("Failed to load factory with name `{}`", factory_name))? + .element_type(); + let pspec = glib::object::ObjectClass::from_type(element_type) + .ok_or_else(|| anyhow!("Failed to create object class from type `{}`", element_type))? + .find_property(property_name) + .ok_or_else(|| { + glib::bool_error!( + "Property `{}` not found on type `{}`", + property_name, + element_type + ) + })?; + let value = unsafe { + glib::SendValue::unsafe_from( + glib::Value::deserialize_with_pspec(string, &pspec)?.into_raw(), + ) + }; + self.0.set_value(property_name, value); + Ok(()) } } -impl ElementFactoryPropertiesMap { +pub struct ElementFactoryPropertiesMapBuilder { + prop_map: ElementFactoryPropertiesMap, +} + +impl ElementFactoryPropertiesMapBuilder { pub fn new(factory_name: &str) -> Self { - Self(gst::Structure::new_empty(factory_name)) + Self { + prop_map: ElementFactoryPropertiesMap(gst::Structure::new_empty(factory_name)), + } } pub fn field(mut self, property_name: &str, value: T) -> Self where T: ToSendValue + Sync, { - self.0.set(property_name, value); + self.prop_map.0.set(property_name, value); self } - /// Parses the given string into a property of element from the - /// given `factory_name` with type based on the property's param spec. - /// - /// This works similar to `GObjectExtManualGst::try_set_property_from_str`. - pub fn field_try_from_str( - mut self, - property_name: &str, - string: &str, - ) -> Result { - self.set_field_from_str(property_name, string)?; - Ok(self) - } - /// Parses the given string into a property of element from the /// given `factory_name` with type based on the property's param spec. /// @@ -205,7 +145,7 @@ impl ElementFactoryPropertiesMap { /// Note: The property will not be set if any of `factory_name`, `property_name` /// or `string` is invalid. pub fn field_from_str(mut self, property_name: &str, string: &str) -> Self { - if let Err(err) = self.set_field_from_str(property_name, string) { + if let Err(err) = self.prop_map.set_field_from_str(property_name, string) { tracing::error!( "Failed to set property `{}` to `{}`: {:?}", property_name, @@ -216,36 +156,8 @@ impl ElementFactoryPropertiesMap { self } - pub fn into_inner(self) -> gst::Structure { - self.0 - } - - fn set_field_from_str( - &mut self, - property_name: &str, - string: &str, - ) -> Result<(), glib::BoolError> { - let factory_name = self.0.name(); - let element = gst::ElementFactory::make(factory_name, None).map_err(|_| { - glib::bool_error!( - "Failed to create element from factory name `{}`", - factory_name - ) - })?; - let pspec = element.find_property(property_name).ok_or_else(|| { - glib::bool_error!( - "Property `{}` not found on type `{}`", - property_name, - element.type_() - ) - })?; - let value = unsafe { - glib::SendValue::unsafe_from( - glib::Value::deserialize_with_pspec(string, &pspec)?.into_raw(), - ) - }; - self.0.set_value(property_name, value); - Ok(()) + pub fn build(self) -> ElementFactoryPropertiesMap { + self.prop_map } } @@ -254,42 +166,27 @@ mod tests { use super::*; #[test] - fn element_properties_general_builder() { + fn element_properties_builder() { gst::init().unwrap(); - let elem_props = ElementProperties::builder_general() - .field("string-prop", "hi") - .field("boolean-prop", true) - .build(); - assert_eq!(elem_props.n_fields(), 2); - assert_eq!(elem_props.name(), "element-properties"); - assert_eq!(elem_props.get::("string-prop").unwrap(), "hi"); - assert!(elem_props.get::("boolean-prop").unwrap()); - } - - #[test] - fn element_properties_map_builder() { - gst::init().unwrap(); - - let props_map = ElementFactoryPropertiesMap::new("vp8enc") + let props_map = ElementFactoryPropertiesMap::builder("vp8enc") .field("cq-level", 13) - .field("resize-allowed", false); + .field("resize-allowed", false) + .build(); let props_map_s = props_map.clone().into_inner(); assert_eq!(props_map_s.n_fields(), 2); assert_eq!(props_map_s.name(), "vp8enc"); assert_eq!(props_map_s.get::("cq-level").unwrap(), 13); assert!(!props_map_s.get::("resize-allowed").unwrap()); - let elem_props = ElementProperties::builder_map() - .item(props_map.clone()) - .build(); - assert_eq!(elem_props.n_fields(), 1); + let elem_props = ElementProperties::builder().item(props_map.clone()).build(); + assert_eq!(elem_props.0.n_fields(), 1); - let list = elem_props.get::("map").unwrap(); + let list = elem_props.0.get::("map").unwrap(); assert_eq!(list.len(), 1); assert_eq!( list.get(0).unwrap().get::().unwrap(), - gst::Structure::from(props_map) + props_map.into_inner() ); } @@ -297,9 +194,10 @@ mod tests { fn element_factory_properties_map_field_from_str() { gst::init().unwrap(); - let prop_map_s = ElementFactoryPropertiesMap::new("vp8enc") + let prop_map_s = ElementFactoryPropertiesMap::builder("vp8enc") .field("threads", 16) .field_from_str("keyframe-mode", "disabled") + .build() .into_inner(); assert_eq!(prop_map_s.n_fields(), 2); assert_eq!(prop_map_s.name(), "vp8enc"); diff --git a/src/pipeline/mod.rs b/src/pipeline/mod.rs index 3af120203..91f7f8c8c 100644 --- a/src/pipeline/mod.rs +++ b/src/pipeline/mod.rs @@ -198,9 +198,9 @@ fn create_profile(video_format: VideoFormat) -> gst_pbutils::EncodingContainerPr ) .video_preset("vp8enc") .video_element_properties( - ElementProperties::builder_map() + ElementProperties::builder() .item( - ElementFactoryPropertiesMap::new("vp8enc") + ElementFactoryPropertiesMap::builder("vp8enc") .field("max-quantizer", 17) .field("cpu-used", 16) .field("cq-level", 13) @@ -208,7 +208,8 @@ fn create_profile(video_format: VideoFormat) -> gst_pbutils::EncodingContainerPr .field("static-threshold", 100) .field_from_str("keyframe-mode", "disabled") .field("buffer-size", 20000) - .field("threads", thread_count), + .field("threads", thread_count) + .build(), ) .build(), ) @@ -222,12 +223,13 @@ fn create_profile(video_format: VideoFormat) -> gst_pbutils::EncodingContainerPr ) .video_preset("x264enc") .video_element_properties( - ElementProperties::builder_map() + ElementProperties::builder() .item( - ElementFactoryPropertiesMap::new("x264enc") + ElementFactoryPropertiesMap::builder("x264enc") .field("qp-max", 17) .field_from_str("speed-preset", "superfast") - .field("threads", thread_count), + .field("threads", thread_count) + .build(), ) .build(), ) @@ -241,12 +243,13 @@ fn create_profile(video_format: VideoFormat) -> gst_pbutils::EncodingContainerPr ) .video_preset("x264enc") .video_element_properties( - ElementProperties::builder_map() + ElementProperties::builder() .item( - ElementFactoryPropertiesMap::new("x264enc") + ElementFactoryPropertiesMap::builder("x264enc") .field("qp-max", 17) .field_from_str("speed-preset", "superfast") - .field("threads", thread_count), + .field("threads", thread_count) + .build(), ) .build(), ) From ebfabdf4ed048556e03f4b9a4c2ef0928bcffac9 Mon Sep 17 00:00:00 2001 From: SeaDve Date: Fri, 26 Aug 2022 21:22:49 +0800 Subject: [PATCH 23/46] Refactor profiles --- src/{pipeline => }/element_properties.rs | 23 +- src/main.rs | 2 + src/{pipeline/mod.rs => pipeline.rs} | 98 +---- src/pipeline/profile.rs | 121 ------ src/profile.rs | 471 +++++++++++++++++++++++ src/utils.rs | 9 +- 6 files changed, 505 insertions(+), 219 deletions(-) rename src/{pipeline => }/element_properties.rs (92%) rename src/{pipeline/mod.rs => pipeline.rs} (81%) delete mode 100644 src/pipeline/profile.rs create mode 100644 src/profile.rs diff --git a/src/pipeline/element_properties.rs b/src/element_properties.rs similarity index 92% rename from src/pipeline/element_properties.rs rename to src/element_properties.rs index 448a89fe9..667ec4ba5 100644 --- a/src/pipeline/element_properties.rs +++ b/src/element_properties.rs @@ -5,11 +5,11 @@ use gtk::glib::{ translate::{ToGlibPtr, UnsafeFrom}, }; -pub trait ElementPropertiesEncodingProfileExt { +pub trait EncodingProfileExtManual { fn set_element_properties(&self, element_properties: ElementProperties); } -impl> ElementPropertiesEncodingProfileExt for P { +impl> EncodingProfileExtManual for P { fn set_element_properties(&self, element_properties: ElementProperties) { unsafe { gst_pbutils::ffi::gst_encoding_profile_set_element_properties( @@ -20,9 +20,20 @@ impl> ElementPropertiesEncodingProfileExt f } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, glib::Boxed)] +#[boxed_type(name = "KoohaElementProperties")] pub struct ElementProperties(gst::Structure); +impl Default for ElementProperties { + fn default() -> Self { + Self( + gst::Structure::builder("element-properties-map") + .field("map", gst::List::from_values([])) + .build(), + ) + } +} + impl ElementProperties { /// Creates an `ElementProperties` builder that build into /// something similar to the following: @@ -32,7 +43,7 @@ impl ElementProperties { /// [x264enc, key-int-max=32, tune=zerolatency], /// } pub fn builder() -> ElementPropertiesBuilder { - ElementPropertiesBuilder::new() + ElementPropertiesBuilder { map: Vec::new() } } pub fn into_inner(self) -> gst::Structure { @@ -47,10 +58,6 @@ pub struct ElementPropertiesBuilder { } impl ElementPropertiesBuilder { - pub fn new() -> Self { - Self { map: Vec::new() } - } - /// Insert a new `element-properties-map` map item. pub fn item(mut self, structure: ElementFactoryPropertiesMap) -> Self { self.map.push(structure.into_inner().to_send_value()); diff --git a/src/main.rs b/src/main.rs index b6f285d34..132888e6b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,8 +26,10 @@ mod area_selector; mod audio_device; mod cancelled; mod config; +mod element_properties; mod help; mod pipeline; +mod profile; mod recording; mod screencast_session; mod settings; diff --git a/src/pipeline/mod.rs b/src/pipeline.rs similarity index 81% rename from src/pipeline/mod.rs rename to src/pipeline.rs index 91f7f8c8c..968dcde37 100644 --- a/src/pipeline/mod.rs +++ b/src/pipeline.rs @@ -1,25 +1,14 @@ -mod element_properties; -mod profile; - use anyhow::{bail, Context, Ok, Result}; use gst::prelude::*; use gst_pbutils::prelude::*; -use gtk::{ - glib, - graphene::{Rect, Size}, -}; +use gtk::graphene::{Rect, Size}; use std::{ - cmp, os::unix::io::RawFd, path::{Path, PathBuf}, }; -use self::{ - element_properties::{ElementFactoryPropertiesMap, ElementProperties}, - profile::Builder as ProfileBuilder, -}; -use crate::{screencast_session::Stream, settings::VideoFormat}; +use crate::{profile::BuiltinProfiles, screencast_session::Stream, settings::VideoFormat, utils}; // TODO // Plugin preferences ui (Show summary on drop down): @@ -31,7 +20,6 @@ use crate::{screencast_session::Stream, settings::VideoFormat}; // * Can we drop filter elements (videorate, videoconvert, videoscale, audioconvert) and let encodebin handle it? // * Add tests -const MAX_THREAD_COUNT: u32 = 64; const GIF_FRAMERATE_OVERRIDE: u32 = 15; #[derive(Debug)] @@ -188,72 +176,13 @@ impl PipelineBuilder { /// Create an encoding profile based on video format fn create_profile(video_format: VideoFormat) -> gst_pbutils::EncodingContainerProfile { - let thread_count = ideal_thread_count(); - let container_profile = match video_format { - VideoFormat::Webm => ProfileBuilder::new( - caps("video/webm"), - caps("video/x-vp8"), - caps("audio/x-opus"), - ) - .video_preset("vp8enc") - .video_element_properties( - ElementProperties::builder() - .item( - ElementFactoryPropertiesMap::builder("vp8enc") - .field("max-quantizer", 17) - .field("cpu-used", 16) - .field("cq-level", 13) - .field("deadline", 1) - .field("static-threshold", 100) - .field_from_str("keyframe-mode", "disabled") - .field("buffer-size", 20000) - .field("threads", thread_count) - .build(), - ) - .build(), - ) - .build(), - VideoFormat::Mkv => ProfileBuilder::new( - caps("video/x-matroska"), - gst::Caps::builder("video/x-h264") - .field("profile", "baseline") - .build(), - caps("audio/x-opus"), - ) - .video_preset("x264enc") - .video_element_properties( - ElementProperties::builder() - .item( - ElementFactoryPropertiesMap::builder("x264enc") - .field("qp-max", 17) - .field_from_str("speed-preset", "superfast") - .field("threads", thread_count) - .build(), - ) - .build(), - ) - .build(), - VideoFormat::Mp4 => ProfileBuilder::new( - caps("video/quicktime"), - gst::Caps::builder("video/x-h264") - .field("profile", "baseline") - .build(), - caps("audio/mpeg"), - ) - .video_preset("x264enc") - .video_element_properties( - ElementProperties::builder() - .item( - ElementFactoryPropertiesMap::builder("x264enc") - .field("qp-max", 17) - .field_from_str("speed-preset", "superfast") - .field("threads", thread_count) - .build(), - ) - .build(), - ) - .build(), + VideoFormat::Webm => BuiltinProfiles::WebM.get().to_encoding_profile().unwrap(), + VideoFormat::Mkv => BuiltinProfiles::Matroska + .get() + .to_encoding_profile() + .unwrap(), + VideoFormat::Mp4 => BuiltinProfiles::Mp4.get().to_encoding_profile().unwrap(), VideoFormat::Gif => panic!("Unsupported video format"), }; @@ -262,11 +191,6 @@ fn create_profile(video_format: VideoFormat) -> gst_pbutils::EncodingContainerPr container_profile } -/// Helper function to create a caps with just a name. -fn caps(name: &str) -> gst::Caps { - gst::Caps::new_simple(name, &[]) -} - /// Helper function for more helpful error messages when failing /// to make an element. fn element_factory_make(factory_name: &str) -> Result { @@ -298,7 +222,7 @@ fn videoconvert_with_default() -> Result { conv.set_property("chroma-mode", gst_video::VideoChromaMode::None); conv.set_property("dither", gst_video::VideoDitherMethod::None); conv.set_property("matrix-mode", gst_video::VideoMatrixMode::OutputOnly); - conv.set_property("n-threads", ideal_thread_count()); + conv.set_property("n-threads", utils::ideal_thread_count()); Ok(conv) } @@ -459,10 +383,6 @@ fn round_to_even_f32(number: f32) -> i32 { number as i32 / 2 * 2 } -fn ideal_thread_count() -> u32 { - cmp::min(glib::num_processors(), MAX_THREAD_COUNT) -} - #[cfg(test)] mod test { use super::*; diff --git a/src/pipeline/profile.rs b/src/pipeline/profile.rs deleted file mode 100644 index fd18cd067..000000000 --- a/src/pipeline/profile.rs +++ /dev/null @@ -1,121 +0,0 @@ -use gst_pbutils::prelude::*; - -use super::element_properties::{ElementProperties, ElementPropertiesEncodingProfileExt}; - -pub struct Builder { - container_caps: gst::Caps, - container_preset_name: Option, - container_element_properties: Option, - - video_caps: gst::Caps, - video_preset_name: Option, - video_element_properties: Option, - - audio_caps: gst::Caps, - audio_preset_name: Option, - audio_element_properties: Option, -} - -#[allow(dead_code)] -impl Builder { - pub fn new(container_caps: gst::Caps, video_caps: gst::Caps, audio_caps: gst::Caps) -> Self { - Self { - container_caps, - container_preset_name: None, - container_element_properties: None, - video_caps, - video_preset_name: None, - video_element_properties: None, - audio_caps, - audio_preset_name: None, - audio_element_properties: None, - } - } - - pub fn container_preset(mut self, preset_name: &str) -> Self { - self.container_preset_name = Some(preset_name.to_string()); - self - } - - pub fn video_preset(mut self, preset_name: &str) -> Self { - self.video_preset_name = Some(preset_name.to_string()); - self - } - - pub fn audio_preset(mut self, preset_name: &str) -> Self { - self.audio_preset_name = Some(preset_name.to_string()); - self - } - - pub fn container_element_properties(mut self, element_properties: ElementProperties) -> Self { - self.container_element_properties = Some(element_properties); - self - } - - pub fn video_element_properties(mut self, element_properties: ElementProperties) -> Self { - self.video_element_properties = Some(element_properties); - self - } - - pub fn audio_element_properties(mut self, element_properties: ElementProperties) -> Self { - self.audio_element_properties = Some(element_properties); - self - } - - pub fn build(self) -> gst_pbutils::EncodingContainerProfile { - let video_profile = { - let mut builder = - gst_pbutils::EncodingVideoProfile::builder(&self.video_caps).presence(0); - - if let Some(ref preset_name) = self.video_preset_name { - builder = builder.preset_name(preset_name); - } - - let profile = builder.build(); - - if let Some(element_properties) = self.video_element_properties { - profile.set_element_properties(element_properties); - } - - profile - }; - - let audio_profile = { - let mut builder = - gst_pbutils::EncodingAudioProfile::builder(&self.audio_caps).presence(0); - - if let Some(ref preset_name) = self.audio_preset_name { - builder = builder.preset_name(preset_name); - } - - let profile = builder.build(); - - if let Some(element_properties) = self.audio_element_properties { - profile.set_element_properties(element_properties); - } - - profile - }; - - let container_profile = { - let mut builder = gst_pbutils::EncodingContainerProfile::builder(&self.container_caps) - .add_profile(&video_profile) - .add_profile(&audio_profile) - .presence(0); - - if let Some(ref preset_name) = self.container_preset_name { - builder = builder.preset_name(preset_name); - } - - let profile = builder.build(); - - if let Some(element_properties) = self.container_element_properties { - profile.set_element_properties(element_properties); - } - - profile - }; - - container_profile - } -} diff --git a/src/profile.rs b/src/profile.rs new file mode 100644 index 000000000..c09fb3889 --- /dev/null +++ b/src/profile.rs @@ -0,0 +1,471 @@ +use anyhow::{anyhow, ensure, Result}; +use gst_pbutils::prelude::*; +use gtk::{glib, subclass::prelude::*}; + +use std::cell::RefCell; + +use crate::{ + element_properties::{ + ElementFactoryPropertiesMap, ElementProperties, EncodingProfileExtManual, + }, + utils, +}; + +pub enum BuiltinProfiles { + WebM, + Mp4, + Matroska, +} + +impl BuiltinProfiles { + pub fn get(self) -> Profile { + match self { + Self::WebM => BUILTIN_PROFILES.with(|profiles| profiles[0].clone()), + Self::Mp4 => BUILTIN_PROFILES.with(|profiles| profiles[1].clone()), + Self::Matroska => BUILTIN_PROFILES.with(|profiles| profiles[2].clone()), + } + } +} + +thread_local! { + static BUILTIN_PROFILES: Vec = vec![ + { + let profile = Profile::new("WebM"); + profile.set_container_preset_name("webmmux"); + profile.set_video_preset_name("vp8enc"); + profile.set_video_element_properties( + ElementProperties::builder() + .item( + ElementFactoryPropertiesMap::builder("vp8enc") + .field("max-quantizer", 17) + .field("cpu-used", 16) + .field("cq-level", 13) + .field("deadline", 1) + .field("static-threshold", 100) + .field_from_str("keyframe-mode", "disabled") + .field("buffer-size", 20000) + .field("threads", utils::ideal_thread_count()) + .build(), + ) + .build(), + ); + profile.set_audio_preset_name("opusenc"); + profile + }, + { + // TODO support "profile" = baseline + let profile = Profile::new("MP4"); + profile.set_container_preset_name("mp4mux"); + profile.set_video_preset_name("x264enc"); + profile.set_video_element_properties( + ElementProperties::builder() + .item( + ElementFactoryPropertiesMap::builder("x264enc") + .field("qp-max", 17) + .field_from_str("speed-preset", "superfast") + .field("threads", utils::ideal_thread_count()) + .build(), + ) + .build(), + ); + profile.set_audio_preset_name("lamemp3enc"); + profile + }, + { + let profile = Profile::new("Matroska"); + profile.set_container_preset_name("matroskamux"); + profile.set_video_preset_name("x264enc"); + profile.set_video_element_properties( + ElementProperties::builder() + .item( + ElementFactoryPropertiesMap::builder("x264enc") + .field("qp-max", 17) + .field_from_str("speed-preset", "superfast") + .field("threads", utils::ideal_thread_count()) + .build(), + ) + .build(), + ); + profile.set_audio_preset_name("opusenc"); + profile + }, + ]; +} + +mod imp { + use super::*; + use once_cell::sync::Lazy; + + #[derive(Debug, Default)] + pub struct Profile { + pub(super) name: RefCell, + pub(super) container_preset_name: RefCell, + pub(super) container_element_properties: RefCell, + pub(super) video_preset_name: RefCell, + pub(super) video_element_properties: RefCell, + pub(super) audio_preset_name: RefCell, + pub(super) audio_element_properties: RefCell, + } + + #[glib::object_subclass] + impl ObjectSubclass for Profile { + const NAME: &'static str = "KoohaProfile"; + type Type = super::Profile; + } + + impl ObjectImpl for Profile { + fn properties() -> &'static [glib::ParamSpec] { + static PROPERTIES: Lazy> = Lazy::new(|| { + vec![ + glib::ParamSpecString::builder("name") + .flags( + glib::ParamFlags::READWRITE + | glib::ParamFlags::EXPLICIT_NOTIFY + | glib::ParamFlags::CONSTRUCT, + ) + .build(), + glib::ParamSpecString::builder("container-preset-name") + .flags(glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY) + .build(), + glib::ParamSpecBoxed::builder( + "container-element-properties", + ElementProperties::static_type(), + ) + .flags(glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY) + .build(), + glib::ParamSpecString::builder("video-preset-name") + .flags(glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY) + .build(), + glib::ParamSpecBoxed::builder( + "video-element-properties", + ElementProperties::static_type(), + ) + .flags(glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY) + .build(), + glib::ParamSpecString::builder("audio-preset-name") + .flags(glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY) + .build(), + glib::ParamSpecBoxed::builder( + "audio-element-properties", + ElementProperties::static_type(), + ) + .flags(glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY) + .build(), + ] + }); + + PROPERTIES.as_ref() + } + + fn set_property( + &self, + obj: &Self::Type, + _id: usize, + value: &glib::Value, + pspec: &glib::ParamSpec, + ) { + match pspec.name() { + "name" => { + let name = value.get().unwrap(); + obj.set_name(name); + } + "container-preset-name" => { + let container_preset_name = value.get().unwrap(); + obj.set_container_preset_name(container_preset_name); + } + "container-element-properties" => { + let container_element_properties = value.get().unwrap(); + obj.set_container_element_properties(container_element_properties); + } + "video-preset-name" => { + let video_preset_name = value.get().unwrap(); + obj.set_video_preset_name(video_preset_name); + } + "video-element-properties" => { + let video_element_properties = value.get().unwrap(); + obj.set_video_element_properties(video_element_properties); + } + "audio-preset-name" => { + let audio_preset_name = value.get().unwrap(); + obj.set_audio_preset_name(audio_preset_name); + } + "audio-element-properties" => { + let audio_element_properties = value.get().unwrap(); + obj.set_audio_element_properties(audio_element_properties); + } + _ => unimplemented!(), + } + } + + fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { + match pspec.name() { + "name" => obj.name().to_value(), + "container-preset-name" => obj.container_preset_name().to_value(), + "container-element-properties" => obj.container_element_properties().to_value(), + "video-preset-name" => obj.video_preset_name().to_value(), + "video-element-properties" => obj.video_element_properties().to_value(), + "audio-preset-name" => obj.audio_preset_name().to_value(), + "audio-element-properties" => obj.audio_element_properties().to_value(), + _ => unimplemented!(), + } + } + } +} + +glib::wrapper! { + pub struct Profile(ObjectSubclass); +} + +impl Profile { + pub fn new(name: &str) -> Self { + glib::Object::builder() + .property("name", name) + .build() + .expect("Failed to create Profile.") + } + + pub fn set_name(&self, name: &str) { + if name == self.name() { + return; + } + + self.imp().name.replace(name.to_string()); + self.notify("name"); + } + + pub fn name(&self) -> String { + self.imp().name.borrow().clone() + } + + pub fn set_container_preset_name(&self, name: &str) { + if name == self.container_preset_name() { + return; + } + + self.imp().container_preset_name.replace(name.to_string()); + self.notify("container-preset-name"); + } + + pub fn container_preset_name(&self) -> String { + self.imp().container_preset_name.borrow().clone() + } + + pub fn set_container_element_properties(&self, properties: ElementProperties) { + if properties == self.container_element_properties() { + return; + } + + self.imp().container_element_properties.replace(properties); + self.notify("container-element-properties"); + } + + pub fn container_element_properties(&self) -> ElementProperties { + self.imp().container_element_properties.borrow().clone() + } + + pub fn set_video_preset_name(&self, name: &str) { + if name == self.video_preset_name() { + return; + } + + self.imp().video_preset_name.replace(name.to_string()); + self.notify("video-preset-name"); + } + + pub fn video_preset_name(&self) -> String { + self.imp().video_preset_name.borrow().clone() + } + + pub fn set_video_element_properties(&self, properties: ElementProperties) { + if properties == self.video_element_properties() { + return; + } + + self.imp().video_element_properties.replace(properties); + self.notify("video-element-properties"); + } + + pub fn video_element_properties(&self) -> ElementProperties { + self.imp().video_element_properties.borrow().clone() + } + + pub fn set_audio_preset_name(&self, name: &str) { + if name == self.audio_preset_name() { + return; + } + + self.imp().audio_preset_name.replace(name.to_string()); + self.notify("audio-preset-name"); + } + + pub fn audio_preset_name(&self) -> String { + self.imp().audio_preset_name.borrow().clone() + } + + pub fn set_audio_element_properties(&self, properties: ElementProperties) { + if properties == self.audio_element_properties() { + return; + } + + self.imp().audio_element_properties.replace(properties); + self.notify("audio-element-properties"); + } + + pub fn audio_element_properties(&self) -> ElementProperties { + self.imp().audio_element_properties.borrow().clone() + } + + pub fn to_encoding_profile(&self) -> Result { + let container_preset_name = self.container_preset_name(); + let container_element_factory = find_element_factory(&container_preset_name)?; + let container_format_caps = profile_format_from_factory(&container_element_factory)?; + + // Video Encoder + let video_preset_name = self.video_preset_name(); + let video_element_factory = find_element_factory(&video_preset_name)?; + let video_format_caps = profile_format_from_factory(&video_element_factory)?; + ensure!( + container_element_factory.can_sink_any_caps(&video_format_caps), + "`{}` src is incompatible on `{}` sink", + video_preset_name, + container_preset_name + ); + let video_profile = gst_pbutils::EncodingVideoProfile::builder(&video_format_caps) + .preset_name(&self.video_preset_name()) + .presence(0) + .build(); + video_profile.set_element_properties(self.video_element_properties()); + + // Audio Encoder + let audio_preset_name = self.audio_preset_name(); + let audio_element_factory = find_element_factory(&audio_preset_name)?; + let audio_format_caps = profile_format_from_factory(&audio_element_factory)?; + ensure!( + container_element_factory.can_sink_any_caps(&audio_format_caps), + "`{}` src is incompatible on `{}` sink", + audio_preset_name, + container_preset_name + ); + let audio_profile = gst_pbutils::EncodingAudioProfile::builder(&audio_format_caps) + .preset_name(&self.audio_preset_name()) + .presence(0) + .build(); + audio_profile.set_element_properties(self.audio_element_properties()); + + // Muxer + let container_profile = + gst_pbutils::EncodingContainerProfile::builder(&container_format_caps) + .add_profile(&video_profile) + .add_profile(&audio_profile) + .presence(0) + .build(); + container_profile.set_element_properties(self.container_element_properties()); + + Ok(container_profile) + } +} + +fn find_element_factory(factory_name: &str) -> Result { + gst::ElementFactory::find(factory_name) + .ok_or_else(|| anyhow!("Failed to find factory `{}`", factory_name)) +} + +fn profile_format_from_factory(factory: &gst::ElementFactory) -> Result { + let factory_name = factory.name(); + + ensure!( + factory.has_type(gst::ElementFactoryType::ENCODER | gst::ElementFactoryType::MUXER), + "Factory`{}` must be an encoder or muxer to be used in a profile", + factory_name + ); + + for template in factory.static_pad_templates() { + if template.direction() == gst::PadDirection::Src { + let template_caps = template.caps(); + if let Some(structure) = template_caps.structure(0) { + let mut caps = gst::Caps::new_empty(); + caps.get_mut() + .unwrap() + .append_structure(structure.to_owned()); + return Ok(caps); + } + } + } + + Err(anyhow!( + "Failed to find profile format for factory `{}`", + factory_name + )) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn new_simple_profile( + container_preset_name: &str, + video_preset_name: &str, + audio_preset_name: &str, + ) -> Profile { + let profile = Profile::new(""); + profile.set_container_preset_name(container_preset_name); + profile.set_video_preset_name(video_preset_name); + profile.set_audio_preset_name(audio_preset_name); + profile + } + + #[test] + fn builtins() { + assert_eq!(BuiltinProfiles::WebM.get().name(), "WebM"); + assert_eq!(BuiltinProfiles::Mp4.get().name(), "MP4"); + assert_eq!(BuiltinProfiles::Matroska.get().name(), "Matroska"); + + BUILTIN_PROFILES.with(|profiles| { + profiles + .iter() + .for_each(|profile| assert!(profile.to_encoding_profile().is_ok())); + }); + } + + #[test] + fn incompatibles() { + let a = new_simple_profile("webmmux", "x264enc", "opusenc"); // webmmux does not support x264enc + assert!(a + .to_encoding_profile() + .err() + .unwrap() + .to_string() + .contains("`x264enc` src is incompatible on `webmmux` sink")); + + let b = new_simple_profile("webmmux", "vp8enc", "lamemp3enc"); // webmmux does not support lamemp3enc + assert!(b + .to_encoding_profile() + .err() + .unwrap() + .to_string() + .contains("`lamemp3enc` src is incompatible on `webmmux` sink")); + } + + #[test] + fn test_profile_format_from_factory_name() { + assert_eq!( + profile_format_from_factory(&find_element_factory("vp8enc").unwrap()).unwrap(), + gst::Caps::builder("video/x-vp8").build(), + ); + assert_eq!( + profile_format_from_factory(&find_element_factory("opusenc").unwrap()).unwrap(), + gst::Caps::builder("audio/x-opus").build(), + ); + assert_eq!( + profile_format_from_factory(&find_element_factory("matroskamux").unwrap()).unwrap(), + gst::Caps::builder("video/x-matroska").build(), + ); + assert!( + profile_format_from_factory(&find_element_factory("audioconvert").unwrap()) + .err() + .unwrap() + .to_string() + .contains("must be an encoder or muxer"), + ); + } +} diff --git a/src/utils.rs b/src/utils.rs index 89e58e52b..9aadc1269 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,8 @@ use gtk::gio::glib; -use std::path::Path; +use std::{cmp, path::Path}; + +const MAX_THREAD_COUNT: u32 = 64; /// Spawns a future in the default [`glib::MainContext`] pub fn spawn + 'static>(fut: F) { @@ -12,3 +14,8 @@ pub fn spawn + 'static>(fut: F) { pub fn is_flatpak() -> bool { Path::new("/.flatpak-info").exists() } + +/// Ideal thread count to use for `GStreamer` processing. +pub fn ideal_thread_count() -> u32 { + cmp::min(glib::num_processors(), MAX_THREAD_COUNT) +} From 4530e8fd7c7e2fc1da415bb71e4affb877ee6f7e Mon Sep 17 00:00:00 2001 From: SeaDve Date: Fri, 26 Aug 2022 22:04:54 +0800 Subject: [PATCH 24/46] Fix test --- src/profile.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/profile.rs b/src/profile.rs index c09fb3889..411181cd8 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -448,24 +448,27 @@ mod tests { #[test] fn test_profile_format_from_factory_name() { - assert_eq!( - profile_format_from_factory(&find_element_factory("vp8enc").unwrap()).unwrap(), - gst::Caps::builder("video/x-vp8").build(), + assert!( + profile_format_from_factory(&find_element_factory("vp8enc").unwrap()) + .unwrap() + .can_intersect(&gst::Caps::builder("video/x-vp8").build()), ); - assert_eq!( - profile_format_from_factory(&find_element_factory("opusenc").unwrap()).unwrap(), - gst::Caps::builder("audio/x-opus").build(), + assert!( + profile_format_from_factory(&find_element_factory("opusenc").unwrap()) + .unwrap() + .can_intersect(&gst::Caps::builder("audio/x-opus").build()) ); - assert_eq!( - profile_format_from_factory(&find_element_factory("matroskamux").unwrap()).unwrap(), - gst::Caps::builder("video/x-matroska").build(), + assert!( + profile_format_from_factory(&find_element_factory("matroskamux").unwrap()) + .unwrap() + .can_intersect(&gst::Caps::builder("video/x-matroska").build()), ); assert!( profile_format_from_factory(&find_element_factory("audioconvert").unwrap()) .err() .unwrap() .to_string() - .contains("must be an encoder or muxer"), + .contains("must be an encoder or muxer") ); } } From 4d32c6e041f0c1a4f3a999c147b572753da90179 Mon Sep 17 00:00:00 2001 From: SeaDve Date: Fri, 26 Aug 2022 22:07:45 +0800 Subject: [PATCH 25/46] Add test --- src/profile.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/profile.rs b/src/profile.rs index 411181cd8..cbeaf61de 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -463,6 +463,11 @@ mod tests { .unwrap() .can_intersect(&gst::Caps::builder("video/x-matroska").build()), ); + assert!( + !profile_format_from_factory(&find_element_factory("matroskamux").unwrap()) + .unwrap() + .can_intersect(&gst::Caps::builder("video/x-vp8").build()), + ); assert!( profile_format_from_factory(&find_element_factory("audioconvert").unwrap()) .err() From 0ca5861c592d162b724bea68fef5eb7f73414e86 Mon Sep 17 00:00:00 2001 From: SeaDve Date: Sat, 27 Aug 2022 08:51:42 +0800 Subject: [PATCH 26/46] Simplify ElementProperties default --- src/element_properties.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/element_properties.rs b/src/element_properties.rs index 667ec4ba5..b38df8b40 100644 --- a/src/element_properties.rs +++ b/src/element_properties.rs @@ -26,11 +26,7 @@ pub struct ElementProperties(gst::Structure); impl Default for ElementProperties { fn default() -> Self { - Self( - gst::Structure::builder("element-properties-map") - .field("map", gst::List::from_values([])) - .build(), - ) + Self::builder().build() } } From 8b2f2551e46d6a3ea47f69f870d95fae09253ed2 Mon Sep 17 00:00:00 2001 From: SeaDve Date: Sat, 27 Aug 2022 11:08:25 +0800 Subject: [PATCH 27/46] Simplify test --- src/profile.rs | 37 ++++++++++++++++--------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/src/profile.rs b/src/profile.rs index cbeaf61de..e760699a8 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -375,7 +375,7 @@ fn profile_format_from_factory(factory: &gst::ElementFactory) -> Result Date: Mon, 29 Aug 2022 14:48:32 +0800 Subject: [PATCH 28/46] Introduce Profiles Window --- Cargo.lock | 1 + Cargo.toml | 1 + data/io.github.seadve.Kooha.gschema.xml.in | 9 - data/resources/resources.gresource.xml | 2 + data/resources/style.css | 11 + data/resources/ui/profile-tile.ui | 116 ++++++ data/resources/ui/profile-window.ui | 100 +++++ data/resources/ui/window.ui | 30 +- src/application.rs | 6 + src/main.rs | 3 + src/pipeline.rs | 82 ++-- src/profile.rs | 130 ++---- src/profile_manager.rs | 347 ++++++++++++++++ src/profile_tile.rs | 217 ++++++++++ src/profile_window.rs | 446 +++++++++++++++++++++ src/recording.rs | 78 ++-- src/window.rs | 36 +- 17 files changed, 1372 insertions(+), 243 deletions(-) create mode 100644 data/resources/ui/profile-tile.ui create mode 100644 data/resources/ui/profile-window.ui create mode 100644 src/profile_manager.rs create mode 100644 src/profile_tile.rs create mode 100644 src/profile_window.rs diff --git a/Cargo.lock b/Cargo.lock index c67b24227..1431e7324 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -744,6 +744,7 @@ dependencies = [ "gdk4-wayland", "gdk4-x11", "gettext-rs", + "glib", "gsettings-macro", "gst-plugin-gif", "gstreamer", diff --git a/Cargo.toml b/Cargo.toml index 56a5b5e25..5422eb242 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ gettext-rs = { version = "0.7.0", features = ["gettext-system"] } gtk = { package = "gtk4", version = "0.4.5" } gdk-wayland = { package = "gdk4-wayland", version = "0.4.5" } gdk-x11 = { package = "gdk4-x11", version = "0.4.5" } +glib = { version = "0.15.12", features = ["v2_72"]} adw = { package = "libadwaita", version = "0.2.0-alpha.2", features = ["v1_2"] } gst = { package = "gstreamer", version = "0.18.2" } gst_video = { package = "gstreamer-video", version = "0.18.2" } diff --git a/data/io.github.seadve.Kooha.gschema.xml.in b/data/io.github.seadve.Kooha.gschema.xml.in index 755b70b5c..a4e4ed67f 100644 --- a/data/io.github.seadve.Kooha.gschema.xml.in +++ b/data/io.github.seadve.Kooha.gschema.xml.in @@ -1,15 +1,6 @@ - - - - - - - - "webm" - diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml index 347c9bd0c..bf44ad7e2 100644 --- a/data/resources/resources.gresource.xml +++ b/data/resources/resources.gresource.xml @@ -12,6 +12,8 @@ icons/scalable/actions/selection-symbolic.svg icons/scalable/actions/source-pick-symbolic.svg style.css + ui/profile-tile.ui + ui/profile-window.ui ui/shortcuts.ui ui/window.ui diff --git a/data/resources/style.css b/data/resources/style.css index f00aab06f..296e18953 100644 --- a/data/resources/style.css +++ b/data/resources/style.css @@ -28,3 +28,14 @@ button.copy-done { color: @window_fg_color; } } + +profiletile { + padding: 18px; +} + +profiletile.selected { + padding: 15px; + border-style: solid; + border-width: 3px; + border-color: @accent_color; +} diff --git a/data/resources/ui/profile-tile.ui b/data/resources/ui/profile-tile.ui new file mode 100644 index 000000000..b82f12c7c --- /dev/null +++ b/data/resources/ui/profile-tile.ui @@ -0,0 +1,116 @@ + + + + diff --git a/data/resources/ui/profile-window.ui b/data/resources/ui/profile-window.ui new file mode 100644 index 000000000..ccf51a2a2 --- /dev/null +++ b/data/resources/ui/profile-window.ui @@ -0,0 +1,100 @@ + + + + diff --git a/data/resources/ui/window.ui b/data/resources/ui/window.ui index 81e657067..a112f3633 100644 --- a/data/resources/ui/window.ui +++ b/data/resources/ui/window.ui @@ -292,6 +292,13 @@ + +
+ + Edit Profiles… + win.edit-profiles + +
_Delay @@ -316,29 +323,6 @@ 10 - - _Video Format - - WebM - win.video-format - webm - - - MKV - win.video-format - mkv - - - MP4 - win.video-format - mp4 - - - GIF - win.video-format - gif - - _Save to… app.select-saving-location diff --git a/src/application.rs b/src/application.rs index e639f847d..905ff7960 100644 --- a/src/application.rs +++ b/src/application.rs @@ -11,6 +11,7 @@ use once_cell::unsync::OnceCell; use crate::{ about, config::{APP_ID, PKGDATADIR, PROFILE, VERSION}, + profile_manager::ProfileManager, settings::Settings, utils, window::Window, @@ -22,6 +23,7 @@ mod imp { #[derive(Debug, Default)] pub struct Application { pub(super) window: OnceCell>, + pub(super) profile_manager: OnceCell, pub(super) settings: Settings, } @@ -91,6 +93,10 @@ impl Application { main_window } + pub fn profile_manager(&self) -> &ProfileManager { + self.imp().profile_manager.get_or_init(ProfileManager::new) + } + pub fn send_record_success_notification(&self, recording_file: &gio::File) { // Translators: This is a message that the user will see when the recording is finished. let notification = gio::Notification::new(&gettext("Screencast recorded")); diff --git a/src/main.rs b/src/main.rs index 132888e6b..20fa20a85 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,6 +30,9 @@ mod element_properties; mod help; mod pipeline; mod profile; +mod profile_manager; +mod profile_tile; +mod profile_window; mod recording; mod screencast_session; mod settings; diff --git a/src/pipeline.rs b/src/pipeline.rs index 968dcde37..9ca277412 100644 --- a/src/pipeline.rs +++ b/src/pipeline.rs @@ -1,27 +1,21 @@ -use anyhow::{bail, Context, Ok, Result}; +use anyhow::{anyhow, bail, Context, Ok, Result}; use gst::prelude::*; use gst_pbutils::prelude::*; use gtk::graphene::{Rect, Size}; use std::{ + ffi::OsStr, os::unix::io::RawFd, path::{Path, PathBuf}, }; -use crate::{profile::BuiltinProfiles, screencast_session::Stream, settings::VideoFormat, utils}; +use crate::{profile::Profile, screencast_session::Stream, utils}; // TODO -// Plugin preferences ui (Show summary on drop down): -// * Bring back GIF support `gifenc repeat=-1 speed=30` -// * Handle missing plugins (Hide profile if missing) -// * Option for vaapi profiles -// // * Do we need restrictions? // * Can we drop filter elements (videorate, videoconvert, videoscale, audioconvert) and let encodebin handle it? // * Add tests -const GIF_FRAMERATE_OVERRIDE: u32 = 15; - #[derive(Debug)] struct SelectAreaContext { pub coords: Rect, @@ -31,9 +25,9 @@ struct SelectAreaContext { #[derive(Debug)] #[must_use] pub struct PipelineBuilder { - file_path: PathBuf, + saving_location: PathBuf, framerate: u32, - format: VideoFormat, + profile: Profile, fd: RawFd, streams: Vec, speaker_source: Option, @@ -43,16 +37,16 @@ pub struct PipelineBuilder { impl PipelineBuilder { pub fn new( - file_path: &Path, + saving_location: &Path, framerate: u32, - format: VideoFormat, + profile: Profile, fd: RawFd, streams: Vec, ) -> Self { Self { - file_path: file_path.to_path_buf(), + saving_location: saving_location.to_path_buf(), framerate, - format, + profile, fd, streams, speaker_source: None, @@ -80,31 +74,34 @@ impl PipelineBuilder { } pub fn build(&self) -> Result { - let pipeline = gst::Pipeline::new(None); + let encoding_profile = self.profile.to_encoding_profile()?; + let file_extension = encoding_profile.file_extension().ok_or_else(|| { + anyhow!( + "Found no file extension for profile with name `{}`", + self.profile.name() + ) + })?; + let file_path = new_recording_path(&self.saving_location, file_extension); let encodebin = element_factory_make("encodebin")?; - encodebin.set_property("profile", &create_profile(self.format)); + encodebin.set_property("profile", &encoding_profile); let queue = element_factory_make("queue")?; let filesink = element_factory_make_named("filesink", Some("filesink"))?; filesink.set_property( "location", - self.file_path + file_path .to_str() .context("Could not convert file path to string")?, ); + let pipeline = gst::Pipeline::new(None); pipeline.add_many(&[&encodebin, &queue, &filesink])?; gst::Element::link_many(&[&encodebin, &queue, &filesink])?; - let framerate = match self.format { - VideoFormat::Gif => GIF_FRAMERATE_OVERRIDE, - _ => self.framerate, - }; - tracing::debug!( - file_path = ?self.file_path, - format = ?self.format, - framerate, + file_path = %file_path.display(), + profile_name = ?self.profile.name(), + framerate = self.framerate, stream_len = self.streams.len(), streams = ?self.streams, speaker_source = ?self.speaker_source, @@ -116,7 +113,7 @@ impl PipelineBuilder { 1 => single_stream_pipewiresrc_bin( self.fd, self.streams.get(0).unwrap(), - framerate, + self.framerate, self.select_area_context.as_ref(), )?, _ => { @@ -124,7 +121,7 @@ impl PipelineBuilder { bail!("Select area is not supported for multiple streams"); } - multi_stream_pipewiresrc_bin(self.fd, &self.streams, framerate)? + multi_stream_pipewiresrc_bin(self.fd, &self.streams, self.framerate)? } }; @@ -174,23 +171,6 @@ impl PipelineBuilder { } } -/// Create an encoding profile based on video format -fn create_profile(video_format: VideoFormat) -> gst_pbutils::EncodingContainerProfile { - let container_profile = match video_format { - VideoFormat::Webm => BuiltinProfiles::WebM.get().to_encoding_profile().unwrap(), - VideoFormat::Mkv => BuiltinProfiles::Matroska - .get() - .to_encoding_profile() - .unwrap(), - VideoFormat::Mp4 => BuiltinProfiles::Mp4.get().to_encoding_profile().unwrap(), - VideoFormat::Gif => panic!("Unsupported video format"), - }; - - tracing::debug!(suggested_file_extension = ?container_profile.file_extension()); - - container_profile -} - /// Helper function for more helpful error messages when failing /// to make an element. fn element_factory_make(factory_name: &str) -> Result { @@ -383,6 +363,18 @@ fn round_to_even_f32(number: f32) -> i32 { number as i32 / 2 * 2 } +fn new_recording_path(saving_location: &Path, extension: impl AsRef) -> PathBuf { + let file_name = glib::DateTime::now_local() + .expect("You are somehow on year 9999") + .format("Kooha-%F-%H-%M-%S") + .expect("Invalid format string"); + + let mut path = saving_location.join(file_name); + path.set_extension(extension); + + path +} + #[cfg(test)] mod test { use super::*; diff --git a/src/profile.rs b/src/profile.rs index e760699a8..c0e34c3b3 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -4,93 +4,7 @@ use gtk::{glib, subclass::prelude::*}; use std::cell::RefCell; -use crate::{ - element_properties::{ - ElementFactoryPropertiesMap, ElementProperties, EncodingProfileExtManual, - }, - utils, -}; - -pub enum BuiltinProfiles { - WebM, - Mp4, - Matroska, -} - -impl BuiltinProfiles { - pub fn get(self) -> Profile { - match self { - Self::WebM => BUILTIN_PROFILES.with(|profiles| profiles[0].clone()), - Self::Mp4 => BUILTIN_PROFILES.with(|profiles| profiles[1].clone()), - Self::Matroska => BUILTIN_PROFILES.with(|profiles| profiles[2].clone()), - } - } -} - -thread_local! { - static BUILTIN_PROFILES: Vec = vec![ - { - let profile = Profile::new("WebM"); - profile.set_container_preset_name("webmmux"); - profile.set_video_preset_name("vp8enc"); - profile.set_video_element_properties( - ElementProperties::builder() - .item( - ElementFactoryPropertiesMap::builder("vp8enc") - .field("max-quantizer", 17) - .field("cpu-used", 16) - .field("cq-level", 13) - .field("deadline", 1) - .field("static-threshold", 100) - .field_from_str("keyframe-mode", "disabled") - .field("buffer-size", 20000) - .field("threads", utils::ideal_thread_count()) - .build(), - ) - .build(), - ); - profile.set_audio_preset_name("opusenc"); - profile - }, - { - // TODO support "profile" = baseline - let profile = Profile::new("MP4"); - profile.set_container_preset_name("mp4mux"); - profile.set_video_preset_name("x264enc"); - profile.set_video_element_properties( - ElementProperties::builder() - .item( - ElementFactoryPropertiesMap::builder("x264enc") - .field("qp-max", 17) - .field_from_str("speed-preset", "superfast") - .field("threads", utils::ideal_thread_count()) - .build(), - ) - .build(), - ); - profile.set_audio_preset_name("lamemp3enc"); - profile - }, - { - let profile = Profile::new("Matroska"); - profile.set_container_preset_name("matroskamux"); - profile.set_video_preset_name("x264enc"); - profile.set_video_element_properties( - ElementProperties::builder() - .item( - ElementFactoryPropertiesMap::builder("x264enc") - .field("qp-max", 17) - .field_from_str("speed-preset", "superfast") - .field("threads", utils::ideal_thread_count()) - .build(), - ) - .build(), - ); - profile.set_audio_preset_name("opusenc"); - profile - }, - ]; -} +use crate::element_properties::{ElementProperties, EncodingProfileExtManual}; mod imp { use super::*; @@ -118,11 +32,7 @@ mod imp { static PROPERTIES: Lazy> = Lazy::new(|| { vec![ glib::ParamSpecString::builder("name") - .flags( - glib::ParamFlags::READWRITE - | glib::ParamFlags::EXPLICIT_NOTIFY - | glib::ParamFlags::CONSTRUCT, - ) + .flags(glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY) .build(), glib::ParamSpecString::builder("container-preset-name") .flags(glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY) @@ -242,6 +152,8 @@ impl Profile { return; } + tracing::debug!("Profile `{}` container set to `{}`", self.name(), name); + self.imp().container_preset_name.replace(name.to_string()); self.notify("container-preset-name"); } @@ -268,6 +180,8 @@ impl Profile { return; } + tracing::debug!("Profile `{}` video set to `{}`", self.name(), name); + self.imp().video_preset_name.replace(name.to_string()); self.notify("video-preset-name"); } @@ -294,6 +208,8 @@ impl Profile { return; } + tracing::debug!("Profile `{}` audio set to `{}`", self.name(), name); + self.imp().audio_preset_name.replace(name.to_string()); self.notify("audio-preset-name"); } @@ -363,6 +279,23 @@ impl Profile { Ok(container_profile) } + + /// Create deep copy of self + pub fn dup(&self) -> Self { + glib::Object::new(&[ + ("name", &self.name()), + ("container-preset-name", &self.container_preset_name()), + ( + "container-element-properties", + &self.container_element_properties(), + ), + ("video-preset-name", &self.video_preset_name()), + ("video-element-properties", &self.video_element_properties()), + ("audio-preset-name", &self.audio_preset_name()), + ("audio-element-properties", &self.audio_element_properties()), + ]) + .expect("Failed to create Profile.") + } } fn find_element_factory(factory_name: &str) -> Result { @@ -414,19 +347,6 @@ mod tests { profile } - #[test] - fn builtins() { - assert_eq!(BuiltinProfiles::WebM.get().name(), "WebM"); - assert_eq!(BuiltinProfiles::Mp4.get().name(), "MP4"); - assert_eq!(BuiltinProfiles::Matroska.get().name(), "Matroska"); - - BUILTIN_PROFILES.with(|profiles| { - profiles - .iter() - .for_each(|profile| assert!(profile.to_encoding_profile().is_ok())); - }); - } - #[test] fn incompatibles() { let a = new_simple_profile("webmmux", "x264enc", "opusenc"); diff --git a/src/profile_manager.rs b/src/profile_manager.rs new file mode 100644 index 000000000..21733ff4e --- /dev/null +++ b/src/profile_manager.rs @@ -0,0 +1,347 @@ +use gst::prelude::*; +use gtk::{gio, glib, prelude::*, subclass::prelude::*}; +use once_cell::unsync::OnceCell; + +use std::cell::RefCell; + +use crate::{ + element_properties::{ElementFactoryPropertiesMap, ElementProperties}, + profile::Profile, + utils, +}; + +// TODO serialize + +const SUPPORTED_MUXERS: [&str; 3] = ["webmmux", "mp4mux", "matroskamux"]; +const SUPPORTED_VIDEO_ENCODERS: [&str; 2] = ["vp8enc", "x264enc"]; +const SUPPORTED_AUDIO_ENCODERS: [&str; 2] = ["opusenc", "lamemp3enc"]; + +mod imp { + use super::*; + use once_cell::sync::Lazy; + + #[derive(Debug, Default)] + pub struct ProfileManager { + pub(super) active_profile: RefCell>, + + pub(super) profiles: RefCell>, + + pub(super) known_muxers: OnceCell, + pub(super) known_audio_encoders: OnceCell, + pub(super) known_video_encoders: OnceCell, + } + + #[glib::object_subclass] + impl ObjectSubclass for ProfileManager { + const NAME: &'static str = "KoohaProfileManager"; + type Type = super::ProfileManager; + type Interfaces = (gio::ListModel,); + } + + impl ObjectImpl for ProfileManager { + fn properties() -> &'static [glib::ParamSpec] { + static PROPERTIES: Lazy> = Lazy::new(|| { + vec![ + glib::ParamSpecObject::builder("active-profile", Profile::static_type()) + .flags(glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY) + .build(), + ] + }); + + PROPERTIES.as_ref() + } + + fn set_property( + &self, + obj: &Self::Type, + _id: usize, + value: &glib::Value, + pspec: &glib::ParamSpec, + ) { + match pspec.name() { + "active-profile" => { + let profile = value.get().unwrap(); + obj.set_active_profile(profile); + } + _ => unimplemented!(), + } + } + + fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { + match pspec.name() { + "active-profile" => obj.active_profile().to_value(), + _ => unimplemented!(), + } + } + + fn constructed(&self, obj: &Self::Type) { + self.parent_constructed(obj); + + for profile in builtin_profiles() { + obj.add_profile(profile); + } + + if let Some(first_item) = obj.get_profile(0) { + obj.set_active_profile(Some(&first_item)); + } + } + } + + impl ListModelImpl for ProfileManager { + fn item_type(&self, _obj: &Self::Type) -> glib::Type { + Profile::static_type() + } + + fn n_items(&self, _obj: &Self::Type) -> u32 { + self.profiles.borrow().len() as u32 + } + + fn item(&self, obj: &Self::Type, position: u32) -> Option { + obj.get_profile(position).map(|profile| profile.upcast()) + } + } +} + +glib::wrapper! { + pub struct ProfileManager(ObjectSubclass) + @implements gio::ListModel; +} + +impl ProfileManager { + pub fn new() -> Self { + glib::Object::new(&[]).expect("Failed to create ProfileManager.") + } + + pub fn active_profile(&self) -> Option { + self.imp().active_profile.borrow().clone() + } + + pub fn set_active_profile(&self, profile: Option<&Profile>) { + if profile == self.active_profile().as_ref() { + return; + } + + tracing::debug!( + "Set active profile to {:?}", + profile.map(|profile| profile.name()) + ); + + if let Some(profile) = profile { + if !self.contains_profile(profile) { + self.add_profile(profile.clone()); + } + } + + self.imp().active_profile.replace(profile.cloned()); + self.notify("active-profile"); + } + + pub fn connect_active_profile_notify(&self, f: F) -> glib::SignalHandlerId + where + F: Fn(&Self) + 'static, + { + self.connect_notify_local(Some("active-profile"), move |obj, _| f(obj)) + } + + pub fn add_profile(&self, profile: Profile) { + let position_appended = { + let mut profiles = self.imp().profiles.borrow_mut(); + profiles.push(profile); + profiles.len() as u32 - 1 + }; + self.items_changed(position_appended, 0, 1); + } + + pub fn remove_profile(&self, profile: &Profile) -> bool { + let imp = self.imp(); + let position = imp + .profiles + .borrow() + .iter() + .position(|stored_profile| stored_profile == profile); + + if let Some(position) = position { + let removed = imp.profiles.borrow_mut().remove(position); + self.items_changed(position as u32, 1, 0); + + if Some(removed) == self.active_profile() { + if let Some(first_item) = self.get_profile(0) { + self.set_active_profile(Some(&first_item)); + } + } + } else { + tracing::debug!( + "Didn't delete profile with name `{}` as it does not exist", + profile.name() + ); + } + + position.is_some() + } + + pub fn known_muxers(&self) -> >k::SortListModel { + self.imp().known_muxers.get_or_init(|| { + new_element_factory_sort_list_model( + gst::ElementFactoryType::MUXER, + gst::Rank::Primary, + &SUPPORTED_MUXERS, + ) + }) + } + + pub fn known_video_encoders(&self) -> >k::SortListModel { + self.imp().known_video_encoders.get_or_init(|| { + new_element_factory_sort_list_model( + gst::ElementFactoryType::VIDEO_ENCODER, + gst::Rank::None, + &SUPPORTED_VIDEO_ENCODERS, + ) + }) + } + + pub fn known_audio_encoders(&self) -> >k::SortListModel { + self.imp().known_audio_encoders.get_or_init(|| { + new_element_factory_sort_list_model( + gst::ElementFactoryType::AUDIO_ENCODER, + gst::Rank::None, + &SUPPORTED_AUDIO_ENCODERS, + ) + }) + } + + fn get_profile(&self, position: u32) -> Option { + self.imp().profiles.borrow().get(position as usize).cloned() + } + + fn contains_profile(&self, profile: &Profile) -> bool { + self.imp().profiles.borrow().contains(profile) + } +} + +impl Default for ProfileManager { + fn default() -> Self { + Self::new() + } +} + +fn builtin_profiles() -> Vec { + // TODO make builtins readonly + vec![ + // TODO bring back gif support `gifenc repeat=-1 speed=30`. Disable `win.record-speaker` and `win.record-mic` actions. 15 fps override + // TODO vaapi? + // TODO Handle missing plugins (Hide profile if missing) + { + let profile = Profile::new("WebM"); + profile.set_container_preset_name("webmmux"); + profile.set_video_preset_name("vp8enc"); + profile.set_video_element_properties( + ElementProperties::builder() + .item( + ElementFactoryPropertiesMap::builder("vp8enc") + .field("max-quantizer", 17) + .field("cpu-used", 16) + .field("cq-level", 13) + .field("deadline", 1) + .field("static-threshold", 100) + .field_from_str("keyframe-mode", "disabled") + .field("buffer-size", 20000) + .field("threads", utils::ideal_thread_count()) + .build(), + ) + .build(), + ); + profile.set_audio_preset_name("opusenc"); + profile + }, + { + // TODO support "profile" = baseline + let profile = Profile::new("MP4"); + profile.set_container_preset_name("mp4mux"); + profile.set_video_preset_name("x264enc"); + profile.set_video_element_properties( + ElementProperties::builder() + .item( + ElementFactoryPropertiesMap::builder("x264enc") + .field("qp-max", 17) + .field_from_str("speed-preset", "superfast") + .field("threads", utils::ideal_thread_count()) + .build(), + ) + .build(), + ); + profile.set_audio_preset_name("lamemp3enc"); + profile + }, + { + let profile = Profile::new("Matroska"); + profile.set_container_preset_name("matroskamux"); + profile.set_video_preset_name("x264enc"); + profile.set_video_element_properties( + ElementProperties::builder() + .item( + ElementFactoryPropertiesMap::builder("x264enc") + .field("qp-max", 17) + .field_from_str("speed-preset", "superfast") + .field("threads", utils::ideal_thread_count()) + .build(), + ) + .build(), + ); + profile.set_audio_preset_name("opusenc"); + profile + }, + ] +} + +fn new_element_factory_sort_list_model( + type_: gst::ElementFactoryType, + min_rank: gst::Rank, + sort_first_names: &'static [&str], +) -> gtk::SortListModel { + fn new_sorter>( + func: impl Fn(&T, &T) -> gtk::Ordering + 'static, + ) -> gtk::Sorter { + gtk::CustomSorter::new(move |a, b| { + let ef_a = a.downcast_ref().unwrap(); + let ef_b = b.downcast_ref().unwrap(); + func(ef_a, ef_b) + }) + .upcast() + } + + let factories = gst::ElementFactory::factories_with_type(type_, min_rank); + + let sorter = gtk::MultiSorter::new(); + sorter.append(&new_sorter( + |a: &gst::ElementFactory, b: &gst::ElementFactory| a.rank().cmp(&b.rank()).reverse().into(), + )); + sorter.append(&new_sorter( + move |a: &gst::ElementFactory, b: &gst::ElementFactory| { + let a_score = sort_first_names + .iter() + .position(|name| *name == a.name()) + .map_or(i32::MAX, |index| index as i32); + let b_score = sort_first_names + .iter() + .position(|name| *name == b.name()) + .map_or(i32::MAX, |index| index as i32); + a_score.cmp(&b_score).into() + }, + )); + + let list_store = gio::ListStore::new(gst::ElementFactory::static_type()); + list_store.splice(0, 0, &factories.collect::>()); + gtk::SortListModel::new(Some(&list_store), Some(&sorter)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn builtin_profiles_work() { + for profile in builtin_profiles() { + assert!(profile.to_encoding_profile().is_ok()); + } + } +} diff --git a/src/profile_tile.rs b/src/profile_tile.rs new file mode 100644 index 000000000..963d5b82c --- /dev/null +++ b/src/profile_tile.rs @@ -0,0 +1,217 @@ +use gtk::{ + glib::{self, closure_local}, + prelude::*, + subclass::prelude::*, +}; + +use std::cell::{Cell, RefCell}; + +use crate::profile::Profile; + +mod imp { + use super::*; + use glib::subclass::Signal; + use gtk::CompositeTemplate; + use once_cell::sync::Lazy; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/io/github/seadve/Kooha/ui/profile-tile.ui")] + pub struct ProfileTile { + #[template_child] + pub(super) name_label: TemplateChild, + #[template_child] + pub(super) muxer_label: TemplateChild, + #[template_child] + pub(super) video_encoder_label: TemplateChild, + #[template_child] + pub(super) audio_encoder_label: TemplateChild, + + pub(super) profile: RefCell>, + pub(super) is_selected: Cell, + + pub(super) binding_group: glib::BindingGroup, + } + + #[glib::object_subclass] + impl ObjectSubclass for ProfileTile { + const NAME: &'static str = "KoohaProfileTile"; + type Type = super::ProfileTile; + type ParentType = gtk::FlowBoxChild; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + + klass.set_css_name("profiletile"); + + klass.install_action("profile-tile.delete", None, |obj, _, _| { + obj.emit_by_name::<()>("delete-request", &[]); + }); + + klass.install_action("profile-tile.copy", None, |obj, _, _| { + obj.emit_by_name::<()>("copy-request", &[]); + }); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for ProfileTile { + fn properties() -> &'static [glib::ParamSpec] { + static PROPERTIES: Lazy> = Lazy::new(|| { + vec![ + // Profile to show by self + glib::ParamSpecObject::builder("profile", Profile::static_type()) + .flags(glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY) + .build(), + // Whether self should be displayed as selected + glib::ParamSpecBoolean::builder("selected") + .flags(glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY) + .build(), + ] + }); + + PROPERTIES.as_ref() + } + + fn set_property( + &self, + obj: &Self::Type, + _id: usize, + value: &glib::Value, + pspec: &glib::ParamSpec, + ) { + match pspec.name() { + "profile" => { + let profile = value.get().unwrap(); + obj.set_profile(profile); + } + "selected" => { + let is_selected = value.get().unwrap(); + obj.set_selected(is_selected); + } + _ => unimplemented!(), + } + } + + fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { + match pspec.name() { + "profile" => obj.profile().to_value(), + "selected" => obj.is_selected().to_value(), + _ => unimplemented!(), + } + } + + fn signals() -> &'static [glib::subclass::Signal] { + static SIGNALS: Lazy> = Lazy::new(|| { + vec![ + Signal::builder("delete-request", &[], <()>::static_type().into()).build(), + Signal::builder("copy-request", &[], <()>::static_type().into()).build(), + ] + }); + + SIGNALS.as_ref() + } + + fn constructed(&self, obj: &Self::Type) { + self.parent_constructed(obj); + + self.binding_group + .bind("name", &self.name_label.get(), "label") + .build(); + self.binding_group + .bind("container-preset-name", &self.muxer_label.get(), "label") + .build(); + self.binding_group + .bind( + "video-preset-name", + &self.video_encoder_label.get(), + "label", + ) + .build(); + self.binding_group + .bind( + "audio-preset-name", + &self.audio_encoder_label.get(), + "label", + ) + .build(); + } + } + + impl WidgetImpl for ProfileTile {} + impl FlowBoxChildImpl for ProfileTile {} +} + +glib::wrapper! { + pub struct ProfileTile(ObjectSubclass) + @extends gtk::Widget, gtk::FlowBoxChild; +} + +impl ProfileTile { + pub fn new(profile: &Profile) -> Self { + glib::Object::new(&[("profile", profile)]).expect("Failed to create ProfileTile.") + } + + pub fn set_profile(&self, profile: Option<&Profile>) { + if profile == self.profile().as_ref() { + return; + } + + let imp = self.imp(); + imp.profile.replace(profile.cloned()); + imp.binding_group.set_source(profile); + self.notify("profile"); + } + + pub fn profile(&self) -> Option { + self.imp().profile.borrow().clone() + } + + pub fn set_selected(&self, is_selected: bool) { + if is_selected == self.is_selected() { + return; + } + + self.imp().is_selected.set(is_selected); + + if is_selected { + self.add_css_class("selected"); + } else { + self.remove_css_class("selected"); + } + + self.notify("selected"); + } + + pub fn is_selected(&self) -> bool { + self.imp().is_selected.get() + } + + pub fn connect_delete_request(&self, f: F) -> glib::SignalHandlerId + where + F: Fn(&Self) + 'static, + { + self.connect_closure( + "delete-request", + true, + closure_local!(|obj: &Self| { + f(obj); + }), + ) + } + + pub fn connect_copy_request(&self, f: F) -> glib::SignalHandlerId + where + F: Fn(&Self) + 'static, + { + self.connect_closure( + "copy-request", + true, + closure_local!(|obj: &Self| { + f(obj); + }), + ) + } +} diff --git a/src/profile_window.rs b/src/profile_window.rs new file mode 100644 index 000000000..ed1232b88 --- /dev/null +++ b/src/profile_window.rs @@ -0,0 +1,446 @@ +use adw::{prelude::*, subclass::prelude::*}; +use gettextrs::{gettext, ngettext}; +use gst::prelude::*; +use gtk::{ + gio, + glib::{self, clone, closure}, +}; +use once_cell::unsync::OnceCell; + +use std::cell::RefCell; + +use crate::{profile::Profile, profile_manager::ProfileManager, profile_tile::ProfileTile}; + +mod imp { + use super::*; + use gtk::CompositeTemplate; + use once_cell::sync::Lazy; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/io/github/seadve/Kooha/ui/profile-window.ui")] + pub struct ProfileWindow { + #[template_child] + pub(super) toast_overlay: TemplateChild, + #[template_child] + pub(super) profiles_box: TemplateChild, + #[template_child] + pub(super) name_row: TemplateChild, + #[template_child] + pub(super) muxer_row: TemplateChild, + #[template_child] + pub(super) video_encoder_row: TemplateChild, + #[template_child] + pub(super) audio_encoder_row: TemplateChild, + + pub(super) model: RefCell>, + + pub(super) profile_purgatory: RefCell>, + pub(super) undo_delete_toast: RefCell>, + + pub(super) encoder_filter: OnceCell, + pub(super) model_signal_handler_ids: RefCell>, + + pub(super) name_row_binding: RefCell>, + pub(super) muxer_row_handler_id: OnceCell, + pub(super) video_encoder_row_handler_id: OnceCell, + pub(super) audio_encoder_row_handler_id: OnceCell, + } + + #[glib::object_subclass] + impl ObjectSubclass for ProfileWindow { + const NAME: &'static str = "KoohaProfileWindow"; + type Type = super::ProfileWindow; + type ParentType = adw::Window; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + + klass.install_action("profile-window.new-profile", None, |obj, _, _| { + if let Some(model) = obj.model() { + model.set_active_profile(Some(&Profile::new("New Profile"))); + } else { + tracing::warn!("Found no model!"); + } + }); + + klass.install_action("undo-delete-toast.dismiss", None, |obj, _, _| { + if let Some(model) = obj.model() { + for profile in obj.imp().profile_purgatory.take() { + model.add_profile(profile); + } + } else { + tracing::warn!("Found no model!"); + } + }); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for ProfileWindow { + fn properties() -> &'static [glib::ParamSpec] { + static PROPERTIES: Lazy> = Lazy::new(|| { + vec![ + // Profile model + glib::ParamSpecObject::builder("model", ProfileManager::static_type()) + .flags(glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY) + .build(), + ] + }); + + PROPERTIES.as_ref() + } + + fn set_property( + &self, + obj: &Self::Type, + _id: usize, + value: &glib::Value, + pspec: &glib::ParamSpec, + ) { + match pspec.name() { + "model" => { + let model = value.get().unwrap(); + obj.set_model(model); + } + _ => unimplemented!(), + } + } + + fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { + match pspec.name() { + "model" => obj.model().to_value(), + _ => unimplemented!(), + } + } + + fn constructed(&self, obj: &Self::Type) { + self.parent_constructed(obj); + + self.profiles_box + .connect_child_activated(clone!(@weak obj => move |_, profile_tile| { + let profile_tile = profile_tile.downcast_ref::().unwrap(); + let profile = profile_tile.profile(); + + obj.model().unwrap().set_active_profile(profile.as_ref()); + })); + + let element_factory_name_expression = + gtk::ClosureExpression::new::( + &[], + closure!(|element_factory: &gst::ElementFactory| { + element_factory + .metadata(&gst::ELEMENT_METADATA_LONGNAME) + .map_or_else(|| element_factory.name(), glib::GString::from) + }), + ); + self.muxer_row + .set_expression(Some(&element_factory_name_expression)); + self.video_encoder_row + .set_expression(Some(&element_factory_name_expression)); + self.audio_encoder_row + .set_expression(Some(&element_factory_name_expression)); + + self.muxer_row + .connect_selected_notify(clone!(@weak obj => move |_| { + obj.encoder_filter().changed(gtk::FilterChange::Different); + })); + + self.muxer_row_handler_id.set(self.muxer_row + .connect_selected_notify(clone!(@weak obj => move |row| { + if let Some(selected_item) = row.selected_item() { + if let Some(profile) = obj.model().and_then(|model| model.active_profile()) { + let element_factory = selected_item.downcast::().unwrap(); + let element_factory_name = element_factory.name(); + profile.set_container_preset_name(&element_factory_name); + } else { + tracing::warn!("No model or active profile found but selected an element"); + } + } + }))).unwrap(); + self.video_encoder_row_handler_id.set(self.video_encoder_row + .connect_selected_notify(clone!(@weak obj => move |row| { + if let Some(selected_item) = row.selected_item() { + if let Some(profile) = obj.model().and_then(|model| model.active_profile()) { + let element_factory = selected_item.downcast::().unwrap(); + let element_factory_name = element_factory.name(); + profile.set_video_preset_name(&element_factory_name); + } else { + tracing::warn!("No model or active profile found but selected an element"); + } + } + }))).unwrap(); + self.audio_encoder_row_handler_id.set(self.audio_encoder_row + .connect_selected_notify(clone!(@weak obj => move |row| { + if let Some(selected_item) = row.selected_item() { + if let Some(profile) = obj.model().and_then(|model| model.active_profile()) { + let element_factory = selected_item.downcast::().unwrap(); + let element_factory_name = element_factory.name(); + profile.set_audio_preset_name(&element_factory_name); + } else { + tracing::warn!("No model or active profile found but selected an element"); + } + } + }))).unwrap(); + } + + fn dispose(&self, obj: &Self::Type) { + obj.disconnect_model_signals(); + } + } + + impl WidgetImpl for ProfileWindow {} + impl WindowImpl for ProfileWindow {} + impl AdwWindowImpl for ProfileWindow {} +} + +glib::wrapper! { + pub struct ProfileWindow(ObjectSubclass) + @extends gtk::Widget, gtk::Window, adw::Window; +} + +impl ProfileWindow { + pub fn new(model: &ProfileManager) -> Self { + glib::Object::new(&[("model", model)]).expect("Failed to create ProfileWindow.") + } + + pub fn set_model(&self, model: Option<&ProfileManager>) { + if model == self.model().as_ref() { + return; + } + + self.disconnect_model_signals(); + + let imp = self.imp(); + imp.model.replace(model.cloned()); + + imp.profiles_box.bind_model( + model, + clone!(@weak self as obj => @default-panic, move |profile| { + obj.create_profile_tile(profile.downcast_ref::().unwrap()).upcast() + }), + ); + + imp.muxer_row + .set_model(model.map(|model| model.known_muxers())); + + imp.video_encoder_row.set_model( + model + .map(|model| { + gtk::FilterListModel::new( + Some(model.known_video_encoders()), + Some(self.encoder_filter()), + ) + }) + .as_ref(), + ); + imp.audio_encoder_row.set_model( + model + .map(|model| { + gtk::FilterListModel::new( + Some(model.known_audio_encoders()), + Some(self.encoder_filter()), + ) + }) + .as_ref(), + ); + + if let Some(model) = model { + self.add_model_signal(model.connect_active_profile_notify( + clone!(@weak self as obj => move |_| { + obj.update_rows(); + }), + )); + } + + self.update_rows(); + + self.notify("model"); + } + + pub fn model(&self) -> Option { + self.imp().model.borrow().clone() + } + + fn encoder_filter(&self) -> >k::BoolFilter { + let imp = self.imp(); + imp.encoder_filter.get_or_init(|| { + let closure = closure!( + |encoder: &gst::ElementFactory, selected_muxer: Option<&glib::Object>| { + selected_muxer.map_or(false, |muxer| { + let muxer = muxer.downcast_ref::().unwrap(); + encoder.static_pad_templates().any(|template| { + template.direction() == gst::PadDirection::Src + && muxer.can_sink_any_caps(&template.caps()) + }) + }) + } + ); + + gtk::BoolFilter::new(Some(>k::ClosureExpression::new::( + &[imp.muxer_row.property_expression("selected-item")], + closure, + ))) + }) + } + + fn show_undo_delete_toast(&self) { + let imp = self.imp(); + + if imp.undo_delete_toast.borrow().is_none() { + let toast = adw::Toast::builder() + .priority(adw::ToastPriority::High) + .button_label(&gettext("_Undo")) + .action_name("undo-delete-toast.dismiss") + .build(); + + toast.connect_dismissed(clone!(@weak self as obj => move |_| { + let imp = obj.imp(); + imp.profile_purgatory.borrow_mut().clear(); + imp.undo_delete_toast.take(); + })); + + imp.toast_overlay.add_toast(&toast); + imp.undo_delete_toast.replace(Some(toast)); + } + + // Add this point we should already have a toast setup + if let Some(ref toast) = *imp.undo_delete_toast.borrow() { + let n_removed = imp.profile_purgatory.borrow().len(); + toast.set_title(&ngettext!( + "Removed {} profile", + "Removed {} profiles", + n_removed as u32, + n_removed + )); + } + } + + fn create_profile_tile(&self, profile: &Profile) -> ProfileTile { + let profile_tile = ProfileTile::new(profile); + + let model = self.model().unwrap(); + + if model.active_profile().as_ref() == Some(profile) { + profile_tile.set_selected(true); + } + + self.add_model_signal(model.connect_active_profile_notify( + clone!(@weak profile_tile => move |model| { + profile_tile.set_selected(profile_tile.profile() == model.active_profile()); + }), + )); + + profile_tile.connect_delete_request(clone!(@weak self as obj => move |profile_tile| { + let to_remove = profile_tile.profile().unwrap(); + if obj.model().unwrap().remove_profile(&to_remove) { + obj.imp().profile_purgatory.borrow_mut().push(to_remove); + obj.show_undo_delete_toast(); + } + })); + + profile_tile.connect_copy_request(clone!(@weak self as obj => move |profile_tile| { + let original = profile_tile.profile().unwrap(); + let duplicate = original.dup(); + duplicate.set_name(&gettext!("{} (Copy)", original.name())); + obj.model().unwrap().set_active_profile(Some(&duplicate)); + })); + + profile_tile + } + + fn add_model_signal(&self, handler_id: glib::SignalHandlerId) { + self.imp() + .model_signal_handler_ids + .borrow_mut() + .push(handler_id); + } + + fn disconnect_model_signals(&self) { + for handler_id in self.imp().model_signal_handler_ids.borrow_mut().drain(..) { + if let Some(model) = self.model() { + model.disconnect(handler_id); + } else { + tracing::warn!("Model removed before disconnecting signals!"); + } + } + } + + fn update_rows(&self) { + let imp = self.imp(); + + if let Some(binding) = imp.name_row_binding.take() { + binding.unbind(); + } + + let active_profile = self.model().and_then(|model| model.active_profile()); + let has_active_profile = active_profile.is_some(); + + imp.name_row.set_visible(has_active_profile); + imp.muxer_row.set_visible(has_active_profile); + imp.video_encoder_row.set_visible(has_active_profile); + imp.audio_encoder_row.set_visible(has_active_profile); + + let active_profile = if let Some(profile) = active_profile { + profile + } else { + return; + }; + + imp.name_row_binding.replace(Some( + active_profile + .bind_property("name", &imp.name_row.get(), "text") + .flags(glib::BindingFlags::SYNC_CREATE | glib::BindingFlags::BIDIRECTIONAL) + .build(), + )); + + imp.muxer_row + .block_signal(imp.muxer_row_handler_id.get().unwrap()); + imp.video_encoder_row + .block_signal(imp.video_encoder_row_handler_id.get().unwrap()); + imp.audio_encoder_row + .block_signal(imp.audio_encoder_row_handler_id.get().unwrap()); + + set_selected_item( + &imp.muxer_row.get(), + |element_factory: gst::ElementFactory| { + // TODO Comp with actual element + element_factory.name() == active_profile.container_preset_name() + }, + ); + set_selected_item( + &imp.video_encoder_row.get(), + |element_factory: gst::ElementFactory| { + element_factory.name() == active_profile.video_preset_name() + }, + ); + set_selected_item( + &imp.audio_encoder_row.get(), + |element_factory: gst::ElementFactory| { + element_factory.name() == active_profile.audio_preset_name() + }, + ); + + imp.muxer_row + .unblock_signal(imp.muxer_row_handler_id.get().unwrap()); + imp.video_encoder_row + .unblock_signal(imp.video_encoder_row_handler_id.get().unwrap()); + imp.audio_encoder_row + .unblock_signal(imp.audio_encoder_row_handler_id.get().unwrap()); + } +} + +fn set_selected_item>(combo: &adw::ComboRow, func: impl Fn(I) -> bool) { + fn find>(model: &gio::ListModel, func: impl Fn(I) -> bool) -> Option { + for i in 0..model.n_items() { + if func(model.item(i).unwrap().downcast().unwrap()) { + return Some(i); + } + } + None + } + + combo.set_selected(find(&combo.model().unwrap(), func).unwrap_or(gtk::INVALID_LIST_POSITION)); +} diff --git a/src/recording.rs b/src/recording.rs index 4ae85ac75..f4f0a43d5 100644 --- a/src/recording.rs +++ b/src/recording.rs @@ -1,4 +1,4 @@ -use anyhow::{ensure, Context, Error, Result}; +use anyhow::{anyhow, ensure, Context, Error, Result}; use gettextrs::gettext; use gst::prelude::*; use gtk::{ @@ -10,7 +10,6 @@ use once_cell::{sync::Lazy, unsync::OnceCell}; use std::{ cell::{Cell, RefCell}, os::unix::prelude::RawFd, - path::{Path, PathBuf}, rc::Rc, time::Duration, }; @@ -21,8 +20,9 @@ use crate::{ cancelled::Cancelled, help::{ErrorExt, ResultExt}, pipeline::PipelineBuilder, + profile::Profile, screencast_session::{CursorMode, PersistMode, ScreencastSession, SourceType, Stream}, - settings::{CaptureMode, Settings, VideoFormat}, + settings::{CaptureMode, Settings}, timer::Timer, utils, }; @@ -139,13 +139,18 @@ impl Recording { glib::Object::new(&[]).expect("Failed to create Recording.") } - pub async fn start(&self, parent: Option<&impl IsA>, settings: Settings) { + pub async fn start( + &self, + parent: Option<&impl IsA>, + settings: &Settings, + profile: &Profile, + ) { if !matches!(self.state(), State::Init) { tracing::error!("Trying to start recording on a non-init state"); return; } - if let Err(err) = self.start_inner(parent, settings).await { + if let Err(err) = self.start_inner(parent, settings, profile).await { self.close_session(); self.set_finished(Err(err)); } @@ -154,7 +159,8 @@ impl Recording { async fn start_inner( &self, parent: Option<&impl IsA>, - settings: Settings, + settings: &Settings, + profile: &Profile, ) -> Result<()> { let imp = self.imp(); @@ -187,16 +193,13 @@ impl Recording { settings.set_screencast_restore_token(&restore_token.unwrap_or_default()); // setup path - let video_format = settings.video_format(); - let recording_path = new_recording_path(&settings.saving_location(), video_format); let mut pipeline_builder = PipelineBuilder::new( - &recording_path, + &settings.saving_location(), settings.video_framerate(), - video_format, + profile.clone(), fd, streams, ); - imp.file.set(gio::File::for_path(&recording_path)).unwrap(); // select area if settings.capture_mode() == CaptureMode::Selection { @@ -373,6 +376,20 @@ impl Recording { ) } + fn file(&self) -> Result<&gio::File> { + let imp = self.imp(); + self.imp().file.get_or_try_init(|| { + let location = imp + .pipeline + .get() + .ok_or_else(|| anyhow!("Pipeline not set"))? + .by_name("filesink") + .ok_or_else(|| anyhow!("Element filesink not found on pipeline"))? + .property::("location"); + Ok(gio::File::for_path(location)) + }) + } + fn set_state(&self, state: State) { if state == self.state() { return; @@ -409,12 +426,14 @@ impl Recording { /// Deletes recording file on background fn delete_file(&self) { - if let Some(file) = self.imp().file.get() { + if let Ok(file) = self.file() { file.delete_async(glib::PRIORITY_DEFAULT_IDLE, gio::Cancellable::NONE, |res| { if let Err(err) = res { tracing::warn!("Failed to delete recording file: {:?}", err); } }); + } else { + tracing::error!("Failed to delete recording file: Failed to get file"); } } @@ -462,9 +481,9 @@ impl Recording { let error = if e.error().matches(gst::ResourceError::OpenWrite) { error.help( gettext("Make sure that the saving location exists and is accessible."), - if let Some(ref path) = imp - .file - .get() + if let Some(ref path) = self + .file() + .ok() .and_then(|f| f.path()) .and_then(|path| path.parent().map(|p| p.to_owned())) { @@ -500,17 +519,7 @@ impl Recording { source_id.remove(); } - let file = imp.file.get().unwrap(); - - debug_assert_eq!( - self.pipeline() - .by_name("filesink") - .map(|fs| fs.property::("location")) - .map(|path| PathBuf::from(&path)), - Some(file.path().unwrap()) - ); - - self.set_finished(Ok(file.clone())); + self.set_finished(Ok(self.file().unwrap().clone())); Continue(false) } @@ -571,23 +580,6 @@ impl Default for Recording { } } -fn new_recording_path(saving_location: &Path, video_format: VideoFormat) -> PathBuf { - let file_name = glib::DateTime::now_local() - .expect("You are somehow on year 9999") - .format("Kooha-%F-%H-%M-%S") - .expect("Invalid format string"); - - let mut path = saving_location.join(file_name); - path.set_extension(match video_format { - VideoFormat::Webm => "webm", - VideoFormat::Mkv => "mkv", - VideoFormat::Mp4 => "mp4", - VideoFormat::Gif => "gif", - }); - - path -} - async fn new_screencast_session( cursor_mode: CursorMode, source_type: SourceType, diff --git a/src/window.rs b/src/window.rs index a45b096f3..ef29f2ddb 100644 --- a/src/window.rs +++ b/src/window.rs @@ -12,8 +12,9 @@ use crate::{ cancelled::Cancelled, config::PROFILE, help::Help, + profile_window::ProfileWindow, recording::{Recording, State as RecordingState}, - settings::{CaptureMode, VideoFormat}, + settings::CaptureMode, toggle_button::ToggleButton, utils, Application, }; @@ -87,6 +88,13 @@ mod imp { .settings() .set_screencast_restore_token(""); }); + + klass.install_action("win.edit-profiles", None, |obj, _, _| { + let profile_window = ProfileWindow::new(Application::default().profile_manager()); + profile_window.set_modal(true); + profile_window.set_transient_for(Some(obj)); + profile_window.present(); + }); } fn instance_init(obj: &glib::subclass::InitializingObject) { @@ -105,7 +113,6 @@ mod imp { obj.setup_settings(); obj.update_view(); - obj.update_audio_toggles_sensitivity(); obj.update_title_label(); } } @@ -244,8 +251,15 @@ impl Window { ]; *imp.recording.lock().await = Some((recording.clone(), handler_ids)); - let settings = Application::default().settings(); - recording.start(Some(self), settings).await; + let application = Application::default(); + recording + .start( + Some(self), + &application.settings(), + // TODO make record button insensitive when no profile + &application.profile_manager().active_profile().unwrap(), + ) + .await; } async fn toggle_pause(&self) -> Result<()> { @@ -385,14 +399,6 @@ impl Window { } } - fn update_audio_toggles_sensitivity(&self) { - let settings = Application::default().settings(); - let is_enabled = settings.video_format() != VideoFormat::Gif; - - self.action_set_enabled("win.record-speaker", is_enabled); - self.action_set_enabled("win.record-mic", is_enabled); - } - fn update_forget_video_sources_action(&self) { let settings = Application::default().settings(); let has_restore_token = !settings.screencast_restore_token().is_empty(); @@ -411,16 +417,11 @@ impl Window { obj.update_title_label(); })); - settings.connect_video_format_changed(clone!(@weak self as obj => move |_| { - obj.update_audio_toggles_sensitivity(); - })); - settings.connect_screencast_restore_token_changed(clone!(@weak self as obj => move |_| { obj.update_forget_video_sources_action(); })); self.update_title_label(); - self.update_audio_toggles_sensitivity(); self.update_forget_video_sources_action(); self.add_action(&settings.create_record_speaker_action()); @@ -428,7 +429,6 @@ impl Window { self.add_action(&settings.create_show_pointer_action()); self.add_action(&settings.create_capture_mode_action()); self.add_action(&settings.create_record_delay_action()); - self.add_action(&settings.create_video_format_action()); } } From 04214b3b698de6f65f5a8d25b82de9123b1ffb50 Mon Sep 17 00:00:00 2001 From: SeaDve Date: Mon, 29 Aug 2022 14:56:19 +0800 Subject: [PATCH 29/46] Add todo --- data/resources/ui/profile-window.ui | 1 + 1 file changed, 1 insertion(+) diff --git a/data/resources/ui/profile-window.ui b/data/resources/ui/profile-window.ui index ccf51a2a2..5d0068d86 100644 --- a/data/resources/ui/profile-window.ui +++ b/data/resources/ui/profile-window.ui @@ -62,6 +62,7 @@ + Name From 8e31acd351ae171721e9b674e401c27136689086 Mon Sep 17 00:00:00 2001 From: SeaDve Date: Mon, 29 Aug 2022 15:01:04 +0800 Subject: [PATCH 30/46] Fix checks --- po/POTFILES.in | 2 ++ 1 file changed, 2 insertions(+) diff --git a/po/POTFILES.in b/po/POTFILES.in index 18662d58a..9f5a016e4 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -1,12 +1,14 @@ data/io.github.seadve.Kooha.desktop.in.in data/io.github.seadve.Kooha.gschema.xml.in data/io.github.seadve.Kooha.metainfo.xml.in.in +data/resources/ui/profile-window.ui data/resources/ui/shortcuts.ui data/resources/ui/window.ui src/about.rs src/application.rs src/audio_device.rs src/main.rs +src/profile_window.rs src/recording.rs src/settings.rs src/window.rs From 848c1bb5dbea024a4e26b6678e59addd4083846f Mon Sep 17 00:00:00 2001 From: SeaDve Date: Mon, 29 Aug 2022 19:32:14 +0800 Subject: [PATCH 31/46] Rename dup to deep_clone --- src/profile.rs | 28 ++++++++++++++-------------- src/profile_window.rs | 6 +++--- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/profile.rs b/src/profile.rs index c0e34c3b3..7ba34370d 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -280,21 +280,21 @@ impl Profile { Ok(container_profile) } - /// Create deep copy of self - pub fn dup(&self) -> Self { - glib::Object::new(&[ - ("name", &self.name()), - ("container-preset-name", &self.container_preset_name()), - ( - "container-element-properties", - &self.container_element_properties(), - ), - ("video-preset-name", &self.video_preset_name()), - ("video-element-properties", &self.video_element_properties()), - ("audio-preset-name", &self.audio_preset_name()), - ("audio-element-properties", &self.audio_element_properties()), - ]) + pub fn deep_clone(&self) -> Self { + glib::Object::with_values( + Self::static_type(), + &self + .list_properties() + .iter() + .map(|pspec| { + let property_name = pspec.name(); + (property_name, self.property_value(property_name)) + }) + .collect::>(), + ) .expect("Failed to create Profile.") + .downcast() + .unwrap() } } diff --git a/src/profile_window.rs b/src/profile_window.rs index ed1232b88..0c22090cd 100644 --- a/src/profile_window.rs +++ b/src/profile_window.rs @@ -343,9 +343,9 @@ impl ProfileWindow { profile_tile.connect_copy_request(clone!(@weak self as obj => move |profile_tile| { let original = profile_tile.profile().unwrap(); - let duplicate = original.dup(); - duplicate.set_name(&gettext!("{} (Copy)", original.name())); - obj.model().unwrap().set_active_profile(Some(&duplicate)); + let deep_clone = original.deep_clone(); + deep_clone.set_name(&gettext!("{} (Copy)", original.name())); + obj.model().unwrap().set_active_profile(Some(&deep_clone)); })); profile_tile From a0f117383f0356b5e0151e3721db5d750920d16c Mon Sep 17 00:00:00 2001 From: SeaDve Date: Mon, 29 Aug 2022 19:39:44 +0800 Subject: [PATCH 32/46] Fix setting element on start --- src/profile_window.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/profile_window.rs b/src/profile_window.rs index 0c22090cd..a4a2331fa 100644 --- a/src/profile_window.rs +++ b/src/profile_window.rs @@ -224,8 +224,14 @@ impl ProfileWindow { ); imp.muxer_row - .set_model(model.map(|model| model.known_muxers())); + .block_signal(imp.muxer_row_handler_id.get().unwrap()); + imp.video_encoder_row + .block_signal(imp.video_encoder_row_handler_id.get().unwrap()); + imp.audio_encoder_row + .block_signal(imp.audio_encoder_row_handler_id.get().unwrap()); + imp.muxer_row + .set_model(model.map(|model| model.known_muxers())); imp.video_encoder_row.set_model( model .map(|model| { @@ -247,6 +253,13 @@ impl ProfileWindow { .as_ref(), ); + imp.muxer_row + .unblock_signal(imp.muxer_row_handler_id.get().unwrap()); + imp.video_encoder_row + .unblock_signal(imp.video_encoder_row_handler_id.get().unwrap()); + imp.audio_encoder_row + .unblock_signal(imp.audio_encoder_row_handler_id.get().unwrap()); + if let Some(model) = model { self.add_model_signal(model.connect_active_profile_notify( clone!(@weak self as obj => move |_| { From 7c7db99c3a47622c303f018f16748d585e0ffae2 Mon Sep 17 00:00:00 2001 From: SeaDve Date: Tue, 30 Aug 2022 19:07:03 +0800 Subject: [PATCH 33/46] Refactor profile --- src/element_factory_profile.rs | 168 ++++++++++++++++++++ src/element_properties.rs | 212 -------------------------- src/main.rs | 2 +- src/profile.rs | 269 +++++++++++++++++---------------- src/profile_manager.rs | 90 +++++------ src/profile_tile.rs | 19 ++- src/profile_window.rs | 47 +++--- src/window.rs | 2 +- 8 files changed, 381 insertions(+), 428 deletions(-) create mode 100644 src/element_factory_profile.rs delete mode 100644 src/element_properties.rs diff --git a/src/element_factory_profile.rs b/src/element_factory_profile.rs new file mode 100644 index 000000000..72ba72ee4 --- /dev/null +++ b/src/element_factory_profile.rs @@ -0,0 +1,168 @@ +use anyhow::{anyhow, Context, Result}; +use gst::prelude::*; +use gtk::glib::{ + self, + translate::{ToGlibPtr, UnsafeFrom}, + ToSendValue, +}; + +pub trait EncodingProfileExtManual { + fn set_element_properties(&self, element_properties: gst::Structure); +} + +impl> EncodingProfileExtManual for P { + fn set_element_properties(&self, element_properties: gst::Structure) { + unsafe { + gst_pbutils::ffi::gst_encoding_profile_set_element_properties( + self.as_ref().to_glib_none().0, + element_properties.to_glib_full(), + ); + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, glib::Boxed)] +#[boxed_type(name = "KoohaElementFactoryProfile", nullable)] +pub struct ElementFactoryProfile(gst::Structure); + +impl ElementFactoryProfile { + pub fn new(factory_name: &str) -> Self { + Self::builder(factory_name).build() + } + + pub fn builder(factory_name: &str) -> ElementFactoryProfileBuilder { + ElementFactoryProfileBuilder { + structure: gst::Structure::new_empty(factory_name), + } + } + + pub fn factory_name(&self) -> &str { + self.0.name() + } + + pub fn into_element_properties(self) -> gst::Structure { + gst::Structure::builder("element-properties-map") + .field("map", gst::List::from(vec![self.0.to_send_value()])) + .build() + } +} + +pub struct ElementFactoryProfileBuilder { + structure: gst::Structure, +} + +impl ElementFactoryProfileBuilder { + pub fn field(mut self, property_name: &str, value: T) -> Self + where + T: ToSendValue + Sync, + { + self.structure.set(property_name, value); + self + } + + /// Parses the given string into a property of element from the + /// given `factory_name` with type based on the property's param spec. + /// + /// This works similar to `GObjectExtManualGst::set_property_from_str`. + /// + /// Note: The property will not be set if any of `factory_name`, `property_name` + /// or `string` is invalid. + pub fn field_from_str(mut self, property_name: &str, value_string: &str) -> Self { + let factory_name = self.structure.name(); + + match value_from_str(factory_name, property_name, value_string) { + Ok(value) => self.structure.set_value(property_name, value), + Err(err) => tracing::warn!( + "Failed to set property `{}` to `{}`: {:?}", + property_name, + value_string, + err + ), + } + + self + } + + pub fn build(self) -> ElementFactoryProfile { + ElementFactoryProfile(self.structure) + } +} + +fn value_from_str( + factory_name: &str, + property_name: &str, + value_string: &str, +) -> Result { + let element_type = gst::ElementFactory::find(factory_name) + .ok_or_else(|| anyhow!("Failed to find factory with name `{}`", factory_name))? + .load() + .with_context(|| anyhow!("Failed to load factory with name `{}`", factory_name))? + .element_type(); + let pspec = glib::object::ObjectClass::from_type(element_type) + .ok_or_else(|| anyhow!("Failed to create object class from type `{}`", element_type))? + .find_property(property_name) + .ok_or_else(|| { + glib::bool_error!( + "Property `{}` not found on type `{}`", + property_name, + element_type + ) + })?; + let value = unsafe { + glib::SendValue::unsafe_from( + glib::Value::deserialize_with_pspec(value_string, &pspec)?.into_raw(), + ) + }; + Ok(value) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn into_element_properties() { + let profile = ElementFactoryProfile::new("vp8enc"); + let element_properties = profile.clone().into_element_properties(); + assert_eq!(element_properties.name(), "element-properties-map"); + assert_eq!( + element_properties + .get::("map") + .unwrap() + .get(0) + .unwrap() + .get::(), + Ok(profile.0) + ); + } + + #[test] + fn builder() { + gst::init().unwrap(); + + let profile = ElementFactoryProfile::builder("vp8enc") + .field("cq-level", 13) + .field("resize-allowed", false) + .build(); + assert_eq!(profile.0.n_fields(), 2); + assert_eq!(profile.0.name(), "vp8enc"); + assert_eq!(profile.0.get::("cq-level").unwrap(), 13); + assert!(!profile.0.get::("resize-allowed").unwrap()); + } + + #[test] + fn builder_field_from_str() { + gst::init().unwrap(); + + let profile = ElementFactoryProfile::builder("vp8enc") + .field("threads", 16) + .field_from_str("keyframe-mode", "disabled") + .build(); + assert_eq!(profile.0.n_fields(), 2); + assert_eq!(profile.0.name(), "vp8enc"); + assert_eq!(profile.0.get::("threads").unwrap(), 16); + + let keyframe_mode_value = profile.0.value("keyframe-mode").unwrap(); + assert!(format!("{:?}", keyframe_mode_value).starts_with("(GstVPXEncKfMode)")); + } +} diff --git a/src/element_properties.rs b/src/element_properties.rs deleted file mode 100644 index b38df8b40..000000000 --- a/src/element_properties.rs +++ /dev/null @@ -1,212 +0,0 @@ -use anyhow::{anyhow, Context, Result}; -use gst::prelude::*; -use gtk::glib::{ - self, - translate::{ToGlibPtr, UnsafeFrom}, -}; - -pub trait EncodingProfileExtManual { - fn set_element_properties(&self, element_properties: ElementProperties); -} - -impl> EncodingProfileExtManual for P { - fn set_element_properties(&self, element_properties: ElementProperties) { - unsafe { - gst_pbutils::ffi::gst_encoding_profile_set_element_properties( - self.as_ref().to_glib_none().0, - element_properties.into_inner().to_glib_full(), - ); - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, glib::Boxed)] -#[boxed_type(name = "KoohaElementProperties")] -pub struct ElementProperties(gst::Structure); - -impl Default for ElementProperties { - fn default() -> Self { - Self::builder().build() - } -} - -impl ElementProperties { - /// Creates an `ElementProperties` builder that build into - /// something similar to the following: - /// - /// element-properties-map, map = { - /// [openh264enc, gop-size=32, ], - /// [x264enc, key-int-max=32, tune=zerolatency], - /// } - pub fn builder() -> ElementPropertiesBuilder { - ElementPropertiesBuilder { map: Vec::new() } - } - - pub fn into_inner(self) -> gst::Structure { - self.0 - } -} - -#[must_use = "The builder must be built to be used"] -#[derive(Debug, Clone)] -pub struct ElementPropertiesBuilder { - map: Vec, -} - -impl ElementPropertiesBuilder { - /// Insert a new `element-properties-map` map item. - pub fn item(mut self, structure: ElementFactoryPropertiesMap) -> Self { - self.map.push(structure.into_inner().to_send_value()); - self - } - - pub fn build(self) -> ElementProperties { - ElementProperties( - gst::Structure::builder("element-properties-map") - .field("map", gst::List::from(self.map)) - .build(), - ) - } -} - -/// Wrapper around `gst::Structure` for an item -/// on a `ElementPropertiesMapBuilder`. -/// -/// # Example -/// -/// ```rust -/// ElementFactoryPropertiesMap::new("vp8enc") -/// .field("max-quantizer", 17) -/// .field_from_str("keyframe-mode", "disabled") -/// .field("buffer-size", 20000) -/// .field("threads", 16), -/// ``` -#[must_use = "The builder must be built to be used"] -#[derive(Debug, Clone)] -pub struct ElementFactoryPropertiesMap(gst::Structure); - -impl ElementFactoryPropertiesMap { - pub fn builder(factory_name: &str) -> ElementFactoryPropertiesMapBuilder { - ElementFactoryPropertiesMapBuilder::new(factory_name) - } - - pub fn into_inner(self) -> gst::Structure { - self.0 - } - - fn set_field_from_str(&mut self, property_name: &str, string: &str) -> Result<()> { - let factory_name = self.0.name(); - let element_type = gst::ElementFactory::find(factory_name) - .ok_or_else(|| anyhow!("Failed to find factory with name `{}`", factory_name))? - .load() - .with_context(|| anyhow!("Failed to load factory with name `{}`", factory_name))? - .element_type(); - let pspec = glib::object::ObjectClass::from_type(element_type) - .ok_or_else(|| anyhow!("Failed to create object class from type `{}`", element_type))? - .find_property(property_name) - .ok_or_else(|| { - glib::bool_error!( - "Property `{}` not found on type `{}`", - property_name, - element_type - ) - })?; - let value = unsafe { - glib::SendValue::unsafe_from( - glib::Value::deserialize_with_pspec(string, &pspec)?.into_raw(), - ) - }; - self.0.set_value(property_name, value); - Ok(()) - } -} - -pub struct ElementFactoryPropertiesMapBuilder { - prop_map: ElementFactoryPropertiesMap, -} - -impl ElementFactoryPropertiesMapBuilder { - pub fn new(factory_name: &str) -> Self { - Self { - prop_map: ElementFactoryPropertiesMap(gst::Structure::new_empty(factory_name)), - } - } - - pub fn field(mut self, property_name: &str, value: T) -> Self - where - T: ToSendValue + Sync, - { - self.prop_map.0.set(property_name, value); - self - } - - /// Parses the given string into a property of element from the - /// given `factory_name` with type based on the property's param spec. - /// - /// This works similar to `GObjectExtManualGst::set_property_from_str`. - /// - /// Note: The property will not be set if any of `factory_name`, `property_name` - /// or `string` is invalid. - pub fn field_from_str(mut self, property_name: &str, string: &str) -> Self { - if let Err(err) = self.prop_map.set_field_from_str(property_name, string) { - tracing::error!( - "Failed to set property `{}` to `{}`: {:?}", - property_name, - string, - err - ); - } - self - } - - pub fn build(self) -> ElementFactoryPropertiesMap { - self.prop_map - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn element_properties_builder() { - gst::init().unwrap(); - - let props_map = ElementFactoryPropertiesMap::builder("vp8enc") - .field("cq-level", 13) - .field("resize-allowed", false) - .build(); - let props_map_s = props_map.clone().into_inner(); - assert_eq!(props_map_s.n_fields(), 2); - assert_eq!(props_map_s.name(), "vp8enc"); - assert_eq!(props_map_s.get::("cq-level").unwrap(), 13); - assert!(!props_map_s.get::("resize-allowed").unwrap()); - - let elem_props = ElementProperties::builder().item(props_map.clone()).build(); - assert_eq!(elem_props.0.n_fields(), 1); - - let list = elem_props.0.get::("map").unwrap(); - assert_eq!(list.len(), 1); - assert_eq!( - list.get(0).unwrap().get::().unwrap(), - props_map.into_inner() - ); - } - - #[test] - fn element_factory_properties_map_field_from_str() { - gst::init().unwrap(); - - let prop_map_s = ElementFactoryPropertiesMap::builder("vp8enc") - .field("threads", 16) - .field_from_str("keyframe-mode", "disabled") - .build() - .into_inner(); - assert_eq!(prop_map_s.n_fields(), 2); - assert_eq!(prop_map_s.name(), "vp8enc"); - assert_eq!(prop_map_s.get::("threads").unwrap(), 16); - - let keyframe_mode_value = prop_map_s.value("keyframe-mode").unwrap(); - assert!(format!("{:?}", keyframe_mode_value).starts_with("(GstVPXEncKfMode)")); - } -} diff --git a/src/main.rs b/src/main.rs index 20fa20a85..8f4aaa2d0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,7 +26,7 @@ mod area_selector; mod audio_device; mod cancelled; mod config; -mod element_properties; +mod element_factory_profile; mod help; mod pipeline; mod profile; diff --git a/src/profile.rs b/src/profile.rs index 7ba34370d..19afe8f4b 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -4,7 +4,7 @@ use gtk::{glib, subclass::prelude::*}; use std::cell::RefCell; -use crate::element_properties::{ElementProperties, EncodingProfileExtManual}; +use crate::element_factory_profile::{ElementFactoryProfile, EncodingProfileExtManual}; mod imp { use super::*; @@ -13,12 +13,13 @@ mod imp { #[derive(Debug, Default)] pub struct Profile { pub(super) name: RefCell, - pub(super) container_preset_name: RefCell, - pub(super) container_element_properties: RefCell, - pub(super) video_preset_name: RefCell, - pub(super) video_element_properties: RefCell, - pub(super) audio_preset_name: RefCell, - pub(super) audio_element_properties: RefCell, + pub(super) muxer_profile: RefCell>, + pub(super) video_encoder_profile: RefCell>, + pub(super) audio_encoder_profile: RefCell>, + + pub(super) muxer_factory: RefCell>, + pub(super) video_encoder_factory: RefCell>, + pub(super) audio_encoder_factory: RefCell>, } #[glib::object_subclass] @@ -34,30 +35,21 @@ mod imp { glib::ParamSpecString::builder("name") .flags(glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY) .build(), - glib::ParamSpecString::builder("container-preset-name") - .flags(glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY) - .build(), glib::ParamSpecBoxed::builder( - "container-element-properties", - ElementProperties::static_type(), + "muxer-profile", + ElementFactoryProfile::static_type(), ) .flags(glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY) .build(), - glib::ParamSpecString::builder("video-preset-name") - .flags(glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY) - .build(), glib::ParamSpecBoxed::builder( - "video-element-properties", - ElementProperties::static_type(), + "video-encoder-profile", + ElementFactoryProfile::static_type(), ) .flags(glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY) .build(), - glib::ParamSpecString::builder("audio-preset-name") - .flags(glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY) - .build(), glib::ParamSpecBoxed::builder( - "audio-element-properties", - ElementProperties::static_type(), + "audio-encoder-profile", + ElementFactoryProfile::static_type(), ) .flags(glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY) .build(), @@ -79,29 +71,17 @@ mod imp { let name = value.get().unwrap(); obj.set_name(name); } - "container-preset-name" => { - let container_preset_name = value.get().unwrap(); - obj.set_container_preset_name(container_preset_name); - } - "container-element-properties" => { - let container_element_properties = value.get().unwrap(); - obj.set_container_element_properties(container_element_properties); + "muxer-profile" => { + let muxer_profile = value.get().unwrap(); + obj.set_muxer_profile(muxer_profile); } - "video-preset-name" => { - let video_preset_name = value.get().unwrap(); - obj.set_video_preset_name(video_preset_name); + "video-encoder-profile" => { + let video_encoder_profile = value.get().unwrap(); + obj.set_video_encoder_profile(video_encoder_profile); } - "video-element-properties" => { - let video_element_properties = value.get().unwrap(); - obj.set_video_element_properties(video_element_properties); - } - "audio-preset-name" => { - let audio_preset_name = value.get().unwrap(); - obj.set_audio_preset_name(audio_preset_name); - } - "audio-element-properties" => { - let audio_element_properties = value.get().unwrap(); - obj.set_audio_element_properties(audio_element_properties); + "audio-encoder-profile" => { + let audio_encoder_profile = value.get().unwrap(); + obj.set_audio_encoder_profile(audio_encoder_profile); } _ => unimplemented!(), } @@ -110,12 +90,9 @@ mod imp { fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { match pspec.name() { "name" => obj.name().to_value(), - "container-preset-name" => obj.container_preset_name().to_value(), - "container-element-properties" => obj.container_element_properties().to_value(), - "video-preset-name" => obj.video_preset_name().to_value(), - "video-element-properties" => obj.video_element_properties().to_value(), - "audio-preset-name" => obj.audio_preset_name().to_value(), - "audio-element-properties" => obj.audio_element_properties().to_value(), + "muxer-profile" => obj.muxer_profile().to_value(), + "video-encoder-profile" => obj.video_encoder_profile().to_value(), + "audio-encoder-profile" => obj.audio_encoder_profile().to_value(), _ => unimplemented!(), } } @@ -127,7 +104,22 @@ glib::wrapper! { } impl Profile { - pub fn new(name: &str) -> Self { + pub fn new( + name: &str, + muxer_profile: &ElementFactoryProfile, + video_encoder_profile: &ElementFactoryProfile, + audio_encoder_profile: &ElementFactoryProfile, + ) -> Self { + glib::Object::builder() + .property("name", name) + .property("muxer-profile", muxer_profile) + .property("video-encoder-profile", video_encoder_profile) + .property("audio-encoder-profile", audio_encoder_profile) + .build() + .expect("Failed to create Profile.") + } + + pub fn new_empty(name: &str) -> Self { glib::Object::builder() .property("name", name) .build() @@ -147,126 +139,138 @@ impl Profile { self.imp().name.borrow().clone() } - pub fn set_container_preset_name(&self, name: &str) { - if name == self.container_preset_name() { + pub fn set_muxer_profile(&self, profile: ElementFactoryProfile) { + if Some(&profile) == self.muxer_profile().as_ref() { return; } - tracing::debug!("Profile `{}` container set to `{}`", self.name(), name); - - self.imp().container_preset_name.replace(name.to_string()); - self.notify("container-preset-name"); + let imp = self.imp(); + imp.muxer_profile.replace(Some(profile)); + imp.muxer_factory.replace(None); + self.notify("muxer-profile"); } - pub fn container_preset_name(&self) -> String { - self.imp().container_preset_name.borrow().clone() + pub fn muxer_profile(&self) -> Option { + self.imp().muxer_profile.borrow().clone() } - pub fn set_container_element_properties(&self, properties: ElementProperties) { - if properties == self.container_element_properties() { + pub fn set_video_encoder_profile(&self, profile: ElementFactoryProfile) { + if Some(&profile) == self.video_encoder_profile().as_ref() { return; } - self.imp().container_element_properties.replace(properties); - self.notify("container-element-properties"); + let imp = self.imp(); + imp.video_encoder_profile.replace(Some(profile)); + imp.video_encoder_factory.replace(None); + self.notify("video-encoder-profile"); } - pub fn container_element_properties(&self) -> ElementProperties { - self.imp().container_element_properties.borrow().clone() + pub fn video_encoder_profile(&self) -> Option { + self.imp().video_encoder_profile.borrow().clone() } - pub fn set_video_preset_name(&self, name: &str) { - if name == self.video_preset_name() { + pub fn set_audio_encoder_profile(&self, profile: ElementFactoryProfile) { + if Some(&profile) == self.audio_encoder_profile().as_ref() { return; } - tracing::debug!("Profile `{}` video set to `{}`", self.name(), name); - - self.imp().video_preset_name.replace(name.to_string()); - self.notify("video-preset-name"); + let imp = self.imp(); + imp.audio_encoder_profile.replace(Some(profile)); + imp.audio_encoder_factory.replace(None); + self.notify("audio-encoder-profile"); } - pub fn video_preset_name(&self) -> String { - self.imp().video_preset_name.borrow().clone() + pub fn audio_encoder_profile(&self) -> Option { + self.imp().audio_encoder_profile.borrow().clone() } - pub fn set_video_element_properties(&self, properties: ElementProperties) { - if properties == self.video_element_properties() { - return; + pub fn muxer_factory(&self) -> Result { + if let Some(ref factory) = *self.imp().muxer_factory.borrow() { + return Ok(factory.clone()); } - self.imp().video_element_properties.replace(properties); - self.notify("video-element-properties"); - } - - pub fn video_element_properties(&self) -> ElementProperties { - self.imp().video_element_properties.borrow().clone() + let factory = find_element_factory( + self.muxer_profile() + .ok_or_else(|| anyhow!("Profile `{}` has no muxer profile", self.name()))? + .factory_name(), + )?; + self.imp().muxer_factory.replace(Some(factory.clone())); + Ok(factory) } - pub fn set_audio_preset_name(&self, name: &str) { - if name == self.audio_preset_name() { - return; + pub fn video_encoder_factory(&self) -> Result { + if let Some(ref factory) = *self.imp().video_encoder_factory.borrow() { + return Ok(factory.clone()); } - tracing::debug!("Profile `{}` audio set to `{}`", self.name(), name); - - self.imp().audio_preset_name.replace(name.to_string()); - self.notify("audio-preset-name"); - } - - pub fn audio_preset_name(&self) -> String { - self.imp().audio_preset_name.borrow().clone() + let factory = find_element_factory( + self.video_encoder_profile() + .ok_or_else(|| anyhow!("Profile `{}` has no video encoder profile", self.name()))? + .factory_name(), + )?; + self.imp() + .video_encoder_factory + .replace(Some(factory.clone())); + Ok(factory) } - pub fn set_audio_element_properties(&self, properties: ElementProperties) { - if properties == self.audio_element_properties() { - return; + pub fn audio_encoder_factory(&self) -> Result { + if let Some(ref factory) = *self.imp().audio_encoder_factory.borrow() { + return Ok(factory.clone()); } - self.imp().audio_element_properties.replace(properties); - self.notify("audio-element-properties"); - } - - pub fn audio_element_properties(&self) -> ElementProperties { - self.imp().audio_element_properties.borrow().clone() + let factory = find_element_factory( + self.audio_encoder_profile() + .ok_or_else(|| anyhow!("Profile `{}` has no audio encoder profile", self.name()))? + .factory_name(), + )?; + self.imp() + .audio_encoder_factory + .replace(Some(factory.clone())); + Ok(factory) } pub fn to_encoding_profile(&self) -> Result { - let container_preset_name = self.container_preset_name(); - let container_element_factory = find_element_factory(&container_preset_name)?; - let container_format_caps = profile_format_from_factory(&container_element_factory)?; + let muxer_factory = self.muxer_factory()?; + let container_format_caps = profile_format_from_factory(&muxer_factory)?; // Video Encoder - let video_preset_name = self.video_preset_name(); - let video_element_factory = find_element_factory(&video_preset_name)?; - let video_format_caps = profile_format_from_factory(&video_element_factory)?; + let video_encoder_factory = self.video_encoder_factory()?; + let video_format_caps = profile_format_from_factory(&video_encoder_factory)?; ensure!( - container_element_factory.can_sink_any_caps(&video_format_caps), + muxer_factory.can_sink_any_caps(&video_format_caps), "`{}` src is incompatible on `{}` sink", - video_preset_name, - container_preset_name + video_encoder_factory.name(), + muxer_factory.name() ); let video_profile = gst_pbutils::EncodingVideoProfile::builder(&video_format_caps) - .preset_name(&self.video_preset_name()) + .preset_name(&video_encoder_factory.name()) .presence(0) .build(); - video_profile.set_element_properties(self.video_element_properties()); + video_profile.set_element_properties( + self.video_encoder_profile() + .ok_or_else(|| anyhow!("Profile `{}` has no video encoder profile", self.name()))? + .into_element_properties(), + ); // Audio Encoder - let audio_preset_name = self.audio_preset_name(); - let audio_element_factory = find_element_factory(&audio_preset_name)?; - let audio_format_caps = profile_format_from_factory(&audio_element_factory)?; + let audio_encoder_factory = self.audio_encoder_factory()?; + let audio_format_caps = profile_format_from_factory(&audio_encoder_factory)?; ensure!( - container_element_factory.can_sink_any_caps(&audio_format_caps), + muxer_factory.can_sink_any_caps(&audio_format_caps), "`{}` src is incompatible on `{}` sink", - audio_preset_name, - container_preset_name + audio_encoder_factory.name(), + muxer_factory.name() ); let audio_profile = gst_pbutils::EncodingAudioProfile::builder(&audio_format_caps) - .preset_name(&self.audio_preset_name()) + .preset_name(&audio_encoder_factory.name()) .presence(0) .build(); - audio_profile.set_element_properties(self.audio_element_properties()); + audio_profile.set_element_properties( + self.audio_encoder_profile() + .ok_or_else(|| anyhow!("Profile `{}` has no audio encoder profile", self.name()))? + .into_element_properties(), + ); // Muxer let container_profile = @@ -275,7 +279,11 @@ impl Profile { .add_profile(&audio_profile) .presence(0) .build(); - container_profile.set_element_properties(self.container_element_properties()); + container_profile.set_element_properties( + self.muxer_profile() + .ok_or_else(|| anyhow!("Profile `{}` has no muxer profile", self.name()))? + .into_element_properties(), + ); Ok(container_profile) } @@ -300,7 +308,7 @@ impl Profile { fn find_element_factory(factory_name: &str) -> Result { gst::ElementFactory::find(factory_name) - .ok_or_else(|| anyhow!("Failed to find factory `{}`", factory_name)) + .ok_or_else(|| anyhow!("`{}` factory not found", factory_name)) } fn profile_format_from_factory(factory: &gst::ElementFactory) -> Result { @@ -336,15 +344,16 @@ mod tests { use super::*; fn new_simple_profile( - container_preset_name: &str, - video_preset_name: &str, - audio_preset_name: &str, + muxer_factory_name: &str, + video_encoder_factory_name: &str, + audio_encoder_factory_name: &str, ) -> Profile { - let profile = Profile::new(""); - profile.set_container_preset_name(container_preset_name); - profile.set_video_preset_name(video_preset_name); - profile.set_audio_preset_name(audio_preset_name); - profile + Profile::new( + "", + &ElementFactoryProfile::new(muxer_factory_name), + &ElementFactoryProfile::new(video_encoder_factory_name), + &ElementFactoryProfile::new(audio_encoder_factory_name), + ) } #[test] diff --git a/src/profile_manager.rs b/src/profile_manager.rs index 21733ff4e..f6c61ec42 100644 --- a/src/profile_manager.rs +++ b/src/profile_manager.rs @@ -4,11 +4,7 @@ use once_cell::unsync::OnceCell; use std::cell::RefCell; -use crate::{ - element_properties::{ElementFactoryPropertiesMap, ElementProperties}, - profile::Profile, - utils, -}; +use crate::{element_factory_profile::ElementFactoryProfile, profile::Profile, utils}; // TODO serialize @@ -229,66 +225,48 @@ fn builtin_profiles() -> Vec { vec![ // TODO bring back gif support `gifenc repeat=-1 speed=30`. Disable `win.record-speaker` and `win.record-mic` actions. 15 fps override // TODO vaapi? - // TODO Handle missing plugins (Hide profile if missing) + // TODO Handle missing plugins add warning if missing { - let profile = Profile::new("WebM"); - profile.set_container_preset_name("webmmux"); - profile.set_video_preset_name("vp8enc"); - profile.set_video_element_properties( - ElementProperties::builder() - .item( - ElementFactoryPropertiesMap::builder("vp8enc") - .field("max-quantizer", 17) - .field("cpu-used", 16) - .field("cq-level", 13) - .field("deadline", 1) - .field("static-threshold", 100) - .field_from_str("keyframe-mode", "disabled") - .field("buffer-size", 20000) - .field("threads", utils::ideal_thread_count()) - .build(), - ) + Profile::new( + "WebM", + &ElementFactoryProfile::new("webmmux"), + &ElementFactoryProfile::builder("vp8enc") + .field("max-quantizer", 17) + .field("cpu-used", 16) + .field("cq-level", 13) + .field("deadline", 1) + .field("static-threshold", 100) + .field_from_str("keyframe-mode", "disabled") + .field("buffer-size", 20000) + .field("threads", utils::ideal_thread_count()) .build(), - ); - profile.set_audio_preset_name("opusenc"); - profile + &ElementFactoryProfile::new("opusenc"), + ) }, { // TODO support "profile" = baseline - let profile = Profile::new("MP4"); - profile.set_container_preset_name("mp4mux"); - profile.set_video_preset_name("x264enc"); - profile.set_video_element_properties( - ElementProperties::builder() - .item( - ElementFactoryPropertiesMap::builder("x264enc") - .field("qp-max", 17) - .field_from_str("speed-preset", "superfast") - .field("threads", utils::ideal_thread_count()) - .build(), - ) + Profile::new( + "MP4", + &ElementFactoryProfile::new("mp4mux"), + &ElementFactoryProfile::builder("x264enc") + .field("qp-max", 17) + .field_from_str("speed-preset", "superfast") + .field("threads", utils::ideal_thread_count()) .build(), - ); - profile.set_audio_preset_name("lamemp3enc"); - profile + &ElementFactoryProfile::new("lamemp3enc"), + ) }, { - let profile = Profile::new("Matroska"); - profile.set_container_preset_name("matroskamux"); - profile.set_video_preset_name("x264enc"); - profile.set_video_element_properties( - ElementProperties::builder() - .item( - ElementFactoryPropertiesMap::builder("x264enc") - .field("qp-max", 17) - .field_from_str("speed-preset", "superfast") - .field("threads", utils::ideal_thread_count()) - .build(), - ) + Profile::new( + "Matroska", + &ElementFactoryProfile::new("matroskamux"), + &ElementFactoryProfile::builder("x264enc") + .field("qp-max", 17) + .field_from_str("speed-preset", "superfast") + .field("threads", utils::ideal_thread_count()) .build(), - ); - profile.set_audio_preset_name("opusenc"); - profile + &ElementFactoryProfile::new("opusenc"), + ) }, ] } diff --git a/src/profile_tile.rs b/src/profile_tile.rs index 963d5b82c..b94ca9bfd 100644 --- a/src/profile_tile.rs +++ b/src/profile_tile.rs @@ -1,3 +1,4 @@ +use gettextrs::gettext; use gtk::{ glib::{self, closure_local}, prelude::*, @@ -6,7 +7,7 @@ use gtk::{ use std::cell::{Cell, RefCell}; -use crate::profile::Profile; +use crate::{element_factory_profile::ElementFactoryProfile, profile::Profile}; mod imp { use super::*; @@ -117,25 +118,35 @@ mod imp { fn constructed(&self, obj: &Self::Type) { self.parent_constructed(obj); + let profile_to_label_func = |_: &glib::Binding, value: &glib::Value| { + let profile = value.get::>().unwrap(); + Some(profile.map_or_else( + || format!("{}", gettext("None")).to_value(), + |profile| profile.factory_name().to_value(), + )) + }; self.binding_group .bind("name", &self.name_label.get(), "label") .build(); self.binding_group - .bind("container-preset-name", &self.muxer_label.get(), "label") + .bind("muxer-profile", &self.muxer_label.get(), "label") + .transform_to(profile_to_label_func) .build(); self.binding_group .bind( - "video-preset-name", + "video-encoder-profile", &self.video_encoder_label.get(), "label", ) + .transform_to(profile_to_label_func) .build(); self.binding_group .bind( - "audio-preset-name", + "audio-encoder-profile", &self.audio_encoder_label.get(), "label", ) + .transform_to(profile_to_label_func) .build(); } } diff --git a/src/profile_window.rs b/src/profile_window.rs index a4a2331fa..2b45b7039 100644 --- a/src/profile_window.rs +++ b/src/profile_window.rs @@ -9,7 +9,10 @@ use once_cell::unsync::OnceCell; use std::cell::RefCell; -use crate::{profile::Profile, profile_manager::ProfileManager, profile_tile::ProfileTile}; +use crate::{ + element_factory_profile::ElementFactoryProfile, profile::Profile, + profile_manager::ProfileManager, profile_tile::ProfileTile, +}; mod imp { use super::*; @@ -57,7 +60,7 @@ mod imp { klass.install_action("profile-window.new-profile", None, |obj, _, _| { if let Some(model) = obj.model() { - model.set_active_profile(Some(&Profile::new("New Profile"))); + model.set_active_profile(Some(&Profile::new_empty("New Profile"))); } else { tracing::warn!("Found no model!"); } @@ -154,7 +157,7 @@ mod imp { if let Some(profile) = obj.model().and_then(|model| model.active_profile()) { let element_factory = selected_item.downcast::().unwrap(); let element_factory_name = element_factory.name(); - profile.set_container_preset_name(&element_factory_name); + profile.set_muxer_profile(ElementFactoryProfile::new(&element_factory_name)); } else { tracing::warn!("No model or active profile found but selected an element"); } @@ -166,7 +169,7 @@ mod imp { if let Some(profile) = obj.model().and_then(|model| model.active_profile()) { let element_factory = selected_item.downcast::().unwrap(); let element_factory_name = element_factory.name(); - profile.set_video_preset_name(&element_factory_name); + profile.set_video_encoder_profile(ElementFactoryProfile::new(&element_factory_name)); } else { tracing::warn!("No model or active profile found but selected an element"); } @@ -178,7 +181,7 @@ mod imp { if let Some(profile) = obj.model().and_then(|model| model.active_profile()) { let element_factory = selected_item.downcast::().unwrap(); let element_factory_name = element_factory.name(); - profile.set_audio_preset_name(&element_factory_name); + profile.set_audio_encoder_profile(ElementFactoryProfile::new(&element_factory_name)); } else { tracing::warn!("No model or active profile found but selected an element"); } @@ -416,25 +419,21 @@ impl ProfileWindow { imp.audio_encoder_row .block_signal(imp.audio_encoder_row_handler_id.get().unwrap()); - set_selected_item( - &imp.muxer_row.get(), - |element_factory: gst::ElementFactory| { - // TODO Comp with actual element - element_factory.name() == active_profile.container_preset_name() - }, - ); - set_selected_item( - &imp.video_encoder_row.get(), - |element_factory: gst::ElementFactory| { - element_factory.name() == active_profile.video_preset_name() - }, - ); - set_selected_item( - &imp.audio_encoder_row.get(), - |element_factory: gst::ElementFactory| { - element_factory.name() == active_profile.audio_preset_name() - }, - ); + set_selected_item(&imp.muxer_row.get(), |item: gst::ElementFactory| { + active_profile + .muxer_factory() + .map_or(false, |factory| factory == item) + }); + set_selected_item(&imp.video_encoder_row.get(), |item: gst::ElementFactory| { + active_profile + .video_encoder_factory() + .map_or(false, |factory| factory == item) + }); + set_selected_item(&imp.audio_encoder_row.get(), |item: gst::ElementFactory| { + active_profile + .audio_encoder_factory() + .map_or(false, |factory| factory == item) + }); imp.muxer_row .unblock_signal(imp.muxer_row_handler_id.get().unwrap()); diff --git a/src/window.rs b/src/window.rs index ef29f2ddb..7d8022fe0 100644 --- a/src/window.rs +++ b/src/window.rs @@ -256,7 +256,7 @@ impl Window { .start( Some(self), &application.settings(), - // TODO make record button insensitive when no profile + // TODO make record button insensitive when no or invalid profile &application.profile_manager().active_profile().unwrap(), ) .await; From 5c11d49935f583ee13ba4d6c37465abefcb0ba27 Mon Sep 17 00:00:00 2001 From: SeaDve Date: Wed, 31 Aug 2022 14:35:29 +0800 Subject: [PATCH 34/46] Drop useless braces --- src/profile_manager.rs | 78 +++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 42 deletions(-) diff --git a/src/profile_manager.rs b/src/profile_manager.rs index f6c61ec42..5c3512702 100644 --- a/src/profile_manager.rs +++ b/src/profile_manager.rs @@ -226,48 +226,42 @@ fn builtin_profiles() -> Vec { // TODO bring back gif support `gifenc repeat=-1 speed=30`. Disable `win.record-speaker` and `win.record-mic` actions. 15 fps override // TODO vaapi? // TODO Handle missing plugins add warning if missing - { - Profile::new( - "WebM", - &ElementFactoryProfile::new("webmmux"), - &ElementFactoryProfile::builder("vp8enc") - .field("max-quantizer", 17) - .field("cpu-used", 16) - .field("cq-level", 13) - .field("deadline", 1) - .field("static-threshold", 100) - .field_from_str("keyframe-mode", "disabled") - .field("buffer-size", 20000) - .field("threads", utils::ideal_thread_count()) - .build(), - &ElementFactoryProfile::new("opusenc"), - ) - }, - { - // TODO support "profile" = baseline - Profile::new( - "MP4", - &ElementFactoryProfile::new("mp4mux"), - &ElementFactoryProfile::builder("x264enc") - .field("qp-max", 17) - .field_from_str("speed-preset", "superfast") - .field("threads", utils::ideal_thread_count()) - .build(), - &ElementFactoryProfile::new("lamemp3enc"), - ) - }, - { - Profile::new( - "Matroska", - &ElementFactoryProfile::new("matroskamux"), - &ElementFactoryProfile::builder("x264enc") - .field("qp-max", 17) - .field_from_str("speed-preset", "superfast") - .field("threads", utils::ideal_thread_count()) - .build(), - &ElementFactoryProfile::new("opusenc"), - ) - }, + Profile::new( + "WebM", + &ElementFactoryProfile::new("webmmux"), + &ElementFactoryProfile::builder("vp8enc") + .field("max-quantizer", 17) + .field("cpu-used", 16) + .field("cq-level", 13) + .field("deadline", 1) + .field("static-threshold", 100) + .field_from_str("keyframe-mode", "disabled") + .field("buffer-size", 20000) + .field("threads", utils::ideal_thread_count()) + .build(), + &ElementFactoryProfile::new("opusenc"), + ), + // TODO support "profile" = baseline + Profile::new( + "MP4", + &ElementFactoryProfile::new("mp4mux"), + &ElementFactoryProfile::builder("x264enc") + .field("qp-max", 17) + .field_from_str("speed-preset", "superfast") + .field("threads", utils::ideal_thread_count()) + .build(), + &ElementFactoryProfile::new("lamemp3enc"), + ), + Profile::new( + "Matroska", + &ElementFactoryProfile::new("matroskamux"), + &ElementFactoryProfile::builder("x264enc") + .field("qp-max", 17) + .field_from_str("speed-preset", "superfast") + .field("threads", utils::ideal_thread_count()) + .build(), + &ElementFactoryProfile::new("opusenc"), + ), ] } From fec75d2429483c247440f70569a865721c4e9655 Mon Sep 17 00:00:00 2001 From: SeaDve Date: Wed, 31 Aug 2022 16:41:03 +0800 Subject: [PATCH 35/46] Feat to change file extension --- data/resources/ui/profile-tile.ui | 3 + data/resources/ui/profile-window.ui | 5 ++ src/element_factory_profile.rs | 6 +- src/pipeline.rs | 6 +- src/profile.rs | 58 +++++++++++--------- src/profile_manager.rs | 85 +++++++++++++++++------------ src/profile_window.rs | 15 ++++- 7 files changed, 109 insertions(+), 69 deletions(-) diff --git a/data/resources/ui/profile-tile.ui b/data/resources/ui/profile-tile.ui index b82f12c7c..cdae22d67 100644 --- a/data/resources/ui/profile-tile.ui +++ b/data/resources/ui/profile-tile.ui @@ -30,6 +30,7 @@ 0 0.5 + True @@ -52,6 +53,7 @@ 0 0.5 + True @@ -74,6 +76,7 @@ 0 0.5 + True diff --git a/data/resources/ui/profile-window.ui b/data/resources/ui/profile-window.ui index 5d0068d86..5a0069944 100644 --- a/data/resources/ui/profile-window.ui +++ b/data/resources/ui/profile-window.ui @@ -68,6 +68,11 @@ Name + + + File Extension + + Container Format diff --git a/src/element_factory_profile.rs b/src/element_factory_profile.rs index 72ba72ee4..31e257b85 100644 --- a/src/element_factory_profile.rs +++ b/src/element_factory_profile.rs @@ -15,7 +15,7 @@ impl> EncodingProfileExtManual for P { unsafe { gst_pbutils::ffi::gst_encoding_profile_set_element_properties( self.as_ref().to_glib_none().0, - element_properties.to_glib_full(), + element_properties.into_ptr(), ); } } @@ -40,7 +40,7 @@ impl ElementFactoryProfile { self.0.name() } - pub fn into_element_properties(self) -> gst::Structure { + pub fn to_element_properties(&self) -> gst::Structure { gst::Structure::builder("element-properties-map") .field("map", gst::List::from(vec![self.0.to_send_value()])) .build() @@ -123,7 +123,7 @@ mod tests { #[test] fn into_element_properties() { let profile = ElementFactoryProfile::new("vp8enc"); - let element_properties = profile.clone().into_element_properties(); + let element_properties = profile.to_element_properties(); assert_eq!(element_properties.name(), "element-properties-map"); assert_eq!( element_properties diff --git a/src/pipeline.rs b/src/pipeline.rs index 9ca277412..5250c7dab 100644 --- a/src/pipeline.rs +++ b/src/pipeline.rs @@ -1,6 +1,5 @@ use anyhow::{anyhow, bail, Context, Ok, Result}; use gst::prelude::*; -use gst_pbutils::prelude::*; use gtk::graphene::{Rect, Size}; use std::{ @@ -74,8 +73,7 @@ impl PipelineBuilder { } pub fn build(&self) -> Result { - let encoding_profile = self.profile.to_encoding_profile()?; - let file_extension = encoding_profile.file_extension().ok_or_else(|| { + let file_extension = self.profile.file_extension().ok_or_else(|| { anyhow!( "Found no file extension for profile with name `{}`", self.profile.name() @@ -84,7 +82,7 @@ impl PipelineBuilder { let file_path = new_recording_path(&self.saving_location, file_extension); let encodebin = element_factory_make("encodebin")?; - encodebin.set_property("profile", &encoding_profile); + encodebin.set_property("profile", &self.profile.to_encoding_profile()?); let queue = element_factory_make("queue")?; let filesink = element_factory_make_named("filesink", Some("filesink"))?; filesink.set_property( diff --git a/src/profile.rs b/src/profile.rs index 19afe8f4b..848fe3dbc 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -13,6 +13,7 @@ mod imp { #[derive(Debug, Default)] pub struct Profile { pub(super) name: RefCell, + pub(super) file_extension: RefCell>, pub(super) muxer_profile: RefCell>, pub(super) video_encoder_profile: RefCell>, pub(super) audio_encoder_profile: RefCell>, @@ -35,6 +36,9 @@ mod imp { glib::ParamSpecString::builder("name") .flags(glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY) .build(), + glib::ParamSpecString::builder("file-extension") + .flags(glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY) + .build(), glib::ParamSpecBoxed::builder( "muxer-profile", ElementFactoryProfile::static_type(), @@ -71,6 +75,10 @@ mod imp { let name = value.get().unwrap(); obj.set_name(name); } + "file-extension" => { + let file_extension = value.get().unwrap(); + obj.set_file_extension(file_extension); + } "muxer-profile" => { let muxer_profile = value.get().unwrap(); obj.set_muxer_profile(muxer_profile); @@ -90,6 +98,7 @@ mod imp { fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { match pspec.name() { "name" => obj.name().to_value(), + "file-extension" => obj.file_extension().to_value(), "muxer-profile" => obj.muxer_profile().to_value(), "video-encoder-profile" => obj.video_encoder_profile().to_value(), "audio-encoder-profile" => obj.audio_encoder_profile().to_value(), @@ -104,22 +113,7 @@ glib::wrapper! { } impl Profile { - pub fn new( - name: &str, - muxer_profile: &ElementFactoryProfile, - video_encoder_profile: &ElementFactoryProfile, - audio_encoder_profile: &ElementFactoryProfile, - ) -> Self { - glib::Object::builder() - .property("name", name) - .property("muxer-profile", muxer_profile) - .property("video-encoder-profile", video_encoder_profile) - .property("audio-encoder-profile", audio_encoder_profile) - .build() - .expect("Failed to create Profile.") - } - - pub fn new_empty(name: &str) -> Self { + pub fn new(name: &str) -> Self { glib::Object::builder() .property("name", name) .build() @@ -139,6 +133,21 @@ impl Profile { self.imp().name.borrow().clone() } + pub fn set_file_extension(&self, file_extension: &str) { + if Some(file_extension) == self.file_extension().as_deref() { + return; + } + + self.imp() + .file_extension + .replace(Some(file_extension.to_string())); + self.notify("file-extension"); + } + + pub fn file_extension(&self) -> Option { + self.imp().file_extension.borrow().clone() + } + pub fn set_muxer_profile(&self, profile: ElementFactoryProfile) { if Some(&profile) == self.muxer_profile().as_ref() { return; @@ -250,7 +259,7 @@ impl Profile { video_profile.set_element_properties( self.video_encoder_profile() .ok_or_else(|| anyhow!("Profile `{}` has no video encoder profile", self.name()))? - .into_element_properties(), + .to_element_properties(), ); // Audio Encoder @@ -269,7 +278,7 @@ impl Profile { audio_profile.set_element_properties( self.audio_encoder_profile() .ok_or_else(|| anyhow!("Profile `{}` has no audio encoder profile", self.name()))? - .into_element_properties(), + .to_element_properties(), ); // Muxer @@ -282,7 +291,7 @@ impl Profile { container_profile.set_element_properties( self.muxer_profile() .ok_or_else(|| anyhow!("Profile `{}` has no muxer profile", self.name()))? - .into_element_properties(), + .to_element_properties(), ); Ok(container_profile) @@ -348,12 +357,11 @@ mod tests { video_encoder_factory_name: &str, audio_encoder_factory_name: &str, ) -> Profile { - Profile::new( - "", - &ElementFactoryProfile::new(muxer_factory_name), - &ElementFactoryProfile::new(video_encoder_factory_name), - &ElementFactoryProfile::new(audio_encoder_factory_name), - ) + let p = Profile::new(""); + p.set_muxer_profile(ElementFactoryProfile::new(muxer_factory_name)); + p.set_video_encoder_profile(ElementFactoryProfile::new(video_encoder_factory_name)); + p.set_audio_encoder_profile(ElementFactoryProfile::new(audio_encoder_factory_name)); + p } #[test] diff --git a/src/profile_manager.rs b/src/profile_manager.rs index 5c3512702..80ba01ae2 100644 --- a/src/profile_manager.rs +++ b/src/profile_manager.rs @@ -226,42 +226,54 @@ fn builtin_profiles() -> Vec { // TODO bring back gif support `gifenc repeat=-1 speed=30`. Disable `win.record-speaker` and `win.record-mic` actions. 15 fps override // TODO vaapi? // TODO Handle missing plugins add warning if missing - Profile::new( - "WebM", - &ElementFactoryProfile::new("webmmux"), - &ElementFactoryProfile::builder("vp8enc") - .field("max-quantizer", 17) - .field("cpu-used", 16) - .field("cq-level", 13) - .field("deadline", 1) - .field("static-threshold", 100) - .field_from_str("keyframe-mode", "disabled") - .field("buffer-size", 20000) - .field("threads", utils::ideal_thread_count()) - .build(), - &ElementFactoryProfile::new("opusenc"), - ), - // TODO support "profile" = baseline - Profile::new( - "MP4", - &ElementFactoryProfile::new("mp4mux"), - &ElementFactoryProfile::builder("x264enc") - .field("qp-max", 17) - .field_from_str("speed-preset", "superfast") - .field("threads", utils::ideal_thread_count()) - .build(), - &ElementFactoryProfile::new("lamemp3enc"), - ), - Profile::new( - "Matroska", - &ElementFactoryProfile::new("matroskamux"), - &ElementFactoryProfile::builder("x264enc") - .field("qp-max", 17) - .field_from_str("speed-preset", "superfast") - .field("threads", utils::ideal_thread_count()) - .build(), - &ElementFactoryProfile::new("opusenc"), - ), + { + let p = Profile::new("WebM"); + p.set_file_extension("webm"); + p.set_muxer_profile(ElementFactoryProfile::new("webmmux")); + p.set_video_encoder_profile( + ElementFactoryProfile::builder("vp8enc") + .field("max-quantizer", 17) + .field("cpu-used", 16) + .field("cq-level", 13) + .field("deadline", 1) + .field("static-threshold", 100) + .field_from_str("keyframe-mode", "disabled") + .field("buffer-size", 20000) + .field("threads", utils::ideal_thread_count()) + .build(), + ); + p.set_audio_encoder_profile(ElementFactoryProfile::new("opusenc")); + p + }, + { + // TODO support "profile" = baseline + let p = Profile::new("MP4"); + p.set_file_extension("mp4"); + p.set_muxer_profile(ElementFactoryProfile::new("mp4mux")); + p.set_video_encoder_profile( + ElementFactoryProfile::builder("x264enc") + .field("qp-max", 17) + .field_from_str("speed-preset", "superfast") + .field("threads", utils::ideal_thread_count()) + .build(), + ); + p.set_audio_encoder_profile(ElementFactoryProfile::new("lamemp3enc")); + p + }, + { + let p = Profile::new("Matroska"); + p.set_file_extension("mkv"); + p.set_muxer_profile(ElementFactoryProfile::new("matroskamux")); + p.set_video_encoder_profile( + ElementFactoryProfile::builder("x264enc") + .field("qp-max", 17) + .field_from_str("speed-preset", "superfast") + .field("threads", utils::ideal_thread_count()) + .build(), + ); + p.set_audio_encoder_profile(ElementFactoryProfile::new("opusenc")); + p + }, ] } @@ -314,6 +326,7 @@ mod tests { fn builtin_profiles_work() { for profile in builtin_profiles() { assert!(profile.to_encoding_profile().is_ok()); + assert!(profile.file_extension().is_some()); } } } diff --git a/src/profile_window.rs b/src/profile_window.rs index 2b45b7039..9f8641ac9 100644 --- a/src/profile_window.rs +++ b/src/profile_window.rs @@ -29,6 +29,8 @@ mod imp { #[template_child] pub(super) name_row: TemplateChild, #[template_child] + pub(super) file_extension_row: TemplateChild, + #[template_child] pub(super) muxer_row: TemplateChild, #[template_child] pub(super) video_encoder_row: TemplateChild, @@ -44,6 +46,7 @@ mod imp { pub(super) model_signal_handler_ids: RefCell>, pub(super) name_row_binding: RefCell>, + pub(super) file_extension_row_binding: RefCell>, pub(super) muxer_row_handler_id: OnceCell, pub(super) video_encoder_row_handler_id: OnceCell, pub(super) audio_encoder_row_handler_id: OnceCell, @@ -60,7 +63,7 @@ mod imp { klass.install_action("profile-window.new-profile", None, |obj, _, _| { if let Some(model) = obj.model() { - model.set_active_profile(Some(&Profile::new_empty("New Profile"))); + model.set_active_profile(Some(&Profile::new("New Profile"))); } else { tracing::warn!("Found no model!"); } @@ -390,11 +393,15 @@ impl ProfileWindow { if let Some(binding) = imp.name_row_binding.take() { binding.unbind(); } + if let Some(binding) = imp.file_extension_row_binding.take() { + binding.unbind(); + } let active_profile = self.model().and_then(|model| model.active_profile()); let has_active_profile = active_profile.is_some(); imp.name_row.set_visible(has_active_profile); + imp.file_extension_row.set_visible(has_active_profile); imp.muxer_row.set_visible(has_active_profile); imp.video_encoder_row.set_visible(has_active_profile); imp.audio_encoder_row.set_visible(has_active_profile); @@ -411,6 +418,12 @@ impl ProfileWindow { .flags(glib::BindingFlags::SYNC_CREATE | glib::BindingFlags::BIDIRECTIONAL) .build(), )); + imp.file_extension_row_binding.replace(Some( + active_profile + .bind_property("file-extension", &imp.file_extension_row.get(), "text") + .flags(glib::BindingFlags::SYNC_CREATE | glib::BindingFlags::BIDIRECTIONAL) + .build(), + )); imp.muxer_row .block_signal(imp.muxer_row_handler_id.get().unwrap()); From 06f02a36fd374a5008b14707d93ead7ad900e5fe Mon Sep 17 00:00:00 2001 From: SeaDve Date: Wed, 31 Aug 2022 16:47:00 +0800 Subject: [PATCH 36/46] Fix crash --- src/profile_window.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/profile_window.rs b/src/profile_window.rs index 9f8641ac9..a10f47f92 100644 --- a/src/profile_window.rs +++ b/src/profile_window.rs @@ -421,6 +421,10 @@ impl ProfileWindow { imp.file_extension_row_binding.replace(Some( active_profile .bind_property("file-extension", &imp.file_extension_row.get(), "text") + .transform_to(|_: &glib::Binding, value: &glib::Value| { + let file_extension = value.get::>().unwrap(); + Some(file_extension.unwrap_or_default().to_value()) + }) .flags(glib::BindingFlags::SYNC_CREATE | glib::BindingFlags::BIDIRECTIONAL) .build(), )); From d53c28bb76a595c5a24f59c2ea2d8d39442436d6 Mon Sep 17 00:00:00 2001 From: SeaDve Date: Wed, 31 Aug 2022 16:58:30 +0800 Subject: [PATCH 37/46] Use the last active profile as the active instead of the first one if the active profile is deleted --- src/profile_manager.rs | 16 ++++++++++++---- src/profile_window.rs | 4 ++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/profile_manager.rs b/src/profile_manager.rs index 80ba01ae2..10a5f46e7 100644 --- a/src/profile_manager.rs +++ b/src/profile_manager.rs @@ -20,6 +20,7 @@ mod imp { pub struct ProfileManager { pub(super) active_profile: RefCell>, + pub(super) last_active_profile: RefCell>, pub(super) profiles: RefCell>, pub(super) known_muxers: OnceCell, @@ -113,10 +114,15 @@ impl ProfileManager { } pub fn set_active_profile(&self, profile: Option<&Profile>) { - if profile == self.active_profile().as_ref() { + let old_profile = self.active_profile(); + + if profile == old_profile.as_ref() { return; } + let imp = self.imp(); + imp.last_active_profile.replace(old_profile); + tracing::debug!( "Set active profile to {:?}", profile.map(|profile| profile.name()) @@ -128,7 +134,7 @@ impl ProfileManager { } } - self.imp().active_profile.replace(profile.cloned()); + imp.active_profile.replace(profile.cloned()); self.notify("active-profile"); } @@ -161,8 +167,10 @@ impl ProfileManager { self.items_changed(position as u32, 1, 0); if Some(removed) == self.active_profile() { - if let Some(first_item) = self.get_profile(0) { - self.set_active_profile(Some(&first_item)); + // Clone to prevent BorrowMutError + let last_active_profile = imp.last_active_profile.borrow().as_ref().cloned(); + if let Some(ref last_active_profile) = last_active_profile { + self.set_active_profile(Some(last_active_profile)); } } } else { diff --git a/src/profile_window.rs b/src/profile_window.rs index a10f47f92..c7a12878e 100644 --- a/src/profile_window.rs +++ b/src/profile_window.rs @@ -69,7 +69,7 @@ mod imp { } }); - klass.install_action("undo-delete-toast.dismiss", None, |obj, _, _| { + klass.install_action("undo-delete-toast.undo", None, |obj, _, _| { if let Some(model) = obj.model() { for profile in obj.imp().profile_purgatory.take() { model.add_profile(profile); @@ -312,7 +312,7 @@ impl ProfileWindow { let toast = adw::Toast::builder() .priority(adw::ToastPriority::High) .button_label(&gettext("_Undo")) - .action_name("undo-delete-toast.dismiss") + .action_name("undo-delete-toast.undo") .build(); toast.connect_dismissed(clone!(@weak self as obj => move |_| { From d0fd49507a801b4ee828ec5d82ba8bd86ddccf01 Mon Sep 17 00:00:00 2001 From: SeaDve Date: Wed, 31 Aug 2022 19:55:27 +0800 Subject: [PATCH 38/46] Add ability to specify caps values --- src/element_factory_profile.rs | 197 +++++++++++++++++++++++++++------ src/profile.rs | 182 +++++------------------------- src/profile_manager.rs | 31 +++--- src/profile_window.rs | 9 +- 4 files changed, 213 insertions(+), 206 deletions(-) diff --git a/src/element_factory_profile.rs b/src/element_factory_profile.rs index 31e257b85..746654196 100644 --- a/src/element_factory_profile.rs +++ b/src/element_factory_profile.rs @@ -1,10 +1,13 @@ -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, ensure, Context, Result}; use gst::prelude::*; use gtk::glib::{ self, translate::{ToGlibPtr, UnsafeFrom}, ToSendValue, }; +use once_cell::unsync::OnceCell; + +use std::cell::RefCell; pub trait EncodingProfileExtManual { fn set_element_properties(&self, element_properties: gst::Structure); @@ -21,42 +24,129 @@ impl> EncodingProfileExtManual for P { } } -#[derive(Debug, Clone, PartialEq, Eq, glib::Boxed)] +#[derive(Debug, Clone, glib::Boxed)] #[boxed_type(name = "KoohaElementFactoryProfile", nullable)] -pub struct ElementFactoryProfile(gst::Structure); +pub struct ElementFactoryProfile { + structure: gst::Structure, + factory: OnceCell, + format: OnceCell, + format_fields: RefCell>>, +} + +impl PartialEq for ElementFactoryProfile { + fn eq(&self, other: &Self) -> bool { + self.structure == other.structure && self.format == other.format + } +} + +impl Eq for ElementFactoryProfile {} impl ElementFactoryProfile { pub fn new(factory_name: &str) -> Self { Self::builder(factory_name).build() } - pub fn builder(factory_name: &str) -> ElementFactoryProfileBuilder { - ElementFactoryProfileBuilder { - structure: gst::Structure::new_empty(factory_name), - } + pub fn builder(factory_name: &str) -> ElementFactoryProfileBuilder<'_> { + ElementFactoryProfileBuilder::new(factory_name) } pub fn factory_name(&self) -> &str { - self.0.name() + self.structure.name() + } + + pub fn factory(&self) -> Result<&gst::ElementFactory> { + self.factory + .get_or_try_init(|| find_element_factory(self.factory_name())) + } + + pub fn format(&self) -> Result<&gst::Caps> { + if let Some(caps) = self.format.get() { + return Ok(caps); + } + + let factory = self.factory()?; + let format = profile_format_from_factory(factory, self.format_fields.take().unwrap())?; + Ok(self.format.try_insert(format).unwrap()) } - pub fn to_element_properties(&self) -> gst::Structure { + pub fn element_properties(&self) -> gst::Structure { gst::Structure::builder("element-properties-map") - .field("map", gst::List::from(vec![self.0.to_send_value()])) + .field("map", gst::List::from(vec![self.structure.to_send_value()])) .build() } } -pub struct ElementFactoryProfileBuilder { - structure: gst::Structure, +fn profile_format_from_factory( + factory: &gst::ElementFactory, + values: Vec<(String, glib::SendValue)>, +) -> Result { + let factory_name = factory.name(); + + ensure!( + factory.has_type(gst::ElementFactoryType::ENCODER | gst::ElementFactoryType::MUXER), + "Factory `{}` must be an encoder or muxer to be used in a profile", + factory_name + ); + + for template in factory.static_pad_templates() { + if template.direction() == gst::PadDirection::Src { + let template_caps = template.caps(); + if let Some(structure) = template_caps.structure(0) { + let mut structure = structure.to_owned(); + + for (f, v) in values { + structure.set_value(&f, v); + } + + let mut caps = gst::Caps::new_empty(); + caps.get_mut().unwrap().append_structure(structure); + return Ok(caps); + } + } + } + + Err(anyhow!( + "Failed to find profile format for factory `{}`", + factory_name + )) } -impl ElementFactoryProfileBuilder { - pub fn field(mut self, property_name: &str, value: T) -> Self +fn find_element_factory(factory_name: &str) -> Result { + gst::ElementFactory::find(factory_name) + .ok_or_else(|| anyhow!("`{}` factory not found", factory_name)) +} + +pub struct ElementFactoryProfileBuilder<'a> { + factory_name: &'a str, + element_properties: Vec<(&'a str, glib::SendValue)>, + format_fields: Vec<(&'a str, glib::SendValue)>, +} + +impl<'a> ElementFactoryProfileBuilder<'a> { + pub fn new(factory_name: &'a str) -> Self { + Self { + factory_name, + element_properties: Vec::new(), + format_fields: Vec::new(), + } + } + + #[allow(clippy::needless_pass_by_value)] + pub fn format_field(mut self, field: &'a str, value: T) -> Self + where + T: ToSendValue + Sync, + { + self.format_fields.push((field, value.to_send_value())); + self + } + + #[allow(clippy::needless_pass_by_value)] + pub fn property(mut self, property_name: &'a str, value: T) -> Self where T: ToSendValue + Sync, { - self.structure.set(property_name, value); + self.element_properties + .push((property_name, value.to_send_value())); self } @@ -67,11 +157,9 @@ impl ElementFactoryProfileBuilder { /// /// Note: The property will not be set if any of `factory_name`, `property_name` /// or `string` is invalid. - pub fn field_from_str(mut self, property_name: &str, value_string: &str) -> Self { - let factory_name = self.structure.name(); - - match value_from_str(factory_name, property_name, value_string) { - Ok(value) => self.structure.set_value(property_name, value), + pub fn property_from_str(mut self, property_name: &'a str, value_string: &str) -> Self { + match value_from_str(self.factory_name, property_name, value_string) { + Ok(value) => self.element_properties.push((property_name, value)), Err(err) => tracing::warn!( "Failed to set property `{}` to `{}`: {:?}", property_name, @@ -84,7 +172,17 @@ impl ElementFactoryProfileBuilder { } pub fn build(self) -> ElementFactoryProfile { - ElementFactoryProfile(self.structure) + ElementFactoryProfile { + structure: gst::Structure::from_iter(self.factory_name, self.element_properties), + factory: OnceCell::new(), + format: OnceCell::new(), + format_fields: RefCell::new(Some( + self.format_fields + .iter() + .map(|(k, v)| (str::to_string(k), v.to_send_value())) + .collect(), + )), + } } } @@ -121,9 +219,9 @@ mod tests { use super::*; #[test] - fn into_element_properties() { + fn element_properties() { let profile = ElementFactoryProfile::new("vp8enc"); - let element_properties = profile.to_element_properties(); + let element_properties = profile.element_properties(); assert_eq!(element_properties.name(), "element-properties-map"); assert_eq!( element_properties @@ -132,7 +230,34 @@ mod tests { .get(0) .unwrap() .get::(), - Ok(profile.0) + Ok(profile.structure) + ); + } + + #[test] + fn test_profile_format_from_factory() { + #[track_caller] + fn profile_format_from_factory_name(factory_name: &str) -> Result { + profile_format_from_factory(&find_element_factory(factory_name).unwrap(), Vec::new()) + } + + assert!(profile_format_from_factory_name("vp8enc") + .unwrap() + .can_intersect(&gst::Caps::builder("video/x-vp8").build())); + assert!(profile_format_from_factory_name("opusenc") + .unwrap() + .can_intersect(&gst::Caps::builder("audio/x-opus").build())); + assert!(profile_format_from_factory_name("matroskamux") + .unwrap() + .can_intersect(&gst::Caps::builder("video/x-matroska").build())); + assert!(!profile_format_from_factory_name("matroskamux") + .unwrap() + .can_intersect(&gst::Caps::builder("video/x-vp8").build()),); + assert_eq!( + profile_format_from_factory_name("audioconvert") + .unwrap_err() + .to_string(), + "Factory `audioconvert` must be an encoder or muxer to be used in a profile" ); } @@ -141,13 +266,13 @@ mod tests { gst::init().unwrap(); let profile = ElementFactoryProfile::builder("vp8enc") - .field("cq-level", 13) - .field("resize-allowed", false) + .property("cq-level", 13) + .property("resize-allowed", false) .build(); - assert_eq!(profile.0.n_fields(), 2); - assert_eq!(profile.0.name(), "vp8enc"); - assert_eq!(profile.0.get::("cq-level").unwrap(), 13); - assert!(!profile.0.get::("resize-allowed").unwrap()); + assert_eq!(profile.structure.n_fields(), 2); + assert_eq!(profile.structure.name(), "vp8enc"); + assert_eq!(profile.structure.get::("cq-level").unwrap(), 13); + assert!(!profile.structure.get::("resize-allowed").unwrap()); } #[test] @@ -155,14 +280,14 @@ mod tests { gst::init().unwrap(); let profile = ElementFactoryProfile::builder("vp8enc") - .field("threads", 16) - .field_from_str("keyframe-mode", "disabled") + .property("threads", 16) + .property_from_str("keyframe-mode", "disabled") .build(); - assert_eq!(profile.0.n_fields(), 2); - assert_eq!(profile.0.name(), "vp8enc"); - assert_eq!(profile.0.get::("threads").unwrap(), 16); + assert_eq!(profile.structure.n_fields(), 2); + assert_eq!(profile.structure.name(), "vp8enc"); + assert_eq!(profile.structure.get::("threads").unwrap(), 16); - let keyframe_mode_value = profile.0.value("keyframe-mode").unwrap(); + let keyframe_mode_value = profile.structure.value("keyframe-mode").unwrap(); assert!(format!("{:?}", keyframe_mode_value).starts_with("(GstVPXEncKfMode)")); } } diff --git a/src/profile.rs b/src/profile.rs index 848fe3dbc..65027b18b 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -17,10 +17,6 @@ mod imp { pub(super) muxer_profile: RefCell>, pub(super) video_encoder_profile: RefCell>, pub(super) audio_encoder_profile: RefCell>, - - pub(super) muxer_factory: RefCell>, - pub(super) video_encoder_factory: RefCell>, - pub(super) audio_encoder_factory: RefCell>, } #[glib::object_subclass] @@ -155,7 +151,6 @@ impl Profile { let imp = self.imp(); imp.muxer_profile.replace(Some(profile)); - imp.muxer_factory.replace(None); self.notify("muxer-profile"); } @@ -170,7 +165,6 @@ impl Profile { let imp = self.imp(); imp.video_encoder_profile.replace(Some(profile)); - imp.video_encoder_factory.replace(None); self.notify("video-encoder-profile"); } @@ -185,7 +179,6 @@ impl Profile { let imp = self.imp(); imp.audio_encoder_profile.replace(Some(profile)); - imp.audio_encoder_factory.replace(None); self.notify("audio-encoder-profile"); } @@ -193,108 +186,56 @@ impl Profile { self.imp().audio_encoder_profile.borrow().clone() } - pub fn muxer_factory(&self) -> Result { - if let Some(ref factory) = *self.imp().muxer_factory.borrow() { - return Ok(factory.clone()); - } - - let factory = find_element_factory( - self.muxer_profile() - .ok_or_else(|| anyhow!("Profile `{}` has no muxer profile", self.name()))? - .factory_name(), - )?; - self.imp().muxer_factory.replace(Some(factory.clone())); - Ok(factory) - } - - pub fn video_encoder_factory(&self) -> Result { - if let Some(ref factory) = *self.imp().video_encoder_factory.borrow() { - return Ok(factory.clone()); - } - - let factory = find_element_factory( - self.video_encoder_profile() - .ok_or_else(|| anyhow!("Profile `{}` has no video encoder profile", self.name()))? - .factory_name(), - )?; - self.imp() - .video_encoder_factory - .replace(Some(factory.clone())); - Ok(factory) - } - - pub fn audio_encoder_factory(&self) -> Result { - if let Some(ref factory) = *self.imp().audio_encoder_factory.borrow() { - return Ok(factory.clone()); - } - - let factory = find_element_factory( - self.audio_encoder_profile() - .ok_or_else(|| anyhow!("Profile `{}` has no audio encoder profile", self.name()))? - .factory_name(), - )?; - self.imp() - .audio_encoder_factory - .replace(Some(factory.clone())); - Ok(factory) - } - pub fn to_encoding_profile(&self) -> Result { - let muxer_factory = self.muxer_factory()?; - let container_format_caps = profile_format_from_factory(&muxer_factory)?; + let muxer_profile = self + .muxer_profile() + .ok_or_else(|| anyhow!("Profile `{}` has no muxer profile", self.name()))?; + let muxer_factory = muxer_profile.factory()?; // Video Encoder - let video_encoder_factory = self.video_encoder_factory()?; - let video_format_caps = profile_format_from_factory(&video_encoder_factory)?; + let video_encoder_profile = self + .video_encoder_profile() + .ok_or_else(|| anyhow!("Profile `{}` has no video encoder profile", self.name()))?; + let video_profile_format = video_encoder_profile.format()?; ensure!( - muxer_factory.can_sink_any_caps(&video_format_caps), + muxer_factory.can_sink_any_caps(video_profile_format), "`{}` src is incompatible on `{}` sink", - video_encoder_factory.name(), - muxer_factory.name() + video_encoder_profile.factory_name(), + muxer_profile.factory_name() ); - let video_profile = gst_pbutils::EncodingVideoProfile::builder(&video_format_caps) - .preset_name(&video_encoder_factory.name()) + let gst_video_profile = gst_pbutils::EncodingVideoProfile::builder(video_profile_format) + .preset_name(video_encoder_profile.factory_name()) .presence(0) .build(); - video_profile.set_element_properties( - self.video_encoder_profile() - .ok_or_else(|| anyhow!("Profile `{}` has no video encoder profile", self.name()))? - .to_element_properties(), - ); + gst_video_profile.set_element_properties(video_encoder_profile.element_properties()); // Audio Encoder - let audio_encoder_factory = self.audio_encoder_factory()?; - let audio_format_caps = profile_format_from_factory(&audio_encoder_factory)?; + let audio_encoder_profile = self + .audio_encoder_profile() + .ok_or_else(|| anyhow!("Profile `{}` has no audio encoder profile", self.name()))?; + let audio_profile_format = audio_encoder_profile.format()?; ensure!( - muxer_factory.can_sink_any_caps(&audio_format_caps), + muxer_factory.can_sink_any_caps(audio_profile_format), "`{}` src is incompatible on `{}` sink", - audio_encoder_factory.name(), - muxer_factory.name() + audio_encoder_profile.factory_name(), + muxer_profile.factory_name() ); - let audio_profile = gst_pbutils::EncodingAudioProfile::builder(&audio_format_caps) - .preset_name(&audio_encoder_factory.name()) + let gst_audio_profile = gst_pbutils::EncodingAudioProfile::builder(audio_profile_format) + .preset_name(audio_encoder_profile.factory_name()) .presence(0) .build(); - audio_profile.set_element_properties( - self.audio_encoder_profile() - .ok_or_else(|| anyhow!("Profile `{}` has no audio encoder profile", self.name()))? - .to_element_properties(), - ); + gst_audio_profile.set_element_properties(audio_encoder_profile.element_properties()); // Muxer - let container_profile = - gst_pbutils::EncodingContainerProfile::builder(&container_format_caps) - .add_profile(&video_profile) - .add_profile(&audio_profile) + let gst_container_profile = + gst_pbutils::EncodingContainerProfile::builder(muxer_profile.format()?) + .add_profile(&gst_video_profile) + .add_profile(&gst_audio_profile) .presence(0) .build(); - container_profile.set_element_properties( - self.muxer_profile() - .ok_or_else(|| anyhow!("Profile `{}` has no muxer profile", self.name()))? - .to_element_properties(), - ); + gst_container_profile.set_element_properties(muxer_profile.element_properties()); - Ok(container_profile) + Ok(gst_container_profile) } pub fn deep_clone(&self) -> Self { @@ -315,39 +256,6 @@ impl Profile { } } -fn find_element_factory(factory_name: &str) -> Result { - gst::ElementFactory::find(factory_name) - .ok_or_else(|| anyhow!("`{}` factory not found", factory_name)) -} - -fn profile_format_from_factory(factory: &gst::ElementFactory) -> Result { - let factory_name = factory.name(); - - ensure!( - factory.has_type(gst::ElementFactoryType::ENCODER | gst::ElementFactoryType::MUXER), - "Factory `{}` must be an encoder or muxer to be used in a profile", - factory_name - ); - - for template in factory.static_pad_templates() { - if template.direction() == gst::PadDirection::Src { - let template_caps = template.caps(); - if let Some(structure) = template_caps.structure(0) { - let mut caps = gst::Caps::new_empty(); - caps.get_mut() - .unwrap() - .append_structure(structure.to_owned()); - return Ok(caps); - } - } - } - - Err(anyhow!( - "Failed to find profile format for factory `{}`", - factory_name - )) -} - #[cfg(test)] mod tests { use super::*; @@ -378,34 +286,4 @@ mod tests { "`lamemp3enc` src is incompatible on `webmmux` sink" ); } - - #[test] - fn test_profile_format_from_factory_name() { - assert!( - profile_format_from_factory(&find_element_factory("vp8enc").unwrap()) - .unwrap() - .can_intersect(&gst::Caps::builder("video/x-vp8").build()), - ); - assert!( - profile_format_from_factory(&find_element_factory("opusenc").unwrap()) - .unwrap() - .can_intersect(&gst::Caps::builder("audio/x-opus").build()) - ); - assert!( - profile_format_from_factory(&find_element_factory("matroskamux").unwrap()) - .unwrap() - .can_intersect(&gst::Caps::builder("video/x-matroska").build()), - ); - assert!( - !profile_format_from_factory(&find_element_factory("matroskamux").unwrap()) - .unwrap() - .can_intersect(&gst::Caps::builder("video/x-vp8").build()), - ); - assert_eq!( - profile_format_from_factory(&find_element_factory("audioconvert").unwrap()) - .unwrap_err() - .to_string(), - "Factory `audioconvert` must be an encoder or muxer to be used in a profile" - ); - } } diff --git a/src/profile_manager.rs b/src/profile_manager.rs index 10a5f46e7..7985a5e88 100644 --- a/src/profile_manager.rs +++ b/src/profile_manager.rs @@ -240,29 +240,29 @@ fn builtin_profiles() -> Vec { p.set_muxer_profile(ElementFactoryProfile::new("webmmux")); p.set_video_encoder_profile( ElementFactoryProfile::builder("vp8enc") - .field("max-quantizer", 17) - .field("cpu-used", 16) - .field("cq-level", 13) - .field("deadline", 1) - .field("static-threshold", 100) - .field_from_str("keyframe-mode", "disabled") - .field("buffer-size", 20000) - .field("threads", utils::ideal_thread_count()) + .property("max-quantizer", 17) + .property("cpu-used", 16) + .property("cq-level", 13) + .property("deadline", 1) + .property("static-threshold", 100) + .property_from_str("keyframe-mode", "disabled") + .property("buffer-size", 20000) + .property("threads", utils::ideal_thread_count()) .build(), ); p.set_audio_encoder_profile(ElementFactoryProfile::new("opusenc")); p }, { - // TODO support "profile" = baseline let p = Profile::new("MP4"); p.set_file_extension("mp4"); p.set_muxer_profile(ElementFactoryProfile::new("mp4mux")); p.set_video_encoder_profile( ElementFactoryProfile::builder("x264enc") - .field("qp-max", 17) - .field_from_str("speed-preset", "superfast") - .field("threads", utils::ideal_thread_count()) + .format_field("profile", "baseline") + .property("qp-max", 17) + .property_from_str("speed-preset", "superfast") + .property("threads", utils::ideal_thread_count()) .build(), ); p.set_audio_encoder_profile(ElementFactoryProfile::new("lamemp3enc")); @@ -274,9 +274,10 @@ fn builtin_profiles() -> Vec { p.set_muxer_profile(ElementFactoryProfile::new("matroskamux")); p.set_video_encoder_profile( ElementFactoryProfile::builder("x264enc") - .field("qp-max", 17) - .field_from_str("speed-preset", "superfast") - .field("threads", utils::ideal_thread_count()) + .format_field("profile", "baseline") + .property("qp-max", 17) + .property_from_str("speed-preset", "superfast") + .property("threads", utils::ideal_thread_count()) .build(), ); p.set_audio_encoder_profile(ElementFactoryProfile::new("opusenc")); diff --git a/src/profile_window.rs b/src/profile_window.rs index c7a12878e..c79a49d63 100644 --- a/src/profile_window.rs +++ b/src/profile_window.rs @@ -438,17 +438,20 @@ impl ProfileWindow { set_selected_item(&imp.muxer_row.get(), |item: gst::ElementFactory| { active_profile - .muxer_factory() + .muxer_profile() + .and_then(|p| p.factory().ok().cloned()) .map_or(false, |factory| factory == item) }); set_selected_item(&imp.video_encoder_row.get(), |item: gst::ElementFactory| { active_profile - .video_encoder_factory() + .video_encoder_profile() + .and_then(|p| p.factory().ok().cloned()) .map_or(false, |factory| factory == item) }); set_selected_item(&imp.audio_encoder_row.get(), |item: gst::ElementFactory| { active_profile - .audio_encoder_factory() + .audio_encoder_profile() + .and_then(|p| p.factory().ok().cloned()) .map_or(false, |factory| factory == item) }); From 2d6b9ab07514a3657752fde4813baa579f64dd3a Mon Sep 17 00:00:00 2001 From: SeaDve Date: Wed, 31 Aug 2022 20:12:31 +0800 Subject: [PATCH 39/46] Missing pot file --- po/POTFILES.in | 1 + 1 file changed, 1 insertion(+) diff --git a/po/POTFILES.in b/po/POTFILES.in index 9f5a016e4..9b28f8bf4 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -8,6 +8,7 @@ src/about.rs src/application.rs src/audio_device.rs src/main.rs +src/profile_tile.rs src/profile_window.rs src/recording.rs src/settings.rs From 3946095ea9eb8d7a24cf20b4352882ee692aaed5 Mon Sep 17 00:00:00 2001 From: SeaDve Date: Thu, 1 Sep 2022 16:06:44 +0800 Subject: [PATCH 40/46] If profile is last active when it is removed, clear out last active --- src/profile_manager.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/profile_manager.rs b/src/profile_manager.rs index 7985a5e88..907027673 100644 --- a/src/profile_manager.rs +++ b/src/profile_manager.rs @@ -166,13 +166,17 @@ impl ProfileManager { let removed = imp.profiles.borrow_mut().remove(position); self.items_changed(position as u32, 1, 0); - if Some(removed) == self.active_profile() { + if Some(&removed) == self.active_profile().as_ref() { // Clone to prevent BorrowMutError let last_active_profile = imp.last_active_profile.borrow().as_ref().cloned(); if let Some(ref last_active_profile) = last_active_profile { self.set_active_profile(Some(last_active_profile)); } } + + if Some(&removed) == imp.last_active_profile.borrow().as_ref() { + imp.last_active_profile.replace(None); + } } else { tracing::debug!( "Didn't delete profile with name `{}` as it does not exist", From fc265499317c9d455b36ccf344be1247d62b3570 Mon Sep 17 00:00:00 2001 From: SeaDve Date: Thu, 1 Sep 2022 16:31:59 +0800 Subject: [PATCH 41/46] Improve profile tile UI --- data/resources/style.css | 8 +- data/resources/ui/profile-tile.ui | 204 ++++++++++++++-------------- data/resources/ui/profile-window.ui | 1 + 3 files changed, 110 insertions(+), 103 deletions(-) diff --git a/data/resources/style.css b/data/resources/style.css index 296e18953..b441292f8 100644 --- a/data/resources/style.css +++ b/data/resources/style.css @@ -29,12 +29,12 @@ button.copy-done { } } -profiletile { - padding: 18px; +profiletile .inner-box { + padding: 24px; } -profiletile.selected { - padding: 15px; +profiletile.selected .inner-box { + padding: 21px; border-style: solid; border-width: 3px; border-color: @accent_color; diff --git a/data/resources/ui/profile-tile.ui b/data/resources/ui/profile-tile.ui index cdae22d67..665f25098 100644 --- a/data/resources/ui/profile-tile.ui +++ b/data/resources/ui/profile-tile.ui @@ -1,114 +1,120 @@ + + + Copy + profile-tile.copy + + + Delete + profile-tile.delete + +