diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3fcf60c..2bacccd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: strategy: matrix: - ffmpeg_version: ["3.4", "4.0", "4.1", "4.2", "4.3", "4.4", "5.0", "5.1", "6.0"] + ffmpeg_version: ["4.3", "4.4", "5.0", "5.1", "6.0"] fail-fast: false steps: @@ -119,7 +119,6 @@ jobs: runs-on: ubuntu-latest container: jrottenberg/ffmpeg:6-ubuntu - steps: - name: Checkout uses: actions/checkout@v3 diff --git a/Cargo.toml b/Cargo.toml index aebbaa9..82a9483 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "video-rs" description = "High-level video toolkit based on ffmpeg." keywords = ["video", "ffmpeg", "encoding", "decoding", "muxing"] categories = ["multimedia", "multimedia::video"] -version = "0.6.1" +version = "0.7.0" authors = ["Oddity.ai Developers "] license = "MIT OR Apache-2.0" edition = "2021" diff --git a/README.md b/README.md index b5d0f25..00bc48a 100644 --- a/README.md +++ b/README.md @@ -29,14 +29,14 @@ on this (`video-rs` depends on the `ffmpeg-next` crate). Then, add the following to your dependencies in `Cargo.toml`: ```toml -video-rs = "0.6" +video-rs = "0.7" ``` Use the `ndarray` feature to be able to use raw frames with the [`ndarray`](https://github.com/rust-ndarray/ndarray) crate: ```toml -video-rs = { version = "0.6", features = ["ndarray"] } +video-rs = { version = "0.7", features = ["ndarray"] } ``` ## 📖 Examples @@ -44,57 +44,45 @@ video-rs = { version = "0.6", features = ["ndarray"] } Decode a video and print the RGB value for the top left pixel: ```rust -use video_rs::{self, Decoder, Locator}; +use video_rs::decode::Decoder; +use video_rs::Url; fn main() { video_rs::init().unwrap(); - let source = Locator::Url( + let source = "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" - .parse() - .unwrap(), - ); - - let mut decoder = Decoder::new(&source) - .expect("failed to create decoder"); + .parse::() + .unwrap(); + let mut decoder = Decoder::new(source).expect("failed to create decoder"); for frame in decoder.decode_iter() { if let Ok((_, frame)) = frame { - let rgb = frame - .slice(ndarray::s![0, 0, ..]) - .to_slice() - .unwrap(); - println!( - "pixel at 0, 0: {}, {}, {}", - rgb[0], - rgb[1], - rgb[2], - ); + let rgb = frame.slice(ndarray::s![0, 0, ..]).to_slice().unwrap(); + println!("pixel at 0, 0: {}, {}, {}", rgb[0], rgb[1], rgb[2],); } else { break; } } } - ``` Encode a 🌈 video, using `ndarray` to create each frame: ```rust -use std::path::PathBuf; +use std::path::Path; use ndarray::Array3; -use video_rs::{Encoder, EncoderSettings, Locator, Time}; +use video_rs::encode::{Encoder, Settings}; +use video_rs::time::Time; fn main() { video_rs::init().unwrap(); - let destination: Locator = PathBuf::from("rainbow.mp4").into(); - let settings = EncoderSettings::for_h264_yuv420p(1280, 720, false); - - let mut encoder = Encoder::new(&destination, settings) - .expect("failed to create encoder"); + let settings = Settings::preset_h264_yuv420p(1280, 720, false); + let mut encoder = + Encoder::new(Path::new("rainbow.mp4"), settings).expect("failed to create encoder"); let duration: Time = Time::from_nth_of_a_second(24); let mut position = Time::zero(); @@ -106,8 +94,7 @@ fn main() { .encode(&frame, &position) .expect("failed to encode frame"); - // Update the current position and add the inter-frame - // duration to it. + // Update the current position and add the inter-frame duration to it. position = position.aligned_with(&duration).add(); } @@ -115,13 +102,12 @@ fn main() { } fn rainbow_frame(p: f32) -> Array3 { - // This is what generated the rainbow effect! We loop through - // the HSV color spectrum and convert to RGB. + // This is what generated the rainbow effect! We loop through the HSV color spectrum and convert + // to RGB. let rgb = hsv_to_rgb(p * 360.0, 100.0, 100.0); - // This creates a frame with height 720, width 1280 and three - // channels. The RGB values for each pixel are equal, and - // determined by the `rgb` we chose above. + // This creates a frame with height 720, width 1280 and three channels. The RGB values for each + // pixel are equal, and determined by the `rgb` we chose above. Array3::from_shape_fn((720, 1280, 3), |(_y, _x, c)| rgb[c]) } @@ -152,7 +138,6 @@ fn hsv_to_rgb(h: f32, s: f32, v: f32) -> [u8; 3] { ((b + m) * 255.0) as u8, ] } - ``` ## 🪲 Debugging diff --git a/src/decode.rs b/src/decode.rs index 3d15dbf..17b51f9 100644 --- a/src/decode.rs +++ b/src/decode.rs @@ -7,25 +7,97 @@ use ffmpeg::software::scaling::{context::Context as AvScaler, flag::Flags as AvS use ffmpeg::util::error::EAGAIN; use ffmpeg::{Error as AvError, Rational as AvRational}; -use crate::ffi::{copy_frame_props, set_decoder_context_time_base}; -use crate::frame::FRAME_PIXEL_FORMAT; -use crate::io::Reader; +use crate::error::Error; +use crate::ffi; +use crate::ffi_hwaccel; +#[cfg(feature = "ndarray")] +use crate::frame::Frame; +use crate::frame::{RawFrame, FRAME_PIXEL_FORMAT}; +use crate::hwaccel::{HardwareAccelerationContext, HardwareAccelerationDeviceType}; +use crate::io::{Reader, ReaderBuilder}; +use crate::location::Location; use crate::options::Options; use crate::packet::Packet; +use crate::resize::Resize; use crate::time::Time; -use crate::{Error, Locator, RawFrame, Resize}; - -#[cfg(feature = "ndarray")] -use crate::{ffi::convert_frame_to_ndarray_rgb24, Frame}; type Result = std::result::Result; +/// Builds a [`Decoder`]. +pub struct DecoderBuilder<'a> { + source: Location, + options: Option<&'a Options>, + resize: Option, + hardware_acceleration_device_type: Option, +} + +impl<'a> DecoderBuilder<'a> { + /// Create a decoder with the specified source. + /// + /// * `source` - Source to decode. + pub fn new(source: impl Into) -> Self { + Self { + source: source.into(), + options: None, + resize: None, + hardware_acceleration_device_type: None, + } + } + + /// Set custom options. Options are applied to the input. + /// + /// * `options` - Custom options. + pub fn with_options(mut self, options: &'a Options) -> Self { + self.options = Some(options); + self + } + + /// Set resizing to apply to frames. + /// + /// * `resize` - Resizing to apply. + pub fn with_resize(mut self, resize: Resize) -> Self { + self.resize = Some(resize); + self + } + + /// Enable hardware acceleration with the specified device type. + /// + /// * `device_type` - Device to use for hardware acceleration. + pub fn with_hardware_acceleration( + mut self, + device_type: HardwareAccelerationDeviceType, + ) -> Self { + self.hardware_acceleration_device_type = Some(device_type); + self + } + + /// Build [`Decoder`]. + pub fn build(self) -> Result { + let mut reader_builder = ReaderBuilder::new(self.source); + if let Some(options) = self.options { + reader_builder = reader_builder.with_options(options); + } + let reader = reader_builder.build()?; + let reader_stream_index = reader.best_video_stream_index()?; + Ok(Decoder { + decoder: DecoderSplit::new( + &reader, + reader_stream_index, + self.resize, + self.hardware_acceleration_device_type, + )?, + reader, + reader_stream_index, + }) + } +} + /// Decode video files and streams. /// /// # Example /// /// ```ignore -/// let decoder = Decoder::new(&PathBuf::from("video.mp4").into()).unwrap(); +/// let decoder = Decoder::new(Path::new("video.mp4")).unwrap(); /// decoder /// .decode_iter() /// .take_while(Result::is_ok) @@ -39,68 +111,14 @@ pub struct Decoder { } impl Decoder { - /// Create a new decoder for the specified file. - /// - /// # Arguments - /// - /// * `source` - Locator to file to decode. - pub fn new(source: &Locator) -> Result { - let reader = Reader::new(source)?; - let reader_stream_index = reader.best_video_stream_index()?; - Ok(Self { - decoder: DecoderSplit::new(&reader, reader_stream_index, None)?, - reader, - reader_stream_index, - }) - } - - /// Create a new decoder for the specified file with input options. + /// Create a decoder to decode the specified source. /// /// # Arguments /// - /// * `source` - Locator to file to decode. - /// * `options` - The input options. - pub fn new_with_options(source: &Locator, options: &Options) -> Result { - let reader = Reader::new_with_options(source, options)?; - let reader_stream_index = reader.best_video_stream_index()?; - Ok(Self { - decoder: DecoderSplit::new(&reader, reader_stream_index, None)?, - reader, - reader_stream_index, - }) - } - - /// Create a new decoder for the specified file with input options and custom dimensions. Each - /// frame will be resized to the given dimensions. - /// - /// # Arguments - /// - /// * `source` - Locator to file to decode. - /// * `options` - The input options. - /// * `resize` - How to resize frames. - /// - /// # Example - /// - /// ```ignore - /// let decoder = Decoder::new_with_options_and_resize( - /// &PathBuf::from("video.mp4").into(), - /// Options::new_with_rtsp_transport_tcp(), - /// Resize::Exact(800, 600), - /// ) - /// .unwrap(); - /// ``` - pub fn new_with_options_and_resize( - source: &Locator, - options: &Options, - resize: Resize, - ) -> Result { - let reader = Reader::new_with_options(source, options)?; - let reader_stream_index = reader.best_video_stream_index()?; - Ok(Self { - decoder: DecoderSplit::new(&reader, reader_stream_index, Some(resize))?, - reader, - reader_stream_index, - }) + /// * `source` - Source to decode. + #[inline] + pub fn new(source: impl Into) -> Result { + DecoderBuilder::new(source).build() } /// Get decoder time base. @@ -248,12 +266,92 @@ impl Decoder { pub struct DecoderSplit { decoder: AvDecoder, decoder_time_base: AvRational, - scaler: AvScaler, + hwaccel_context: Option, + scaler: Option, size: (u32, u32), size_out: (u32, u32), } impl DecoderSplit { + /// Create a new [`DecoderSplit`]. + /// + /// # Arguments + /// + /// * `reader` - [`Reader`] to initialize decoder from. + /// * `resize` - Optional resize strategy to apply to frames. + pub fn new( + reader: &Reader, + reader_stream_index: usize, + resize: Option, + hwaccel_device_type: Option, + ) -> Result { + let reader_stream = reader + .input + .stream(reader_stream_index) + .ok_or(AvError::StreamNotFound)?; + + let mut decoder = AvContext::new(); + ffi::set_decoder_context_time_base(&mut decoder, reader_stream.time_base()); + decoder.set_parameters(reader_stream.parameters())?; + + let hwaccel_context = match hwaccel_device_type { + Some(device_type) => Some(HardwareAccelerationContext::new(&mut decoder, device_type)?), + None => None, + }; + + let decoder = decoder.decoder().video()?; + let decoder_time_base = decoder.time_base(); + + if decoder.format() == AvPixel::None || decoder.width() == 0 || decoder.height() == 0 { + return Err(Error::MissingCodecParameters); + } + + let (resize_width, resize_height) = match resize { + Some(resize) => resize + .compute_for((decoder.width(), decoder.height())) + .ok_or(Error::InvalidResizeParameters)?, + None => (decoder.width(), decoder.height()), + }; + + let scaler_input_format = if hwaccel_context.is_some() { + // If hardware acceleration is enabled, we pre-converted the pixel format when + // downloading the frame from the device. + FRAME_PIXEL_FORMAT + } else { + decoder.format() + }; + + let is_scaler_needed = !(scaler_input_format == FRAME_PIXEL_FORMAT + && decoder.width() == resize_width + && decoder.height() == resize_height); + + let scaler = if is_scaler_needed { + Some(AvScaler::get( + scaler_input_format, + decoder.width(), + decoder.height(), + FRAME_PIXEL_FORMAT, + resize_width, + resize_height, + AvScalerFlags::AREA, + )?) + } else { + None + }; + + let size = (decoder.width(), decoder.height()); + let size_out = (resize_width, resize_height); + + Ok(Self { + decoder, + decoder_time_base, + hwaccel_context, + scaler, + size, + size_out, + }) + } + /// Get decoder time base. #[inline] pub fn time_base(&self) -> AvRational { @@ -277,7 +375,7 @@ impl DecoderSplit { // encoder will use when encoding for the `PTS` field. let timestamp = Time::new(Some(frame.packet().dts), self.decoder_time_base); let frame = - convert_frame_to_ndarray_rgb24(&mut frame).map_err(Error::BackendError)?; + ffi::convert_frame_to_ndarray_rgb24(&mut frame).map_err(Error::BackendError)?; Ok(Some((timestamp, frame))) } @@ -303,14 +401,29 @@ impl DecoderSplit { match self.decoder_receive_frame()? { Some(frame) => { - let mut frame_scaled = RawFrame::empty(); - self.scaler - .run(&frame, &mut frame_scaled) - .map_err(Error::BackendError)?; - - copy_frame_props(&frame, &mut frame_scaled); - - Ok(Some(frame_scaled)) + let frame = match self.hwaccel_context.as_ref() { + Some(hwaccel_context) if hwaccel_context.format() == frame.format() => { + let mut frame_downloaded = RawFrame::empty(); + frame_downloaded.set_format(FRAME_PIXEL_FORMAT); + ffi_hwaccel::hwdevice_transfer_frame(&mut frame_downloaded, &frame)?; + frame_downloaded + } + _ => frame, + }; + + let frame = match self.scaler.as_mut() { + Some(scaler) => { + let mut frame_scaled = RawFrame::empty(); + scaler + .run(&frame, &mut frame_scaled) + .map_err(Error::BackendError)?; + ffi::copy_frame_props(&frame, &mut frame_scaled); + frame_scaled + } + _ => frame, + }; + + Ok(Some(frame)) } None => Ok(None), } @@ -329,61 +442,6 @@ impl DecoderSplit { self.size_out } - /// Create a new [`DecoderSplit`]. - /// - /// # Arguments - /// - /// * `reader` - [`Reader`] to initialize decoder from. - /// * `resize` - Optional resize strategy to apply to frames. - pub fn new( - reader: &Reader, - reader_stream_index: usize, - resize: Option, - ) -> Result { - let reader_stream = reader - .input - .stream(reader_stream_index) - .ok_or(AvError::StreamNotFound)?; - - let mut decoder = AvContext::new(); - set_decoder_context_time_base(&mut decoder, reader_stream.time_base()); - decoder.set_parameters(reader_stream.parameters())?; - let decoder = decoder.decoder().video()?; - let decoder_time_base = decoder.time_base(); - - let (resize_width, resize_height) = match resize { - Some(resize) => resize - .compute_for((decoder.width(), decoder.height())) - .ok_or(Error::InvalidResizeParameters)?, - None => (decoder.width(), decoder.height()), - }; - - if decoder.format() == AvPixel::None || decoder.width() == 0 || decoder.height() == 0 { - return Err(Error::MissingCodecParameters); - } - - let scaler = AvScaler::get( - decoder.format(), - decoder.width(), - decoder.height(), - FRAME_PIXEL_FORMAT, - resize_width, - resize_height, - AvScalerFlags::AREA, - )?; - - let size = (decoder.width(), decoder.height()); - let size_out = (resize_width, resize_height); - - Ok(Self { - decoder, - decoder_time_base, - scaler, - size, - size_out, - }) - } - /// Pull a decoded frame from the decoder. This function also implements retry mechanism in case /// the decoder signals `EAGAIN`. fn decoder_receive_frame(&mut self) -> Result> { diff --git a/src/encode.rs b/src/encode.rs index a3e96b4..15e9462 100644 --- a/src/encode.rs +++ b/src/encode.rs @@ -16,31 +16,96 @@ use ffmpeg::util::picture::Type as AvFrameType; use ffmpeg::Error as AvError; use ffmpeg::Rational as AvRational; -use crate::{ - ffi::{codec_context_as, get_encoder_time_base}, - frame::FRAME_PIXEL_FORMAT, - io::{private::Write, Writer}, - options::Options, - Error, Locator, PixelFormat, RawFrame, -}; - +use crate::error::Error; +use crate::ffi; +#[cfg(feature = "ndarray")] +use crate::frame::Frame; +use crate::frame::{PixelFormat, RawFrame, FRAME_PIXEL_FORMAT}; +use crate::io::private::Write; +use crate::io::{Writer, WriterBuilder}; +use crate::location::Location; +use crate::options::Options; #[cfg(feature = "ndarray")] -use crate::{ffi::convert_ndarray_to_frame_rgb24, Frame, Time}; +use crate::time::Time; type Result = std::result::Result; +/// Builds an [`Encoder`]. +pub struct EncoderBuilder<'a> { + destination: Location, + settings: Settings, + options: Option<&'a Options>, + format: Option<&'a str>, + interleaved: bool, +} + +impl<'a> EncoderBuilder<'a> { + /// Create an encoder with the specified destination and settings. + /// + /// * `destination` - Where to encode to. + /// * `settings` - Encoding settings. + pub fn new(destination: impl Into, settings: Settings) -> Self { + Self { + destination: destination.into(), + settings, + options: None, + format: None, + interleaved: false, + } + } + + /// Set the output options for the encoder. + /// + /// # Arguments + /// + /// * `options` - The output options. + pub fn with_options(mut self, options: &'a Options) -> Self { + self.options = Some(options); + self + } + + /// Set the container format for the encoder. + /// + /// # Arguments + /// + /// * `format` - Container format to use. + pub fn with_format(mut self, format: &'a str) -> Self { + self.format = Some(format); + self + } + + /// Set interleaved. This will cause the encoder to use interleaved write instead of normal + /// write. + pub fn interleaved(mut self) -> Self { + self.interleaved = true; + self + } + + /// Build an [`Encoder`]. + pub fn build(self) -> Result { + let mut writer_builder = WriterBuilder::new(self.destination); + if let Some(options) = self.options { + writer_builder = writer_builder.with_options(options); + } + if let Some(format) = self.format { + writer_builder = writer_builder.with_format(format); + } + Encoder::from_writer(writer_builder.build()?, self.interleaved, self.settings) + } +} + /// Encodes frames into a video stream. /// /// # Example /// /// ```ignore /// let encoder = Encoder::new( -/// &PathBuf::from("video_in.mp4").into(), +/// Path::new("video_in.mp4"), /// Settings::for_h264_yuv420p(800, 600, 30.0) /// ) /// .unwrap(); /// -/// let decoder = Decoder::new(&PathBuf::from("video_out.mkv").into()).unwrap(); +/// let decoder = Decoder::new(Path::new("video_out.mkv")).unwrap(); /// decoder /// .decode_iter() /// .take_while(Result::is_ok) @@ -66,70 +131,13 @@ pub struct Encoder { impl Encoder { const KEY_FRAME_INTERVAL: u64 = 12; - /// Create a new encoder that writes to the specified file. - /// - /// # Arguments - /// - /// * `dest` - Locator to file to encode to. - /// * `settings` - Encoder settings to use. - pub fn new(dest: &Locator, settings: Settings) -> Result { - Self::from_writer(Writer::new(dest)?, settings) - } - - /// Create a new encoder that writes to the specified file with the given output options. - /// - /// # Arguments - /// - /// * `dest` - Locator to file to encode to. - /// * `settings` - Encoder settings to use. - /// * `options` - The output options. - pub fn new_with_options(dest: &Locator, settings: Settings, options: &Options) -> Result { - Self::from_writer(Writer::new_with_options(dest, options)?, settings) - } - - /// Create a new encoder that writes to the specified file with the given format. - /// - /// # Arguments - /// - /// * `dest` - Locator to file to encode to. - /// * `settings` - Encoder settings to use. - /// * `format` - Container format to use. - pub fn new_with_format(dest: &Locator, settings: Settings, format: &str) -> Result { - Self::from_writer(Writer::new_with_format(dest, format)?, settings) - } - - /// Create a new encoder that writes to the specified file with the given format and output - /// options. - /// - /// # Arguments + /// Create an encoder with the specified destination and settings. /// - /// * `dest` - Locator to file to encode to. - /// * `settings` - Encoder settings to use. - /// * `format` - Container format to use. - /// * `options` - The output options. - pub fn new_with_format_and_options( - dest: &Locator, - settings: Settings, - format: &str, - options: &Options, - ) -> Result { - Self::from_writer( - Writer::new_with_format_and_options(dest, format, options)?, - settings, - ) - } - - /// Turn the encoder into an interleaved version, that automatically reorders packets when - /// necessary. - pub fn interleaved(mut self) -> Self { - self.interleaved = true; - self - } - - /// Get encoder time base. + /// * `destination` - Where to encode to. + /// * `settings` - Encoding settings. #[inline] - pub fn time_base(&self) -> AvRational { - self.encoder_time_base + pub fn new(destination: impl Into, settings: Settings) -> Result { + EncoderBuilder::new(destination, settings).build() } /// Encode a single `ndarray` frame. @@ -149,7 +157,7 @@ impl Encoder { return Err(Error::InvalidFrameFormat); } - let mut frame = convert_ndarray_to_frame_rgb24(frame).map_err(Error::BackendError)?; + let mut frame = ffi::convert_ndarray_to_frame_rgb24(frame).map_err(Error::BackendError)?; frame.set_pts( source_timestamp @@ -213,13 +221,20 @@ impl Encoder { Ok(()) } + /// Get encoder time base. + #[inline] + pub fn time_base(&self) -> AvRational { + self.encoder_time_base + } + /// Create an encoder from a `FileWriter` instance. /// /// # Arguments /// - /// * `writer` - `FileWriter` to create encoder from. + /// * `writer` - [`Writer`] to create encoder from. + /// * `interleaved` - Whether or not to use interleaved write. /// * `settings` - Encoder settings to use. - fn from_writer(mut writer: Writer, settings: Settings) -> Result { + fn from_writer(mut writer: Writer, interleaved: bool, settings: Settings) -> Result { let global_header = writer .output .format() @@ -230,7 +245,7 @@ impl Encoder { let writer_stream_index = writer_stream.index(); let mut encoder_context = match settings.codec() { - Some(codec) => codec_context_as(&codec)?, + Some(codec) => ffi::codec_context_as(&codec)?, None => AvContext::new(), }; @@ -248,7 +263,7 @@ impl Encoder { encoder.set_time_base(TIME_BASE); let encoder = encoder.open_with(settings.options().to_dict())?; - let encoder_time_base = get_encoder_time_base(&encoder); + let encoder_time_base = ffi::get_encoder_time_base(&encoder); writer_stream.set_parameters(&encoder); @@ -269,7 +284,7 @@ impl Encoder { writer_stream_index, encoder, encoder_time_base, - interleaved: false, + interleaved, scaler, scaler_width, scaler_height, @@ -365,6 +380,7 @@ impl Drop for Encoder { } /// Holds a logical combination of encoder settings. +#[derive(Debug, Clone)] pub struct Settings { width: u32, height: u32, @@ -380,11 +396,11 @@ impl Settings { /// Create encoder settings for an H264 stream with YUV420p pixel format. This will encode to /// arguably the most widely compatible video file since H264 is a common codec and YUV420p is /// the most commonly used pixel format. - pub fn for_h264_yuv420p(width: usize, height: usize, realtime: bool) -> Settings { + pub fn preset_h264_yuv420p(width: usize, height: usize, realtime: bool) -> Settings { let options = if realtime { - Options::new_h264_realtime() + Options::preset_h264_realtime() } else { - Options::new_h264() + Options::preset_h264() }; Self { @@ -409,7 +425,7 @@ impl Settings { /// # Return value /// /// A `Settings` instance with the specified configuration.+ - pub fn for_h264_custom( + pub fn preset_h264_custom( width: usize, height: usize, pixel_format: PixelFormat, diff --git a/src/error.rs b/src/error.rs index d3f9645..0943b89 100644 --- a/src/error.rs +++ b/src/error.rs @@ -13,6 +13,8 @@ pub enum Error { MissingCodecParameters, UnsupportedCodecParameterSets, InvalidResizeParameters, + UninitializedCodec, + UnsupportedCodecHardwareAccelerationDeviceType, BackendError(FfmpegError), } @@ -26,6 +28,8 @@ impl std::error::Error for Error { Error::MissingCodecParameters => None, Error::UnsupportedCodecParameterSets => None, Error::InvalidResizeParameters => None, + Error::UninitializedCodec => None, + Error::UnsupportedCodecHardwareAccelerationDeviceType => None, Error::BackendError(ref internal) => Some(internal), } } @@ -51,6 +55,12 @@ impl std::fmt::Display for Error { Error::InvalidResizeParameters => { write!(f, "cannot resize frame into provided dimensions") } + Error::UninitializedCodec => { + write!(f, "codec context is not initialized properly") + } + Error::UnsupportedCodecHardwareAccelerationDeviceType => { + write!(f, "codec does not supported hardware acceleration device") + } Error::BackendError(ref internal) => internal.fmt(f), } } diff --git a/src/extradata.rs b/src/extradata.rs index 4eea777..6056c26 100644 --- a/src/extradata.rs +++ b/src/extradata.rs @@ -1,4 +1,4 @@ -use crate::Error; +use crate::error::Error; type Result = std::result::Result; diff --git a/src/ffi.rs b/src/ffi.rs index 46526cc..a0b8cde 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -26,7 +26,7 @@ use ffmpeg::ffi::*; /// /// * `format` - String to indicate the container format, like "mp4". /// -/// # Examples +/// # Example /// /// ```ignore /// let output = ffi::output_raw("mp4"); diff --git a/src/ffi_hwaccel.rs b/src/ffi_hwaccel.rs new file mode 100644 index 0000000..afebaaf --- /dev/null +++ b/src/ffi_hwaccel.rs @@ -0,0 +1,126 @@ +extern crate ffmpeg_next as ffmpeg; + +use crate::hwaccel::HardwareAccelerationDeviceType; + +pub struct HardwareDeviceContext { + ptr: *mut ffmpeg::ffi::AVBufferRef, +} + +impl HardwareDeviceContext { + pub fn new( + device_type: HardwareAccelerationDeviceType, + ) -> Result { + let mut ptr: *mut ffmpeg::ffi::AVBufferRef = std::ptr::null_mut(); + + unsafe { + match ffmpeg::ffi::av_hwdevice_ctx_create( + (&mut ptr) as *mut *mut ffmpeg::ffi::AVBufferRef, + device_type.into(), + std::ptr::null(), + std::ptr::null_mut(), + 0, + ) { + 0 => Ok(HardwareDeviceContext { ptr }), + e => Err(ffmpeg::error::Error::from(e)), + } + } + } + + unsafe fn ref_raw(&self) -> *mut ffmpeg::ffi::AVBufferRef { + ffmpeg::ffi::av_buffer_ref(self.ptr) + } +} + +impl Drop for HardwareDeviceContext { + fn drop(&mut self) { + unsafe { + ffmpeg::ffi::av_buffer_unref(&mut self.ptr); + } + } +} + +pub fn hwdevice_list_available_device_types() -> Vec { + let mut hwdevice_types = Vec::new(); + let mut hwdevice_type = unsafe { + ffmpeg::ffi::av_hwdevice_iterate_types(ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_NONE) + }; + while hwdevice_type != ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_NONE { + hwdevice_types.push(HardwareAccelerationDeviceType::from(hwdevice_type).unwrap()); + hwdevice_type = unsafe { ffmpeg::ffi::av_hwdevice_iterate_types(hwdevice_type) }; + } + hwdevice_types +} + +pub fn hwdevice_transfer_frame( + target_frame: &mut ffmpeg::frame::Frame, + hwdevice_frame: &ffmpeg::frame::Frame, +) -> Result<(), ffmpeg::error::Error> { + unsafe { + match ffmpeg::ffi::av_hwframe_transfer_data( + target_frame.as_mut_ptr(), + hwdevice_frame.as_ptr(), + 0, + ) { + 0 => Ok(()), + e => Err(ffmpeg::error::Error::from(e)), + } + } +} + +pub fn codec_find_corresponding_hwaccel_pixfmt( + codec: &ffmpeg::codec::codec::Codec, + hwaccel_type: HardwareAccelerationDeviceType, +) -> Option { + let mut i = 0; + loop { + unsafe { + let hw_config = ffmpeg::ffi::avcodec_get_hw_config(codec.as_ptr(), i); + if !hw_config.is_null() { + let hw_config_supports_codec = (((*hw_config).methods) as i32 + & ffmpeg::ffi::AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX as i32) + != 0; + if hw_config_supports_codec && (*hw_config).device_type == hwaccel_type.into() { + break Some((*hw_config).pix_fmt.into()); + } + } else { + break None; + } + } + i += 1; + } +} + +pub fn codec_context_hwaccel_set_get_format( + codec_context: &mut ffmpeg::codec::context::Context, + hw_pixfmt: ffmpeg::format::pixel::Pixel, +) { + unsafe { + (*codec_context.as_mut_ptr()).opaque = + ffmpeg::ffi::AVPixelFormat::from(hw_pixfmt) as i32 as _; + (*codec_context.as_mut_ptr()).get_format = Some(hwaccel_get_format); + } +} + +pub fn codec_context_hwaccel_set_hw_device_ctx( + codec_context: &mut ffmpeg::codec::context::Context, + hardware_device_context: &HardwareDeviceContext, +) { + unsafe { + (*codec_context.as_mut_ptr()).hw_device_ctx = hardware_device_context.ref_raw(); + } +} + +#[no_mangle] +unsafe extern "C" fn hwaccel_get_format( + ctx: *mut ffmpeg::ffi::AVCodecContext, + pix_fmts: *const ffmpeg::ffi::AVPixelFormat, +) -> ffmpeg::ffi::AVPixelFormat { + let mut p = pix_fmts; + while *p != ffmpeg::ffi::AVPixelFormat::AV_PIX_FMT_NONE { + if *p == std::mem::transmute((*ctx).opaque as i32) { + return *p; + } + p = p.add(1); + } + ffmpeg::ffi::AVPixelFormat::AV_PIX_FMT_NONE +} diff --git a/src/hwaccel.rs b/src/hwaccel.rs new file mode 100644 index 0000000..eb76aad --- /dev/null +++ b/src/hwaccel.rs @@ -0,0 +1,142 @@ +extern crate ffmpeg_next as ffmpeg; + +use crate::error::Error; +use crate::ffi_hwaccel; + +type Result = std::result::Result; + +pub(crate) struct HardwareAccelerationContext { + pixel_format: ffmpeg::util::format::Pixel, + _hardware_device_context: ffi_hwaccel::HardwareDeviceContext, +} + +impl HardwareAccelerationContext { + pub(crate) fn new( + decoder: &mut ffmpeg::codec::Context, + device_type: HardwareAccelerationDeviceType, + ) -> Result { + let codec = ffmpeg::codec::decoder::find(decoder.id()).ok_or(Error::UninitializedCodec)?; + let pixel_format = + ffi_hwaccel::codec_find_corresponding_hwaccel_pixfmt(&codec, device_type) + .ok_or(Error::UnsupportedCodecHardwareAccelerationDeviceType)?; + + ffi_hwaccel::codec_context_hwaccel_set_get_format(decoder, pixel_format); + + let hardware_device_context = ffi_hwaccel::HardwareDeviceContext::new(device_type)?; + ffi_hwaccel::codec_context_hwaccel_set_hw_device_ctx(decoder, &hardware_device_context); + + Ok(HardwareAccelerationContext { + pixel_format, + _hardware_device_context: hardware_device_context, + }) + } + + pub(crate) fn format(&self) -> ffmpeg::util::format::Pixel { + self.pixel_format + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum HardwareAccelerationDeviceType { + /// Video Decode and Presentation API for Unix (VDPAU) + Vdpau, + /// NVIDIA CUDA + Cuda, + /// Video Acceleration API (VA-API) + VaApi, + /// DirectX Video Acceleration 2.0 + Dxva2, + /// Quick Sync Video + Qsv, + /// VideoToolbox + VideoToolbox, + /// Direct3D 11 Video Acceleration + D3D11Va, + /// Linux Direct Rendering Manager + Drm, + /// OpenCL + OpenCl, + /// MediaCodec + MeiaCodec, + /// Vulkan + Vulkan, + /// Direct3D 12 Video Acceleration + D3D12Va, +} + +impl HardwareAccelerationDeviceType { + /// Whether or not the device type is available on this system. + pub fn is_available(self) -> bool { + Self::list_available().contains(&self) + } + + /// List available hardware acceleration device types on this system. + /// + /// Uses `av_hwdevice_iterate_types` internally. + pub fn list_available() -> Vec { + ffi_hwaccel::hwdevice_list_available_device_types() + } +} + +impl HardwareAccelerationDeviceType { + pub fn from(value: ffmpeg::ffi::AVHWDeviceType) -> Option { + match value { + ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_VDPAU => Some(Self::Vdpau), + ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_CUDA => Some(Self::Cuda), + ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI => Some(Self::VaApi), + ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_DXVA2 => Some(Self::Dxva2), + ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_QSV => Some(Self::Qsv), + ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_VIDEOTOOLBOX => Some(Self::VideoToolbox), + ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_D3D11VA => Some(Self::D3D11Va), + ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_DRM => Some(Self::Drm), + ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_OPENCL => Some(Self::OpenCl), + ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_MEDIACODEC => Some(Self::MeiaCodec), + ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_VULKAN => Some(Self::Vulkan), + // ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_D3D12VA => Self::D3D12Va, + ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_NONE => None, + } + } +} + +impl From for ffmpeg::ffi::AVHWDeviceType { + fn from(value: HardwareAccelerationDeviceType) -> Self { + match value { + HardwareAccelerationDeviceType::Vdpau => { + ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_VDPAU + } + HardwareAccelerationDeviceType::Cuda => { + ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_CUDA + } + HardwareAccelerationDeviceType::VaApi => { + ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI + } + HardwareAccelerationDeviceType::Dxva2 => { + ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_DXVA2 + } + HardwareAccelerationDeviceType::Qsv => { + ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_QSV + } + HardwareAccelerationDeviceType::VideoToolbox => { + ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_VIDEOTOOLBOX + } + HardwareAccelerationDeviceType::D3D11Va => { + ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_D3D11VA + } + HardwareAccelerationDeviceType::Drm => { + ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_DRM + } + HardwareAccelerationDeviceType::OpenCl => { + ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_OPENCL + } + HardwareAccelerationDeviceType::MeiaCodec => { + ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_MEDIACODEC + } + HardwareAccelerationDeviceType::Vulkan => { + ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_VULKAN + } + HardwareAccelerationDeviceType::D3D12Va => { + unimplemented!() + } + } + } +} diff --git a/src/io.rs b/src/io.rs index 709a6ca..fc0a47e 100644 --- a/src/io.rs +++ b/src/io.rs @@ -1,72 +1,96 @@ extern crate ffmpeg_next as ffmpeg; -use std::path::{Path, PathBuf}; - use ffmpeg::codec::packet::Packet as AvPacket; use ffmpeg::ffi::AV_TIME_BASE_Q; use ffmpeg::format::context::{Input as AvInput, Output as AvOutput}; use ffmpeg::media::Type as AvMediaType; use ffmpeg::Error as AvError; +use crate::error::Error; use crate::ffi; +use crate::location::Location; use crate::options::Options; -use crate::{Error, Packet, StreamInfo}; +use crate::packet::Packet; +use crate::stream::StreamInfo; type Result = std::result::Result; -/// Re-export `url::Url` since it is an input type for callers of the API. -pub use url::Url; - -/// Video reader that can read from files. -pub struct Reader { - pub source: Locator, - pub input: AvInput, +/// Builds a [`Reader`]. +/// +/// # Example +/// +/// ```ignore +/// let mut options = HashMap::new(); +/// options.insert( +/// "rtsp_transport".to_string(), +/// "tcp".to_string(), +/// ); +/// +/// let mut reader = ReaderBuilder::new(Path::new("my_file.mp4")) +/// .with_options(&options.into()) +/// .unwrap(); +/// ``` +pub struct ReaderBuilder<'a> { + source: Location, + options: Option<&'a Options>, } -impl Reader { - /// Create a new video file reader on a given source (path, URL, etc.). +impl<'a> ReaderBuilder<'a> { + /// Create a new reader with the specified locator. /// /// # Arguments /// - /// * `source` - Source to read from. - pub fn new(source: &Locator) -> Result { - let input = ffmpeg::format::input(&source.resolve())?; - - Ok(Self { - source: source.clone(), - input, - }) + /// * `source` - Source to read. + pub fn new(source: impl Into) -> Self { + Self { + source: source.into(), + options: None, + } } - /// Create a new video file reader with options for the backend. + /// Specify options for the backend. /// /// # Arguments /// - /// * `source` - Source to read from. - /// * `options` - Options to pass on. + /// * `options` - Options to pass on to input. + pub fn with_options(mut self, options: &'a Options) -> Self { + self.options = Some(options); + self + } + + /// Build [`Reader`]. + pub fn build(self) -> Result { + match self.options { + None => Ok(Reader { + input: ffmpeg::format::input(&self.source.as_path())?, + source: self.source, + }), + Some(options) => Ok(Reader { + input: ffmpeg::format::input_with_dictionary( + &self.source.as_path(), + options.to_dict(), + )?, + source: self.source, + }), + } + } +} + +/// Video reader that can read from files. +pub struct Reader { + pub source: Location, + pub input: AvInput, +} + +impl Reader { + /// Create a new video file reader on a given source (path, URL, etc.). /// - /// # Examples + /// # Arguments /// - /// ```ignore - /// let mut options = HashMap::new(); - /// options.insert( - /// "rtsp_transport".to_string(), - /// "tcp".to_string(), - /// ); - /// - /// let mut reader = Reader::new( - /// &PathBuf::from("my_file.mp4").into(), - /// &options.into() - /// ) - /// .unwrap(); - /// ``` - pub fn new_with_options(source: &Locator, options: &Options) -> Result { - let input = ffmpeg::format::input_with_dictionary(&source.resolve(), options.to_dict())?; - - Ok(Self { - source: source.clone(), - input, - }) + /// * `source` - Source to read from. + #[inline] + pub fn new(source: impl Into) -> Result { + ReaderBuilder::new(source).build() } /// Read a single packet from the source video file. @@ -75,12 +99,12 @@ impl Reader { /// /// * `stream_index` - Index of stream to read from. /// - /// # Examples + /// # Example /// - /// Read a single packet. + /// Read a single packet: /// /// ```ignore - /// let mut reader = Reader(&PathBuf::from("my_video.mp4").into()).unwrap(); + /// let mut reader = Reader::new(Path::new("my_video.mp4")).unwrap(); /// let stream = reader.best_video_stream_index().unwrap(); /// let mut packet = reader.read(stream).unwrap(); /// ``` @@ -158,94 +182,108 @@ unsafe impl Sync for Reader {} /// Any type that implements this can write video packets. pub trait Write: private::Write + private::Output {} -/// File writer for video files. -pub struct Writer { - pub dest: Locator, - pub(crate) output: AvOutput, +/// Build a [`Writer`]. +pub struct WriterBuilder<'a> { + destination: Location, + format: Option<&'a str>, + options: Option<&'a Options>, } -impl Writer { - /// Create a new file writer for video files. +impl<'a> WriterBuilder<'a> { + /// Create a new writer with the specified destination. /// /// # Arguments /// - /// * `dest` - Where to write to. - pub fn new(dest: &Locator) -> Result { - let output = ffmpeg::format::output(&dest.resolve())?; - - Ok(Self { - dest: dest.clone(), - output, - }) + /// * `destination` - Destination to write to. + pub fn new(destination: impl Into) -> Self { + Self { + destination: destination.into(), + format: None, + options: None, + } } - /// Create a new file writer for video files with a custom format specifier. + /// Specify a custom format for the writer. /// /// # Arguments /// - /// * `dest` - Where to write to. /// * `format` - Container format to use. - pub fn new_with_format(dest: &Locator, format: &str) -> Result { - let output = ffmpeg::format::output_as(&dest.resolve(), format)?; - - Ok(Self { - dest: dest.clone(), - output, - }) + pub fn with_format(mut self, format: &'a str) -> Self { + self.format = Some(format); + self } - /// Create a new file writer for video files with custom options for the ffmpeg backend. + /// Specify options for the backend. /// /// # Arguments /// - /// * `dest` - Where to write to. - /// * `options` - Options to pass on. - /// - /// # Examples - /// - /// Create a video writer that produces fragmented MP4. - /// - /// ```ignore - /// let mut options = HashMap::new(); - /// options.insert( - /// "movflags".to_string(), - /// "frag_keyframe+empty_moov".to_string(), - /// ); - /// - /// let mut writer = FileWriter::new( - /// &PathBuf::from("my_file.mp4").into(), - /// &options.into(), - /// ) - /// .unwrap(); - /// ``` - pub fn new_with_options(dest: &Locator, options: &Options) -> Result { - let output = ffmpeg::format::output_with(&dest.resolve(), options.to_dict())?; - - Ok(Self { - dest: dest.clone(), - output, - }) + /// * `options` - Options to pass on to output. + pub fn with_options(mut self, options: &'a Options) -> Self { + self.options = Some(options); + self + } + + /// Build [`Writer`]. + pub fn build(self) -> Result { + match (self.format, self.options) { + (None, None) => Ok(Writer { + output: ffmpeg::format::output(&self.destination.as_path())?, + destination: self.destination, + }), + (Some(format), None) => Ok(Writer { + output: ffmpeg::format::output_as(&self.destination.as_path(), format)?, + destination: self.destination, + }), + (None, Some(options)) => Ok(Writer { + output: ffmpeg::format::output_with( + &self.destination.as_path(), + options.to_dict(), + )?, + destination: self.destination, + }), + (Some(format), Some(options)) => Ok(Writer { + output: ffmpeg::format::output_as_with( + &self.destination.as_path(), + format, + options.to_dict(), + )?, + destination: self.destination, + }), + } } +} - /// Create a new file writer for video files with a custom format specifier and custom options - /// for the ffmpeg backend. +/// File writer for video files. +/// +/// # Example +/// +/// Create a video writer that produces fragmented MP4: +/// +/// ```ignore +/// let mut options = HashMap::new(); +/// options.insert( +/// "movflags".to_string(), +/// "frag_keyframe+empty_moov".to_string(), +/// ); +/// +/// let mut writer = WriterBuilder::new(Path::new("my_file.mp4")) +/// .with_options(&options.into()) +/// .unwrap(); +/// ``` +pub struct Writer { + pub destination: Location, + pub(crate) output: AvOutput, +} + +impl Writer { + /// Create a new file writer for video files. /// /// # Arguments /// /// * `dest` - Where to write to. - /// * `format` - Container format to use. - /// * `options` - Options to pass on. - pub fn new_with_format_and_options( - dest: &Locator, - format: &str, - options: &Options, - ) -> Result { - let output = ffmpeg::format::output_as_with(&dest.resolve(), format, options.to_dict())?; - - Ok(Self { - dest: dest.clone(), - output, - }) + #[inline] + pub fn new(destination: impl Into) -> Result { + WriterBuilder::new(destination).build() } } @@ -260,40 +298,66 @@ pub type Buf = Vec; /// Type alias for multiple buffers. pub type Bufs = Vec; -/// Video writer that writes to a buffer. -pub struct BufWriter { - pub(crate) output: AvOutput, - options: Options, +/// Build a [`BufWriter`]. +pub struct BufWriterBuilder<'a> { + format: &'a str, + options: Option<&'a Options>, } -impl BufWriter { - /// Create a video writer that writes to a buffer and returns the resulting bytes. +impl<'a> BufWriterBuilder<'a> { + /// Create a new writer that writes to a buffer. /// /// # Arguments /// /// * `format` - Container format to use. + pub fn new(format: &'a str) -> Self { + Self { + format, + options: None, + } + } + + /// Specify options for the backend. /// - /// # Examples + /// # Arguments /// - /// ```ignore - /// let mut writer = BufWriter::new("mp4").unwrap(); - /// let bytes = writer.write_header()?; - /// ``` - pub fn new(format: &str) -> Result { - Self::new_with(format, Default::default()) + /// * `options` - Options to pass on to output. + pub fn with_options(mut self, options: &'a Options) -> Self { + self.options = Some(options); + self } - /// Create a video writer that writes to a buffer and returns the resulting bytes. This - /// constructor also allows for passing options for the ffmpeg backend. + /// Build [`BufWriter`]. + pub fn build(self) -> Result { + Ok(BufWriter { + output: ffi::output_raw(self.format)?, + options: self.options.cloned().unwrap_or_default(), + }) + } +} + +/// Video writer that writes to a buffer. +/// +/// # Example +/// +/// ```ignore +/// let mut writer = BufWriter::new("mp4").unwrap(); +/// let bytes = writer.write_header()?; +/// ``` +pub struct BufWriter { + pub(crate) output: AvOutput, + options: Options, +} + +impl BufWriter { + /// Create a video writer that writes to a buffer and returns the resulting bytes. /// /// # Arguments /// /// * `format` - Container format to use. - /// * `options` - Options to pass on to ffmpeg. - pub fn new_with(format: &str, options: Options) -> Result { - let output = ffi::output_raw(format)?; - - Ok(Self { output, options }) + #[inline] + pub fn new(format: &str) -> Result { + BufWriterBuilder::new(format).build() } fn begin_write(&mut self) { @@ -318,7 +382,54 @@ impl Drop for BufWriter { unsafe impl Send for BufWriter {} unsafe impl Sync for BufWriter {} -/// Video writer that writes to a packetized buffer. +/// Build a [`PacketizedBufWriter`]. +pub struct PacketizedBufWriterBuilder<'a> { + format: &'a str, + options: Option<&'a Options>, +} + +impl<'a> PacketizedBufWriterBuilder<'a> { + /// Create a new writer that writes to a packetized buffer. + /// + /// # Arguments + /// + /// * `format` - Container format to use. + pub fn new(format: &'a str) -> Self { + Self { + format, + options: None, + } + } + + /// Specify options for the backend. + /// + /// # Arguments + /// + /// * `options` - Options to pass on to output. + pub fn with_options(mut self, options: &'a Options) -> Self { + self.options = Some(options); + self + } + + /// Build [`PacketizedBufWriter`]. + pub fn build(self) -> Result { + Ok(PacketizedBufWriter { + output: ffi::output_raw(self.format)?, + options: self.options.cloned().unwrap_or_default(), + buffers: Vec::new(), + }) + } +} + +/// Video writer that writes multiple packets to a buffer and returns the resulting +/// bytes for each packet. +/// +/// # Example +/// +/// ```ignore +/// let mut writer = BufPacketizedWriter::new("rtp").unwrap(); +/// let bytes = writer.write_header()?; +/// ``` pub struct PacketizedBufWriter { pub(crate) output: AvOutput, options: Options, @@ -335,33 +446,9 @@ impl PacketizedBufWriter { /// # Arguments /// /// * `format` - Container format to use. - /// - /// # Examples - /// - /// ```ignore - /// let mut writer = BufPacketizedWriter::new("rtp").unwrap(); - /// let bytes = writer.write_header()?; - /// ``` + #[inline] pub fn new(format: &str) -> Result { - Self::new_with(format, Default::default()) - } - - /// Create a video writer that writes multiple packets to a buffer and returns the resulting - /// bytes for each packet. This constructor also allows for passing options for the ffmpeg - /// backend. - /// - /// # Arguments - /// - /// * `format` - Container format to use. - /// * `options` - Options to pass on to ffmpeg. - pub fn new_with(format: &str, options: Options) -> Result { - let output = ffi::output_raw(format)?; - - Ok(Self { - output, - options, - buffers: Vec::new(), - }) + PacketizedBufWriterBuilder::new(format).build() } fn begin_write(&mut self) { @@ -392,48 +479,6 @@ impl Write for PacketizedBufWriter {} unsafe impl Send for PacketizedBufWriter {} unsafe impl Sync for PacketizedBufWriter {} -/// Wrapper type for any valid video source. Currently, this could be a URI, file path or any other -/// input the backend will accept. Later, we might add some scaffolding to have stricter typing. -#[derive(Clone)] -pub enum Locator { - Path(PathBuf), - Url(Url), -} - -impl Locator { - /// Resolves the locator into a `PathBuf` for usage with `ffmpeg-next`. - fn resolve(&self) -> &Path { - match self { - Locator::Path(path) => path.as_path(), - Locator::Url(url) => Path::new(url.as_str()), - } - } -} - -/// Allow conversion from path to `Locator`. -impl From for Locator { - fn from(path: PathBuf) -> Locator { - Locator::Path(path) - } -} - -/// Allow conversion from `Url` to `Locator`. -impl From for Locator { - fn from(url: Url) -> Locator { - Locator::Url(url) - } -} - -/// Allow conversion to string and display for locator types. -impl std::fmt::Display for Locator { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Locator::Path(ref path) => write!(f, "{}", path.display()), - Locator::Url(ref url) => write!(f, "{url}"), - } - } -} - pub(crate) mod private { use super::*; diff --git a/src/lib.rs b/src/lib.rs index 46aed92..54da3b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,38 +1,36 @@ -mod decode; -mod encode; -mod error; -mod extradata; +pub mod decode; +pub mod encode; +pub mod error; +pub mod extradata; +pub mod frame; +pub mod hwaccel; +pub mod init; +pub mod io; +pub mod location; +pub mod mux; +pub mod options; +pub mod packet; +pub mod resize; +pub mod rtp; +pub mod stream; +pub mod time; + mod ffi; -mod frame; -mod init; -mod io; -mod mux; -mod options; -mod packet; -mod resize; -mod rtp; -mod stream; -mod time; +mod ffi_hwaccel; -pub use decode::{Decoder, DecoderSplit}; -pub use encode::{Encoder, Settings as EncoderSettings}; +pub use decode::{Decoder, DecoderBuilder}; +pub use encode::{Encoder, EncoderBuilder}; pub use error::Error; -pub use extradata::{Pps, Sps}; -pub use frame::PixelFormat; -pub use frame::RawFrame; +#[cfg(feature = "ndarray")] +pub use frame::Frame; pub use init::init; -pub use io::{Buf, Reader, Write, Writer}; -pub use io::{Locator, Url}; -pub use mux::{BufMuxer, FileMuxer, PacketizedBufMuxer}; +pub use io::{Reader, ReaderBuilder, Writer, WriterBuilder}; +pub use location::{Location, Url}; +pub use mux::{Muxer, MuxerBuilder}; pub use options::Options; pub use packet::Packet; pub use resize::Resize; -pub use rtp::{RtpBuf, RtpMuxer}; -pub use stream::StreamInfo; -pub use time::{Aligned, Time}; - -#[cfg(feature = "ndarray")] -pub use frame::Frame; +pub use time::Time; -/// Re-export inner `ffmpeg` library. +/// Re-export backend `ffmpeg` library. pub use ffmpeg_next as ffmpeg; diff --git a/src/location.rs b/src/location.rs new file mode 100644 index 0000000..01398d7 --- /dev/null +++ b/src/location.rs @@ -0,0 +1,64 @@ +/// Re-export [`url::Url`] since it is an input type for callers of the API. +pub use url::Url; + +/// Represents a video file or stream location. Can be either a file resource (a path) or a network +/// resource (a URL). +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Location { + /// File source. + File(std::path::PathBuf), + /// Network source. + Network(Url), +} + +impl Location { + /// Coerce underlying location to a path. + /// + /// This will create a path with a URL in it (which is kind of weird but we use it to pass on + /// URLs to ffmpeg). + pub fn as_path(&self) -> &std::path::Path { + match self { + Location::File(path) => path.as_path(), + Location::Network(url) => std::path::Path::new(url.as_str()), + } + } +} + +impl From<&Location> for Location { + fn from(value: &Location) -> Location { + value.clone() + } +} + +impl From for Location { + fn from(value: std::path::PathBuf) -> Location { + Location::File(value) + } +} + +impl From<&std::path::Path> for Location { + fn from(value: &std::path::Path) -> Location { + Location::File(value.to_path_buf()) + } +} + +impl From for Location { + fn from(value: Url) -> Location { + Location::Network(value) + } +} + +impl From<&Url> for Location { + fn from(value: &Url) -> Location { + Location::Network(value.clone()) + } +} + +impl std::fmt::Display for Location { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Location::File(path) => write!(f, "{}", path.display()), + Location::Network(url) => write!(f, "{url}"), + } + } +} diff --git a/src/mux.rs b/src/mux.rs index 948e888..daedf35 100644 --- a/src/mux.rs +++ b/src/mux.rs @@ -1,195 +1,56 @@ extern crate ffmpeg_next as ffmpeg; -use std::collections::HashMap; - use ffmpeg::codec::Id as AvCodecId; use ffmpeg::{Error as AvError, Rational as AvRational}; -use crate::extradata::extract_parameter_sets_h264; +use crate::error::Error; +use crate::extradata::{extract_parameter_sets_h264, Pps, Sps}; use crate::ffi::extradata; -use crate::io::{BufWriter, PacketizedBufWriter, Reader, Write, Writer}; -use crate::options::Options; -use crate::{Error, Locator, Packet, Pps, Sps, StreamInfo}; +use crate::io::{Reader, Write}; +use crate::packet::Packet; +use crate::stream::StreamInfo; type Result = std::result::Result; -/// Represents a muxer. A muxer allows muxing media packets into a new container format. Muxing does -/// not require encoding and/or decoding. -pub struct Muxer { - pub(crate) writer: W, - mapping: HashMap, +/// Builds a [`Muxer`]. +pub struct MuxerBuilder { + writer: W, interleaved: bool, - have_written_header: bool, - have_written_trailer: bool, -} - -/// Represents a muxer that writes to a file. -pub type FileMuxer = Muxer; - -/// Represents a muxer that writes to a buffer. -pub type BufMuxer = Muxer; - -/// Represents a muxer that writes to a packetized buffer. -pub type PacketizedBufMuxer = Muxer; - -impl Muxer { - /// Create a muxer that writes to a file. - /// - /// # Arguments - /// - /// * `dest` - Locator to mux to, like a file or URL. - /// - /// # Examples - /// - /// Mux to an MKV file. - /// - /// ```ignore - /// let reader = Reader::new(&PathBuf::from("from_file.mp4").into()).unwrap(); - /// let muxer = Muxer::new_to_file(&PathBuf::from("to_file.mkv").into()) - /// .unwrap() - /// .with_streams(&reader) - /// .unwrap(); - /// - /// while let Ok(packet) = reader.read() { - /// muxer.mux(packet).unwrap(); - /// } - /// - /// muxer.finish().unwrap(); - /// ``` - pub fn new_to_file(dest: &Locator) -> Result { - Self::new(Writer::new(dest)?) - } - - /// Create a muxer that writes to a file and allows for specifying ffmpeg options for the - /// destination writer. - /// - /// # Arguments - /// - /// * `dest` - Locator to mux to, like a file or URL. - /// * `format` - Format to mux into. - pub fn new_to_file_with_format(dest: &Locator, format: &str) -> Result { - Self::new(Writer::new_with_format(dest, format)?) - } -} - -impl Muxer { - /// Create a muxer that writes to a packetized buffer. This is the packetized variant of - /// `new_to_buf`. - /// - /// # Arguments - /// - /// * `format` - Format to mux into. - pub fn new_to_packetized_buf(format: &str) -> Result { - Self::new(PacketizedBufWriter::new(format)?) - } - - /// Create a muxer that writes to a packetized buffer and allows for specifying ffmpeg options - /// for the destination writer. - /// - /// # Arguments - /// - /// * `format` - Format to mux into. - /// * `options` - Options for the writer. - pub fn new_to_packetized_buf_with_options(format: &str, options: Options) -> Result { - Self::new(PacketizedBufWriter::new_with(format, options)?) - } + mapping: std::collections::HashMap, } -impl Muxer { - /// Create a muxer that writes to a buffer. - /// - /// # Arguments - /// - /// * `format` - Format to mux into. - /// - /// # Examples - /// - /// Mux from file to mp4 and print length of first 100 buffer segments. - /// - /// ```ignore - /// let reader = Reader::new(&PathBuf::from("my_file.mp4").into()).unwrap(); - /// let mut muxer = Muxer::new_to_buf("mp4") - /// .unwrap() - /// .with_streams(&reader) - /// .unwrap(); - /// - /// for _ in 0..100 { - /// println!("len: {}", muxer.mux().unwrap().len()); - /// } - /// - /// muxer.finish()?; - /// ``` - pub fn new_to_buf(format: &str) -> Result { - Self::new(BufWriter::new(format)?) - } - - /// Create a muxer that writes to a buffer and allows for specifying ffmpeg options for the - /// destination writer. - /// - /// # Arguments - /// - /// * `format` - Format to mux into. - /// * `options` - Options for the writer. - pub fn new_to_buf_with_options(format: &str, options: Options) -> Result { - Self::new(BufWriter::new_with(format, options)?) - } -} - -impl Muxer { - /// Create a muxer. - /// - /// # Arguments - /// - /// * `writer` - Video writer that implements the `Writer` trait. - /// - /// # Examples - /// - /// ```ignore - /// let muxer = Muxer::new(BufWriter::new("mp4").unwrap()).unwrap(); - /// ``` - fn new(writer: W) -> Result { - Ok(Self { +impl MuxerBuilder { + /// Create a new [`MuxerBuilder`]. + pub fn new(writer: W) -> Self { + Self { writer, - mapping: HashMap::new(), interleaved: false, - have_written_header: false, - have_written_trailer: false, - }) - } - - /// Turn the muxer into an interleaved version, that automatically reorders packets when - /// necessary. - pub fn interleaved(mut self) -> Self { - self.interleaved = true; - self + mapping: std::collections::HashMap::new(), + } } /// Add an output stream to the muxer based on an input stream from a reader. Any packets - /// provided to `mux` from the given input stream will be muxed to the corresponding output - /// stream. + /// provided to [`Muxer::mux()`] from the given input stream will be muxed to the corresponding + /// output stream. /// /// At least one stream must be added before any muxing can take place. /// /// # Arguments /// /// * `stream_info` - Stream information. Usually this information is retrieved by calling - /// `reader.stream_info(index)`. + /// [`Reader::stream_info()`]. pub fn with_stream(mut self, stream_info: StreamInfo) -> Result { let (index, codec_parameters, reader_stream_time_base) = stream_info.into_parts(); - let mut writer_stream = self .writer .output_mut() .add_stream(ffmpeg::encoder::find(codec_parameters.id()))?; writer_stream.set_parameters(codec_parameters); - let stream_description = StreamDescription { index: writer_stream.index(), source_time_base: reader_stream_time_base, }; - self.mapping.insert(index, stream_description); - Ok(self) } @@ -204,15 +65,75 @@ impl Muxer { for stream in reader.input.streams() { self = self.with_stream(reader.stream_info(stream.index())?)?; } - Ok(self) } + /// Set interleaved. This will cause the muxer to use interleaved write instead of normal + /// write. + pub fn interleaved(mut self) -> Self { + self.interleaved = true; + self + } + + /// Build [`Muxer`]. + pub fn build(self) -> Muxer { + Muxer { + writer: self.writer, + mapping: self.mapping, + interleaved: self.interleaved, + have_written_header: false, + have_written_trailer: false, + } + } +} + +/// Represents a muxer. A muxer allows muxing media packets into a new container format. Muxing does +/// not require encoding and/or decoding. +/// +/// # Examples +/// +/// Mux to an MKV file: +/// +/// ```ignore +/// let reader = Reader::new(Path::new("from_file.mp4")).unwrap(); +/// let writer = Writer::new(Path::new("to_file.mkv")).unwrap(); +/// let muxer = MuxerBuilder::new(writer) +/// .with_streams(&reader) +/// .build() +/// .unwrap(); +/// while let Ok(packet) = reader.read() { +/// muxer.mux(packet).unwrap(); +/// } +/// muxer.finish().unwrap(); +/// ``` +/// +/// Mux from file to MP4 and print length of first 100 buffer segments: +/// +/// ```ignore +/// let reader = Reader::new(Path::new("my_file.mp4")).unwrap(); +/// let writer = BufWriter::new("mp4").unwrap(); +/// let mut muxer = MuxerBuilder::new(writer) +/// .with_streams(&reader) +/// .unwrap(); +/// for _ in 0..100 { +/// println!("len: {}", muxer.mux().unwrap().len()); +/// } +/// muxer.finish()?; +/// ``` +pub struct Muxer { + pub(crate) writer: W, + mapping: std::collections::HashMap, + interleaved: bool, + have_written_header: bool, + have_written_trailer: bool, +} + +impl Muxer { /// Mux a single packet. This will mux a single packet. /// /// # Arguments /// - /// * `packet` - Packet to mux. + /// * `packet` - [`Packet`] to mux. pub fn mux(&mut self, packet: Packet) -> Result { if self.have_written_header { let mut packet = packet.into_inner(); @@ -248,6 +169,17 @@ impl Muxer { } } + /// Signal to the muxer that writing has finished. This will cause a trailer to be written if + /// the container format has one. + pub fn finish(&mut self) -> Result> { + if self.have_written_header && !self.have_written_trailer { + self.have_written_trailer = true; + self.writer.write_trailer().map(Some) + } else { + Ok(None) + } + } + /// Get parameter sets corresponding to each internal stream. The parameter set contains one SPS /// (Sequence Parameter Set) and zero or more PPSs (Picture Parameter Sets). /// @@ -267,17 +199,6 @@ impl Muxer { }) .collect::>() } - - /// Signal to the muxer that writing has finished. This will cause a trailer to be written if - /// the container format has one. - pub fn finish(&mut self) -> Result> { - if self.have_written_header && !self.have_written_trailer { - self.have_written_trailer = true; - self.writer.write_trailer().map(Some) - } else { - Ok(None) - } - } } unsafe impl Send for Muxer {} @@ -285,6 +206,7 @@ unsafe impl Sync for Muxer {} /// Internal structure that holds the stream index and the time base of the source packet for /// rescaling. +#[derive(Debug, Clone, PartialEq, Eq)] struct StreamDescription { index: usize, source_time_base: AvRational, diff --git a/src/options.rs b/src/options.rs index 040983a..a4c3d23 100644 --- a/src/options.rs +++ b/src/options.rs @@ -5,7 +5,7 @@ use std::collections::HashMap; use ffmpeg::Dictionary as AvDictionary; /// A wrapper type for ffmpeg options. -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct Options(AvDictionary<'static>); impl Options { @@ -13,7 +13,7 @@ impl Options { /// the default UDP format). /// /// This sets the `rtsp_transport` to `tcp` in ffmpeg options. - pub fn new_with_rtsp_transport_tcp() -> Self { + pub fn preset_rtsp_transport_tcp() -> Self { let mut opts = AvDictionary::new(); opts.set("rtsp_transport", "tcp"); @@ -26,7 +26,7 @@ impl Options { /// /// This sets the `rtsp_transport` to `tcp` in ffmpeg options, it also sets `rw_timeout` to /// `4000000` and `stimeout` to `4000000`. - pub fn new_with_rtsp_transport_tcp_and_sane_timeouts() -> Self { + pub fn preset_rtsp_transport_tcp_and_sane_timeouts() -> Self { let mut opts = AvDictionary::new(); opts.set("rtsp_transport", "tcp"); // These can't be too low because ffmpeg takes its sweet time when connecting to RTSP @@ -43,7 +43,7 @@ impl Options { /// This modifies the `movflags` key to supported fragmented output. The muxer output will not /// have a header and each packet contains enough metadata to be streamed without the header. /// Muxer output should be compatiable with MSE. - pub fn new_with_fragmented_mov() -> Self { + pub fn preset_fragmented_mov() -> Self { let mut opts = AvDictionary::new(); opts.set( "movflags", @@ -54,7 +54,7 @@ impl Options { } /// Default options for a H264 encoder. - pub fn new_h264() -> Self { + pub fn preset_h264() -> Self { let mut opts = AvDictionary::new(); // Set H264 encoder to the medium preset. opts.set("preset", "medium"); @@ -64,7 +64,7 @@ impl Options { /// Options for a H264 encoder that are tuned for low-latency encoding such as for real-time /// streaming. - pub fn new_h264_realtime() -> Self { + pub fn preset_h264_realtime() -> Self { let mut opts = AvDictionary::new(); // Set H264 encoder to the medium preset. opts.set("preset", "medium"); @@ -93,7 +93,7 @@ impl From> for Options { /// /// * `item` - Item to convert from. /// - /// # Examples + /// # Example /// /// ```ignore /// let my_opts = HashMap::new(); diff --git a/src/resize.rs b/src/resize.rs index 45b3111..734e770 100644 --- a/src/resize.rs +++ b/src/resize.rs @@ -2,7 +2,7 @@ type Dims = (u32, u32); /// Represents the possible resize strategies. -#[derive(Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum Resize { /// When resizing with `Resize::Exact`, each frame will be resized to the exact width and height /// given, without taking into account aspect ratio. diff --git a/src/rtp.rs b/src/rtp.rs index 2008dc4..f45a68b 100644 --- a/src/rtp.rs +++ b/src/rtp.rs @@ -1,31 +1,38 @@ +use crate::error::Error; +use crate::extradata::{Pps, Sps}; use crate::ffi::{rtp_h264_mode_0, rtp_seq_and_timestamp, sdp}; -use crate::io::Buf; -use crate::{Error, Packet, PacketizedBufMuxer, Pps, Reader, Sps, StreamInfo}; +use crate::io::{Buf, PacketizedBufWriter, Reader}; +use crate::mux::{Muxer, MuxerBuilder}; +use crate::packet::Packet; +use crate::stream::StreamInfo; type Result = std::result::Result; -/// Represents a muxer that muxes into the RTP format and streams the -/// output over RTP. -pub struct RtpMuxer(PacketizedBufMuxer); +/// Build an [`RtpMuxer`]. +pub struct RtpMuxerBuilder { + inner: MuxerBuilder, +} -impl RtpMuxer { - /// Create a new muxer that produces an RTP stream to a buffer. - pub fn new() -> Result { - PacketizedBufMuxer::new_to_packetized_buf("rtp").map(RtpMuxer) +impl RtpMuxerBuilder { + /// Create a new [`RtpMuxerBuilder`]. + pub fn new() -> Result { + Ok(RtpMuxerBuilder { + inner: MuxerBuilder::new(PacketizedBufWriter::new("rtp")?), + }) } - /// Add an output stream to the muxer based on an input stream from a reader. Any packets - /// provided to `mux` from the given input stream will be muxed to the corresponding output - /// stream. + /// Add an output stream to the muxer based on an input stream from a reader. /// /// At least one stream must be added before any muxing can take place. /// /// # Arguments /// /// * `stream_info` - Stream information. Usually this information is retrieved by calling - /// `reader.stream_info(index)`. - pub fn with_stream(self, stream_info: StreamInfo) -> Result { - self.0.with_stream(stream_info).map(RtpMuxer) + /// [`Reader::stream_info()`]. + #[inline] + pub fn with_stream(mut self, stream_info: StreamInfo) -> Result { + self.inner = self.inner.with_stream(stream_info)?; + Ok(self) } /// Add output streams from reader to muxer. This will add all streams in the reader and @@ -35,8 +42,46 @@ impl RtpMuxer { /// # Arguments /// /// * `reader` - Reader to add streams from. - pub fn with_streams(self, reader: &Reader) -> Result { - self.0.with_streams(reader).map(RtpMuxer) + #[inline] + pub fn with_streams(mut self, reader: &Reader) -> Result { + self.inner = self.inner.with_streams(reader)?; + Ok(self) + } + + /// Build [`RtpMuxer`]. + /// + /// The muxer will not write in interleaved mode. + #[inline] + pub fn build(self) -> RtpMuxer { + RtpMuxer(self.inner.build()) + } +} + +/// Represents a muxer that muxes into the RTP format and streams the output over RTP. +pub struct RtpMuxer(Muxer); + +impl RtpMuxer { + /// Create a new non-interleaved writing [`RtpMuxer`]. + /// + /// The muxer muxes into the RTP format and streams the output over RTP. + pub fn new() -> Result { + Ok(RtpMuxerBuilder::new()?.build()) + } + + /// Mux a single packet. This will cause the muxer to try and read packets from the preferred + /// stream, mux it and return one or more RTP buffers. + pub fn mux(&mut self, packet: Packet) -> Result> { + self.0 + .mux(packet) + .map(|bufs| bufs.into_iter().map(|buf| buf.into()).collect()) + } + + /// Signal to the muxer that writing has finished. This will cause trailing packets to be + /// returned if the container format has one. + pub fn finish(&mut self) -> Result>> { + self.0 + .finish() + .map(|bufs| bufs.map(|bufs| bufs.into_iter().map(|buf| buf.into()).collect())) } /// Get the RTP packetization mode used by the muxer. @@ -86,22 +131,6 @@ impl RtpMuxer { pub fn sdp(&self) -> Result { sdp(&self.0.writer.output).map_err(Error::BackendError) } - - /// Mux a single packet. This will cause the muxer to try and read packets from the preferred - /// stream, mux it and return one or more RTP buffers. - pub fn mux(&mut self, packet: Packet) -> Result> { - self.0 - .mux(packet) - .map(|bufs| bufs.into_iter().map(|buf| buf.into()).collect()) - } - - /// Signal to the muxer that writing has finished. This will cause trailing packets to be - /// returned if the container format has one. - pub fn finish(&mut self) -> Result>> { - self.0 - .finish() - .map(|bufs| bufs.map(|bufs| bufs.into_iter().map(|buf| buf.into()).collect())) - } } unsafe impl Send for RtpMuxer {} diff --git a/src/stream.rs b/src/stream.rs index bc7c576..13d57a4 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -3,7 +3,8 @@ extern crate ffmpeg_next as ffmpeg; use ffmpeg::codec::Parameters as AvCodecParameters; use ffmpeg::{Error as AvError, Rational as AvRational}; -use crate::{io::Reader, Error}; +use crate::error::Error; +use crate::io::Reader; type Result = std::result::Result;