Java

更新时间:
复制为 MD 格式

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

功能简介

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

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

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

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

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

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

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

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

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

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

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

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

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

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

示例项目

SDK 包中提供了示例程序:

示例

文件

说明

功能演示

Demo/MainTest.java

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

运行示例:

cd Demo
sh run.sh

退出示例:命令行输入 exit

前置条件

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

  • Java 运行时:JDK 8+。

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

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

实现步骤

步骤一:导入 SDK

SDK 包目录结构如下:

AliRTCSDK_Linux/
└── Java/
    ├── libs/
    │   ├── AliRtcCoreService                      # 后台服务进程(须指定绝对路径)
    │   ├── alirtc_linux_java_multiprocess.jar      # Java 层与 C++ 引擎的桥接 JAR
    │   ├── gson-2.11.0.jar                        # JSON 序列化依赖
    │   └── libAliRtcLinuxEngine.so                # SDK 完整动态库
    └── Demo/
        ├── MainTest.java                          # 功能演示示例
        └── run.sh                                 # 编译运行脚本

编译并运行示例:

cd Demo
javac -g -cp ../libs/alirtc_linux_java_multiprocess.jar:../libs/gson-2.11.0.jar \
      -encoding utf-8 MainTest.java
java -cp .:../libs/alirtc_linux_java_multiprocess.jar:../libs/gson-2.11.0.jar MainTest

运行前确保动态库可被找到:

export LD_LIBRARY_PATH=/path/to/Java/libs:$LD_LIBRARY_PATH

步骤二:实现事件回调类

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

import com.alivc.rtc.multiprocess.AliRTCLinuxEngine;
import com.alivc.rtc.multiprocess.AliRTCLinuxEngineListener;

class VideoCallEventHandler implements AliRTCLinuxEngineListener {

    @Override
    public void onJoinChannelResult(int result, String channel, String userId) {
        if (result == 0) {
            System.out.printf("[onJoinChannelResult] User %s joined channel %s successfully%n",
                    userId, channel);
        } else {
            System.out.printf("[onJoinChannelResult] Failed to join, error: %d%n", result);
        }
    }

    @Override
    public void onRemoteUserOnLineNotify(String uid) {
        System.out.printf("[onRemoteUserOnLineNotify] uid: %s%n", uid);
    }

    @Override
    public void onRemoteUserOffLineNotify(String uid) {
        System.out.printf("[onRemoteUserOffLineNotify] uid: %s%n", uid);
    }

    @Override
    public void onRemoteTrackAvailableNotify(String uid,
            AliRTCLinuxEngine.AudioTrack audioTrack,
            AliRTCLinuxEngine.VideoTrack videoTrack) {
        System.out.printf("[onRemoteTrackAvailableNotify] uid: %s, audio: %s, video: %s%n",
                uid, audioTrack, videoTrack);
    }

    // onSubscribeMixedAudioFrame: 接收混音后的远端 PCM 音频帧
    // 订阅配置中 subscribeAudioFormat = AudioFormatMixedPcm 时触发
    @Override
    public void onSubscribeMixedAudioFrame(AliRTCLinuxEngine.AudioFrame frame) {
        if (frame != null && frame.type == AliRTCLinuxEngine.AudioFrameType.AudioFrameRawPcm) {
            AliRTCLinuxEngine.AudioPcmFrame pcmFrame = frame.pcm;

            // pcmFrame.data        PCM 数据(byte[ ],int16_t 格式)

            // pcmFrame.channels    声道数
            // pcmFrame.sampleRates 采样率
            // 在此写入文件、送入音频设备或解码播放
        }
    }

    // onSubscribeAudioFrame: 接收逐路远端用户的未混音 PCM 帧,uid 区分不同远端用户的音频流
    // 订阅配置中 subscribeAudioFormat = AudioFormatPcmBeforeMixing 时触发
    @Override
    public void onSubscribeAudioFrame(String uid, AliRTCLinuxEngine.AudioFrame frame) {
        if (frame != null && frame.type == AliRTCLinuxEngine.AudioFrameType.AudioFrameRawPcm) {
            AliRTCLinuxEngine.AudioPcmFrame pcmFrame = frame.pcm;
            // uid 标识该帧来自哪个远端用户
            // 在此按用户分别处理音频数据
        }
    }

    // onRemoteVideoSample: 接收远端视频帧
    // uid 区分不同远端用户的视频流
    @Override
    public void onRemoteVideoSample(String uid, AliRTCLinuxEngine.VideoFrame frame) {
        if (frame != null && frame.type == AliRTCLinuxEngine.VideoFrameType.VideoFrameH264) {
            AliRTCLinuxEngine.VideoH264Frame h264Frame = frame.h264;
            // uid 标识该帧来自哪个远端用户
            // 在此写入文件、送入渲染器或视频解码器
        }
    }

