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