AndroidAv_zh - aopacloud/aopa-rtc GitHub Wiki

实现音视频互动

本文介绍如何集成奥帕实时互动 SDK,通过少量代码从 0 开始实现一个简单的实时互动 App,适用于互动直播和视频通话场景。

首先,你需要了解以下有关音视频实时互动的基础概念:

  • 奥帕实时互动 SDK:由奥帕开发的、帮助开发者在 App 中实现实时音视频互动的 SDK。
  • 频道:用于传输数据的通道,在同一个频道内的用户可以进行实时互动。
  • 主播:可以在频道内发布音视频,同时也可以订阅其他主播发布的音视频。
  • 观众:可以在频道内订阅音视频,不具备发布音视频权限。

下图展示在 App 中实现音视频互动的基本工作流程:

实现音视频互动

  1. 所有用户调用 joinChannel 方法加入频道,并根据需要设置用户角色:
    • 互动直播:如果用户需要在频道中发流,则设为主播;如果用户只需要收流,则设为观众。
    • 视频通话:将所有的用户角色都为主播。
  2. 加入频道后,不同角色的用户具备不同的行为:
    • 所有用户默认都可以接收频道中的音视频流。
    • 主播可以在频道内发布音视频流。
    • 观众如果需要发流,可在频道内调用 setClientRole 方法修改用户角色,使其具备发流权限。

前提条件​

在实现功能以前,请按照以下要求准备开发环境:

  • Android Studio 4.1 以上版本。
  • Android API 级别 16 或以上。
  • 两台运行 Android 4.1 或以上版本的移动设备。
  • 可以访问互联网的计算机。如果你的网络环境部署了防火墙,参考[应对防火墙限制]以正常使用奥帕服务。
  • 一个有效的奥帕账号以及奥帕项目。请参考[开通服务]从奥帕控制台获得以下信息:
    • App ID:奥帕随机生成的字符串,用于识别你的项目。
    • 临时 Token:Token 也称为动态密钥,在客户端加入频道时对用户鉴权。临时 Token 的有效期为 24 小时。

创建项目​

本小节介绍如何创建项目并为项目添加体验实时互动所需的权限。

  1. (可选) 创建新项目。详见 Create a project

    1. 打开 Android Studio,选择 New Project

    2. 选择 Phone and Tablet > Empty Views Activity,点击 Next

    3. 设置项目名称和存储路径,选择语言为 Java,点击 Finish 创建 Android 项目。

      注意

      创建项目后,Android Studio 会自动开始同步 gradle,稍等片刻至同步成功后再进行下一步操作。

  2. 添加网络及设备权限。

    打开 /app/src/main/AndroidManifest.xml 文件,在 </application> 后面添加如下权限:

    XML

    <!--必要权限-->  
    <uses-permission android:name="android.permission.INTERNET"/>  
      
    <!--可选权限-->  
    <uses-permission android:name="android.permission.CAMERA"/>  
    <uses-permission android:name="android.permission.RECORD_AUDIO"/>  
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>  
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>  
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>  
    <uses-permission android:name="android.permission.BLUETOOTH"/>  
    <!-- 对于 Android 12.0 及以上且集成 v4.1.0 以下 SDK 的设备,还需要添加以下权限 -->  
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>  
    <!-- 对于 Android 12.0 及以上设备,还需要添加以下权限 -->  
    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>  
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>  
    
    
  3. 防止代码混淆。

    打开 /app/proguard-rules.pro 文件,添加如下行以防止奥帕 SDK 的代码被混淆:

    Java

    -keep class io.aopa.**{*;}  
    
    

集成 SDK​

