文件转码:demux -> decode -> encode -> mux(解封装->解码->编码->封装)
对了,这里我并没有修改格式,只是走一个流程,中间的格式修改由于涉及很多东西并没有写出来,因此算的上是阉割版的,辅助学习的。
分成两步骤,两渠道。
两步骤:
解封装->解码 inputThread
编码->封装 outputThread
两渠道:自然就是视频和音频了。
设计模式用的建造者,不知道纯不纯。
因此我将转码视为一个主体,视音频转码的线程类作为子对象,因为我在不断的修改转码优化这个转码,后面陆续做了视频裁剪和暂停转码,所以这些多余的就全注释掉了。
package com.example.myapplication3.TranscodeBuildUtils; import android.media.MediaCodec; import android.media.MediaExtractor; import android.media.MediaMuxer; /** * Created by weizheng.huang on 2019-10-30. */ public class Transcode { private InputThread audioInputThread; private OutputThread audioOutputThread; private InputThread videoInputThread; private OutputThread videoOutputThread; void setAudioInputThread(MediaExtractor extractor,MediaCodec decodec,MediaCodec encodec,int formatIndex , double totalMS) { this.audioInputThread = new InputThread(extractor,decodec,encodec,formatIndex,totalMS); } void setAudioOutputThread(MediaMuxer muxer,MediaCodec encodec , String MIME ,double totalMS) { this.audioOutputThread = new OutputThread(muxer,encodec,MIME,totalMS); } void setVideoInputThread(MediaExtractor extractor,MediaCodec decodec , MediaCodec encodec,int formatIndex,double totalMS) { this.videoInputThread = new InputThread(extractor,decodec,encodec,formatIndex,totalMS); } void setVideoOutputThread(MediaMuxer muxer, MediaCodec encodec , String MIME , double totalMS) { this.videoOutputThread = new OutputThread(muxer,encodec,MIME,totalMS); } public void setTimeUS(long TIME_US){ audioInputThread.setTIME_US(TIME_US); audioOutputThread.setTIME_US(TIME_US); videoInputThread.setTIME_US(TIME_US); videoOutputThread.setTIME_US(TIME_US); } // public void setPauseTranscode(boolean TRUEORFALSE){ // audioOutputThread.setPauseOutput(TRUEORFALSE); // audioInputThread.setPauseInput(TRUEORFALSE); // videoInputThread.setPauseInput(TRUEORFALSE); // videoOutputThread.setPauseOutput(TRUEORFALSE); // } // // public void setTailTime(long startTime ,long endTime){ // videoInputThread.setTailTime(startTime,endTime); // audioInputThread.setTailTime(startTime,endTime); // } public void start(){ videoOutputThread.start(); audioOutputThread.start(); videoInputThread.start(); audioInputThread.start(); } }接下来就是对两步骤的讲解:
inputThread: 解封装->解码
核心方法是inputLoop()
package com.example.myapplication3.TranscodeBuildUtils; import android.media.MediaCodec; import android.media.MediaExtractor; import android.util.Log; import androidx.annotation.Nullable; import java.nio.ByteBuffer; /** * Created by weizheng.huang on 2019-10-30. */ class InputThread extends Thread{ // private boolean isNeedTailed = false; // private boolean pauseInput = false; private int formatIndex; // private double totalMS; // private long startTime = 0; // private long endTime = 0; private long TIME_US = 50000l; private MediaExtractor extractor; private MediaCodec decodec; private MediaCodec encodec; /** * * @param extractor 这里都懂吧,就是传入一个解封装器 * @param decodec 传入解码器 * @param encodec 传入编码器 * @param formatIndex 传入解码器获取的渠道 * @param totalMS 传入播放的总时间 用于视频裁剪可忽略 */ public InputThread(@Nullable MediaExtractor extractor , @Nullable MediaCodec decodec , @Nullable MediaCodec encodec , int formatIndex ,double totalMS){ this.extractor = extractor; this.decodec = decodec; this.encodec = encodec; this.formatIndex = formatIndex; // this.totalMS = totalMS; } // public void setPauseInput(boolean pauseInput) { // this.pauseInput = pauseInput; // } public void setTIME_US(long TIME_US) { this.TIME_US = TIME_US; } // // public void setTailTime(long startTime,long endTime){ // long s = 0; // long e = (long)totalMS; // startTime *= 1000000; // endTime *= 1000000; // this.startTime = startTime > s ? startTime : s; // // this.endTime = (endTime < e) && (endTime != 0) ? endTime : e; // this.isNeedTailed = (startTime > 0) && (endTime < e); // // } @Override public void run() { inputLoop(); extractor.release(); decodec.stop(); decodec.release(); Log.v("tag","released decode"); } private void inputLoop(){ extractor.selectTrack(formatIndex); MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); boolean closeExtractor = false; // if (isNeedTailed){ // extractor.seekTo(startTime,MediaExtractor.SEEK_TO_CLOSEST_SYNC); // } while(!Thread.interrupted()){ if (!closeExtractor){ int inputIndex = decodec.dequeueInputBuffer(TIME_US); if (inputIndex >= 0){ ByteBuffer inputBuffer = decodec.getInputBuffer(inputIndex); int size = extractor.readSampleData(inputBuffer,0); if (size < 0){ decodec.queueInputBuffer(inputIndex,0,0,0,MediaCodec.BUFFER_FLAG_END_OF_STREAM); closeExtractor = true; }else{ // if (extractor.getSampleTime() > endTime && isNeedTailed){ // decodec.queueInputBuffer(inputIndex,0,0,0,MediaCodec.BUFFER_FLAG_END_OF_STREAM); // closeExtractor = true; // }else{ decodec.queueInputBuffer(inputIndex,0,size,extractor.getSampleTime(),extractor.getSampleFlags()); extractor.advance(); // } } } } int outputIndex = decodec.dequeueOutputBuffer(info,TIME_US); if (outputIndex >= 0){ ByteBuffer outputBuffer = decodec.getOutputBuffer(outputIndex); int inputBufferEncodeIndex = encodec.dequeueInputBuffer(TIME_US); if (inputBufferEncodeIndex >= 0){ if (info.size < 0){ encodec.queueInputBuffer(inputBufferEncodeIndex,0,0,0,MediaCodec.BUFFER_FLAG_END_OF_STREAM); }else{ ByteBuffer inputEncodeBuffer = encodec.getInputBuffer(inputBufferEncodeIndex); inputEncodeBuffer.put(outputBuffer); encodec.queueInputBuffer(inputBufferEncodeIndex,0,info.size,info.presentationTimeUs,info.flags); } } decodec.releaseOutputBuffer(outputIndex,false); } if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0){ Log.d("tag","start release Decode"); break; } // while(pauseInput){ // try { // Thread.sleep(10l); // } catch (InterruptedException e) { // e.printStackTrace(); // } // } } } }同样的
OutputThread:编码和封装
核心方法OutputLoop();
package com.example.myapplication3.TranscodeBuildUtils; import android.media.MediaCodec; import android.media.MediaFormat; import android.media.MediaMuxer; import android.util.Log; import androidx.annotation.Nullable; import java.nio.ByteBuffer; /** * Created by weizheng.huang on 2019-10-30. */ class OutputThread extends Thread{ // private boolean pauseOutput = false; private static boolean isMuxerStarted = false; private static int videoTrackIndex = -1; private static int audioTrackIndex = -1; private static int isMuxed = 0; private long TIME_US; // private double totalMS = 0; private String MIME; private MediaCodec encodec; private static MediaMuxer muxer; // private ProgressBarDialog.MyHandler handler = ProgressBarDialog.getHandler(); /** * * @param muxer 新鲜的封装器 * @param encodec 编码器 * @param MIME 对应渠道的类型 * @param totalMS 播放总时间 */ public OutputThread(@Nullable MediaMuxer muxer , @Nullable MediaCodec encodec ,@Nullable String MIME , double totalMS){ this.muxer = muxer; this.encodec = encodec; this.MIME = MIME; // this.totalMS = totalMS; } // public void setPauseOutput(boolean pauseOutput) { // this.pauseOutput = pauseOutput; // } public void setTIME_US(long TIME_US) { this.TIME_US = TIME_US; } @Override public void run() { outputLoop(); encodec.stop(); encodec.release(); Log.v("tag","released encodec" + MIME); isMuxed++; releaseMuxer(); } private void outputLoop(){ MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); while(!Thread.interrupted()) { int outputIndex = encodec.dequeueOutputBuffer(info, TIME_US); switch (outputIndex){ case MediaCodec.INFO_TRY_AGAIN_LATER:{ // Log.d("tag","try again Later"); break; } case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED: { Log.d("tag","format changed " +MIME); MediaFormat format = encodec.getOutputFormat(); MIME = format.getString(MediaFormat.KEY_MIME); if (videoTrackIndex < 0 && MIME.startsWith("video")) videoTrackIndex = muxer.addTrack(format); if (audioTrackIndex < 0 && MIME.startsWith("audio")) audioTrackIndex = muxer.addTrack(format); break; } default:{ ByteBuffer outputBuffer = encodec.getOutputBuffer(outputIndex); if (!isMuxerStarted){ startMuxer(); } if (info.size >= 0 && isMuxerStarted){ // if (0 < info.presentationTimeUs){ // double currentMS = info.presentationTimeUs; // Bundle bundle = new Bundle(); // if (MIME.startsWith("video")){ // bundle.putString("videoProgress",new DecimalFormat(".0").format((currentMS / totalMS) * 100)); // } // if (MIME.startsWith("audio")){ // bundle.putString("audioProgress",new DecimalFormat(".0").format((currentMS / totalMS) * 100)); // } // Message message = new Message(); // message.setData(bundle); // message.setTarget(handler); // handler.sendMessage(message); // } int trackIndex = MIME.startsWith("video") ? videoTrackIndex : audioTrackIndex; muxer.writeSampleData(trackIndex , outputBuffer , info); Log.d("tag",MIME + " muxing"); } encodec.releaseOutputBuffer(outputIndex,false); } } if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0){ Log.d("tag","start release " + MIME + " Encode"); break; } // while(pauseOutput){ // try { // Thread.sleep(10l); // } catch (InterruptedException e) { // e.printStackTrace(); // } // } } } private static synchronized void startMuxer(){ if ((0 <= audioTrackIndex ) && (0 <= videoTrackIndex ) && (!isMuxerStarted)){ muxer.start(); isMuxerStarted = true; } } private static synchronized void releaseMuxer(){ if (isMuxed == 2){ isMuxed++; muxer.stop(); muxer.release(); Log.v("tag","released muxer"); } } }建造者模式:Builder是不可少滴
package com.example.myapplication3.TranscodeBuildUtils; import android.media.MediaCodec; import android.media.MediaExtractor; import android.media.MediaMuxer; import androidx.annotation.Nullable; /** * Created by weizheng.huang on 2019-10-30. */ public class TranscodeBuilder { private Transcode transcode = new Transcode(); public void buildVideoInputThread(@Nullable MediaExtractor extractor , @Nullable MediaCodec decodec , @Nullable MediaCodec encodec , int formatIndex , double totalMS){ transcode.setVideoInputThread(extractor, decodec, encodec, formatIndex, totalMS); } public void buildAudioInputThread(@Nullable MediaExtractor extractor , @Nullable MediaCodec decodec , @Nullable MediaCodec encodec , int formatIndex ,double totalMS){ transcode.setAudioInputThread(extractor, decodec, encodec, formatIndex, totalMS); } public void buildVideoOutputThread(@Nullable MediaMuxer muxer , @Nullable MediaCodec encodec , @Nullable String MIME , double totalMS){ transcode.setVideoOutputThread(muxer, encodec, MIME, totalMS); } public void buildAudioOutputThread(@Nullable MediaMuxer muxer , @Nullable MediaCodec encodec ,@Nullable String MIME , double totalMS){ transcode.setAudioOutputThread(muxer, encodec, MIME, totalMS); } public void setTIMEUS(long TIME_US){ transcode.setTimeUS(TIME_US); } public Transcode build(){ return transcode; } }接下来基础都做完了,可以用了
package com.example.myapplication3; import android.content.res.AssetFileDescriptor; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaExtractor; import android.media.MediaFormat; import android.media.MediaMuxer; import com.example.myapplication3.TranscodeBuildUtils.Transcode; import com.example.myapplication3.TranscodeBuildUtils.TranscodeBuilder; import java.io.IOException; /** * Created by weizheng.huang on 2019-10-30. */ public class TranscodeWrapperDemo2 { private int audioIndex = -1; private int videoIndex = -1; private double assignSizeRate = 1; private double durationTotal = 0; private String audioFormatType; private String videoFormatType; private String filePath; private AssetFileDescriptor srcFilePath; private AssetFileDescriptor srcFilePath2; private MediaExtractor extractor,audioExtractor; private MediaCodec decodec,encodec; private MediaCodec audioDecodec,audioEncodec; private MediaMuxer muxer; private Transcode transcode; ///public public void setAssignSize(double assignSizeRate) { this.assignSizeRate = assignSizeRate; } public void setPauseTranscode(boolean pauseTranscode) { transcode.setPauseTranscode(pauseTranscode); } public void setTailTime(long startTime ,long endTime){ transcode.setTailTime(startTime,endTime); } public TranscodeWrapperDemo2(String filePath, AssetFileDescriptor srcFilePath, AssetFileDescriptor srcFilePath2) { this.filePath = filePath; this.srcFilePath = srcFilePath; this.srcFilePath2 = srcFilePath2; } public boolean startTranscode(){ transcode.start(); return true; } public void init(){ initMediaExtractor(); initMediaCodec(); initMediaMuxer(); initTranscode(); } ///private // private void initTranscode(){ TranscodeBuilder transcodeBuilder = new TranscodeBuilder(); transcodeBuilder.buildAudioInputThread(audioExtractor,audioDecodec,audioEncodec,audioIndex,durationTotal); transcodeBuilder.buildVideoInputThread(extractor,decodec,encodec,videoIndex,durationTotal); transcodeBuilder.buildAudioOutputThread(muxer,audioEncodec,audioFormatType,durationTotal); transcodeBuilder.buildVideoOutputThread(muxer,encodec,videoFormatType,durationTotal); transcodeBuilder.setTIMEUS(50000l); transcode = transcodeBuilder.build(); } private void initMediaExtractor(){ extractor = new MediaExtractor(); try { extractor.setDataSource(srcFilePath); } catch (IOException e) { e.printStackTrace(); } audioExtractor = new MediaExtractor(); try { audioExtractor.setDataSource(srcFilePath2); } catch (IOException e) { e.printStackTrace(); } } private int frameRate; private int bitRate; private int width; private int height; private int audioBitRate; private int sampleRate; private int channelCount; private void initMediaCodec(){ decode/// for (int i = 0; i < extractor.getTrackCount(); i++){ MediaFormat format = extractor.getTrackFormat(i); String formatType = format.getString(MediaFormat.KEY_MIME); if (formatType.startsWith("video")){ videoIndex = i; try { decodec = MediaCodec.createDecoderByType(formatType); } catch (IOException e) { e.printStackTrace(); } videoFormatType = formatType; frameRate = format.getInteger(MediaFormat.KEY_FRAME_RATE); bitRate = format.getInteger(MediaFormat.KEY_BIT_RATE); width = format.getInteger(MediaFormat.KEY_WIDTH); height = format.getInteger(MediaFormat.KEY_HEIGHT); durationTotal = format.getLong(MediaFormat.KEY_DURATION); decodec.configure(format,null,null,0); decodec.start(); continue; } if (formatType.startsWith("audio")){ audioIndex = i; try { audioDecodec = MediaCodec.createDecoderByType(formatType); } catch (IOException e) { e.printStackTrace(); } audioFormatType = formatType; audioBitRate = format.getInteger(MediaFormat.KEY_BIT_RATE); sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); audioDecodec.configure(format,null,null,0); audioDecodec.start(); } } MediaFormat videoFormat = MediaFormat.createVideoFormat(videoFormatType, width, height); videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate); videoFormat.setInteger(MediaFormat.KEY_BIT_RATE,(int)(bitRate * assignSizeRate )); videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible); videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL,frameRate * 2); MediaFormat audioFormat = MediaFormat.createAudioFormat(audioFormatType, sampleRate, channelCount); audioFormat.setInteger(MediaFormat.KEY_BIT_RATE,(int)(audioBitRate * assignSizeRate )); try { encodec = MediaCodec.createEncoderByType(videoFormatType); } catch (IOException e) { e.printStackTrace(); } encodec.configure(videoFormat, null,null,MediaCodec.CONFIGURE_FLAG_ENCODE); encodec.start(); try { audioEncodec = MediaCodec.createEncoderByType(audioFormatType); } catch (IOException e) { e.printStackTrace(); } audioEncodec.configure(audioFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE); audioEncodec.start(); } private void initMediaMuxer(){ try { muxer = new MediaMuxer(filePath,MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); } catch (IOException e) { e.printStackTrace(); } } }最后总结:最好浏览完所有代码再来看,就知道我总结的是哪里了。😊
1.TIME_US的设置问题
在修改这个值的时候,我换过-1、0、100000l等等,发现-1会导致线程阻塞,也许是因为一旦出现申请不到的操作的话,一直等待会直接阻塞某一线程,但是呢输入和输出必须挂钩,一旦有一者阻塞了,另一者势必因为缓冲区无法清除或无法申请而挂掉不能塞数据从而抛出IllegalArgumentException。我换成大于0的时候,那么必然意味着丢帧,于是一点点调整,发现50000l以上基本没变化了(其实不过是作者懒得往细调了,直接50000l,可能小一点的丢帧也相差不大,这些就是随心了)
2.为什么要用两个extractor和两个视频文件
看到inputLoop没,我selectTrack也放在里边了,两个线程不能只selectTrack一个吧,另一个总会覆盖掉的。
所以用两个才是明智的选择。然后就是这里还会有问题,不过我就不贴出来了,初始化有点多。extractor我setdatasource这里用的是两个视频文件,一份原始视频文件,另一份则是原始文件的拷贝文件。为什么这么做的原因是因为,如果两个extractor同时setdatasource设置一个视频文件,读取时会抛出read on track x error with x ,这里就是读取时容易互相产生干扰,对于这里作者也是头疼的狠,暂时无法想出更好的方法。
3.关于format取渠道时,设置encodec、muxer的问题
关于videoEncodec,这里的format一定得保证width、height、formatType、colorFormat、frameRate、IFrameInteval这几个属性要设置好。audioEncodec的话就是sampleRate、channelCount、formatType。bitRate都是可选的
muxer这里就有点意思了,网上找的基本都是瞎鸡儿设置,按官网写的配在outputIndex这里,当outputIndex为MediaCodec.INFO_OUTPUT_FORMAT_CHANGED的时候,这里添加渠道就行了。为什么呢,因为编码器启动后,第一帧(没数据的)或者说OutputIndex第一次取一定会是这个值,如果不是那就肯定编码器失误了。
4.将解码输出和编码输入放在一起确实方便,就是不用生成文件了很舒服。
5.MainActivity我就不给了,提供的几个public方法依次调用就行了。
6.新增此文大部分使用还是正确,但是已经不合适了建议往后看后更新的转码demo。
https://blog.csdn.net/h2948203216/article/details/103694508