MediaExtractor、MediaMuxer 分离和合成 mp4详解手机开发

MediaExtractor

视音频分离器,将一些格式的视频分离出视频轨道和音频轨道。
主要流程,也是主要的 API:

  • setDataSource(String path):设置数据源
  • getTrackCount():获取通道数
  • getTrackFormat(int index):获取通道格式
  • readSampleData(ByteBuffer byteBuf, int offset):读取指定通道中的数据
  • getSampleTime():获取当前时间戳
  • advance():下一帧
  • release():释放资源

MediaMuxer

视音频合成器,将视频和音频合成相应的格式

  • MediaMuxer(String path, int format):初始化输出文件名称和输出文件的格式:mpeg4
  • addTrack(MediaFormat format):添加轨道,返回 track Index,会在 writeSampleData 中使用
  • start():开始合成文件
  • writeSampleData(int, ByteBuffer, MediaCodec.BufferInfo):写数据
  • stop():停止合成文件
  • release():释放资源
大体流程:

以下是分离 mp4 音频和视频的源码及注释:

    private String seperateMedia(String fileName, boolean isAudio) { 
        String type = isAudio ? "audio/" : "video/"; 
        MediaExtractor mMediaExtractor = new MediaExtractor();; 
        MediaMuxer mMediaMuxer = null; 
        try { 
            mMediaMuxer = new MediaMuxer(getSDPath() + "/" + fileName, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); 
            // 获取assert中的资源文件 
            AssetFileDescriptor fileDescriptor = getAssetFileSource(); 
            // 设置资源文件 
            mMediaExtractor.setDataSource(fileDescriptor.getFileDescriptor(), fileDescriptor.getStartOffset(), fileDescriptor.getLength()); 
            int mMediaIndex = 0; 
            for (int i = 0; i < mMediaExtractor.getTrackCount(); i++) { 
                //获取码流的详细格式/配置信息 
                MediaFormat format = mMediaExtractor.getTrackFormat(i); 
                String mine = format.getString(MediaFormat.KEY_MIME); 
                // 查找音频:"audio/" 或者视频:"video/"的轨道 
                if (mine.startsWith(type)) { 
                    mMediaIndex = i; 
                    break; 
                } 
            } 
            // 选择感兴趣的轨道 
            mMediaExtractor.selectTrack(mMediaIndex); 
            // 获取通道格式,可以自己新建,但是有坑 
            MediaFormat mediaFormat = mMediaExtractor.getTrackFormat(mMediaIndex); 
            int muxerTrackIndex = mMediaMuxer.addTrack(mediaFormat); 
            // 当采集视频的使用,需要获取帧率,音频轨道没有这个参数 
            int framerate = isAudio ? 0 : mediaFormat.getInteger(MediaFormat.KEY_FRAME_RATE); 
            mMediaMuxer.start(); 
 
            ByteBuffer byteBuffer = ByteBuffer.allocate(500 * 1024); 
            int readSize = 0; 
            // writeSampleData需要BufferInfo参数 
            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); 
            while((readSize = mMediaExtractor.readSampleData(byteBuffer, 0)) > 0) { 
                bufferInfo.size = readSize; 
                bufferInfo.flags = mMediaExtractor.getSampleFlags(); //设置为关键帧等 
                bufferInfo.offset = 0; 
                if (!isAudio) { // 时间戳,音频和视频的处理方式不一样 
                    bufferInfo.presentationTimeUs += 1000 * 1000 / framerate; 
                } else { 
                    bufferInfo.presentationTimeUs = mMediaExtractor.getSampleTime(); 
                } 
                mMediaMuxer.writeSampleData(muxerTrackIndex, byteBuffer, bufferInfo); 
                Log.d("getSampleTime", "seperateMedia: " + mMediaExtractor.getSampleTime() ); 
                mMediaExtractor.advance(); //下一帧 
            } 
            return "success"; 
        } catch (IOException e) { 
            e.printStackTrace(); 
        } finally { 
            // 释放资源 
            if(mMediaExtractor != null ) { 
                mMediaExtractor.release(); 
                mMediaExtractor = null; 
            } 
            if(mMediaMuxer != null) { 
                mMediaMuxer.stop(); 
                mMediaMuxer.release(); 
                mMediaMuxer = null; 
            } 
        } 
        return null; 
    }

