基于K230的http视频推流源代码

一、概述
本代码经实跑,在01Studio的K230开发板上能够正常运行,其主要功能包括:建立WiFi热点(名称K230,密码12345678)、在IDELCD上显示拍摄到的视频,支持浏览器通过192.168.1.88查看视频,同时也请大侠们帮忙搭一个html的框架,在不需要APP的条件下遥控车、船或者飞机。

实际运行效果:浏览器和IDE显示

实际运行效果:WiFi热点

实际运行效果:手机浏览器查看视频

二、代码功能介绍

  1. 代码中包含了三个主要线程:
    三个线程间共用图像 img ,用锁 imgLock 防止访问冲突
  • th_Camera():定时拍摄照片,并将照片存储在img变量中,供其他线程处理和使用。
  • th_Display():显示img变量里面的图像。
  • http_server():创建WiFi 热点并建立HTTP服务器,推送视频图像至客户端浏览器。
  1. __main__函数中,启动了三个线程分别执行上述三个功能函数。

三、待改进的问题

  1. WiFi热点和通讯
  • 画面会自己停止
    推送画面帧数从来没超过 4000 帧,最多约 6 分钟左右视频就会停止。
    画面停止时系统报错Error No 11
    不知道是硬件问题,还是底层驱动问题,也可能是内存占用问题,或者浏览器缓存问题。

  • 无法再次绑定80端口
    通讯失败后,我试图重新启动 WiFi 热点,在重启 HTTP 服务时无法绑定 80 端口
    可能是 socket.close 函数没有释放端口资源

  • 多设备接入
    正常情况下,WiFiHTTP 服务应该允许接入多个设备的。
    应该在每一次 accept 成功后,建立一个对应的 HTTP 服务线程,访问结束就关闭线程。
    但我注意到,有两个设备接入热点时,之前访问成功的浏览器视频明显出现卡顿,所以就没做。

  • 客户端 IPv4 地址
    socket.accept函数得到的client_addr我没能解析成形如 192.168.1.20IPv4 地址。

  1. 缺少DHCP服务
    硬件或者底层似乎有多设备接入的IP地址分配能力,但IP地址局限于192.168.1.20之后。

  2. HTTP服务
    没做 HTTP 协议的应答和解析,通过浏览器仅能看视频,无法控制设备。

  3. 视频编码
    本程序是按 jpeg 格式推送图片的,若修改为视频码流模式帧率会高很多。比如 H265 编码视频格式,并打包进 http 协议命令里,发送给 3 个视频框。

四、协作需求

作为遥控车 / 船 / 飞机的控制界面,需要定义一个 HTML 框架,里面应包含以下内容:

  • 云台控制 6 组:上 / 下 / 左 / 右 / 远 / 近,幅度(输入框 + 滚动条,与每个按钮对应)。
  • 运动控制 4 组:进 / 退 / 左 / 右,速度 / 幅度(输入框 + 滚动条,与每个按钮对应)。
  • 视频框 3 个:前摄 / 左摄 / 右摄。受限于网速,分辨率应该不超过 640x480(GC2093 的长宽比 为1920:1080,满足这个条件的图像才不会畸变)。K230 支持的 3 个摄像头,可以用作视觉摄像,也可分别为视觉、深度、热成像。

请各位大侠帮忙一起动手,看看能否解决上述问题;同时欢迎大家参与和探讨,寻找更好的方法。
请大神们赐教,一起解决这些问题,让 K230 的应用更加完善!:smiley:

from media.sensor import *
from media.display import *
from media.media import *
import utime,ubinascii,ujson,usocket,_thread,network,gc

