Golang

更新时间:
复制 MD 格式

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

功能简介

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

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

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

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

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

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

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

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

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

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

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

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

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

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

示例项目

SDK 包中提供了示例程序:

示例

文件

说明

功能演示

demo.go

包含完整推拉流、Token 生成等功能演示

运行示例:

cd Go
go run demo.go

前置条件

  • 操作系统:Linux(内核 2.6+)

  • Go 版本:Go 1.16+

  • 网络环境:稳定的互联网连接

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

实现步骤

步骤一:导入 SDK

SDK 包目录结构如下:

AliRTCSDK_Linux/
└── Go/
    ├── alirtc/
    │   ├── AliRTCEngine.go             # Go 接口封装
    │   ├── AliRTCEngineImpl.go         # 接口实现
    │   ├── AliRTCLinuxSdkDefine.go     # 数据结构与枚举定义
    │   └── lib/
    │       ├── AliRtcCoreService       # 后台服务进程(须指定绝对路径)
    │       └── libAliRtcLinuxEngine.so # SDK 完整动态库
    ├── demo.go                         # 功能演示示例
    └── go.mod

在您的 Go 文件中导入 SDK:

import "alirtc"

配置 Go Modules(如遇 module 问题):

cd alirtc && go mod init alirtc && cd ..
go mod init your_project
go mod edit -replace alirtc=./alirtc
go mod tidy

运行前设置动态库搜索路径:

export LD_LIBRARY_PATH=/path/to/Go/alirtc/lib:$LD_LIBRARY_PATH

步骤二:实现事件回调结构体

实现 alirtc.EngineEventHandlerInterface 接口,用于接收 SDK 推送的各类通知。

package main

import (
    "fmt"
    "alirtc"
)

type VideoCallEventHandler struct{}

func (e *VideoCallEventHandler) OnJoinChannelResult(result int, channel, userId string) {
    if result == 0 {
        fmt.Printf("[OnJoinChannelResult] User %s joined channel %s successfully\n", userId, channel)
    } else {
        fmt.Printf("[OnJoinChannelResult] Failed to join, error: %d\n", result)
    }
}

func (e *VideoCallEventHandler) OnRemoteUserOnLineNotify(uid string) {
    fmt.Printf("[OnRemoteUserOnLineNotify] uid: %s\n", uid)
}

func (e *VideoCallEventHandler) OnRemoteUserOffLineNotify(uid string) {
    fmt.Printf("[OnRemoteUserOffLineNotify] uid: %s\n", uid)
}

func (e *VideoCallEventHandler) OnRemoteTrackAvailableNotify(uid string,
    audioTrack alirtc.AudioTrack, videoTrack alirtc.VideoTrack) {
    fmt.Printf("[OnRemoteTrackAvailableNotify] uid: %s, audio: %d, video: %d\n",
        uid, audioTrack, videoTrack)
}

// OnSubscribeMixAudioFrame: 接收混音后的远端 PCM 音频帧
// 订阅配置中 SubscribeAudioFormat = AudioFormatMixedPcm 时触发
func (e *VideoCallEventHandler) OnSubscribeMixAudioFrame(frame alirtc.AudioFrame) {

    // frame.Pcm.Data        PCM 数据([ ]byte,int16_t 格式)

    // frame.Pcm.Channels    声道数
    // frame.Pcm.SampleRates 采样率
    // 在此写入文件、送入音频设备或解码播放
}

// OnSubscribeAudioFrame: 接收逐路远端用户的未混音 PCM 帧,uid 区分不同远端用户的音频流
// 订阅配置中 SubscribeAudioFormat = AudioFormatPcmBeforMixing 时触发
func (e *VideoCallEventHandler) OnSubscribeAudioFrame(uid string, frame alirtc.AudioFrame) {
    // uid 标识该帧来自哪个远端用户
    // 在此按用户分别处理音频数据
}

// OnRemoteVideoSample: 接收远端视频帧
// uid 区分不同远端用户的视频流
func (e *VideoCallEventHandler) OnRemoteVideoSample(uid string, frame alirtc.VideoFrame) {
    // uid 标识该帧来自哪个远端用户
    // frame.Frame.Width / frame.Frame.Height 分辨率
    // 在此写入文件、送入渲染器或视频解码器
}

