AR 眼镜之-直播-技术方案

目录

📂 前言

直播技术框图

🔱 1. AI 眼镜数据源概述

1.1 硬件分工与音视频采集

1.2 跨芯片传输与无线输出链路

2. 💠 AI 眼镜直播流程概述

3. 🔱 Wi-Fi P2P 连接方案讨论

3.1 相关概念

3.2 P2P/IP/TCP/Socket

3.3 三种 P2P 连接方案

3.3.1 方案一 Phone Auto Connect

3.3.2 方案二 Phone Manual Connect

3.3.3 方案三 Phone GO

3.3.4 方案异同点

3.3.5 WifiP2pConfig 讨论

1)P2pFrequencyHelper

2)setGroupOperatingBand 与 setGroupOperatingFrequency

4. ⚛️ 手机实时预览实现

4.1 视频预览

4.2 音频播放

5. ✅ 手机推流概述

5.1 腾讯直播 SDK 初始化与参数配置

5.2 视频推送

5.3 音频推送

5.4 房间管理、心跳与弹幕交互

📂 小结


📂 前言

        随着可穿戴设备与人工智能技术的深度融合,基于 RTOS(实时操作系统)的轻量化 AI 眼镜正逐渐成为第一视角(POV)实时直播与交互的重要终端,RTOS 平台具备极致的低功耗、快速启动和高实时性优势,但受限于硬件资源与功耗预算,其在音视频编码、复杂网络协议栈处理及长距离无线传输方面面临天然挑战,为突破上述瓶颈,本技术方案设计并实现了一套“AI 眼镜采集 — 手机中枢处理 — 云端分发互动”的三端协同直播架构:

  1. AI 眼镜端(AI Glass):采用低功耗双芯片物理隔离架构,STM32N6 主控挂载 Camera 完成画面采集与 H.264/MJPEG 硬件压缩,物奇 WuQi 芯片处理麦克风阵列数据及 3A 降噪算法(输出 Opus 音频流),通过板载 I2S/UART 高速总线进行跨芯片数据汇聚;

  2. 手机端(Phone):作为中转与控制中枢,基于“蓝牙信令控制 + Wi-Fi P2P 高带宽传输”的双通道握手机制建链,利用“生产者-消费者”异步线程队列与 MediaCodec 硬解码实现本地零拷贝低延迟预览,同时集成腾讯云直播 SDK(V2TXLivePusher)进行横屏码率自适应推流;

  3. 云端(Cloud):基于直播云平台(如腾讯云)完成音视频流的转码、存储与 CDN 全网分发,结合 IM 互动系统将弹幕消息实时反向回传至眼镜端,形成完整的闭环体验。

直播技术框图

        下图为系统的整体技术架构框图,概述了各模块间的分工、协议传输与数据流向。

🔱 1. AI 眼镜数据源概述

        本方案基于 RTOS AI 眼镜的双芯片硬件架构,在进行手机端直播与推流前,眼镜端负责完成音视频的原生采集、前级处理以及跨芯片的同步传输等。

1.1 硬件分工与音视频采集

        眼镜端采用低功耗双芯片架构,音频与视频采集物理隔离:

  • 视频源(STM32N6 主控): 负责挂载 Camera 模块,采集画面并进行硬件压缩,输出 H264 / MJPEG 视频帧。

  • 音频源(物奇 WuQi 芯片): 负责麦克风阵列的数据捕获,经底层 3A 算法(降噪、回声消除)处理后,输出 Opus 音频流。

1.2 跨芯片传输与无线输出链路

        由于音视频挂载在不同芯片上,系统根据不同的无线连接场景,在眼镜内部通过高速物理总线(I2S / UART)进行一级数据汇聚,最终通过 Wi-Fi 或蓝牙(BT)将数据流交付给手机端:

无线模式

眼镜端内部流向与无线输出

Wi-Fi 模式

视频:STM32N6 → Wi-Fi → 手机端

音频:物奇 (Audio) → I2S 总线 → STM32N6 → Wi-Fi → 手机端

