Android端实现语聊房

本文档将介绍如何在您的Android项目中集成 ARTC SDK, 快速实现一个简单的纯音频互动App,适用于语音通话、语聊房等场景。

功能介绍

在开始前,您需要了解以下有关音视频实时互动的基本概念:

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

  • 频道:房间的概念,在同一个频道内的用户可以进行实时互动。

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

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

下图展示了实现音频通话及语聊房的基本流程:

image
  1. 用户需要先调用joinChannel加入频道,才能进行推流、拉流:

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

    • 语聊房场景:需要在频道内推流的用户设置主播角色;如果用户只需要拉流,不需要推流,则设置观众角色;

    • 通过setClientRole为用户设置不同的角色。

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

    • 所有频道内的用户都可以接收相同频道内的音视频流;

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

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

示例项目

阿里云ARTC SDK提供了开源的示例项目供客户参考,您可以前往下载或查看示例源码

前提条件

在实现功能以前,请确保您的开发环境满足:

  • 开发工具:Android Studio 2020.3.1 及以上版本。

  • 测试设备:Android 5.0(SDK API Level 21)及以上版本的测试设备。

说明

推荐使用真机测试,模拟机可能存在功能缺失。

  • 网络环境:需要稳定的网络连接。

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

  • 创建项目和配置:已创建项目并为项目添加了音频、网络等音视频互动的相关权限,此外需要集成 ARTC SDK,相关步骤请参考Android端实现音视频通话

实现步骤

下面将以语聊房场景为例进行演示,相关功能时序如下:

image

语聊房场景主要特点如下:

  • 纯音频:频道内仅包含音频,不包含视频。

  • 主播/观众角色:频道内角色分为主播和观众角色,主播角色可以推拉音频流,观众角色只能拉取主播推送的音频流;观众角色可以切换为主播角色。

实现纯音频互动

1、申请权限请求

进入音视频通话时,检查是否已在App中授予了所需要的权限:

private static final int REQUEST_PERMISSION_CODE = 101;

private static final String[] PERMISSION_MANIFEST = {
    Manifest.permission.RECORD_AUDIO,
    Manifest.permission.READ_PHONE_STATE,
    Manifest.permission.WRITE_EXTERNAL_STORAGE,
    Manifest.permission.READ_EXTERNAL_STORAGE,
    Manifest.permission.CAMERA
};

private static final String[] PERMISSION_MANIFEST33 = {
    Manifest.permission.RECORD_AUDIO,
    Manifest.permission.READ_PHONE_STATE,
    Manifest.permission.CAMERA
};

private static String[] getPermissions() {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
        return PERMISSION_MANIFEST;
    }
    return PERMISSION_MANIFEST33;
}

public boolean checkOrRequestPermission() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        if (ContextCompat.checkSelfPermission(this, "android.permission.CAMERA") != PackageManager.PERMISSION_GRANTED
                || ContextCompat.checkSelfPermission(this, "android.permission.RECORD_AUDIO") != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(getPermissions(), REQUEST_PERMISSION_CODE);
            return false;
        }
    }
    return true;
}

2、鉴权Token

加入ARTC频道需要一个鉴权Token,用于鉴权用户的合法身份,其鉴权Token生成规则详情请参见:Token鉴权。Token 生成有两种方式:单参数方式和多参数方式,不同的Token生成方式需要调用SDK不同的加入频道(joinChannel)的接口。

上线发布阶段

由于Token的生成需要使用AppKey,写死在客户端存在泄露的风险,因此强烈建议线上业务通过业务Server生成下发给客户端。

开发调试阶段

开发调试阶段,如果业务Server还没有生成Token的逻辑,可以暂时参考APIExample上的Token生成逻辑,生成临时Token,其参考代码如下:

public final class ARTCTokenHelper {
    /**
     * RTC AppId
     */
    public static String AppId = "";

    /**
     * RTC AppKey
     */
    public static String AppKey = "";