    @Override
    public void onError(int errorCode) {
        System.out.printf("[onError] error_code: 0x%X%n", errorCode);
    }
}

步骤三:鉴权 Token

Java 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 java.time.Instant;

AliRTCLinuxEngine.AuthInfo authInfo = new AliRTCLinuxEngine.AuthInfo();
authInfo.appid    = "your_app_id";
authInfo.channel  = "your_channel_id";
authInfo.userid   = "your_user_id";
authInfo.username = "your_user_id";
authInfo.nonce    = null;
authInfo.timestamp = Instant.now().getEpochSecond() + 24 * 60 * 60; // 24 小时后过期

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

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

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

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

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

VideoCallEventHandler listener = new VideoCallEventHandler();

String coreServicePath = "/path/to/Java/libs/AliRtcCoreService"; // 指定 AliRtcCoreService 绝对路径
boolean h5mode = false; // 与 Web 端互通请设置为 true

String extra = "{\"user_specified_disable_audio_ranking\":\"true\"}";

AliRTCLinuxEngine engineIns = AliRTCLinuxEngine.createInstance(
    listener,
    42000, 45000,   // IPC 端口范围,用于 Java 层与 AliRtcCoreService 进程通信
    "/tmp",         // 日志文件目录
    coreServicePath,
    h5mode,
    extra
);

if (engineIns == null) {
    System.err.println("Failed to create RTC engine");
    return;
}
说明

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

步骤五:设置音视频属性

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

// 调用 setClientRole 设置用户角色为互动模式(主播),可同时发布和订阅
engineIns.setClientRole(AliRTCLinuxEngine.AliEngineClientRole.AliEngineClientRoleInteractive);

// 调用 setVideoEncoderConfiguration 设置视频编码参数
AliRTCLinuxEngine.AliEngineVideoEncoderConfiguration videoConfig =
        new AliRTCLinuxEngine.AliEngineVideoEncoderConfiguration();
videoConfig.dimensions = new AliRTCLinuxEngine.AliEngineVideoDimensions(720, 1280);
videoConfig.frameRate  = AliRTCLinuxEngine.AliEngineFrameRate.AliEngineFrameRateFps15;
videoConfig.bitrate    = 1200;
engineIns.setVideoEncoderConfiguration(videoConfig);

步骤六:设置推拉流属性

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

// 调用 publishLocalVideoStream / publishLocalAudioStream 开启本地音视频发布
engineIns.publishLocalVideoStream(true);
engineIns.publishLocalAudioStream(true);

// Linux 无内置摄像头/麦克风,调用 setExternalVideoSource 启用外部视频源,
// 通过 pushExternalVideoFrame 输入 YUV 帧数据
engineIns.setExternalVideoSource(true,
        AliRTCLinuxEngine.VideoSource.VideoSourceCamera,
        AliRTCLinuxEngine.RenderMode.RenderModeFill);

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

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

AliRTCLinuxEngine.JoinChannelConfig joinConfig = new AliRTCLinuxEngine.JoinChannelConfig();
joinConfig.channelProfile    = AliRTCLinuxEngine.ChannelProfile.ChannelProfileInteractiveLive;
joinConfig.publishMode       = AliRTCLinuxEngine.PublishMode.PublishAutomatically;     // 自动推流
joinConfig.subscribeMode     = AliRTCLinuxEngine.SubscribeMode.SubscribeAutomatically; // 自动订阅
joinConfig.publishAvsyncMode = AliRTCLinuxEngine.PublishAvsyncMode.PublishAvsyncWithPts;

// 音频订阅格式有两种选择:
// AudioFormatMixedPcm: 接收混音后的整频道 PCM,触发 onSubscribeMixedAudioFrame
// AudioFormatPcmBeforeMixing: 接收每个远端用户的逐路 PCM,触发 onSubscribeAudioFrame(含 uid)
joinConfig.subscribeAudioFormat = AliRTCLinuxEngine.AudioFormat.AudioFormatMixedPcm;

// 接收 H264 视频帧,触发 onRemoteVideoSample
joinConfig.subscribeVideoFormat = AliRTCLinuxEngine.VideoFormat.VideoFormatH264;

步骤七:加入频道

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

// 调用 joinChannel 加入频道
engineIns.joinChannel(authInfo, joinConfig);
说明

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

步骤八:推送外部视频帧

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

