Remove ffmpeg implementation

This commit is contained in:
Jeremy Soller 2024-10-09 11:15:04 -06:00
parent d61426957d
commit f10350c7ec
No known key found for this signature in database
GPG key ID: D02FD439211AF56F
11 changed files with 605 additions and 2304 deletions

View file

@ -4,12 +4,7 @@ use cosmic::{
cosmic_config::{self, cosmic_config_derive::CosmicConfigEntry, CosmicConfigEntry},
theme,
};
use lexopt::prelude::*;
use serde::{Deserialize, Serialize};
use std::{path::PathBuf, process};
#[cfg(feature = "ffmpeg")]
use crate::app::hardware::DeviceType;
pub const CONFIG_VERSION: u64 = 1;
@ -34,68 +29,12 @@ impl AppTheme {
#[serde(default)]
pub struct Config {
pub app_theme: AppTheme,
#[cfg(feature = "ffmpeg")]
pub hw_decoder: DeviceType,
}
impl Default for Config {
fn default() -> Self {
Self {
app_theme: AppTheme::System,
#[cfg(feature = "ffmpeg")]
hw_decoder: DeviceType::default(),
}
}
}
#[cfg(feature = "ffmpeg")]
impl Config {
pub fn with_args(&mut self, args: &mut Args) {
if let Some(decoder) = args.decoder {
self.hw_decoder = decoder;
}
}
}
#[cfg(feature = "ffmpeg")]
pub struct Args {
pub paths: Vec<PathBuf>,
pub decoder: Option<DeviceType>,
}
#[cfg(feature = "ffmpeg")]
impl Args {
pub fn parse_args() -> Result<Self, lexopt::Error> {
let mut paths = Vec::new();
let mut decoder = None;
let mut parser = lexopt::Parser::from_env();
while let Some(arg) = parser.next()? {
match arg {
Long("list-hwdec") => {
println!("Supported hardware decoders:");
for hwdec in DeviceType::supported_devices() {
println!("\t* [{}] {hwdec}", hwdec.short_name());
}
process::exit(0);
}
Long("hwdec") => {
decoder = Some(parser.value()?.parse()?);
}
Value(path) => {
let path = path.parse()?;
paths.push(path);
}
_ => return Err(arg.unexpected()),
}
}
if paths.is_empty() {
return Err(lexopt::Error::MissingValue {
option: Some("missing video path".into()),
});
}
Ok(Self { paths, decoder })
}
}
}

View file

@ -1,182 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
use std::{fmt, str::FromStr};
use ffmpeg_next::ffi::AVHWDeviceType;
use serde::{
de::{value::Error as DeError, Error as DeErrorTrait, Unexpected},
Deserialize, Serialize,
};
use super::iter::SupportedDeviceIter;
/// Delegate type for [`ffmpeg_next::ffi::AVHWDeviceType`] for configs.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DeviceType {
None,
/// Compute Unified Device Architecture
/// Nvidia only.
/// https://developer.nvidia.com/video-codec-sdk
Cuda,
/// Direct3D 11 Video API
/// https://learn.microsoft.com/en-us/windows/win32/medfound/direct3d-11-video-apis
D3d11va,
/// Direct3D 12 Video API
/// https://learn.microsoft.com/en-us/windows/win32/medfound/direct3d-12-video-overview
D3d12va,
/// DirectX Video Acceleration 2.0
/// https://learn.microsoft.com/en-us/windows/win32/medfound/about-dxva-2-0
Dxva2,
/// Direct Rendering Manager
/// https://dri.freedesktop.org/wiki/DRM/
Drm,
/// MediaCodec
/// Android only
/// https://developer.android.com/reference/android/media/MediaCodec
MediaCodec,
/// OpenCL
/// Only used in filters
/// https://www.khronos.org/opencl/
OpenCl,
/// Intel Quick Sync Video
/// https://www.intel.com/content/www/us/en/developer/tools/vpl/overview.html
Qsv,
/// Video Acceleration API
/// https://www.intel.com/content/www/us/en/developer/articles/technical/linuxmedia-vaapi.html
Vaapi,
/// Video Decode and Presentation API for Unix
/// https://www.freedesktop.org/wiki/Software/VDPAU/
Vdpau,
/// Video Toolbox
/// https://developer.apple.com/documentation/videotoolbox
VideoToolbox,
/// Vulkan
Vulkan,
}
impl DeviceType {
/// Hardware device names for user facing interfaces (logging, configs).
pub const fn name(self) -> &'static str {
match self {
Self::None => "None",
Self::Cuda => "CUDA",
Self::Dxva2 => "DirectX Video Acceleration 2.0",
Self::D3d11va => "DirectX 11 Video Acceleration",
Self::D3d12va => "DirectX 12 Video Acceleration",
Self::Drm => "Direct Rendering Manager (DRM)",
Self::MediaCodec => "MediaCodec",
Self::OpenCl => "OpenCL",
Self::Qsv => "Intel Quick Video Sync",
Self::Vaapi => "VA-API",
Self::Vdpau => "VDPAU",
Self::VideoToolbox => "VideoToolbox",
Self::Vulkan => "Vulkan",
}
}
/// Short name for CLI arguments
pub const fn short_name(self) -> &'static str {
match self {
Self::None => "none",
Self::Cuda => "cuda",
Self::Dxva2 => "dxva2",
Self::D3d11va => "d3d11va",
Self::D3d12va => "d3d12va",
Self::Drm => "drm",
Self::MediaCodec => "mediacodec",
Self::OpenCl => "opencl",
Self::Qsv => "qsv",
Self::Vaapi => "vaapi",
Self::Vdpau => "vdpau",
Self::VideoToolbox => "videotoolbox",
Self::Vulkan => "vulkan",
}
}
/// System's supported hardware decoders
pub fn supported_devices() -> SupportedDeviceIter {
SupportedDeviceIter::default()
}
}
impl FromStr for DeviceType {
type Err = DeError;
// av_hwdevice_find_type_by_name returns None for invalid device type names, but this type
// is used for deserializing configs (etc.) so the error is preserved.
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"none" => Ok(Self::None),
"cuda" => Ok(Self::Cuda),
"dxva2" => Ok(Self::Dxva2),
"d3d11va" => Ok(Self::D3d11va),
"d3d12va" => Ok(Self::D3d12va),
"drm" => Ok(Self::Drm),
"mediacodec" => Ok(Self::MediaCodec),
"opencl" => Ok(Self::OpenCl),
"qsv" => Ok(Self::Qsv),
"vaapi" => Ok(Self::Vaapi),
"vdpau" => Ok(Self::Vdpau),
"videotoolbox" => Ok(Self::VideoToolbox),
"vulkan" => Ok(Self::Vulkan),
_ => Err(DeError::invalid_value(
Unexpected::Str(s),
&"valid hardware decoder",
)),
}
}
}
impl fmt::Display for DeviceType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name())
}
}
impl From<AVHWDeviceType> for DeviceType {
fn from(value: AVHWDeviceType) -> Self {
match value {
AVHWDeviceType::AV_HWDEVICE_TYPE_NONE => Self::None,
AVHWDeviceType::AV_HWDEVICE_TYPE_CUDA => Self::Cuda,
AVHWDeviceType::AV_HWDEVICE_TYPE_DXVA2 => Self::Dxva2,
AVHWDeviceType::AV_HWDEVICE_TYPE_D3D11VA => Self::D3d11va,
// This variant exists in ffmpeg's C lib but not in Rust's crate yet.
// AVHWDeviceType::AV_HWDEVICE_TYPE_D3D12VA => Self::D3d12va
AVHWDeviceType::AV_HWDEVICE_TYPE_DRM => Self::Drm,
AVHWDeviceType::AV_HWDEVICE_TYPE_MEDIACODEC => Self::MediaCodec,
AVHWDeviceType::AV_HWDEVICE_TYPE_OPENCL => Self::OpenCl,
AVHWDeviceType::AV_HWDEVICE_TYPE_QSV => Self::Qsv,
AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI => Self::Vaapi,
AVHWDeviceType::AV_HWDEVICE_TYPE_VDPAU => Self::Vdpau,
AVHWDeviceType::AV_HWDEVICE_TYPE_VIDEOTOOLBOX => Self::VideoToolbox,
AVHWDeviceType::AV_HWDEVICE_TYPE_VULKAN => Self::Vulkan,
}
}
}
impl From<DeviceType> for AVHWDeviceType {
fn from(value: DeviceType) -> Self {
match value {
DeviceType::None => Self::AV_HWDEVICE_TYPE_NONE,
DeviceType::Cuda => Self::AV_HWDEVICE_TYPE_CUDA,
DeviceType::D3d11va => Self::AV_HWDEVICE_TYPE_D3D11VA,
// NOTE: Next FFmpeg release
DeviceType::D3d12va => Self::AV_HWDEVICE_TYPE_NONE,
DeviceType::Dxva2 => Self::AV_HWDEVICE_TYPE_DXVA2,
DeviceType::Drm => Self::AV_HWDEVICE_TYPE_DRM,
DeviceType::MediaCodec => Self::AV_HWDEVICE_TYPE_MEDIACODEC,
DeviceType::OpenCl => Self::AV_HWDEVICE_TYPE_OPENCL,
DeviceType::Qsv => Self::AV_HWDEVICE_TYPE_QSV,
DeviceType::Vaapi => Self::AV_HWDEVICE_TYPE_VAAPI,
DeviceType::Vdpau => Self::AV_HWDEVICE_TYPE_VDPAU,
DeviceType::VideoToolbox => Self::AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
DeviceType::Vulkan => Self::AV_HWDEVICE_TYPE_VULKAN,
}
}
}
impl Default for DeviceType {
fn default() -> Self {
Self::Vaapi
}
}