    /**
     * 根据channelId,userId, timestamp, nonce 生成单参数入会 的token
     * Generate a single-parameter meeting token based on channelId, userId, and nonce
     */
    public static String generateSingleParameterToken(String appId, String appKey, String channelId, String userId, long timestamp,  String nonce) {

        StringBuilder stringBuilder = new StringBuilder()
                .append(appId)
                .append(appKey)
                .append(channelId)
                .append(userId)
                .append(timestamp);
        String token =  getSHA256(stringBuilder.toString());
        try{
            JSONObject tokenJson = new JSONObject();
            tokenJson.put("appid", AppId);
            tokenJson.put("channelid", channelId);
            tokenJson.put("userid", userId);
            tokenJson.put("nonce", nonce);
            tokenJson.put("timestamp", timestamp);
            tokenJson.put("token", token);
            String base64Token = Base64.encodeToString(tokenJson.toString().getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP);
            return base64Token;
        }catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 根据channelId,userId, timestamp 生成单参数入会 的token
     * Generate a single-parameter meeting token based on channelId, userId, and timestamp
     */
    public static String generateSingleParameterToken(String appId, String appKey, String channelId, String userId, long timestamp) {
        return generateSingleParameterToken(appId, appKey, channelId, userId, timestamp, "");
    }

    public static String getSHA256(String str) {
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
            byte[] hash = messageDigest.digest(str.getBytes(StandardCharsets.UTF_8));
            return byte2Hex(hash);
        } catch (NoSuchAlgorithmException e) {
            // Consider logging the exception and/or re-throwing as a RuntimeException
            e.printStackTrace();
        }
        return "";
    }

    private static String byte2Hex(byte[] bytes) {
        StringBuilder stringBuilder = new StringBuilder();
        for (byte b : bytes) {
            String hex = Integer.toHexString(0xff & b);
            if (hex.length() == 1) {
                // Use single quote for char
                stringBuilder.append('0');
            }
            stringBuilder.append(hex);
        }
        return stringBuilder.toString();
    }

    public static long getTimesTamp() {
        return System.currentTimeMillis() / 1000 + 60 * 60 * 24;
    }
}

3. 创建并初始化引擎

  • 创建 RTC 引擎

调用getInstance创建 RTC 引擎对象。

private AliRtcEngine mAliRtcEngine = null;

if(mAliRtcEngine == null) {
    mAliRtcEngine = AliRtcEngine.getInstance(this);
}
  • 初始化引擎

    • 调用setChannelProfile接口设置频道为互动模式。

    • 根据业务场景中用户的角色,调用setClientRole接口为用户设置主播/观众角色。

    • 调用setAudioProfile接口设置音频质量与场景模式。

// 设置频道模式为互动模式,RTC下都使用AliRTCSdkInteractiveLive
mAliRtcEngine.setChannelProfile(AliRtcEngine.AliRTCSdkChannelProfile.AliRTCSdkInteractiveLive);
// 设置用户角色,既需要推流也需要拉流使用AliRTCSdkInteractive, 只拉流不推流使用AliRTCSdkLive
if(isAnchor) {
    //如果需要推音视频流,则设置AliRTCSdkInteractive
    mAliRtcEngine.setClientRole(AliRtcEngine.AliRTCSdkClientRole.AliRTCSdkInteractive);
} else {
    //如果只需要拉流,不需要推音视频流,AliRTCSdkLive
    mAliRtcEngine.setClientRole(AliRtcEngine.AliRTCSdkClientRole.AliRTCSdkLive);
}

//设置音频Profile,默认使用高音质模式AliRtcEngineHighQualityMode及音乐模式AliRtcSceneMusicMode
mAliRtcEngine.setAudioProfile(AliRtcEngine.AliRtcAudioProfile.AliRtcEngineHighQualityMode, AliRtcEngine.AliRtcAudioScenario.AliRtcSceneMusicMode);
  • 实现常用回调

SDK 在运行过程中如遇到异常情况,会优先尝试内部重试机制以自动恢复。对于无法自行解决的错误,SDK 会通过预定义的回调接口通知您的应用程序。

以下是一些 SDK 无法处理、需由应用层监听和响应的关键回调:

异常发生原因

回调及参数

解决方案

说明

鉴权失败

onJoinChannelResult回调中的result返回AliRtcErrJoinBadToken

发生错误时App需要检查Token是否正确。

在用户主动调用API时,若鉴权失败,系统将在调用API的回调中返回鉴权失败的错误信息。

鉴权将要过期

onWillAuthInfoExpire

