天天看點

android 錄制螢幕 帶聲音 可直播方案 截屏

這篇部落格應該是相當有分量的部落格了。篇幅會比較長,因為内容很多。我盡力的想寫的詳細,而又不至于繁瑣。這之間的程度是很難把握的,話不多說 進入主題。

首先,在這之前,需要對幾個類,以及他們的方法的有所了解。

MediaCodec

谷歌對這個類的描述如下,MediaCodec類可用于通路底層媒體編解碼器,即編碼器/解碼器元件。它是Android底層多媒體支援基礎架構的一部分(通常與MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface和AudioTrack一起使用)。重點是 編碼解碼器,因為系統産生的資料 ,都是原始的資料,需要他進行處理。

原理:

下面這張圖,隻需要粗略看一看,你隻需要知道 MediaCodec 有兩個ByteBuffer,一個是輸入,一個是輸出。這也很好了解。畢竟編碼解碼器,肯定是要你給它舊資料,它編碼解碼完,還給你一個新資料。兩個ByteBuffer 就相當于兩個籃子,接受發送資料。

android 錄制螢幕 帶聲音 可直播方案 截屏

重要方法:

//傳回要用有效資料填充的輸入緩沖區的索引,如果目前沒有可用的緩沖區,則傳回-1。如果timeoutUs == 0,該方法将立即傳回;如果timeoutUs < 0,則無限期等待輸入緩沖區的可用性;如果timeoutUs > 0,則等待“timeoutUs”微秒。
public int dequeueInputBuffer (long timeoutUs)
           

這個方法呢,就是傳回 輸入緩沖區的索引(mediaCodec可以通過索引找到緩沖區)。也就是上面的ByteBuffer。

//通過上面的索引,找到輸入緩沖區。
public ByteBuffer getInputBuffer (int index)
           

注意 上面這個是input

//傳回輸出緩沖區隊列索引,最多阻塞“timeoutUs”微秒。傳回已成功解碼的輸出緩沖區和INFO_*常量之一的索引。
//info 就是描述輸出緩沖區資料的,例如時間,大小
public int dequeueOutputBuffer (MediaCodec.BufferInfo info,  long timeoutUs)
           
//通過上面的索引,找到輸出緩沖區。
public ByteBuffer getOutputBuffer (int index)
           

注意 上面這個是output

//釋放輸出緩沖區 ,這個也好了解,你從輸出緩沖區取完資料了,得要把緩沖區清空,放回去,取下一次的資料
public void releaseOutputBuffer (int index,  boolean render)
           

MediaMuxer

谷歌描述:MediaMuxer為muxing基本流提供便利。目前MediaMuxer支援MP4、Webm和3GP檔案作為輸出。它還支援muxing b幀在MP4自從Android牛軋糖。

上面編碼解碼完的資料,還需要寫入到檔案裡面,這個類呢,主要就是幫助我們寫檔案的。

重要方法

//添加具有指定格式的跟蹤。
public int addTrack (MediaFormat format)
           

上面這個呢,如果你了解視訊的話就知道,視訊裡面畫面 和聲音 是兩個不同的東西,但是都在一個檔案裡面。是以,他們有一個叫信道的東西。比如,聲音在1信道,畫面在2信道之類的。MediaMuxer隻有知道信道,才知道接下來的資料要寫到哪裡。

//寫入資料的方法
public void writeSampleData (int trackIndex, ByteBuffer byteBuf, MediaCodec.BufferInfo bufferInfo)
           

上面這個 byteBuf參數 就是mediaCodec的輸出緩沖區,info 也是mediaCodec輸出緩沖區的,我們可以info裡面的值進行更改,例如時間,這樣就可以暫停視訊

附加:

錄制視訊畫面的方向。

public void setOrientationHint (int degrees)
           

要注意,應該在start()之前調用這個方法

MediaRecorder

這個類呢,綜合了上面mediaCodec和mediaMuxer,使用這個類,你可以很輕松的錄制到本地。這個隻能視訊錄制到本地,不能用于直播,因為你取不到編碼後的資料。

權限申請

錄屏權限

錄屏權限是需要申請的,每次錄制都必須要申請。

