ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android视频转换工具源码解析:Java+MediaCodec实现格式互转

Android视频转换工具源码解析:Java+MediaCodec实现格式互转 做Android开发这些年被问得最多的一件事就是手机里这个视频格式不支持能不能转一下以前我都直接甩一个转码软件给对方直到有个朋友说“你搞开发的为什么不做个自己的转换工具”——于是就有了这个用Java语言写的Android Studio视频格式转换软件源代码项目。这个项目核心解决的是移动端视频格式不互通的问题网盘里下载的mkv在手机上放不了相机录的HEVC视频发到老设备上没法播想给视频压一压省点存储空间这些都是实际到不能再实际的场景。它本质上是一个完整的Android App工程用Java语言编写基于Android Studio开发核心思路是调用系统底层的媒体编解码能力把输入视频从一种编码格式转为另一种同时控制分辨率、码率、帧率这些关键参数。整个工程结构清晰从文件选择、权限处理到转码调度、进度反馈都有完整实现不依赖外部C库纯Java层就能跑通。这篇内容适合正在学Android多媒体开发的读者也适合手里拿到这类源码想跑通、想二次改造的人。我会把整个项目的设计思路、核心代码实现、踩过的坑全部摊开讲从为什么选Java而不是Kotlin到MediaCodec怎么用才不掉帧再到真机验证的具体流程一次说透直接可以照着做。1. 项目概述与技术选型思路1.1 为什么在Android端做视频转换先说需求。移动端视频转换放在几年前基本是PC的活儿但现在手机成了主力创作设备转码需求也跟着硬起来了。短视频剪辑要把竖屏拍的内容转成横屏通用格式、网盘下回来的视频在相册里打不开、想把相机录的4K大文件压缩成适合微信发送的小体积版本全都得靠转码。做这个项目之前必须先认清两个Android平台特有的前提。第一硬件编解码能力参差不齐。中低端机型和旗舰机的编码器支持列表完全不同有的设备支持HEVC硬解有的只有软件解码有的编码器对分辨率有严格限制强行配置超高分辨率会直接configure失败。第二系统API版本跨度大。Android 5.0的MediaCodec和Android 14的MediaCodec在行为细节上差了很多尤其涉及Surface模式、异步回调、分区存储时版本差异直接决定代码怎么写。所以这个项目在设计时定的调子就是不追求“什么都能转”而是追求“能转的都转得稳”。目标设备范围锁定在API 21到API 34通过运行时查询编解码器能力来做动态适配避免写死某一种编码参数。后面所有代码都是围绕这个思路展开的。1.2 两条技术路线MediaCodec原生与FFmpegAndroid上做视频转码业界基本就是两条路。路线A是纯Java方案用系统自带的MediaExtractor、MediaCodec、MediaMuxer三个类完成整个转码流程。这条路不走任何外部库APK体积小巧硬编硬解速度快电量消耗也低因为全程调用的是GPU和专用DSP。缺点也很直接支持的输入输出格式受限。MediaMuxer原生只支持MP4、WebM、3GP这类常见容器编解码器种类取决于设备硬件想输出MKV、FLV原生API做不到。路线B是Java层调用FFmpeg通过JNI把FFmpeg编译成so库然后在Java代码里用native方法操作。FFmpeg的格式支持非常全几乎你能想到的封装和编码格式它都能处理还能做滤镜、拼接、裁剪这些复杂操作。这方案的代价是APK体积暴增一个全功能的FFmpeg so库动辄十几甚至几十MB集成复杂度也高先要用NDK交叉编译出各个ABI架构的库再处理JNI层的复杂签名出了问题排查难度直线上升。1.3 方案对比与最终选择我把两个方案的关键差异做了个对比这也是我最终选型时的依据。对比维度MediaCodec原生方案FFmpeg方案APK体积几乎无额外增加增加10MB以上取决于裁剪程度转码性能硬件加速速度快功耗低看编译配置可开硬件加速但复杂格式支持容器和编码格式受设备限制覆盖面极广几乎所有主流格式集成复杂度系统API无外部依赖需要NDK交叉编译、JNI封装后期扩展性滤镜、拼接等功能较难实现功能强大扩展空间大排错难度相对简单日志直观崩溃栈在native层排查费劲最终选择以MediaCodec为主路线原因有三个。第一这个项目要的是“看得见的稳定”系统API虽然能力有限但不会出现so库兼容性爆炸的问题。第二性能是关键指标纯Java调系统API的转码速度在绝大多数情况下优于未优化的FFmpeg软解软编。第三源码的定位是教学和二次开发MediaCodec三件套的逻辑清晰比JNI层面调试黑盒友好得多。FFmpeg我留在后期扩展方案里等基础功能稳定了再考虑接进来。2. 核心机制拆解转码流程与Java层设计2.1 视频转码的本质流程把视频转码想成一次“翻译再重新印刷”的过程就很好理解了。原始视频文件是一个封装容器里面装着编码后的视频流和音频流。转码要做的就是把视频流解码成原始像素再用另一种编码格式压缩最后和音频一起重新封装。这个流程分成四步解封装、解码、编码、封装。解封装用MediaExtractor把MP4、MKV这类容器里的压缩数据一条轨道一条轨道地抽出来解码用MediaCodec的decoder模式把压缩数据还原成YUV像素帧编码用MediaCodec的encoder模式把YUV像素帧重新压缩成目标格式的码流封装用MediaMuxer把新的视频码流和音频码流写回一个新文件。这四步是串行流水线的关系前一环的输出是后一环的输入任何一个环节出错视频就废了。2.2 MediaExtractor、MediaCodec、MediaMuxer三件套的职责与使用要点MediaExtractor的核心作用是读取和分离。它通过setDataSource把文件打开后内部会解析容器结构得到轨道信息然后用getTrackFormat拿到轨道的MediaFormat。注意它不会去解码视频内容它只负责把压缩字节流喂出来所以它的工作方式和文件读取很像按顺序调用readSampleData取数据用advance移动到下一个样本getSampleTime拿到当前样本的时间戳。MediaCodec是整个转码的心脏。它有两种模式创建时传入的具体MIME类型决定它是解码器还是编码器。解码时通过dequeueInputBuffer拿可用的输入Buffer把压缩数据填进去后queueInputBuffer交给解码器解码结果从dequeueOutputBuffer取出来。编码流程完全相同只是输入的是原始像素帧输出的是压缩码流。Buffer的获取和归还有一套严格的生命周期这里最容易出Bug后面我会详细说。MediaMuxer相对简单它就是负责“装订成册”的。调用addTrack告诉它轨道格式然后不断writeSampleData往里写样本。要注意两个关键点addTrack必须在拿到编码器输出格式之后调用而且必须在第一次writeSampleData之前muxer.start()之后就不能再添加轨道了一次转换只有一次机会。2.3 Surface输入输出链路与缓冲机制MediaCodec最强大的特性之一就是支持Surface输入输出。在转码场景里可以让解码器把帧渲染到一个Surface上然后把这个Surface作为编码器的输入源这样解码到编码之间就不经过Java层的ByteBuffer副本性能提升非常明显。具体实现是这样的编码器configure完成之后调用createInputSurface得到一个Surface这个Surface内部连接着编码器专属的BufferQueue。解码器configure的时候把这个Surface作为输出目标传进去于是解码器每解码出一帧画面直接渲染到这个BufferQueue上编码器自动从同一个BufferQueue里取帧并压缩。整个像素流转全部在图形管线内部完成CPU内存完全不参与速度和功耗都好看很多。这个链路的好处不止是快。手动在Java层处理YUV数据需要处理色彩格式转换、对齐、旋转等一堆细节很容易写出花屏或者掉色的问题。Surface方案绕开了这些繁琐操作让系统图形栈处理底层的像素排布对开发者的技术水平要求大幅度降低。这是Android 19之后官方推荐的做法而这个项目minSdk是21所以可以放心用。2.4 项目工程结构与模块划分源码的工程结构直接影响后期维护我这个项目按职责拆成了几个包。入口层MainActivity负责文件选择和权限处理ConvertActivity展示转换进度。核心逻辑全部放在transcoder包下面TranscoderManager是对外提供的统一管理类上层只跟它打交道VideoTranscoder是视频轨道转码的真正实现AudioRemuxer负责音频轨道处理可以选择直接拷贝原始音频流或者丢弃音频ConvertTask是一个Runnable封装把转码放到后台线程执行。util包下面放一些MIME判断、文件大小计算之类的工具方法。接口回调用ProgressListener统一组织。app/src/main/java/com/example/videoformatter/ ├── MainActivity.java ├── ConvertActivity.java ├── transcoder/ │ ├── TranscoderManager.java │ ├── VideoTranscoder.java │ ├── AudioRemuxer.java │ └── ConvertTask.java ├── util/ │ ├── MimeUtils.java │ └── FileUtils.java └── callback/ └── ProgressListener.java这个分层的好处是上层界面代码基本不感知底层调度细节想换FFmpeg实现时只需要重写TranscoderManager的实现Activity层可以完全不动。3. 实操实现从零搭建一个可用的转码工具3.1 Gradle配置与Android Studio环境准备先解决环境问题。项目用的是Android Studio的Gradle构建体系新建工程时注意AGP版本和Gradle版本要匹配否则sync阶段就会报错。我用的AGP是8.2.2对应Gradle 8.2及以上版本compileSdk设到34minSdk 21维持兼容性。build.gradle关键配置如下plugins { id com.android.application } android { compileSdk 34 defaultConfig { applicationId com.example.videoformatter minSdk 21 targetSdk 34 versionCode 1 versionName 1.0 } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } } dependencies { implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.11.0 implementation androidx.constraintlayout:constraintlayout:2.1.4 }这里编译选项用Java 17是因为新版AGP要求实际代码并不需要用到新语法特性。如果你刚装了Android Studio发现界面是英文不习惯可以直接在Settings的Plugins里搜索Chinese Language Pack安装中文语言包装完重启就是中文界面了。3.2 权限适配与文件选择实现转换视频首先得能拿到视频文件。Android的存储权限这几年改了好几次最简单的做法是用系统文件选择器完全不需要申请存储权限。用ActivityResultContracts.OpenDocument把选择器弹出来用户选定视频后返回一个Uri然后通过ContentResolver拿到文件路径。这套方案的好处是不管什么Android版本都通用用户也安心因为App只访问他明确选中的文件。private final ActivityResultLauncherString[] pickVideoLauncher registerForActivityResult(new ActivityResultContracts.OpenDocument(), uri - { if (uri ! null) { String srcPath FileUtils.getRealPathFromUri(this, uri); startConvert(srcPath); } }); public void onClickChooseVideo(View view) { pickVideoLauncher.launch(new String[]{video/*}); }如果要做批量转换不能走文件选择器就需要申请存储权限了。Android 13及以上用READ_MEDIA_VIDEOAndroid 12及以下用READ_EXTERNAL_STORAGE。权限申请用ActivityResultContracts.RequestPermission处理这里不再展开。3.3 转码核心代码实现进入重点。转码主流程封装在VideoTranscoder类里对外暴露一个transcode方法。第一步用MediaExtractor打开源文件遍历轨道找到第一个video轨拿到源格式。第二步根据目标参数创建MediaFormat配置编码器。第三步创建编码器的输入Surface把解码器配置到这个Surface上形成Surface链路。第四步创建MediaMuxer。核心代码结构如下public boolean transcode(String inputPath, String outputPath, String targetMime, int targetWidth, int targetHeight, int bitRate, int frameRate) { MediaExtractor extractor new MediaExtractor(); MediaCodec decoder null; MediaCodec encoder null; MediaMuxer muxer null; try { extractor.setDataSource(inputPath); int srcVideoTrack selectTrack(extractor, video/); if (srcVideoTrack 0) { Log.e(TAG, no video track found); return false; } MediaFormat srcFormat extractor.getTrackFormat(srcVideoTrack); extractor.selectTrack(srcVideoTrack); String srcMime srcFormat.getString(MediaFormat.KEY_MIME); decoder MediaCodec.createDecoderByType(srcMime); MediaFormat dstFormat MediaFormat.createVideoFormat( targetMime, targetWidth 0 ? targetWidth : srcFormat.getInteger(MediaFormat.KEY_WIDTH), targetHeight 0 ? targetHeight : srcFormat.getInteger(MediaFormat.KEY_HEIGHT)); dstFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); dstFormat.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate 0 ? frameRate : srcFormat.containsKey(MediaFormat.KEY_FRAME_RATE) ? srcFormat.getInteger(MediaFormat.KEY_FRAME_RATE) : 30); dstFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); dstFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); encoder MediaCodec.createEncoderByType(targetMime); encoder.configure(dstFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); Surface encoderInputSurface encoder.createInputSurface(); encoder.start(); decoder.configure(srcFormat, encoderInputSurface, null, 0); decoder.start(); muxer new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); return runTranscodeLoop(extractor, decoder, encoder, muxer); } catch (Exception e) { Log.e(TAG, transcode failed, e); return false; } finally { // 按顺序释放decoder、encoder、encoderInputSurface、muxer、extractor } }selectTrack的实现很简单遍历轨道找mime前缀匹配的索引即可。真正的难点在runTranscodeLoop它同时驱动解码器输入、解码器输出、编码器输出三个环节。用MediaCodec的同步模式循环里先往解码器喂数据再取解码输出让它驱动编码器最后从编码器拿压缩码流写入muxer。代码长约60行核心逻辑如下private boolean runTranscodeLoop(MediaExtractor extractor, MediaCodec decoder, MediaCodec encoder, MediaMuxer muxer) { MediaCodec.BufferInfo encodeInfo new MediaCodec.BufferInfo(); int[] encoderTrackIndex {-1}; boolean inputDone false; boolean outputDone false; boolean muxerStarted false; while (!outputDone) { if (!inputDone) { int inIndex decoder.dequeueInputBuffer(TIMEOUT_US); if (inIndex 0) { ByteBuffer inBuffer decoder.getInputBuffer(inIndex); int size extractor.readSampleData(inBuffer, 0); if (size 0) { decoder.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); inputDone true; } else { decoder.queueInputBuffer(inIndex, 0, size, extractor.getSampleTime(), 0); extractor.advance(); } } } int decOutIndex decoder.dequeueOutputBuffer(encodeInfo, TIMEOUT_US); if (decOutIndex 0) { // Surface模式下像素渲染由图形管线自动完成 decoder.releaseOutputBuffer(decOutIndex, true); } int encOutIndex encoder.dequeueOutputBuffer(encodeInfo, TIMEOUT_US); if (encOutIndex 0) { ByteBuffer encBuffer encoder.getOutputBuffer(encOutIndex); if ((encodeInfo.flags MediaCodec.BUFFER_FLAG_CODEC_CONFIG) ! 0 encodeInfo.size 0) { int mediaTrackIndex muxer.addTrack(encoder.getOutputFormat()); if (mediaTrackIndex 0) { encoderTrackIndex[0] mediaTrackIndex; muxer.start(); muxerStarted true; } } if ((encodeInfo.flags MediaCodec.BUFFER_FLAG_END_OF_STREAM) ! 0) { outputDone true; } else if (encodeInfo.size 0) { if (muxerStarted encoderTrackIndex[0] 0) { muxer.writeSampleData(encoderTrackIndex[0], encBuffer, encodeInfo); } } encoder.releaseOutputBuffer(encOutIndex, false); } if (sleepWhenIdle) { Thread.sleep(1); // 避免忙轮询导致CPU飙高 } } return true; }这段代码几个关键点要记住addTrack的时机必须等编码器输出CODEC_CONFIG数据之后因为这时才能拿到完整的输出格式信息包括csd-0、csd-1这些解码器需要的参数muxer.start()要在addTrack之后立即调用每次writeSampleData都要用编码器对应Buffer的BufferInfo时间戳、flags都不能乱填。3.4 进度反馈与UI集成转码是耗时操作必须在后台线程跑。ConvertTask实现Runnable放到ExecutorService里执行进度通过ProgressListener接口回调接口里定义onProgress和onComplete方法。主线程注册回调后通过runOnUiThread把进度更新到进度条和百分比文本上。public interface ProgressListener { void onProgress(float percent); void onComplete(boolean success, String outputPath); }要拿到真实进度可以在解码循环里统计已经处理的时间戳。用extractor的当前时间戳除以总时长就能得到一个百分比。总时长可以在转码前用MediaMetadataRetriever获取MediaMetadataRetriever retriever new MediaMetadataRetriever(); retriever.setDataSource(srcPath); String durationStr retriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_DURATION); long totalDurationUs Long.parseLong(durationStr) * 1000;然后每隔几十毫秒更新一次注意不要太频繁否则UI线程会频繁刷新动画导致卡顿。我一般控制在每秒10次以内。3.5 构建打包与真机验证代码写完后构建Release包。如果只用在手机上建议在build.gradle里配置abiFilters只保留arm64-v8a和armeabi-v7a减少包体积。真机验证流程我习惯按这个顺序先测一个分辨率高一点的MP4转H.264确认基本链路没问题再测H.265源转H.264验证解码器兼容性然后测不同分辨率缩放最后测低端机重点观察内存和耗时。这里要特别提醒不要只在模拟器上测视频转码。模拟器的软编软解性能和真机差距巨大而且很多MediaCodec行为在模拟器上跟真机不一致转码类项目必须真机验证。4. 常见问题与排查技巧实录4.1 环境构建期的常见坑Gradle sync卡住是新手遇到最多的问题。如果项目依赖下载进度一直不动先检查网络环境再确认Gradle JDK版本是否和AGP匹配。这里不涉及任何加速手段官方渠道正常下载即可多等一会儿也好过用不靠谱的第三方源。资源重复错误也是一个高频问题现象通常是Android Studio提示“duplicate resources”或者“resource linking failed”。绝大多数原因是复制文件时同一个资源被带进了两个目录比如drawable和drawable-xxhdpi里放了同名文件或者项目迁移时把整个res目录重复引入。解决办法是把重复文件删干净不要在多个目录放同名资源。如果遇到Gradle版本和AGP版本不兼容报错信息一般会直接告诉你哪个版本不匹配按提示调整即可。常用的对应关系是AGP 8.x对应Gradle 8.xAGP 7.4对应Gradle 7.5以上。4.2 转码运行期典型故障这个部分全是血泪教训我整理成一个排查表。现象可能原因排查方向MediaCodec.configure抛异常目标格式不兼容或分辨率不合法检查设备MediaCodecList确认是否支持目标编码器转码后黑屏Surface链路断开或addTrack时序错误确认encoderInputSurface没有提前释放画面旋转90度源视频的rotation标记未处理用MediaMetadataRetriever读取旋转角度并做矩阵变换花屏绿屏时间戳错乱或Buffer复用错误确认queueInputBuffer的presentationTimeUs按样本时间填写转换后没有声音音频轨道没有拷贝或拷贝失败检查AudioRemuxer是否执行输出文件是否混入音频轨文件大小异常巨大码率参数设置错误换算目标码率1080p建议不超过8MbpsOOM崩溃多路Buffer同时占用内存降低并发Buffer数量检查输入分辨率是否过大先讲黑屏问题。Surface链路最怕encoderInputSurface被意外释放。在转码循环还没有结束信号时绝对不要在finally块里提前release这个Surface。另外addTrack的时机必须和本篇前面说的一致在第一个CODEC_CONFIG buffer出现时处理。再讲旋转问题。很多手机录的视频在容器里存了rotation元数据播放器会根据旋转信息自动转正画面但转码输出可能丢掉这个标记导致画面横竖颠倒。处理办法是读取rotation后在解码器到编码器的Surface链路上做矩阵变换一般用SurfaceView或者OpenGL完成。最简单粗暴的方案是直接用MediaExtractor读取KEY_ROTATION然后把旋转角度对应的变换应用到Surface上。这块代码有一定复杂度要在真正处理竖屏视频时加进来。无声是最容易漏掉的需求。很多人以为视频转换只用处理视频轨结果转出来画面正常但没有声音。这个项目里的AudioRemuxer专门处理音频把源文件的音频轨道抽取出来不转码直接写入muxer因为MediaMuxer支持同时写入多条轨道只要在addTrack后把音频轨道也add进去后续writeSampleData时让音频数据走自己的trackIndex就行。如果源音频编码和目标容器不兼容比如MKV里的DTS音频要进MP4那就得考虑音频转码这属于进阶扩展。4.3 性能优化与内存治理转码性能有几个值得优化的地方。第一分辨率设置必须是偶数。几乎所有硬件编码器都要求视频宽高为偶数否则configure会报错或者编码出花屏。缩放时按源比例计算取整数后再修正到偶数。第二码率不要盲目拉高。1080p的视频用AVC编码6Mbps已经相当清楚用HEVC只需要3到4Mbps。码率设得过高不但文件膨胀编码器反而因为数据量大而拖慢速度。第三Surface链路已经省掉了内存拷贝但如果你在某些老设备上发现编码器不支持Surface输入只能退回ByteBuffer模式时要注意YUV格式转换的耗时这时甚至可以提示用户该设备不支持Surface转码。内存治理方面最重要的原则是别创建过多的编解码器实例。每次转码创建一次decoder和encoder用完立即释放不要在循环里重复创建。另外MediaCodec内部申请的Buffer是重量级资源OOM经常不是因为Java堆不够而是因为底层图形内存被耗光。所以转码前检查输入视频分辨率如果超过硬件能力先做降采样处理比等到OOM再抢救靠谱得多。5. 二次开发与扩展方向5.1 源码改造增加更多格式支持这套源码默认输出H.264AAC封装的MP4文件因为你把targetMime换成video/avc就能得到H.264换成video/hevc就是H.265换成video/x-vnd.on2.vp9就是VP9。编码器类型在运行时通过MediaCodecList查询如果设备不支持目标编码器会自动降级或提示用户。以此为基础最简单的扩展是把界面里写死的转码参数改成用户可配置项分辨率、码率、帧率、编码格式各来一个下拉框或输入框TranscoderManager把参数透传给VideoTranscoder即可。更进一步的扩展是加批量转换队列用一个任务队列管理多个文件逐个转换再把进度汇总展示。批量场景下需要处理文件选择的多选模式用OpenMultipleDocuments实现这就不展开了。5.2 从Java到Kotlin的迁移感受这个项目之所以坚持用Java写是因为它的核心价值在于教学和二次开发Java的参考资料最多遇到问题更容易搜到解决方案。Java代码对新手也更友好方法签名一目了然不像Kotlin的扩展函数、协程、data class那一套上手门槛高。如果你后续想迁移到Kotlin我的建议是保留VideoTranscoder和TranscoderManager的核心逻辑不动先把Activity层和工具类改成Kotlin观察迁移过程对现有逻辑的影响。MediaCodec三层API是Java标准的Kotlin调用没有任何问题协程可以很好地替代现成的线程池代码会更简洁。但不要为了迁移而迁移项目逻辑稳定之后再动手收益才大。5.3 进阶接入FFmpeg的注意事项当业务需求超出原生API能力时比如输出MKV、FLV容器或者要做视频拼接、滤镜、抽帧就该考虑接入FFmpeg了。我建议用现成的封装库而不是自己编译全套FFmpeg因为编译FFmpeg涉及NDK版本、工具链、编译参数稍有不慎就浪费一周时间。接入FFmpeg时注意三点一是模块裁剪只编译需要的解码器、编码器和复用器比如只留libavcodec、libavformat、libswscale和必要的filter其他都关掉能把so库体积控制在一半以内。二是abiFilters一定要设置不要四五个ABI全量打进一个APK。三是JNI层的String和byte[]转换要处理正确FFmpeg很多函数接收char指针和长度Java层传入的byte[]需要转换成C字符串编码集问题最容易导致路径中含中文的文件转码失败。我个人在实际开发中的体会是如果只是做基础的格式转换MediaCodec这套方案已经完全够用而且稳定性更容易掌控。真的需要FFmpeg的时候往往意味着产品已经从“格式转换”走向了“视频处理工具”这时候再引入也不迟。最后再分享一个小技巧无论是MediaCodec还是FFmpeg永远先用一个短视频、一套标准参数跑通全流程再加复杂功能。视频转码是典型的“链路长、状态多”的任务一次性写完再debug远没有“每个环节写一段、跑一段”来得高效。我在做这个项目的过程中反复用这个策略每次改动都只动一个变量出了问题能立刻定位。这个习惯帮我省下了大量排查时间后续做其他多媒体相关项目时也一直在用希望也能帮到你。
返回列表