Skip to content

Commit f8d410d

Browse files
committed
Merge branch 'dev'
2 parents e128099 + f27fa3c commit f8d410d

File tree

5 files changed

+175
-107
lines changed

5 files changed

+175
-107
lines changed

src/picframe/interface_http.py

Lines changed: 3 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -82,48 +82,6 @@ def heif_to_image(fname: str) -> Optional[Image.Image]:
8282
logger.warning("Failed attempt to convert %s due to %s \n** Have you installed pi_heif? **", fname, e)
8383
return None
8484

85-
86-
def frame_to_image(fname: str) -> Optional[Image.Image]:
87-
"""
88-
Converts the first frame of a video file to an image.
89-
90-
This function attempts to extract the first frame of a video file
91-
using the `VideoFrameExtractor` from the `picframe.video_streamer` module.
92-
If the extraction is successful, it ensures the image is in RGB mode
93-
before returning it. If the extraction fails or an error occurs, it logs
94-
a warning and returns either an empty byte string or None.
95-
96-
Args:
97-
fname (str): The file path to the video file.
98-
99-
Returns:
100-
PIL.Image.Image or bytes or None: The extracted image in RGB mode if successful,
101-
an empty byte string if the frame extraction fails, or None if an exception occurs.
102-
103-
Notes:
104-
- Ensure that `ffmpeg` is installed and available in the system for
105-
`VideoFrameExtractor` to work correctly.
106-
- Logs warnings if the frame extraction or conversion fails.
107-
"""
108-
logger = logging.getLogger("interface_http.frame_to_jpg")
109-
try:
110-
from picframe.video_streamer import VideoFrameExtractor
111-
image = VideoFrameExtractor.get_first_frame_as_image(fname)
112-
if image is None:
113-
logger.warning("Failed to extract frames from %s \n** Have you installed ffmpeg? **", fname)
114-
return None
115-
116-
if image.mode not in ("RGB", "RGBA"):
117-
image = image.convert("RGB")
118-
return image
119-
except ImportError:
120-
logger.warning("Failed to import required module for converting %s \n** Have you installed ffmpeg? **", fname)
121-
return None
122-
except (OSError, IOError) as e:
123-
logger.warning("Failed attempt to convert %s due to %s \n** Have you installed ffmpeg? **", fname, e)
124-
return None
125-
126-
12785
class RequestHandler(BaseHTTPRequestHandler):
12886

12987
def do_AUTHHEAD(self):
@@ -182,16 +140,10 @@ def do_GET(self): # noqa: C901
182140
is_bytes = True
183141
elif extension in VIDEO_EXTENSIONS:
184142
# as current_image may be video
185-
image = frame_to_image(page)
186-
if image is not None:
187-
buf = io.BytesIO()
188-
image.save(buf, format="JPEG")
189-
buf.seek(0)
190-
page_bytes = buf.read()
191-
else:
192-
page_bytes = b""
143+
file = os.path.splitext(page)[0]
144+
page = file + ".1.frame"
193145
content_type = EXTENSION_TO_MIMETYPE['.jpg']
194-
is_bytes = True
146+
is_bytes = False
195147
else:
196148
is_bytes = False
197149
else:

src/picframe/model.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,8 @@ def __init__(self, configfile=DEFAULT_CONFIGFILE):
167167
except yaml.YAMLError as exc:
168168
self.__logger.error("Can't parse yaml config file: %s: %s", configfile, exc)
169169
root_logger = logging.getLogger()
170-
root_logger.setLevel(self.get_model_config()['log_level']) # set root logger
170+
level = getattr(logging, self.get_model_config()['log_level'].upper(), logging.WARNING)
171+
root_logger.setLevel(level)
171172
log_file = self.get_model_config()['log_file']
172173
if log_file != '':
173174
filehandler = logging.FileHandler(log_file) # NB default appending so needs monitoring

src/picframe/video_player.py

Lines changed: 157 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -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

246350
def 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

Comments
 (0)