FFmpeg有七个模块,在我们的使用中会被编译成七个so库文件,其中最重要的就是avformat、avcodec和avutil。
其中avformat处理的是对音视频的解封装相关,视频通常至少包含两个数据,音频流和视频流,然后这两个数据会被封装成一个整体,即视频文件,如常见的mp4、flv等。而avformat就是处理这些文件的,它能够将其解封装,拆解出对应的音频流和视频流。当然反过来它也可以将音频流和视频流再重新封装,封装成不同的格式。
解封装后的音频流和视频流还是不能直接播放的,因为它们都是压缩后的。原始的音频流和视频流都是非常大的,直接存储会占用大量的存储空间,因此会将其进行压缩,也称为编码。音频流常用编码aac等,视频流常见编码h264等,想要播放音频或视频,就必须将其解码成原始数据,如音频流的pcm数据,视频流的yuv数据等。而avcodec就是用来解码的模块,当然返过来他也可以对其进行编码。

而avutil就是工具类模块,其中封装了大量的常用的函数,基本上每个模块都会用到它。
封装&解封装
封装和解封装是针对视频的,因为视频文件是多路流一起封装起来的。所谓的解封装就是将一个视频文件中的视频流和音频流拆解出来,而封装则是将视频流和音频流合并成一个完整的视频文件。封装和解封装的概念是针对于视频文件的,只有我们需要处理视频文件时,才需要考虑解封装。
FFmpeg处理视频文件的流程就是先打开文件,然后查找到对应的流信息,接着就是读取一个个的Packet,这些Packet就是流数据,不管是视频流还是音频流,都会被读成AVPacket。如果仅仅是为了解封装,其实到这里我们就已经做到了,我们完全可以将这些Packet重新写入到另一个封装容器中,从而实现换格式的场景。
大概的流程如下:

实际上看到解封装和重新封装的流程很简单,就是不断的读packet,再不断地写入到新文件中即可,实际上的编码也是一样的简单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| extern "C" { #include “libavformat/avformat.h” }
AVFormatContext *inFormatCtx = nullptr, *outFormatCtx = nullptr; AVPacket *packet = nullptr;
void free() { if(inFormatCtx != nullptr) { avformat_close_input(&inFormatCtx); inFormatCtx = nullptr; } if(outFormatCtx != nullptr) { avformat_free_context(outFormatCtx); outFormatCtx = nullptr; } if(packet != nullptr) { av_packet_free(&packet); packet = nullptr; } }
int convert(char* inputFile, char* outputFile) { int ret; ret = avformat_open_input(&inFormatCtx, inputFile, nullptr, nullptr); if(ret) { printf("error in open stream"); free(); return -1; } ret = avformat_find_stream_info(inFormatCtx, nullptr); if(ret < 0) { printf("find stream error"); free(); return -1; } ret = avformat_alloc_output_context2(&outFormatCtx, nullptr, nullptr, outputFile); if(ret < 0) { printf("error in open output"); free(); return -1; } for(int i = 0; i < inFormatCtx->nb_streams; i++) { AVStream *stream = inFormatCtx->streams[i]; AVStream *out = avformat_new_stream(outFormatCtx, nullptr); avcodec_parameters_copy(out->codecpar, stream->codecpar); } ret = avio_open2(&outFormatCtx->pb, outputFile, AVIO_FLAG_WRITE, nullptr, nullptr); if(ret < 0) { printf("error in open AVIO"); free(); return -1; } packet = av_packet_alloc(); ret = avformat_write_header(outFormatCtx, nullptr); while (true) { ret = av_read_frame(inFormatCtx, packet); if(ret < 0) { printf("end of file"); break; } int index = packet->stream_index; AVStream *in = inFormatCtx->streams[index]; AVStream *out = outFormatCtx->streams[index]; av_packet_rescale_ts(packet, in->time_base, out->time_base); av_interleaved_write_frame(outFormatCtx, packet); } av_write_trailer(outFormatCtx); free(); return 0; }
|
过程并没有什么复杂的,按照流程一步一步来即可。然后是在Android中使用,其实方法实现了,我们只需要在jni中使用即可:
1 2 3 4 5 6 7 8 9 10 11 12 13
| extern "C" JNIEXPORT void JNICALL Java_com_example_ffmpegdemo_MainActivity_convert( JNIEnv* env, jobject , jstring input, jstring output) {
const char* in = env->GetStringUTFChars(input, nullptr); const char* out = env->GetStringUTFChars(output, nullptr); convert(in, out); env->ReleaseStringUTFChars(input, in); env->ReleaseStringUTFChars(output, out); }
|
然后就是在MainActivity中调用即可:
1 2 3 4 5 6 7 8 9 10
| binding.button.setOnClickListener { thread { val inputFile = File(cacheDir, "1.mkv") val outputFile = File(cacheDir, "2.mp4") convert(inputFile.absolutePath, outputFile.absolutePath) } }
|
然后找一个mkv视频文件,重命名为1.mkv,然后push到应用的缓存目录中,/data/data/com.example.ffmpegdemo/files/cache目录下,再运行即可。
总结
视频封装就是将音频流、视频流等封装成一个文件,根据其封装格式其内部有不同的结构,可能是一段音频一段视频这样交错组织,也可能是以别的模式组织。而解封装的意义就是将这些组织在一块的音频和视频分开,我们通过FFmpeg读取到的AVPacket就代表着这样的一段音频或者视频数据,注意的是,这些数据仍是压缩后的数据,想要播放的话还需要对这些数据进行解码才行。