#http服务
def http_server():
    global img,imgLock,RunCamera,ssCNT

    img = None       #拍摄到的照片,多线程运行时防未赋值出错
    RunCamera = True #线程退出条件,比如按Key键3秒后修改此值即可关闭程序

    while img==None and RunCamera==True: #死等摄像头启动后赋值给img
        os.exitpoint()
        utime.sleep_ms(100)

    while RunCamera: #线程退出条件,比如按Key键3秒后修改此值即可关闭程序
        os.exitpoint()

        try:
        # 创建WiFi热点
        # 待改进:
        #    不知道是硬件还是底层驱动问题,没超过4000帧画面就会停止----------
        #    通讯失败后重新启动WiFi热点,试图解决WiFi通讯中断的问题
            print('\tCreate WiFi hostpot with (ssid=K230,key=12345678)')
            ap = network.WLAN(network.AP_IF)
            ap.active(True)
            ap.config(essid='K230', password='12345678')
        # 缺DHCP服务
        #   硬件或者底层似乎有多设备接入的IP地址分配能力,但IP地址局限于192.168.1.20之后
        #   有两个设备接入热点时,已经打开的终端浏览器的帧率明显降低
            ap.ifconfig(('192.168.1.88', '255.255.255.0', '',''))#配置本机IP为非路由器常用的192.168.1.1

            # 创建 HTTP 服务器
            IPaddress = ap.ifconfig()[0]
            Port = 80
            ai = usocket.getaddrinfo(IPaddress, Port)
            addr = ai[0][-1]
            print(f'\tCreate HTTP server listen at {IPaddress}:{Port}\n\n')
            s = usocket.socket(usocket.AF_INET, usocket.SOCK_STREAM)
            s.bind(addr) #再次绑定端口时会报错,估计底层驱动有错,关闭socket时没有释放端口
            s.listen(5)
        except Exception as e:
            print(f"\n\t建立HTTP服务时出错:{e}")
            continue

        while RunCamera: #线程退出条件,比如按Key键3秒后修改此值即可关闭程序
            os.exitpoint()
            try:
                cl, addr = s.accept() #没能理解addr的表达方法,未见形如192.168.1.20的IPv4的地址数据
                ssCNT = 0 #本次成功发送的帧计数器清零,-------实测未超过4000帧就不通讯了---------------
                request = cl.recv(1024)
                if not request:
                    continue
                request_str = request.decode()

                #待改进:
                #        未对HTTP协议进行解析和应答,无法通过浏览器控制设备
                #        应使用形如 if 'GET /stream' in request_str:的命令解析HTTP指令
                #需要做的工作:
                #    作为遥控车/船/飞机,应该定义一个html框架,里面应包含以下内容:
                #        云台控制 6组:上/下/左/右/远/近,幅度(输入框+滚动条,与每个按钮对应)
                #        运动控制 4组:进/退/左/右,速度/幅度(输入框+滚动条,与每个按钮对应)
                #        视 频 框 3个:前摄/左摄/右摄
                #                    分辨率640x360(GC2093使用比例1920:1080,图像才无畸变)
                #                    K230支持的3个摄像头,也可分别为视觉、深度、热成像
                #    解析http协议的GET/POST命令,并执行相应的指令
                header =    "HTTP/1.1 200 OK\r\n" \
                            "Server: Tao\r\n" \
                            "Content-Type: multipart/x-mixed-replace;boundary=Tao\r\n" \
                            "Cache-Control: no-cache\r\n" \
                            "Pragma: no-cache\r\n\r\n"
                # ********* 这个http内容来自网友Tao **********
                cl.send(header)
            except Exception as e:
                print(f"\n\tHTTP错误:{e}")
                break

            #被前面的accept和recv阻塞了
            #   若无阻塞,下面的代码不需要单独做循环,至少可跟recv放进同一个循环里
            while RunCamera: #线程退出条件,比如按Key键3秒后修改此值即可关闭程序
                os.exitpoint()
                try:
                    if imgLock.acquire(1,1): #申请img变量的锁,阻塞1秒
                        img_bytes = img.compress(quality=50)
                        imgLock.release()
                        # ********* 这个http内容来自网友Tao **********
                        header = f"--Tao\r\nContent-Type: image/jpeg\r\nContent-Length: {len(img_bytes)}\r\n\r\n"
                        #待改进
                        #   按图片格式推送jpeg图片
                        #   实际上按视频码流模式的帧率会高很多
                        #需要做的工作
                        #   按视频流格式编码,比如H265视频格式
                        #   打包进http协议里,发送给3个视频框
                        cl.send(header)
                        cl.send(img_bytes)

                        #摄像头0的推送
                        #摄像头1的推送

                        del img_bytes
                        gc.collect()
                    else:
                        print('img变量锁申请超时')
                except Exception as e:
                    #if b'[Errno 11] EAGAIN' in e:
                    if e.errno == 11:
                        print('\t-----Error 11-----')
                        break
                    else:
                        print(f"\n\tHTTP错误:{e}")
                        break
                utime.sleep_ms(100) #限制帧率不超过10,给其它线程留下CPU时间
                ssCNT += 1 #帧计数---------------------------

        cl.close()
        s.close()
        ap.active(False)
        print('\t----- Restart HTTP server -----')
        utime.sleep(5) #等5秒,重启WiFi