//擷取MediaProjectionManager ,通過這個類 申請權限,
            // 錄屏是一個危險的權限,是以每次錄屏的時候都得這麼申請,使用者同意了才行
            MediaProjectionManager  projectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
            //開啟activity 然後在onActivityResult回調裡面判斷 使用者是否同意錄屏權限
            startActivityForResult(projectionManager.createScreenCaptureIntent(), REQUSET_VIDEO);
           

申請後需要檢視使用者是否同意,在onActivityResult周期裡面檢視

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == REQUSET_VIDEO && resultCode == RESULT_OK) {//已經成功擷取到權限   
            //這個是生成虛拟螢幕所需要的
            MediaProjection projection = projectionManager.getMediaProjection(resultCode, data);
    
        }
    }
           

注意上面的 MediaProjection,我下面所有的代碼,都會用它,一定要記得了。

錄音和讀寫檔案權限

在androidManiFest.xml 裡面

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
           

從android6.0以後還需要 動态申請一下。

/**
     * 請求錄音讀寫權限
     */
    void requestPermission() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PERMISSION_DENIED) {
            String[] permission = {Manifest.permission.RECORD_AUDIO};
            requestPermissions(permission, REQUSET_AUDIO);
        }
    }
           

在onRequestPermissionsResult周期裡面得到結果

@Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUSET_AUDIO) {
            for (int i = 0; i < grantResults.length; i++) {
                if (grantResults[i] == PERMISSION_GRANTED) {
                    Toast.makeText(this, "獲得權限" + permissions[i], Toast.LENGTH_SHORT).show();
                }
            }
        }
    }
           

MediaRecorder使用範例

注意将上面申請錄屏後生成的MediaProjection 傳入

//錄制視訊 放在子線程最好,是以線程
    class MediaRecordThread extends Thread {
        private int mWidth;//錄制視訊的寬
        private int mHeight;//錄制視訊的高
        private int mBitRate;//比特率 bits per second  這個經過我測試 并不是 一定能達到這個值
        private int mDpi;//視訊的DPI
        private String mDstPath;//錄制視訊檔案存放地點
        private MediaRecorder mMediaRecorder;//通過這個類錄制
        private MediaProjection mediaProjection;//通過這個類 生成虛拟螢幕
        private final int FRAME_RATE = 60;//視訊幀數 一秒多少張畫面 并不一定能達到這個值
        private VirtualDisplay virtualDisplay;

        MediaRecordThread(int width, int height, int bitrate, int dpi, MediaProjection mediaProjection, String dstPath) {
            mWidth = width;
            mHeight = height;
            mBitRate = bitrate;
            mDpi = dpi;
            this.mediaProjection = mediaProjection;
            mDstPath = dstPath;
        }

        @Override
        public void run() {
            try {
                //先執行個體化
                initMediaRecorder();
                //下面這個方法的 width height  并不是錄制視訊的寬高。他更明顯是虛拟螢幕的寬高
                //注意 mediaRecorder.getSurface() 這裡我們mediaRecorder的surface 傳遞給虛拟螢幕,
                // 虛拟螢幕顯示的内容就會反映在這個surface上面,自然也就可以錄制了
                virtualDisplay = mediaProjection.createVirtualDisplay("luing", mWidth, mHeight, mDpi,
                        DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, mediaRecorder.getSurface(), null, null
                );
                //開始
                mMediaRecorder.start();
                Zprint.log(this.getClass(), "錄屏線程内部開始工作");
            } catch (IllegalStateException | IOException e) {
//                e.printStackTrace();
                Zprint.log(this.getClass(), " 異常  ", e.toString());
            }
        }

        //執行個體化MediaRecordor
        void initMediaRecorder() throws IOException {
            mMediaRecorder = mediaRecorder = new MediaRecorder();
            //設定視訊來源  錄屏嘛 肯定是使用一個Surface作為視訊源,如果錄制視訊的話 就是使用攝像頭作為來源了 CAMERA
            mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
            //設定要用于錄制的音頻源,必須在setOutputFormat()之前調用。參數有很多,英文注釋也很簡單,下面這個是錄制麥克風,也就是外放的
            //記住 這個必要獲得錄制視訊權限才行,要不然 報錯
            mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
            //設定錄制期間生成的輸出檔案的格式。必須在prepare()之前調用
            mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
            //錄制檔案存放位置
            mediaRecorder.setOutputFile(mDstPath);
            //錄制視訊的寬高
            mediaRecorder.setVideoSize(mWidth, mHeight);
            //FPS
            mediaRecorder.setVideoFrameRate(FRAME_RATE);
            //比特率
            mediaRecorder.setVideoEncodingBitRate(mBitRate);
            //視訊編碼格式
            mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
            //音頻編碼格式
            mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
            //準備 到這裡 就可以開始錄制視訊了
            mediaRecorder.prepare();
        }

        /**
         * 釋放資源
         */
        void release() {
            if (mediaRecorder != null) {
                mediaRecorder.setOnErrorListener(null);
                mediaRecorder.stop();
                mediaRecorder.reset();
                mediaRecorder.release();
                mediaRecorder = null;//help GC
            }
            if (virtualDisplay != null) {
                virtualDisplay.release();
                virtualDisplay = null;//help GC
            }
            if (mediaProjection != null) {
                mediaProjection.stop();
                mediaProjection = null;//help GC
            }
        }
    }
           