View file

@ -1,58 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
use std::iter::FusedIterator;
use ffmpeg_next::ffi::{av_hwdevice_iterate_types, AVHWDeviceType};
use super::device_type::DeviceType;
/// Iterator over system's supported hardware decoders.
pub struct SupportedDeviceIter {
current: AVHWDeviceType,
}
impl Default for SupportedDeviceIter {
fn default() -> Self {
// SAFETY: FFmpeg's documentation states that the iterator is delimited by AV_HWDEVICE_TYPE_NONE.
let current = unsafe { av_hwdevice_iterate_types(AVHWDeviceType::AV_HWDEVICE_TYPE_NONE) };
Self { current }
}
}
impl Iterator for SupportedDeviceIter {
type Item = DeviceType;
fn next(&mut self) -> Option<Self::Item> {
// None is a sentinel value that indicates the iterator is exhausted
if self.current == AVHWDeviceType::AV_HWDEVICE_TYPE_NONE {
None
} else {
let prev = self.current;
// SAFETY: The docs and examples state that the iterator yields the next value
// when the previous is passed in.
self.current = unsafe { av_hwdevice_iterate_types(prev) };
Some(prev.into())
}
}
}
impl FusedIterator for SupportedDeviceIter {}
#[cfg(test)]
mod tests {
use std::hint::black_box;
use super::*;
// The iterator's yielded values aren't important since hardware decoders vary by system
// This is just a sanity check to ensure the iterator works
#[test]
fn supported_device_iter_doesnt_seg_fault() {
for decoder in DeviceType::supported_devices() {
black_box(decoder);
}
let _decoders: Vec<_> = black_box(DeviceType::supported_devices().collect());
}
}

View file

@ -1,7 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
pub mod device_type;
pub mod iter;
pub use device_type::DeviceType;
pub use iter::SupportedDeviceIter;

View file

@ -1,439 +0,0 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::{
app::{message, Command, Core, Settings},
cosmic_config::{self, CosmicConfigEntry},
cosmic_theme, executor,
iced::{
event::{self, Event},
keyboard::{Event as KeyEvent, Key, Modifiers},
subscription::{self, Subscription},
window, Alignment, Length, Limits,
},
widget, Application, ApplicationExt, Element,
};
use std::{
any::TypeId,
collections::HashMap,
env, process,
sync::{mpsc, Arc, Mutex},
time::{Duration, Instant},
};
use crate::{
config::{AppTheme, Args, Config, CONFIG_VERSION},
fl,
key_bind::{key_binds, KeyBind},
localize,
};
pub mod hardware;
mod player;
use player::{PlayerMessage, VideoFrame, VideoQueue};
/// Runs application with these settings
#[rustfmt::skip]
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
localize::localize();
let mut args = match Args::parse_args() {
Ok(args) => args,
Err(e) => {
log::error!("{e}");
process::exit(1);
}
};
let (config_handler, config) = match cosmic_config::Config::new(App::APP_ID, CONFIG_VERSION) {
Ok(config_handler) => {
let mut config = match Config::get_entry(&config_handler) {
Ok(ok) => ok,
Err((errs, config)) => {
log::info!("errors loading config: {:?}", errs);
config
}
};
// Update config with command line args
config.with_args(&mut args);
(Some(config_handler), config)
}
Err(err) => {
log::error!("failed to create config handler: {}", err);
(None, Config::default())
}
};
//TODO: support using multiple paths
let Args { mut paths, .. } = args;
let path = paths.pop().unwrap();
// TODO: Update video player config when it's updated via the app
let (player_tx, video_queue_lock) = player::run(path, config.clone());
let mut settings = Settings::default();
settings = settings.theme(config.app_theme.theme());
settings = settings.size_limits(Limits::NONE.min_width(360.0).min_height(180.0));
let flags = Flags {
config_handler,
config,
player_tx,
video_queue_lock,
};
cosmic::app::run::<App>(settings, flags)?;
Ok(())
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Action {
Todo,
SeekBackward,
SeekForward,
}
impl Action {
pub fn message(&self) -> Message {
match self {
Self::Todo => Message::Todo,
Self::SeekBackward => Message::Player(PlayerMessage::SeekRelative(-10.0)),
Self::SeekForward => Message::Player(PlayerMessage::SeekRelative(10.0)),
}
}
}
#[derive(Clone)]
pub struct Flags {
config_handler: Option<cosmic_config::Config>,
config: Config,
player_tx: mpsc::Sender<PlayerMessage>,
video_queue_lock: Arc<Mutex<VideoQueue>>,
}
/// Messages that are used specifically by our [`App`].
#[derive(Clone, Debug)]
pub enum Message {
Todo,
AppTheme(AppTheme),
Config(Config),
Key(Modifiers, Key),
Player(PlayerMessage),
SystemThemeModeChange(cosmic_theme::ThemeMode),
Tick(Instant),
ToggleContextPage(ContextPage),
WindowClose,
WindowNew,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ContextPage {
Settings,
}
impl ContextPage {
fn title(&self) -> String {
match self {
Self::Settings => fl!("settings"),
}
}
}
/// The [`App`] stores application-specific state.
pub struct App {
core: Core,
flags: Flags,
app_themes: Vec<String>,
context_page: ContextPage,
key_binds: HashMap<KeyBind, Action>,
handle_opt: Option<widget::image::Handle>,
}
impl App {
fn update_config(&mut self) -> Command<Message> {
cosmic::app::command::set_theme(self.flags.config.app_theme.theme())
}
fn update_title(&mut self) -> Command<Message> {
let title = "COSMIC Media Player";
self.set_header_title(title.to_string());
self.set_window_title(title.to_string())
}
fn settings(&self) -> Element<Message> {
let app_theme_selected = match self.flags.config.app_theme {
AppTheme::Dark => 1,
AppTheme::Light => 2,
AppTheme::System => 0,
};
widget::settings::view_column(vec![widget::settings::view_section(fl!("appearance"))
.add(
widget::settings::item::builder(fl!("theme")).control(widget::dropdown(
&self.app_themes,
Some(app_theme_selected),
move |index| {
Message::AppTheme(match index {
1 => AppTheme::Dark,
2 => AppTheme::Light,
_ => AppTheme::System,
})
},
)),
)
.into()])
.into()
}
}
/// Implement [`Application`] to integrate with COSMIC.
impl Application for App {
/// Default async executor to use with the app.
type Executor = executor::Default;
/// Argument received
type Flags = Flags;
/// Message type specific to our [`App`].
type Message = Message;
/// The unique application ID to supply to the window manager.
const APP_ID: &'static str = "com.system76.CosmicPlayer";
fn core(&self) -> &Core {
&self.core
}
fn core_mut(&mut self) -> &mut Core {
&mut self.core
}
/// Creates the application, and optionally emits command on initialize.
fn init(core: Core, flags: Self::Flags) -> (Self, Command<Self::Message>) {
let app_themes = vec![fl!("match-desktop"), fl!("dark"), fl!("light")];
let mut app = App {
core,
flags,
app_themes,
context_page: ContextPage::Settings,
key_binds: key_binds(),
handle_opt: None,
};
let command = app.update_title();
(app, command)
}
fn on_escape(&mut self) -> Command<Message> {
if self.core.window.show_context {
// Close context drawer if open
self.core.window.show_context = false;
}
Command::none()
}
/// Handle application events here.
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
// Helper for updating config values efficiently
macro_rules! config_set {
($name: ident, $value: expr) => {
match &self.flags.config_handler {
Some(config_handler) => {
match paste::paste! { self.flags.config.[<set_ $name>](config_handler, $value) } {
Ok(_) => {}
Err(err) => {
log::warn!(
"failed to save config {:?}: {}",
stringify!($name),
err
);
}
}
}
None => {
self.flags.config.$name = $value;
log::warn!(
"failed to save config {:?}: no config handler",
stringify!($name)
);
}
}
};
}
match message {
Message::Todo => {
log::warn!("TODO");
}
Message::AppTheme(app_theme) => {
config_set!(app_theme, app_theme);
return self.update_config();
}
Message::Config(config) => {
if config != self.flags.config {
log::info!("update config");
//TODO: update syntax theme by clearing tabs, only if needed
self.flags.config = config;
return self.update_config();
}
}
Message::Key(modifiers, key) => {
for (key_bind, action) in self.key_binds.iter() {
if key_bind.matches(modifiers, &key) {
return self.update(action.message());
}
}
}
Message::Player(player_message) => {
self.flags.player_tx.send(player_message).unwrap();
}
Message::SystemThemeModeChange(_theme_mode) => {
return self.update_config();
}
Message::Tick(frame_time) => {
let start = Instant::now();
let mut video_frame_opt: Option<VideoFrame> = None;
let delayed_time = {
let mut video_queue = self.flags.video_queue_lock.lock().unwrap();
let delayed_time = frame_time - video_queue.delay;
while let Some(video_frame) = video_queue.data.pop_front() {
if video_frame.1.unwrap_or(delayed_time) <= delayed_time {
if let Some(old_frame) = video_frame_opt {
//TODO: log this outside of locking video_queue_lock?
log::warn!("skipping video frame {:?}", old_frame.0.pts());
}
// Frame is ready to be shown
video_frame_opt = Some(video_frame);
} else {
// Put frame back and exit loop
video_queue.data.push_front(video_frame);
break;
}
}
delayed_time
};
match video_frame_opt {
Some(video_frame) => {
let pts = video_frame.0.pts();
let present_time_opt = video_frame.1;
self.handle_opt = Some(video_frame.into_handle());
let duration = start.elapsed();
log::debug!(
"converted video frame at {:?} to handle in {:?}",
pts,
duration
);
if let Some(present_time) = present_time_opt {
if present_time > delayed_time {
let ahead = present_time - delayed_time;
if ahead > Duration::from_millis(1) {
log::debug!("video ahead {:?}", ahead);
}
} else {
let behind = delayed_time - present_time;
if behind > Duration::from_millis(1) {
log::debug!("video behind {:?}", behind);
}
}
}
}
None => {}
}
}
Message::ToggleContextPage(context_page) => {
//TODO: ensure context menus are closed
if self.context_page == context_page {
self.core.window.show_context = !self.core.window.show_context;
} else {
self.context_page = context_page;
self.core.window.show_context = true;
}
self.set_context_title(context_page.title());
}
Message::WindowClose => {
return window::close(window::Id::MAIN);
}
Message::WindowNew => match env::current_exe() {
Ok(exe) => match process::Command::new(&exe).spawn() {
Ok(_child) => {}
Err(err) => {
log::error!("failed to execute {:?}: {}", exe, err);
}
},
Err(err) => {
log::error!("failed to get current executable path: {}", err);
}
},
}
Command::none()
}
fn context_drawer(&self) -> Option<Element<Message>> {
if !self.core.window.show_context {
return None;
}
Some(match self.context_page {
ContextPage::Settings => self.settings(),
})
}
/// Creates a view after each update.
fn view(&self) -> Element<Self::Message> {
let content: Element<_> = match &self.handle_opt {
Some(handle) => widget::image(handle.clone())
.width(Length::Fill)
.height(Length::Fill)
.into(),
None => widget::text("Loading").into(),
};
// Uncomment to debug layout:
//content.explain(cosmic::iced::Color::WHITE)
content
}
fn subscription(&self) -> Subscription<Self::Message> {
struct ConfigSubscription;
struct ThemeSubscription;
Subscription::batch([
window::frames().map(|(_window_id, instant)| Message::Tick(instant)),
event::listen_with(|event, _status| match event {
Event::Keyboard(KeyEvent::KeyPressed { key, modifiers, .. }) => {
Some(Message::Key(modifiers, key))
}
_ => None,
}),
cosmic_config::config_subscription(
TypeId::of::<ConfigSubscription>(),
Self::APP_ID.into(),
CONFIG_VERSION,
)
.map(|update| {
if !update.errors.is_empty() {
log::debug!("errors loading config: {:?}", update.errors);
}
Message::SystemThemeModeChange(update.config)
}),
cosmic_config::config_subscription::<_, cosmic_theme::ThemeMode>(
TypeId::of::<ThemeSubscription>(),
cosmic_theme::THEME_MODE_ID.into(),
cosmic_theme::ThemeMode::version(),
)
.map(|update| {
if !update.errors.is_empty() {
log::debug!("errors loading theme mode: {:?}", update.errors);
}
Message::SystemThemeModeChange(update.config)
}),
])
}
}

