Skip to main content

video_player_wgpu/
main.rs

1use std::sync::Arc;
2use std::time::Instant;
3use std::{collections::VecDeque, env};
4
5use anyhow::{Context, Result};
6use bytemuck::{Pod, Zeroable};
7use wgpu::util::DeviceExt;
8use winit::{
9    dpi::PhysicalSize,
10    event::{Event, WindowEvent},
11    event_loop::EventLoop,
12    window::WindowBuilder,
13};
14
15use video_sys::VideoStream;
16
17#[repr(C)]
18#[derive(Clone, Copy, Debug, Pod, Zeroable)]
19struct Vertex {
20    pos: [f32; 2],
21    uv: [f32; 2],
22}
23
24static VERTEX_ATTRS: [wgpu::VertexAttribute; 2] =
25    wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2];
26
27impl Vertex {
28    fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
29        wgpu::VertexBufferLayout {
30            array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
31            step_mode: wgpu::VertexStepMode::Vertex,
32            attributes: &VERTEX_ATTRS,
33        }
34    }
35}
36
37struct State {
38    surface: wgpu::Surface<'static>,
39    device: wgpu::Device,
40    queue: wgpu::Queue,
41    config: wgpu::SurfaceConfiguration,
42    size: PhysicalSize<u32>,
43
44    pipeline: wgpu::RenderPipeline,
45    vbuf: wgpu::Buffer,
46    ibuf: wgpu::Buffer,
47    icount: u32,
48
49    texture: wgpu::Texture,
50    texture_view: wgpu::TextureView,
51    sampler: wgpu::Sampler,
52    bind_group: wgpu::BindGroup,
53
54    video: VideoStream,
55
56    stash: std::collections::VecDeque<video_sys::VideoFrame>,
57    started_at: Option<std::time::Instant>,
58    base_pts_us: Option<i64>,
59    last_presented_pts: i64,
60
61    render_fps_t0: Instant,
62    render_fps_n: u32,
63
64    video_fps_t0: Instant,
65    video_fps_n: u32,
66    video_delta_sum_us: i64,
67    video_delta_n: u32,
68}
69
70impl State {
71    async fn new(window: Arc<winit::window::Window>, path: &str) -> Result<Self> {
72        let size = window.inner_size();
73
74        let instance = wgpu::Instance::default();
75        let surface = instance
76            .create_surface(window.clone())
77            .context("create_surface")?;
78
79        let adapter = instance
80            .request_adapter(&wgpu::RequestAdapterOptions {
81                power_preference: wgpu::PowerPreference::HighPerformance,
82                compatible_surface: Some(&surface),
83                force_fallback_adapter: false,
84            })
85            .await
86            .context("request_adapter")?;
87
88        let (device, queue) = adapter
89            .request_device(
90                &wgpu::DeviceDescriptor {
91                    label: None,
92                    required_features: wgpu::Features::empty(),
93                    required_limits: wgpu::Limits::default(),
94                },
95                None,
96            )
97            .await
98            .context("request_device")?;
99
100        let caps = surface.get_capabilities(&adapter);
101        let format = caps.formats[0];
102
103        let config = wgpu::SurfaceConfiguration {
104            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
105            format,
106            width: size.width.max(1),
107            height: size.height.max(1),
108            present_mode: wgpu::PresentMode::Fifo,
109            alpha_mode: caps.alpha_modes[0],
110            view_formats: vec![],
111            desired_maximum_frame_latency: 2,
112        };
113        surface.configure(&device, &config);
114
115        // Load video.
116        let video = VideoStream::open(path).context("VideoStream::open")?;
117        let tex_w = video.width();
118        let tex_h = video.height();
119
120        // Texture for RGBA frames.
121        let texture = device.create_texture(&wgpu::TextureDescriptor {
122            label: Some("video_texture"),
123            size: wgpu::Extent3d {
124                width: tex_w,
125                height: tex_h,
126                depth_or_array_layers: 1,
127            },
128            mip_level_count: 1,
129            sample_count: 1,
130            dimension: wgpu::TextureDimension::D2,
131            format: wgpu::TextureFormat::Rgba8UnormSrgb,
132            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
133            view_formats: &[],
134        });
135        let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
136        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
137            mag_filter: wgpu::FilterMode::Linear,
138            min_filter: wgpu::FilterMode::Linear,
139            ..Default::default()
140        });
141
142        // Shaders/pipeline.
143        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
144            label: Some("shader"),
145            source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
146        });
147
148        let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
149            label: Some("bgl"),
150            entries: &[
151                wgpu::BindGroupLayoutEntry {
152                    binding: 0,
153                    visibility: wgpu::ShaderStages::FRAGMENT,
154                    ty: wgpu::BindingType::Texture {
155                        multisampled: false,
156                        view_dimension: wgpu::TextureViewDimension::D2,
157                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
158                    },
159                    count: None,
160                },
161                wgpu::BindGroupLayoutEntry {
162                    binding: 1,
163                    visibility: wgpu::ShaderStages::FRAGMENT,
164                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
165                    count: None,
166                },
167            ],
168        });
169
170        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
171            label: Some("bg"),
172            layout: &bind_group_layout,
173            entries: &[
174                wgpu::BindGroupEntry {
175                    binding: 0,
176                    resource: wgpu::BindingResource::TextureView(&texture_view),
177                },
178                wgpu::BindGroupEntry {
179                    binding: 1,
180                    resource: wgpu::BindingResource::Sampler(&sampler),
181                },
182            ],
183        });
184
185        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
186            label: Some("pl"),
187            bind_group_layouts: &[&bind_group_layout],
188            push_constant_ranges: &[],
189        });
190
191        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
192            label: Some("pipeline"),
193            layout: Some(&pipeline_layout),
194            vertex: wgpu::VertexState {
195                module: &shader,
196                entry_point: "vs_main",
197                buffers: &[Vertex::desc()],
198            },
199            fragment: Some(wgpu::FragmentState {
200                module: &shader,
201                entry_point: "fs_main",
202                targets: &[Some(wgpu::ColorTargetState {
203                    format: config.format,
204                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
205                    write_mask: wgpu::ColorWrites::ALL,
206                })],
207            }),
208            primitive: wgpu::PrimitiveState {
209                topology: wgpu::PrimitiveTopology::TriangleList,
210                ..Default::default()
211            },
212            depth_stencil: None,
213            multisample: wgpu::MultisampleState::default(),
214            multiview: None,
215        });
216
217        let vertices = [
218            Vertex {
219                pos: [-1.0, -1.0],
220                uv: [0.0, 1.0],
221            },
222            Vertex {
223                pos: [1.0, -1.0],
224                uv: [1.0, 1.0],
225            },
226            Vertex {
227                pos: [1.0, 1.0],
228                uv: [1.0, 0.0],
229            },
230            Vertex {
231                pos: [-1.0, 1.0],
232                uv: [0.0, 0.0],
233            },
234        ];
235        let indices: [u16; 6] = [0, 1, 2, 0, 2, 3];
236
237        let vbuf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
238            label: Some("vbuf"),
239            contents: bytemuck::cast_slice(&vertices),
240            usage: wgpu::BufferUsages::VERTEX,
241        });
242        let ibuf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
243            label: Some("ibuf"),
244            contents: bytemuck::cast_slice(&indices),
245            usage: wgpu::BufferUsages::INDEX,
246        });
247
248        Ok(Self {
249            surface,
250            device,
251            queue,
252            config,
253            size,
254            pipeline,
255            vbuf,
256            ibuf,
257            icount: indices.len() as u32,
258            texture,
259            texture_view,
260            sampler,
261            bind_group,
262            video,
263            stash: VecDeque::new(),
264            started_at: None,
265            base_pts_us: None,
266            last_presented_pts: -1,
267            render_fps_t0: Instant::now(),
268            render_fps_n: 0,
269            video_fps_t0: Instant::now(),
270            video_fps_n: 0,
271            video_delta_sum_us: 0,
272            video_delta_n: 0,
273        })
274    }
275
276    fn resize(&mut self, new_size: PhysicalSize<u32>) {
277        if new_size.width == 0 || new_size.height == 0 {
278            return;
279        }
280        self.size = new_size;
281        self.config.width = new_size.width;
282        self.config.height = new_size.height;
283        self.surface.configure(&self.device, &self.config);
284    }
285
286    fn update(&mut self) -> Result<()> {
287        // 1) Drain decoded frames from VideoStream into local stash.
288        while let Some(f) = self.video.try_recv_one() {
289            self.stash.push_back(f);
290        }
291
292        // 2) Initialize timing on first frame.
293        if self.started_at.is_none() {
294            if let Some(front) = self.stash.front() {
295                self.started_at = Some(std::time::Instant::now());
296                self.base_pts_us = Some(front.pts_us);
297            } else {
298                return Ok(());
299            }
300        }
301
302        let started_at = self.started_at.unwrap();
303        let elapsed_us = started_at.elapsed().as_micros() as i64;
304        let base = self.base_pts_us.unwrap();
305        let target_pts_us = base + elapsed_us;
306
307        // 3) Pop all frames that are due; keep latest.
308        let mut latest_due = None;
309        while let Some(front) = self.stash.front() {
310            if front.pts_us <= target_pts_us {
311                latest_due = self.stash.pop_front();
312            } else {
313                break;
314            }
315        }
316
317        if let Some(frame) = latest_due {
318            if frame.pts_us != self.last_presented_pts {
319                let dt = frame.pts_us - self.last_presented_pts;
320                log::info!("present dt={}us", dt);
321                self.last_presented_pts = frame.pts_us;
322                self.upload_frame(&frame.data, frame.width, frame.height);
323            }
324        }
325
326        Ok(())
327    }
328
329    fn upload_frame(&mut self, rgba: &[u8], width: u32, height: u32) {
330        // wgpu requires bytes_per_row to be a multiple of 256.
331        let bytes_per_pixel = 4usize;
332        let row_bytes = width as usize * bytes_per_pixel;
333        let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
334        let padded_row_bytes = (row_bytes + align - 1) / align * align;
335
336        if padded_row_bytes == row_bytes {
337            self.queue.write_texture(
338                wgpu::ImageCopyTexture {
339                    texture: &self.texture,
340                    mip_level: 0,
341                    origin: wgpu::Origin3d::ZERO,
342                    aspect: wgpu::TextureAspect::All,
343                },
344                rgba,
345                wgpu::ImageDataLayout {
346                    offset: 0,
347                    bytes_per_row: Some(row_bytes as u32),
348                    rows_per_image: Some(height),
349                },
350                wgpu::Extent3d {
351                    width,
352                    height,
353                    depth_or_array_layers: 1,
354                },
355            );
356            return;
357        }
358
359        let mut padded = vec![0u8; padded_row_bytes * height as usize];
360        for y in 0..height as usize {
361            let src = &rgba[y * row_bytes..(y + 1) * row_bytes];
362            let dst = &mut padded[y * padded_row_bytes..y * padded_row_bytes + row_bytes];
363            dst.copy_from_slice(src);
364        }
365
366        self.queue.write_texture(
367            wgpu::ImageCopyTexture {
368                texture: &self.texture,
369                mip_level: 0,
370                origin: wgpu::Origin3d::ZERO,
371                aspect: wgpu::TextureAspect::All,
372            },
373            &padded,
374            wgpu::ImageDataLayout {
375                offset: 0,
376                bytes_per_row: Some(padded_row_bytes as u32),
377                rows_per_image: Some(height),
378            },
379            wgpu::Extent3d {
380                width,
381                height,
382                depth_or_array_layers: 1,
383            },
384        );
385    }
386
387    fn render(&mut self) -> Result<()> {
388        let output = self
389            .surface
390            .get_current_texture()
391            .context("get_current_texture")?;
392        let view = output
393            .texture
394            .create_view(&wgpu::TextureViewDescriptor::default());
395
396        let mut encoder = self
397            .device
398            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
399                label: Some("encoder"),
400            });
401
402        {
403            let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
404                label: Some("render_pass"),
405                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
406                    view: &view,
407                    resolve_target: None,
408                    ops: wgpu::Operations {
409                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
410                        store: wgpu::StoreOp::Store,
411                    },
412                })],
413                depth_stencil_attachment: None,
414                timestamp_writes: None,
415                occlusion_query_set: None,
416            });
417
418            rpass.set_pipeline(&self.pipeline);
419            rpass.set_bind_group(0, &self.bind_group, &[]);
420            rpass.set_vertex_buffer(0, self.vbuf.slice(..));
421            rpass.set_index_buffer(self.ibuf.slice(..), wgpu::IndexFormat::Uint16);
422            rpass.draw_indexed(0..self.icount, 0, 0..1);
423        }
424
425        self.queue.submit(Some(encoder.finish()));
426        output.present();
427        Ok(())
428    }
429}
430
431fn main() -> Result<()> {
432    env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));
433
434    let mut args = env::args().skip(1);
435    let path = args.next().context("Usage: video-player-wgpu <file.mp4>")?;
436
437    let event_loop = EventLoop::new()?;
438    let window = Arc::new(
439        WindowBuilder::new()
440            .with_title("video-player-wgpu")
441            .build(&event_loop)?,
442    );
443
444    let mut state = pollster::block_on(State::new(window.clone(), &path))?;
445
446    event_loop.run(move |event, elwt| {
447        elwt.set_control_flow(winit::event_loop::ControlFlow::Poll);
448
449        match event {
450            Event::WindowEvent { event, .. } => match event {
451                WindowEvent::CloseRequested => elwt.exit(),
452                WindowEvent::Resized(s) => state.resize(s),
453                // WindowEvent::ScaleFactorChanged { new_inner_size, .. } => state.resize(*new_inner_size),
454                WindowEvent::RedrawRequested => {
455                    if let Err(e) = state.update().and_then(|_| state.render()) {
456                        log::error!("render error: {e:?}");
457                    }
458                }
459                _ => {}
460            },
461            Event::AboutToWait => {
462                window.request_redraw();
463            }
464            _ => {}
465        }
466    })?;
467
468    Ok(())
469}