发生该异常时App需要重新获取最新的鉴权信息后,再调用refreshAuthInfo刷新鉴权信息。

鉴权过期错误在两种情况下出现:用户调用API或程序执行期间。因此,错误反馈将通过API回调或通过独立的错误回调通知。

鉴权过期

onAuthInfoExpired

发生该异常时App需要重新入会。

鉴权过期错误在两种情况下出现:用户调用API或程序执行期间。因此,错误反馈将通过API回调或通过独立的错误回调通知。

网络连接异常

onConnectionStatusChange回调返回AliRtcConnectionStatusFailed

发生该异常时APP需要重新入会。

SDK具备一定时间断网自动恢复能力,但若断线时间超出预设阈值,会触发超时并断开连接。此时,App应检查网络状态并指导用户重新加入会议。

被踢下线

onBye

  • AliRtcOnByeUserReplaced:当发生该异常时排查用户userid是否相同。

  • AliRtcOnByeBeKickedOut:当发生该异常时,表示被业务踢下线,需要重新入会。

  • AliRtcOnByeChannelTerminated:当发生该异常时,表示房间被销毁,需要重新入会。

RTC服务提供了管理员可以主动移除参与者的功能。

本地设备异常

onLocalDeviceException

发生该异常时App需要检测权限、设备硬件是否正常。

RTC服务支持设备检测和异常诊断的能力;当本地设备发生异常时,RTC服务会通过回调的方式通知客户本地设备异常,此时,若SDK无法自行解决问题,则App需要介入以查看设备是否正常。

private AliRtcEngineEventListener mRtcEngineEventListener = new AliRtcEngineEventListener() {
    @Override
    public void onJoinChannelResult(int result, String channel, String userId, int elapsed) {
        super.onJoinChannelResult(result, channel, userId, elapsed);
        handleJoinResult(result, channel, userId);
    }

    @Override
    public void onLeaveChannelResult(int result, AliRtcEngine.AliRtcStats stats){
        super.onLeaveChannelResult(result, stats);
    }

    @Override
    public void onConnectionStatusChange(AliRtcEngine.AliRtcConnectionStatus status, AliRtcEngine.AliRtcConnectionStatusChangeReason reason){
        super.onConnectionStatusChange(status, reason);

        handler.post(new Runnable() {
            @Override
            public void run() {
                if(status == AliRtcEngine.AliRtcConnectionStatus.AliRtcConnectionStatusFailed) {
                    /* TODO: 务必处理;建议业务提示客户,此时SDK内部已经尝试了各种恢复策略已经无法继续使用时才会上报 */
                    ToastHelper.showToast(VideoChatActivity.this, R.string.video_chat_connection_failed, Toast.LENGTH_SHORT);
                } else {
                    /* TODO: 可选处理;增加业务代码,一般用于数据统计、UI变化 */
                }
            }
        });
    }
    @Override
    public void OnLocalDeviceException(AliRtcEngine.AliRtcEngineLocalDeviceType deviceType, AliRtcEngine.AliRtcEngineLocalDeviceExceptionType exceptionType, String msg){
        super.OnLocalDeviceException(deviceType, exceptionType, msg);
        /* TODO: 务必处理;建议业务提示设备错误,此时SDK内部已经尝试了各种恢复策略已经无法继续使用时才会上报 */
        handler.post(new Runnable() {
            @Override
            public void run() {
                String str = "OnLocalDeviceException deviceType: " + deviceType + " exceptionType: " + exceptionType + " msg: " + msg;
                ToastHelper.showToast(VideoChatActivity.this, str, Toast.LENGTH_SHORT);
            }
        });
    }

};

