帮你快速理解、总结文档立即下载

设备端接入

最近更新时间:2026-07-30 12:00:37

我的收藏
本文将介绍设备如何实现响应 APP 发起的监控请求推送音频帧与视频帧切换清晰度,帮助您快速了解实时监控的功能。

前提条件

在开始之前,请您先完成 快速接入 文档中跑通 tc_iot_login 登录功能。如果您已经完成,可以跳过该操作。

接入步骤

步骤 1:注册监控事件回调

登录成功后,调用 tc_iot_av_init 注册 tc_iot_av_observer_s,实时监控相关的回调如下:
回调
说明
on_monitor_begin
APP 发起监控查看请求时触发,携带 channel_id(本路监控的通道标识)与 option(媒体类型、清晰度等)。
on_monitor_end
对端结束监控查看时触发,携带 channel_id
on_monitor_switch
APP 在监控查看期间切换清晰度时触发,携带 channel_id 与切换后的 quality(可选实现,详见 步骤 5)。
#include "tc_iot_av.h"

static void on_monitor_begin(int channel_id, tc_iot_call_option_t option);
static void on_monitor_end(int channel_id);
static void on_monitor_switch(int channel_id, tc_iot_video_quality_e quality);

tc_iot_av_observer_s observer;
memset(&observer, 0, sizeof(observer));
observer.on_monitor_begin = on_monitor_begin;
observer.on_monitor_end = on_monitor_end;
observer.on_monitor_switch = on_monitor_switch;

tc_iot_error_e av_rc = tc_iot_av_init(&observer);
if (av_rc != TC_IOT_ERR_SUCCESS) {
printf("tc_iot_av_init failed: %d\\n", av_rc);
}

步骤 2:响应 on_monitor_begin,创建发送通道

根据 option.media_content,在 on_monitor_begin 回调中创建对应的发送通道:
接口
说明
tc_iot_create_video_channel(channel_id, quality)
创建视频发送通道,channel_id 需与回调中的 channel_id 一致,quality 为视频清晰度。
tc_iot_create_audio_channel()
创建音频发送通道,无需传入 channel_id
注意:
设备通常只有一路麦克风采集源,tc_iot_create_audio_channel 建议全局只创建一次;多路视频监控同时在线时,可共享同一路音频发送通道,无需重复创建。
static tc_iot_av_channel_t *g_video_channel = NULL;
static tc_iot_av_channel_t *g_audio_channel = NULL;

static void on_monitor_begin(int channel_id, tc_iot_call_option_t option) {
printf("[demo] on_monitor_begin channel_id=%d media=%d quality=%d\\n",
channel_id, option.media_content, option.video_quality);

if (option.media_content == TC_IOT_MEDIA_CONTENT_VIDEO ||
option.media_content == TC_IOT_MEDIA_CONTENT_AUDIO_VIDEO) {
g_video_channel = tc_iot_create_video_channel(channel_id, option.video_quality);
if (!g_video_channel) {
printf("[demo] create video channel failed, channel_id=%d\\n", channel_id);
}
}

if ((option.media_content == TC_IOT_MEDIA_CONTENT_AUDIO ||
option.media_content == TC_IOT_MEDIA_CONTENT_AUDIO_VIDEO) &&
!g_audio_channel) {
g_audio_channel = tc_iot_create_audio_channel();
if (!g_audio_channel) {
printf("[demo] create audio channel failed\\n");
}
}

// 启动本地采集(示意,详见步骤 3/4 推流示例)
// start_capture_and_push(...)
}

步骤 3:推送视频帧

