Skip to content

Commit 16e608a

Browse files
committed
Add ability to copy frame to clipboard
1 parent cd20629 commit 16e608a

File tree

5 files changed

+72
-14
lines changed

5 files changed

+72
-14
lines changed

Cargo.lock

Lines changed: 6 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/gui/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ repository = "https://github.com/valadaptive/ntsc-rs/tree/main/crates/gui"
1010

1111
[dependencies]
1212
ntscrs = { path = "../ntscrs" }
13+
arboard = "3.3.2"
1314
eframe = { version = "0.27", features=["persistence"] }
1415
env_logger = "0.10.0"
1516
image = "0.24.7"

crates/gui/src/bin/ntsc-rs-standalone.rs

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,22 @@ use std::{
1818
thread,
1919
};
2020

21-
use eframe::egui::{self, pos2, vec2, Rect, util::undoer::Undoer, Response};
21+
use eframe::egui::{self, pos2, util::undoer::Undoer, vec2, ColorImage, Rect, Response};
2222
use futures_lite::{Future, FutureExt};
2323
use glib::clone::Downgrade;
24-
use gstreamer::{glib, prelude::*, ClockTime};
24+
use gstreamer::{
25+
glib::{self, subclass::types::ObjectSubclassExt},
26+
prelude::*,
27+
ClockTime,
28+
};
2529
use gstreamer_video::{VideoCapsBuilder, VideoFormat, VideoInterlaceMode};
2630

