Optional field with strict format - rust

I am trying to build nom parser to examine URLs with ID as UUID
rooms/e19c94cf-53eb-4048-9c94-7ae74ff6d912
I created the following:
extern crate uuid;
use uuid::Uuid;
named!(room_uuid<&str, Option<Uuid>>,
do_parse!(
tag_s!("rooms") >>
id: opt!(complete!(preceded!(
tag_s!("/"),
map_res!(take_s!(36), FromStr::from_str)
))) >>
(id)
)
);
It handles almost all cases well:
assert_eq!(room_uuid("rooms"), Done("", None));
assert_eq!(room_uuid("rooms/"), Done("/", None));
assert_eq!(room_uuid("rooms/e19c94cf-53eb-4048-9c94-7ae74ff6d912"), Done("", Some(Uuid::parse_str("e19c94cf-53eb-4048-9c94-7ae74ff6d912").unwrap())));
Except cases where ID is not a valid UUID:
assert!(room_uuid("rooms/123").is_err()); # it fails
# room_uuid("rooms/123").to_result() => Ok(None)
As far as I understand it happens because opt! converts inner Err into None.
I would like to have ID as optional section but if it is present it should be a valid UUID.
Unfortunately, I don't understand how to combine both those things: optionality and strict format.

I've only started working with nom myself in the last couple of weeks but I found one way of solving this. It doesn't fit exclusively within a macro but it does give the correct behavior with one modification. I swallow the / rather than leave it dangling after when a UUID is not given.
#[macro_use]
extern crate nom;
extern crate uuid;
use std::str::FromStr;
use nom::IResult;
use uuid::Uuid;
fn room_uuid(input: &str) -> IResult<&str, Option<Uuid>> {
// Check that it starts with "rooms"
let res = tag_s!(input, "rooms");
let remaining = match res {
IResult::Incomplete(i) => return IResult::Incomplete(i),
IResult::Error(e) => return IResult::Error(e),
IResult::Done(i, _) => i
};
// If a slash is not present, return early
let optional_slash = opt!(remaining, tag_s!("/"));
let remaining = match optional_slash {
IResult::Error(_) |
IResult::Incomplete(_) => return IResult::Done(remaining, None),
IResult::Done(i, _) => i
};
// If something follows a slash, make sure
// it's a valid UUID
if remaining.len() > 0 {
let res = complete!(remaining, map_res!(take_s!(36), FromStr::from_str));
match res {
IResult::Done(i, o) => IResult::Done(i, Some(o)),
IResult::Error(e) => IResult::Error(e),
IResult::Incomplete(n) => IResult::Incomplete(n)
}
} else {
// This branch allows for "rooms/"
IResult::Done(remaining, None)
}
}
#[test]
fn match_room_plus_uuid() {
use nom::IResult::*;
assert_eq!(room_uuid("rooms"), Done("", None));
assert_eq!(room_uuid("rooms/"), Done("", None));
assert_eq!(room_uuid("rooms/e19c94cf-53eb-4048-9c94-7ae74ff6d912"), Done("", Some(Uuid::parse_str("e19c94cf-53eb-4048-9c94-7ae74ff6d912").unwrap())));
assert!(room_uuid("rooms/123").is_err());
}

Given parsing URLs doesn't need a streaming interface you could use synom instead. It was maintained as part of the syn crate, but is sadly not maintained anymore (it was merged into syn and changed to only process rust tokens).
Sadly synom doesn't provide take_s! and eof! (the latter one is going to forbid the trailing "unparsed" 123), but it's easy enough to implement those.
Using eof! also means you can't return an unparsed "/" (although I consider that a good thing); and the nested option! needs some unwrapping at the end (you could return Option<Option<Uuid>> instead to detect the trailing "/").
Playground
#[macro_use]
extern crate synom;
extern crate uuid;
use uuid::Uuid;
macro_rules! take_s {
($i:expr, $length:expr) => {{
let length: usize = $length;
if 0 == length {
synom::IResult::Done($i, "")
} else {
let mut ci = $i.char_indices().skip(length - 1);
match ci.next() {
None => synom::IResult::Error,
Some(_) => {
match ci.next() {
None => synom::IResult::Done("", $i),
Some((pos, _)) => {
let (value, rem) = $i.split_at(pos);
synom::IResult::Done(rem, value)
},
}
}
}
}
}};
}
macro_rules! eof {
($i:expr,) => {{
if $i.is_empty() {
synom::IResult::Done($i, ())
} else {
synom::IResult::Error
}
}};
}
named!(room_uuid -> Option<Uuid>,
do_parse!(
tag!("rooms") >>
id: option!(preceded!(
tag!("/"),
option!(
switch!(map!(take_s!(36), str::parse),
Ok(v) => value!(v)
)
)
)) >>
eof!() >>
(id.unwrap_or(None))
)
);
fn main() {
use synom::IResult::*;
assert_eq!(room_uuid("rooms"), Done("", None));
assert_eq!(room_uuid("rooms/"), Done("", None));
assert_eq!(
room_uuid("rooms/e19c94cf-53eb-4048-9c94-7ae74ff6d912"),
Done(
"",
Some(Uuid::parse_str("e19c94cf-53eb-4048-9c94-7ae74ff6d912").unwrap())
)
);
assert_eq!(room_uuid("rooms/123"), Error);
}

