22import logging
33from typing import List , Optional , Set , Tuple , cast
44
5+ import numpy as np
56from eventkinds import AppEventKind , ControlEventKind
67from kivy .app import App
8+ from kivy .clock import Clock
79from kivy .core .window import Window
10+ from kivy .graphics import Rectangle
11+ from kivy .graphics .texture import Texture
812from kivy .lang .builder import Builder
913from kivy .properties import BooleanProperty
1014from kivy .uix .label import Label
4751 text: "Connect"
4852 on_press: app.send_app_event(AppEventKind.ENTER_CONTROL_SESSION)
4953
50-
5154<UserMessagePopup>:
5255 size_hint: None, None
5356 size: 400, 200
107110
108111<LiveScreen>:
109112 BoxLayout:
110- Label:
111- text: "Here you can see your glasses in action."
113+ Widget:
114+ id: display
115+ size_hint_x: 0.8
116+ size_hint_y: 1
117+ BoxLayout:
118+ orientation: "vertical"
119+ size_hint_x: 0.2
120+ Button:
121+ text: "Start"
122+ on_press: app.send_control_event(ControlEventKind.START_LIVE)
123+ Button:
124+ text: "Stop"
125+ on_press: app.send_control_event(ControlEventKind.STOP_LIVE)
112126
113127<SelectableList>:
114128 viewclass: 'SelectableLabel'
@@ -242,7 +256,8 @@ def set_recording_status(self, is_recording: bool) -> None:
242256
243257
244258class LiveScreen (Screen ):
245- pass
259+ def clear (self , * args ):
260+ self .ids .display .canvas .clear ()
246261
247262
248263class UserMessagePopup (Popup ):
@@ -256,6 +271,8 @@ def __init__(self, **kwargs):
256271 self .tasks : Set [asyncio .Task ] = set ()
257272 self .app_events : asyncio .Queue [AppEventKind ] = asyncio .Queue ()
258273 self .control_events : asyncio .Queue [ControlEventKind ] = asyncio .Queue ()
274+ self .live_stream_task : Optional [asyncio .Task ] = None
275+ self .read_frames_task : Optional [asyncio .Task ] = None
259276 self .add_widget (DiscoveryScreen (name = "discovery" ))
260277 self .add_widget (ControlScreen (name = "control" ))
261278
@@ -391,8 +408,81 @@ async def handle_control_event(self, g3: Glasses3, event: ControlEventKind) -> N
391408 await g3 .recorder .stop ()
392409 case ControlEventKind .DELETE_RECORDING :
393410 await self .delete_selected_recording (g3 )
411+ case ControlEventKind .START_LIVE :
412+ self .start_live_stream (g3 )
413+ case ControlEventKind .STOP_LIVE :
414+ await self .stop_live_stream ()
394415 self .get_screen ("control" ).set_task_running_status (False )
395416
417+ def start_live_stream (self , g3 : Glasses3 ) -> None :
418+ async def live_stream ():
419+ async with g3 .stream_rtsp () as streams :
420+ async with streams .scene_camera .decode () as decoded_stream :
421+ live_screen = self .get_screen ("control" ).ids .sm .get_screen ("live" )
422+ Window .bind (on_resize = live_screen .clear )
423+ self .latest_frame = await decoded_stream .get ()
424+ self .read_frames_task = self .create_task (
425+ update_frame (decoded_stream , streams ), name = "update_frame"
426+ )
427+ self .scheduling_delay = 1 / 25
428+ Clock .schedule_once (draw_frame , self .scheduling_delay )
429+ await self .read_frames_task
430+
431+ async def update_frame (decoded_stream , streams ):
432+ while True :
433+ self .latest_frame = await decoded_stream .get ()
434+ logging .debug (streams .scene_camera .stats )
435+
436+ def draw_frame (dt ):
437+ """NOTE: The scheduling if frame redraw would ideally be made with Clock.schedule_interval at 25Hz, but due to performance issues a dynamic scheduling delay is implemented as a temporary fix."""
438+ display = self .get_screen ("control" ).ids .sm .get_screen ("live" ).ids .display
439+ update_scheduling_delay (dt )
440+ if self .read_frames_task is not None :
441+ if not self .read_frames_task .done ():
442+ Clock .schedule_once (draw_frame , self .scheduling_delay )
443+ with display .canvas :
444+ image = np .flip (self .latest_frame .to_ndarray (format = "bgr24" ), 0 )
445+ texture = Texture .create (
446+ size = (image .shape [1 ], image .shape [0 ]), colorfmt = "bgr"
447+ )
448+ image = np .reshape (image , - 1 )
449+ texture .blit_buffer (image , colorfmt = "bgr" , bufferfmt = "ubyte" )
450+ Rectangle (
451+ texture = texture ,
452+ pos = (0 , (display .top - display .width * 9 / 16 ) / 2 ),
453+ size = (display .width , display .width * 9 / 16 ),
454+ )
455+
456+ def update_scheduling_delay (last_delay ):
457+ if last_delay > self .scheduling_delay :
458+ self .scheduling_delay = last_delay
459+ elif last_delay < self .scheduling_delay :
460+ self .scheduling_delay -= (self .scheduling_delay - last_delay ) / 2
461+
462+ def live_stream_task_running () -> bool :
463+ if self .live_stream_task is not None :
464+ return not self .live_stream_task .done ()
465+ else :
466+ return False
467+
468+ if live_stream_task_running ():
469+ logging .info ("Task not started: live_stream_task already running." )
470+ else :
471+ self .live_stream_task = self .create_task (
472+ live_stream (), name = "live_stream_task"
473+ )
474+
475+ async def stop_live_stream (self ) -> None :
476+ if self .read_frames_task is not None :
477+ if not self .read_frames_task .cancelled ():
478+ await self .cancel_task (self .read_frames_task )
479+ if self .live_stream_task is not None :
480+ if not self .live_stream_task .cancelled ():
481+ await self .cancel_task (self .live_stream_task )
482+ live_screen = self .get_screen ("control" ).ids .sm .get_screen ("live" )
483+ Window .unbind (on_resize = live_screen .clear )
484+ live_screen .clear ()
485+
396486 async def delete_selected_recording (self , g3 : Glasses3 ) -> None :
397487 selected = (
398488 self .get_screen ("control" )
0 commit comments