使用 Agora 为Android APP添加视频直播

视频互动直播是当前比较热门的玩法,我们经常见到有PK 连麦、直播答题、一起 KTV、电商直播、互动大班课、视频相亲等。

本文将演示如何通过声网Agora 视频 SDK 在 Android 端实现一个视频直播应用。点击这里注册声网账号后,开发者每个月可获得 10000 分钟的免费使用额度,可实现各类实时音视频场景。

话不多说,我们开始动手实操。

一些前提条件

一、 通过开源Demo,体验视频直播

可能有些人,还不了解我们要实现的功能最后是怎样的。所以我们在 GitHub上提供一个开源的基础视频直播示例项目,在开始开发之前你可以通过该示例项目体验视频直播的体验效果。
Github:GitHub - Meherdeep/agora-android-live-streaming

在这里,我添加了两个直播流,同时可以让多个观众订阅它。

二、 视频直播的技术原理

我们在这里要实现的是视频直播,Agora 的视频直播可以实现互动效果,所以也经常叫互动直播。你可以理解为是多个用户通过加入同一个频道,实现的音视频的互通,而这个频道的数据,会通过声网的 Agora SD-RTN 实时网络来进行低延时传输的。

需要特别说明的是,Agora互动直播不同于视频通话。视频通话不区分主播和观众,所有用户都可以发言并看见彼此;而互动直播的用户分为主播和观众,只有主播可以自由发言,且被其他用户看见。
下图展示在 App 中集成 Agora 互动直播的基本工作流程:

实现互动直播的步骤如下:

  1. 设置角色:互动直播频道中,用户角色可以是主播或者观众。主播在频道内发布音视频流,观众仅可订阅音视频流。
  2. 获取 Token:当 App 客户端加入频道时,你需要通过 Token 验证用户身份。App 客户端向 App 服务器发送请求,并获取 Token,然后在客户端加入频道时验证用户身份。
  3. 加入频道:调用 joinChannel 创建并加入频道。使用同一频道名称的 App 客户端默认加入同一频道。
  4. 在频道内发布和订阅音视频:加入频道后,角色为主播的 App 客户端可以发布音视频。对于角色为观众的客户端,如果想要发布音视频,可以调用 setClientRole 切换用户角色。

App 客户端加入频道需要以下信息:

  • 频道名称:用于标识直播频道的字符串。
  • App ID:Agora 随机生成的字符串,用于识别你的 App,可从 Agora 控制台获取,(Agora控制台链接:Console
  • 用户ID:用户的唯一标识。你需要自行设置用户 ID,并确保它在频道内是唯一的。
  • Token:在测试或生产环境中,你的 App 客户端会从你的服务器中获取 Token。为方便快速测试,你也可以获取临时 Token。临时 Token 的有效期为 24 小时。

三、 开发环境

声网Agora SDK 的兼容性良好,对硬件设备和软件系统的要求不高,开发环境和测试环境满足以下条件即可:
• Android SDK API Level >= 16
• Android Studio 2.0 或以上版本
• 支持语音和视频功能的真机
• App 要求 Android 4.1 或以上设备

以下是本文的开发环境和测试环境:

开发环境

• Windows 10 家庭中文版
• Java Version SE 8
• Android Studio 3.2 Canary 4

测试环境

• Samsung Nexus (Android 4.4.2 API 19)
• Mi Note 3 (Android 7.1.1 API 25)

如果你此前还未接触过声网 Agora SDK,那么你还需要做以下准备工作:

• 注册一个声网账号,进入后台创建 AppID、获取 Token,详细方法可参考这篇教程;(这篇教程:404 - 知乎
• 下载声网官方最新的互动直播SDK;(互动直播SDK链接:https://docs.agora.io/cn/All/downloads?platform=All%20Platforms#SDK%20Downloads)

四、 项目设置

1. 实现互动直播之前,参考如下步骤设置你的项目:

如需创建新项目,在 Android Studio里,依次选择 Phone and Tablet > Empty Activity,创建 Android 项目。(创建 Android 项目链接:https://developer.android.com/studio/projects/create-project)
创建项目后,Android Studio会自动开始同步 gradle。请确保同步成功再进行下一步操作。

2. 集成SDK, 本文推荐使用gradle方式集成Agora SDK:

a. 在 /Gradle Scripts/build.gradle(Project: ) 文件中添加如下代码,以添加 jcenter依赖:

buildscript {
     repositories {
         ...
         jcenter()
     }
     ...
}
 
  allprojects {
     repositories {
         ...
         jcenter()
     }
}

b. 在 /Gradle Scripts/build.gradle(Module: .App) 文件中添加如下代码,将 Agora 视频 SDK 集成到你的 Android 项目中:

...
dependencies {
 ...
 // x.y.z,请填写具体的 SDK 版本号,如:3.5.0。
 // 通过发版说明获取最新版本号。
 implementation 'io.agora.rtc:full-sdk:x.y.z'
//本例使用布局相关设置constraintlayout
implementation  'androidx.constraintlayout:constraintlayout:2.0.4'
}

3. 权限设置

在 /App/Manifests/AndroidManifest.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" />

4. 导入Agora相关的类

在/app/src/main/java/com/agora/samtan/agorabroadcast/VideoActivity文件中,加入如下代码:

package com.agora.samtan.agorabroadcast;
import io.agora.rtc.Constants;
import io.agora.rtc.IRtcEngineEventHandler;
import io.agora.rtc.RtcEngine;
import io.agora.rtc.video.VideoCanvas;
import io.agora.rtc.video.VideoEncoderConfiguration;

5. 设置Agora账号信息

在/app/src/main/res/values/strings.xml文件中,将你的AppID填写到private_App_id中:

<resources>
    ……
<string name="private_App_id">填写位置</string>
……
</resources>

五、 客户端实现

本节介绍如何使用Agora视频SDK在你的App里实现视频直播的几个小贴士:

1. 检查并获取必要权限

启动应用程序时,检查是否已在App中授予了实现视频直播所需的权限。在onCreate函数中调用如下代码:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        int MY_PERMISSIONS_REQUEST_CAMERA = 0;
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO}, MY_PERMISSIONS_REQUEST_CAMERA);

        }
}

