请尊重分享成果,转载请注明出处,本文来自Coder包子哥,原文连接:http://blog.csdn.net/zxccxzzxz/article/details/55230272html
Android实现录屏直播(一)ScreenRecorder的简单分析java
Android实现录屏直播(二)需求才是硬道理之产品功能调研git
看到有网友在后台私信和询问录屏这部分推流相关的问题,感受这篇博客早该写完了。事实上除了繁忙的工做加上春节假期一会儿拖了近一个月之久。近期更新了Demo,加入了视频帧推流,须要的朋友能够看看Demo。github
不管是音频仍是视频编码,咱们都须要原始的数据源,拿视频举例子,实际录屏直播也只是将屏幕的每个视图间接的获取充当原始视频帧,和摄像头获取视频帧原理区别不大。现在Android设备几乎都已经知足硬编码的条件了(虽然有好有坏),因此咱们伪装曾经那些兼容性问题都不存在。算法
咱们向服务器发送视频数据的时候,还须要先发送视频参数的数据给服务器以区分咱们的视频源的格式、类型及FLV封装的一些关键信息(File Header / File TAG等如sps / pps等相关的meta data)给解码器。(PS: 关于FLV格式封装等视频编解码的分析推荐看看雷博的相关博文)MediaCodec的细节能够自行查阅官方API。shell
而且推荐这些对我极为有用的文章资料,感谢这些做者的无私分享:服务器
做者自行封装及实现的一个Android实施滤镜、RTMP推流的类库。代码结构须要花点时间理解和读懂,值得深刻学习其中的实现,由于使用MediaProjection / VirtualDisplay 来进行录屏的话,官方并不提供帧率的控制,这须要用到OpenGL ES将VirtualDisplay中的surface进行绘制到MediaCodec中的surface。然而在这个库中做者已经实现了所有的操做,本篇文章也会围绕该库进行大体的分析。网络
在找到上面的类库以前,仍是这位做者给出的思路才可以一点点往OpenGL ES这个坑里跳,越入越深,差点没爬出来。因为做者不方便放出源码,我只能经过他的描述一点点的实现。而且StackOverFlow中网友fadden也给出了相关的思路:controlling-frame-rate-of-virtualdisplay多线程
Google官方给的Demo,基本涵盖了OpenGL的各种用法,好好看看吧。并发
一个Android MediaCodec的超详细的博客,实例Demo很明确。
问题的原因来自工做中某些需求所引发的,接下来我一一描述。
好不容易开发完成录屏直播,结果在低码率或者网络波动大的状况下,不少机型(尤为是小米)在60帧满帧的条件打出来的视频是那样的酸爽,动态的画面简直眼瞎。老板要求改帧率!下降帧率到30看看什么状况,最后实际选择使用了15FPS。
在快速滑动屏幕或者画面变换频繁的状况下改善视频模糊的作法:(参见https://github.com/lakeinchina/librestreaming/issues/11)
性能比起Bilibili仍是要差一些,这里挖个坑,以后再填。
后期设定的方案是将推流放到remote service当中,该service为前台独立进程的service,对主进程的依赖性减弱一些(虽然APP在被杀死的时候也可能被杀死)
经过开启远程服务并与APP的进程进行进程间通讯(IPC),寻求保活的方式花了一段时间,最后对MIUI的系统机制仍是无果,Debug的时候发现MIUI拥有一个PowerKeeper,一旦触发就会对任何后台进程的APP(听说有白名单)进行KillApplication操做,在个人压力测试下,无一应用幸免(包括优化得极其稳定的Bilibili,GooglePlay录屏APP排行第一的AZ ScreenRecorder)。
首先推荐看看这些对我极为有用的文章资料,感谢这些做者的无私分享:
做者自行封装及实现的一个Android实施滤镜、RTMP推流的类库。代码结构须要花点时间理解和读懂,值得深刻学习其中的实现,由于使用MediaProjection / VirtualDisplay 来进行录屏的话,官方并不提供帧率的控制,这须要用到OpenGL ES将VirtualDisplay中的surface进行绘制到MediaCodec中的surface。然而在这个库中做者已经实现了所有的操做,本篇文章也会围绕该库进行大体的分析。
在找到上面的类库以前,仍是这位做者给出的思路才可以一点点往OpenGL ES这个坑里跳,越入越深,差点没爬出来。因为做者不方便放出源码,我只能经过他的描述一点点的实现。而且StackOverFlow中网友fadden也给出了相关的思路:controlling-frame-rate-of-virtualdisplay
Google官方给的Demo,基本涵盖了OpenGL的各种用法,好好看看吧。
一个Android MediaCodec的超详细的博客,实例Demo很明确。
以前阿里云搞活动,12块买了个1核2G / 1M 半年的服务器,正好一直闲置没用,为了完成这篇博客我也真是够拼的了,先按照上述连接搭建一个基于Nginx + RTMP协议的流媒体服务器。搭服务器的目的是为了完成推流的操做,毕竟不想用公司的资源来进行私人的活动。
不少朋友都问推流何时才有,那么今天我就完完整整的将录屏推流这块完善,Android客户端的Demo + 推流服务器的步骤实现,时间有限,只注重实现,代码质量以后重构。
丑话再说在前头,我本着一颗开源分享和学习的心来写博客和Demo,认为好的点赞、评论你们随意,可是本人能力和精力有限,这本属于一个Demo,若是认为太烂没参考价值,那么还请留点口德,默默关闭本页便可,有问题提出来我会回复并以改正,请求勿喷,谢谢~
大概将会包含几个部分:
录屏推流的流程:
客户端原始帧编码为H264裸流(MediaCodec,Android API) —> 再封装为FLV格式的视频流(Java) —> 按照rtmp流媒体协议经过librtmp(JNI + C)推流到RTMP流媒体服务器 —> 客户端播放器解码观看
注意:
Demo省略了librestreaming中的OpenGL处理帧率的过程,也就意味着咱们使用的是MediaCodec直接编码后的数据,并无OpenGL绘制VirtualDisplay映射给MediaCodec.createInputSurface中Surface的这个过程,目的是将流程简单化。OpenGL方面的使用以后我也会介绍说明。
能够看到实现录屏到本地的ScreenRecorder直接经过下面的方法进行音视频写入,而推流的话须要作以修改。
mMuxer.writeSampleData(mVideoTrackIndex, encodedData, mBufferInfo);
FLV的头文件信息发送给服务器后,就能够将咱们的关键帧发送,注意流媒体服务器解析的时候首先要先获得第一帧关键帧才会开始解析后面的视频帧,因此咱们还须要在编码器获取IDR帧的时候进行发送。MediaCodec的INFO_OUTPUT_FORMAT_CHANGED
这个状态能够获取sps / pps,再将数据处理包装后打到FLV的TAG中。
private void sendAVCDecoderConfigurationRecord(long tms, MediaFormat format) { byte[] AVCDecoderConfigurationRecord = Packager.H264Packager.generateAVCDecoderConfigurationRecord(format); int packetLen = Packager.FLVPackager.FLV_VIDEO_TAG_LENGTH + AVCDecoderConfigurationRecord.length; byte[] finalBuff = new byte[packetLen]; Packager.FLVPackager.fillFlvVideoTag(finalBuff, 0, true, true, AVCDecoderConfigurationRecord.length); System.arraycopy(AVCDecoderConfigurationRecord, 0, finalBuff, Packager.FLVPackager.FLV_VIDEO_TAG_LENGTH, AVCDecoderConfigurationRecord.length); RESFlvData resFlvData = new RESFlvData(); resFlvData.droppable = false; resFlvData.byteBuffer = finalBuff; resFlvData.size = finalBuff.length; resFlvData.dts = (int) tms; resFlvData.flvTagType = RESFlvData.FLV_RTMP_PACKET_TYPE_VIDEO; resFlvData.videoFrameType = RESFlvData.NALU_TYPE_IDR; dataCollecter.collect(resFlvData, RESRtmpSender.FROM_VIDEO); }
public static byte[] generateAVCDecoderConfigurationRecord(MediaFormat mediaFormat) { ByteBuffer SPSByteBuff = mediaFormat.getByteBuffer("csd-0"); SPSByteBuff.position(4); ByteBuffer PPSByteBuff = mediaFormat.getByteBuffer("csd-1"); PPSByteBuff.position(4); int spslength = SPSByteBuff.remaining(); int ppslength = PPSByteBuff.remaining(); int length = 11 + spslength + ppslength; byte[] result = new byte[length]; SPSByteBuff.get(result, 8, spslength); PPSByteBuff.get(result, 8 + spslength + 3, ppslength); /** * UB[8]configurationVersion * UB[8]AVCProfileIndication * UB[8]profile_compatibility * UB[8]AVCLevelIndication * UB[8]lengthSizeMinusOne */ result[0] = 0x01; result[1] = result[9]; result[2] = result[10]; result[3] = result[11]; result[4] = (byte) 0xFF; /** * UB[8]numOfSequenceParameterSets * UB[16]sequenceParameterSetLength */ result[5] = (byte) 0xE1; ByteArrayTools.intToByteArrayTwoByte(result, 6, spslength); /** * UB[8]numOfPictureParameterSets * UB[16]pictureParameterSetLength */ int pos = 8 + spslength; result[pos] = (byte) 0x01; ByteArrayTools.intToByteArrayTwoByte(result, pos + 1, ppslength); return result; }
在librestreaming中Packager.java这个类主要就是作了上面这些事。仅两个方法实现,做者代码逻辑清晰易懂,这里就很少说了,再次感谢Lake哥!
能够看到音视频编码线程共用同一个RESFlvDataCollecter接口,负责监听编码线程的一举一动,从接口中将音视频编码帧送到同一个帧队列,发送线程取数据时取到什么就把数据喂给RtmpStreamingSender发送出去。
dataCollecter = new RESFlvDataCollecter() { @Override public void collect(RESFlvData flvData, int type) { rtmpSender.feed(flvData, type); } };
在librestreaming中使用了HandlerThread为WorkHandler提供Looper,经过Handler的消息队列循环机制来控制数据的发送,实际也能够自定义线程,手动管理视频帧收发队列,但涉及到了并发抢占资源的问题,更推荐Handler这种方式,而且Handler处理除了效率高、逻辑清晰,易管理以外还有一个好处,若是使用OpenGL绘制Surface时,正好能够Handler处理其中的异步操做。
不过在Demo中我修改成了一个普通的Runnable任务,run()
中循环处理frameQueue
中的数据。代码以下:
public class RtmpStreamingSender implements Runnable { private static final int MAX_QUEUE_CAPACITY = 50; private AtomicBoolean mQuit = new AtomicBoolean(false); private LinkedBlockingDeque<RESFlvData> frameQueue = new LinkedBlockingDeque<>(MAX_QUEUE_CAPACITY); private final Object syncWriteMsgNum = new Object(); private FLvMetaData fLvMetaData; private RESCoreParameters coreParameters; private volatile int state; private long jniRtmpPointer = 0; private int maxQueueLength = 150; private int writeMsgNum = 0; private String rtmpAddr = null; private static class STATE { private static final int START = 0; private static final int RUNNING = 1; private static final int STOPPED = 2; } public RtmpStreamingSender() { coreParameters = new RESCoreParameters(); coreParameters.mediacodecAACBitRate = 32 * 1024; coreParameters.mediacodecAACSampleRate = 44100; coreParameters.mediacodecAVCFrameRate = 20; coreParameters.videoWidth = 1280; coreParameters.videoHeight = 720; fLvMetaData = new FLvMetaData(coreParameters); } @Override public void run() { while (!mQuit.get()) { if (frameQueue.size() > 0) { switch (state) { case STATE.START: LogTools.d("RESRtmpSender,WorkHandler,tid=" + Thread.currentThread().getId()); if (TextUtils.isEmpty(rtmpAddr)) { LogTools.e("rtmp address is null!"); break; } jniRtmpPointer = RtmpClient.open(rtmpAddr, true); final int openR = jniRtmpPointer == 0 ? 1 : 0; String serverIpAddr = null; if (openR == 0) { serverIpAddr = RtmpClient.getIpAddr(jniRtmpPointer); LogTools.d("server ip address = " + serverIpAddr); } if (jniRtmpPointer == 0) { break; } else { byte[] MetaData = fLvMetaData.getMetaData(); RtmpClient.write(jniRtmpPointer, MetaData, MetaData.length, RESFlvData.FLV_RTMP_PACKET_TYPE_INFO, 0); state = STATE.RUNNING; } break; case STATE.RUNNING: synchronized (syncWriteMsgNum) { --writeMsgNum; } if (state != STATE.RUNNING) { break; } RESFlvData flvData = frameQueue.pop(); if (writeMsgNum >= (maxQueueLength * 2 / 3) && flvData.flvTagType == RESFlvData.FLV_RTMP_PACKET_TYPE_VIDEO && flvData.droppable) { LogTools.d("senderQueue is crowded,abandon video"); break; } final int res = RtmpClient.write(jniRtmpPointer, flvData.byteBuffer, flvData.byteBuffer.length, flvData.flvTagType, flvData.dts); if (res == 0) { if (flvData.flvTagType == RESFlvData.FLV_RTMP_PACKET_TYPE_VIDEO) { LogTools.d("video frame sent = " + flvData.size); } else { LogTools.d("audio frame sent = " + flvData.size); } } else { LogTools.e("writeError = " + res); } break; case STATE.STOPPED: if (state == STATE.STOPPED || jniRtmpPointer == 0) { break; } final int closeR = RtmpClient.close(jniRtmpPointer); serverIpAddr = null; LogTools.e("close result = " + closeR); break; } } } } public void sendStart(String rtmpAddr) { synchronized (syncWriteMsgNum) { writeMsgNum = 0; } this.rtmpAddr = rtmpAddr; state = STATE.START; } public void sendStop() { synchronized (syncWriteMsgNum) { writeMsgNum = 0; } state = STATE.STOPPED; } public void sendFood(RESFlvData flvData, int type) { synchronized (syncWriteMsgNum) { //LAKETODO optimize if (writeMsgNum <= maxQueueLength) { frameQueue.add(flvData); ++writeMsgNum; } else { LogTools.d("senderQueue is full,abandon"); } } } public final void quit() { mQuit.set(true); } }
RtmpStreamingSender.java这个类的大部分方法都引入了librestreaming中RESRtmpSender.java类中的代码实现,只是将其改成了以前所说的线程循环机制,并无用Handler。
RtmpClient中包含了jni的native方法,能够看到有对应了screenrecorderrtmp.h中的几个方法:
public static native long open(String url, boolean isPublishMode); public static native int read(long rtmpPointer, byte[] data, int offset, int size); public static native int write(long rtmpPointer, byte[] data, int size, int type, int ts); public static native int close(long rtmpPointer); public static native String getIpAddr(long rtmpPointer);
咱们可根据RtmpClient.java这个Jni入口类生成 jni的c文件screenrecorderrtmp.h,再到项目的java目录,使用如下命令在同级目录下建立一个jni/screenrecorderrtmp.h 文件。
javah -d jni net.yrom.screenrecorder.rtmp.RtmpClient
接着对应h文件编写c的rtmp推流代码,screenrecorderrtmp.c 以下:
#include <jni.h> #include <screenrecorderrtmp.h> #include <malloc.h> #include "rtmp.h" JNIEXPORT jlong JNICALL Java_net_yrom_screenrecorder_rtmp_RtmpClient_open (JNIEnv * env, jobject thiz, jstring url_, jboolean isPublishMode) { const char *url = (*env)->GetStringUTFChars(env, url_, 0); LOGD("RTMP_OPENING:%s",url); RTMP* rtmp = RTMP_Alloc(); if (rtmp == NULL) { LOGD("RTMP_Alloc=NULL"); return NULL; } RTMP_Init(rtmp); int ret = RTMP_SetupURL(rtmp, url); if (!ret) { RTMP_Free(rtmp); rtmp=NULL; LOGD("RTMP_SetupURL=ret"); return NULL; } if (isPublishMode) { RTMP_EnableWrite(rtmp); } ret = RTMP_Connect(rtmp, NULL); if (!ret) { RTMP_Free(rtmp); rtmp=NULL; LOGD("RTMP_Connect=ret"); return NULL; } ret = RTMP_ConnectStream(rtmp, 0); if (!ret) { ret = RTMP_ConnectStream(rtmp, 0); RTMP_Close(rtmp); RTMP_Free(rtmp); rtmp=NULL; LOGD("RTMP_ConnectStream=ret"); return NULL; } (*env)->ReleaseStringUTFChars(env, url_, url); LOGD("RTMP_OPENED"); return rtmp; } /* * Class: net_yrom_screenrecorder_rtmp_RtmpClient * Method: read * Signature: (J[BII)I */ JNIEXPORT jint JNICALL Java_net_yrom_screenrecorder_rtmp_RtmpClient_read (JNIEnv * env, jobject thiz,jlong rtmp, jbyteArray data_, jint offset, jint size) { char* data = malloc(size*sizeof(char)); int readCount = RTMP_Read((RTMP*)rtmp, data, size); if (readCount > 0) { (*env)->SetByteArrayRegion(env, data_, offset, readCount, data); // copy } free(data); return readCount; } /* * Class: net_yrom_screenrecorder_rtmp_RtmpClient * Method: write * Signature: (J[BIII)I */ JNIEXPORT jint JNICALL Java_net_yrom_screenrecorder_rtmp_RtmpClient_write (JNIEnv * env, jobject thiz,jlong rtmp, jbyteArray data, jint size, jint type, jint ts) { LOGD("start write"); jbyte *buffer = (*env)->GetByteArrayElements(env, data, NULL); RTMPPacket *packet = (RTMPPacket*)malloc(sizeof(RTMPPacket)); RTMPPacket_Alloc(packet, size); RTMPPacket_Reset(packet); if (type == RTMP_PACKET_TYPE_INFO) { // metadata packet->m_nChannel = 0x03; } else if (type == RTMP_PACKET_TYPE_VIDEO) { // video packet->m_nChannel = 0x04; } else if (type == RTMP_PACKET_TYPE_AUDIO) { //audio packet->m_nChannel = 0x05; } else { packet->m_nChannel = -1; } packet->m_nInfoField2 = ((RTMP*)rtmp)->m_stream_id; LOGD("write data type: %d, ts %d", type, ts); memcpy(packet->m_body, buffer, size); packet->m_headerType = RTMP_PACKET_SIZE_LARGE; packet->m_hasAbsTimestamp = FALSE; packet->m_nTimeStamp = ts; packet->m_packetType = type; packet->m_nBodySize = size; int ret = RTMP_SendPacket((RTMP*)rtmp, packet, 0); RTMPPacket_Free(packet); free(packet); (*env)->ReleaseByteArrayElements(env, data, buffer, 0); if (!ret) { LOGD("end write error %d", sockerr); return sockerr; }else { LOGD("end write success"); return 0; } } /* * Class: net_yrom_screenrecorder_rtmp_RtmpClient * Method: close * Signature: (J)I */ JNIEXPORT jint JNICALL Java_net_yrom_screenrecorder_rtmp_RtmpClient_close (JNIEnv * env,jlong rtmp, jobject thiz) { RTMP_Close((RTMP*)rtmp); RTMP_Free((RTMP*)rtmp); return 0; } /* * Class: net_yrom_screenrecorder_rtmp_RtmpClient * Method: getIpAddr * Signature: (J)Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_net_yrom_screenrecorder_rtmp_RtmpClient_getIpAddr (JNIEnv * env,jobject thiz,jlong rtmp) { if(rtmp!=0){ RTMP* r= (RTMP*)rtmp; return (*env)->NewStringUTF(env, r->ipaddr); }else { return (*env)->NewStringUTF(env, ""); } }
更多的请看Demo源码,说下目前未实现的功能和问题:
参照Nginx + rtmp搭建了流媒体服务器用来测试,Demo中键入如下地址即可推流,yourstramingkey自定义,
rtmp://59.110.159.133/live/<yourstramingkey> 例如:rtmp://59.110.159.133/live/test
传送门:GitHub