#拍摄
#   定时拍摄照片到img变量
#   其它线程可以使用或修改img变量,获得图像能力
def th_Camera():
    global img,imgLock,RunCamera,ssCNT

    cam = Sensor() #默认摄像头2,一共支持3个摄像头
    cam.reset()
    cam.set_framesize(width=640, height=360, chn = CAM_CHN_ID_0) #每个摄像头有3个通道,我们使用ch0
    cam.set_pixformat(Sensor.RGB565, chn = CAM_CHN_ID_0) #目前还不支持rgb888转换到jpg格式

    #摄像头0配置
    #摄像头1配置

    Display.init(Display.ST7701, to_ide=True, osd_num = 2)
    MediaManager.init()
    cam.run()

    RunCamera = True
    clock = time.clock()
    fps = 0

    ssCNT = 0 #计时------------------------------
    while RunCamera: #线程退出条件,比如按Key键3秒后修改此值即可关闭程序
        clock.tick()
        os.exitpoint()
        if imgLock.acquire(1,1): #申请变量img的锁,阻塞1秒
            del img
            gc.collect()
            img = cam.snapshot()
            img.draw_string_advanced(5,5,36,'%d fps: %.1f'%(ssCNT,fps),color=(255,0,0))

            #摄像头0的拍摄
            #摄像头1的拍摄

            imgLock.release()
        utime.sleep_ms(50) #为保证数据及时刷新,2倍于推送帧率
        fps = clock.fps()
    cam.stop()
    utime.sleep_ms(100)
    MediaManager.deinit()

#显示,其它线程可以修改img变量获得显示能力
def th_Display():
    global img,imgLock,RunCamera

    RunCamera = True #线程退出条件,比如按Key键3秒后修改此值即可关闭程序
    img = None
    while img == None and RunCamera==True: #死等摄像头启动后赋值给img
        os.exitpoint()
        utime.sleep_ms(100)

    while RunCamera: #线程退出条件,比如按Key键3秒后修改此值即可关闭程序
        os.exitpoint()
        if imgLock.acquire(0,0):#申请变量img的锁,无阻塞
            Display.show_image(img,x=80,y=60) #显示图片
            #显示摄像头0的图像
            #显示摄像头1的图像
            imgLock.release()
        utime.sleep_ms(100)


if __name__ == "__main__":
    global RunCamera

    RunCamera = True #线程退出条件,比如按Key键3秒后修改此值即可关闭程序
    imgLock = _thread.allocate_lock() #线程锁实例

    _thread.start_new_thread(th_Camera, ())   #摄像头线程
    _thread.start_new_thread(th_Display, ())  #显示线程
    _thread.start_new_thread(http_server, ()) #推流线程
    while RunCamera:
        os.exitpoint()
        utime.sleep(1)
2 个赞

关于accept函数返回的addr 我认为是底层程序存在错误,理由如下

我确信客户端IP是’192.168.1.20’的条件下,使用如下语句的时候
cl, addr = s.accept()
addr的值是bytearray(112,242,127,187,63,0,0,0,70,49,1,0,0,0,0,0)
且每次不一样,这些数据里面明显没有IP地址’192.168.1.20’

考虑到编程环境有可能存在问题,我用库函数生成了一个IP地址作为对比,
addr=(usocket.getaddrinfo(‘192.168.1.88’,80))[0][-1]
addr的值是bytearray(2,0,0,80,192,168,1,88,0,0,0,0,0,0,0,0)
明显看到’192.168.1.88’的首字节出现在addr[4]的位置,端口号80出现在addr[3]位置,其高字节应该是addr[3],明显与函数约定的(‘192.168.1.88’,80)格式不同。

等我把usocket换成socket试试,后面分享给大家

失败了,换成socket模块的accept()函数得到16字节的addr,里面没看到192.168.1的数据

图传得稳定性改善:

把AP模式改为STA模式,但目前还是只能单设备,图传稳定性已经极大改善了,但也带来了缺点需要连接路由器使用.

实测

QQ_1726407861198

import utime, ubinascii, ujson, usocket, _thread, network, gc
from media.sensor import *
from media.display import *
from media.media import *
time.sleep(5) #等待WiFi模块初始化
# 全局变量
img = None  # 拍摄到的照片
imgLock = _thread.allocate_lock()  # 图像锁
RunCamera = True  # 线程退出标志
ssCNT = 0  # 帧计数
gc_threshold = 200  # 每发送200张图像进行一次垃圾回收

SSID = ''
PASSWORD = ''

def network_use_wlan(is_wlan=True):
    if is_wlan:
        sta=network.WLAN(0)
        sta.connect(SSID, PASSWORD)
        print(sta.status())
        while sta.ifconfig()[0] == '0.0.0.0':
            os.exitpoint()
        print(sta.ifconfig())
        ip = sta.ifconfig()[0]
        return ip
    else:
        a=network.LAN()
        if not a.active():
            raise RuntimeError("LAN interface is not active.")
        a.ifconfig("dhcp")
        print(a.ifconfig())
        ip = a.ifconfig()[0]
        return ip