2731
use gui::{
2832
expression_parser::eval_expression_string,
2933
gst_utils::{
3034
clock_format::{clock_time_format, clock_time_parser},
31-
egui_sink::{EffectPreviewSetting, EguiCtx, SinkTexture},
32-
elements::{EguiSink, NtscFilter, VideoPadFilter},
35+
egui_sink::{EffectPreviewSetting, EguiCtx, EguiSink, SinkTexture},
36+
elements,
3337
gstreamer_error::GstreamerError,
3438
ntscrs_filter::NtscFilterSettings,
3539
pipeline_utils::{create_pipeline, PipelineError},
@@ -44,7 +48,7 @@ use ntscrs::settings::{
4448
NtscEffect, NtscEffectFullSettings, ParseSettingsError, SettingDescriptor, SettingID,
4549
SettingKind, SettingsList, UseField,
4650
};
47-
use snafu::prelude::*;
51+
use snafu::{prelude::*, ResultExt};
4852

4953
use log::debug;
5054

@@ -76,21 +80,21 @@ fn initialize_gstreamer() -> Result<(), GstreamerError> {
7680
None,
7781
"eguisink",
7882
gstreamer::Rank::None,
79-
EguiSink::static_type(),
83+
elements::EguiSink::static_type(),
8084
)?;
8185

8286
gstreamer::Element::register(
8387
None,
8488
"ntscfilter",
8589
gstreamer::Rank::None,
86-
NtscFilter::static_type(),
90+
elements::NtscFilter::static_type(),
8791
)?;
8892

8993
gstreamer::Element::register(
9094
None,
9195
"videopadfilter",
9296
gstreamer::Rank::None,
93-
VideoPadFilter::static_type(),
97+
elements::VideoPadFilter::static_type(),
9498
)?;
9599

96100
// PulseAudio has a severe bug that will greatly delay initial playback to the point of unusability:
@@ -2218,8 +2222,9 @@ impl NtscApp {
22182222
egui::TopBottomPanel::top("video_info").show_inside(ui, |ui| {
22192223
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
22202224
let mut remove_pipeline = false;
2221-
let mut res = None;
2225+
let mut change_framerate_res = None;
22222226
let mut save_image_to: Option<(PathBuf, PathBuf)> = None;
2227+
let mut copy_image_res: Option<Result<ColorImage, GstreamerError>> = None;
22232228
if let Some(info) = &mut self.pipeline {
22242229
let mut metadata = info.metadata.lock().unwrap();
22252230
if ui.button("🗙").clicked() {
@@ -2228,13 +2233,21 @@ impl NtscApp {
22282233

22292234
ui.separator();
22302235

2231-
if ui.button("Save image").clicked() {
2236+
if ui.button("Save frame").clicked() {
22322237
let src_path = info.path.clone();
22332238

22342239
let dst_path = src_path.with_extension("");
22352240
save_image_to = Some((src_path, dst_path));
22362241
}
22372242

2243+
if ui.button("Copy frame").clicked() {
2244+
let egui_sink =
2245+
info.egui_sink.downcast_ref::<elements::EguiSink>().unwrap();
2246+
2247+
let egui_sink = EguiSink::from_obj(egui_sink);
2248+
copy_image_res = Some(egui_sink.get_image().map_err(|e| e.into()));
2249+
}
2250+
22382251
if let Some(current_framerate) = metadata.framerate {
22392252
ui.separator();
22402253
match metadata.is_still_image {
@@ -2258,7 +2271,7 @@ impl NtscApp {
22582271
metadata.framerate = Some(new_framerate);
22592272
}
22602273

2261-
res = Some(changed_framerate);
2274+
change_framerate_res = Some(changed_framerate);
22622275
}
22632276
}
22642277
}
@@ -2292,10 +2305,30 @@ impl NtscApp {
22922305
});
22932306
}
22942307

2295-
if let Some(res) = res {
2308+
if let Some(res) = change_framerate_res {
22962309
self.handle_result(res);
22972310
}
22982311

2312+
if let Some(res) = copy_image_res {
2313+
match res {
2314+
Ok(image) => {
2315+
let res = arboard::Clipboard::new().and_then(|mut cb| {
2316+
let data = arboard::ImageData {
2317+
width: image.width(),
2318+
height: image.height(),
2319+
bytes: Cow::from(image.as_raw()),
2320+
};
2321+
cb.set_image(data)?;
2322+
Ok(())
2323+
});
2324+
self.handle_result(res);
2325+
}
2326+
Err(e) => {
2327+
self.handle_error(&e);
2328+
}
2329+
}
2330+
}
2331+
22992332
if remove_pipeline {
23002333
self.handle_result_with(|app| app.remove_pipeline());
23012334
}

crates/gui/src/gst_utils/egui_sink.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,17 @@ impl EguiSink {
9797
Ok(())
9898
}
9999

100+
pub fn get_image(&self) -> Result<ColorImage, gstreamer::FlowError> {
101+
let vframe = self.last_frame.lock().unwrap();
102+
let (vframe, ..) = vframe.as_ref().ok_or(gstreamer::FlowError::Error)?;
103+
104+
let width = vframe.width() as usize;
105+
let height = vframe.height() as usize;
106+
let mut image = ColorImage::new([width, height], Color32::BLACK);
107+
self.apply_effect(vframe, &mut image, None)?;
108+
Ok(image)
109+
}
110+
100111
pub fn update_texture(&self) -> Result<(), gstreamer::FlowError> {
101112
let mut tex = self.texture.lock().unwrap();
102113
let vframe = self.last_frame.lock().unwrap();

crates/gui/src/gst_utils/gstreamer_error.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub enum GstreamerError {
88
BoolError(glib::BoolError),
99
PadLinkError(gstreamer::PadLinkError),
1010
StateChangeError(gstreamer::StateChangeError),
11+
FlowError(gstreamer::FlowError),
1112
}
1213

1314
impl From<glib::Error> for GstreamerError {
@@ -34,13 +35,20 @@ impl From<gstreamer::StateChangeError> for GstreamerError {
3435
}
3536
}
3637

38+
impl From<gstreamer::FlowError> for GstreamerError {
39+
fn from(value: gstreamer::FlowError) -> Self {
40+
GstreamerError::FlowError(value)
41+
}
42+
}
43+
3744
impl Display for GstreamerError {
3845
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3946
match self {
4047
GstreamerError::GlibError(e) => e.fmt(f),
4148
GstreamerError::BoolError(e) => e.fmt(f),
4249
GstreamerError::PadLinkError(e) => e.fmt(f),
4350
GstreamerError::StateChangeError(e) => e.fmt(f),
51+
GstreamerError::FlowError(e) => e.fmt(f),
4452
}
4553
}
4654
}
@@ -52,6 +60,7 @@ impl Error for GstreamerError {
5260
GstreamerError::BoolError(e) => Some(e),
5361
GstreamerError::PadLinkError(e) => Some(e),
5462
GstreamerError::StateChangeError(e) => Some(e),
63+
GstreamerError::FlowError(e) => Some(e),
5564
}
5665
}
5766
}

0 commit comments

Comments
 (0)