使用 MediaCodec 在 Android 上進行硬解碼

future_li發表於2024-04-23

要使用 MediaCodec 在 Android 上進行硬解碼,並獲取 RGBA 資料,你可以按照以下步驟進行操作:

建立 MediaExtractor 物件並設定要解碼的 MP4 檔案路徑:

MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(filePath);

根據需要選擇音訊或影片軌道:

int trackCount = extractor.getTrackCount();
int trackIndex = -1;
for (int i = 0; i < trackCount; i++) {
    MediaFormat format = extractor.getTrackFormat(i);
    String mime = format.getString(MediaFormat.KEY_MIME);
    if (mime.startsWith("video/")) {
        trackIndex = i;
        break;
    }
}
if (trackIndex >= 0) {
    extractor.selectTrack(trackIndex);
}

建立 MediaCodec 物件並配置解碼器:

MediaFormat format = extractor.getTrackFormat(trackIndex);
String mime = format.getString(MediaFormat.KEY_MIME);
MediaCodec codec = MediaCodec.createDecoderByType(mime);
codec.configure(format, null, null, 0);
codec.start();

迴圈解碼並獲取 RGBA 資料:

MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
boolean isEOS = false;
while (!isEOS) {
    int inputBufferIndex = codec.dequeueInputBuffer(timeoutUs);
    if (inputBufferIndex >= 0) {
        ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferIndex);
        int sampleSize = extractor.readSampleData(inputBuffer, 0);
        if (sampleSize < 0) {
            codec.queueInputBuffer(inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
            isEOS = true;
        } else {
            long presentationTimeUs = extractor.getSampleTime();
            codec.queueInputBuffer(inputBufferIndex, 0, sampleSize, presentationTimeUs, 0);
            extractor.advance();
        }
    }

    int outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, timeoutUs);
    if (outputBufferIndex >= 0) {
        ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferIndex);
        // 解碼後的資料位於 outputBuffer 中,根據需要進行 RGBA 資料的提取和處理
        // outputBuffer 中的資料格式可能是 YUV 或其他格式,需要根據解碼器設定的輸出格式進行相應的轉換
        codec.releaseOutputBuffer(outputBufferIndex, false);
    } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
        // 解碼器輸出格式已更改,可以透過 codec.getOutputFormat() 獲取新的格式
    }
}

在上述程式碼中,你需要根據解碼器輸出的資料格式進行相應的轉換,以獲取 RGBA 資料。具體的轉換流程和程式碼取決於解碼器輸出的資料格式和你的需求。

需要注意的是,硬解碼的支援和效能可能因裝置、Android 版本和影片編碼格式的不同而有所差異。確保你的裝置支援硬解碼,並且適當處理解碼器的輸入和輸出緩衝區。

以下是完整程式碼:

import android.media.MediaCodec;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.util.Log;

import java.nio.ByteBuffer;

public class MP4Decoder {
    private static final int TIMEOUT_US = 10000;
    private static final String TAG = "MP4Decoder";

    public interface FrameCallback {
        void onFrameDecoded(byte[] rgbaData, int width, int height);
    }

    public static void decodeMP4(String filePath, FrameCallback callback) {
        new Thread(() -> {
            MediaExtractor extractor = new MediaExtractor();
            try {
                extractor.setDataSource(filePath);

                int trackIndex = selectVideoTrack(extractor);
                if (trackIndex < 0) {
                    return;
                }

                MediaFormat format = extractor.getTrackFormat(trackIndex);
                String mime = format.getString(MediaFormat.KEY_MIME);
                MediaCodec codec = MediaCodec.createDecoderByType(mime);
                codec.configure(format, null, null, 0);
                codec.start();

                MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
                boolean isEOS = false;
                while (!isEOS) {
                    int inputBufferIndex = codec.dequeueInputBuffer(TIMEOUT_US);
                    if (inputBufferIndex >= 0) {
                        ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferIndex);
                        int sampleSize = extractor.readSampleData(inputBuffer, 0);
                        if (sampleSize < 0) {
                            codec.queueInputBuffer(inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                            isEOS = true;
                        } else {
                            long presentationTimeUs = extractor.getSampleTime();
                            codec.queueInputBuffer(inputBufferIndex, 0, sampleSize, presentationTimeUs, 0);
                            extractor.advance();
                        }
                    }

                    int outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, TIMEOUT_US);
                    if (outputBufferIndex >= 0) {
                        ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferIndex);
                        // 解碼後的資料位於 outputBuffer 中,根據需要進行 RGBA 資料的提取和處理
                        // outputBuffer 中的資料格式可能是 YUV 或其他格式,需要根據解碼器設定的輸出格式進行相應的轉換
                        byte[] rgbaData = convertToRGBA(outputBuffer, format);
                        int width = format.getInteger(MediaFormat.KEY_WIDTH);
                        int height = format.getInteger(MediaFormat.KEY_HEIGHT);
                        callback.onFrameDecoded(rgbaData, width, height);
                        codec.releaseOutputBuffer(outputBufferIndex, false);
                    } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                        // 解碼器輸出格式已更改,可以透過 codec.getOutputFormat() 獲取新的格式
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                extractor.release();
            }
        }).start();
    }

    private static int selectVideoTrack(MediaExtractor extractor) {
        int trackCount = extractor.getTrackCount();
        int trackIndex = -1;
        for (int i = 0; i < trackCount; i++) {
            MediaFormat format = extractor.getTrackFormat(i);
            String mime = format.getString(MediaFormat.KEY_MIME);
            if (mime.startsWith("video/")) {
                trackIndex = i;
                break;
            }
        }
        if (trackIndex >= 0) {
            extractor.selectTrack(trackIndex);
        }
        return trackIndex;
    }

    private static byte[] convertToRGBA(ByteBuffer buffer, MediaFormat format) {
        // 根據解碼器設定的輸出格式進行 RGBA 資料的提取和處理
        // 這裡只是一個示例,實際的轉換過程可能會更復雜,取決於輸出格式和需求
        int width = format.getInteger(MediaFormat.KEY_WIDTH);
        int height = format.getInteger(MediaFormat.KEY_HEIGHT);
        int remaining = buffer.remaining();
        byte[] rgbaData = new byte[remaining];
        buffer.rewind();
        buffer.get(rgbaData);
        Log.i(TAG, "convertToRGBA: count:" + width + "; " + height + "; " + remaining);
        // todo: 進行 YUV 到 RGBA 的轉換
        return rgbaData;
    }
}

相關文章