# HTTP服务
def http_server():
    global img, imgLock, RunCamera, ssCNT, gc_threshold

    # 连接网络,获取IP地址
    IPaddress = network_use_wlan()
    Port = 80
    ai = usocket.getaddrinfo(IPaddress, Port)
    addr = ai[0][-1]
    print(f'\tCreate HTTP server listen at {IPaddress}:{Port}\n\n')
    
    # 创建 HTTP 服务器
    s = usocket.socket()
    s.setsockopt(usocket.SOL_SOCKET, usocket.SO_REUSEADDR, 1)
    s.bind(addr)
    s.listen(5)

    while RunCamera:
        try:
            cl, addr = s.accept()
            print(f"Client connected from {addr}")
            request = cl.recv(1024)
            if not request:
                continue
            request_str = request.decode()
            header = "HTTP/1.1 200 OK\r\n" \
                     "Server: Tao\r\n" \
                     "Content-Type: multipart/x-mixed-replace;boundary=Tao\r\n" \
                     "Cache-Control: no-cache\r\n" \
                     "Pragma: no-cache\r\n\r\n"
            cl.send(header.encode())

            while RunCamera:
                try:
                    if imgLock.acquire(1, 1):  # 申请img变量的锁,阻塞1秒
                        if img is not None:  # 检查img是否为None
                            img_bytes = img.compress(quality=50)
                            imgLock.release()
                            header = f"--Tao\r\nContent-Type: image/jpeg\r\nContent-Length: {len(img_bytes)}\r\n\r\n"
                            cl.send(header.encode())
                            cl.send(img_bytes)

                            del img_bytes
                            if ssCNT % gc_threshold == 0:
                                gc.collect()
                                print(f"Garbage collected at frame {ssCNT}")
                        else:
                            imgLock.release()
                            print('img变量为None')
                    else:
                        print('img变量锁申请超时')
                except Exception as e:
                    if e.errno == 11:
                        print('\t-----Error 11-----')
                        break
                    else:
                        print(f"\n\tHTTP错误:{e}")
                        break
                utime.sleep_ms(100)  # 限制帧率不超过10,给其它线程留下CPU时间
                ssCNT += 1  # 帧计数
        except Exception as e:
            print(f"\n\t建立HTTP服务时出错:{e}")
            cl.close()
            s.close()
            ap.active(False)
            utime.sleep(5)  # 等5秒,重启WiFi
            s = usocket.socket(usocket.AF_INET, usocket.SOCK_STREAM)
            s.bind(addr)
            s.listen(5)
        finally:
            cl.close()
            utime.sleep(1)  # 等1秒,确保所有数据都已发送完毕

# 拍摄
def th_Camera():
    global img, imgLock, RunCamera, ssCNT

    cam = Sensor()  # 默认摄像头2,一共支持3个摄像头
    cam.reset()
    cam.set_framesize(width=640, height=360, chn=CAM_CHN_ID_0)  # 每个摄像头有3个通道,我们使用ch0
    cam.set_pixformat(Sensor.RGB565, chn=CAM_CHN_ID_0)  # 目前还不支持rgb888转换到jpg格式

    Display.init(Display.ST7701, to_ide=True, osd_num=2)
    MediaManager.init()
    cam.run()

    clock = time.clock()
    fps = 0

    while RunCamera:
        clock.tick()
        if imgLock.acquire(1, 1):  # 申请变量img的锁,阻塞1秒
            del img
            gc.collect()
            img = cam.snapshot()
            # 只显示帧数,不显示FPS
            img.draw_string_advanced(5, 5, 36, f'{ssCNT}', color=(255, 0, 0))
            imgLock.release()
        utime.sleep_ms(50)  # 为保证数据及时刷新,2倍于推送帧率
        fps = clock.fps()
        # 在控制台打印FPS
        print(f'当前FPS: {fps:.1f}')
    cam.stop()
    utime.sleep_ms(100)
    MediaManager.deinit()

# 显示
def th_Display():
    global img, imgLock, RunCamera

    while img == None and RunCamera == True:  # 死等摄像头启动后赋值给img
        utime.sleep_ms(100)

    while RunCamera:
        if imgLock.acquire(0, 0):  # 申请变量img的锁,无阻塞
            display = Display()
            display.show_image(img, x=80, y=60)  # 显示图片
            imgLock.release()
        utime.sleep_ms(100)

if __name__ == "__main__":
    RunCamera = True  # 线程退出条件,比如按Key键3秒后修改此值即可关闭程序
    imgLock = _thread.allocate_lock()  # 线程锁实例

    _thread.start_new_thread(th_Camera, ())   # 摄像头线程
    _thread.start_new_thread(th_Display, ())  # 显示线程
    _thread.start_new_thread(http_server, ()) # 推流线程

    while RunCamera:
        utime.sleep(1)

是否意味着WiFi-AP模式存在稳定性问题?

已经反馈了