1. 程式人生 > >Android實現錄屏直播(一)ScreenRecorder的簡單分析

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的實現,之後我會再說明我的實現方式。

具體的原理在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實現直播ScreenRecorder簡單分析

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

Android實現直播需求才是硬道理之產品功能調研

前面的Android實現錄屏直播(一)ScreenRecorder的簡單分析一文中我們對 ScreenRecorder 這個開源 Demo 中的實現機制大概有了瞭解,但在繼續寫這個系列文章的時候發現每一個細節都太緊密了,稍微不注意就會深入每個知識

Android實現音樂播放器

simple ani call ket 打開文件 界面 方式 .cn 點擊 Graphical User Interface 本篇文章記錄了我實現Android簡單音樂播放器的過程,(一)中介紹了怎麽構建音樂播放器的前端頁面。首先大家看一下,界面最後是這樣的(界面有

Android RecyclerView優雅實現複雜列表佈局

轉載:https://blog.csdn.net/huang3513/article/details/62044688 前言 在多彩佈局不斷呈現的今天,多佈局混合排布成為一個時尚,今天就結合Holder實現RecyclerView複雜列表佈局。  效果圖如下(三種佈局): 

零基礎實現攝像頭的全平臺直播 內網直播實現

背景需求 我是一個個體戶,沒有任何計算機基礎知識,但是我有個店面,有幾個攝像頭,我想在網站上看到我的攝像頭或者用手機微信也可以看到我的攝像頭視訊? 實現方式 相關的專業術語也不贅述,直接上實現步驟 前期準備 硬體:網路攝像機以及知道網路攝像機的rtsp地址、

Android 百度地圖開發如何呼叫百度地圖介面和在專案中顯示百度地圖以及實現定位

二、下載百度地圖API庫 然後新增到專案中即可。   三、在專案清單AndroidMainifest.xml配置百度地圖API key和新增相關許可權                         四、在專案呼叫百度地圖專案功能,這篇文章就首先講講顯示地圖和定位的功能 首先

android-音樂播放器實現及原始碼下載

從本文開始,詳細講述一個音樂播放器的實現,以及從網路解析資料獲取最新推薦歌曲以及歌曲下載的功能。 功能介紹如下: 1、獲取本地歌曲列表,實現歌曲播放功能。 2、利用硬體加速感應器,搖動手機實現切換歌曲的功能 3、利用jsoup解析網頁資料,從網路獲取歌曲

Android Studio酷炫外掛——自動化快速實現Parcelable介面序列化

一、前言 相信資料序列化大家都多多少少有接觸到,比如自定義了一個實體類,需要在activity之間傳輸該類物件,就需要將資料序列化。android中實現方式有兩種,第一、實現Serializable介面,這種比較簡單,直接宣告就好;第二種,實現Parcelable介面,這種

實現Splash頁的正確方式, 解決啟動閃現象

1. 由於在系統載入Activity的過程中,首先會讀取Activity的Theme,然後根據Theme中的配置來繪製,當Activity載入完畢後,才會替換為真正的介面。所以這裡通過android:

Android Plugin插樁式實現外掛化開發-實現原理及Activity外掛化實現

1. 前言在現在一些大型的Android應用中都採用了外掛化的開發方式,比如美團,支付寶及我們常用的微信等採用了插修的化的開發方式來進行開發的,既然國內一流的網際網路公司都採用這樣的方式來開發那它一定能帶給開發部署大型應用帶來很大的便捷,那麼外掛化的優勢在哪裡呢?1.1 外掛

Android RecyclerView優雅實現複雜列表佈局

前言 在多彩佈局不斷呈現的今天,多佈局混合排布成為一個時尚,今天就結合Holder實現RecyclerView複雜列表佈局。  效果圖如下(三種佈局):  1.首先在我們主佈局中加入我們的RecyclerView控制元件。  2.在MainActivity中初始化我們

使用OpenCL+OpenCV實現圖像旋轉

posit 段落 大致 pro 什麽 string cpp base wechat [題外話]近期申請了一個微信公眾號:平凡程式人生。有興趣的朋友可以關註,那裏將會涉及更多更新OpenCL+OpenCV以及圖像處理方面的文章。 最近在學習《OPENCL異構計算》,其中有

android入門 — 多線程

xtend 分享 調用 管理 ava 導致 ui線程 rec thread   android中的一些耗時操作,例如網絡請求,如果不能及時響應,就會導致主線程被阻塞,出現ANR,非常影響用戶體驗,所以一些耗時的操作,我們會想辦法放在子線程中去完成。   android的U

android深入之設計模式托付模式

-h listen back != new 聚合 string static data- (一)托付模式簡單介紹 托付模式是主要的設計模式之中的一個。托付。即是讓還有一個對象幫你做事情。 更多的模式,如狀態模式、策略模式、訪問者模式本質上是在更特殊的場合採用了托

C#基於LibUsbDotNet實現USB通信

cti sha esc log gist ces pos 簡單 src 網上C#USB通信的資料比較少, 基本上都是基於LibUsbDotNet 和 CyUsb, 關於打印機設備的還有一個OPOS。 本篇文章基於LibUsbDotNet。   1. 下載並安裝 L

Android網絡編程HTTP協議原理

客戶 獲取版本 接口 開發人員 linu 系統 拒絕 sts inter 相關文章 Android網絡編程(一)HTTP協議原理 Android網絡編程(二)HttpClient與HttpURLConnection Android網絡編程(三)V

Mycat實現讀寫分離

mycatMycat介紹Mycat是一個國產中間件產品,作用在應用層和數據庫之間架橋,使應用通過MyCat來對後端數據庫進行管理,是一款國人自主的開源的中間件產品。算是比較優秀的一款,前身是阿裏公司在維護,很多公司也慢慢的在嘗試接入這個產品,但不得不說官方文檔似乎做的不太友好。至於為什麽選擇MyCat可能只有

Android OpenGL ES 入門系列 --- 了解OpenGL ES的前世今生

target 初始化 vertex 單獨 http hang tex 變化 3d圖 轉載請註明出處 本文出自Hansion的博客 OpenGL ES (OpenGL for Embedded Systems) 是 OpenGL 三維圖形 API 的子集,

Android開發模板代碼——簡單打開圖庫選擇照片

image string code index targe contex 數字 vid equals 首先,先貼上樣本代碼 //檢查權限 public void checkPermission() { if (ContextCompat.c

實現簡易聊天室

ima log body .com 麻煩 導入 定義 右鍵 正常 預備工作: (1)讀取文件的時候可能會遇到多個文件一起傳,可以用線程池。 (2)發送不同類型的請求時,如發送的是聊天信息,發送的是文件,發送的是好友上線請求等,但對於接受者來說都是字節流無法分別,這就需要我們