IBigerBiger的成长之路

移动端直播开发(四)播放与弹幕评论

上一篇已经介绍了关于RTMP推流相关的知识,那么推完流后视频服务器就会对推流进行转发,那么这一篇主要就是介绍下,关于移动端播放与弹幕评论相关的知识。

一.播放

关于播放其实一般情况我们都是使用第三方的播放库,因为这些库相对来说支持各种不同的协议比如RTMP,HLS或者其他的视频协议,那我们这里就使用Bilibili开发并开源的IJKPlayer播放器。

首先认识下IJKPlayer


IJKPlayer 是一个基于 ffplay 的轻量级 Android/iOS 视频播放器。API 易于集成;编译配置可裁剪,方便控制安装包大小;支持 硬件加速解码,更加省电。而DanmakuFlameMaster 架构清晰,简单易用,支持多种高效率绘制方式选择,支持多种自定义功能设置。

实现的特性有:

  • 移除 FFmpeg 中不常用的特性以减小体积。
  • 对一些在线视频播放的 BUG 修复
  • 支持安卓 API 9-22 和 iOS 5.1.1-8.3.X
  • 使用各种平台原生的渲染方式进行优化

IJKPlayer 由Bilibili开发并开源,这里是IJKPlayer的地址

IJKPlayer的编译

如果紧紧是使用而已的话,其实可以选择不对IJKPlayer进行编译,直接添加官方依赖:

1
2
3
4
5
6
7
8
9
10
11
12
# required, enough for most devices.
compile 'tv.danmaku.ijk.media:ijkplayer-java:0.6.3'
compile 'tv.danmaku.ijk.media:ijkplayer-armv7a:0.6.3'
# Other ABIs: optional
compile 'tv.danmaku.ijk.media:ijkplayer-armv5:0.6.3'
compile 'tv.danmaku.ijk.media:ijkplayer-arm64:0.6.3'
compile 'tv.danmaku.ijk.media:ijkplayer-x86:0.6.3'
compile 'tv.danmaku.ijk.media:ijkplayer-x86_64:0.6.3'
# ExoPlayer as IMediaPlayer: optional, experimental
compile 'tv.danmaku.ijk.media:ijkplayer-exo:0.6.3'

然后使用就可以了,当然我们这里选择对IJKPlayer,这样可以对整个流程进行了解,因为可能以后会对IJKPlayer进行定制

首先我们得确定是否配置了NDK与SDK环境

NDK和SDK的下载就不贴上来,确定本机有这两者之后

配置环境变量

1
open -e .bash_profile

在文件后加入

1
2
3
4
export ANDROID_DIR=/Users/admin/Documents
export ANDROID_SDK=$ANDROID_DIR/sdk
export ANDROID_NDK=$ANDROID_DIR/android-ndk-r12b
export PATH=$PATH:$ANDROID_NDK:$ANDROID_SDK/tools:$ANDROID_SDK/platform-tools

保存并关闭更新刚配置的环境变量

1
source .bash_profile

接下来验证是否配置成功
输入

1
2
3
4
ndk-build
adb
echo ANDROID_NDK
echo ANDROID_SDK

只要这些都有反应,说明配置已经成功了

接下来就可以对IJKPlayer进行编译了

首先需要下载IJKPlayer源码了

1
git clone https://github.com/Bilibili/ijkplayer.git ijkplayer-android

接下来进行编译

1
2
3
4
5
6
7
8
9
10
cd ijkplayer-android
./init-android.sh
cd config
rm module.sh
ln -s module-default.sh module.sh
cd android/contrib
./compile-ffmpeg.sh clean
./compile-ffmpeg.sh all
cd ..
./compile-ijk.sh all

编译好后,我们可以找到ijkplayer文件夹


图1 ijkplayer文件

这个是可以直接导入AS的,导入后运行即可


图2 运行截图

选择视频就可以播放了

IJKPlayer的精简优化

进行到这里,我们会发现上面的ijkplayer里面的东西太多了,很多我们是用不到的,所以,我们把不需要的东西剔除掉,留下我们需要的东西,暂时精简一下

首先新建一个Android工程

接下来把ijkplayer-armv7a/src/main/libs下的文件拷贝到新的工程的libs下。


图3 libs

然后把ijkplayer-java/build/outputs/aar/ijkplayer-java-release.aar复制到新工程的libs下


图4 aar

再后面就是把ijkplayer-example/src/main/java/tv中的部门代码拷过来


图5 代码

