11from maix import app , webrtc , camera , image , display , touchscreen , time
2+ import shutil , subprocess
23
34font_size = 16
45image .load_font ("font" , "/maixapp/share/font/SourceHanSansCN-Regular.otf" , size = font_size )
1920choice_bitrate = 0
2021choice_res = 0
2122
23+ choice_rc_type = 0
24+ rc_types = ["CBR" , "VBR" ]
25+
2226def in_box (t , box ):
2327 return t [2 ] and box [0 ] <= t [0 ] <= box [0 ]+ box [2 ] and box [1 ] <= t [1 ] <= box [1 ]+ box [3 ]
2428
2529def config_page ():
26- global choice_encoder , choice_bitrate , choice_res
30+ def tailscale_config_page ():
31+ BG = image .Color .from_rgb (15 , 15 , 15 )
32+ CARD_BG = image .Color .from_rgb (35 , 35 , 35 )
33+ ACCENT = image .Color .from_rgb (0 , 110 , 255 )
34+ ACCENT_P = image .Color .from_rgb (0 , 80 , 200 )
35+ SUCCESS = image .Color .from_rgb (52 , 199 , 89 )
36+ DANGER = image .Color .from_rgb (255 , 59 , 48 )
37+ DANGER_P = image .Color .from_rgb (180 , 40 , 60 )
38+ GRAY = image .Color .from_rgb (90 , 90 , 95 )
39+ GRAY_P = image .Color .from_rgb (60 , 60 , 65 )
40+ TXT = image .Color .from_rgb (255 , 255 , 255 )
41+ WARN = image .Color .from_rgb (255 , 180 , 0 )
42+
43+ screen_w , screen_h = disp .width (), disp .height ()
44+
45+ btn_exit = [20 , 15 , 52 , 52 ]
46+ img_exit = image .load ("./assets/exit.jpg" ).resize (52 , 52 )
47+ img_exit_p = image .load ("./assets/exit_touch.jpg" ).resize (52 , 52 )
48+
49+ btn_h = 52
50+ btn_y = screen_h - btn_h - 20
51+ gap = 10
52+ btn_w = (screen_w - 40 - (gap * 2 )) // 3
53+
54+ btn_on = [20 , btn_y , btn_w , btn_h ]
55+ btn_off = [20 + btn_w + gap , btn_y , btn_w , btn_h ]
56+ btn_logout = [20 + (btn_w + gap ) * 2 , btn_y , btn_w , btn_h ]
57+
58+ def get_status ():
59+ try :
60+ out = subprocess .check_output (["tailscale" , "status" ], universal_newlines = True , timeout = 1 )
61+ running = "stopped" not in out .lower ()
62+ ip = subprocess .check_output (["tailscale" , "ip" ], universal_newlines = True , timeout = 1 ).strip ().split ("\n " )[0 ] if running else "-"
63+ return running , ip
64+ except : return False , "-"
65+
66+ login_url = None
67+ is_busy = False
68+
69+ while not app .need_exit ():
70+ img = image .Image (screen_w , screen_h , image .Format .FMT_RGB888 )
71+ img .draw_rect (0 , 0 , screen_w , screen_h , BG , thickness = - 1 )
72+
73+ running , ip = get_status ()
74+ if running and login_url :
75+ login_url = None
76+ t = ts .read ()
77+
78+ exit_img = img_exit_p if in_box (t , btn_exit ) else img_exit
79+ img .draw_image (btn_exit [0 ], btn_exit [1 ], exit_img )
80+ img .draw_string (85 , 28 , "Tailscale" , color = TXT , scale = 1.8 )
81+
82+ card_x , card_y = 20 , 85
83+ card_w , card_h = screen_w - 40 , 120
84+ img .draw_rect (card_x , card_y , card_w , card_h , CARD_BG , thickness = - 1 )
85+
86+ status_text = "ONLINE" if running else "OFFLINE"
87+ status_col = SUCCESS if running else DANGER
88+
89+ img .draw_circle (card_x + 30 , card_y + 35 , 9 , status_col , thickness = - 1 )
90+ img .draw_string (card_x + 55 , card_y + 20 , status_text , color = status_col , scale = 2.5 )
91+ img .draw_string (card_x + 30 , card_y + 80 , f"IP Address: { ip } " , color = image .Color .from_rgb (200 , 200 , 200 ), scale = 1.6 )
92+
93+ if login_url :
94+ msg_y = card_y + card_h + 10
95+ box_h = 100
96+ img .draw_rect (card_x , msg_y , card_w , box_h , image .Color .from_rgb (35 , 30 , 10 ), thickness = - 1 )
97+ tip_scale = 1.8
98+ url_scale = 1.5
99+ tip_txt = "Please log in using a web browser:"
100+ tip_size = image .string_size (tip_txt , scale = tip_scale )
101+ url_txt = login_url .replace ("https://" , "" )
102+ if len (url_txt ) > 40 :
103+ url_txt = url_txt [:40 ] + "..."
104+ url_size = image .string_size (url_txt , scale = url_scale )
105+ tip_y = msg_y + 15
106+ url_y = tip_y + tip_size .height () + 10
107+ img .draw_string (card_x + (card_w - tip_size .width ())// 2 , tip_y , tip_txt , color = WARN , scale = tip_scale )
108+ img .draw_string (card_x + (card_w - url_size .width ())// 2 , url_y , url_txt , color = ACCENT , scale = url_scale )
109+
110+ def draw_action_btn (box , text , color , press_color , enabled ):
111+ if not enabled :
112+ fill = image .Color .from_rgb (50 , 50 , 50 )
113+ text_col = image .Color .from_rgb (100 , 100 , 100 )
114+ else :
115+ is_pressed = in_box (t , box ) and not is_busy
116+ fill = press_color if is_pressed else color
117+ text_col = TXT
118+
119+ img .draw_rect (box [0 ], box [1 ], box [2 ], box [3 ], fill , thickness = - 1 )
120+
121+ display_txt = "..." if (is_busy and in_box (t , box )) else text
122+ tsize = image .string_size (display_txt , scale = 1.3 )
123+ img .draw_string (box [0 ]+ (box [2 ]- tsize .width ())// 2 , box [1 ]+ (box [3 ]- tsize .height ())// 2 , display_txt , color = text_col , scale = 1.3 )
124+
125+ draw_action_btn (btn_on , "Start" , ACCENT , ACCENT_P , not running )
126+ draw_action_btn (btn_off , "Stop" , GRAY , GRAY_P , running )
127+ draw_action_btn (btn_logout , "Logout" , DANGER , DANGER_P , True )
128+
129+ disp .show (img )
130+
131+ if t [2 ] and not is_busy :
132+ if in_box (t , btn_exit ):
133+ break
134+
135+ if in_box (t , btn_on ) and not running :
136+ is_busy = True
137+ subprocess .call (["systemctl" , "enable" , "--now" , "tailscaled.service" ])
138+ proc = subprocess .Popen (["tailscale" , "up" ], stdout = subprocess .PIPE , stderr = subprocess .STDOUT , universal_newlines = True )
139+ for _ in range (15 ):
140+ line = proc .stdout .readline ()
141+ if "https://login.tailscale.com" in line :
142+ login_url = line [line .find ("https://" ):].strip ()
143+ break
144+ time .sleep (1 )
145+ is_busy = False
146+
147+ elif in_box (t , btn_off ) and running :
148+ is_busy = True
149+ subprocess .call (["systemctl" , "disable" , "--now" , "tailscaled.service" ])
150+ subprocess .Popen (["tailscale" , "down" ])
151+ login_url = None
152+ time .sleep (1.2 )
153+ is_busy = False
154+
155+ elif in_box (t , btn_logout ):
156+ is_busy = True
157+ subprocess .Popen (["tailscale" , "logout" ])
158+ login_url = None
159+ time .sleep (1.2 )
160+ is_busy = False
161+
162+ time .sleep_ms (25 )
163+
164+ global choice_encoder , choice_bitrate , choice_res , choice_rc_type
27165
28166 BG = image .Color .from_rgb (25 , 25 , 25 )
29167 CARD = image .Color .from_rgb (45 , 45 , 45 )
@@ -52,14 +190,31 @@ def config_page():
52190 btn_x = card_x + btn_padding
53191 gap = int ((card_h - btn_h * 3 - btn_padding * 2 ) / 2 )
54192
193+
55194 btn_enc = [btn_x , card_y + btn_padding , btn_w , btn_h ]
56- btn_bps = [btn_x , card_y + btn_padding + btn_h + gap , btn_w , btn_h ]
195+ bps_w = int (btn_w * 0.60 )
196+ rc_gap = 20
197+ rc_w = int (btn_w * 0.40 ) - rc_gap
198+ btn_bps = [btn_x , card_y + btn_padding + btn_h + gap , bps_w , btn_h ]
199+ btn_rc = [btn_x + bps_w + rc_gap , card_y + btn_padding + btn_h + gap , rc_w , btn_h ]
57200 btn_res = [btn_x , card_y + btn_padding + (btn_h + gap ) * 2 , btn_w , btn_h ]
58201
59- exit_btn_w = int (screen_w * 0.15 )
60- btn_go_w = screen_w - exit_btn_w - card_margin * 3
61- btn_go = [card_margin , screen_h - bottom_btn_height - bottom_margin , btn_go_w , bottom_btn_height ]
62- btn_exit = [btn_go [0 ] + btn_go_w + card_margin , screen_h - bottom_btn_height - bottom_margin , exit_btn_w , bottom_btn_height ]
202+ exit_btn_w = int (screen_w * 0.16 )
203+ tailscale_btn_w = int (screen_w * 0.26 )
204+ go_btn_w = int (screen_w * 0.42 )
205+ rc_gap = 20
206+ tailscale_installed = shutil .which ("tailscale" ) is not None
207+
208+ if tailscale_installed :
209+ total_btn_w = go_btn_w + tailscale_btn_w + exit_btn_w + rc_gap * 2
210+ left = (screen_w - total_btn_w ) // 2
211+ btn_go = [left , screen_h - bottom_btn_height - bottom_margin , go_btn_w , bottom_btn_height ]
212+ btn_tailscale = [btn_go [0 ] + go_btn_w + rc_gap , screen_h - bottom_btn_height - bottom_margin , tailscale_btn_w , bottom_btn_height ]
213+ btn_exit = [btn_tailscale [0 ] + tailscale_btn_w + rc_gap , screen_h - bottom_btn_height - bottom_margin , exit_btn_w , bottom_btn_height ]
214+ else :
215+ btn_go_w = screen_w - exit_btn_w - card_margin * 3
216+ btn_go = [card_margin , screen_h - bottom_btn_height - bottom_margin , btn_go_w , bottom_btn_height ]
217+ btn_exit = [btn_go [0 ] + btn_go_w + card_margin , screen_h - bottom_btn_height - bottom_margin , exit_btn_w , bottom_btn_height ]
63218
64219 def draw_round_rect (img , x , y , w , h , color ):
65220 img .draw_rect (x , y , w , h , color , thickness = - 1 )
@@ -83,8 +238,24 @@ def draw_setting_row(img, box, label, value, pressed=False):
83238 vx = box [0 ] + box [2 ] - pad_x - vsize .width ()
84239 img .draw_string (vx , ty , value , color = TXT , scale = scale )
85240
86- while not app .need_exit ():
241+ def draw_rc_switch (img , box , selected ):
242+ gap = 18
243+ w = (box [2 ] - gap ) // 2
244+ h = box [3 ]
245+ x0 = box [0 ]
246+ x1 = box [0 ] + w + gap
247+ y = box [1 ]
248+
249+ draw_round_rect (img , x0 , y , w , h , BTN_P if selected == 0 else BTN )
250+ cbr_size = image .string_size ("CBR" , scale = 2 )
251+ img .draw_string (x0 + (w - cbr_size .width ())// 2 , y + (h - cbr_size .height ())// 2 , "CBR" , color = TXT , scale = 2 )
252+
253+ draw_round_rect (img , x1 , y , w , h , BTN_P if selected == 1 else BTN )
254+ vbr_size = image .string_size ("VBR" , scale = 2 )
255+ img .draw_string (x1 + (w - vbr_size .width ())// 2 , y + (h - vbr_size .height ())// 2 , "VBR" , color = TXT , scale = 2 )
256+
87257
258+ while not app .need_exit ():
88259 img = image .Image (disp .width (), disp .height (), image .Format .FMT_RGB888 )
89260 img .clear ()
90261 img .draw_rect (0 , 0 , disp .width (), disp .height (), BG , thickness = - 1 )
@@ -97,6 +268,7 @@ def draw_setting_row(img, box, label, value, pressed=False):
97268
98269 draw_setting_row (img , btn_enc , "Encoder" , encoders [choice_encoder ])
99270 draw_setting_row (img , btn_bps , "Bitrate" , bitrates [choice_bitrate ])
271+ draw_rc_switch (img , btn_rc , choice_rc_type )
100272 draw_setting_row (img , btn_res , "Resolution" , resolutions [choice_res ])
101273
102274 draw_round_rect (img , btn_go [0 ], btn_go [1 ], btn_go [2 ], btn_go [3 ], BTN_P )
@@ -106,6 +278,20 @@ def draw_setting_row(img, box, label, value, pressed=False):
106278 ty = btn_go [1 ] + (btn_go [3 ] - tsize .height ()) // 2
107279 img .draw_string (tx , ty , txt , color = TXT , scale = 2 )
108280
281+ t = ts .read ()
282+
283+ if tailscale_installed :
284+ draw_round_rect (img , btn_tailscale [0 ], btn_tailscale [1 ], btn_tailscale [2 ], btn_tailscale [3 ], image .Color .from_rgb (60 , 180 , 120 ))
285+ txt_ts = "Tailscale"
286+ tsize_ts = image .string_size (txt_ts , scale = 2 )
287+ tx_ts = btn_tailscale [0 ] + (btn_tailscale [2 ] - tsize_ts .width ()) // 2
288+ ty_ts = btn_tailscale [1 ] + (btn_tailscale [3 ] - tsize_ts .height ()) // 2
289+ img .draw_string (tx_ts , ty_ts , txt_ts , color = TXT , scale = 2 )
290+ if in_box (t , btn_tailscale ):
291+ time .sleep_ms (150 )
292+ tailscale_config_page ()
293+ continue
294+
109295 draw_round_rect (img , btn_exit [0 ], btn_exit [1 ], btn_exit [2 ], btn_exit [3 ], image .Color .from_rgb (200 , 60 , 60 ))
110296 txt_exit = "Exit"
111297 tsize_exit = image .string_size (txt_exit , scale = 2 )
@@ -115,7 +301,6 @@ def draw_setting_row(img, box, label, value, pressed=False):
115301
116302 disp .show (img )
117303
118- t = ts .read ()
119304 if not t [2 ]:
120305 time .sleep_ms (60 )
121306 continue
@@ -124,6 +309,8 @@ def draw_setting_row(img, box, label, value, pressed=False):
124309 choice_encoder = (choice_encoder + 1 ) % len (encoders )
125310 elif in_box (t , btn_bps ):
126311 choice_bitrate = (choice_bitrate + 1 ) % len (bitrates )
312+ elif in_box (t , btn_rc ):
313+ choice_rc_type = (choice_rc_type + 1 ) % 2
127314 elif in_box (t , btn_res ):
128315 choice_res = (choice_res + 1 ) % len (resolutions )
129316 elif in_box (t , btn_go ):
@@ -162,8 +349,10 @@ def start_streaming():
162349 cam = camera .Camera (W , H , image .Format .FMT_YVU420SP , fps = 30 )
163350 cam2 = cam .add_channel (disp .width (), disp .height ())
164351
352+ rc_type = webrtc .WebRTCRCType .WEBRTC_RC_CBR if choice_rc_type == 0 else webrtc .WebRTCRCType .WEBRTC_RC_VBR
165353 server = webrtc .WebRTC (
166354 stream_type = stream_type ,
355+ rc_type = rc_type ,
167356 bitrate = bitrate_value ,
168357 gop = 15 ,
169358 stun_server = "stun:stun.miwifi.com:3478" ,
@@ -177,8 +366,12 @@ def start_streaming():
177366
178367 img_exit = image .load ("./assets/exit.jpg" ).resize (50 , 50 )
179368 img_exit_touch = image .load ("./assets/exit_touch.jpg" ).resize (50 , 50 )
369+ img_eye_open = image .load ("./assets/img_eye_open.png" ).resize (50 , 50 )
370+ img_eye_close = image .load ("./assets/img_eye_close.png" ).resize (50 , 50 )
371+ img_eye_last_change = time .ticks_ms ()
180372
181373 need_exit = False
374+ show_urls = False
182375
183376 while not app .need_exit ():
184377 try :
@@ -188,37 +381,41 @@ def start_streaming():
188381 continue
189382
190383 t = ts .read ()
191- box = [20 , 15 , img_exit .width (), img_exit .height ()]
192- if in_box (t , box ):
193- img .draw_image (box [0 ], box [1 ], img_exit_touch )
384+
385+ box_exit = [20 , 15 , img_exit .width (), img_exit .height ()]
386+ if in_box (t , box_exit ):
387+ img .draw_image (box_exit [0 ], box_exit [1 ], img_exit_touch )
194388 need_exit = True
195389 else :
196- img .draw_image (box [0 ], box [1 ], img_exit )
390+ img .draw_image (box_exit [0 ], box_exit [1 ], img_exit )
391+
392+ box_eye = [20 , 15 + img_exit .height () + 18 , img_eye_open .width (), img_eye_open .height ()]
393+ if in_box (t , box_eye ) and time .ticks_ms () - img_eye_last_change > 200 :
394+ img_eye_last_change = time .ticks_ms ()
395+ show_urls = not show_urls
197396
198- if urls :
397+ if show_urls :
398+ img .draw_image (box_eye [0 ], box_eye [1 ], img_eye_open )
399+ else :
400+ img .draw_image (box_eye [0 ], box_eye [1 ], img_eye_close )
401+
402+ if show_urls and urls :
199403 screen_w = disp .width ()
200404 screen_h = disp .height ()
201405 url_scale = max (2 , int (screen_w / 300 ))
202-
203406 url_margin = int (screen_w * 0.05 )
204- url_max_width = int (screen_w * 0.85 )
205-
206407 title_text = "WebRTC URL:"
207408 title_size = image .string_size (title_text , scale = url_scale )
208409 x = screen_w - title_size .width () - url_margin
209410 y = int (screen_h * 0.05 )
210-
211411 img .draw_string (x , y , title_text ,
212412 color = image .Color .from_rgb (0 ,255 ,0 ),
213413 scale = url_scale )
214-
215414 line_spacing = int (title_size .height () * 2 )
216415 y += line_spacing
217-
218416 for u in urls :
219417 url_size = image .string_size (u , scale = url_scale )
220418 x = max (url_margin , screen_w - url_size .width () - url_margin )
221-
222419 img .draw_string (x , y , u ,
223420 color = image .Color .from_rgb (0 ,255 ,0 ),
224421 scale = url_scale )
@@ -231,7 +428,7 @@ def start_streaming():
231428
232429 del server
233430
234- while True :
431+ while not app . need_exit () :
235432 if not config_page ():
236433 break
237434 start_streaming ()
0 commit comments