代码地址 :https://github.com/deepsadness/MediaProjectionDemo
上一边文章的最后说使用录制的Api进行录屏直播。本来这边文章是预计在5月份完成的。结果过了这么久,终于有时间了。就来填坑了。
使用MediaProjection示意图.png
整体流程就是通过创建VirtualDisplay,并且直接通过MediaCodec的Surface直接得到数据。通过MediaCodec得到编码完成之后的数据,进行 flv格式的封装,最后通过rtmp协议进行发送。
1. 使用MediaCodec Surface
这部分基本上和上一遍文章相同,不同的就是使用MediaCodec来获取Surface
@Override
public @Nullable
Surface createSurface(int width, int height) {
mBufferInfo = new MediaCodec.BufferInfo();
//创建视频的mediaFormat
MediaFormat 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给VirtualDisplay
try {
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. 获取编码后的数据
private void createEncoderThread() {
HandlerThread encoder = new HandlerThread("Encoder");
encoder.start();
Looper looper = encoder.getLooper();
workHanlder = new Handler(looper);
}
//这里的1s延迟是因为开启encoder之后,硬件编码器进行初始化需要点时间
workHanlder.postDelayed(new Runnable() {
@Override
public 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推流。
1. 认识 rtmp 协议
RTMP协议是Real Time Message Protocol(实时信息传输协议)的缩写,它是由Adobe公司提出的一种应用层的协议,用来解决多媒体数据传输流的多路复用(Multiplexing)和分包(packetizing)的问题。
2. RTMP Connection
握手(HandShake)
一个RTMP连接以握手开始,双方分别发送大小固定的三个数据块
image
理论上来讲只要满足以上条件,如何安排6个Message的顺序都是可以的,但实际实现中为了在保证握手的身份验证功能的基础上尽量减少通信的次数,一般的发送顺序是这样的:
建立网络连接(NetConnection)
服务器在收到客户端发送的连接请求后发送如下信息:
image
主要是告诉客户端确认窗口大小,设置节点带宽,然后服务器把“连接”连接到指定的应用并返回结果,“网络连接成功”。并且返回流开始的的消息(Stream Begin 0)。
建立网络流(NetStream)
推流流程
推流流程
播流流程
播流流程
3. 代码集成
1. 集成RTMP
直接使用librestreaming 中的RTMP的代码,将其放到CMake中进行编译。
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-lib
SHARED
src/main/cpp/rtmp-hanlde.cpp
${PROJECT_SOURCES}
)
find_library( # Sets the name of the path variable.
log-lib
log)
target_link_libraries( # Specifies the target library.
rtmp-lib
${log-lib})
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连接部分整体的流程
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;
}
//初始化RTMP
RTMP_Init(rtmp);
int ret = RTMP_SetupURL(rtmp, const_cast(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");
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) { // 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;
}
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() {
@Override
public void run() {
doExtract(mEncoder, new FrameCallback() {
@Override
public void render(MediaCodec.BufferInfo info, ByteBuffer outputBuffer) {
Sender.getInstance().rtmpSend(info, outputBuffer);
}
@Override
public 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&CodecID
dst[pos] = isIDR ? (byte) 0x17 : (byte) 0x27;
//AVCPacketType
dst[pos + 1] = isAVCSequenceHeader ? (byte) 0x00 : (byte) 0x01;
//LAKETODO CompositionTime
dst[pos + 2] = 0x00;
dst[pos + 3] = 0x00;
dst[pos + 4] = 0x00;
if (!isAVCSequenceHeader) {
//NALU HEADER
ByteArrayTools.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服务器
对比之前的一遍文章
Android PC投屏简单尝试
获取数据的方式
都是通过MediaProjection.createVirtualDisplay的方式来获取截屏的数据。
不同的是,上一边文章使用ImageReader来获取一张一张的截图。
而这边文章直接是用了MediaCodec硬编码,直接得到编码后的h264数据。
传输协议
上一边文章使用的webSocket,将得到的Bitmap的字节流,通过socket传输,接收方,只要接受到Socket,并且将其解析成Bitmap来展示就可以。
优点是方便,而且可以自定义协议内容。
但是缺点是,不能通用,必须编写对应的客户端才能完成。
这边文章使用了rtmp的流媒体协议,优点是只要支持该协议的播放器都可以直接播放我们的投屏流。
参考文章
Android实现录屏直播(一)ScreenRecorder的简单分析
直播推流实现RTMP协议的一些注意事项
投屏尝试系列文章
作者:deep_sadness
链接:https://www.jianshu.com/p/6dde380d9b1e
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。