#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
mod player {
use std::sync::Arc;
use mpv_engine::{Engine, ExportOptions, PlaybackEvent};
use winit::application::ApplicationHandler;
use winit::dpi::PhysicalSize;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy};
use winit::window::{Window, WindowId};
struct Gfx {
window: Arc<Window>,
surface: wgpu::Surface<'static>,
device: wgpu::Device,
queue: wgpu::Queue,
config: wgpu::SurfaceConfiguration,
last: Option<wgpu::Texture>,
}
struct App {
engine: Engine,
proxy: EventLoopProxy<()>,
path: String,
gfx: Option<Gfx>,
}
fn gfx_for_window(window: Arc<Window>) -> Gfx {
let mut descriptor =
wgpu::InstanceDescriptor::new_with_display_handle(Box::new(window.clone()));
descriptor.backends = mpv_engine::WGPU_BACKEND;
let instance = wgpu::Instance::new(descriptor);
let surface = instance
.create_surface(window.clone())
.expect("create surface");
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
compatible_surface: Some(&surface),
..Default::default()
}))
.expect("no wgpu adapter for the native backend");
let device_descriptor = wgpu::DeviceDescriptor {
required_features: mpv_engine::REQUIRED_WGPU_FEATURES,
..Default::default()
};
let (device, queue) =
pollster::block_on(adapter.request_device(&device_descriptor)).expect("wgpu device");
let caps = surface.get_capabilities(&adapter);
assert!(
caps.formats.contains(&wgpu::TextureFormat::Bgra8Unorm),
"surface does not offer Bgra8Unorm (got {:?})",
caps.formats
);
let size = window.inner_size();
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST,
format: wgpu::TextureFormat::Bgra8Unorm,
width: size.width.max(1),
height: size.height.max(1),
present_mode: wgpu::PresentMode::AutoVsync,
alpha_mode: wgpu::CompositeAlphaMode::Auto,
color_space: wgpu::SurfaceColorSpace::Auto,
view_formats: vec![],
desired_maximum_frame_latency: 2,
};
surface.configure(&device, &config);
Gfx {
window,
surface,
device,
queue,
config,
last: None,
}
}
impl App {
fn resize(&mut self, size: PhysicalSize<u32>) {
let Some(gfx) = &mut self.gfx else { return };
gfx.config.width = size.width.max(1);
gfx.config.height = size.height.max(1);
gfx.surface.configure(&gfx.device, &gfx.config);
self.engine
.set_export_size(size.width, size.height)
.expect("set_export_size");
}
fn redraw(&mut self) {
let engine = &self.engine;
let Some(gfx) = self.gfx.as_mut() else { return };
match engine.acquire_frame() {
Ok(Some(frame)) => match frame.into_wgpu_texture(&gfx.device) {
Ok(texture) => gfx.last = Some(texture),
Err(e) => eprintln!("frame import failed: {e}"),
},
Ok(None) => {} Err(_) => return,
}
let Some(texture) = gfx.last.as_ref() else {
return;
};
use wgpu::CurrentSurfaceTexture as Cst;
let target = match gfx.surface.get_current_texture() {
Cst::Success(target) | Cst::Suboptimal(target) => target,
Cst::Timeout | Cst::Occluded => return,
Cst::Outdated | Cst::Lost | Cst::Validation => {
gfx.surface.configure(&gfx.device, &gfx.config);
gfx.window.request_redraw();
return;
}
};
let extent = wgpu::Extent3d {
width: texture.width().min(target.texture.width()),
height: texture.height().min(target.texture.height()),
depth_or_array_layers: 1,
};
let mut encoder = gfx.device.create_command_encoder(&Default::default());
encoder.copy_texture_to_texture(
texture.as_image_copy(),
target.texture.as_image_copy(),
extent,
);
gfx.queue.submit([encoder.finish()]);
gfx.window.pre_present_notify();
gfx.queue.present(target);
}
}
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.gfx.is_some() {
return;
}
let window = Arc::new(
event_loop
.create_window(Window::default_attributes().with_title("mpv-engine"))
.expect("create window"),
);
let gfx = gfx_for_window(window);
let size = gfx.window.inner_size();
let on_update = {
let proxy = self.proxy.clone();
move || {
let _ = proxy.send_event(());
}
};
self.engine
.attach_exported_render(
ExportOptions::new(size.width.max(1), size.height.max(1)),
on_update,
)
.expect("attach exported render");
let proxy = self.proxy.clone();
self.engine.set_wakeup_callback(move || {
let _ = proxy.send_event(());
});
self.engine.load_when_ready(&self.path).expect("load");
self.gfx = Some(gfx);
}
fn user_event(&mut self, event_loop: &ActiveEventLoop, (): ()) {
for event in self.engine.pump_events() {
match event {
PlaybackEvent::Ended { .. } | PlaybackEvent::Shutdown => event_loop.exit(),
PlaybackEvent::Failed { message, .. } => {
eprintln!("playback failed: {message}");
event_loop.exit();
}
_ => {}
}
}
if let Some(gfx) = &self.gfx {
gfx.window.request_redraw();
}
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
_window_id: WindowId,
event: WindowEvent,
) {
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::Resized(size) => self.resize(size),
WindowEvent::RedrawRequested => self.redraw(),
_ => {}
}
}
}
pub fn run() {
let Some(path) = std::env::args().nth(1) else {
eprintln!("usage: winit_player <video file>");
std::process::exit(2);
};
let engine = Engine::video()
.property("keep-open", "no")
.build()
.expect("mpv engine");
let event_loop = EventLoop::with_user_event().build().expect("event loop");
let proxy = event_loop.create_proxy();
let mut app = App {
engine,
proxy,
path,
gfx: None,
};
event_loop.run_app(&mut app).expect("event loop run");
app.engine.detach_render();
}
}
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
fn main() {
player::run();
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
fn main() {
eprintln!("the exported-frame backend needs macOS, Linux or Windows");
}