采集到一帧编码后的视频数据后,填充 tc_iot_video_frame 并调用 tc_iot_push_video_frame 推送到视频发送通道:
字段
说明
codec
视频编码格式,TC_IOT_VIDEO_CODEC_H264TC_IOT_VIDEO_CODEC_H265
type
帧类型,关键帧 TC_IOT_VIDEO_FRAME_TYPE_IDR 或非关键帧 TC_IOT_VIDEO_FRAME_TYPE_P
rotation
画面旋转角度,如 TC_IOT_VIDEO_ROTATION_0
width / height
画面宽高(像素)。
data / data_size
编码后的视频裸流数据指针与长度。
pts_ms
帧时间戳(毫秒)。
说明:
建议推送第一个 IDR 关键帧之后才开始持续推流,以保证对端能正常解码;在 IDR 帧之前推送 P 帧,对端可能无法解码画面。
void push_video_frame_sample(tc_iot_av_channel_t *video_channel,
const uint8_t *encoded_data, size_t encoded_size,
bool is_key_frame) {
if (!video_channel) {
return;
}

tc_iot_video_frame frame;
memset(&frame, 0, sizeof(frame));
frame.codec = TC_IOT_VIDEO_CODEC_H264;
frame.type = is_key_frame ? TC_IOT_VIDEO_FRAME_TYPE_IDR : TC_IOT_VIDEO_FRAME_TYPE_P;
frame.rotation = TC_IOT_VIDEO_ROTATION_0;
frame.width = 480;
frame.height = 320;
frame.data = (uint8_t *)encoded_data;
frame.data_size = encoded_size;
frame.pts_ms = current_time_ms(); // 由业务自行实现,返回当前毫秒时间戳

tc_iot_error_e err = tc_iot_push_video_frame(video_channel, &frame);
if (err != TC_IOT_ERR_SUCCESS) {
printf("[demo] push video frame failed: %d\\n", err);
}
}

步骤 4:推送音频帧

采集到一帧编码后的音频数据后,填充 tc_iot_audio_frame 并调用 tc_iot_push_audio_frame 推送到音频发送通道:
字段
说明
codec
音频编码格式,支持 TC_IOT_AUDIO_CODEC_PCM / AAC / OPUS / G722
sample_rate
采样率,如 TC_IOT_AUDIO_SAMPLE_RATE_16000
channels
声道数,TC_IOT_AUDIO_CHANNEL_MONO(单声道)或 TC_IOT_AUDIO_CHANNEL_STEREO(双声道)。
frame_duration_ms
单帧时长(毫秒),如 20ms 一帧。
data / data_size
编码后的音频裸流数据指针与长度。
pts_ms
帧时间戳(毫秒)。
void push_audio_frame_sample(tc_iot_av_channel_t *audio_channel,
const uint8_t *pcm_data, size_t pcm_size) {
if (!audio_channel) {
return;
}

tc_iot_audio_frame frame;
memset(&frame, 0, sizeof(frame));
frame.codec = TC_IOT_AUDIO_CODEC_PCM;
frame.sample_rate = TC_IOT_AUDIO_SAMPLE_RATE_16000;
frame.channels = TC_IOT_AUDIO_CHANNEL_MONO;
frame.frame_duration_ms = 20;
frame.data = (uint8_t *)pcm_data;
frame.data_size = pcm_size;
frame.pts_ms = current_time_ms(); // 由业务自行实现,返回当前毫秒时间戳

tc_iot_error_e err = tc_iot_push_audio_frame(audio_channel, &frame);
if (err != TC_IOT_ERR_SUCCESS) {
printf("[demo] push audio frame failed: %d\\n", err);
}
}

步骤 5:响应 on_monitor_switch,切换视频清晰度(可选)

APP 端在监控查看期间切换清晰度时,设备端会通过 on_monitor_switch 回调收到通知,携带 channel_id(对应的监控通道)与 quality(切换后的清晰度)。quality 的可选取值(tc_iot_video_quality_e)如下:
取值
说明
TC_IOT_VIDEO_QUALITY_UNKNOWN
未知/未设置。
TC_IOT_VIDEO_QUALITY_LD
流畅(低清晰度)。
TC_IOT_VIDEO_QUALITY_SD
标清。
TC_IOT_VIDEO_QUALITY_HD
高清。
TC_IOT_VIDEO_QUALITY_FHD
超清(全高清)。
视频发送通道创建时已固定清晰度,无法直接修改,因此收到 on_monitor_switch 后需要销毁旧通道、按新清晰度重新创建,后续视频帧继续推送到新通道即可:
static void on_monitor_switch(int channel_id, tc_iot_video_quality_e quality) {
printf("[demo] on_monitor_switch channel_id=%d quality=%d\\n", channel_id, quality);

if (g_video_channel) {
tc_iot_destroy_video_channel(g_video_channel);
g_video_channel = NULL;
}

g_video_channel = tc_iot_create_video_channel(channel_id, quality);
if (!g_video_channel) {
printf("[demo] recreate video channel failed, channel_id=%d\\n", channel_id);
}
// 后续采集到的视频帧继续调用 tc_iot_push_video_frame 推送到新的 g_video_channel
}
注意:
重建通道期间会短暂中断视频推流,建议尽快完成切换,并在重建后尽快推送一个 IDR 关键帧,保证画面能快速恢复。

