scuffle_flv/
script.rs

1//! Script data structures
2
3use core::fmt;
4use std::io;
5
6use bytes::Bytes;
7use scuffle_amf0::de::MultiValue;
8use scuffle_amf0::decoder::Amf0Decoder;
9use scuffle_amf0::{Amf0Object, Amf0Value};
10use scuffle_bytes_util::{BytesCursorExt, StringCow};
11use serde::de::VariantAccess;
12use serde_derive::Deserialize;
13
14use crate::audio::header::enhanced::AudioFourCc;
15use crate::audio::header::legacy::SoundFormat;
16use crate::error::FlvError;
17use crate::video::header::enhanced::VideoFourCc;
18use crate::video::header::legacy::VideoCodecId;
19
20/// FLV `onMetaData` audio codec ID.
21///
22/// Either a legacy [`SoundFormat`] or an enhanced [`AudioFourCc`].
23/// Appears as `audiocodecid` in the [`OnMetaData`] script data.
24#[derive(Debug, Clone, PartialEq)]
25pub enum OnMetaDataAudioCodecId {
26    /// Legacy audio codec ID.
27    Legacy(SoundFormat),
28    /// Enhanced audio codec ID.
29    Enhanced(AudioFourCc),
30}
31
32impl<'de> serde::Deserialize<'de> for OnMetaDataAudioCodecId {
33    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
34    where
35        D: serde::Deserializer<'de>,
36    {
37        let n: u32 = serde::Deserialize::deserialize(deserializer)?;
38
39        // Since SoundFormat is a u8, we can be sure that the number represents an AudioFourCc if it is greater
40        // than u8::MAX.
41        // Additionally, since the smallest possible AudioFourCc (4 spaces) is greater than u8::MAX,
42        // we can be sure that the number cannot represent an AudioFourCc if it is smaller than u8::MAX.
43        if n > u8::MAX as u32 {
44            Ok(Self::Enhanced(AudioFourCc::from(n.to_be_bytes())))
45        } else {
46            Ok(Self::Legacy(SoundFormat::from(n as u8)))
47        }
48    }
49}
50
51/// FLV `onMetaData` video codec ID.
52///
53/// Either a legacy [`VideoCodecId`] or an enhanced [`VideoFourCc`].
54/// Appears as `videocodecid` in the [`OnMetaData`] script data.
55#[derive(Debug, Clone, PartialEq)]
56pub enum OnMetaDataVideoCodecId {
57    /// Legacy video codec ID.
58    Legacy(VideoCodecId),
59    /// Enhanced video codec ID.
60    Enhanced(VideoFourCc),
61}
62
63impl<'de> serde::Deserialize<'de> for OnMetaDataVideoCodecId {
64    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
65    where
66        D: serde::Deserializer<'de>,
67    {
68        let n: u32 = serde::Deserialize::deserialize(deserializer)?;
69
70        // Since VideoCodecId is a u8, we can be sure that the number represents an VideoFourCc if it is greater
71        // than u8::MAX.
72        // Additionally, since the smallest possible VideoFourCc (4 spaces) is greater than u8::MAX,
73        // we can be sure that the number cannot represent an VideoFourCc if it is smaller than u8::MAX.
74        if n > u8::MAX as u32 {
75            Ok(Self::Enhanced(VideoFourCc::from(n.to_be_bytes())))
76        } else {
77            Ok(Self::Legacy(VideoCodecId::from(n as u8)))
78        }
79    }
80}
81
82/// FLV `onMetaData` script data
83///
84/// Defined by:
85/// - Legacy FLV spec, Annex E.5
86/// - Enhanced RTMP spec, page 13-16, Enhancing onMetaData
87#[derive(Debug, Clone, PartialEq, Deserialize)]
88#[serde(rename_all = "camelCase", bound = "'a: 'de")]
89pub struct OnMetaData<'a> {
90    /// Audio codec ID used in the file.
91    #[serde(default)]
92    pub audiocodecid: Option<OnMetaDataAudioCodecId>,
93    /// Audio bitrate, in kilobits per second.
94    #[serde(default)]
95    pub audiodatarate: Option<f64>,
96    /// Delay introduced by the audio codec, in seconds.
97    #[serde(default)]
98    pub audiodelay: Option<f64>,
99    /// Frequency at which the audio stream is replayed.
100    #[serde(default)]
101    pub audiosamplerate: Option<f64>,
102    /// Resolution of a single audio sample.
103    #[serde(default)]
104    pub audiosamplesize: Option<f64>,
105    /// Indicating the last video frame is a key frame.
106    #[serde(default)]
107    pub can_seek_to_end: Option<bool>,
108    /// Creation date and time.
109    #[serde(default)]
110    pub creationdate: Option<String>,
111    /// Total duration of the file, in seconds.
112    #[serde(default)]
113    pub duration: Option<f64>,
114    /// Total size of the file, in bytes.
115    #[serde(default)]
116    pub filesize: Option<f64>,
117    /// Number of frames per second.
118    #[serde(default)]
119    pub framerate: Option<f64>,
120    /// Height of the video, in pixels.
121    #[serde(default)]
122    pub height: Option<f64>,
123    /// Indicates stereo audio.
124    #[serde(default)]
125    pub stereo: Option<bool>,
126    /// Video codec ID used in the file.
127    #[serde(default)]
128    pub videocodecid: Option<OnMetaDataVideoCodecId>,
129    /// Video bitrate, in kilobits per second.
130    #[serde(default)]
131    pub videodatarate: Option<f64>,
132    /// Width of the video, in pixels.
133    #[serde(default)]
134    pub width: Option<f64>,
135    /// The audioTrackIdInfoMap and videoTrackIdInfoMap objects are designed to store
136    /// metadata for audio and video tracks respectively. Each object uses a TrackId as
137    /// a key to map to properties that detail the unique characteristics of each
138    /// individual track, diverging from the default configurations.
139    ///
140    /// Key-Value Structure:
141    /// - Keys: Each TrackId acts as a unique identifier for a specific audio or video track.
142    /// - Values: Track Objects containing metadata that specify characteristics which deviate from the default track settings.
143    ///
144    /// Properties of Each Track Object:
145    /// - These properties detail non-standard configurations needed for
146    ///   custom handling of the track, facilitating specific adjustments
147    ///   to enhance track performance and quality for varied conditions.
148    /// - For videoTrackIdInfoMap:
149    ///   - Properties such as width, height, videodatarate, etc.
150    ///     specify video characteristics that differ from standard
151    ///     settings.
152    /// - For audioTrackIdInfoMap:
153    ///   - Properties such as audiodatarate, channels, etc., define
154    ///     audio characteristics that differ from standard
155    ///     configurations.
156    ///
157    /// Purpose:
158    /// - The purpose of these maps is to specify unique properties for
159    ///   each track, ensuring tailored configurations that optimize
160    ///   performance and quality for specific media content and delivery
161    ///   scenarios.
162    ///
163    /// This structure provides a framework for detailed customization and control over
164    /// the media tracks, ensuring optimal management and delivery across various types
165    /// of content and platforms.
166    #[serde(default, borrow)]
167    pub audio_track_id_info_map: Option<Amf0Object<'a>>,
168    /// See [`OnMetaData::audio_track_id_info_map`].
169    #[serde(default, borrow)]
170    pub video_track_id_info_map: Option<Amf0Object<'a>>,
171    /// Any other metadata contained in the script data.
172    #[serde(flatten, borrow)]
173    pub other: Amf0Object<'a>,
174}
175
176/// XMP Metadata
177///
178/// Defined by:
179/// - Legacy FLV spec, Annex E.6
180#[derive(Debug, Clone, PartialEq, Deserialize)]
181#[serde(rename_all = "camelCase", bound = "'a: 'de")]
182pub struct OnXmpData<'a> {
183    /// XMP metadata, formatted according to the XMP metadata specification.
184    ///
185    /// For further details, see [www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart3.pdf](https://web.archive.org/web/20090306165322/https://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart3.pdf).
186    #[serde(default, rename = "liveXML")]
187    live_xml: Option<StringCow<'a>>,
188    /// Any other metadata contained in the script data.
189    #[serde(flatten, borrow)]
190    other: Amf0Object<'a>,
191}
192
193/// FLV `SCRIPTDATA` tag
194///
195/// Defined by:
196/// - Legacy FLV spec, Annex E.4.4.1
197#[derive(Debug, Clone, PartialEq)]
198pub enum ScriptData<'a> {
199    /// `onMetaData` script data.
200    ///
201    /// Boxed because it's so big.
202    OnMetaData(Box<OnMetaData<'a>>),
203    /// `onXMPData` script data.
204    OnXmpData(OnXmpData<'a>),
205    /// Any other script data.
206    Other {
207        /// The name of the script data.
208        name: StringCow<'a>,
209        /// The data of the script data.
210        data: Vec<Amf0Value<'static>>,
211    },
212}
213
214impl<'de> serde::Deserialize<'de> for ScriptData<'de> {
215    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
216    where
217        D: serde::Deserializer<'de>,
218    {
219        struct Visitor;
220
221        const SCRIPT_DATA: &str = "ScriptData";
222        const ON_META_DATA: &str = "onMetaData";
223        const ON_XMP_DATA: &str = "onXMPData";
224
225        impl<'de> serde::de::Visitor<'de> for Visitor {
226            type Value = ScriptData<'de>;
227
228            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
229                formatter.write_str(SCRIPT_DATA)
230            }
231
232            fn visit_enum<A>(self, data: A) -> Result<Self::Value, A::Error>
233            where
234                A: serde::de::EnumAccess<'de>,
235            {
236                let (name, content): (StringCow<'de>, A::Variant) = data.variant()?;
237
238                match name.as_ref() {
239                    ON_META_DATA => Ok(ScriptData::OnMetaData(Box::new(content.newtype_variant()?))),
240                    ON_XMP_DATA => Ok(ScriptData::OnXmpData(content.newtype_variant()?)),
241                    _ => Ok(ScriptData::Other {
242                        name,
243                        data: content
244                            .newtype_variant::<MultiValue<Vec<Amf0Value>>>()?
245                            .0
246                            .into_iter()
247                            .map(|v| v.into_owned())
248                            .collect(),
249                    }),
250                }
251            }
252        }
253
254        deserializer.deserialize_enum(SCRIPT_DATA, &[ON_META_DATA, ON_XMP_DATA], Visitor)
255    }
256}
257
258impl ScriptData<'_> {
259    /// Demux the [`ScriptData`] from the given reader.
260    pub fn demux(reader: &mut io::Cursor<Bytes>) -> Result<Self, FlvError> {
261        let buf = reader.extract_remaining();
262        let mut decoder = Amf0Decoder::from_buf(buf);
263
264        serde::de::Deserialize::deserialize(&mut decoder).map_err(FlvError::Amf0)
265    }
266}
267
268#[cfg(test)]
269#[cfg_attr(all(test, coverage_nightly), coverage(off))]
270mod tests {
271    use scuffle_amf0::Amf0Marker;
272    use scuffle_amf0::encoder::Amf0Encoder;
273
274    use super::*;
275
276    #[test]
277    fn script_on_meta_data() {
278        let width = 1280.0f64.to_be_bytes();
279        #[rustfmt::skip]
280        let data = [
281            Amf0Marker::String as u8,
282            0, 10, // Length (10 bytes)
283            b'o', b'n', b'M', b'e', b't', b'a', b'D', b'a', b't', b'a',// "onMetaData"
284            Amf0Marker::Object as u8,
285            0, 5, // Length (5 bytes)
286            b'w', b'i', b'd', b't', b'h', // "width"
287            Amf0Marker::Number as u8,
288            width[0],
289            width[1],
290            width[2],
291            width[3],
292            width[4],
293            width[5],
294            width[6],
295            width[7],
296            0, 0, Amf0Marker::ObjectEnd as u8,
297        ];
298
299        let mut reader = io::Cursor::new(Bytes::from_owner(data));
300
301        let script_data = ScriptData::demux(&mut reader).unwrap();
302
303        let ScriptData::OnMetaData(metadata) = script_data else {
304            panic!("expected onMetaData");
305        };
306
307        assert_eq!(
308            *metadata,
309            OnMetaData {
310                audiocodecid: None,
311                audiodatarate: None,
312                audiodelay: None,
313                audiosamplerate: None,
314                audiosamplesize: None,
315                can_seek_to_end: None,
316                creationdate: None,
317                duration: None,
318                filesize: None,
319                framerate: None,
320                height: None,
321                stereo: None,
322                videocodecid: None,
323                videodatarate: None,
324                width: Some(1280.0),
325                audio_track_id_info_map: None,
326                video_track_id_info_map: None,
327                other: Amf0Object::new(),
328            }
329        );
330    }
331
332    #[test]
333    fn script_on_meta_data_full() {
334        let mut data = Vec::new();
335        let mut encoder = Amf0Encoder::new(&mut data);
336
337        let audio_track_id_info_map = [("test".into(), Amf0Value::Number(1.0))].into_iter().collect();
338        let video_track_id_info_map = [("test2".into(), Amf0Value::Number(2.0))].into_iter().collect();
339
340        encoder.encode_string("onMetaData").unwrap();
341        let object: Amf0Object = [
342            (
343                "audiocodecid".into(),
344                Amf0Value::Number(u32::from_be_bytes(AudioFourCc::Aac.0) as f64),
345            ),
346            ("audiodatarate".into(), Amf0Value::Number(128.0)),
347            ("audiodelay".into(), Amf0Value::Number(0.0)),
348            ("audiosamplerate".into(), Amf0Value::Number(44100.0)),
349            ("audiosamplesize".into(), Amf0Value::Number(16.0)),
350            ("canSeekToEnd".into(), Amf0Value::Boolean(true)),
351            ("creationdate".into(), Amf0Value::String("2025-01-01T00:00:00Z".into())),
352            ("duration".into(), Amf0Value::Number(60.0)),
353            ("filesize".into(), Amf0Value::Number(1024.0)),
354            ("framerate".into(), Amf0Value::Number(30.0)),
355            ("height".into(), Amf0Value::Number(720.0)),
356            ("stereo".into(), Amf0Value::Boolean(true)),
357            (
358                "videocodecid".into(),
359                Amf0Value::Number(u32::from_be_bytes(VideoFourCc::Avc.0) as f64),
360            ),
361            ("videodatarate".into(), Amf0Value::Number(1024.0)),
362            ("width".into(), Amf0Value::Number(1280.0)),
363            ("audioTrackIdInfoMap".into(), Amf0Value::Object(audio_track_id_info_map)),
364            ("videoTrackIdInfoMap".into(), Amf0Value::Object(video_track_id_info_map)),
365        ]
366        .into_iter()
367        .collect();
368        encoder.encode_object(&object).unwrap();
369
370        let mut reader = io::Cursor::new(Bytes::from_owner(data));
371        let script_data = ScriptData::demux(&mut reader).unwrap();
372
373        let ScriptData::OnMetaData(metadata) = script_data else {
374            panic!("expected onMetaData");
375        };
376
377        assert_eq!(
378            *metadata,
379            OnMetaData {
380                audiocodecid: Some(OnMetaDataAudioCodecId::Enhanced(AudioFourCc::Aac)),
381                audiodatarate: Some(128.0),
382                audiodelay: Some(0.0),
383                audiosamplerate: Some(44100.0),
384                audiosamplesize: Some(16.0),
385                can_seek_to_end: Some(true),
386                creationdate: Some("2025-01-01T00:00:00Z".to_string()),
387                duration: Some(60.0),
388                filesize: Some(1024.0),
389                framerate: Some(30.0),
390                height: Some(720.0),
391                stereo: Some(true),
392                videocodecid: Some(OnMetaDataVideoCodecId::Enhanced(VideoFourCc::Avc)),
393                videodatarate: Some(1024.0),
394                width: Some(1280.0),
395                audio_track_id_info_map: Some([("test".into(), Amf0Value::Number(1.0))].into_iter().collect()),
396                video_track_id_info_map: Some([("test2".into(), Amf0Value::Number(2.0))].into_iter().collect()),
397                other: Amf0Object::new(),
398            }
399        );
400    }
401
402    #[test]
403    fn script_on_xmp_data() {
404        #[rustfmt::skip]
405        let data = [
406            Amf0Marker::String as u8,
407            0, 9, // Length (9 bytes)
408            b'o', b'n', b'X', b'M', b'P', b'D', b'a', b't', b'a',// "onXMPData"
409            Amf0Marker::Object as u8,
410            0, 7, // Length (7 bytes)
411            b'l', b'i', b'v', b'e', b'X', b'M', b'L', // "liveXML"
412            Amf0Marker::String as u8,
413            0, 5, // Length (5 bytes)
414            b'h', b'e', b'l', b'l', b'o', // "hello"
415            0, 4, // Length (7 bytes)
416            b't', b'e', b's', b't', // "test"
417            Amf0Marker::Null as u8,
418            0, 0, Amf0Marker::ObjectEnd as u8,
419        ];
420
421        let mut reader = io::Cursor::new(Bytes::from_owner(data));
422
423        let script_data = ScriptData::demux(&mut reader).unwrap();
424
425        let ScriptData::OnXmpData(xmp_data) = script_data else {
426            panic!("expected onXMPData");
427        };
428
429        assert_eq!(
430            xmp_data,
431            OnXmpData {
432                live_xml: Some("hello".into()),
433                other: [("test".into(), Amf0Value::Null)].into_iter().collect(),
434            }
435        );
436    }
437
438    #[test]
439    fn script_other() {
440        #[rustfmt::skip]
441        let data = [
442            Amf0Marker::String as u8,
443            0, 10, // Length (10 bytes)
444            b'o', b'n', b'W', b'h', b'a', b't', b'e', b'v', b'e', b'r',// "onWhatever"
445            Amf0Marker::Object as u8,
446            0, 4, // Length (4 bytes)
447            b't', b'e', b's', b't', // "test"
448            Amf0Marker::String as u8,
449            0, 5, // Length (5 bytes)
450            b'h', b'e', b'l', b'l', b'o', // "hello"
451            0, 0, Amf0Marker::ObjectEnd as u8,
452        ];
453
454        let mut reader = io::Cursor::new(Bytes::from_owner(data));
455
456        let script_data = ScriptData::demux(&mut reader).unwrap();
457
458        let ScriptData::Other { name, data } = script_data else {
459            panic!("expected onXMPData");
460        };
461
462        let object: Amf0Object = [("test".into(), Amf0Value::String("hello".into()))].into_iter().collect();
463
464        assert_eq!(name, "onWhatever");
465        assert_eq!(data.len(), 1);
466        assert_eq!(data[0], Amf0Value::Object(object));
467    }
468}