add-live-streaming-to-your-android-app-using-agora-featured1024×512 121 KB

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

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

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

一些前提条件

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

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

588×1228 79.9 KB

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

二、 视频直播的技术原理

我们在这里要实现的是视频直播,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控制台链接:Dashboard

  • 用户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链接:下载 - 全部产品 - 文档中心 - 声网Agora

四、 项目设置

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使用时序见下图:

image822×1048 106 KB

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

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/) 1 也可以访问后台获取更进一步的技术支持(链接:Agora Support 1

使用 Agora 为Android APP添加视频直播的更多相关文章

  1. uni-app仿抖音APP短视频+直播+聊天实例|uniapp全屏滑动小视频+直播

    基于uniapp+uView-ui跨端H5+小程序+APP短视频|直播项目uni-ttLive. uni-ttLive一款全新基于uni-app技术开发的仿制抖音/快手短视频直播项目.支持全屏丝滑般上 ...

  2. 超强教程:如何搭建一个 iOS 系统的视频直播 App?

    现今,直播市场热火朝天,不少人喜欢在手机端安装各类直播 App,便于随时随地观看直播或者自己当主播.作为开发者来说,搭建一个稳定性强.延迟率低.可用性强的直播平台,需要考虑到部署视频源.搭建聊天室.优 ...

  3. Android中直播视频技术探究之---视频直播服务端环境搭建(Nginx+RTMP)

    一.前言 前面介绍了Android中视频直播中的一个重要类ByteBuffer,不了解的同学可以 点击查看 到这里开始,我们开始动手开发了,因为我们后续肯定是需要直播视频功能,然后把视频推流到服务端, ...

  4. iOS视频直播初窥:高仿<喵播APP>

    视频直播初窥 视频直播,可以分为 采集,前处理,编码,传输, 服务器处理,解码,渲染 采集: iOS系统因为软硬件种类不多, 硬件适配性比较好, 所以比较简单. 而Android端市面上机型众多, 要 ...

  5. Android 视频直播 SDK

    Android 视频直播 SDK接入说明 一.名词解释 分辨率:用于计算机视频处理的图像,以水平和垂直方向上所能显示的像素数来表示分辨率.常见视频分辨率的有1080P即1920x1080,720P即1 ...

  6. Android实现录屏直播(三)MediaProjection + VirtualDisplay + librtmp + MediaCodec实现视频编码并推流到rtmp服务器

    请尊重分享成果,转载请注明出处,本文来自Coder包子哥,原文链接:http://blog.csdn.net/zxccxzzxz/article/details/55230272 Android实现录 ...

  7. 带着问题写React Native原生控件--Android视频直播控件

    最近在做的采用React Native项目有一个需求,视频直播与直播流播放同一个布局中,带着问题去思考如何实现,能更容易找到问题关键点,下面分析这个控件解决方法: 现在条件:视频播放控件(开源的ijk ...

  8. Android音乐、视频类APP常用控件:DraggablePanel(2)

     Android音乐.视频类APP常用控件:DraggablePanel(2) 附录文章1主要演示了如何使用DraggablePanel 的DraggableView.DraggablePanel ...

  9. 视频直播APP开发分析

    视频直播APP开发到目前为止都还是热门的一个行业,而且发展到现在直播的种类非常多,很多行业都打入了直播行业,再也不是单纯的人物直播这么单一了.视频直播APP开发行业就像是吃螃蟹,来的早的人不懂如何吃, ...

  10. Android视频直播解决方案(rstp、udp)

    做局域网视频直播有两种方案,通过rstp或udp协议. 1.rstp协议,网络上有个开源项目,基于Android,且这个项目也是一个服务端,里面也集成了http访问页面,可以通过http或者rstp直 ...

随机推荐

  1. css实现图片在div中居中的效果

    利用图片的margin属性将图片水平居中,利用div的padding属性将图片垂直居中. 结构代码同上: css代码如下: div {width:300px; height:150px; paddin ...

  2. 使用signalr不使用连接服务器和前台的js的方法

    1:使用这种方式,,就不需要前后台链接的js 2:新建一个empty的MVC项目 3:新建一个controller和index.html 4: 新建一个signalr 集线器类名为PersonHub, ...

  3. Mysql学习:3、sql数据类型及命令

    1.sql功能分类: 2.常见数据类型: 3.sql命令: DDL命令: a.创建数据库: create database testdatabase(数据库名称) character set utf8 ...

  4. MySQL innodb存储引擎的数据存储结构

    InnoDB存储引擎的数据存储结构 B+ 树 为什么选择B+树? 因为B+树的叶子节点存储了所有的data,所以它的非叶子节点可以存储更多的key,使得树更矮:树的高度几乎就是I/O的次数,所以选择更 ...

  5. Control M 复习笔记

    记录一些复习过程想通的知识点 1.我们教案中看到的图基本都是复平面,从来没有看到过所谓s域或z域,不同的稳定区域只是因为从复平面到函数中存在不同的映射过程(s函数和z函数). s函数是纯粹的频域,也就 ...

  6. nRF51822蓝牙学习 进程记录 3:蓝牙协议学习--简单使用

    三天打鱼两天晒网,又学起了蓝牙,不过还好的是终于开始学习蓝牙协议部分了. 但是,一看起来增加了蓝牙协议的例程,真是没头绪啊.本身的教程资料解说太差了,看青风的蓝牙原理详解也是一头雾水. 经过不断地看各 ...

  7. 扫描线总结【线段树特殊性质,没有pushdown、query操作】

    扫描线 题意 多个矩阵求交集,线段树的特殊操作,非常特殊的情况,一堆证明之后,就没有pushdown操作. 没有pushdown操作,也没有query操作,直接tr[1].len. 亚特兰蒂斯 由于点 ...

  8. nodejs 利用URL和querystring获取get查询参数

    为深入理解request的get url信息及参数传递,利用URL和querystring获取对应的信息,测试成功,记录如下: 1.编写server.js文件 http=require("h ...

  9. vue中的普通函数与箭头函数以及this关键字

    普通函数 普通函数指的是用function定义的函数 var hello = function () { console.log("Hello, Fundebug!"); } 箭头 ...

  10. 1004 Counting Leaves (30分)

    今天在热心网友的督促下完成了第一道PAT编程题. 太久没有保持训练了,整个人都很懵. 解题方法: 1.读懂题意 2.分析重点 3.确定算法 4.代码实现 该题需要计算每层的叶子节点个数,所以选用BFS ...