你可以选用以下任一方式集成奥帕实时互动 SDK。

  • 通过 Maven Central 集成
  • 手动集成
  1. 打开项目根目录下的 settings.gradle 文件,添加 Maven Central 依赖 (如果已有可忽略):

    Java

    repositories {  
        ...    mavenCentral()    ...}  
    
    

    注意

    如果你的 Android 项目设置了 dependencyResolutionManagement,添加 Maven Central 依赖的方式可能存在差异。

  2. 打开 /app/build.gradle 文件,在 dependencies 中添加奥帕 RTC SDK 的依赖。你可以从[发版说明])中查询 SDK 的最新版本,并将 x.y.z 替换为具体的版本号。

    Java

    ...  
    dependencies {  
        ...    // x.y.z 替换为具体的 SDK 版本号,如:4.0.0 或 4.1.0-1    implementation 'io.aopa.rtc:full-sdk:x.y.z'}  
    
    
  3. 在[下载]页面下载最新版本的 Android 实时互动 SDK,并解压。

  4. 打开解压文件,将以下文件或子文件夹复制到你的项目路径中。

    文件或子文件夹 项目路径
    aopa-rtc-sdk.aar 文件 /app/libs/
    arm64-v8a 文件夹 /app/src/main/jniLibs/
    armeabi-v7a 文件夹 /app/src/main/jniLibs/
  5. Android Studio 的左侧导航栏上选择 Project Files/app/libs/aopa-rtc-sdk.jar 文件,右键单击,在下拉菜单中选择 add as a library

创建用户界面​

根据实时音视频互动的场景需要,为你的项目创建两个视图框,分别用于展示本地视频和远端视频。如下图所示:

图片

复制以下代码到 /app/src/main/res/layout/activity_main.xml 文件中替换原有内容,即可快速创建场景所需的用户界面。

创建用户界面示例代码

XML

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/RoomIDTextView"
        android:layout_width="60dp"
        android:layout_height="30dp"
        android:layout_marginLeft="50dp"
        android:layout_marginTop="40dp"
        android:text="房间ID"
        android:textColor="@android:color/black" />

    <EditText
        android:id="@+id/RoomIdEditText"
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:layout_alignTop="@+id/RoomIDTextView"
        android:layout_alignParentRight="true"
        android:layout_marginTop="-6dp"
        android:layout_marginRight="50dp"
        android:layout_toRightOf="@+id/RoomIDTextView"
        android:gravity="left"
        android:inputType="text"
        android:singleLine="true"
        android:text="room123"
        android:textColor="@android:color/black"
        android:textSize="15dp" />

    <TextView
        android:id="@+id/AppIDTextView"
        android:layout_width="60dp"
        android:layout_height="30dp"
        android:layout_below="@+id/RoomIDTextView"
        android:layout_marginLeft="50dp"
        android:layout_marginTop="4dp"
        android:text="APP ID"
        android:textColor="@android:color/black" />

    <Spinner
        android:id="@+id/AppIdSpinner"
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:layout_alignTop="@+id/AppIDTextView"
        android:layout_alignParentRight="true"
        android:layout_marginTop="-6dp"
        android:layout_marginRight="50dp"
        android:layout_toRightOf="@+id/AppIDTextView"
        android:gravity="center"
        android:textSize="15dp" />

    <TextView
        android:id="@+id/UserIdTextView"
        android:layout_width="60dp"
        android:layout_height="30dp"
        android:layout_below="@+id/AppIDTextView"
        android:layout_marginLeft="50dp"
        android:layout_marginTop="4dp"
        android:text="用户ID"
        android:textColor="@android:color/black" />

    <EditText
        android:id="@+id/UserIdEditText"
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:layout_alignTop="@+id/UserIdTextView"
        android:layout_alignParentRight="true"
        android:layout_marginTop="-6dp"
        android:layout_marginRight="50dp"
        android:layout_toRightOf="@+id/UserIdTextView"
        android:gravity="left"
        android:inputType="text"
        android:singleLine="true"
        android:text=""
        android:textColor="@android:color/black"
        android:textSize="15dp" />

    <TextView
        android:id="@+id/ServerAddrTextView"
        android:layout_width="60dp"
        android:layout_height="30dp"
        android:layout_below="@+id/UserIdTextView"
        android:layout_marginLeft="50dp"
        android:layout_marginTop="4dp"
        android:text="服务环境"
        android:textColor="@android:color/black"
        android:visibility="visible"
        tools:visibility="visible" />


    <Spinner
        android:id="@+id/ServerSpinner"
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:layout_alignTop="@+id/ServerAddrTextView"
        android:layout_alignParentRight="true"
        android:layout_marginTop="-6dp"
        android:layout_marginRight="50dp"
        android:layout_toRightOf="@+id/ServerAddrTextView"
        android:gravity="center"
        android:textSize="15dp"
        android:visibility="visible" />

    <TextView
        android:id="@+id/AudioQualityTextView"
        android:textColor="@android:color/black"
        android:layout_width="60dp"
        android:layout_height="30dp"
        android:layout_below="@+id/ServerAddrTextView"
        android:layout_marginLeft="50dp"
        android:layout_marginTop="4dp"
        android:text="音质" />

    <Spinner
        android:id="@+id/AudioQualitySpinner"
        android:layout_width="wrap_content"
        android:layout_height="30dp"
        android:layout_alignTop="@+id/AudioQualityTextView"
        android:layout_alignParentRight="true"
        android:layout_marginTop="-6dp"
        android:layout_marginRight="50dp"
        android:layout_toRightOf="@+id/AudioQualityTextView"
        android:gravity="center"
        android:textSize="15dp" />

    <TextView
        android:id="@+id/ScenarioTextView"
        android:textColor="@android:color/black"
        android:layout_width="60dp"
        android:layout_height="30dp"
        android:layout_below="@+id/AudioQualityTextView"
        android:layout_marginLeft="50dp"
        android:layout_marginTop="4dp"
        android:text="场景" />

    <Spinner
        android:id="@+id/ScenarioSpinner"
        android:layout_width="wrap_content"
        android:layout_height="30dp"
        android:layout_toRightOf="@+id/ScenarioTextView"
        android:layout_alignTop="@+id/ScenarioTextView"
        android:layout_alignParentRight="true"
        android:layout_marginRight="50dp"
        android:layout_marginTop="-6dp"
        android:textSize="15dp"
        android:gravity="center" />

    <TextView
        android:id="@+id/RoleTextView"
        android:layout_width="60dp"
        android:layout_height="30dp"
        android:layout_below="@+id/ScenarioTextView"
        android:layout_marginLeft="50dp"
        android:layout_marginTop="4dp"
        android:text="角色"
        android:textColor="@android:color/black" />

    <Spinner
        android:id="@+id/RoleSpinner"
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:layout_alignTop="@+id/RoleTextView"
        android:layout_alignParentRight="true"
        android:layout_marginTop="-6dp"
        android:layout_marginRight="50dp"
        android:layout_toRightOf="@+id/RoleTextView"
        android:gravity="center"
        android:textSize="15dp" />

    <TextView
        android:id="@+id/ResolutionTextView"
        android:layout_width="wrap_content"
        android:layout_height="30dp"
        android:layout_below="@+id/RoleTextView"
        android:layout_marginLeft="50dp"
        android:layout_marginTop="4dp"
        android:text="采集分辨率"
        android:textColor="@android:color/black" />

    <Spinner
        android:id="@+id/ResolutionSpinner"
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:layout_alignTop="@+id/ResolutionTextView"
        android:layout_alignParentRight="true"
        android:layout_marginTop="-6dp"
        android:layout_marginRight="50dp"
        android:layout_toRightOf="@+id/ResolutionTextView"
        android:gravity="center"
        android:textSize="15dp" />

    <TextView
        android:id="@+id/CustomServerTextView"
        android:layout_width="80dp"
        android:layout_height="30dp"
        android:layout_below="@+id/ResolutionSpinner"
        android:layout_marginLeft="50dp"
        android:layout_marginTop="4dp"
        android:text="自定义服务"
        android:textColor="@android:color/black" />

    <EditText
        android:id="@+id/CustomServerEditText"
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:layout_alignTop="@+id/CustomServerTextView"
        android:layout_alignParentRight="true"
        android:layout_marginTop="-6dp"
        android:layout_marginRight="50dp"
        android:layout_toRightOf="@+id/CustomServerTextView"
        android:gravity="left"
        android:inputType="text"
        android:singleLine="true"
        android:text=""
        android:textColor="@android:color/black"
        android:textSize="15dp"
        android:tooltipText="输入服务器IP"
        android:visibility="invisible"
        tools:visibility="visible" />

    <CheckBox
        android:id="@+id/TokenCheckBox"
        android:layout_width="120dp"
        android:layout_height="30dp"
        android:layout_below="@+id/CustomServerTextView"
        android:layout_marginLeft="50dp"
        android:layout_marginTop="10dp"
        android:text="Token校验" />

    <CheckBox
        android:id="@+id/StatsCheckBox"
        android:layout_width="120dp"
        android:layout_height="30dp"
        android:layout_alignTop="@+id/TokenCheckBox"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="0dp"
        android:layout_toRightOf="@+id/TokenCheckBox"
        android:text="统计数据" />

    <CheckBox
        android:id="@+id/SpeakerCheckBox"
        android:layout_width="120dp"
        android:layout_height="30dp"
        android:layout_below="@+id/TokenCheckBox"
        android:layout_marginLeft="50dp"
        android:layout_marginTop="10dp"
        android:text="扬声器输出" />

    <CheckBox
        android:id="@+id/QuicCheckBox"
        android:layout_width="120dp"
        android:layout_height="30dp"
        android:layout_alignTop="@+id/SpeakerCheckBox"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="0dp"
        android:layout_toRightOf="@+id/InearCheckBox"
        android:text="QUIC" />

    <CheckBox
        android:id="@+id/InearCheckBox"
        android:layout_width="120dp"
        android:layout_height="30dp"
        android:layout_below="@+id/SpeakerCheckBox"
        android:layout_marginLeft="50dp"
        android:layout_marginTop="10dp"
        android:text="耳返" />

    <CheckBox
        android:id="@+id/ReverbCheckBox"
        android:layout_width="120dp"
        android:layout_height="30dp"
        android:layout_alignTop="@+id/InearCheckBox"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="0dp"
        android:layout_toRightOf="@+id/InearCheckBox"
        android:text="混响" />

    <CheckBox
        android:id="@+id/AAACheckBox"
        android:layout_width="120dp"
        android:layout_height="30dp"
        android:layout_below="@+id/InearCheckBox"
        android:layout_marginLeft="50dp"
        android:layout_marginTop="5dp"
        android:text="3A处理" />

    <CheckBox
        android:id="@+id/VideoCheckBox"
        android:layout_width="120dp"
        android:layout_height="30dp"
        android:layout_below="@+id/InearCheckBox"
        android:layout_alignLeft="@+id/ReverbCheckBox"
        android:layout_marginTop="5dp"
        android:text="开启视频" />

    <CheckBox
        android:id="@+id/MulticastCheckBox"
        android:layout_width="120dp"
        android:layout_height="30dp"
        android:layout_below="@+id/AAACheckBox"
        android:layout_marginLeft="50dp"
        android:layout_marginTop="5dp"
        android:text="大小视频" />

    <CheckBox
        android:id="@+id/DetachCheckBox"
        android:layout_width="120dp"
        android:layout_height="30dp"
        android:layout_below="@+id/VideoCheckBox"
        android:layout_alignLeft="@+id/VideoCheckBox"
        android:layout_marginTop="5dp"
        android:text="推拉流分离" />

    <Button
        android:id="@+id/RoomBnt"
        android:layout_width="120dp"
        android:layout_height="48dp"
        android:layout_below="@id/MulticastCheckBox"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="20dp"
        android:onClick="onJoinClick"
        android:text="多人视频" />

    <Button
        android:id="@+id/SingleBnt"
        android:layout_width="120dp"
        android:layout_height="48dp"
        android:layout_below="@id/RoomBnt"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="5dp"
        android:onClick="onSingleClick"
        android:text="单人视频" />

    <TextView
        android:id="@+id/VersionText"
        android:layout_width="60dp"
        android:layout_height="20dp"
        android:singleLine="true"
        android:text=""
        android:textSize="14dp"
        android:layout_alignParentRight="true"
        android:layout_alignParentBottom="true" />

</RelativeLayout>

实现流程​

本小节介绍如何实现一个实时音视频互动 App。你可以先复制完整的示例代码到你的项目中,快速体验实时音视频互动的基础功能,再按照实现步骤了解核心 API 调用。

下图展示了使用奥帕 RTC SDK 实现音视频互动的基本流程:

实现流程

下面列出了一段实现实时互动基本流程的完整代码以供参考。复制以下代码到 /app/src/main/java/com/example/<projectname>/MainActivity.java 文件中替换 package com.example.<projectname> 后的全部内容,即可快速体验实时互动基础功能。

信息

appIdtokenchannelName 字段中传入你在控制台获取到的 App ID、临时 Token,以及生成临时 Token 时填入的频道名。

实现实时音视频互动示例代码

Java

import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.SurfaceView;
import android.widget.FrameLayout;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import org.banban.rtc.ChannelMediaOptions;
import org.banban.rtc.Constants;
import org.banban.rtc.IRtcEngineEventHandler;
import org.banban.rtc.RtcEngine;
import org.banban.rtc.RtcEngineConfig;
import org.banban.rtc.video.VideoCanvas;

public class MainActivity extends AppCompatActivity {

    // 填写声网控制台中获取的 App ID
    private String appId = "<#Your App ID#>";
    // 填写频道名
    private String channelName = "<#Your channel name#>";
    // 填写声网控制台中生成的临时 Token
    private String token = "<#Your Token#>";

    private RtcEngine mRtcEngine;

    private final IRtcEngineEventHandler mRtcEventHandler = new IRtcEngineEventHandler() {
        // 成功加入频道回调
        @Override
        public void onJoinChannelSuccess(String channel, int uid, int elapsed) {
            super.onJoinChannelSuccess(channel, uid, elapsed);
            runOnUiThread(() -> {
                Toast.makeText(MainActivity.this, "Join channel success", Toast.LENGTH_SHORT).show();
            });
        }

        // 远端用户或主播加入当前频道回调
        @Override
        public void onUserJoined(int uid, int elapsed) {
            runOnUiThread(() -> {
                // 当远端用户加入频道后,显示指定 uid 的远端视频流
                setupRemoteVideo(uid);
            });
        }

        // 远端用户或主播离开当前频道回调
        @Override
        public void onUserOffline(int uid, int reason) {
            super.onUserOffline(uid, reason);
            runOnUiThread(() -> {
                Toast.makeText(MainActivity.this, "User offline: " + uid, Toast.LENGTH_SHORT).show();
            });
        }
    };

    private void initializeAndJoinChannel() {
        try {
            // 创建 RtcEngineConfig 对象,并进行配置
            RtcEngineConfig config = new RtcEngineConfig();
            config.mContext = getBaseContext();
            config.mAppId = appId;
            config.mEventHandler = mRtcEventHandler;
            // 创建并初始化 RtcEngine
            mRtcEngine = RtcEngine.create(config);
        } catch (Exception e) {
            throw new RuntimeException("Check the error.");
        }
        // 启用视频模块
        mRtcEngine.enableVideo();

        // 创建一个 SurfaceView 对象,并将其作为 FrameLayout 的子对象
        FrameLayout container = findViewById(R.id.local_video_view_container);
        SurfaceView surfaceView = new SurfaceView (getBaseContext());
        container.addView(surfaceView);
        // 将 SurfaceView 对象传入声网实时互动 SDK,设置本地视图
        mRtcEngine.setupLocalVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_FIT, 0));

        // 开启本地预览
        mRtcEngine.startPreview();

        // 创建 ChannelMediaOptions 对象,并进行配置
        ChannelMediaOptions options = new ChannelMediaOptions();
        // 设置用户角色为 BROADCASTER (主播) 或 AUDIENCE (观众)
        options.clientRoleType = Constants.CLIENT_ROLE_BROADCASTER;
        // 设置频道场景为 BROADCASTING (直播场景)
        options.channelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING;
        // 发布麦克风采集的音频
        options.publishMicrophoneTrack = true;
        // 发布摄像头采集的视频
        options.publishCameraTrack = true;
        // 自动订阅所有音频流
        options.autoSubscribeAudio = true;
        // 自动订阅所有视频流
        options.autoSubscribeVideo = true;
        // 使用临时 Token 和频道名加入频道,uid 为 0 表示引擎内部随机生成用户名
        // 成功后会触发 onJoinChannelSuccess 回调
        mRtcEngine.joinChannel(token, channelName, 0, options);
    }

    private void setupRemoteVideo(int uid) {
        FrameLayout container = findViewById(R.id.remote_video_view_container);
        SurfaceView surfaceView = new SurfaceView (getBaseContext());
        surfaceView.setZOrderMediaOverlay(true);
        container.addView(surfaceView);
        // 将 SurfaceView 对象传入声网实时互动 SDK,设置远端视图
        mRtcEngine.setupRemoteVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_FIT, uid));
    }

    private static final int PERMISSION_REQ_ID = 22;
    private String[] getRequiredPermissions(){
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
            return new String[]{
                    Manifest.permission.RECORD_AUDIO,
                    Manifest.permission.CAMERA,
                    Manifest.permission.READ_PHONE_STATE, 
                    Manifest.permission.BLUETOOTH_CONNECT
            };
        } else {
            return new String[]{
                    Manifest.permission.RECORD_AUDIO,
                    Manifest.permission.CAMERA
            };
        }
    }

    private boolean checkPermissions() {
        for (String permission : getRequiredPermissions()) {
            int permissionCheck = ContextCompat.checkSelfPermission(this, permission);
            if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
                return false;
            }
        }
        return true;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (checkPermissions()) {
            initializeAndJoinChannel();
        } else {
            ActivityCompat.requestPermissions(this, getRequiredPermissions(), PERMISSION_REQ_ID);
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (checkPermissions()) {
            initializeAndJoinChannel();
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mRtcEngine != null) {
            mRtcEngine.stopPreview();
            mRtcEngine.leaveChannel();
            mRtcEngine = null;
            RtcEngine.destroy();
        }
    }
}