View file

@ -1,654 +0,0 @@
extern crate ffmpeg_next as ffmpeg;
use cosmic::widget;
use cpal::{
traits::{DeviceTrait, HostTrait, StreamTrait},
FromSample, SizedSample,
};
use ffmpeg::{
codec, ffi,
format::{input, Pixel},
media::Type,
software::{resampling, scaling},
util::{
channel_layout, error,
format::sample,
frame::{audio::Audio, video::Video},
},
Packet,
};
use std::{
cmp,
collections::VecDeque,
error::Error,
path::{Path, PathBuf},
ptr, slice,
sync::{mpsc, Arc, Mutex},
thread,
time::{Duration, Instant},
};
use crate::config::Config;
//TODO: calculate presentation time of end of queue
pub struct AudioQueue {
pub channels: usize,
pub rate: f64,
pub data: VecDeque<f32>,
// Delay for data to hit speakers, used to sync with video
pub delay: Duration,
}
impl AudioQueue {
pub fn new(channels: cpal::ChannelCount, rate: cpal::SampleRate) -> Self {
Self {
channels: channels as usize,
rate: rate.0 as f64,
data: VecDeque::new(),
delay: Duration::default(),
}
}
pub fn duration(&self) -> Duration {
self.duration_for_samples(self.data.len())
}
pub fn duration_for_samples(&self, samples: usize) -> Duration {
let frames = samples / self.channels;
let seconds = (frames as f64) / self.rate;
Duration::from_secs_f64(seconds)
}
}
#[derive(Clone, Debug)]
pub enum PlayerMessage {
SeekRelative(f64),
}
pub struct VideoFrame(pub Video, pub Option<Instant>);
impl VideoFrame {
pub fn into_handle(self) -> widget::image::Handle {
let width = self.0.width();
let height = self.0.height();
widget::image::Handle::from_pixels(width, height, self)
}
}
impl AsRef<[u8]> for VideoFrame {
fn as_ref(&self) -> &[u8] {
self.0.data(0)
}
}
pub struct VideoQueue {
pub data: VecDeque<VideoFrame>,
// Delay to add to each frame to sync with audio
pub delay: Duration,
}
impl VideoQueue {
pub fn new() -> Self {
Self {
data: VecDeque::new(),
delay: Duration::default(),
}
}
pub fn push(&mut self, frame: VideoFrame) {
// Discard all frames that are newer than frame to fix seeking and duration calculation
self.data
.retain(|other| other.1.map_or(true, |x| x <= frame.1.unwrap_or(x)));
self.data.push_back(frame);
}
pub fn duration(&self) -> Duration {
//TODO: can accurate duration actually be calculated since one frame would count as zero?
let mut start_end_opt = None;
for frame in self.data.iter() {
if let Some(frame_time) = frame.1 {
start_end_opt = Some(match start_end_opt {
Some((start, end)) => (cmp::min(start, frame_time), cmp::max(end, frame_time)),
None => (frame_time, frame_time),
});
}
}
if let Some((start, end)) = start_end_opt {
end.duration_since(start)
} else {
Duration::default()
}
}
}
fn cpal() -> (
cpal::SupportedStreamConfig,
Box<dyn StreamTrait>,
Arc<Mutex<AudioQueue>>,
) {
let host = cpal::default_host();
let device = host
.default_output_device()
.expect("failed to get default audio output device");
let config = device
.default_output_config()
.expect("failed to get default audio output config");
println!("{:?}: {:?}", device.name(), config);
let audio_queue_lock = Arc::new(Mutex::new(AudioQueue::new(
config.channels(),
config.sample_rate(),
)));
let stream = {
let config = config.clone();
let audio_queue_lock = audio_queue_lock.clone();
match config.sample_format() {
cpal::SampleFormat::I8 => cpal_stream::<i8>(device, config.into(), audio_queue_lock),
cpal::SampleFormat::I16 => cpal_stream::<i16>(device, config.into(), audio_queue_lock),
// cpal::SampleFormat::I24 => cpal_stream::<I24>(device, config.into(), audio_queue_lock),
cpal::SampleFormat::I32 => cpal_stream::<i32>(device, config.into(), audio_queue_lock),
// cpal::SampleFormat::I48 => cpal_stream::<I48>(device, config.into(), audio_queue_lock),
cpal::SampleFormat::I64 => cpal_stream::<i64>(device, config.into(), audio_queue_lock),
cpal::SampleFormat::U8 => cpal_stream::<u8>(device, config.into(), audio_queue_lock),
cpal::SampleFormat::U16 => cpal_stream::<u16>(device, config.into(), audio_queue_lock),
// cpal::SampleFormat::U24 => cpal_stream::<U24>(device, config.into(), audio_queue_lock),
cpal::SampleFormat::U32 => cpal_stream::<u32>(device, config.into(), audio_queue_lock),
// cpal::SampleFormat::U48 => cpal_stream::<U48>(device, config.into(), audio_queue_lock),
cpal::SampleFormat::U64 => cpal_stream::<u64>(device, config.into(), audio_queue_lock),
cpal::SampleFormat::F32 => cpal_stream::<f32>(device, config.into(), audio_queue_lock),
cpal::SampleFormat::F64 => cpal_stream::<f64>(device, config.into(), audio_queue_lock),
sample_format => panic!("unsupported sample format '{sample_format}'"),
}
.unwrap()
};
(config, stream, audio_queue_lock)
}
fn cpal_stream<T>(
device: cpal::Device,
config: cpal::StreamConfig,
audio_queue_lock: Arc<Mutex<AudioQueue>>,
) -> Result<Box<dyn StreamTrait>, Box<dyn Error>>
where
T: SizedSample + FromSample<f32>,
{
let data_fn = {
move |samples: &mut [T], info: &cpal::OutputCallbackInfo| {
let timestamp = info.timestamp();
let delay = timestamp.playback.duration_since(&timestamp.callback);
let mut underrun = 0;
{
//TODO: buffer audio
let mut audio_queue = audio_queue_lock.lock().unwrap();
//TODO: also add samples time?
audio_queue.delay = delay.unwrap_or_default();
for sample in samples {
let float = match audio_queue.data.pop_front() {
Some(some) => some,
None => {
underrun += 1;
0.0
}
};
*sample = T::from_sample(float);
}
};
if underrun > 0 {
log::error!("audio underrun {}", underrun);
}
}
};
let err_fn = |err| eprintln!("an error occurred on stream: {}", err);
let stream = device.build_output_stream(&config, data_fn, err_fn, None)?;
Ok(Box::new(stream))
}
fn ffmpeg_thread<P: AsRef<Path>>(
path: P,
player_rx: mpsc::Receiver<PlayerMessage>,
video_queue_lock: Arc<Mutex<VideoQueue>>,
config: Config,
) -> Result<(), Box<dyn Error>> {
let (audio_config, cpal_stream, audio_queue_lock) = cpal();
let mut ictx = input(&path)?;
let video_stream = ictx
.streams()
.best(Type::Video)
.ok_or(ffmpeg::Error::StreamNotFound)?;
let video_stream_index = video_stream.index();
let video_time_base = f64::from(video_stream.time_base());
let mut video_decoder = {
let mut video_decoder_context =
codec::context::Context::from_parameters(video_stream.parameters())?;
//TODO: safe wrappers
let mut hw_device_ctx = ptr::null_mut();
unsafe {
//TODO: support other types
let hw_device_kind = config.hw_decoder;
if ffi::av_hwdevice_ctx_create(
&mut hw_device_ctx,
hw_device_kind.into(),
ptr::null(),
ptr::null_mut(),
0,
) == 0
{
log::info!("using {hw_device_kind} decoding");
(&mut *video_decoder_context.as_mut_ptr()).hw_device_ctx =
ffi::av_buffer_ref(hw_device_ctx);
} else {
//TODO: support other hardware devices
log::warn!(
"failed to use {hw_device_kind} decoding, falling back to software decoding"
);
}
}
video_decoder_context.decoder().video()?
};
let (cpu_frame_tx, cpu_frame_rx) = mpsc::channel::<(Video, Option<Instant>)>();
{
let video_format = video_decoder.format();
let video_width = video_decoder.width();
let video_height = video_decoder.height();
let video_queue_lock = video_queue_lock.clone();
thread::Builder::new()
.name("video_scale".to_string())
.spawn(move || {
let mut video_scaler = scaling::context::Context::get(
video_format,
video_width,
video_height,
Pixel::RGBA,
video_width,
video_height,
scaling::Flags::FAST_BILINEAR,
)
.unwrap();
loop {
let mut recv_opt: Option<(Video, Option<Instant>)> = None;
/*TODO: SKIP
while let Ok(recv) = cpu_frame_rx.try_recv() {
if let Some((old_frame, _)) = recv_opt {
//TODO: only skip if behind (frames come in weird timing from codecs)
log::warn!("skipping cpu video frame at {:?}", old_frame.pts());
}
recv_opt = Some(recv);
}
*/
let (cpu_frame, sync_time_opt) = match recv_opt {
Some(some) => some,
None => cpu_frame_rx.recv().unwrap(),
};
let pts_opt = cpu_frame.pts();
// Start count after blocking recv
let start = Instant::now();
video_scaler.cached(
cpu_frame.format(),
cpu_frame.width(),
cpu_frame.height(),
Pixel::RGBA,
cpu_frame.width(),
cpu_frame.height(),
scaling::Flags::FAST_BILINEAR,
);
let mut scaled_frame = Video::empty();
video_scaler.run(&cpu_frame, &mut scaled_frame).unwrap();
scaled_frame.set_pts(pts_opt);
let present_time_opt = if let Some(pts) = pts_opt {
let expected_float = pts as f64 * video_time_base;
let expected = Duration::from_secs_f64(expected_float);
if let Some(sync_time) = sync_time_opt {
Some(sync_time + expected)
} else {
None
}
} else {
None
};
let video_frame = VideoFrame(scaled_frame, present_time_opt);
{
let mut video_queue = video_queue_lock.lock().unwrap();
video_queue.push(video_frame);
}
let duration = start.elapsed();
log::debug!("scaled video frame at {:?} in {:?}", pts_opt, duration,);
}
})?
};
// Sync channel to prevent allocation issues and falling behind
let (gpu_frame_tx, gpu_frame_rx) = mpsc::sync_channel::<(Video, Option<Instant>)>(2);
thread::Builder::new()
.name("video_map_gpu_cpu".to_string())
.spawn(move || {
loop {
let mut recv_opt: Option<(Video, Option<Instant>)> = None;
/*TODO: SKIP
while let Ok(recv) = gpu_frame_rx.try_recv() {
if let Some((old_frame, _)) = recv_opt {
//TODO: only skip if behind (frames come in weird timing from codecs)
log::warn!("skipping gpu video frame at {:?}", old_frame.pts());
}
recv_opt = Some(recv);
}
*/
let (gpu_frame, sync_time_opt) = match recv_opt {
Some(some) => some,
None => gpu_frame_rx.recv().unwrap(),
};
let pts = gpu_frame.pts();
// Start timer after blocking recv
let start = Instant::now();
let mut cpu_frame = Video::empty();
unsafe {
if (&*gpu_frame.as_ptr()).hw_frames_ctx.is_null() {
cpu_frame = gpu_frame;
} else {
if ffi::av_hwframe_transfer_data(
cpu_frame.as_mut_ptr(),
gpu_frame.as_ptr(),
0,
) < 0
{
panic!("av_hwframe_transfer_data failed");
}
/*TODO: MAP OR TRANSFER?
if ffi::av_hwframe_map(
cpu_frame.as_mut_ptr(),
gpu_frame.as_ptr(),
ffi::AV_HWFRAME_MAP_READ as i32,
) < 0
{
panic!("av_hwframe_map failed");
}
*/
}
}
cpu_frame.set_pts(pts);
cpu_frame_tx.send((cpu_frame, sync_time_opt)).unwrap();
let duration = start.elapsed();
log::debug!("map gpu video frame to cpu at {:?} in {:?}", pts, duration);
}
})?;
// Sync channel to prevent getting too far behind
let (video_packet_tx, video_packet_rx) = mpsc::sync_channel::<(Packet, Option<Instant>)>(2);
thread::Builder::new()
.name("video_decode".to_string())
.spawn(move || {
let mut eof = false;
while !eof {
let mut sync_time_opt = None;
{
let packet_res = video_packet_rx.recv();
// Start timer after blocking recv
let start = Instant::now();
let mut packet_pts = None;
match packet_res {
Ok((packet, time_opt)) => {
packet_pts = packet.pts();
sync_time_opt = time_opt;
video_decoder.send_packet(&packet).unwrap();
}
Err(_err) => {
video_decoder.send_eof().unwrap();
eof = true;
}
}
let duration = start.elapsed();
log::debug!("sent packet at {:?} in {:?}", packet_pts, duration);
}
let start = Instant::now();
let mut pts = None;
let mut video_frames = 0;
loop {
let mut gpu_frame = Video::empty();
if video_decoder.receive_frame(&mut gpu_frame).is_ok() {
pts = gpu_frame.pts();
gpu_frame_tx.send((gpu_frame, sync_time_opt)).unwrap();
video_frames += 1;
} else {
break;
}
}
if video_frames > 0 {
let duration = start.elapsed();
log::debug!(
"received {} video frames at {:?} in {:?}",
video_frames,
pts,
duration
);
}
}
})?;
let audio_stream = ictx
.streams()
.best(Type::Audio)
.ok_or(ffmpeg::Error::StreamNotFound)?;
let audio_stream_index = audio_stream.index();
let audio_time_base = f64::from(audio_stream.time_base());
let audio_context_decoder =
codec::context::Context::from_parameters(audio_stream.parameters())?;
let mut audio_decoder = audio_context_decoder.decoder().audio()?;
let mut audio_resampler = resampling::Context::get(
audio_decoder.format(),
audio_decoder.channel_layout(),
audio_decoder.rate(),
//TODO: support other formats?
sample::Sample::F32(sample::Type::Packed),
match audio_config.channels() {
1 => channel_layout::ChannelLayout::MONO,
2 => channel_layout::ChannelLayout::STEREO,
//TODO: more channel configs
unsupported => {
panic!("unsupported audio channels {:?}", unsupported);
}
},
audio_config.sample_rate().0,
)?;
let min_sleep = Duration::from_millis(1);
let min_skip = Duration::from_millis(1);
let mut receive_and_process_decoded_audio_frames = |decoder: &mut ffmpeg::decoder::Audio,
sync_time_opt: &mut Option<Instant>|
-> Result<(), ffmpeg::Error> {
let mut decoded = Audio::empty();
let mut resampled = Audio::empty();
let mut pts_opt = None;
while decoder.receive_frame(&mut decoded).is_ok() {
pts_opt = decoded.pts();
audio_resampler.run(&decoded, &mut resampled)?;
{
// plane method doesn't work with packed samples, so do it manually
let plane = unsafe {
slice::from_raw_parts(
(*resampled.as_ptr()).data[0] as *const f32,
resampled.samples() * resampled.channels() as usize,
)
};
{
let mut audio_queue = audio_queue_lock.lock().unwrap();
audio_queue.data.extend(plane);
}
}
}
if let Some(pts) = pts_opt {
let expected_float = pts as f64 * audio_time_base;
let expected = Duration::from_secs_f64(expected_float);
if let Some(sync_time) = &sync_time_opt {
// Sync with audio
let actual = sync_time.elapsed();
if expected > actual {
let sleep = expected - actual;
if sleep > min_sleep {
// We leave min_sleep of buffer room
log::debug!("audio ahead {:?}", sleep);
}
} else {
let skip = actual - expected;
if skip > min_skip {
//TODO: handle frame skipping
log::debug!("audio behind {:?}", skip);
}
}
} else {
// Set up sync
*sync_time_opt = Some(Instant::now() - expected);
}
}
Ok(())
};
//TODO: dynamically choose this
let buffer_duration = Duration::from_millis(250);
// Start CPAL stream
cpal_stream.play()?;
let mut sync_time_opt = None;
let mut seconds_opt = None;
loop {
let mut packet = Packet::empty();
match packet.read(&mut ictx) {
Ok(()) => {
if packet.stream() == video_stream_index {
video_packet_tx.send((packet, sync_time_opt)).unwrap();
} else if packet.stream() == audio_stream_index {
audio_decoder.send_packet(&packet)?;
receive_and_process_decoded_audio_frames(
&mut audio_decoder,
&mut sync_time_opt,
)?;
if let Some(pts) = packet.pts() {
seconds_opt = Some(pts as f64 * audio_time_base);
}
}
}
Err(error::Error::Eof) => break,
Err(_err) => {}
}
let (audio_queue_duration, audio_queue_delay) = {
let audio_queue = audio_queue_lock.lock().unwrap();
(audio_queue.duration(), audio_queue.delay)
};
let (video_queue_duration, video_queue_delay) = {
let mut video_queue = video_queue_lock.lock().unwrap();
let video_queue_duration = video_queue.duration();
if video_queue_duration < buffer_duration {
// If we do not have enough video queued, delay the video output
video_queue.delay = buffer_duration - video_queue_duration;
} else {
video_queue.delay = Duration::default();
}
// Add audio queue delay to sync with audio
video_queue.delay += audio_queue_delay;
(video_queue_duration, video_queue.delay)
};
log::debug!(
"video: {:?}, {:?} audio: {:?}, {:?}",
video_queue_duration,
video_queue_delay,
audio_queue_duration,
audio_queue_delay
);
let min_queue_duration = cmp::min(video_queue_duration, audio_queue_duration);
if min_queue_duration > buffer_duration {
// If we have enough queued, we can sleep
let sleep = min_queue_duration - buffer_duration;
log::debug!("sleep {:?}", sleep);
thread::sleep(sleep);
}
while let Ok(message) = player_rx.try_recv() {
match message {
PlayerMessage::SeekRelative(seek_seconds) => {
if let Some(seconds) = seconds_opt {
//TODO: use time base instead of hardcoded values
let timestamp = ((seconds + seek_seconds) * 1000000.0) as i64;
if seek_seconds.is_sign_negative() {
println!(
"backwards {} from {} = {}",
seek_seconds, seconds, timestamp
);
ictx.seek(timestamp, ..timestamp)?;
} else {
println!("forwards {} from {} = {}", seek_seconds, seconds, timestamp);
ictx.seek(timestamp, timestamp..)?;
}
// Clear audio sync time
sync_time_opt = None;
// Clear audio and video queues
{
let mut audio_queue = audio_queue_lock.lock().unwrap();
audio_queue.data.clear();
}
{
//TODO: clear pending data stuck in channels
let mut video_queue = video_queue_lock.lock().unwrap();
video_queue.data.clear();
}
}
}
}
}
}
audio_decoder.send_eof()?;
receive_and_process_decoded_audio_frames(&mut audio_decoder, &mut sync_time_opt)?;
Ok(())
}
pub fn run(path: PathBuf, config: Config) -> (mpsc::Sender<PlayerMessage>, Arc<Mutex<VideoQueue>>) {
ffmpeg::init().unwrap();
let (player_tx, player_rx) = mpsc::channel();
let video_queue_lock = Arc::new(Mutex::new(VideoQueue::new()));
{
let video_queue_lock = video_queue_lock.clone();
thread::Builder::new()
.name("ffmpeg".to_string())
.spawn(move || {
ffmpeg_thread(path, player_rx, video_queue_lock, config).unwrap();
})
.unwrap();
}
(player_tx, video_queue_lock)
}

