Implement a voice chat room on Android

更新时间:
复制 MD 格式

This document describes how to integrate the Alibaba Real-Time Communication (ARTC) SDK into your Android project to quickly build a simple audio-only interactive application. This application is suitable for scenarios such as voice calls and voice chat rooms.

Function introduction

Before you begin, understand the following basic concepts of real-time audio and video interaction:

  • ARTC SDK: The software development kit for ApsaraVideo Real-time Communication. It is an Alibaba Cloud product that helps developers quickly implement real-time audio and video interactions.

  • Channel: A concept similar to a room. Users in the same channel can interact in real time.

  • Streamer: A user who can publish audio and video streams in a channel and subscribe to streams published by other streamers.

  • Viewer: A user who can subscribe to audio and video streams in a channel but cannot publish them.

The following figure shows the basic flow for implementing a voice call or a voice chat room:

image
  1. Users must call joinChannel to join a channel before they can ingest or pull streams:

    • Audio-only call scenario: All users are streamers and can ingest and pull streams.

    • Voice chat room scenario: Users who need to ingest streams in the channel must have the streamer role. If a user only needs to pull streams, you can set their role to viewer.

    • You can use setClientRole to set different roles for users.

  2. After joining a channel, the stream ingest and pulling behaviors vary based on the user's role:

    • All users in a channel can receive audio and video streams from other users in the same channel.

    • Streamers can ingest audio and video streams into the channel.

    • If a viewer needs to ingest a stream, they must call the setClientRole method to switch their role to streamer before they can ingest the stream.

Example project

The Alibaba Cloud ARTC SDK provides an open-source example project for your reference. You can download the project or view the sample source code.

Prerequisites

Before you begin, make sure that your development environment meets the following requirements:

  • Development tool: Android Studio 2020.3.1 or later.

  • Test device: A test device running Android 5.0 (SDK API Level 21) or later.

Note

We recommend testing on a physical device. Emulators might not support all features.

  • Network environment: A stable network connection is required.

  • Application preparation: Obtain the AppID and AppKey for your ApsaraVideo Real-time Communication application. For more information, see Create an application.

  • Project creation and configuration: You have created a project, added the required permissions for audio and video interaction, such as audio and network permissions, and integrated the ARTC SDK. For more information, see Implement an audio and video call on Android.

Implementation steps

The following section uses a voice chat room scenario as an example. The process is as follows:

image

The main features of a voice chat room scenario are as follows:

  • Audio-only: The channel contains only audio, not video.

  • Streamer and viewer roles: The roles in the channel are divided into streamer and viewer. Streamers can ingest and pull audio streams. Viewers can only pull audio streams that are ingested by streamers. Viewers can switch their role to streamer.

Implement audio-only interaction

1. Request permissions

When starting a video call, check if the required permissions have been granted in the 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. Get an authentication token

Joining an ARTC channel requires an authentication token to verify the user's identity. For details on how the token is generated, see Implement token-based authentication. A token can be generated using a single-parameter method or a multi-parameter method. The method you use determines which joinChannel API you need to call.

For production environments:

Because generating a token requires your AppKey, hardcoding it on the client side poses a security risk. In a production environment, we strongly recommend generating the token on your server and sending it to the client.

For development and debugging:

