有關Android截圖與錄屏功能的學習

本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家發(fā)布

這篇文章启搂,會帶你學習如何使用MediaProjection艘狭,MediaCodec以及MediaMuxer來實現(xiàn)簡單的截屏和錄屏功能萎庭。
因為MediaProjection是5.0以上才出現(xiàn)的,所以今天所講述功能實現(xiàn),只在5.0以上的系統(tǒng)有效驶悟。
才疏學淺牙捉,講不深入竹揍,見諒。

截屏:

步驟如下:

1:獲取MediaProjectionManager
2:通過MediaProjectionManager.createScreenCaptureIntent()獲取Intent
3:通過startActivityForResult傳入Intent然后在onActivityResult中通過MediaProjectionManager.getMediaProjection(resultCode,data)獲取MediaProjection
4:創(chuàng)建ImageReader,構建VirtualDisplay
5:最后就是通過ImageReader截圖邪铲,就可以從ImageReader里獲得Image對象芬位。
6:將Image對象轉換成bitmap

實現(xiàn):

步驟已經給出了,我們就按照步驟來實現(xiàn)代碼吧带到。

首先MediaProjectionManager是系統(tǒng)服務昧碉,我們通過getSystemService(MEDIA_PROJECTION_SERVICE)獲取它

projectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);

然后調用startActivityForResult傳入projectionManager.createScreenCaptureIntent()創(chuàng)建的Intent

startActivityForResult(projectionManager.createScreenCaptureIntent(),
                SCREEN_SHOT);

緊接著我們就可以在onActivityResult(int requestCode, int resultCode, Intent data)中通過resultCode和data來獲取MediaProjection

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if(requestCode == SCREEN_SHOT){
            if(resultCode == RESULT_OK){
                //獲取MediaProjection
                mediaProjection = projectionManager.getMediaProjection(requestCode,data);
            }
        }
    }

然后就是創(chuàng)建ImageReader和VirtualDisplay

        imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1);
        if(imageReader!=null){
            Log.d(TAG, "imageReader Successful");
        }
        mediaProjection.createVirtualDisplay("ScreenShout",
                width,height,dpi,
                DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                imageReader.getSurface(),null,null);

這里我們依次講解一下。

首先是ImageReader.newInstance方法:
public static ImageReader newInstance(int width, int height, int format, int maxImages)

方法里接收四個參數(shù)揽惹。
前兩個width,height是用來指定生成圖像的寬和高被饿。
第三個參數(shù)format是圖像的格式,這個格式必須是 ImageFormat
PixelFormat中的一個搪搏,這兩個Format里有很多格式狭握,大家可以點進去看看,我們例子中使用的是PixelFormat.RGBA_8888格式(需要注意的是并不是所有的格式都被ImageReader支持疯溺,比如說ImageFormat.NV21)论颅。
第四個參數(shù)是maxImages哎垦,這個參數(shù)指的是你想同時在ImageReader里獲取到的Image對象的個數(shù),這個參數(shù)我不是很懂恃疯,我不理解同時的意思漏设。我的理解是ImageReader是一個類似數(shù)組的東西,然后我們可以通過acquireLatestImage()或acquireNextImage()方法來得到里面的Image對象(可能有誤澡谭,僅供參考)愿题。這個值應該設置的越小越好,但是得大于0蛙奖,所以我們上面設置的是1潘酗。

然后我們看看mediaProjection.createVirtualDisplay方法:
createVirtualDisplay(@NonNull String name,
            int width, int height, int dpi, int flags, @Nullable Surface surface,
            @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler)

