1use std::collections::BTreeMap;
2
3use renderbox_dsl::ParamValue;
4
5use crate::ops::{add_video_filter, params, ExprNum, OptParam};
6use crate::sorts::Video;
7use crate::graph::Stream;
8
9pub fn scale(w: u32, h: u32) -> impl Fn(Stream<Video>) -> Stream<Video> {
14 move |input| add_video_filter(input, "scale", params! { "w" => w, "h" => h })
15}
16
17pub fn crop(w: u32, h: u32) -> impl Fn(Stream<Video>) -> Stream<Video> {
18 move |input| add_video_filter(input, "crop", params! { "w" => w, "h" => h })
19}
20
21pub fn crop_at(w: u32, h: u32, x: u32, y: u32) -> impl Fn(Stream<Video>) -> Stream<Video> {
22 move |input| {
23 add_video_filter(input, "crop", params! { "w" => w, "h" => h, "x" => x, "y" => y })
24 }
25}
26
27pub fn pad(w: u32, h: u32) -> impl Fn(Stream<Video>) -> Stream<Video> {
28 move |input| {
29 add_video_filter(
30 input,
31 "pad",
32 params! { "w" => ExprNum(w as f64), "h" => ExprNum(h as f64) },
33 )
34 }
35}
36
37pub fn pad_at(
38 w: u32,
39 h: u32,
40 x: u32,
41 y: u32,
42 color: &str,
43) -> impl Fn(Stream<Video>) -> Stream<Video> {
44 let color = color.to_string();
45 move |input| {
46 add_video_filter(
47 input,
48 "pad",
49 params! { "w" => ExprNum(w as f64), "h" => ExprNum(h as f64), "x" => ExprNum(x as f64), "y" => ExprNum(y as f64), "color" => color.as_str() },
50 )
51 }
52}
53
54pub fn rotate(angle: f64) -> impl Fn(Stream<Video>) -> Stream<Video> {
55 move |input| add_video_filter(input, "rotate", params! { "angle" => angle })
56}
57
58pub fn hflip() -> impl Fn(Stream<Video>) -> Stream<Video> {
59 move |input| add_video_filter(input, "hflip", params! {})
60}
61
62pub fn vflip() -> impl Fn(Stream<Video>) -> Stream<Video> {
63 move |input| add_video_filter(input, "vflip", params! {})
64}
65
66#[derive(Debug, Clone, Copy)]
67pub enum Transpose {
68 CClockFlip,
69 Clock,
70 CClock,
71 ClockFlip,
72}
73
74impl Transpose {
75 fn as_value(&self) -> i32 {
76 match self {
77 Transpose::CClockFlip => 0,
78 Transpose::Clock => 1,
79 Transpose::CClock => 2,
80 Transpose::ClockFlip => 3,
81 }
82 }
83}
84
85pub fn transpose(dir: Transpose) -> impl Fn(Stream<Video>) -> Stream<Video> {
86 move |input| add_video_filter(input, "transpose", params! { "dir" => dir.as_value() })
87}
88
89pub fn format(pix_fmt: &str) -> impl Fn(Stream<Video>) -> Stream<Video> {
90 let pix_fmt = pix_fmt.to_string();
91 move |input| add_video_filter(input, "format", params! { "pix_fmts" => pix_fmt.as_str() })
92}
93
94#[derive(Default)]
99pub struct Eq {
100 pub brightness: Option<f64>,
101 pub contrast: Option<f64>,
102 pub saturation: Option<f64>,
103 pub gamma: Option<f64>,
104}
105
106pub fn eq(opts: Eq) -> impl Fn(Stream<Video>) -> Stream<Video> {
107 move |input| {
108 let mut p = BTreeMap::new();
109 opts.brightness.insert_into("brightness", &mut p);
110 opts.contrast.insert_into("contrast", &mut p);
111 opts.saturation.insert_into("saturation", &mut p);
112 opts.gamma.insert_into("gamma", &mut p);
113 add_video_filter(input, "eq", p)
114 }
115}
116
117#[derive(Default)]
118pub struct Hue {
119 pub h: Option<f64>,
120 pub s: Option<f64>,
121 pub b: Option<f64>,
122}
123
124pub fn hue(opts: Hue) -> impl Fn(Stream<Video>) -> Stream<Video> {
125 move |input| {
126 let mut p = BTreeMap::new();
127 opts.h.insert_into("h", &mut p);
128 opts.s.insert_into("s", &mut p);
129 opts.b.insert_into("b", &mut p);
130 add_video_filter(input, "hue", p)
131 }
132}
133
134#[derive(Debug, Clone, Copy)]
135pub enum CurvePreset {
136 None,
137 ColorNegative,
138 CrossProcess,
139 Darker,
140 IncreaseContrast,
141 Lighter,
142 LinearContrast,
143 MediumContrast,
144 Negative,
145 StrongContrast,
146 Vintage,
147}
148
149impl CurvePreset {
150 fn as_str(&self) -> &'static str {
151 match self {
152 CurvePreset::None => "none",
153 CurvePreset::ColorNegative => "color_negative",
154 CurvePreset::CrossProcess => "cross_process",
155 CurvePreset::Darker => "darker",
156 CurvePreset::IncreaseContrast => "increase_contrast",
157 CurvePreset::Lighter => "lighter",
158 CurvePreset::LinearContrast => "linear_contrast",
159 CurvePreset::MediumContrast => "medium_contrast",
160 CurvePreset::Negative => "negative",
161 CurvePreset::StrongContrast => "strong_contrast",
162 CurvePreset::Vintage => "vintage",
163 }
164 }
165}
166
167pub fn curves(preset: CurvePreset) -> impl Fn(Stream<Video>) -> Stream<Video> {
168 move |input| add_video_filter(input, "curves", params! { "preset" => preset.as_str() })
169}
170
171pub fn lut3d(file: &str) -> impl Fn(Stream<Video>) -> Stream<Video> {
172 let file = file.to_string();
173 move |input| add_video_filter(input, "lut3d", params! { "file" => file.as_str() })
174}
175
176pub fn colorgrade(lut: &str) -> impl Fn(Stream<Video>) -> Stream<Video> {
177 let lut = lut.to_string();
178 move |input| add_video_filter(input, "colorgrade", params! { "lut" => lut.as_str() })
179}
180
181#[derive(Default)]
182pub struct ColorBalance {
183 pub rs: Option<f64>,
184 pub gs: Option<f64>,
185 pub bs: Option<f64>,
186 pub rm: Option<f64>,
187 pub gm: Option<f64>,
188 pub bm: Option<f64>,
189 pub rh: Option<f64>,
190 pub gh: Option<f64>,
191 pub bh: Option<f64>,
192}
193
194pub fn colorbalance(opts: ColorBalance) -> impl Fn(Stream<Video>) -> Stream<Video> {
195 move |input| {
196 let mut p = BTreeMap::new();
197 opts.rs.insert_into("rs", &mut p);
198 opts.gs.insert_into("gs", &mut p);
199 opts.bs.insert_into("bs", &mut p);
200 opts.rm.insert_into("rm", &mut p);
201 opts.gm.insert_into("gm", &mut p);
202 opts.bm.insert_into("bm", &mut p);
203 opts.rh.insert_into("rh", &mut p);
204 opts.gh.insert_into("gh", &mut p);
205 opts.bh.insert_into("bh", &mut p);
206 add_video_filter(input, "colorbalance", p)
207 }
208}
209
210pub fn colortemperature(temperature: f64) -> impl Fn(Stream<Video>) -> Stream<Video> {
211 move |input| {
212 add_video_filter(input, "colortemperature", params! { "temperature" => temperature })
213 }
214}
215
216#[derive(Default)]
217pub struct ColorChannelMixer {
218 pub rr: Option<f64>,
219 pub rg: Option<f64>,
220 pub rb: Option<f64>,
221 pub gr: Option<f64>,
222 pub gg: Option<f64>,
223 pub gb: Option<f64>,
224 pub br: Option<f64>,
225 pub bg: Option<f64>,
226 pub bb: Option<f64>,
227}
228
229pub fn colorchannelmixer(opts: ColorChannelMixer) -> impl Fn(Stream<Video>) -> Stream<Video> {
230 move |input| {
231 let mut p = BTreeMap::new();
232 opts.rr.insert_into("rr", &mut p);
233 opts.rg.insert_into("rg", &mut p);
234 opts.rb.insert_into("rb", &mut p);
235 opts.gr.insert_into("gr", &mut p);
236 opts.gg.insert_into("gg", &mut p);
237 opts.gb.insert_into("gb", &mut p);
238 opts.br.insert_into("br", &mut p);
239 opts.bg.insert_into("bg", &mut p);
240 opts.bb.insert_into("bb", &mut p);
241 add_video_filter(input, "colorchannelmixer", p)
242 }
243}
244
245pub fn gblur(sigma: f64) -> impl Fn(Stream<Video>) -> Stream<Video> {
250 move |input| add_video_filter(input, "gblur", params! { "sigma" => sigma })
251}
252
253pub fn boxblur(luma_radius: &str) -> impl Fn(Stream<Video>) -> Stream<Video> {
254 let luma_radius = luma_radius.to_string();
255 move |input| {
256 add_video_filter(input, "boxblur", params! { "luma_radius" => luma_radius.as_str() })
257 }
258}
259
260#[derive(Default)]
261pub struct Unsharp {
262 pub luma_msize_x: Option<u32>,
263 pub luma_msize_y: Option<u32>,
264 pub luma_amount: Option<f64>,
265}
266
267pub fn unsharp(opts: Unsharp) -> impl Fn(Stream<Video>) -> Stream<Video> {
268 move |input| {
269 let mut p = BTreeMap::new();
270 opts.luma_msize_x.insert_into("luma_msize_x", &mut p);
271 opts.luma_msize_y.insert_into("luma_msize_y", &mut p);
272 opts.luma_amount.insert_into("luma_amount", &mut p);
273 add_video_filter(input, "unsharp", p)
274 }
275}
276
277pub fn bilateral(sigma_s: f64, sigma_r: f64) -> impl Fn(Stream<Video>) -> Stream<Video> {
278 move |input| {
279 add_video_filter(
280 input,
281 "bilateral",
282 params! { "sigmaS" => sigma_s, "sigmaR" => sigma_r },
283 )
284 }
285}
286
287pub fn nlmeans(s: f64) -> impl Fn(Stream<Video>) -> Stream<Video> {
288 move |input| add_video_filter(input, "nlmeans", params! { "s" => s })
289}
290
291pub fn denoise(strength: f64) -> impl Fn(Stream<Video>) -> Stream<Video> {
292 move |input| add_video_filter(input, "denoise", params! { "strength" => strength })
293}
294
295pub fn hqdn3d(luma_spatial: f64) -> impl Fn(Stream<Video>) -> Stream<Video> {
296 move |input| {
297 add_video_filter(input, "hqdn3d", params! { "luma_spatial" => luma_spatial })
298 }
299}
300
301#[derive(Debug, Clone, Copy)]
306pub enum FadeType {
307 In,
308 Out,
309}
310
311impl FadeType {
312 fn as_str(&self) -> &'static str {
313 match self {
314 FadeType::In => "in",
315 FadeType::Out => "out",
316 }
317 }
318}
319
320#[derive(Default)]
321pub struct Fade {
322 pub fade_type: Option<FadeType>,
323 pub start: Option<f64>,
324 pub duration: Option<f64>,
325 pub color: Option<String>,
326 pub alpha: Option<f64>,
327}
328
329pub fn fade(opts: Fade) -> impl Fn(Stream<Video>) -> Stream<Video> {
330 move |input| {
331 let mut p = BTreeMap::new();
332 if let Some(t) = &opts.fade_type {
333 p.insert("type".into(), ParamValue::String(t.as_str().into()));
334 }
335 opts.start.insert_into("st", &mut p);
336 opts.duration.insert_into("d", &mut p);
337 opts.color.as_deref().insert_into("color", &mut p);
338 opts.alpha.insert_into("alpha", &mut p);
339 add_video_filter(input, "fade", p)
340 }
341}
342
343pub fn setpts(expr: &str) -> impl Fn(Stream<Video>) -> Stream<Video> {
344 let expr = expr.to_string();
345 move |input| add_video_filter(input, "setpts", params! { "expr" => expr.as_str() })
346}
347
348pub fn trim(start: f64, end: f64) -> impl Fn(Stream<Video>) -> Stream<Video> {
349 move |input| add_video_filter(input, "trim", params! { "start" => start, "end" => end })
350}
351
352pub fn fps(rate: f64) -> impl Fn(Stream<Video>) -> Stream<Video> {
353 move |input| add_video_filter(input, "fps", params! { "fps" => rate })
354}
355
356pub fn r#loop(count: i32, size: u32, start: u32) -> impl Fn(Stream<Video>) -> Stream<Video> {
357 move |input| {
358 add_video_filter(
359 input,
360 "loop",
361 params! { "loop" => count, "size" => size, "start" => start },
362 )
363 }
364}
365
366pub fn reverse() -> impl Fn(Stream<Video>) -> Stream<Video> {
367 move |input| add_video_filter(input, "reverse", params! {})
368}
369
370pub fn select(expr: &str) -> impl Fn(Stream<Video>) -> Stream<Video> {
371 let expr = expr.to_string();
372 move |input| add_video_filter(input, "select", params! { "expr" => expr.as_str() })
373}
374
375pub fn framestep(step: u32) -> impl Fn(Stream<Video>) -> Stream<Video> {
376 move |input| add_video_filter(input, "framestep", params! { "step" => step })
377}
378
379#[derive(Default)]
384pub struct DrawText {
385 pub text: Option<String>,
386 pub textfile: Option<String>,
387 pub fontfile: Option<String>,
388 pub fontsize: Option<f64>,
389 pub fontcolor: Option<String>,
390 pub x: Option<String>,
391 pub y: Option<String>,
392 pub boxcolor: Option<String>,
393 pub borderw: Option<f64>,
394 pub shadowcolor: Option<String>,
395 pub shadowx: Option<f64>,
396 pub shadowy: Option<f64>,
397 pub alpha: Option<f64>,
398 pub font: Option<String>,
399 pub line_spacing: Option<f64>,
400 pub enable: Option<String>,
401}
402
403pub fn drawtext(opts: DrawText) -> impl Fn(Stream<Video>) -> Stream<Video> {
404 move |input| {
405 let mut p = BTreeMap::new();
406 opts.text.as_deref().insert_into("text", &mut p);
407 opts.textfile.as_deref().insert_into("textfile", &mut p);
408 opts.fontfile.as_deref().insert_into("fontfile", &mut p);
409 opts.fontsize.insert_into("fontsize", &mut p);
410 opts.fontcolor.as_deref().insert_into("fontcolor", &mut p);
411 opts.x.as_deref().insert_into("x", &mut p);
412 opts.y.as_deref().insert_into("y", &mut p);
413 opts.boxcolor.as_deref().insert_into("boxcolor", &mut p);
414 opts.borderw.insert_into("borderw", &mut p);
415 opts.shadowcolor
416 .as_deref()
417 .insert_into("shadowcolor", &mut p);
418 opts.shadowx.insert_into("shadowx", &mut p);
419 opts.shadowy.insert_into("shadowy", &mut p);
420 opts.alpha.insert_into("alpha", &mut p);
421 opts.font.as_deref().insert_into("font", &mut p);
422 opts.line_spacing.insert_into("line_spacing", &mut p);
423 opts.enable.as_deref().insert_into("enable", &mut p);
424 add_video_filter(input, "drawtext", p)
425 }
426}
427
428pub fn drawbox(
429 x: u32,
430 y: u32,
431 w: u32,
432 h: u32,
433 color: &str,
434 thickness: u32,
435) -> impl Fn(Stream<Video>) -> Stream<Video> {
436 let color = color.to_string();
437 move |input| {
438 add_video_filter(
439 input,
440 "drawbox",
441 params! {
442 "x" => x, "y" => y, "w" => w, "h" => h,
443 "color" => color.as_str(), "t" => thickness
444 },
445 )
446 }
447}
448
449pub fn subtitles(filename: &str) -> impl Fn(Stream<Video>) -> Stream<Video> {
450 let filename = filename.to_string();
451 move |input| {
452 add_video_filter(input, "subtitles", params! { "filename" => filename.as_str() })
453 }
454}
455
456pub fn zoompan(zoom: &str, d: u32, s: &str) -> impl Fn(Stream<Video>) -> Stream<Video> {
461 let zoom = zoom.to_string();
462 let s = s.to_string();
463 move |input| {
464 add_video_filter(
465 input,
466 "zoompan",
467 params! { "zoom" => zoom.as_str(), "d" => d, "s" => s.as_str() },
468 )
469 }
470}
471
472pub fn chromakey(color: &str, similarity: f64, blend: f64) -> impl Fn(Stream<Video>) -> Stream<Video> {
473 let color = color.to_string();
474 move |input| {
475 add_video_filter(
476 input,
477 "chromakey",
478 params! { "color" => color.as_str(), "similarity" => similarity, "blend" => blend },
479 )
480 }
481}
482
483pub fn colorkey(color: &str, similarity: f64, blend: f64) -> impl Fn(Stream<Video>) -> Stream<Video> {
484 let color = color.to_string();
485 move |input| {
486 add_video_filter(
487 input,
488 "colorkey",
489 params! { "color" => color.as_str(), "similarity" => similarity, "blend" => blend },
490 )
491 }
492}
493
494pub fn deband() -> impl Fn(Stream<Video>) -> Stream<Video> {
495 move |input| add_video_filter(input, "deband", params! {})
496}
497
498pub fn vignette() -> impl Fn(Stream<Video>) -> Stream<Video> {
499 move |input| add_video_filter(input, "vignette", params! {})
500}
501
502pub fn noise(alls: u32) -> impl Fn(Stream<Video>) -> Stream<Video> {
503 move |input| add_video_filter(input, "noise", params! { "alls" => alls })
504}
505
506pub fn edgedetect() -> impl Fn(Stream<Video>) -> Stream<Video> {
507 move |input| add_video_filter(input, "edgedetect", params! {})
508}
509
510pub fn denoise_nlmeans(s: f64) -> impl Fn(Stream<Video>) -> Stream<Video> {
511 nlmeans(s)
512}
513
514pub fn grayscale() -> impl Fn(Stream<Video>) -> Stream<Video> {
515 format("gray")
516}
517
518pub fn negate() -> impl Fn(Stream<Video>) -> Stream<Video> {
519 move |input| add_video_filter(input, "negate", params! {})
520}
521
522pub fn minterpolate(fps: f64) -> impl Fn(Stream<Video>) -> Stream<Video> {
523 move |input| add_video_filter(input, "minterpolate", params! { "fps" => fps })
524}
525
526pub fn deshake() -> impl Fn(Stream<Video>) -> Stream<Video> {
527 move |input| add_video_filter(input, "deshake", params! {})
528}
529
530pub fn deflicker() -> impl Fn(Stream<Video>) -> Stream<Video> {
531 move |input| add_video_filter(input, "deflicker", params! {})
532}
533
534#[cfg(test)]
535mod tests {
536 use super::*;
537 use crate::graph::input;
538
539 #[test]
540 fn scale_produces_correct_opnode() {
541 let (v, _a) = input("test.mp4");
542 let v = v.pipe(scale(1920, 1080));
543 let graph = v.graph.borrow();
544 let node = graph.arena.get(v.node_id);
545 match node {
546 renderbox_dsl::OpNode::Filter { name, params, .. } => {
547 assert_eq!(name, "scale");
548 assert_eq!(params["w"], ParamValue::Number(1920.0));
549 assert_eq!(params["h"], ParamValue::Number(1080.0));
550 }
551 other => panic!("expected Filter, got {:?}", other),
552 }
553 }
554
555 #[test]
556 fn hflip_produces_paramless_filter() {
557 let (v, _a) = input("test.mp4");
558 let v = v.pipe(hflip());
559 let graph = v.graph.borrow();
560 let node = graph.arena.get(v.node_id);
561 match node {
562 renderbox_dsl::OpNode::Filter { name, params, .. } => {
563 assert_eq!(name, "hflip");
564 assert!(params.is_empty());
565 }
566 other => panic!("expected Filter, got {:?}", other),
567 }
568 }
569
570 #[test]
571 fn curves_enum_param() {
572 let (v, _a) = input("test.mp4");
573 let v = v.pipe(curves(CurvePreset::Vintage));
574 let graph = v.graph.borrow();
575 let node = graph.arena.get(v.node_id);
576 match node {
577 renderbox_dsl::OpNode::Filter { name, params, .. } => {
578 assert_eq!(name, "curves");
579 assert_eq!(
580 params["preset"],
581 ParamValue::String("vintage".into())
582 );
583 }
584 other => panic!("expected Filter, got {:?}", other),
585 }
586 }
587
588 #[test]
589 fn eq_optional_params() {
590 let (v, _a) = input("test.mp4");
591 let v = v.pipe(eq(Eq {
592 brightness: Some(0.1),
593 contrast: Some(1.5),
594 ..Default::default()
595 }));
596 let graph = v.graph.borrow();
597 let node = graph.arena.get(v.node_id);
598 match node {
599 renderbox_dsl::OpNode::Filter { name, params, .. } => {
600 assert_eq!(name, "eq");
601 assert_eq!(params["brightness"], ParamValue::Number(0.1));
602 assert_eq!(params["contrast"], ParamValue::Number(1.5));
603 assert!(!params.contains_key("saturation"));
604 assert!(!params.contains_key("gamma"));
605 }
606 other => panic!("expected Filter, got {:?}", other),
607 }
608 }
609
610 #[test]
611 fn transpose_integer_enum() {
612 let (v, _a) = input("test.mp4");
613 let v = v.pipe(transpose(Transpose::Clock));
614 let graph = v.graph.borrow();
615 match graph.arena.get(v.node_id) {
616 renderbox_dsl::OpNode::Filter { name, params, .. } => {
617 assert_eq!(name, "transpose");
618 assert_eq!(params["dir"], ParamValue::Number(1.0));
619 }
620 other => panic!("expected Filter, got {:?}", other),
621 }
622 }
623
624 #[test]
625 fn pad_at_uses_expr_num() {
626 let (v, _a) = input("test.mp4");
627 let v = v.pipe(pad_at(1920, 1200, 0, 60, "black"));
628 let graph = v.graph.borrow();
629 match graph.arena.get(v.node_id) {
630 renderbox_dsl::OpNode::Filter { name, params, .. } => {
631 assert_eq!(name, "pad");
632 assert_eq!(params["w"], ParamValue::String("1920.0".into()));
633 assert_eq!(params["h"], ParamValue::String("1200.0".into()));
634 assert_eq!(params["x"], ParamValue::String("0.0".into()));
635 assert_eq!(params["y"], ParamValue::String("60.0".into()));
636 assert_eq!(params["color"], ParamValue::String("black".into()));
637 }
638 other => panic!("expected Filter, got {:?}", other),
639 }
640 }
641
642 #[test]
643 fn fade_builder_with_type() {
644 let (v, _a) = input("test.mp4");
645 let v = v.pipe(fade(Fade {
646 fade_type: Some(FadeType::In),
647 start: Some(0.0),
648 duration: Some(2.0),
649 ..Default::default()
650 }));
651 let graph = v.graph.borrow();
652 match graph.arena.get(v.node_id) {
653 renderbox_dsl::OpNode::Filter { name, params, .. } => {
654 assert_eq!(name, "fade");
655 assert_eq!(params["type"], ParamValue::String("in".into()));
656 assert_eq!(params["st"], ParamValue::Number(0.0));
657 assert_eq!(params["d"], ParamValue::Number(2.0));
658 assert!(!params.contains_key("color"));
659 assert!(!params.contains_key("alpha"));
660 }
661 other => panic!("expected Filter, got {:?}", other),
662 }
663 }
664
665 #[test]
666 fn drawtext_mixed_params() {
667 let (v, _a) = input("test.mp4");
668 let v = v.pipe(drawtext(DrawText {
669 text: Some("Hello".into()),
670 fontsize: Some(48.0),
671 fontcolor: Some("white".into()),
672 x: Some("(w-text_w)/2".into()),
673 y: Some("h-th-10".into()),
674 ..Default::default()
675 }));
676 let graph = v.graph.borrow();
677 match graph.arena.get(v.node_id) {
678 renderbox_dsl::OpNode::Filter { name, params, .. } => {
679 assert_eq!(name, "drawtext");
680 assert_eq!(params["text"], ParamValue::String("Hello".into()));
681 assert_eq!(params["fontsize"], ParamValue::Number(48.0));
682 assert_eq!(params["fontcolor"], ParamValue::String("white".into()));
683 assert_eq!(params["x"], ParamValue::String("(w-text_w)/2".into()));
684 assert!(!params.contains_key("textfile"));
685 assert!(!params.contains_key("shadowcolor"));
686 }
687 other => panic!("expected Filter, got {:?}", other),
688 }
689 }
690
691 #[test]
692 fn setpts_expression_string() {
693 let (v, _a) = input("test.mp4");
694 let v = v.pipe(setpts("2.0*PTS"));
695 let graph = v.graph.borrow();
696 match graph.arena.get(v.node_id) {
697 renderbox_dsl::OpNode::Filter { name, params, .. } => {
698 assert_eq!(name, "setpts");
699 assert_eq!(params["expr"], ParamValue::String("2.0*PTS".into()));
700 }
701 other => panic!("expected Filter, got {:?}", other),
702 }
703 }
704
705 #[test]
706 fn filter_pipeline_chains() {
707 let (v, a) = input("test.mp4");
708 let v = v
709 .pipe(scale(1920, 1080))
710 .pipe(curves(CurvePreset::Vintage))
711 .pipe(gblur(2.0));
712 let bytes = crate::graph::output("out.mp4", v, a).build().unwrap();
713 assert!(!bytes.is_empty());
714 }
715}