Skip to content

Commit b2aa371

Browse files
authored
Merge pull request #159 from 916BGAI/dev
app webrtc add VBR encode support
2 parents 62bd500 + a306940 commit b2aa371

File tree

4 files changed

+220
-23
lines changed

4 files changed

+220
-23
lines changed
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
from maix import time, webrtc, camera, image
22

3-
cam = camera.Camera(640, 480, image.Format.FMT_YVU420SP)
3+
cam = camera.Camera(640, 480, image.Format.FMT_YVU420SP, fps=30)
44
server = webrtc.WebRTC()
55
server.bind_camera(cam)
66
server.start()
77

88
print(server.get_url())
99

10-
while True:
10+
while not app.need_exit():
1111
time.sleep(1)
1.15 KB
Loading
1.03 KB
Loading

projects/app_webrtc_stream/main.py

Lines changed: 218 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from maix import app, webrtc, camera, image, display, touchscreen, time
2+
import shutil, subprocess
23

34
font_size = 16
45
image.load_font("font", "/maixapp/share/font/SourceHanSansCN-Regular.otf", size = font_size)
@@ -19,11 +20,148 @@
1920
choice_bitrate = 0
2021
choice_res = 0
2122

23+
choice_rc_type = 0
24+
rc_types = ["CBR", "VBR"]
25+
2226
def 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

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

Comments
 (0)