日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > Android >内容正文

Android

android实现录像功能吗,Android实现录屏直播(一)ScreenRecorder的简单分析

發(fā)布時間:2024/9/19 Android 42 豆豆
生活随笔 收集整理的這篇文章主要介紹了 android实现录像功能吗,Android实现录屏直播(一)ScreenRecorder的简单分析 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

應(yīng)項目需求瞄準了Bilibili的錄屏直播功能,基本就仿著做一個吧。研究后發(fā)現(xiàn)Bilibili是使用的MediaProjection 與 VirtualDisplay結(jié)合實現(xiàn)的,需要 Android 5.0 Lollipop API 21以上的系統(tǒng)才能使用。

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

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

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

ScreenRecorder

具體的原理在Demo的README中已經(jīng)說得很明白了:

Display 可以“投影”到一個 VirtualDisplay

通過 MediaProjectionManager 取得的 MediaProjection創(chuàng)建VirtualDisplay

VirtualDisplay 會將圖像渲染到 Surface中,而這個Surface是由MediaCodec所創(chuàng)建的

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 得到的圖像元數(shù)據(jù)封裝并輸出到MP4文件中

int index = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_US);

...

ByteBuffer encodedData = mEncoder.getOutputBuffer(index);

...

mMuxer.writeSampleData(mVideoTrackIndex, encodedData, mBufferInfo);

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

Demo很簡單,兩個Java文件:

MainActivity.java

ScreenRecorder.java

MainActivity

類中僅僅是實現(xiàn)的入口,最重要的方法是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

這是一個線程,結(jié)構(gòu)很清晰,run()方法中完成了MediaCodec的初始化,VirtualDisplay的創(chuàng)建,以及循環(huán)進行編碼的全部實現(xiàn)。

線程主體

@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的初始化

方法中進行了編碼器的參數(shù)配置與啟動、Surface的創(chuàng)建兩個關(guān)鍵的步驟

private void prepareEncoder() throws IOException {

MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, mWidth, mHeight);

format.setInteger(MediaFormat.KEY_COLOR_FORMAT,

MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); // 錄屏必須配置的參數(shù)

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()之前才能創(chuàng)建,源碼注釋寫的很清楚

Log.d(TAG, "created input surface: " + mSurface);

mEncoder.start();

}

編碼器實現(xiàn)循環(huán)編碼

下面的代碼就是編碼過程,由于作者使用的是Muxer來進行視頻的采集,所以在resetOutputFormat方法中實際意義是將編碼后的視頻參數(shù)信息傳遞給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只讀狀態(tài)

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; // 關(guān)鍵幀

/**

* 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; // 該狀態(tài)表示當前數(shù)據(jù)是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;

實現(xiàn)編碼:

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)已經(jīng)在之前的resetOutputFormat()中喂給了Muxer,此處已經(jīng)用不到了,然而在我的項目中這一步卻是十分重要的一步,因為我需要手動提前實現(xiàn)sps, pps的合成發(fā)送給流媒體服務(wù)器

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是編碼后的視頻幀,但注意作者在此并沒有進行關(guān)鍵幀與普通視頻幀的區(qū)別,統(tǒng)一將數(shù)據(jù)寫入Muxer

mMuxer.writeSampleData(mVideoTrackIndex, encodedData, mBufferInfo);

Log.i(TAG, "sent " + mBufferInfo.size + " bytes to muxer...");

}

}

以上就是對ScreenRecorder這個Demo的大體分析,由于總結(jié)時間倉促,很多細節(jié)部分我也沒有進行深入的發(fā)掘研究,所以請大家抱著懷疑的態(tài)度閱讀,如果說明有誤或是理解不到位的地方,希望大家?guī)兔χ赋?#xff0c;謝謝!

參考文檔

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

總結(jié)

以上是生活随笔為你收集整理的android实现录像功能吗,Android实现录屏直播(一)ScreenRecorder的简单分析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。