阅读:9975回复:4

【音视频开发】Android使用Mediacodec播放H264 rtsp直播流

楼主#
更多 发布于:2019-07-10 19:53
可以到我的CSDN看该篇文章的其他内容:
https://blog.csdn.net/fanx9339/article/details/80776688?spm=1001.2014.3001.5501
Android的硬解码器MediaCodec相信大家都不会陌生,今天我来给大家分享下使用mediacodec来播放H264的rtsp视频流。

首先介绍下几个概念:

视频文件:
让我们从图片开始,我们用相机拍摄的一张照片,首先呢是有相机的sensor传感器(感光元件)捕捉到光信息转化为电信号。一般的sensor都有明确的规格,分辨率是多大,就是最宽支持多少个像素点,最长支持多少哥像素点。每一个像素点在sensor处理的时候,都是原始的RGB(红绿蓝)颜色格式。这个数据量是非常大的。一张4K分辨率的RGB565原始照片,文件大小就有3840*2160*16/8 = 16Mbyte。然后我们都知道视频其实就是一秒钟内快速抓怕多张照片就变成了视频,每一张照片,我们叫做每一帧,例如一秒钟内抓拍10张照片,即为10帧,也叫10fps(frame per second),一般来讲,帧率达到25fps以上才能感觉不大卡顿。通过我们刚刚的计算,可以想象,在sensor的内部,我们对视频进行编码前,这个数据量是非常大的。如果按照简单的叠加图片来保存视频,那么可想而知,1个半小时时长的视频要占多大的存储空间。根本不现实。所以才有了视频压缩算法和标准,咱们程序员经常挂嘴边的H264,H265都是视频压缩标准之一。

H264:
是国际标准组织制定的一个数字视频编码格式标准,具有比较高的压缩率,目前广泛应用于各大视频多媒体领域。一般称为H.264/AVC。


H264编码是怎么进行的,又是如何能够不改变视频内容和质量,减少视频数据的大小的呢?看图:

图片:H264编码过程.png




由上面图可知,视频数据编码前就是一帧帧(张)彼此单独完整的图片,例如一个4K的未编码的视频,它的每一帧都包含3840*2160个像素点,每一帧(张)都可以单独拿出来,直接播放,视频整体占用空间巨大。而经过H264编码后,除了I帧是完整的,后面所有的P帧都是参考这个I帧的,都是有关联的,解码的过程就是通过反向计算有I帧P帧解码出每一个完整帧,所以当I帧在传输过程中丢失了,那么这个I帧之后到下一个I帧之前的所有P帧都解码错误,也就是我们经常看到的播放视频时候的马赛克现象,因为找不到I帧来做参考了。这样编码使得视频数据大大减小。这就是H264编码。

RTSP:
全称为Real Time Streaming Protocol,即实时流媒体传输协议,属于应用层协议。对流媒体提供播放、暂停、后退、前进的操作。通俗的讲,就是定义了要怎么样把这个这么大的视频流,按照什么样的数据包,怎么分段发送,接收方怎么拼接,怎么排序这个一整套的协议定义。

rtsp协议可以用来发送各种编码格式的视频流,不仅仅H264格式的视频流,发送不同格式的视频流,封装的数据包格式也不一样,这里只关注发送的视频流为h264的情况。

整个播放过程分为以下几个大的步骤:
1.rtsp协议的TCP交互握手部分
2.udp接收媒体数据包
3.组包:将接收到的udp数据包按照格式组包,组合成完整的H264的数据帧
4.将完整的H264的数据帧按照顺序喂给Android MediaCodec硬解码器
5.硬解码器解码完成,渲染到surfaceview进行播放
======================================================================

1.rtsp协议的TCP交互握手部分
从架构上来看,rtsp协议分为服务端和客户端。服务端负责发送视频流数据包,接收端负责接收数据包。从通信内容上面看,rtsp协议分为tcp部分的命令通道(tcp是可靠传输)和udp的视频流通道。客户端首先通过tcp数据包和服务器进行握手,查询,请求视频流,服务器同样tcp回复确认信息,当双方一切都确认好后。客户端开始从服务器接收udp的视频流数据包。rtsp交互(握手)协议主要包括:

图片:RTSP握手概括.png


* C表示客户端,S表示服务端

上面的6个步骤很清晰的介绍了客户端和服务端之间的RTSP交互(握手),可以看到,都是客户端向服务端发一个请求,每次服务端都会回复请求的结果给客户端。下面具体用实际的电脑VLC播放rtsp网络流的抓包数据来讲解:

当我们在VLC播放器中输入链接“rtsp://192.168.8.8:8554/stream”,点击播放后,第一时间播放器作为客户端将创建一个tcp的socket发送,向地址“192.168.8.8”端口“8554”发送如下信息,OPTION命令查询服务端有哪些方法可用。
OPTIONS rtsp://192.168.8.8:8554/stream RTSP/1.0
CSeq: 2
User-Agent: LibVLC/2.2.8 (LIVE555 Streaming Media v2016.02.22)