2. 实现互动直播逻辑

打开你的App,创建RtcEngine实例,启用视频后加入频道。如果本地用户是主播,则将本地视频发布到用户界面下方的视图中。如果另一主播加入该频道,你的App会捕捉到这一加入事件,并将远端视频添加到用户界面右上角的视图中。
互动直播的API使用时序见下图:

按照以下步骤实现该逻辑:

a) 初始化RtcEngine
RtcEngine类包含应用程序调用的主要方法,调用RtcEngine的接口最好在同一个线程进行,不建议在不同的线程同时调用。

目前Agora Native SDK只支持一个RtcEngine实例,每个应用程序仅创建一个RtcEngine对象。RtcEngine类的所有接口函数,如无特殊说明,都是异步调用,对接口的调用建议在同一个线程进行。所有返回值为int型的API,如无特殊说明,返回值0为调用成功,返回值小于0为调用失败。

在VideoActivity文件中,通过initializeAgoraEngine用于初始化RtcEngine的方法:

    private void initalizeAgoraEngine() {
        try {
            mRtcEngine = RtcEngine.create(getBaseContext(), getString(R.string.private_App_id), mRtcEventHandler);
        } catch (Exception e) {
            e.printStackTrace();
        }
}

另外,有个重要的IRtcEngineEventHandler接口类用于SDK向应用程序发送回调事件通知,应用程序通过继承该接口类的方法获取SDK的事件通知。

接口类的所有方法都有缺省(空)实现,应用程序可以根据需要只继承关心的事件。在回调方法中,应用程序不应该做耗时或者调用可能会引起阻塞的API(如SendMessage),否则可能影响SDK的运行。内容如下:

private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler()
{
    /**Reports a warning during SDK runtime.
     * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/
    @Override
    public void onWarning(int warn)
    {
        Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn)));
    }
 
    /**Reports an error during SDK runtime.
     * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/
    @Override
    public void onError(int err)
    {
        Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err)));
        showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err)));
    }
 
    /**Occurs when a user leaves the channel.
     * @param stats With this callback, the Application retrieves the channel information,
     *              such as the call duration and statistics.*/
    @Override
    public void onLeaveChannel(RtcStats stats)
    {
        super.onLeaveChannel(stats);
        Log.i(TAG, String.format("local user %d leaveChannel!", myUid));
        showLongToast(String.format("local user %d leaveChannel!", myUid));
    }
 
    /**Occurs when the local user joins a specified channel.
     * The channel name assignment is based on channelName specified in the joinChannel method.
     * If the uid is not specified when joinChannel is called, the server automatically assigns a uid.
     * @param channel Channel name
     * @param uid User ID
     * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/
    @Override
    public void onJoinChannelSuccess(String channel, int uid, int elapsed)
    {
        Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid));
        showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid));
        myUid = uid;
        joined = true;
        handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                join.setEnabled(true);
                join.setText(getString(R.string.leave));
            }
        });
    }
 
    @Override
    public void onRemoteAudioStats(io.agora.rtc.IRtcEngineEventHandler.RemoteAudioStats remoteAudioStats) {
        statisticsInfo.setRemoteAudioStats(remoteAudioStats);
        updateRemoteStats();
    }
 
    @Override
    public void onLocalAudioStats(io.agora.rtc.IRtcEngineEventHandler.LocalAudioStats localAudioStats) {
        statisticsInfo.setLocalAudioStats(localAudioStats);
        updateLocalStats();
    }
 
    @Override
    public void onRemoteVideoStats(io.agora.rtc.IRtcEngineEventHandler.RemoteVideoStats remoteVideoStats) {
        statisticsInfo.setRemoteVideoStats(remoteVideoStats);
        updateRemoteStats();
    }
 
    @Override
    public void onLocalVideoStats(io.agora.rtc.IRtcEngineEventHandler.LocalVideoStats localVideoStats) {
        statisticsInfo.setLocalVideoStats(localVideoStats);
        updateLocalStats();
    }
 
    @Override
    public void onRtcStats(io.agora.rtc.IRtcEngineEventHandler.RtcStats rtcStats) {
        statisticsInfo.setRtcStats(rtcStats);
    }
};

所以,在我们的initialize函数中,我们将mRtcEventHandler作为参数之一传递给了create方法,这设置了一系列回调事件,每当用户加入频道或离开频道时就会触发这些事件。

private IRtcEngineEventHandler mRtcEventHandler = new IRtcEngineEventHandler() {

        @Override
        public void onUserJoined(final int uid, int elapsed) {
            super.onUserJoined(uid, elapsed);
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    setupRemoteVideo(uid);
                }
            });
        }

        @Override
        public void onUserOffline(int uid, int reason) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    onRemoteUserLeft();
                }
            });
        }
    };

b) 设置频道场景和角色
setChannelProfile()是一个使用我们AgoraRtcEngine对象引用的方法。Agora提供了各种配置文件,可以通过该方法调用并集成到应用中。

setClientRole()方法,将用户的角色设置为主播或观众(默认)。这个方法应该在加入频道之前调用。加入频道后可以再次调用,切换客户端角色。

为了方便体验互动直播中主播角色和观众角色的效果,我们将在我们的MainActivity类中添加两个方法:
• 当用户从单选按钮中选择一个选项时,将调用第一个方法。我们将相应地设置一个变量。我们将其设置为一个值,该值将确定用户是主播还是观众。

public void onRadioButtonClicked(View view) {
        boolean checked = ((RadioButton) view).isChecked();
        switch (view.getId()) {
            case R.id.host:
                if (checked) {
                    channelProfile = Constants.CLIENT_ROLE_BROADCASTER;
                }
                break;
            case R.id.audience:
                if (checked) {
                    channelProfile = Constants.CLIENT_ROLE_AUDIENCE;
                }
                break;
        }
}

• 然后我们实现一个在用户提交详细信息时调用的函数。在这里,我们将获得我们需要的所有详细信息,并将它们发送到下一个activity。

public void onSubmit(View view) {
        EditText channel = (EditText) findViewById(R.id.channel);
        String channelName = channel.getText().toString();
        Intent intent = new Intent(this, VideoActivity.class);
        intent.putExtra(channelMessage, channelName);
        intent.putExtra(profileMessage, channelProfile);
        startActivity(intent);
}

c) 开始视频
setupVideoProfile()函数用于定义视频需要渲染的方式。你可以对帧速率、比特率、方向、镜像模式和降级偏好等属性使用自己的自定义配置。

private void setupVideoProfile() {
        mRtcEngine.enableVideo();

        mRtcEngine.setVideoEncoderConfiguration(new VideoEncoderConfiguration(VideoEncoderConfiguration.VD_640x480, VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15,
                VideoEncoderConfiguration.STANDARD_BITRATE,
                VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_FIXED_PORTRAIT));
}

d) 设置本地视频
setupLocalVideo()函数用于从我们的AgoraRtcEngine中引用setupLocalVideo方法,我们通过它为我们的本地用户设置一个在直播流中使用的表面视图:

private void setupLocalVideo() {
        FrameLayout container = (FrameLayout) findViewById(R.id.local_video_view_container);
        SurfaceView surfaceView = RtcEngine.CreateRendererView(getBaseContext());
        surfaceView.setZOrderMediaOverlay(true);
        container.addView(surfaceView);
        mRtcEngine.setupLocalVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_FIT, 0));
    }

e) 加入频道
频道是人们在同一个视频通话中的公共空间。joinChannel()方法可以这样调用:

private void joinChannel() {
        mRtcEngine.joinChannel(token, channelName, "Optional Data", 0);
}

该方法需要四个参数才能成功运行:
• Token:建议对在生产环境中运行的所有RTE APP进行Token身份验证。更多关于声网Agora平台基于令牌的认证信息,请参见https://docs.agora.io/cn/Video/token?platform=All%20Platforms。
• 频道名称:需要一个字符串,让用户进入视频通话。
• 可选信息:这是一个可选字段,你可以通过它传递有关频道的其他信息。
• uid:每个加入频道的用户的唯一ID。如果传入0或null值,Agora会自动为每个用户分配一个uid。

注意:此项目仅供参考和开发环境使用,不适用于生产环境。建议对在生产环境中运行的所有RTE APP进行Token身份验证。

本例中初始化App,调用核心方法来创建并加入Agora直播频道。在VideoActivity文件中,在onCreate函数后添加如下代码:

package com.agora.samtan.agorabroadcast;
import android.content.Intent;
import android.graphics.PorterDuff;
import android.os.Bundle;
import android.util.Log;
import android.view.SurfaceView;
import android.view.View;   ;//;.;
import android.widget.FrameLayout;
import android.widget.ImageView;
import androidx.Appcompat.App.AppCompatActivity;
import io.agora.rtc.Constants;
import io.agora.rtc.IRtcEngineEventHandler;
import io.agora.rtc.RtcEngine;
import io.agora.rtc.video.VideoCanvas;
import io.agora.rtc.video.VideoEncoderConfiguration;

public class VideoActivity extends AppCompatActivity {
  private RtcEngine mRtcEngine;
  private String channelName;
  private int channelProfile;
  
  @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_video);

        Intent intent = getIntent();
        channelName = intent.getStringExtra(MainActivity.channelMessage);
        channelProfile = intent.getIntExtra(MainActivity.profileMessage, -1);

        if (channelProfile == -1) {
            Log.e("TAG: ", "No profile");
        }

        initAgoraEngineAndJoinChannel();
    }

}

我们宣布了一个名为initAgoraEngineAndJoinChannel的方法,它将调用直播过程中所需的所有其他方法。我们还定义了事件处理程序,它将决定当远程用户加入或离开或静音时调用哪些方法。

private void initAgoraEngineAndJoinChannel() {
        initalizeAgoraEngine();
        mRtcEngine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING);
        mRtcEngine.setClientRole(channelProfile);
        setupVideoProfile();
        setupLocalVideo();
        joinChannel();
}

f) 当远端主播加入频道时添加远端界面
在VideoActivity文件中,initializeAndJoinChannel函数后加入如下代码:

    private void setupRemoteVideo(int uid) {
        FrameLayout container = (FrameLayout) findViewById(R.id.remote_video_view_container);
        SurfaceView surfaceView = RtcEngine.CreateRendererView(getBaseContext());
        container.addView(surfaceView);
        mRtcEngine.setupRemoteVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_FIT, uid));
    }

g) 释放资源
最后,我们添加onDestroy方法来释放我们使用过的资源。相关代码如下:

@Override
    protected void onDestroy() {
        super.onDestroy();

        leaveChannel();
        RtcEngine.destroy();
        mRtcEngine = null;
}

至此,完成,运行看看效果。拿两部手机安装编译好的App,加入同一个频道名,分别选择主播角色和观众角色,如果2个手机都能看见同一个自己,说明你成功了。

如果你在开发过程中遇到问题,可以访问论坛提问与声网工程师交流(链接:https://rtcdeveloper.agora.io/)
也可以访问后台获取更进一步的技术支持(链接:Agora Support

推荐阅读
相关专栏
SDK 教程
164 文章
本专栏仅用于分享音视频相关的技术文章,与其他开发者和声网 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。