步骤 6:响应 on_monitor_end,销毁发送通道

对端结束监控查看时触发 on_monitor_end,此时应停止采集并销毁对应的发送通道:
static void on_monitor_end(int channel_id) {
printf("[demo] on_monitor_end channel_id=%d\\n", channel_id);

// 停止本地采集(示意,详见业务实现)
// stop_capture(...)

if (g_video_channel) {
tc_iot_destroy_video_channel(g_video_channel);
g_video_channel = NULL;
}
if (g_audio_channel) {
tc_iot_destroy_audio_channel(g_audio_channel);
g_audio_channel = NULL;
}
}
说明:
单个设备可同时存在多路监控发送通道(对应不同的 channel_id),本文仅演示单路场景;多路并发时建议用数组或列表管理各 channel_id 对应的通道指针与采集资源。

完整示例代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/time.h>
#include "tc_iot.h"
#include "tc_iot_av.h"

static volatile int s_login_done = 0;
static tc_iot_error_e s_login_rc = TC_IOT_ERR_SUCCESS;

static tc_iot_av_channel_t *g_video_channel = NULL;
static tc_iot_av_channel_t *g_audio_channel = NULL;
static volatile bool g_pushing = false;
static pthread_t g_video_thread;
static pthread_t g_audio_thread;

static uint64_t current_time_ms(void) {
struct timeval tv;
gettimeofday(&tv, NULL);
return (uint64_t)tv.tv_sec * 1000 + tv.tv_usec / 1000;
}

// 登录回调:与快速接入一致
static void on_login(tc_iot_error_e error_code, const char *error_message) {
s_login_rc = error_code;
s_login_done = 1;
if (error_code != TC_IOT_ERR_SUCCESS) {
printf("[demo] login failed: %d, msg=%s\\n", error_code,
error_message ? error_message : "");
}
}

// 模拟视频采集:独立线程定时生成占位视频帧(首帧标记为 IDR)并推送。
// 实际接入时替换为真实摄像头采集与 H264/H265 编码逻辑。
static void *video_push_thread_func(void *arg) {
static uint8_t dummy_video[64] = {0};
bool first_frame = true;

while (g_pushing) {
if (g_video_channel) {
tc_iot_video_frame vframe;
memset(&vframe, 0, sizeof(vframe));
vframe.codec = TC_IOT_VIDEO_CODEC_H264;
vframe.type = first_frame ? TC_IOT_VIDEO_FRAME_TYPE_IDR
: TC_IOT_VIDEO_FRAME_TYPE_P;
vframe.rotation = TC_IOT_VIDEO_ROTATION_0;
vframe.width = 480;
vframe.height = 320;
vframe.data = dummy_video;
vframe.data_size = sizeof(dummy_video);
vframe.pts_ms = current_time_ms();
tc_iot_push_video_frame(g_video_channel, &vframe);
first_frame = false;
}
usleep(40 * 1000); // 模拟 25fps
}
return NULL;
}

