本文转自:http://blog.csdn.net/jyt0551/article/details/58714595

EasyPusher是一款非常棒的推送客户端。稳定、高效、低延迟,音视频同步等都特别好。装在安卓上可作为一款单兵设备来用。说到单兵,在项目中通常都需要边传边录的功能,因此后来EasyPusher也加入了该特性。该文章将结合代码来阐述下这个功能是如何实现的。

EasyPusher在设置里增加了相关选项来方便开启和关闭本地录像功能,如下图所示,在设置里勾选后就可以推送的同时进行录像了。

EasyPusher用来实现录像的类叫做EasyMuxer,该类对安卓系统的MediaMuxer进行了一些封装,专门对从MediaCodec编码出来的音视频数据进行录像,同时实现了录像自动分段存储的功能。其构造函数如下:

public EasyMuxer(String path, long durationMillis) {
mFilePath = path;
this.durationMillis = durationMillis;
Object mux = null;
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
mux = new MediaMuxer(path + "-" + index++ + ".mp4", MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
mMuxer = (MediaMuxer) mux;
}
}

构造函数有两个参数,第一个参数为本次录像的文件路径;第二个参数为单个文件最大时长,用来做录像分段(由于MediaMuxerJELLY_BEAN_MR2以上才加入,因此这里做了判断,低于该版本的系统直接忽略了)。函数内部创建了一个MediaMuxer对象,用来实现真正的录像存储功能。

MediaMuxer在录像之前需要加入Video Track和Audio Track,EasyMuxuer进行了如下封装。

    public synchronized void addTrack(MediaFormat format, boolean isVideo) {
// now that we have the Magic Goodies, start the muxer
if (mAudioTrackIndex != -1 && mVideoTrackIndex != -1)
throw new RuntimeException("already add all tracks"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
int track = mMuxer.addTrack(format);
if (VERBOSE)
Log.i(TAG, String.format("addTrack %s result %d", isVideo ? "video" : "audio", track));
if (isVideo) {
mVideoFormat = format;
mVideoTrackIndex = track;
if (mAudioTrackIndex != -1) {
if (VERBOSE)
Log.i(TAG, "both audio and video added,and muxer is started");
mMuxer.start();
mBeginMillis = System.currentTimeMillis();
}
} else {
mAudioFormat = format;
mAudioTrackIndex = track;
if (mVideoTrackIndex != -1) {
mMuxer.start();
mBeginMillis = System.currentTimeMillis();
}
}
}
}

这个函数有两个参数,第一个为要加入的媒体流的MediaFormat,可通过MediaCodec获取到,第二个表示加入的是Video还是Audio。从代码可以看到,在音频和视频都添加成功后会自动启动录像,这样做的好处是应用层的音视频的处理完全可以独立,不用操心录像的事情,减少了代码耦合性。同时,录像启动的时候会记录下来开始时间,后面需要用这个开始时间来计算文件时长。

录像启动后,接下来就应该保存数据了。我们再继续看下面的函数封装。