BT 蓝牙模式

视频:STM32N6 (Video) → UART 串口 → 物奇 → 蓝牙 → 手机端

音频:物奇 (Audio) → 蓝牙 → 手机端

注:本文主要阐述 Wi-Fi 模式,BT 模式就不在此赘述。

2. 💠 AI 眼镜直播流程概述

        本方案采用 “蓝牙信令控制 + Wi-Fi P2P 高带宽传输” 的双通道架构,同时手机端通过蓝牙动态获取眼镜端的 Socket 接口(IP/Port),实现了无缝的 P2P 直连建链。

  1. 初始化(Bluetooth 交互)

    • 指令触发:用户在眼镜端发起直播,眼镜通过 蓝牙通道 向手机发送开启直播控制指令;

    • 任务初始化:手机蓝牙模块接收指令并创建直播本地任务,同时通过蓝牙指令唤醒眼镜 Wi-Fi,并拉取眼镜 Wi-Fi 的 MAC、IP、Port 信息。

  2. 网络建立与数据传输通道建链(Wi-Fi P2P / Socket)

    • 网络建立:手机蓝牙模块向 Wi-Fi 模块发起连接网络连接;

    • 数据传输通道建链:Wi-Fi 模块完成 P2P 点对点握手 与 Socket 数据通道建链,同时通过蓝牙模块通知眼镜直播开启成功。

  3. 音视频推流与处理(Wi-Fi 高速通道)

    • 推流:眼镜通过 Wi-Fi 通道开始高速推送音视频流;

    • 分发与渲染:手机网络层接收流数据并分发至 App 业务层进行解码、播放以及转推至 CDN。

3. 🔱 Wi-Fi P2P 连接方案讨论

3.1 相关概念

概念

定义

在 AI 眼镜 P2P 场景中的作用

P2P(Peer-to-Peer)

点对点通信方式,设备之间直接建立连接进行数据交换

实现手机与 AI 眼镜直接通信,用于传输音视频和控制数据

Wi-Fi P2P(Wi-Fi Direct)

基于 Wi-Fi 的设备直连技术,无需依赖路由器

提供手机与眼镜之间高速、低延迟的数据传输链路

GO(Group Owner)

Wi-Fi P2P 网络中的管理设备,类似 Wi-Fi 热点角色

创建 P2P 网络,并管理 Client 设备接入

GC(Group Client)

加入 Wi-Fi P2P 网络的设备

连接 GO,获取网络信息后与对端进行通信

SSID

Wi-Fi 网络名称,用于标识无线网络

标识 P2P 网络,例如 DIRECT-P2P-A1B2,用于设备发现和连接

MAC 地址

网络设备的硬件唯一标识

蓝牙发现设备时使用,并用于生成 P2P SSID,例如 DIRECT-P2P-A1B2

Band(频段)

表示 Wi-Fi 工作在哪个频段

决定在哪个频率范围工作,例如 5GHz 或 2.4GHz

Channel(信道)

表示频段下面的多个信道

决定该频段中的具体信道,例如 5GHz Channel 153 或 2.4GHz Channel 6

goIntent(Group Owner Intent)

希望自己成为 GO 的意愿值

取值范围 0~15,数值越大,越希望自己成为 GO,默认值 2 是倾向做Client

IP 地址

网络中设备的唯一地址,用于设备定位

Wi-Fi P2P 建立后,通过 IP 地址找到手机或眼镜设备

Port(端口)

用于区分设备上的不同网络服务

区分视频、音频、控制等不同 Socket 服务

TCP

可靠传输协议,保证数据完整和顺序

用于传输控制指令、配置数据等可靠性要求高的数据

UDP

面向实时传输的协议,延迟较低

用于传输实时音视频数据,降低直播延迟

Socket

应用程序进行网络通信的接口

手机 App 与眼镜通过 Socket 建立连接,实现数据收发