Ok, so I got it working with nom and the extended URL format api/v1/rooms/UUID/tracks/UUID.
The basics are the same as before: you want to check for eof, ignore trailing "/" and never wait for incomplete results (alt_complete! is doing a good job here).
Regarding your ErrorKind::Verify wish: I don't think the error kind is actually important, just ignore it, or map it to whatever you want manually.
Be careful with the alt_complete! branches: in case of overlaps the preferred option (usually the "longer one") should come first.
I like my with! helper, but you could also inline it.
Playground doesn't support nom, so no link this time.
#[macro_use]
extern crate nom;
extern crate uuid;
use uuid::Uuid;
named!(uuid<&str, Uuid>, preceded!(
tag_s!("/"),
map_res!(take_s!(36), str::parse)
));
#[derive(Clone, PartialEq, Eq, Debug)]
enum ApiRequest {
Rooms,
Room { room: Uuid },
Tracks { room: Uuid },
Track { room: Uuid, track: Uuid },
}
/// shortcut for: `do_parse!(name: expr >> r: otherexpr >> (r))`
///
/// `otherexpr` should use `name`, otherwise you could just use `preceded!`.
macro_rules! with {
($i:expr, $var:ident: $submac:ident!( $($args:tt)* ) >> $($rest:tt)*) => {
do_parse!($i, $var: $submac!($($args)*) >> r: $($rest)* >> (r));
};
($i:expr, $var:ident: $submac:ident >> $($rest:tt)*) => {
do_parse!($i, $var: $submac >> r: $($rest)* >> (r));
};
}
// /api/v1/rooms/UUID/tracks/UUID
named!(apiv1<&str, ApiRequest>, preceded!(tag_s!("/api/v1"),
alt_complete!(
preceded!(tag_s!("/rooms"), alt_complete!(
with!(room: uuid >> alt_complete!(
preceded!(tag_s!("/tracks"), alt_complete!(
with!(track: uuid >> alt_complete!(
// ... sub track requests?
value!(ApiRequest::Track{room, track})
))
|
value!(ApiRequest::Tracks{room})
))
// other room requests
|
value!(ApiRequest::Room{room})
))
|
value!(ApiRequest::Rooms)
))
// | ... other requests
)
));
named!(api<&str, ApiRequest>, terminated!(
alt_complete!(
apiv1
// | ... other versions
// also could wrap in new enum like:
// apiv1 => { ApiRequest::V1 }
// |
// apiv2 => { ApiRequest::V2 }
),
tuple!(
alt_complete!(tag_s!("/") | value!("")), // ignore trailing "/"
eof!() // make sure full URL was parsed
)
));
fn main() {
use nom::IResult::*;
use nom::ErrorKind;
let room = Uuid::parse_str("e19c94cf-53eb-4048-9c94-7ae74ff6d912").unwrap();
let track = Uuid::parse_str("83d235e8-03cd-420d-a8c6-6e42440a5573").unwrap();
assert_eq!(api("/api/v1/rooms"), Done("", ApiRequest::Rooms));
assert_eq!(api("/api/v1/rooms/"), Done("", ApiRequest::Rooms));
assert_eq!(
api("/api/v1/rooms/e19c94cf-53eb-4048-9c94-7ae74ff6d912"),
Done("", ApiRequest::Room { room })
);
assert_eq!(
api("/api/v1/rooms/e19c94cf-53eb-4048-9c94-7ae74ff6d912/"),
Done("", ApiRequest::Room { room })
);
assert_eq!(
api("/api/v1/rooms/e19c94cf-53eb-4048-9c94-7ae74ff6d912/tracks"),
Done("", ApiRequest::Tracks { room })
);
assert_eq!(
api("/api/v1/rooms/e19c94cf-53eb-4048-9c94-7ae74ff6d912/tracks/"),
Done("", ApiRequest::Tracks { room })
);
assert_eq!(
api("/api/v1/rooms/e19c94cf-53eb-4048-9c94-7ae74ff6d912/tracks/83d235e8-03cd-420d-a8c6-6e42440a5573"),
Done("", ApiRequest::Track{room, track})
);
assert_eq!(
api("/api/v1/rooms/e19c94cf-53eb-4048-9c94-7ae74ff6d912/tracks/83d235e8-03cd-420d-a8c6-6e42440a5573/"),
Done("", ApiRequest::Track{room, track})
);
assert_eq!(api("/api/v1"), Error(ErrorKind::Alt));
assert_eq!(api("/api/v1/foo"), Error(ErrorKind::Alt));
assert_eq!(api("/api/v1/rooms/123"), Error(ErrorKind::Eof));
assert_eq!(
api("/api/v1/rooms/e19c94cf-53eb-4048-9c94-7ae74ff6d912/bar"),
Error(ErrorKind::Eof)
);
assert_eq!(
api("/api/v1/rooms/e19c94cf-53eb-4048-9c94-7ae74ff6d912/tracks/83d235e8-03cd-420d-a8c6-6e42440a5573/123"),
Error(ErrorKind::Eof)
);
assert_eq!(api("/api/v2"), Error(ErrorKind::Alt));
}
You could also use a more strict alt_full_opt_slash! branch method, which would ensure a branch only matches if it fully parsed the input.
You could then use a more "flat" way (although nested branches should still be working) to parse the alternatives (although this means you might end up parsing some UUIDs more than once; also now all errors are of kind Alt):
/// Similar to alt_complete, but also requires the branch parses until
/// the end of the input (but ignores a trailing "/").
macro_rules! alt_full_opt_slash {
(__impl_push2 ($i:expr,) ($($new:tt)*), $($rest:tt)*) => {
alt_full_opt_slash!(__impl ($i, $($new)*), $($rest)*)
};
(__impl_push2 ($i:expr, $($result:tt)+) ($($new:tt)*), $($rest:tt)*) => {
alt_full_opt_slash!(__impl ($i, $($result)+ | $($new)*), $($rest)*)
};
(__impl_push ($($result:tt)*) ($($new:tt)*), $($rest:tt)*) => {
// modify branch:
alt_full_opt_slash!(__impl_push2 ($($result)*) (
terminated!(
$($new)*,
tuple!(
alt_complete!(tag_s!("/") | value!("")), // ignore trailing "/"
eof!() // make sure full URL was parsed
)
)
), $($rest)*)
};
(__impl ($($result:tt)*), $e:ident | $($rest:tt)*) => {
alt_full_opt_slash!(__impl_push ($($result)*) ( $e ), $($rest)*)
};
(__impl ($($result:tt)*), $subrule:ident!( $($args:tt)*) | $($rest:tt)*) => {
alt_full_opt_slash!(__impl_push ($($result)*) ( $subrule!($($args)*) ), $($rest)*)
};
(__impl ($($result:tt)*), $subrule:ident!( $($args:tt)* ) => { $gen:expr } | $($rest:tt)*) => {
alt_full_opt_slash!(__impl_push ($($result)*) ( $subrule!($($args)*) => { $gen } ), $($rest)*)
};
(__impl ($($result:tt)*), $e:ident => { $gen:expr } | $($rest:tt)*) => {
alt_full_opt_slash!(__impl_push ($($result)*) ( $e => { $gen } ), $($rest)*)
};
(__impl ($i:expr, $($result:tt)*), __end) => {
alt_complete!($i, $($result)*)
};
($i:expr, $($rest:tt)*) => {{
alt_full_opt_slash!(__impl ($i, ), $($rest)* | __end)
}};
}
// /api/v1/rooms/UUID/tracks/UUID
named!(apiv1<&str, ApiRequest>, preceded!(tag_s!("/api/v1"),
alt_full_opt_slash!(
do_parse!(
tag_s!("/rooms") >>
(ApiRequest::Rooms)
)
|
do_parse!(
tag_s!("/rooms") >>
room: uuid >>
(ApiRequest::Room{room})
)
|
do_parse!(
tag_s!("/rooms") >>
room: uuid >>
tag_s!("/tracks") >>
(ApiRequest::Tracks{room})
)
|
do_parse!(
tag_s!("/rooms") >>
room: uuid >>
tag_s!("/tracks") >>
track: uuid >>
(ApiRequest::Track{room, track})
)
)
));
named!(api<&str, ApiRequest>, alt_complete!(
apiv1
// | ... other versions
// also could wrap in new enum like:
// apiv1 => { ApiRequest::V1 }
// |
// apiv2 => { ApiRequest::V2 }
));

