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