這個類很簡單 ,沒什麼說的 ,注釋也很多。

MediaCodec , MediaMuxer,AudioRecord

實作錄屏加錄音 同步方式, 可直播,可推流

//錄制螢幕 加聲音 同步方式
    class AudioRecorderThread extends Thread {
        private AudioRecord mAudiorecord;//錄音類

        private MediaMuxer mMediaMuxer;//通過這個将視訊流寫入本地檔案,如果直播的話 不需要這個

        // 音頻源:音頻輸入-麥克風  我使用其他格式 就會報錯
        private final static int AUDIO_SOURCE = MediaRecorder.AudioSource.MIC;
        // 采樣率
        // 44100是目前的标準,但是某些裝置仍然支援22050,16000,11025
        // 采樣頻率一般共分為22.05KHz、44.1KHz、48KHz三個等級
        private final static int AUDIO_SAMPLE_RATE = 44100;
        // 音頻通道 預設的 可以是單聲道 立體聲道
        private final int AUDIO_CHANNEL = AudioFormat.CHANNEL_IN_DEFAULT;
        // 音頻格式:PCM編碼   傳回音頻資料的格式
        private final int AUDIO_ENCODING = AudioFormat.ENCODING_PCM_16BIT;
        //記錄期間寫入音頻資料的緩沖區的總大小(以位元組為機關)。
        private int audioBufferSize = 0;
        //緩沖數組 ,用來讀取audioRecord的音頻資料
        private byte[] byteBuffer;

        private int audioIndex;//通過MediaMuxer 向本地檔案寫入資料時候,這個标志是用來确定信道的
        private int videoIndex;//上同

        private MediaCodec mAudioMediaCodec;//音頻編碼器

        private MediaCodec mVideoMediaCodec;//視訊編碼器

        private MediaFormat audioFormat;//音頻編碼器 輸出資料的格式
        private MediaFormat videoFormat;//視訊編碼器 輸出資料的格式

        private MediaProjection mediaProjection;//通過這個類 生成虛拟螢幕
        private Surface surface;//視訊編碼器 生成的surface ,用于充當 視訊編碼器的輸入源
        private VirtualDisplay virtualDisplay; //虛拟螢幕
        //這個是每次在編碼器 取資料的時候,這個info 攜帶取出資料的資訊,例如 時間,大小 類型之類的  關鍵幀 可以通過這裡的flags辨識
        private MediaCodec.BufferInfo audioInfo = new MediaCodec.BufferInfo();
        private MediaCodec.BufferInfo videoInfo = new MediaCodec.BufferInfo();

        private volatile boolean isRun = true;//用于控制 是否錄制,這個無關緊要


        private int mWidth;//錄制視訊的寬
        private int mHeight;//錄制視訊的高
        private int mBitRate;//比特率 bits per second  這個經過我測試 并不是 一定能達到這個值
        private int mDpi;//視訊的DPI
        private String mDstPath;//錄制視訊檔案存放地點
        private final int FRAME_RATE = 60;//視訊幀數 一秒多少張畫面 并不一定能達到這個值

        public AudioRecorderThread(int width, int height, int bitrate, int dpi, MediaProjection mediaProjection, String dstPath) {
            this.mediaProjection = mediaProjection;
            this.mWidth = width;
            this.mHeight = height;
            mBitRate = bitrate;
            mDpi = dpi;
            mDstPath = dstPath;
        }

        @Override
        public void run() {
            super.run();
            try {
                //執行個體化 AudioRecord
                initAudioRecord();
                //執行個體化 寫入檔案的類
                initMediaMuxer();
                //執行個體化 音頻編碼器
                initAudioMedicode();
                //執行個體化 視訊編碼器
                initVideoMedicodec();
                //開始
                mAudioMediaCodec.start();
                mVideoMediaCodec.start();
                int timeoutUs = -1;//這個主要是為了 第一次進入while循環 視訊編碼器 能阻塞到 有視訊資料輸出 才運作
                String TAG = "audio";
                while (isRun) {
                    //擷取 輸出緩沖區的索引 通過索引 可以去到緩沖區,緩沖區裡面存着 編碼後的視訊資料 。 timeoutUs為負數的話,會一直阻塞到有緩沖區索引,0的話 立刻傳回
                    int videoOutputID = mVideoMediaCodec.dequeueOutputBuffer(videoInfo, timeoutUs);
                    Log.d(TAG, "video flags " + videoInfo.flags);
                    timeoutUs = 0;//第二次 視訊編碼器 就不需要 阻塞了  0 立刻傳回
                    //索引大于等于0 就代表有資料了
                    if (videoOutputID >= 0) {
                        Zprint.log(this.getClass(), "VIDEO 輸出", videoOutputID, videoInfo.presentationTimeUs);
                        //flags是2的時候 代表輸出的資料 是配置資訊,不是媒體資訊
                        if (videoInfo.flags != 2) {
                            //得到緩沖區
                            //這裡就可以取出資料 進行網絡傳輸
                            ByteBuffer outBuffer = mVideoMediaCodec.getOutputBuffer(videoOutputID);
                            outBuffer.flip();//準備讀取
                            //寫入檔案中  注意 videoIndex
                            mMediaMuxer.writeSampleData(videoIndex, outBuffer, videoInfo);
                        }
                        //釋放緩沖區,畢竟緩沖區一共就兩個 一個輸入 一個輸出,用完是要還回去的
                        mVideoMediaCodec.releaseOutputBuffer(videoOutputID, false);
                    } else if (videoOutputID == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {//輸出格式有變化
                        Zprint.log(this.getClass(), "video Format 改變");
                        videoFormat = mVideoMediaCodec.getOutputFormat();//得到新的輸出格式
                        videoIndex = mMediaMuxer.addTrack(videoFormat);//重新确定信道
                    }
                    //得到 輸入緩沖區的索引
                    int audioInputID = mAudioMediaCodec.dequeueInputBuffer(0);
                    //也是大于等于0 代表 可以輸入資料啦
                    if (audioInputID >= 0) {
                        Zprint.log(this.getClass(), "audio 輸入", audioInputID);
                        ByteBuffer audioInputBuffer = mAudioMediaCodec.getInputBuffer(audioInputID);
                        audioInputBuffer.clear();
                        //從 audiorecord 裡面 讀取原始的音頻資料
                        int read = mAudiorecord.read(byteBuffer, 0, audioBufferSize);
                        if (read < audioBufferSize) {
                            System.out.println(" 讀取的資料" + read);
                        }
                        //上面read可能小于audioBufferSize  要注意
                        audioInputBuffer.put(byteBuffer, 0, read);
                        //入列  注意下面的時間,這個是确定這段資料 時間的 ,視訊音頻 都是一段段的資料,每個資料都有時間 ,這樣播放器才知道 先播放那個資料
                        // 串聯起來 就是連續的了
                        mAudioMediaCodec.queueInputBuffer(audioInputID, 0, read, System.nanoTime() / 1000L, 0);
                    }
                    //音頻輸出
                    int audioOutputID = mAudioMediaCodec.dequeueOutputBuffer(audioInfo, 0);
                    Log.d(TAG, "audio flags " + audioInfo.flags);
                    if (audioOutputID >= 0) {
                        Zprint.log(this.getClass(), "audio 輸出", audioOutputID, audioInfo.presentationTimeUs);
                        audioInfo.presentationTimeUs = videoInfo.presentationTimeUs;//保持 視訊和音頻的統一,防止 時間畫面聲音 不同步
                        if (audioInfo.flags != 2) {
                            //這裡就可以取出資料 進行網絡傳輸
                            ByteBuffer audioOutBuffer = mAudioMediaCodec.getOutputBuffer(audioOutputID);
                            audioOutBuffer.limit(audioInfo.offset + audioInfo.size);//這是另一種 和上面的 flip 沒差別
                            audioOutBuffer.position(audioInfo.offset);
                            mMediaMuxer.writeSampleData(audioIndex, audioOutBuffer, audioInfo);//寫入
                        }
                        //釋放緩沖區
                        mAudioMediaCodec.releaseOutputBuffer(audioOutputID, false);
                    } else if (audioOutputID == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                        Zprint.log(this.getClass(), "audio Format 改變");
                        audioFormat = mAudioMediaCodec.getOutputFormat();
                        audioIndex = mMediaMuxer.addTrack(audioFormat);
                        //注意 這裡  隻在start  視訊哪裡沒有這個,這個方法隻能調用一次
                        mMediaMuxer.start();
                    }
                }
                //釋放資源
                stopRecorder();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        //執行個體化 AUDIO 的編碼器
        void initAudioMedicode() throws IOException {
            audioFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, AUDIO_SAMPLE_RATE, AUDIO_CHANNEL);
            audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, 96000);//比特率
            //描述要使用的AAC配置檔案的鍵(僅适用于AAC音頻格式)。
            audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
            audioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, audioBufferSize << 1);//最大輸入

            //這裡注意  如果 你不确定 你要生成的編碼器類型,就通過下面的 通過類型生成編碼器
            mAudioMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
            //配置
            mAudioMediaCodec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        }

        //執行個體化 VIDEO 的編碼器
        void initVideoMedicodec() throws IOException {
            //這裡的width height 就是錄制視訊的分辨率,可以更改  如果這裡的分辨率小于 虛拟螢幕的分辨率 ,你會發現 視訊隻錄制了 螢幕部分内容
            videoFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, mWidth, mHeight);
            videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
            videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, mBitRate);//比特率 bit機關
            videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 60);//FPS
            videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);//關鍵幀  是完整的一張圖檔,其他的都是部分圖檔
            //通過類型建立編碼器  同理 建立解碼器也是一樣
            mVideoMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
            //配置
            mVideoMediaCodec.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            //讓視訊編碼器 生成一個弱引用的surface, 這個surface不會保證視訊編碼器 不被回收,這樣 編碼視訊的時候 就不需要 傳輸資料進去了
            surface = mVideoMediaCodec.createInputSurface();
            //建立虛拟螢幕,讓虛拟螢幕内容 渲染在上面的surface上面 ,這樣 才能 不用傳輸資料進去
            virtualDisplay = mediaProjection.createVirtualDisplay("video", mWidth, mHeight, dpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, surface, null, null);
        }

        //錄音的類,用于給音頻編碼器 提供原始資料
        void initAudioRecord() {
            //得到 音頻錄制時候 最小的緩沖區大小
            audioBufferSize = AudioRecord.getMinBufferSize(AUDIO_SAMPLE_RATE, AUDIO_CHANNEL, AUDIO_ENCODING);
            byteBuffer = new byte[audioBufferSize];
            //兩種方式 都可以
//            mAudiorecord = new AudioRecord(AUDIO_SOURCE, AUDIO_SAMPLE_RATE, AUDIO_CHANNEL, AUDIO_ENCODING, audioBufferSize);
            //通過builder方式建立
            mAudiorecord = new AudioRecord.Builder()
                    .setAudioSource(MediaRecorder.AudioSource.MIC)
                    .setAudioFormat(new AudioFormat.Builder()
                            .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
                            .setSampleRate(32000)
                            .setChannelMask(AudioFormat.CHANNEL_IN_MONO)
                            .build())
                    .setBufferSizeInBytes(audioBufferSize)
                    .build();
            //開始錄制,這裡可以檢查一下狀态,但隻要代碼無誤,檢查是無需的 state
            mAudiorecord.startRecording();
        }

        //如果 要錄制mp4檔案的話,需要調用這個方法 建立 MediaMuxer
        private void initMediaMuxer() throws Exception {
            //注意格式  建立錄制的檔案
            String filePath = filePath("luyi.mp4");
            //執行個體化 MediaMuxer 編碼器取出的資料,通過它寫入檔案中
            mMediaMuxer = new MediaMuxer(filePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
        }

        //釋放資源
        void stopRecorder() {
            if (mVideoMediaCodec != null) {
                mVideoMediaCodec.stop();
                mVideoMediaCodec.release();
                mVideoMediaCodec = null;
            }
            if (mAudioMediaCodec != null) {
                mAudioMediaCodec.stop();
                mAudioMediaCodec.release();
                mAudioMediaCodec = null;
            }
            if (mAudiorecord != null) {
                mAudiorecord.stop();
                mAudiorecord.release();
                mAudiorecord = null;
            }

            mMediaMuxer.stop();
            mMediaMuxer.release();
            mMediaMuxer = null;

            virtualDisplay.release();
            virtualDisplay = null;
        }
    }
           

