Android實現錄屏直播(一)ScreenRecorder的簡單分析


http://blog.csdn.net/zxccxzzxz/article/details/54150396

Android實現錄屏直播(一)ScreenRecorder的簡單分析

Android實現錄屏直播(二)需求才是硬道理之產品功能調研

Android實現錄屏直播(三)MediaProjection + VirtualDisplay + librtmp + MediaCodec實現視頻編碼並推流到rtmp服務器

應項目需求瞄准了Bilibili的錄屏直播功能,基本就仿着做一個吧。研究后發現Bilibili是使用的MediaProjection 與 VirtualDisplay結合實現的,需要 Android 5.0 Lollipop API 21以上的系統才能使用。

其實官方提供的android-ScreenCapture這個Sample中已經有了MediaRecorder的實現與使用方式,還有使用MediaRecorder實現的錄制屏幕到本地文件的Demo,從中我們都能了解這些API的使用。

而如果需要直播推流的話就需要自定義MediaCodec,再從MediaCodec進行編碼后獲取編碼后的幀,免去了我們進行原始幀的采集的步驟省了不少事。可是問題來了,因為之前沒有仔細了解H264文件的結構與FLV封裝的相關技術,其中爬了不少坑,此后我會一一記錄下來,希望對用到的朋友有幫助。

項目中對我參考意義最大的一個Demo是網友Yrom的GitHub項目ScreenRecorder,Demo中實現了錄屏並將視頻流存為本地的MP4文件(咳咳,其實Yrom就是Bilibili的員工吧?( ゜- ゜)つロ)��。在此先大致分析一下該Demo的實現,之后我會再說明我的實現方式。

ScreenRecorder

具體的原理在Demo的README中已經說得很明白了:

  • Display 可以“投影”到一個 VirtualDisplay
  • 通過 MediaProjectionManager 取得的 MediaProjection創建VirtualDisplay
  • VirtualDisplay 會將圖像渲染到 Surface中,而這個Surface是由MediaCodec所創建的
mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
... mSurface = mEncoder.createInputSurface(); ... mVirtualDisplay = mMediaProjection.createVirtualDisplay(name, mWidth, mHeight, mDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, mSurface, null, null);
  • 1
  • 2
  • 3
  • 4
  • 5
  • MediaMuxer 將從 MediaCodec 得到的圖像元數據封裝並輸出到MP4文件中
int index = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_US);
... ByteBuffer encodedData = mEncoder.getOutputBuffer(index); ... mMuxer.writeSampleData(mVideoTrackIndex, encodedData, mBufferInfo); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

所以其實在Android 4.4上可以通過DisplayManager來創建VirtualDisplay也是可以實現錄屏,但因為權限限制需要ROOT。 (see DisplayManager.createVirtualDisplay())

Demo很簡單,兩個Java文件:

  • MainActivity.java
  • ScreenRecorder.java

MainActivity

類中僅僅是實現的入口,最重要的方法是onActivityResult,因為MediaProjection就需要從該方法開啟。但是別忘了先進行MediaProjectionManager的初始化

