1use std::{
2 io::{Read, Seek},
3 sync::{Arc, atomic::AtomicU64},
4};
5
6use bon::Builder;
7use kithara_bufpool::{BytePool, PcmPool};
8use kithara_stream::{AudioCodec, ContainerFormat, MediaInfo, SegmentLayout, SharedHooks};
9
10use super::probe::{
11 ProbeHint, codec_from_mp4_fourcc, container_from_extension, probe_codec,
12 resolve_codec_container,
13};
14use crate::{
15 Decoder,
16 error::{DecodeError, DecodeResult},
17 mp4::sniff_mp4_codec,
18 traits::BoxedSource,
19};
20
21#[non_exhaustive]
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
38pub enum DecoderBackend {
39 #[cfg(all(feature = "apple", any(target_os = "macos", target_os = "ios")))]
41 #[cfg_attr(
42 all(
43 not(feature = "symphonia"),
44 feature = "apple",
45 any(target_os = "macos", target_os = "ios")
46 ),
47 default
48 )]
49 Apple,
50 #[cfg(all(feature = "android", target_os = "android"))]
52 #[cfg_attr(
53 all(
54 not(feature = "symphonia"),
55 feature = "android",
56 target_os = "android",
57 not(all(feature = "apple", any(target_os = "macos", target_os = "ios")))
58 ),
59 default
60 )]
61 Android,
62 #[cfg(feature = "symphonia")]
65 #[default]
66 Symphonia,
67}
68
69impl std::fmt::Display for DecoderBackend {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 match self {
72 #[cfg(all(feature = "apple", any(target_os = "macos", target_os = "ios")))]
73 Self::Apple => f.write_str("apple"),
74 #[cfg(all(feature = "android", target_os = "android"))]
75 Self::Android => f.write_str("android"),
76 #[cfg(feature = "symphonia")]
77 Self::Symphonia => f.write_str("symphonia"),
78 }
79 }
80}
81
82#[derive(Clone, Builder)]
94#[builder(state_mod(vis = "pub"))]
95#[non_exhaustive]
96pub struct DecoderConfig {
97 #[builder(default)]
99 pub backend: DecoderBackend,
100 pub byte_len_handle: Option<Arc<AtomicU64>>,
102 pub byte_pool: Option<BytePool>,
105 pub hint: Option<String>,
107 pub hooks: Option<SharedHooks>,
110 pub pcm_pool: Option<PcmPool>,
113 pub segment_layout: Option<Arc<dyn SegmentLayout>>,
115 #[builder(default = true)]
117 pub gapless: bool,
118 #[builder(default)]
120 pub epoch: u64,
121}
122
123impl Default for DecoderConfig {
124 fn default() -> Self {
125 Self::builder().build()
126 }
127}
128
129pub struct DecoderFactory;
138
139impl DecoderFactory {
140 pub(crate) fn create<R>(
142 source: R,
143 hint: &ProbeHint,
144 config: &DecoderConfig,
145 ) -> DecodeResult<Box<dyn Decoder>>
146 where
147 R: Read + Seek + Send + Sync + 'static,
148 {
149 let source: BoxedSource = Box::new(source);
150 Self::dispatch_backend(source, hint, config)
151 }
152
153 pub fn create_from_media_info<R>(
162 source: R,
163 media_info: &MediaInfo,
164 config: &DecoderConfig,
165 ) -> DecodeResult<Box<dyn Decoder>>
166 where
167 R: Read + Seek + Send + Sync + 'static,
168 {
169 tracing::debug!(?media_info, "create_from_media_info called");
170
171 let hint = ProbeHint {
172 codec: media_info.codec,
173 container: media_info.container,
174 extension: None,
175 mime: None,
176 };
177
178 Self::create(source, &hint, config)
179 }
180
181 pub fn create_with_probe<R>(
189 source: R,
190 hint: Option<&str>,
191 config: &DecoderConfig,
192 ) -> DecodeResult<Box<dyn Decoder>>
193 where
194 R: Read + Seek + Send + Sync + 'static,
195 {
196 let mut source = source;
197 let mut probe_hint = ProbeHint {
198 container: hint.and_then(container_from_extension),
199 extension: hint.map(String::from),
200 ..Default::default()
201 };
202
203 if matches!(
207 probe_hint.container,
208 Some(ContainerFormat::Mp4 | ContainerFormat::Fmp4)
209 ) && let Some(codec) = sniff_mp4_codec(&mut source).and_then(codec_from_mp4_fourcc)
210 {
211 probe_hint.codec = Some(codec);
212 }
213
214 probe_codec(&probe_hint)?;
215 Self::create(source, &probe_hint, config)
216 }
217
218 pub(super) fn dispatch_backend(
219 source: BoxedSource,
220 hint: &ProbeHint,
221 config: &DecoderConfig,
222 ) -> DecodeResult<Box<dyn Decoder>> {
223 let (codec, container) = resolve_codec_container(hint)?;
224
225 tracing::debug!(
226 ?codec,
227 ?container,
228 backend = ?config.backend,
229 "DecoderFactory::create called"
230 );
231
232 match config.backend {
233 #[cfg(all(feature = "apple", any(target_os = "macos", target_os = "ios")))]
234 DecoderBackend::Apple => create_apple(source, codec, container, config),
235 #[cfg(all(feature = "android", target_os = "android"))]
236 DecoderBackend::Android => create_android(source, codec, container, config),
237 #[cfg(feature = "symphonia")]
238 DecoderBackend::Symphonia => create_symphonia(source, codec, container, config),
239 }
240 }
241}
242
243#[cfg(all(feature = "apple", any(target_os = "macos", target_os = "ios")))]
244fn create_apple(
245 source: BoxedSource,
246 codec: AudioCodec,
247 container: Option<ContainerFormat>,
248 config: &DecoderConfig,
249) -> DecodeResult<Box<dyn Decoder>> {
250 use crate::apple::AppleCodec;
251
252 if should_use_segment_aware(codec, container, config)
253 && let Some(layout) = config.segment_layout.clone()
254 {
255 if AppleCodec::supports(codec) {
256 tracing::debug!(
257 ?codec,
258 "fmp4_segment: dispatching to segment-aware Apple HW codec path"
259 );
260 let gapless = config.gapless;
261 return build_fmp4_segment_decoder(source, layout, config, |track| {
262 AppleCodec::open_with_config(track, gapless)
263 });
264 }
265 #[cfg(feature = "symphonia")]
266 return create_fmp4_segment_symphonia(source, codec, layout, config);
267 #[cfg(not(feature = "symphonia"))]
268 {
269 let _ = layout;
270 return Err(DecodeError::UnsupportedCodec(codec));
271 }
272 }
273
274 if apple_standalone_supports(codec, container) {
275 tracing::debug!(
276 ?codec,
277 ?container,
278 "apple-standalone: routing via AudioFileServices"
279 );
280 return build_apple_standalone_decoder(source, codec, container, config);
281 }
282
283 #[cfg(feature = "symphonia")]
284 return create_symphonia(source, codec, container, config);
285 #[cfg(not(feature = "symphonia"))]
286 {
287 let _ = (source, container, config);
288 Err(DecodeError::UnsupportedCodec(codec))
289 }
290}
291
292#[cfg(all(feature = "apple", any(target_os = "macos", target_os = "ios")))]
293fn apple_standalone_supports(codec: AudioCodec, container: Option<ContainerFormat>) -> bool {
294 crate::apple::AppleAudioFileDemuxer::supports(codec, container)
295}
296
297#[cfg(all(feature = "apple", any(target_os = "macos", target_os = "ios")))]
298fn build_apple_standalone_decoder(
299 mut source: BoxedSource,
300 codec: AudioCodec,
301 container: Option<ContainerFormat>,
302 config: &DecoderConfig,
303) -> DecodeResult<Box<dyn Decoder>> {
304 use crate::{
305 apple::{AppleAudioFileDemuxer, AppleCodec},
306 composed::ComposedDecoder,
307 demuxer::Demuxer,
308 gapless::scoped_probe,
309 };
310 let probed_gapless = if config.gapless {
311 scoped_probe(&mut *source, codec)?
312 } else {
313 None
314 };
315 let mut demuxer = AppleAudioFileDemuxer::open_for(source, codec, container)?;
316 if probed_gapless.is_some() {
317 demuxer.set_gapless(probed_gapless);
318 }
319 let codec_impl = AppleCodec::open_with_config(demuxer.track_info(), config.gapless)?;
320 let pool = config
321 .pcm_pool
322 .clone()
323 .unwrap_or_else(|| PcmPool::default().clone());
324 let decoder = ComposedDecoder::new(
325 demuxer,
326 codec_impl,
327 pool,
328 config.epoch,
329 config.byte_len_handle.clone(),
330 config.hooks.clone(),
331 );
332 Ok(Box::new(decoder))
333}
334
335#[cfg(all(feature = "android", target_os = "android"))]
336fn create_android(
337 source: BoxedSource,
338 codec: AudioCodec,
339 container: Option<ContainerFormat>,
340 config: &DecoderConfig,
341) -> DecodeResult<Box<dyn Decoder>> {
342 use crate::android::AndroidCodec;
343
344 if should_use_segment_aware(codec, container, config)
345 && let Some(layout) = config.segment_layout.clone()
346 {
347 if AndroidCodec::supports(codec) {
348 tracing::debug!(
349 ?codec,
350 "fmp4_segment: dispatching to segment-aware Android HW codec path"
351 );
352 return build_fmp4_segment_decoder(source, layout, config, |track| {
353 AndroidCodec::open_with_config(track)
354 });
355 }
356 #[cfg(feature = "symphonia")]
357 return create_fmp4_segment_symphonia(source, codec, layout, config);
358 #[cfg(not(feature = "symphonia"))]
359 {
360 let _ = layout;
361 return Err(DecodeError::UnsupportedCodec(codec));
362 }
363 }
364
365 if android_standalone_supports(codec, container) {
366 tracing::debug!(
367 ?codec,
368 ?container,
369 "android-standalone: routing via AMediaExtractor"
370 );
371 return build_android_standalone_decoder(source, codec, container, config);
372 }
373
374 #[cfg(feature = "symphonia")]
375 return create_symphonia(source, codec, container, config);
376 #[cfg(not(feature = "symphonia"))]
377 {
378 let _ = (source, container, config);
379 Err(DecodeError::UnsupportedCodec(codec))
380 }
381}
382
383#[cfg(all(feature = "android", target_os = "android"))]
384fn android_standalone_supports(codec: AudioCodec, container: Option<ContainerFormat>) -> bool {
385 matches!(
386 (codec, container),
387 (AudioCodec::Pcm, Some(ContainerFormat::Wav))
388 | (AudioCodec::Mp3, Some(ContainerFormat::MpegAudio))
389 | (AudioCodec::Alac, Some(ContainerFormat::Mp4))
390 )
391}
392
393#[cfg(all(feature = "android", target_os = "android"))]
394fn build_android_standalone_decoder(
395 source: BoxedSource,
396 codec: AudioCodec,
397 container: Option<ContainerFormat>,
398 config: &DecoderConfig,
399) -> DecodeResult<Box<dyn Decoder>> {
400 use crate::{
401 android::{AndroidCodec, AndroidMediaExtractorDemuxer},
402 composed::ComposedDecoder,
403 demuxer::Demuxer,
404 };
405 let demuxer = match (codec, container) {
406 (AudioCodec::Pcm, Some(ContainerFormat::Wav)) => {
407 AndroidMediaExtractorDemuxer::open_wav(source)?
408 }
409 (AudioCodec::Mp3, Some(ContainerFormat::MpegAudio)) => {
410 AndroidMediaExtractorDemuxer::open_mp3(source)?
411 }
412 (AudioCodec::Alac, Some(ContainerFormat::Mp4)) => {
413 AndroidMediaExtractorDemuxer::open_alac_m4a(source)?
414 }
415 _ => return Err(DecodeError::UnsupportedCodec(codec)),
416 };
417 let codec_impl = AndroidCodec::open_with_config(demuxer.track_info())?;
418 let pool = config
419 .pcm_pool
420 .clone()
421 .unwrap_or_else(|| PcmPool::default().clone());
422 let decoder = ComposedDecoder::new(
423 demuxer,
424 codec_impl,
425 pool,
426 config.epoch,
427 config.byte_len_handle.clone(),
428 config.hooks.clone(),
429 );
430 Ok(Box::new(decoder))
431}
432
433#[cfg(feature = "symphonia")]
434fn create_symphonia(
435 source: BoxedSource,
436 codec: AudioCodec,
437 container: Option<ContainerFormat>,
438 config: &DecoderConfig,
439) -> DecodeResult<Box<dyn Decoder>> {
440 if should_use_segment_aware(codec, container, config)
441 && let Some(layout) = config.segment_layout.clone()
442 {
443 return create_fmp4_segment_symphonia(source, codec, layout, config);
444 }
445 create_file_symphonia_universal(source, codec, container, config)
446}
447
448#[cfg(feature = "symphonia")]
449fn create_file_symphonia_universal(
450 mut source: BoxedSource,
451 codec: AudioCodec,
452 container: Option<ContainerFormat>,
453 config: &DecoderConfig,
454) -> DecodeResult<Box<dyn Decoder>> {
455 use crate::{
456 composed::ComposedDecoder,
457 demuxer::Demuxer,
458 gapless::scoped_probe,
459 symphonia::{SymphoniaCodec, SymphoniaConfig, SymphoniaDemuxer},
460 };
461
462 tracing::debug!(
463 ?codec,
464 ?container,
465 "file-symphonia: dispatching to ComposedDecoder<SymphoniaDemuxer, SymphoniaCodec>"
466 );
467
468 let probed_gapless = if config.gapless {
469 scoped_probe(&mut *source, codec)?
470 } else {
471 None
472 };
473
474 let (mut demuxer, _byte_len) = SymphoniaDemuxer::open_file(
475 source,
476 config.hint.clone(),
477 container,
478 config.byte_len_handle.clone(),
479 config.segment_layout.clone(),
480 )?;
481 if probed_gapless.is_some() {
482 demuxer.set_gapless(probed_gapless);
483 }
484 let symphonia_config = SymphoniaConfig {
485 gapless: config.gapless,
486 ..Default::default()
487 };
488 let codec_impl = if SymphoniaCodec::supports(codec) {
489 SymphoniaCodec::open_with_config(demuxer.track_info(), &symphonia_config)?
490 } else {
491 SymphoniaCodec::open_native(demuxer.native_params())?
492 };
493 let pool = config
494 .pcm_pool
495 .clone()
496 .unwrap_or_else(|| PcmPool::default().clone());
497 let decoder = ComposedDecoder::new(
498 demuxer,
499 codec_impl,
500 pool,
501 config.epoch,
502 config.byte_len_handle.clone(),
503 config.hooks.clone(),
504 );
505 Ok(Box::new(decoder))
506}
507
508fn should_use_segment_aware(
513 codec: AudioCodec,
514 container: Option<ContainerFormat>,
515 config: &DecoderConfig,
516) -> bool {
517 matches!(
518 codec,
519 AudioCodec::AacLc | AudioCodec::AacHe | AudioCodec::AacHeV2 | AudioCodec::Flac
520 ) && matches!(container, Some(ContainerFormat::Fmp4))
521 && config.segment_layout.is_some()
522}
523
524#[cfg(feature = "symphonia")]
525fn create_fmp4_segment_symphonia(
526 source: BoxedSource,
527 codec: AudioCodec,
528 layout: Arc<dyn SegmentLayout>,
529 config: &DecoderConfig,
530) -> DecodeResult<Box<dyn Decoder>> {
531 use crate::symphonia::{SymphoniaCodec, SymphoniaConfig};
532
533 tracing::debug!(
534 ?codec,
535 "fmp4_segment: dispatching to segment-aware Symphonia path"
536 );
537 match codec {
538 AudioCodec::AacLc | AudioCodec::AacHe | AudioCodec::AacHeV2 | AudioCodec::Flac => {
539 let symphonia_config = SymphoniaConfig {
540 gapless: config.gapless,
541 ..Default::default()
542 };
543 build_fmp4_segment_decoder(source, layout, config, |track| {
544 SymphoniaCodec::open_with_config(track, &symphonia_config)
545 })
546 }
547 other => Err(DecodeError::UnsupportedCodec(other)),
548 }
549}
550
551fn build_fmp4_segment_decoder<C, F>(
556 source: BoxedSource,
557 layout: Arc<dyn SegmentLayout>,
558 config: &DecoderConfig,
559 open_codec: F,
560) -> DecodeResult<Box<dyn Decoder>>
561where
562 C: crate::codec::FrameCodec + 'static,
563 F: FnOnce(&crate::demuxer::TrackInfo) -> DecodeResult<C>,
564{
565 use crate::{composed::ComposedDecoder, demuxer::Demuxer, fmp4::Fmp4SegmentDemuxer};
566
567 let demuxer = Fmp4SegmentDemuxer::open(source, layout)?;
568 let codec = open_codec(demuxer.track_info())?;
569 let pool = config
570 .pcm_pool
571 .clone()
572 .unwrap_or_else(|| PcmPool::default().clone());
573 let decoder = ComposedDecoder::new(
574 demuxer,
575 codec,
576 pool,
577 config.epoch,
578 config.byte_len_handle.clone(),
579 config.hooks.clone(),
580 );
581 Ok(Box::new(decoder))
582}