這個類呢,首先用AudioRecord來擷取音頻資料,用virtualDisplay來擷取視訊資料。因為,有音頻和視訊,是以 要用兩個mediaCodec 來進行工作。mediaCodec有兩種方式,一種是同步的,像上面這樣,還有一種是異步的。兩個codec配合的話,我感覺還是同步的比較好。異步也是可以實作的,隻是較為繁瑣。需要用到隊列之類的。

細心的讀者會發現,上面videoCodec 是沒有取出輸入緩沖區的。因為 ,它的輸入工作 被surface 代替了。virtualDisplay的内容直接渲染在videoCodec的surface 裡面,充當了 輸入資料。

還有如果,你想進行直播的話,取出輸出緩沖區的資料傳輸,要注意 還有頭部資訊,這個下面例子中。

MediaCodec 錄屏 異步方式實作

//錄屏 異步模式  沒有聲音的
    class ScreenAsyn implements Runnable {
        private MediaProjection mediaProjection;
        private MediaFormat videoFormat;
        private MediaCodec mVideoMediaCodec;
        private VirtualDisplay virtualDisplay;
        private MediaMuxer mediaMuxer;
        private int videoIndex;

        private int mWidth;//錄制視訊的寬
        private int mHeight;//錄制視訊的高
        private int mBitRate;//比特率 bits per second  這個經過我測試 并不是 一定能達到這個值
        private int mDpi;//視訊的DPI
        private String mDstPath;//錄制視訊檔案存放地點
        private final int FRAME_RATE = 60;//視訊幀數 一秒多少張畫面 并不一定能達到這個值

        public ScreenAsyn(int width, int height, int bitrate, int dpi, MediaProjection mediaProjection, String dstPath) {
            this.mWidth = width;
            mHeight = height;
            mBitRate = bitrate;
            mDpi = dpi;
            this.mediaProjection = mediaProjection;
            mDstPath = dstPath;
        }

        @Override
        public void run() {
            try {
                mediaMuxer = new MediaMuxer(mDstPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);

                Surface surface;
                //這裡的width height 就是錄制視訊的分辨率,可以更改  如果這裡的分辨率小于 虛拟螢幕的分辨率 ,你會發現 視訊隻錄制了 螢幕部分内容
                videoFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, mWidth, mHeight);
                videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
                videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, mBitRate);//比特率 bit機關
                videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 60);//FPS
                videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);//關鍵幀  是完整的一張圖檔,其他的都是部分圖檔
                //通過類型建立編碼器  同理 建立解碼器也是一樣
                mVideoMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
                mVideoMediaCodec.setCallback(new MediaCodec.Callback() {
                    @Override
                    public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {

                    }

                    @Override
                    public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {
                        Zprint.log(this.getClass(), "info", info.offset, info.size, info.presentationTimeUs);
                        ByteBuffer outBuffer = codec.getOutputBuffer(index);
                        outBuffer.flip();
                        mediaMuxer.writeSampleData(videoIndex, outBuffer, info);
                        codec.releaseOutputBuffer(index, false);
                    }

                    @Override
                    public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) {

                    }

                    @Override
                    public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) {
                        //這是h.264需要的
                        ByteBuffer sps = format.getByteBuffer("csd-0");    // SPS
                        ByteBuffer pps = format.getByteBuffer("csd-1");    // PPS
                        //VP9 需要的
                        ByteBuffer CodecPrivate = format.getByteBuffer("csd-0"); //
                        videoIndex = mediaMuxer.addTrack(format);
                        mediaMuxer.start();
                    }
                });

                mVideoMediaCodec.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
                surface = mVideoMediaCodec.createInputSurface();
                virtualDisplay = mediaProjection.createVirtualDisplay("asyn", mWidth, mHeight, mDpi, mBitRate, surface, null, null);
                mVideoMediaCodec.start();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        //暫停錄制
        void stopRecord() {
            if (mVideoMediaCodec != null) {
                mVideoMediaCodec.stop();
                mVideoMediaCodec.release();
                mVideoMediaCodec = null;
            }
            if (mediaMuxer != null) {
                mediaMuxer.release();
                mediaMuxer = null;
            }
            if (virtualDisplay != null) {
                virtualDisplay.release();
            }
        }
    }

           