// 模拟音频采集:独立线程定时生成占位音频帧并推送。
// 实际接入时替换为真实麦克风采集与 PCM/AAC 编码逻辑。
static void *audio_push_thread_func(void *arg) {
static uint8_t dummy_audio[320] = {0}; // 16kHz/mono/20ms 对应的 PCM 采样点数

while (g_pushing) {
if (g_audio_channel) {
tc_iot_audio_frame aframe;
memset(&aframe, 0, sizeof(aframe));
aframe.codec = TC_IOT_AUDIO_CODEC_PCM;
aframe.sample_rate = TC_IOT_AUDIO_SAMPLE_RATE_16000;
aframe.channels = TC_IOT_AUDIO_CHANNEL_MONO;
aframe.frame_duration_ms = 20;
aframe.data = dummy_audio;
aframe.data_size = sizeof(dummy_audio);
aframe.pts_ms = current_time_ms();
tc_iot_push_audio_frame(g_audio_channel, &aframe);
}
usleep(20 * 1000); // 20ms 一帧
}
return NULL;
}

static void on_monitor_begin(int channel_id, tc_iot_call_option_t option) {
printf("[demo] on_monitor_begin channel_id=%d media=%d quality=%d\\n",
channel_id, option.media_content, option.video_quality);

if (option.media_content == TC_IOT_MEDIA_CONTENT_VIDEO ||
option.media_content == TC_IOT_MEDIA_CONTENT_AUDIO_VIDEO) {
g_video_channel = tc_iot_create_video_channel(channel_id, option.video_quality);
}
if ((option.media_content == TC_IOT_MEDIA_CONTENT_AUDIO ||
option.media_content == TC_IOT_MEDIA_CONTENT_AUDIO_VIDEO) &&
!g_audio_channel) {
g_audio_channel = tc_iot_create_audio_channel();
}

if (!g_pushing) {
g_pushing = true;
pthread_create(&g_video_thread, NULL, video_push_thread_func, NULL);
pthread_create(&g_audio_thread, NULL, audio_push_thread_func, NULL);
}
}

static void on_monitor_end(int channel_id) {
printf("[demo] on_monitor_end channel_id=%d\\n", channel_id);

if (g_pushing) {
g_pushing = false;
pthread_join(g_video_thread, NULL);
pthread_join(g_audio_thread, NULL);
}
if (g_video_channel) {
tc_iot_destroy_video_channel(g_video_channel);
g_video_channel = NULL;
}
if (g_audio_channel) {
tc_iot_destroy_audio_channel(g_audio_channel);
g_audio_channel = NULL;
}
}

// 切换清晰度(本文步骤 5 核心内容,可选实现)
static void on_monitor_switch(int channel_id, tc_iot_video_quality_e quality) {
printf("[demo] on_monitor_switch channel_id=%d quality=%d\\n", channel_id, quality);

if (g_video_channel) {
tc_iot_destroy_video_channel(g_video_channel);
g_video_channel = NULL;
}
g_video_channel = tc_iot_create_video_channel(channel_id, quality);
if (!g_video_channel) {
printf("[demo] recreate video channel failed, channel_id=%d\\n", channel_id);
}
}

int main(int argc, char **argv) {
if (argc < 7) {
printf("Usage: %s -p <product_id> -d <device_id> -s <device_secret>\\n", argv[0]);
return 0;
}

const char *product_id = NULL;
const char *device_id = NULL;
const char *device_secret = NULL;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) {
product_id = argv[++i];
} else if (strcmp(argv[i], "-d") == 0 && i + 1 < argc) {
device_id = argv[++i];
} else if (strcmp(argv[i], "-s") == 0 && i + 1 < argc) {
device_secret = argv[++i];
}
}
if (!product_id || !device_id || !device_secret) {
printf("device info error\\n");
return 1;
}

// 初始化与登录:与快速接入一致
tc_iot_config_s config;
memset(&config, 0, sizeof(config));
config.storage_path = "./";
config.log_level = TC_IOT_LOG_LEVEL_INFO;

tc_iot_error_e rc = tc_iot_init(&config);
if (rc != TC_IOT_ERR_SUCCESS) {
printf("tc_iot_init failed: %d\\n", rc);
return 1;
}

tc_iot_device_info_s device_info;
device_info.product_id = product_id;
device_info.device_id = device_id;
device_info.device_secret = device_secret;
device_info.region = "ap-guangzhou";

