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

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

public class Mp4Demuxer {
    private static final String TAG = "Mp4Demuxer";
    // 优先使用应用私有目录(无需权限,Android 10+ 推荐)
    private static final String OUTPUT_DIR = AppContext.getAppContext().getExternalFilesDir(
            Environment.DIRECTORY_MOVIES) + "/MP4_Demux/";
    private static final int BUFFER_SIZE = 4 * 1024 * 1024;

    private final String inputFilePath;
    private String videoOutputPath;
    private String audioOutputPath;
    private int videoTrackIndex = -1;
    private int audioTrackIndex = -1;

    // 全局 Application 上下文(避免内存泄漏)
    public static class AppContext {
        private static android.content.Context sContext;
        public static void init(android.content.Context context) {
            sContext = context.getApplicationContext();
        }
        public static android.content.Context getAppContext() {
            return sContext;
        }
    }

    public Mp4Demuxer(String inputFilePath) {
        this.inputFilePath = inputFilePath;
        // 验证输入文件有效性(关键修复)
        if (!validateInputFile()) {
            throw new IllegalArgumentException("输入文件无效:" + inputFilePath);
        }
        createOutputDir();
    }

    /**
     * 验证输入文件是否有效(解决 80% 的异常)
     */
    private boolean validateInputFile() {
        if (inputFilePath == null || inputFilePath.isEmpty()) {
            Log.e(TAG, "文件路径为空");
            return false;
        }

        File inputFile = new File(inputFilePath);
        if (!inputFile.exists()) {
            Log.e(TAG, "文件不存在:" + inputFilePath);
            return false;
        }

        if (!inputFile.isFile()) {
            Log.e(TAG, "路径不是文件(是目录):" + inputFilePath);
            return false;
        }

        if (!inputFile.canRead()) {
            Log.e(TAG, "文件无读取权限:" + inputFilePath);
            return false;
        }

        // 可选:验证文件后缀(基础校验)
        if (!inputFilePath.toLowerCase().endsWith(".mp4")) {
            Log.w(TAG, "文件后缀不是 MP4,可能格式不支持");
        }
        return true;
    }

    // 以下方法与之前优化版一致,省略重复代码...
    public boolean startDemux() {
        MediaExtractor extractor = null;
        FileOutputStream videoOs = null;
        FileOutputStream audioOs = null;

        try {
            extractor = new MediaExtractor();
            // 关键:添加异常捕获,明确失败原因
            try {
                extractor.setDataSource(inputFilePath);
            } catch (IOException e) {
                Log.e(TAG, "设置数据源失败(格式不支持或文件损坏):" + e.getMessage(), e);
                return false;
            }

            findAndSelectTracks(extractor);
            if (videoTrackIndex == -1 && audioTrackIndex == -1) {
                Log.e(TAG, "未找到支持的音视频轨道");
                return false;
            }

            videoOs = initVideoOutputStream(extractor);
            audioOs = initAudioOutputStream();
            extractTracksSimultaneously(extractor, videoOs, audioOs);

            Log.d(TAG, "解封装完成!");
            Log.d(TAG, "视频输出:" + videoOutputPath);
            Log.d(TAG, "音频输出:" + audioOutputPath);
            return true;

        } catch (IOException e) {
            Log.e(TAG, "解封装失败:" + e.getMessage(), e);
            return false;
        } finally {
            if (extractor != null) extractor.release();
            closeStream(videoOs, "视频");
            closeStream(audioOs, "音频");
        }
    }

    // 省略 findAndSelectTracks、initVideoOutputStream 等重复方法...
    private void findAndSelectTracks(MediaExtractor extractor) {
        for (int i = 0; i < extractor.getTrackCount(); i++) {
            MediaFormat format = extractor.getTrackFormat(i);
            String mime = format.getString(MediaFormat.KEY_MIME);
            if (mime == null) continue;

            if (mime.startsWith("video/") && videoTrackIndex == -1) {
                videoTrackIndex = i;
                extractor.selectTrack(videoTrackIndex);
                Log.d(TAG, "视频轨道:" + mime + ",分辨率:" +
                        format.getInteger(MediaFormat.KEY_WIDTH) + "x" +
                        format.getInteger(MediaFormat.KEY_HEIGHT));
            } else if (mime.startsWith("audio/") && audioTrackIndex == -1) {
                audioTrackIndex = i;
                extractor.selectTrack(audioTrackIndex);
                Log.d(TAG, "音频轨道:" + mime + ",采样率:" +
                        format.getInteger(MediaFormat.KEY_SAMPLE_RATE) + "Hz");
            }

            if (videoTrackIndex != -1 && audioTrackIndex != -1) break;
        }
    }

    private FileOutputStream initVideoOutputStream(MediaExtractor extractor) throws IOException {
        if (videoTrackIndex == -1) return null;
        MediaFormat format = extractor.getTrackFormat(videoTrackIndex);
        String ext = format.getString(MediaFormat.KEY_MIME).equals("video/avc") ? "h264" : "h265";
        videoOutputPath = OUTPUT_DIR + "output_video." + ext;
        return new FileOutputStream(videoOutputPath);
    }

    private FileOutputStream initAudioOutputStream() throws IOException {
        if (audioTrackIndex == -1) return null;
        audioOutputPath = OUTPUT_DIR + "output_audio.aac";
        return new FileOutputStream(audioOutputPath);
    }