func (e *VideoCallEventHandler) OnError(errorCode alirtc.ErrorCode) {
    fmt.Printf("[OnError] error_code: 0x%X\n", errorCode)
}

步骤三:鉴权 Token

Go SDK 内置 GenerateToken 方法,可在客户端直接生成单参数入会 Token。

Token 生成流程:

  1. 拼接字符串:appId + appKey + channelId + userId + nonce + timestamp

  2. SHA-256 哈希得到十六进制字符串

  3. 组装 JSON:{"appid":..., "channelid":..., "userid":..., "nonce":..., "timestamp":..., "token":<sha256>}

  4. Base64 编码

import "time"

authInfo := alirtc.AuthInfo{
    AppID:    "your_app_id",
    Channel:  "your_channel_id",
    UserID:   "your_user_id",
    UserName: "your_user_id",
}

expire := time.Now().Add(24 * time.Hour)
authInfo.Timestamp = int64(expire.Unix()) // 24 小时后过期

appKey := "your_app_key" // 生产环境请勿将 AppKey 暴露在客户端代码中

// 调用 GenerateToken 生成单参数入会 Token(需先创建引擎实例)
authInfo.Token = linuxEngine.GenerateToken(authInfo, appKey)
说明

Token 安全提示:示例中在客户端本地生成 Token,仅适用于开发和测试阶段。生产环境中,Token 必须由您的业务服务端生成并下发,避免将 AppKey 暴露在客户端代码中。

步骤四:创建并初始化音视频引擎

调用 alirtc.CreateAliRTCEngine 创建引擎实例,并传入事件回调对象。

import (
    "encoding/json"
    "fmt"
    "os"
    "alirtc"
)

eventHandler := &VideoCallEventHandler{}

coreServicePath := "/path/to/Go/alirtc/lib/AliRtcCoreService" // 指定 AliRtcCoreService 绝对路径
h5mode := false // 与 Web 端互通请设置为 true

extraJobj := map[string]interface{}{
    "user_specified_disable_audio_ranking": "true",
}
extraBytes, _ := json.Marshal(extraJobj)
extra := string(extraBytes)

linuxEngine := alirtc.CreateAliRTCEngine(
    eventHandler,
    42000, 45000,    // IPC 端口范围,用于与 AliRtcCoreService 进程通信
    "/tmp",          // 日志文件目录
    coreServicePath,
    h5mode,
    extra,
)

if linuxEngine == nil {
    fmt.Fprintln(os.Stderr, "Failed to create RTC engine")
    os.Exit(1)
}
说明

每创建一个引擎实例,SDK 会启动一个对应的 AliRtcCoreService 后台进程(对应一个虚拟用户)。

步骤五:设置音视频属性

调用 SetClientRole 设置用户角色,调用 SetVideoEncoderConfiguration 配置视频编码参数。

// 调用 SetClientRole 设置用户角色为互动模式(主播),可同时发布和订阅
linuxEngine.SetClientRole(alirtc.AliEngineClientRoleInteractive)

// 调用 SetVideoEncoderConfiguration 设置视频编码参数
videoConfigPtr := alirtc.NewAliEngineVideoEncoderConfiguration(
    720, 1280,
    alirtc.AliEngineFrameRateFps15,
    1200, 0,
    alirtc.AliEngineVideoEncoderOrientationModeAdaptive,
    alirtc.AliEngineVideoMirrorModeDisabled,
    alirtc.AliEngineRotationMode_0,
)
videoConfig := *videoConfigPtr
linuxEngine.SetVideoEncoderConfiguration(videoConfig)

步骤六:设置推拉流属性

配置音视频的发布和订阅行为,并启用 Linux 平台特有的外部音视频源模式。

// 调用 PublishLocalVideoStream / PublishLocalAudioStream 开启本地音视频发布
linuxEngine.PublishLocalVideoStream(true)
linuxEngine.PublishLocalAudioStream(true)

