]>
Commit | Line | Data |
---|---|---|
69b93cb5 KS |
1 | extern crate sdl2; |
2 | extern crate nihav_core; | |
3 | extern crate nihav_registry; | |
4 | extern crate nihav_allstuff; | |
5 | ||
6 | use std::env; | |
7 | use std::fs::File; | |
8 | use std::io::Write; | |
9 | use std::path::Path; | |
10 | use std::time::{Duration, Instant}; | |
11 | use std::thread; | |
4e72c04a | 12 | use std::sync::atomic::{AtomicU8, Ordering}; |
69b93cb5 KS |
13 | |
14 | use sdl2::event::{Event, WindowEvent}; | |
8a51f59b | 15 | use sdl2::keyboard::{Keycode, Mod}; |
2ef3529c | 16 | use sdl2::mouse::{MouseButton, MouseWheelDirection}; |
69b93cb5 KS |
17 | use sdl2::render::{Canvas, Texture, TextureCreator}; |
18 | use sdl2::pixels::PixelFormatEnum; | |
19 | use sdl2::video::{Window, WindowContext}; | |
20 | ||
21 | use nihav_registry::detect; | |
22 | use nihav_core::frame::*; | |
23 | use nihav_core::io::byteio::{FileReader, ByteReader}; | |
24 | use nihav_core::reorder::*; | |
25 | use nihav_core::codecs::*; | |
26 | use nihav_core::demuxers::*; | |
27 | use nihav_registry::register::*; | |
28 | use nihav_allstuff::*; | |
29 | ||
35d0d63a KS |
30 | #[cfg(feature="hwaccel")] |
31 | use hwdec_vaapi::*; | |
32 | ||
69b93cb5 KS |
33 | mod audiodec; |
34 | use audiodec::*; | |
35 | mod videodec; | |
36 | use videodec::*; | |
8686f9f6 KS |
37 | mod osd; |
38 | use osd::*; | |
69b93cb5 | 39 | |
4e72c04a | 40 | #[repr(u8)] |
bbd69555 | 41 | #[derive(Clone,Copy,Debug,PartialEq,Default)] |
4e72c04a | 42 | enum DecodingState { |
bbd69555 | 43 | #[default] |
4e72c04a KS |
44 | Normal, |
45 | Waiting, | |
46 | Flush, | |
47 | Prefetch, | |
48 | Error, | |
49 | End, | |
50 | } | |
51 | ||
4e72c04a KS |
52 | impl From<u8> for DecodingState { |
53 | fn from(val: u8) -> Self { | |
54 | match val { | |
55 | 0 => DecodingState::Normal, | |
56 | 1 => DecodingState::Waiting, | |
57 | 2 => DecodingState::Flush, | |
58 | 3 => DecodingState::Prefetch, | |
59 | 4 => DecodingState::End, | |
60 | _ => DecodingState::Error, | |
61 | } | |
62 | } | |
63 | } | |
64 | ||
65 | struct DecoderState { | |
66 | state: AtomicU8 | |
67 | } | |
68 | ||
69 | impl DecoderState { | |
70 | const fn new() -> Self { | |
71 | Self { | |
72 | state: AtomicU8::new(DecodingState::Normal as u8) | |
73 | } | |
74 | } | |
75 | fn set_state(&self, state: DecodingState) { | |
76 | self.state.store(state as u8, Ordering::Release); | |
77 | } | |
78 | fn get_state(&self) -> DecodingState { | |
79 | self.state.load(Ordering::Acquire).into() | |
80 | } | |
81 | fn is_flushing(&self) -> bool { | |
82 | matches!(self.get_state(), DecodingState::Flush | DecodingState::Error) | |
83 | } | |
84 | } | |
85 | ||
69b93cb5 KS |
86 | #[cfg(feature="debug")] |
87 | macro_rules! debug_log { | |
88 | ($log: expr; $blk: block) => { | |
89 | $log.logfile.write($blk.as_bytes()).unwrap(); | |
90 | $log.logfile.write(b"\n").unwrap(); | |
91 | }; | |
92 | } | |
93 | #[cfg(not(feature="debug"))] | |
94 | macro_rules! debug_log { | |
95 | ($log: expr; $blk: block) => {}; | |
96 | } | |
97 | ||
549511e9 KS |
98 | enum ScaleSize { |
99 | Auto, | |
100 | Times(f32), | |
101 | Fixed(usize, usize) | |
102 | } | |
103 | ||
104 | impl FromStr for ScaleSize { | |
105 | type Err = (); | |
106 | fn from_str(s: &str) -> Result<Self, Self::Err> { | |
107 | if matches!(s, "" | "auto") { | |
108 | Ok(ScaleSize::Auto) | |
109 | } else if s.ends_with('x') || s.ends_with('X') { | |
110 | let factor = s[..s.len() - 1].parse::<f32>().map_err(|_| ())?; | |
111 | if factor > 0.0 { | |
112 | Ok(ScaleSize::Times(factor)) | |
113 | } else { | |
114 | Err(()) | |
115 | } | |
116 | } else if s.contains('x') | s.contains('X') { | |
117 | let mut dims = if s.contains('x') { s.split('x') } else { s.split('X') }; | |
118 | let w = dims.next().unwrap(); | |
119 | let h = dims.next().unwrap(); | |
120 | let width = w.parse::<usize>().map_err(|_| ())?; | |
121 | let height = h.parse::<usize>().map_err(|_| ())?; | |
122 | if width > 0 && height > 0 { | |
123 | Ok(ScaleSize::Fixed(width, height)) | |
124 | } else { | |
125 | Err(()) | |
126 | } | |
127 | } else { | |
128 | Err(()) | |
129 | } | |
130 | } | |
131 | } | |
132 | ||
69b93cb5 KS |
133 | pub enum PktSendEvent { |
134 | Packet(NAPacket), | |
37f130a7 | 135 | GetFrames, |
69b93cb5 KS |
136 | Flush, |
137 | End, | |
138 | ImmediateEnd, | |
139 | HurryUp, | |
140 | } | |
141 | ||
37f130a7 KS |
142 | pub enum DecoderType { |
143 | Audio(Box<dyn NADecoder + Send>), | |
144 | Video(Box<dyn NADecoder + Send>, Box<dyn FrameReorderer + Send>), | |
145 | VideoMT(Box<dyn NADecoderMT + Send>, MTFrameReorderer), | |
35d0d63a KS |
146 | #[cfg(feature="hwaccel")] |
147 | VideoHW(Box<dyn HWDecoder + Send>), | |
37f130a7 KS |
148 | } |
149 | ||
69b93cb5 KS |
150 | pub struct DecoderStuff { |
151 | pub dsupp: Box<NADecoderSupport>, | |
37f130a7 | 152 | pub dec: DecoderType, |
69b93cb5 KS |
153 | } |
154 | ||
155 | fn format_time(ms: u64) -> String { | |
156 | let s = ms / 1000; | |
157 | let ds = (ms % 1000) / 100; | |
158 | let (min, s) = (s / 60, s % 60); | |
159 | let (h, min) = (min / 60, min % 60); | |
160 | if h == 0 { | |
161 | if min == 0 { | |
162 | format!("{}.{}", s, ds) | |
163 | } else { | |
164 | format!("{}:{:02}.{}", min, s, ds) | |
165 | } | |
166 | } else { | |
167 | format!("{}:{:02}:{:02}.{}", h, min, s, ds) | |
168 | } | |
169 | } | |
170 | ||
171 | const FRAME_QUEUE_LEN: usize = 25; | |
172 | const MAX_VOLUME: usize = 200; | |
173 | ||
174 | pub type FrameRecord = (NABufferType, u64); | |
175 | ||
176 | pub struct TimeKeep { | |
177 | ref_time: Instant, | |
178 | ref_ts: u64, | |
179 | } | |
180 | ||
181 | impl TimeKeep { | |
182 | fn new() -> Self { | |
183 | Self { | |
184 | ref_time: Instant::now(), | |
185 | ref_ts: 0, | |
186 | } | |
187 | } | |
188 | pub fn get_cur_time(&self) -> u64 { | |
189 | let add = self.ref_time.elapsed().as_millis() as u64; | |
190 | self.ref_ts + add | |
191 | } | |
192 | fn reset_ts(&mut self) { | |
193 | self.ref_ts = 0; | |
194 | } | |
195 | fn reset_all(&mut self, ts: u64) { | |
196 | self.ref_time = Instant::now(); | |
197 | self.ref_ts = ts; | |
198 | } | |
199 | fn set_ts(&mut self) { | |
200 | self.ref_ts = self.get_cur_time(); | |
201 | } | |
202 | fn set_time(&mut self) { | |
203 | self.ref_time = Instant::now(); | |
204 | } | |
205 | } | |
206 | ||
207 | pub struct DispFrame<'a> { | |
208 | pub ts: u64, | |
209 | pub is_yuv: bool, | |
210 | pub valid: bool, | |
211 | pub rgb_tex: Texture<'a>, | |
212 | pub yuv_tex: Texture<'a>, | |
213 | } | |
214 | ||
215 | pub struct DispQueue<'a> { | |
216 | pub pool: Vec<DispFrame<'a>>, | |
217 | pub first_ts: u64, | |
218 | pub last_ts: u64, | |
219 | pub start: usize, | |
220 | pub end: usize, | |
221 | pub len: usize, | |
222 | pub width: usize, | |
223 | pub height: usize, | |
224 | } | |
225 | ||
226 | impl<'a> DispQueue<'a> { | |
227 | fn new(texture_creator: &'a TextureCreator<WindowContext>, width: usize, height: usize, len: usize) -> Self { | |
228 | let mut pool = Vec::with_capacity(len); | |
229 | for _ in 0..len + 1 { | |
749a451e KS |
230 | let rgb_tex = texture_creator.create_texture_streaming(PixelFormatEnum::RGB24, width as u32, height as u32).expect("failed to create RGB texture"); |
231 | let yuv_tex = texture_creator.create_texture_streaming(PixelFormatEnum::IYUV, ((width + 1) & !1) as u32, ((height + 1) & !1) as u32).expect("failed to create YUV texture"); | |
69b93cb5 KS |
232 | pool.push(DispFrame{ ts: 0, is_yuv: false, valid: false, rgb_tex, yuv_tex }); |
233 | } | |
234 | pool[len].is_yuv = false; | |
235 | pool[len].rgb_tex.with_lock(None, |buffer: &mut [u8], _pitch: usize| { | |
236 | for el in buffer.iter_mut() { *el = 0; } | |
749a451e | 237 | }).expect("RGB texture could not be locked"); |
69b93cb5 KS |
238 | |
239 | Self { pool, first_ts: 0, last_ts: 0, start: 0, end: 0, len, width, height } | |
240 | } | |
241 | ||
242 | fn flush(&mut self) { | |
243 | self.start = 0; | |
244 | self.end = 0; | |
245 | self.first_ts = 0; | |
246 | self.last_ts = 0; | |
247 | for frm in self.pool.iter_mut() { | |
248 | frm.valid = false; | |
249 | } | |
250 | } | |
251 | ||
8686f9f6 | 252 | fn get_last_texture(&mut self, osd: &OSD) -> &Texture<'a> { |
69b93cb5 | 253 | if self.pool[self.len].is_yuv { |
8686f9f6 KS |
254 | if osd.is_active() { |
255 | self.pool[self.len].yuv_tex.with_lock(None, |buffer: &mut [u8], pitch: usize| { | |
256 | osd.draw_yuv(buffer, pitch); | |
749a451e | 257 | }).expect("YUV texture locking failure"); |
8686f9f6 | 258 | } |
69b93cb5 KS |
259 | &self.pool[self.len].yuv_tex |
260 | } else { | |
8686f9f6 KS |
261 | if osd.is_active() { |
262 | self.pool[self.len].rgb_tex.with_lock(None, |buffer: &mut [u8], pitch: usize| { | |
263 | osd.draw_rgb(buffer, pitch); | |
749a451e | 264 | }).expect("RGB texture locking failure"); |
8686f9f6 | 265 | } |
69b93cb5 KS |
266 | &self.pool[self.len].rgb_tex |
267 | } | |
268 | } | |
269 | pub fn is_empty(&self) -> bool { self.start == self.end } | |
270 | pub fn is_full(&self) -> bool { self.len == 0 || self.start == (self.end + 1) % self.len } | |
271 | pub fn move_end(&mut self) { | |
272 | self.end += 1; | |
273 | if self.end >= self.len { | |
274 | self.end -= self.len; | |
275 | } | |
276 | } | |
277 | pub fn move_start(&mut self) { | |
278 | self.pool.swap(self.start, self.len); | |
279 | self.start += 1; | |
280 | if self.start >= self.len { | |
281 | self.start -= self.len; | |
282 | } | |
283 | if !self.is_empty() { | |
284 | self.first_ts = self.pool[self.start].ts; | |
285 | } | |
286 | } | |
287 | } | |
288 | ||
8686f9f6 | 289 | fn try_display(disp_queue: &mut DispQueue, canvas: &mut Canvas<Window>, osd: &mut OSD, ctime: &TimeKeep) -> Option<u64> { |
69b93cb5 KS |
290 | while !disp_queue.is_empty() { |
291 | let disp_time = disp_queue.first_ts; | |
292 | let ctime = ctime.get_cur_time(); | |
293 | if disp_time > ctime + 10 { | |
294 | return Some(disp_time - ctime); | |
295 | } else if disp_time + 10 < ctime { | |
296 | disp_queue.move_start(); | |
297 | } else { | |
8686f9f6 KS |
298 | if osd.is_active() { |
299 | osd.prepare(ctime); | |
300 | } | |
301 | let frm = &mut disp_queue.pool[disp_queue.start]; | |
302 | let texture = if frm.is_yuv { | |
303 | if osd.is_active() { | |
304 | frm.yuv_tex.with_lock(None, |buffer: &mut [u8], pitch: usize| { | |
305 | osd.draw_yuv(buffer, pitch); | |
749a451e | 306 | }).expect("YUV texture locking failure"); |
8686f9f6 KS |
307 | } |
308 | &frm.yuv_tex | |
309 | } else { | |
310 | if osd.is_active() { | |
311 | frm.rgb_tex.with_lock(None, |buffer: &mut [u8], pitch: usize| { | |
312 | osd.draw_rgb(buffer, pitch); | |
749a451e | 313 | }).expect("RGB texture locking failure"); |
8686f9f6 KS |
314 | } |
315 | &frm.rgb_tex | |
316 | }; | |
69b93cb5 | 317 | canvas.clear(); |
749a451e | 318 | canvas.copy(texture, None, None).expect("canvas blit failure"); |
69b93cb5 KS |
319 | canvas.present(); |
320 | ||
321 | disp_queue.move_start(); | |
322 | if !disp_queue.is_empty() { | |
323 | return Some((disp_queue.first_ts - ctime).saturating_sub(2)); | |
324 | } else { | |
325 | return None; | |
326 | } | |
327 | } | |
328 | } | |
329 | None | |
330 | } | |
331 | ||
332 | struct Player { | |
333 | sdl_context: sdl2::Sdl, | |
334 | vsystem: sdl2::VideoSubsystem, | |
335 | asystem: sdl2::AudioSubsystem, | |
336 | ||
337 | acontrol: AudioControl, | |
338 | vcontrol: VideoControl, | |
339 | ||
340 | play_video: bool, | |
341 | play_audio: bool, | |
342 | has_video: bool, | |
343 | has_audio: bool, | |
344 | video_str: u32, | |
345 | audio_str: u32, | |
549511e9 | 346 | sc_size: ScaleSize, |
69b93cb5 | 347 | |
37f130a7 KS |
348 | vthreads: usize, |
349 | use_mt: bool, | |
35d0d63a KS |
350 | #[cfg(feature="hwaccel")] |
351 | use_hwaccel: bool, | |
37f130a7 | 352 | |
69b93cb5 KS |
353 | paused: bool, |
354 | mute: bool, | |
355 | volume: usize, | |
356 | end: bool, | |
357 | ||
358 | tkeep: TimeKeep, | |
359 | ||
360 | debug: bool, | |
8686f9f6 | 361 | osd: OSD, |
69b93cb5 KS |
362 | |
363 | #[cfg(feature="debug")] | |
364 | logfile: File, | |
365 | } | |
366 | ||
367 | impl Player { | |
368 | fn new() -> Self { | |
749a451e KS |
369 | let sdl_context = sdl2::init().expect("SDL2 init failure"); |
370 | let vsystem = sdl_context.video().expect("video subsystem init failure"); | |
371 | let asystem = sdl_context.audio().expect("audio subsystem init failure"); | |
69b93cb5 | 372 | vsystem.disable_screen_saver(); |
e2ca0dbe | 373 | let acontrol = AudioControl::new(None, None, false, &asystem); |
69b93cb5 KS |
374 | let vcontrol = VideoControl::new(None, 0, 0, 0, 0); |
375 | Self { | |
376 | sdl_context, asystem, vsystem, | |
377 | ||
378 | acontrol, vcontrol, | |
379 | ||
380 | play_video: true, | |
381 | play_audio: true, | |
382 | has_video: false, | |
383 | has_audio: false, | |
384 | video_str: 0, | |
385 | audio_str: 0, | |
549511e9 | 386 | sc_size: ScaleSize::Auto, |
69b93cb5 | 387 | |
37f130a7 KS |
388 | vthreads: 3, |
389 | use_mt: true, | |
35d0d63a KS |
390 | #[cfg(feature="hwaccel")] |
391 | use_hwaccel: true, | |
37f130a7 | 392 | |
69b93cb5 KS |
393 | paused: false, |
394 | mute: false, | |
395 | volume: 100, | |
396 | end: false, | |
397 | ||
398 | tkeep: TimeKeep::new(), | |
399 | ||
400 | debug: false, | |
8686f9f6 | 401 | osd: OSD::new(), |
69b93cb5 KS |
402 | |
403 | #[cfg(feature="debug")] | |
749a451e | 404 | logfile: File::create("debug.log").expect("'debug.log' should be available for writing"), |
69b93cb5 KS |
405 | } |
406 | } | |
4e72c04a | 407 | fn seek(&mut self, off: u64, fwd: bool, dmx: &mut Demuxer, disp_queue: &mut DispQueue) -> Result<(), ()> { |
69b93cb5 KS |
408 | let cur_time = self.tkeep.get_cur_time(); |
409 | let seektime = if fwd { cur_time + off * 1000 } else { | |
410 | cur_time.saturating_sub(off * 1000) }; | |
411 | debug_log!(self; {format!(" seek to {}", seektime)}); | |
412 | ||
413 | let ret = dmx.seek(NATimePoint::Milliseconds(seektime)); | |
414 | if ret.is_err() { | |
415 | println!(" seek error"); | |
4e72c04a | 416 | return Ok(()); //TODO: not ignore some of seek errors? |
69b93cb5 KS |
417 | } |
418 | ||
419 | self.acontrol.flush(); | |
420 | self.vcontrol.flush(); | |
421 | disp_queue.flush(); | |
422 | ||
423 | self.tkeep.reset_ts(); | |
4e72c04a | 424 | self.prefill(dmx, disp_queue)?; |
69b93cb5 KS |
425 | if !disp_queue.is_empty() { |
426 | self.tkeep.reset_all(disp_queue.first_ts); | |
427 | } else { | |
428 | let mut iterations = 0; | |
429 | let mut time = self.acontrol.get_time(); | |
430 | while time.is_none() { | |
431 | iterations += 1; | |
432 | std::thread::yield_now(); | |
433 | if iterations > 1000000 { println!(" still no time set?!"); break; } | |
434 | time = self.acontrol.get_time(); | |
435 | } | |
436 | if let Some(time) = time { | |
437 | self.tkeep.reset_all(time); | |
438 | } | |
439 | } | |
440 | if !self.paused { | |
441 | self.acontrol.resume(); | |
442 | } | |
4e72c04a | 443 | Ok(()) |
69b93cb5 | 444 | } |
4e72c04a | 445 | fn prefill(&mut self, dmx: &mut Demuxer, disp_queue: &mut DispQueue) -> Result<(), ()> { |
69b93cb5 KS |
446 | debug_log!(self; {" prefilling"}); |
447 | while self.vcontrol.get_queue_size() < FRAME_QUEUE_LEN { | |
448 | let mut try_send = self.acontrol.get_queue_size() < FRAME_QUEUE_LEN && (!self.has_video || (!self.vcontrol.is_filled(FRAME_QUEUE_LEN) && !disp_queue.is_full())); | |
449 | ||
450 | if !self.vcontrol.try_send_queued() && self.vcontrol.get_queue_size() > FRAME_QUEUE_LEN / 2 { | |
451 | try_send = false; | |
452 | } | |
453 | if !self.acontrol.try_send_queued() && self.acontrol.get_queue_size() > FRAME_QUEUE_LEN / 2 { | |
454 | try_send = false; | |
455 | } | |
456 | if try_send { | |
457 | match dmx.get_frame() { | |
458 | Err(DemuxerError::EOF) => break, | |
459 | Err(_) => break, | |
460 | Ok(pkt) => { | |
461 | let streamno = pkt.get_stream().get_id(); | |
462 | if self.has_video && streamno == self.video_str { | |
463 | self.vcontrol.try_send_video(PktSendEvent::Packet(pkt)); | |
464 | } else if self.has_audio && streamno == self.audio_str { | |
465 | self.acontrol.try_send_audio(PktSendEvent::Packet(pkt)); | |
466 | } | |
467 | } | |
468 | }; | |
469 | } | |
470 | self.vcontrol.fill(disp_queue); | |
471 | ||
472 | if !try_send { | |
473 | break; | |
474 | } | |
475 | } | |
476 | if self.has_video { | |
477 | while self.vcontrol.get_queue_size() > 0 && !disp_queue.is_full() { | |
478 | self.vcontrol.try_send_queued(); | |
479 | self.vcontrol.fill(disp_queue); | |
480 | std::thread::sleep(Duration::from_millis(10)); | |
481 | } | |
4e72c04a | 482 | self.vcontrol.wait_for_frames()?; |
69b93cb5 KS |
483 | self.vcontrol.fill(disp_queue); |
484 | } | |
485 | debug_log!(self; {format!(" prefilling done, frames {}-{} audio {}", disp_queue.start, disp_queue.end, self.acontrol.get_fill())}); | |
4e72c04a | 486 | Ok(()) |
69b93cb5 | 487 | } |
ff58185a KS |
488 | fn toggle_pause(&mut self) { |
489 | self.paused = !self.paused; | |
490 | if self.paused { | |
491 | self.vsystem.enable_screen_saver(); | |
492 | self.tkeep.set_ts(); | |
493 | } else { | |
494 | self.vsystem.disable_screen_saver(); | |
495 | self.tkeep.set_time(); | |
496 | } | |
497 | if self.paused { | |
498 | self.acontrol.pause(); | |
499 | } else { | |
500 | self.acontrol.resume(); | |
501 | } | |
502 | } | |
4e72c04a | 503 | fn handle_events(&mut self, event_pump: &mut sdl2::EventPump, canvas: &mut Canvas<Window>, dmx: &mut Demuxer, disp_queue: &mut DispQueue) -> Result<bool, ()> { |
69b93cb5 KS |
504 | for event in event_pump.poll_iter() { |
505 | if let Event::Quit {..} = event { | |
506 | self.end = true; | |
507 | println!(); | |
4e72c04a | 508 | return Ok(true); |
69b93cb5 KS |
509 | } |
510 | if let Event::Window {win_event: WindowEvent::Exposed, ..} = event { | |
511 | canvas.clear(); | |
749a451e | 512 | canvas.copy(disp_queue.get_last_texture(&self.osd), None, None).expect("blitting failure"); |
69b93cb5 KS |
513 | canvas.present(); |
514 | } | |
86da7709 KS |
515 | if let Event::MouseButtonDown {mouse_btn, ..} = event { |
516 | match mouse_btn { | |
517 | MouseButton::Right => self.toggle_pause(), | |
518 | MouseButton::Middle => self.osd.toggle(), | |
519 | _ => {}, | |
520 | }; | |
ff58185a | 521 | } |
2ef3529c KS |
522 | if let Event::MouseWheel {direction: MouseWheelDirection::Normal, x: 0, y, ..} = event { |
523 | self.seek(10, y > 0, dmx, disp_queue)?; | |
524 | } | |
8a51f59b | 525 | if let Event::KeyDown {keycode: Some(keycode), keymod, ..} = event { |
69b93cb5 | 526 | match keycode { |
8a51f59b KS |
527 | Keycode::Escape => { |
528 | self.end = true; | |
529 | println!(); | |
530 | return Ok(true); | |
531 | }, | |
ab76e2d6 | 532 | Keycode::Q if keymod.contains(Mod::RSHIFTMOD) || keymod.contains(Mod::LSHIFTMOD) => { |
69b93cb5 KS |
533 | self.end = true; |
534 | println!(); | |
4e72c04a | 535 | return Ok(true); |
69b93cb5 | 536 | }, |
7133dd98 | 537 | Keycode::Return | Keycode::KpEnter => return Ok(true), |
e678289f | 538 | Keycode::R => { self.seek(0, true, dmx, disp_queue)?; }, |
6efb6f30 KS |
539 | Keycode::Right | Keycode::Kp6 => { self.seek(10, true, dmx, disp_queue)?; }, |
540 | Keycode::Left | Keycode::Kp4 => { self.seek(10, false, dmx, disp_queue)?; }, | |
541 | Keycode::Up | Keycode::Kp8 => { self.seek(60, true, dmx, disp_queue)?; }, | |
542 | Keycode::Down | Keycode::Kp2 => { self.seek(60, false, dmx, disp_queue)?; }, | |
543 | Keycode::PageUp | Keycode::Kp9 => { self.seek(600, true, dmx, disp_queue)?; }, | |
544 | Keycode::PageDown | Keycode::Kp3 => { self.seek(600, false, dmx, disp_queue)?; }, | |
ff58185a | 545 | Keycode::Space => { self.toggle_pause(); }, |
69b93cb5 KS |
546 | Keycode::Plus | Keycode::KpPlus => { |
547 | self.volume = (self.volume + 10).min(MAX_VOLUME); | |
548 | if !self.mute { | |
549 | self.acontrol.set_volume(self.volume); | |
550 | } | |
551 | }, | |
552 | Keycode::Minus | Keycode::KpMinus => { | |
553 | self.volume = self.volume.saturating_sub(10); | |
554 | if !self.mute { | |
555 | self.acontrol.set_volume(self.volume); | |
556 | } | |
557 | }, | |
558 | Keycode::D => { | |
559 | self.debug = !self.debug; | |
560 | }, | |
561 | Keycode::M => { | |
562 | self.mute = !self.mute; | |
563 | if self.mute { | |
564 | self.acontrol.set_volume(0); | |
565 | } else { | |
566 | self.acontrol.set_volume(self.volume); | |
567 | } | |
568 | }, | |
569 | Keycode::H => { | |
570 | self.vcontrol.try_send_video(PktSendEvent::HurryUp); | |
571 | }, | |
8686f9f6 | 572 | Keycode::O => { |
5a268da1 KS |
573 | if keymod.contains(Mod::RSHIFTMOD) || keymod.contains(Mod::LSHIFTMOD) { |
574 | self.osd.toggle_perm(); | |
575 | } else { | |
576 | self.osd.toggle(); | |
577 | } | |
8686f9f6 | 578 | }, |
69b93cb5 KS |
579 | _ => {}, |
580 | }; | |
581 | if !self.paused { | |
582 | print!("{:60}\r", ' '); | |
583 | std::io::stdout().flush().unwrap(); | |
584 | } | |
585 | } | |
586 | } | |
4e72c04a | 587 | Ok(false) |
69b93cb5 | 588 | } |
6cdca687 | 589 | fn play(&mut self, mut window: Window, name: &str, start_time: NATimePoint) -> Window { |
69b93cb5 KS |
590 | debug_log!(self; {format!("Playing {}", name)}); |
591 | ||
592 | // prepare data source | |
593 | let path = Path::new(name); | |
749a451e | 594 | let mut file = if let Ok(handle) = File::open(path) { |
0d3a25c7 KS |
595 | if let Ok(meta) = handle.metadata() { |
596 | if meta.is_dir() { | |
597 | return window; | |
598 | } | |
599 | } | |
749a451e KS |
600 | handle |
601 | } else { | |
602 | println!("failed to open {}", name); | |
6cdca687 | 603 | return window; |
749a451e | 604 | }; |
69b93cb5 KS |
605 | let mut fr = FileReader::new_read(&mut file); |
606 | let mut br = ByteReader::new(&mut fr); | |
607 | let res = detect::detect_format(name, &mut br); | |
608 | if res.is_none() { | |
609 | println!("cannot detect format for {}", name); | |
6cdca687 | 610 | return window; |
69b93cb5 KS |
611 | } |
612 | let (dmx_name, _score) = res.unwrap(); | |
613 | debug_log!(self; {format!(" found demuxer {} with score {:?}", dmx_name, _score)}); | |
614 | println!("trying demuxer {} on {}", dmx_name, name); | |
615 | ||
616 | let mut dmx_reg = RegisteredDemuxers::new(); | |
617 | nihav_register_all_demuxers(&mut dmx_reg); | |
618 | let mut dec_reg = RegisteredDecoders::new(); | |
619 | nihav_register_all_decoders(&mut dec_reg); | |
37f130a7 KS |
620 | let mut mtdec_reg = RegisteredMTDecoders::new(); |
621 | if self.use_mt { | |
622 | nihav_register_all_mt_decoders(&mut mtdec_reg); | |
623 | } | |
69b93cb5 KS |
624 | |
625 | let ret = dmx_reg.find_demuxer(dmx_name); | |
626 | if ret.is_none() { | |
627 | println!("error finding {} demuxer", dmx_name); | |
6cdca687 | 628 | return window; |
69b93cb5 | 629 | } |
bbd69555 | 630 | let dmx_fact = ret.unwrap(); |
749a451e | 631 | br.seek(SeekFrom::Start(0)).expect("should be able to seek to the start"); |
69b93cb5 KS |
632 | let ret = create_demuxer(dmx_fact, &mut br); |
633 | if ret.is_err() { | |
634 | println!("error creating demuxer"); | |
6cdca687 | 635 | return window; |
69b93cb5 KS |
636 | } |
637 | let mut dmx = ret.unwrap(); | |
638 | if start_time != NATimePoint::None { | |
639 | debug_log!(self; {format!(" start seek to {}", start_time)}); | |
640 | if dmx.seek(start_time).is_err() { | |
641 | println!("initial seek failed"); | |
642 | } | |
643 | } | |
644 | ||
645 | let mut width = 640; | |
646 | let mut height = 480; | |
647 | let mut tb_num = 0; | |
648 | let mut tb_den = 0; | |
649 | let mut ainfo: Option<NAAudioInfo> = None; | |
e2ca0dbe | 650 | let mut sbr_hack = false; |
69b93cb5 KS |
651 | |
652 | let mut video_dec: Option<DecoderStuff> = None; | |
653 | let mut audio_dec: Option<DecoderStuff> = None; | |
654 | ||
655 | let duration = dmx.get_duration(); | |
656 | if duration != 0 { | |
657 | println!(" total duration {}", format_time(duration)); | |
658 | } | |
659 | self.has_video = false; | |
660 | self.has_audio = false; | |
2425989e KS |
661 | self.osd.reset(); |
662 | self.osd.set_duration(duration); | |
69b93cb5 KS |
663 | for i in 0..dmx.get_num_streams() { |
664 | let s = dmx.get_stream(i).unwrap(); | |
665 | let info = s.get_info(); | |
666 | let decfunc = dec_reg.find_decoder(info.get_name()); | |
37f130a7 | 667 | let decfunc_mt = mtdec_reg.find_decoder(info.get_name()); |
69b93cb5 KS |
668 | println!("stream {} - {} {}", i, s, info.get_name()); |
669 | debug_log!(self; {format!(" stream {} - {} {}", i, s, info.get_name())}); | |
670 | let str_id = s.get_id(); | |
671 | if info.is_video() { | |
672 | if video_dec.is_none() && self.play_video { | |
35d0d63a KS |
673 | #[cfg(feature="hwaccel")] |
674 | if info.get_name() == "h264" && self.use_hwaccel { | |
675 | let mut dec = new_h264_hwdec(); | |
676 | let dsupp = Box::new(NADecoderSupport::new()); | |
677 | let props = info.get_properties().get_video_info().unwrap(); | |
678 | if props.get_width() != 0 { | |
679 | width = props.get_width(); | |
680 | height = props.get_height(); | |
681 | } | |
682 | if dec.init(info.clone()).is_err() { | |
683 | println!("failed to initialise hwaccel video decoder"); | |
684 | } else { | |
685 | video_dec = Some(DecoderStuff{ dsupp, dec: DecoderType::VideoHW(dec) }); | |
686 | self.video_str = str_id; | |
687 | let (tbn, tbd) = s.get_timebase(); | |
688 | tb_num = tbn; | |
689 | tb_den = tbd; | |
690 | self.has_video = true; | |
691 | println!(" using hardware-accelerated decoding"); | |
692 | continue; | |
693 | } | |
694 | } | |
37f130a7 KS |
695 | if let Some(decfunc) = decfunc_mt { |
696 | let mut dec = (decfunc)(); | |
697 | let mut dsupp = Box::new(NADecoderSupport::new()); | |
698 | let props = info.get_properties().get_video_info().unwrap(); | |
699 | if props.get_width() != 0 { | |
700 | width = props.get_width(); | |
701 | height = props.get_height(); | |
702 | } | |
703 | if dec.init(&mut dsupp, info.clone(), self.vthreads).is_ok() { | |
704 | video_dec = Some(DecoderStuff{ dsupp, dec: DecoderType::VideoMT(dec, MTFrameReorderer::new()) }); | |
705 | self.video_str = str_id; | |
706 | let (tbn, tbd) = s.get_timebase(); | |
707 | tb_num = tbn; | |
708 | tb_den = tbd; | |
709 | self.has_video = true; | |
710 | continue; | |
711 | } else { | |
712 | println!("failed to create multi-threaded decoder, falling back"); | |
713 | } | |
714 | } | |
69b93cb5 KS |
715 | if let Some(decfunc) = decfunc { |
716 | let mut dec = (decfunc)(); | |
717 | let mut dsupp = Box::new(NADecoderSupport::new()); | |
718 | let props = info.get_properties().get_video_info().unwrap(); | |
719 | if props.get_width() != 0 { | |
720 | width = props.get_width(); | |
721 | height = props.get_height(); | |
722 | } | |
723 | let desc = get_codec_description(info.get_name()); | |
724 | let (reorder_depth, reord) = if desc.is_none() || (desc.unwrap().caps & CODEC_CAP_COMPLEX_REORDER) == 0 { | |
725 | let reord: Box<dyn FrameReorderer + Send> = Box::new(IPBReorderer::new()); | |
726 | (3, reord) | |
727 | } else { | |
728 | let reord: Box<dyn FrameReorderer + Send> = Box::new(ComplexReorderer::new()); | |
729 | (16, reord) | |
730 | }; | |
731 | dsupp.pool_u8 = NAVideoBufferPool::new(reorder_depth); | |
732 | dsupp.pool_u16 = NAVideoBufferPool::new(reorder_depth); | |
733 | dsupp.pool_u32 = NAVideoBufferPool::new(reorder_depth); | |
749a451e KS |
734 | if dec.init(&mut dsupp, info).is_err() { |
735 | println!("failed to initialise video decoder"); | |
6cdca687 | 736 | return window; |
749a451e | 737 | } |
37f130a7 | 738 | video_dec = Some(DecoderStuff{ dsupp, dec: DecoderType::Video(dec, reord) }); |
69b93cb5 KS |
739 | self.video_str = str_id; |
740 | let (tbn, tbd) = s.get_timebase(); | |
741 | tb_num = tbn; | |
742 | tb_den = tbd; | |
743 | self.has_video = true; | |
744 | } else { | |
745 | println!("no video decoder for {} found!", info.get_name()); | |
746 | } | |
747 | } | |
748 | } else if info.is_audio() { | |
749 | if audio_dec.is_none() && self.play_audio { | |
750 | if let Some(decfunc) = decfunc { | |
751 | let mut dec = (decfunc)(); | |
752 | let mut dsupp = Box::new(NADecoderSupport::new()); | |
753 | ainfo = info.get_properties().get_audio_info(); | |
e2ca0dbe KS |
754 | if let (true, Some(ref ai)) = (info.get_name() == "aac", ainfo) { |
755 | if ai.sample_rate < 32000 { | |
756 | sbr_hack = true; | |
757 | } | |
758 | } | |
749a451e KS |
759 | if dec.init(&mut dsupp, info).is_err() { |
760 | println!("failed to initialise audio decoder"); | |
6cdca687 | 761 | return window; |
749a451e | 762 | } |
37f130a7 | 763 | audio_dec = Some(DecoderStuff{ dsupp, dec: DecoderType::Audio(dec) }); |
69b93cb5 KS |
764 | self.audio_str = str_id; |
765 | self.has_audio = true; | |
766 | } else { | |
767 | println!("no audio decoder for {} found!", info.get_name()); | |
768 | } | |
769 | } | |
770 | } else { | |
771 | println!("decoder {} not found", info.get_name()); | |
772 | } | |
773 | } | |
774 | if !self.has_video && !self.has_audio { | |
775 | println!("No playable streams found."); | |
6cdca687 | 776 | return window; |
69b93cb5 KS |
777 | } |
778 | ||
549511e9 KS |
779 | match self.sc_size { |
780 | ScaleSize::Auto => { | |
781 | while (width <= 384) && (height <= 288) { | |
782 | width <<= 1; | |
783 | height <<= 1; | |
784 | } | |
785 | }, | |
786 | ScaleSize::Times(factor) => { | |
787 | let nw = ((width as f32) * factor).ceil() as usize; | |
788 | let nh = ((height as f32) * factor).ceil() as usize; | |
789 | if nw > 0 && nh > 0 { | |
790 | width = nw; | |
791 | height = nh; | |
792 | } | |
793 | }, | |
794 | ScaleSize::Fixed(w, h) => { | |
795 | width = w; | |
796 | height = h; | |
797 | }, | |
798 | }; | |
69b93cb5 KS |
799 | |
800 | // prepare playback structure | |
801 | let mut new_vcontrol = VideoControl::new(video_dec, width, height, tb_num, tb_den); | |
802 | std::mem::swap(&mut self.vcontrol, &mut new_vcontrol); | |
803 | ||
e2ca0dbe | 804 | let mut new_acontrol = AudioControl::new(audio_dec, ainfo, sbr_hack, &self.asystem); |
69b93cb5 KS |
805 | std::mem::swap(&mut self.acontrol, &mut new_acontrol); |
806 | ||
807 | if self.mute { | |
808 | self.acontrol.set_volume(0); | |
809 | } else { | |
810 | self.acontrol.set_volume(self.volume); | |
811 | } | |
812 | ||
813 | let fname = path.file_name(); | |
814 | let wname = if let Some(fname) = fname { | |
75da2ee6 KS |
815 | // workaround for libSDL2 workaround for non-UTF8 windowing systems |
816 | // see https://github.com/libsdl-org/SDL/pull/4290 for detais | |
c66ce565 | 817 | let nname = fname.to_str().expect("should be able to set window title").replace('\u{2013}', "-").replace('\u{2014}', "-"); |
75da2ee6 | 818 | "NihAV player - ".to_owned() + &nname |
69b93cb5 KS |
819 | } else { |
820 | "NihAV player".to_owned() | |
821 | }; | |
6cdca687 | 822 | window.set_title(&wname).expect("set window title"); |
034bfb62 KS |
823 | if window.size() != (width as u32, height as u32) { |
824 | window.set_size(width as u32, height as u32).expect("resize window"); | |
825 | } | |
6cdca687 | 826 | window.show(); |
749a451e | 827 | let mut canvas = window.into_canvas().build().expect("should be able to build canvas"); |
69b93cb5 KS |
828 | let texture_creator = canvas.texture_creator(); |
829 | let mut disp_q = DispQueue::new(&texture_creator, width, height, if self.has_video { FRAME_QUEUE_LEN } else { 0 }); | |
830 | if !self.has_video { | |
831 | canvas.clear(); | |
749a451e | 832 | canvas.copy(disp_q.get_last_texture(&self.osd), None, None).expect("blit failure"); |
69b93cb5 KS |
833 | canvas.present(); |
834 | } | |
835 | ||
836 | self.has_audio = self.acontrol.has_audio(); | |
837 | if !self.has_video && !self.has_audio { | |
838 | println!("No playable streams."); | |
6cdca687 | 839 | return canvas.into_window(); |
69b93cb5 KS |
840 | } |
841 | ||
842 | // play | |
4e72c04a KS |
843 | if self.prefill(&mut dmx, &mut disp_q).is_err() { |
844 | std::mem::swap(&mut self.vcontrol, &mut new_vcontrol); | |
845 | new_vcontrol.finish(); | |
846 | std::mem::swap(&mut self.acontrol, &mut new_acontrol); | |
847 | new_acontrol.finish(); | |
6cdca687 | 848 | return canvas.into_window(); |
4e72c04a | 849 | } |
0b064d41 | 850 | self.tkeep.reset_all(if !disp_q.is_empty() { disp_q.first_ts } else { 0 }); |
69b93cb5 KS |
851 | if !self.paused { |
852 | self.acontrol.resume(); | |
853 | } | |
749a451e | 854 | let mut event_pump = self.sdl_context.event_pump().expect("should be able to create event pump"); |
69b93cb5 KS |
855 | let mut last_disp = Instant::now(); |
856 | let mut has_data = true; | |
857 | 'main: loop { | |
4e72c04a KS |
858 | let ret = self.handle_events(&mut event_pump, &mut canvas, &mut dmx, &mut disp_q); |
859 | if matches!(ret, Ok(true) | Err(_)) { | |
69b93cb5 KS |
860 | println!(); |
861 | break 'main; | |
862 | } | |
863 | if !self.paused { | |
864 | let mut try_send = self.acontrol.get_queue_size() < FRAME_QUEUE_LEN && self.vcontrol.get_queue_size() < FRAME_QUEUE_LEN; | |
865 | if !self.vcontrol.try_send_queued() && self.vcontrol.is_filled(FRAME_QUEUE_LEN) { | |
866 | try_send = false; | |
867 | } | |
868 | if !self.acontrol.try_send_queued() { | |
869 | try_send = false; | |
870 | } | |
871 | while has_data && try_send { | |
872 | match dmx.get_frame() { | |
873 | Err(DemuxerError::EOF) => { | |
874 | self.vcontrol.try_send_video(PktSendEvent::End); | |
875 | self.acontrol.try_send_audio(PktSendEvent::End); | |
876 | has_data = false; | |
877 | }, | |
6a5b7102 KS |
878 | Err(err) => { |
879 | println!("demuxer error {:?}", err); | |
880 | if err == DemuxerError::IOError { | |
881 | self.vcontrol.try_send_video(PktSendEvent::End); | |
882 | self.acontrol.try_send_audio(PktSendEvent::End); | |
883 | has_data = false; | |
884 | } | |
885 | }, | |
69b93cb5 KS |
886 | Ok(pkt) => { |
887 | let streamno = pkt.get_stream().get_id(); | |
888 | if self.has_video && streamno == self.video_str { | |
889 | debug_log!(self; {" sending video packet"}); | |
890 | self.vcontrol.try_send_video(PktSendEvent::Packet(pkt)); | |
891 | if self.vcontrol.is_filled(FRAME_QUEUE_LEN) { | |
892 | try_send = false; | |
893 | } | |
894 | } else if self.has_audio && streamno == self.audio_str { | |
895 | debug_log!(self; {" sending audio packet"}); | |
896 | self.acontrol.try_send_audio(PktSendEvent::Packet(pkt)); | |
897 | if self.acontrol.get_queue_size() >= FRAME_QUEUE_LEN { | |
898 | try_send = false; | |
899 | } | |
900 | } | |
901 | } | |
902 | }; | |
903 | } | |
904 | self.vcontrol.fill(&mut disp_q); | |
905 | let mut sleep_time = 25; | |
906 | debug_log!(self; {format!(" time {}", self.tkeep.get_cur_time())}); | |
907 | if self.has_video { | |
908 | debug_log!(self; {format!(" disp queue {}-{}, {}-{} vqueue fill {}", disp_q.first_ts, disp_q.last_ts, disp_q.start, disp_q.end, self.vcontrol.get_queue_size())}); | |
8686f9f6 | 909 | let ret = try_display(&mut disp_q, &mut canvas, &mut self.osd, &self.tkeep); |
69b93cb5 KS |
910 | if let Some(next_time) = ret { |
911 | sleep_time = sleep_time.min(next_time); | |
912 | } | |
913 | } | |
914 | if self.has_audio { | |
915 | let time_left = self.acontrol.get_time_left(); | |
916 | debug_log!(self; {format!(" audio left {}", time_left)}); | |
917 | sleep_time = sleep_time.min(time_left); | |
918 | } | |
919 | debug_log!(self; {format!(" sleep {}ms", sleep_time)}); | |
920 | if last_disp.elapsed().as_millis() >= 10 { | |
921 | let c_time = self.tkeep.get_cur_time(); | |
922 | ||
923 | if !self.debug { | |
924 | print!(" {} {}% \r", format_time(c_time), self.acontrol.get_volume()); | |
925 | } else { | |
926 | print!(" {} {} {}% {:3} {:6}\r", format_time(c_time), if self.vcontrol.is_yuv() { 'Y' } else { 'R' }, self.acontrol.get_volume(), (disp_q.end + disp_q.len - disp_q.start) % disp_q.len, self.acontrol.get_fill()); | |
927 | } | |
928 | std::io::stdout().flush().unwrap(); | |
929 | last_disp = Instant::now(); | |
930 | } | |
931 | let mut end = true; | |
932 | if self.has_video && !self.vcontrol.is_video_end() { | |
933 | end = false; | |
934 | } | |
935 | if self.has_audio && !self.acontrol.is_audio_end() { | |
936 | end = false; | |
937 | } | |
938 | if end { | |
939 | break; | |
940 | } | |
941 | thread::sleep(Duration::from_millis(sleep_time)); | |
942 | } else { | |
943 | thread::sleep(Duration::from_millis(20)); | |
944 | } | |
945 | } | |
946 | println!(); | |
947 | std::mem::swap(&mut self.vcontrol, &mut new_vcontrol); | |
948 | new_vcontrol.finish(); | |
949 | std::mem::swap(&mut self.acontrol, &mut new_acontrol); | |
950 | new_acontrol.finish(); | |
6cdca687 | 951 | canvas.into_window() |
69b93cb5 KS |
952 | } |
953 | } | |
954 | ||
955 | fn main() { | |
956 | let args: Vec<String> = env::args().collect(); | |
957 | ||
958 | if args.len() == 1 { | |
959 | println!("usage: nihav-player file1 file2 ..."); | |
960 | return; | |
961 | } | |
962 | ||
963 | let mut player = Player::new(); | |
6cdca687 KS |
964 | let mut builder = player.vsystem.window("NihAV Player", 640, 480); |
965 | let mut window = builder.position_centered().hidden().build().expect("should be able to centre window"); | |
69b93cb5 KS |
966 | |
967 | let mut aiter = args.iter().skip(1); | |
968 | let mut seek_time = NATimePoint::None; | |
969 | while let Some(arg) = aiter.next() { | |
970 | match arg.as_str() { | |
971 | "-an" => { player.play_audio = false; }, | |
972 | "-ae" => { player.play_audio = true; }, | |
973 | "-vn" => { player.play_video = false; }, | |
974 | "-ve" => { player.play_video = true; }, | |
b452b665 | 975 | "-seek" | "-start" => { |
69b93cb5 KS |
976 | if let Some(arg) = aiter.next() { |
977 | if let Ok(time) = arg.parse::<NATimePoint>() { | |
978 | seek_time = time; | |
979 | } else { | |
980 | println!("wrong seek time"); | |
981 | seek_time = NATimePoint::None; | |
982 | } | |
983 | } | |
984 | }, | |
985 | "-vol" => { | |
986 | if let Some(arg) = aiter.next() { | |
987 | if let Ok(vol) = arg.parse::<usize>() { | |
988 | player.volume = vol.min(MAX_VOLUME); | |
989 | } else { | |
990 | println!("wrong volume"); | |
991 | } | |
992 | } | |
993 | }, | |
994 | "-debug" => { | |
995 | player.debug = true; | |
996 | }, | |
997 | "-nodebug" => { | |
998 | player.debug = false; | |
999 | }, | |
37f130a7 KS |
1000 | "-mt" => { |
1001 | player.use_mt = true; | |
1002 | }, | |
1003 | "-nomt" => { | |
1004 | player.use_mt = false; | |
1005 | }, | |
35d0d63a KS |
1006 | #[cfg(feature="hwaccel")] |
1007 | "-hwaccel" => { | |
1008 | player.use_hwaccel = true; | |
1009 | }, | |
1010 | #[cfg(feature="hwaccel")] | |
1011 | "-nohwaccel" => { | |
1012 | player.use_hwaccel = false; | |
1013 | }, | |
37f130a7 KS |
1014 | "-threads" => { |
1015 | if let Some(arg) = aiter.next() { | |
1016 | if let Ok(val) = arg.parse::<usize>() { | |
1017 | player.vthreads = val.max(1); | |
1018 | } else { | |
1019 | println!("wrong number of threads"); | |
1020 | } | |
1021 | } | |
1022 | }, | |
549511e9 KS |
1023 | "-scale" => { |
1024 | if let Some(arg) = aiter.next() { | |
1025 | if let Ok(ssize) = arg.parse::<ScaleSize>() { | |
1026 | player.sc_size = ssize; | |
1027 | } else { | |
1028 | println!("invalid scale size"); | |
1029 | } | |
1030 | } | |
1031 | }, | |
69b93cb5 | 1032 | _ => { |
6cdca687 | 1033 | window = player.play(window, arg, seek_time); |
69b93cb5 KS |
1034 | if player.end { break; } |
1035 | seek_time = NATimePoint::None; | |
1036 | }, | |
1037 | }; | |
1038 | } | |
1039 | } |