处理权限请求​

本小节介绍如何导入 Android 相关的类并获取 Android 设备的摄像头、录音等权限。

  1. 导入 Android 相关的类

    Java

    import android.Manifest;  
    import android.content.pm.PackageManager;  
    import android.os.Bundle;  
    import android.view.SurfaceView;  
    import android.widget.FrameLayout;  
    import android.widget.Toast;  
      
    import androidx.annotation.NonNull;  
    import androidx.appcompat.app.AppCompatActivity;  
    import androidx.core.app.ActivityCompat;  
    import androidx.core.content.ContextCompat;  
    
    
  2. 获取 Android 权限

    启动应用程序时,检查是否已在 App 中授予了实现实时互动所需的权限。

    Java

       private static final int PERMISSION_REQ_ID = 22;
     private String[] getRequiredPermissions(){
      if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
        return new String[]{
                Manifest.permission.RECORD_AUDIO,
                Manifest.permission.CAMERA, 
                Manifest.permission.READ_PHONE_STATE,
                Manifest.permission.BLUETOOTH_CONNECT 
        };
      } else {
        return new String[]{
                Manifest.permission.RECORD_AUDIO,
                Manifest.permission.CAMERA
        };
     }
    }
    private boolean checkPermissions() {
     for (String permission : getRequiredPermissions()) {
        int permissionCheck = ContextCompat.checkSelfPermission(this, permission);
        if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
            return false;
        }
     }
     return true;
    }
    

