2017-12-05 07:52:31 +00:00
|
|
|
// Copyright (C) 2017 Sebastian Dröge <sebastian@centricular.com>
|
|
|
|
//
|
2022-01-15 18:40:12 +00:00
|
|
|
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
|
|
|
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
|
|
|
// <https://mozilla.org/MPL/2.0/>.
|
2017-12-05 07:52:31 +00:00
|
|
|
//
|
2022-01-15 18:40:12 +00:00
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
2017-12-05 07:52:31 +00:00
|
|
|
|
2021-06-03 18:20:54 +00:00
|
|
|
use gst::glib;
|
2017-12-05 07:52:31 +00:00
|
|
|
use gst::prelude::*;
|
|
|
|
|
|
|
|
use gtk::prelude::*;
|
2018-07-27 10:35:24 +00:00
|
|
|
use std::cell::RefCell;
|
2017-12-05 07:52:31 +00:00
|
|
|
|
|
|
|
fn create_pipeline() -> (
|
|
|
|
gst::Pipeline,
|
|
|
|
gst::Pad,
|
|
|
|
gst::Pad,
|
|
|
|
gst::Element,
|
|
|
|
gst::Element,
|
|
|
|
) {
|
2022-10-22 16:06:29 +00:00
|
|
|
let pipeline = gst::Pipeline::default();
|
2017-12-05 07:52:31 +00:00
|
|
|
|
2022-10-19 16:18:43 +00:00
|
|
|
let video_src = gst::ElementFactory::make("videotestsrc")
|
|
|
|
.property("is-live", true)
|
|
|
|
.property_from_str("pattern", "ball")
|
|
|
|
.build()
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
let timeoverlay = gst::ElementFactory::make("timeoverlay")
|
|
|
|
.property("font-desc", "Monospace 20")
|
|
|
|
.build()
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
let video_tee = gst::ElementFactory::make("tee").build().unwrap();
|
|
|
|
let video_queue1 = gst::ElementFactory::make("queue").build().unwrap();
|
|
|
|
let video_queue2 = gst::ElementFactory::make("queue").build().unwrap();
|
|
|
|
|
|
|
|
let video_convert1 = gst::ElementFactory::make("videoconvert").build().unwrap();
|
|
|
|
let video_convert2 = gst::ElementFactory::make("videoconvert").build().unwrap();
|
|
|
|
|
|
|
|
let video_sink = gst::ElementFactory::make("gtk4paintablesink")
|
|
|
|
.build()
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
let video_enc = gst::ElementFactory::make("x264enc")
|
|
|
|
.property("rc-lookahead", 10i32)
|
|
|
|
.property("key-int-max", 30u32)
|
|
|
|
.build()
|
|
|
|
.unwrap();
|
|
|
|
let video_parse = gst::ElementFactory::make("h264parse").build().unwrap();
|
|
|
|
|
|
|
|
let audio_src = gst::ElementFactory::make("audiotestsrc")
|
|
|
|
.property("is-live", true)
|
|
|
|
.property_from_str("wave", "ticks")
|
|
|
|
.build()
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
let audio_tee = gst::ElementFactory::make("tee").build().unwrap();
|
|
|
|
let audio_queue1 = gst::ElementFactory::make("queue").build().unwrap();
|
|
|
|
let audio_queue2 = gst::ElementFactory::make("queue").build().unwrap();
|
|
|
|
|
|
|
|
let audio_convert1 = gst::ElementFactory::make("audioconvert").build().unwrap();
|
|
|
|
let audio_convert2 = gst::ElementFactory::make("audioconvert").build().unwrap();
|
|
|
|
|
|
|
|
let audio_sink = gst::ElementFactory::make("autoaudiosink").build().unwrap();
|
|
|
|
|
|
|
|
let audio_enc = gst::ElementFactory::make("lamemp3enc").build().unwrap();
|
|
|
|
let audio_parse = gst::ElementFactory::make("mpegaudioparse").build().unwrap();
|
|
|
|
|
|
|
|
let togglerecord = gst::ElementFactory::make("togglerecord").build().unwrap();
|
|
|
|
|
|
|
|
let mux_queue1 = gst::ElementFactory::make("queue").build().unwrap();
|
|
|
|
let mux_queue2 = gst::ElementFactory::make("queue").build().unwrap();
|
|
|
|
|
|
|
|
let mux = gst::ElementFactory::make("mp4mux").build().unwrap();
|
|
|
|
|
|
|
|
let file_sink = gst::ElementFactory::make("filesink")
|
|
|
|
.property("location", "recording.mp4")
|
|
|
|
.property("async", false)
|
|
|
|
.property("sync", false)
|
|
|
|
.build()
|
|
|
|
.unwrap();
|
2017-12-05 07:52:31 +00:00
|
|
|
|
|
|
|
pipeline
|
2023-03-09 14:46:52 +00:00
|
|
|
.add_many([
|
2017-12-05 07:52:31 +00:00
|
|
|
&video_src,
|
|
|
|
&timeoverlay,
|
|
|
|
&video_tee,
|
|
|
|
&video_queue1,
|
|
|
|
&video_queue2,
|
|
|
|
&video_convert1,
|
|
|
|
&video_convert2,
|
|
|
|
&video_sink,
|
|
|
|
&video_enc,
|
|
|
|
&video_parse,
|
|
|
|
&audio_src,
|
|
|
|
&audio_tee,
|
|
|
|
&audio_queue1,
|
|
|
|
&audio_queue2,
|
|
|
|
&audio_convert1,
|
|
|
|
&audio_convert2,
|
|
|
|
&audio_sink,
|
|
|
|
&audio_enc,
|
|
|
|
&audio_parse,
|
|
|
|
&togglerecord,
|
|
|
|
&mux_queue1,
|
|
|
|
&mux_queue2,
|
|
|
|
&mux,
|
|
|
|
&file_sink,
|
|
|
|
])
|
|
|
|
.unwrap();
|
|
|
|
|
2023-03-09 14:46:52 +00:00
|
|
|
gst::Element::link_many([
|
2017-12-05 07:52:31 +00:00
|
|
|
&video_src,
|
|
|
|
&timeoverlay,
|
|
|
|
&video_tee,
|
|
|
|
&video_queue1,
|
|
|
|
&video_convert1,
|
|
|
|
&video_sink,
|
2018-10-11 10:49:48 +00:00
|
|
|
])
|
|
|
|
.unwrap();
|
2017-12-05 07:52:31 +00:00
|
|
|
|
2023-03-09 14:46:52 +00:00
|
|
|
gst::Element::link_many([
|
2017-12-05 07:52:31 +00:00
|
|
|
&video_tee,
|
|
|
|
&video_queue2,
|
|
|
|
&video_convert2,
|
|
|
|
&video_enc,
|
|
|
|
&video_parse,
|
2018-10-11 10:49:48 +00:00
|
|
|
])
|
|
|
|
.unwrap();
|
2017-12-05 07:52:31 +00:00
|
|
|
|
2019-04-22 06:41:42 +00:00
|
|
|
video_parse
|
|
|
|
.link_pads(Some("src"), &togglerecord, Some("sink"))
|
|
|
|
.unwrap();
|
|
|
|
togglerecord
|
|
|
|
.link_pads(Some("src"), &mux_queue1, Some("sink"))
|
|
|
|
.unwrap();
|
|
|
|
mux_queue1
|
|
|
|
.link_pads(Some("src"), &mux, Some("video_%u"))
|
|
|
|
.unwrap();
|
2017-12-05 07:52:31 +00:00
|
|
|
|
2023-03-09 14:46:52 +00:00
|
|
|
gst::Element::link_many([
|
2017-12-05 07:52:31 +00:00
|
|
|
&audio_src,
|
|
|
|
&audio_tee,
|
|
|
|
&audio_queue1,
|
|
|
|
&audio_convert1,
|
|
|
|
&audio_sink,
|
2018-10-11 10:49:48 +00:00
|
|
|
])
|
|
|
|
.unwrap();
|
2017-12-05 07:52:31 +00:00
|
|
|
|
2023-03-09 14:46:52 +00:00
|
|
|
gst::Element::link_many([
|
2017-12-05 07:52:31 +00:00
|
|
|
&audio_tee,
|
|
|
|
&audio_queue2,
|
|
|
|
&audio_convert2,
|
|
|
|
&audio_enc,
|
|
|
|
&audio_parse,
|
2018-10-11 10:49:48 +00:00
|
|
|
])
|
|
|
|
.unwrap();
|
2017-12-05 07:52:31 +00:00
|
|
|
|
|
|
|
audio_parse
|
2019-04-22 06:41:42 +00:00
|
|
|
.link_pads(Some("src"), &togglerecord, Some("sink_0"))
|
2017-12-05 07:52:31 +00:00
|
|
|
.unwrap();
|
|
|
|
togglerecord
|
2019-04-22 06:41:42 +00:00
|
|
|
.link_pads(Some("src_0"), &mux_queue2, Some("sink"))
|
|
|
|
.unwrap();
|
|
|
|
mux_queue2
|
|
|
|
.link_pads(Some("src"), &mux, Some("audio_%u"))
|
2017-12-05 07:52:31 +00:00
|
|
|
.unwrap();
|
|
|
|
|
2023-03-09 14:46:52 +00:00
|
|
|
gst::Element::link_many([&mux, &file_sink]).unwrap();
|
2017-12-05 07:52:31 +00:00
|
|
|
|
|
|
|
(
|
|
|
|
pipeline,
|
2021-04-20 12:58:11 +00:00
|
|
|
video_queue2.static_pad("sink").unwrap(),
|
|
|
|
audio_queue2.static_pad("sink").unwrap(),
|
2017-12-05 07:52:31 +00:00
|
|
|
togglerecord,
|
|
|
|
video_sink,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn create_ui(app: >k::Application) {
|
2022-08-26 17:31:19 +00:00
|
|
|
let (pipeline, video_pad, audio_pad, togglerecord, video_sink) = create_pipeline();
|
2017-12-05 07:52:31 +00:00
|
|
|
|
2022-08-26 17:31:19 +00:00
|
|
|
let window = gtk::ApplicationWindow::new(app);
|
2017-12-05 07:52:31 +00:00
|
|
|
window.set_default_size(320, 240);
|
|
|
|
|
2022-08-26 17:31:19 +00:00
|
|
|
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 6);
|
|
|
|
let picture = gtk::Picture::new();
|
|
|
|
let paintable = video_sink.property::<gtk::gdk::Paintable>("paintable");
|
|
|
|
picture.set_paintable(Some(&paintable));
|
|
|
|
vbox.append(&picture);
|
|
|
|
|
|
|
|
let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 6);
|
|
|
|
hbox.set_hexpand(true);
|
|
|
|
hbox.set_homogeneous(true);
|
2019-04-22 06:41:42 +00:00
|
|
|
let position_label = gtk::Label::new(Some("Position: 00:00:00"));
|
2022-08-26 17:31:19 +00:00
|
|
|
hbox.append(&position_label);
|
2019-04-22 06:41:42 +00:00
|
|
|
let recorded_duration_label = gtk::Label::new(Some("Recorded: 00:00:00"));
|
2022-08-26 17:31:19 +00:00
|
|
|
hbox.append(&recorded_duration_label);
|
|
|
|
vbox.append(&hbox);
|
2017-12-05 07:52:31 +00:00
|
|
|
|
2022-08-26 17:31:19 +00:00
|
|
|
let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 6);
|
|
|
|
hbox.set_hexpand(true);
|
|
|
|
hbox.set_homogeneous(true);
|
2020-06-16 08:56:48 +00:00
|
|
|
let record_button = gtk::Button::with_label("Record");
|
2022-08-26 17:31:19 +00:00
|
|
|
hbox.append(&record_button);
|
2020-06-16 08:56:48 +00:00
|
|
|
let finish_button = gtk::Button::with_label("Finish");
|
2022-08-26 17:31:19 +00:00
|
|
|
hbox.append(&finish_button);
|
|
|
|
vbox.append(&hbox);
|
2017-12-05 07:52:31 +00:00
|
|
|
|
2022-08-26 17:31:19 +00:00
|
|
|
window.set_child(Some(&vbox));
|
|
|
|
window.show();
|
2017-12-05 07:52:31 +00:00
|
|
|
|
|
|
|
app.add_window(&window);
|
|
|
|
|
2018-07-27 10:35:24 +00:00
|
|
|
let video_sink_weak = video_sink.downgrade();
|
|
|
|
let togglerecord_weak = togglerecord.downgrade();
|
2020-09-14 13:47:27 +00:00
|
|
|
let timeout_id = glib::timeout_add_local(std::time::Duration::from_millis(100), move || {
|
2023-10-30 09:34:35 +00:00
|
|
|
let Some(video_sink) = video_sink_weak.upgrade() else {
|
|
|
|
return glib::ControlFlow::Break;
|
2018-07-27 10:35:24 +00:00
|
|
|
};
|
|
|
|
|
2023-10-30 09:34:35 +00:00
|
|
|
let Some(togglerecord) = togglerecord_weak.upgrade() else {
|
|
|
|
return glib::ControlFlow::Break;
|
2018-07-27 10:35:24 +00:00
|
|
|
};
|
2017-12-05 07:52:31 +00:00
|
|
|
|
2017-12-16 15:42:42 +00:00
|
|
|
let position = video_sink
|
|
|
|
.query_position::<gst::ClockTime>()
|
2021-05-28 16:35:28 +00:00
|
|
|
.unwrap_or(gst::ClockTime::ZERO);
|
2023-01-25 08:23:46 +00:00
|
|
|
position_label.set_text(&format!("Position: {position:.1}"));
|
2017-12-05 07:52:31 +00:00
|
|
|
|
2017-12-09 17:41:54 +00:00
|
|
|
let recording_duration = togglerecord
|
2021-04-20 12:58:11 +00:00
|
|
|
.static_pad("src")
|
2017-12-16 15:42:42 +00:00
|
|
|
.unwrap()
|
|
|
|
.query_position::<gst::ClockTime>()
|
2021-05-28 16:35:28 +00:00
|
|
|
.unwrap_or(gst::ClockTime::ZERO);
|
2023-01-25 08:23:46 +00:00
|
|
|
recorded_duration_label.set_text(&format!("Recorded: {recording_duration:.1}"));
|
2017-12-05 07:52:31 +00:00
|
|
|
|
2023-07-06 13:27:28 +00:00
|
|
|
glib::ControlFlow::Continue
|
2017-12-05 07:52:31 +00:00
|
|
|
});
|
|
|
|
|
2018-07-27 10:35:24 +00:00
|
|
|
let togglerecord_weak = togglerecord.downgrade();
|
2017-12-05 07:52:31 +00:00
|
|
|
record_button.connect_clicked(move |button| {
|
2023-10-30 09:34:35 +00:00
|
|
|
let Some(togglerecord) = togglerecord_weak.upgrade() else {
|
|
|
|
return;
|
2018-07-27 10:35:24 +00:00
|
|
|
};
|
2017-12-05 07:52:31 +00:00
|
|
|
|
2021-11-08 09:55:40 +00:00
|
|
|
let recording = !togglerecord.property::<bool>("record");
|
|
|
|
togglerecord.set_property("record", recording);
|
2017-12-05 07:52:31 +00:00
|
|
|
|
|
|
|
button.set_label(if recording { "Stop" } else { "Record" });
|
|
|
|
});
|
|
|
|
|
2018-07-27 10:35:24 +00:00
|
|
|
let record_button_weak = record_button.downgrade();
|
2017-12-05 07:52:31 +00:00
|
|
|
finish_button.connect_clicked(move |button| {
|
2023-10-30 09:34:35 +00:00
|
|
|
let Some(record_button) = record_button_weak.upgrade() else {
|
|
|
|
return;
|
2018-07-27 10:35:24 +00:00
|
|
|
};
|
|
|
|
|
2017-12-05 07:52:31 +00:00
|
|
|
record_button.set_sensitive(false);
|
|
|
|
button.set_sensitive(false);
|
|
|
|
|
2020-06-24 22:33:57 +00:00
|
|
|
video_pad.send_event(gst::event::Eos::new());
|
|
|
|
audio_pad.send_event(gst::event::Eos::new());
|
2017-12-05 07:52:31 +00:00
|
|
|
});
|
|
|
|
|
2018-07-27 10:35:24 +00:00
|
|
|
let app_weak = app.downgrade();
|
2022-08-26 17:31:19 +00:00
|
|
|
window.connect_close_request(move |_| {
|
2023-10-30 09:34:35 +00:00
|
|
|
let Some(app) = app_weak.upgrade() else {
|
|
|
|
return glib::Propagation::Stop;
|
2018-07-27 10:35:24 +00:00
|
|
|
};
|
|
|
|
|
2017-12-05 07:52:31 +00:00
|
|
|
app.quit();
|
2023-08-02 11:16:55 +00:00
|
|
|
glib::Propagation::Stop
|
2017-12-05 07:52:31 +00:00
|
|
|
});
|
|
|
|
|
2021-04-12 12:49:54 +00:00
|
|
|
let bus = pipeline.bus().unwrap();
|
2019-09-18 10:11:33 +00:00
|
|
|
let app_weak = app.downgrade();
|
2023-04-14 09:46:43 +00:00
|
|
|
let bus_watch = bus
|
|
|
|
.add_watch_local(move |_, msg| {
|
|
|
|
use gst::MessageView;
|
|
|
|
|
2023-10-30 09:34:35 +00:00
|
|
|
let Some(app) = app_weak.upgrade() else {
|
|
|
|
return glib::ControlFlow::Break;
|
2023-04-14 09:46:43 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
match msg.view() {
|
|
|
|
MessageView::Eos(..) => app.quit(),
|
|
|
|
MessageView::Error(err) => {
|
|
|
|
println!(
|
|
|
|
"Error from {:?}: {} ({:?})",
|
|
|
|
msg.src().map(|s| s.path_string()),
|
|
|
|
err.error(),
|
|
|
|
err.debug()
|
|
|
|
);
|
|
|
|
app.quit();
|
|
|
|
}
|
|
|
|
_ => (),
|
|
|
|
};
|
|
|
|
|
2023-07-06 13:27:28 +00:00
|
|
|
glib::ControlFlow::Continue
|
2023-04-14 09:46:43 +00:00
|
|
|
})
|
|
|
|
.expect("Failed to add bus watch");
|
2017-12-05 07:52:31 +00:00
|
|
|
|
2019-01-11 23:45:05 +00:00
|
|
|
pipeline.set_state(gst::State::Playing).unwrap();
|
2018-07-27 10:35:24 +00:00
|
|
|
|
|
|
|
// Pipeline reference is owned by the closure below, so will be
|
|
|
|
// destroyed once the app is destroyed
|
|
|
|
let timeout_id = RefCell::new(Some(timeout_id));
|
2023-04-14 09:46:43 +00:00
|
|
|
let bus_watch = RefCell::new(Some(bus_watch));
|
2017-12-05 07:52:31 +00:00
|
|
|
app.connect_shutdown(move |_| {
|
2023-04-14 09:46:43 +00:00
|
|
|
drop(bus_watch.borrow_mut().take());
|
2019-01-11 23:45:05 +00:00
|
|
|
pipeline.set_state(gst::State::Null).unwrap();
|
2017-12-05 07:52:31 +00:00
|
|
|
|
2018-07-27 10:35:24 +00:00
|
|
|
if let Some(timeout_id) = timeout_id.borrow_mut().take() {
|
2021-10-12 06:31:25 +00:00
|
|
|
timeout_id.remove();
|
2018-07-27 10:35:24 +00:00
|
|
|
}
|
|
|
|
});
|
2017-12-05 07:52:31 +00:00
|
|
|
}
|
|
|
|
|
2023-01-27 16:58:59 +00:00
|
|
|
fn main() -> glib::ExitCode {
|
2017-12-05 07:52:31 +00:00
|
|
|
gst::init().unwrap();
|
|
|
|
gtk::init().unwrap();
|
|
|
|
|
2019-07-02 10:51:11 +00:00
|
|
|
gsttogglerecord::plugin_register_static().expect("Failed to register togglerecord plugin");
|
2022-08-26 17:31:19 +00:00
|
|
|
gstgtk4::plugin_register_static().expect("Failed to register gtk4paintablesink plugin");
|
2017-12-05 07:52:31 +00:00
|
|
|
|
2023-01-16 09:51:10 +00:00
|
|
|
let app = gtk::Application::new(None::<&str>, gio::ApplicationFlags::FLAGS_NONE);
|
2017-12-05 07:52:31 +00:00
|
|
|
|
|
|
|
app.connect_activate(create_ui);
|
2023-01-27 16:58:59 +00:00
|
|
|
app.run()
|
2017-12-05 07:52:31 +00:00
|
|
|
}
|