快速开始
此处示例代码均为 C 语言实现。
初始化
- 声明并定义
qn_event.h
中定义的回调接口,用于接收 SDK 的事件通知,开发者可以选择性的声明并定义部分只满足自己场景需求的接口即可,比如声明如下(具体实现请参考 demo 源码:callback.c
):
// join_room 本地加入房间通知
void on_join_result (int error_code, const char* error_msg,
QNRtcUserInfo* user_info_ptr_list[], size_t user_size,
QNRtcTrackInfo* track_info_ptr_list[],
size_t track_size, int reconnect_flag);
void on_leave (int error_code_, const char* error_str_, const char* user_id_);
// 房间状态变化通知
void on_room_state_change(QNRtcRoomState state);
// 本地 tracks 发布结果通知
void on_publish_tracks_result(int error_code, const char* error_msg, QNRtcTrackInfo* tracks[], size_t tracks_count);
// 订阅远端 tracks 的结果通知
void on_subscribe_tracks_result (int error_code, const char* error_msg, QNRtcTrackInfo* tracks[], size_t tracks_count);
// 远端发布 tracks 的通知,收到此通知后,本端便可订阅
void on_remote_add_tracks(QNRtcTrackInfo* tracks[], size_t tracks_count);
// 远端取消发布 tracks 的通知,收到此通知后,本端需取消订阅并释放相应的资源
void on_remote_delete_tracks(char* tracks[], size_t tracks_count);
// 远端用户加入房间的通知
void on_remote_user_join (const char* user_id, const char* user_data);
// 远端用户离开房间的通知
void on_remote_user_leave(const char* user_id, int error_code);
// 视频帧解码回调
void on_decode_video_frame(
const QNRtcEncodedFrame* encoded_frame,
QNRtcDecodedFrame** decoded_frame
);
// 码率自适应回调
void on_encoder_adjust_setting(
int bitrate,
int frame_rate
);
- 初始化 SDK 事件接收及日志配置:
// 回调/事件初始化
QNRtcEvent rtc_event;
memset(&rtc_event, 0, sizeof(rtc_event));
rtc_event.on_join_result = on_join_result;
rtc_event.on_leave = on_leave;
rtc_event.on_room_state_change = on_room_state_change;
rtc_event.on_publish_tracks_result = on_publish_tracks_result;
rtc_event.on_subscribe_tracks_result = on_subscribe_tracks_result;
rtc_event.on_remote_add_tracks = on_remote_add_tracks;
rtc_event.on_remote_delete_tracks = on_remote_delete_tracks;
rtc_event.on_remote_user_join = on_remote_user_join;
rtc_event.on_remote_user_leave = on_remote_user_leave;
rtc_event.on_kickout_result = on_kickout_result;
rtc_event.decode_video_frame = on_decode_video_frame;
rtc_event.on_encoder_adjust_setting = on_encoder_adjust_setting;
qn_rtc_init(&rtc_event, LOG_INFO, "rtc.log");
获取 RoomToken
在进入一个实时音视频房间之前,您需要通过自己七牛云账户的 AK/SK 签算出一个 roomToken。roomToken 是一个包含了一次连麦所需要的主要信息的 token,这些信息包括 七牛的账户标识、连麦的应用 ID(appId)、连麦的房间号 (roomName)、连麦的用户名(userId)、有效期等。这个 token 通过自己七牛账户的私钥进行加密,因为涉及到私钥加密,所以建议您自己搭建一个业务服务来向您的实时音视频应用提供 roomToken。
RoomToken 的签算规则请参阅 RoomToken 签算规则。
加入房间
// join room, if success auto publish audio & video tracks, and auto subscribe remote tracks
qn_rtc_join_room(room_token, "User custom data");
...
> 在 RoomToken 正确的前提下,加入房间的结果由 `on_join_result` 事件通知,如果成功,则自动发布本地音、视频 Tracks。
```c
// join_room 本地加入房间通知
void on_join_result (int error_code, const char* error_msg,
QNRtcUserInfo* user_info_ptr_list[], size_t user_size,
QNRtcTrackInfo* track_info_ptr_list[],
size_t track_size, int reconnect_flag)
{
fprintf(stdout, "%s error code:%d, error_msg:%s, user_size:%lu, track_size:%lu, reconn flag:%s\n",
__FUNCTION__,
error_code,
error_msg,
user_size,
track_size,
reconnect_flag ? "true" : "false"
);
if (error_code != 0) {
fprintf(stderr, "加入房间失败, 错误码:%d, 自动登陆中...\n", error_code);
// auto rejoin
rejoin_room();
return;
}
fprintf(stdout, "加入房间成功!\n");
if (track_size > 0) {
// 由 on_remote_add_tracks 接口通知, 并由其决定是否订阅
on_remote_add_tracks(track_info_ptr_list, track_size);
}
if (!reconnect_flag) {
// 自动发布本地音、视频 Tracks
start_publish();
}
}
发布音/视频
- 发布音视频流需先创建对应的 TrackInfo,可在已成功登录房间的前提下进行发布:
void start_publish()
{
QNRtcTrackInfo* track_info_list[2];
QNRtcTrackInfo audio_track, video_track;
memset(&audio_track, 0, sizeof(audio_track));
memset(&video_track, 0, sizeof(video_track));
int track_count = 0;
audio_track.kind = KIND_AUDIO;
audio_track.tag = "microphone";
audio_track.is_master = 1;
audio_track.is_muted = 0;
audio_track.max_bitrate = 64 * 1024;
track_info_list[track_count] = &audio_track;
track_count++;
video_track.camera_device_id = "";
video_track.kind = KIND_VIDEO;
video_track.tag = "external";
video_track.is_master = 1;
video_track.is_muted = 0;
video_track.width = 540; // only test
video_track.height = 960; // only for test
video_track.max_fps = 30; // only for test
video_track.max_bitrate = 1000 * 1024; // only for test
video_track.source_type = tst_ExternalYUV;
track_info_list[track_count] = &video_track;
track_count++;
int ret = qn_rtc_publish(track_info_list, track_count);
if (ret != 0) {
fprintf(stderr, "%s qn_rtc_publish failed, error code:%d\n", __FUNCTION__, ret);
}
}
- 定时导入视频 H.264 Nalu 数据
由于嵌入式设备资源有限,且不同厂家使用的视频编码器接口互不兼容,故七牛 Linux SDK 不包含内置视频编码器(默认仅包含音频 OPUS 编解码器),所以需要有视频通话需求的用户手动调用外部视频编码器生成 H.264 Nalu 数据并通过 qn_rtc_input_h264_frame
接口导入 SDK。
// 调用 qn_rtc_publish 发布本地 tracks 发布结果的异步回调
void on_publish_tracks_result(int error_code, const char* error_msg, QNRtcTrackInfo* tracks[], size_t tracks_count)
{
if (error_code != 0) {
fprintf(stderr, "发布失败,错误码:%d,错误信息:%s\n", error_code, error_msg);
return;
} else {
fprintf(stdout, "发布成功,Track 数量:%lu\n", tracks_count);
}
// record local tracks id
if (local_tracks_count > 0) {
size_t i = 0;
for(; i < local_tracks_count; i++) {
free(local_tracks_id[i]);
}
free(local_tracks_id);
}
local_tracks_count = tracks_count;
local_tracks_id = malloc(local_tracks_count * sizeof(char*));
size_t i = 0;
for(; i < tracks_count; ++i) {
fprintf(stdout, "%s track id:%s, kind::%s, tag:%s, width:%d, height:%d\n",
__FUNCTION__,
tracks[i]->track_id,
tracks[i]->kind,
tracks[i]->tag,
tracks[i]->width,
tracks[i]->height
);
local_tracks_id[i] = (char*)malloc(strlen(tracks[i]->track_id) + 1);
memset(local_tracks_id[i], 0, strlen(tracks[i]->track_id));
memcpy(local_tracks_id[i], tracks[i]->track_id, strlen(tracks[i]->track_id));
}
// start import h.264 nalus
start_import_thread();
}
start_import_thread()
为定时生成并导入 H.264 数据的线程入口函数。
订阅远端音/视频
- 在远端用户发布媒体流的事件通知
on_remote_add_tracks
中订阅其 TrackId 即可:
// 远端发布 tracks 的通知,收到此通知后,本端便可订阅
void on_remote_add_tracks(QNRtcTrackInfo* tracks[], size_t tracks_count)
{
fprintf(stdout, "远端发布了 Tracks, count:%ld,本端将自动订阅。\n", tracks_count);
// Demo 上演示为自动订阅,用户可根据场景自行决定订阅时机
int ret = qn_rtc_subscribe(tracks, tracks_count);
if (0 != ret) {
fprintf(stderr, "订阅失败! qn_rtc_subscribe failed, return:%d\n", ret);
}
}
至此,设备端与其它端同时进入同一个 APPId 下的 RoomId 便可以进行基本的音视频通话了。