Android實現錄屏直播(一)ScreenRecorder的簡單分析
應專案需求瞄準了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);
MediaMuxer 將從 MediaCodec 得到的影像後設資料封裝並輸出到MP4檔案中
int index = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_US);
...
ByteBuffer encodedData = mEncoder.getOutputBuffer(index);
...
mMuxer.writeSampleData(mVideoTrackIndex, encodedData, mBufferInfo);
所以其實在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);
}
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();
}
}
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();
}
編碼器實現迴圈編碼
下面的程式碼就是編碼過程,由於作者使用的是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);
}
}
}
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);
}
獲取sps pps的ByteBuffer,注意此處的sps pps都是read-only只讀狀態
private void getSpsPpsByteBuffer(MediaFormat newFormat) {
ByteBuffer rawSps = newFormat.getByteBuffer("csd-0");
ByteBuffer rawPps = newFormat.getByteBuffer("csd-1");
}
錄屏視訊幀的編碼過程
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;
實現編碼:
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...");
}
}
以上就是對ScreenRecorder這個Demo的大體分析,由於總結時間倉促,很多細節部分我也沒有進行深入的發掘研究,所以請大家抱著懷疑的態度閱讀,如果說明有誤或是理解不到位的地方,希望大家幫忙指出,謝謝!
參考文件
在功能的開發中還參考了很多有價值的資料與文章:
相關文章
- 直播帶貨系統是如何實現直播錄屏的
- 直播平臺軟體開發,一個簡單的Android登入實現demoAndroid
- 直播系統搭建,簡單實現Android應用的啟動頁Android
- 手機直播原始碼,Android studio 實現簡單的視訊播放原始碼Android
- 簡單實現一個全面屏切換效果
- Android中SharePreferences的簡單實現Android
- 一隻android簡訊控制馬的簡單分析Android
- Windows 11實現錄屏直播,搭建Nginx的rtmp服務WindowsNginx
- Iris for Mac:簡單易用的錄屏神器Mac
- 一對一直播原始碼,實現一個簡單的登入介面原始碼
- 簡單又好用的錄屏軟體——錄大咖
- 線上直播系統原始碼,簡單實現Android應用的啟動頁原始碼Android
- Android應用加固的簡單實現方案Android
- redux簡單實現與分析Redux
- LayoutTransiton實現簡單的錄製按鈕
- Android應用加固的簡單實現方案(二)Android
- Android 時間軸的實現(RecyclerView更簡單)AndroidView
- 使用RecyclerView簡單快捷地擼一個直播公屏出來View
- Windows11實現錄屏直播,H5頁面直播 HLS ,不依賴FlashWindowsH5
- 實現一個簡單的TomcatTomcat
- React 簡單實現(一)React
- 手機直播原始碼,Android 簡單的彈框原始碼Android
- android 簡單實現指紋識別功能Android
- 騰訊直播SDK接入及封裝(一)之錄屏直播封裝
- 基於vue實現一個簡單的MVVM框架(原始碼分析)VueMVVM框架原始碼
- 依存句法分析器的簡單實現
- 用JS實現簡單的螢幕錄影機JS
- 什麼遊戲錄屏軟體免費?這樣錄屏超簡單!遊戲
- 實現一個簡單的 RESTful APIRESTAPI
- 實現一個簡單的MVVM(Compile)MVVMCompile
- 簡單的實現一個原型鏈原型
- php實現一個簡單的socketPHP
- web頁面錄屏實現Web
- 實現一個簡單的 jQuery 的 APIjQueryAPI
- 基於Netty的Android系統IM簡單實現原理NettyAndroid
- win10免費錄屏軟體怎麼用?超簡單錄屏技巧Win10
- Android開屏、鎖屏、解鎖監聽實現Android
- laravel_admin 單一登入的簡單實現Laravel