Android之间互相的录屏直播 --点对点传输(tcp长连接发送h264)
来源:互联网 发布:外卖软件下载 编辑:程序博客网 时间:2024/05/22 09:01
前言
- 转载请注明出处 ,来自:
http://blog.csdn.net/baidu_33546245/article/details/78670220
- 近期准备学习一些流媒体的资料.因此找到了来疯直播这个开源项目,研究了段时间,对来疯的代码架构佩服的五体投地.
感谢来疯直播,先附上来疯直播的github地址
于是在来疯直播的基础上,自己添加了一个利用tcp点对点推送的代码.. 因为只是个学习的demo,所以很多地方,只用了一些超简单的代码进行交互.
这个只能算是一个初级demo. 延时基本在1s以内,只记录下自己学习的过程.偏向于学习.demo代码在文章最后.
另外,本人才疏学浅,,如有各种理解错误,还望各位大神指出…把这个调研过程分为4部分,
(1),简单的了解h264的数据结构.
(2),学习来疯直播的代码,在来疯的架构上添加点对点传输代码.
(3),tcp传输的交互过程.
(4),实时播放h264数据.
- 1,h264的数据结构
1,关于sps和pps: 根据Android develop上的描述,简单概括:sps是序列参数集(Sequence Parametar
Sets),pps是图片参数集(Picture Parametar Sets)参考:https://developer.android.google.cn/reference/android/media/MediaCodec.html
2,关于IDR帧:
I帧:帧内编码帧是一种自带全部信息的独立帧,无需参考其它图像便可独立进行解码,视频序列中的第一个帧始终都是I帧.
IDR帧: I和IDR帧都是使用帧内预测的。它们都是同一个东西而已,在编码和解码中为了方便,要首个I帧和其他I帧区别开,所以才把第一个首个I帧叫IDR,这样就方便控制编码和解码流程。
IDR帧的作用是立刻刷新,使错误不致传播,从IDR帧开始,重新算一个新的序列开始编码。而I帧不具有随机访问的能力,这个功能是由IDR承担。总结:I帧就是一个记录了图片去全部信息的帧,而IDR帧不仅记录了图片的全部信息,而且播放器解析到IDR帧时会根据配置信息立刻刷新.比如直播过程中,当编码器的帧的长宽发生变化,如果中间没有IDR帧,播放器就会继续按错误的配置信息解码,从而不能正确的解析数据.
参考:http://blog.csdn.net/jammg/article/details/52357245
3,Android MediaCodec产生的h264数据
经实际debug测试,Android产生的每一个nal是以(0x00,0x00,0x00,0x01)开头的,(有文章说 这是RTP
格式的H264的特征,未能确定)..根据查询资料得知:
Sps 开头【0x00, 0x00, 0x00, 0x01, 0x67】
Pps 开头【0x00,0x00, 0x00, 0x01, 0x68】
IDR 开头【0x00, 0x00, 0x00, 0x01,0x65】根据来疯直播中的判断条件得知:来疯中的KeyFrame 既指IDR帧.
private boolean isKeyFrame(byte[] frame) { if (frame.length < 1) { return false; } // 5bits, 7.3.1 NAL unit syntax, // H.264-AVC-ISO_IEC_14496-10.pdf, page 44. // 7: SPS, 8: PPS, 5: I Frame, 1: P Frame int nal_unit_type = (frame[0] & 0x1f); return nal_unit_type == IDR; }
参考:https://www.cnblogs.com/lidabo/p/5384073.html
- 4,我们需要的数据格式: 根据以上描述,我们可以得知,如果点对点传输H264的裸流的话,需要的数据格式如下:
Sps + Pps + IDR + ……. + Sps + Pps + IDR + …
经过查看来疯打包flv的代码,我们做出如下修改…(手动把h264的头部给加回去,可能有更优的算法可以实现,也在学习研究…)
private byte[] header = {0x00, 0x00, 0x00, 0x01}; public void analyseVideoDataonlyH264(ByteBuffer bb, MediaCodec.BufferInfo bi) { bb.position(bi.offset); bb.limit(bi.offset + bi.size); ArrayList<byte[]> frames = new ArrayList<>(); boolean isKeyFrame = false; while (bb.position() < bi.offset + bi.size) { byte[] frame = annexbDemux(bb, bi); if (frame == null) { SopCastLog.e(SopCastConstant.TAG, "annexb not match."); break; } // ignore the nalu type aud(9) if (isAccessUnitDelimiter(frame)) { continue; } // for pps if (isPps(frame)) { mPps = frame; continue; } // for sps if (isSps(frame)) { mSps = frame; continue; } // for IDR frame if (isKeyFrame(frame)) { isKeyFrame = true; } else { isKeyFrame = false; } frames.add(header);//本来是加flv的头,改为加h264的头 frames.add(frame); } if (mPps != null && mSps != null && mListener != null && mUploadPpsSps) { if (mListener != null) { mListener.onSpsPps(mSps, mPps); } mUploadPpsSps = false; } if (frames.size() == 0 || mListener == null) { return; } int size = 0; for (int i = 0; i < frames.size(); i++) { byte[] frame = frames.get(i); size += frame.length; } byte[] data = new byte[size]; int currentSize = 0; for (int i = 0; i < frames.size(); i++) { byte[] frame = frames.get(i); System.arraycopy(frame, 0, data, currentSize, frame.length); currentSize += frame.length; } if (mListener != null) { mListener.onVideo(data, isKeyFrame); } }
2,在来疯直播的基础上添加逻辑:
1,来疯直播执行流程:
数据采集 -> 打包 ->进入缓存队列 ->发送
看了流程,想必大家已经知道,我们需要修改,打包和发送两个环节.好在来疯的架构是特别好的,我们可以很方便的实现来疯的Packer和Sender接口,
自定义逻辑来满足我们自己的需求.2,实现TcpPacker,
如果回调Sps,Pps则发送,
如果回调的帧是IDR帧,则在前面加上Sps和Pps..
@Override public void onSpsPps(byte[] sps, byte[] pps) { ByteBuffer byteBuffer = ByteBuffer.allocate(sps.length + pps.length + 8); byteBuffer.put(header); byteBuffer.put(sps); byteBuffer.put(header); byteBuffer.put(pps); mSpsPps = byteBuffer.array(); packetListener.onSpsPps(mSpsPps); // onSpsPps()回调没有参与发送逻辑,想要发送这些信息需要回调onPacket(); packetListener.onPacket(mSpsPps, HEADER); isHeaderWrite = true; }
@Override public void onVideo(byte[] video, boolean isKeyFrame) { if (packetListener == null || !isHeaderWrite) { return; } int packetType = INTER_FRAME; if (isKeyFrame) { isKeyFrameWrite = true; packetType = KEY_FRAME; } //确保第一帧是关键帧,避免一开始出现灰色模糊界面 if (!isKeyFrameWrite) { return; } ByteBuffer bb; if (isKeyFrame) { bb = ByteBuffer.allocate(4 + video.length + mSpsPps.length); bb.put(mSpsPps); bb.put(video); } else { bb = ByteBuffer.allocate(4 + video.length ); bb.put(video); } packetListener.onPacket(bb.array(), packetType); }
3,实现TcpSender:
- 没有什么特别注意的,就是一个普通的Tcp连接
3,tcp传输:
1,为了传输h264数据,我们需要在两个设备之间建立一个长连接,这里采用ServerSoket,为了确保数据在tcp建立成功后,才开始传输,我自定义了一个逻辑,当播放端初始化成功后,
会给推送端发送一个OK的字符串,而推送端收到OK的字符串后,才会开启发送线程,进行发送数据(有点类似Rtmp的握手)2,为了保证播放的连贯性:
我们在写某段字节前,会在这段字节前加上字节数组的长度,这样我们就可以保证,播放端每读到一帧数据,就可以立刻把数据写给播放代码..
public class EncodeV1 { //`发送时加上byte数组长度 private byte[] buff; //要发送的某一帧的byte数组 public EncodeV1(byte[] buff) { this.buff = buff; } public byte[] buildSendContent() { if (buff == null || buff.length == 0) { return null; } ByteBuffer bb = ByteBuffer.allocate(4 + buff.length); bb.put(ByteUtil.int2Bytes(buff.length)); bb.put(buff); return bb.array(); }}
//在播放端读取时,先读4个字节获取某一帧的byte数组长度;再根据长度去读字节,值得注意的是一次read很可能读不完指定字节,需要根据已读到的字节长度去判断是否读完,如果没有读完,则需要再次去读指定字节,直到读够为止... readByte()方法就是处理这种情况的. while (startFlag) { byte[] length = readByte(InputStream, 4); if (length.length == 0){ continue; } int buffLength = bytesToInt(length); byte[] buff = readByte(InputStream, buffLength); listener.acceptBuff(buff); }
private byte[] readByte(InputStream is, int readSize) throws IOException { byte[] buff = new byte[readSize]; int len = 0; int eachLen = 0; ByteArrayOutputStream baos = new ByteArrayOutputStream(); while (len < readSize) { eachLen = is.read(buff); if (eachLen != -1) { len += eachLen; baos.write(buff, 0, eachLen); } else { break; } if (len < readSize) { buff = new byte[readSize - len]; } } byte[] b = baos.toByteArray(); baos.close(); return b; }
4,实时播放h264数据:
- 1,我们首先在播放端建立代码结构,(思路如下)
(1), Server去监听端口号 ->
(2),建立长连接 ->
(3),在长连接中根据我们在字节数组前加的标记去读取每一帧数据 ->
(4),将数据放入到一个容器中,容器选择:ArrayBlockQueue,一个自带阻塞的队列,
private ArrayBlockingQueue<byte[]> mPlayQueue; private String TAG = "NormalPlayQueue"; private static final int NORMAL_FRAME_BUFFER_SIZE = 150; //缓存区大小 public NormalPlayQueue() { mPlayQueue = new ArrayBlockingQueue<byte[]>(NORMAL_FRAME_BUFFER_SIZE, true); } public byte[] takeByte() { try { return mPlayQueue.take(); } catch (InterruptedException e) { Log.e(TAG,"take bytes exception" + e.toString()); return null; } } public void putByte(byte[] bytes) { try { mPlayQueue.put(bytes); } catch (InterruptedException e) { Log.e(TAG, "put bytes exception" + e.toString()); } }
(5),遍历容器,将每一帧数据数据写入播放代码中..
播放逻辑参考这位大神的逻辑,修改了一下,每读到一个帧的byte数组,就把字节写进去播放..
原地址
最后
调用参考来疯直播的代码,直接创建出TcpPacker,TcpSender
然后把这两个设置给StreamController,
private void startRecord() { TcpPacker packer = new TcpPacker();// FlvPacker flvPacker = new FlvPacker();// packer.initAudioParams(AudioConfiguration.DEFAULT_FREQUENCY, 16, false); mVideoConfiguration = new VideoConfiguration.Builder().build(); setVideoConfiguration(mVideoConfiguration); setRecordPacker(packer); tcpSender = new TcpSender(ip, port); tcpSender.setSenderListener(this); tcpSender.setVideoParams(mVideoConfiguration); tcpSender.connect(); LocalSender localSender = new LocalSender(); setRecordSender(tcpSender); startRecording(); }
最后附上发送端也就是数据采集端,和播放端demo的github地址:
数据采集端的地址
播放端的地址
需要注意一下:数据采集端要发送的ip写死在了代码里…
- Android之间互相的录屏直播 --点对点传输(tcp长连接发送h264)
- android tcp 长连接
- TCP的连接传输
- 两个 select 之间内容互相的传输
- 基于Apache mina 的android 客户端tcp长连接实现
- TCP的传输连接管理
- iOS和Android的点对点连接
- iOS和Android的点对点连接
- TCP接受和发送程序以及长连接的处理方法
- activeMq的点对点发送
- TCP的长连接与短连接
- TCP的长连接与短连接
- TCP的长连接与短连接
- TCP的长连接和短连接
- TCP的长连接与短连接
- TCP的长连接和短连接
- 两个Docker容器之间创建一个点对点的连接
- 客户端C和服务器S之间建立一个TCP连接,该连接总是以1KB的最大段长发送TCP段,客户端C有足够的数据要发送。当拥塞窗口为16KB的时候发生超时,如果接下来的4个RTT往返时间内的TCP段的传输是成
- Scrapy使用shell命令报错scrape shell TypeError: 'float' object is not iterable
- Linux系统进程控制编程(三)——exec函数族的使用
- linux中安装hive的步骤以及关于jline报错的问题
- Java ClassLoader初探
- Spring学习(三)之依赖注入实现
- Android之间互相的录屏直播 --点对点传输(tcp长连接发送h264)
- netty源码分析之-Channel注册流程详解(8)
- 小白学Git(3)——添加远程库(github)
- C语言字符数组赋初值
- AI技术与伦理
- excel之列联表分析
- c语言的自增自减练习
- maven的相关配置
- yum 安装报错 File "/usr/bin/yum", line 30