ライブ画面録画のAndroid実装 (a) ScreenRecorderの簡易解析
画面録画ライブの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のデモの一般的な分析であり、時間の要約のために急いで、私も綿密な調査を探求しなかった部分の多くの詳細は、指示が間違っているか、場所を理解していない場合は、懐疑的な態度で読んでください、私はあなたが指摘し、ありがとうございます助ける願っています!.
参考資料
その他、多くの貴重な参考文献や記事が、この機能の開発に利用されています。
関連
-
android studio3.2 a pitfall: リソースの処理に失敗しました。詳細は上記の aapt の出力を参照してください。
-
ADBサーバーがACKしない問題を解決 (pro-test)
-
解決策 エラーです。jarfile にアクセスできません。\ʕ-̫͡-ʔ
-
JSONException: java.lang.String は JSONObject ソリューションに変換できません。
-
AndroidでSPAN_EXCLUSIVE_EXCLUSIVEスパンが長さ0にできない場合、EditTextでコンテンツを削除する
-
Android 開発の問題点:ActivityNotFoundException: 明示的なアクティビティクラスを見つけることができません
-
Android画像角丸
-
Android Studioのヒント - これを読めば、すべてのヒントが役に立つ
-
Android Studioの設定 Gradleの概要
-
Mac OS X用Android Studioショートカット
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン
おすすめ
-
ADBサーバーがACKしない ソリューション
-
BindView 問題 NULLオブジェクト参照で仮想メソッド 'void android ...' を呼び出そうとする
-
アプリケーションがメインスレで仕事をしすぎている可能性がある
-
Androidのレイアウトにおけるmarginとpaddingの違いについて
-
Androidの内部育成に磨きをかける2年間
-
Androidスレッドの詳細
-
AndroidのSMSメッセージ
-
MPAndroidChartのPieChartで、セクターが表示されず、中央のテキストのみが表示される。
-
INSTALL_FAILED_INVALID_APK: 分割された lib_slice_5_apk が複数回定義されている 例外
-
SQLiteReadOnlyDatabaseException: 読み取り専用のデータベースを書き込もうとした (コード 1032)