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