]> git.nihav.org Git - nihav-player.git/blobdiff - videoplayer/src/main.rs
videoplayer: introduce refresh command
[nihav-player.git] / videoplayer / src / main.rs
index 25ddcabe245f7141ac0dced820b9f9a99e808984..0ee3c1472033a2000b0f7dcf7d2630dfecb56f9b 100644 (file)
@@ -9,9 +9,11 @@ use std::io::Write;
 use std::path::Path;
 use std::time::{Duration, Instant};
 use std::thread;
+use std::sync::atomic::{AtomicU8, Ordering};
 
 use sdl2::event::{Event, WindowEvent};
-use sdl2::keyboard::Keycode;
+use sdl2::keyboard::{Keycode, Mod};
+use sdl2::mouse::{MouseButton, MouseWheelDirection};
 use sdl2::render::{Canvas, Texture, TextureCreator};
 use sdl2::pixels::PixelFormatEnum;
 use sdl2::video::{Window, WindowContext};
@@ -25,10 +27,61 @@ use nihav_core::demuxers::*;
 use nihav_registry::register::*;
 use nihav_allstuff::*;
 
+#[cfg(feature="hwaccel")]
+use hwdec_vaapi::*;
+
 mod audiodec;
 use audiodec::*;
 mod videodec;
 use videodec::*;
+mod osd;
+use osd::*;
+
+#[repr(u8)]
+#[derive(Clone,Copy,Debug,PartialEq,Default)]
+enum DecodingState {
+    #[default]
+    Normal,
+    Waiting,
+    Flush,
+    Prefetch,
+    Error,
+    End,
+}
+
+impl From<u8> for DecodingState {
+    fn from(val: u8) -> Self {
+        match val {
+            0 => DecodingState::Normal,
+            1 => DecodingState::Waiting,
+            2 => DecodingState::Flush,
+            3 => DecodingState::Prefetch,
+            4 => DecodingState::End,
+            _ => DecodingState::Error,
+        }
+    }
+}
+
+struct DecoderState {
+    state:  AtomicU8
+}
+
+impl DecoderState {
+    const fn new() -> Self {
+        Self {
+            state:  AtomicU8::new(DecodingState::Normal as u8)
+        }
+    }
+    fn set_state(&self, state: DecodingState) {
+        self.state.store(state as u8, Ordering::Release);
+    }
+    fn get_state(&self) -> DecodingState {
+        self.state.load(Ordering::Acquire).into()
+    }
+    fn is_flushing(&self) -> bool {
+        matches!(self.get_state(), DecodingState::Flush | DecodingState::Error)
+    }
+}
 
 #[cfg(feature="debug")]
 macro_rules! debug_log {
@@ -42,18 +95,61 @@ macro_rules! debug_log {
     ($log: expr; $blk: block) => {};
 }
 
+enum ScaleSize {
+    Auto,
+    Times(f32),
+    Fixed(usize, usize)
+}
+
+impl FromStr for ScaleSize {
+    type Err = ();
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if matches!(s, "" | "auto") {
+            Ok(ScaleSize::Auto)
+        } else if s.ends_with('x') || s.ends_with('X') {
+            let factor = s[..s.len() - 1].parse::<f32>().map_err(|_| ())?;
+            if factor > 0.0 {
+                Ok(ScaleSize::Times(factor))
+            } else {
+                Err(())
+            }
+        } else if s.contains('x') | s.contains('X') {
+            let mut dims = if s.contains('x') { s.split('x') } else { s.split('X') };
+            let w = dims.next().unwrap();
+            let h = dims.next().unwrap();
+            let width  = w.parse::<usize>().map_err(|_| ())?;
+            let height = h.parse::<usize>().map_err(|_| ())?;
+            if width > 0 && height > 0 {
+                Ok(ScaleSize::Fixed(width, height))
+            } else {
+                Err(())
+            }
+        } else {
+            Err(())
+        }
+    }
+}
+
 pub enum PktSendEvent {
     Packet(NAPacket),
+    GetFrames,
     Flush,
     End,
     ImmediateEnd,
     HurryUp,
 }
 