3.2 P2P/IP/TCP/Socket

        咱们一起回顾下,P2P、IP、TCP、Socket等关键技术,在 OSI 七层模型的联系。

        在OSI模型中,物理层和链路层的“Wi-Fi P2P”打通设备直连,网络层的“IP”负责寻址定位,传输层的“TCP”确保数据可靠送达,而会话层的“Socket”则作为它们与应用层之间的通信接口,共同支撑了上层音视频和控制数据的稳定传输。

3.3 三种 P2P 连接方案

        本节的核心内容,是对第 2 节 AI 眼镜直播流程图 Wi-Fi Connect 部分的细化,基于手机 App 与 AI 眼镜之间通过 Wi-Fi P2P 进行直连,目前调研并完成联调实现了三种连接方案:

  1. 眼镜作为 GO,手机自动连接眼镜(Phone Auto Connect)

  2. 眼镜作为 GO,手机手动连接(Phone Manual Connect)

  3. 手机作为 GO,眼镜手动连接(Phone GO)

3.3.1 方案一 Phone Auto Connect

        第 2 节 AI 眼镜直播流程图采用的方案一,手机 App 自动发现眼镜,并主动建立 Wi-Fi P2P 连接,连接成功后通过 Socket 进行音视频数据传输。

  1. 清理与重置环境:

    • 调用 WifiP2pManager.removeGroup 移除已有的历史组网;

    • 内部延迟 300ms,确保旧连接状态彻底清理完成。

  2. 设备搜索与连接:

    • 调用 WifiP2pManager.discoverPeers 开启设备搜索;

    • 收到 WIFI_P2P_PEERS_CHANGED_ACTION 广播后,调用 WifiP2pManager.requestPeers 获取设备列表;

    • 调用 WifiP2pManager.connect(MAC) 向指定的 MAC 地址发起连接请求。

  3. 连接成功与服务启动:

    • 收到 WIFI_P2P_CONNECTION_CHANGED_ACTION 广播,确认 P2P 网络连接成功;

    • 启动客户端通信服务 start Client(IP,Port);

    • 最后返回 P2pConnect Success,完成整个连接。

3.3.2 方案二 Phone Manual Connect

        方案二相比方案一,省去 Discover + MAC 匹配步骤,在已知 SSID/密码/信道时可更快建联,不足的是此方案需要 Android Q+。

        方案二通过预先指定 P2P 网络名称(SSID)、密码及信道参数,跳过了方案一中漫长且不稳定的“设备扫描(discoverPeers)与广播回调(requestPeers)”过程,实现了从配置构建到发起连接(connect)的一步直连,大幅提升了 P2P 建连的响应速度和连接成功率,差异总结如下表:

对比维度

方案一(传统 MAC 模式)

方案二(网络名称直连模式)

关键步骤

discoverPeers -> 监听广播 -> requestPeers -> MAC 连接

传入完整 Wi-Fi 参数 -> 构建包含 SSID/密码的 WifiP2pConfig

连接时延

较长。需要经历 Peer 搜索与设备列表更新的广播等待,受周围无线环境干扰大。

极短。省略了扫描设备和获取 Peer 列表的过程,直接发起 P2P 连接。

稳定性与成功率

容易受阻。若扫描阶段未发现目标设备,后续连接将直接失败。

。只要已知目标 P2P 网络的具体配置参数(SSID/密码/信道),即可跳过搜寻直连。

3.3.3 方案三 Phone GO

        方案三则将GO角色切换为手机,由手机创建 P2P 网络,眼镜作为 Client 主动加入,该方案可降低眼镜侧 Wi-Fi P2P 建组复杂度,便于统一管理网络参数。但由于此方案与前两个方案 AUTO / MANUAL 的差异较大,Phone GO 是改为由手机建立 P2P 组,再 BT 通知眼镜开 WiFi 并加入,于是本方案基于第 2 节 AI 眼镜直播流程图作为对比输出。

        方案三由手机主动创建 Wi-Fi P2P Group(担任 Group Owner,GO),眼镜通过蓝牙获取手机下发的 Wi-Fi 配置参数(SSID、密码、信道等)后,主动开启 Wi-Fi 并连接手机创建的 P2P 网络,手机无需进行设备扫描(discoverPeers)和 Peer 列表获取(requestPeers),而是先完成 GO 创建,再等待眼镜接入,实现稳定、快速的 P2P 建连。