這是異步實作的,個人感覺異步的實作比較好。

下面這個就是進行網絡直播前,流的一些必要資訊。

android 錄制螢幕 帶聲音 可直播方案 截屏

流的資料:

android 錄制螢幕 帶聲音 可直播方案 截屏

取出上面的 outBuffer 資料,就是媒體資料了,上面這個是H.264格式的資料。

總結

網上的這方面資料比較少,我研究了一段時間,寫出來。在這裡說一下個人心得吧。一開始可以看看網上的部落格,然後,你如果想深入,那麼還是看開發者文檔。

附帶 截圖

/**
     * 截圖
     * @param mediaProjection
     * @return bitmap 
     */
    public Bitmap screenShot(MediaProjection mediaProjection){
        Objects.requireNonNull(mediaProjection);
        ImageReader imageReader = ImageReader.newInstance(1080, 1920, PixelFormat.RGBA_8888, 60);
        VirtualDisplay virtualDisplay = mediaProjection.createVirtualDisplay("screen", width, height, dpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,imageReader.getSurface(), null, null);
        //這個是取 現在最新的圖檔
        Image image = imageReader.acquireLatestImage();
        //也可以挨個取圖檔
//        Image image = imageReader.acquireNextImage();
        return image2Bitmap(image);
    }

    public static Bitmap image2Bitmap(Image image) {
        if (image == null) {
            System.out.println("image 為空");
            return null;
        }
        int width = image.getWidth();
        int height = image.getHeight();
        System.out.println(width+"    "+height);
        final Image.Plane[] planes = image.getPlanes();
        final ByteBuffer buffer = planes[0].getBuffer();
        int pixelStride = planes[0].getPixelStride();
        int rowStride = planes[0].getRowStride();
        int rowPadding = rowStride - pixelStride * width;

        Bitmap bitmap = Bitmap.createBitmap(width+ rowPadding / pixelStride , height, Bitmap.Config.ARGB_8888);
        bitmap.copyPixelsFromBuffer(buffer);

    /*    //壓縮圖檔
        Matrix matrix = new Matrix();
        matrix.setScale(0.5F, 0.5F);
        System.out.println(bitmap.isMutable());
        bitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, false);
*/
        image.close();
        return bitmap;
    }
           

代碼位址

如果有不懂的,可以關注我的公衆号 “知我飯否” 向我留言。我會每天更新一些文章,有興趣的可以 微信 搜尋"知我飯否" or 掃描我的 部落格頭像。

android 錄制螢幕 帶聲音 可直播方案 截屏