// Linux 无内置摄像头/麦克风,调用 SetExternalVideoSource 启用外部视频源,
// 通过 PushExternalVideoFrame 输入 YUV 帧数据
linuxEngine.SetExternalVideoSource(true, alirtc.VideoSourceCamera, alirtc.RenderModeFill)

// 调用 SetExternalAudioSource 启用外部音频源,通过 PushExternalAudioFrameRawData 输入 PCM 帧数据
linuxEngine.SetExternalAudioSource(true, 16000 /* 采样率 */, 1 /* 声道数 */)

配置入会时的订阅模式(在步骤七的 JoinChannelConfig 中设置):

joinConfig := alirtc.NewJoinChannelConfig()
joinConfig.ChannelProfile    = alirtc.ChannelProfileInteractiveLive
joinConfig.PublishMode       = alirtc.PublishAutomatically     // 自动推流
joinConfig.SubscribeMode     = alirtc.SubscribeAutomatically   // 自动订阅
joinConfig.PublishAvsyncMode = alirtc.PublishAvsyncWithPts

// 音频订阅格式有两种选择:
// AudioFormatMixedPcm: 接收混音后的整频道 PCM,触发 OnSubscribeMixAudioFrame
// AudioFormatPcmBeforMixing: 接收每个远端用户的逐路 PCM,触发 OnSubscribeAudioFrame(含 uid)
joinConfig.SubscribeAudioFormat = alirtc.AudioFormatMixedPcm

// 接收 H264 视频帧,触发 OnRemoteVideoSample
joinConfig.SubscribeVideoFormat = alirtc.VideoFormatH264

步骤七:加入频道

调用 JoinChannel 传入步骤三生成的 Token 和频道配置加入频道。

// 调用 JoinChannel 加入频道(单参数入会)
linuxEngine.JoinChannel(authInfo.Token, authInfo.Channel, authInfo.UserID, authInfo.UserName, joinConfig)
说明

请勿重复调用 JoinChannel。GenerateToken 接口仅供开发和测试使用,生产环境中请通过业务服务端获取 Token,避免 AppKey 泄漏。

步骤八:推送外部视频帧

Linux 平台没有内置摄像头驱动接口,通过外部输入将 YUV 视频数据送入 SDK。以下示例从 I420 格式 YUV 文件循环读取帧数据推送,实际业务中可替换为摄像头驱动或视频解码器输出。

import (
    "os"
    "time"
    "alirtc"
)

go func() {
    width, height, fps := 720, 1280, 15
    frameSize := width * height * 3 / 2 // I420

    videoFile, err := os.Open("/tmp/test_720p.yuv")
    if err != nil {
        panic(err)
    }
    defer videoFile.Close()

    vTs := 0
    for running {

        data := make([ ]byte, frameSize)

        n, err := videoFile.Read(data)
        if err != nil || n < frameSize {
            videoFile.Seek(0, 0) // 文件读完后循环重读
            continue
        }

        videoSample := alirtc.VideoDataSample{
            Width:      width,
            Height:     height,
            Format:     alirtc.VideoDataFormatI420,
            BufferType: alirtc.VideoBufferTypeRawData,
            Rotation:   0,
        }
        videoSample.StrideY   = width
        videoSample.StrideU   = width / 2
        videoSample.StrideV   = width / 2
        videoSample.DataLen   = frameSize
        videoSample.Data      = data
        videoSample.TimeStamp = vTs

        linuxEngine.PushExternalVideoFrame(&videoSample, alirtc.VideoSourceCamera)
        vTs += 1000 / fps
        time.Sleep(time.Duration(1000/fps) * time.Millisecond)
    }
}()

步骤九:推送外部音频帧

Linux 平台没有内置麦克风录音接口,通过外部输入将 PCM 音频数据送入 SDK。以下示例从 PCM 文件(int16_t,16kHz,单声道)循环读取帧数据推送,实际业务中可替换为麦克风驱动或音频解码器输出。

