代碼地址 :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})
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) 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。