View file

@ -1,585 +0,0 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::{
app::{message, Command, Core, Settings},
cosmic_config::{self, CosmicConfigEntry},
cosmic_theme, executor, font,
iced::{
event::{self, Event},
keyboard::{Event as KeyEvent, Key, Modifiers},
mouse::Event as MouseEvent,
subscription::Subscription,
window, Alignment, Color, Length, Limits,
},
theme,
widget::{self, Slider},
Application, ApplicationExt, Element,
};
use iced_video_player::{
gst::{self, prelude::*},
gst_pbutils, Video, VideoPlayer,
};
use std::{
any::TypeId,
collections::HashMap,
time::{Duration, Instant},
};
use crate::{
config::{Config, CONFIG_VERSION},
key_bind::{key_binds, KeyBind},
localize,
};
static CONTROLS_TIMEOUT: Duration = Duration::new(2, 0);
const GST_PLAY_FLAG_VIDEO: i32 = 1 << 0;
const GST_PLAY_FLAG_AUDIO: i32 = 1 << 1;
const GST_PLAY_FLAG_TEXT: i32 = 1 << 2;
/// Runs application with these settings
#[rustfmt::skip]
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
localize::localize();
let (config_handler, config) = match cosmic_config::Config::new(App::APP_ID, CONFIG_VERSION) {
Ok(config_handler) => {
let config = match Config::get_entry(&config_handler) {
Ok(ok) => ok,
Err((errs, config)) => {
log::info!("errors loading config: {:?}", errs);
config
}
};
(Some(config_handler), config)
}
Err(err) => {
log::error!("failed to create config handler: {}", err);
(None, Config::default())
}
};
let mut settings = Settings::default();
settings = settings.theme(config.app_theme.theme());
settings = settings.size_limits(Limits::NONE.min_width(360.0).min_height(180.0));
let url = url::Url::from_file_path(
std::env::args().nth(1).unwrap()
)
.unwrap();
let flags = Flags {
config_handler,
config,
url,
};
cosmic::app::run::<App>(settings, flags)?;
Ok(())
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Action {
Fullscreen,
PlayPause,
SeekBackward,
SeekForward,
}
impl Action {
pub fn message(&self) -> Message {
match self {
Self::Fullscreen => Message::Fullscreen,
Self::PlayPause => Message::PlayPause,
Self::SeekBackward => Message::SeekRelative(-10.0),
Self::SeekForward => Message::SeekRelative(10.0),
}
}
}
#[derive(Clone)]
pub struct Flags {
config_handler: Option<cosmic_config::Config>,
config: Config,
url: url::Url,
}
/// Messages that are used specifically by our [`App`].
#[derive(Clone, Debug)]
pub enum Message {
Config(Config),
Fullscreen,
Key(Modifiers, Key),
AudioCode(usize),
TextCode(usize),
PlayPause,
Seek(f64),
SeekRelative(f64),
SeekRelease,
EndOfStream,
MissingPlugin(gst::Message),
NewFrame,
Reload,
ShowControls,
SystemThemeModeChange(cosmic_theme::ThemeMode),
}
/// The [`App`] stores application-specific state.
pub struct App {
core: Core,
flags: Flags,
controls: bool,
controls_time: Instant,
fullscreen: bool,
key_binds: HashMap<KeyBind, Action>,
video_opt: Option<Video>,
position: f64,
duration: f64,
dragging: bool,
audio_codes: Vec<String>,
current_audio: i32,
text_codes: Vec<String>,
current_text: i32,
}
impl App {
fn close(&mut self) {
self.video_opt = None;
self.position = 0.0;
self.duration = 0.0;
self.dragging = false;
self.audio_codes = Vec::new();
self.current_audio = -1;
self.text_codes = Vec::new();
self.current_text = -1;
}
fn load(&mut self) -> Command<Message> {
self.close();
let video = match Video::new(&self.flags.url) {
Ok(ok) => ok,
Err(err) => {
log::warn!("failed to open {:?}: {err}", self.flags.url);
return Command::none();
}
};
self.duration = video.duration().as_secs_f64();
let pipeline = video.pipeline();
self.video_opt = Some(video);
let n_audio = pipeline.property::<i32>("n-audio");
self.audio_codes = Vec::with_capacity(n_audio as usize);
for i in 0..n_audio {
let tags: gst::TagList = pipeline.emit_by_name("get-audio-tags", &[&i]);
log::info!("audio stream {i}: {tags:?}");
self.audio_codes.push(
if let Some(language_code) = tags.get::<gst::tags::LanguageCode>() {
language_code.get().to_string()
} else {
format!("Audio #{i}")
},
);
}
self.current_audio = pipeline.property::<i32>("current-audio");
let n_text = pipeline.property::<i32>("n-text");
self.text_codes = Vec::with_capacity(n_text as usize);
for i in 0..n_text {
let tags: gst::TagList = pipeline.emit_by_name("get-text-tags", &[&i]);
log::info!("text stream {i}: {tags:?}");
self.text_codes.push(
if let Some(language_code) = tags.get::<gst::tags::LanguageCode>() {
language_code.get().to_string()
} else {
format!("Subtitle #{i}")
},
);
}
self.current_text = pipeline.property::<i32>("current-text");
//TODO: Flags can be used to enable/disable subtitles
let flags_value = pipeline.property_value("flags");
println!("original flags {:?}", flags_value);
match flags_value.transform::<i32>() {
Ok(flags_transform) => match flags_transform.get::<i32>() {
Ok(mut flags) => {
flags |= GST_PLAY_FLAG_VIDEO | GST_PLAY_FLAG_AUDIO | GST_PLAY_FLAG_TEXT;
match gst::glib::Value::from(flags).transform_with_type(flags_value.type_()) {
Ok(value) => pipeline.set_property("flags", value),
Err(err) => {
log::warn!("failed to transform int to flags: {err}");
}
}
}
Err(err) => {
log::warn!("failed to get flags as int: {err}");
}
},
Err(err) => {
log::warn!("failed to transform flags to int: {err}");
}
}
println!("updated flags {:?}", pipeline.property_value("flags"));
self.update_title()
}
fn update_controls(&mut self, in_use: bool) {
if in_use {
self.controls = true;
self.controls_time = Instant::now();
} else if self.controls && self.controls_time.elapsed() > CONTROLS_TIMEOUT {
self.controls = false;
}
}
fn update_config(&mut self) -> Command<Message> {
cosmic::app::command::set_theme(self.flags.config.app_theme.theme())
}
fn update_title(&mut self) -> Command<Message> {
//TODO: filename?
let title = "COSMIC Media Player";
self.set_window_title(title.to_string())
}
}
/// Implement [`cosmic::Application`] to integrate with COSMIC.
impl Application for App {
/// Default async executor to use with the app.
type Executor = executor::Default;
/// Argument received [`cosmic::Application::new`].
type Flags = Flags;
/// Message type specific to our [`App`].
type Message = Message;
/// The unique application ID to supply to the window manager.
const APP_ID: &'static str = "com.system76.CosmicPlayer";
fn core(&self) -> &Core {
&self.core
}
fn core_mut(&mut self) -> &mut Core {
&mut self.core
}
/// Creates the application, and optionally emits command on initialize.
fn init(mut core: Core, flags: Self::Flags) -> (Self, Command<Self::Message>) {
core.window.content_container = false;
let mut app = App {
core,
flags,
controls: true,
controls_time: Instant::now(),
fullscreen: false,
key_binds: key_binds(),
video_opt: None,
position: 0.0,
duration: 0.0,
dragging: false,
audio_codes: Vec::new(),
current_audio: -1,
text_codes: Vec::new(),
current_text: -1,
};
let command = app.load();
(app, command)
}
fn on_escape(&mut self) -> Command<Self::Message> {
if self.fullscreen {
return self.update(Message::Fullscreen);
} else {
Command::none()
}
}
/// Handle application events here.
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
match message {
Message::Config(config) => {
if config != self.flags.config {
log::info!("update config");
self.flags.config = config;
return self.update_config();
}
}
Message::Fullscreen => {
self.fullscreen = !self.fullscreen;
self.core.window.show_headerbar = !self.fullscreen;
return window::change_mode(
window::Id::MAIN,
if self.fullscreen {
window::Mode::Fullscreen
} else {
window::Mode::Windowed
},
);
}
Message::Key(modifiers, key) => {
for (key_bind, action) in self.key_binds.iter() {
if key_bind.matches(modifiers, &key) {
return self.update(action.message());
}
}
}
Message::AudioCode(code) => {
if let Ok(code) = i32::try_from(code) {
if let Some(video) = &self.video_opt {
let pipeline = video.pipeline();
pipeline.set_property("current-audio", code);
self.current_audio = pipeline.property("current-audio");
}
}
}
Message::TextCode(code) => {
if let Ok(code) = i32::try_from(code) {
if let Some(video) = &self.video_opt {
let pipeline = video.pipeline();
pipeline.set_property("current-text", code);
self.current_text = pipeline.property("current-text");
}
}
}
Message::PlayPause => {
if let Some(video) = &mut self.video_opt {
video.set_paused(!video.paused());
self.update_controls(true);
}
}
Message::Seek(secs) => {
if let Some(video) = &mut self.video_opt {
self.dragging = true;
self.position = secs;
video.set_paused(true);
let duration = Duration::try_from_secs_f64(self.position).unwrap_or_default();
video.seek(duration, true).expect("seek");
self.update_controls(true);
}
}
Message::SeekRelative(secs) => {
if let Some(video) = &mut self.video_opt {
self.position = video.position().as_secs_f64();
let duration =
Duration::try_from_secs_f64(self.position + secs).unwrap_or_default();
video.seek(duration, true).expect("seek");
}
}
Message::SeekRelease => {
if let Some(video) = &mut self.video_opt {
self.dragging = false;
let duration = Duration::try_from_secs_f64(self.position).unwrap_or_default();
video.seek(duration, true).expect("seek");
video.set_paused(false);
self.update_controls(true);
}
}
Message::EndOfStream => {
println!("end of stream");
}
Message::MissingPlugin(element) => {
if let Some(video) = &mut self.video_opt {
video.set_paused(true);
}
return Command::perform(
async move {
tokio::task::spawn_blocking(move || {
match gst_pbutils::MissingPluginMessage::parse(&element) {
Ok(missing_plugin) => {
let mut install_ctx = gst_pbutils::InstallPluginsContext::new();
install_ctx
.set_desktop_id(&format!("{}.desktop", Self::APP_ID));
let install_detail = missing_plugin.installer_detail();
println!("installing plugins: {}", install_detail);
let status = gst_pbutils::missing_plugins::install_plugins_sync(
&[&install_detail],
Some(&install_ctx),
);
log::info!("plugin install status: {}", status);
log::info!(
"gstreamer registry update: {:?}",
gst::Registry::update()
);
}
Err(err) => {
log::warn!("failed to parse missing plugin message: {err}");
}
}
message::app(Message::Reload)
})
.await
.unwrap()
},
|x| x,
);
}
Message::NewFrame => {
if let Some(video) = &self.video_opt {
if !self.dragging {
self.position = video.position().as_secs_f64();
self.update_controls(false);
}
}
}
Message::Reload => {
return self.load();
}
Message::ShowControls => {
self.update_controls(true);
}
Message::SystemThemeModeChange(_theme_mode) => {
return self.update_config();
}
}
Command::none()
}
fn header_start(&self) -> Vec<Element<Self::Message>> {
let mut row = widget::row::with_capacity(4)
.align_items(Alignment::Center)
.spacing(8);
if !self.audio_codes.is_empty() {
//TODO: allow mute/unmute/change volume
row = row.push(widget::icon::from_name("audio-volume-high-symbolic").size(16));
row = row.push(widget::dropdown(
&self.audio_codes,
usize::try_from(self.current_audio).ok(),
Message::AudioCode,
));
}
if !self.text_codes.is_empty() {
//TODO: allow toggling subtitles
row = row.push(widget::icon::from_name("media-view-subtitles-symbolic").size(16));
row = row.push(widget::dropdown(
&self.text_codes,
usize::try_from(self.current_text).ok(),
Message::TextCode,
));
}
vec![row.into()]
}
/// Creates a view after each update.
fn view(&self) -> Element<Self::Message> {
let format_time = |time_float: f64| -> String {
let time = time_float.floor() as i64;
let seconds = time % 60;
let minutes = (time / 60) % 60;
let hours = (time / 60) / 60;
format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
};
let Some(video) = &self.video_opt else {
//TODO: open button if no video?
return widget::container(widget::text("No video open"))
.width(Length::Fill)
.height(Length::Fill)
.style(theme::Container::WindowBackground)
.into();
};
let video_player = VideoPlayer::new(video)
.mouse_hidden(!self.controls)
.on_end_of_stream(Message::EndOfStream)
.on_missing_plugin(Message::MissingPlugin)
.on_new_frame(Message::NewFrame)
.width(Length::Fill)
.height(Length::Fill);
let mouse_area = widget::mouse_area(video_player).on_double_press(Message::Fullscreen);
let mut popover = widget::popover(mouse_area).position(widget::popover::Position::Bottom);
if self.controls {
popover = popover.popup(
widget::container(
widget::row::with_capacity(5)
.align_items(Alignment::Center)
.spacing(8)
.padding([0, 8])
.push(
widget::button::icon(
if self.video_opt.as_ref().map_or(true, |video| video.paused()) {
widget::icon::from_name("media-playback-start-symbolic")
.size(16)
} else {
widget::icon::from_name("media-playback-pause-symbolic")
.size(16)
},
)
.on_press(Message::PlayPause),
)
.push(widget::text(format_time(self.position)).font(font::mono()))
.push(
Slider::new(0.0..=self.duration, self.position, Message::Seek)
.step(0.1)
.on_release(Message::SeekRelease),
)
.push(
widget::text(format_time(self.duration - self.position))
.font(font::mono()),
)
.push(
widget::button::icon(
widget::icon::from_name("view-fullscreen-symbolic").size(16),
)
.on_press(Message::Fullscreen),
),
)
.style(theme::Container::WindowBackground),
);
}
widget::container(popover)
.width(Length::Fill)
.height(Length::Fill)
.style(theme::Container::Custom(Box::new(|_theme| {
widget::container::Appearance::default().with_background(Color::BLACK)
})))
.into()
}
fn subscription(&self) -> Subscription<Self::Message> {
struct ConfigSubscription;
struct ThemeSubscription;
Subscription::batch([
event::listen_with(|event, _status| match event {
Event::Keyboard(KeyEvent::KeyPressed { key, modifiers, .. }) => {
Some(Message::Key(modifiers, key))
}
Event::Mouse(MouseEvent::CursorMoved { .. }) => Some(Message::ShowControls),
_ => None,
}),
cosmic_config::config_subscription(
TypeId::of::<ConfigSubscription>(),
Self::APP_ID.into(),
CONFIG_VERSION,
)
.map(|update| {
if !update.errors.is_empty() {
log::debug!("errors loading config: {:?}", update.errors);
}
Message::SystemThemeModeChange(update.config)
}),
cosmic_config::config_subscription::<_, cosmic_theme::ThemeMode>(
TypeId::of::<ThemeSubscription>(),
cosmic_theme::THEME_MODE_ID.into(),
cosmic_theme::ThemeMode::version(),
)
.map(|update| {
if !update.errors.is_empty() {
log::debug!("errors loading theme mode: {:?}", update.errors);
}
Message::SystemThemeModeChange(update.config)
}),
])
}
}