Thread videoThread = new Thread(() -> {
    int width     = 720;
    int height    = 1280;
    int fps       = 15;
    int frameSize = width * height * 3 / 2; // I420

    try (FileInputStream fis = new FileInputStream("/tmp/test_720p.yuv")) {

        byte[ ] buf = new byte[frameSize];

        AliRTCLinuxEngine.VideoDataSample sample = new AliRTCLinuxEngine.VideoDataSample();

        while (running && !Thread.currentThread().isInterrupted()) {
            int readSize = fis.read(buf, 0, frameSize);
            if (readSize != frameSize) {
                fis.getChannel().position(0); // 文件读完后循环重读
                continue;
            }

            sample.data       = buf;
            sample.format     = AliRTCLinuxEngine.VideoDataFormat.VideoDataFormatI420;
            sample.bufferType = AliRTCLinuxEngine.VideoBufferType.VideoBufferTypeRawData;
            sample.width      = width;
            sample.height     = height;
            sample.strideY    = width;
            sample.strideU    = width / 2;
            sample.strideV    = width / 2;
            sample.dataLen    = frameSize;
            sample.rotation   = 0;
            sample.timeStamp  = System.currentTimeMillis();

            int ret = engineIns.pushExternalVideoFrame(sample,
                    AliRTCLinuxEngine.VideoSource.VideoSourceCamera);
            if (ret < 0) break;

            Thread.sleep(1000 / fps);
        }
    } catch (IOException | InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});
videoThread.start();

步骤九:推送外部音频帧

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

Thread audioThread = new Thread(() -> {
    int sampleRate = 16000;
    int channels   = 1;
    int frameMs    = 20;  // 每帧 20ms
    int frameSize  = (sampleRate / 1000) * frameMs * 2 * channels; // int16_t = 2 bytes

    try (FileInputStream fis = new FileInputStream("/tmp/test_16k_mono.pcm")) {

        byte[ ] buf = new byte[frameSize];


        while (running && !Thread.currentThread().isInterrupted()) {
            int readSize = fis.read(buf, 0, frameSize);
            if (readSize != frameSize) {
                fis.getChannel().position(0); // 文件读完后循环重读
                continue;
            }

            int ret = engineIns.pushExternalAudioFrameRawData(buf, frameSize, 0);
            if (ret != 0) {
                // SDK buffer 已满,回退文件指针并稍后重试
                fis.getChannel().position(fis.getChannel().position() - readSize);
                Thread.sleep(20);
                continue;
            }

            Thread.sleep(frameMs);
        }
    } catch (IOException | InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});
audioThread.start();

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

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

音频播放

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

// AudioFormatMixedPcm 模式:接收所有远端用户混音后的整频道 PCM 数据
@Override
public void onSubscribeMixedAudioFrame(AliRTCLinuxEngine.AudioFrame frame) {
    if (frame != null && frame.type == AliRTCLinuxEngine.AudioFrameType.AudioFrameRawPcm) {
        AliRTCLinuxEngine.AudioPcmFrame pcmFrame = frame.pcm;

        // pcmFrame.data        PCM 数据(byte[ ],int16_t 格式)

        // pcmFrame.channels    声道数
        // pcmFrame.sampleRates 采样率
        // 在此写入文件、送入音频设备或解码播放
    }
}

// AudioFormatPcmBeforeMixing 模式:按用户接收未混音的逐路 PCM 数据
@Override
public void onSubscribeAudioFrame(String uid, AliRTCLinuxEngine.AudioFrame frame) {
    if (frame != null && frame.type == AliRTCLinuxEngine.AudioFrameType.AudioFrameRawPcm) {
        // uid 标识该帧来自哪个远端用户
        // 在此按用户分别处理音频数据
    }
}

视频播放

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

@Override
public void onRemoteVideoSample(String uid, AliRTCLinuxEngine.VideoFrame frame) {
    if (frame != null && frame.type == AliRTCLinuxEngine.VideoFrameType.VideoFrameH264) {
        AliRTCLinuxEngine.VideoH264Frame h264Frame = frame.h264;
        // uid 标识该帧来自哪个远端用户
        // 在此写入文件、送入渲染器或视频解码器
    }
}

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

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

// 停止外部推流线程
running = false;
videoThread.interrupt();
audioThread.interrupt();
videoThread.join(1000);
audioThread.join(1000);

// 调用 publishLocalVideoStream(false) / publishLocalAudioStream(false) 停止发布
engineIns.publishLocalVideoStream(false);
engineIns.publishLocalAudioStream(false);

// 调用 leaveChannel 离开频道
engineIns.leaveChannel();

// 等待 onLeaveChannelResult 回调后再销毁引擎
// 调用 release 销毁引擎(须在 leaveChannel 之后调用)
engineIns.release();
engineIns = null;

常见问题

Q:创建引擎返回 null 怎么办?

请确认 AliRtcCoreService 路径正确且有执行权限:

chmod +x /path/to/Java/libs/AliRtcCoreService

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/Java/libs:$LD_LIBRARY_PATH