move back and make the context setup shared
(this is in preparation for making it possible to do HW encoding)
This commit is contained in:
parent
fa929641e5
commit
2da9b38974
5 changed files with 149 additions and 120 deletions
|
@ -18,6 +18,8 @@ futures-util = { version = "0.3", default-features = false, features = ["sink",
|
||||||
|
|
||||||
# ffmpeg
|
# ffmpeg
|
||||||
ffmpeg-the-third = "2.0.1"
|
ffmpeg-the-third = "2.0.1"
|
||||||
|
|
||||||
|
# misc stuff
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
serde = "1.0.209"
|
serde = "1.0.209"
|
||||||
serde_json = "1.0.128"
|
serde_json = "1.0.128"
|
||||||
|
|
|
@ -5,7 +5,7 @@ use std::{
|
||||||
use tokio::sync::mpsc::{self, error::TryRecvError};
|
use tokio::sync::mpsc::{self, error::TryRecvError};
|
||||||
|
|
||||||
use super::ffmpeg;
|
use super::ffmpeg;
|
||||||
use super::h264_encoder_sw::H264EncoderSW;
|
use super::h264_encoder::H264Encoder;
|
||||||
|
|
||||||
pub enum EncodeThreadInput {
|
pub enum EncodeThreadInput {
|
||||||
Init { size: crate::types::Size },
|
Init { size: crate::types::Size },
|
||||||
|
@ -40,7 +40,7 @@ fn encoder_thread_main(
|
||||||
) {
|
) {
|
||||||
let mut packet = ffmpeg::Packet::empty();
|
let mut packet = ffmpeg::Packet::empty();
|
||||||
|
|
||||||
let mut encoder: Option<H264EncoderSW> = None;
|
let mut encoder: Option<H264Encoder> = None;
|
||||||
let mut sws = None;
|
let mut sws = None;
|
||||||
|
|
||||||
let mut yuv_frame = None;
|
let mut yuv_frame = None;
|
||||||
|
@ -73,8 +73,9 @@ fn encoder_thread_main(
|
||||||
.expect("Failed to create SWS conversion context"),
|
.expect("Failed to create SWS conversion context"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO: HW support!
|
||||||
encoder = Some(
|
encoder = Some(
|
||||||
H264EncoderSW::new(size, 60, 3 * (1000 * 1000))
|
H264Encoder::new_software(size, 60, 3 * (1000 * 1000))
|
||||||
.expect("Failed to create encoder"),
|
.expect("Failed to create encoder"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
142
server/src/video/h264_encoder.rs
Normal file
142
server/src/video/h264_encoder.rs
Normal file
|
@ -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<lavc::Context, ffmpeg::Error> {
|
||||||
|
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<Self> {
|
||||||
|
// 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<ControlFlow> 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(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<lavc::Context, ffmpeg::Error> {
|
|
||||||
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<Self> {
|
|
||||||
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<ControlFlow> 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(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
pub mod h264_encoder_sw;
|
pub mod h264_encoder;
|
||||||
pub mod encoder_thread;
|
pub mod encoder_thread;
|
||||||
|
|
||||||
pub use ffmpeg_the_third as ffmpeg;
|
pub use ffmpeg_the_third as ffmpeg;
|
||||||
|
|
Loading…
Reference in a new issue