During development, if your business server does not yet have the logic to generate tokens, you can temporarily use the token generation logic from the APIExample to create a temporary token. The reference code is as follows:

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

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

    /**
     * Generate a single-parameter token for joining a meeting based on channelId, userId, timestamp, 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;
    }

    /**
     * Generate a single-parameter token for joining a meeting 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. Create and initialize the engine

  • Create an RTC engine

Call getInstance to create an RTC engine object.

private AliRtcEngine mAliRtcEngine = null;

if(mAliRtcEngine == null) {
    mAliRtcEngine = AliRtcEngine.getInstance(this);
}
  • Initialize the engine

    • Call the setChannelProfile method to set the channel to interactive mode.

    • Based on the user's role in the business scenario, call the setClientRole method to set the user's role to streamer or viewer.

    • Call the setAudioProfile method to set the audio quality and scenario mode.

// Set the channel profile to interactive live. AliRTCSdkInteractiveLive is used for all RTC scenarios.
mAliRtcEngine.setChannelProfile(AliRtcEngine.AliRTCSdkChannelProfile.AliRTCSdkInteractiveLive);
// Set the user role. Use AliRTCSdkInteractive for users who need to both ingest and pull streams. Use AliRTCSdkLive for users who only pull streams.
if(isAnchor) {
    // To ingest audio and video streams, set the role to AliRTCSdkInteractive.
    mAliRtcEngine.setClientRole(AliRtcEngine.AliRTCSdkClientRole.AliRTCSdkInteractive);
} else {
    // If you only need to pull streams and not ingest them, set the role to AliRTCSdkLive.
    mAliRtcEngine.setClientRole(AliRtcEngine.AliRTCSdkClientRole.AliRTCSdkLive);
}

// Set the audio profile. The default is high-quality mode (AliRtcEngineHighQualityMode) and music mode (AliRtcSceneMusicMode).
mAliRtcEngine.setAudioProfile(AliRtcEngine.AliRtcAudioProfile.AliRtcEngineHighQualityMode, AliRtcEngine.AliRtcAudioScenario.AliRtcSceneMusicMode);
  • Implement common callbacks

If the SDK encounters an exception during operation, it first attempts to automatically recover using its internal retry mechanism. For errors that the SDK cannot resolve, it notifies your application through predefined callback methods.

The following table describes key callbacks for issues that the SDK cannot handle and that your application layer must listen for and respond to:

Causes

Callback and parameters

Solution

Description

Authentication failed

The result parameter in the onJoinChannelResult callback returns AliRtcErrJoinBadToken.

When this error occurs, the application must check whether the token is correct.

If authentication fails when a user calls an API, the system returns an authentication failure error message in the API callback.

Authentication token is about to expire

onWillAuthInfoExpire

When this exception occurs, the application must obtain the latest authentication information and then call refreshAuthInfo to refresh the authentication information.

An authentication expiration error can occur when a user calls an API or during program execution. Therefore, the error feedback is sent through an API callback or a separate error callback.

Authentication token expired

onAuthInfoExpired

When this exception occurs, the application must rejoin the channel.

An authentication expiration error can occur when a user calls an API or during program execution. Therefore, the error feedback is sent through an API callback or a separate error callback.

Abnormal network connectivity

The onConnectionStatusChange callback returns AliRtcConnectionStatusFailed.

When this exception occurs, the application must rejoin the channel.

The SDK can automatically recover from network disconnections for a specific period. However, if the disconnection time exceeds the preset threshold, the connection times out and disconnects. In this case, the application should check the network status and prompt the user to rejoin the channel.

Kicked offline

onBye

  • AliRtcOnByeUserReplaced: When this exception occurs, check whether the user IDs are the same.

  • AliRtcOnByeBeKickedOut: When this exception occurs, the user was kicked out by the business service and must rejoin the channel.

  • AliRtcOnByeChannelTerminated: When this exception occurs, the channel was destroyed, and the user must rejoin.

The RTC service provides a feature that allows administrators to remove participants.

On-premises device exception

onLocalDeviceException

When this exception occurs, the application must check whether the permissions and device hardware are normal.

The RTC service supports device detection and exception diagnosis. When an on-premises device exception occurs, the RTC service notifies the client through a callback. If the SDK cannot resolve the issue, the application must intervene to check whether the device is working correctly.

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: You must handle this. We recommend that your application notifies the user. This callback is triggered only after the SDK has tried all internal recovery policies and still cannot continue. */
                    ToastHelper.showToast(VideoChatActivity.this, R.string.video_chat_connection_failed, Toast.LENGTH_SHORT);
                } else {
                    /* TODO: Optional handling. Add business logic, typically for data statistics or UI changes. */
                }
            }
        });
    }
    @Override
    public void OnLocalDeviceException(AliRtcEngine.AliRtcEngineLocalDeviceType deviceType, AliRtcEngine.AliRtcEngineLocalDeviceExceptionType exceptionType, String msg){
        super.OnLocalDeviceException(deviceType, exceptionType, msg);
        /* TODO: You must handle this. We recommend notifying the user of the device error. This callback is triggered only after the SDK has tried all internal recovery policies and still cannot continue. */
        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: You must handle this. The token is about to expire. Your application needs to get the latest authentication information for the current channel and user, and then call refreshAuthInfo. */
    }

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

    // In the onRemoteUserOffLineNotify callback, release the settings for the remote video stream rendering control.
    @Override
    public void onRemoteUserOffLineNotify(String uid, AliRtcEngine.AliRtcUserOfflineReason reason){
        super.onRemoteUserOffLineNotify(uid, reason);
    }

    // In the onRemoteTrackAvailableNotify callback, set the remote video stream rendering control.
    @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);
                        }
                    }
                }
            }
        });
    }

    /* Your service might encounter a situation where different devices with the same UserID compete for access. You need to handle this case here. */
    @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);
            }
        });
    }
};

// Set callbacks
mAliRtcEngine.setRtcEngineEventListener(mRtcEngineEventListener);
mAliRtcEngine.setRtcEngineNotify(mRtcEngineNotify);

4. Set stream ingest and pulling properties

By default, the SDK automatically ingests and pulls audio and video streams in the channel.

  • After you set the role to viewer, you can only pull streams. The publishLocalAudioStream method has no effect.

  • The following configuration can be set for both streamers and viewers.

// The SDK publishes the audio stream by default. This method is invalid for viewers.
mAliRtcEngine.publishLocalAudioStream(true);
// In a voice chat scenario, you do not need to publish the video stream.
mAliRtcEngine.publishLocalVideoStream(false);

// Set the default subscription to remote audio streams.
mAliRtcEngine.setDefaultSubscribeAllRemoteAudioStreams(true);
mAliRtcEngine.subscribeAllRemoteAudioStreams(true);

5. Join a channel to start the audio-only interaction

Call the joinChannel method to join the channel.

Note

If the token is generated using the single-parameter rule, you must call the SDK's single-parameter joinChannel[1/3] method. If the token is generated using the multi-parameter rule, you must call the SDK's multi-parameter joinChannel[2/3] method. After you call the method to join the channel, you can obtain the result in the onJoinChannelResult callback. If the result is 0, you have successfully joined the channel. Otherwise, check whether the provided token is invalid.

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

6. End the audio-only interaction

When the audio interaction ends, you must leave the channel and destroy the engine. To end the audio and video interaction, perform the following steps:

  1. Call leaveChannel to leave the channel.

  2. Call destroy to destroy the engine and release related resources.

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

7. (Optional) Viewers becoming streamers and vice versa

In a business scenario, if a user with the viewer role wants to ingest a stream, they must call setClientRole to switch their role to streamer.

// Switch to the streamer role
mAliRtcEngine.setClientRole(AliRtcEngine.AliRTCSdkClientRole.AliRTCSdkInteractive);

// Switch to the viewer role
mAliRtcEngine.setClientRole(AliRtcEngine.AliRTCSdkClientRole.AliRTCSdkLive);

References