From 8d699cdae9c84bbc62b73cb3c34a3517e808067a Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 23 Jul 2023 18:34:19 -0500 Subject: [PATCH 1/9] Update scan_screens.py --- src/seedsigner/gui/screens/scan_screens.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/seedsigner/gui/screens/scan_screens.py b/src/seedsigner/gui/screens/scan_screens.py index 6484fec11..20ef51104 100644 --- a/src/seedsigner/gui/screens/scan_screens.py +++ b/src/seedsigner/gui/screens/scan_screens.py @@ -1,7 +1,8 @@ import time from dataclasses import dataclass -from typing import List, Tuple +from PIL import Image +from typing import Tuple from seedsigner.gui import renderer from seedsigner.hardware.buttons import HardwareButtonsConstants @@ -9,8 +10,8 @@ from seedsigner.models import DecodeQR, DecodeQRStatus from seedsigner.models.threads import BaseThread -from .screen import BaseScreen, BaseTopNavScreen, ButtonListScreen -from ..components import BaseComponent, Button, GUIConstants, Fonts, IconButton, TextArea, calc_text_centering +from .screen import BaseScreen, ButtonListScreen +from ..components import GUIConstants, Fonts, TextArea @@ -20,7 +21,7 @@ class ScanScreen(BaseScreen): decoder: DecodeQR = None instructions_text: str = "< back | Scan a QR code" resolution: Tuple[int,int] = (480, 480) - framerate: int = 12 + framerate: int = 4 render_rect: Tuple[int,int,int,int] = None @@ -75,7 +76,8 @@ def run(self): with self.renderer.lock: if frame.width > self.render_width or frame.height > self.render_height: frame = frame.resize( - (self.render_width, self.render_height) + (self.render_width, self.render_height), + resample=Image.NEAREST ) self.renderer.canvas.paste( frame, @@ -99,9 +101,8 @@ def run(self): self.renderer.show_image() end = timer() - # print(f"{1.0/(end - start)} fps") # Time in seconds, e.g. 5.38091952400282 + print(f"{1.0/(end - start):0.2f} fps") # Time in seconds, e.g. 5.38091952400282 - time.sleep(0.05) # turn this up or down to tune performance while decoding psbt if self.camera._video_stream is None: break @@ -112,9 +113,12 @@ def _run(self): Screen. Once interaction starts, the display updates have to be managed in _run(). The live preview is an extra-complex case. """ + from timeit import default_timer as timer while True: + start = timer() frame = self.camera.read_video_stream() if frame is not None: + print("Decoder checking next frame") status = self.decoder.add_image(frame) if status in (DecodeQRStatus.COMPLETE, DecodeQRStatus.INVALID): @@ -126,6 +130,10 @@ def _run(self): self.camera.stop_video_stream_mode() break + elapsed = (timer() - start) + if elapsed < 1.0/float(self.framerate): + time.sleep(1.0/float(self.framerate) - elapsed) + @dataclass From 20cd1b4011e5ba21b10ac606247c956eaff280c7 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 23 Jul 2023 18:37:49 -0500 Subject: [PATCH 2/9] temp fps display --- src/seedsigner/gui/screens/scan_screens.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/seedsigner/gui/screens/scan_screens.py b/src/seedsigner/gui/screens/scan_screens.py index 20ef51104..3d82d3849 100644 --- a/src/seedsigner/gui/screens/scan_screens.py +++ b/src/seedsigner/gui/screens/scan_screens.py @@ -65,6 +65,8 @@ def run(self): from timeit import default_timer as timer instructions_font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.BUTTON_FONT_SIZE) + + framerate = 0 while self.keep_running: start = timer() frame = self.camera.read_video_stream(as_image=True) @@ -84,13 +86,14 @@ def run(self): (self.render_rect[0], self.render_rect[1]) ) - if scan_text: + if scan_text or True: self.renderer.draw.text( xy=( int(self.renderer.canvas_width/2), self.renderer.canvas_height - GUIConstants.EDGE_PADDING ), - text=scan_text, + # text=scan_text, + text=f"{framerate:0.2f} fps", fill=GUIConstants.BODY_FONT_COLOR, font=instructions_font, stroke_width=4, @@ -101,7 +104,8 @@ def run(self): self.renderer.show_image() end = timer() - print(f"{1.0/(end - start):0.2f} fps") # Time in seconds, e.g. 5.38091952400282 + framerate = 1.0/(end - start) + # print(f"{framerate:0.2f} fps") # Time in seconds, e.g. 5.38091952400282 if self.camera._video_stream is None: break From 9b01cdc2f102148e036f51c240907dbb7a0d946b Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 24 Jul 2023 12:52:43 -0500 Subject: [PATCH 3/9] cleanup, documentation, more stable fps calc --- src/seedsigner/gui/screens/scan_screens.py | 76 +++++++++++++++------- src/seedsigner/hardware/camera.py | 1 - 2 files changed, 54 insertions(+), 23 deletions(-) diff --git a/src/seedsigner/gui/screens/scan_screens.py b/src/seedsigner/gui/screens/scan_screens.py index 3d82d3849..b8762845e 100644 --- a/src/seedsigner/gui/screens/scan_screens.py +++ b/src/seedsigner/gui/screens/scan_screens.py @@ -1,8 +1,7 @@ import time from dataclasses import dataclass -from PIL import Image -from typing import Tuple +from PIL import Image, ImageDraw from seedsigner.gui import renderer from seedsigner.hardware.buttons import HardwareButtonsConstants @@ -18,11 +17,33 @@ @dataclass class ScanScreen(BaseScreen): + """ + Live preview has to balance three competing threads: + * Camera capturing frames and making them available to read. + * Decoder analyzing frames for QR codes. + * Live preview display writing frames to the screen. + + All of this would ideally be rewritten as in C/C++/Rust with python bindings for + vastly improved performance. + + Until then, we have to balance the resources the Pi Zero has to work with. Thus, we + set a modest fps target for the camera: 4fps. At this pace, the decoder and the live + display can more or less keep up with the flow of frames without much wasted effort + in any of the threads. + + The resolution (480x480) has not been tweaked in order to guarantee that our + decoding abilities remain as-is. It's possible that more optimizations could be made + here (e.g. higher res w/no performance impact? Lower res w/same decoding but faster + performance? etc). + + Note: This is quite a lot of important tasks for a Screen to be managing; much of + this should probably be refactored into the Controller. + """ decoder: DecodeQR = None instructions_text: str = "< back | Scan a QR code" - resolution: Tuple[int,int] = (480, 480) - framerate: int = 4 - render_rect: Tuple[int,int,int,int] = None + resolution: tuple[int,int] = (480, 480) + framerate: int = 4 # TODO: alternate optimization for Pi Zero 2W + render_rect: tuple[int,int,int,int] = None def __post_init__(self): @@ -43,7 +64,7 @@ def __post_init__(self): class LivePreviewThread(BaseThread): - def __init__(self, camera: Camera, decoder: DecodeQR, renderer: renderer.Renderer, instructions_text: str, render_rect: Tuple[int,int,int,int]): + def __init__(self, camera: Camera, decoder: DecodeQR, renderer: renderer.Renderer, instructions_text: str, render_rect: tuple[int,int,int,int]): self.camera = camera self.decoder = decoder self.renderer = renderer @@ -55,9 +76,6 @@ def __init__(self, camera: Camera, decoder: DecodeQR, renderer: renderer.Rendere self.render_width = self.render_rect[2] - self.render_rect[0] self.render_height = self.render_rect[3] - self.render_rect[1] - print(f"render_width: {self.render_width}") - print(f"render_height: {self.render_height}") - super().__init__() @@ -66,14 +84,25 @@ def run(self): instructions_font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.BUTTON_FONT_SIZE) - framerate = 0 + start_time = time.time() + num_frames = 0 + show_framerate = True # enable for debugging / testing while self.keep_running: start = timer() frame = self.camera.read_video_stream(as_image=True) if frame is not None: - scan_text = self.instructions_text + num_frames += 1 + cur_time = time.time() + cur_fps = num_frames / (cur_time - start_time) if self.decoder and self.decoder.get_percent_complete() > 0 and self.decoder.is_psbt: scan_text = str(self.decoder.get_percent_complete()) + "% Complete" + if show_framerate: + scan_text += f" {cur_fps:0.2f} fps" + else: + if show_framerate: + scan_text = f"{cur_fps:0.2f} fps" + else: + scan_text = self.instructions_text with self.renderer.lock: if frame.width > self.render_width or frame.height > self.render_height: @@ -81,19 +110,20 @@ def run(self): (self.render_width, self.render_height), resample=Image.NEAREST ) - self.renderer.canvas.paste( - frame, - (self.render_rect[0], self.render_rect[1]) - ) + # self.renderer.canvas.paste( + # frame, + # (self.render_rect[0], self.render_rect[1]) + # ) + + draw = ImageDraw.Draw(frame) - if scan_text or True: - self.renderer.draw.text( + if scan_text: + draw.text( xy=( int(self.renderer.canvas_width/2), self.renderer.canvas_height - GUIConstants.EDGE_PADDING ), - # text=scan_text, - text=f"{framerate:0.2f} fps", + text=scan_text, fill=GUIConstants.BODY_FONT_COLOR, font=instructions_font, stroke_width=4, @@ -101,11 +131,11 @@ def run(self): anchor="ms" ) - self.renderer.show_image() + self.renderer.disp.ShowImage(frame, 0, 0) end = timer() framerate = 1.0/(end - start) - # print(f"{framerate:0.2f} fps") # Time in seconds, e.g. 5.38091952400282 + # print(f"{framerate:0.2f} fps") if self.camera._video_stream is None: break @@ -122,7 +152,7 @@ def _run(self): start = timer() frame = self.camera.read_video_stream() if frame is not None: - print("Decoder checking next frame") + # print("Decoder checking next frame") status = self.decoder.add_image(frame) if status in (DecodeQRStatus.COMPLETE, DecodeQRStatus.INVALID): @@ -134,6 +164,8 @@ def _run(self): self.camera.stop_video_stream_mode() break + # Have the decoder thread sleep until (roughly) the next frame is ready in + # order to avoid wasting CPU cycles re-checking the same frame. elapsed = (timer() - start) if elapsed < 1.0/float(self.framerate): time.sleep(1.0/float(self.framerate) - elapsed) diff --git a/src/seedsigner/hardware/camera.py b/src/seedsigner/hardware/camera.py index c458339fe..b07ed3aae 100644 --- a/src/seedsigner/hardware/camera.py +++ b/src/seedsigner/hardware/camera.py @@ -1,5 +1,4 @@ import io -import numpy from picamera import PiCamera from PIL import Image From 2634b86f62c1e6325058d448c81efd44909d9989 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 24 Jul 2023 15:11:32 -0500 Subject: [PATCH 4/9] Update scan_screens.py --- src/seedsigner/gui/screens/scan_screens.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/seedsigner/gui/screens/scan_screens.py b/src/seedsigner/gui/screens/scan_screens.py index b8762845e..c6f7e7de4 100644 --- a/src/seedsigner/gui/screens/scan_screens.py +++ b/src/seedsigner/gui/screens/scan_screens.py @@ -126,8 +126,8 @@ def run(self): text=scan_text, fill=GUIConstants.BODY_FONT_COLOR, font=instructions_font, - stroke_width=4, - stroke_fill=GUIConstants.BACKGROUND_COLOR, + # stroke_width=4, + # stroke_fill=GUIConstants.BACKGROUND_COLOR, anchor="ms" ) From 658890eece906b7aed41c304f404c44dd5cb26fa Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 24 Jul 2023 15:32:14 -0500 Subject: [PATCH 5/9] Update scan_screens.py --- src/seedsigner/gui/screens/scan_screens.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/seedsigner/gui/screens/scan_screens.py b/src/seedsigner/gui/screens/scan_screens.py index c6f7e7de4..2641de044 100644 --- a/src/seedsigner/gui/screens/scan_screens.py +++ b/src/seedsigner/gui/screens/scan_screens.py @@ -42,7 +42,7 @@ class ScanScreen(BaseScreen): decoder: DecodeQR = None instructions_text: str = "< back | Scan a QR code" resolution: tuple[int,int] = (480, 480) - framerate: int = 4 # TODO: alternate optimization for Pi Zero 2W + framerate: int = 5 # TODO: alternate optimization for Pi Zero 2W render_rect: tuple[int,int,int,int] = None @@ -75,6 +75,7 @@ def __init__(self, camera: Camera, decoder: DecodeQR, renderer: renderer.Rendere self.render_rect = (0, 0, self.renderer.canvas_width, self.renderer.canvas_height) self.render_width = self.render_rect[2] - self.render_rect[0] self.render_height = self.render_rect[3] - self.render_rect[1] + self.decoder_fps = "0.0" super().__init__() @@ -97,10 +98,10 @@ def run(self): if self.decoder and self.decoder.get_percent_complete() > 0 and self.decoder.is_psbt: scan_text = str(self.decoder.get_percent_complete()) + "% Complete" if show_framerate: - scan_text += f" {cur_fps:0.2f} fps" + scan_text += f" {cur_fps:0.2f} | {self.decoder_fps}" else: if show_framerate: - scan_text = f"{cur_fps:0.2f} fps" + scan_text = f"{cur_fps:0.2f} | {self.decoder_fps}" else: scan_text = self.instructions_text @@ -148,6 +149,8 @@ def _run(self): _run(). The live preview is an extra-complex case. """ from timeit import default_timer as timer + num_frames = 0 + start_time = time.time() while True: start = timer() frame = self.camera.read_video_stream() @@ -155,6 +158,10 @@ def _run(self): # print("Decoder checking next frame") status = self.decoder.add_image(frame) + num_frames += 1 + decoder_fps = f"{num_frames / (time.time() - start_time):0.2f}" + self.threads[0].decoder_fps = decoder_fps + if status in (DecodeQRStatus.COMPLETE, DecodeQRStatus.INVALID): self.camera.stop_video_stream_mode() break From 4a3afa8212b28a47e193f916f8ecdd004376ebd6 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 24 Jul 2023 15:38:29 -0500 Subject: [PATCH 6/9] Update scan_screens.py --- src/seedsigner/gui/screens/scan_screens.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seedsigner/gui/screens/scan_screens.py b/src/seedsigner/gui/screens/scan_screens.py index 2641de044..877833203 100644 --- a/src/seedsigner/gui/screens/scan_screens.py +++ b/src/seedsigner/gui/screens/scan_screens.py @@ -42,7 +42,7 @@ class ScanScreen(BaseScreen): decoder: DecodeQR = None instructions_text: str = "< back | Scan a QR code" resolution: tuple[int,int] = (480, 480) - framerate: int = 5 # TODO: alternate optimization for Pi Zero 2W + framerate: int = 4 # TODO: alternate optimization for Pi Zero 2W render_rect: tuple[int,int,int,int] = None From f4b0c263e983a1fd099d5c8b10bcfa0e665d2445 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 24 Jul 2023 20:23:37 -0500 Subject: [PATCH 7/9] Update scan_screens.py --- src/seedsigner/gui/screens/scan_screens.py | 55 ++++++++++------------ 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/src/seedsigner/gui/screens/scan_screens.py b/src/seedsigner/gui/screens/scan_screens.py index 877833203..7c4709694 100644 --- a/src/seedsigner/gui/screens/scan_screens.py +++ b/src/seedsigner/gui/screens/scan_screens.py @@ -27,10 +27,12 @@ class ScanScreen(BaseScreen): vastly improved performance. Until then, we have to balance the resources the Pi Zero has to work with. Thus, we - set a modest fps target for the camera: 4fps. At this pace, the decoder and the live + set a modest fps target for the camera: 5fps. At this pace, the decoder and the live display can more or less keep up with the flow of frames without much wasted effort in any of the threads. + Note: performance tuning was targeted for the Pi Zero. + The resolution (480x480) has not been tweaked in order to guarantee that our decoding abilities remain as-is. It's possible that more optimizations could be made here (e.g. higher res w/no performance impact? Lower res w/same decoding but faster @@ -42,7 +44,7 @@ class ScanScreen(BaseScreen): decoder: DecodeQR = None instructions_text: str = "< back | Scan a QR code" resolution: tuple[int,int] = (480, 480) - framerate: int = 4 # TODO: alternate optimization for Pi Zero 2W + framerate: int = 5 # TODO: alternate optimization for Pi Zero 2W render_rect: tuple[int,int,int,int] = None @@ -87,9 +89,8 @@ def run(self): start_time = time.time() num_frames = 0 - show_framerate = True # enable for debugging / testing + show_framerate = False # enable for debugging / testing while self.keep_running: - start = timer() frame = self.camera.read_video_stream(as_image=True) if frame is not None: num_frames += 1 @@ -111,32 +112,32 @@ def run(self): (self.render_width, self.render_height), resample=Image.NEAREST ) - # self.renderer.canvas.paste( - # frame, - # (self.render_rect[0], self.render_rect[1]) - # ) draw = ImageDraw.Draw(frame) if scan_text: - draw.text( - xy=( - int(self.renderer.canvas_width/2), - self.renderer.canvas_height - GUIConstants.EDGE_PADDING - ), - text=scan_text, - fill=GUIConstants.BODY_FONT_COLOR, - font=instructions_font, - # stroke_width=4, - # stroke_fill=GUIConstants.BACKGROUND_COLOR, - anchor="ms" - ) + # Temp solution: render a slight 1px shadow behind the text + draw.text(xy=( + int(self.renderer.canvas_width/2 + 1), + self.renderer.canvas_height - GUIConstants.EDGE_PADDING + 1 + ), + text=scan_text, + fill="black", + font=instructions_font, + anchor="ms") + + # Render the onscreen instructions + draw.text(xy=( + int(self.renderer.canvas_width/2), + self.renderer.canvas_height - GUIConstants.EDGE_PADDING + ), + text=scan_text, + fill=GUIConstants.BODY_FONT_COLOR, + font=instructions_font, + anchor="ms") self.renderer.disp.ShowImage(frame, 0, 0) - - end = timer() - framerate = 1.0/(end - start) - # print(f"{framerate:0.2f} fps") + print(f" {cur_fps:0.2f} | {self.decoder_fps}") if self.camera._video_stream is None: break @@ -171,12 +172,6 @@ def _run(self): self.camera.stop_video_stream_mode() break - # Have the decoder thread sleep until (roughly) the next frame is ready in - # order to avoid wasting CPU cycles re-checking the same frame. - elapsed = (timer() - start) - if elapsed < 1.0/float(self.framerate): - time.sleep(1.0/float(self.framerate) - elapsed) - @dataclass From 03389bf4378c1d410f1ff3ce9681815ece97c48f Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 24 Jul 2023 20:47:13 -0500 Subject: [PATCH 8/9] Update scan_screens.py --- src/seedsigner/gui/screens/scan_screens.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/seedsigner/gui/screens/scan_screens.py b/src/seedsigner/gui/screens/scan_screens.py index 7c4709694..f3d82e283 100644 --- a/src/seedsigner/gui/screens/scan_screens.py +++ b/src/seedsigner/gui/screens/scan_screens.py @@ -117,6 +117,8 @@ def run(self): if scan_text: # Temp solution: render a slight 1px shadow behind the text + # TODO: Replace the instructions_text with a disappearing + # toast/popup (see: QR Brightness UI)? draw.text(xy=( int(self.renderer.canvas_width/2 + 1), self.renderer.canvas_height - GUIConstants.EDGE_PADDING + 1 @@ -137,7 +139,7 @@ def run(self): anchor="ms") self.renderer.disp.ShowImage(frame, 0, 0) - print(f" {cur_fps:0.2f} | {self.decoder_fps}") + # print(f" {cur_fps:0.2f} | {self.decoder_fps}") if self.camera._video_stream is None: break @@ -149,11 +151,9 @@ def _run(self): Screen. Once interaction starts, the display updates have to be managed in _run(). The live preview is an extra-complex case. """ - from timeit import default_timer as timer num_frames = 0 start_time = time.time() while True: - start = timer() frame = self.camera.read_video_stream() if frame is not None: # print("Decoder checking next frame") From b01a4c068aa1ee7d1233bb472bc0657f40bc5da4 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 24 Jul 2023 20:54:42 -0500 Subject: [PATCH 9/9] Renderer support for max speed --- src/seedsigner/gui/renderer.py | 7 ++++++- src/seedsigner/gui/screens/scan_screens.py | 14 +++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/seedsigner/gui/renderer.py b/src/seedsigner/gui/renderer.py index 1191eeec2..cc733bfae 100644 --- a/src/seedsigner/gui/renderer.py +++ b/src/seedsigner/gui/renderer.py @@ -34,7 +34,12 @@ def configure_instance(cls): renderer.draw = ImageDraw.Draw(renderer.canvas) - def show_image(self, image=None, alpha_overlay=None): + def show_image(self, image=None, alpha_overlay=None, show_direct=False): + if show_direct: + # Use the incoming image as the canvas and immediately render + self.disp.ShowImage(image, 0, 0) + return + if alpha_overlay: if image == None: image = self.canvas diff --git a/src/seedsigner/gui/screens/scan_screens.py b/src/seedsigner/gui/screens/scan_screens.py index f3d82e283..85fc3a27e 100644 --- a/src/seedsigner/gui/screens/scan_screens.py +++ b/src/seedsigner/gui/screens/scan_screens.py @@ -44,7 +44,7 @@ class ScanScreen(BaseScreen): decoder: DecodeQR = None instructions_text: str = "< back | Scan a QR code" resolution: tuple[int,int] = (480, 480) - framerate: int = 5 # TODO: alternate optimization for Pi Zero 2W + framerate: int = 5 # TODO: alternate optimization for Pi Zero 2W? render_rect: tuple[int,int,int,int] = None @@ -110,18 +110,20 @@ def run(self): if frame.width > self.render_width or frame.height > self.render_height: frame = frame.resize( (self.render_width, self.render_height), - resample=Image.NEAREST + resample=Image.NEAREST # Use nearest neighbor for max speed ) draw = ImageDraw.Draw(frame) if scan_text: + # Note: shadowed text (adding a 'stroke' outline) can + # significantly slow down the rendering. # Temp solution: render a slight 1px shadow behind the text # TODO: Replace the instructions_text with a disappearing # toast/popup (see: QR Brightness UI)? draw.text(xy=( - int(self.renderer.canvas_width/2 + 1), - self.renderer.canvas_height - GUIConstants.EDGE_PADDING + 1 + int(self.renderer.canvas_width/2 + 2), + self.renderer.canvas_height - GUIConstants.EDGE_PADDING + 2 ), text=scan_text, fill="black", @@ -138,7 +140,7 @@ def run(self): font=instructions_font, anchor="ms") - self.renderer.disp.ShowImage(frame, 0, 0) + self.renderer.show_image(frame, show_direct=True) # print(f" {cur_fps:0.2f} | {self.decoder_fps}") if self.camera._video_stream is None: @@ -156,7 +158,6 @@ def _run(self): while True: frame = self.camera.read_video_stream() if frame is not None: - # print("Decoder checking next frame") status = self.decoder.add_image(frame) num_frames += 1 @@ -167,7 +168,6 @@ def _run(self): self.camera.stop_video_stream_mode() break - # TODO: KEY_UP gives control to NavBar; use its back arrow to cancel if self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_RIGHT) or self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_LEFT): self.camera.stop_video_stream_mode() break