View file

@ -5,7 +5,7 @@ use cosmic::{
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fmt};
use crate::app::Action;
use crate::Action;
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum Modifier {

View file

@ -1,18 +1,588 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::{
app::{message, Command, Core, Settings},
cosmic_config::{self, CosmicConfigEntry},
cosmic_theme, executor, font,
iced::{
event::{self, Event},
keyboard::{Event as KeyEvent, Key, Modifiers},
mouse::Event as MouseEvent,
subscription::Subscription,
window, Alignment, Color, Length, Limits,
},
theme,
widget::{self, Slider},
Application, ApplicationExt, Element,
};
use iced_video_player::{
gst::{self, prelude::*},
gst_pbutils, Video, VideoPlayer,
};
use std::{
any::TypeId,
collections::HashMap,
time::{Duration, Instant},
};
use crate::{
config::{Config, CONFIG_VERSION},
key_bind::{key_binds, KeyBind},
};
mod config;
mod key_bind;
mod localize;
#[cfg(feature = "ffmpeg")]
#[path = "ffmpeg/mod.rs"]
mod app;
static CONTROLS_TIMEOUT: Duration = Duration::new(2, 0);
#[cfg(feature = "gstreamer")]
#[path = "gstreamer/mod.rs"]
mod app;
const GST_PLAY_FLAG_VIDEO: i32 = 1 << 0;
const GST_PLAY_FLAG_AUDIO: i32 = 1 << 1;
const GST_PLAY_FLAG_TEXT: i32 = 1 << 2;
fn main() -> Result<(), Box<dyn std::error::Error>> {
app::main()
/// Runs application with these settings
#[rustfmt::skip]
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
localize::localize();
let (config_handler, config) = match cosmic_config::Config::new(App::APP_ID, CONFIG_VERSION) {
Ok(config_handler) => {
let config = match Config::get_entry(&config_handler) {
Ok(ok) => ok,
Err((errs, config)) => {
log::info!("errors loading config: {:?}", errs);
config
}
};
(Some(config_handler), config)
}
Err(err) => {
log::error!("failed to create config handler: {}", err);
(None, Config::default())
}
};
let mut settings = Settings::default();
settings = settings.theme(config.app_theme.theme());
settings = settings.size_limits(Limits::NONE.min_width(360.0).min_height(180.0));
let url = url::Url::from_file_path(
std::env::args().nth(1).unwrap()
)
.unwrap();
let flags = Flags {
config_handler,
config,
url,
};
cosmic::app::run::<App>(settings, flags)?;
Ok(())
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Action {
Fullscreen,
PlayPause,
SeekBackward,
SeekForward,
}
impl Action {
pub fn message(&self) -> Message {
match self {
Self::Fullscreen => Message::Fullscreen,
Self::PlayPause => Message::PlayPause,
Self::SeekBackward => Message::SeekRelative(-10.0),
Self::SeekForward => Message::SeekRelative(10.0),
}
}
}
#[derive(Clone)]
pub struct Flags {
config_handler: Option<cosmic_config::Config>,
config: Config,
url: url::Url,
}
/// Messages that are used specifically by our [`App`].
#[derive(Clone, Debug)]
pub enum Message {
Config(Config),
Fullscreen,
Key(Modifiers, Key),
AudioCode(usize),
TextCode(usize),
PlayPause,
Seek(f64),
SeekRelative(f64),
SeekRelease,
EndOfStream,
MissingPlugin(gst::Message),
NewFrame,
Reload,
ShowControls,
SystemThemeModeChange(cosmic_theme::ThemeMode),
}
/// The [`App`] stores application-specific state.
pub struct App {
core: Core,
flags: Flags,
controls: bool,
controls_time: Instant,
fullscreen: bool,
key_binds: HashMap<KeyBind, Action>,
video_opt: Option<Video>,
position: f64,
duration: f64,
dragging: bool,
audio_codes: Vec<String>,
current_audio: i32,
text_codes: Vec<String>,
current_text: i32,
}
impl App {
fn close(&mut self) {
self.video_opt = None;
self.position = 0.0;
self.duration = 0.0;
self.dragging = false;
self.audio_codes = Vec::new();
self.current_audio = -1;
self.text_codes = Vec::new();
self.current_text = -1;
}
fn load(&mut self) -> Command<Message> {
self.close();
let video = match Video::new(&self.flags.url) {
Ok(ok) => ok,
Err(err) => {
log::warn!("failed to open {:?}: {err}", self.flags.url);
return Command::none();
}
};
self.duration = video.duration().as_secs_f64();
let pipeline = video.pipeline();
self.video_opt = Some(video);
let n_audio = pipeline.property::<i32>("n-audio");
self.audio_codes = Vec::with_capacity(n_audio as usize);
for i in 0..n_audio {
let tags: gst::TagList = pipeline.emit_by_name("get-audio-tags", &[&i]);
log::info!("audio stream {i}: {tags:?}");
self.audio_codes.push(
if let Some(language_code) = tags.get::<gst::tags::LanguageCode>() {
language_code.get().to_string()
} else {
format!("Audio #{i}")
},
);
}
self.current_audio = pipeline.property::<i32>("current-audio");
let n_text = pipeline.property::<i32>("n-text");
self.text_codes = Vec::with_capacity(n_text as usize);
for i in 0..n_text {
let tags: gst::TagList = pipeline.emit_by_name("get-text-tags", &[&i]);
log::info!("text stream {i}: {tags:?}");
self.text_codes.push(
if let Some(language_code) = tags.get::<gst::tags::LanguageCode>() {
language_code.get().to_string()
} else {
format!("Subtitle #{i}")
},
);
}
self.current_text = pipeline.property::<i32>("current-text");
//TODO: Flags can be used to enable/disable subtitles
let flags_value = pipeline.property_value("flags");
println!("original flags {:?}", flags_value);
match flags_value.transform::<i32>() {
Ok(flags_transform) => match flags_transform.get::<i32>() {
Ok(mut flags) => {
flags |= GST_PLAY_FLAG_VIDEO | GST_PLAY_FLAG_AUDIO | GST_PLAY_FLAG_TEXT;
match gst::glib::Value::from(flags).transform_with_type(flags_value.type_()) {
Ok(value) => pipeline.set_property("flags", value),
Err(err) => {
log::warn!("failed to transform int to flags: {err}");
}
}
}
Err(err) => {
log::warn!("failed to get flags as int: {err}");
}
},
Err(err) => {
log::warn!("failed to transform flags to int: {err}");
}
}
println!("updated flags {:?}", pipeline.property_value("flags"));
self.update_title()
}
fn update_controls(&mut self, in_use: bool) {
if in_use {
self.controls = true;
self.controls_time = Instant::now();
} else if self.controls && self.controls_time.elapsed() > CONTROLS_TIMEOUT {
self.controls = false;
}
}
fn update_config(&mut self) -> Command<Message> {
cosmic::app::command::set_theme(self.flags.config.app_theme.theme())
}
fn update_title(&mut self) -> Command<Message> {
//TODO: filename?
let title = "COSMIC Media Player";
self.set_window_title(title.to_string())
}
}
/// Implement [`cosmic::Application`] to integrate with COSMIC.
impl Application for App {
/// Default async executor to use with the app.
type Executor = executor::Default;
/// Argument received [`cosmic::Application::new`].
type Flags = Flags;
/// Message type specific to our [`App`].
type Message = Message;
/// The unique application ID to supply to the window manager.
const APP_ID: &'static str = "com.system76.CosmicPlayer";
fn core(&self) -> &Core {
&self.core
}
fn core_mut(&mut self) -> &mut Core {
&mut self.core
}
/// Creates the application, and optionally emits command on initialize.
fn init(mut core: Core, flags: Self::Flags) -> (Self, Command<Self::Message>) {
core.window.content_container = false;
let mut app = App {
core,
flags,
controls: true,
controls_time: Instant::now(),
fullscreen: false,
key_binds: key_binds(),
video_opt: None,
position: 0.0,
duration: 0.0,
dragging: false,
audio_codes: Vec::new(),
current_audio: -1,
text_codes: Vec::new(),
current_text: -1,
};
let command = app.load();
(app, command)
}
fn on_escape(&mut self) -> Command<Self::Message> {
if self.fullscreen {
return self.update(Message::Fullscreen);
} else {
Command::none()
}
}
/// Handle application events here.
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
match message {
Message::Config(config) => {
if config != self.flags.config {
log::info!("update config");
self.flags.config = config;
return self.update_config();
}
}
Message::Fullscreen => {
self.fullscreen = !self.fullscreen;
self.core.window.show_headerbar = !self.fullscreen;
return window::change_mode(
window::Id::MAIN,
if self.fullscreen {
window::Mode::Fullscreen
} else {
window::Mode::Windowed
},
);
}
Message::Key(modifiers, key) => {
for (key_bind, action) in self.key_binds.iter() {
if key_bind.matches(modifiers, &key) {
return self.update(action.message());
}
}
}
Message::AudioCode(code) => {
if let Ok(code) = i32::try_from(code) {
if let Some(video) = &self.video_opt {
let pipeline = video.pipeline();
pipeline.set_property("current-audio", code);
self.current_audio = pipeline.property("current-audio");
}
}
}
Message::TextCode(code) => {
if let Ok(code) = i32::try_from(code) {
if let Some(video) = &self.video_opt {
let pipeline = video.pipeline();
pipeline.set_property("current-text", code);
self.current_text = pipeline.property("current-text");
}
}
}
Message::PlayPause => {
if let Some(video) = &mut self.video_opt {
video.set_paused(!video.paused());
self.update_controls(true);
}
}
Message::Seek(secs) => {
if let Some(video) = &mut self.video_opt {
self.dragging = true;
self.position = secs;
video.set_paused(true);
let duration = Duration::try_from_secs_f64(self.position).unwrap_or_default();
video.seek(duration, true).expect("seek");
self.update_controls(true);
}
}
Message::SeekRelative(secs) => {
if let Some(video) = &mut self.video_opt {
self.position = video.position().as_secs_f64();
let duration =
Duration::try_from_secs_f64(self.position + secs).unwrap_or_default();
video.seek(duration, true).expect("seek");
}
}
Message::SeekRelease => {
if let Some(video) = &mut self.video_opt {
self.dragging = false;
let duration = Duration::try_from_secs_f64(self.position).unwrap_or_default();
video.seek(duration, true).expect("seek");
video.set_paused(false);
self.update_controls(true);
}
}
Message::EndOfStream => {
println!("end of stream");
}
Message::MissingPlugin(element) => {
if let Some(video) = &mut self.video_opt {
video.set_paused(true);
}
return Command::perform(
async move {
tokio::task::spawn_blocking(move || {
match gst_pbutils::MissingPluginMessage::parse(&element) {
Ok(missing_plugin) => {
let mut install_ctx = gst_pbutils::InstallPluginsContext::new();
install_ctx
.set_desktop_id(&format!("{}.desktop", Self::APP_ID));
let install_detail = missing_plugin.installer_detail();
println!("installing plugins: {}", install_detail);
let status = gst_pbutils::missing_plugins::install_plugins_sync(
&[&install_detail],
Some(&install_ctx),
);
log::info!("plugin install status: {}", status);
log::info!(
"gstreamer registry update: {:?}",
gst::Registry::update()
);
}
Err(err) => {
log::warn!("failed to parse missing plugin message: {err}");
}
}
message::app(Message::Reload)
})
.await
.unwrap()
},
|x| x,
);
}
Message::NewFrame => {
if let Some(video) = &self.video_opt {
if !self.dragging {
self.position = video.position().as_secs_f64();
self.update_controls(false);
}
}
}
Message::Reload => {
return self.load();
}
Message::ShowControls => {
self.update_controls(true);
}
Message::SystemThemeModeChange(_theme_mode) => {
return self.update_config();
}
}
Command::none()
}
fn header_start(&self) -> Vec<Element<Self::Message>> {
let mut row = widget::row::with_capacity(4)
.align_items(Alignment::Center)
.spacing(8);
if !self.audio_codes.is_empty() {
//TODO: allow mute/unmute/change volume
row = row.push(widget::icon::from_name("audio-volume-high-symbolic").size(16));
row = row.push(widget::dropdown(
&self.audio_codes,
usize::try_from(self.current_audio).ok(),
Message::AudioCode,
));
}
if !self.text_codes.is_empty() {
//TODO: allow toggling subtitles
row = row.push(widget::icon::from_name("media-view-subtitles-symbolic").size(16));
row = row.push(widget::dropdown(
&self.text_codes,
usize::try_from(self.current_text).ok(),
Message::TextCode,
));
}
vec![row.into()]
}
/// Creates a view after each update.
fn view(&self) -> Element<Self::Message> {
let format_time = |time_float: f64| -> String {
let time = time_float.floor() as i64;
let seconds = time % 60;
let minutes = (time / 60) % 60;
let hours = (time / 60) / 60;
format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
};
let Some(video) = &self.video_opt else {
//TODO: open button if no video?
return widget::container(widget::text("No video open"))
.width(Length::Fill)
.height(Length::Fill)
.style(theme::Container::WindowBackground)
.into();
};
let video_player = VideoPlayer::new(video)
.mouse_hidden(!self.controls)
.on_end_of_stream(Message::EndOfStream)
.on_missing_plugin(Message::MissingPlugin)
.on_new_frame(Message::NewFrame)
.width(Length::Fill)
.height(Length::Fill);
let mouse_area = widget::mouse_area(video_player).on_double_press(Message::Fullscreen);
let mut popover = widget::popover(mouse_area).position(widget::popover::Position::Bottom);
if self.controls {
popover = popover.popup(
widget::container(
widget::row::with_capacity(5)
.align_items(Alignment::Center)
.spacing(8)
.padding([0, 8])
.push(
widget::button::icon(
if self.video_opt.as_ref().map_or(true, |video| video.paused()) {
widget::icon::from_name("media-playback-start-symbolic")
.size(16)
} else {
widget::icon::from_name("media-playback-pause-symbolic")
.size(16)
},
)
.on_press(Message::PlayPause),
)
.push(widget::text(format_time(self.position)).font(font::mono()))
.push(
Slider::new(0.0..=self.duration, self.position, Message::Seek)
.step(0.1)
.on_release(Message::SeekRelease),
)
.push(
widget::text(format_time(self.duration - self.position))
.font(font::mono()),
)
.push(
widget::button::icon(
widget::icon::from_name("view-fullscreen-symbolic").size(16),
)
.on_press(Message::Fullscreen),
),
)
.style(theme::Container::WindowBackground),
);
}
widget::container(popover)
.width(Length::Fill)
.height(Length::Fill)
.style(theme::Container::Custom(Box::new(|_theme| {
widget::container::Appearance::default().with_background(Color::BLACK)
})))
.into()
}
fn subscription(&self) -> Subscription<Self::Message> {
struct ConfigSubscription;
struct ThemeSubscription;
Subscription::batch([
event::listen_with(|event, _status| match event {
Event::Keyboard(KeyEvent::KeyPressed { key, modifiers, .. }) => {
Some(Message::Key(modifiers, key))
}
Event::Mouse(MouseEvent::CursorMoved { .. }) => Some(Message::ShowControls),
_ => None,
}),
cosmic_config::config_subscription(
TypeId::of::<ConfigSubscription>(),
Self::APP_ID.into(),
CONFIG_VERSION,
)
.map(|update| {
if !update.errors.is_empty() {
log::debug!("errors loading config: {:?}", update.errors);
}
Message::SystemThemeModeChange(update.config)
}),
cosmic_config::config_subscription::<_, cosmic_theme::ThemeMode>(
TypeId::of::<ThemeSubscription>(),
cosmic_theme::THEME_MODE_ID.into(),
cosmic_theme::ThemeMode::version(),
)
.map(|update| {
if !update.errors.is_empty() {
log::debug!("errors loading theme mode: {:?}", update.errors);
}
Message::SystemThemeModeChange(update.config)
}),
])
}
}