导入奥帕相关的类​

导入奥帕 RTC SDK 相关的类和接口:

Java

import org.banban.rtc.ChannelMediaOptions;
import org.banban.rtc.Constants;
import org.banban.rtc.IRtcEngineEventHandler;
import org.banban.rtc.RtcEngine;
import org.banban.rtc.RtcEngineConfig;
import org.banban.rtc.video.VideoCanvas;

定义 App ID 和 Token​

传入从奥帕控制台获取的 App ID、临时 Token,以及生成临时 Token 时填入的频道名,用于后续初始化引擎和加入频道。

Java

// 填写奥帕控制台中获取的 App ID  
private String appId = "<#Your App ID#>";  
// 填写频道名  
private String channelName = "<#Your channel name#>";  
// 填写奥帕控制台中生成的临时 Token  
private String token = "<#Your Token#>";  

初始化引擎​

调用 create 方法初始化 RtcEngine

注意

在初始化 SDK 前,需确保终端用户已经充分了解并同意相关的隐私政策。

Java

private RtcEngine mRtcEngine;  
  
private final IRtcEngineEventHandler mRtcEventHandler = new IRtcEngineEventHandler() {  
    ...};  
  
// 创建 RtcEngineConfig 对象,并进行配置  
RtcEngineConfig config = new RtcEngineConfig();  
config.mContext = getBaseContext();  
config.mAppId = appId;  
config.mEventHandler = mRtcEventHandler;  
// 创建并初始化 RtcEngine  
mRtcEngine = RtcEngine.create(config);  

