From 955a5ed8fb30fec10182cf31508a7b33cb055fbd Mon Sep 17 00:00:00 2001 From: Reagan McFarland Date: Wed, 16 Jun 2021 22:18:31 -0400 Subject: [PATCH] Plugin: from_mp4 and UntaggedValue::duration fix (#3618) * plugin: basic from_mp4 implementation This patch introduces a very basic implementation of from_mp4, with only a few bits of meta-data available. The rest of the available meta-data (which is more than half left), will be included in a later patch * Mp4: Almost all track metadata is implemented Only meta-data that is not implemented is duration, facing some weird issue I am going to check on later * Mp4: All meta-data fields implemented All meta-data fields that can be retrieved are now retrieved, with the exception of duration for both tracks and the entire file itself because there is still an issue. However, that will be fixed in the upcoming patches * fix: UntaggedValue::duration() serializes correctly now Previous to this patch, there was an issue where when you would use UntaggedValue::duration() it would result in an invalid JSONRPC resulting string when using the protocol. This patch fixes this issue * Mp4: Duration fixed for file and tracks * plugins: Add plugin extra to src/plugins * Mp4: Replace unwrap() with expect() * Fix: Remove test mp4 file --- Cargo.lock | 28 ++++ crates/nu-protocol/src/value/primitive.rs | 1 + crates/nu_plugin_from_mp4/Cargo.toml | 20 +++ crates/nu_plugin_from_mp4/src/from_mp4.rs | 174 ++++++++++++++++++++++ crates/nu_plugin_from_mp4/src/lib.rs | 4 + crates/nu_plugin_from_mp4/src/main.rs | 6 + crates/nu_plugin_from_mp4/src/nu/mod.rs | 46 ++++++ crates/nu_plugin_from_mp4/src/nu/tests.rs | 1 + src/plugins/nu_plugin_extra_from_mp4.rs | 6 + 9 files changed, 286 insertions(+) create mode 100644 crates/nu_plugin_from_mp4/Cargo.toml create mode 100644 crates/nu_plugin_from_mp4/src/from_mp4.rs create mode 100644 crates/nu_plugin_from_mp4/src/lib.rs create mode 100644 crates/nu_plugin_from_mp4/src/main.rs create mode 100644 crates/nu_plugin_from_mp4/src/nu/mod.rs create mode 100644 crates/nu_plugin_from_mp4/src/nu/tests.rs create mode 100644 src/plugins/nu_plugin_extra_from_mp4.rs diff --git a/Cargo.lock b/Cargo.lock index 7cc597db17..28f07f6851 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 e0af80d838..7ba2cd28e1 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 0000000000..5b4193feaf --- /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 0000000000..747e1a1673 --- /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 0000000000..8de6281f24 --- /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 0000000000..539b494cf6 --- /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 0000000000..4aa09ca7d5 --- /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 0000000000..d14c19bee3 --- /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 0000000000..539b494cf6 --- /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()) +}