Android 音视频合成经验总结

news/2024/11/8 11:58:47/

刚刚实现了音视频合成的需求,趁热打铁记录一下遇到的问题

需求描述:

将给定的音频及视频合成一个视频,如果音频时长小于视频,则重复播放;如果大于视频时长则截取到视频结束。

采用的是MediaMuxer、MediaCodec原生方案。

MediaCodec主要处理音频格式的问题,音频可能有mp3 m4a等一些格式,解析得到的音频编码有一个是安卓不支持的编码格式,如audio/mpeg,添加音轨时就会抛出异常。所以这里统一用MediaCodec解析成audio/aac格式,解析方法参考了网上的解析

    /**** @param audioPath* @param audioStartTimeUs -1 表示不截取* @param audioEndTimeUs -1 表示不截取* @return*/public String audioToAAC(String audioPath,long audioStartTimeUs,long audioEndTimeUs){long a=System.currentTimeMillis();int audioExtractorTrackIndex=-1;int audioMuxerTrackIndex=-1;int channelCount=1;int sourceSampleRate = 0;String newAudioAAc = "";long sourceDuration=0;try {File tempFlie = new File(audioPath);String tempName = tempFlie.getName();String suffix = tempName.substring(tempName.lastIndexOf(".") + 1);
//             newAudioAAc= tempFlie.getParentFile().getAbsolutePath() + "/" + tempName.replace(suffix, "aac");newAudioAAc= Objects.requireNonNull(tempFlie.getParentFile()).getAbsolutePath() + "/" + tempName.replace(suffix, "aac");
//            muxer = new MediaMuxer(newAudioAAc, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);Log.i(TAG,"audioPath=="+audioPath+",newAudioAAC=="+newAudioAAc+",newAudioAAc=="+newAudioAAc);//音频信息获取MediaExtractor audioExtractor = new MediaExtractor();audioExtractor.setDataSource(audioPath);int trackCount = audioExtractor.getTrackCount();MediaFormat sourceFormat=null;String sourceMimeType="";int timeOutUs=300;for (int i = 0; i < trackCount; i++) {sourceFormat = audioExtractor.getTrackFormat(i);sourceMimeType = sourceFormat.getString(MediaFormat.KEY_MIME);channelCount=sourceFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);sourceSampleRate = sourceFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);sourceDuration = sourceFormat.getLong(MediaFormat.KEY_DURATION);if (sourceMimeType.startsWith("audio/")) { //找到音轨Log.i(TAG,"sourceMimeType=="+sourceMimeType+",channelCount=="+channelCount+",sourceSampleRate=="+sourceSampleRate);audioExtractorTrackIndex = i;break;}}//初始化解码器MediaCodec audioDecoder = null;audioDecoder = MediaCodec.createDecoderByType(sourceMimeType);audioDecoder.configure(sourceFormat, null, null, 0);audioDecoder.start();//初始化编码MediaCodec mEncorder = null;mEncorder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);MediaFormat format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, 44100, channelCount);format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC) ;format.setInteger(MediaFormat.KEY_BIT_RATE, 128000);format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 1024*1024 * 10);mEncorder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);mEncorder.start();audioExtractor.selectTrack(audioExtractorTrackIndex);MediaCodec.BufferInfo sourceAudioBufferInfo = new MediaCodec.BufferInfo();MediaCodec.BufferInfo audioBufferInfo = new MediaCodec.BufferInfo();ByteBuffer audioByteBuffer = ByteBuffer.allocate(1024*1024*10);FileOutputStream mFileStream = new FileOutputStream(newAudioAAc);while (true) {int readSampleSize = audioExtractor.readSampleData(audioByteBuffer, 0);Log.i(TAG, "readSampleSize==" + readSampleSize+",audioByteBuffer.limit=="+audioByteBuffer.limit()+",timeOutUs=="+timeOutUs);if (readSampleSize < 0) {audioExtractor.unselectTrack(audioExtractorTrackIndex);break;}long audioSampleTime=audioExtractor.getSampleTime();//可以做进度回调Log.i(TAG, "audioSampleTime==" +audioSampleTime+",,progress=="+((float)audioSampleTime/(float)sourceDuration));if (audioStartTimeUs !=-1 && audioSampleTime < audioStartTimeUs) {audioExtractor.advance();continue;}if (audioEndTimeUs !=-1 && audioSampleTime > audioEndTimeUs) {break;}int audioSampleFlags=audioExtractor.getSampleFlags();//解码int sourceInputBufferIndex = audioDecoder.dequeueInputBuffer(timeOutUs);if (sourceInputBufferIndex >= 0) {ByteBuffer sourceInputBuffer = audioDecoder.getInputBuffer(sourceInputBufferIndex);sourceInputBuffer.clear();sourceInputBuffer.put(audioByteBuffer);audioDecoder.queueInputBuffer(sourceInputBufferIndex, 0, readSampleSize, audioSampleTime, audioSampleFlags);}int sourceOutputBufferIndex = audioDecoder.dequeueOutputBuffer(sourceAudioBufferInfo, timeOutUs);if (sourceOutputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {// 后续输出格式变化}while (sourceOutputBufferIndex >= 0) {ByteBuffer decoderOutputBuffer = audioDecoder.getOutputBuffer(sourceOutputBufferIndex);//编码int inputBufferIndex = mEncorder.dequeueInputBuffer(timeOutUs);if (inputBufferIndex >= 0) {ByteBuffer inputBuffer = mEncorder.getInputBuffer(inputBufferIndex);inputBuffer.clear();inputBuffer.put(decoderOutputBuffer);mEncorder.queueInputBuffer(inputBufferIndex, 0, decoderOutputBuffer.limit(), audioSampleTime, audioSampleFlags);}int outputBufferIndex = mEncorder.dequeueOutputBuffer(audioBufferInfo, timeOutUs);if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {// 后续输出格式变化}while (outputBufferIndex >= 0) {ByteBuffer outputBuffer = mEncorder.getOutputBuffer(outputBufferIndex);int outBufferSize = outputBuffer.limit() + 7;byte[] aacBytes = new byte[outBufferSize];addADTStoPacket(aacBytes, outBufferSize, channelCount);outputBuffer.get(aacBytes, 7, outputBuffer.limit());mFileStream.write(aacBytes);mEncorder.releaseOutputBuffer(outputBufferIndex, false);outputBufferIndex = mEncorder.dequeueOutputBuffer(audioBufferInfo, timeOutUs);}audioDecoder.releaseOutputBuffer(sourceOutputBufferIndex, false);sourceOutputBufferIndex = audioDecoder.dequeueOutputBuffer(sourceAudioBufferInfo, timeOutUs);}audioExtractor.advance();}//释放资源mEncorder.stop();mFileStream.flush();mFileStream.close();long b=System.currentTimeMillis()-a;Log.i(TAG,"编码结束=="+b);} catch (Exception e) {e.printStackTrace();}return newAudioAAc;}

得到aac编码的音频数据后,我们就可以将音视频合入到同一段视频中

    @SuppressLint("WrongConstant")public void mix() {MediaExtractor videoExtractor = null;MediaExtractor audioExtractor = null;MediaMuxer mixMediaMuxer = null;try {videoExtractor = new MediaExtractor();videoExtractor.setDataSource(outputVideoFilePath);int videoIndex = -1;MediaFormat videoTrackFormat = null;int trackCount = videoExtractor.getTrackCount();for (int i = 0; i < trackCount; i++) {videoTrackFormat = videoExtractor.getTrackFormat(i);if (Objects.requireNonNull(videoTrackFormat.getString(MediaFormat.KEY_MIME)).startsWith("video/")) {videoIndex = i;}}audioExtractor = new MediaExtractor();audioExtractor.setDataSource(outputAudioFilePath);int audioIndex = -1;MediaFormat audioTrackFormat = null;trackCount = audioExtractor.getTrackCount();for (int i = 0; i < trackCount; i++) {audioTrackFormat = audioExtractor.getTrackFormat(i);if (Objects.requireNonNull(audioTrackFormat.getString(MediaFormat.KEY_MIME)).startsWith("audio/")) {audioIndex = i;}}videoExtractor.selectTrack(videoIndex);audioExtractor.selectTrack(audioIndex);// 初始化视频和音频缓冲区信息MediaCodec.BufferInfo videoBufferInfo = new MediaCodec.BufferInfo();MediaCodec.BufferInfo audioBufferInfo = new MediaCodec.BufferInfo();// 初始化MediaMuxer,用于合成输出文件mixMediaMuxer = new MediaMuxer(outputFilePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);assert videoTrackFormat != null;int videoTrackIndex = mixMediaMuxer.addTrack(videoTrackFormat);assert audioTrackFormat != null;int audioTrackIndex = mixMediaMuxer.addTrack(audioTrackFormat);mixMediaMuxer.start();ByteBuffer byteBuffer = ByteBuffer.allocate(1024 * 1024);long audioTime = 0L;long cycleTimes = 0;videoExtractor.unselectTrack(videoIndex);videoExtractor.selectTrack(videoIndex);while (true) {int data = videoExtractor.readSampleData(byteBuffer, 0);if (data < 0) {break;}videoBufferInfo.size = data;videoBufferInfo.presentationTimeUs = videoExtractor.getSampleTime();videoBufferInfo.offset = 0;videoBufferInfo.flags = videoExtractor.getSampleFlags();mixMediaMuxer.writeSampleData(videoTrackIndex, byteBuffer, videoBufferInfo);videoExtractor.advance();}while (true) {int data = audioExtractor.readSampleData(byteBuffer, 0);if (data < 0) {if (audioBufferInfo.presentationTimeUs < videoBufferInfo.presentationTimeUs) {if (cycleTimes == 0) {audioTime = audioBufferInfo.presentationTimeUs;}cycleTimes ++;audioExtractor.seekTo(0, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);data = audioExtractor.readSampleData(byteBuffer, 0);} else {break;}}audioBufferInfo.size = data;audioBufferInfo.presentationTimeUs = audioTime * cycleTimes + audioExtractor.getSampleTime();audioBufferInfo.offset = 0;audioBufferInfo.flags = audioExtractor.getSampleFlags();mixMediaMuxer.writeSampleData(audioTrackIndex, byteBuffer, audioBufferInfo);audioExtractor.advance();if (videoBufferInfo.presentationTimeUs <= audioBufferInfo.presentationTimeUs) {break;}}myCallBack.muxerResult(true);} catch (IOException e) {Log.i("dealBug", "mix exception " + e);e.printStackTrace();myCallBack.muxerResult(false);} finally {if (mixMediaMuxer != null) {mixMediaMuxer.stop();mixMediaMuxer.release();}videoExtractor.release();audioExtractor.release();}}

这里遇到的坑,原先在网上找了类似的audioTime和videoTime他们采用的策略是获取头两个关键帧取标准值,作为音视频的时间戳。但这样获取会存在不准的问题,advance之后,每次获取到的data值并不是固定的,所以录出的视频播放速度可能存在要么快或者慢的问题。

记录一下,防止下次忘记!


http://www.ppmy.cn/news/1545348.html

相关文章

vue3 css的样式如果background没有,如何覆盖有background的样式

在Vue 3中&#xff0c;如果你想要覆盖一个有background样式的元素&#xff0c;你可以使用更具体的CSS选择器来提升你的样式规则的优先级&#xff0c;或者使用!important规则。 .some-class { background: url(path/to/image.jpg) no-repeat center center; } 你可以通过提供一…

linux网络编程自定义协议和多进程多线程并发-TCP编程

1.三次握手及后面过程 计算机A是客户端, B是服务端 1.1三次握手&#xff1a; 1客户端给服务端SYN报文 2服务端返回SYNACK报文 3客户端返回ACK报文 客户端发完ACK后加入到服务端的维护队列中&#xff0c;accept()调用后就能和客户端建立连接&#xff0c;然后建立通讯 1.2关闭…

C++ 线程初始化编译报错

这是一个很简单的开启一个线程, 用于演示一个线程和生命周期之间的错误,但是还没有把这个错误暴露出来, 就遇见了一个编译问题. 线程中执行指定逻辑的代码 线程的执行方法, 声明写在了ThreadRun.h 实现写在 ThreadRun.cpp中. class ThreadRun { public: void func(); };void T…

java访问华为网管软件iMaster NCE的北向接口时传递参数问题

上一篇文章介绍了利用《java访问华为网管软件iMaster NCE的北向接口》的一般性步骤&#xff0c;这里详细介绍其中一个读取性能数据的示例。原因是读取华为网管软件北向接口&#xff0c;完全找不到可供参考的例子。如果不需要传递什么参数&#xff0c;就能获取到结果&#xff0c…

特征检测与特征匹配方法笔记+代码分享

在一幅图像中&#xff0c;总能发现其独特的像素点&#xff0c;这些点可以被视为该图像的特征&#xff0c;我们称之为特征点。在计算机视觉领域中&#xff0c;基于特征点的图像特征匹配是一项至关重要的任务&#xff0c;因此&#xff0c;如何定义并识别一幅图像中的特征点显得尤…

【vue2.0入门】认识vue工程

目录 引言一、工程目录介绍1. package.json文件2. src\App.vue3. src\components 文件夹4. src\assets 文件夹5. node_modules 文件夹6. 其他 二、安装 vuejs devtools 插件1. 下载插件2. 配置插件3. 使用插件 三、总结 引言 本系列教程旨在帮助一些零基础的玩家快速上手前端开…

物联优化汽车齿轮锻造

在汽车齿轮的锻造工艺中&#xff0c;锻造温度、锻造压力与行程、锻造速度与锤击方式以及热处理工艺等核心参数扮演着举足轻重的角色。这些参数的精准控制与实时监测&#xff0c;对于提升生产效率、确保产品质量、削减生产成本以及推动生产智能化转型具有不可估量的价值。明达技…

遥测终端机RTU产品如何选型和配置

在配置远程终端单元&#xff08;RTU&#xff09;时&#xff0c;首先需要确认其支持的数据接口类型&#xff0c;例如常见的RS-232、RS-485接口&#xff0c;以及以太网连接方式。这些接口类型将决定RTU如何与其他设备进行数据交换和通信。 接下来&#xff0c;需要确定RTU支持的输…