Related

How to match a struct instantiation with macro_rules

Since this took me a while to figure out, I may as well share how I fixed it.
I was trying to wrap every item on a struct with some function, in my case Arc::new(Mutex::new(item)) with macro_rules
My initial attempt was many variations on this:
macro_rules! decl_sr {
(
$name:ident {
$( $it:ident : $value:expr) ,*
}
) => {
$name {
$( $it: Arc::new(Mutex::new( $value )) ),*
}
};
}
And the idea was to use it like this:
let mut value = decl_sr!{
StructName {
field_1: Value1::from_function_call(parameter1, parameter2),
// -- snip
field_n: ValueN::from_function_call(parameter1, parameter2),
}
}
So it actually resulted in this:
let mut value = decl_sr!{
StructName {
field_1: Arc::new(Mutex::new(Value1::from_function_call(parameter1, parameter2))),
// -- snip
field_n: Arc::new(Mutex::new(ValueN::from_function_call(parameter1, parameter2))),
}
}
The correct answer was this:
macro_rules! decl_sr {
(
$name:ident {
$( $it:ident : $value:expr, )*
}
) => {
$name {
$( $it: Arc::new(Mutex::new( $value )) ),*
}
};
With the comma (',') inside the repetition pattern.
Otherwise it would throw the classic macro error no rules expected token '}'. -Z macro-backtrace and trace_macros!(true); didn't help at all.
The only thing that tipped me off was this other question, but it wasn't exactly about this.
I may have picked too loose/bad fragment specifiers, feel free to correct me/suggest better options in the comments. Most of the time trying to make this works was thinking they were the problem, not the comma so I was glad it worked at all.

Accept multiple values on proc macro attribute

I wanted to be able to retrieve the content from an attribute like this:
#[foreign_key(table = "some_table", column = "some_column")]
This is how I am trying:
impl TryFrom<&&Attribute> for EntityFieldAnnotation {
type Error = syn::Error;
fn try_from(attribute: &&Attribute) -> Result<Self, Self::Error> {
if attribute.path.is_ident("foreign_key") {
match attribute.parse_args()? {
syn::Meta::NameValue(nv) =>
println!("NAME VALUE: {:?}, {:?}, {:?}",
nv.path.get_ident(),
nv.eq_token.to_token_stream(),
nv.lit.to_token_stream(),
),
_ => println!("Not interesting")
}
} else {
println!("No foreign key")
}
// ... More Rust code
}
Everything works fine if I just put in there only one NameValue. When I add the comma,
everything brokes.
The only error:
error: unexpected token
How can I fix my logic to enable the possibility of have more than just one NameValue?
Thanks
UPDATE: While writing this answer, I had forgotten that Meta has List variant as well which gives you NestedMeta. I would generally prefer doing that instead of what I did in the answer below for more flexibility.
Although, for your particular case, using Punctuated still seems simpler and cleaner to me.
MetaNameValue represents only a single name-value pair. In your case it is delimited by ,, so, you need to parse all of those delimited values as MetaNameValue instead.
Instead of calling parse_args, you can use parse_args_with along with Punctuated::parse_terminated:
use syn::{punctuated::Punctuated, MetaNameValue, Token};
let name_values: Punctuated<MetaNameValue, Token![,]> = attribute.parse_args_with(Punctuated::parse_terminated).unwrap(); // handle error instead of unwrap
Above name_values has type Punctuated which is an iterator. You can iterate over it to get various MetaNameValue in your attribute.
Updates based on comments:
Getting value out as String from MetaNameValue:
let name_values: Result<Punctuated<MetaNameValue, Token![,]>, _> = attr.parse_args_with(Punctuated::parse_terminated);
match name_values {
Ok(name_value) => {
for nv in name_value {
println!("Meta NV: {:?}", nv.path.get_ident());
let value = match nv.lit {
syn::Lit::Str(v) => v.value(),
_ => panic!("expeced a string value"), // handle this err and don't panic
};
println!( "Meta VALUE: {:?}", value )
}
},
Err(_) => todo!(),
};

Dynamically creating parameters in nested rust macros

I've been tinkering around with Rust's macro system for a while now, and recently got interested in nesting two macros together, like this:
macro_rules! foo {
() => {
macro_rules! bar {
() => {}
}
}
}
Relating to the example, I wanted to dynamically make parameter names in bar! which were passed into foo!, to obtain a result like this:
foo!(bar, baz);
// The above call creates a macro, bar!, with the following definition:
macro_rules! bar {
( $bar:literal, $baz:literal ) => {
println!("{}", stringify!( $bar, $baz ));
}
}
To give a better idea of what I'm trying to do, here was my initial thought process on how this would work (this should parse exactly to the definition shown above):
macro_rules! foo {
( $( $attr:ident ), * ) => {
macro_rules! bar {
// the designator $$attr:literal consists of two parts - $attr,
// which should be replaced with the arguments passed into foo!,
// and $__:literal, which creates a literal designator for each of
// the arguments from foo! for bar!
( $( $$attr:literal ), * ) => {
// $( $$attr ), * follows the same logic as above
println!("{}", stringify!( $( $$attr ), * ));
}
}
}
}
This does look very weird, and sure enough, it didn't work, giving an error mentioning meta-variable expressions and this issue, both of which looked unrelated (full error can be seen on the playground).
Does anyone know if it is possible to dynamically create a macro with variables like this, and if so, how to do it?
Yes, however...
You cannot insert the $ sign, as it is reserved for metavariables.
You have two options to tackle that.
On stable, you need to pass $ to the macro. Then it can refer to it using the metavariable.
macro_rules! foo {
( $dollar:tt $( $attr:ident ), * ) => {
macro_rules! bar {
( $( $dollar $attr:literal ), * ) => {
println!("{}", stringify!( $( $dollar $attr ), * ));
}
}
}
}
foo!($ bar, baz);
Playground.
On nightly, you can escape the dollar sign: this is part of the feature macro_metavar_expr the compiler mentioned. You do it using $$:
#![feature(macro_metavar_expr)]
macro_rules! foo {
( $( $attr:ident ), * ) => {
macro_rules! bar {
( $( $$ $attr:literal ), * ) => {
println!("{}", stringify!( $( $$ $attr ), * ));
}
}
}
}
foo!(bar, baz);
Playground.

Question on invoking another macro_rules in macro_rules definition

I'm implementing writing TLV packet to somewhat impl std::io::Write.
First I implement WriteBE<T> trait, whose write_be(&mut self, data: T) method can write data with type T to Self. (implementation details omitted)
And I'm trying to use macro_rules! to implement calculation of total packet length in compile time (because most packets have fixed length in my case). macros are as follows:
macro_rules! len_in_expr {
(
self.write_be( $data: expr $(,)? ) $(?)? $(;)*
) => {
std::mem::size_of_val(&$data)
};
(
write_be(self, $data: expr $(,)? ) $(?)? $(;)*
) => {
std::mem::size_of_val(&$data)
};
(
$other: expr
) => {
0
};
}
/// calculate total write size in block
macro_rules! tlv_len_in_block {
({
$( $e: expr );* $(;)?
}) => {
0 $(
+ ( len_in_expr!($e) )
)*
};
}
But when I calculating total length like this:
fn main() {
let y = tlv_len_in_block!({
write_be(self, 0u32,)?;
});
println!("y={}", y);
}
I get a result 0.
If I comment the $other: expr match arm, I get a compile error:
6 | macro_rules! len_in_expr {
| ------------------------ when calling this macro
...
30 | + ( len_in_expr!($e) )
| ^^ no rules expected this token in macro call
...
39 | let y = tlv_len_in_block!({
| _____________-
40 | | write_be(self, 0u32,)?;
41 | | });
| |______- in this macro invocation
What's the problem with my code? And how can I fix it?
Once metavariables inside macro_rules! are captured into some fragment specifier (e.g. expr), they cannot be decomposed anymore. Quoting the reference:
When forwarding a matched fragment to another macro-by-example, matchers in the second macro will see an opaque AST of the fragment type. The second macro can't use literal tokens to match the fragments in the matcher, only a fragment specifier of the same type. The ident, lifetime, and tt fragment types are an exception, and can be matched by literal tokens. The following illustrates this restriction:
macro_rules! foo {
($l:expr) => { bar!($l); }
// ERROR: ^^ no rules expected this token in macro call
}
macro_rules! bar {
(3) => {}
}
foo!(3);
The following illustrates how tokens can be directly matched after matching a tt fragment:
// compiles OK
macro_rules! foo {
($l:tt) => { bar!($l); }
}
macro_rules! bar {
(3) => {}
}
foo!(3);
Once tlv_len_in_block!() captured write_be(self, 0u32,)? inside $e, it cannot be decomposed into write_be(self, $data:expr $(,)? ) and thus, cannot be matched by the second case of the len_in_expr!()` macro, as it should have been.
There are generally two solutions to this problem:
The first is, if possible, decomposing them from the beginning. The problem is that this is not always possible. In this case, for example, I don't see a way for that to work.
The second way is much more complicated and it is using the Push-down Accumulation technique together with tt Munching.
The idea is as follows: instead of parsing the input as whole, we parse each piece one at a time. Then, recursively, we forward the parsed bits and the yet-to-parse bit to ourselves. We also should have a stop condition on an empty input.
Here is how it will look like in your example:
macro_rules! tlv_len_in_block_impl {
// Stop condition - no input left to parse.
(
parsed = [ $($parsed:tt)* ]
rest = [ ]
) => {
$($parsed)*
};
(
parsed = [ $($parsed:tt)* ]
rest = [
self.write_be( $data:expr $(,)? ) $(?)? ;
$($rest:tt)*
]
) => {
tlv_len_in_block_impl!(
parsed = [
$($parsed)*
+ std::mem::size_of_val(&$data)
]
rest = [ $($rest)* ]
)
};
(
parsed = [ $($parsed:tt)* ]
rest = [
write_be(self, $data:expr $(,)? ) $(?)? ;
$($rest:tt)*
]
) => {
tlv_len_in_block_impl!(
parsed = [
$($parsed)*
+ std::mem::size_of_val(&$data)
]
rest = [ $($rest)* ]
)
};
}
/// calculate total write size in block
macro_rules! tlv_len_in_block {
({
$($input:tt)*
}) => {
tlv_len_in_block_impl!(
parsed = [ 0 ]
rest = [ $($input)* ]
)
};
}
(Note that this is not exactly the same as your macro - mine requires a trailing semicolon, while in yours it's optional. It's possible to make it optional here, too, but it will be much more complicated.
Playground.

Gstreamer Editing Service rust bindings error while setting render settings

I'm very new to gstreamer and rust and am trying to render a video made from sections of other videos. Based on the docs, gstreamer-rs examples, and this question about doing the same thing in python, I think my code looks pretty good, but throws errors.
This is my code:
use gstreamer as gst;
use gstreamer::{ElementExt, ElementExtManual, GstObjectExt};
use gstreamer_editing_services as ges;
use gstreamer_editing_services::{GESPipelineExt, LayerExt, TimelineExt};
use gstreamer_pbutils as gst_pbutils;
use gstreamer_pbutils::{EncodingProfileBuilder};
pub fn clip_video() {
match gst::init() {
Err(e) => eprintln!("{:?}", e),
_ => (),
}
match ges::init() {
Err(e) => eprintln!("{:?}", e),
_ => (),
}
let timeline = ges::Timeline::new_audio_video();
let layer = timeline.append_layer();
let pipeline = ges::Pipeline::new();
match pipeline.set_timeline(&timeline) {
Err(e) => eprintln!("{:?}", e),
_ => (),
}
let video_profile = gst_pbutils::EncodingVideoProfileBuilder::new()
.name("h.264")
.description("h.264-profile")
.format(&gst::caps::Caps::new_simple("video/x-h264", &[]))
.build()
.unwrap();
let audio_profile = gst_pbutils::EncodingAudioProfileBuilder::new()
.name("mp3")
.description("mp3-profile")
.format(&gst::caps::Caps::new_simple(
"audio/mpeg",
&[("mpegversion", &"1"), ("layer", &"3")],
))
.build()
.unwrap();
let contianer_profile = gst_pbutils::EncodingContainerProfileBuilder::new()
.name("default-mp4-profile")
.description("mp4-with-h.264-mp3")
.format(&gst::caps::Caps::new_simple(
"video/quicktime",
&[("variant", &"iso")],
))
.enabled(true)
.add_profile(&video_profile)
.add_profile(&audio_profile)
.build()
.unwrap();
let asset = ges::UriClipAsset::request_sync("file:///home/ryan/repos/auto-highlighter-processing-service/input/test-video.mp4").expect("Failed to create asset");
match layer.add_asset(
&asset,
0 * gst::SECOND,
10 * gst::SECOND,
10 * gst::SECOND,
ges::TrackType::CUSTOM,
) {
Err(e) => eprintln!("{:?}", e),
_ => (),
}
match pipeline.set_render_settings("file:///home/ryan/repos/auto-highlighter-processing-service/output/test-video.mp4", &contianer_profile){
Err(e) => eprintln!("{:?}", e),
_ => (),
}
match pipeline.set_mode(ges::PipelineFlags::RENDER) {
Err(e) => eprintln!("{:?}", e),
_ => (),
}
match pipeline.set_state(gst::State::Playing) {
Err(e) => eprintln!("{:?}", e),
_ => (),
}
let bus = pipeline.get_bus().unwrap();
for msg in bus.iter_timed(gst::CLOCK_TIME_NONE) {
use gst::MessageView;
match msg.view() {
MessageView::Eos(..) => break,
MessageView::Error(err) => {
println!(
"Error from {:?}: {} ({:?})",
err.get_src().map(|s| s.get_path_string()),
err.get_error(),
err.get_debug()
);
break;
}
_ => (),
}
}
}
The errors that I am getting:
BoolError { message: "Failed to set render settings", filename: "/home/ryan/.cargo/registry/src/github.com-1ecc6299db9ec823/gstreamer-editing-services-0.16.5/src/auto/pipeline.rs", function: "gstreamer_editing_services::auto::pipeline", line: 228 }
StateChangeError
I'm struggling to find what to do about these errors or what the problem could be. From what I know I'm using the set_render_settings() and set_mode() functions correctly.
I didn't try running your code, but one problem I found when reading was the following
.format(&gst::caps::Caps::new_simple(
"audio/mpeg",
&[("mpegversion", &"1"), ("layer", &"3")],
))
The "mpegversion" and "layer" fields of the caps are not strings but integers. If you use them as such it should work (or at least work better)
.format(&gst::caps::Caps::new_simple(
"audio/mpeg",
&[("mpegversion", &1i32), ("layer", &3i32)],
))
Everything else looks correct to me.
You can find more details about such errors by making use of the GStreamer debugging system. You can enable that via the GST_DEBUG environment variable, e.g. by setting that to 6.
Although this answer is over a year late, I thought I'd post anyway, as examples in Rust for GES are sparse, with only a single (though good) example of applying a 'agingtv' effect on the gstreamer-rs repo. Additionally, the OP's sample code above will result in rendering 10 seconds of black video, and does not, as the OP mentioned, result in the desired output.
Using the example listed in the original question above:
On gstreamer-rs 0.19:
Build video profile (note the caps now must be passed via builder() ):
let video_profile = gstreamer_pbutils::EncodingVideoProfile::builder(
&gst::Caps::builder("video/x-h264").build(),
)
.name("video_profile")
.build();
Build the Audio profile:
let audio_profile = gstreamer_pbutils::EncodingAudioProfile::builder(
&gstreamer::Caps::new_simple("audio/x-aac", &[]),
)
.name("audio_profile")
.build();
Build the Container profile:
let container_profile = gstreamer_pbutils::EncodingContainerProfile::builder(
&gstreamer::Caps::new_simple("video/x-matroska", &[]),
)
.name("container_profile")
.add_profile(&audio_profile)
.add_profile(&video_profile)
.build();
Note: as an alternative you can build the whole encoding profile in one go from the DiscovererInfo if you ran the gst-discover on the media first. This will result in an output file very similar to the input file in it's encoding settings.
let encoding_profile =
gstreamer_pbutils::EncodingProfile::from_discoverer(&m_info.discover_info)?;
The following example will clip the video, add a transition, and merge in an additional clip to fade to:
let timeline = ges::Timeline::new_audio_video();
timeline.set_auto_transition(true);
let layer = timeline.append_layer();
let pipeline = ges::Pipeline::new();
pipeline.set_timeline(&timeline)?;
let audio_profile = gstreamer_pbutils::EncodingAudioProfile::builder(
&gstreamer::Caps::new_simple("audio/x-aac", &[]),
)
.name("audio_profile")
.build();
let video_profile = gstreamer_pbutils::EncodingVideoProfile::builder(
&gst::Caps::builder("video/x-h264").build(),
)
.name("video_profile")
.build();
let container_profile = gstreamer_pbutils::EncodingContainerProfile::builder(
&gstreamer::Caps::new_simple("video/x-matroska", &[]),
)
.name("container_profile")
.add_profile(&audio_profile)
.add_profile(&video_profile)
.build();
/* alternatively
let encoding_profile = gstreamer_pbutils::EncodingProfile::from_discoverer(&m_info.discover_info)?;
*/
/* original video */
let clip = ges::UriClip::new("file:///home/ryan/repos/auto-highlighter-processing-service/input/test-video.mp4")?;
layer.add_clip(&clip)?;
clip.set_inpoint(gst::ClockTime::from_seconds(0));
clip.set_duration(gst::ClockTime::from_seconds(10));
/* video to transition to with a fade */
let clip_transition_to = ges::UriClip::new("/some/2/second/video/file.mp4")?;
clip_transition_to.set_start(gst::ClockTime::from_seconds(9)); //this should overlap the original video clip, but not completely
clip_transition_to.set_inpoint(gst::ClockTime::from_seconds(0));
clip_transition_to.set_duration(gst::ClockTime::from_seconds(2));
layer.add_clip(&clip_transition_to)?;
pipeline.set_render_settings("file:///home/ryan/repos/auto-highlighter-processing-service/output/test-video.mp4", &container_profile)?; //or &encoding_profile
pipeline.set_mode(ges::PipelineFlags::RENDER)?;
pipeline.set_state(gst::State::Playing)?;
let bus = pipeline
.bus()
.expect("Pipeline without bus. Shouldn't happen!");
for msg in bus.iter_timed(gst::ClockTime::NONE) {
use gst::MessageView;
match msg.view() {
MessageView::Eos(..) => break,
MessageView::Error(err) => {
pipeline.set_state(gst::State::Null)?;
match err.details() {
Some(details) if details.name() == "error-details" => details
.get::<&ErrorValue>("error")
.unwrap()
.clone()
.0
.lock()
.unwrap()
.take()
.map(Result::Err)
.expect("error-details message without actual error"),
_ => Err({
let err_src = msg
.src()
.map(|s| String::from(s.path_string()))
.unwrap_or_else(|| String::from("None"));
log!(
Level::Error,
"A GStreamer Error was Encountered {}",
&err_src
);
ErrorMessage {
src: err_src,
error: err.error().to_string(),
debug: err.debug(),
source: err.error(),
}
.into()
}),
}?;
}
MessageView::StateChanged(state_changed) => {
// if state_changed.src().map(|s| s == decodebin).unwrap_or(false)
// && state_changed.current() == gst::State::Playing
// {
// // Generate a dot graph of the pipeline to GST_DEBUG_DUMP_DOT_DIR if defined
// let bin_ref = decodebin.downcast_ref::<gst::Bin>().unwrap();
// bin_ref.debug_to_dot_file(gst::DebugGraphDetails::all(), "PLAYING");
// }
let msg = format!(
"State changed from {:?}: {:?} -> {:?} ({:?})",
state_changed.src().map(|s| s.path_string()),
state_changed.old(),
state_changed.current(),
state_changed.pending()
);
log!(Level::Debug, "{}", msg)
}
_ => (),
}
}
let log_msg = "Play run complete, changing state...";
log!(Level::Info, "{}", &log_msg);
pipeline.set_state(gst::State::Null)?;
The result will be a 10 second video, with a fade out to a 2 second video (e.g. something that says "the end", etc).
I hope this helps someone. It took a some reading and research to achieve the desired effect, and hopefully this will help someone else on the same journey.
gstreamer is excellent software and, although it's been a bit of work to get things functional, it's been great to work with the rust bindings.

Resources