首先這個方法返回的是VirtualDisplay。
前四個不用說了雁仲,分別是VirtualDisplay的名字仔夺,寬,高和dpi攒砖。
第五個參數(shù)缸兔,大家可以點 DisplayManager查看所有的flags,我沒有具體的研究過吹艇,在本次要實現(xiàn)的例子里惰蜜,除了VIRTUAL_DISPLAY_FLAG_SECURE這個會報錯,其他的flags效果都一樣受神。
第六個參數(shù)抛猖,是一個Surface。我這里表達一下我的理解鼻听,當VirtualDisplay被創(chuàng)建出來時财著,也就是createVirtualDisplay調用后,你在真實屏幕上的每一幀都會輸入到Surface參數(shù)里撑碴。也就是說撑教,如果你放個SurfaceView,然后傳入SurfaceView的Surface那么你在屏幕上的操作都會顯示在SurfaceView里(這里我們后面錄屏會講)醉拓。我們這里傳入的是ImageReader的Surface伟姐。這其中的邏輯我的理解是這樣的,真實屏幕的每一幀都都會傳給ImageReader亿卤,根據ImageReader的maxImages參數(shù)玫镐,比如說maxImages是2,那么ImageReader始終保持兩幀圖片怠噪,但這兩幀圖片是一直隨著真實屏幕的操作而更新的(不知道大家有沒有聽懂)恐似。
第七個參數(shù),是一個回調函數(shù)傍念,在VirtualDisplay狀態(tài)改變時調用矫夷。因為我們這里沒有葛闷,所以傳null。
第八個參數(shù)双藕,這里我給出原文:“The Handler on which the callback should be invoked, or null if the callback should be invoked on the calling thread's main Looper
.”因為我翻譯不好淑趾。不過和普通的Handler使用場景類似。

現(xiàn)在我們ImageReader和VirtualDisplay忧陪,接下來我們就可以通過ImageReader的acquireLatestImage()或acquireNextImage()來得到Image對象了扣泊。

        SystemClock.sleep(1000);
        Image image = imageReader.acquireNextImage();

這里有個坑,就是你在獲取Image的時候嘶摊,得先暫停1秒左右延蟹,不然就會獲取失敗(原因未知)。

現(xiàn)在我們有了Image對象叶堆,但是Image對象并不能直接作為UI資源被使用阱飘,我們可以將它轉換成Bitmap對象。

        int width = image.getWidth();
        int height = image.getHeight();
        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.createBitmap(width + rowPadding / pixelStride, height, Bitmap.Config.ARGB_8888);
        bitmap.copyPixelsFromBuffer(buffer);
        image.close();

這里最主要的邏輯就是像素與字節(jié)的轉換虱颗,我們需要將Image對象的字節(jié)流寫進Bitmap里沥匈,但是Bitmap接收的是像素格式的。
我們一行一行來看:
首先獲取image對象的寬和高忘渔,注意width和height是像素格式的高帖。
然后獲取ByteBuffer,里面存放的就是圖片的字節(jié)流畦粮,是字節(jié)格式的棋恼。我是這么理解的,ByteBuffer里面是一長串的字節(jié)序列锈玉,按照某種格式分成行列就變成了圖片。
然后獲取PixelStride义起,這指的是兩個像素的距離(就是一個像素頭部到相鄰像素的頭部)拉背,這是字節(jié)格式的。
RowStride是一行占用的距離(就是一行像素頭部到相鄰行像素的頭部)默终,這個大小和width有關椅棺,這里需要注意,因為內存對齊的原因齐蔽,所以每行會有一些空余两疚。這個值也是字節(jié)格式的。
緊接著我們需要創(chuàng)建一個Bitmap用來接受Image的buffer的輸入含滴,buffer是字節(jié)流诱渤,它會按照我們設置的format轉換成像素,所以這里最重要的一個地方就是Bitmap創(chuàng)建的大小谈况,因為高度就是行數(shù)所以就是height勺美,但是寬度因為上面說的內存對齊問題會有些空余递胧,所以我們要先求出空余部分,然后加上width赡茸。

int rowPadding = rowStride - pixelStride * width;

這句話用整行的距離減去了一行里像素及空隙占用的距離缎脾,剩下的就是空余部分。但是這個是字節(jié)格式的占卧。我們將它除以pixelStride遗菠,也就是一個像素及空隙占用的字節(jié)大小,就轉換成了像素格式华蜒。
然后:

width + rowPadding / pixelStride

這個就是一行里像素的占用了辙纬,我們將它傳給Bitmap:

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

創(chuàng)建出合適大小的Bitmap,然后把Image的buffer傳給它友多,就成功的將Image對象轉換成了Bitmap牲平。
這里我可能講的不清楚,我給大家畫了張圖:

IMG_3469.JPG

上面的一小格一小格是一塊塊像素域滥。
好了纵柿,現(xiàn)在我們已經獲取到了bitmap了,我們可以把它放到ImageView里顯示一下启绰,我寫了一個例子昂儒,效果如下:

5.gif

點擊按鈕,彈出一個對話框請求截屏委可,點擊立即開始的話渊跋,截屏就會顯示在下面的ImageView里。

截屏就這樣着倾,我已經盡力了拾酝,╮(╯▽╰)╭

錄屏:

步驟:

錄屏的前三步和截屏是一樣的,出現(xiàn)分歧點的地方在于VirtualDisplay創(chuàng)建時傳入的Surface卡者,還記得我們上面說的嗎蒿囤,說在創(chuàng)建VirtualDisplay的時候,傳入一個SurfaceView的Surface的話崇决,那么你在真實屏幕上的操作材诽,都會重現(xiàn)在SurfaceView上。我們來試一下:

mediaProjection.createVirtualDisplay("ScreenShout",
                width,height,dpi,
                DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                surfaceView.getHolder().getSurface(),null,null);

我們在Surface參數(shù)中傳入一個SurfaceView的Surface
效果如下:

6.gif

可以看到我們放了一個Button恒傻,放了一個ImageView脸侥,放了一個SurfaceView。
點擊Button盈厘,然后點立即開始之后睁枕,真實屏幕就映射到了SurfaceView里。
所以當創(chuàng)建VirtualDisplay時,真實屏幕就映射到了Surface譬重,也就是我們可以再Surface里拿到屏幕的一個輸入拒逮。那我們要錄屏的話,就只要把Surface轉換成我們需要的格式就行了臀规,在本篇文章的例子中滩援,我們會將Surface對象轉換成mp4格式。這就需要用到MediaCodec類和MediaMuxer類塔嬉。MediaCodec生成一個Surface用來接收屏幕的輸出并按照格式編碼玩徊,然后傳給MediaMuxer用來封裝成mp4格式的視頻。

        //第一個參數(shù)是mime類型谨究,我們傳入video/avc
        //第二第三個參數(shù)是寬和高
        MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);
        //COLOR_FormatSurface這里表明數(shù)據將是一個graphicbuffer元數(shù)據
        format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
                MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        //設置碼率恩袱,碼率越大視頻越清晰,相對的占用內存也要更大
        format.setInteger(MediaFormat.KEY_BIT_RATE, 6000000);
        //設置幀數(shù)
        format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
        //設置兩個關鍵幀的間隔胶哲,這個值你設置成多少對我們這個例子都沒啥影響
        //這個值做視頻的朋友可能會懂畔塔,反正我不是很懂,大概就是你預覽的時候鸯屿,比如你設置為10澈吨,那么你10秒內的預覽圖都是同一張
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10);
        //創(chuàng)建一個MediaCodec實例
        mediaCodec = MediaCodec.createEncoderByType("video/avc");
        //第一個參數(shù)將我們上面設置的format傳進去
        //第二個參數(shù)是Surface,如果我們需要讀取MediaCodec編碼后的數(shù)據就要傳寄摆,但我們這里不需要所以傳null
        //第三個參數(shù)關于加解密的谅辣,我們不需要,傳null
        //第四個參數(shù)是一個確定的標志位婶恼,也就是我們現(xiàn)在傳的這個
        mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        //獲取MediaCodec的surface桑阶,這個surface其實就是一個入口,屏幕作為輸入源就會進入這個入口勾邦,然后交給MediaCodec編碼
        surface = mediaCodec.createInputSurface();
        mediaCodec.start();

上面講了MediaCodec的創(chuàng)建蚣录,我們也可以從中看到屏幕數(shù)據是怎么進入MediaCodec的。具體的我已經注釋了眷篇。

接下來我們創(chuàng)建一個MediaMuxer對象:

//第一個參數(shù)是輸出的地址
//第二個參數(shù)是輸出的格式萎河,我們設置的是mp4格式
mediaMuxer = new MediaMuxer(filePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);

然后創(chuàng)建VirtualDisplay,把MediaCodec的surface傳進去:

virtualDisplay = mediaProjection.createVirtualDisplay(TAG + "-display",
                            width, height, dpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,
                            surface, null, null);