最后就是修改build.gradle,把so和aar文件依赖添加上去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
apply plugin: 'com.android.application'
android {
compileSdkVersion 24
buildToolsVersion "24.0.0"
defaultConfig {
applicationId "com.example.ijkplayerdemo"
minSdkVersion 15
targetSdkVersion 24
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
}
repositories {
mavenCentral()
flatDir {
dirs 'libs' //this way we can find the .aar file in libs folder
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:24.0.0'
compile 'com.android.support:design:24.0.0'
compile(name: 'ijkplayer-java-release', ext: 'aar')
}

到这里ijkplayer的精简工作就完成了

接下来我们来写个简单的例子验证下

1
2
3
4
5
6
7
8
videoView.setAspectRatio(IRenderView.AR_ASPECT_FIT_PARENT);
videoView.setVideoURI(Uri.parse("http://zv.3gv.ifeng.com/live/zhongwen800k.m3u8"));
videoView.setOnPreparedListener(new IMediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(IMediaPlayer mp) {
videoView.start();
}
});

运行如下


图6 运行截图

那么对于直播的话,我们只需要把直播的视频转发的地址设置进去就好了

如下


图1 直播播放

由于录制视频再转GIF不太方便,所以就单独上一张图了,明白是什么意思就好。

其实很多直播平台也是用的IJKPlayer做的播放器,比如斗鱼呀等等

对于现在的直播来说,弹幕评论也是直播平台中很重要的一部分,所以接下来我们来探究下关于移动端关于弹幕评论的实现

二.弹幕评论

对于弹幕评论,其实客户端我们可以想到的方法就是轮询来不断的获取最新的评论数据,但是可想而知不停的网络请求访问,会对整个客户端的性能带来一定的问题,而且也会导致对于手机电量的消耗增大,所以我们得想出另外的方案来解决这个问题,自然就是参考现在比较成熟的PC网站是如何实现这部分的需求的,其实现在有部分的网站已经在使用WebSocket来实现实时弹幕的效果了,当然在我们移动端也是可以使用WebScoket的,所以我们就用WebScoket方案来实现弹幕评论

工欲善其事必先利其器,所以我们先了解下什么是WebScoket

WebScoket

WebSocket协议是一种建立在TCP连接基础上的全双工通信的协议,同http一样通过TCP来传输数据,但是它和http最大的不同有两点:1.WebSocket是一种双向通信协议,在建立连接后,WebSocket服务器和Browser/UA都能主动的向对方发送或接收数据,就像Socket一样,不同的是WebSocket是一种建立在Web基础上的一种简单模拟Socket的协议;2.WebSocket需要通过握手连接,类似于TCP它也需要客户端和服务器端进行握手连接,连接成功后才能相互通信。

协议内容组成如下


图2 WebScoket协议内容

WebSocket按上面图中协议规则进行传输,上图称为一个数据帧。

  • FIN,共1位,标记消息是否是最后1帧,1个消息由1个或多个数据帧构成,若消息由1帧构成,起始帧就是结束帧。

  • RSV1,RSV2,RSV3,各1位,预留位,用于自定义扩展。如果没有扩展,各位值为0;如果定义了扩展,即为非0值。如果接收的帧中此处为非0,但是扩展中却没有该值的定义,那么关闭连接。

  • OPCODE,共4位,帧类型,分为控制帧和非控制帧。如果接收到未知帧,接收端必须关闭连接。

    WebSocket的控制帧有3种,关闭帧、Ping帧、Pong帧,关闭帧很好理解,客户端如果收到关闭帧直接关闭连接即可,当然客户端也可以发送关闭帧给服务器端。而Ping帧和Pong帧则是WebSocket的心跳检测,用于保证客户端是在线的,一般来说,只有服务端给客户端发送Ping帧,然后客户端发送Pong帧进行回应,表示自己还在线,可以进行后续通信。

  • MASK,共1位,掩码位,表示帧中的数据是否经过加密,客户端发出的数据帧需要经过掩码处理,这个值都是1。如果值是1,那么Masking-key域的数据就是掩码秘钥,用于解码PayloadData,否则Masking-key长度为0

  • Payload len,7位或者7+16位或者7+64位,表示数据帧中数据大小,这里有好几种情况。

    如果值为0-125,那么该值就是payload data的真实长度。
    如果值为126,那么该7位后面紧跟着的2个字节就是payload data的真实长度。
    如果值为127,那么该7位后面紧跟着的8个字节就是payload data的真实长度。
    长度遵循一个原则,就是用最少的字节表示长度,举个例子,当payload data的真实长度是124时,在0-125之间,必须用7位表示;不允许将这7位表示成126或者127,然后后面用2个字节或者8个字节表示124,这样做就违反了原则。

  • Masking-key ,0或者4个字节,当MASK位为1时,4个字节,否则0个字节。如果MASK值为1,则发出去的数据需要经过加密处理

  • Payload data,其大小是(x+y)个字节,x是Extension data,即扩展数据,y是Application data,即程序数据,扩展数据可能为0。 如果扩展数据不为0,必须提前进行协商,规定其长度,否则是不合法的数据帧。

以上是WebSocket数据传输的帧内容,大致了解即可。

对于WebSocket和前面说的RTMP一样也是有握手协议的过程,接下来我们就看一下WebSocket握手协议

客户端发送get请求协议升级

1
2
3
4
5
6
7
GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key:dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat,superchat
Sec-WebSocket-Version: 13

该请求会在请求头上带上WebSocket的版本号,这里是13,以及客户端随机生成的Sec-WebSocket-Key,服务器端收到后根据这个key进行一些处理,返回一个Sec-WebSocket-Accept的值给客户端。

服务端返回同意升级到WebSocket协议

1
2
3
4
5
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept:s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

收到响应后,响应头中包含Sec-WebSocket-Accept值,该值表示服务器端同意握手,值的计算方式如下:

1
$(Sec-WebSocket-Accept)=BASE64(SHA1($(Sec-WebSocket-Key)+"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))

客户端得到该值后,对本地的Sec-WebSocket-Key进行同样的编码,然后对比,如果相同则可以进行后续处理。

关于WebSocket协议,一般来说,如果是通过https协议开始升级而来的,那么一般是wss://开头,如果是http协议开始升级而来的,那么一般是ws://开头

WebScoket实现弹幕效果

前面说了相关协议的东西,那么接下来就是实现具体的效果了,对于Android平台,我们可以Java-WebSocket库来实现WebSocket通信,当然除了这个还可以选择其他的库,我这里选择Java-WebSocket来实现我们的功能了。

WebScoket服务器搭建

对于WebScoket服务器搭建,可以使用Java API javax.websocket包中的WebSocket相关类(注意Java API只实现了标准的RFC 6455(JSR256),如果你非要选择其它早期草案则需要用Java-WebSocket来实现,在Java-WebSocket中连接协议“Draft_17”就是标准的RFC 6455(JSR256),另外要使用Java API javax.websocket包中的WebSocket相关类要求JDK7及以上,Tomcat 7.0.49及以上):

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@ServerEndpoint(value = "/websocket/{user}")
public class ChatServerEndpoint {
private static Set<Session> sessions = new HashSet<Session>();
private Session session;
@OnOpen
public void open(Session session, @PathParam(value = "user") String user) {
this.session = session;
sessions.add(this.session);
sendToAll(session.getRequestURI() + "进入房间");
System.out.println(session.getRequestURI() + " 进入房间");
}
@OnClose
public void close() {
sessions.remove(session);
sendToAll(session.getRequestURI() + " 离开房间");
System.out.println(session.getRequestURI() + " 离开房间");
}
@OnMessage
public void message(String message) {
sendToAll("[" + session.getRequestURI() + "]" + message);
System.out.println("[" + session.getRequestURI() + "]" + message);
}
private void sendToAll(String text) {
for (Session client : sessions) {
synchronized (client) {
client.getAsyncRemote().sendText(text);
}
}
}
}

这里服务器端的代码主要是消息转发功能,进入,离开,以及用户发送信息都会转发给连接的客户端。

启动tomcat就可以用Android客户端来连接进行聊天、接收推送了。

Android端创建

首先添加Java-WebSocket的依赖如下

1
compile 'org.java-websocket:Java-WebSocket:1.3.0'

接下来就可以创建一个WebSockeClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
WebSocketClient client = new WebSocketClient(new URI(""), new Draft_17()) {
@Override
public void onOpen(ServerHandshake handshakedata) {
Log.e("hxy", "已经连接到服务器【" + getURI() + "】");
}
@Override
public void onMessage(String message) {
Log.e("hxy", "获取到服务器信息【" + message + "】");
}
@Override
public void onClose(int code, String reason, boolean remote) {
Log.e("hxy", "断开服务器连接【" + getURI() + ",状态码: " + code + ",断开原因:" + reason + "】");
}
@Override
public void onError(Exception ex) {
Log.e("hxy", "连接发生了异常【异常原因:" + ex + "】");
}
};
client.connect();

这里我们只有设置前面搭建的服务器端的地址即可,然后就可以监听到连接,断开,异常等信息,同时也可以监听到服务器发出的信息。

同样我们也可以向服务器发送信息,代码如下

1
client.send("message from client");

通过这样就可以实现服务器与客户端的双向通信了

运行如下:


图3 webSocket通讯

可以看到这里客户端A连接服务器端,并发生消息,服务器端转发连接信息与来自客户端A的信息,客户端A接收后将信息显示出来,然后用另外的客户端B进行连接发送信息,客户端A同样可以接收到后将信息显示出来。

所以这里就实现了WebSocket相关的双向通信了,

至于怎么弹幕,有了数据,弹幕只是表现形式咯,这个实现比较简单就不做说明了,其实可以使用同样Bilibili开发并开源的DanmakuFlameMaster

写在后面的话

那么到这里就完成了移动直播相关的基本的问题了,我这边更加着重于对于原理的讲解,而不是把代码贴出来简单的概述而过,跟着这些原理一步一步研究出来显然会对自己提升更大,至于视频直播的滤镜问题,可以参考我写的滤镜系列文章,当然我这里仅仅是实现了直播的整套流程,但是对于直播其实还有很多的问题,这里就不做过多的研究了,有兴趣的朋友可以进行进一步的研究,peace~~~