private AliRtcEngineNotify mRtcEngineNotify = new AliRtcEngineNotify() {
    @Override
    public void onAuthInfoWillExpire() {
        super.onAuthInfoWillExpire();
        /* TODO: 务必处理;Token即将过期,需要业务触发重新获取当前channel,user的鉴权信息,然后设置refreshAuthInfo即可 */
    }

    @Override
    public void onRemoteUserOnLineNotify(String uid, int elapsed){
        super.onRemoteUserOnLineNotify(uid, elapsed);
    }

    //在onRemoteUserOffLineNotify回调中解除远端视频流渲染控件的设置
    @Override
    public void onRemoteUserOffLineNotify(String uid, AliRtcEngine.AliRtcUserOfflineReason reason){
        super.onRemoteUserOffLineNotify(uid, reason);
    }

    //在onRemoteTrackAvailableNotify回调中设置远端视频流渲染控件
    @Override
    public void onRemoteTrackAvailableNotify(String uid, AliRtcEngine.AliRtcAudioTrack audioTrack, AliRtcEngine.AliRtcVideoTrack videoTrack){
        handler.post(new Runnable() {
            @Override
            public void run() {
                if(videoTrack == AliRtcVideoTrackCamera) {
                    SurfaceView surfaceView = mAliRtcEngine.createRenderSurfaceView(VideoChatActivity.this);
                    surfaceView.setZOrderMediaOverlay(true);
                    FrameLayout view = getAvailableView();
                    if (view == null) {
                        return;
                    }
                    remoteViews.put(uid, view);
                    view.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
                    AliRtcEngine.AliRtcVideoCanvas remoteVideoCanvas = new AliRtcEngine.AliRtcVideoCanvas();
                    remoteVideoCanvas.view = surfaceView;
                    mAliRtcEngine.setRemoteViewConfig(remoteVideoCanvas, uid, AliRtcVideoTrackCamera);
                } else if(videoTrack == AliRtcVideoTrackNo) {
                    if(remoteViews.containsKey(uid)) {
                        ViewGroup view = remoteViews.get(uid);
                        if(view != null) {
                            view.removeAllViews();
                            remoteViews.remove(uid);
                            mAliRtcEngine.setRemoteViewConfig(null, uid, AliRtcVideoTrackCamera);
                        }
                    }
                }
            }
        });
    }

    /* 业务可能会触发同一个UserID的不同设备抢占的情况,所以这个地方也需要处理 */
    @Override
    public void onBye(int code){
        handler.post(new Runnable() {
            @Override
            public void run() {
                String msg = "onBye code:" + code;
                ToastHelper.showToast(VideoChatActivity.this, msg, Toast.LENGTH_SHORT);
            }
        });
    }
};

// 设置回调
mAliRtcEngine.setRtcEngineEventListener(mRtcEngineEventListener);
mAliRtcEngine.setRtcEngineNotify(mRtcEngineNotify);

4. 设置推拉流属性

SDK 默认情况下会自动推送和拉取频道内的音视频流

  • 设置为观众模式后只能拉流,publishLocalAudioStream 无效。

  • 对于主播和观众均可以设置为下面的配置。

//SDK默认会publish音频,对于观众此接口无效
mAliRtcEngine.publishLocalAudioStream(true);
//语聊场景,不需要publish视频
mAliRtcEngine.publishLocalVideoStream(false);

//设置默认订阅远端的音频
mAliRtcEngine.setDefaultSubscribeAllRemoteAudioStreams(true);
mAliRtcEngine.subscribeAllRemoteAudioStreams(true);

5. 加入频道开始纯音频互动

调用joinChannel接口加入频道。

说明

如果token是单参数规则生成的,需要调用SDK单参数的joinChannel[1/3]接口,如果是多参数规则生成的,需要调用SDK多参数的joinChannel[2/3]接口。调用完加入频道后,可以在onJoinChannelResult回调中拿到加入频道结果,如果result0,则表示加入频道成功,否则需要检查传进来的Token是否非法。

 mAliRtcEngine.joinChannel(token, null, null, null);

6. 结束纯音频互动

音频互动结束,需要离开房间并销毁引擎,按照下列步骤结束音视频互动

  1. 调用leaveChannel离会。

  2. 调用destroy销毁引擎,并释放相关资源。

mAliRtcEngine.leaveChannel();
mAliRtcEngine.destroy();
mAliRtcEngine = null;

7. (可选)观众上下麦

业务场景中,如果观众角色的用户想要推流,需要调用setClientRole将观众角色切换为主播角色。

// 切换为主播角色
mAliRtcEngine.setClientRole(AliRtcEngine.AliRTCSdkClientRole.AliRTCSdkInteractive);

// 切换为观众角色
mAliRtcEngine.setClientRole(AliRtcEngine.AliRTCSdkClientRole.AliRTCSdkLive);

相关文档