android用MediaCodeC將opengl繪製內容錄製為一個mp4

来源:https://www.cnblogs.com/xiaxveliang/archive/2020/03/02/12396015.html
-Advertisement-
Play Games

效果圖 實現源碼(已上傳我的GitHub): "https://github.com/xiaxveliang/GL_AUDIO_VIDEO_RECODE" 參考: "http://bigflake.com/mediacodec/EncodeAndMuxTest.java.txt" 對於以上代碼,我做 ...


效果圖

在這裡插入圖片描述
在這裡插入圖片描述

實現源碼(已上傳我的GitHub):

https://github.com/xiaxveliang/GL_AUDIO_VIDEO_RECODE

參考:

http://bigflake.com/mediacodec/EncodeAndMuxTest.java.txt

對於以上代碼,我做了一個簡單的註釋,代碼如下:


import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.media.MediaMuxer;
import android.opengl.EGL14;
import android.opengl.EGLConfig;
import android.opengl.EGLContext;
import android.opengl.EGLDisplay;
import android.opengl.EGLExt;
import android.opengl.EGLSurface;
import android.opengl.GLES20;
import android.os.Environment;
import android.test.AndroidTestCase;
import android.util.Log;
import android.view.Surface;

import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;

// wiki:
// http://bigflake.com/mediacodec/EncodeAndMuxTest.java.txt

public class EncodeAndMuxTest extends AndroidTestCase {
    private static final String TAG = EncodeAndMuxTest.class.getSimpleName();

    // 輸出文件路徑
    private static final File MY_OUTPUT_DIR = Environment.getExternalStorageDirectory();
    // H.264編碼
    private static final String MY_MIME_TYPE = "video/avc";
    // 視頻文件的寬高
    private static final int MY_VIDEO_WIDTH = 480;
    private static final int MY_VIDEO_HEIGHT = 480;
    // 視頻碼率
    private static final int MY_BIT_RATE = 800000;
    // 每秒鐘15幀
    private static final int MY_FPS = 15;


    // 總共30幀,每一秒15幀,所以30幀為2秒鐘
    private static final int NUM_FRAMES = 30;


    // RGB color values for generated frames
    private static final int TEST_R0 = 0;
    private static final int TEST_G0 = 136;
    private static final int TEST_B0 = 0;
    //
    private static final int TEST_R1 = 236;
    private static final int TEST_G1 = 50;
    private static final int TEST_B1 = 186;


    // encoder / muxer state
    private MediaCodec mEncoder;
    // H.264 轉 mp4
    private MediaMuxer mMuxer;


    private CodecInputSurface mInputSurface;

    private int mTrackIndex;
    private boolean mMuxerStarted;

    // allocate one of these up front so we don't need to do it every time
    private MediaCodec.BufferInfo mBufferInfo;


    /**
     * opengl繪製一個buffer,轉成MP4,程式入口
     */
    public void testEncodeVideoToMp4() {

        try {
            // 初始化Encoder
            initVideoEncoder();
            // 設置 EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx
            mInputSurface.makeCurrent();

            // 共30幀
            for (int i = 0; i < NUM_FRAMES; i++) {
                // mEncoder從緩衝區取數據,然後交給mMuxer編碼
                drainEncoder(false);
                // opengl繪製一幀
                generateSurfaceFrame(i);
                // 設置圖像,發送給EGL的顯示時間
                mInputSurface.setPresentationTime(computePresentationTimeNsec(i));
                // Submit it to the encoder
                mInputSurface.swapBuffers();
            }
            // send end-of-stream to encoder, and drain remaining output
            drainEncoder(true);
        } finally {
            // release encoder, muxer, and input Surface
            releaseEncoder();
        }
    }

    /**
     * 初始化視頻編碼器
     */
    private void initVideoEncoder() {
        // 創建一個buffer
        mBufferInfo = new MediaCodec.BufferInfo();

        //-----------------MediaFormat-----------------------
        // mediaCodeC採用的是H.264編碼
        MediaFormat format = MediaFormat.createVideoFormat(MY_MIME_TYPE, MY_VIDEO_WIDTH, MY_VIDEO_HEIGHT);
        // 數據來源自surface
        format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
                MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        // 視頻碼率
        format.setInteger(MediaFormat.KEY_BIT_RATE, MY_BIT_RATE);
        // fps
        format.setInteger(MediaFormat.KEY_FRAME_RATE, MY_FPS);
        //設置關鍵幀的時間
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10);

