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