rc = tc_iot_login(&device_info, on_login);
if (rc != TC_IOT_ERR_SUCCESS) {
printf("tc_iot_login call failed: %d\\n", rc);
tc_iot_deinit();
return 1;
}

int waited_ms = 0;
while (!s_login_done && waited_ms < 5000) {
usleep(100 * 1000);
waited_ms += 100;
}
if (!s_login_done || s_login_rc != TC_IOT_ERR_SUCCESS) {
printf("login timeout or failed\\n");
tc_iot_deinit();
return 1;
}
printf("[demo] login success\\n");

// 注册监控事件回调(本文核心内容)
tc_iot_av_observer_s observer;
memset(&observer, 0, sizeof(observer));
observer.on_monitor_begin = on_monitor_begin;
observer.on_monitor_end = on_monitor_end;
observer.on_monitor_switch = on_monitor_switch;

rc = tc_iot_av_init(&observer);
if (rc != TC_IOT_ERR_SUCCESS) {
printf("tc_iot_av_init failed: %d\\n", rc);
tc_iot_deinit();
return 1;
}

printf("[demo] ready, waiting for monitor request from app/cloud (60s)...\\n");
sleep(60);

// 释放资源
if (g_pushing) {
g_pushing = false;
pthread_join(g_video_thread, NULL);
pthread_join(g_audio_thread, NULL);
}
if (g_video_channel) {
tc_iot_destroy_video_channel(g_video_channel);
}
if (g_audio_channel) {
tc_iot_destroy_audio_channel(g_audio_channel);
}
tc_iot_av_deinit();
tc_iot_logout();
tc_iot_deinit();
printf("[demo] main exit\\n");
return 0;
}

编译并运行

将上述代码覆盖到 快速接入 中已创建的项目 main.cCMakeLists.txt 无需改动,重新编译并运行即可。
1. 重新编译。
cmake --build build -j
2. 运行可执行文件,传入设备三元组。
./build/iot_quickstart_demo -p YOUR_PRODUCT_ID -d YOUR_DEVICE_NAME -s 'YOUR_DEVICE_SECRET'
3. 通过 APP 控制台对该设备发起监控查看请求,观察设备端日志输出 on_monitor_begin 及后续推流日志,即代表监控推流链路验证成功。

常见问题

现象
排查建议
发起监控查看后,设备端未收到 on_monitor_begin
确认设备已成功登录且 tc_iot_av_init 调用成功;确认 on_monitor_begin 回调已正确赋值(未被 memset 清零覆盖)。
APP 端看不到监控画面
确认已在 on_monitor_begin 中正确创建视频发送通道;确认推送的首帧为 IDR 关键帧;确认 tc_iot_push_video_frame 返回值为 TC_IOT_ERR_SUCCESS
APP 端听不到声音
确认 option.media_content 包含音频(TC_IOT_MEDIA_CONTENT_AUDIOTC_IOT_MEDIA_CONTENT_AUDIO_VIDEO);确认已创建音频发送通道且 tc_iot_push_audio_frame 调用成功。
多路监控同时在线时声音异常(重复/杂音)
检查是否为每路监控重复创建了音频发送通道;音频发送通道应全局只创建一路,多路视频共享推送。
tc_iot_push_video_frame / tc_iot_push_audio_frame 返回失败
确认传入的发送通道指针非空且尚未被销毁;确认 on_monitor_end 触发后已停止继续推流。
切换清晰度后,设备端未收到 on_monitor_switch
确认设备已成功登录且 tc_iot_av_init 调用成功;确认 on_monitor_switch 已正确赋值(未被 memset 清零覆盖);确认监控会话已建立(on_monitor_begin 已触发)。
收到 on_monitor_switch 回调但画面清晰度未变化
确认已先销毁旧的视频发送通道再按新 quality 创建;确认推送视频帧时使用的是重建后的新通道指针,而非旧指针。
切换清晰度时画面卡顿或短暂黑屏
通道重建期间存在短暂推流中断,属正常现象;建议重建后尽快推送一个 IDR 关键帧,保证画面能快速恢复。

联系我们

如果您在接入或使用过程中有任何疑问或者建议,欢迎 联系我们 提交反馈。