1. 程式人生 > >關於手機錄屏功能小記(2)

關於手機錄屏功能小記(2)

本小記繼續上一篇的程式碼

主要實現功能為:對編碼器編譯壓縮後輸出的視訊流用MediaMuxer封裝到MP4容器。

//採用執行緒進行錄屏的耗時操作
public class VideoThread extends Thread {

    private Context mContext;
    private static final String MIME_TYPE = "video/avc";
    private VirtualDisplay virtualDisplay;
    private MediaProjection projection;
    private AtomicBoolean mQuit = new AtomicBoolean(false);
    private int videoWidth;
    private int videoHeight;

    private Handler handler = null;

	//建構函式,傳入上下文和螢幕大小
    public VideoThread(Context context, int width, int height){
        mContext = context;
        videoWidth = width;
        videoHeight = height;
    }

    public final void quit(){
        mQuit.set(true);
    }

    @Override
    public void run(){
        Surface surface = null;
        MediaMuxer mediaMuxer = null;  //MediaMuxer的使用要按照Constructor -> addTrack -> start -> writeSampleData -> stop 的順序
        MediaCodec mediaCodec = null;

		//獲取檔案儲存路徑
        long timeMillis = System.currentTimeMillis();
        Date date = new Date(timeMillis);
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss");
        String path = "ScreenShot" + simpleDateFormat.format(date) + ".mp4";
        File file = new File(Environment.getExternalStorageDirectory(), path);
        File parentFile = file.getParentFile();
        if (parentFile == null || !parentFile.exists()) {
            parentFile.mkdir();
        }

		//初始化編碼器、建立MediaFormat設定Media格式
        MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, videoWidth, videoHeight);
        format.setInteger(MediaFormat.KEY_FRAME_RATE, 32);           //幀數
        format.setInteger(MediaFormat.KEY_BIT_RATE, 2500000);        //碼流
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);      //關鍵幀
        format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);  //顏色格式
        format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 0);         //音訊的頻道數(單聲道或者雙聲道)
        format.setInteger(MediaFormat.KEY_CAPTURE_RATE, 32);         //捕獲率 (當CAPTURE_RATE和FRAME_RATE不一樣時,視訊播放會加快或減慢)
        format.setInteger(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, 1000000 / 15);

        try {
            try {//建立編碼器(MediaCodec用於將音視訊進行壓縮編碼,可以對Surface內容進行編碼)
                mediaCodec = MediaCodec.createEncoderByType(MIME_TYPE);
            } catch (Exception e) {
                e.printStackTrace();
            }
			//配置MedidaCodec解碼器、獲取surface作為輸入(必須在MediaCodec解碼器configure之後,start之前)
            mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            surface = mediaCodec.createInputSurface();//請求一個surface用於編碼器的輸入,而不是常用的inputbuffer輸入
            mediaCodec.start();

            try {//MediaMuxer來封裝編碼後的視訊流和音訊流到mp4容器、通過Muxer指定視訊檔案輸出路徑和檔案格式
                mediaMuxer = new MediaMuxer(file.getAbsolutePath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
            } catch (Exception e) {
                e.printStackTrace();
                mediaMuxer = null;
            }

            if (mediaMuxer == null) {
                return;
            }
			
			/*建立虛擬螢幕以捕獲螢幕內容
			*
			*  MediaProjectionManager manager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
			*  startActivityForResult(manager.createScreenCaptureIntent(), 1); //返回一個必須傳遞給startActivityForResult()的意圖,以便開始螢幕捕獲。(該活動將提示使用者是否允許螢幕捕獲)
			*
			*  @Override
            *  public void onActivityResult(int requestCode, int resultCode, Intent data){
            *      if (requestCode == 1) {
            *          if (resultCode != Activity.RESULT_OK) {
            *              return;
            *          }
            *     mResultCode = resultCode;
            *     mResultData = data;
            *     }
            *  }
			*
			*  public static MediaProjection getProjection(){
            *      if (projection == null) {
            *          projection = manager.getMediaProjection(mResultCode, mResultData); //成功獲取螢幕捕獲請求後,檢索MediaProjection。
            *      }
            *      return projection;
            *  }
			*
			*/
            projection = MainActivity.getProjection();
            virtualDisplay = projection.createVirtualDisplay("display", videoWidth, videoHeight, 1, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, surface, null, null);

            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); //獲取每一個輸出資料buffer的元資料資訊,例如偏差,在相關解碼器中有效的資料大小
            int videoTrackIndex = -1;

            while (!mQuit.get()) {

                int index = mediaCodec.dequeueOutputBuffer(bufferInfo, 10000); //嘗試獲取輸出資料的資訊,關於bytebuffer的資訊將封裝在bufferinfo裡面,返回該bytebuffer在佇列中的位置
                //MediaCodec在一開始呼叫dequeueOutputBuffer()時會返回一次INFO_OUTPUT_FORMAT_CHANGED訊息
				switch (index) {
                    case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
                        MediaFormat newFormat = mediaCodec.getOutputFormat(); //在dequeueOutputBuffer 返回 INFO_OUTPUT_FORMAT_CHANGED資訊後呼叫,可以檢視當前媒體格式資訊
                        videoTrackIndex = mediaMuxer.addTrack(newFormat);     //添加當前媒體格式的索引
                        mediaMuxer.start();
                        break;
                    case MediaCodec.INFO_TRY_AGAIN_LATER:   //如果等待timeoutUs時間還沒響應則跳過,返回TRY_AGAIN_LATER
                        try {
                            Thread.sleep(10);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        break;
                    default:
                        ByteBuffer byteBuffer = mediaCodec.getOutputBuffer(index); //根據Index獲取編碼成功的bytebuffer

                        if (byteBuffer != null) {
                            byteBuffer.position(bufferInfo.offset);                //設定buffer位置,下一個要被讀或寫的元素的索引,每次讀寫緩衝區資料時都會改變改值,為下次讀寫作準備
                            byteBuffer.limit(bufferInfo.offset + bufferInfo.size); //limit表示緩衝區的當前終點,不能對緩衝區超過極限的位置進行讀寫操作。且極限是可以修改的
                            mediaMuxer.writeSampleData(videoTrackIndex, byteBuffer, bufferInfo);  //將當前解碼器的buffer資料和buffer資料對應的bufferInfo寫入Muxer
                        }
                        mediaCodec.releaseOutputBuffer(index, false);  //釋放剛剛從Codec取出資料的bytebuffer,供Codec繼續放資料。
                        break;
                }
            }
        }
        finally {
			//錄製完成後,釋放各項資源
            try {
                if (mediaMuxer != null) {
                    mediaMuxer.stop();
                    mediaMuxer.release();
                }
            }catch (Exception e){
                e.printStackTrace();
            }

            try {
                if (surface != null) {
                    surface.release();
                }
            }catch (Exception e){
                e.printStackTrace();
            }

            try {
                if (mediaCodec != null) {
                    mediaCodec.release();
                }
            }catch (Exception e){
                e.printStackTrace();
            }

            File dstFile = new File(file.getAbsolutePath());
            dstFile.setReadable(true, false); //第一個true表示是否可讀,第二個引數(true : 對所有人有效 false:對檔案擁有者有效)
        }

        if (file.exists()) {
			/*用於執行緒中的訊息處理,因為非主執行緒沒有預設建立Looper物件,需要呼叫該方法啟動Looper
			* 通過Handler物件來與Looper進行互動的。Handler可看做是Looper的一個介面,用來向指定的Looper傳送訊息及定義處理方法。 
			* Looper.loop(); 讓Looper開始工作,從訊息佇列裡取訊息,處理訊息。
			* 注意:寫在Looper.loop()之後的程式碼不會被執行,這個函式內部應該是一個迴圈,當呼叫mHandler.getLooper().quit()後,loop才會中止,其後的程式碼才能得以執行。
			*/
            Looper.prepare();                
            Message message = new Message();
            android.os.Handler handler = new android.os.Handler(){
                @Override
                public void handleMessage(Message msg){
                    super.handleMessage(msg);
                    switch (msg.what) {
                        case 1 :
                            Toast.makeText(mContext, "File is exists", Toast.LENGTH_SHORT).show();break;
                    }
                }
            };
            message.what = 1;
            handler.sendMessage(message);
            Looper.loop();
        }
    }
}