对比维度

方案二(Phone Manual Connect)

方案三(Phone GO)

GO角色

眼镜作为 GO

手机作为 GO

建网方式

手机主动连接眼镜

手机主动创建 Group,等待眼镜连接

是否 discoverPeers

是否 requestPeers

Wi-Fi 参数

眼镜提供 SSID、密码、信道

手机生成 SSID、密码、信道并下发给眼镜

建连流程

手机 connect 到眼镜

手机 createGroup,眼镜主动加入

建连速度

稳定性

适用场景

眼镜长期作为视频源

手机作为直播中心,更便于统一管理网络和后续业务连接

3.3.4 方案异同点

        对于三种 P2P 连接方案,手机在给眼镜发送开启 Wi-Fi 指令时,都是调用 sendWifiOnCmd,通过 isSupport5g 指定是否使用 5G 频段,channel 指定具体信道等,并通过 wifiConnectType 区分连接方案类型。因此,眼镜侧开启 Wi-Fi 的流程保持一致,仅根据不同的连接类型执行对应的建连逻辑。

fun sendWifiOnCmd(
    isSupport5g: Int,
    channel: Int,
    ssid: String,
    password: String,
    wifiConnectType: WifiConnectType
)

        不同的是,各方案的 P2P 建立方式存在明显差异:

  • 方案一(Phone Auto Connect):传统 MAC 连接,手机通过 discoverPeers 搜索周围 P2P 设备,获取 Peer 列表后,再根据目标设备的 MAC 地址调用 connect 发起连接,该方案依赖设备发现及广播回调,建连时延较长,容易受到无线环境影响。

  • 方案二(Phone Manual Connect):眼镜作为 Group Owner(GO),手机提前获取眼镜广播的 SSID、密码及信道等参数,直接构建 WifiP2pConfig 发起连接,省略设备扫描及 Peer 获取过程,连接速度和稳定性均优于方案一。

  • 方案三(Phone GO):手机主动创建 P2P Group 并作为 Group Owner(GO),通过蓝牙将 SSID、密码、信道等网络配置下发给眼镜,眼镜主动连接手机创建的 P2P 网络,手机无需执行设备扫描,仅需等待眼镜加入后建立 Socket 通信,更适合作为直播、音视频等业务的数据中心。

        三种方案本质上均基于 Android Wi-Fi P2P 框架实现,区别主要体现在 Group Owner 的角色分配、P2P 网络建立方式以及网络参数的获取来源,随着方案从一到三演进,逐步减少了对设备扫描和广播回调的依赖,使 P2P 建连流程更加可控,显著降低了建连时延,提高了连接成功率和整体稳定性。

3.3.5 WifiP2pConfig 讨论
1)P2pFrequencyHelper

        在 Phone GO 模式下,WifiP2pManager.createGroup() 需要指定 P2P Group 的工作频率,实际测试发现,不同国家和地区支持的 Wi-Fi 信道存在较大差异(例如欧洲/日本对 5.8GHz 高频段信道有严格监管,而中国常用的 153 信道在部分海外地区可能无法发包),如果固定使用同一信道,部分手机与眼镜组合会出现概率性的 P2P 建连失败。

        因此,引入 P2pFrequencyHelper 统一管理 P2P 工作频率,通过按照 Wi-Fi Country Code -> Network Country -> SIM Country 的多级优先级获取设备所在国家,动态选择合适的 Wi-Fi 信道,再将其转换为 Android 系统所需的 MHz 频率,供 WifiP2pConfig.setGroupOperatingFrequency 使用。

/**
 * Description:    P2P 频率助手类
 * CreateDate:     2026/7/21 10:46
 * Author:         agg
 */
