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