MediaExtractor 解封装mp4
·
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();
}
}
}
}
更多推荐


所有评论(0)