go func() {
    sampleRate := 16000
    channels   := 1
    frameMs    := 20 // 每帧 20ms
    frameSize  := (sampleRate / 1000) * frameMs * 2 * channels // int16_t = 2 bytes

    audioFile, err := os.Open("/tmp/test_16k_mono.pcm")
    if err != nil {
        panic(err)
    }
    defer audioFile.Close()

    aTs := 0
    for running {

        data := make([ ]byte, frameSize)

        n, err := audioFile.Read(data)
        if err != nil || n < frameSize {
            audioFile.Seek(0, 0) // 文件读完后循环重读
            continue
        }

        ret := linuxEngine.PushExternalAudioFrameRawData(data, frameSize, int64(aTs))
        if ret != 0 {
            // SDK buffer 已满,回退文件指针并稍后重试
            audioFile.Seek(int64(-n), 1)
            time.Sleep(20 * time.Millisecond)
            continue
        }

        aTs += frameMs
        time.Sleep(time.Duration(frameMs) * time.Millisecond)
    }
}()

步骤十:处理远端音视频播放

Linux 平台没有内置音视频播放设备,远端音视频数据通过回调帧的方式交给应用层自行处理,例如写入文件、送入解码器或对接播放设备。

音频播放

根据步骤六中 SubscribeAudioFormat 的配置,收到远端音频帧时会触发以下回调之一:

// AudioFormatMixedPcm 模式:接收所有远端用户混音后的整频道 PCM 数据
func (e *VideoCallEventHandler) OnSubscribeMixAudioFrame(frame alirtc.AudioFrame) {

    // frame.Pcm.Data        PCM 数据([ ]byte,int16_t 格式)

    // frame.Pcm.Channels    声道数
    // frame.Pcm.SampleRates 采样率
    // 在此写入文件、送入音频设备或解码播放
}

// AudioFormatPcmBeforMixing 模式:按用户接收未混音的逐路 PCM 数据
func (e *VideoCallEventHandler) OnSubscribeAudioFrame(uid string, frame alirtc.AudioFrame) {
    // uid 标识该帧来自哪个远端用户
    // 在此按用户分别处理音频数据
}

视频播放

收到远端视频帧时触发 OnRemoteVideoSample 回调,帧格式由步骤六中 SubscribeVideoFormat 决定:

func (e *VideoCallEventHandler) OnRemoteVideoSample(uid string, frame alirtc.VideoFrame) {
    // uid 标识该帧来自哪个远端用户
    // frame.Frame.Width / frame.Frame.Height 分辨率
    // 在此写入文件、送入渲染器或视频解码器
}

步骤十一:离开频道并销毁引擎

正确释放资源,依次停止推流、离会、销毁引擎。

// 停止外部推流 goroutine
running = false
// 等待 goroutine 退出(可使用 sync.WaitGroup)

// 调用 PublishLocalVideoStream(false) / PublishLocalAudioStream(false) 停止发布
linuxEngine.PublishLocalVideoStream(false)
linuxEngine.PublishLocalAudioStream(false)

// 调用 LeaveChannel 离开频道
linuxEngine.LeaveChannel()

// 等待 OnLeaveChannelResult 回调(stopSignal 置 true)后再销毁引擎
for !stopSignal {
    time.Sleep(time.Second)
}

// 调用 Release 销毁引擎(须在 LeaveChannel 之后调用)
linuxEngine.Release()
linuxEngine = nil

常见问题

Q:运行 go run demo.go 报 module 错误怎么办?

删除当前目录及 alirtc 目录下的 go.mod 文件,依次执行:

cd alirtc && go mod init alirtc && cd ..
go mod init your_project
go mod edit -replace alirtc=./alirtc
go mod tidy
go run demo.go

Q:h5mode 什么时候设置为 true?

只有与 Web 端(H5 页面)互通时才需要开启。纯 Linux 端互通设置为 false 即可。

Q:Token 过期后如何处理?

监听 OnAuthInfoWillExpire 回调(Token 即将过期),重新生成 Token 并调用引擎刷新接口更新凭证,无需重新入会。

监听 OnAuthInfoExpired 回调(Token 已过期),需离开频道并以新 Token 重新入会。

Q:运行时提示找不到动态库

error while loading shared libraries: libAliRtcLinuxEngine.so: cannot open shared object file

解决:执行 export LD_LIBRARY_PATH=/path/to/Go/alirtc/lib:$LD_LIBRARY_PATH