    private void extractTracksSimultaneously(MediaExtractor extractor, FileOutputStream videoOs, FileOutputStream audioOs) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
        int videoFrameCount = 0, audioFrameCount = 0;

        while (true) {
            int sampleSize = extractor.readSampleData(buffer, 0);
            if (sampleSize < 0) break;

            int currentTrack = extractor.getSampleTrackIndex();
            bufferInfo.offset = 0;
            bufferInfo.size = sampleSize;
            bufferInfo.presentationTimeUs = extractor.getSampleTime();
            bufferInfo.flags = extractor.getSampleFlags();

            if (currentTrack == videoTrackIndex && videoOs != null) {
                writeDataToStream(buffer, bufferInfo, videoOs);
                videoFrameCount++;
            } else if (currentTrack == audioTrackIndex && audioOs != null) {
                writeDataToStream(buffer, bufferInfo, audioOs);
                audioFrameCount++;
            }

            extractor.advance();
            buffer.clear();
        }
        Log.d(TAG, "提取完成 - 视频:" + videoFrameCount + "帧,音频:" + audioFrameCount + "帧");
    }

    private void writeDataToStream(ByteBuffer buffer, MediaCodec.BufferInfo info, FileOutputStream os) throws IOException {
        buffer.flip();
        byte[] data = new byte[info.size];
        buffer.get(data);
        os.write(data);
        os.flush();
    }

    private void createOutputDir() {
        File dir = new File(OUTPUT_DIR);
        if (!dir.exists() && !dir.mkdirs()) {
            Log.e(TAG, "输出目录创建失败:" + OUTPUT_DIR);
        }
    }

    private void closeStream(FileOutputStream os, String type) {
        if (os != null) {
            try {
                os.close();
            } catch (IOException e) {
                Log.e(TAG, type + "流关闭失败:" + e.getMessage(), e);
            }
        }
    }

    public String getVideoOutputPath() { return videoOutputPath; }
    public String getAudioOutputPath() { return audioOutputPath; }
}

使用

import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

public class DemuxTestActivity extends AppCompatActivity {
    private static final int REQUEST_PERMISSION = 100;
    private String inputFilePath;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_demux_test);

        // 初始化全局上下文
        Mp4Demuxer.AppContext.init(getApplicationContext());

        // 选择数据源(三选一,根据你的文件位置)
        // 1. 应用私有目录(推荐,无需权限)
        inputFilePath = getExternalFilesDir(Environment.DIRECTORY_MOVIES) + "/test.mp4";

        // 2. 从 Asset 复制文件(如果文件在 asset 目录)
        // try {
        //     inputFilePath = copyAssetToLocal("test.mp4");
        // } catch (IOException e) {
        //     Toast.makeText(this, "Asset 文件复制失败", Toast.LENGTH_SHORT).show();
        //     e.printStackTrace();
        // }

        // 3. 公共下载目录(需权限)
        // inputFilePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/test.mp4";

        // 申请权限
        requestPermissions();

        Button startBtn = findViewById(R.id.btn_start);
        startBtn.setOnClickListener(v -> startDemux());
    }

    /**
     * 动态申请权限
     */
    private void requestPermissions() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            // Android 13+ 申请媒体权限
            if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_VIDEO)
                    != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(this,
                        new String[]{Manifest.permission.READ_MEDIA_VIDEO, Manifest.permission.READ_MEDIA_AUDIO},
                        REQUEST_PERMISSION);
            }
        } else {
            // Android 6.0-12 申请存储权限
            if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
                    != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(this,
                        new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE},
                        REQUEST_PERMISSION);
            }
        }
    }

    /**
     * 开始解封装(子线程执行)
     */
    private void startDemux() {
        new Thread(() -> {
            try {
                Mp4Demuxer demuxer = new Mp4Demuxer(inputFilePath);
                boolean success = demuxer.startDemux();

                runOnUiThread(() -> {
                    if (success) {
                        String msg = "提取成功!\n视频:" + demuxer.getVideoOutputPath() + "\n音频:" + demuxer.getAudioOutputPath();
                        Toast.makeText(DemuxTestActivity.this, msg, Toast.LENGTH_LONG).show();
                    } else {
                        Toast.makeText(DemuxTestActivity.this, "提取失败", Toast.LENGTH_SHORT).show();
                    }
                });
            } catch (IllegalArgumentException e) {
                runOnUiThread(() -> Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show());
            }
        }).start();
    }

    /**
     * 复制 Asset 文件到本地(如果需要读取 asset 中的 MP4)
     */
    private String copyAssetToLocal(String assetFileName) throws IOException {
        File localFile = new File(getExternalFilesDir(Environment.DIRECTORY_MOVIES), assetFileName);
        if (localFile.exists()) return localFile.getAbsolutePath();

        InputStream inputStream = getAssets().open(assetFileName);
        FileOutputStream outputStream = new FileOutputStream(localFile);
        byte[] buffer = new byte[4096];
        int len;
        while ((len = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, len);
        }
        inputStream.close();
        outputStream.close();
        return localFile.getAbsolutePath();
    }

    /**
     * 权限申请回调
     */
    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_PERMISSION) {
            boolean granted = true;
            for (int result : grantResults) {
                if (result != PackageManager.PERMISSION_GRANTED) {
                    granted = false;
                    break;
                }
            }
            if (!granted) {
                Toast.makeText(this, "需要存储权限才能执行解封装", Toast.LENGTH_SHORT).show();
            }
        }
    }
}
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