启用视频模块​

按照以下步骤启用视频模块:

  1. 调用 enableVideo 方法,启用视频模块。
  2. 调用 setupLocalVideo 方法初始化本地视图,同时设置本地的视频显示属性。
  3. 调用 startPreview 方法,开启本地视频预览。

Java

// 启用视频模块  
mRtcEngine.enableVideo();  
  
// 创建一个 SurfaceView 对象,并将其作为 FrameLayout 的子对象  
FrameLayout container = findViewById(R.id.local_video_view_container);  
SurfaceView surfaceView = new SurfaceView (getBaseContext());  
container.addView(surfaceView);  
// 将 SurfaceView 对象传入奥帕实时互动 SDK,设置本地视图  
mRtcEngine.setupLocalVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_FIT, 0));  
  
// 开启本地预览  
mRtcEngine.startPreview();  

加入频道并发布音视频流​

调用 joinChannel 加入频道。在 ChannelMediaOptions 中进行如下配置:

  • 设置频道场景为 BROADCASTING (直播场景) 并设置用户角色设置为 BROADCASTER (主播) 或 AUDIENCE (观众)。
  • publishMicrophoneTrackpublishCameraTrack 设置为 true,发布麦克风采集的音频和摄像头采集的视频。
  • autoSubscribeAudioautoSubscribeVideo 设置为 true,自动订阅所有音视频流。

