8 changed files with 361 additions and 64 deletions
@ -0,0 +1,262 @@ |
|||||||
|
//! Collection of methods for videos.
|
||||||
|
|
||||||
|
use std::sync::{Arc, Mutex}; |
||||||
|
|
||||||
|
use futures_channel::oneshot; |
||||||
|
use gst::prelude::*; |
||||||
|
use gst_video::prelude::*; |
||||||
|
use gtk::{gio, glib, glib::clone, prelude::*}; |
||||||
|
use image::GenericImageView; |
||||||
|
use matrix_sdk::attachment::{BaseVideoInfo, Thumbnail}; |
||||||
|
use tracing::warn; |
||||||
|
|
||||||
|
use super::{ |
||||||
|
image::{prepare_thumbnail_for_sending, thumbnail_dimensions}, |
||||||
|
load_gstreamer_media_info, |
||||||
|
}; |
||||||
|
|
||||||
|
/// A channel sender to send the result of a video thumbnail.
|
||||||
|
type ThumbnailResultSender = oneshot::Sender<Result<Thumbnail, ()>>; |
||||||
|
|
||||||
|
/// Load information and try to generate a thumbnail for the video in the given
|
||||||
|
/// file.
|
||||||
|
pub async fn load_video_info(file: &gio::File) -> (BaseVideoInfo, Option<Thumbnail>) { |
||||||
|
let mut info = BaseVideoInfo { |
||||||
|
duration: None, |
||||||
|
width: None, |
||||||
|
height: None, |
||||||
|
size: None, |
||||||
|
blurhash: None, |
||||||
|
}; |
||||||
|
|
||||||
|
let Some(media_info) = load_gstreamer_media_info(file).await else { |
||||||
|
return (info, None); |
||||||
|
}; |
||||||
|
|
||||||
|
info.duration = media_info.duration().map(Into::into); |
||||||
|
|
||||||
|
if let Some(stream_info) = media_info |
||||||
|
.video_streams() |
||||||
|
.first() |
||||||
|
.and_then(|s| s.downcast_ref::<gst_pbutils::DiscovererVideoInfo>()) |
||||||
|
{ |
||||||
|
info.width = Some(stream_info.width().into()); |
||||||
|
info.height = Some(stream_info.height().into()); |
||||||
|
} |
||||||
|
|
||||||
|
let thumbnail = generate_video_thumbnail(file).await; |
||||||
|
|
||||||
|
(info, thumbnail) |
||||||
|
} |
||||||
|
|
||||||
|
/// Generate a thumbnail for the video in the given file.
|
||||||
|
async fn generate_video_thumbnail(file: &gio::File) -> Option<Thumbnail> { |
||||||
|
let (sender, receiver) = oneshot::channel(); |
||||||
|
let sender = Arc::new(Mutex::new(Some(sender))); |
||||||
|
|
||||||
|
let pipeline = match create_thumbnailer_pipeline(&file.uri(), sender.clone()) { |
||||||
|
Ok(pipeline) => pipeline, |
||||||
|
Err(error) => { |
||||||
|
warn!("Could not create pipeline for video thumbnail: {error}"); |
||||||
|
return None; |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
if pipeline.set_state(gst::State::Paused).is_err() { |
||||||
|
warn!("Could not initialize pipeline for video thumbnail"); |
||||||
|
return None; |
||||||
|
} |
||||||
|
|
||||||
|
let bus = pipeline.bus().expect("Pipeline has a bus"); |
||||||
|
|
||||||
|
let mut started = false; |
||||||
|
let _bus_guard = bus |
||||||
|
.add_watch(clone!( |
||||||
|
#[weak] |
||||||
|
pipeline, |
||||||
|
#[upgrade_or] |
||||||
|
glib::ControlFlow::Break, |
||||||
|
move |_, message| { |
||||||
|
match message.view() { |
||||||
|
gst::MessageView::AsyncDone(_) => { |
||||||
|
if !started { |
||||||
|
// AsyncDone means that the pipeline has started now.
|
||||||
|
if pipeline.set_state(gst::State::Playing).is_err() { |
||||||
|
warn!("Could not start pipeline for video thumbnail"); |
||||||
|
send_video_thumbnail_result(&sender, Err(())); |
||||||
|
|
||||||
|
return glib::ControlFlow::Break; |
||||||
|
}; |
||||||
|
|
||||||
|
started = true; |
||||||
|
} |
||||||
|
|
||||||
|
glib::ControlFlow::Continue |
||||||
|
} |
||||||
|
gst::MessageView::Eos(_) => { |
||||||
|
// We have the thumbnail or we cannot have one.
|
||||||
|
glib::ControlFlow::Break |
||||||
|
} |
||||||
|
gst::MessageView::Error(error) => { |
||||||
|
warn!("Could not generate video thumbnail: {error}"); |
||||||
|
send_video_thumbnail_result(&sender, Err(())); |
||||||
|
|
||||||
|
glib::ControlFlow::Break |
||||||
|
} |
||||||
|
_ => glib::ControlFlow::Continue, |
||||||
|
} |
||||||
|
} |
||||||
|
)) |
||||||
|
.expect("Setting bus watch succeeds"); |
||||||
|
|
||||||
|
let thumbnail = receiver.await; |
||||||
|
|
||||||
|
// Clean up.
|
||||||
|
let _ = pipeline.set_state(gst::State::Null); |
||||||
|
bus.set_flushing(true); |
||||||
|
|
||||||
|
thumbnail.ok().transpose().ok().flatten() |
||||||
|
} |
||||||
|
|
||||||
|
/// Create a GStreamer pipeline to get a thumbnail of the first frame.
|
||||||
|
fn create_thumbnailer_pipeline( |
||||||
|
uri: &str, |
||||||
|
sender: Arc<Mutex<Option<ThumbnailResultSender>>>, |
||||||
|
) -> Result<gst::Pipeline, glib::Error> { |
||||||
|
// Create our pipeline from a pipeline description string.
|
||||||
|
let pipeline = gst::parse::launch(&format!( |
||||||
|
"uridecodebin uri={uri} ! videoconvert ! appsink name=sink" |
||||||
|
))? |
||||||
|
.downcast::<gst::Pipeline>() |
||||||
|
.expect("Element is a pipeline"); |
||||||
|
|
||||||
|
let appsink = pipeline |
||||||
|
.by_name("sink") |
||||||
|
.expect("Sink element is in the pipeline") |
||||||
|
.downcast::<gst_app::AppSink>() |
||||||
|
.expect("Sink element is an appsink"); |
||||||
|
|
||||||
|
// Don't synchronize on the clock, we only want a snapshot asap.
|
||||||
|
appsink.set_property("sync", false); |
||||||
|
|
||||||
|
// Tell the appsink what format we want, for simplicity we only accept 8-bit
|
||||||
|
// RGB.
|
||||||
|
appsink.set_caps(Some( |
||||||
|
&gst_video::VideoCapsBuilder::new() |
||||||
|
.format(gst_video::VideoFormat::Rgbx) |
||||||
|
.build(), |
||||||
|
)); |
||||||
|
|
||||||
|
let mut got_snapshot = false; |
||||||
|
|
||||||
|
// Listen to callbacks to get the data.
|
||||||
|
appsink.set_callbacks( |
||||||
|
gst_app::AppSinkCallbacks::builder() |
||||||
|
.new_sample(move |appsink| { |
||||||
|
// Pull the sample out of the buffer.
|
||||||
|
let sample = appsink.pull_sample().map_err(|_| gst::FlowError::Eos)?; |
||||||
|
let Some(buffer) = sample.buffer() else { |
||||||
|
warn!("Could not get buffer from appsink"); |
||||||
|
send_video_thumbnail_result(&sender, Err(())); |
||||||
|
|
||||||
|
return Err(gst::FlowError::Error); |
||||||
|
}; |
||||||
|
|
||||||
|
// Make sure that we only get a single buffer.
|
||||||
|
if got_snapshot { |
||||||
|
return Err(gst::FlowError::Eos); |
||||||
|
} |
||||||
|
got_snapshot = true; |
||||||
|
|
||||||
|
let Some(caps) = sample.caps() else { |
||||||
|
warn!("Got video sample without caps"); |
||||||
|
send_video_thumbnail_result(&sender, Err(())); |
||||||
|
|
||||||
|
return Err(gst::FlowError::Error); |
||||||
|
}; |
||||||
|
let Ok(info) = gst_video::VideoInfo::from_caps(caps) else { |
||||||
|
warn!("Could not parse video caps"); |
||||||
|
send_video_thumbnail_result(&sender, Err(())); |
||||||
|
|
||||||
|
return Err(gst::FlowError::Error); |
||||||
|
}; |
||||||
|
|
||||||
|
let frame = gst_video::VideoFrameRef::from_buffer_ref_readable(buffer, &info) |
||||||
|
.map_err(|_| { |
||||||
|
warn!("Could not map video buffer readable"); |
||||||
|
send_video_thumbnail_result(&sender, Err(())); |
||||||
|
|
||||||
|
gst::FlowError::Error |
||||||
|
})?; |
||||||
|
|
||||||
|
// Create a FlatSamples around the borrowed video frame data from GStreamer with
|
||||||
|
// the correct stride.
|
||||||
|
let img = image::FlatSamples::<&[u8]> { |
||||||
|
samples: frame.plane_data(0).unwrap(), |
||||||
|
layout: image::flat::SampleLayout { |
||||||
|
channels: 3, // RGB
|
||||||
|
channel_stride: 1, // 1 byte from component to component
|
||||||
|
width: frame.width(), |
||||||
|
width_stride: 4, // 4 bytes from pixel to pixel
|
||||||
|
height: frame.height(), |
||||||
|
height_stride: frame.plane_stride()[0] as usize, // stride from line to line
|
||||||
|
}, |
||||||
|
color_hint: Some(image::ColorType::Rgb8), |
||||||
|
}; |
||||||
|
|
||||||
|
let Ok(view) = img.as_view::<image::Rgb<u8>>() else { |
||||||
|
warn!("Could not parse frame as view"); |
||||||
|
send_video_thumbnail_result(&sender, Err(())); |
||||||
|
|
||||||
|
return Err(gst::FlowError::Error); |
||||||
|
}; |
||||||
|
|
||||||
|
// Reduce the dimensions if the thumbnail is bigger than the wanted size.
|
||||||
|
let thumbnail = if let Some((target_width, target_height)) = thumbnail_dimensions( |
||||||
|
frame.width() * info.par().numer() as u32, |
||||||
|
frame.height() * info.par().denom() as u32, |
||||||
|
) { |
||||||
|
image::imageops::thumbnail(&view, target_width, target_height) |
||||||
|
} else { |
||||||
|
image::ImageBuffer::from_fn(view.width(), view.height(), |x, y| { |
||||||
|
view.get_pixel(x, y) |
||||||
|
}) |
||||||
|
}; |
||||||
|
|
||||||
|
// Prepare it.
|
||||||
|
if let Some(thumbnail) = prepare_thumbnail_for_sending(thumbnail.into()) { |
||||||
|
send_video_thumbnail_result(&sender, Ok(thumbnail)); |
||||||
|
|
||||||
|
Err(gst::FlowError::Eos) |
||||||
|
} else { |
||||||
|
warn!("Failed to convert video thumbnail"); |
||||||
|
send_video_thumbnail_result(&sender, Err(())); |
||||||
|
|
||||||
|
Err(gst::FlowError::Error) |
||||||
|
} |
||||||
|
}) |
||||||
|
.build(), |
||||||
|
); |
||||||
|
|
||||||
|
Ok(pipeline) |
||||||
|
} |
||||||
|
|
||||||
|
/// Try to send the given video thumbnail result through the given sender.
|
||||||
|
fn send_video_thumbnail_result( |
||||||
|
sender: &Mutex<Option<ThumbnailResultSender>>, |
||||||
|
result: Result<Thumbnail, ()>, |
||||||
|
) { |
||||||
|
let mut sender = match sender.lock() { |
||||||
|
Ok(sender) => sender, |
||||||
|
Err(error) => { |
||||||
|
warn!("Failed to lock video thumbnail mutex: {error}"); |
||||||
|
return; |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
if let Some(sender) = sender.take() { |
||||||
|
if sender.send(result).is_err() { |
||||||
|
warn!("Failed to send video thumbnail result through channel"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue