前言

  • 转载请注明出处 ,来自:

暂时两篇:
(1) Android之间互相的录屏直播 –点对点传输(tcp长连接发送h264)(一)
http://blog.csdn.net/baidu_33546245/article/details/78670220
(2)Android之间互相的录屏相机直播-(增加声音直播)(二)
https://blog.csdn.net/baidu_33546245/article/details/80503091

  • 近期准备学习一些流媒体的资料.因此找到了来疯直播这个开源项目,研究了段时间,对来疯的代码架构佩服的五体投地.
  • 感谢来疯直播,先附上来疯直播的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 Frameint 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 ppsif (isPps(frame)) {mPps = frame;continue;}// for spsif (isSps(frame)) {mSps = frame;continue;}// for IDR frameif (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. 1,来疯直播执行流程:
    数据采集 -> 打包 ->进入缓存队列 ->发送
    看了流程,想必大家已经知道,我们需要修改,打包和发送两个环节.好在来疯的架构是特别好的,我们可以很方便的实现来疯的Packer和Sender接口,
    自定义逻辑来满足我们自己的需求.

  2. 2,实现TcpPacker,
    如果回调Sps,Pps则发送,
    如果回调的帧是IDR帧,则在前面加上Sps和Pps..

   @Overridepublic 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;}
  @Overridepublic 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)(一)相关推荐

  1. android使用RTMP实现录屏直播推送音视频(已在享学公众号发表)

    目录 一.MediaCodec编码音频 创建音频编码器,指定AAC格式,采样率44100,码率64_000,单声道: 创建AudioRecord录音对象,设置参数与编码器对应: 启动编码器和录音器: ...

  2. android4.2屏幕录像,android——使用自带录屏工具进行屏幕录像

    在做开源项目的时候,想传一个gif效果图上去.但是,要有连贯的动画效果.所以,就想到先录制视频,然后视频转gif.但是,用第三录屏软件总是不完美. 那么,怎么办呢? android4.4 提供了自带录 ...

  3. Android实现录屏直播(二)需求才是硬道理之产品功能调研

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

  4. Android PC投屏简单尝试(录屏直播)2—硬解章(MediaCodec+RMTP)

    代码地址 :https://github.com/deepsadness/MediaProjectionDemo 想法来源 上一边文章的最后说使用录制的Api进行录屏直播.本来这边文章是预计在5月份完 ...

  5. android实现录像功能吗,Android实现录屏直播(一)ScreenRecorder的简单分析

    应项目需求瞄准了Bilibili的录屏直播功能,基本就仿着做一个吧.研究后发现Bilibili是使用的MediaProjection 与 VirtualDisplay结合实现的,需要 Android ...

  6. android 录屏自动运行,Android实现录屏直播+远程控制(二)

    前言 前面Android实现录屏直播+远程控制(一)的文章说到Android5.0的录屏直播实现方式,今天来说说实现录屏直播的另外一种方案 1 启动一个后台服务实现录屏 1.服务的创建这边就不在赘述了 ...

  7. c语言安卓录屏,手写 Android 录屏直播

    简介 观看手游直播时,我们观众端看到的是选手的屏幕上的内容,这是如何实现的呢?这篇博客将手写一个录屏直播 Demo,实现类似手游直播的效果 获取屏幕数据很简单,Android 系统有提供对应的服务,难 ...

  8. iOS录屏直播(一)初识ReplayKit

    Morris_2019.05.08 本篇主要功能: 认识ReplayKit框架 RPScreenRecorder实现在应用内录屏功能 RPPreviewViewController查看录屏内容 RPB ...

  9. iOS录屏直播(四)主App和宿主App数据共享,通信功能实现

    文章目录 CFNotificationCenterGetDarwinNotifyCenter 发送通知 接收通知 注意事项 遗留问题 补充 Morris_ 2019.06.17 上一篇总结了一下App ...

最新文章

  1. 如何从零开始搭建 CI/CD 流水线
  2. [How TO]-git/gerrit配置方法
  3. php获取当月天数及当月第一天及最后一天、上月第一天及最后一天实现方法
  4. 小波的秘密9_图像处理应用:图像增强
  5. python中itertools groupby函数是干嘛的_Python-如何使用itertools.groupby()?
  6. sgi 之heap, priority_queue
  7. 开放世界下的混合域适应 ——面向真实自然场景下的全新迁移学习范式
  8. 通过流程构建组织的【个人能力】和【团队能力】
  9. STL之multiset简介
  10. cocos2d-x之SimpleGame分析
  11. iOS DLNA
  12. 苹果Mac全能视频播放器:Playr
  13. 编译OpenJDK8:error: control reaches end of non-void function [-Werror=return-type]
  14. 磁力泵的结构特点及使用与维修
  15. 计算机配置无线网卡在哪能找到,台式机无线网卡驱动位置在哪
  16. OpenOffice安装及使用
  17. 一个老程序员在情人节对中国软件业的致言!(转)
  18. 有个程序员的老公是种什么体验,嫁给程序员,我超级后悔!
  19. 心形线(Java语言实现) 原理与具体实现
  20. ASCII码对照表:

热门文章

  1. git查看提交修改的文件列表
  2. 【智能家居市场】华为hilink
  3. TP6(thinkphp6)队列与延时队列
  4. ckplayer 播放器参数详细设置
  5. 20220321在MT6739的android8.1下调试GPIO引脚
  6. 【渝粤教育】国家开放大学2018年春季 0169-22T工程制图基础 参考试题
  7. 航空公司客户画像和客户价值分析
  8. JavaScript 数组 函数 对象
  9. DNS(域名系统)详解
  10. 抓包工具anyproxy使用总结