最后就是視頻的編碼與轉換MP4還有保存了:

    private void recordVirtualDisplay() {
        while (!mQuit.get()) {
            //dequeueOutputBuffer方法你可以這么理解铅歼,它會出列一個輸出buffer(你可以理解為一幀畫面),返回值是這一幀畫面的順序位置(類似于數(shù)組的下標)
            //第二個參數(shù)是超時時間,如果超過這個時間了還沒成功出列换可,那么就會跳過這一幀椎椰,去出列下一幀,并返回INFO_TRY_AGAIN_LATER標志位
            int index = mediaCodec.dequeueOutputBuffer(bufferInfo, 10000);
            //當格式改變的時候嗎沾鳄,我們需要重新設置格式
            //在本例中慨飘,只第一次開始的時候會返回這個值
            if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                resetOutputFormat();

            } else if (index >= 0) {//這里說明dequeueOutputBuffer執(zhí)行正常
                //這里執(zhí)行我們轉換成mp4的邏輯
                encodeToVideoTrack(index);
                mediaCodec.releaseOutputBuffer(index, false);
            }
        }
    }
    //這里是將數(shù)據傳給MediaMuxer,將其轉換成mp4
    private void encodeToVideoTrack(int index) {
        //通過index獲取到ByteBuffer(可以理解為一幀)
        ByteBuffer encodedData = mediaCodec.getOutputBuffer(index);
        //當bufferInfo返回這個標志位時,就說明已經傳完數(shù)據了瓤的,我們將bufferInfo.size設為0休弃,準備將其回收
        if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
            bufferInfo.size = 0;
        }
        if (bufferInfo.size == 0) {
            encodedData = null;
        } 
        if (encodedData != null) {
            encodedData.position(bufferInfo.offset);//設置我們該從哪個位置讀取數(shù)據
            encodedData.limit(bufferInfo.offset + bufferInfo.size);//設置我們該讀多少數(shù)據
            //這里將數(shù)據寫入
            //第一個參數(shù)是每一幀畫面要放置的順序
            //第二個是要寫入的數(shù)據
            //第三個參數(shù)是bufferInfo,這個數(shù)據包含的是encodedData的offset和size
            mediaMuxer.writeSampleData(videoTrackIndex, encodedData, bufferInfo);
            
        }
    }

    //這個方法其實就是設置MediaMuxer的Format
    private void resetOutputFormat() {
        //將MediaCodec的Format設置給MediaMuxer
        MediaFormat newFormat = mediaCodec.getOutputFormat();
        //獲取videoTrackIndex圈膏,這個值是每一幀畫面要放置的順序
        videoTrackIndex = mediaMuxer.addTrack(newFormat);
        mediaMuxer.start();
        muxerStarted = true;
    }

好了塔猾,錄屏到此結束了。
我們來看下實例演示:

7.gif

總結:

這篇博客寫的真是費時費力稽坤,果然水平未到就不該強行寫文丈甸。

我不知道我是不是寫清楚了,但還是希望大家看了之后能有一絲絲的收獲尿褪,這就是對我最大的鼓勵睦擂。

本篇博客的錄屏代碼參考自:ScreenRecorder

本篇博客的實例代碼:ScreenRecorderShoter

最后:

如果有問題,請在評論區(qū)留言杖玲,才疏學淺顿仇,歡迎大家批評指正。

最后的最后:

感謝我可愛的女朋友摆马。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末臼闻,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子今膊,更是在濱河造成了極大的恐慌些阅,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件斑唬,死亡現(xiàn)場離奇詭異市埋,居然都是意外死亡,警方通過查閱死者的電腦和手機恕刘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門缤谎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人褐着,你說我怎么就攤上這事坷澡。” “怎么了含蓉?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵频敛,是天一觀的道長。 經常有香客問我馅扣,道長斟赚,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任差油,我火速辦了婚禮拗军,結果婚禮上任洞,老公的妹妹穿的比我還像新娘。我一直安慰自己发侵,他們只是感情好交掏,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著刃鳄,像睡著了一般盅弛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上铲汪,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天熊尉,我揣著相機與錄音,去河邊找鬼掌腰。 笑死狰住,一個胖子當著我的面吹牛,可吹牛的內容都是我干的齿梁。 我是一名探鬼主播催植,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼勺择!你這毒婦竟也來了创南?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤省核,失蹤者是張志新(化名)和其女友劉穎稿辙,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體气忠,經...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡邻储,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了旧噪。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吨娜。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖淘钟,靈堂內的尸體忽然破棺而出宦赠,到底是詐尸還是另有隱情,我是刑警寧澤米母,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布勾扭,位于F島的核電站,受9級特大地震影響铁瞒,放射性物質發(fā)生泄漏妙色。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一精拟、第九天 我趴在偏房一處隱蔽的房頂上張望燎斩。 院中可真熱鬧,春花似錦蜂绎、人聲如沸栅表。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽怪瓶。三九已至,卻和暖如春践美,著一層夾襖步出監(jiān)牢的瞬間洗贰,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工陨倡, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留敛滋,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓兴革,卻偏偏與公主長得像绎晃,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子杂曲,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內容