Reference
概述
实现同屏需要一个服务端和至少一个客户端。服务端是发送音视频数据的,数据来源可以是相机、屏幕捕捉等。客户端是接收音视频数据的,接收到的数据会实时在屏幕上显示。
MediaMirror
是libstreaming
的 demo,实现了服务端通过相机采集数据,并发送数据给客户端,客户端实时在屏幕上显示的功能。
实现同屏的操作是:
1、服务端和客户端连接同一个 AP。
2、服务端安装在 Android 6.0+ 系统上需要手动授权。
3、客户端需要修改 IP 地址指向服务端。
4、先启动服务端,再启动客户端。
MediaMirror
基于libstreaming
,对代码有修改,以下分析基于MediaMirror
。
启动服务端
入口 Activity
代码很少,全部贴出来:
|
|
布局文件中只有一个普通的SurfaceView
,在SurfaceView
回调surfaceCreated(SurfaceHolder holder)
方法时会调用buildSession()
方法,buildSession()
设置了Session
的参数并最终调用SessionBuilder#build()
方法进行初始化。
然后,创建了RtspServer
实例并启动。
SessionBuilder#build()
创建 H.264 视频的对应流并设置给Session
:
|
|
创建 AAC 音频的对应流并设置给Session
:
|
|
H264Stream 构造函数
H264Stream
是VideoStream
的子类,这里调用了VideoStream
的有参构造函数:
|
|
创建了H264Packetizer
实例:
|
|
VideoStream 有参构造函数
VideoStream
是MediaStream
的子类,这里调用了MediaStream
的无参构造函数。
H264Packetizer 构造函数
H264Packetizer
是AbstractPacketizer
的子类,这里调用了AbstractPacketizer
的无参构造函数。
AbstractPacketizer 构造函数
创建了RtpSocket
实例:
|
|
####### RtpSocket 构造函数
创建 UDP 的数据包数组:
|
|
创建MulticastSocket
实例:
|
|
AacStream 构造函数
AacStream
是AudioStream
的子类,这里调用了AudioStream
的无参构造函数。
AudioStream 构造函数
AudioStream
是MediaStream
的子类,这里调用了MediaStream
的无参构造函数。
RtspServer#start()
创建RequestListener
实例:
|
|
RequestListener 构造函数
RequestListener
是一个线程,在构造函数中会创建ServerSocket
实例,并开启这个线程:
|
|
RequestListener#run()
创建WorkerThread
实例,WorkerThread
同样是个线程,创建后立刻开启线程:
|
|
WorkerThread 有参构造函数
创建客户端输入流的缓冲流和输出流:
|
|
创建Session
实例:
|
|
####### WorkerThread#run()
等待客户端连接,如果接收到客户端请求,解析该请求,并做出对应的响应,最后发送响应给客户端。
当客户端断开连接后,停止流传输并释放资源:
|
|
启动客户端
入口 Activity
代码很少,全部贴出来:
|
|
布局文件中只有一个 Android 原生的VideoView
,VideoView
是SurfaceView
的子类,在SurfaceView
回调surfaceCreated(SurfaceHolder holder)
方法时,调用configureMediaPlayer()
方法,该方法中会创建MediaPlayer
实例,并在设置MediaPlayer
的参数后开始准备,最后在onPrepared(MediaPlayer mp)
回调中开始播放。
VideoView
支持RTSP
,RTSP
请求都是自发的。
服务端接收到请求
解析该请求:
|
|
并做出对应的响应:
|
|
最后发送响应给客户端。
|
|
RtspServer#processRequest(Request request)
该方法会根据不同的客户端请求,执行相应处理,并拼接响应报文,最后返回给客户端。
1、当是DESCRIBE
请求时,调用了handleRequest()
方法(该方法创建了Session
,所以该请求是建立同步的入口):
|
|
拼接会话描述信息:
|
|
2、当是SETUP
请求时,会将音视频通道分别映射到客户端指定的端口,如果客户端没有指定端口,则使用默认端口:
|
|
调用了Session#syncStart()
方法,该方法用于开始同步:
|
|
拼接会话描述信息:
|
|
RtspServer#handleRequest(String uri, Socket client)
调用了UriParser#parse()
方法,该方法用于解析客户端传递的参数:
|
|
设置会话的源地址和目的地址:
|
|
Session#syncStart(int id)
开始同步流:
|
|
对于H264
视频,依次调用了H264Stream#start()
->VideoStream#start()
->MediaStream#start()
->MediaStream#encodeWithMediaCodec()
->VideoStream#encodeWithMediaCodec()
->VideoStream#encodeWithMediaCodecMethod1()
。
VideoStream#encodeWithMediaCodecMethod1()
开始相机预览:
|
|
调试编码参数后运行编码器:
|
|
将预览回调的帧数据进行转换,再由编码器编码:
|
|
将数据写入打包器,启动打包器线程:
|
|
####### H264Packetizer#run()
调用了send()
方法发送数据。
######## H264Packetizer#send()
生成NAL
单元LENGTH
:
|
|
生成时间戳和NAL
单元长度:
|
|
如果出现NAL
单元类型为IDR
类型,需要调用AbstractPacketizer#send()
方法发送该数据:
|
|
如果NAL
长度低于设置的最大包大小,会调用AbstractPacketizer#send()
方法发送该数据,否则,拆分NAL
单元为FU-A
单元再调用AbstractPacketizer#send()
方法发送:
|
|
######### AbstractPacketizer#send(int length)
调用了RtpSocket#commitBuffer()
方法:
|
|
########## RtpSocket#commitBuffer(int length)
调用updateSequence()
方法,updateSequence()
方法调用了setLong()
方法,setLong()
方法用于扩充数组索引长度。原来是 1 个字节表示一个索引,则最多可以表示 256 个索引,现在用连续的 2 个字节表示一个索引。
设置UDP
包当前索引对应数据的长度:
|
|
如果UDP
包下一个索引已超过设置的缓冲大小时,从头开始替换:
|
|
如果RtpSocket
线程未启动,则启动该线程:
|
|
########### RtpSocket#run()
调用SenderReport#update()
方法更新数据:
|
|
忽略前 30 帧,之后发送缓冲区中的数据:
|
|
如果UDP
包下一个索引已超过设置的缓冲大小时,从头开始发送:
|
|
############ SenderReport#update(int length, long rtpTimestamp)
更新数据,最后调用了send()
方法:
|
|
############ SenderReport#send(long ntpTimestamp, long rtpTimestamp)
发送RTCP
数据:
|
|