From 6b74060b9a9587275a6a8938418666885bfa7f32 Mon Sep 17 00:00:00 2001 From: Troy Benson Date: Fri, 10 Jan 2025 17:07:09 +0000 Subject: [PATCH 1/6] cleanup amf0 crate Cleans up the AMF0 decoder / encoder crate and adds some documentation around its usage. --- Cargo.lock | 31 +- Cargo.toml | 1 + crates/amf0/Cargo.toml | 9 +- crates/amf0/LICENSE.Apache-2.0 | 1 + crates/amf0/LICENSE.MIT | 1 + crates/amf0/README.md | 17 ++ crates/amf0/src/decode.rs | 275 ++++++++++++++++++ crates/amf0/src/define.rs | 135 ++++++++- crates/amf0/src/encode.rs | 144 +++++++++ crates/amf0/src/errors.rs | 124 +++++--- crates/amf0/src/lib.rs | 41 ++- crates/amf0/src/reader.rs | 173 ----------- crates/amf0/src/tests.rs | 203 ------------- crates/amf0/src/writer.rs | 66 ----- crates/flv/Cargo.toml | 2 +- crates/flv/src/define.rs | 4 +- crates/flv/src/errors.rs | 6 +- crates/flv/src/flv.rs | 9 +- crates/flv/src/tests/demuxer.rs | 157 +++++----- crates/flv/src/tests/error.rs | 2 +- crates/rtmp/Cargo.toml | 2 +- crates/rtmp/src/messages/define.rs | 12 +- crates/rtmp/src/messages/errors.rs | 2 +- crates/rtmp/src/messages/parser.rs | 30 +- crates/rtmp/src/messages/tests.rs | 36 +-- crates/rtmp/src/netconnection/errors.rs | 2 +- crates/rtmp/src/netconnection/tests.rs | 32 +- crates/rtmp/src/netconnection/writer.rs | 39 ++- crates/rtmp/src/netstream/errors.rs | 2 +- crates/rtmp/src/netstream/tests.rs | 23 +- crates/rtmp/src/netstream/writer.rs | 21 +- .../src/protocol_control_messages/reader.rs | 4 +- .../src/protocol_control_messages/tests.rs | 2 +- crates/rtmp/src/session/server_session.rs | 53 ++-- crates/rtmp/src/session/tests.rs | 12 +- crates/transmuxer/Cargo.toml | 2 +- crates/transmuxer/src/lib.rs | 31 +- 37 files changed, 959 insertions(+), 747 deletions(-) create mode 120000 crates/amf0/LICENSE.Apache-2.0 create mode 120000 crates/amf0/LICENSE.MIT create mode 100644 crates/amf0/README.md create mode 100644 crates/amf0/src/decode.rs create mode 100644 crates/amf0/src/encode.rs delete mode 100644 crates/amf0/src/reader.rs delete mode 100644 crates/amf0/src/tests.rs delete mode 100644 crates/amf0/src/writer.rs diff --git a/Cargo.lock b/Cargo.lock index 36db70c38..aa34d8f09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -53,18 +53,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "amf0" -version = "0.0.1" -dependencies = [ - "byteorder", - "bytes", - "num-derive", - "num-traits", - "scuffle-bytes-util", - "scuffle-workspace-hack", -] - [[package]] name = "android-tzdata" version = "0.1.1" @@ -941,7 +929,6 @@ dependencies = [ name = "flv" version = "0.0.1" dependencies = [ - "amf0", "av1", "byteorder", "bytes", @@ -950,6 +937,7 @@ dependencies = [ "num-derive", "num-traits", "scuffle-aac", + "scuffle-amf0", "scuffle-bytes-util", "scuffle-workspace-hack", ] @@ -2263,7 +2251,6 @@ dependencies = [ name = "rtmp" version = "0.0.1" dependencies = [ - "amf0", "async-trait", "byteorder", "bytes", @@ -2273,6 +2260,7 @@ dependencies = [ "num-derive", "num-traits", "rand", + "scuffle-amf0", "scuffle-bytes-util", "scuffle-future-ext", "scuffle-workspace-hack", @@ -2458,6 +2446,19 @@ dependencies = [ "scuffle-workspace-hack", ] +[[package]] +name = "scuffle-amf0" +version = "0.0.1" +dependencies = [ + "byteorder", + "bytes", + "num-derive", + "num-traits", + "scuffle-bytes-util", + "scuffle-workspace-hack", + "thiserror 2.0.7", +] + [[package]] name = "scuffle-batching" version = "0.0.4" @@ -3327,7 +3328,6 @@ dependencies = [ name = "transmuxer" version = "0.0.1" dependencies = [ - "amf0", "av1", "byteorder", "bytes", @@ -3336,6 +3336,7 @@ dependencies = [ "h265", "mp4", "scuffle-aac", + "scuffle-amf0", "scuffle-bytes-util", "scuffle-workspace-hack", "serde", diff --git a/Cargo.toml b/Cargo.toml index c13d2c19f..909b72d75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ scuffle-ffmpeg-sys = { path = "crates/ffmpeg-sys", version = "7.1.0" } scuffle-future-ext = { path = "crates/future-ext", version = "0.0.1" } scuffle-bytes-util = { path = "crates/bytes-util", version = "0.0.1" } scuffle-expgolomb = { path = "crates/expgolomb", version = "0.0.1" } +scuffle-amf0 = { path = "crates/amf0", version = "0.0.1" } [profile.release-debug] inherits = "release" diff --git a/crates/amf0/Cargo.toml b/crates/amf0/Cargo.toml index f3f2b893b..1bd102d66 100644 --- a/crates/amf0/Cargo.toml +++ b/crates/amf0/Cargo.toml @@ -1,8 +1,14 @@ [package] -name = "amf0" +name = "scuffle-amf0" version = "0.0.1" edition = "2021" license = "MIT OR Apache-2.0" +description = "A pure-rust implementation of AMF0 encoder and decoder." +repository = "https://github.com/scufflecloud/scuffle" +keywords = ["amf0", "rtmp", "flash", "video", "flv"] + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } [dependencies] bytes = "1.5" @@ -10,4 +16,5 @@ byteorder = "1.5" num-traits = "0.2" num-derive = "0.4" scuffle-bytes-util.workspace = true +thiserror = "2" scuffle-workspace-hack.workspace = true diff --git a/crates/amf0/LICENSE.Apache-2.0 b/crates/amf0/LICENSE.Apache-2.0 new file mode 120000 index 000000000..5a4558f07 --- /dev/null +++ b/crates/amf0/LICENSE.Apache-2.0 @@ -0,0 +1 @@ +../../LICENSE.Apache-2.0 \ No newline at end of file diff --git a/crates/amf0/LICENSE.MIT b/crates/amf0/LICENSE.MIT new file mode 120000 index 000000000..244dbbf0b --- /dev/null +++ b/crates/amf0/LICENSE.MIT @@ -0,0 +1 @@ +../../LICENSE.MIT \ No newline at end of file diff --git a/crates/amf0/README.md b/crates/amf0/README.md new file mode 100644 index 000000000..39169d3ed --- /dev/null +++ b/crates/amf0/README.md @@ -0,0 +1,17 @@ +# scuffle-amf0 + +> [!WARNING] +> This crate is under active development and may not be stable. + +[![crates.io](https://img.shields.io/crates/v/scuffle-amf0.svg)](https://crates.io/crates/scuffle-amf0) [![docs.rs](https://img.shields.io/docsrs/scuffle-amf0)](https://docs.rs/scuffle-amf0) + +--- + +A pure-rust implementation of AMF0 encoder and decoder. + +## License + +This project is licensed under the [MIT](./LICENSE.MIT) or [Apache-2.0](./LICENSE.Apache-2.0) license. +You can choose between one of them if you use this work. + +`SPDX-License-Identifier: MIT OR Apache-2.0` diff --git a/crates/amf0/src/decode.rs b/crates/amf0/src/decode.rs new file mode 100644 index 000000000..8758af89d --- /dev/null +++ b/crates/amf0/src/decode.rs @@ -0,0 +1,275 @@ +use std::borrow::Cow; +use std::io::{Cursor, Seek, SeekFrom}; + +use byteorder::{BigEndian, ReadBytesExt}; +use num_traits::FromPrimitive; + +use super::{Amf0Marker, Amf0ReadError, Amf0Value}; + +/// An AMF0 Decoder. +/// This decoder takes a reference to a byte slice and reads the AMF0 data from +/// it. All returned objects are references to the original byte slice. Making +/// it very cheap to use. +pub struct Amf0Decoder<'a> { + cursor: Cursor<&'a [u8]>, +} + +impl<'a> Amf0Decoder<'a> { + /// Create a new AMF0 decoder. + pub fn new(buff: &'a [u8]) -> Self { + Self { + cursor: Cursor::new(buff), + } + } + + /// Check if the decoder has reached the end of the AMF0 data. + pub fn is_empty(&self) -> bool { + self.cursor.get_ref().len() == self.cursor.position() as usize + } + + fn read_bytes(&mut self, len: usize) -> Result<&'a [u8], Amf0ReadError> { + let pos = self.cursor.position(); + self.cursor.seek(SeekFrom::Current(len as i64))?; + Ok(&self.cursor.get_ref()[pos as usize..pos as usize + len]) + } + + /// Read all the encoded values from the decoder. + pub fn decode_all(&mut self) -> Result>, Amf0ReadError> { + let mut results = vec![]; + + while !self.is_empty() { + results.push(self.decode()?); + } + + Ok(results) + } + + /// Read the next encoded value from the decoder. + pub fn decode(&mut self) -> Result, Amf0ReadError> { + let marker = self.cursor.read_u8()?; + let marker = Amf0Marker::from_u8(marker).ok_or(Amf0ReadError::UnknownMarker(marker))?; + + match marker { + Amf0Marker::Number => Ok(Amf0Value::Number(self.read_number()?)), + Amf0Marker::Boolean => Ok(Amf0Value::Boolean(self.read_bool()?)), + Amf0Marker::String => Ok(Amf0Value::String(self.read_string()?)), + Amf0Marker::Object => Ok(Amf0Value::Object(self.read_object()?.into())), + Amf0Marker::Null => Ok(Amf0Value::Null), + Amf0Marker::EcmaArray => Ok(Amf0Value::Object(self.read_ecma_array()?.into())), + Amf0Marker::LongString => Ok(Amf0Value::LongString(self.read_long_string()?)), + _ => Err(Amf0ReadError::UnsupportedType(marker)), + } + } + + /// Read the next encoded value from the decoder and check if it matches the + /// specified marker. + pub fn decode_with_type(&mut self, specified_marker: Amf0Marker) -> Result, Amf0ReadError> { + let marker = self.cursor.read_u8()?; + self.cursor.seek(SeekFrom::Current(-1))?; // seek back to the original position + + let marker = Amf0Marker::from_u8(marker).ok_or(Amf0ReadError::UnknownMarker(marker))?; + if marker != specified_marker { + return Err(Amf0ReadError::WrongType(specified_marker, marker)); + } + + self.decode() + } + + fn read_number(&mut self) -> Result { + Ok(self.cursor.read_f64::()?) + } + + fn read_bool(&mut self) -> Result { + Ok(self.cursor.read_u8()? == 1) + } + + fn read_string(&mut self) -> Result, Amf0ReadError> { + let l = self.cursor.read_u16::()?; + let bytes = self.read_bytes(l as usize)?; + + Ok(Cow::Borrowed(std::str::from_utf8(bytes)?)) + } + + fn is_read_object_eof(&mut self) -> Result { + let pos = self.cursor.position(); + let marker = self.cursor.read_u24::(); + self.cursor.seek(SeekFrom::Start(pos))?; + + match Amf0Marker::from_u32(marker?) { + Some(Amf0Marker::ObjectEnd) => { + self.cursor.read_u24::()?; + Ok(true) + } + _ => Ok(false), + } + } + + fn read_object(&mut self) -> Result, Amf0Value<'a>)>, Amf0ReadError> { + let mut properties = Vec::new(); + + loop { + let is_eof = self.is_read_object_eof()?; + + if is_eof { + break; + } + + let key = self.read_string()?; + let val = self.decode()?; + + properties.push((key, val)); + } + + Ok(properties) + } + + fn read_ecma_array(&mut self) -> Result, Amf0Value<'a>)>, Amf0ReadError> { + let len = self.cursor.read_u32::()?; + + let mut properties = Vec::new(); + + for _ in 0..len { + let key = self.read_string()?; + let val = self.decode()?; + properties.push((key, val)); + } + + // Sometimes the object end marker is present and sometimes it is not. + // If it is there just read it, if not then we are done. + self.is_read_object_eof().ok(); // ignore the result + + Ok(properties) + } + + fn read_long_string(&mut self) -> Result, Amf0ReadError> { + let l = self.cursor.read_u32::()?; + + let buff = self.read_bytes(l as usize)?; + let val = std::str::from_utf8(buff)?; + + Ok(Cow::Borrowed(val)) + } +} + +impl<'a> Iterator for Amf0Decoder<'a> { + type Item = Result, Amf0ReadError>; + + fn next(&mut self) -> Option { + if self.is_empty() { + return None; + } + + Some(self.decode()) + } +} + +#[cfg(test)] +#[cfg_attr(all(test, coverage_nightly), coverage(off))] +mod tests { + use super::*; + + #[test] + fn test_reader_bool() { + let amf0_bool = vec![0x01, 0x01]; // true + let mut amf_reader = Amf0Decoder::new(&amf0_bool); + let value = amf_reader.decode_with_type(Amf0Marker::Boolean).unwrap(); + assert_eq!(value, Amf0Value::Boolean(true)); + } + + #[test] + fn test_reader_number() { + let mut amf0_number = vec![0x00]; + amf0_number.extend_from_slice(&772.161_f64.to_be_bytes()); + + let mut amf_reader = Amf0Decoder::new(&amf0_number); + let value = amf_reader.decode_with_type(Amf0Marker::Number).unwrap(); + assert_eq!(value, Amf0Value::Number(772.161)); + } + + #[test] + fn test_reader_string() { + let mut amf0_string = vec![0x02, 0x00, 0x0b]; // 11 bytes + amf0_string.extend_from_slice(b"Hello World"); + + let mut amf_reader = Amf0Decoder::new(&amf0_string); + let value = amf_reader.decode_with_type(Amf0Marker::String).unwrap(); + assert_eq!(value, Amf0Value::String(Cow::Borrowed("Hello World"))); + } + + #[test] + fn test_reader_long_string() { + let mut amf0_string = vec![0x0c, 0x00, 0x00, 0x00, 0x0b]; // 11 bytes + amf0_string.extend_from_slice(b"Hello World"); + + let mut amf_reader = Amf0Decoder::new(&amf0_string); + let value = amf_reader.decode_with_type(Amf0Marker::LongString).unwrap(); + assert_eq!(value, Amf0Value::LongString(Cow::Borrowed("Hello World"))); + } + + #[test] + fn test_reader_object() { + let mut amf0_object = vec![0x03, 0x00, 0x04]; // 1 property with 4 bytes + amf0_object.extend_from_slice(b"test"); + amf0_object.extend_from_slice(&[0x05]); // null + amf0_object.extend_from_slice(&[0x00, 0x00, 0x09]); // object end (0x00 0x00 0x09) + + let mut amf_reader = Amf0Decoder::new(&amf0_object); + let value = amf_reader.decode_with_type(Amf0Marker::Object).unwrap(); + + assert_eq!(value, Amf0Value::Object(vec![("test".into(), Amf0Value::Null)].into())); + } + + #[test] + fn test_reader_ecma_array() { + let mut amf0_object = vec![0x08, 0x00, 0x00, 0x00, 0x01]; // 1 property + amf0_object.extend_from_slice(&[0x00, 0x04]); // 4 bytes + amf0_object.extend_from_slice(b"test"); + amf0_object.extend_from_slice(&[0x05]); // null + + let mut amf_reader = Amf0Decoder::new(&amf0_object); + let value = amf_reader.decode_with_type(Amf0Marker::EcmaArray).unwrap(); + + assert_eq!(value, Amf0Value::Object(vec![("test".into(), Amf0Value::Null)].into())); + } + + #[test] + fn test_reader_multi_value() { + let mut amf0_multi = vec![0x00]; + amf0_multi.extend_from_slice(&772.161_f64.to_be_bytes()); + amf0_multi.extend_from_slice(&[0x01, 0x01]); // true + amf0_multi.extend_from_slice(&[0x02, 0x00, 0x0b]); // 11 bytes + amf0_multi.extend_from_slice(b"Hello World"); + amf0_multi.extend_from_slice(&[0x03, 0x00, 0x04]); // 1 property with 4 bytes + amf0_multi.extend_from_slice(b"test"); + amf0_multi.extend_from_slice(&[0x05]); // null + amf0_multi.extend_from_slice(&[0x00, 0x00, 0x09]); // object end (0x00 0x00 0x09) + + let mut amf_reader = Amf0Decoder::new(&amf0_multi); + let values = amf_reader.decode_all().unwrap(); + + assert_eq!(values.len(), 4); + + assert_eq!(values[0], Amf0Value::Number(772.161)); + assert_eq!(values[1], Amf0Value::Boolean(true)); + assert_eq!(values[2], Amf0Value::String(Cow::Borrowed("Hello World"))); + assert_eq!(values[3], Amf0Value::Object(vec![("test".into(), Amf0Value::Null)].into())); + } + + #[test] + fn test_reader_iterator() { + let mut amf0_multi = vec![0x00]; + amf0_multi.extend_from_slice(&772.161_f64.to_be_bytes()); + amf0_multi.extend_from_slice(&[0x01, 0x01]); // true + amf0_multi.extend_from_slice(&[0x02, 0x00, 0x0b]); // 11 bytes + amf0_multi.extend_from_slice(b"Hello World"); + + let amf_reader = Amf0Decoder::new(&amf0_multi); + let values = amf_reader.collect::, _>>().unwrap(); + + assert_eq!(values.len(), 3); + + assert_eq!(values[0], Amf0Value::Number(772.161)); + assert_eq!(values[1], Amf0Value::Boolean(true)); + assert_eq!(values[2], Amf0Value::String(Cow::Borrowed("Hello World"))); + } +} diff --git a/crates/amf0/src/define.rs b/crates/amf0/src/define.rs index 756140f65..88827e553 100644 --- a/crates/amf0/src/define.rs +++ b/crates/amf0/src/define.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::borrow::Cow; use num_derive::FromPrimitive; @@ -27,20 +27,145 @@ pub enum Amf0Marker { AVMPlusObject = 0x11, // AMF3 marker } +/// AMF0 value types. +/// Defined in amf0_spec_121207.pdf section 2.2-2.14 #[derive(PartialEq, Clone, Debug)] -pub enum Amf0Value { +pub enum Amf0Value<'a> { /// Number Type defined section 2.2 Number(f64), /// Boolean Type defined section 2.3 Boolean(bool), /// String Type defined section 2.4 - String(String), + String(Cow<'a, str>), /// Object Type defined section 2.5 - Object(HashMap), + Object(Cow<'a, [(Cow<'a, str>, Amf0Value<'a>)]>), /// Null Type defined section 2.7 Null, /// Undefined Type defined section 2.8 ObjectEnd, /// LongString Type defined section 2.14 - LongString(String), + LongString(Cow<'a, str>), +} + +impl Amf0Value<'_> { + /// Get the marker of the value. + pub fn marker(&self) -> Amf0Marker { + match self { + Self::Boolean(_) => Amf0Marker::Boolean, + Self::Number(_) => Amf0Marker::Number, + Self::String(_) => Amf0Marker::String, + Self::Object(_) => Amf0Marker::Object, + Self::Null => Amf0Marker::Null, + Self::ObjectEnd => Amf0Marker::ObjectEnd, + Self::LongString(_) => Amf0Marker::LongString, + } + } + + /// Get the owned value. + pub fn to_owned(&self) -> Amf0Value<'static> { + match self { + Self::String(s) => Amf0Value::String(Cow::Owned(s.to_string())), + Self::LongString(s) => Amf0Value::LongString(Cow::Owned(s.to_string())), + Self::Object(o) => Amf0Value::Object(o.iter().map(|(k, v)| (Cow::Owned(k.to_string()), v.to_owned())).collect()), + Self::Number(n) => Amf0Value::Number(*n), + Self::Boolean(b) => Amf0Value::Boolean(*b), + Self::Null => Amf0Value::Null, + Self::ObjectEnd => Amf0Value::ObjectEnd, + } + } +} + +#[cfg(test)] +#[cfg_attr(all(test, coverage_nightly), coverage(off))] +mod tests { + use num_traits::FromPrimitive; + + use super::*; + + #[test] + fn test_marker() { + let cases = [ + (Amf0Value::Number(1.0), Amf0Marker::Number), + (Amf0Value::Boolean(true), Amf0Marker::Boolean), + (Amf0Value::String(Cow::Borrowed("test")), Amf0Marker::String), + ( + Amf0Value::Object(Cow::Borrowed(&[(Cow::Borrowed("test"), Amf0Value::Number(1.0))])), + Amf0Marker::Object, + ), + (Amf0Value::Null, Amf0Marker::Null), + (Amf0Value::ObjectEnd, Amf0Marker::ObjectEnd), + (Amf0Value::LongString(Cow::Borrowed("test")), Amf0Marker::LongString), + ]; + + for (value, marker) in cases { + assert_eq!(value.marker(), marker); + } + } + + #[test] + fn test_to_owned() { + let value = Amf0Value::Object(Cow::Borrowed(&[( + Cow::Borrowed("test"), + Amf0Value::LongString(Cow::Borrowed("test")), + )])); + let owned = value.to_owned(); + assert_eq!( + owned, + Amf0Value::Object(Cow::Owned(vec![( + "test".to_string().into(), + Amf0Value::LongString(Cow::Owned("test".to_string())) + )])) + ); + + let value = Amf0Value::String(Cow::Borrowed("test")); + let owned = value.to_owned(); + assert_eq!(owned, Amf0Value::String(Cow::Owned("test".to_string()))); + + let value = Amf0Value::Number(1.0); + let owned = value.to_owned(); + assert_eq!(owned, Amf0Value::Number(1.0)); + + let value = Amf0Value::Boolean(true); + let owned = value.to_owned(); + assert_eq!(owned, Amf0Value::Boolean(true)); + + let value = Amf0Value::Null; + let owned = value.to_owned(); + assert_eq!(owned, Amf0Value::Null); + + let value = Amf0Value::ObjectEnd; + let owned = value.to_owned(); + assert_eq!(owned, Amf0Value::ObjectEnd); + } + + #[test] + fn test_marker_primitive() { + let cases = [ + (Amf0Marker::Number, 0x00), + (Amf0Marker::Boolean, 0x01), + (Amf0Marker::String, 0x02), + (Amf0Marker::Object, 0x03), + (Amf0Marker::MovieClipMarker, 0x04), + (Amf0Marker::Null, 0x05), + (Amf0Marker::Undefined, 0x06), + (Amf0Marker::Reference, 0x07), + (Amf0Marker::EcmaArray, 0x08), + (Amf0Marker::ObjectEnd, 0x09), + (Amf0Marker::StrictArray, 0x0a), + (Amf0Marker::Date, 0x0b), + (Amf0Marker::LongString, 0x0c), + (Amf0Marker::Unsupported, 0x0d), + (Amf0Marker::Recordset, 0x0e), + (Amf0Marker::XmlDocument, 0x0f), + (Amf0Marker::TypedObject, 0x10), + (Amf0Marker::AVMPlusObject, 0x11), + ]; + + for (marker, value) in cases { + assert_eq!(marker as u8, value); + assert_eq!(Amf0Marker::from_u8(value), Some(marker)); + } + + assert!(Amf0Marker::from_u8(0x12).is_none()); + } } diff --git a/crates/amf0/src/encode.rs b/crates/amf0/src/encode.rs new file mode 100644 index 000000000..1004f9538 --- /dev/null +++ b/crates/amf0/src/encode.rs @@ -0,0 +1,144 @@ +use std::borrow::Cow; +use std::io; + +use byteorder::{BigEndian, WriteBytesExt}; + +use super::define::Amf0Marker; +use super::{Amf0Value, Amf0WriteError}; + +/// AMF0 encoder. +/// Allows for encoding an AMF0 to some writer. +pub struct Amf0Encoder; + +impl Amf0Encoder { + /// Encode a generic AMF0 value + pub fn encode(writer: &mut impl io::Write, value: &Amf0Value) -> Result<(), Amf0WriteError> { + match value { + Amf0Value::Boolean(val) => Self::encode_bool(writer, *val), + Amf0Value::Null => Self::encode_null(writer), + Amf0Value::Number(val) => Self::encode_number(writer, *val), + Amf0Value::String(val) => Self::encode_string(writer, val), + Amf0Value::Object(val) => Self::encode_object(writer, val), + _ => Err(Amf0WriteError::UnsupportedType(value.marker())), + } + } + + fn object_eof(writer: &mut impl io::Write) -> Result<(), Amf0WriteError> { + writer.write_u24::(Amf0Marker::ObjectEnd as u32)?; + Ok(()) + } + + /// Encode an AMF0 number + pub fn encode_number(writer: &mut impl io::Write, value: f64) -> Result<(), Amf0WriteError> { + writer.write_u8(Amf0Marker::Number as u8)?; + writer.write_f64::(value)?; + Ok(()) + } + + /// Encode an AMF0 boolean + pub fn encode_bool(writer: &mut impl io::Write, value: bool) -> Result<(), Amf0WriteError> { + writer.write_u8(Amf0Marker::Boolean as u8)?; + writer.write_u8(value as u8)?; + Ok(()) + } + + /// Encode an AMF0 string + pub fn encode_string(writer: &mut impl io::Write, value: &str) -> Result<(), Amf0WriteError> { + if value.len() > (u16::MAX as usize) { + return Err(Amf0WriteError::NormalStringTooLong); + } + + writer.write_u8(Amf0Marker::String as u8)?; + writer.write_u16::(value.len() as u16)?; + writer.write_all(value.as_bytes())?; + Ok(()) + } + + /// Encode an AMF0 null + pub fn encode_null(writer: &mut impl io::Write) -> Result<(), Amf0WriteError> { + writer.write_u8(Amf0Marker::Null as u8)?; + Ok(()) + } + + /// Encode an AMF0 object + pub fn encode_object( + writer: &mut impl io::Write, + properties: &[(Cow<'_, str>, Amf0Value<'_>)], + ) -> Result<(), Amf0WriteError> { + writer.write_u8(Amf0Marker::Object as u8)?; + for (key, value) in properties { + writer.write_u16::(key.len() as u16)?; + writer.write_all(key.as_bytes())?; + Self::encode(writer, value)?; + } + + Self::object_eof(writer)?; + Ok(()) + } +} + +#[cfg(test)] +#[cfg_attr(all(test, coverage_nightly), coverage(off))] +mod tests { + use super::*; + + #[test] + fn test_write_number() { + let mut amf0_number = vec![0x00]; + amf0_number.extend_from_slice(&772.161_f64.to_be_bytes()); + + let mut vec = Vec::::new(); + + Amf0Encoder::encode_number(&mut vec, 772.161).unwrap(); + + assert_eq!(vec, amf0_number); + } + + #[test] + fn test_write_boolean() { + let amf0_boolean = vec![0x01, 0x01]; + + let mut vec = Vec::::new(); + + Amf0Encoder::encode_bool(&mut vec, true).unwrap(); + + assert_eq!(vec, amf0_boolean); + } + + #[test] + fn test_write_string() { + let mut amf0_string = vec![0x02, 0x00, 0x0b]; + amf0_string.extend_from_slice(b"Hello World"); + + let mut vec = Vec::::new(); + + Amf0Encoder::encode_string(&mut vec, "Hello World").unwrap(); + + assert_eq!(vec, amf0_string); + } + + #[test] + fn test_write_null() { + let amf0_null = vec![0x05]; + + let mut vec = Vec::::new(); + + Amf0Encoder::encode_null(&mut vec).unwrap(); + + assert_eq!(vec, amf0_null); + } + + #[test] + fn test_write_object() { + let mut amf0_object = vec![0x03, 0x00, 0x04]; + amf0_object.extend_from_slice(b"test"); + amf0_object.extend_from_slice(&[0x05]); + amf0_object.extend_from_slice(&[0x00, 0x00, 0x09]); + + let mut vec = Vec::::new(); + + Amf0Encoder::encode_object(&mut vec, &[("test".into(), Amf0Value::Null)]).unwrap(); + + assert_eq!(vec, amf0_object); + } +} diff --git a/crates/amf0/src/errors.rs b/crates/amf0/src/errors.rs index 0fddecae0..9396afe26 100644 --- a/crates/amf0/src/errors.rs +++ b/crates/amf0/src/errors.rs @@ -1,65 +1,97 @@ -use std::{fmt, io, str}; +use std::io; use super::define::Amf0Marker; -use super::Amf0Value; -#[derive(Debug)] +/// Errors that can occur when decoding AMF0 data. +#[derive(Debug, thiserror::Error)] pub enum Amf0ReadError { + /// An unknown marker was encountered. + #[error("unknown marker: {0}")] UnknownMarker(u8), + /// An unsupported type was encountered. + #[error("unsupported type: {0:?}")] UnsupportedType(Amf0Marker), - StringParseError(str::Utf8Error), - IO(io::Error), - WrongType, + /// A string parse error occurred. + #[error("string parse error: {0}")] + StringParseError(#[from] std::str::Utf8Error), + /// An IO error occurred. + #[error("io error: {0}")] + Io(#[from] io::Error), + /// A wrong type was encountered. Created when using + /// `Amf0Decoder::next_with_type` and the next value is not the expected + /// type. + #[error("wrong type: expected {0:?}, got {1:?}")] + WrongType(Amf0Marker, Amf0Marker), } -macro_rules! from_error { - ($tt:ty, $val:expr, $err:ty) => { - impl From<$err> for $tt { - fn from(error: $err) -> Self { - $val(error) - } - } - }; -} - -from_error!(Amf0ReadError, Self::StringParseError, str::Utf8Error); -from_error!(Amf0ReadError, Self::IO, io::Error); - -#[derive(Debug)] +/// Errors that can occur when encoding AMF0 data. +#[derive(Debug, thiserror::Error)] pub enum Amf0WriteError { + /// A normal string was too long. + #[error("normal string too long")] NormalStringTooLong, - IO(io::Error), - UnsupportedType(Amf0Value), + /// An IO error occurred. + #[error("io error: {0}")] + Io(#[from] io::Error), + /// An unsupported type was encountered. + #[error("unsupported type: {0:?}")] + UnsupportedType(Amf0Marker), } -from_error!(Amf0WriteError, Self::IO, io::Error); +#[cfg(test)] +#[cfg_attr(all(test, coverage_nightly), coverage(off))] +mod tests { + use byteorder::ReadBytesExt; + use io::Cursor; -impl fmt::Display for Amf0ReadError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Self::UnknownMarker(marker) => { - write!(f, "unknown marker: {}", marker) - } - Self::UnsupportedType(marker) => { - write!(f, "unsupported type: {:?}", marker) - } - Self::WrongType => write!(f, "wrong type"), - Self::StringParseError(err) => write!(f, "string parse error: {}", err), - Self::IO(err) => write!(f, "io error: {}", err), + use super::*; + + #[test] + fn test_read_error_display() { + let cases = [ + (Amf0ReadError::UnknownMarker(100), "unknown marker: 100"), + ( + Amf0ReadError::UnsupportedType(Amf0Marker::Reference), + "unsupported type: Reference", + ), + ( + Amf0ReadError::WrongType(Amf0Marker::Reference, Amf0Marker::Boolean), + "wrong type: expected Reference, got Boolean", + ), + ( + Amf0ReadError::StringParseError( + #[allow(unknown_lints, invalid_from_utf8)] + std::str::from_utf8(b"\xFF\xFF").unwrap_err(), + ), + "string parse error: invalid utf-8 sequence of 1 bytes from index 0", + ), + ( + Amf0ReadError::Io(Cursor::new(Vec::::new()).read_u8().unwrap_err()), + "io error: failed to fill whole buffer", + ), + ]; + + for (err, expected) in cases { + assert_eq!(err.to_string(), expected); } } -} -impl fmt::Display for Amf0WriteError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Self::NormalStringTooLong => { - write!(f, "normal string too long") - } - Self::UnsupportedType(value_type) => { - write!(f, "unsupported type: {:?}", value_type) - } - Self::IO(error) => write!(f, "io error: {}", error), + #[test] + fn test_write_error_display() { + let cases = [ + ( + Amf0WriteError::UnsupportedType(Amf0Marker::ObjectEnd), + "unsupported type: ObjectEnd", + ), + ( + Amf0WriteError::Io(Cursor::new(Vec::::new()).read_u8().unwrap_err()), + "io error: failed to fill whole buffer", + ), + (Amf0WriteError::NormalStringTooLong, "normal string too long"), + ]; + + for (err, expected) in cases { + assert_eq!(err.to_string(), expected); } } } diff --git a/crates/amf0/src/lib.rs b/crates/amf0/src/lib.rs index 080e2a873..4c5e2ffb3 100644 --- a/crates/amf0/src/lib.rs +++ b/crates/amf0/src/lib.rs @@ -1,12 +1,39 @@ +#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))] + +//! A pure-rust implementation of AMF0 encoder and decoder. +//! +//! This crate provides a simple interface for encoding and decoding AMF0 data. +//! +//! # Examples +//! +//! ```rust +//! # fn test() -> Result<(), Box> { +//! use scuffle_amf0::Amf0Decoder; +//! use scuffle_amf0::Amf0Encoder; +//! # let bytes = &[0x01, 0x01]; +//! # let mut writer = Vec::new(); +//! +//! // Create a new decoder +//! let mut reader = Amf0Decoder::new(bytes); +//! let value = reader.decode()?; +//! +//! // .. do something with the value +//! +//! // Encode a value into a writer +//! Amf0Encoder::encode(&mut writer, &value)?; +//! +//! # assert_eq!(writer, bytes); +//! # Ok(()) +//! # } +//! # test().expect("test failed"); +//! ``` + +mod decode; mod define; +mod encode; mod errors; -mod reader; -mod writer; +pub use crate::decode::Amf0Decoder; pub use crate::define::{Amf0Marker, Amf0Value}; +pub use crate::encode::Amf0Encoder; pub use crate::errors::{Amf0ReadError, Amf0WriteError}; -pub use crate::reader::Amf0Reader; -pub use crate::writer::Amf0Writer; - -#[cfg(test)] -mod tests; diff --git a/crates/amf0/src/reader.rs b/crates/amf0/src/reader.rs deleted file mode 100644 index 0bfc0fffe..000000000 --- a/crates/amf0/src/reader.rs +++ /dev/null @@ -1,173 +0,0 @@ -use std::collections::HashMap; -use std::io::{Cursor, Seek, SeekFrom}; - -use byteorder::{BigEndian, ReadBytesExt}; -use bytes::Bytes; -use num_traits::FromPrimitive; - -use super::{Amf0Marker, Amf0ReadError, Amf0Value}; - -pub struct Amf0Reader { - cursor: Cursor, -} - -impl Amf0Reader { - pub fn new(buff: Bytes) -> Self { - Self { - cursor: Cursor::new(buff), - } - } - - fn is_empty(&self) -> bool { - self.cursor.get_ref().len() == self.cursor.position() as usize - } - - fn read_bytes(&mut self, len: usize) -> Result { - let pos = self.cursor.position(); - self.cursor.seek(SeekFrom::Current(len as i64))?; - Ok(self.cursor.get_ref().slice(pos as usize..pos as usize + len)) - } - - pub fn read_all(&mut self) -> Result, Amf0ReadError> { - let mut results = vec![]; - - loop { - let result = self.read_any()?; - - match result { - Amf0Value::ObjectEnd => { - break; - } - _ => { - results.push(result); - } - } - } - - Ok(results) - } - - pub fn read_any(&mut self) -> Result { - if self.is_empty() { - return Ok(Amf0Value::ObjectEnd); - } - - let marker = self.cursor.read_u8()?; - let marker = Amf0Marker::from_u8(marker).ok_or(Amf0ReadError::UnknownMarker(marker))?; - - match marker { - Amf0Marker::Number => self.read_number(), - Amf0Marker::Boolean => self.read_bool(), - Amf0Marker::String => self.read_string(), - Amf0Marker::Object => self.read_object(), - Amf0Marker::Null => self.read_null(), - Amf0Marker::EcmaArray => self.read_ecma_array(), - Amf0Marker::LongString => self.read_long_string(), - _ => Err(Amf0ReadError::UnsupportedType(marker)), - } - } - - pub fn read_with_type(&mut self, specified_marker: Amf0Marker) -> Result { - let marker = self.cursor.read_u8()?; - self.cursor.seek(SeekFrom::Current(-1))?; // seek back to the original position - - let marker = Amf0Marker::from_u8(marker).ok_or(Amf0ReadError::UnknownMarker(marker))?; - if marker != specified_marker { - return Err(Amf0ReadError::WrongType); - } - - self.read_any() - } - - pub fn read_number(&mut self) -> Result { - let number = self.cursor.read_f64::()?; - let value = Amf0Value::Number(number); - Ok(value) - } - - pub fn read_bool(&mut self) -> Result { - let value = self.cursor.read_u8()?; - - match value { - 1 => Ok(Amf0Value::Boolean(true)), - _ => Ok(Amf0Value::Boolean(false)), - } - } - - fn read_raw_string(&mut self) -> Result { - let l = self.cursor.read_u16::()?; - - let bytes = self.read_bytes(l as usize)?; - - Ok(std::str::from_utf8(&bytes)?.to_string()) - } - - pub fn read_string(&mut self) -> Result { - let raw_string = self.read_raw_string()?; - Ok(Amf0Value::String(raw_string)) - } - - pub fn read_null(&mut self) -> Result { - Ok(Amf0Value::Null) - } - - pub fn is_read_object_eof(&mut self) -> Result { - let pos = self.cursor.position(); - let marker = self.cursor.read_u24::(); - self.cursor.seek(SeekFrom::Start(pos))?; - - match Amf0Marker::from_u32(marker?) { - Some(Amf0Marker::ObjectEnd) => { - self.cursor.read_u24::()?; - Ok(true) - } - _ => Ok(false), - } - } - - pub fn read_object(&mut self) -> Result { - let mut properties = HashMap::new(); - - loop { - let is_eof = self.is_read_object_eof()?; - - if is_eof { - break; - } - - let key = self.read_raw_string()?; - let val = self.read_any()?; - - properties.insert(key, val); - } - - Ok(Amf0Value::Object(properties)) - } - - pub fn read_ecma_array(&mut self) -> Result { - let len = self.cursor.read_u32::()?; - - let mut properties = HashMap::new(); - - for _ in 0..len { - let key = self.read_raw_string()?; - let val = self.read_any()?; - properties.insert(key, val); - } - - // Sometimes the object end marker is present and sometimes it is not. - // If it is there just read it, if not then we are done. - self.is_read_object_eof().ok(); // ignore the result - - Ok(Amf0Value::Object(properties)) - } - - pub fn read_long_string(&mut self) -> Result { - let l = self.cursor.read_u32::()?; - - let buff = self.read_bytes(l as usize)?; - let val = std::str::from_utf8(&buff)?; - - Ok(Amf0Value::LongString(val.to_string())) - } -} diff --git a/crates/amf0/src/tests.rs b/crates/amf0/src/tests.rs deleted file mode 100644 index 20ae3fb0a..000000000 --- a/crates/amf0/src/tests.rs +++ /dev/null @@ -1,203 +0,0 @@ -use std::collections::HashMap; -use std::io::Cursor; - -use byteorder::ReadBytesExt; - -use crate::{Amf0Marker, Amf0ReadError, Amf0Reader, Amf0Value, Amf0WriteError, Amf0Writer}; - -#[test] -fn test_reader_bool() { - let amf0_bool = vec![0x01, 0x01]; // true - let mut amf_reader = Amf0Reader::new(amf0_bool.into()); - let value = amf_reader.read_with_type(Amf0Marker::Boolean).unwrap(); - assert_eq!(value, Amf0Value::Boolean(true)); -} - -#[test] -fn test_reader_number() { - let mut amf0_number = vec![0x00]; - amf0_number.extend_from_slice(&772.161_f64.to_be_bytes()); - - let mut amf_reader = Amf0Reader::new(amf0_number.into()); - let value = amf_reader.read_with_type(Amf0Marker::Number).unwrap(); - assert_eq!(value, Amf0Value::Number(772.161)); -} - -#[test] -fn test_reader_string() { - let mut amf0_string = vec![0x02, 0x00, 0x0b]; // 11 bytes - amf0_string.extend_from_slice(b"Hello World"); - - let mut amf_reader = Amf0Reader::new(amf0_string.into()); - let value = amf_reader.read_with_type(Amf0Marker::String).unwrap(); - assert_eq!(value, Amf0Value::String("Hello World".to_string())); -} - -#[test] -fn test_reader_long_string() { - let mut amf0_string = vec![0x0c, 0x00, 0x00, 0x00, 0x0b]; // 11 bytes - amf0_string.extend_from_slice(b"Hello World"); - - let mut amf_reader = Amf0Reader::new(amf0_string.into()); - let value = amf_reader.read_with_type(Amf0Marker::LongString).unwrap(); - assert_eq!(value, Amf0Value::LongString("Hello World".to_string())); -} - -#[test] -fn test_reader_object() { - let mut amf0_object = vec![0x03, 0x00, 0x04]; // 1 property with 4 bytes - amf0_object.extend_from_slice(b"test"); - amf0_object.extend_from_slice(&[0x05]); // null - amf0_object.extend_from_slice(&[0x00, 0x00, 0x09]); // object end (0x00 0x00 0x09) - - let mut amf_reader = Amf0Reader::new(amf0_object.into()); - let value = amf_reader.read_with_type(Amf0Marker::Object).unwrap(); - - assert_eq!( - value, - Amf0Value::Object(HashMap::from([("test".to_string(), Amf0Value::Null)])) - ); -} - -#[test] -fn test_reader_ecma_array() { - let mut amf0_object = vec![0x08, 0x00, 0x00, 0x00, 0x01]; // 1 property - amf0_object.extend_from_slice(&[0x00, 0x04]); // 4 bytes - amf0_object.extend_from_slice(b"test"); - amf0_object.extend_from_slice(&[0x05]); // null - - let mut amf_reader = Amf0Reader::new(amf0_object.into()); - let value = amf_reader.read_with_type(Amf0Marker::EcmaArray).unwrap(); - - assert_eq!( - value, - Amf0Value::Object(HashMap::from([("test".to_string(), Amf0Value::Null)])) - ); -} - -#[test] -fn test_reader_multi_value() { - let mut amf0_multi = vec![0x00]; - amf0_multi.extend_from_slice(&772.161_f64.to_be_bytes()); - amf0_multi.extend_from_slice(&[0x01, 0x01]); // true - amf0_multi.extend_from_slice(&[0x02, 0x00, 0x0b]); // 11 bytes - amf0_multi.extend_from_slice(b"Hello World"); - amf0_multi.extend_from_slice(&[0x03, 0x00, 0x04]); // 1 property with 4 bytes - amf0_multi.extend_from_slice(b"test"); - amf0_multi.extend_from_slice(&[0x05]); // null - amf0_multi.extend_from_slice(&[0x00, 0x00, 0x09]); // object end (0x00 0x00 0x09) - - let mut amf_reader = Amf0Reader::new(amf0_multi.into()); - let values = amf_reader.read_all().unwrap(); - - assert_eq!(values.len(), 4); - - assert_eq!(values[0], Amf0Value::Number(772.161)); - assert_eq!(values[1], Amf0Value::Boolean(true)); - assert_eq!(values[2], Amf0Value::String("Hello World".to_string())); - assert_eq!( - values[3], - Amf0Value::Object(HashMap::from([("test".to_string(), Amf0Value::Null)])) - ); -} - -#[test] -fn test_read_error_display() { - assert_eq!(Amf0ReadError::UnknownMarker(100).to_string(), "unknown marker: 100"); - - assert_eq!( - Amf0ReadError::UnsupportedType(Amf0Marker::Reference).to_string(), - "unsupported type: Reference" - ); - - assert_eq!(Amf0ReadError::WrongType.to_string(), "wrong type"); - - assert_eq!( - Amf0ReadError::StringParseError( - #[allow(unknown_lints, invalid_from_utf8)] - std::str::from_utf8(b"\xFF\xFF").unwrap_err() - ) - .to_string(), - "string parse error: invalid utf-8 sequence of 1 bytes from index 0" - ); - - assert_eq!( - Amf0ReadError::IO(Cursor::new(Vec::::new()).read_u8().unwrap_err()).to_string(), - "io error: failed to fill whole buffer" - ); -} - -#[test] -fn test_write_error_display() { - assert_eq!( - Amf0WriteError::UnsupportedType(Amf0Value::ObjectEnd).to_string(), - "unsupported type: ObjectEnd" - ); - - assert_eq!( - Amf0WriteError::IO(Cursor::new(Vec::::new()).read_u8().unwrap_err()).to_string(), - "io error: failed to fill whole buffer" - ); - - assert_eq!(Amf0WriteError::NormalStringTooLong.to_string(), "normal string too long"); -} - -#[test] -fn test_write_number() { - let mut amf0_number = vec![0x00]; - amf0_number.extend_from_slice(&772.161_f64.to_be_bytes()); - - let mut vec = Vec::::new(); - - Amf0Writer::write_number(&mut vec, 772.161).unwrap(); - - assert_eq!(vec, amf0_number); -} - -#[test] -fn test_write_boolean() { - let amf0_boolean = vec![0x01, 0x01]; - - let mut vec = Vec::::new(); - - Amf0Writer::write_bool(&mut vec, true).unwrap(); - - assert_eq!(vec, amf0_boolean); -} - -#[test] -fn test_write_string() { - let mut amf0_string = vec![0x02, 0x00, 0x0b]; - amf0_string.extend_from_slice(b"Hello World"); - - let mut vec = Vec::::new(); - - Amf0Writer::write_string(&mut vec, "Hello World").unwrap(); - - assert_eq!(vec, amf0_string); -} - -#[test] -fn test_write_null() { - let amf0_null = vec![0x05]; - - let mut vec = Vec::::new(); - - Amf0Writer::write_null(&mut vec).unwrap(); - - assert_eq!(vec, amf0_null); -} - -#[test] -fn test_write_object() { - let mut amf0_object = vec![0x03, 0x00, 0x04]; - amf0_object.extend_from_slice(b"test"); - amf0_object.extend_from_slice(&[0x05]); - amf0_object.extend_from_slice(&[0x00, 0x00, 0x09]); - - let mut vec = Vec::::new(); - - Amf0Writer::write_object(&mut vec, &HashMap::from([("test".to_string(), Amf0Value::Null)])).unwrap(); - - assert_eq!(vec, amf0_object); -} diff --git a/crates/amf0/src/writer.rs b/crates/amf0/src/writer.rs deleted file mode 100644 index 6d2730c3b..000000000 --- a/crates/amf0/src/writer.rs +++ /dev/null @@ -1,66 +0,0 @@ -use std::collections::HashMap; -use std::io; - -use byteorder::{BigEndian, WriteBytesExt}; - -use super::define::Amf0Marker; -use super::{Amf0Value, Amf0WriteError}; - -pub struct Amf0Writer; - -impl Amf0Writer { - pub fn write_any(writer: &mut impl io::Write, value: &Amf0Value) -> Result<(), Amf0WriteError> { - match value { - Amf0Value::Boolean(val) => Self::write_bool(writer, *val), - Amf0Value::Null => Self::write_null(writer), - Amf0Value::Number(val) => Self::write_number(writer, *val), - Amf0Value::String(val) => Self::write_string(writer, val.as_str()), - Amf0Value::Object(val) => Self::write_object(writer, val), - _ => Err(Amf0WriteError::UnsupportedType(value.clone())), - } - } - - fn write_object_eof(writer: &mut impl io::Write) -> Result<(), Amf0WriteError> { - writer.write_u24::(Amf0Marker::ObjectEnd as u32)?; - Ok(()) - } - - pub fn write_number(writer: &mut impl io::Write, value: f64) -> Result<(), Amf0WriteError> { - writer.write_u8(Amf0Marker::Number as u8)?; - writer.write_f64::(value)?; - Ok(()) - } - - pub fn write_bool(writer: &mut impl io::Write, value: bool) -> Result<(), Amf0WriteError> { - writer.write_u8(Amf0Marker::Boolean as u8)?; - writer.write_u8(value as u8)?; - Ok(()) - } - - pub fn write_string(writer: &mut impl io::Write, value: &str) -> Result<(), Amf0WriteError> { - if value.len() > (u16::MAX as usize) { - return Err(Amf0WriteError::NormalStringTooLong); - } - writer.write_u8(Amf0Marker::String as u8)?; - writer.write_u16::(value.len() as u16)?; - writer.write_all(value.as_bytes())?; - Ok(()) - } - - pub fn write_null(writer: &mut impl io::Write) -> Result<(), Amf0WriteError> { - writer.write_u8(Amf0Marker::Null as u8)?; - Ok(()) - } - - pub fn write_object(writer: &mut impl io::Write, properties: &HashMap) -> Result<(), Amf0WriteError> { - writer.write_u8(Amf0Marker::Object as u8)?; - for (key, value) in properties { - writer.write_u16::(key.len() as u16)?; - writer.write_all(key.as_bytes())?; - Self::write_any(writer, value)?; - } - - Self::write_object_eof(writer)?; - Ok(()) - } -} diff --git a/crates/flv/Cargo.toml b/crates/flv/Cargo.toml index f6b372d08..839dafcda 100644 --- a/crates/flv/Cargo.toml +++ b/crates/flv/Cargo.toml @@ -14,6 +14,6 @@ av1 = { path = "../av1" } h264 = { path = "../h264" } h265 = { path = "../h265" } scuffle-aac = { path = "../aac" } -amf0 = { path = "../amf0" } scuffle-bytes-util.workspace = true +scuffle-amf0.workspace = true scuffle-workspace-hack.workspace = true diff --git a/crates/flv/src/define.rs b/crates/flv/src/define.rs index 96130789e..f3825af39 100644 --- a/crates/flv/src/define.rs +++ b/crates/flv/src/define.rs @@ -1,9 +1,9 @@ -use amf0::Amf0Value; use av1::AV1CodecConfigurationRecord; use bytes::Bytes; use h264::AVCDecoderConfigurationRecord; use h265::HEVCDecoderConfigurationRecord; use num_derive::FromPrimitive; +use scuffle_amf0::Amf0Value; #[derive(Debug, Clone, PartialEq)] /// FLV File @@ -62,7 +62,7 @@ pub enum FlvTagData { /// VideoData defined in the FLV specification. Chapter 1 - FLV Video Tags Video { frame_type: FrameType, data: FlvTagVideoData }, /// ScriptData defined in the FLV specification. Chapter 1 - FLV Data Tags - ScriptData { name: String, data: Vec }, + ScriptData { name: String, data: Vec> }, /// Data we don't know how to parse Unknown { tag_type: u8, data: Bytes }, } diff --git a/crates/flv/src/errors.rs b/crates/flv/src/errors.rs index fa1d3fb12..e0705ae97 100644 --- a/crates/flv/src/errors.rs +++ b/crates/flv/src/errors.rs @@ -3,7 +3,7 @@ use std::{fmt, io}; #[derive(Debug)] pub enum FlvDemuxerError { IO(io::Error), - Amf0Read(amf0::Amf0ReadError), + Amf0Read(scuffle_amf0::Amf0ReadError), InvalidFlvHeader, InvalidScriptDataName, InvalidEnhancedPacketType(u8), @@ -19,8 +19,8 @@ impl From for FlvDemuxerError { } } -impl From for FlvDemuxerError { - fn from(value: amf0::Amf0ReadError) -> Self { +impl From for FlvDemuxerError { + fn from(value: scuffle_amf0::Amf0ReadError) -> Self { Self::Amf0Read(value) } } diff --git a/crates/flv/src/flv.rs b/crates/flv/src/flv.rs index a00a19ad4..72d16e747 100644 --- a/crates/flv/src/flv.rs +++ b/crates/flv/src/flv.rs @@ -2,13 +2,13 @@ use std::io::{ Read, {self}, }; -use amf0::{Amf0Reader, Amf0Value}; use av1::AV1CodecConfigurationRecord; use byteorder::{BigEndian, ReadBytesExt}; use bytes::{Buf, Bytes}; use h264::AVCDecoderConfigurationRecord; use h265::HEVCDecoderConfigurationRecord; use num_traits::FromPrimitive; +use scuffle_amf0::{Amf0Decoder, Amf0Value}; use scuffle_bytes_util::BytesCursorExt; use crate::define::Flv; @@ -145,7 +145,8 @@ impl FlvTagData { }) } Some(FlvTagType::ScriptData) => { - let values = Amf0Reader::new(reader.extract_remaining()).read_all()?; + let remaining = reader.extract_remaining(); + let values = Amf0Decoder::new(&remaining).decode_all()?; let name = match values.first() { Some(Amf0Value::String(name)) => name, @@ -153,8 +154,8 @@ impl FlvTagData { }; Ok(FlvTagData::ScriptData { - name: name.clone(), - data: values.into_iter().skip(1).collect(), + name: name.to_string(), + data: values.into_iter().skip(1).map(|v| v.to_owned()).collect(), }) } None => Ok(FlvTagData::Unknown { diff --git a/crates/flv/src/tests/demuxer.rs b/crates/flv/src/tests/demuxer.rs index a4fb74464..47e72f435 100644 --- a/crates/flv/src/tests/demuxer.rs +++ b/crates/flv/src/tests/demuxer.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::io; use std::path::PathBuf; @@ -47,113 +48,115 @@ fn test_demux_flv_avc_aac() { // Script data should be an AMF0 object let object = match &script_data[0] { - amf0::Amf0Value::Object(object) => object, + scuffle_amf0::Amf0Value::Object(object) => object, _ => panic!("expected object"), }; + let map = object.iter().map(|(k, v)| (k.as_ref(), v)).collect::>(); + // Should have a audio sample size property - let audio_sample_size = match object.get("audiosamplesize") { - Some(amf0::Amf0Value::Number(number)) => number, + let audio_sample_size = match map.get("audiosamplesize") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected audio sample size"), }; assert_eq!(audio_sample_size, &16.0); // Should have a audio sample rate property - let audio_sample_rate = match object.get("audiosamplerate") { - Some(amf0::Amf0Value::Number(number)) => number, + let audio_sample_rate = match map.get("audiosamplerate") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected audio sample rate"), }; assert_eq!(audio_sample_rate, &48000.0); // Should have a stereo property - let stereo = match object.get("stereo") { - Some(amf0::Amf0Value::Boolean(boolean)) => boolean, + let stereo = match map.get("stereo") { + Some(scuffle_amf0::Amf0Value::Boolean(boolean)) => boolean, _ => panic!("expected stereo"), }; assert_eq!(stereo, &true); // Should have an audio codec id property - let audio_codec_id = match object.get("audiocodecid") { - Some(amf0::Amf0Value::Number(number)) => number, + let audio_codec_id = match map.get("audiocodecid") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected audio codec id"), }; assert_eq!(audio_codec_id, &10.0); // AAC // Should have a video codec id property - let video_codec_id = match object.get("videocodecid") { - Some(amf0::Amf0Value::Number(number)) => number, + let video_codec_id = match map.get("videocodecid") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected video codec id"), }; assert_eq!(video_codec_id, &7.0); // AVC // Should have a duration property - let duration = match object.get("duration") { - Some(amf0::Amf0Value::Number(number)) => number, + let duration = match map.get("duration") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected duration"), }; assert_eq!(duration, &1.088); // 1.088 seconds // Should have a width property - let width = match object.get("width") { - Some(amf0::Amf0Value::Number(number)) => number, + let width = match map.get("width") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected width"), }; assert_eq!(width, &3840.0); // Should have a height property - let height = match object.get("height") { - Some(amf0::Amf0Value::Number(number)) => number, + let height = match map.get("height") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected height"), }; assert_eq!(height, &2160.0); // Should have a framerate property - let framerate = match object.get("framerate") { - Some(amf0::Amf0Value::Number(number)) => number, + let framerate = match map.get("framerate") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected framerate"), }; assert_eq!(framerate, &60.0); // Should have a videodatarate property - match object.get("videodatarate") { - Some(amf0::Amf0Value::Number(number)) => number, + match map.get("videodatarate") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected videodatarate"), }; // Should have a audiodatarate property - match object.get("audiodatarate") { - Some(amf0::Amf0Value::Number(number)) => number, + match map.get("audiodatarate") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected audiodatarate"), }; // Should have a minor version property - let minor_version = match object.get("minor_version") { - Some(amf0::Amf0Value::String(number)) => number, + let minor_version = match map.get("minor_version") { + Some(scuffle_amf0::Amf0Value::String(number)) => number, _ => panic!("expected minor version"), }; assert_eq!(minor_version, "512"); // Should have a major brand property - let major_brand = match object.get("major_brand") { - Some(amf0::Amf0Value::String(string)) => string, + let major_brand = match map.get("major_brand") { + Some(scuffle_amf0::Amf0Value::String(string)) => string, _ => panic!("expected major brand"), }; assert_eq!(major_brand, "iso5"); // Should have a compatible_brands property - let compatible_brands = match object.get("compatible_brands") { - Some(amf0::Amf0Value::String(string)) => string, + let compatible_brands = match map.get("compatible_brands") { + Some(scuffle_amf0::Amf0Value::String(string)) => string, _ => panic!("expected compatible brands"), }; @@ -330,91 +333,93 @@ fn test_demux_flv_av1_aac() { // Script data should be an AMF0 object let object = match &script_data[0] { - amf0::Amf0Value::Object(object) => object, + scuffle_amf0::Amf0Value::Object(object) => object, _ => panic!("expected object"), }; + let map = object.iter().map(|(k, v)| (k.as_ref(), v)).collect::>(); + // Should have a audio sample size property - let audio_sample_size = match object.get("audiosamplesize") { - Some(amf0::Amf0Value::Number(number)) => number, + let audio_sample_size = match map.get("audiosamplesize") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected audio sample size"), }; assert_eq!(audio_sample_size, &16.0); // Should have a audio sample rate property - let audio_sample_rate = match object.get("audiosamplerate") { - Some(amf0::Amf0Value::Number(number)) => number, + let audio_sample_rate = match map.get("audiosamplerate") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected audio sample rate"), }; assert_eq!(audio_sample_rate, &48000.0); // Should have a stereo property - let stereo = match object.get("stereo") { - Some(amf0::Amf0Value::Boolean(boolean)) => boolean, + let stereo = match map.get("stereo") { + Some(scuffle_amf0::Amf0Value::Boolean(boolean)) => boolean, _ => panic!("expected stereo"), }; assert_eq!(stereo, &true); // Should have an audio codec id property - let audio_codec_id = match object.get("audiocodecid") { - Some(amf0::Amf0Value::Number(number)) => number, + let audio_codec_id = match map.get("audiocodecid") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected audio codec id"), }; assert_eq!(audio_codec_id, &10.0); // AAC // Should have a video codec id property - let video_codec_id = match object.get("videocodecid") { - Some(amf0::Amf0Value::Number(number)) => number, + let video_codec_id = match map.get("videocodecid") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected video codec id"), }; assert_eq!(video_codec_id, &7.0); // AVC // Should have a duration property - let duration = match object.get("duration") { - Some(amf0::Amf0Value::Number(number)) => number, + let duration = match map.get("duration") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected duration"), }; assert_eq!(duration, &0.0); // 0 seconds (this was a live stream) // Should have a width property - let width = match object.get("width") { - Some(amf0::Amf0Value::Number(number)) => number, + let width = match map.get("width") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected width"), }; assert_eq!(width, &2560.0); // Should have a height property - let height = match object.get("height") { - Some(amf0::Amf0Value::Number(number)) => number, + let height = match map.get("height") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected height"), }; assert_eq!(height, &1440.0); // Should have a framerate property - let framerate = match object.get("framerate") { - Some(amf0::Amf0Value::Number(number)) => number, + let framerate = match map.get("framerate") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected framerate"), }; assert_eq!(framerate, &144.0); // Should have a videodatarate property - match object.get("videodatarate") { - Some(amf0::Amf0Value::Number(number)) => number, + match map.get("videodatarate") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected videodatarate"), }; // Should have a audiodatarate property - match object.get("audiodatarate") { - Some(amf0::Amf0Value::Number(number)) => number, + match map.get("audiodatarate") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected audiodatarate"), }; } @@ -579,91 +584,93 @@ fn test_demux_flv_hevc_aac() { // Script data should be an AMF0 object let object = match &script_data[0] { - amf0::Amf0Value::Object(object) => object, + scuffle_amf0::Amf0Value::Object(object) => object, _ => panic!("expected object"), }; + let map = object.iter().map(|(k, v)| (k.as_ref(), v)).collect::>(); + // Should have a audio sample size property - let audio_sample_size = match object.get("audiosamplesize") { - Some(amf0::Amf0Value::Number(number)) => number, + let audio_sample_size = match map.get("audiosamplesize") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected audio sample size"), }; assert_eq!(audio_sample_size, &16.0); // Should have a audio sample rate property - let audio_sample_rate = match object.get("audiosamplerate") { - Some(amf0::Amf0Value::Number(number)) => number, + let audio_sample_rate = match map.get("audiosamplerate") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected audio sample rate"), }; assert_eq!(audio_sample_rate, &48000.0); // Should have a stereo property - let stereo = match object.get("stereo") { - Some(amf0::Amf0Value::Boolean(boolean)) => boolean, + let stereo = match map.get("stereo") { + Some(scuffle_amf0::Amf0Value::Boolean(boolean)) => boolean, _ => panic!("expected stereo"), }; assert_eq!(stereo, &true); // Should have an audio codec id property - let audio_codec_id = match object.get("audiocodecid") { - Some(amf0::Amf0Value::Number(number)) => number, + let audio_codec_id = match map.get("audiocodecid") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected audio codec id"), }; assert_eq!(audio_codec_id, &10.0); // AAC // Should have a video codec id property - let video_codec_id = match object.get("videocodecid") { - Some(amf0::Amf0Value::Number(number)) => number, + let video_codec_id = match map.get("videocodecid") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected video codec id"), }; assert_eq!(video_codec_id, &7.0); // AVC // Should have a duration property - let duration = match object.get("duration") { - Some(amf0::Amf0Value::Number(number)) => number, + let duration = match map.get("duration") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected duration"), }; assert_eq!(duration, &0.0); // 0 seconds (this was a live stream) // Should have a width property - let width = match object.get("width") { - Some(amf0::Amf0Value::Number(number)) => number, + let width = match map.get("width") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected width"), }; assert_eq!(width, &2560.0); // Should have a height property - let height = match object.get("height") { - Some(amf0::Amf0Value::Number(number)) => number, + let height = match map.get("height") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected height"), }; assert_eq!(height, &1440.0); // Should have a framerate property - let framerate = match object.get("framerate") { - Some(amf0::Amf0Value::Number(number)) => number, + let framerate = match map.get("framerate") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected framerate"), }; assert_eq!(framerate, &144.0); // Should have a videodatarate property - match object.get("videodatarate") { - Some(amf0::Amf0Value::Number(number)) => number, + match map.get("videodatarate") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected videodatarate"), }; // Should have a audiodatarate property - match object.get("audiodatarate") { - Some(amf0::Amf0Value::Number(number)) => number, + match map.get("audiodatarate") { + Some(scuffle_amf0::Amf0Value::Number(number)) => number, _ => panic!("expected audiodatarate"), }; } diff --git a/crates/flv/src/tests/error.rs b/crates/flv/src/tests/error.rs index 56b3a691e..04e4be5f9 100644 --- a/crates/flv/src/tests/error.rs +++ b/crates/flv/src/tests/error.rs @@ -8,7 +8,7 @@ fn test_error_display() { let error = FlvDemuxerError::IO(std::io::Error::new(std::io::ErrorKind::Other, "test")); assert_eq!(error.to_string(), "io error: test"); - let error = FlvDemuxerError::Amf0Read(amf0::Amf0ReadError::UnknownMarker(0)); + let error = FlvDemuxerError::Amf0Read(scuffle_amf0::Amf0ReadError::UnknownMarker(0)); assert_eq!(error.to_string(), "amf0 read error: unknown marker: 0"); let error = FlvDemuxerError::InvalidFlvHeader; diff --git a/crates/rtmp/Cargo.toml b/crates/rtmp/Cargo.toml index e2afbeb5a..d7ddaef67 100644 --- a/crates/rtmp/Cargo.toml +++ b/crates/rtmp/Cargo.toml @@ -19,7 +19,7 @@ futures = "0.3" async-trait = "0.1" tracing = "0.1" -amf0 = { path = "../amf0" } +scuffle-amf0.workspace = true scuffle-workspace-hack.workspace = true scuffle-bytes-util.workspace = true scuffle-future-ext.workspace = true diff --git a/crates/rtmp/src/messages/define.rs b/crates/rtmp/src/messages/define.rs index a53057d95..bb43a79c8 100644 --- a/crates/rtmp/src/messages/define.rs +++ b/crates/rtmp/src/messages/define.rs @@ -1,14 +1,14 @@ -use amf0::Amf0Value; use bytes::Bytes; use num_derive::FromPrimitive; +use scuffle_amf0::Amf0Value; #[derive(Debug)] -pub enum RtmpMessageData { +pub enum RtmpMessageData<'a> { Amf0Command { - command_name: Amf0Value, - transaction_id: Amf0Value, - command_object: Amf0Value, - others: Vec, + command_name: Amf0Value<'a>, + transaction_id: Amf0Value<'a>, + command_object: Amf0Value<'a>, + others: Vec>, }, AmfData { data: Bytes, diff --git a/crates/rtmp/src/messages/errors.rs b/crates/rtmp/src/messages/errors.rs index 55dcd259b..2744fed7c 100644 --- a/crates/rtmp/src/messages/errors.rs +++ b/crates/rtmp/src/messages/errors.rs @@ -1,6 +1,6 @@ use std::fmt; -use amf0::Amf0ReadError; +use scuffle_amf0::Amf0ReadError; use crate::macros::from_error; use crate::protocol_control_messages::ProtocolControlMessageError; diff --git a/crates/rtmp/src/messages/parser.rs b/crates/rtmp/src/messages/parser.rs index ef965dcb1..a39ecc296 100644 --- a/crates/rtmp/src/messages/parser.rs +++ b/crates/rtmp/src/messages/parser.rs @@ -1,4 +1,4 @@ -use amf0::{Amf0Marker, Amf0Reader}; +use scuffle_amf0::{Amf0Decoder, Amf0Marker}; use super::define::{MessageTypeID, RtmpMessageData}; use super::errors::MessageError; @@ -8,19 +8,19 @@ use crate::protocol_control_messages::ProtocolControlMessageReader; pub struct MessageParser; impl MessageParser { - pub fn parse(chunk: Chunk) -> Result, MessageError> { + pub fn parse(chunk: &Chunk) -> Result>, MessageError> { match chunk.message_header.msg_type_id { // Protocol Control Messages MessageTypeID::CommandAMF0 => { - let mut amf_reader = Amf0Reader::new(chunk.payload); - let command_name = amf_reader.read_with_type(Amf0Marker::String)?; - let transaction_id = amf_reader.read_with_type(Amf0Marker::Number)?; - let command_object = match amf_reader.read_with_type(Amf0Marker::Object) { + let mut amf_reader = Amf0Decoder::new(&chunk.payload); + let command_name = amf_reader.decode_with_type(Amf0Marker::String)?; + let transaction_id = amf_reader.decode_with_type(Amf0Marker::Number)?; + let command_object = match amf_reader.decode_with_type(Amf0Marker::Object) { Ok(val) => val, - Err(_) => amf_reader.read_with_type(Amf0Marker::Null)?, + Err(_) => amf_reader.decode_with_type(Amf0Marker::Null)?, }; - let others = amf_reader.read_all()?; + let others = amf_reader.decode_all()?; Ok(Some(RtmpMessageData::Amf0Command { command_name, @@ -30,17 +30,23 @@ impl MessageParser { })) } // Data Messages - AUDIO - MessageTypeID::Audio => Ok(Some(RtmpMessageData::AudioData { data: chunk.payload })), + MessageTypeID::Audio => Ok(Some(RtmpMessageData::AudioData { + data: chunk.payload.clone(), + })), // Data Messages - VIDEO - MessageTypeID::Video => Ok(Some(RtmpMessageData::VideoData { data: chunk.payload })), + MessageTypeID::Video => Ok(Some(RtmpMessageData::VideoData { + data: chunk.payload.clone(), + })), // Protocol Control Messages MessageTypeID::SetChunkSize => { - let chunk_size = ProtocolControlMessageReader::read_set_chunk_size(chunk.payload)?; + let chunk_size = ProtocolControlMessageReader::read_set_chunk_size(&chunk.payload)?; Ok(Some(RtmpMessageData::SetChunkSize { chunk_size })) } // Metadata - MessageTypeID::DataAMF0 | MessageTypeID::DataAMF3 => Ok(Some(RtmpMessageData::AmfData { data: chunk.payload })), + MessageTypeID::DataAMF0 | MessageTypeID::DataAMF3 => Ok(Some(RtmpMessageData::AmfData { + data: chunk.payload.clone(), + })), _ => Ok(None), } } diff --git a/crates/rtmp/src/messages/tests.rs b/crates/rtmp/src/messages/tests.rs index 823caea3c..a9462c759 100644 --- a/crates/rtmp/src/messages/tests.rs +++ b/crates/rtmp/src/messages/tests.rs @@ -1,7 +1,7 @@ -use std::collections::HashMap; +use std::borrow::Cow; -use amf0::{Amf0ReadError, Amf0Value, Amf0Writer}; use bytes::Bytes; +use scuffle_amf0::{Amf0Encoder, Amf0Marker, Amf0ReadError, Amf0Value}; use super::{MessageError, MessageParser, MessageTypeID, RtmpMessageData}; use crate::chunk::{Chunk, ChunkEncodeError}; @@ -9,8 +9,8 @@ use crate::protocol_control_messages::ProtocolControlMessageError; #[test] fn test_error_display() { - let error = MessageError::Amf0Read(Amf0ReadError::WrongType); - assert_eq!(error.to_string(), "amf0 read error: wrong type"); + let error = MessageError::Amf0Read(Amf0ReadError::WrongType(Amf0Marker::String, Amf0Marker::Date)); + assert_eq!(error.to_string(), "amf0 read error: wrong type: expected String, got Date"); let error = MessageError::ProtocolControlMessage(ProtocolControlMessageError::ChunkEncode(ChunkEncodeError::UnknownReadState)); @@ -24,15 +24,15 @@ fn test_error_display() { fn test_parse_command() { let mut amf0_writer = Vec::new(); - Amf0Writer::write_string(&mut amf0_writer, "connect").unwrap(); - Amf0Writer::write_number(&mut amf0_writer, 1.0).unwrap(); - Amf0Writer::write_null(&mut amf0_writer).unwrap(); + Amf0Encoder::encode_string(&mut amf0_writer, "connect").unwrap(); + Amf0Encoder::encode_number(&mut amf0_writer, 1.0).unwrap(); + Amf0Encoder::encode_null(&mut amf0_writer).unwrap(); let amf_data = Bytes::from(amf0_writer); let chunk = Chunk::new(0, 0, MessageTypeID::CommandAMF0, 0, amf_data); - let message = MessageParser::parse(chunk).expect("no errors").expect("message"); + let message = MessageParser::parse(&chunk).expect("no errors").expect("message"); match message { RtmpMessageData::Amf0Command { command_name, @@ -40,7 +40,7 @@ fn test_parse_command() { command_object, others, } => { - assert_eq!(command_name, Amf0Value::String("connect".to_string())); + assert_eq!(command_name, Amf0Value::String(Cow::Borrowed("connect"))); assert_eq!(transaction_id, Amf0Value::Number(1.0)); assert_eq!(command_object, Amf0Value::Null); assert_eq!(others, vec![]); @@ -53,7 +53,7 @@ fn test_parse_command() { fn test_parse_audio_packet() { let chunk = Chunk::new(0, 0, MessageTypeID::Audio, 0, vec![0x00, 0x00, 0x00, 0x00].into()); - let message = MessageParser::parse(chunk).expect("no errors").expect("message"); + let message = MessageParser::parse(&chunk).expect("no errors").expect("message"); match message { RtmpMessageData::AudioData { data } => { assert_eq!(data, vec![0x00, 0x00, 0x00, 0x00]); @@ -66,7 +66,7 @@ fn test_parse_audio_packet() { fn test_parse_video_packet() { let chunk = Chunk::new(0, 0, MessageTypeID::Video, 0, vec![0x00, 0x00, 0x00, 0x00].into()); - let message = MessageParser::parse(chunk).expect("no errors").expect("message"); + let message = MessageParser::parse(&chunk).expect("no errors").expect("message"); match message { RtmpMessageData::VideoData { data } => { assert_eq!(data, vec![0x00, 0x00, 0x00, 0x00]); @@ -79,7 +79,7 @@ fn test_parse_video_packet() { fn test_parse_set_chunk_size() { let chunk = Chunk::new(0, 0, MessageTypeID::SetChunkSize, 0, vec![0x00, 0xFF, 0xFF, 0xFF].into()); - let message = MessageParser::parse(chunk).expect("no errors").expect("message"); + let message = MessageParser::parse(&chunk).expect("no errors").expect("message"); match message { RtmpMessageData::SetChunkSize { chunk_size } => { assert_eq!(chunk_size, 0x00FFFFFF); @@ -92,17 +92,13 @@ fn test_parse_set_chunk_size() { fn test_parse_metadata() { let mut amf0_writer = Vec::new(); - Amf0Writer::write_string(&mut amf0_writer, "onMetaData").unwrap(); - Amf0Writer::write_object( - &mut amf0_writer, - &HashMap::from([("duration".to_string(), Amf0Value::Number(0.0))]), - ) - .unwrap(); + Amf0Encoder::encode_string(&mut amf0_writer, "onMetaData").unwrap(); + Amf0Encoder::encode_object(&mut amf0_writer, &[("duration".into(), Amf0Value::Number(0.0))]).unwrap(); let amf_data = Bytes::from(amf0_writer); let chunk = Chunk::new(0, 0, MessageTypeID::DataAMF0, 0, amf_data.clone()); - let message = MessageParser::parse(chunk).expect("no errors").expect("message"); + let message = MessageParser::parse(&chunk).expect("no errors").expect("message"); match message { RtmpMessageData::AmfData { data } => { assert_eq!(data, amf_data); @@ -115,5 +111,5 @@ fn test_parse_metadata() { fn test_unsupported_message_type() { let chunk = Chunk::new(0, 0, MessageTypeID::Aggregate, 0, vec![0x00, 0x00, 0x00, 0x00].into()); - assert!(MessageParser::parse(chunk).expect("no errors").is_none()) + assert!(MessageParser::parse(&chunk).expect("no errors").is_none()) } diff --git a/crates/rtmp/src/netconnection/errors.rs b/crates/rtmp/src/netconnection/errors.rs index 49353fc86..17a25e4c3 100644 --- a/crates/rtmp/src/netconnection/errors.rs +++ b/crates/rtmp/src/netconnection/errors.rs @@ -1,6 +1,6 @@ use std::fmt; -use amf0::Amf0WriteError; +use scuffle_amf0::Amf0WriteError; use crate::chunk::ChunkEncodeError; use crate::macros::from_error; diff --git a/crates/rtmp/src/netconnection/tests.rs b/crates/rtmp/src/netconnection/tests.rs index 17032282b..e5a7a951d 100644 --- a/crates/rtmp/src/netconnection/tests.rs +++ b/crates/rtmp/src/netconnection/tests.rs @@ -1,7 +1,7 @@ -use std::collections::HashMap; +use std::borrow::Cow; -use amf0::{Amf0Reader, Amf0Value, Amf0WriteError}; use bytes::{BufMut, BytesMut}; +use scuffle_amf0::{Amf0Decoder, Amf0Value, Amf0WriteError}; use super::NetConnection; use crate::chunk::{ChunkDecoder, ChunkEncodeError, ChunkEncoder}; @@ -41,26 +41,26 @@ fn test_netconnection_connect_response() { assert_eq!(chunk.message_header.msg_type_id as u8, 0x14); assert_eq!(chunk.message_header.msg_stream_id, 0); - let mut amf0_reader = Amf0Reader::new(chunk.payload); - let values = amf0_reader.read_all().unwrap(); + let mut amf0_reader = Amf0Decoder::new(&chunk.payload); + let values = amf0_reader.decode_all().unwrap(); assert_eq!(values.len(), 4); - assert_eq!(values[0], Amf0Value::String("_result".to_string())); // command name + assert_eq!(values[0], Amf0Value::String("_result".into())); // command name assert_eq!(values[1], Amf0Value::Number(1.0)); // transaction id assert_eq!( values[2], - Amf0Value::Object(HashMap::from([ - ("fmsVer".to_string(), Amf0Value::String("flashver".to_string())), - ("capabilities".to_string(), Amf0Value::Number(31.0)), + Amf0Value::Object(Cow::Owned(vec![ + ("fmsVer".into(), Amf0Value::String("flashver".into())), + ("capabilities".into(), Amf0Value::Number(31.0)), ])) ); // command object assert_eq!( values[3], - Amf0Value::Object(HashMap::from([ - ("code".to_string(), Amf0Value::String("status".to_string())), - ("level".to_string(), Amf0Value::String("idk".to_string())), - ("description".to_string(), Amf0Value::String("description".to_string())), - ("objectEncoding".to_string(), Amf0Value::Number(0.0)), + Amf0Value::Object(Cow::Owned(vec![ + ("level".into(), Amf0Value::String("idk".into())), + ("code".into(), Amf0Value::String("status".into())), + ("description".into(), Amf0Value::String("description".into())), + ("objectEncoding".into(), Amf0Value::Number(0.0)), ])) ); // info object } @@ -79,11 +79,11 @@ fn test_netconnection_create_stream_response() { assert_eq!(chunk.message_header.msg_type_id as u8, 0x14); assert_eq!(chunk.message_header.msg_stream_id, 0); - let mut amf0_reader = Amf0Reader::new(chunk.payload); - let values = amf0_reader.read_all().unwrap(); + let mut amf0_reader = Amf0Decoder::new(&chunk.payload); + let values = amf0_reader.decode_all().unwrap(); assert_eq!(values.len(), 4); - assert_eq!(values[0], Amf0Value::String("_result".to_string())); // command name + assert_eq!(values[0], Amf0Value::String("_result".into())); // command name assert_eq!(values[1], Amf0Value::Number(1.0)); // transaction id assert_eq!(values[2], Amf0Value::Null); // command object assert_eq!(values[3], Amf0Value::Number(1.0)); // stream id diff --git a/crates/rtmp/src/netconnection/writer.rs b/crates/rtmp/src/netconnection/writer.rs index bf8b88bc7..25b3ef772 100644 --- a/crates/rtmp/src/netconnection/writer.rs +++ b/crates/rtmp/src/netconnection/writer.rs @@ -1,8 +1,7 @@ -use std::collections::HashMap; use std::io; -use amf0::{Amf0Value, Amf0Writer}; use bytes::Bytes; +use scuffle_amf0::{Amf0Encoder, Amf0Value}; use super::errors::NetConnectionError; use crate::chunk::{Chunk, ChunkEncoder, DefinedChunkStreamID}; @@ -34,23 +33,23 @@ impl NetConnection { ) -> Result<(), NetConnectionError> { let mut amf0_writer = Vec::new(); - Amf0Writer::write_string(&mut amf0_writer, "_result")?; - Amf0Writer::write_number(&mut amf0_writer, transaction_id)?; - Amf0Writer::write_object( + Amf0Encoder::encode_string(&mut amf0_writer, "_result")?; + Amf0Encoder::encode_number(&mut amf0_writer, transaction_id)?; + Amf0Encoder::encode_object( &mut amf0_writer, - &HashMap::from([ - ("fmsVer".to_string(), Amf0Value::String(fmsver.to_string())), - ("capabilities".to_string(), Amf0Value::Number(capabilities)), - ]), + &[ + ("fmsVer".into(), Amf0Value::String(fmsver.into())), + ("capabilities".into(), Amf0Value::Number(capabilities)), + ], )?; - Amf0Writer::write_object( + Amf0Encoder::encode_object( &mut amf0_writer, - &HashMap::from([ - ("level".to_string(), Amf0Value::String(level.to_string())), - ("code".to_string(), Amf0Value::String(code.to_string())), - ("description".to_string(), Amf0Value::String(description.to_string())), - ("objectEncoding".to_string(), Amf0Value::Number(encoding)), - ]), + &[ + ("level".into(), Amf0Value::String(level.into())), + ("code".into(), Amf0Value::String(code.into())), + ("description".into(), Amf0Value::String(description.into())), + ("objectEncoding".into(), Amf0Value::Number(encoding)), + ], )?; Self::write_chunk(encoder, Bytes::from(amf0_writer), writer) @@ -64,10 +63,10 @@ impl NetConnection { ) -> Result<(), NetConnectionError> { let mut amf0_writer = Vec::new(); - Amf0Writer::write_string(&mut amf0_writer, "_result")?; - Amf0Writer::write_number(&mut amf0_writer, transaction_id)?; - Amf0Writer::write_null(&mut amf0_writer)?; - Amf0Writer::write_number(&mut amf0_writer, stream_id)?; + Amf0Encoder::encode_string(&mut amf0_writer, "_result")?; + Amf0Encoder::encode_number(&mut amf0_writer, transaction_id)?; + Amf0Encoder::encode_null(&mut amf0_writer)?; + Amf0Encoder::encode_number(&mut amf0_writer, stream_id)?; Self::write_chunk(encoder, Bytes::from(amf0_writer), writer) } diff --git a/crates/rtmp/src/netstream/errors.rs b/crates/rtmp/src/netstream/errors.rs index ac9082767..e3c733a6e 100644 --- a/crates/rtmp/src/netstream/errors.rs +++ b/crates/rtmp/src/netstream/errors.rs @@ -1,6 +1,6 @@ use std::fmt; -use amf0::Amf0WriteError; +use scuffle_amf0::Amf0WriteError; use crate::chunk::ChunkEncodeError; use crate::macros::from_error; diff --git a/crates/rtmp/src/netstream/tests.rs b/crates/rtmp/src/netstream/tests.rs index 462fc237b..288ae710d 100644 --- a/crates/rtmp/src/netstream/tests.rs +++ b/crates/rtmp/src/netstream/tests.rs @@ -1,7 +1,5 @@ -use std::collections::HashMap; - -use amf0::{Amf0Reader, Amf0Value, Amf0WriteError}; use bytes::{BufMut, BytesMut}; +use scuffle_amf0::{Amf0Decoder, Amf0Value, Amf0WriteError}; use crate::chunk::{ChunkDecoder, ChunkEncodeError, ChunkEncoder}; use crate::netstream::{NetStreamError, NetStreamWriter}; @@ -29,19 +27,22 @@ fn test_netstream_write_on_status() { assert_eq!(chunk.message_header.msg_type_id as u8, 0x14); assert_eq!(chunk.message_header.msg_stream_id, 0); - let mut amf0_reader = Amf0Reader::new(chunk.payload); - let values = amf0_reader.read_all().unwrap(); + let mut amf0_reader = Amf0Decoder::new(&chunk.payload); + let values = amf0_reader.decode_all().unwrap(); assert_eq!(values.len(), 4); - assert_eq!(values[0], Amf0Value::String("onStatus".to_string())); // command name + assert_eq!(values[0], Amf0Value::String("onStatus".into())); // command name assert_eq!(values[1], Amf0Value::Number(1.0)); // transaction id assert_eq!(values[2], Amf0Value::Null); // command object assert_eq!( values[3], - Amf0Value::Object(HashMap::from([ - ("code".to_string(), Amf0Value::String("idk".to_string())), - ("level".to_string(), Amf0Value::String("status".to_string())), - ("description".to_string(), Amf0Value::String("description".to_string())), - ])) + Amf0Value::Object( + vec![ + ("level".into(), Amf0Value::String("status".into())), + ("code".into(), Amf0Value::String("idk".into())), + ("description".into(), Amf0Value::String("description".into())), + ] + .into() + ) ); // info object } diff --git a/crates/rtmp/src/netstream/writer.rs b/crates/rtmp/src/netstream/writer.rs index fb85e8631..2e167798f 100644 --- a/crates/rtmp/src/netstream/writer.rs +++ b/crates/rtmp/src/netstream/writer.rs @@ -1,8 +1,7 @@ -use std::collections::HashMap; use std::io; -use amf0::{Amf0Value, Amf0Writer}; use bytes::Bytes; +use scuffle_amf0::{Amf0Encoder, Amf0Value}; use super::errors::NetStreamError; use crate::chunk::{Chunk, ChunkEncoder, DefinedChunkStreamID}; @@ -36,16 +35,16 @@ impl NetStreamWriter { ) -> Result<(), NetStreamError> { let mut amf0_writer = Vec::new(); - Amf0Writer::write_string(&mut amf0_writer, "onStatus")?; - Amf0Writer::write_number(&mut amf0_writer, transaction_id)?; - Amf0Writer::write_null(&mut amf0_writer)?; - Amf0Writer::write_object( + Amf0Encoder::encode_string(&mut amf0_writer, "onStatus")?; + Amf0Encoder::encode_number(&mut amf0_writer, transaction_id)?; + Amf0Encoder::encode_null(&mut amf0_writer)?; + Amf0Encoder::encode_object( &mut amf0_writer, - &HashMap::from([ - ("level".to_string(), Amf0Value::String(level.to_string())), - ("code".to_string(), Amf0Value::String(code.to_string())), - ("description".to_string(), Amf0Value::String(description.to_string())), - ]), + &[ + ("level".into(), Amf0Value::String(level.into())), + ("code".into(), Amf0Value::String(code.into())), + ("description".into(), Amf0Value::String(description.into())), + ], )?; Self::write_chunk(encoder, Bytes::from(amf0_writer), writer) diff --git a/crates/rtmp/src/protocol_control_messages/reader.rs b/crates/rtmp/src/protocol_control_messages/reader.rs index 966a67761..a79edd02b 100644 --- a/crates/rtmp/src/protocol_control_messages/reader.rs +++ b/crates/rtmp/src/protocol_control_messages/reader.rs @@ -1,17 +1,15 @@ use std::io::Cursor; use byteorder::{BigEndian, ReadBytesExt}; -use bytes::Bytes; use super::errors::ProtocolControlMessageError; pub struct ProtocolControlMessageReader; impl ProtocolControlMessageReader { - pub fn read_set_chunk_size(data: Bytes) -> Result { + pub fn read_set_chunk_size(data: &[u8]) -> Result { let mut cursor = Cursor::new(data); let chunk_size = cursor.read_u32::()?; - Ok(chunk_size) } } diff --git a/crates/rtmp/src/protocol_control_messages/tests.rs b/crates/rtmp/src/protocol_control_messages/tests.rs index e7bd0b63e..a7419b2f8 100644 --- a/crates/rtmp/src/protocol_control_messages/tests.rs +++ b/crates/rtmp/src/protocol_control_messages/tests.rs @@ -17,7 +17,7 @@ fn test_error_display() { #[test] fn test_reader_read_set_chunk_size() { let data = vec![0x00, 0x00, 0x00, 0x01]; - let chunk_size = ProtocolControlMessageReader::read_set_chunk_size(data.into()).unwrap(); + let chunk_size = ProtocolControlMessageReader::read_set_chunk_size(&data).unwrap(); assert_eq!(chunk_size, 1); } diff --git a/crates/rtmp/src/session/server_session.rs b/crates/rtmp/src/session/server_session.rs index 21ae8cf91..5560625fc 100644 --- a/crates/rtmp/src/session/server_session.rs +++ b/crates/rtmp/src/session/server_session.rs @@ -1,9 +1,8 @@ -use std::collections::HashMap; -use std::io; +use std::borrow::Cow; use std::time::Duration; -use amf0::Amf0Value; use bytes::BytesMut; +use scuffle_amf0::Amf0Value; use scuffle_bytes_util::BytesCursorExt; use scuffle_future_ext::FutureExt; use tokio::io::{AsyncReadExt, AsyncWriteExt}; @@ -154,7 +153,7 @@ impl Session { bytes_read += n; } - let mut cursor = io::Cursor::new(self.read_buf.split().freeze()); + let mut cursor = std::io::Cursor::new(self.read_buf.split().freeze()); handshaker.handshake(&mut cursor, &mut self.write_buf)?; @@ -212,7 +211,7 @@ impl Session { let timestamp = chunk.message_header.timestamp; let msg_stream_id = chunk.message_header.msg_stream_id; - if let Some(msg) = MessageParser::parse(chunk)? { + if let Some(msg) = MessageParser::parse(&chunk)? { self.process_messages(msg, msg_stream_id, timestamp).await?; } } @@ -223,7 +222,7 @@ impl Session { /// Process rtmp messages async fn process_messages( &mut self, - rtmp_msg: RtmpMessageData, + rtmp_msg: RtmpMessageData<'_>, stream_id: u32, timestamp: u32, ) -> Result<(), SessionError> { @@ -286,10 +285,10 @@ impl Session { async fn on_amf0_command_message( &mut self, stream_id: u32, - command_name: Amf0Value, - transaction_id: Amf0Value, - command_object: Amf0Value, - others: Vec, + command_name: Amf0Value<'_>, + transaction_id: Amf0Value<'_>, + command_object: Amf0Value<'_>, + others: Vec>, ) -> Result<(), SessionError> { let cmd = RtmpCommand::from(match command_name { Amf0Value::String(ref s) => s, @@ -303,24 +302,24 @@ impl Session { let obj = match command_object { Amf0Value::Object(obj) => obj, - _ => HashMap::new(), + _ => Cow::Owned(Vec::new()), }; match cmd { RtmpCommand::Connect => { - self.on_command_connect(transaction_id, stream_id, obj, others).await?; + self.on_command_connect(transaction_id, stream_id, &obj, others).await?; } RtmpCommand::CreateStream => { - self.on_command_create_stream(transaction_id, stream_id, obj, others).await?; + self.on_command_create_stream(transaction_id, stream_id, &obj, others).await?; } RtmpCommand::DeleteStream => { - self.on_command_delete_stream(transaction_id, stream_id, obj, others).await?; + self.on_command_delete_stream(transaction_id, stream_id, &obj, others).await?; } RtmpCommand::Play => { return Err(SessionError::PlayNotSupported); } RtmpCommand::Publish => { - self.on_command_publish(transaction_id, stream_id, obj, others).await?; + self.on_command_publish(transaction_id, stream_id, &obj, others).await?; } RtmpCommand::CloseStream | RtmpCommand::ReleaseStream => { // Not sure what this is for @@ -348,8 +347,8 @@ impl Session { &mut self, transaction_id: f64, _stream_id: u32, - command_obj: HashMap, - _others: Vec, + command_obj: &[(Cow<'_, str>, Amf0Value<'_>)], + _others: Vec>, ) -> Result<(), SessionError> { ProtocolControlMessagesWriter::write_window_acknowledgement_size( &self.chunk_encoder, @@ -364,15 +363,15 @@ impl Session { 2, // 2 = dynamic )?; - let app_name = command_obj.get("app"); + let app_name = command_obj.iter().find(|(key, _)| key == "app"); let app_name = match app_name { - Some(Amf0Value::String(app)) => app, + Some((_, Amf0Value::String(app))) => app, _ => { return Err(SessionError::NoAppName); } }; - self.app_name = Some(app_name.to_owned()); + self.app_name = Some(app_name.to_string()); // The only AMF encoding supported by this server is AMF0 // So we ignore the objectEncoding value sent by the client @@ -406,8 +405,8 @@ impl Session { &mut self, transaction_id: f64, _stream_id: u32, - _command_obj: HashMap, - _others: Vec, + _command_obj: &[(Cow<'_, str>, Amf0Value<'_>)], + _others: Vec>, ) -> Result<(), SessionError> { // 1.0 is the Stream ID of the stream we are creating NetConnection::write_create_stream_response(&self.chunk_encoder, &mut self.write_buf, transaction_id, 1.0)?; @@ -423,8 +422,8 @@ impl Session { &mut self, transaction_id: f64, _stream_id: u32, - _command_obj: HashMap, - others: Vec, + _command_obj: &[(Cow<'_, str>, Amf0Value<'_>)], + others: Vec>, ) -> Result<(), SessionError> { let stream_id = match others.first() { Some(Amf0Value::Number(stream_id)) => *stream_id, @@ -455,8 +454,8 @@ impl Session { &mut self, transaction_id: f64, stream_id: u32, - _command_obj: HashMap, - others: Vec, + _command_obj: &[(Cow<'_, str>, Amf0Value<'_>)], + others: Vec>, ) -> Result<(), SessionError> { let stream_name = match others.first() { Some(Amf0Value::String(val)) => val, @@ -475,7 +474,7 @@ impl Session { .publish_request_producer .send(PublishRequest { app_name: app_name.clone(), - stream_name: stream_name.clone(), + stream_name: stream_name.to_string(), response, }) .await diff --git a/crates/rtmp/src/session/tests.rs b/crates/rtmp/src/session/tests.rs index 20dd861ad..3f20716dc 100644 --- a/crates/rtmp/src/session/tests.rs +++ b/crates/rtmp/src/session/tests.rs @@ -1,3 +1,5 @@ +use scuffle_amf0::Amf0Marker; + use crate::chunk::{ChunkDecodeError, ChunkEncodeError}; use crate::handshake::{DigestError, HandshakeError}; use crate::messages::MessageError; @@ -15,8 +17,14 @@ fn test_error_display() { let error = SessionError::Handshake(HandshakeError::Digest(DigestError::NotEnoughData)); assert_eq!(error.to_string(), "handshake error: digest error: not enough data"); - let error = SessionError::Message(MessageError::Amf0Read(amf0::Amf0ReadError::WrongType)); - assert_eq!(error.to_string(), "message error: amf0 read error: wrong type"); + let error = SessionError::Message(MessageError::Amf0Read(scuffle_amf0::Amf0ReadError::WrongType( + Amf0Marker::String, + Amf0Marker::EcmaArray, + ))); + assert_eq!( + error.to_string(), + "message error: amf0 read error: wrong type: expected String, got EcmaArray" + ); let error = SessionError::ChunkDecode(ChunkDecodeError::TooManyPreviousChunkHeaders); assert_eq!(error.to_string(), "chunk decode error: too many previous chunk headers"); diff --git a/crates/transmuxer/Cargo.toml b/crates/transmuxer/Cargo.toml index b497818a7..aa218e16b 100644 --- a/crates/transmuxer/Cargo.toml +++ b/crates/transmuxer/Cargo.toml @@ -12,7 +12,7 @@ h264 = { path = "../h264" } h265 = { path = "../h265" } av1 = { path = "../av1" } scuffle-aac = { path = "../aac" } -amf0 = { path = "../amf0" } +scuffle-amf0.workspace = true flv = { path = "../flv" } mp4 = { path = "../mp4" } scuffle-bytes-util.workspace = true diff --git a/crates/transmuxer/src/lib.rs b/crates/transmuxer/src/lib.rs index a365769c9..f08ccaf85 100644 --- a/crates/transmuxer/src/lib.rs +++ b/crates/transmuxer/src/lib.rs @@ -1,10 +1,10 @@ #![allow(clippy::single_match)] +use std::borrow::Cow; use std::collections::{HashMap, VecDeque}; use std::fmt::Debug; use std::io; -use amf0::Amf0Value; use byteorder::{BigEndian, ReadBytesExt}; use bytes::{Buf, Bytes}; use flv::{ @@ -39,6 +39,7 @@ use mp4::types::trex::Trex; use mp4::types::trun::Trun; use mp4::types::vmhd::Vmhd; use mp4::BoxType; +use scuffle_amf0::Amf0Value; mod codecs; mod define; @@ -47,6 +48,12 @@ mod errors; pub use define::*; pub use errors::TransmuxError; +struct Tags { + video_sequence_header: Option, + audio_sequence_header: Option, + scriptdata_tag: Option, Amf0Value<'static>>>, +} + #[derive(Debug, Clone)] pub struct Transmuxer { // These durations are measured in timescales @@ -283,13 +290,7 @@ impl Transmuxer { } /// Internal function to find the tags we need to create the init segment. - fn find_tags( - &self, - ) -> ( - Option, - Option, - Option>, - ) { + fn find_tags(&self) -> Tags { let tags = self.tags.iter(); let mut video_sequence_header = None; let mut audio_sequence_header = None; @@ -336,7 +337,7 @@ impl Transmuxer { let meta_object = data.iter().find(|v| matches!(v, Amf0Value::Object(_))); if let Some(Amf0Value::Object(meta_object)) = meta_object { - scriptdata_tag = Some(meta_object.clone()); + scriptdata_tag = Some(meta_object.iter().map(|(k, v)| (k.clone(), v.clone())).collect()); } } } @@ -344,7 +345,11 @@ impl Transmuxer { } } - (video_sequence_header, audio_sequence_header, scriptdata_tag) + Tags { + video_sequence_header, + audio_sequence_header, + scriptdata_tag, + } } /// Create the init segment. @@ -354,7 +359,11 @@ impl Transmuxer { ) -> Result, TransmuxError> { // We need to find the tag that is the video sequence header // and the audio sequence header - let (video_sequence_header, audio_sequence_header, scriptdata_tag) = self.find_tags(); + let Tags { + video_sequence_header, + audio_sequence_header, + scriptdata_tag, + } = self.find_tags(); let Some(video_sequence_header) = video_sequence_header else { return Ok(None); From ee8c4ae1b2a769cdb75e20b5c8a7d279bf532065 Mon Sep 17 00:00:00 2001 From: Lennart Kloock Date: Sat, 11 Jan 2025 00:39:58 +0100 Subject: [PATCH 2/6] refactor(amf0): one small line --- crates/amf0/src/lib.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/amf0/src/lib.rs b/crates/amf0/src/lib.rs index 4c5e2ffb3..f346877c1 100644 --- a/crates/amf0/src/lib.rs +++ b/crates/amf0/src/lib.rs @@ -1,5 +1,3 @@ -#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))] - //! A pure-rust implementation of AMF0 encoder and decoder. //! //! This crate provides a simple interface for encoding and decoding AMF0 data. @@ -27,6 +25,7 @@ //! # } //! # test().expect("test failed"); //! ``` +#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))] mod decode; mod define; From a6c7fe5e7efbc529b484e372369866be91127114 Mon Sep 17 00:00:00 2001 From: Lennart Kloock Date: Sat, 11 Jan 2025 02:07:26 +0100 Subject: [PATCH 3/6] docs(amf0): fix doc comments --- crates/amf0/src/decode.rs | 1 + crates/amf0/src/encode.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/crates/amf0/src/decode.rs b/crates/amf0/src/decode.rs index 8758af89d..3043b7850 100644 --- a/crates/amf0/src/decode.rs +++ b/crates/amf0/src/decode.rs @@ -7,6 +7,7 @@ use num_traits::FromPrimitive; use super::{Amf0Marker, Amf0ReadError, Amf0Value}; /// An AMF0 Decoder. +/// /// This decoder takes a reference to a byte slice and reads the AMF0 data from /// it. All returned objects are references to the original byte slice. Making /// it very cheap to use. diff --git a/crates/amf0/src/encode.rs b/crates/amf0/src/encode.rs index 1004f9538..8d0da1fe4 100644 --- a/crates/amf0/src/encode.rs +++ b/crates/amf0/src/encode.rs @@ -7,6 +7,7 @@ use super::define::Amf0Marker; use super::{Amf0Value, Amf0WriteError}; /// AMF0 encoder. +/// /// Allows for encoding an AMF0 to some writer. pub struct Amf0Encoder; From 6290c44903c44e8a8d76b88af80ca6fb3f57c00c Mon Sep 17 00:00:00 2001 From: Troy Benson Date: Sat, 11 Jan 2025 13:37:54 +0000 Subject: [PATCH 4/6] fix thiserror to 2.0 --- crates/amf0/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/amf0/Cargo.toml b/crates/amf0/Cargo.toml index 1bd102d66..ee992fec5 100644 --- a/crates/amf0/Cargo.toml +++ b/crates/amf0/Cargo.toml @@ -16,5 +16,5 @@ byteorder = "1.5" num-traits = "0.2" num-derive = "0.4" scuffle-bytes-util.workspace = true -thiserror = "2" +thiserror = "2.0" scuffle-workspace-hack.workspace = true From 6911209bad1b182d01dc69008b3781e3799cfc65 Mon Sep 17 00:00:00 2001 From: Troy Benson Date: Sat, 11 Jan 2025 13:42:01 +0000 Subject: [PATCH 5/6] fix pr comments --- crates/amf0/src/decode.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/crates/amf0/src/decode.rs b/crates/amf0/src/decode.rs index 3043b7850..299be56be 100644 --- a/crates/amf0/src/decode.rs +++ b/crates/amf0/src/decode.rs @@ -17,14 +17,14 @@ pub struct Amf0Decoder<'a> { impl<'a> Amf0Decoder<'a> { /// Create a new AMF0 decoder. - pub fn new(buff: &'a [u8]) -> Self { + pub const fn new(buff: &'a [u8]) -> Self { Self { cursor: Cursor::new(buff), } } /// Check if the decoder has reached the end of the AMF0 data. - pub fn is_empty(&self) -> bool { + pub const fn is_empty(&self) -> bool { self.cursor.get_ref().len() == self.cursor.position() as usize } @@ -81,7 +81,7 @@ impl<'a> Amf0Decoder<'a> { } fn read_bool(&mut self) -> Result { - Ok(self.cursor.read_u8()? == 1) + Ok(self.cursor.read_u8()? > 0) } fn read_string(&mut self) -> Result, Amf0ReadError> { @@ -93,15 +93,14 @@ impl<'a> Amf0Decoder<'a> { fn is_read_object_eof(&mut self) -> Result { let pos = self.cursor.position(); - let marker = self.cursor.read_u24::(); - self.cursor.seek(SeekFrom::Start(pos))?; + let marker = self.cursor.read_u24::().map(|m| Amf0Marker::from_u32(m)); - match Amf0Marker::from_u32(marker?) { - Some(Amf0Marker::ObjectEnd) => { - self.cursor.read_u24::()?; - Ok(true) - } - _ => Ok(false), + match marker { + Ok(Some(Amf0Marker::ObjectEnd)) => Ok(true), + _ => { + self.cursor.seek(SeekFrom::Start(pos))?; + Ok(false) + }, } } From f1e9b8608df7974e82b29fa97fffd20275c19f6e Mon Sep 17 00:00:00 2001 From: Troy Benson Date: Sat, 11 Jan 2025 13:44:37 +0000 Subject: [PATCH 6/6] fix format --- crates/amf0/src/decode.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/amf0/src/decode.rs b/crates/amf0/src/decode.rs index 299be56be..1e2c17336 100644 --- a/crates/amf0/src/decode.rs +++ b/crates/amf0/src/decode.rs @@ -93,14 +93,14 @@ impl<'a> Amf0Decoder<'a> { fn is_read_object_eof(&mut self) -> Result { let pos = self.cursor.position(); - let marker = self.cursor.read_u24::().map(|m| Amf0Marker::from_u32(m)); + let marker = self.cursor.read_u24::().map(Amf0Marker::from_u32); match marker { Ok(Some(Amf0Marker::ObjectEnd)) => Ok(true), _ => { self.cursor.seek(SeekFrom::Start(pos))?; Ok(false) - }, + } } }