diff --git a/tutorials/Cargo.toml b/tutorials/Cargo.toml index 8eeaf3bc2..3833077c2 100644 --- a/tutorials/Cargo.toml +++ b/tutorials/Cargo.toml @@ -16,8 +16,13 @@ gst-app = { package = "gstreamer-app", path = "../gstreamer-app" } gst-pbutils = { package = "gstreamer-pbutils", path = "../gstreamer-pbutils" } byte-slice-cast = "1" anyhow = "1" +termion = { version = "1.5", optional = true } [features] tutorial5 = ["gtk", "gdk", "gst-video"] tutorial5-x11 = ["tutorial5"] tutorial5-quartz = ["tutorial5"] + +[[bin]] +name = "basic-tutorial-13" +required-features = ["termion"] diff --git a/tutorials/src/bin/basic-tutorial-13.rs b/tutorials/src/bin/basic-tutorial-13.rs new file mode 100644 index 000000000..3e47ae88a --- /dev/null +++ b/tutorials/src/bin/basic-tutorial-13.rs @@ -0,0 +1,208 @@ +use gst::event::{Seek, Step}; +use gst::prelude::*; +use gst::{Element, SeekFlags, SeekType, State}; + +use anyhow::Error; + +use termion::event::Key; +use termion::input::TermRead; +use termion::raw::IntoRawMode; + +use std::{io, thread, time}; + +#[path = "../tutorials-common.rs"] +mod tutorials_common; + +// Commands that we get from the terminal and we send to the main thread. +#[derive(Clone, Copy, PartialEq)] +enum Command { + PlayPause, + DataRateUp, + DataRateDown, + ReverseRate, + NextFrame, + Quit, +} + +fn send_seek_event(pipeline: &Element, rate: f64) -> bool { + // Obtain the current position, needed for the seek event + let position = match pipeline.query_position::() { + Some(pos) => pos, + None => { + eprintln!("Unable to retrieve current position..."); + return false; + } + }; + + // Create the seek event + let seek_event = if rate > 0. { + Seek::new( + rate, + SeekFlags::FLUSH | SeekFlags::ACCURATE, + SeekType::Set, + position, + SeekType::End, + gst::ClockTime::from(0), + ) + } else { + Seek::new( + rate, + SeekFlags::FLUSH | SeekFlags::ACCURATE, + SeekType::Set, + position, + SeekType::Set, + position, + ) + }; + + // If we have not done so, obtain the sink through which we will send the seek events + if let Ok(Some(video_sink)) = pipeline + .get_property("video-sink") + .unwrap() + .get::() + { + println!("Current rate: {}", rate); + // Send the event + video_sink.send_event(seek_event) + } else { + eprintln!("Failed to update rate..."); + false + } +} + +// This is where we get the user input from the terminal. +fn handle_keyboard(ready_tx: glib::Sender) { + // We set the terminal in "raw mode" so that we can get the keys without waiting for the user + // to press return. + let _stdout = io::stdout().into_raw_mode().unwrap(); + let mut stdin = termion::async_stdin().keys(); + + loop { + if let Some(Ok(input)) = stdin.next() { + let command = match input { + Key::Char('p') | Key::Char('P') => Command::PlayPause, + Key::Char('s') => Command::DataRateDown, + Key::Char('S') => Command::DataRateUp, + Key::Char('d') | Key::Char('D') => Command::ReverseRate, + Key::Char('n') | Key::Char('N') => Command::NextFrame, + Key::Char('q') | Key::Char('Q') => Command::Quit, + Key::Ctrl('c') | Key::Ctrl('C') => Command::Quit, + _ => continue, + }; + ready_tx + .send(command) + .expect("failed to send data through channel"); + if command == Command::Quit { + break; + } + } + thread::sleep(time::Duration::from_millis(50)); + } +} + +fn tutorial_main() -> Result<(), Error> { + // Initialize GStreamer. + gst::init()?; + + // Print usage map. + println!( + "\ +USAGE: Choose one of the following options, then press enter: + 'P' to toggle between PAUSE and PLAY + 'S' to increase playback speed, 's' to decrease playback speed + 'D' to toggle playback direction + 'N' to move to next frame (in the current direction, better in PAUSE) + 'Q' to quit" + ); + + // Get a main context... + let main_context = glib::MainContext::default(); + // ... and make it the main context by default so that we can then have a channel to send the + // commands we received from the terminal. + main_context.acquire(); + + // Build the channel to get the terminal inputs from a different thread. + let (ready_tx, ready_rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT); + + thread::spawn(move || handle_keyboard(ready_tx)); + + // Build the pipeline. + let uri = + "https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm"; + let pipeline = gst::parse_launch(&format!("playbin uri={}", uri))?; + + // Start playing. + let _ = pipeline.set_state(State::Playing)?; + + let main_loop = glib::MainLoop::new(Some(&main_context), false); + let main_loop_clone = main_loop.clone(); + let pipeline_weak = pipeline.downgrade(); + + // Setting up "play" information. + let mut playing = true; + let mut rate = 1.; + + ready_rx.attach(Some(&main_loop.get_context()), move |command: Command| { + let pipeline = match pipeline_weak.upgrade() { + Some(pipeline) => pipeline, + None => return glib::Continue(true), + }; + match command { + Command::PlayPause => { + let status = if playing { + let _ = pipeline.set_state(State::Paused); + "PAUSE" + } else { + let _ = pipeline.set_state(State::Playing); + "PLAYING" + }; + playing = !playing; + println!("Setting state to {}", status); + } + Command::DataRateUp => { + if send_seek_event(&pipeline, rate * 2.) { + rate *= 2.; + } + } + Command::DataRateDown => { + if send_seek_event(&pipeline, rate / 2.) { + rate /= 2.; + } + } + Command::ReverseRate => { + if send_seek_event(&pipeline, rate * -1.) { + rate *= -1.; + } + } + Command::NextFrame => { + if let Ok(Some(video_sink)) = pipeline + .get_property("video-sink") + .unwrap() + .get::() + { + // Send the event + let step = Step::new(gst::format::Buffers(Some(1)), rate.abs(), true, false); + video_sink.send_event(step); + println!("Stepping one frame"); + } + } + Command::Quit => { + main_loop_clone.quit(); + } + } + glib::Continue(true) + }); + + main_loop.run(); + + pipeline.set_state(State::Null)?; + + Ok(()) +} + +fn main() { + match tutorials_common::run(tutorial_main) { + Ok(_) => {} + Err(err) => eprintln!("Failed: {}", err), + } +}