object P2pFrequencyHelper {

    const val FREQ_5G_153_CHINA = 153 // 国内默认:153信道 (5765 MHz)
    const val FREQ_5G_36_GLOBAL = 36 // 境外/全球安全:36信道 (5180 MHz)
    const val FREQ_2G_CHANNEL = 6 // 2.4G 默认信道号:6信道 (2437 MHz)
    private const val TAG = "P2pFrequencyHelper"

    @RequiresApi(Build.VERSION_CODES.Q)
    fun buildWifiP2pConfig(
        context: Context, networkName: String, passphrase: String, channelFrequency: Int
    ): WifiP2pConfig =
        WifiP2pConfig.Builder().setNetworkName(networkName).setPassphrase(passphrase).apply {
            if (getBestCountryCode(context).equals("jp", ignoreCase = true)) {
                // 兼容日本:创建5G P2P群组,按频段自动配置,保证5G也增强信道兼容性
                logI(TAG, "buildWifiP2pConfig Auto 5G for JP country")
                setGroupOperatingBand(WifiP2pConfig.GROUP_OWNER_BAND_5GHZ)
            } else if (channelFrequency > 0) {
                // setGroupOperatingFrequency 设置信道的方案,与setGroupOperatingBand不能同时使用,否则会崩溃
                logI(TAG, "buildWifiP2pConfig setGroupOperatingFrequency=$channelFrequency")
                setGroupOperatingFrequency(channelFrequency)
            } else {
                logI(TAG, "buildWifiP2pConfig default 5G")
                setGroupOperatingBand(WifiP2pConfig.GROUP_OWNER_BAND_5GHZ)
            }
        }.build()

    /**
     * 将 Wi-Fi 信道号转换为频率(MHz)
     * @param channel Wi-Fi 信道号
     * @return 对应的频率(MHz),若信道号不合法则返回 0
     */
    fun channelToFrequency(channel: Int): Int {
        if (channel in 1..13) {
            return 2412 + (channel - 1) * 5
        } else if (channel in 34..165) {
            return 5170 + (channel - 34) * 5
        }
        return 0
    }

    /**
     * 获取适合当前国家/地区的 5G P2P 频段
     */
    fun getP2pOperatingFrequency(context: Context): Int {
        val countryCode = getBestCountryCode(context)

        logI(TAG, "getP2pOperatingFrequency: countryCode=$countryCode")
        return if (countryCode.equals("cn", ignoreCase = true)) {
            // 1、只有明确在国内(CN)时才使用 5765 MHz (153信道)
            FREQ_5G_153_CHINA
//            // 2、日本 (JP):为了规避 W52 户外违规以及 DFS 导致 GO 创建失败的问题,强制使用 2.4GHz 保底——在日本验证不成功
//        } else if (countryCode.equals("jp", ignoreCase = true)) {
//            FREQ_2G_CHANNEL
        } else {
            // 3、其他所有国家以及无法获取国家码时,统一兜底使用 5180 MHz (36信道)
            FREQ_5G_36_GLOBAL
        }
    }

    /**
     * 多层级精准获取国家码
     */
    fun getBestCountryCode(context: Context): String {
        // 1. 优先获取 Wi-Fi 驱动当前的实际国家码(最精准,反映了 802.11d 广播和芯片实时状态)
        val wifiCountry = getWifiCountryCode(context)
        if (wifiCountry.isNotEmpty()) {
            return wifiCountry
        }

        val telephonyManager =
            context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager

        // 2. 其次读取当前基站/网络国家码(反映设备当前物理所在的国家)
        val networkCountry = telephonyManager?.networkCountryIso
        logI(TAG, "getBestCountryCode: networkCountryIso=$networkCountry")
        if (!networkCountry.isNullOrEmpty()) {
            return networkCountry.lowercase()
        }

        // 3. 读取 SIM 卡发行国(判断是否为中国卡)
        val simCountry = telephonyManager?.simCountryIso
        logI(TAG, "3 getBestCountryCode: simCountryIso=$simCountry")
        if (!simCountry.isNullOrEmpty()) {
            return simCountry.lowercase()
        }

        // 4. 无法获取任何网络/SIM卡信息时(如纯 Wi-Fi 平板/无卡/开飞行模式),返回空字符串
        // 外部逻辑会将其判断为非 CN,安全兜底到 5180 MHz
        return ""
    }