坑点:

bufferInfo 信息中需要写入正确的 presentationTimeUs,而看网上的参考基本都是 MediaExtractor.getSampleTime(),但在实际使用中会异常报错,也有使用通过计算前两个关键帧的差值,然后逐步增加,因为而且 presentationTimeUs 是需要递增的,但是实际上最后分离出来的视频模糊卡顿的现象。

Android 中如何提取和生成 mp4 文件 中看到根据帧率来计算 presentationTimeUs,效果比较正常,结合音频使用 mMediaExtractor.getSampleTime();

视频: int frameRate=mediaFormat=getInteger(MediaFormat.KEY_FRAME_RATE); //1s 的帧数
bufferInfo.presentationTimeUs = 1000 * 1000/frameRate; // 对于帧率为 x f/s 的视频而言,时间戳的间隔就是 1000/x ms
音频: bufferInfo.presentationTimeUs = mMediaExtractor.getSampleTime();

最近发现两个问题:
1. 为什么 getSampleTime() 不是逐步递增的?时间戳难道不是应该持续上升的吗?
2. 在某些机型上出现没有 MediaFormat.KEY_FRAME_RATE,该怎么计算 presentationTimeUs?

源码

public class MediaExtractorActivity extends AppCompatActivity implements View.OnClickListener{
 
private static String TAG = MediaExtractorActivity.class.getSimpleName(); 
public static String[] STORAGE = {Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}; 
@Override 
protected void onCreate(@Nullable Bundle savedInstanceState) { 
super.onCreate(savedInstanceState); 
setContentView(R.layout.activity_mediaextractor); 
findViewById(R.id.seperate_audio_btn).setOnClickListener(this); 
findViewById(R.id.seperate_media_btn).setOnClickListener(this); 
findViewById(R.id.muxer_btn).setOnClickListener(this); 
} 
@Override 
public void onClick(View view) { 
if(ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { 
ActivityCompat.requestPermissions(this, STORAGE, 2); 
return; 
} 
switch (view.getId()) { 
case R.id.seperate_audio_btn: 
new MediaAsyncTask().execute(1); 
break; 
case R.id.seperate_media_btn: 
new MediaAsyncTask().execute(2); 
break; 
case R.id.muxer_btn: 
new MediaAsyncTask().execute(3); 
break; 
default: 
break; 
} 
findViewById(R.id.seperate_audio_btn).setEnabled(false); 
findViewById(R.id.seperate_media_btn).setEnabled(false); 
findViewById(R.id.muxer_btn).setEnabled(false); 
} 
private class MediaAsyncTask extends AsyncTask<Integer, Integer, String> {
 
@Override 
protected void onPreExecute() { 
super.onPreExecute(); 
Toast.makeText(MediaExtractorActivity.this, "开始转化", Toast.LENGTH_SHORT).show(); 
} 
@Override 
protected String doInBackground(Integer... param) { 
if (param.length < 1){ 
return null; 
} 
switch (param[0]) { 
case 1: 
return seperateMedia("audio.mp4", true); 
case 2: 
return seperateMedia("video.mp4", false); 
case 3: 
return muxerMediaAndAudio("video.mp4","audio.mp4", "result.mp4"); 
default: 
return null; 
} 
} 
@Override 
protected void onPostExecute(String s) { 
findViewById(R.id.seperate_audio_btn).setEnabled(true); 
findViewById(R.id.seperate_media_btn).setEnabled(true); 
findViewById(R.id.muxer_btn).setEnabled(true); 
if (s != null) { 
Toast.makeText(MediaExtractorActivity.this, "转化完成 " + s, Toast.LENGTH_LONG).show(); 
} else { 
Toast.makeText(MediaExtractorActivity.this, "转化失败", Toast.LENGTH_SHORT).show(); 
} 
} 
} 
private String muxerMediaAndAudio(String mediaFileName, String audioFileName, String resultName) { 
File mediaFile = new File(getSDPath(), mediaFileName); 
File audioFile = new File(getSDPath(), audioFileName); 
if (!mediaFile.exists() || !audioFile.exists()) { 
return "音视频文件不存在"; 
} 
MediaMuxer mMediaMuxer = null; 
MediaExtractor mMediaExtractor = new MediaExtractor(); 
MediaExtractor mAudioExtractor = new MediaExtractor(); 
try { 
mMediaMuxer = new MediaMuxer(getSDPath() + "/" + resultName, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); 
mMediaExtractor.setDataSource(mediaFile.getAbsolutePath()); 
mAudioExtractor.setDataSource(audioFile.getAbsolutePath()); 
int mMediaTrack = 0; 
int mAudioTrack = 0; 
for (int i = 0; i < mMediaExtractor.getTrackCount(); i++) { 
String mine = mMediaExtractor.getTrackFormat(i).getString(MediaFormat.KEY_MIME); 
if (mine.startsWith("video/")) { 
mMediaTrack = i; 
break; 
} 
} 
for (int i = 0; i < mAudioExtractor.getTrackCount(); i++) { 
String mine = mAudioExtractor.getTrackFormat(i).getString(MediaFormat.KEY_MIME); 
if (mine.startsWith("audio/")) { 
mAudioTrack = i; 
break; 
} 
} 
mMediaExtractor.selectTrack(mMediaTrack); 
mAudioExtractor.selectTrack(mAudioTrack); 
MediaFormat mediaFormat = mMediaExtractor.getTrackFormat(mMediaTrack); 
MediaFormat audioFormat = mAudioExtractor.getTrackFormat(mAudioTrack); 
MediaCodec.BufferInfo mediaBufferInfo = new MediaCodec.BufferInfo(); 
MediaCodec.BufferInfo audioBufferInfo = new MediaCodec.BufferInfo(); 
int mediaMuxerTrack = mMediaMuxer.addTrack(mediaFormat); 
int audioMuxerTrack = mMediaMuxer.addTrack(audioFormat); 
mMediaMuxer.start(); 
ByteBuffer byteBuffer = ByteBuffer.allocate(500 * 1024); 
int readSize = 0; 
int mMediaFramerate = mediaFormat.getInteger(MediaFormat.KEY_FRAME_RATE); 
while ((readSize = mMediaExtractor.readSampleData(byteBuffer, 0)) > 0) { 
mediaBufferInfo.presentationTimeUs += 1000 * 1000 / mMediaFramerate; 
mediaBufferInfo.offset = 0; 
mediaBufferInfo.flags = mMediaExtractor.getSampleFlags(); 
mediaBufferInfo.size = readSize; 
mMediaMuxer.writeSampleData(mediaMuxerTrack, byteBuffer, mediaBufferInfo); 
mMediaExtractor.advance(); 
} 
while ((readSize = mAudioExtractor.readSampleData(byteBuffer, 0)) > 0) { 
audioBufferInfo.presentationTimeUs = mAudioExtractor.getSampleTime(); 
audioBufferInfo.offset = 0; 
audioBufferInfo.flags = mAudioExtractor.getSampleFlags(); 
audioBufferInfo.size = readSize; 
mMediaMuxer.writeSampleData(audioMuxerTrack, byteBuffer, audioBufferInfo); 
mAudioExtractor.advance(); 
} 
mMediaMuxer.stop(); 
return getSDPath() + resultName; 
} catch (IOException e) { 
e.printStackTrace(); 
} finally { 
if(mMediaExtractor != null ) { 
mMediaExtractor.release(); 
mMediaExtractor = null; 
} 
if(mAudioExtractor != null ) { 
mAudioExtractor.release(); 
mAudioExtractor = null; 
} 
if(mMediaMuxer != null) { 
mMediaMuxer.release(); 
mMediaMuxer = null; 
} 
} 
return null; 
} 
private String seperateMedia(String fileName, boolean isAudio) { 
String type = isAudio ? "audio/" : "video/"; 
MediaExtractor mMediaExtractor = new MediaExtractor();; 
MediaMuxer mMediaMuxer = null; 
try { 
mMediaMuxer = new MediaMuxer(getSDPath() + "/" + fileName, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); 
// 获取assert中的资源文件 
AssetFileDescriptor fileDescriptor = getAssetFileSource(); 
// 设置资源文件 
mMediaExtractor.setDataSource(fileDescriptor.getFileDescriptor(), fileDescriptor.getStartOffset(), fileDescriptor.getLength()); 
int mMediaIndex = 0; 
for (int i = 0; i < mMediaExtractor.getTrackCount(); i++) { 
//获取码流的详细格式/配置信息 
MediaFormat format = mMediaExtractor.getTrackFormat(i); 
String mine = format.getString(MediaFormat.KEY_MIME); 
// 查找音频:"audio/" 或者视频:"video/"的轨道 
if (mine.startsWith(type)) { 
mMediaIndex = i; 
break; 
} 
} 
// 选择感兴趣的轨道 
mMediaExtractor.selectTrack(mMediaIndex); 
// 获取通道格式,可以自己新建,但是有坑 
MediaFormat mediaFormat = mMediaExtractor.getTrackFormat(mMediaIndex); 
int muxerTrackIndex = mMediaMuxer.addTrack(mediaFormat); 
// 当采集视频的使用,需要获取帧率,音频轨道没有这个参数 
int framerate = 0; 
if (mediaFormat.containsKey(MediaFormat.KEY_FRAME_RATE)) { 
framerate = mediaFormat.getInteger(MediaFormat.KEY_FRAME_RATE); 
} 
mMediaMuxer.start(); 
ByteBuffer byteBuffer = ByteBuffer.allocate(500 * 1024); 
int readSize = 0; 
// writeSampleData需要BufferInfo参数 
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); 
while((readSize = mMediaExtractor.readSampleData(byteBuffer, 0)) > 0) { 
bufferInfo.size = readSize; 
bufferInfo.flags = mMediaExtractor.getSampleFlags(); //设置为关键帧等 
bufferInfo.offset = 0; 
if (framerate != 0) { // 时间戳,音频和视频的处理方式不一样 
bufferInfo.presentationTimeUs += 1000 * 1000 / framerate; 
} else { 
bufferInfo.presentationTimeUs = mMediaExtractor.getSampleTime(); 
} 
mMediaMuxer.writeSampleData(muxerTrackIndex, byteBuffer, bufferInfo); 
Log.d("getSampleTime", "seperateMedia: " + mMediaExtractor.getSampleTime() ); 
mMediaExtractor.advance(); //下一帧 
} 
mMediaMuxer.stop(); 
return "success"; 
} catch (IOException e) { 
e.printStackTrace(); 
} finally { 
// 释放资源 
if(mMediaExtractor != null ) { 
mMediaExtractor.release(); 
mMediaExtractor = null; 
} 
if(mMediaMuxer != null) { 
mMediaMuxer.release(); 
mMediaMuxer = null; 
} 
} 
return null; 
} 
private AssetFileDescriptor getAssetFileSource() throws IOException { 
AssetManager manager = getAssets(); 
return manager.openFd("screen_recorder.mp4"); 
} 
public static String getSDPath() { 
// 判断是否挂载 
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { 
return Environment.getExternalStorageDirectory().getAbsolutePath(); 
} 
return Environment.getRootDirectory().getAbsolutePath(); 
} 
} 

参考:
云图网

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/6279.html

(0)
上一篇 2021年7月17日
下一篇 2021年7月17日

相关推荐

发表回复

登录后才能评论