1use std::fs;
4use std::path::{Path, PathBuf};
5
6use crate::capabilities::{
7 NativeVideoCodec, NativeVideoEncoderSelection, VideoEncoderOpenRequest, open_video_encoder,
8};
9use crate::convert::{
10 codec_thread_budget, configure_rgba_to_yuv, encode_adapter_error, encode_adapter_error_at,
11 tick_base,
12};
13use crate::encode::{EncodeStageTimings, timed};
14use crate::partial::PartialOutput;
15use alpha2_ffmpeg_bridge::OutputFile;
16use alpha2_ffmpeg_bridge::Yuv420pFrameBuffer;
17use alpha2_media::{
18 CancelToken, ColorMatrix, ColorMetadata, Container, DecodedVideoFrame, EncodeError,
19 EncodeEvent, EncodeSink, Orientation, PixelFormat, SampleAspectRatio, VideoRateControl,
20};
21use alpha2_time::{FrameRate, MediaTime};
22use rsmpeg::avcodec::AVCodecContext;
23use rsmpeg::avutil::{AVFrameWithImage, AVImage, AVRational, av_rescale_q, ra};
24use rsmpeg::error::RsmpegError;
25use rsmpeg::ffi;
26use rsmpeg::swscale::SwsContext;
27
28#[derive(Copy, Clone, Debug, PartialEq)]
30pub struct VideoEncodeConfig {
31 pub width: u32,
33 pub height: u32,
35 pub frame_rate: FrameRate,
37 pub color: ColorMetadata,
39 pub rate_control: VideoRateControl,
42 pub codec: NativeVideoCodec,
44 pub display: VideoDisplay,
46 pub container: Container,
48}
49
50#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
53pub struct VideoDisplay {
54 pub orientation: Orientation,
56 pub sample_aspect_ratio: SampleAspectRatio,
59}
60
61pub(crate) fn check_video_config(config: &VideoEncodeConfig) -> Result<(), EncodeError> {
63 let refuse = |reason: String| Err(EncodeError::Adapter(reason));
64 if !config.container.offers_video(config.codec.model()) {
65 return refuse(format!(
66 "{} does not offer video codec {}",
67 config.container,
68 config.codec.as_str()
69 ));
70 }
71 if config.width == 0
72 || config.height == 0
73 || !config.width.is_multiple_of(2)
74 || !config.height.is_multiple_of(2)
75 {
76 return refuse(format!(
77 "a {}x{} output is refused: 4:2:0 video needs an even, non-zero width and height",
78 config.width, config.height
79 ));
80 }
81 if config.rate_control.bitrate == 0 {
82 return refuse("the output bitrate must be positive".into());
83 }
84 if config.color.is_hdr() {
85 return refuse(format!(
86 "the output is HDR ({:?}), and native encoding writes SDR only",
87 config.color.transfer
88 ));
89 }
90 if config.color.matrix == ColorMatrix::Rgb {
91 return refuse(
92 "the output is tagged as RGB, and 4:2:0 video is Y'CbCr: tag the matrix it is encoded with"
93 .into(),
94 );
95 }
96 let sar = config.display.sample_aspect_ratio;
97 if sar.num == 0 || sar.den == 0 || i32::try_from(sar.num.max(sar.den)).is_err() {
98 return refuse(format!(
99 "the pixel aspect ratio {}:{} is out of range",
100 sar.num, sar.den
101 ));
102 }
103 Ok(())
104}
105
106pub(crate) fn describe_output_stream(
109 stream: &mut rsmpeg::avformat::AVStreamMut<'_>,
110 encoder: &AVCodecContext,
111 display: VideoDisplay,
112 container: Container,
113) -> Result<(), EncodeError> {
114 let sar = display.sample_aspect_ratio;
115 if sar != SampleAspectRatio::default() {
116 stream.set_sample_aspect_ratio(ra(
117 i32::try_from(sar.num).unwrap_or(1),
118 i32::try_from(sar.den).unwrap_or(1),
119 ));
120 }
121 if let Some(matrix) = crate::convert::display_of(display.orientation) {
122 alpha2_ffmpeg_bridge::set_stream_display_matrix(stream, matrix)
123 .map_err(encode_adapter_error)?;
124 }
125 let tag = crate::convert::container_codec_tag(
126 container,
127 encoder.codec_id,
128 stream.codecpar().codec_tag,
129 false,
130 );
131 alpha2_ffmpeg_bridge::set_stream_codec_tag(stream, tag);
132 Ok(())
133}
134
135pub struct NativeVideoEncodeSink {
140 pub(crate) final_path: PathBuf,
141 pub(crate) partial_path: PathBuf,
142 pub(crate) _partial_lock: crate::partial::PartialLock,
144 pub(crate) output: Option<OutputFile>,
145 pub(crate) encoder: AVCodecContext,
146 pub(crate) stream_index: i32,
147 pub(crate) encoder_time_base: AVRational,
148 pub(crate) frame_units: i64,
150 pub(crate) stream_time_base: AVRational,
151 pub(crate) scaler: SwsContext,
152 pub(crate) yuv_input: crate::convert::Yuv420pConverter,
154 pub(crate) width: i32,
155 pub(crate) height: i32,
156 pub(crate) last_pts: Option<i64>,
157 pub(crate) complete: bool,
158 pub(crate) selection: NativeVideoEncoderSelection,
159 pub(crate) stages: EncodeStageTimings,
160}
161
162impl NativeVideoEncodeSink {
163 pub fn create(path: impl AsRef<Path>, config: VideoEncodeConfig) -> Result<Self, EncodeError> {
168 Self::create_selected(path, config, None, &codec_thread_budget)
169 }
170
171 pub fn create_with_encoder(
177 path: impl AsRef<Path>,
178 config: VideoEncodeConfig,
179 encoder_name: &str,
180 ) -> Result<Self, EncodeError> {
181 Self::create_selected(path, config, Some(encoder_name), &codec_thread_budget)
182 }
183
184 pub fn create_with_encoder_threads(
201 path: impl AsRef<Path>,
202 config: VideoEncodeConfig,
203 encoder_name: Option<&str>,
204 software_threads: &dyn Fn() -> i64,
205 ) -> Result<Self, EncodeError> {
206 Self::create_selected(path, config, encoder_name, software_threads)
207 }
208
209 #[allow(clippy::too_many_lines)]
210 pub(crate) fn create_selected(
211 path: impl AsRef<Path>,
212 config: VideoEncodeConfig,
213 encoder_name: Option<&str>,
214 software_threads: &dyn Fn() -> i64,
215 ) -> Result<Self, EncodeError> {
216 check_video_config(&config)?;
217 let final_path = path.as_ref().to_owned();
218 let partial = PartialOutput::claim(&final_path)?;
219 let partial_path = partial.path().to_owned();
220 let mut output = partial.open_muxer(config.container)?;
221 let width = i32::try_from(config.width)
222 .map_err(|_| EncodeError::Adapter("width is out of range".into()))?;
223 let height = i32::try_from(config.height)
224 .map_err(|_| EncodeError::Adapter("height is out of range".into()))?;
225 let (encoder, selection) = open_video_encoder(&VideoEncoderOpenRequest {
226 config: &config,
227 requested_name: encoder_name,
228 threads: crate::capabilities::SoftwareEncoderThreads::new(software_threads),
229 global_header: output.oformat().flags
230 & i32::try_from(ffi::AVFMT_GLOBALHEADER).unwrap_or(0)
231 != 0,
232 width,
233 height,
234 })?;
235 let encoder_time_base = encoder.time_base;
237 let mut stream_time_base;
238 let stream_index;
239 {
240 let mut stream = output.new_stream();
241 stream_time_base = encoder_time_base;
242 stream.set_time_base(stream_time_base);
243 let parameters = encoder.extract_codecpar();
244 stream.set_codecpar(parameters);
245 describe_output_stream(&mut stream, &encoder, config.display, config.container)?;
246 stream_index = stream.index;
247 }
248 output
249 .write_header(&mut crate::convert::muxer_header_options(config.container))
250 .map_err(encode_adapter_error)?;
251 stream_time_base = output.streams()[usize::try_from(stream_index).unwrap_or(0)].time_base;
252 let mut scaler = SwsContext::get_context(
253 width,
254 height,
255 ffi::AV_PIX_FMT_RGBA,
256 width,
257 height,
258 ffi::AV_PIX_FMT_YUV420P,
259 ffi::SWS_BILINEAR,
260 None,
261 None,
262 None,
263 )
264 .ok_or_else(|| EncodeError::Adapter("cannot create RGBA to YUV scaler".into()))?;
265 configure_rgba_to_yuv(&mut scaler, config.color).map_err(encode_adapter_error)?;
266 let partial_lock = partial.keep();
267 Ok(Self {
268 final_path,
269 partial_path,
270 _partial_lock: partial_lock,
271 output: Some(output),
272 encoder,
273 stream_index,
274 encoder_time_base,
275 frame_units: crate::mux::frame_units(config.frame_rate, encoder_time_base),
276 stream_time_base,
277 scaler,
278 yuv_input: crate::convert::Yuv420pConverter::new(config.color),
279 width,
280 height,
281 last_pts: None,
282 complete: false,
283 selection,
284 stages: EncodeStageTimings::default(),
285 })
286 }
287
288 #[must_use]
290 pub fn stage_timings(&self) -> EncodeStageTimings {
291 self.stages
292 }
293
294 #[must_use]
296 pub fn selection(&self) -> &NativeVideoEncoderSelection {
297 &self.selection
298 }
299
300 pub(crate) fn drain_packets(&mut self, flushing: bool) -> Result<(), EncodeError> {
301 loop {
302 let received = timed(&mut self.stages.video_receive_packet, || {
303 self.encoder.receive_packet()
304 });
305 match received {
306 Ok(mut packet) => {
307 packet.set_stream_index(self.stream_index);
308 crate::mux::fill_frame_duration(&mut packet, self.frame_units);
309 packet.rescale_ts(self.encoder_time_base, self.stream_time_base);
310 let output = self
311 .output
312 .as_mut()
313 .ok_or_else(|| EncodeError::Adapter("muxer is already closed".into()))?;
314 timed(&mut self.stages.video_mux_write, || {
315 output.interleaved_write_frame(&mut packet)
316 })
317 .map_err(|error| encode_adapter_error_at("packet write failed", error))?;
318 }
319 Err(RsmpegError::EncoderDrainError) if !flushing => return Ok(()),
320 Err(RsmpegError::EncoderFlushedError | RsmpegError::EncoderDrainError)
321 if flushing =>
322 {
323 return Ok(());
324 }
325 Err(error) => {
326 return Err(encode_adapter_error_at(
327 "video encoder packet drain failed",
328 error,
329 ));
330 }
331 }
332 }
333 }
334
335 pub fn write_prepared_video(
346 &mut self,
347 frame: &mut Yuv420pFrameBuffer,
348 pts: MediaTime,
349 ) -> Result<(), EncodeError> {
350 let (width, height) = frame.size();
351 if width != usize::try_from(self.width).unwrap_or(0)
352 || height != usize::try_from(self.height).unwrap_or(0)
353 {
354 return Err(EncodeError::Adapter(
355 "encoder input dimensions do not match its configuration".into(),
356 ));
357 }
358 let pts = av_rescale_q(pts.ticks(), tick_base(), self.encoder_time_base);
359 if self.last_pts.is_some_and(|previous| pts <= previous) {
360 return Err(EncodeError::InvalidTimestamp);
361 }
362 frame.set_presentation(pts, self.encoder_time_base);
363 timed(&mut self.stages.video_send_frame, || {
364 self.encoder.send_frame(Some(frame.frame()))
365 })
366 .map_err(|error| {
367 encode_adapter_error_at("video encoder rejected a YUV420P frame", error)
368 })?;
369 self.last_pts = Some(pts);
370 self.drain_packets(false)
371 }
372
373 pub(crate) fn write_video(&mut self, frame: &DecodedVideoFrame) -> Result<(), EncodeError> {
374 if frame.visible_rect.width != u32::try_from(self.width).unwrap_or(0)
375 || frame.visible_rect.height != u32::try_from(self.height).unwrap_or(0)
376 {
377 return Err(EncodeError::Adapter(
378 "encoder input dimensions do not match its configuration".into(),
379 ));
380 }
381 let pts = av_rescale_q(frame.pts.ticks(), tick_base(), self.encoder_time_base);
382 if self.last_pts.is_some_and(|previous| pts <= previous) {
383 return Err(EncodeError::InvalidTimestamp);
384 }
385 if let Some(mut yuv) = self.yuv_input.convert(frame, self.width, self.height)? {
386 yuv.set_pts(pts);
387 yuv.set_time_base(self.encoder_time_base);
388 timed(&mut self.stages.video_send_frame, || {
389 self.encoder.send_frame(Some(&yuv))
390 })
391 .map_err(|error| {
392 encode_adapter_error_at("video encoder rejected a YUV420P frame", error)
393 })?;
394 self.last_pts = Some(pts);
395 return self.drain_packets(false);
396 }
397 if frame.pixel_format != PixelFormat::Rgba8 || frame.planes.len() != 1 {
398 return Err(EncodeError::Adapter(
399 "encoder expects YUV planes or one tightly packed RGBA8 plane".into(),
400 ));
401 }
402 let plane = &frame.planes[0];
403 let expected = usize::try_from(self.width)
404 .ok()
405 .and_then(|width| {
406 usize::try_from(self.height)
407 .ok()
408 .and_then(|height| width.checked_mul(height))
409 })
410 .and_then(|pixels| pixels.checked_mul(4))
411 .ok_or_else(|| EncodeError::Adapter("frame dimensions overflow".into()))?;
412 if plane.bytes.len() != expected
413 || plane.stride != u32::try_from(self.width).unwrap_or(0) * 4
414 {
415 return Err(EncodeError::Adapter(
416 "RGBA plane is not tightly packed".into(),
417 ));
418 }
419 let rgba = alpha2_ffmpeg_bridge::rgba_frame(&plane.bytes, self.width, self.height)
423 .map_err(encode_adapter_error)?;
424 let yuv_image = AVImage::new(ffi::AV_PIX_FMT_YUV420P, self.width, self.height, 1)
425 .ok_or_else(|| EncodeError::Adapter("cannot allocate YUV input".into()))?;
426 let mut yuv = AVFrameWithImage::new(yuv_image);
427 self.scaler
428 .scale_frame(&rgba, 0, self.height, &mut yuv)
429 .map_err(encode_adapter_error)?;
430 yuv.set_pts(pts);
431 yuv.set_time_base(self.encoder_time_base);
432 timed(&mut self.stages.video_send_frame, || {
433 self.encoder.send_frame(Some(&yuv))
434 })
435 .map_err(|error| {
436 encode_adapter_error_at("video encoder rejected a converted YUV420P frame", error)
437 })?;
438 self.last_pts = Some(pts);
439 self.drain_packets(false)
440 }
441}
442
443impl EncodeSink for NativeVideoEncodeSink {
444 fn write(&mut self, event: EncodeEvent, cancel: &CancelToken) -> Result<(), EncodeError> {
445 if cancel.is_cancelled() {
446 self.abort()?;
447 return Err(EncodeError::Cancelled);
448 }
449 match event {
450 EncodeEvent::Video(frame) => self.write_video(&frame),
451 EncodeEvent::Audio(_) => Err(EncodeError::Adapter(
452 "this sink was configured for video-only output".into(),
453 )),
454 }
455 }
456
457 fn finish(&mut self, cancel: &CancelToken) -> Result<(), EncodeError> {
458 if cancel.is_cancelled() {
459 self.abort()?;
460 return Err(EncodeError::Cancelled);
461 }
462 timed(&mut self.stages.video_send_frame, || {
463 self.encoder.send_frame(None)
464 })
465 .map_err(encode_adapter_error)?;
466 self.drain_packets(true)?;
467 crate::encode::beat_encode_call();
468 self.output
469 .as_mut()
470 .ok_or_else(|| EncodeError::Adapter("muxer is already closed".into()))?
471 .write_trailer()
472 .map_err(encode_adapter_error)?;
473 if let Some(output) = self.output.take() {
475 crate::encode::beat_encode_call();
476 output
477 .close()
478 .map_err(|error| encode_adapter_error_at("closing the output", error))?;
479 }
480 crate::partial::publish(&self.partial_path, &self.final_path)?;
481 self.complete = true;
482 Ok(())
483 }
484
485 fn abort(&mut self) -> Result<(), EncodeError> {
486 drop(self.output.take());
487 match fs::remove_file(&self.partial_path) {
488 Ok(()) => Ok(()),
489 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
490 Err(error) => Err(EncodeError::Adapter(error.to_string())),
491 }
492 }
493}
494
495impl Drop for NativeVideoEncodeSink {
496 fn drop(&mut self) {
497 if !self.complete {
498 drop(self.output.take());
499 let _ = fs::remove_file(&self.partial_path);
500 }
501 }
502}