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-the-third = "2.0.1"
|
||||
|
||||
# misc stuff
|
||||
rand = "0.8.5"
|
||||
serde = "1.0.209"
|
||||
serde_json = "1.0.128"
|
||||
|
|
|
@ -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<H264EncoderSW> = None;
|
||||
let mut encoder: Option<H264Encoder> = 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"),
|
||||
);
|
||||
}
|
||||
|
|
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 use ffmpeg_the_third as ffmpeg;
|
||||
|
|
Loading…
Reference in a new issue