Android PC投屏简单尝试(录屏直播)2—硬解章(MediaCodec+RMTP)
代码地址 :https://github.com/deepsadness/MediaProjectionDemo
想法来源
上一边文章的最后说使用录制的Api进行录屏直播。本来这边文章是预计在5月份完成的。结果过了这么久,终于有时间了。就来填坑了。
主要思路
- 直接使用硬件编码器进行录制直播。
- 使用rtmp协议进行直播推流
使用MediaProjection示意图.png
整体流程就是通过创建VirtualDisplay,并且直接通过MediaCodec的Surface直接得到数据。通过MediaCodec得到编码完成之后的数据,进行 flv格式的封装,最后通过rtmp协议进行发送。
获取屏幕的截屏
1. 使用MediaCodec Surface
这部分基本上和上一遍文章相同,不同的就是使用MediaCodec来获取Surface
@Overridepublic @NullableSurface createSurface(int width, int height) {mBufferInfo = new MediaCodec.BufferInfo();//创建视频的mediaFormatMediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, height);//还需要对器进行插值。设置自己设置的一些变量format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);format.setInteger(MediaFormat.KEY_COLOR_FORMAT,MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);if (VERBOSE) Log.d(TAG, "format: " + format);// 创建一个MediaCodec编码器,并且使用format 进行configure.然后将其 Get a Surface给VirtualDisplaytry {mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);mInputSurface = mEncoder.createInputSurface();//直接开启编码器mEncoder.start();//...省去部分代码return mInputSurface;} catch (IOException e) {e.printStackTrace();}return null;}
2. 获取编码后的数据
- 创建Encoder HanderThread
不断获取编码后的数据需要在一个新的线程内进行。所以我们先创建一个HanderThread进行异步操作和异步通行。
private void createEncoderThread() {HandlerThread encoder = new HandlerThread("Encoder");encoder.start();Looper looper = encoder.getLooper();workHanlder = new Handler(looper);}
- 开始获取数据的任务
在上面编码器开启之后,直接推入一个任务运行
//这里的1s延迟是因为开启encoder之后,硬件编码器进行初始化需要点时间workHanlder.postDelayed(new Runnable() {@Overridepublic void run() {doExtract(mEncoder,null);}, 1000);
注意是的是,这里推入任务,需要稍微的延迟,因为初始化和开启硬件编码器需要一点时间。
- 获取编码后的数据
/*** 不断循环获取,直到我们手动结束.同步的方式* @param encoder 编码器* @param frameCallback 获取的回调*/private void doExtract(MediaCodec encoder,FrameCallback frameCallback) {final int TIMEOUT_USEC = 10000;long firstInputTimeNsec = -1;boolean outputDone = false;//没有手动停止,就只能不断进行while (!outputDone) {//如果手动停止了。就结束吧if (mIsStopRequested) {Log.d(TAG, "Stop requested");return;}//因为给编码器获取状态和喂数据的方法都直接通过Surface直接进行了,这里只要直接获取解码后的状态就可以了int decoderStatus = encoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);if (decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {// no output available yet
// if (VERBOSE) Log.d(TAG, "no output from decoder available");} else if (decoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {// not important for us, since we're using Surface
// if (VERBOSE) Log.d(TAG, "decoder output buffers changed");} else if (decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {//上面几种状态,我们都可以直接忽略。这里是进行MediaCodec开始编码后,会得到一个有cs-0 和cs-1的数据,对应sps和pps .获取之后,我们后面需要处理,所以先设置成一个回调就好。MediaFormat newFormat = encoder.getOutputFormat();if (VERBOSE) Log.d(TAG, "decoder output format changed: " + newFormat);if (frameCallback != null) {frameCallback.formatChange(newFormat);}} else if (decoderStatus < 0) {//这种情况下是出错了。暂时先直接出异常吧throw new RuntimeException("unexpected result from decoder.dequeueOutputBuffer: " +decoderStatus);} else { // decoderStatus >= 0//这里是正确获取到编码后的数据了if (firstInputTimeNsec != 0) {long nowNsec = System.nanoTime();Log.d(TAG, "startup lag " + ((nowNsec - firstInputTimeNsec) / 1000000.0) + " ms");firstInputTimeNsec = 0;}if (VERBOSE) Log.d(TAG, "surface decoder given buffer " + decoderStatus +" (size=" + mBufferInfo.size + ")");//获取到最后的数据了。这里就跳出循环。我们这个地方基本也不用用到if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {if (VERBOSE) Log.d(TAG, "output EOS");outputDone = true;}//当size 大于0时,需要送显boolean doRender = (mBufferInfo.size != 0);//这个时候,来获取编码后的buffer,回调给外面if (doRender && frameCallback != null) {ByteBuffer outputBuffer = encoder.getOutputBuffer(decoderStatus);frameCallback.render(mBufferInfo, outputBuffer);}encoder.releaseOutputBuffer(decoderStatus, doRender);}}}
通过这样的循环获取,就可以通过回调获取编码后的数据了。
后面,我们可以将编码后的数据进行让rtmp推流。
使用 RTMP 推流
- 认识 rtmp 协议
- RMTP Connection
- 代码
1. 认识 rtmp 协议
RTMP协议是Real Time Message Protocol(实时信息传输协议)的缩写,它是由Adobe公司提出的一种应用层的协议,用来解决多媒体数据传输流的多路复用(Multiplexing)和分包(packetizing)的问题。
- 基于TCP
在基于传输层协议的链接建立完成后,RTMP协议也要客户端和服务器通过“握手”来建立基于传输层链接之上的RTMP Connection链接。在Connection链接上会传输一些控制信息,如SetChunkSize,SetACKWindowSize。其中CreateStream命令会创建一个Stream链接,用于传输具体的音视频数据和控制这些信息传输的命令信息。RTMP协议传输时会对数据做自己的格式化,这种格式的消息我们称之为RTMP Message,而实际传输的时候为了更好地实现多路复用、分包和信息的公平性,发送端会把Message划分为带有Message ID的Chunk,每个Chunk可能是一个单独的Message,也可能是Message的一部分,在接受端会根据chunk中包含的data的长度,message id和message的长度把chunk还原成完整的Message,从而实现信息的收发。
2. RTMP Connection
握手(HandShake)
一个RTMP连接以握手开始,双方分别发送大小固定的三个数据块
- 握手开始于客户端发送C0、C1块。服务器收到C0或C1后发送S0和S1。
- 当客户端收齐S0和S1后,开始发送C2。当服务器收齐C0和C1后,开始发送S2。
- 当客户端和服务器分别收到S2和C2后,握手完成。
image
理论上来讲只要满足以上条件,如何安排6个Message的顺序都是可以的,但实际实现中为了在保证握手的身份验证功能的基础上尽量减少通信的次数,一般的发送顺序是这样的:
- Client发送C0+C1到Sever
- Server发送S0+S1+S2到Client
- Client发送C2到Server,握手完成
建立网络连接(NetConnection)
- 客户端发送命令消息中的“连接”(connect)到服务器,请求与一个服务应用实例建立连接。
- 服务器接收到连接命令消息后,发送确认窗口大小(Window Acknowledgement Size)协议消息到客户端,同时连接到连接命令中提到的应用程序。
- 服务器发送设置带宽(Set Peer Bandwitdh)协议消息到客户端。
- 客户端处理设置带宽协议消息后,发送确认窗口大小(Window Acknowledgement Size)协议消息到服务器端。
- 服务器发送用户控制消息中的“流开始”(Stream Begin)消息到客户端。
- 服务器发送命令消息中的“结果”(_result),通知客户端连接的状态。
- 客户端在收到服务器发来的消息后,返回确认窗口大小,此时网络连接创建完成。
服务器在收到客户端发送的连接请求后发送如下信息:
image
主要是告诉客户端确认窗口大小,设置节点带宽,然后服务器把“连接”连接到指定的应用并返回结果,“网络连接成功”。并且返回流开始的的消息(Stream Begin 0)。
建立网络流(NetStream)
- 客户端发送命令消息中的“创建流”(createStream)命令到服务器端。
- 服务器端接收到“创建流”命令后,发送命令消息中的“结果”(_result),通知客户端流的状态。
推流流程
- 客户端发送publish推流指令。
- 服务器发送用户控制消息中的“流开始”(Stream Begin)消息到客户端。
- 客户端发送元数据(分辨率、帧率、音频采样率、音频码率等等)。
- 客户端发送音频数据。
- 客户端发送服务器发送设置块大小(ChunkSize)协议消息。
- 服务器发送命令消息中的“结果”(_result),通知客户端推送的状态。
- 客户端收到后,发送视频数据直到结束。
推流流程
播流流程
- 客户端发送命令消息中的“播放”(play)命令到服务器。
- 接收到播放命令后,服务器发送设置块大小(ChunkSize)协议消息。
- 服务器发送用户控制消息中的“streambegin”,告知客户端流ID。
- 播放命令成功的话,服务器发送命令消息中的“响应状态” NetStream.Play.Start & NetStream.Play.reset,告知客户端“播放”命令执行成功。
- 在此之后服务器发送客户端要播放的音频和视频数据。
播流流程
3. 代码集成
1. 集成RTMP
直接使用librestreaming 中的RTMP的代码,将其放到CMake中进行编译。
将项目中的librtmp到 libs下
image.png
- 根据原来的Android.mk文件,配置
CMakeList
cmake_minimum_required(VERSION 3.4.1)
add_definitions("-DNO_CRYPTO")
include_directories(${CMAKE_SOURCE_DIR}/libs/rtmp/librtmp)
#native-lib
file(GLOB PROJECT_SOURCES "${CMAKE_SOURCE_DIR}/libs/rtmp/librtmp/*.c")
add_library(rtmp-libSHAREDsrc/main/cpp/rtmp-hanlde.cpp${PROJECT_SOURCES})
find_library( # Sets the name of the path variable.log-liblog)
target_link_libraries( # Specifies the target library.rtmp-lib${log-lib})
- 创建java文件,并编写jni
public class RtmpClient {static {System.loadLibrary("rtmp-lib");}/*** @param url* @param isPublishMode* @return rtmpPointer ,pointer to native rtmp struct*/public static native long open(String url, boolean isPublishMode);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);
}
2. RMTP推流
之前的文章,有分析过FLV的数据格式。这样还需要再将编码后的数据。
这里就不赘述了。
RTMP连接部分整体的流程
- 连接RTMP URL
整体的连接的过程。上面的了解也有提到过。
const char *url = env->GetStringUTFChars(url_, 0);LOGD("RTMP_OPENING:%s", url);//分配RTMP对象RTMP *rtmp = RTMP_Alloc();if (rtmp == NULL) {LOGD("RTMP_Alloc=NULL");return NULL;}//初始化RTMPRTMP_Init(rtmp);int ret = RTMP_SetupURL(rtmp, const_cast<char *>(url));if (!ret) {RTMP_Free(rtmp);rtmp = NULL;LOGD("RTMP_SetupURL=ret");return NULL;}if (isPublishMode) {RTMP_EnableWrite(rtmp);}//2. 开始Connect 。建立网络连接的过程。其中包括握手ret = RTMP_Connect(rtmp, NULL);if (!ret) {RTMP_Free(rtmp);rtmp = NULL;LOGD("RTMP_Connect=ret");return NULL;}//3. create stream 建立网络流的过程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(url_, url);LOGD("RTMP_OPENED");
- 在得到MediaFormat回调时,将其进行推流发送,进行publish
- 不断得到编码后的数据,不断推流
这两者主要的不同,在编码上就是type不同。我们知道第一个message必须为一个完整的message,必须为meta_data才可以。
jbyte *buffer = env->GetByteArrayElements(data_, NULL);LOGD("start write");RTMPPacket *packet = (RTMPPacket *) malloc(sizeof(RTMPPacket));RTMPPacket_Alloc(packet, size);RTMPPacket_Reset(packet);if (type == RTMP_PACKET_TYPE_INFO) { // metadatapacket->m_nChannel = 0x03;} else if (type == RTMP_PACKET_TYPE_VIDEO) { // videopacket->m_nChannel = 0x04;} else if (type == RTMP_PACKET_TYPE_AUDIO) { //audiopacket->m_nChannel = 0x05;} else {packet->m_nChannel = -1;}RTMP *r = (RTMP *) rtmpPointer;packet->m_nInfoField2 = r->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 *) rtmpPointer, packet, 0);RTMPPacket_Free(packet);free(packet);env->ReleaseByteArrayElements(data_, buffer, 0);if (!ret) {LOGD("end write error %d", ret);return ret;} else {LOGD("end write success");return 0;}
- 最后关闭
RTMP_Close((RTMP *) rtmpPointer);RTMP_Free((RTMP *) rtmpPointer);
接受编码后的数据回调
workHanlder.postDelayed(new Runnable() {@Overridepublic void run() {doExtract(mEncoder, new FrameCallback() {@Overridepublic void render(MediaCodec.BufferInfo info, ByteBuffer outputBuffer) {Sender.getInstance().rtmpSend(info, outputBuffer);}@Overridepublic void formatChange(MediaFormat mediaFormat) {Sender.getInstance().rtmpSendFormat(mediaFormat);}});}}, 1000);
通过回调MediaFormat
之前对flv的格式详解,我们知道要实现flv推流。
需要将cs0 和cs1的头部位置进行推流才能正常显示。并且必须作为第一条信息。
这里通过这方法读取cs0 和cs1
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;}
根据flv格式的分析。填充到flv中
public static void fillFlvVideoTag(byte[] dst, int pos, boolean isAVCSequenceHeader, boolean isIDR, int readDataLength) {//FrameType&CodecIDdst[pos] = isIDR ? (byte) 0x17 : (byte) 0x27;//AVCPacketTypedst[pos + 1] = isAVCSequenceHeader ? (byte) 0x00 : (byte) 0x01;//LAKETODO CompositionTimedst[pos + 2] = 0x00;dst[pos + 3] = 0x00;dst[pos + 4] = 0x00;if (!isAVCSequenceHeader) {//NALU HEADERByteArrayTools.intToByteArrayFull(dst, pos + 5, readDataLength);}}
然后发送。
发送实际数据
public static RESFlvData sendRealData(long tms, ByteBuffer realData) {int realDataLength = realData.remaining();int packetLen = Packager.FLVPackager.FLV_VIDEO_TAG_LENGTH +Packager.FLVPackager.NALU_HEADER_LENGTH +realDataLength;byte[] finalBuff = new byte[packetLen];realData.get(finalBuff, Packager.FLVPackager.FLV_VIDEO_TAG_LENGTH +Packager.FLVPackager.NALU_HEADER_LENGTH,realDataLength);int frameType = finalBuff[Packager.FLVPackager.FLV_VIDEO_TAG_LENGTH +Packager.FLVPackager.NALU_HEADER_LENGTH] & 0x1F;Packager.FLVPackager.fillFlvVideoTag(finalBuff,0,false,frameType == 5,realDataLength);RESFlvData resFlvData = new RESFlvData();resFlvData.droppable = true;resFlvData.byteBuffer = finalBuff;resFlvData.size = finalBuff.length;resFlvData.dts = (int) tms;resFlvData.flvTagType = RESFlvData.FLV_RTMP_PACKET_TYPE_VIDEO;resFlvData.videoFrameType = frameType;return resFlvData;
// dataCollecter.collect(resFlvData, RESRtmpSender.FROM_VIDEO);}
RMTP服务器
RMTP服务器的建立,可以简单的使用
RMTP服务器
总结
对比之前的一遍文章
Android PC投屏简单尝试
获取数据的方式
都是通过MediaProjection.createVirtualDisplay的方式来获取截屏的数据。
不同的是,上一边文章使用ImageReader来获取一张一张的截图。
而这边文章直接是用了MediaCodec硬编码,直接得到编码后的h264数据。传输协议
上一边文章使用的webSocket,将得到的Bitmap的字节流,通过socket传输,接收方,只要接受到Socket,并且将其解析成Bitmap来展示就可以。
优点是方便,而且可以自定义协议内容。
但是缺点是,不能通用,必须编写对应的客户端才能完成。
这边文章使用了rtmp的流媒体协议,优点是只要支持该协议的播放器都可以直接播放我们的投屏流。
参考文章
Android实现录屏直播(一)ScreenRecorder的简单分析
直播推流实现RTMP协议的一些注意事项
投屏尝试系列文章
- Android PC投屏简单尝试- 自定义协议章(Socket+Bitmap)
- Android PC投屏简单尝试(录屏直播)2—硬解章(MediaCodec+RMTP)
作者:deep_sadness
链接:https://www.jianshu.com/p/6dde380d9b1e
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
Android PC投屏简单尝试(录屏直播)2—硬解章(MediaCodec+RMTP)相关推荐
- Android PC投屏简单尝试(录屏直播)3—软解章(ImageReader+FFMpeg with X264)
使用FFmpeg进行软件解码并通过RTMP进行推流 编译带有x264的FFmpeg 编写FFmpeg代码进行推流 通过ImageReader的回调,我们就可以得到截屏的数据了.第一遍文章是通过自定义的 ...
- Android PC投屏简单尝试—最终章2
源码地址:https://github.com/deepsadness/AppRemote 上一章中,我们简单实现了PC的投屏功能. 但是还是存在这一些缺陷. 屏幕的尺寸数据是写死的 不能通过PC来对 ...
- Android PC投屏简单尝试—最终章1
回顾之前的几遍文章,我们分别通过RMTP协议和简单的Socket 发送Bitmap图片的Base64编码来完成投屏. 回想这系列文章的想法来源-Vysor,它通过 USB来进行连接的.又看到了 scr ...
- Android PC投屏简单尝试- 自定义协议章(Socket+Bitmap)
代码地址 :https://github.com/deepsadness/MediaProjectionDemo 效果预览 投屏效果预览 简单说明: 使用Android MediaProjection ...
- java无线投屏代码,Android PC投屏功能实现的示例代码
本文介绍了Android PC投屏功能实现的示例代码,分享给大家,具体如下: 效果预览 投屏效果预览 简单说明: 使用Android MediaProjection Api来完成视频的截图 通过Web ...
- mac android 录屏软件,mac录屏怎么录内置声音?详细的解决方案
原标题:mac录屏怎么录内置声音?详细的解决方案 mac录屏怎么录内置声音?大家使用Mac电脑最大的问题就是对操作系统的不熟悉,就像是刚接触Windows系统一样,开始都会有一定的不熟悉,其实并没有那 ...
- Android 如何实现App在后台录屏
在 Android 中实现 App 在后台录屏主要需要使用到 MediaProjection API. MediaProjection API 是 Android 5.0(API Level 21)引 ...
- 录屏工具下载哪个好?分享:超简单的录屏工具及实用方法
在短视频盛行,知识付费,粉丝经济的背景下,下载一款好用的录屏工具可以如虎添翼,为自己制作视频助力.然而面对各种各样的录屏工具,很多人不知道下载哪个好.一旦录屏工具没有选对,即使下载了,不会用也没有意义 ...
- 电脑有什么超简单的录屏方法
录屏是我们经常需要使用的一项功能,授课.演示.讲解.会议记录等等经常需要使用这一功能,但是,相比于手机端的录屏功能来说,电脑端的录屏功能显得还没有那么完善. 一番探索之后,我发现了解决电脑录屏问题的一 ...
最新文章
- json解析:[1]gson解析json
- IT部门在企业信息化中的转变
- 机器学习入门:线性回归及梯度下降
- Spring-data-redis:特性与实例--转载
- 在SSH上传文件出现Permission denied错误
- ITK:使用Viola Wells互信息执行多模式注册
- 在eclipse中安装properties插件PropertiesEditor及设置(附图),ASCII码转换成中文
- 显示和快速隐藏Mac桌面所有图标
- sicktim571操作手册_SICK激光传感器TIM310操作说明书
- javaweb c3p0连接oracle12c
- oracle10g备份导入
- SAP 函数积累(转
- FusionCharts参数说明——3D饼图属性(Pie3D.swf )
- ajax---post跨域思路
- 英国汇丰银行拒绝为客户处理加密货币交易
- Kotlin StandardKt 标准库源码走一波
- win10专业版虚拟机配置服务器,如何在Win10专业版中添加Hyper-V虚拟机?
- 乌龟Git误点跳过工作树的解决方法
- 复盘 2019 ,展望 2020
- PR AE PS安装资源包
热门文章
- 转: eclipse 快捷键列表(功能清晰版本)
- 策略模式(stragegy)
- asp 与 database (3)
- http://www.mamicode.com/info-detail-1208112.html
- python 如何引用同一个目录下的另一个py文件
- Tesorflow:module 'pandas.core.computation' has no attribute 'expressions'
- Linux下的图形界面编程
- 将自己数据集转化为lmdb格式
- 《Neural network and deep learning》学习笔记(一)
- 科大星云诗社动态20210813