scuffle_flv/
lib.rs

1//! A pure Rust implementation of the FLV format, allowing for demuxing of FLV
2//! files and streams.
3#![cfg_attr(feature = "docs", doc = "\n\nSee the [changelog][changelog] for a full release history.")]
4#![cfg_attr(feature = "docs", doc = "## Feature flags")]
5#![cfg_attr(feature = "docs", doc = document_features::document_features!())]
6//! ## Specifications
7//!
8//! | Name | Version | Link | Comments |
9//! | --- | --- | --- | --- |
10//! | Video File Format Specification | `10` | <https://github.com/veovera/enhanced-rtmp/blob/main/docs/legacy/video-file-format-v10-0-spec.pdf> | |
11//! | Adobe Flash Video File Format Specification | `10.1` | <https://github.com/veovera/enhanced-rtmp/blob/main/docs/legacy/video-file-format-v10-1-spec.pdf> | Refered to as 'Legacy FLV spec' in this documentation |
12//! | Enhancing RTMP, FLV | `v1-2024-02-29-r1` | <https://github.com/veovera/enhanced-rtmp/blob/main/docs/enhanced/enhanced-rtmp-v1.pdf> | |
13//! | Enhanced RTMP | `v2-2024-10-22-b1` | <https://github.com/veovera/enhanced-rtmp/blob/main/docs/enhanced/enhanced-rtmp-v2.pdf> | Refered to as 'Enhanced RTMP spec' in this documentation |
14//!
15//! ## License
16//!
17//! This project is licensed under the MIT or Apache-2.0 license.
18//! You can choose between one of them if you use this work.
19//!
20//! `SPDX-License-Identifier: MIT OR Apache-2.0`
21#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))]
22#![cfg_attr(docsrs, feature(doc_auto_cfg))]
23#![deny(missing_docs)]
24#![deny(unsafe_code)]
25#![deny(unreachable_pub)]
26#![deny(clippy::mod_module_files)]
27
28pub mod audio;
29pub mod common;
30pub mod error;
31pub mod file;
32pub mod header;
33pub mod script;
34pub mod tag;
35pub mod video;
36
37#[cfg(test)]
38#[cfg_attr(all(test, coverage_nightly), coverage(off))]
39mod tests {
40    use std::io;
41    use std::path::PathBuf;
42
43    use bytes::Bytes;
44    use scuffle_aac::{AudioObjectType, PartialAudioSpecificConfig};
45    use scuffle_amf0::Amf0Value;
46    use scuffle_av1::ObuHeader;
47    use scuffle_av1::seq::SequenceHeaderObu;
48    use scuffle_bytes_util::StringCow;
49    use scuffle_h264::Sps;
50    use scuffle_h265::{ConstantFrameRate, NumTemporalLayers};
51
52    use crate::audio::AudioData;
53    use crate::audio::body::AudioTagBody;
54    use crate::audio::body::legacy::LegacyAudioTagBody;
55    use crate::audio::body::legacy::aac::AacAudioData;
56    use crate::audio::header::AudioTagHeader;
57    use crate::audio::header::legacy::{LegacyAudioTagHeader, SoundFormat, SoundRate, SoundSize, SoundType};
58    use crate::file::FlvFile;
59    use crate::script::{OnMetaDataAudioCodecId, OnMetaDataVideoCodecId, ScriptData};
60    use crate::tag::FlvTagData;
61    use crate::video::VideoData;
62    use crate::video::body::VideoTagBody;
63    use crate::video::body::enhanced::{ExVideoTagBody, VideoPacket, VideoPacketSequenceStart};
64    use crate::video::body::legacy::LegacyVideoTagBody;
65    use crate::video::header::enhanced::VideoFourCc;
66    use crate::video::header::legacy::{LegacyVideoTagHeader, LegacyVideoTagHeaderAvcPacket, VideoCodecId};
67    use crate::video::header::{VideoFrameType, VideoTagHeader, VideoTagHeaderData};
68
69    fn file_path(item: &str) -> PathBuf {
70        if let Some(env) = std::env::var_os("ASSETS_DIR") {
71            PathBuf::from(env).join(item)
72        } else {
73            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(format!("../../assets/{item}"))
74        }
75    }
76
77    #[test]
78    fn test_demux_flv_avc_aac() {
79        let data = Bytes::from(std::fs::read(file_path("avc_aac.flv")).expect("failed to read file"));
80        let mut reader = io::Cursor::new(data);
81
82        let flv = FlvFile::demux(&mut reader).expect("failed to demux flv");
83
84        assert_eq!(flv.header.version, 1);
85        assert!(flv.header.is_audio_present);
86        assert!(flv.header.is_video_present);
87        assert_eq!(flv.header.extra.len(), 0);
88
89        let mut tags = flv.tags.into_iter();
90
91        // Metadata tag
92        {
93            let tag = tags.next().expect("expected tag");
94            assert_eq!(tag.timestamp_ms, 0);
95            assert_eq!(tag.stream_id, 0);
96
97            // This is a metadata tag
98            let on_meta_data = match tag.data {
99                FlvTagData::ScriptData(ScriptData::OnMetaData(data)) => data,
100                _ => panic!("expected script data"),
101            };
102
103            assert_eq!(on_meta_data.audiosamplesize, Some(16.0));
104            assert_eq!(on_meta_data.audiosamplerate, Some(48000.0));
105            assert_eq!(on_meta_data.stereo, Some(true));
106            assert_eq!(
107                on_meta_data.audiocodecid,
108                Some(OnMetaDataAudioCodecId::Legacy(SoundFormat::Aac))
109            ); // AAC
110            assert_eq!(
111                on_meta_data.videocodecid,
112                Some(OnMetaDataVideoCodecId::Legacy(VideoCodecId::Avc))
113            ); // AVC
114            assert_eq!(on_meta_data.duration, Some(1.088)); // 1.088 seconds
115            assert_eq!(on_meta_data.width, Some(3840.0));
116            assert_eq!(on_meta_data.height, Some(2160.0));
117            assert_eq!(on_meta_data.framerate, Some(60.0));
118            assert!(on_meta_data.videodatarate.is_some());
119            assert!(on_meta_data.audiodatarate.is_some());
120
121            // Should have a minor version property
122            let minor_version = match on_meta_data.other.get(&StringCow::from_static("minor_version")) {
123                Some(Amf0Value::String(number)) => number,
124                _ => panic!("expected minor version"),
125            };
126
127            assert_eq!(minor_version, "512");
128
129            // Should have a major brand property
130            let major_brand = match on_meta_data.other.get(&StringCow::from_static("major_brand")) {
131                Some(Amf0Value::String(string)) => string,
132                _ => panic!("expected major brand"),
133            };
134
135            assert_eq!(major_brand, "iso5");
136
137            // Should have a compatible_brands property
138            let compatible_brands = match on_meta_data.other.get(&StringCow::from_static("compatible_brands")) {
139                Some(Amf0Value::String(string)) => string,
140                _ => panic!("expected compatible brands"),
141            };
142
143            assert_eq!(compatible_brands, "iso5iso6mp41");
144        }
145
146        // Video Sequence Header Tag
147        {
148            let tag = tags.next().expect("expected tag");
149            assert_eq!(tag.timestamp_ms, 0);
150            assert_eq!(tag.stream_id, 0);
151
152            // This is a video tag
153            let (frame_type, avc_decoder_configuration_record) = match tag.data {
154                FlvTagData::Video(VideoData {
155                    header: VideoTagHeader { frame_type, .. },
156                    body: VideoTagBody::Legacy(LegacyVideoTagBody::AvcVideoPacketSeqHdr(avc_decoder_configuration_record)),
157                }) => (frame_type, avc_decoder_configuration_record),
158                _ => panic!("expected video data"),
159            };
160
161            assert_eq!(frame_type, VideoFrameType::KeyFrame);
162
163            // The avc sequence header should be able to be decoded into an avc decoder
164            // configuration record
165            assert_eq!(avc_decoder_configuration_record.profile_indication, 100);
166            assert_eq!(avc_decoder_configuration_record.profile_compatibility, 0);
167            assert_eq!(avc_decoder_configuration_record.level_indication, 51); // 5.1
168            assert_eq!(avc_decoder_configuration_record.length_size_minus_one, 3);
169            assert_eq!(avc_decoder_configuration_record.sps.len(), 1);
170            assert_eq!(avc_decoder_configuration_record.pps.len(), 1);
171            assert_eq!(avc_decoder_configuration_record.extended_config, None);
172
173            let sps =
174                Sps::parse_with_emulation_prevention(&mut std::io::Cursor::new(&avc_decoder_configuration_record.sps[0]))
175                    .expect("expected sequence parameter set");
176
177            insta::assert_debug_snapshot!(sps, @r"
178            Sps {
179                nal_ref_idc: 3,
180                nal_unit_type: NALUnitType::SPS,
181                profile_idc: 100,
182                constraint_set0_flag: false,
183                constraint_set1_flag: false,
184                constraint_set2_flag: false,
185                constraint_set3_flag: false,
186                constraint_set4_flag: false,
187                constraint_set5_flag: false,
188                level_idc: 51,
189                seq_parameter_set_id: 0,
190                ext: Some(
191                    SpsExtended {
192                        chroma_format_idc: 1,
193                        separate_color_plane_flag: false,
194                        bit_depth_luma_minus8: 0,
195                        bit_depth_chroma_minus8: 0,
196                        qpprime_y_zero_transform_bypass_flag: false,
197                        scaling_matrix: [],
198                    },
199                ),
200                log2_max_frame_num_minus4: 0,
201                pic_order_cnt_type: 0,
202                log2_max_pic_order_cnt_lsb_minus4: Some(
203                    4,
204                ),
205                pic_order_cnt_type1: None,
206                max_num_ref_frames: 4,
207                gaps_in_frame_num_value_allowed_flag: false,
208                pic_width_in_mbs_minus1: 239,
209                pic_height_in_map_units_minus1: 134,
210                mb_adaptive_frame_field_flag: None,
211                direct_8x8_inference_flag: true,
212                frame_crop_info: None,
213                sample_aspect_ratio: Some(
214                    SarDimensions {
215                        aspect_ratio_idc: AspectRatioIdc::Square,
216                        sar_width: 0,
217                        sar_height: 0,
218                    },
219                ),
220                overscan_appropriate_flag: None,
221                color_config: None,
222                chroma_sample_loc: None,
223                timing_info: Some(
224                    TimingInfo {
225                        num_units_in_tick: 1,
226                        time_scale: 120,
227                    },
228                ),
229            }
230            ");
231        }
232
233        // Audio Sequence Header Tag
234        {
235            let tag = tags.next().expect("expected tag");
236            assert_eq!(tag.timestamp_ms, 0);
237            assert_eq!(tag.stream_id, 0);
238
239            let (data, sound_rate, sound_size, sound_type) = match tag.data {
240                FlvTagData::Audio(AudioData {
241                    header:
242                        AudioTagHeader::Legacy(LegacyAudioTagHeader {
243                            sound_rate,
244                            sound_size,
245                            sound_type,
246                            ..
247                        }),
248                    body,
249                }) => (body, sound_rate, sound_size, sound_type),
250                _ => panic!("expected audio data"),
251            };
252
253            assert_eq!(sound_rate, SoundRate::Hz44000);
254            assert_eq!(sound_size, SoundSize::Bit16);
255            assert_eq!(sound_type, SoundType::Stereo);
256
257            // Audio data should be an AAC sequence header
258            let data = match data {
259                AudioTagBody::Legacy(LegacyAudioTagBody::Aac(AacAudioData::SequenceHeader(data))) => data,
260                _ => panic!("expected aac sequence header"),
261            };
262
263            // The aac sequence header should be able to be decoded into an aac decoder
264            // configuration record
265            let aac_decoder_configuration_record =
266                PartialAudioSpecificConfig::parse(&data).expect("expected aac decoder configuration record");
267
268            assert_eq!(
269                aac_decoder_configuration_record.audio_object_type,
270                AudioObjectType::AacLowComplexity
271            );
272            assert_eq!(aac_decoder_configuration_record.sampling_frequency, 48000);
273            assert_eq!(aac_decoder_configuration_record.channel_configuration, 2);
274        }
275
276        // Rest of the tags should be video / audio data
277        let mut last_timestamp = 0;
278        let mut read_seq_end = false;
279        for tag in tags {
280            assert!(tag.timestamp_ms >= last_timestamp);
281            assert_eq!(tag.stream_id, 0);
282
283            last_timestamp = tag.timestamp_ms;
284
285            match tag.data {
286                FlvTagData::Audio(AudioData {
287                    body,
288                    header:
289                        AudioTagHeader::Legacy(LegacyAudioTagHeader {
290                            sound_rate,
291                            sound_size,
292                            sound_type,
293                            ..
294                        }),
295                }) => {
296                    assert_eq!(sound_rate, SoundRate::Hz44000);
297                    assert_eq!(sound_size, SoundSize::Bit16);
298                    assert_eq!(sound_type, SoundType::Stereo);
299                    match body {
300                        AudioTagBody::Legacy(LegacyAudioTagBody::Aac(AacAudioData::Raw(data))) => data,
301                        _ => panic!("expected aac raw packet"),
302                    };
303                }
304                FlvTagData::Video(VideoData {
305                    header:
306                        VideoTagHeader {
307                            frame_type,
308                            data: VideoTagHeaderData::Legacy(data),
309                        },
310                    ..
311                }) => {
312                    match frame_type {
313                        VideoFrameType::KeyFrame => (),
314                        VideoFrameType::InterFrame => (),
315                        _ => panic!("expected keyframe or interframe"),
316                    }
317
318                    match data {
319                        LegacyVideoTagHeader::AvcPacket(LegacyVideoTagHeaderAvcPacket::Nalu { .. }) => {
320                            assert!(!read_seq_end)
321                        }
322                        LegacyVideoTagHeader::AvcPacket(LegacyVideoTagHeaderAvcPacket::EndOfSequence) => {
323                            assert!(!read_seq_end);
324                            read_seq_end = true;
325                        }
326                        _ => panic!("expected avc nalu packet: {data:?}"),
327                    }
328                }
329                _ => panic!("unexpected data"),
330            };
331        }
332
333        assert!(read_seq_end);
334    }
335
336    #[test]
337    fn test_demux_flv_av1_aac() {
338        let data = Bytes::from(std::fs::read(file_path("av1_aac.flv")).expect("failed to read file"));
339        let mut reader = io::Cursor::new(data);
340
341        let flv = FlvFile::demux(&mut reader).expect("failed to demux flv");
342
343        assert_eq!(flv.header.version, 1);
344        assert!(flv.header.is_audio_present);
345        assert!(flv.header.is_video_present);
346        assert_eq!(flv.header.extra.len(), 0);
347
348        let mut tags = flv.tags.into_iter();
349
350        // Metadata tag
351        {
352            let tag = tags.next().expect("expected tag");
353            assert_eq!(tag.timestamp_ms, 0);
354            assert_eq!(tag.stream_id, 0);
355
356            // This is a metadata tag
357            let on_meta_data = match tag.data {
358                FlvTagData::ScriptData(ScriptData::OnMetaData(data)) => data,
359                _ => panic!("expected script data"),
360            };
361
362            assert_eq!(on_meta_data.audiosamplesize, Some(16.0));
363            assert_eq!(on_meta_data.audiosamplerate, Some(48000.0));
364            assert_eq!(on_meta_data.stereo, Some(true));
365            assert_eq!(
366                on_meta_data.audiocodecid,
367                Some(OnMetaDataAudioCodecId::Legacy(SoundFormat::Aac))
368            ); // AAC
369            assert_eq!(
370                on_meta_data.videocodecid,
371                Some(OnMetaDataVideoCodecId::Legacy(VideoCodecId::Avc))
372            ); // AVC
373            assert_eq!(on_meta_data.duration, Some(0.0)); // 0 seconds (this was a live stream)
374            assert_eq!(on_meta_data.width, Some(2560.0));
375            assert_eq!(on_meta_data.height, Some(1440.0));
376            assert_eq!(on_meta_data.framerate, Some(144.0));
377            assert!(on_meta_data.videodatarate.is_some());
378            assert!(on_meta_data.audiodatarate.is_some());
379        }
380
381        // Audio Sequence Header Tag
382        {
383            let tag = tags.next().expect("expected tag");
384            assert_eq!(tag.timestamp_ms, 0);
385            assert_eq!(tag.stream_id, 0);
386
387            let (body, sound_rate, sound_size, sound_type) = match tag.data {
388                FlvTagData::Audio(AudioData {
389                    body,
390                    header:
391                        AudioTagHeader::Legacy(LegacyAudioTagHeader {
392                            sound_rate,
393                            sound_size,
394                            sound_type,
395                            ..
396                        }),
397                }) => (body, sound_rate, sound_size, sound_type),
398                _ => panic!("expected audio data"),
399            };
400
401            assert_eq!(sound_rate, SoundRate::Hz44000);
402            assert_eq!(sound_size, SoundSize::Bit16);
403            assert_eq!(sound_type, SoundType::Stereo);
404
405            // Audio data should be an AAC sequence header
406            let data = match body {
407                AudioTagBody::Legacy(LegacyAudioTagBody::Aac(AacAudioData::SequenceHeader(data))) => data,
408                _ => panic!("expected aac sequence header"),
409            };
410
411            // The aac sequence header should be able to be decoded into an aac decoder
412            // configuration record
413            let aac_decoder_configuration_record =
414                PartialAudioSpecificConfig::parse(&data).expect("expected aac decoder configuration record");
415
416            assert_eq!(
417                aac_decoder_configuration_record.audio_object_type,
418                AudioObjectType::AacLowComplexity
419            );
420            assert_eq!(aac_decoder_configuration_record.sampling_frequency, 48000);
421            assert_eq!(aac_decoder_configuration_record.channel_configuration, 2);
422        }
423
424        // Video Sequence Header Tag
425        {
426            let tag = tags.next().expect("expected tag");
427            assert_eq!(tag.timestamp_ms, 0);
428            assert_eq!(tag.stream_id, 0);
429
430            // This is a video tag
431            let frame_type = match tag.data {
432                FlvTagData::Video(VideoData {
433                    header: VideoTagHeader { frame_type, .. },
434                    ..
435                }) => frame_type,
436                _ => panic!("expected video data"),
437            };
438
439            assert_eq!(frame_type, VideoFrameType::KeyFrame);
440
441            // Video data should be an AVC sequence header
442            let config = match tag.data {
443                FlvTagData::Video(VideoData {
444                    body:
445                        VideoTagBody::Enhanced(ExVideoTagBody::NoMultitrack {
446                            video_four_cc: VideoFourCc::Av1,
447                            packet: VideoPacket::SequenceStart(VideoPacketSequenceStart::Av1(config)),
448                        }),
449                    ..
450                }) => config,
451                _ => panic!("expected video data"),
452            };
453
454            assert_eq!(config.chroma_sample_position, 0);
455            assert!(config.chroma_subsampling_x); // 5.1
456            assert!(config.chroma_subsampling_y);
457            assert!(!config.high_bitdepth);
458            assert!(!config.twelve_bit);
459
460            let mut reader = std::io::Cursor::new(config.config_obu);
461
462            let header = ObuHeader::parse(&mut reader).expect("expected obu header");
463
464            let seq_obu = SequenceHeaderObu::parse(header, &mut reader).expect("expected sequence obu");
465
466            assert_eq!(seq_obu.max_frame_height, 1440);
467            assert_eq!(seq_obu.max_frame_width, 2560);
468        }
469
470        // Rest of the tags should be video / audio data
471        let mut last_timestamp = 0;
472        let mut read_seq_end = false;
473        for tag in tags {
474            assert!(tag.timestamp_ms >= last_timestamp || tag.timestamp_ms == 0); // Timestamps should be monotonically increasing or 0
475            assert_eq!(tag.stream_id, 0);
476
477            if tag.timestamp_ms != 0 {
478                last_timestamp = tag.timestamp_ms;
479            }
480
481            match tag.data {
482                FlvTagData::Audio(AudioData {
483                    body,
484                    header:
485                        AudioTagHeader::Legacy(LegacyAudioTagHeader {
486                            sound_rate,
487                            sound_size,
488                            sound_type,
489                            ..
490                        }),
491                }) => {
492                    assert_eq!(sound_rate, SoundRate::Hz44000);
493                    assert_eq!(sound_size, SoundSize::Bit16);
494                    assert_eq!(sound_type, SoundType::Stereo);
495                    match body {
496                        AudioTagBody::Legacy(LegacyAudioTagBody::Aac(AacAudioData::Raw(data))) => data,
497                        _ => panic!("expected aac raw packet"),
498                    };
499                }
500                FlvTagData::Video(VideoData {
501                    header: VideoTagHeader { frame_type, .. },
502                    body: VideoTagBody::Enhanced(body),
503                }) => {
504                    match frame_type {
505                        VideoFrameType::KeyFrame => (),
506                        VideoFrameType::InterFrame => (),
507                        _ => panic!("expected keyframe or interframe"),
508                    }
509
510                    match body {
511                        ExVideoTagBody::NoMultitrack {
512                            video_four_cc: VideoFourCc::Av1,
513                            packet: VideoPacket::CodedFrames(_),
514                        } => {
515                            assert!(!read_seq_end);
516                        }
517                        ExVideoTagBody::NoMultitrack {
518                            video_four_cc: VideoFourCc::Av1,
519                            packet: VideoPacket::CodedFramesX { .. },
520                        } => {
521                            assert!(!read_seq_end);
522                        }
523                        ExVideoTagBody::ManyTracks(tracks) => {
524                            assert!(!read_seq_end);
525                            assert!(tracks.is_empty());
526                        }
527                        ExVideoTagBody::NoMultitrack {
528                            video_four_cc: VideoFourCc::Av1,
529                            packet: VideoPacket::SequenceEnd,
530                        } => {
531                            assert!(!read_seq_end);
532                            read_seq_end = true;
533                        }
534                        _ => panic!("expected av1 raw packet: {body:?}"),
535                    };
536                }
537                _ => panic!("unexpected data"),
538            };
539        }
540
541        assert!(read_seq_end);
542    }
543
544    #[test]
545    fn test_demux_flv_hevc_aac() {
546        let data = Bytes::from(std::fs::read(file_path("hevc_aac.flv")).expect("failed to read file"));
547        let mut reader = io::Cursor::new(data);
548
549        let flv = FlvFile::demux(&mut reader).expect("failed to demux flv");
550
551        assert_eq!(flv.header.version, 1);
552        assert!(flv.header.is_audio_present);
553        assert!(flv.header.is_video_present);
554        assert_eq!(flv.header.extra.len(), 0);
555
556        let mut tags = flv.tags.into_iter();
557
558        // Metadata tag
559        {
560            let tag = tags.next().expect("expected tag");
561            assert_eq!(tag.timestamp_ms, 0);
562            assert_eq!(tag.stream_id, 0);
563
564            let on_meta_data = match tag.data {
565                FlvTagData::ScriptData(ScriptData::OnMetaData(data)) => data,
566                _ => panic!("expected script data"),
567            };
568
569            assert_eq!(on_meta_data.audiosamplesize, Some(16.0));
570            assert_eq!(on_meta_data.audiosamplerate, Some(48000.0));
571            assert_eq!(on_meta_data.stereo, Some(true));
572            assert_eq!(
573                on_meta_data.audiocodecid,
574                Some(OnMetaDataAudioCodecId::Legacy(SoundFormat::Aac))
575            ); // AAC
576            assert_eq!(
577                on_meta_data.videocodecid,
578                Some(OnMetaDataVideoCodecId::Enhanced(VideoFourCc::Hevc))
579            ); // HEVC
580            assert_eq!(on_meta_data.duration, Some(2.038));
581            assert_eq!(on_meta_data.width, Some(3840.0));
582            assert_eq!(on_meta_data.height, Some(2160.0));
583            assert_eq!(on_meta_data.framerate, Some(60.0));
584            assert!(on_meta_data.videodatarate.is_some());
585            assert!(on_meta_data.audiodatarate.is_some());
586        }
587
588        // Video Sequence Header Tag
589        {
590            let tag = tags.next().expect("expected tag");
591            assert_eq!(tag.timestamp_ms, 0);
592            assert_eq!(tag.stream_id, 0);
593
594            // This is a video tag
595            let (frame_type, config) = match tag.data {
596                FlvTagData::Video(VideoData {
597                    header: VideoTagHeader { frame_type, .. },
598                    body:
599                        VideoTagBody::Enhanced(ExVideoTagBody::NoMultitrack {
600                            video_four_cc: VideoFourCc::Hevc,
601                            packet: VideoPacket::SequenceStart(VideoPacketSequenceStart::Hevc(config)),
602                        }),
603                }) => (frame_type, config),
604                _ => panic!("expected video data"),
605            };
606
607            assert_eq!(frame_type, VideoFrameType::KeyFrame);
608
609            assert_eq!(config.avg_frame_rate, 0);
610            assert_eq!(config.constant_frame_rate, ConstantFrameRate::Unknown);
611            assert_eq!(config.num_temporal_layers, NumTemporalLayers::NotScalable);
612
613            // We should be able to find a SPS NAL unit in the sequence header
614            let Some(sps) = config
615                .arrays
616                .iter()
617                .find(|a| a.nal_unit_type == scuffle_h265::NALUnitType::SpsNut)
618                .and_then(|v| v.nalus.first())
619            else {
620                panic!("expected sps");
621            };
622
623            // We should be able to find a PPS NAL unit in the sequence header
624            let Some(_) = config
625                .arrays
626                .iter()
627                .find(|a| a.nal_unit_type == scuffle_h265::NALUnitType::PpsNut)
628                .and_then(|v| v.nalus.first())
629            else {
630                panic!("expected pps");
631            };
632
633            // We should be able to decode the SPS NAL unit
634            let sps = scuffle_h265::SpsNALUnit::parse(io::Cursor::new(sps.clone())).expect("expected sps");
635
636            insta::assert_debug_snapshot!(sps);
637        }
638
639        // Audio Sequence Header Tag
640        {
641            let tag = tags.next().expect("expected tag");
642            assert_eq!(tag.timestamp_ms, 0);
643            assert_eq!(tag.stream_id, 0);
644
645            let (body, sound_rate, sound_size, sound_type) = match tag.data {
646                FlvTagData::Audio(AudioData {
647                    body,
648                    header:
649                        AudioTagHeader::Legacy(LegacyAudioTagHeader {
650                            sound_rate,
651                            sound_size,
652                            sound_type,
653                            ..
654                        }),
655                }) => (body, sound_rate, sound_size, sound_type),
656                _ => panic!("expected audio data"),
657            };
658
659            assert_eq!(sound_rate, SoundRate::Hz44000);
660            assert_eq!(sound_size, SoundSize::Bit16);
661            assert_eq!(sound_type, SoundType::Stereo);
662
663            // Audio data should be an AAC sequence header
664            let data = match body {
665                AudioTagBody::Legacy(LegacyAudioTagBody::Aac(AacAudioData::SequenceHeader(data))) => data,
666                _ => panic!("expected aac sequence header"),
667            };
668
669            // The aac sequence header should be able to be decoded into an aac decoder
670            // configuration record
671            let aac_decoder_configuration_record =
672                PartialAudioSpecificConfig::parse(&data).expect("expected aac decoder configuration record");
673
674            assert_eq!(
675                aac_decoder_configuration_record.audio_object_type,
676                AudioObjectType::AacLowComplexity
677            );
678            assert_eq!(aac_decoder_configuration_record.sampling_frequency, 48000);
679            assert_eq!(aac_decoder_configuration_record.channel_configuration, 2);
680        }
681
682        // Rest of the tags should be video / audio data
683        let mut last_timestamp = 0;
684        for tag in tags {
685            assert!(tag.timestamp_ms >= last_timestamp || tag.timestamp_ms == 0); // Timestamps should be monotonically increasing or 0
686            assert_eq!(tag.stream_id, 0);
687
688            if tag.timestamp_ms != 0 {
689                last_timestamp = tag.timestamp_ms;
690            }
691
692            match tag.data {
693                FlvTagData::Audio(AudioData {
694                    body,
695                    header:
696                        AudioTagHeader::Legacy(LegacyAudioTagHeader {
697                            sound_rate,
698                            sound_size,
699                            sound_type,
700                            ..
701                        }),
702                }) => {
703                    assert_eq!(sound_rate, SoundRate::Hz44000);
704                    assert_eq!(sound_size, SoundSize::Bit16);
705                    assert_eq!(sound_type, SoundType::Stereo);
706                    match body {
707                        AudioTagBody::Legacy(LegacyAudioTagBody::Aac(AacAudioData::Raw(data))) => data,
708                        _ => panic!("expected aac raw packet"),
709                    };
710                }
711                FlvTagData::Video(VideoData {
712                    header: VideoTagHeader { frame_type, .. },
713                    body:
714                        VideoTagBody::Enhanced(ExVideoTagBody::NoMultitrack {
715                            video_four_cc: VideoFourCc::Hevc,
716                            ..
717                        }),
718                }) => match frame_type {
719                    VideoFrameType::KeyFrame => (),
720                    VideoFrameType::InterFrame => (),
721                    VideoFrameType::Command => (),
722                    _ => panic!("expected keyframe, interframe or command"),
723                },
724                _ => panic!("unexpected data"),
725            };
726        }
727    }
728}
729
730/// Changelogs generated by [scuffle_changelog]
731#[cfg(feature = "docs")]
732#[scuffle_changelog::changelog]
733pub mod changelog {}