服务端回复内容,告诉客户端,服务端支持:OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, GET_PARAMETER, SET_PARAMETER这些命令。
RTSP/1.0 200 OK
Cseq: 2
Date: Tue, May 29 2018 01:46:27 BLUECAM_GMT
Public: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, GET_PARAMETER, SET_PARAMETER

接下来,服务端发送DESCRIBE命令查询服务器的媒体的信息:
DESCRIBE rtsp://192.168.8.8:8554/stream RTSP/1.0
CSeq: 3
User-Agent: LibVLC/2.2.8 (LIVE555 Streaming Media v2016.02.22)
Accept: application/sdp

服务器回复当前推送的媒体流信息如下:
RTSP/1.0 200 OK
Cseq: 3
Date: Tue, May 29 2018 01:46:27 BLUECAM_GMT
Content-Type: application/sdp
Content-Length: 258


v=0
o=- 1527558387 1527558388 IN IP4 192.168.8.8
s=streamed by the RTSP server
t=0 0
m=video 0 RTP/AVP 96
a=rtpmap:96 H264/90000
a=control:track0
a=fmtp:96 packetization-mode=1;profile-level-id=64001F;sprop-parameter-sets=Z0IAH5WoFAFuQA==,aM48gA==

接下来客户端知道了服务器的音视频格式,准备设置端口接收媒体流数据:
SETUP rtsp://192.168.8.8:8554/stream/track0 RTSP/1.0
CSeq: 4
User-Agent: LibVLC/2.2.8 (LIVE555 Streaming Media v2016.02.22)
Transport: RTP/AVP;unicast;client_port=50498-50499

服务器收到了SETUP命令,设置相应的推送端口,回复客户端设置结果,这里注意:当客户端发送SETUP命令设置UDP的端口后,服务端回复信息中多了一个Session字段,用来表示当前客户端的唯一性,表面你这个客户端设置的端口,因为rtsp是C/S架构,同一个服务端可以同时对应多个播放客户端,所以后续所有的交互握手,播放暂停,都需要发送该Session端口字段,以便让服务器知道是哪一个服务端要播放/暂停
RTSP/1.0 200 OK
Cseq: 4
Date: Tue, May 29 2018 01:46:27 BLUECAM_GMT
Session: 1675273474520913322
Transport: RTP/AVP;unicast;client_port=50498-50499;server_port=58564-58565

客户端收到了结果,确认无误,向服务端发送播放命令,开始播放:
PLAY rtsp://192.168.8.8:8554/stream RTSP/1.0
CSeq: 5
User-Agent: LibVLC/2.2.8 (LIVE555 Streaming Media v2016.02.22)
Session: 1675273474520913322
Range: npt=0.000-

服务端收到播放命令,开始播放,并且回复客户端播放是否成功,客户端收到以下命令后,说明服务端已经开始通过udp端口发送媒体流了,客户端也可以开始创建udp的socket来接收数据解码渲染播放了:
RTSP/1.0 200 OK
Cseq: 5
Date: Tue, May 29 2018 01:46:27 BLUECAM_GMT
Session: 1675273474520913322

正常播放过程中,客户端需要每隔一段时间发送以下命令,来告知服务器,当前客户端还保持在线。长时间不发GET_PARAMETER命令,服务端会认为该客户端可能已经关闭或者掉线,服务端为了网络和性能,会停止对该客户端的端口发送媒体流数据:
GET_PARAMETER rtsp://192.168.8.8:8554/stream RTSP/1.0
CSeq: 6
User-Agent: LibVLC/2.2.8 (LIVE555 Streaming Media v2016.02.22)
Session: 1675273474520913322

服务端收到GET_PARAMETER命令,回复客户端:
RTSP/1.0 200 OK
Cseq: 6
Date: Tue, May 29 2018 01:46:27 BLUECAM_GMT
Session: 1675273474520913322

播放了一会,不想看了,客户端发送TEARDOWN命令来告诉服务端,请关闭当前客户端的推送
TEARDOWN rtsp://192.168.8.8:8554/stream RTSP/1.0
CSeq: 7
User-Agent: LibVLC/2.2.8 (LIVE555 Streaming Media v2016.02.22)
Session: 1675273474520913322

服务端收到,停止了对该客户端端口的udp数据包发送,并且回复:
RTSP/1.0 200 OK
Cseq: 7
Date: Tue, May 29 2018 01:46:32 BLUECAM_GMT

以上就是一个播放器正常播放一个RTSP视频流的常用交互的完整过程。知道了着整个过程,写成代码也就很简单了,下面给出伪代码:
//创建tcp socket
socket = new Socket();
//设置rtsp服务端的IP地址和端口,上面例子中为:192.168.8.8,8554端口
SocketAddress socketAddress = new InetSocketAddress(playUrlIp, playUrlPort);


socket.connect(socketAddress, 3000);
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
outputStream = socket.getOutputStream();
writer = new BufferedWriter(new OutputStreamWriter(outputStream));