+pub enum DecoderType {
+    Audio(Box<dyn NADecoder + Send>),
+    Video(Box<dyn NADecoder + Send>, Box<dyn FrameReorderer + Send>),
+    VideoMT(Box<dyn NADecoderMT + Send>, MTFrameReorderer),
+    #[cfg(feature="hwaccel")]
+    VideoHW(Box<dyn HWDecoder + Send>),
+}
+
 pub struct DecoderStuff {
     pub dsupp:  Box<NADecoderSupport>,
-    pub dec:    Box<dyn NADecoder + Send>,
-    pub reord:  Box<dyn FrameReorderer + Send>,
+    pub dec:    DecoderType,
 }
 
 fn format_time(ms: u64) -> String {
@@ -131,14 +227,14 @@ impl<'a> DispQueue<'a> {
     fn new(texture_creator: &'a TextureCreator<WindowContext>, width: usize, height: usize, len: usize) -> Self {
         let mut pool = Vec::with_capacity(len);
         for _ in 0..len + 1 {
-            let rgb_tex = texture_creator.create_texture_streaming(PixelFormatEnum::RGB24, width as u32, height as u32).unwrap();
-            let yuv_tex = texture_creator.create_texture_streaming(PixelFormatEnum::IYUV, ((width + 1) & !1) as u32, ((height + 1) & !1) as u32).unwrap();
+            let rgb_tex = texture_creator.create_texture_streaming(PixelFormatEnum::RGB24, width as u32, height as u32).expect("failed to create RGB texture");
+            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");
             pool.push(DispFrame{ ts: 0, is_yuv: false, valid: false, rgb_tex, yuv_tex });
         }
         pool[len].is_yuv = false;
         pool[len].rgb_tex.with_lock(None, |buffer: &mut [u8], _pitch: usize| {
                 for el in buffer.iter_mut() { *el = 0; }
-            }).unwrap();
+            }).expect("RGB texture could not be locked");
 
         Self { pool, first_ts: 0, last_ts: 0, start: 0, end: 0, len, width, height }
     }
@@ -153,10 +249,20 @@ impl<'a> DispQueue<'a> {
         }
     }
 
-    fn get_last_texture(&self) -> &Texture<'a> {
+    fn get_last_texture(&mut self, osd: &OSD) -> &Texture<'a> {
         if self.pool[self.len].is_yuv {
+            if osd.is_active() {
+                self.pool[self.len].yuv_tex.with_lock(None, |buffer: &mut [u8], pitch: usize| {
+                        osd.draw_yuv(buffer, pitch);
+                    }).expect("YUV texture locking failure");
+            }
             &self.pool[self.len].yuv_tex
         } else {
+            if osd.is_active() {
+                self.pool[self.len].rgb_tex.with_lock(None, |buffer: &mut [u8], pitch: usize| {
+                        osd.draw_rgb(buffer, pitch);
+                    }).expect("RGB texture locking failure");
+            }
             &self.pool[self.len].rgb_tex
         }
     }
@@ -180,7 +286,7 @@ impl<'a> DispQueue<'a> {
     }
 }
 
