物联网开发平台 Linux & RTOS 设备(视频通话)

这篇将介绍 App 呼叫设备,实现 App 和设备之间的双向通话,包括响应来电(接听/拒绝)和挂断通话。

相关产品
物联网开发平台 产品页,含版本规格、价格与购买入口。

一、前提条件

在接入通话功能前,请你先完成 登录与注册 文档中跑通 tc_iot_login 登录功能。如果你已经完成,可以跳过该操作。

二、步骤 1:注册通话事件回调

登录成功后,调用 tc_iot_av_init 注册 tc_iot_av_observer_s,通话相关的回调如下:

回调 说明
on_call_requested 收到对端来电时触发,携带 contact(对端信息)与 option(通话媒体类型等)。
on_call_timeout 来电无人应答超时时触发。
on_call_hangup 对端挂断通话时触发。

#include "tc_iot_av.h"static void on_call_requested(const tc_iot_contact_s *contact, tc_iot_call_option_t option);static void on_call_timeout(const tc_iot_contact_s *contact);static void on_call_hangup(const tc_iot_contact_s *contact);tc_iot_av_observer_s observer;memset(&observer, 0, sizeof(observer));observer.on_call_requested = on_call_requested;observer.on_call_timeout = on_call_timeout;observer.on_call_hangup = on_call_hangup;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_call_requested 后,根据业务逻辑调用 tc_iot_av_accept 接听,或调用 tc_iot_av_reject 拒绝:

static void on_call_requested(const tc_iot_contact_s *contact, tc_iot_call_option_t option) { printf("[demo] on_call_requested user_id=%s media=%d\\n", contact->user_id, option.media_content); // 示例:直接接听,实际业务可结合按键、APP 联动等方式决定接听或拒绝 tc_iot_error_e rc = tc_iot_av_accept(contact, NULL); if (rc != TC_IOT_ERR_SUCCESS) { printf("[demo] tc_iot_av_accept failed: %d\\n", rc); } // 拒绝来电:tc_iot_av_reject(contact, NULL);}

设备默认接收参数如下:

参数项 默认值
视频编码 H264
音频编码 AAC
分辨率 320×480
帧率 8 fps

客户可根据实际产品需求修改上述参数,调用以下接口进行设置:

 tc_iot_call_experiment_api("set_expected_av_params", "{\\"expected_audio_codec\\":\\"G722\\"," "\\"expected_video_codec\\":\\"MJPEG\\"," "\\"expected_video_rotation\\":0," "\\"max_fps\\":5," "\\"max_video_width\\":240," "\\"max_video_height\\":320}", NULL);

参数说明:

参数名 说明 取值范围 / 可选值
expected_video_codec 期望接收的视频编码格式。 "H264"、"H265"、"MJPEG"
expected_audio_codec 期望接收的音频编码格式。 "AAC"、"OPUS"、"G722"
max_fps 最大接收帧率。 5~10
max_video_width 期望接收的视频宽度。 根据设备实际需求填写。
max_video_height 期望接收的视频高度。 根据设备实际需求填写。
expected_video_rotation 期望微信发送的视频方向。 0:竖屏;1:横屏。

注意:必须在 tc_iot_login 之前调用 tc_iot_call_experiment_api() 接口设置 "set_expected_av_params"。 设备 SDK 登录后会立即上报 expected_av_params 参数,且整个生命周期内仅上报一次。若在登录后调用,设置将不会生效。

四、步骤 3:创建发送通道

己方接听来电成功后,通话即建立,此时根据通话的 media_content 创建对应的发送通道:

接口 说明
tc_iot_create_video_channel(channel_id, quality) 创建视频发送通道,通话场景固定使用通道 0,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 create_call_channels(tc_iot_media_content_e media_content, tc_iot_video_quality_e quality) { if (media_content == TC_IOT_MEDIA_CONTENT_VIDEO || media_content == TC_IOT_MEDIA_CONTENT_AUDIO_VIDEO) { g_video_channel = tc_iot_create_video_channel(0, quality); if (!g_video_channel) { printf("[demo] create video channel failed\\n"); } } if ((media_content == TC_IOT_MEDIA_CONTENT_AUDIO || 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"); } } // 启动本地采集(示意,详见步骤 4/5 推流示例) // start_capture_and_push(…)}

五、步骤 4:推送视频帧

采集到一帧编码后的视频数据后,填充 tc_iot_video_frame 并调用 tc_iot_push_video_frame 推送到视频发送通道:

字段 说明
codec 视频编码格式,TC_IOT_VIDEO_CODEC_H264 或 TC_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); }}

六、步骤 5:推送音频帧

采集到一帧编码后的音频数据后,填充 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); }}