@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { MediaProjection mediaProjection = mMediaProjectionManager.getMediaProjection(resultCode, data); if (mediaProjection == null) { Log.e("@@", "media projection is null"); return; } // video size final int width = 1280; final int height = 720; File file = new File(Environment.getExternalStorageDirectory(), "record-" + width + "x" + height + "-" + System.currentTimeMillis() + ".mp4"); final int bitrate = 6000000; mRecorder = new ScreenRecorder(width, height, bitrate, 1, mediaProjection, file.getAbsolutePath()); mRecorder.start(); mButton.setText("Stop Recorder"); Toast.makeText(this, "Screen recorder is running...", Toast.LENGTH_SHORT).show(); moveTaskToBack(true); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

ScreenRecorder

這是一個線程,結構很清晰,run()方法中完成了MediaCodec的初始化,VirtualDisplay的創建,以及循環進行編碼的全部實現。

線程主體

 @Override public void run() { try { try { prepareEncoder(); mMuxer = new MediaMuxer(mDstPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); } catch (IOException e) { throw new RuntimeException(e); } mVirtualDisplay = mMediaProjection.createVirtualDisplay(TAG + "-display", mWidth, mHeight, mDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, mSurface, null, null); Log.d(TAG, "created virtual display: " + mVirtualDisplay); recordVirtualDisplay(); } finally { release(); } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

MediaCodec的初始化

方法中進行了編碼器的參數配置與啟動、Surface的創建兩個關鍵的步驟

private void prepareEncoder() throws IOException { MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, mWidth, mHeight); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); // 錄屏必須配置的參數 format.setInteger(MediaFormat.KEY_BIT_RATE, mBitRate); format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL); Log.d(TAG, "created video format: " + format); mEncoder = MediaCodec.createEncoderByType(MIME_TYPE); mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); mSurface = mEncoder.createInputSurface(); // 需要在createEncoderByType之后和start()之前才能創建,源碼注釋寫的很清楚 Log.d(TAG, "created input surface: " + mSurface); mEncoder.start(); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

編碼器實現循環編碼

下面的代碼就是編碼過程,由於作者使用的是Muxer來進行視頻的采集,所以在resetOutputFormat方法中實際意義是將編碼后的視頻參數信息傳遞給Muxer並啟動Muxer。

private void recordVirtualDisplay() { while (!mQuit.get()) { int index = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_US); Log.i(TAG, "dequeue output buffer index=" + index); if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { resetOutputFormat(); } else if (index == MediaCodec.INFO_TRY_AGAIN_LATER) { Log.d(TAG, "retrieving buffers time out!"); try { // wait 10ms Thread.sleep(10); } catch (InterruptedException e) { } } else if (index >= 0) { if (!mMuxerStarted) { throw new IllegalStateException("MediaMuxer dose not call addTrack(format) "); } encodeToVideoTrack(index); mEncoder.releaseOutputBuffer(index, false); } } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
private void resetOutputFormat() { // should happen before receiving buffers, and should only happen once if (mMuxerStarted) { throw new IllegalStateException("output format already changed!"); } MediaFormat newFormat = mEncoder.getOutputFormat(); // 在此也可以進行sps與pps的獲取,獲取方式參見方法getSpsPpsByteBuffer() Log.i(TAG, "output format changed.\n new format: " + newFormat.toString()); mVideoTrackIndex = mMuxer.addTrack(newFormat); mMuxer.start(); mMuxerStarted = true; Log.i(TAG, "started media muxer, videoIndex=" + mVideoTrackIndex); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

獲取sps pps的ByteBuffer,注意此處的sps pps都是read-only只讀狀態

private void getSpsPpsByteBuffer(MediaFormat newFormat) { ByteBuffer rawSps = newFormat.getByteBuffer("csd-0"); ByteBuffer rawPps = newFormat.getByteBuffer("csd-1"); }
  • 1
  • 2
  • 3
  • 4

錄屏視頻幀的編碼過程

BufferInfo.flags表示當前編碼的信息,如源碼注釋:

 /** * This indicates that the (encoded) buffer marked as such contains * the data for a key frame. */ public static final int BUFFER_FLAG_KEY_FRAME = 1; // 關鍵幀 /** * This indicated that the buffer marked as such contains codec * initialization / codec specific data instead of media data. */ public static final int BUFFER_FLAG_CODEC_CONFIG = 2; // 該狀態表示當前數據是avcc,可以在此獲取sps pps /** * This signals the end of stream, i.e. no buffers will be available * after this, unless of course, {@link #flush} follows. */ public static final int BUFFER_FLAG_END_OF_STREAM = 4;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

實現編碼:

private void encodeToVideoTrack(int index) { ByteBuffer encodedData = mEncoder.getOutputBuffer(index); if ((mBufferInfo.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. // 大致意思就是配置信息(avcc)已經在之前的resetOutputFormat()中喂給了Muxer,此處已經用不到了,然而在我的項目中這一步卻是十分重要的一步,因為我需要手動提前實現sps, pps的合成發送給流媒體服務器 Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG"); mBufferInfo.size = 0; } if (mBufferInfo.size == 0) { Log.d(TAG, "info.size == 0, drop it."); encodedData = null; } else { Log.d(TAG, "got buffer, info: size=" + mBufferInfo.size + ", presentationTimeUs=" + mBufferInfo.presentationTimeUs + ", offset=" + mBufferInfo.offset); } if (encodedData != null) { encodedData.position(mBufferInfo.offset); encodedData.limit(mBufferInfo.offset + mBufferInfo.size); // encodedData是編碼后的視頻幀,但注意作者在此並沒有進行關鍵幀與普通視頻幀的區別,統一將數據寫入Muxer mMuxer.writeSampleData(mVideoTrackIndex, encodedData, mBufferInfo); Log.i(TAG, "sent " + mBufferInfo.size + " bytes to muxer..."); } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

以上就是對ScreenRecorder這個Demo的大體分析,由於總結時間倉促,很多細節部分我也沒有進行深入的發掘研究,所以請大家抱着懷疑的態度閱讀,如果說明有誤或是理解不到位的地方,希望大家幫忙指出,謝謝!

參考文檔

在功能的開發中還參考了很多有價值的資料與文章:

  1. Android屏幕直播方案
  2. Google官方的EncodeVirtualDisplayTest
  3. FLV文件格式解析
  4. 使用librtmp進行H264與AAC直播
  5. 后續更新…
 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM