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
This commit is contained in:
Reagan McFarland 2021-06-16 22:18:31 -04:00 committed by GitHub
parent a59414203f
commit 955a5ed8fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 286 additions and 0 deletions

28
Cargo.lock generated
View File

@ -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]]

View File

@ -45,6 +45,7 @@ pub enum Primitive {
/// A date value
Date(DateTime<FixedOffset>),
/// A count in the number of nanoseconds
#[serde(with = "serde_bigint")]
Duration(BigInt),
/// A range of values
Range(Box<Range>),

View File

@ -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]

View File

@ -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<u8>,
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<Value, mp4::Error> {
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<u8>, tag: Tag) -> Result<Value, std::io::Error> {
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<u8>, name_tag: Tag) -> Result<Vec<ReturnValue>, 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,
)),
}
}

View File

@ -0,0 +1,4 @@
mod from_mp4;
mod nu;
pub use from_mp4::FromMp4;

View File

@ -0,0 +1,6 @@
use nu_plugin::serve_plugin;
use nu_plugin_from_mp4::FromMp4;
fn main() {
serve_plugin(&mut FromMp4::new())
}

View File

@ -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<Signature, ShellError> {
Ok(Signature::build("from mp4")
.desc("Get meta-data of mp4 file")
.filter())
}
fn begin_filter(&mut self, call_info: CallInfo) -> Result<Vec<ReturnValue>, ShellError> {
self.name_tag = call_info.name_tag;
Ok(vec![])
}
fn filter(&mut self, input: Value) -> Result<Vec<ReturnValue>, 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<Vec<ReturnValue>, ShellError> {
crate::from_mp4::from_mp4(self.state.clone(), Tag::unknown())
}
}

View File

@ -0,0 +1 @@
mod integration {}

View File

@ -0,0 +1,6 @@
use nu_plugin::serve_plugin;
use nu_plugin_from_mp4::FromMp4;
fn main() {
serve_plugin(&mut FromMp4::new())
}