七、步骤 6:挂断通话

通话中主动挂断,调用 tc_iot_av_hangup;对端挂断时会触发 on_call_hangup,此时应停止本地推流并销毁发送通道:

void hangup_call(const char *user_id) { tc_iot_contact_s contact = {0}; contact.user_id = user_id; tc_iot_error_e rc = tc_iot_av_hangup(&contact, NULL); if (rc != TC_IOT_ERR_SUCCESS) { printf("[demo] tc_iot_av_hangup failed: %d\\n", rc); }}static void on_call_hangup(const tc_iot_contact_s *contact) { printf("[demo] on_call_hangup user_id=%s\\n", contact->user_id); // 停止推流并销毁发送通道(同实时监控场景的 on_monitor_end 处理)}

注意:说明:

注意:己方来电未及时处理时会触发 on_call_timeout,处理方式与 on_call_hangup 一致:停止推流并释放资源。

八、完整示例代码

以下示例整合登录、通话事件响应、发起语音/视频呼叫、以及使用模拟数据推送音视频帧的完整流程,用于验证通话链路是否正常。实际接入时,请将模拟数据替换为真实的摄像头/麦克风采集数据。

#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 start_call_streams(tc_iot_media_content_e media_content) { if (media_content == TC_IOT_MEDIA_CONTENT_VIDEO || media_content == TC_IOT_MEDIA_CONTENT_AUDIO_VIDEO) { g_video_channel = tc_iot_create_video_channel(0, TC_IOT_VIDEO_QUALITY_SD); } if (media_content == TC_IOT_MEDIA_CONTENT_AUDIO || media_content == TC_IOT_MEDIA_CONTENT_AUDIO_VIDEO) { 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 stop_call_streams(void) { 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; }}static void on_call_requested(const tc_iot_contact_s *contact, tc_iot_call_option_t option) { printf("[demo] on_call_requested user_id=%s media=%d\\n", contact->user_id, option.media_content); tc_iot_error_e rc = tc_iot_av_accept(contact, NULL); if (rc != TC_IOT_ERR_SUCCESS) { printf("[demo] tc_iot_av_accept failed: %d\\n", rc); return; } start_call_streams(option.media_content);}static void on_call_timeout(const tc_iot_contact_s *contact) { printf("[demo] on_call_timeout user_id=%s\\n", contact->user_id); stop_call_streams();}static void on_call_hangup(const tc_iot_contact_s *contact) { printf("[demo] on_call_hangup user_id=%s\\n", contact->user_id); stop_call_streams();}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; 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_call_requested = on_call_requested; observer.on_call_timeout = on_call_timeout; observer.on_call_hangup = on_call_hangup; 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 incoming call from app (60s)…\\n"); sleep(60); // 释放资源 stop_call_streams(); tc_iot_av_deinit(); tc_iot_logout(); tc_iot_deinit(); printf("[demo] main exit\\n"); return 0;}

九、编译并运行

将上述代码覆盖到快速接入中已创建的项目 main.c,CMakeLists.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_call_requested 及后续推流日志;挂断后观察 on_call_hangup 日志,即代表通话链路验证成功。

十、常见问题

现象 排查建议
APP 发起呼叫后,设备端未收到 on_call_requested。 确认设备已成功登录且 tc_iot_av_init 调用成功;确认 on_call_requested 已正确赋值(未被 memset 清零覆盖)。
tc_iot_av_accept 返回失败。 确认设备已成功登录;确认 contact.user_id 有效;确认当前没有其他通话正在进行。
通话已接通,但对端看不到画面或听不到声音。 确认接通后已根据 media_content 创建对应的发送通道并开始推流;确认视频首帧为 IDR 关键帧。
多次通话后设备资源占用持续增长。 检查 on_call_hangup / on_call_timeout 是否都正确停止了推流并销毁了发送通道,避免通道未释放导致的资源泄漏。

十一、联系我们

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

赞 (0)
上一篇 4天前
下一篇 4天前