1. ホーム
  2. Android

ライブ画面録画のAndroid実装 (a) ScreenRecorderの簡易解析

2022-02-17 19:02:37
<パス

画面録画ライブのAndroid実装(a)ScreenRecorderの簡易解析

Androidライブ画面録画(2) オンデマンドでの製品機能研究はハードルが高い

Androidライブ動画撮影(3) MediaProjection + VirtualDisplay + librtmp + MediaCodecで動画をエンコードしてrtmpサーバにプッシュする。

Bilibiliのライブビデオ録画機能を目的としたプロジェクト要件に対し、基本的にバーで行うことを模倣しています。調べてみると、BilibiliはMediaProjectionとVirtualDisplayの組み合わせで、Android 5.0 Lollipop API 21以上でないと動作しないことがわかりました。

実は、公式の アンドロイドスクリーンキャプチャー このSampleには、すでにMediaRecorderの実装と使い方、MediaRecorderを使って画面をローカルファイルに記録するデモがあり、そこからこれらのAPIの使い方を理解することができます。

ライブ配信が必要な場合は、MediaCodecをカスタマイズして、エンコードされたフレームをMediaCodecから取得する必要があるので、元のフレームをキャプチャする手間が省けますね。しかし、問題は、我々は慎重に多くのポットホールを登ったH264ファイルとFLVのカプセル化関連技術の構造を理解していなかったため、私は一つずつを記録します、私はそれがそれを使用する人のために役立つことを願っています。

このプロジェクトの中で、私にとって最も重要なデモのひとつは、YromさんのGitHubプロジェクト スクリーンレコーダー これは、画面を録画して、ビデオストリームをローカルのMP4ファイルとして保存するデモです(エヘン、実はYromはBilibiliの社員なんですよね?( ゜-゜)つロ゜)つロ゜)つロ゜)つロ゜)つロ゜)つロ゜)つ。ここでは、デモの実装を一般的に分析し、私の実装は後ほど説明します。

スクリーンレコーダー

特定 原理 は、デモの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  は削除されます。  int index = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_US); ... ByteBuffer encodedData = mEncoder.getOutputBuffer(index); ... mMuxer.writeSampleData(mVideoTrackIndex, encodedData, mBufferInfo);  画像のメタデータを MP4 ファイルに出力します。
DisplayManager

そのため、実際には アンドロイド4.4 を使用すると、Android 4.4では VirtualDisplay を作成します。 onActivityResult また、画面を録画することも可能ですが、権限制限の関係で ルート . (参照  DisplayManager.createVirtualDisplay() )

Demoはシンプルで、2つのJavaファイルです。

  • MainActivity.java
  • ScreenRecorder.java

MainActivity

クラスは実装への入り口に過ぎず、最も重要なメソッドは @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); } なぜなら、MediaProjectionはそのメソッドから開く必要があるからです。しかし、最初にMediaProjectionManagerを初期化することを忘れないでください。

run()

スクリーンレコーダー

このスレッドは、非常に明確な構造を持つ @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の初期化、VirtualDisplayの作成、およびエンコード用のループの完全な実装をすべて行います。

スレッド本体

private void prepareEncoder() throws IOException {
    MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, mWidth, mHeight);
    format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
            MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); // parameters that must be configured for screen recording
    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(); // need to be created after createEncoderByType and before start(), the source code comment is very clear
    Log.d(TAG, "created input surface: " + mSurface);
    mEncoder.start();
}


MediaCodecの初期化

メソッドでは、エンコーダのパラメータを設定して起動し、Surfaceを作成するための2つの重要なステップが実行されます。

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();
    // Here you can also get sps and pps, see method 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);
}

巡回型符号化を実現するエンコーダ

以下のコードは、エンコード処理です。筆者はビデオキャプチャにMuxerを使っているので、resetOutputFormatメソッドでの実際の意味は、エンコードしたビデオパラメータ情報をMuxerに渡してMuxerを起動することです。

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

 /**
 * 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; // key frame

/* This indicates that the buffer is marked with a key frame.
 * 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; // This indicates that the current data is avcc and you can get sps pps here

/*
 * 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;

sps ppsのByteBufferを取得、ここでのsps ppsは読み取り専用であることに注意

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.
        // The general idea is that the configuration information (avcc) was already fed to the muxer in resetOutputFormat() and is no longer used here, however, in my project this step is a very important one, because I need to manually implement the sps, pps synthesis in advance to send to the streaming server
        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 is the encoded video frame, but note that the author does not make a distinction between key frames and normal video frames here, and writes the data to Muxer uniformly
        mMuxer.writeSampleData(mVideoTrackIndex, encodedData, mBufferInfo);
        Log.i(TAG, "sent " + mBufferInfo.size + " bytes to muxer... ");
    }
}

録画したビデオフレームのエンコード処理

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; // key frame

/* This indicates that the buffer is marked with a key frame.
 * 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; // This indicates that the current data is avcc and you can get sps pps here

/*
 * 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.
        // The general idea is that the configuration information (avcc) was already fed to the muxer in resetOutputFormat() and is no longer used here, however, in my project this step is a very important one, because I need to manually implement the sps, pps synthesis in advance to send to the streaming server
        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 is the encoded video frame, but note that the author does not make a distinction between key frames and normal video frames here, and writes the data to Muxer uniformly
        mMuxer.writeSampleData(mVideoTrackIndex, encodedData, mBufferInfo);
        Log.i(TAG, "sent " + mBufferInfo.size + " bytes to muxer... ");
    }
}

上記は、ScreenRecorderのデモの一般的な分析であり、時間の要約のために急いで、私も綿密な調査を探求しなかった部分の多くの詳細は、指示が間違っているか、場所を理解していない場合は、懐疑的な態度で読んでください、私はあなたが指摘し、ありがとうございます助ける願っています!.

参考資料

その他、多くの貴重な参考文献や記事が、この機能の開発に利用されています。

  1. Androidスクリーンキャスティングソリューション
  2. Google公式EncodeVirtualDisplayTest
  3. FLVファイルフォーマット解析
  4. H264とAACのライブストリーミングのためのlibrtmpの使用
  5. その後のアップデート...