开源:Air8101 硬件平台 RTMP 视频推流实现

近期有行业同行咨询,是否存在基Ar8101的RTMP推流方案,这里明确说明:该方案已开源,可直接参考使用。

Air8101是支持2.4G WIFI6和蓝牙的 WiFi SoC,最大支持 200W 像素的静态图像拍照,支持1024*720分辨率的10寸屏显示,同时兼容DVP/USB摄像头即插即用,支持多路视频采集及RTMP推流功能。

本文将以Air8101核心板与AirCAMERA_1030摄像头配件板为硬件示例,详细演示如何基于LuatOS和合宙音视频平台,快速搭建USB摄像头实时推流应用,为视频上云部署提供技术参考。

一、主要硬件准备:

为确保方案顺利部署,请提前准备以下关键硬件:

Air8101核心板

USB摄像头配件板AirCAMERA_1030
[TYPE-C数据线用于供电及固件烧录调试

Win10及其以上PC电脑

接线方式如下:

Air8101核心板通过Type-C USB口供电(核心板正面的拨码开发拨到5V一端,背面的功耗测试开关拨到OFF一端),

AirCAMERA_1030直插在Air8101核心板上。USB HUB(可选,接入多路摄像头时使用)

AirCAMERA_1030 配件板 + Air8101 核心板,硬件连接示意图:

单路摄像头链接方式

wKgZPGnnGMSAI6xWABzZhTJFwZI146.png

四路摄像头链接方式 如图所示,将四路 USB 摄像头接入 HUB 中,然后将 HUB 通过 USB 口连接到 Air8101 核心板上;

wKgZO2nnGMaAdNPgABwYOurgaKI127.png

二、开源示例

RTMP 的主要特点为低延迟、协议兼容性较好,在直播传输、安防视频监控、远程设备视频巡检等对实时性有要求的物联网应用场景中较为常用。设备可通过 RTMP 协议进行推流,实现音视频数据的实时传输,终端设备如手机、PC 可直接接收并实时播放画面,无需等待文件录制完成后再进行上传。

本示例基于Air8101核心板与AirCAMERA_1030配件板,实现USB摄像头的RTMP实时推流。

2.1 RTMP业务逻辑:

订阅IP_READY信息:确认联网后执行RTMP推流任务;

获取RTMP推流地址:通过http.request()请求从服务器获取推流地址;

初始化摄像头:excamera.open()初始化USB摄像头;

RTMP推流:excamera.rtmp()确保摄像头状态正常,然后rtmpc:start()开始推流逻辑;

监控推流状态:循环监控推流过程,每20秒打印一次内存信息。

处理异常情况:推流异常时发送RECONNECT_RTMP信息,然后根据重连函数进行重连;

释放资源:推流结束时excamera.close()关闭摄像头,释放资源。

核心代码如下,完整demo详见源码仓库最新文件

本文件为USB摄像头RTMP推流功能模块,核心业务逻辑为:

1. USB 摄像头初始化、帧率配置与H264视频编码

2. 通过HTTP请求获取RTMP推流地址

3. 连接RTMP服务器并进行视频推流

-- 引入excamera扩展库
local excamera = require("excamera")

local RTMP_TASK_NAME = "rtmp_app_task" -- RTMP任务名称

-- HTTP请求获取RTMP服务器地址函数
local function rtmp_http_request()
    local get_device_id = netdrv.mac(socket.LWIP_STA) -- Air8101用STA MAC作为设备ID
    log.info("打印设备的ID号", get_device_id)

    local url = "http://video.luatos.com:10030/api-system/deviceVideo/get" .. "/" .. get_device_id
    log.info("打印的URL", url)
    local camera_header = {
        ["Accept-Encoding"] = "identity",
        ["Host"] = "video.luatos.com:10030",
        ["Content-Type"] = "application/json"
    }
    local post_body = {
        deviceAccess = "8",     -- 8 代表接入方式为RTMP
        deviceUser = "admin",   -- 平台录像机设置的设备用户(不是登录用的用户名)
        devicePsd = "Air123456" -- 平台录像机设置的设备密码(不是登录用的密码)
    }
    -- 发送POST请求并等待响应
    local code, headers, body = http.request("POST", url, camera_header, json.encode(post_body)).wait()
    log.info("打印的请求code", code)

    if code ~= 200 then
        log.error("HTTP请求失败")
        return false, nil
    end

    log.info("打印的请求body", body)
    local json_body = json.decode(body)
    if not json_body or json_body.code ~= 200 then
        log.error("请求视频URL失败", json_body and json_body.msg or "未知错误")
        return false, nil
    end

    local rtmp_url = json_body.data.urlList[1]
    log.info("请求得到的RTMP地址", rtmp_url)
    if not rtmp_url then
        log.error("未获取到RTMP地址")
        return false, nil
    end
    return true, rtmp_url
end


local g_s_rtmp_state

-- RTMP状态回调函数
-- 连接过程中,如果连接失败,state状态依次为STATE_IDLE->STATE_DISCONNECTING->STATE_IDLE->STATE_ERROR
-- 已经建立了连接,推流过程中,如果本地调用disconnect接口,state状态依次为STATE_IDLE->STATE_DISCONNECTING->STATE_IDLE
-- 已经建立了连接,推流过程中,如果网络或者服务器出现了异常,或者本地发送数据出现了异常,state状态依次为STATE_IDLE->STATE_DISCONNECTING->STATE_IDLE->STATE_ERROR
local function rtmp_state_callback(state)
    -- 打印RTMP状态变化基础日志
    log.info("rtmp_state_callback state", state)

    -- 根据不同状态执行对应逻辑
    if state == rtmp.STATE_IDLE then
        log.info("空闲状态,可能和推流时效有关,需要等待一段时间,再尝试重连")
        if g_s_rtmp_state==rtmp.STATE_DISCONNECTING then
            sys.sendMsg(RTMP_TASK_NAME, "RTMP_EVENT", "DISCONNECTED")
        end
    elseif state == rtmp.STATE_CONNECTING then
        log.info("正在连接")
    elseif state == rtmp.STATE_HANDSHAKING then
        log.info("握手中")
    elseif state == rtmp.STATE_CONNECTED then
        log.info("已连接")
        sys.sendMsg(RTMP_TASK_NAME, "RTMP_EVENT", "CONNECTED")
    elseif state == rtmp.STATE_PUBLISHING then
        log.info("推流中")
    elseif state == rtmp.STATE_DISCONNECTING then
        log.info("正在断开")
    elseif state == rtmp.STATE_ERROR then
        log.info("错误")
    end
    g_s_rtmp_state = state
end

-- RTMP main task 的任务处理函数
local function rtmp_task()
    local camera_opened, msg, rtmpc, success, rtmp_url

    while true do
        -- 1. 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
        while not socket.adapter(socket.dft()) do
            log.warn("rtmp_task", "wait IP_READY", socket.dft())
            -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
            -- 或者等待1秒超时退出阻塞等待状态;
            -- 注意:此处的1000毫秒超时不要修改的更长;
            -- 因为当使用exnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
            -- 当exnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
            -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
            sys.waitUntil("IP_READY", 1000)
        end

        -- 检测到了IP_READY消息
        log.info("rtmp_task", "recv IP_READY", socket.dft())

        -- 清空此task绑定的消息队列中的未处理的消息
        sys.cleanMsg(RTMP_TASK_NAME)

        -- 2. HTTP请求获取RTMP服务器地址
        success, rtmp_url = rtmp_http_request()
        if not success then
            log.error("获取RTMP地址失败")
            goto EXCEPTION_PROC
        end

        -- 3. 配置摄像头
        camera_opened = excamera.open({
            id = camera.USB,
            sensor_width = 1280,
            sensor_height = 720,
            usb_port = 1
        })

        if not camera_opened then
            log.error("摄像头初始化失败")
            goto EXCEPTION_PROC
        end

        -- 启动摄像头
        log.info("启动摄像头...")
        if not excamera.rtmp() then
            log.error("无法启动摄像头")
            goto EXCEPTION_PROC
        end

        -- 创建RTMP客户端
        rtmpc = rtmp.create(rtmp_url)
        if not rtmpc then
            log.error("rtmp.create", "创建RTMP客户端失败")
            goto EXCEPTION_PROC
        end
        log.info("rtmp.create", "RTMP客户端创建成功")

        -- 设置RTMP状态回调
        rtmpc:setCallback(rtmp_state_callback)

        -- 连接RTMP服务器
        log.info("开始连接RTMP服务器...")
        success = rtmpc:connect()
        if not success then
            log.error("连接RTMP服务器失败")
            goto EXCEPTION_PROC
        end

        -- 推流状态的处理调度逻辑
        while true do
            -- 等待消息
            msg = sys.waitMsg(RTMP_TASK_NAME, "RTMP_EVENT")
            if msg then
                log.info("rtmp_task waitMsg", msg[2], msg[3], msg[4])

                -- 连接成功
                if msg[2] == "CONNECTED" then
                    -- 直接启动推流,不检查返回值
                    log.info("准备开始推流")
                    rtmpc:start()
                    log.info("推流已启动")

            -- 连接失败/连接断开
            elseif msg[2] == "DISCONNECTED" then
                break

            -- 需要主动关闭连接
            -- 用户需要主动关闭rtmp连接时,可以调用sys.sendMsg(RTMP_TASK_NAME, "RTMP_EVENT", "CLOSE")
            elseif msg[2] == "CLOSE" then
                -- 主动断开rtmp client连接
                rtmpc:disconnect()
            end
            end
        end

        -- 出现异常
        ::EXCEPTION_PROC::

        -- 清空此task绑定的消息队列中的未处理的消息
        sys.cleanMsg(RTMP_TASK_NAME)

        -- 5. 关闭推流
        log.info("推流结束,开始释放资源")

        -- 关闭摄像头
        if camera_opened then
            excamera.close()
            log.info("excamera已关闭")
        end

        -- 关闭RTMP客户端
        if rtmpc then
            rtmpc:stop()
            log.info("RTMP推流已停止")
            rtmpc:disconnect()
            log.info("RTMP连接已断开")
            rtmpc:destroy()
            log.info("RTMP客户端已销毁")
        end

        -- 确保所有状态重置
        log.info("所有资源已释放,5秒后重连")

        -- 5秒后跳转到循环体开始位置,自动发起重连
        sys.wait(5000)
    end
end

local function wifi_sta_func(evt, data)
    -- evt 可能的值有: "CONNECTED", "DISCONNECTED"
    -- 当evt=CONNECTED, data是连接的AP的ssid, 字符串类型
    -- 当evt=DISCONNECTED, data断开的原因, 整数类型
    log.info("收到STA事件", evt, data)
    if evt == "DISCONNECTED" then
        sys.sendMsg(RTMP_TASK_NAME, "RTMP_EVENT", "DISCONNECTED")
    end
end

-- 内存检查函数
local function memory_check()
    while true do
        -- 等待20秒
        sys.wait(20000)
        -- 打印系统内存使用信息
        log.info("系统内存使用情况", rtos.meminfo("sys"))
        -- 打印Lua虚拟机内存使用信息
        log.info("Lua虚拟机内存使用情况", rtos.meminfo("lua"))
    end
end

-- wifi的STA相关事件
sys.subscribe("WLAN_STA_INC", wifi_sta_func)

-- 运行这个task的处理函数rtmp_task
sys.taskInitEx(rtmp_task, RTMP_TASK_NAME)

-- 启动内存检查任务
sys.taskInit(memory_check)

2.2 RTMP推流功能核心步骤:

搭建好硬件环境;

修改rtmp_app.lua中的deviceUser、devicePsd参数;

修改netdrv_wifi.lua 中的Wi-Fi账号密码;

打开main.lua文件中require “rtmp_app”,同时注释掉:

require “take_photo_http_post”;

烧录内核固件和相关demo成功后,自动开机运行;

可以看到代码运行结果如下,日志中如果出现以下类似打印则说明RTMP推流成功。

wKgZO2nnEpeAOPY8AAg_ypsiczw520.png

三、合宙音视频平台查看

目前,合宙音视频测试平台已开放安卓系统移动端APP下载和PC端浏览器操作。

本文以PC端为例,支持使用共享账号,在线查看合宙挂测设备,也支持用户使用合宙IoT账号自主添加配置在线设备。

3.1 账号密码

账号密码与您的合宙IoT平台账号可通用

wKgZO2nnEvGAU5SxAAVKQ-7DdLw831.png

3.2 新增设备

参照下图按步点击,注意设备账户密码需与代码中日配置一致。

wKgZO2nnEwKAeHhyAAGv1r_rAAw063.pngwKgZO2nnExCABdL0AAKo87q1g40127.png

3.3 查看推流视频

点击实时视频,查看自己设备的推流情况。

wKgZO2nnE4KAcAaEAACiQexJBR0407.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值