    /**
     * 通过反射读取 Android 底层 Wi-Fi 芯片的 CountryCode
     */
    private fun getWifiCountryCode(context: Context): String {
        try {
            val wifiManager =
                context.applicationContext.getSystemService(Context.WIFI_SERVICE) as? WifiManager
            if (wifiManager != null) {
                val method = wifiManager.javaClass.getMethod("getCountryCode")
                val countryCode = method.invoke(wifiManager) as? String
                if (!countryCode.isNullOrEmpty()) {
                    return countryCode.lowercase()
                }
            }
        } catch (_: Exception) {
            // 部分厂商 ROM 封堵了此隐藏 API 或抛出 SecurityException,安全忽略即可
        }
        return ""
    }

}

        P2pFrequencyHelper 的核心优势在于:

  • 默认信道选择:国内默认使用 5GHz Channel 153,日本默认使用 5GHz 按频段自动配置,其他国家默认使用 5GHz Channel 36;若设备不支持 5GHz,则使用 2.4GHz Channel 6

  • 信道转换:将 Wi-Fi Channel 转换为 Android WifiP2pConfig 所需的工作频率,例如 Channel 153 -> 5765 MHz,Channel 36 -> 5180 MHz,Channel 6 -> 2437 MHz。

2)setGroupOperatingBand 与 setGroupOperatingFrequency

        在 API 29 (Android 10) 及以上版本中,Android 官方在 WifiP2pConfig.Builder 中新增了 setGroupOperatingBand(WifiP2pConfig.GROUP_OWNER_BAND_5GHZ) 方法,允许开发者直接指定按频段自动配置创建 5G P2P 群组,不过在实践和演进过程中,这两个 API 有两个坑点需要注意。

  1. 不能同时调用:setGroupOperatingBand(指定粗粒度频段)与 setGroupOperatingFrequency(指定细粒度频率),在 Android 系统框架及底层 wpa_supplicant 中存在逻辑互斥,同时配置了会导致应用崩溃;

  2. 手动动态指定频率(Frequency)优于直接强设频段(Band):因为直接强设 5G Band 存在严重的兼容性缺陷,比如:DFS(雷达信道)避让问题、国产 ROM 如小米的系统级 Bug 等。

4. ⚛️ 手机实时预览实现

        实时预览(Live Preview)是检验端到端延迟与传输稳定性的核心功能,为避免阻塞主线程并保证播放流畅度,手机端采用了 “生产者-消费者” 线程池队列 方案,主要通过 MediaCodec 硬解码 H.264 视频流、并利用 SurfaceView 实现低延迟渲染,使用 AudioTrack 播放 PCM 音频流。

4.1 视频预览

        视频解码使用 Android 系统自带的 MediaCodec 硬解码器(video/avc),采用异步回调 MediaCodec.Callback() 模式管理输入 Buffer。

        在 releaseOutputBuffer(index, true) 中传入 true,MediaCodec 会直接将解码后的 YUV 数据推送到 configure 时绑定的 Surface 上,实现零拷贝(Zero-Copy)渲染,大幅降低 CPU 开销与延迟。