//通过socket发送OPTIONS命令
writer.write(sendOptions());
writer.flush();
//通过socket接收OPTIONS命令的回复
getResponse();

//通过socket发送DESCRIBE命令
writer.write(sendDescribe());
writer.flush();
//通过socket接收DESCRIBE命令的回复
getResponse();
writer.write(sendSetup());
writer.flush();
getResponse();
writer.write(sendPlay());
writer.flush();
getResponse();


2.udp接收媒体数据包
OK,上面提到了,当我们客户端发送了PLAY命令启动播放之后,我提到,这时候客户端可以启动UDP socket来接收
udp的媒体(音视频码流)数据包了。下面代码指示怎么来接收UDP的数据包:

//创建DatagramSocket来接收UDP数据包
dataSocket = new DatagramSocket(videoPort);
dataSocket.setSoTimeout(3000);
//创建接收缓冲区
byte[] receiveByte = new byte[48 * 1024];
//从udp读取的数据长度
int offHeadsize = 0;
//当前帧长度
int frameLen = 0;
//完整帧筛选用缓冲区
byte[] frame = new byte[FRAME_MAX_LEN];
//创建UDP端口的接收包裹对象
dataPacket = new DatagramPacket(receiveByte, receiveByte.length);


//循环从UDP包裹中不停的获取接收的UDP数据
while (isPlaying) {
        //Log.d(TAG, "T=" + test);
        dataSocket.receive(dataPacket);
        offHeadsize = dataPacket.getLength() - 12;
        ... ...
        ... ...
}


3.组包:将接收到的udp数据包按照格式组包,组合成完整的H264的数据帧

下面是重点中的重点,关键中的关键,相信大家都会有疑问,我们从UDP端口中不停的收包,每一包的大小取决于网络MTU单元的大小(一般的MTU单元设置都是1500字节),我们收到了这些数据是什么格式的数据,我们应该怎么处理呢?怎么把这些字节的数据播放出来?

这里大家需要静下来心,要知道这不停的接收的高频率的数据包怎么处理,怎么拼接,我们需要了解RTSP协议在装载H264格式的时候,是怎么进行切分的。怎么样打包发送的。我这里讲个大概原理:
首先我们知道H264编码后的数据也是一帧一帧的,主要包括I,B,SPS,PPS,SEI等数据帧。其中I,B两种类型的帧一般的分辨率情况下,都会远远大于1500字节,而且还要减去UDP包本身带的包头,一个数据包肯定发送不玩,这个时候就需要对这一帧进行切片处理,分成多个包来发送这一帧数据。SPS,PPS,SEI非常小,可以一包直接发完。那么相应的,在接收到这些包的时候,对应I,B帧,我们需要对多个包进行拼接,组合成一个完整的帧。

至于具体的格式,怎么切片,怎么计算,请参考这篇文章,写的非常详细,不懂得可以在帖子回复问我:https://blog.csdn.net/chen495810242/article/details/39207305


4.将完整的H264的数据帧按照顺序喂给Android MediaCodec硬解码器

这一步就是将H264编码后的I帧,P帧数据,通过解码器还原成独立的完整的帧数据,每一帧都是完整的像素点,用YUV表示的颜色数据,严格来说,这里还原成的是NV12格式的颜色数据,surface可以使用这些数据格式直接渲染出像素点颜色,展现出画面。

5.硬解码器解码完成,渲染到surfaceview进行播放
这两步需要讲解的比较少,直接参考代码即可,代码在附件中可以下载。
这样你也可以写出自己的H264播放器了。完全自主的哦,欢迎点赞分享。
GeneROVPlayer.txt(附件文件后缀名改为.java,后续我会更新完整的demo AS工程)

最新喜欢:

NiocoNioco zhaoyf13zhaoyf... yamyam
If you have nothing to lose, then you can do anything.
沙发#
发布于:2019-07-10 20:14
不明觉厉
[url]http://190.lsal.cn/195/1329.gif?0728100424873[/url]
yam
yam
论坛版主
论坛版主
  • 社区居民
  • 优秀斑竹
  • VIP会员
  • 荣誉会员
板凳#
发布于:2019-07-10 20:15
牛逼了
地板#
发布于:2019-07-10 20:36
zhaoyf13:不明觉厉回到原帖
时间仓促,写的不是特别详细,明天修改在写详细点,保证应届毕业生都能看懂理解。
If you have nothing to lose, then you can do anything.
4楼#
发布于:2021-08-12 19:40
最近也在调试rtsp摄像头。遇到几个问题,可以分享一下:

1. 先保证设备能相互访问:adb下能否ping通远程rtsp的IP

2. 调试命令:am start -n com.android.gallery3d/.app.MovieActivity -d URL

3. logcat必备

4. 必要时抓tcpdump日志: adb shell tcpdump -i any -p -s 0 -w /sdcard/rtsp.pcap,然后pull到PC,用wireshark查看,rtsp握手、RTP包等

5. 对比测试:可用的公共源:rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov

6. 对比应用:KMPLAYER,PC和android都可以测一下
游客

返回顶部