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::Mp4Muxer;
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 = Mp4Muxer::open(request.output, &context, time_base, request.audio)
366 .map_err(HardwareEncodeOpenError::Failed)?;
367 Ok((
368 VulkanEncodeSession {
369 context,
370 time_base,
371 muxer,
372 stages: EncodeStageTimings::default(),
373 selection,
374 generation,
375 },
376 input,
377 ))
378}
379
380fn plane_texture(
385 device: &wgpu::Device,
386 label: &'static str,
387 width: u32,
388 height: u32,
389 format: wgpu::TextureFormat,
390) -> wgpu::Texture {
391 device.create_texture(&wgpu::TextureDescriptor {
392 label: Some(label),
393 size: wgpu::Extent3d {
394 width,
395 height,
396 depth_or_array_layers: 1,
397 },
398 mip_level_count: 1,
399 sample_count: 1,
400 dimension: wgpu::TextureDimension::D2,
401 format,
402 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
403 view_formats: &[],
404 })
405}
406
407struct Nv12PlanePair {
409 luma: wgpu::Texture,
410 chroma: wgpu::Texture,
411}
412
413fn open_encoder(
415 device: &AVHWDeviceContext,
416 width: u32,
417 height: u32,
418 rate_control: alpha2_media::VideoRateControl,
419 frame_rate: (i32, i32),
420 color: ColorMetadata,
421) -> Result<
422 (
423 AVHWFramesContext,
424 AVCodecContext,
425 crate::NativeVideoEncoderSelection,
426 ),
427 EncodeError,
428> {
429 let encoder = AVCodec::find_encoder_by_name(c"h264_vulkan").ok_or_else(|| {
430 EncodeError::Adapter("this FFmpeg runtime has no h264_vulkan encoder".into())
431 })?;
432 let coded_width =
433 i32::try_from(width).map_err(|_| EncodeError::Adapter("width is out of range".into()))?;
434 let coded_height =
435 i32::try_from(height).map_err(|_| EncodeError::Adapter("height is out of range".into()))?;
436 let frames = vulkan_encode_frames(device, coded_width, coded_height)?;
437 let rate = u32::try_from(frame_rate.0)
438 .ok()
439 .zip(u32::try_from(frame_rate.1).ok())
440 .and_then(|(num, den)| alpha2_time::FrameRate::new(num, den).ok())
441 .ok_or_else(|| EncodeError::Adapter("frame rate is out of range".into()))?;
442 let open = |quantized: Option<&crate::rate_control::MechanismRequest>| {
443 let mut context = AVCodecContext::new(&encoder);
444 context.set_width(coded_width);
445 context.set_height(coded_height);
446 context.set_pix_fmt(ffi::AV_PIX_FMT_VULKAN);
447 context.set_time_base(ra(frame_rate.1, frame_rate.0));
450 context.set_framerate(ra(frame_rate.0, frame_rate.1));
451 context.set_bit_rate(quantized.map_or_else(
452 || i64::try_from(rate_control.bitrate).unwrap_or(i64::MAX),
453 |quantized| quantized.bit_rate,
454 ));
455 context.set_gop_size(crate::capabilities::keyframe_interval(rate));
456 context.set_max_b_frames(0);
460 context.set_hw_frames_ctx(frames.clone());
461 context.set_flags(
464 context.flags | i32::try_from(ffi::AV_CODEC_FLAG_GLOBAL_HEADER).unwrap_or(0),
465 );
466 let color = crate::convert::color_options(color);
469 let options = match quantized {
470 Some(quantized) => quantized.dictionary(color),
471 None => crate::rate_control::rate_control_options("h264_vulkan", rate_control, color),
472 };
473 let unused = alpha2_ffmpeg_bridge::open_codec_context(&mut context, options)
474 .map_err(|error| encode_adapter_error_at_open(&error))?;
475 crate::convert::reject_unused_encode_options(unused)?;
476 if context.extradata_size <= 0 {
477 return Err(EncodeError::Adapter(
478 "h264_vulkan opened without SPS/PPS extradata for the MP4 header".into(),
479 ));
480 }
481 Ok::<_, EncodeError>(context)
482 };
483 let (context, applied, quantizer_refused) =
486 match crate::rate_control::quantizer_request("h264_vulkan", rate_control) {
487 Some(Ok(quantized)) => match open(Some(&quantized)) {
488 Ok(context) => (context, crate::AppliedRateControl::Quantizer, None),
489 Err(error) => (
490 open(None)?,
491 crate::AppliedRateControl::Bitrate,
492 Some(format!("{} refused: {error}", quantized.describe())),
493 ),
494 },
495 Some(Err(reason)) => (
496 open(None)?,
497 crate::AppliedRateControl::Bitrate,
498 Some(reason),
499 ),
500 None => (open(None)?, crate::AppliedRateControl::Bitrate, None),
501 };
502 if let Some(reason) = &quantizer_refused {
503 alpha2_os::diagnostic!(
504 "alpha2 native export: h264_vulkan runs the bitrate fallback: {reason}"
505 );
506 }
507 let selection = crate::NativeVideoEncoderSelection {
510 encoder_name: "h264_vulkan".to_owned(),
511 class: crate::NativeEncoderClass::Hardware,
512 hardware_accelerated: true,
513 rejected: Vec::new(),
514 rate_control: applied,
515 quantizer_refused,
516 software_threads: None,
517 };
518 Ok((frames, context, selection))
519}
520
521pub(super) struct VulkanEncodeSession {
523 context: AVCodecContext,
524 time_base: AVRational,
525 muxer: Mp4Muxer,
526 stages: EncodeStageTimings,
527 selection: crate::NativeVideoEncoderSelection,
528 generation: u64,
529}
530
531pub(super) struct VulkanEncodeInput {
545 copier: alpha2_native_gpu::VulkanEncodeCopier,
546 planes: Vec<Nv12PlanePair>,
547 frames: AVHWFramesContext,
548 size: (u32, u32),
549 time_base: AVRational,
550 color: ColorMetadata,
551 stats: EncoderPoolStats,
552 images: HashSet<u64>,
555}
556
557pub(super) struct VulkanEncoderFrame(AVFrame);
559
560impl VulkanEncoderFrame {
561 pub(super) fn set_pts(&mut self, pts: i64) {
562 self.0.set_pts(pts);
563 }
564}
565
566impl VulkanEncodeInput {
567 pub(super) fn descriptor(&self, generation: u64) -> EncoderSurfaceDescriptor {
568 EncoderSurfaceDescriptor {
569 backend: HardwareEncodeBackend::Vulkan,
570 pool_generation: generation,
571 layout: EncoderInputLayout::Nv12,
572 visible: self.size,
573 color: self.color,
574 }
575 }
576
577 pub(super) fn acquire_frame(&mut self) -> Result<VulkanEncoderFrame, EncodeError> {
584 let mut frame = AVFrame::new();
585 self.frames
586 .get_buffer(&mut frame)
587 .map_err(encode_adapter_error)?;
588 let image = alpha2_ffmpeg_bridge::vulkan_frame_info(&frame)
589 .map_err(encode_adapter_error)?
590 .images[0];
591 self.images.insert(image);
592 self.stats.acquired = self.stats.acquired.saturating_add(1);
593 self.stats.allocated = u64::try_from(self.images.len()).unwrap_or(u64::MAX);
594 Ok(VulkanEncoderFrame(frame))
595 }
596
597 pub(super) fn begin_frame(&mut self) -> Result<InputFrame, EncodeError> {
601 let slot = self.copier.await_slot().map_err(encode_adapter_error)?;
602 if slot >= self.planes.len() {
603 return Err(EncodeError::Adapter(format!(
604 "encode copier reported slot {slot}"
605 )));
606 }
607 Ok(InputFrame { slot })
608 }
609
610 pub(super) fn planes(&self, frame: &InputFrame) -> (&wgpu::Texture, &wgpu::Texture) {
611 let pair = &self.planes[frame.slot];
612 (&pair.luma, &pair.chroma)
613 }
614
615 #[allow(clippy::needless_pass_by_value)]
620 pub(super) fn submit(
621 &mut self,
622 frame: InputFrame,
623 backing: &mut VulkanEncoderFrame,
624 ) -> Result<(), EncodeError> {
625 let InputFrame { slot } = frame;
626 let pair = &self.planes[slot];
627 let mut access = alpha2_ffmpeg_bridge::VulkanEncodeFrameAccess::begin(&mut backing.0)
628 .map_err(encode_adapter_error)?;
629 self.copier
630 .submit(&mut access, &pair.luma, &pair.chroma, self.size)
631 .map_err(encode_adapter_error)?;
632 self.stats.copy_operations = self.stats.copy_operations.saturating_add(2);
633 self.stats.copy_bytes = self
634 .stats
635 .copy_bytes
636 .saturating_add(EncoderInputLayout::Nv12.frame_bytes(self.size.0, self.size.1));
637 Ok(())
638 }
639
640 pub(super) fn timestamp(&self, time: MediaTime) -> i64 {
642 av_rescale_q(time.ticks(), tick_base(), self.time_base)
643 }
644
645 pub(super) const fn stats(&self) -> EncoderPoolStats {
646 self.stats
647 }
648
649 pub(super) fn finish(self) -> Result<(), EncodeError> {
651 let Self { copier, planes, .. } = self;
652 copier.finish().map_err(encode_adapter_error)?;
653 drop(planes);
654 Ok(())
655 }
656}
657
658impl VulkanEncodeSession {
659 pub(super) const fn selection(&self) -> &crate::NativeVideoEncoderSelection {
660 &self.selection
661 }
662
663 pub(super) fn stage_timings(&self) -> EncodeStageTimings {
664 self.muxer.with_audio_timings(self.stages)
665 }
666
667 pub(super) fn encode(
669 &mut self,
670 generation: u64,
671 frame: &VulkanEncoderFrame,
672 ) -> Result<(), EncodeError> {
673 if generation != self.generation {
674 return Err(EncodeError::Adapter(format!(
675 "encoder surface from pool {generation} reached session {}",
676 self.generation
677 )));
678 }
679 timed(&mut self.stages.video_send_frame, || {
680 self.context.send_frame(Some(&frame.0))
681 })
682 .map_err(encode_adapter_error)?;
683 self.drain()
684 }
685
686 pub(super) fn write_audio(
687 &mut self,
688 block: &alpha2_media::AudioBlock,
689 ) -> Result<(), EncodeError> {
690 self.muxer.write_audio(block, &mut self.stages)
691 }
692
693 pub(super) fn finish(&mut self) -> Result<(), EncodeError> {
697 self.muxer.finish_audio()?;
699 crate::encode::beat_encode_call();
700 self.context
701 .send_frame(None)
702 .map_err(encode_adapter_error)?;
703 self.drain()?;
704 self.muxer.write_ready_audio(true, &mut self.stages)?;
705 self.muxer.close()
706 }
707
708 pub(super) fn abort(&mut self) {
709 self.muxer.abort();
710 }
711
712 fn drain(&mut self) -> Result<(), EncodeError> {
713 loop {
714 let received = timed(&mut self.stages.video_receive_packet, || {
715 self.context.receive_packet()
716 });
717 match received {
718 Ok(mut packet) => self.muxer.write_video(&mut packet, self.time_base)?,
719 Err(RsmpegError::EncoderDrainError | RsmpegError::EncoderFlushedError) => {
720 return self.muxer.write_ready_audio(false, &mut self.stages);
721 }
722 Err(error) => return Err(encode_adapter_error(error)),
723 }
724 }
725 }
726}
727
728fn vulkan_encode_frames(
735 device: &AVHWDeviceContext,
736 width: i32,
737 height: i32,
738) -> Result<AVHWFramesContext, EncodeError> {
739 let mut frames = device.hwframe_ctx_alloc();
740 {
741 let data = frames.data();
742 data.format = ffi::AV_PIX_FMT_VULKAN;
743 data.sw_format = ffi::AV_PIX_FMT_NV12;
744 data.width = width;
745 data.height = height;
746 data.initial_pool_size = 0;
747 }
748 alpha2_ffmpeg_bridge::vulkan_frames_add_usage(
749 &mut frames,
750 alpha2_ffmpeg_bridge::VULKAN_ENCODE_INPUT_USAGE,
751 )
752 .map_err(encode_adapter_error)?;
753 frames.init().map_err(encode_adapter_error)?;
754 Ok(frames)
755}
756
757fn encode_adapter_error_at_open(error: &RsmpegError) -> EncodeError {
758 EncodeError::Adapter(format!("h264_vulkan session could not open: {error}"))
759}