Java

// 创建 ChannelMediaOptions 对象,并进行配置  
ChannelMediaOptions options = new ChannelMediaOptions();  
// 设置用户角色为 BROADCASTER (主播) 或 AUDIENCE (观众)  
options.clientRoleType = Constants.CLIENT_ROLE_BROADCASTER;  
// 设置频道场景为 BROADCASTING (直播场景)  
options.channelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING;  
// 发布麦克风采集的音频  
options.publishMicrophoneTrack = true;  
// 发布摄像头采集的视频  
options.publishCameraTrack = true;  
// 自动订阅所有音频流  
options.autoSubscribeAudio = true;  
// 自动订阅所有视频流  
options.autoSubscribeVideo = true;  
// 使用临时 Token 和频道名加入频道,uid 为 0 表示引擎内部随机生成用户名  
// 成功后会触发 onJoinChannelSuccess 回调  
mRtcEngine.joinChannel(token, channelName, 0, options);  

设置远端视图​

调用 setupRemoteVideo 方法初始化远端用户视图,同时设置远端用户的视图在本地显示属性。你可以通过 onUserJoined 回调获取远端用户的 uid

Java

private void setupRemoteVideo(int uid) {
    FrameLayout container = findViewById(R.id.remote_video_view_container);
    SurfaceView surfaceView = new SurfaceView (getBaseContext());
    surfaceView.setZOrderMediaOverlay(true);
    container.addView(surfaceView);
    // 将 SurfaceView 对象传入声网实时互动 SDK,设置远端视图
    mRtcEngine.setupRemoteVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_FIT, uid));
}

