diff --git a/Cargo.lock b/Cargo.lock index 7cc597db1..28f07f685 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3087,6 +3087,20 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "mp4" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "369762a9ab26451c57e0860102029db0f5c6b142baf6f373e759f311a828b50e" +dependencies = [ + "byteorder", + "bytes 0.5.6", + "num-rational 0.3.2", + "serde 1.0.126", + "serde_json", + "thiserror", +] + [[package]] name = "multiversion" version = "0.6.1" @@ -3807,6 +3821,18 @@ dependencies = [ "num-traits 0.2.14", ] +[[package]] +name = "nu_plugin_from_mp4" +version = "0.1.0" +dependencies = [ + "mp4", + "nu-errors", + "nu-plugin", + "nu-protocol", + "nu-source", + "tempfile", +] + [[package]] name = "nu_plugin_from_sqlite" version = "0.32.1" @@ -4143,8 +4169,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" dependencies = [ "autocfg", + "num-bigint 0.3.2", "num-integer", "num-traits 0.2.14", + "serde 1.0.126", ] [[package]] diff --git a/crates/nu-protocol/src/value/primitive.rs b/crates/nu-protocol/src/value/primitive.rs index e0af80d83..7ba2cd28e 100644 --- a/crates/nu-protocol/src/value/primitive.rs +++ b/crates/nu-protocol/src/value/primitive.rs @@ -45,6 +45,7 @@ pub enum Primitive { /// A date value Date(DateTime), /// A count in the number of nanoseconds + #[serde(with = "serde_bigint")] Duration(BigInt), /// A range of values Range(Box), diff --git a/crates/nu_plugin_from_mp4/Cargo.toml b/crates/nu_plugin_from_mp4/Cargo.toml new file mode 100644 index 000000000..5b4193fea --- /dev/null +++ b/crates/nu_plugin_from_mp4/Cargo.toml @@ -0,0 +1,20 @@ +[package] +authors = ["The Nu Project Contributors"] +description = "A converter plugin to the mp4 format for Nushell" +edition = "2018" +license = "MIT" +name = "nu_plugin_from_mp4" +version = "0.1.0" + +[lib] +doctest = false + +[dependencies] +nu-errors = { path = "../nu-errors", version = "0.32.1" } +nu-plugin = { path = "../nu-plugin", version = "0.32.1" } +nu-protocol = { path = "../nu-protocol", version = "0.32.1" } +nu-source = { path = "../nu-source", version = "0.32.1" } +tempfile = "3.2.0" +mp4 = "0.8.2" + +[build-dependencies] diff --git a/crates/nu_plugin_from_mp4/src/from_mp4.rs b/crates/nu_plugin_from_mp4/src/from_mp4.rs new file mode 100644 index 000000000..747e1a167 --- /dev/null +++ b/crates/nu_plugin_from_mp4/src/from_mp4.rs @@ -0,0 +1,174 @@ +use nu_errors::ShellError; +use nu_protocol::{ReturnSuccess, ReturnValue, TaggedDictBuilder, UntaggedValue, Value}; +use nu_source::Tag; +use std::fs::File; +use std::io::Write; +use std::path::Path; + +#[derive(Default)] +pub struct FromMp4 { + pub state: Vec, + pub name_tag: Tag, +} + +impl FromMp4 { + pub fn new() -> Self { + Self { + state: vec![], + name_tag: Tag::unknown(), + } + } +} + +pub fn convert_mp4_file_to_nu_value(path: &Path, tag: Tag) -> Result { + let mp4 = mp4::read_mp4(File::open(path).expect("Could not open mp4 file to read metadata"))?; + + let mut dict = TaggedDictBuilder::new(tag.clone()); + + // Build tracks table + let mut tracks = Vec::new(); + for track in mp4.tracks() { + let mut curr_track_dict = TaggedDictBuilder::new(tag.clone()); + + curr_track_dict.insert_untagged("track id", UntaggedValue::int(track.track_id())); + + curr_track_dict.insert_untagged( + "track type", + match track.track_type() { + Ok(t) => UntaggedValue::string(t.to_string()), + Err(_) => UntaggedValue::from("Unknown"), + }, + ); + + curr_track_dict.insert_untagged( + "media type", + match track.media_type() { + Ok(t) => UntaggedValue::string(t.to_string()), + Err(_) => UntaggedValue::from("Unknown"), + }, + ); + + curr_track_dict.insert_untagged( + "box type", + match track.box_type() { + Ok(t) => UntaggedValue::string(t.to_string()), + Err(_) => UntaggedValue::from("Unknown"), + }, + ); + + curr_track_dict.insert_untagged("width", UntaggedValue::int(track.width())); + curr_track_dict.insert_untagged("height", UntaggedValue::int(track.height())); + curr_track_dict.insert_untagged("frame_rate", UntaggedValue::from(track.frame_rate())); + + curr_track_dict.insert_untagged( + "sample freq index", + match track.sample_freq_index() { + Ok(sfi) => UntaggedValue::string(format!("{}", sfi.freq())), // this is a string for formatting reasons + Err(_) => UntaggedValue::from("Unknown"), + }, + ); + + curr_track_dict.insert_untagged( + "channel config", + match track.channel_config() { + Ok(cc) => UntaggedValue::string(cc.to_string()), + Err(_) => UntaggedValue::from("Unknown"), + }, + ); + + curr_track_dict.insert_untagged("language", UntaggedValue::string(track.language())); + curr_track_dict.insert_untagged("timescale", UntaggedValue::int(track.timescale())); + curr_track_dict.insert_untagged( + "duration", + UntaggedValue::duration(track.duration().as_nanos()), + ); + curr_track_dict.insert_untagged("bitrate", UntaggedValue::int(track.bitrate())); + curr_track_dict.insert_untagged("sample count", UntaggedValue::int(track.sample_count())); + + curr_track_dict.insert_untagged( + "video profile", + match track.video_profile() { + Ok(vp) => UntaggedValue::string(vp.to_string()), + Err(_) => UntaggedValue::from("Unknown"), + }, + ); + + curr_track_dict.insert_untagged( + "audio profile", + match track.audio_profile() { + Ok(ap) => UntaggedValue::string(ap.to_string()), + Err(_) => UntaggedValue::from("Unknown"), + }, + ); + + curr_track_dict.insert_untagged( + "sequence parameter set", + match track.sequence_parameter_set() { + Ok(sps) => UntaggedValue::string(format!("{:X?}", sps)), + Err(_) => UntaggedValue::from("Unknown"), + }, + ); + + curr_track_dict.insert_untagged( + "picture parameter set", + match track.picture_parameter_set() { + Ok(pps) => UntaggedValue::string(format!("{:X?}", pps)), + Err(_) => UntaggedValue::from("Unknown"), + }, + ); + + // push curr track to tracks vec + tracks.push(curr_track_dict.into_value()); + } + + dict.insert_untagged("size", UntaggedValue::big_int(mp4.size())); + + dict.insert_untagged( + "major brand", + UntaggedValue::string(mp4.major_brand().to_string()), + ); + + dict.insert_untagged("minor version", UntaggedValue::int(mp4.minor_version())); + + dict.insert_untagged( + "compatible brands", + UntaggedValue::string(format!("{:?}", mp4.compatible_brands())), + ); + + dict.insert_untagged( + "duration", + UntaggedValue::duration(mp4.duration().as_nanos()), + ); + + dict.insert_untagged("timescale", UntaggedValue::int(mp4.timescale())); + dict.insert_untagged("is fragmented", UntaggedValue::boolean(mp4.is_fragmented())); + dict.insert_untagged("tracks", UntaggedValue::Table(tracks).into_value(&tag)); + + Ok(dict.into_value()) +} + +pub fn from_mp4_bytes_to_value(mut bytes: Vec, tag: Tag) -> Result { + let mut tempfile = tempfile::NamedTempFile::new()?; + tempfile.write_all(bytes.as_mut_slice())?; + match convert_mp4_file_to_nu_value(tempfile.path(), tag) { + Ok(value) => Ok(value), + Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)), + } +} + +pub fn from_mp4(bytes: Vec, name_tag: Tag) -> Result, ShellError> { + match from_mp4_bytes_to_value(bytes, name_tag.clone()) { + Ok(x) => match x { + Value { + value: UntaggedValue::Table(list), + .. + } => Ok(list.into_iter().map(ReturnSuccess::value).collect()), + _ => Ok(vec![ReturnSuccess::value(x)]), + }, + Err(_) => Err(ShellError::labeled_error( + "Could not parse as MP4", + "input cannot be parsed as MP4", + &name_tag, + )), + } +} diff --git a/crates/nu_plugin_from_mp4/src/lib.rs b/crates/nu_plugin_from_mp4/src/lib.rs new file mode 100644 index 000000000..8de6281f2 --- /dev/null +++ b/crates/nu_plugin_from_mp4/src/lib.rs @@ -0,0 +1,4 @@ +mod from_mp4; +mod nu; + +pub use from_mp4::FromMp4; diff --git a/crates/nu_plugin_from_mp4/src/main.rs b/crates/nu_plugin_from_mp4/src/main.rs new file mode 100644 index 000000000..539b494cf --- /dev/null +++ b/crates/nu_plugin_from_mp4/src/main.rs @@ -0,0 +1,6 @@ +use nu_plugin::serve_plugin; +use nu_plugin_from_mp4::FromMp4; + +fn main() { + serve_plugin(&mut FromMp4::new()) +} diff --git a/crates/nu_plugin_from_mp4/src/nu/mod.rs b/crates/nu_plugin_from_mp4/src/nu/mod.rs new file mode 100644 index 000000000..4aa09ca7d --- /dev/null +++ b/crates/nu_plugin_from_mp4/src/nu/mod.rs @@ -0,0 +1,46 @@ +#[cfg(test)] +mod tests; + +use crate::FromMp4; +use nu_errors::ShellError; +use nu_plugin::Plugin; +use nu_protocol::{CallInfo, Primitive, ReturnValue, Signature, UntaggedValue, Value}; +use nu_source::Tag; + +impl Plugin for FromMp4 { + fn config(&mut self) -> Result { + Ok(Signature::build("from mp4") + .desc("Get meta-data of mp4 file") + .filter()) + } + + fn begin_filter(&mut self, call_info: CallInfo) -> Result, ShellError> { + self.name_tag = call_info.name_tag; + Ok(vec![]) + } + + fn filter(&mut self, input: Value) -> Result, ShellError> { + match input { + Value { + value: UntaggedValue::Primitive(Primitive::Binary(b)), + .. + } => { + self.state.extend_from_slice(&b); + } + Value { tag, .. } => { + return Err(ShellError::labeled_error_with_secondary( + "Expected binary from pipeline", + "requires binary input", + self.name_tag.clone(), + "value originates from here", + tag, + )); + } + } + Ok(vec![]) + } + + fn end_filter(&mut self) -> Result, ShellError> { + crate::from_mp4::from_mp4(self.state.clone(), Tag::unknown()) + } +} diff --git a/crates/nu_plugin_from_mp4/src/nu/tests.rs b/crates/nu_plugin_from_mp4/src/nu/tests.rs new file mode 100644 index 000000000..d14c19bee --- /dev/null +++ b/crates/nu_plugin_from_mp4/src/nu/tests.rs @@ -0,0 +1 @@ +mod integration {} diff --git a/src/plugins/nu_plugin_extra_from_mp4.rs b/src/plugins/nu_plugin_extra_from_mp4.rs new file mode 100644 index 000000000..539b494cf --- /dev/null +++ b/src/plugins/nu_plugin_extra_from_mp4.rs @@ -0,0 +1,6 @@ +use nu_plugin::serve_plugin; +use nu_plugin_from_mp4::FromMp4; + +fn main() { + serve_plugin(&mut FromMp4::new()) +}