本文介绍如何在 Linux C++ 项目中集成 ARTC SDK,快速实现一个简单的实时音视频互动程序,适用于视频会议、互动直播、云端录制等服务端场景。
功能简介
在开始之前,了解以下几个关键概念会很有帮助:
ARTC SDK:这是阿里云的实时音视频产品,帮助开发者快速实现实时音视频互动的SDK。
GRTN:阿里云全球实时传输网络,提供超低延时、高音质、安全可靠的音视频通讯服务。
频道:相当于一个虚拟的房间,所有加入同一频道的用户都可以进行实时音视频互动。
主播:可在频道内发布音视频流,并可订阅其他主播发布的音视频流。
观众:可在频道内订阅音视频流,不能发布音视频流。
实现实时音视频互动的基本流程如下:
用户需要调用
setChannelProfile(设置频道场景),后调用joinChannel加入频道:视频通话场景:所有用户都是主播角色,可以进行推流和拉流。
互动直播场景:需要调用
setClientRole(设置角色),在频道内推流的用户设置主播角色;如果用户只需要拉流,不需要推流,则设置观众角色。
加入频道后,不同角色的用户有不同的推拉流行为:
所有加入频道内的用户都可以接收频道内的音视频流。
主播角色可以在频道内推音视频流。
观众如果需要推流,需要调用
setClientRole方法,将用户角色切换成主播,便可以推流。
示例项目
可以前往SDK下载获取最新的Linux SDK并解压获取示例项目。
前提条件
在运行示例项目之前,请确保开发环境满足以下要求:
实现音视频通话
导入SDK
解压Linux SDK压缩包,打开解压后的文件夹里面的Cpp文件夹,交付产物如下:
├── Release
│ ├── include ##此目录包含需要引入的头文件
│ │ ├── AliRTCEngineCentralInterface.h ##多进程版本使用
│ │ ├── AliRTCSdkDefineCentral.h ##多进程版本使用
│ │ ├── AliRTCEngineInterface.h ##单进程版本使用
│ │ ├── AliRTCLinuxSdkDefine.h ##单进程版本使用
│ │ ├── AliRTCMediaPlayerInterface.h
│ │ └── IAliRTCEngine.h
│ └── lib ##此目录包含需要链接的SDK动态库
│ ├── AliRtcCoreService ##多进程版本使用
│ ├── libAliRtcCentralEngine.so ##多进程版本使用
│ ├── libAliRtcLinuxEngine.so ##单进程版本使用
│ └── libonnxruntime.so.1.16.3
└── Demo ##简易例程
├── CMakeLists.txt
├── fake_linux_event_listener.cc ##包含音、视频拉流的回调逻辑
├── fake_linux_event_listener.h
└── simple_main.cc ##demo主体,包含初始化、入会、推流等操作说明:
Release目录包含需要引入的头文件(include)、需要链接的SDK动态库(lib)。API与数据结构均在include目录中的各头文件中声明,要求配置正确的动态库链接地址,如:export LD_LIBRARY_PATH=./lib
Demo目录提供简易例程,其中simple_main.cc是demo主体,包含初始化、入会、推流等操作,fake_linux_event_listener包含音、视频拉流的回调逻辑。
在您的项目CMakeLists.txt 中添加头文件路径和动态库链接:
include_directories(path/to/Release/include)
link_directories(path/to/Release/lib)
target_link_libraries(your_target AliRtcCentralEngine)运行前设置动态库搜索路径:
export LD_LIBRARY_PATH=/path/to/Release/lib:$LD_LIBRARY_PATH实现事件回调类
继承 EngineEventHandlerInterface 实现事件回调,用于接收 SDK 推送的各类通知:
#include "AliRTCEngineCentralInterface.h"
class VideoCallEventHandler : public AliRTCSdk::Central::EngineEventHandlerInterface {
public:
// 加入频道结果回调
void OnJoinChannelResult(int result, const char* channel, const char* userId) override {
if (result == 0) {
fprintf(stdout, "[OnJoinChannelResult] User %s joined channel %s successfully\n",
userId, channel);
} else {
fprintf(stdout, "[OnJoinChannelResult] Failed to join, error: %d\n", result);
}
}
// 远端用户上线通知
void OnRemoteUserOnLineNotify(const char* uid) override {
fprintf(stdout, "[OnRemoteUserOnLineNotify] uid: %s\n", uid);
}
// 远端用户音视频轨道变化通知(在此处理远端流订阅逻辑)
void OnRemoteTrackAvailableNotify(const char* uid,
AliRTCSdk::Central::AudioTrack audioTrack,
AliRTCSdk::Central::VideoTrack videoTrack) override {
fprintf(stdout, "[OnRemoteTrackAvailableNotify] uid: %s, audio: %d, video: %d\n",
uid, (int)audioTrack, (int)videoTrack);
}
// 接收远端混合音频 PCM 帧(subscribeAudioFormat = AudioFormatMixedPcm 时触发)
void OnSubscribeMixAudioFrame(const AliRTCSdk::Central::AudioFrame* frame) override {
// 在此处理接收到的音频数据,例如写入文件或送入播放设备
}
// 接收远端视频 YUV 帧(subscribeVideoFormat = VideoFormatYUV 时触发)
void OnRemoteVideoSample(const char* uid, const AliRTCSdk::Central::VideoFrame* frame) override {
// 在此处理接收到的视频数据,例如写入文件或送入显示设备
}
// TODO: 务必处理;发生不可恢复错误时,建议业务重新加入频道
void OnError(AliRTCSdk::Central::ERROR_CODE error_code) override {
fprintf(stdout, "[OnError] error_code: 0x%X\n", error_code);
}
};创建并初始化引擎
调用 CreateAliRTCEngine 创建引擎实例,并将事件回调对象注册到引擎。后面只需要调用AliRTCEngine实例的各方法完成推拉流设置
如果AliRTCEngine实例创建失败,需要销毁EventHandler实例
说明:每创建一个AliRTCEngine,将会启动一个进程对应一个虚拟用户
VideoCallEventHandler* eventHandler = new VideoCallEventHandler();
// extra: disable audio ranking to receive all remote audio streams
std::string extra = "{\"user_specified_disable_audio_ranking\":\"true\"}";
AliRTCSdk::Central::AliRTCEngineInterface* engine = AliRTCSdk::Central::CreateAliRTCEngine(
eventHandler,
42000, 45000, // IPC port range for AliRtcCoreService
"/tmp", // Log file directory
nullptr, // AliRtcCoreService path; nullptr = same dir as executable
false, // h5mode: set true for Web interoperability
extra.c_str()
);
if (!engine) {
fprintf(stderr, "Failed to create RTC engine\n");
delete eventHandler;
return -1;
}CreateAliRTCEngine函数的参数如下:
EngineEventHandlerInterface * eventHandler:回调对象,负责处理回调逻辑int lowPort:端口号下限,lowPort~highPort之间的端口将被随机选择用于进程间通信int highPort:端口号上限const char * logPath:SDK运行过程中,日志文件的保存路径const char * coreServicePath:AliRtcCoreService的实际路径bool h5mode:h5兼容模式,一般false即可const char * extra:对SDK进行额外配置时传入的JSON格式字符串
设置音视频属性
调用 SetClientRole 设置用户角色,调用 SetVideoEncoderConfiguration 配置视频编码参数。
// 调用 SetClientRole 设置用户角色为互动模式(主播),可同时发布和订阅
engine->SetClientRole(AliRTCSdk::Central::AliEngineClientRoleInteractive);
// 调用 SetVideoEncoderConfiguration 设置视频编码参数
AliRTCSdk::Central::AliEngineVideoEncoderConfiguration videoConfig;
videoConfig.dimensions.width = 720;
videoConfig.dimensions.height = 1280;
videoConfig.frameRate = (AliRTCSdk::Central::AliEngineFrameRate)15;
videoConfig.bitrate = 1200;
engine->SetVideoEncoderConfiguration(videoConfig);设置推拉流属性
配置音视频的发布和订阅行为,并启用 外部音视频源模式。
// 调用 PublishLocalVideoStream / PublishLocalAudioStream 开启本地音视频发布
engine->PublishLocalVideoStream(true);
engine->PublishLocalAudioStream(true);
// Linux 无内置摄像头/麦克风,调用 SetExternalVideoSource 启用外部视频源,
// 通过 PushExternalVideoFrame 输入 YUV 帧数据
engine->SetExternalVideoSource(true, AliRTCSdk::Central::VideoSourceCamera,
AliRTCSdk::Central::RenderModeAuto);
// 调用 SetExternalAudioSource 启用外部音频源,通过 PushExternalAudioFrameRawData 输入 PCM 帧数据
engine->SetExternalAudioSource(true, 16000 /* sample rate */, 1 /* channels */);
配置入会时的订阅模式(在加入频道 JoinChannelConfig 中设置):
AliRTCSdk::Central::JoinChannelConfig joinConfig;
joinConfig.channelProfile = AliRTCSdk::Central::ChannelProfileInteractiveLive;
joinConfig.publishMode = AliRTCSdk::Central::PublishAutomatically; // 自动推流
joinConfig.subscribeMode = AliRTCSdk::Central::SubscribeAutomatically; // 自动订阅
joinConfig.subscribeVideoFormat = AliRTCSdk::Central::VideoFormatYUV; // 接收 YUV 视频帧,触发 OnRemoteVideoSample
// 音频订阅格式有两种选择:
// AudioFormatMixedPcm: 接收混音后的整频道 PCM,触发 OnSubscribeMixAudioFrame
// AudioFormatPcmBeforMixing: 接收每个远端用户的逐路 PCM,触发 OnSubscribeAudioFrame(含 uid)
joinConfig.subscribeAudioFormat = AliRTCSdk::Central::AudioFormatMixedPcm;加入频道
请先了解鉴权功能,详见Token鉴权。
说明
入会存在两个版本的接口,其中单参数入会是在多参数入会的基础上做的语法糖,剥离了业务方传入nonce、timestamp以入会的必要性,在直接获取token的场景下更易于使用。
JoinChannel提供两个重载版本,推荐优先使用单参数入会接口。
单参数入会(推荐)
将步骤三生成的 Base64 Token 直接传入,无需手动构造 AuthInfo,适用于从业务服务端获取 Token 的常见场景。
const char* token = token.c_str(); // 步骤三生成的 Base64 Token
const char* channel = "your_channel_id";
const char* userid = "your_user_id";
const char* username = "your_user_id";
// 调用 JoinChannel 单参数重载加入频道
engine->JoinChannel(token, channel, userid, username, joinConfig);
多参数入会
由业务方直接填写 AuthInfo 各字段(含 nonce、timestamp、gslb 等),适用于需要精确控制鉴权参数的场景。单参数入会是在此接口基础上封装的语法糖。
AliRTCSdk::Central::AuthInfo authInfo;
authInfo.appid = "your_app_id";
authInfo.channel = "your_channel_id";
authInfo.userid = "your_user_id";
authInfo.username = "your_user_id";
authInfo.nonce = "";
authInfo.token = ""; // 调用 CreateMultiParameterToken 生成的 SHA-256 哈希值
authInfo.timestamp = ARTCTokenHelper::GetExpirationTimestamp();
// 调用 JoinChannel 多参数重载加入频道
engine->JoinChannel(authInfo, joinConfig);
请勿重复调用 JoinChannel。Demo 中的 Token 生成接口仅供开发和测试使用,生产环境中请通过业务服务端获取 Token,避免 AppKey 泄漏。
推送外部视频帧
Linux 平台没有内置摄像头驱动接口,通过外部输入PushExternalVideoFrame将 YUV 视频数据送入 SDK。以下示例从 I420 格式 YUV 文件循环读取帧数据推送,实际业务中可替换为摄像头驱动或视频解码器输出。
```cpp
std::thread videoThread([&]() {
const int width = 720;
const int height = 1280;
const int fps = 15;
const size_t frameSize = width * height * 3 / 2; // I420
FILE* fin = fopen("/tmp/test_720p.yuv", "rb");
void* buf = malloc(frameSize);
int frameCount = 0;
int64_t startTime = currentTimeMs();
while (running) {
size_t readSize = fread(buf, 1, frameSize, fin);
if (readSize != frameSize) {
fseek(fin, 0, SEEK_SET); // loop when file ends
continue;
}
AliRTCSdk::Central::VideoDataSample sample;
sample.data = (unsigned char*)buf;
sample.format = AliRTCSdk::Central::VideoDataFormatI420;
sample.width = width;
sample.height = height;
sample.strideY = width;
sample.strideU = width / 2;
sample.strideV = width / 2;
sample.dataLen = frameSize;
sample.timeStamp = frameCount * 1000 / fps;
sample.rotation = 0;
int ret = engine->PushExternalVideoFrame(&sample, AliRTCSdk::Central::VideoSourceCamera);
if (ret == 0x01070101) {
// SDK buffer full, rewind and retry after a short delay
long pos = ftell(fin);
fseek(fin, pos - (long)readSize, SEEK_SET);
usleep(100000);
continue;
}
frameCount++;
int64_t sleepTime = (int64_t)frameCount * 1000 / fps - (currentTimeMs() - startTime) - 1;
if (sleepTime > 0) sleepMs(sleepTime);
}
free(buf);
fclose(fin);
});
```推送外部音频帧
Linux 平台没有内置麦克风录音接口,通过外部输入PushExternalAudioFrameRawData将 PCM 音频数据送入 SDK。以下示例从 PCM 文件(int16_t,16kHz,单声道)循环读取帧数据推送,实际业务中可替换为麦克风驱动或音频解码器输出。
```cpp
std::thread audioThread([&]() {
const int sampleRate = 16000;
const int channels = 1;
const int frameMs = 20; // 20ms per frame
const size_t frameSize = (sampleRate / 1000) * frameMs * sizeof(int16_t) * channels;
FILE* fin = fopen("/tmp/test_16k_mono.pcm", "rb");
void* buf = malloc(frameSize);
int64_t totalSentMs = 0;
int64_t startTime = currentTimeMs();
while (running) {
size_t readSize = fread(buf, 1, frameSize, fin);
if (readSize != frameSize) {
fseek(fin, 0, SEEK_SET); // loop when file ends
continue;
}
int ret = engine->PushExternalAudioFrameRawData(buf, (unsigned int)frameSize, totalSentMs);
if (ret != 0) {
// SDK buffer full, rewind and retry after a short delay
long pos = ftell(fin);
fseek(fin, pos - (long)readSize, SEEK_SET);
usleep(20000);
continue;
}
totalSentMs += frameMs;
int64_t sleepTime = totalSentMs - (currentTimeMs() - startTime) - 1;
if (sleepTime > 0) sleepMs(sleepTime);
}
free(buf);
fclose(fin);
});
```处理音视频播放
Linux 平台没有内置音视频播放设备,远端音视频数据通过回调帧的方式交给应用层自行处理,例如写入文件、送入解码器或对接播放设备。
音频播放
根据步骤六中 subscribeAudioFormat 的配置,收到远端音频帧时会触发以下回调之一:
// AudioFormatMixedPcm 模式:接收所有远端用户混音后的整频道 PCM 数据
void OnSubscribeMixAudioFrame(const AliRTCSdk::Central::AudioFrame* frame) override {
// frame->data PCM 数据指针(int16_t)
// frame->dataSize 数据字节数
// frame->sampleRate / frame->channel 采样率与声道数
// 在此写入文件、送入音频设备或解码播放
}
// AudioFormatPcmBeforMixing 模式:按用户接收未混音的逐路 PCM 数据
void OnSubscribeAudioFrame(const std::string& uid,
const AliRTCSdk::Central::AudioFrame* frame) override {
// uid 标识该帧来自哪个远端用户
// 在此按用户分别处理音频数据
}
视频播放
收到远端视频帧时触发 OnRemoteVideoSample 回调,帧格式由步骤六中 subscribeVideoFormat 决定:
void OnRemoteVideoSample(const char* uid,
const AliRTCSdk::Central::VideoFrame* frame) override {
// uid 标识该帧来自哪个远端用户
// frame->data YUV 数据指针(I420 格式)
// frame->width / frame->height 分辨率
// 在此写入文件、送入渲染器或视频编码器
}
离开频道并销毁引擎
正确释放资源,依次停止推流、离会、销毁引擎。
// Stop external push threads first
running = false;
videoThread.join();
audioThread.join();
// 调用 PublishLocalVideoStream(false) / PublishLocalAudioStream(false) 停止发布
engine->PublishLocalVideoStream(false);
engine->PublishLocalAudioStream(false);
// 调用 LeaveChannel 离开频道
engine->LeaveChannel();
// 调用 Release 销毁引擎(须在 LeaveChannel 之后调用)
engine->Release();
engine = nullptr;
delete eventHandler;
eventHandler = nullptr;
常见问题
Q:运行时提示找不到动态库
error while loading shared libraries: libAliRtcLinuxEngine.so: cannot open shared object file解决:执行 export LD_LIBRARY_PATH=/path/to/Release/lib:$LD_LIBRARY_PATH。
Q:引擎创建失败(CreateAliRTCEngine 返回 nullptr)
可能原因:
AliRtcCoreService路径不正确,确保该文件与可执行文件在同一目录,或在CreateAliRTCEngine中传入正确的绝对路径。端口范围(
lowPort~highPort)被占用,可尝试更换端口范围。
Q:加入频道失败(OnJoinChannelResult 返回非 0)
可能原因:
AppID 或 Token 不正确,检查 AppID 和生成 Token 所用的 AppKey 是否匹配。
网络不通,检查服务器出口网络是否正常。
Token 已过期,
authInfo.timestamp应设置为未来某个时间点的 Unix 时间戳。
Q:推视频帧时返回 0x01070101
这是 SDK 缓冲区满的信号,需回退文件指针,等待约 100ms 后重试推送当前帧。