实现常用回调​

根据使用场景,定义必要的回调。以下示例代码展示如何实现 onJoinChannelSuccessonUserJoinedonUserOffline 回调。

Java

// 成功加入频道回调
@Override
public void onJoinChannelSuccess(String channel, int uid, int elapsed) {
    super.onJoinChannelSuccess(channel, uid, elapsed);
    runOnUiThread(() -> {
        Toast.makeText(MainActivity.this, "Join channel success", Toast.LENGTH_SHORT).show();
    });
}

// 远端用户或主播加入当前频道回调
@Override
public void onUserJoined(int uid, int elapsed) {
    runOnUiThread(() -> {
        // 当远端用户加入频道后,显示指定 uid 的远端视频流
        setupRemoteVideo(uid);
    });
}

// 远端用户或主播离开当前频道回调
@Override
public void onUserOffline(int uid, int reason) {
    super.onUserOffline(uid, reason);
    runOnUiThread(() -> {
        Toast.makeText(MainActivity.this, "User offline: " + uid, Toast.LENGTH_SHORT).show();
    });
}

开始音视频互动​

onCreate 中调用一系列方法加载界面布局、检查 App 是否获取实时互动所需权限,并加入频道开始音视频互动。

Java

// 成功加入频道回调
@Override
public void onJoinChannelSuccess(String channel, int uid, int elapsed) {
    super.onJoinChannelSuccess(channel, uid, elapsed);
    runOnUiThread(() -> {
        Toast.makeText(MainActivity.this, "Join channel success", Toast.LENGTH_SHORT).show();
    });
}

