1use std::collections::HashSet;
10use std::ffi::CString;
11
12use alpha2_media::{ColorMetadata, EncodeError};
13use alpha2_time::MediaTime;
14use rsmpeg::avcodec::{AVCodec, AVCodecContext};
15use rsmpeg::avutil::{AVFrame, AVHWDeviceContext, AVHWFramesContext, AVRational, av_rescale_q, ra};
16use rsmpeg::error::RsmpegError;
17use rsmpeg::ffi;
18
19use super::{
20 EncoderInputHandoff, EncoderInputLayout, EncoderPoolStats, EncoderSurfaceDescriptor,
21 HardwareEncodeBackend, HardwareEncodeCapability, HardwareEncodeOpenError,
22 HardwareEncodeRequest, InputFrame,
23};
24use crate::NativeVideoCodec;
25use crate::convert::{encode_adapter_error, tick_base};
26use crate::encode::{EncodeStageTimings, timed};
27use crate::mux::Muxer;
28
29#[derive(Clone, Debug, Eq, PartialEq)]
31pub struct VulkanEncodeProbe {
32 pub encoder: String,
34 pub coded_size: (u32, u32),
36 pub frames_usage: u32,
40 pub images_per_frame: usize,
43 pub initial_layout: u32,
45 pub queue_family: u32,
47 pub has_timeline_semaphore: bool,
49 pub packets: u32,
51 pub packet_bytes: u32,
53}
54
55pub fn probe_vulkan_encode(
65 device: &AVHWDeviceContext,
66 width: u32,
67 height: u32,
68) -> Result<VulkanEncodeProbe, EncodeError> {
69 let encoder = AVCodec::find_encoder_by_name(c"h264_vulkan").ok_or_else(|| {
70 EncodeError::Adapter("this FFmpeg runtime has no h264_vulkan encoder".into())
71 })?;
72 let coded_width =
73 i32::try_from(width).map_err(|_| EncodeError::Adapter("width is out of range".into()))?;
74 let coded_height =
75 i32::try_from(height).map_err(|_| EncodeError::Adapter("height is out of range".into()))?;
76
77 let mut frames = vulkan_encode_frames(device, coded_width, coded_height)?;
78 let frames_usage = alpha2_ffmpeg_bridge::vulkan_frames_usage(&frames);
79
80 let mut context = AVCodecContext::new(&encoder);
81 context.set_width(coded_width);
82 context.set_height(coded_height);
83 context.set_pix_fmt(ffi::AV_PIX_FMT_VULKAN);
84 context.set_time_base(ra(1, 30));
87 context.set_framerate(ra(30, 1));
88 context.set_bit_rate(12_000_000);
89 context.set_gop_size(12);
90 context.set_max_b_frames(0);
92 context.set_hw_frames_ctx(frames.clone());
93 context
94 .open(None)
95 .map_err(|error| encode_adapter_error_at_open(&error))?;
96
97 let mut frame = AVFrame::new();
98 frames
99 .get_buffer(&mut frame)
100 .map_err(encode_adapter_error)?;
101 let descriptor =
102 alpha2_ffmpeg_bridge::vulkan_frame_info(&frame).map_err(encode_adapter_error)?;
103 frame.set_pts(0);
104
105 context
106 .send_frame(Some(&frame))
107 .map_err(encode_adapter_error)?;
108 context.send_frame(None).map_err(encode_adapter_error)?;
109 let mut packets = 0;
110 let mut packet_bytes = 0_u32;
111 loop {
112 match context.receive_packet() {
113 Ok(packet) => {
114 packets += 1;
115 packet_bytes = packet_bytes.saturating_add(packet.size.unsigned_abs());
116 }
117 Err(RsmpegError::EncoderDrainError | RsmpegError::EncoderFlushedError) => break,
118 Err(error) => return Err(encode_adapter_error(error)),
119 }
120 }
121 if packets == 0 {
122 return Err(EncodeError::Adapter(
123 "h264_vulkan opened but produced no packet".into(),
124 ));
125 }
126 Ok(VulkanEncodeProbe {
127 encoder: encoder.name().to_string_lossy().into_owned(),
128 coded_size: (
129 descriptor.coded_width.unsigned_abs(),
130 descriptor.coded_height.unsigned_abs(),
131 ),
132 frames_usage,
133 images_per_frame: descriptor.plane_count as usize,
134 initial_layout: descriptor.layouts[0],
135 queue_family: descriptor.queue_families[0],
136 has_timeline_semaphore: descriptor.semaphores[0] != 0,
137 packets,
138 packet_bytes,
139 })
140}
141
142pub(crate) fn probe_vulkan_rate_control(
151 encoder: &rsmpeg::avcodec::AVCodecRef<'static>,
152 width: u32,
153 height: u32,
154 request: &crate::rate_control::MechanismRequest,
155) -> Result<(u32, String), String> {
156 let named = std::env::var("ALPHA2_VULKAN_DEVICE").ok();
160 let shared = alpha2_native_gpu::shared_device_index(named.as_deref());
161 let candidates = match (&named, shared) {
162 (Some(_), Ok(index)) => vec![index.to_string()],
163 (Some(_), Err(error)) => return Err(error.to_string()),
164 (None, shared) => {
165 let first = shared.ok();
166 first
167 .into_iter()
168 .chain((0..8).filter(|index| Some(*index) != first))
169 .map(|index| index.to_string())
170 .collect::<Vec<_>>()
171 }
172 };
173 let mut last = "no Vulkan device".to_owned();
174 for device in candidates {
175 match probe_vulkan_rate_control_on(encoder, &device, width, height, request) {
176 Ok(packets) => return Ok((packets, device)),
177 Err(VulkanProbeError::NoDevice(error)) => {
178 last = error;
179 break;
180 }
181 Err(VulkanProbeError::NoEncodeQueue(error)) if named.is_none() => last = error,
182 Err(VulkanProbeError::NoEncodeQueue(error) | VulkanProbeError::Refused(error)) => {
183 return Err(format!("on Vulkan device {device}: {error}"));
184 }
185 }
186 }
187 Err(last)
188}
189
190enum VulkanProbeError {
191 NoDevice(String),
193 NoEncodeQueue(String),
195 Refused(String),
197}
198
199fn probe_vulkan_rate_control_on(
200 encoder: &rsmpeg::avcodec::AVCodecRef<'static>,
201 device_name: &str,
202 width: u32,
203 height: u32,
204 request: &crate::rate_control::MechanismRequest,
205) -> Result<u32, VulkanProbeError> {
206 let device_name = CString::new(device_name)
207 .map_err(|_| VulkanProbeError::NoDevice("device name contains a NUL byte".to_owned()))?;
208 let extensions = rsmpeg::avutil::AVDictionary::new(
211 c"device_extensions",
212 c"VK_KHR_video_queue+VK_KHR_video_encode_queue+VK_KHR_video_encode_h264+VK_KHR_video_encode_h265+VK_KHR_video_encode_av1+VK_KHR_video_maintenance1",
213 0,
214 );
215 let device = AVHWDeviceContext::create(
216 ffi::AV_HWDEVICE_TYPE_VULKAN,
217 Some(&device_name),
218 Some(&extensions),
219 0,
220 )
221 .map_err(|error| VulkanProbeError::NoDevice(format!("no Vulkan device: {error}")))?;
222 let refused = |error: String| VulkanProbeError::Refused(error);
223 let coded_width =
224 i32::try_from(width).map_err(|_| refused("width is out of range".to_owned()))?;
225 let coded_height =
226 i32::try_from(height).map_err(|_| refused("height is out of range".to_owned()))?;
227 let mut frames = vulkan_encode_frames(&device, coded_width, coded_height)
228 .map_err(|error| VulkanProbeError::NoEncodeQueue(error.to_string()))?;
229 let mut context = AVCodecContext::new(encoder);
230 context.set_width(coded_width);
231 context.set_height(coded_height);
232 context.set_pix_fmt(ffi::AV_PIX_FMT_VULKAN);
233 context.set_time_base(ra(1, 30));
234 context.set_framerate(ra(30, 1));
235 context.set_bit_rate(request.bit_rate);
236 context.set_gop_size(12);
237 context.set_max_b_frames(0);
238 context.set_hw_frames_ctx(frames.clone());
239 let unused = alpha2_ffmpeg_bridge::open_codec_context(
240 &mut context,
241 request.dictionary(crate::convert::color_options(ColorMetadata::EXPORT)),
242 )
243 .map_err(|error| {
244 let message = error.to_string();
246 if matches!(error, RsmpegError::AVError(code) if code == -38) {
247 VulkanProbeError::NoEncodeQueue(message)
248 } else {
249 refused(message)
250 }
251 })?;
252 crate::convert::reject_unused_encode_options(unused)
253 .map_err(|error| refused(error.to_string()))?;
254 let mut frame = AVFrame::new();
255 frames
256 .get_buffer(&mut frame)
257 .map_err(|error| refused(error.to_string()))?;
258 frame.set_pts(0);
259 context
260 .send_frame(Some(&frame))
261 .map_err(|error| refused(error.to_string()))?;
262 context
263 .send_frame(None)
264 .map_err(|error| refused(error.to_string()))?;
265 let mut packets = 0;
266 loop {
267 match context.receive_packet() {
268 Ok(_) => packets += 1,
269 Err(RsmpegError::EncoderDrainError | RsmpegError::EncoderFlushedError) => break,
270 Err(error) => return Err(refused(error.to_string())),
271 }
272 }
273 if packets == 0 {
274 return Err(refused("opened but produced no packet".to_owned()));
275 }
276 Ok(packets)
277}
278
279pub(super) fn capability(
281 shared: &alpha2_native_gpu::SharedVulkanContext,
282) -> Result<HardwareEncodeCapability, String> {
283 if shared.encode_codecs().is_empty() {
284 return Err("this Vulkan device has no video-encode queue".to_owned());
285 }
286 let extent = shared
287 .encode_extent()
288 .ok_or_else(|| "this device did not report its H.264 encode extents".to_owned())?;
289 Ok(HardwareEncodeCapability {
290 backend: HardwareEncodeBackend::Vulkan,
291 encoder: "h264_vulkan",
292 codec: NativeVideoCodec::H264,
293 gpu_input_only: true,
294 input_layout: EncoderInputLayout::Nv12,
295 handoff: EncoderInputHandoff::GpuCopy,
296 shares_renderer_device: true,
297 minimum: extent.minimum,
298 maximum: extent.maximum,
299 granularity: extent.granularity,
300 max_p_picture_l0_references: Some(extent.max_p_picture_l0_references),
301 max_b_picture_l0_references: Some(extent.max_b_picture_l0_references),
302 max_l1_references: Some(extent.max_l1_references),
303 rate_control_modes: extent.rate_control_modes.clone(),
304 plane_ring_slots: alpha2_native_gpu::VulkanEncodeCopier::slots(),
305 handoff_retained_surfaces: alpha2_native_gpu::VulkanEncodeCopier::slots(),
307 codec_retained_surfaces: 0,
309 })
310}
311
312pub(super) fn open(
314 shared: &alpha2_native_gpu::SharedVulkanContext,
315 request: &HardwareEncodeRequest<'_>,
316 generation: u64,
317) -> Result<(VulkanEncodeSession, VulkanEncodeInput), HardwareEncodeOpenError> {
318 let refused = |error: EncodeError| HardwareEncodeOpenError::Refused(error.to_string());
319 let (width, height) = (request.width, request.height);
320 let frame_rate = super::session_frame_rate(request.frame_rate).map_err(refused)?;
321 let (frames, context, selection) = open_encoder(
322 &shared.ffmpeg_device(),
323 width,
324 height,
325 request.rate_control,
326 frame_rate,
327 request.color,
328 )
329 .map_err(refused)?;
330 let copier = alpha2_native_gpu::VulkanEncodeCopier::new(shared)
331 .map_err(|error| HardwareEncodeOpenError::Refused(format!("{error:?}")))?;
332 let gpu = shared.gpu();
333 let planes = (0..alpha2_native_gpu::VulkanEncodeCopier::slots())
336 .map(|_| Nv12PlanePair {
337 luma: plane_texture(
338 gpu.device(),
339 "alpha2 NV12 luma",
340 width,
341 height,
342 wgpu::TextureFormat::R8Unorm,
343 ),
344 chroma: plane_texture(
345 gpu.device(),
346 "alpha2 NV12 chroma",
347 width / 2,
348 height / 2,
349 wgpu::TextureFormat::Rg8Unorm,
350 ),
351 })
352 .collect();
353 let time_base = ra(frame_rate.1, frame_rate.0);
354 let input = VulkanEncodeInput {
355 copier,
356 planes,
357 frames,
358 size: (width, height),
359 time_base,
360 color: request.color,
361 stats: EncoderPoolStats::default(),
362 images: HashSet::new(),
363 };
364 let muxer = Muxer::open(
366 request.output,
367 request.container,
368 &context,
369 time_base,
370 request.audio,
371 )
372 .map_err(HardwareEncodeOpenError::Failed)?;
373 Ok((
374 VulkanEncodeSession {
375 context,
376 time_base,
377 muxer,
378 stages: EncodeStageTimings::default(),
379 selection,
380 generation,
381 },
382 input,
383 ))
384}
385
386fn plane_texture(
391 device: &wgpu::Device,
392 label: &'static str,
393 width: u32,
394 height: u32,
395 format: wgpu::TextureFormat,
396) -> wgpu::Texture {
397 device.create_texture(&wgpu::TextureDescriptor {
398 label: Some(label),
399 size: wgpu::Extent3d {
400 width,
401 height,
402 depth_or_array_layers: 1,
403 },
404 mip_level_count: 1,
405 sample_count: 1,
406 dimension: wgpu::TextureDimension::D2,
407 format,
408 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
409 view_formats: &[],
410 })
411}
412
413struct Nv12PlanePair {
415 luma: wgpu::Texture,
416 chroma: wgpu::Texture,
417}
418
419fn open_encoder(
421 device: &AVHWDeviceContext,
422 width: u32,
423 height: u32,
424 rate_control: alpha2_media::VideoRateControl,
425 frame_rate: (i32, i32),
426 color: ColorMetadata,
427) -> Result<
428 (
429 AVHWFramesContext,
430 AVCodecContext,
431 crate::NativeVideoEncoderSelection,
432 ),
433 EncodeError,
434> {
435 let encoder = AVCodec::find_encoder_by_name(c"h264_vulkan").ok_or_else(|| {
436 EncodeError::Adapter("this FFmpeg runtime has no h264_vulkan encoder".into())
437 })?;
438 let coded_width =
439 i32::try_from(width).map_err(|_| EncodeError::Adapter("width is out of range".into()))?;
440 let coded_height =
441 i32::try_from(height).map_err(|_| EncodeError::Adapter("height is out of range".into()))?;
442 let frames = vulkan_encode_frames(device, coded_width, coded_height)?;
443 let rate = u32::try_from(frame_rate.0)
444 .ok()
445 .zip(u32::try_from(frame_rate.1).ok())
446 .and_then(|(num, den)| alpha2_time::FrameRate::new(num, den).ok())
447 .ok_or_else(|| EncodeError::Adapter("frame rate is out of range".into()))?;
448 let open = |quantized: Option<&crate::rate_control::MechanismRequest>| {
449 let mut context = AVCodecContext::new(&encoder);
450 context.set_width(coded_width);
451 context.set_height(coded_height);
452 context.set_pix_fmt(ffi::AV_PIX_FMT_VULKAN);
453 context.set_time_base(ra(frame_rate.1, frame_rate.0));
456 context.set_framerate(ra(frame_rate.0, frame_rate.1));
457 context.set_bit_rate(quantized.map_or_else(
458 || i64::try_from(rate_control.bitrate).unwrap_or(i64::MAX),
459 |quantized| quantized.bit_rate,
460 ));
461 context.set_gop_size(crate::capabilities::keyframe_interval(rate));
462 context.set_max_b_frames(0);
466 context.set_hw_frames_ctx(frames.clone());
467 context.set_flags(
470 context.flags | i32::try_from(ffi::AV_CODEC_FLAG_GLOBAL_HEADER).unwrap_or(0),
471 );
472 let color = crate::convert::color_options(color);
475 let options = match quantized {
476 Some(quantized) => quantized.dictionary(color),
477 None => crate::rate_control::rate_control_options("h264_vulkan", rate_control, color),
478 };
479 let unused = alpha2_ffmpeg_bridge::open_codec_context(&mut context, options)
480 .map_err(|error| encode_adapter_error_at_open(&error))?;
481 crate::convert::reject_unused_encode_options(unused)?;
482 if context.extradata_size <= 0 {
483 return Err(EncodeError::Adapter(
484 "h264_vulkan opened without SPS/PPS extradata for the MP4 header".into(),
485 ));
486 }
487 Ok::<_, EncodeError>(context)
488 };
489 let (context, applied, quantizer_refused) =
492 match crate::rate_control::quantizer_request("h264_vulkan", rate_control) {
493 Some(Ok(quantized)) => match open(Some(&quantized)) {
494 Ok(context) => (context, crate::AppliedRateControl::Quantizer, None),
495 Err(error) => (
496 open(None)?,
497 crate::AppliedRateControl::Bitrate,
498 Some(format!("{} refused: {error}", quantized.describe())),
499 ),
500 },
501 Some(Err(reason)) => (
502 open(None)?,
503 crate::AppliedRateControl::Bitrate,
504 Some(reason),
505 ),
506 None => (open(None)?, crate::AppliedRateControl::Bitrate, None),
507 };
508 if let Some(reason) = &quantizer_refused {
509 alpha2_os::diagnostic!(
510 "alpha2 native export: h264_vulkan runs the bitrate fallback: {reason}"
511 );
512 }
513 let selection = crate::NativeVideoEncoderSelection {
516 encoder_name: "h264_vulkan".to_owned(),
517 class: crate::NativeEncoderClass::Hardware,
518 hardware_accelerated: true,
519 rejected: Vec::new(),
520 rate_control: applied,
521 quantizer_refused,
522 software_threads: None,
523 };
524 Ok((frames, context, selection))
525}
526
527pub(super) struct VulkanEncodeSession {
529 context: AVCodecContext,
530 time_base: AVRational,
531 muxer: Muxer,
532 stages: EncodeStageTimings,
533 selection: crate::NativeVideoEncoderSelection,
534 generation: u64,
535}
536
537pub(super) struct VulkanEncodeInput {
551 copier: alpha2_native_gpu::VulkanEncodeCopier,
552 planes: Vec<Nv12PlanePair>,
553 frames: AVHWFramesContext,
554 size: (u32, u32),
555 time_base: AVRational,
556 color: ColorMetadata,
557 stats: EncoderPoolStats,
558 images: HashSet<u64>,
561}
562
563pub(super) struct VulkanEncoderFrame(AVFrame);
565
566impl VulkanEncoderFrame {
567 pub(super) fn set_pts(&mut self, pts: i64) {
568 self.0.set_pts(pts);
569 }
570}
571
572impl VulkanEncodeInput {
573 pub(super) fn descriptor(&self, generation: u64) -> EncoderSurfaceDescriptor {
574 EncoderSurfaceDescriptor {
575 backend: HardwareEncodeBackend::Vulkan,
576 pool_generation: generation,
577 layout: EncoderInputLayout::Nv12,
578 visible: self.size,
579 color: self.color,
580 }
581 }
582
583 pub(super) fn acquire_frame(&mut self) -> Result<VulkanEncoderFrame, EncodeError> {
590 let mut frame = AVFrame::new();
591 self.frames
592 .get_buffer(&mut frame)
593 .map_err(encode_adapter_error)?;
594 let image = alpha2_ffmpeg_bridge::vulkan_frame_info(&frame)
595 .map_err(encode_adapter_error)?
596 .images[0];
597 self.images.insert(image);
598 self.stats.acquired = self.stats.acquired.saturating_add(1);
599 self.stats.allocated = u64::try_from(self.images.len()).unwrap_or(u64::MAX);
600 Ok(VulkanEncoderFrame(frame))
601 }
602
603 pub(super) fn begin_frame(&mut self) -> Result<InputFrame, EncodeError> {
607 let slot = self.copier.await_slot().map_err(encode_adapter_error)?;
608 if slot >= self.planes.len() {
609 return Err(EncodeError::Adapter(format!(
610 "encode copier reported slot {slot}"
611 )));
612 }
613 Ok(InputFrame { slot })
614 }
615
616 pub(super) fn planes(&self, frame: &InputFrame) -> (&wgpu::Texture, &wgpu::Texture) {
617 let pair = &self.planes[frame.slot];
618 (&pair.luma, &pair.chroma)
619 }
620
621 #[allow(clippy::needless_pass_by_value)]
626 pub(super) fn submit(
627 &mut self,
628 frame: InputFrame,
629 backing: &mut VulkanEncoderFrame,
630 ) -> Result<(), EncodeError> {
631 let InputFrame { slot } = frame;
632 let pair = &self.planes[slot];
633 let mut access = alpha2_ffmpeg_bridge::VulkanEncodeFrameAccess::begin(&mut backing.0)
634 .map_err(encode_adapter_error)?;
635 self.copier
636 .submit(&mut access, &pair.luma, &pair.chroma, self.size)
637 .map_err(encode_adapter_error)?;
638 self.stats.copy_operations = self.stats.copy_operations.saturating_add(2);
639 self.stats.copy_bytes = self
640 .stats
641 .copy_bytes
642 .saturating_add(EncoderInputLayout::Nv12.frame_bytes(self.size.0, self.size.1));
643 Ok(())
644 }
645
646 pub(super) fn timestamp(&self, time: MediaTime) -> i64 {
648 av_rescale_q(time.ticks(), tick_base(), self.time_base)
649 }
650
651 pub(super) const fn stats(&self) -> EncoderPoolStats {
652 self.stats
653 }
654
655 pub(super) fn finish(self) -> Result<(), EncodeError> {
657 let Self { copier, planes, .. } = self;
658 copier.finish().map_err(encode_adapter_error)?;
659 drop(planes);
660 Ok(())
661 }
662}
663
664impl VulkanEncodeSession {
665 pub(super) const fn selection(&self) -> &crate::NativeVideoEncoderSelection {
666 &self.selection
667 }
668
669 pub(super) fn stage_timings(&self) -> EncodeStageTimings {
670 self.muxer.with_audio_timings(self.stages)
671 }
672
673 pub(super) fn encode(
675 &mut self,
676 generation: u64,
677 frame: &VulkanEncoderFrame,
678 ) -> Result<(), EncodeError> {
679 if generation != self.generation {
680 return Err(EncodeError::Adapter(format!(
681 "encoder surface from pool {generation} reached session {}",
682 self.generation
683 )));
684 }
685 timed(&mut self.stages.video_send_frame, || {
686 self.context.send_frame(Some(&frame.0))
687 })
688 .map_err(encode_adapter_error)?;
689 self.drain()
690 }
691
692 pub(super) fn write_audio(
693 &mut self,
694 block: &alpha2_media::AudioBlock,
695 ) -> Result<(), EncodeError> {
696 self.muxer.write_audio(block, &mut self.stages)
697 }
698
699 pub(super) fn finish(&mut self) -> Result<(), EncodeError> {
703 self.muxer.finish_audio()?;
705 crate::encode::beat_encode_call();
706 self.context
707 .send_frame(None)
708 .map_err(encode_adapter_error)?;
709 self.drain()?;
710 self.muxer.write_ready_audio(true, &mut self.stages)?;
711 self.muxer.close()
712 }
713
714 pub(super) fn abort(&mut self) {
715 self.muxer.abort();
716 }
717
718 fn drain(&mut self) -> Result<(), EncodeError> {
719 loop {
720 let received = timed(&mut self.stages.video_receive_packet, || {
721 self.context.receive_packet()
722 });
723 match received {
724 Ok(mut packet) => self.muxer.write_video(&mut packet, self.time_base)?,
725 Err(RsmpegError::EncoderDrainError | RsmpegError::EncoderFlushedError) => {
726 return self.muxer.write_ready_audio(false, &mut self.stages);
727 }
728 Err(error) => return Err(encode_adapter_error(error)),
729 }
730 }
731 }
732}
733
734fn vulkan_encode_frames(
741 device: &AVHWDeviceContext,
742 width: i32,
743 height: i32,
744) -> Result<AVHWFramesContext, EncodeError> {
745 let mut frames = device.hwframe_ctx_alloc();
746 {
747 let data = frames.data();
748 data.format = ffi::AV_PIX_FMT_VULKAN;
749 data.sw_format = ffi::AV_PIX_FMT_NV12;
750 data.width = width;
751 data.height = height;
752 data.initial_pool_size = 0;
753 }
754 alpha2_ffmpeg_bridge::vulkan_frames_add_usage(
755 &mut frames,
756 alpha2_ffmpeg_bridge::VULKAN_ENCODE_INPUT_USAGE,
757 )
758 .map_err(encode_adapter_error)?;
759 frames.init().map_err(encode_adapter_error)?;
760 Ok(frames)
761}
762
763fn encode_adapter_error_at_open(error: &RsmpegError) -> EncodeError {
764 EncodeError::Adapter(format!("h264_vulkan session could not open: {error}"))
765}