        //-----------------Encoder-----------------------
        try {
            mEncoder = MediaCodec.createEncoderByType(MY_MIME_TYPE);
            mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            // 創建一個surface
            Surface surface = mEncoder.createInputSurface();
            // 創建一個CodecInputSurface,其中包含GL相關
            mInputSurface = new CodecInputSurface(surface);
            //
            mEncoder.start();
        } catch (Exception e) {
            e.printStackTrace();
        }

        //-----------------輸出文件路徑-----------------------
        // 輸出文件路徑
        String outputPath = new File(MY_OUTPUT_DIR,
                "test." + MY_VIDEO_WIDTH + "x" + MY_VIDEO_HEIGHT + ".mp4").toString();

        //-----------------MediaMuxer-----------------------
        try {
            // 輸出為MP4
            mMuxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
        } catch (IOException ioe) {
            throw new RuntimeException("MediaMuxer creation failed", ioe);
        }

        mTrackIndex = -1;
        mMuxerStarted = false;
    }

    /**
     * Releases encoder resources.  May be called after partial / failed initialization.
     * 釋放資源
     */
    private void releaseEncoder() {
        if (mEncoder != null) {
            mEncoder.stop();
            mEncoder.release();
            mEncoder = null;
        }
        if (mInputSurface != null) {
            mInputSurface.release();
            mInputSurface = null;
        }
        if (mMuxer != null) {
            mMuxer.stop();
            mMuxer.release();
            mMuxer = null;
        }
    }


    /**
     * mEncoder從緩衝區取數據,然後交給mMuxer編碼
     *
     * @param endOfStream 是否停止錄製
     */
    private void drainEncoder(boolean endOfStream) {
        final int TIMEOUT_USEC = 10000;

        // 停止錄製
        if (endOfStream) {
            mEncoder.signalEndOfInputStream();
        }
        //拿到輸出緩衝區,用於取到編碼後的數據
        ByteBuffer[] encoderOutputBuffers = mEncoder.getOutputBuffers();
        while (true) {
            //拿到輸出緩衝區的索引
            int encoderStatus = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
            if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
                // no output available yet
                if (!endOfStream) {
                    break;      // out of while
                } else {

                }
            } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                //拿到輸出緩衝區,用於取到編碼後的數據
                encoderOutputBuffers = mEncoder.getOutputBuffers();
            } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                // should happen before receiving buffers, and should only happen once
                if (mMuxerStarted) {
                    throw new RuntimeException("format changed twice");
                }
                //
                MediaFormat newFormat = mEncoder.getOutputFormat();
                // now that we have the Magic Goodies, start the muxer
                mTrackIndex = mMuxer.addTrack(newFormat);
                //
                mMuxer.start();
                mMuxerStarted = true;
            } else if (encoderStatus < 0) {
            } else {
                //獲取解碼後的數據
                ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
                if (encodedData == null) {
                    throw new RuntimeException("encoderOutputBuffer " + encoderStatus +
                            " was null");
                }
                //
                if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
                    mBufferInfo.size = 0;
                }
                //
                if (mBufferInfo.size != 0) {
                    if (!mMuxerStarted) {
                        throw new RuntimeException("muxer hasn't started");
                    }
                    // adjust the ByteBuffer values to match BufferInfo (not needed?)
                    encodedData.position(mBufferInfo.offset);
                    encodedData.limit(mBufferInfo.offset + mBufferInfo.size);
                    // 編碼
                    mMuxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo);
                }
                //釋放資源
                mEncoder.releaseOutputBuffer(encoderStatus, false);

                if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                    if (!endOfStream) {
                        Log.w(TAG, "reached end of stream unexpectedly");
                    } else {

                    }
                    break;      // out of while
                }
            }
        }
    }

    /**
     * Generates a frame of data using GL commands.  We have an 8-frame animation
     * sequence that wraps around.  It looks like this:
     * <pre>
     *   0 1 2 3
     *   7 6 5 4
     * </pre>
     * We draw one of the eight rectangles and leave the rest set to the clear color.
     */
    private void generateSurfaceFrame(int frameIndex) {
        frameIndex %= 8;

        int startX, startY;
        if (frameIndex < 4) {
            // (0,0) is bottom-left in GL
            startX = frameIndex * (MY_VIDEO_WIDTH / 4);
            startY = MY_VIDEO_HEIGHT / 2;
        } else {
            startX = (7 - frameIndex) * (MY_VIDEO_WIDTH / 4);
            startY = 0;
        }

        GLES20.glClearColor(TEST_R0 / 255.0f, TEST_G0 / 255.0f, TEST_B0 / 255.0f, 1.0f);
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

        GLES20.glEnable(GLES20.GL_SCISSOR_TEST);
        GLES20.glScissor(startX, startY, MY_VIDEO_WIDTH / 4, MY_VIDEO_HEIGHT / 2);
        GLES20.glClearColor(TEST_R1 / 255.0f, TEST_G1 / 255.0f, TEST_B1 / 255.0f, 1.0f);
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
        GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
    }

    /**
     * Generates the presentation time for frame N, in nanoseconds.
     * 好像是生成當前幀的時間,具體怎麼計算的,不懂呀??????????????????????????
     */
    private static long computePresentationTimeNsec(int frameIndex) {
        final long ONE_BILLION = 1000000000;
        return frameIndex * ONE_BILLION / MY_FPS;
    }


    /**
     * Holds state associated with a Surface used for MediaCodec encoder input.
     * <p>
     * The constructor takes a Surface obtained from MediaCodec.createInputSurface(), and uses that
     * to create an EGL window surface.  Calls to eglSwapBuffers() cause a frame of data to be sent
     * to the video encoder.
     * <p>
     * This object owns the Surface -- releasing this will release the Surface too.
     */
    private static class CodecInputSurface {
        private static final int EGL_RECORDABLE_ANDROID = 0x3142;

        private EGLDisplay mEGLDisplay = EGL14.EGL_NO_DISPLAY;
        private EGLContext mEGLContext = EGL14.EGL_NO_CONTEXT;
        private EGLSurface mEGLSurface = EGL14.EGL_NO_SURFACE;

        private Surface mSurface;

        /**
         * Creates a CodecInputSurface from a Surface.
         */
        public CodecInputSurface(Surface surface) {
            if (surface == null) {
                throw new NullPointerException();
            }
            mSurface = surface;

            initEGL();
        }

        /**
         * 初始化EGL
         */
        private void initEGL() {

            //--------------------mEGLDisplay-----------------------
            // 獲取EGL Display
            mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
            // 錯誤檢查
            if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
                throw new RuntimeException("unable to get EGL14 display");
            }
            // 初始化
            int[] version = new int[2];
            if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) {
                throw new RuntimeException("unable to initialize EGL14");
            }

            // Configure EGL for recording and OpenGL ES 2.0.
            int[] attribList = {
                    EGL14.EGL_RED_SIZE, 8,
                    EGL14.EGL_GREEN_SIZE, 8,
                    EGL14.EGL_BLUE_SIZE, 8,
                    EGL14.EGL_ALPHA_SIZE, 8,
                    //
                    EGL14.EGL_RENDERABLE_TYPE,
                    EGL14.EGL_OPENGL_ES2_BIT,
                    // 錄製android
                    EGL_RECORDABLE_ANDROID,
                    1,
                    EGL14.EGL_NONE
            };
            EGLConfig[] configs = new EGLConfig[1];
            int[] numConfigs = new int[1];
            // eglCreateContext RGB888+recordable ES2
            EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.length, numConfigs, 0);

            // Configure context for OpenGL ES 2.0.
            int[] attrib_list = {
                    EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
                    EGL14.EGL_NONE
            };
            //--------------------mEGLContext-----------------------
            //  eglCreateContext
            mEGLContext = EGL14.eglCreateContext(mEGLDisplay, configs[0], EGL14.EGL_NO_CONTEXT,
                    attrib_list, 0);
            checkEglError("eglCreateContext");

            //--------------------mEGLSurface-----------------------
            // 創建一個WindowSurface並與surface進行綁定,這裡的surface來自mEncoder.createInputSurface();
            // Create a window surface, and attach it to the Surface we received.
            int[] surfaceAttribs = {
                    EGL14.EGL_NONE
            };
            // eglCreateWindowSurface
            mEGLSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, configs[0], mSurface,
                    surfaceAttribs, 0);
            checkEglError("eglCreateWindowSurface");
        }

        /**
         * Discards all resources held by this class, notably the EGL context.  Also releases the
         * Surface that was passed to our constructor.
         * 釋放資源
         */
        public void release() {
            if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
                EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
                        EGL14.EGL_NO_CONTEXT);
                EGL14.eglDestroySurface(mEGLDisplay, mEGLSurface);
                EGL14.eglDestroyContext(mEGLDisplay, mEGLContext);
                EGL14.eglReleaseThread();
                EGL14.eglTerminate(mEGLDisplay);
            }

            mSurface.release();

            mEGLDisplay = EGL14.EGL_NO_DISPLAY;
            mEGLContext = EGL14.EGL_NO_CONTEXT;
            mEGLSurface = EGL14.EGL_NO_SURFACE;

            mSurface = null;
        }

        /**
         * Makes our EGL context and surface current.
         * 設置 EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx
         */
        public void makeCurrent() {
            EGL14.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext);
            checkEglError("eglMakeCurrent");
        }

        /**
         * Calls eglSwapBuffers.  Use this to "publish" the current frame.
         * 用該方法,發送當前Frame
         */
        public boolean swapBuffers() {
            boolean result = EGL14.eglSwapBuffers(mEGLDisplay, mEGLSurface);
            checkEglError("eglSwapBuffers");
            return result;
        }

        /**
         * Sends the presentation time stamp to EGL.  Time is expressed in nanoseconds.
         * 設置圖像,發送給EGL的時間間隔
         */
        public void setPresentationTime(long nsecs) {
            // 設置發動給EGL的時間間隔
            EGLExt.eglPresentationTimeANDROID(mEGLDisplay, mEGLSurface, nsecs);
            checkEglError("eglPresentationTimeANDROID");
        }

        /**
         * Checks for EGL errors.  Throws an exception if one is found.
         * 檢查錯誤,代碼可以忽略
         */
        private void checkEglError(String msg) {
            int error;
            if ((error = EGL14.eglGetError()) != EGL14.EGL_SUCCESS) {
                throw new RuntimeException(msg + ": EGL error: 0x" + Integer.toHexString(error));
            }
        }
    }
}

========== THE END ==========

wx_gzh.jpg


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、摘要 1.七牛上傳文件,用hash來唯一標識七牛存儲空間中的某個文件,該hash是以ETag演算法計算出的一段哈希值; 2.演算法介紹:https://developer.qiniu.com/kodo/manual/1231/appendix; 3.七牛的提供的實現語言中(https://githu ...
  • Android JsBridge源碼學習 眾所周知Android 4.2以下的WebView存在addJavascriptInterface漏洞的問題,不太瞭解的同學可參考 "Android4.2下 WebView的addJavascriptInterface漏洞解決方案" "@Javascript ...
  • RxJava2 使用 及 源碼閱讀 RxJava是什麼?根據RxJava在GitHub上給出的描述: RxJava – Reactive Extensions for the JVM – a library for composing asynchronous and event based pro ...
  • 今天在三星S8上遇見一個奇葩問題 一、出現場景 + 三星手機S8 android 8.0 + targetSdkVersion 27 + 透明Activity 二、解決方案 manifest中移除 三、原因(源碼中尋找) 查看Android 8.0源碼 3.1、ActivityRecord setR ...
  • HashMap源碼來自:android 25/java/util/HashMap 一、構造方法 下麵通過跟中源碼查看: table數組初始化 介紹put(K key, V value)方法前,先簡單介紹table數組初始化 ps: 這裡預設初始化了一個數組容量為16的table數組,其中關於roun ...
  • 我們在用MAT(Memory Analyzer Tool)分析Android記憶體時,會發現大量的bitmap對象占了記憶體使用。但是很難定位究竟是哪張圖片占用了記憶體,這裡介紹一種查看bitmap的方法。 MAT、GIMP下載 MAT http://www.eclipse.org/mat/downloa ...
  • SparseArray源碼來自:android 25/java/util/SparseArray ArrayMap源碼來自:25.3.1/support compat 25.3.1/android/android.support.v4.util.ArrayMap 一、SparseArray實現源碼學 ...
  • 英文原文地址 "Memory optimization for feeds on Android" 讀後感 在Java中HashSet只能存放繼承自Objcet的對象,這中情況下“基本數據類型”轉化為繼承自Object的( 、`Long`等)會產生很多中間Object對象,占用過多的記憶體,從而引發垃 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...