// 远端用户或主播加入当前频道回调
@Override
public void onUserJoined(int uid, int elapsed) {
    runOnUiThread(() -> {
        // 当远端用户加入频道后,显示指定 uid 的远端视频流
        setupRemoteVideo(uid);
    });
}

// 远端用户或主播离开当前频道回调
@Override
public void onUserOffline(int uid, int reason) {
    super.onUserOffline(uid, reason);
    runOnUiThread(() -> {
        Toast.makeText(MainActivity.this, "User offline: " + uid, Toast.LENGTH_SHORT).show();
    });
}

结束音视频互动​

按照以下步骤结束音视频互动:

  1. 调用 stopPreview 停止视频预览。

  2. 调用 leaveChannel 离开当前频道,释放所有会话相关的资源。

  3. 调用 destroy 销毁引擎,并释放奥帕 SDK 中使用的所有资源。

    警告

    调用 destroy 后,你将无法再使用 SDK 的所有方法和回调。如需再次使用实时音视频互动功能,你必须重新创建一个新的引擎。详见初始化引擎。

    Java

    @Override
    protected void onDestroy() {
      super.onDestroy();
      if (mRtcEngine != null) {
        // 停止本地视频预览
        mRtcEngine.stopPreview();
        // 离开频道
        mRtcEngine.leaveChannel();
        mRtcEngine = null;
        // 销毁引擎
        RtcEngine.destroy();
      }
    }
    

测试 App​

按照以下步骤测试直播 App:

  1. 开启 Android 设备的开发者选项,打开 USB 调试,通过 USB 连接线将 Android 设备接入电脑,并在 Android 设备选项中勾选你的 Android 设备。

  2. 在 Android Studio 中,点击 图片 (Sync Project with Gradle Files) 进行 Gradle 同步。

  3. 待同步成功后,点击 图片 (Run 'app') 开始编译。片刻后,App 便会安装到你的 Android 设备上。

  4. 启动 App,授予录音和摄像头权限,如果你将用户角色设置为主播,便会在本地视图中看到自己。

  5. 使用第二台 Android 设备,重复以上步骤,在该设备上安装 App、打开 App 加入频道,观察测试结果:

    • 如果两台设备均作为主播加入频道,则可以看到对方并且听到对方的声音。
    • 如果两台设备分别作为主播和观众加入,则主播可以在本地视频窗口看到自己;观众可以在远端视频窗口看到主播、并听到主播的声音。

图片

后续步骤​

在完成音视频互动后,你可以阅读以下文档进一步了解:

  • 本文的示例使用了临时 Token 加入频道。在测试或生产环境中,为保证通信安全,奥帕推荐从服务器中获取 Token,详情请参考[使用 Token 鉴权]。
  • 如果你想要实现极速直播场景,可以在实时音视频互动的基础上,通过修改观众端的延时级别为低延时 (AUDIENCE_LATENCY_LEVEL_LOW_LATENCY) 实现。详见[实现极速直播]

相关信息​

本节提供了额外的信息供参考。

示例项目​

奥帕提供了开源的实时音视频互动示例项目供你参考,你可以前往下载或查看其中的源代码。

⚠️ **GitHub.com Fallback** ⚠️