diff --git a/server/Cargo.toml b/server/Cargo.toml index beae758..9fd7cfd 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -18,6 +18,8 @@ futures-util = { version = "0.3", default-features = false, features = ["sink", # ffmpeg ffmpeg-the-third = "2.0.1" + +# misc stuff rand = "0.8.5" serde = "1.0.209" serde_json = "1.0.128" diff --git a/server/src/video/encoder_thread.rs b/server/src/video/encoder_thread.rs index ec3f7ec..4d73695 100644 --- a/server/src/video/encoder_thread.rs +++ b/server/src/video/encoder_thread.rs @@ -5,7 +5,7 @@ use std::{ use tokio::sync::mpsc::{self, error::TryRecvError}; use super::ffmpeg; -use super::h264_encoder_sw::H264EncoderSW; +use super::h264_encoder::H264Encoder; pub enum EncodeThreadInput { Init { size: crate::types::Size }, @@ -40,7 +40,7 @@ fn encoder_thread_main( ) { let mut packet = ffmpeg::Packet::empty(); - let mut encoder: Option = None; + let mut encoder: Option = None; let mut sws = None; let mut yuv_frame = None; @@ -73,8 +73,9 @@ fn encoder_thread_main( .expect("Failed to create SWS conversion context"), ); + // TODO: HW support! encoder = Some( - H264EncoderSW::new(size, 60, 3 * (1000 * 1000)) + H264Encoder::new_software(size, 60, 3 * (1000 * 1000)) .expect("Failed to create encoder"), ); } diff --git a/server/src/video/h264_encoder.rs b/server/src/video/h264_encoder.rs new file mode 100644 index 0000000..cb253cf --- /dev/null +++ b/server/src/video/h264_encoder.rs @@ -0,0 +1,142 @@ +use super::ffmpeg; +use anyhow::Context; +use ffmpeg::error::EAGAIN; + +use ffmpeg::codec as lavc; // lavc + +use crate::types::Size; + +/// this is required for libx264 to like. Work +pub fn create_context_from_codec(codec: ffmpeg::Codec) -> Result { + unsafe { + let context = ffmpeg::sys::avcodec_alloc_context3(codec.as_ptr()); + if context.is_null() { + return Err(ffmpeg::Error::Unknown); + } + + let context = lavc::Context::wrap(context, None); + Ok(context) + } +} + +fn create_context_and_set_common_parameters( + codec: &str, + size: Size, + max_framerate: u32, + bitrate: usize, +) -> anyhow::Result<(ffmpeg::Codec, ffmpeg::encoder::video::Video)> { + let encoder = ffmpeg::encoder::find_by_name(codec) + .expect(&format!("could not find the codec \"{codec}\"")); + + let mut video_encoder_context = create_context_from_codec(encoder)?.encoder().video()?; + + // TODO: Either no GOP, or a fairly large one. + // idk + let gop = /*if max_framerate / 2 != 0 { + max_framerate / 2 + } else { + max_framerate + } */ + i32::MAX as u32; + + video_encoder_context.set_width(size.width); + video_encoder_context.set_height(size.height); + video_encoder_context.set_frame_rate(Some(ffmpeg::Rational(1, max_framerate as i32))); + + video_encoder_context.set_bit_rate(bitrate); + //video_encoder_context.set_max_bit_rate(bitrate); + + // qp TODO: + //video_encoder_context.set_qmax(30); + //video_encoder_context.set_qmin(35); + + video_encoder_context.set_time_base(ffmpeg::Rational(1, max_framerate as i32).invert()); + video_encoder_context.set_format(ffmpeg::format::Pixel::YUV420P); + + video_encoder_context.set_gop(gop); + video_encoder_context.set_max_b_frames(0); + + unsafe { + (*video_encoder_context.as_mut_ptr()).delay = 0; + (*video_encoder_context.as_mut_ptr()).refs = 0; + } + + Ok((encoder, video_encoder_context)) +} + +/// A simple H.264 encoder. Currently software only, however +/// pieces are being put in place to eventually allow HW encoding. +pub struct H264Encoder { + encoder: ffmpeg::encoder::video::Encoder, +} + +impl H264Encoder { + pub fn new_software(size: Size, max_framerate: u32, bitrate: usize) -> anyhow::Result { + // Create the libx264 context + let (encoder, mut video_encoder_context) = + create_context_and_set_common_parameters("libx264", size, max_framerate, bitrate)?; + + video_encoder_context.set_format(ffmpeg::format::Pixel::YUV420P); + + let threads = std::thread::available_parallelism().expect("ggg").get() / 8; + + // FIXME: tracing please. + println!("H264Encoder::new_software(): Using {threads} threads to encode"); + + // Frame-level threading causes [N] frames of latency + // so we use slice-level threading to reduce the latency + // as much as possible while still allowing threading + video_encoder_context.set_threading(ffmpeg::threading::Config { + kind: ffmpeg::threading::Type::Slice, + count: threads, + }); + + // Set libx264 applicable dictionary options + let mut dict = ffmpeg::Dictionary::new(); + dict.set("tune", "zerolatency"); + dict.set("preset", "veryfast"); + + // This could probably be moved but then it would mean returning the dictionary too + // which is fine I guess it just seems a bit rickity + dict.set("profile", "main"); + + // TODO: + dict.set("crf", "43"); + dict.set("crf_max", "48"); + + let encoder = video_encoder_context + .open_as_with(encoder, dict) + .with_context(|| "While opening x264 video codec")?; + + Ok(Self { encoder: encoder }) + } + + pub fn send_frame(&mut self, frame: &ffmpeg::Frame) { + self.encoder.send_frame(frame).unwrap(); + } + + pub fn send_eof(&mut self) { + self.encoder.send_eof().unwrap(); + } + + // Shuold this return a Result so we can make it easier to know when to continue? + pub fn receive_packet(&mut self, packet: &mut ffmpeg::Packet) -> anyhow::Result<()> { + loop { + match self.encoder.receive_packet(packet) { + Ok(_) => break, + Err(ffmpeg::Error::Other { errno }) => { + if errno != EAGAIN { + return Err(ffmpeg::Error::Other { errno: errno }.into()); + } else { + // EAGAIN is not fatal, and simply means + // we should just try again + break; + } + } + Err(e) => return Err(e.into()), + } + } + + Ok(()) + } +} diff --git a/server/src/video/h264_encoder_sw.rs b/server/src/video/h264_encoder_sw.rs deleted file mode 100644 index 83d936a..0000000 --- a/server/src/video/h264_encoder_sw.rs +++ /dev/null @@ -1,116 +0,0 @@ -use super::ffmpeg; -use anyhow::Context; -use ffmpeg::error::EAGAIN; - -use ffmpeg::codec as lavc; // lavc - -use crate::types::Size; - -/// this is required for libx264 to like. Work -pub fn create_context_from_codec(codec: ffmpeg::Codec) -> Result { - unsafe { - let context = ffmpeg::sys::avcodec_alloc_context3(codec.as_ptr()); - if context.is_null() { - return Err(ffmpeg::Error::Unknown); - } - - let context = lavc::Context::wrap(context, None); - Ok(context) - } -} - -/// A simple software H.264 encoder. -pub struct H264EncoderSW { - encoder: ffmpeg::encoder::video::Encoder, -} - -impl H264EncoderSW { - pub fn new(size: Size, max_framerate: u32, bitrate: usize) -> anyhow::Result { - let encoder = ffmpeg::encoder::find(lavc::Id::H264).expect("could not find libx264"); - - let mut video_encoder_context = create_context_from_codec(encoder)?.encoder().video()?; - - let gop = /*if max_framerate / 2 != 0 { - max_framerate / 2 - } else { - max_framerate - } */ - i32::MAX as u32; - - video_encoder_context.set_width(size.width); - video_encoder_context.set_height(size.height); - video_encoder_context.set_frame_rate(Some(ffmpeg::Rational(1, max_framerate as i32))); - - video_encoder_context.set_bit_rate(bitrate); - //video_encoder_context.set_max_bit_rate(bitrate); - - // qp TODO: - //video_encoder_context.set_qmax(30); - //video_encoder_context.set_qmin(35); - - video_encoder_context.set_time_base(ffmpeg::Rational(1, max_framerate as i32).invert()); - video_encoder_context.set_format(ffmpeg::format::Pixel::YUV420P); - - video_encoder_context.set_gop(gop); - video_encoder_context.set_max_b_frames(0); - - unsafe { - (*video_encoder_context.as_mut_ptr()).delay = 0; - (*video_encoder_context.as_mut_ptr()).refs = 0; - } - - let threads = 4; - println!("using {threads} threads to encode"); - - // frame-level threading causes [N] frames of latency - // so we use slice-level threading to reduce the latency - // as much as possible while still using it - video_encoder_context.set_threading(ffmpeg::threading::Config { - kind: ffmpeg::threading::Type::Slice, - count: threads, - }); - - let mut dict = ffmpeg::Dictionary::new(); - dict.set("tune", "zerolatency"); - dict.set("preset", "veryfast"); - dict.set("profile", "main"); - // TODO: - dict.set("crf", "43"); - dict.set("crf_max", "48"); - - let encoder = video_encoder_context - .open_as_with(encoder, dict) - .with_context(|| "While opening x264 video codec")?; - - Ok(Self { encoder: encoder }) - } - - pub fn send_frame(&mut self, frame: &ffmpeg::Frame) { - self.encoder.send_frame(frame).unwrap(); - } - - pub fn send_eof(&mut self) { - self.encoder.send_eof().unwrap(); - } - - // Shuold this return a Result so we can make it easier to know when to continue? - pub fn receive_packet(&mut self, packet: &mut ffmpeg::Packet) -> anyhow::Result<()> { - loop { - match self.encoder.receive_packet(packet) { - Ok(_) => break, - Err(ffmpeg::Error::Other { errno }) => { - if errno != EAGAIN { - return Err(ffmpeg::Error::Other { errno: errno }.into()); - } else { - // EAGAIN is not fatal, and simply means - // we should just try again - break; - } - } - Err(e) => return Err(e.into()), - } - } - - Ok(()) - } -} diff --git a/server/src/video/mod.rs b/server/src/video/mod.rs index acc3bcd..0ecc478 100644 --- a/server/src/video/mod.rs +++ b/server/src/video/mod.rs @@ -1,4 +1,4 @@ -pub mod h264_encoder_sw; +pub mod h264_encoder; pub mod encoder_thread; pub use ffmpeg_the_third as ffmpeg;