//创建解码器 H264的Type为  AVC
mediaCodec = MediaCodec.createDecoderByType("video/avc")
mediaCodec.setCallback(object : MediaCodec.Callback() {
    override fun onInputBufferAvailable(mediaCodec: MediaCodec, i: Int) {
        inputBufferQueue.put(i)
    }
    override fun onOutputBufferAvailable(
        mediaCodec: MediaCodec,
        i: Int,
        bufferInfo: MediaCodec.BufferInfo
    ) {
        if (state == State.UNINITIALIZED) {
            return
        }
        try {
            mediaCodec.releaseOutputBuffer(i, true)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
    override fun onError(mediaCodec: MediaCodec, e: MediaCodec.CodecException) {
    }
    override fun onOutputFormatChanged(
        mediaCodec: MediaCodec,
        mediaFormat: MediaFormat
    ) {
    }
})
//创建配置
val mediaFormat = MediaFormat.createVideoFormat("video/avc", 1280, 720)
//设置解码预期的帧速率【以帧/秒为单位的视频格式的帧速率的键】
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 25)
//配置绑定mediaFormat和surface
mediaCodec!!.configure(mediaFormat, surface, null, 0)

        由于接收的是网络裸流,VideoRunnable 线程充当消费者,负责从 frameBlockingQueue 中取出 H.264 帧,并控制 25 FPS 的平滑播放节奏:

inner class VideoRunnable : Runnable {
    override fun run() {
        while (threadRunning) {
            if (frameBlockingQueue.isNotEmpty() && inputBufferQueue.isNotEmpty()) {
                val i = inputBufferQueue.poll()
                val data = frameBlockingQueue.poll()
                if (data != null) {
                    val currentTime = System.currentTimeMillis()
                    val sleepTime: Long = FRAME_INTERVAL - (currentTime - lastFrameTime)
                    if (sleepTime > 0) {
                        try {
                            Thread.sleep(sleepTime)
                        } catch (e: InterruptedException) {
                            throw RuntimeException(e)
                        }
                    }
                    if (i != null && state == State.RUNNING) {
                        try {
                            val codecInputBuffer = mediaCodec.getInputBuffer(i)
                            codecInputBuffer!!.clear()
                            codecInputBuffer.put(data)
                            mediaCodec.queueInputBuffer(i, 0, data.size, 1, 0)
                            lastFrameTime = System.currentTimeMillis()
                        } catch (e:Exception) {
                            logE(TAG,"VideoRunnable Exception : ${e.printStackTrace()}")
                        }
                    }
                }
            }
        }
    }
}

4.2 音频播放

        音频解码部分针对特定的压缩格式(如 WQ 压缩格式)进行了解包,还原为 16kHz 单声道 16Bit 的 PCM 裸流,并通过 AudioTrack.MODE_STREAM 模式实时写入音频缓冲区:

private fun initAudioTrack() {
    // AudioTrack 得到播放最小缓冲区的大小
    minBufSize = AudioTrack.getMinBufferSize(
        16000,  // 设置音频数据的采样率
        AudioFormat.CHANNEL_OUT_MONO,  // 设置输出声道CHANNEL_OUT_MONO单声道
        AudioFormat.ENCODING_PCM_16BIT
    ) // 设置音频数据块是8位还是16位,这里设置为16位
    // 实例化播放音频对象
    audioTrack = AudioTrack(
        AudioAttributes.Builder()
            .setUsage(AudioAttributes.USAGE_MEDIA)
            .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
            .build(),
        AudioFormat.Builder().setSampleRate(16000)
            .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
            .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
            .build(),
        minBufSize,
        AudioTrack.MODE_STREAM,
        AudioManager.AUDIO_SESSION_ID_GENERATE
    )
}

inner class AudioRunnable : Runnable {
    override fun run() {
        while (threadRunning) {
            if (audioBlockingQueue.isNotEmpty()) {
                val bytes = audioBlockingQueue.poll()
                if (bytes != null) {
                    audioBuffer.clear()
                    SpeechFormatUtils.unpackWQ(bytes).forEach { pcm ->
                        audioBuffer.write(pcm)
                    }
                    val audioData = audioBuffer.readByteArray()
                    if (state == State.RUNNING) {
                        audioTrack!!.write(audioData, 0, audioData.size)
                    }
                }
            }
        }
    }
}

5. ✅ 手机推流概述

        手机直播推流模块是系统连接眼镜端音视频采集与云端直播平台的核心桥梁,该功能核心是由 LiveClientTask 单例类统一管理,集成了网络通信、硬件解码、音视频数据重组以及腾讯云直播 SDK(V2TXLivePusher)等模块。

        整个推流过程,在数据传输、处理部分与手机实时预览的方案一致,参考第四章节,核心不同在于手机实时预览是本地播放音视频,手机推流是推送音视频给腾讯云直播 SDK。

5.1 腾讯直播 SDK 初始化与参数配置

        推流器使用 V2TXLivePusherImpl 实现,针对眼镜端采集特性,开启了自定义音视频采集模式,并设置了 1280x720 720P 横屏码率自适应编码。

  • 自定义采集开启:调用 enableCustomAudioCapture(true) 和 enableCustomVideoCapture(true) 拦截默认摄像头/麦克风输入;

  • 视频编码参数设置如下

mLivePusher.setVideoQuality(
    V2TXLiveDef.V2TXLiveVideoEncoderParam(V2TXLiveDef.V2TXLiveVideoResolution.V2TXLiveVideoResolution1280x720).apply {
        videoBitrate = 2200
        minVideoBitrate = 1500
        videoResolutionMode = V2TXLiveDef.V2TXLiveVideoResolutionMode.V2TXLiveVideoResolutionModeLandscape
    }
)

5.2 视频推送

        视频帧写入 MediaCodec 后,在 MediaCodec.Callback 的 onOutputBufferAvailable 回调中将输出的 YUV420 数据提取封装为 V2TXLiveVideoFrame 格式推送到 SDK:

val videoFrame = V2TXLiveDef.V2TXLiveVideoFrame().apply {
    pixelFormat = V2TXLiveDef.V2TXLivePixelFormat.V2TXLivePixelFormatI420
    bufferType = V2TXLiveDef.V2TXLiveBufferType.V2TXLiveBufferTypeByteArray
    data = yuvData
    width = 1280
    height = 720
}
mLivePusher.sendCustomVideoFrame(videoFrame)

5.3 音频推送

mLivePusher.sendCustomAudioFrame(V2TXLiveAudioFrame().apply {
    data = audioData
    channel = 1
    sampleRate = 16000
})

5.4 房间管理、心跳与弹幕交互

  • 建房与推流:会话建立后异步调用 LiveRepository.createRoom() 创建房间,成功后获取 pushUrl 并执行 V2TXLivePusher.startPush(pushUrl) 开启推流;

  • 封面抓取与心跳维持:推流启动 5 秒后通过 V2TXLivePusher.snapshot() 截取画面上传作为直播封面,并启动 Handler 定时器(每 4 分钟)发送 LIVE_ROOM_HEART_BEAT 心跳;

  • IM 互动弹幕反馈:通过 V2TIMManager 加入直播群组,监听群成员进入(onMemberEnter)和文本弹幕(onRecvGroupTextMessage),将消息处理截断后通过蓝牙/局域网信道反向发送给眼镜端,实现眼镜端的实时弹幕提醒。

📂 小结

        本方案基于双芯片物理隔离架构,在眼镜端由 STM32N6(Camera)与物奇芯片(Audio)协同完成 H.264/Opus 原生采集、硬件压缩与底层 3A 降噪,同时系统通过 BLE/BT 通道进行信令控制与 Socket 参数协商,配合智能信道适配(P2pFrequencyHelper)建立高带宽 Wi-Fi P2P 直连,克服了 RTOS 设备的网络协议栈与高功耗传输瓶颈。

        手机端引入“生产者-消费者”线程队列进行解耦,利用 MediaCodec 硬解码与 SurfaceView 实现零拷贝低延迟本地预览,同时接入腾讯云直播 SDK(V2TXLivePusher)完成 720P 码率自适应推流,并打通 IM 弹幕反向回传至眼镜端,实现了第一视角(POV)直播的全链路闭环。

        另外,由于本人能力有限,如有错误,敬请批评指正,谢谢。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值