-fn try_display(disp_queue: &mut DispQueue, canvas: &mut Canvas<Window>, ctime: &TimeKeep) -> Option<u64> {
+fn try_display(disp_queue: &mut DispQueue, canvas: &mut Canvas<Window>, osd: &mut OSD, ctime: &TimeKeep) -> Option<u64> {
     while !disp_queue.is_empty() {
         let disp_time = disp_queue.first_ts;
         let ctime = ctime.get_cur_time();
@@ -189,10 +295,27 @@ fn try_display(disp_queue: &mut DispQueue, canvas: &mut Canvas<Window>, ctime: &
         } else if disp_time + 10 < ctime {
             disp_queue.move_start();
         } else {
-            let frm = &disp_queue.pool[disp_queue.start];
-            let texture = if frm.is_yuv { &frm.yuv_tex } else { &frm.rgb_tex };
+            if osd.is_active() {
+                osd.prepare(ctime);
+            }
+            let frm = &mut disp_queue.pool[disp_queue.start];
+            let texture = if frm.is_yuv {
+                    if osd.is_active() {
+                        frm.yuv_tex.with_lock(None, |buffer: &mut [u8], pitch: usize| {
+                                osd.draw_yuv(buffer, pitch);
+                            }).expect("YUV texture locking failure");
+                    }
+                    &frm.yuv_tex
+                } else {
+                    if osd.is_active() {
+                        frm.rgb_tex.with_lock(None, |buffer: &mut [u8], pitch: usize| {
+                                osd.draw_rgb(buffer, pitch);
+                            }).expect("RGB texture locking failure");
+                    }
+                    &frm.rgb_tex
+                };
             canvas.clear();
-            canvas.copy(texture, None, None).unwrap();
+            canvas.copy(texture, None, None).expect("canvas blit failure");
             canvas.present();
 
             disp_queue.move_start();
@@ -220,6 +343,12 @@ struct Player {
     has_audio:      bool,
     video_str:      u32,
     audio_str:      u32,
+    sc_size:        ScaleSize,
+
+    vthreads:       usize,
+    use_mt:         bool,
+    #[cfg(feature="hwaccel")]
+    use_hwaccel:    bool,
 
     paused:         bool,
     mute:           bool,
@@ -229,6 +358,7 @@ struct Player {
     tkeep:          TimeKeep,
 
     debug:          bool,
+    osd:            OSD,
 
     #[cfg(feature="debug")]
     logfile:        File,
@@ -236,11 +366,11 @@ struct Player {
 
 impl Player {
     fn new() -> Self {
-        let sdl_context = sdl2::init().unwrap();
-        let vsystem = sdl_context.video().unwrap();
-        let asystem = sdl_context.audio().unwrap();
+        let sdl_context = sdl2::init().expect("SDL2 init failure");
+        let vsystem = sdl_context.video().expect("video subsystem init failure");
+        let asystem = sdl_context.audio().expect("audio subsystem init failure");
         vsystem.disable_screen_saver();
-        let acontrol = AudioControl::new(None, None, &asystem);
+        let acontrol = AudioControl::new(None, None, false, &asystem);
         let vcontrol = VideoControl::new(None, 0, 0, 0, 0);
         Self {
             sdl_context, asystem, vsystem,
@@ -253,6 +383,12 @@ impl Player {
             has_audio:      false,
             video_str:      0,
             audio_str:      0,
+            sc_size:        ScaleSize::Auto,
+
+            vthreads:       3,
+            use_mt:         true,
+            #[cfg(feature="hwaccel")]
+            use_hwaccel:    true,
 
             paused:         false,
             mute:           false,
@@ -262,12 +398,13 @@ impl Player {
             tkeep:          TimeKeep::new(),
 
             debug:          false,
+            osd:            OSD::new(),
 
             #[cfg(feature="debug")]
-            logfile:        File::create("debug.log").unwrap(),
+            logfile:        File::create("debug.log").expect("'debug.log' should be available for writing"),
         }
     }
-    fn seek(&mut self, off: u64, fwd: bool, dmx: &mut Demuxer, disp_queue: &mut DispQueue) {
+    fn seek(&mut self, off: u64, fwd: bool, dmx: &mut Demuxer, disp_queue: &mut DispQueue) -> Result<(), ()> {
         let cur_time = self.tkeep.get_cur_time();
         let seektime = if fwd { cur_time + off * 1000 } else {
                 cur_time.saturating_sub(off * 1000) };
@@ -276,7 +413,7 @@ impl Player {
         let ret = dmx.seek(NATimePoint::Milliseconds(seektime));
         if ret.is_err() {
             println!(" seek error");
-            return;
+            return Ok(()); //TODO: not ignore some of seek errors?
         }
 
         self.acontrol.flush();
@@ -284,7 +421,7 @@ impl Player {
         disp_queue.flush();
 
         self.tkeep.reset_ts();
-        self.prefill(dmx, disp_queue);
+        self.prefill(dmx, disp_queue)?;
         if !disp_queue.is_empty() {
             self.tkeep.reset_all(disp_queue.first_ts);
         } else {
@@ -303,8 +440,9 @@ impl Player {
         if !self.paused {
             self.acontrol.resume();
         }
+        Ok(())
     }
-    fn prefill(&mut self, dmx: &mut Demuxer, disp_queue: &mut DispQueue) {
+    fn prefill(&mut self, dmx: &mut Demuxer, disp_queue: &mut DispQueue) -> Result<(), ()> {
         debug_log!(self; {" prefilling"});
         while self.vcontrol.get_queue_size() < FRAME_QUEUE_LEN {
             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()));
@@ -341,51 +479,70 @@ impl Player {
                 self.vcontrol.fill(disp_queue);
                 std::thread::sleep(Duration::from_millis(10));
             }
+            self.vcontrol.wait_for_frames()?;
             self.vcontrol.fill(disp_queue);
         }
         debug_log!(self; {format!(" prefilling done, frames {}-{} audio {}", disp_queue.start, disp_queue.end, self.acontrol.get_fill())});
+        Ok(())
+    }
+    fn toggle_pause(&mut self) {
+        self.paused = !self.paused;
+        if self.paused {
+            self.vsystem.enable_screen_saver();
+            self.tkeep.set_ts();
+        } else {
+            self.vsystem.disable_screen_saver();
+            self.tkeep.set_time();
+        }
+        if self.paused {
+            self.acontrol.pause();
+        } else {
+            self.acontrol.resume();
+        }
     }
-    fn handle_events(&mut self, event_pump: &mut sdl2::EventPump, canvas: &mut Canvas<Window>, dmx: &mut Demuxer, disp_queue: &mut DispQueue) -> bool {
+    fn handle_events(&mut self, event_pump: &mut sdl2::EventPump, canvas: &mut Canvas<Window>, dmx: &mut Demuxer, disp_queue: &mut DispQueue) -> Result<bool, ()> {
         for event in event_pump.poll_iter() {
             if let Event::Quit {..} = event {
                 self.end = true;
                 println!();
-                return true;
+                return Ok(true);
             }
             if let Event::Window {win_event: WindowEvent::Exposed, ..} = event {
                 canvas.clear();
-                canvas.copy(disp_queue.get_last_texture(), None, None).unwrap();
+                canvas.copy(disp_queue.get_last_texture(&self.osd), None, None).expect("blitting failure");
                 canvas.present();
             }
-            if let Event::KeyDown {keycode: Some(keycode), ..} = event {
+            if let Event::MouseButtonDown {mouse_btn, ..} = event {
+                match mouse_btn {
+                    MouseButton::Right => self.toggle_pause(),
+                    MouseButton::Middle => self.osd.toggle(),
+                    _ => {},
+                };
+            }
+            if let Event::MouseWheel {direction: MouseWheelDirection::Normal, x: 0, y, ..} = event {
+                self.seek(10, y > 0, dmx, disp_queue)?;
+            }
+            if let Event::KeyDown {keycode: Some(keycode), keymod, ..} = event {
                 match keycode {
-                    Keycode::Escape | Keycode::Q => {
+                    Keycode::Escape => {
                         self.end = true;
                         println!();
-                        return true;
+                        return Ok(true);
                     },
-                    Keycode::Return => return true,
-                    Keycode::Right      => { self.seek(10, true,  dmx, disp_queue); },
-                    Keycode::Left       => { self.seek(10, false, dmx, disp_queue); },
-                    Keycode::Up         => { self.seek(60, true,  dmx, disp_queue); },
-                    Keycode::Down       => { self.seek(60, false, dmx, disp_queue); },
-                    Keycode::PageUp     => { self.seek(600, true,  dmx, disp_queue); },
-                    Keycode::PageDown   => { self.seek(600, false, dmx, disp_queue); },
-                    Keycode::Space => {
-                        self.paused = !self.paused;
-                        if self.paused {
-                            self.vsystem.enable_screen_saver();
-                            self.tkeep.set_ts();
-                        } else {
-                            self.vsystem.disable_screen_saver();
-                            self.tkeep.set_time();
-                        }
-                        if self.paused {
-                            self.acontrol.pause();
-                        } else {
-                            self.acontrol.resume();
-                        }
+                    Keycode::Q if keymod.contains(Mod::RSHIFTMOD) || keymod.contains(Mod::LSHIFTMOD) => {
+                        self.end = true;
+                        println!();
+                        return Ok(true);
                     },
+                    Keycode::Return | Keycode::KpEnter => return Ok(true),
+                    Keycode::R          => { self.seek(0, true,  dmx, disp_queue)?; },
+                    Keycode::Right      => { self.seek(10, true,  dmx, disp_queue)?; },
+                    Keycode::Left       => { self.seek(10, false, dmx, disp_queue)?; },
+                    Keycode::Up         => { self.seek(60, true,  dmx, disp_queue)?; },
+                    Keycode::Down       => { self.seek(60, false, dmx, disp_queue)?; },
+                    Keycode::PageUp     => { self.seek(600, true,  dmx, disp_queue)?; },
+                    Keycode::PageDown   => { self.seek(600, false, dmx, disp_queue)?; },
+                    Keycode::Space => { self.toggle_pause(); },
                     Keycode::Plus | Keycode::KpPlus => {
                         self.volume = (self.volume + 10).min(MAX_VOLUME);
                         if !self.mute {
@@ -412,6 +569,13 @@ impl Player {
                     Keycode::H => {
                         self.vcontrol.try_send_video(PktSendEvent::HurryUp);
                     },
+                    Keycode::O => {
+                        if keymod.contains(Mod::RSHIFTMOD) || keymod.contains(Mod::LSHIFTMOD) {
+                            self.osd.toggle_perm();
+                        } else {
+                            self.osd.toggle();
+                        }
+                    },
                     _ => {},
                 };
                 if !self.paused {
@@ -420,21 +584,30 @@ impl Player {
                 }
             }
         }
-        false
+        Ok(false)
     }
-    fn play(&mut self, name: &str, start_time: NATimePoint) {
+    fn play(&mut self, mut window: Window, name: &str, start_time: NATimePoint) -> Window {
         debug_log!(self; {format!("Playing {}", name)});
 
         // prepare data source
         let path = Path::new(name);
-        let mut file = File::open(path).unwrap();
-        let dmx_fact;
+        let mut file = if let Ok(handle) = File::open(path) {
+                if let Ok(meta) = handle.metadata() {
+                    if meta.is_dir() {
+                        return window;
+                    }
+                }
+                handle
+            } else {
+                println!("failed to open {}", name);
+                return window;
+            };
         let mut fr = FileReader::new_read(&mut file);
         let mut br = ByteReader::new(&mut fr);
         let res = detect::detect_format(name, &mut br);
         if res.is_none() {
             println!("cannot detect format for {}", name);
-            return;
+            return window;
         }
         let (dmx_name, _score) = res.unwrap();
         debug_log!(self; {format!(" found demuxer {} with score {:?}", dmx_name, _score)});
@@ -444,18 +617,22 @@ impl Player {
         nihav_register_all_demuxers(&mut dmx_reg);
         let mut dec_reg = RegisteredDecoders::new();
         nihav_register_all_decoders(&mut dec_reg);
+        let mut mtdec_reg = RegisteredMTDecoders::new();
+        if self.use_mt {
+            nihav_register_all_mt_decoders(&mut mtdec_reg);
+        }
 
         let ret = dmx_reg.find_demuxer(dmx_name);
         if ret.is_none() {
             println!("error finding {} demuxer", dmx_name);
-            return;
+            return window;
         }
-        dmx_fact = ret.unwrap();
-        br.seek(SeekFrom::Start(0)).unwrap();
+        let dmx_fact = ret.unwrap();
+        br.seek(SeekFrom::Start(0)).expect("should be able to seek to the start");
         let ret = create_demuxer(dmx_fact, &mut br);
         if ret.is_err() {
             println!("error creating demuxer");
-            return;
+            return window;
         }
         let mut dmx = ret.unwrap();
         if start_time != NATimePoint::None {
@@ -470,6 +647,7 @@ impl Player {
         let mut tb_num = 0;
         let mut tb_den = 0;
         let mut ainfo: Option<NAAudioInfo> = None;
+        let mut sbr_hack = false;
 
         let mut video_dec: Option<DecoderStuff> = None;
         let mut audio_dec: Option<DecoderStuff> = None;
@@ -480,15 +658,60 @@ impl Player {
         }
         self.has_video = false;
         self.has_audio = false;
+        self.osd.reset();
+        self.osd.set_duration(duration);
         for i in 0..dmx.get_num_streams() {
             let s = dmx.get_stream(i).unwrap();
             let info = s.get_info();
             let decfunc = dec_reg.find_decoder(info.get_name());
+            let decfunc_mt = mtdec_reg.find_decoder(info.get_name());
             println!("stream {} - {} {}", i, s, info.get_name());
             debug_log!(self; {format!(" stream {} - {} {}", i, s, info.get_name())});
             let str_id = s.get_id();
             if info.is_video() {
                 if video_dec.is_none() && self.play_video {
+                    #[cfg(feature="hwaccel")]
+                    if info.get_name() == "h264" && self.use_hwaccel {
+                        let mut dec = new_h264_hwdec();
+                        let dsupp = Box::new(NADecoderSupport::new());
+                        let props = info.get_properties().get_video_info().unwrap();
+                        if props.get_width() != 0 {
+                            width  = props.get_width();
+                            height = props.get_height();
+                        }
+                        if dec.init(info.clone()).is_err() {
+                            println!("failed to initialise hwaccel video decoder");
+                        } else {
+                            video_dec = Some(DecoderStuff{ dsupp, dec: DecoderType::VideoHW(dec) });
+                            self.video_str = str_id;
+                            let (tbn, tbd) = s.get_timebase();
+                            tb_num = tbn;
+                            tb_den = tbd;
+                            self.has_video = true;
+                            println!(" using hardware-accelerated decoding");
+                            continue;
+                        }
+                    }
+                    if let Some(decfunc) = decfunc_mt {
+                        let mut dec = (decfunc)();
+                        let mut dsupp = Box::new(NADecoderSupport::new());
+                        let props = info.get_properties().get_video_info().unwrap();
+                        if props.get_width() != 0 {
+                            width  = props.get_width();
+                            height = props.get_height();
+                        }
+                        if dec.init(&mut dsupp, info.clone(), self.vthreads).is_ok() {
+                            video_dec = Some(DecoderStuff{ dsupp, dec: DecoderType::VideoMT(dec, MTFrameReorderer::new()) });
+                            self.video_str = str_id;
+                            let (tbn, tbd) = s.get_timebase();
+                            tb_num = tbn;
+                            tb_den = tbd;
+                            self.has_video = true;
+                            continue;
+                        } else {
+                            println!("failed to create multi-threaded decoder, falling back");
+                        }
+                    }
                     if let Some(decfunc) = decfunc {
                         let mut dec = (decfunc)();
                         let mut dsupp = Box::new(NADecoderSupport::new());
@@ -508,8 +731,11 @@ impl Player {
                         dsupp.pool_u8 = NAVideoBufferPool::new(reorder_depth);
                         dsupp.pool_u16 = NAVideoBufferPool::new(reorder_depth);
                         dsupp.pool_u32 = NAVideoBufferPool::new(reorder_depth);
-                        dec.init(&mut dsupp, info).unwrap();
-                        video_dec = Some(DecoderStuff{ dsupp, dec, reord });
+                        if dec.init(&mut dsupp, info).is_err() {
+                            println!("failed to initialise video decoder");
+                            return window;
+                        }
+                        video_dec = Some(DecoderStuff{ dsupp, dec: DecoderType::Video(dec, reord) });
                         self.video_str = str_id;
                         let (tbn, tbd) = s.get_timebase();
                         tb_num = tbn;
@@ -525,9 +751,16 @@ impl Player {
                         let mut dec = (decfunc)();
                         let mut dsupp = Box::new(NADecoderSupport::new());
                         ainfo = info.get_properties().get_audio_info();
-                        dec.init(&mut dsupp, info).unwrap();
-                        let reord = Box::new(NoReorderer::new());
-                        audio_dec = Some(DecoderStuff{ dsupp, dec, reord });
+                        if let (true, Some(ref ai)) = (info.get_name() == "aac", ainfo) {
+                            if ai.sample_rate < 32000 {
+                                sbr_hack = true;
+                            }
+                        }
+                        if dec.init(&mut dsupp, info).is_err() {
+                            println!("failed to initialise audio decoder");
+                            return window;
+                        }
+                        audio_dec = Some(DecoderStuff{ dsupp, dec: DecoderType::Audio(dec) });
                         self.audio_str = str_id;
                         self.has_audio = true;
                     } else {
@@ -540,19 +773,35 @@ impl Player {
         }
         if !self.has_video && !self.has_audio {
             println!("No playable streams found.");
-            return;
+            return window;
         }
 
-        while (width <= 384) && (height <= 288) {
-            width <<= 1;
-            height <<= 1;
-        }
+        match self.sc_size {
+            ScaleSize::Auto => {
+                while (width <= 384) && (height <= 288) {
+                    width <<= 1;
+                    height <<= 1;
+                }
+            },
+            ScaleSize::Times(factor) => {
+                let nw = ((width  as f32) * factor).ceil() as usize;
+                let nh = ((height as f32) * factor).ceil() as usize;
+                if nw > 0 && nh > 0 {
+                    width  = nw;
+                    height = nh;
+                }
+            },
+            ScaleSize::Fixed(w, h) => {
+                width  = w;
+                height = h;
+            },
+        };
 
         // prepare playback structure
         let mut new_vcontrol = VideoControl::new(video_dec, width, height, tb_num, tb_den);
         std::mem::swap(&mut self.vcontrol, &mut new_vcontrol);
 
-        let mut new_acontrol = AudioControl::new(audio_dec, ainfo, &self.asystem);
+        let mut new_acontrol = AudioControl::new(audio_dec, ainfo, sbr_hack, &self.asystem);
         std::mem::swap(&mut self.acontrol, &mut new_acontrol);
 
         if self.mute {
@@ -563,38 +812,51 @@ impl Player {
 
         let fname = path.file_name();
         let wname = if let Some(fname) = fname {
-                "NihAV player - ".to_owned() + fname.to_str().unwrap()
+                // workaround for libSDL2 workaround for non-UTF8 windowing systems
+                // see https://github.com/libsdl-org/SDL/pull/4290 for detais
+                let nname = fname.to_str().expect("should be able to set window title").replace('\u{2013}', "-").replace('\u{2014}', "-");
+                "NihAV player - ".to_owned() + &nname
             } else {
                 "NihAV player".to_owned()
             };
-        let window = self.vsystem.window(&wname, width as u32, height as u32)
-            .position_centered().build().unwrap();
-        let mut canvas = window.into_canvas().build().unwrap();
+        window.set_title(&wname).expect("set window title");
+        if window.size() != (width as u32, height as u32) {
+            window.set_size(width as u32, height as u32).expect("resize window");
+        }
+        window.show();
+        let mut canvas = window.into_canvas().build().expect("should be able to build canvas");
         let texture_creator = canvas.texture_creator();
         let mut disp_q = DispQueue::new(&texture_creator, width, height, if self.has_video { FRAME_QUEUE_LEN } else { 0 });
         if !self.has_video {
             canvas.clear();
-            canvas.copy(disp_q.get_last_texture(), None, None).unwrap();
+            canvas.copy(disp_q.get_last_texture(&self.osd), None, None).expect("blit failure");
             canvas.present();
         }
 
         self.has_audio = self.acontrol.has_audio();
         if !self.has_video && !self.has_audio {
             println!("No playable streams.");
-            return;
+            return canvas.into_window();
         }
 
         // play
-        self.prefill(&mut dmx, &mut disp_q);
-        self.tkeep.reset_all(0);
+        if self.prefill(&mut dmx, &mut disp_q).is_err() {
+            std::mem::swap(&mut self.vcontrol, &mut new_vcontrol);
+            new_vcontrol.finish();
+            std::mem::swap(&mut self.acontrol, &mut new_acontrol);
+            new_acontrol.finish();
+            return canvas.into_window();
+        }
+        self.tkeep.reset_all(if !disp_q.is_empty() { disp_q.first_ts } else { 0 });
         if !self.paused {
             self.acontrol.resume();
         }
-        let mut event_pump = self.sdl_context.event_pump().unwrap();
+        let mut event_pump = self.sdl_context.event_pump().expect("should be able to create event pump");
         let mut last_disp = Instant::now();
         let mut has_data = true;
         'main: loop {
-            if self.handle_events(&mut event_pump, &mut canvas, &mut dmx, &mut disp_q) {
+            let ret = self.handle_events(&mut event_pump, &mut canvas, &mut dmx, &mut disp_q);
+            if matches!(ret, Ok(true) | Err(_)) {
                 println!();
                 break 'main;
             }
@@ -613,7 +875,14 @@ impl Player {
                             self.acontrol.try_send_audio(PktSendEvent::End);
                             has_data = false;
                         },
-                        Err(err) => { println!("demuxer error {:?}", err); },
+                        Err(err) => {
+                            println!("demuxer error {:?}", err);
+                            if err == DemuxerError::IOError {
+                                self.vcontrol.try_send_video(PktSendEvent::End);
+                                self.acontrol.try_send_audio(PktSendEvent::End);
+                                has_data = false;
+                            }
+                        },
                         Ok(pkt) => {
                             let streamno = pkt.get_stream().get_id();
                             if self.has_video && streamno == self.video_str {
@@ -637,7 +906,7 @@ impl Player {
                 debug_log!(self; {format!(" time {}", self.tkeep.get_cur_time())});
                 if self.has_video {
                     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())});
-                    let ret = try_display(&mut disp_q, &mut canvas, &self.tkeep);
+                    let ret = try_display(&mut disp_q, &mut canvas, &mut self.osd, &self.tkeep);
                     if let Some(next_time) = ret {
                         sleep_time = sleep_time.min(next_time);
                     }
@@ -679,6 +948,7 @@ impl Player {
         new_vcontrol.finish();
         std::mem::swap(&mut self.acontrol, &mut new_acontrol);
         new_acontrol.finish();
+        canvas.into_window()
     }
 }
 
@@ -691,6 +961,8 @@ fn main() {
     }
 
     let mut player = Player::new();
+    let mut builder = player.vsystem.window("NihAV Player", 640, 480);
+    let mut window = builder.position_centered().hidden().build().expect("should be able to centre window");
 
     let mut aiter = args.iter().skip(1);
     let mut seek_time = NATimePoint::None;
@@ -700,7 +972,7 @@ fn main() {
             "-ae" => { player.play_audio = true; },
             "-vn" => { player.play_video = false; },
             "-ve" => { player.play_video = true; },
-            "-seek" => {
+            "-seek" | "-start" => {
                 if let Some(arg) = aiter.next() {
                     if let Ok(time) = arg.parse::<NATimePoint>() {
                         seek_time = time;
@@ -725,8 +997,40 @@ fn main() {
             "-nodebug" => {
                 player.debug = false;
             },
+            "-mt" => {
+                player.use_mt = true;
+            },
+            "-nomt" => {
+                player.use_mt = false;
+            },
+            #[cfg(feature="hwaccel")]
+            "-hwaccel" => {
+                player.use_hwaccel = true;
+            },
+            #[cfg(feature="hwaccel")]
+            "-nohwaccel" => {
+                player.use_hwaccel = false;
+            },
+            "-threads" => {
+                if let Some(arg) = aiter.next() {
+                    if let Ok(val) = arg.parse::<usize>() {
+                        player.vthreads = val.max(1);
+                    } else {
+                        println!("wrong number of threads");
+                    }
+                }
+            },
+            "-scale" => {
+                if let Some(arg) = aiter.next() {
+                    if let Ok(ssize) = arg.parse::<ScaleSize>() {
+                        player.sc_size = ssize;
+                    } else {
+                        println!("invalid scale size");
+                    }
+                }
+            },
             _ => {
-                player.play(arg, seek_time);
+                window = player.play(window, arg, seek_time);
                 if player.end { break; }
                 seek_time = NATimePoint::None;
             },