C++

更新时间:
复制为 MD 格式

本文介绍如何在 Linux C++ 项目中集成 ARTC SDK,快速实现一个简单的实时音视频互动程序,适用于视频会议、互动直播、云端录制等服务端场景。

功能简介

在开始之前,了解以下几个关键概念会很有帮助:

  • ARTC SDK:这是阿里云的实时音视频产品,帮助开发者快速实现实时音视频互动的SDK。

  • GRTN:阿里云全球实时传输网络,提供超低延时、高音质、安全可靠的音视频通讯服务。

  • 频道:相当于一个虚拟的房间,所有加入同一频道的用户都可以进行实时音视频互动。

  • 主播:可在频道内发布音视频流,并可订阅其他主播发布的音视频流。

  • 观众:可在频道内订阅音视频流,不能发布音视频流。

实现实时音视频互动的基本流程如下:

image
  1. 用户需要调用setChannelProfile(设置频道场景),后调用joinChannel加入频道:

    • 视频通话场景:所有用户都是主播角色,可以进行推流和拉流

    • 互动直播场景:需要调用setClientRole(设置角色),在频道内推流的用户设置主播角色;如果用户只需要拉流,不需要推流,则设置观众角色。

  2. 加入频道后,不同角色的用户有不同的推拉流行为:

    • 所有加入频道内的用户都可以接收频道内的音视频流。

    • 主播角色可以在频道内推音视频流

    • 观众如果需要推流,需要调用setClientRole方法,将用户角色切换成主播,便可以推流。

示例项目

可以前往SDK下载获取最新的Linux SDK并解压获取示例项目。

前提条件

在运行示例项目之前,请确保开发环境满足以下要求:

  • 操作系统:Linux(推荐 Ubuntu 18.04 / CentOS 7 及以上)。

  • 编译器:g++ 4.8 及以上,支持 C++11。

  • 构建工具:CMake 3.8 及以上。

  • 网络环境:稳定的互联网连接。如果配置了防火墙,请参考创建应用

  • 应用准备:获取实时音视频应用的AppIDAppKey,详情请参见创建应用

实现音视频通话

导入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.ccdemo主体,包含初始化、入会、推流等操作,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

可能原因:

  1. AliRtcCoreService 路径不正确,确保该文件与可执行文件在同一目录,或在 CreateAliRTCEngine 中传入正确的绝对路径。

  2. 端口范围(lowPort~highPort)被占用,可尝试更换端口范围。

Q:加入频道失败(OnJoinChannelResult 返回非 0)

可能原因:

  1. AppID 或 Token 不正确,检查 AppID 和生成 Token 所用的 AppKey 是否匹配。

  2. 网络不通,检查服务器出口网络是否正常。

  3. Token 已过期,authInfo.timestamp 应设置为未来某个时间点的 Unix 时间戳。

Q:推视频帧时返回 0x01070101

这是 SDK 缓冲区满的信号,需回退文件指针,等待约 100ms 后重试推送当前帧。