public synchronized void pumpStream(ByteBuffer outputBuffer, MediaCodec.BufferInfo bufferInfo, boolean isVideo) {
if (mAudioTrackIndex == -1 || mVideoTrackIndex == -1) {
Log.i(TAG, String.format("pumpStream [%s] but muxer is not start.ignore..", isVideo ? "video" : "audio"));
return;
}
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
// The codec config data was pulled out and fed to the muxer when we got
// the INFO_OUTPUT_FORMAT_CHANGED status. Ignore it.
} else if (bufferInfo.size != 0) {
if (isVideo && mVideoTrackIndex == -1) {
throw new RuntimeException("muxer hasn't started");
} // adjust the ByteBuffer values to match BufferInfo (not needed?)
outputBuffer.position(bufferInfo.offset);
outputBuffer.limit(bufferInfo.offset + bufferInfo.size); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
mMuxer.writeSampleData(isVideo ? mVideoTrackIndex : mAudioTrackIndex, outputBuffer, bufferInfo);
}
if (VERBOSE)
Log.d(TAG, String.format("sent %s [" + bufferInfo.size + "] with timestamp:[%d] to muxer", isVideo ? "video" : "audio", bufferInfo.presentationTimeUs / 1000));
} if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
if (VERBOSE)
Log.i(TAG, "BUFFER_FLAG_END_OF_STREAM received");
} if (System.currentTimeMillis() - mBeginMillis >= durationMillis) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
if (VERBOSE)
Log.i(TAG, String.format("record file reach expiration.create new file:" + index));
mMuxer.stop();
mMuxer.release();
mMuxer = null;
mVideoTrackIndex = mAudioTrackIndex = -1;
try {
mMuxer = new MediaMuxer(mFilePath + "-" + index++ + ".mp4", MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
addTrack(mVideoFormat, true);
addTrack(mAudioFormat, false);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

需要注意的是音视频的处理分别是在不同的线程中进行的,因此上面这两个函数都是需要加锁的。

pumpStream方法会把从MediaCodec得到的音视频数据,保存到录像文件中。第一个参数表示媒体数据;第二个参数表示帧格式信息;第三个参数表示是音频还是视频。我们之前说到,音视频可能在不同的线程进行采集的,因此可能会有不同步的情况,比如音频已经开始了,视频还没出来。那这时候的音频数据,实际上是在录像开始之前pump,那它就不会保存下来;当录像开始后,muxer会通过writeSampleData方法来写入媒体数据。

同时,这个函数会判断是否已经达到了最大文件时长,是的话,会更换文件再存储。这里先把muxer关闭,再创建新的,并使用之前的MediaFormat来加入音视频轨道信息。为什么可以这样?因为这里音视频的媒体信息没有发生更改,所以完全是可以复用的。

最后,录像结束后,需要调用release来释放资源并生产最后一个完整的MP4文件。

    public synchronized void release() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
if (mMuxer != null) {
if (mAudioTrackIndex != -1 && mVideoTrackIndex != -1) {
if (VERBOSE)
Log.i(TAG, String.format("muxer is started. now it will be stoped."));
try {
mMuxer.stop();
mMuxer.release();
} catch (IllegalStateException ex) {
ex.printStackTrace();
}
}
}
}
}

release函数很简单,无需再介绍了。

结合音视频线程,函数调用的流程图如下:

Created with Raphaël 2.1.0视频视频MuxerMuxer音频音频创建EasyMuxer将EasyMuxer传递给音频获取到了MediaFormatAddTrack获取到了MediaFormatAddTrack启动录像pumpStreampumpStream更换文件线程结束线程结束Release

经测试,EasyMuxer非常稳定、好用。一个小时的测试,基本没有出现录像丢失和文件损坏的情况。详细代码参见Github。

项目地址:

https://github.com/EasyDarwin/EasyPusher_Android

APP下载:

https://fir.im/EasyPusher

获取更多信息

邮件:support@easydarwin.org

WEB:www.EasyDarwin.org

Copyright © EasyDarwin.org 2012-2017

安卓Android手机直播推送同步录像功能设计与实现源码的更多相关文章

  1. EasyPusher安卓Android手机直播推送之RTSP流媒体协议流程

    EasyPusher移动端推送同我们平时用的RTSP直播推送流程一样,都是采用标准RTSP/RTP推送流程:ANNOUNCE->SETUP->PLAY->RTP/RTCP->T ...

  2. EasyPusher安卓Android手机直播推送之MediaCodec 硬编码H264格式

    本文转自Holo的博客:http://blog.csdn.net/u013758734/article/details/50834770 最近在研究EasyDarwin的Push库EasyPusher ...

  3. 基于EasyDarwin EasyPusher实现Android手机直播推送功能

    EasyPusher直播推送在之前就已经稳定支持了Windows.Linux.ARM上的RTSP直播推送功能,配合EasyDarwin开源流媒体服务器,延时基本在1s以内,这个技术方案经过一年多时间, ...

  4. EasyDarwin开源手机直播方案:EasyPusher手机直播推送,EasyDarwin流媒体服务器,EasyPlayer手机播放器

    在不断进行EasyDarwin开源流媒体服务器的功能和性能完善的同时,我们也配套实现了目前在安防和移动互联网行业比较火热的移动端手机直播方案,主要就是我们的 EasyPusher直播推送项目 和 Ea ...

  5. EasyDarwin开源手机直播方案:EasyPusher手机直播推送,EasyDarwin流媒体server,EasyPlayer手机播放器

    在不断进行EasyDarwin开源流媒体server的功能和性能完好的同一时候,我们也配套实现了眼下在安防和移动互联网行业比較火热的移动端手机直播方案,主要就是我们的 EasyPusher直播推送项目 ...

  6. EasyRTMP安卓Android手机直播之AAC采集、编码与RTMP推送

    本文转自EasyDarwin团队Kim的博客:http://blog.csdn.net/jinlong0603/article/details/52963378 EasyRTMP Android版de ...

  7. EasyPusher手机直播推送是如何实现后台直播推送的

    本文由EasyDarwin开源团队成员John提供:http://blog.csdn.net/jyt0551/article/details/52276062 EasyPusher Android是使 ...

  8. EasyRTMP手机直播推送rtmp流flash无法正常播放问题

    本文转自EasyDarwin团队Kim的博客:http://blog.csdn.net/jinlong0603/article/details/52960750 问题简介 EasyRTMP是EasyD ...

  9. Android本地消息推送

    项目介绍:cocos2dx跨平台游戏 项目需求:实现本地消息推送,需求①:定点推送:需求②:根据游戏内逻辑实现推送(比如玩家体力满时,需要计算后到点推送):需求③:清理后台程序或重启后依然能够实现本地 ...

