@@ -47,6 +47,13 @@ def __init__(self, x: int, y: int, w: int, h: int, fit_display: bool = False) ->
4747 self .fit_display = fit_display
4848 self .cmd_queue : queue .Queue [str ] = queue .Queue ()
4949 self .stdin_thread = threading .Thread (target = self ._stdin_reader , daemon = True )
50+ self ._vlc_event_manager : Optional [vlc .EventManager ] = None
51+ self ._vlc_event_callbacks_registered : bool = False
52+ self ._show_window_request : bool = False
53+ self ._hide_window_request : bool = False
54+ self ._last_time : int = 0
55+ self ._last_progress_time : float = 0.0
56+ self ._startup : bool = True
5057
5158 def setup (self ) -> bool :
5259 """Initialize SDL2, create window, and set up VLC player."""
@@ -77,7 +84,7 @@ def setup(self) -> bool:
7784 return False
7885
7986 # Initialize VLC
80- vlc_args = ['--no-audio' ]
87+ vlc_args = ['--no-audio' , '--quiet' , '--verbose=0' ]
8188 try :
8289 self .instance = vlc .Instance (vlc_args )
8390 self .player = self .instance .media_player_new ()
@@ -118,8 +125,63 @@ def setup(self) -> bool:
118125 aspect_ratio = f"{ self .w } :{ self .h } "
119126 self .player .video_set_aspect_ratio (aspect_ratio )
120127
128+ # Register VLC event callbacks
129+ self ._vlc_event_manager = self .player .event_manager ()
130+ self ._register_vlc_events ()
131+
121132 return True
122133
134+ def _register_vlc_events (self ):
135+ """Attach VLC event callbacks for playback state changes."""
136+ if self ._vlc_event_manager and not self ._vlc_event_callbacks_registered :
137+ self ._vlc_event_manager .event_attach (vlc .EventType .MediaPlayerPlaying , self ._on_vlc_playing )
138+ self ._vlc_event_manager .event_attach (vlc .EventType .MediaPlayerStopped , self ._on_vlc_stopped )
139+ self ._vlc_event_manager .event_attach (vlc .EventType .MediaPlayerEndReached , self ._on_vlc_ended )
140+ self ._vlc_event_manager .event_attach (vlc .EventType .MediaPlayerEncounteredError , self ._on_vlc_error )
141+ self ._vlc_event_callbacks_registered = True
142+
143+ def _on_vlc_playing (self , event ):
144+ self .logger .debug ("VLC event: MediaPlayerPlaying" )
145+ self ._show_window_request = True
146+ self ._last_time = 0
147+ self ._last_progress_time = time .time ()
148+ self ._startup = True
149+
150+ def _on_vlc_stopped (self , event : vlc .Event ) -> None :
151+ """
152+ VLC event handler for MediaPlayerStopped.
153+
154+ Args:
155+ event (vlc.Event): VLC event object.
156+ """
157+ self .logger .debug ("VLC event: MediaPlayerStopped" )
158+ self ._hide_window_request = True
159+ self ._send_state ("ENDED" )
160+
161+ def _on_vlc_ended (self , event : vlc .Event ) -> None :
162+ """
163+ VLC event handler for MediaPlayerEndReached.
164+
165+ Args:
166+ event (vlc.Event): VLC event object.
167+ """
168+ self .logger .debug ("VLC event: MediaPlayerEndReached" )
169+ self ._hide_window_request = True
170+ self ._send_state ("ENDED" )
171+
172+ def _on_vlc_error (self , event : vlc .Event ) -> None :
173+ """
174+ VLC event handler for MediaPlayerEncounteredError.
175+
176+ Args:
177+ event (vlc.Event): VLC event object.
178+ """
179+ self .logger .error ("VLC event: MediaPlayerEncounteredError" )
180+ self ._hide_window_request = True
181+ if self .player :
182+ self .player .stop ()
183+ self ._send_state ("ENDED" )
184+
123185 def _poll_events (self ) -> bool :
124186 """Poll SDL2 events, return False if quit event is received."""
125187 while sdl2 .SDL_PollEvent (ctypes .byref (self .event )):
@@ -148,62 +210,81 @@ def _stdin_reader(self):
148210 break
149211 self .cmd_queue .put (line )
150212
213+ def check_video_progress (self ) -> bool :
214+ """
215+ Checks the progress of the currently playing video and determines if the player is stuck.
216+
217+ This method monitors the playback time of the video player. If the playback time does not advance
218+ for more than 3 seconds, or if the player reports that no media is loaded, it is considered stuck,
219+ and playback is stopped.
220+
221+ Returns:
222+ bool: True if the video is progressing normally, False if the player is stuck or not initialized.
223+ """
224+ if not self .player :
225+ self .logger .error ("Player not initialized, cannot check video progress." )
226+ return False
227+
228+ current_time = self .player .get_time ()
229+ now = time .time ()
230+
231+ if not self ._startup and current_time == self ._last_time :
232+ # No progress, check if we've been stuck for more than 3 seconds
233+ if now - self ._last_progress_time > 3.0 :
234+ self .logger .error ("vlc is stuck while playing for more than 3 seconds. Stopping it!" )
235+ self .logger .debug ("vlc current time: %d, last time: %d" , current_time , self ._last_time )
236+ self .player .stop ()
237+ return False
238+ elif current_time == - 1 : # VLC returns -1 if no media is loaded
239+ self .logger .warning ("No media loaded or media is invalid." )
240+ self .player .stop ()
241+ return False
242+ else :
243+ # Progress detected, reset timer
244+ self ._last_progress_time = now
245+ if self ._startup and current_time > 0 :
246+ self .logger .debug ("Video started playing." )
247+ self .logger .debug ("vlc current time: %d" , current_time )
248+ self ._send_state ("PLAYING" )
249+ self ._startup = False
250+
251+ self ._last_time = current_time
252+ return True
253+
151254 def run (self ) -> None :
152- """Main event loop: handle commands and playback state ."""
255+ """Main event loop: handle SDL2 events and commands ."""
153256 if not self .player :
154257 self .logger .error ("Player not initialized, cannot run." )
155258 return
156259 self .stdin_thread .start ()
157260 try :
158- while True :
159- self ._poll_events ()
160- # Check player state ignore opening and buffering
161- # to avoid flickering
162- state = self .player .get_state ()
163- if state in [vlc .State .Ended , vlc .State .Stopped ,
164- vlc .State .Error ]:
165- if sdl2 .SDL_GetWindowFlags (self .window ) & sdl2 .SDL_WINDOW_SHOWN :
166- sdl2 .SDL_HideWindow (self .window )
167- self .player .stop ()
168- self .player .set_media (None )
169- self ._send_state ("ENDED" )
170- elif state in [vlc .State .Playing , vlc .State .Paused ]:
171- self ._send_state ("PLAYING" )
172- # Show window only if not already visible
261+ running = True
262+ while running :
263+ running = self ._poll_events ()
264+
265+ # Handle window show/hide requests from VLC callbacks
266+ if self ._show_window_request :
173267 if not sdl2 .SDL_GetWindowFlags (self .window ) & sdl2 .SDL_WINDOW_SHOWN :
174268 sdl2 .SDL_ShowWindow (self .window )
175- # Wait until the window is actually shown
176- shown = False
177- start_time = time .time ()
178- timeout = 4 # seconds
179- window_id = sdl2 .SDL_GetWindowID (self .window ) # Get window ID once
180- while not shown and (time .time () - start_time ) < timeout :
181- while sdl2 .SDL_PollEvent (ctypes .byref (self .event )) != 0 :
182- if (self .event .type == sdl2 .SDL_WINDOWEVENT and
183- self .event .window .event == sdl2 .SDL_WINDOWEVENT_SHOWN and
184- self .event .window .windowID == window_id ):
185- shown = True
186- break
187- if shown : # If event found, break outer loop
188- break
189- time .sleep (0.01 )
190-
191- if not shown : # If timeout occurred
192- self .logger .warning (
193- "Player window not shown within %d seconds." , timeout
194- )
195- else :
196- # Wait a bit longer to ensure compositor has mapped the window
197- time .sleep (0.3 )
198- sdl2 .SDL_ShowCursor (sdl2 .SDL_DISABLE )
199- sdl2 .SDL_WarpMouseInWindow (self .window , self .w - 1 , self .h - 1 )
200- elif state in [vlc .State .Opening ,
201- vlc .State .Buffering ,
202- vlc .State .NothingSpecial ]:
269+ self ._wait_for_window_shown (timeout = 4.0 )
270+ sdl2 .SDL_ShowCursor (sdl2 .SDL_DISABLE )
271+ sdl2 .SDL_WarpMouseInWindow (self .window , self .w - 1 , self .h - 1 )
272+ self ._show_window_request = False
273+
274+ if sdl2 .SDL_ShowCursor (sdl2 .SDL_QUERY ) == 1 :
275+ self .logger .debug ("Mouse pointer is visible, hiding it." )
276+ sdl2 .SDL_ShowCursor (sdl2 .SDL_DISABLE )
277+
278+ if self ._hide_window_request :
203279 if sdl2 .SDL_GetWindowFlags (self .window ) & sdl2 .SDL_WINDOW_SHOWN :
204280 sdl2 .SDL_HideWindow (self .window )
205- self ._send_state ("ENDED" )
206- # check for commands in the queue
281+ self ._hide_window_request = False
282+
283+ state = self .player .get_state () if self .player else None
284+ if state == vlc .State .Playing :
285+ self .check_video_progress ()
286+
287+ # Only handle commands
207288 try :
208289 line = self .cmd_queue .get_nowait ()
209290 except queue .Empty :
@@ -214,6 +295,8 @@ def run(self) -> None:
214295 continue
215296 self ._handle_command (cmd )
216297 finally :
298+ if self .player :
299+ self .player .stop ()
217300 sdl2 .SDL_DestroyWindow (self .window )
218301 sdl2 .SDL_Quit ()
219302
@@ -226,7 +309,6 @@ def _handle_command(self, cmd: list[str]) -> None:
226309 media_path = " " .join (cmd [1 :])
227310 if os .path .exists (media_path ):
228311 self .player .stop ()
229- self .player .set_media (None )
230312 media = self .instance .media_new_path (media_path )
231313 self .player .set_media (media )
232314 self .player .play ()
@@ -240,7 +322,29 @@ def _handle_command(self, cmd: list[str]) -> None:
240322 if sdl2 .SDL_GetWindowFlags (self .window ) & sdl2 .SDL_WINDOW_SHOWN :
241323 sdl2 .SDL_HideWindow (self .window )
242324 self .player .stop ()
243- self .player .set_media (None )
325+
326+ def _wait_for_window_shown (self , timeout : float = 4.0 ) -> bool :
327+ """Wait for the SDL_WINDOWEVENT_SHOWN event for this window."""
328+ start_time = time .time ()
329+ window_id = sdl2 .SDL_GetWindowID (self .window )
330+ shown = False
331+ while not shown and (time .time () - start_time ) < timeout :
332+ while sdl2 .SDL_PollEvent (ctypes .byref (self .event )) != 0 :
333+ if (
334+ self .event .type == sdl2 .SDL_WINDOWEVENT and
335+ self .event .window .event == sdl2 .SDL_WINDOWEVENT_SHOWN and
336+ self .event .window .windowID == window_id
337+ ):
338+ shown = True
339+ break
340+ if shown :
341+ break
342+ time .sleep (0.01 )
343+ if not shown : # If timeout occurred
344+ self .logger .warning (
345+ "Player window not shown within %d seconds." , timeout
346+ )
347+ return shown
244348
245349
246350def parse_args () -> argparse .Namespace :
@@ -257,6 +361,8 @@ def parse_args() -> argparse.Namespace:
257361 parser .add_argument ("--w" , type = int , default = 640 )
258362 parser .add_argument ("--h" , type = int , default = 480 )
259363 parser .add_argument ("--fit_display" , action = "store_true" )
364+ parser .add_argument ("--log_level" , type = str , default = "info" , choices = ["debug" , "info" , "warning" , "error" , "critical" ],
365+ help = "Set the logging level (default: info)" )
260366 return parser .parse_args ()
261367
262368
@@ -265,8 +371,9 @@ def main() -> None:
265371 Entry point for the video player application.
266372 Initializes logging, parses arguments, sets up the video player, and starts the event loop.
267373 """
268- logging .basicConfig (level = logging .DEBUG )
269374 args = parse_args ()
375+ log_level = getattr (logging , args .log_level .upper (), logging .INFO )
376+ logging .basicConfig (level = log_level )
270377 player = VideoPlayer (args .x , args .y , args .w , args .h , args .fit_display )
271378 if player .setup ():
272379 player .run ()
0 commit comments