任务需求及分析
我选择的是任务五:网络相机
具体要求如下:
- 完成相机驱动,定时拍摄图片,并将图片通过网络传到电脑或服务器,实现长时间拍摄
- 通过电脑端编程将图片合成为一个视频
Sipeed团队为我们提供了一个通过网络推送摄像头获取到的图片的demo:camera_streaming_through_wifi,在此基础上,我修复了一些bug并做了一些易用性方面的优化,还制作了一个上位机用于接收、显示图片和存储为视频。
硬件介绍
本次使用的开发板为来自Sipeed的M1s Dock开发板,主要包含了一个博流智能的BL808的SoC,附带了一个摄像头和屏幕。关于硬件的介绍具体可参考Sipeed的Wiki:M1s DOCK 开发板 - Sipeed Wiki
软件流程图
推流交互细节
嵌入式部分代码
由于主要的连接WiFi和推流部分代码已由Sipeed提供并运行在E907核中,我们的主要代码只需要给E907核传参即可。不过在此之前,需要修复Sipeed遗留在SDK中的问题并重新编译刷入固件(现在应该已经修复了这个问题)。重新给E907刷入固件的教程可参考Sipeed提供的仓库sipeed/M1s_BL808_example: M1s_BL808_example (github.com)中的说明Flash Guide - 4.Download e907 firmware
diff --git a/components/sipeed/e907/m1s_e907_xram/src/m1s_e907_xram_wifi.c b/components/sipeed/e907/m1s_e907_xram/src/m1s_e907_xram_wifi.c
index 36ea2f8..2bad7dc 100644
--- a/components/sipeed/e907/m1s_e907_xram/src/m1s_e907_xram_wifi.c
+++ b/components/sipeed/e907/m1s_e907_xram/src/m1s_e907_xram_wifi.c
@@ -216,8 +216,8 @@ _retry:
}
client_addr.sin_family = AF_INET;
- client_addr.sin_port = htons(8888);
- client_addr.sin_addr.s_addr = inet_addr("10.42.0.1");
+ client_addr.sin_port = htons(private.port);
+ client_addr.sin_addr.s_addr = inet_addr(private.ip);
memset(&(client_addr.sin_zero), 0, sizeof(client_addr.sin_zero));
程序主体参考camera_streaming_through_wifi例程即可。为了能方便地修改Wifi信息及服务端信息,我添加了从Flash中读取文件并解析这些信息。
读取文件部分代码
// get wifi ssid and password from flash(udisk in PC)
fatfs_register();
int fd = -1;
char *file_name = "/flash/wifi.json";
if (0 > (fd = aos_open(file_name, 0))) {
printf("[failed] open file\r\n");
return;
}
int file_length = aos_lseek(fd, 0, SEEK_END);
if (0 >= file_length) {
aos_close(fd);
printf("[failed] the length of file is less then zero\r\n");
return;
}
uint8_t *file_buf = NULL;
if (NULL == (file_buf = malloc(file_length + 1))) {
aos_close(fd);
printf("[failed] malloc buffer to read file\r\n");
return;
}
memset(file_buf, 0, file_length + 1); // make sure the last byte is '\0'
aos_lseek(fd, 0, SEEK_SET);
if (file_length != aos_read(fd, file_buf, file_length)) {
aos_close(fd);
printf("[failed] err occur while reading file\r\n");
return;
}
aos_close(fd);
printf("read from file:%s\r\n", file_buf);
解析部分使用了cJSON库,需要把cJSON仓库加入编译
修改文件夹内的bouffalo.mk
COMPONENT_ADD_INCLUDEDIRS += . \
cJSON
COMPONENT_SRCS :=
COMPONENT_OBJS := $(patsubst %.c,%.o, $(COMPONENT_SRCS))
COMPONENT_SRCDIRS := . \
cJSON
解析代码具体如下
// get the wifi info from json file
cJSON* cjson_root = NULL;
cJSON* cjson_ssid = NULL;
cjson_root = cJSON_Parse((const char *)file_buf);
if (NULL == cjson_root) {
printf("[failed] parse json file\r\n");
return;
}
cjson_ssid = cJSON_GetObjectItem(cjson_root, "ssid");
if (NULL == cjson_ssid) {
printf("[failed] get ssid from json file\r\n");
return;
}
char* ssid = cjson_ssid->valuestring;
// 解析ssid ip port信息类似,省略
之后就是根据读出的信息连接WiFi并推流
m1s_xram_wifi_init();
m1s_xram_wifi_connect(ssid, password);
m1s_xram_wifi_upload_stream(ip, port);
为了避免内存泄漏问题,最后加上
cJSON_Delete(cjson_root);
之后在M1s Dock插上电脑虚拟出的U盘中新建文件wifi.json
并填入信息就会让BL808连接对应WiFi并对指定地址和端口推流了。
{
"ssid":"<your wifi ssid>",
"password":"<your wifi password>",
"ip": "<your server ip>",
"port": <your server port>
}
PC端代码
PC部分主要分为接收和显示存储
接收部分主要面向socket套接字编程,根据前文提及的协议接收,并把图片丢到队列中
def handle_tcp(sock, addr):
global img_queue
print(f"new client: {addr}")
length = 0
buffer = b""
while True:
data = sock.recv(1500)
if not data:
break
# print(f"recv: {[hex(int(each)) for each in data]}")
# 第一次接收,单独4byte
if length == 0:
length = data[-4] + (data[-3] << 8) + \
(data[-2] << 16) + (data[-1] << 24)
# print(f"length: {length}")
sock.send(b"1")
continue
# 后面的接收,
# 根据0xff 0xd8和0xff 0xd9判断开始和结束
# 为了避免末尾小包,先拼包
buffer += data
if data[0] == 0xff and data[1] == 0xd8:
# print("frame start")
buffer = data
elif buffer[-6] == 0xff and buffer[-5] == 0xd9:
# print("frame end")
# print(f"recv frame length: {len(buffer) - 4}")
length = buffer[-4] + (buffer[-3] << 8) + \
(buffer[-2] << 16) + (buffer[-1] << 24)
# print(f"next frame length: {length}")
buffer = buffer[:-4]
img_queue.put(buffer)
sock.send(b"1")
def server_thread():
print("start server")
s = socket.socket()
s.bind(('', 8888))
s.listen()
# 等待接收一个client,为这个client开一个线程接收
while True:
print("wait for client")
sock, addr = s.accept()
t = threading.Thread(target=handle_tcp, args=(sock, addr))
t.setDaemon(True)
t.start()
time.sleep(0.1)
显示和存储部分主要使用opencv的api,由于opencv不自带编解码器,需要使用思科开源的openh264-1.8.0.dll
动态库来进行编码,放置于python文件同个文件夹即可(暂未测试除win外的平台)。
def ui_thread():
global img_queue
global video
print("start ui")
img = None
filename = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime())
while True:
try:
img1 = img_queue.get(timeout=0.05)
img1 = np.frombuffer(img1, np.uint8)
img = cv.imdecode(img1, cv.IMREAD_ANYCOLOR)
if video is None:
(height, width, _) = img.shape
print(f"video width: {width}, height: {height}")
os.path.exists("video") or os.mkdir("video")
video = cv.VideoWriter(
f'video/{filename}.mp4',
cv.VideoWriter_fourcc(*'avc1'),
20.0,
(width, height)
)
print(f"start record video: {filename}.mp4")
except:
if img is None:
continue
cv.imshow("frame", img)
video_mutex.acquire()
video.write(img)
video_mutex.release()
if cv.waitKey(1) & 0xFF == ord('q'):
break
print("quit from ui")
signal.raise_signal(signal.SIGABRT)
需要注意的是需要在结束录制时调用video.release()
,否则会导致文件不完整打不开的问题。
并且,python使用threading库会导致Ctrl+C的响应问题,我们需要重写系统信号的接收。
def save_video():
global video
if video is not None:
video_mutex.acquire()
video.release()
video_mutex.release()
else:
print("video is None")
if __name__ == '__main__':
def quit(signum, frame):
save_video()
print("quit")
sys.exit()
signal.signal(signal.SIGINT, quit)
signal.signal(signal.SIGTERM, quit)
signal.signal(signal.SIGABRT, quit)
t = threading.Thread(target=server_thread, args=())
t.setDaemon(True)
t.start()
t = threading.Thread(target=ui_thread, args=())
t.setDaemon(True)
t.start()
while True:
try:
time.sleep(0.1)
except:
break
实现的效果
推流
存储
遇到的问题及解决方案
- Server等不到Client的连接:检查防火墙,一般来说,将网络改为专用网络而不是公用网络即可。
- opencv录制使用h264编码库openh264版本问题:需要使用1.8.0而不是2.x。
- opencv录制后不存在文件问题:需要首先建立文件夹,而不会自动建。
- opencv录制文件打开问题:检查img和opencv设置的视频帧的大小。需要注意的是,视频录制传入的参的宽高于img的shape顺序相反。
- python多线程不响应Ctrl+C问题:需要设置线程为守护
t.setDaemon(True)
,重定向signal,并且主进程不能阻塞(如等待新client连接等)