随机推荐

  1. 创建 Image

    本节演示如何通过 Web GUI 和 CLI 两种方法创建 Image. OpenStack 为终端用户提供了 Web UI(Horizon)和命令行 CLI 两种交换界面.两种方式我们都要会用. 可 ...

  2. luogu 1354 房间最短路问题 线段与直线相交 最短路

    题目链接 题目描述 在一个长宽均为10,入口出口分别为(0,5).(10,5)的房间里,有几堵墙,每堵墙上有两个缺口,求入口到出口的最短路经. 输入输出格式 输入格式: 第一排为n(n<=20) ...

  3. Vue.js实战:初识Vue.js

    一.Vue.js是什么 简单小巧的核心,渐进式技术栈,足以应付任何规模的应用. 简单小巧指的是Vue.js 压缩后大小仅有17KB 所谓渐进式(Progressive)就是你一步一步,有阶段性地来使用 ...

  4. 总结下web开发中基础性的常识

    一,HTML/5 1,浏览器渲染过程 主流浏览器渲染过程叫法有区别,但是主要流程还是相同的.Gecko 将视觉格式化元素组成的树称为“框架树”.每个元素都是一个框架.WebKit 使用的术语是“呈现树 ...

  5. ZSTU 4241 圣杯战争(ST表+二分)

    题目链接  ZSTU 4241 问题转化为有很多区间,现在每次给定一个区间求这个区间和之前所有区间中的某一个的交集的最大长度. 强制在线. 首先我们把所有的区间预处理出来. 然后去重(那些被包含的小区 ...

  6. Codeforces 576D Flights for Regular Customers(矩阵加速DP)

    题目链接  Flights for Regular Customers 首先按照$d$的大小升序排序 然后分成$m$个时刻,每条路径一次处理过来. $can[i][j]$表示当前时刻$i$能否走到$j ...

  7. Codeforces 196C Paint Tree(贪心+极角排序)

    题目链接 Paint Tree 给你一棵n个点的树和n个直角坐标系上的点,现在要把树上的n个点映射到直角坐标系的n个点中,要求是除了在顶点处不能有线段的相交. 我们先选一个在直角坐标系中的最左下角的点 ...

  8. 洛谷——P1078 文化之旅

    P1078 文化之旅 题目描述 有一位使者要游历各国,他每到一个国家,都能学到一种文化,但他不愿意学习任何一种文化超过一次(即如果他学习了某种文化,则他就不能到达其他有这种文化的国家).不同的国家可能 ...

  9. CentOS6.7源码安装MySQL5.6

    1.源码安装MySQL5.6 # CentOS6操作系统安装完成后,默认会在/etc目录下存在一个my.cnf, # 强制卸载了mysql-libs之后,my.cnf就会消失 rpm -qa | gr ...

  10. luogu P2158 [SDOI2008]仪仗队

    题目描述 作为体育委员,C君负责这次运动会仪仗队的训练.仪仗队是由学生组成的N * N的方阵,为了保证队伍在行进中整齐划一,C君会跟在仪仗队的左后方,根据其视线所及的学生人数来判断队伍是否整齐(如下图 ...