ffmpeg|从零开始仿写一个抖音App——基于FFmpeg的极简视频播放器

本文首发于微信公众号——世界上有意思的事,搬运转载请注明出处,否则将追究版权责任。微信号:a1018998632,交流qq群:859640274

  • 1.从零开始仿写一个抖音app——开始
  • 4.从零开始仿写一个抖音App——日志和埋点以及后端初步架构
  • 5.从零开始仿写一个抖音App——app架构更新与网络层定制
  • 6.从零开始仿写一个抖音App——音视频开篇
  • 8.从零开始仿写一个抖音App——跨平台视频编辑SDK项目搭建
GitHub地址
好久不见,最近加班比较多所以第二篇音视频方面的文章 delay 了一周,大家多包涵哈。本文预计阅读时间二十分钟。
本文分为以下章节,读者可以按需阅读
  • 1.FFmpeg源码食用——Clion中编译、修改、引用FFmpeg源码
  • 2.FFmpeg Api食用——FFmpeg 数据结构以及官方 demo 解析
  • 3.极简视频播放器——写一个基于 FFmpeg 的极简 Android 视频播放器
一、FFmpeg源码食用 注意事项:
  • 1.需要一些 git 的知识,git中文文档。
  • 2.我的FFmpeg:我 fork 的 FFmpeg 项目,源码的编译已经完成,编译的 shell 脚本在根目录下。
  • 3.FFmpeg-learing:本文章的示例代码
  • 4.下面代码块中,使用 -----代码块x,本文发自简书、掘金:何时夕----- 来区分各个代码块,该文字不属于代码的一部分
  • 5.下面使用 project 指代 clone 下来的 FFmpeg 项目的路径。
  • 6.下面的操作都是基于 Mac 平台,linux 平台应该也能顺利运行,win 平台的话笔者实在没时间去折腾(靠你们自己啦)。
  • 7.开始前需要安装一些前置软件:Clion(百度)、make(mac 可以用 brew 装、linux 可以用 apt 装)
1.开始
拿到一个项目,我们一般有两种方式可以使用它:一个是使用它编译打包后的产物,一个是自己引用他的项目集成到自己的项目中。我们在这一章就来讲讲如何食用 FFmpeg 的源码,将我们的代码写入 FFmpeg项目中,然后编译到 android 项目中。 FFmpeg-learing,强烈建议大家依照项目代码进行文章的阅读。
  • 1.首先将 FFmpeg官方项目 fork 到我们自己的 github 上,以便以后对这个项目的修改。
  • 2.clone 自己的 FFmpeg 项目到电脑上。
  • 3.以后我的代码修改和编译会基于 FFmpeg 3.3.8 这个版本(这个版本好编译一点),所以我们需要新建一个分支 local_build_base_on_3.3.8。然后使用 git reset --hard 18c9d5d3e80dc0b47e0a260b51f5230bdd499e8b 来到 FFmpeg 的 tag 为 n3.3.8 这个 commit 上。
  • 4.现在我们就可以开始编译代码了。编译的流程网上很多,我就简单说一下。
    • 1.将 project/configure 文件中 3305-3308行,这四行代码换成代码块1中的代码。
    • 2.将代码块2中的代码保存为 project/build_android.sh 文件,然后执行 ./build_android.sh 命令。
-----代码块1,本文发自简书、掘金:何时夕-----# SLIBNAME_WITH_MAJOR='$(SLIBNAME).$(LIBMAJOR)' # LIB_INSTALL_EXTRA_CMD='$$(RANLIB) "$(LIBDIR)/$(LIBNAME)"' # SLIB_INSTALL_NAME='$(SLIBNAME_WITH_VERSION)' # SLIB_INSTALL_LINKS='$(SLIBNAME_WITH_MAJOR) $(SLIBNAME)'SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)' LIB_INSTALL_EXTRA_CMD='$$(RANLIB) "$(LIBDIR)/$(LIBNAME)"' SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)' SLIB_INSTALL_LINKS='$(SLIBNAME)' 复制代码

-----代码块2,本文发自简书、掘金:何时夕-----#!/bin/bash # 切换到 FFmpeg 的目录 cd /Users/whensunset/AndroidStudioProjects/KSVideoProject/ffmpeg# NDK的路径,根据自己的安装位置进行设置 export NDK=/Users/whensunset/AndroidStudioProjects/KSVideoProject/android-ndk-r14b export SYSROOT=$NDK/platforms/android-16/arch-arm/ export TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64 export CPU=arm# 配置编译后的产物放置路径 export PREFIX=$(pwd)/android/$CPU export ADDI_CFLAGS="-marm"# 创建一个方法,这个方法使用 configure 这个文件传入一些参数来对 FFmpeg 进行编译,可以使用 configure -help 命令来对参数进行了解 function build_one { ./configure \ --prefix=$PREFIX \ --target-os=linux \ --cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \ --arch=arm \ --sysroot=$SYSROOT \ --extra-cflags="-Os -fpic $ADDI_CFLAGS" \ --extra-ldflags="$ADDI_LDFLAGS" \ --cc=$TOOLCHAIN/bin/arm-linux-androideabi-gcc \ --nm=$TOOLCHAIN/bin/arm-linux-androideabi-nm \ --enable-shared \ --enable-runtime-cpudetect \ --enable-gpl \ --enable-small \ --enable-cross-compile \ --disable-debug \ --disable-static \ --disable-doc \ --disable-asm \ --disable-ffmpeg \ --disable-ffplay \ --disable-ffprobe \ --disable-ffserver \ --enable-postproc \ --enable-avdevice \ --disable-symver \ --disable-stripping \ $ADDITIONAL_CONFIGURE_FLAG sed -i '' 's/HAVE_LRINT 0/HAVE_LRINT 1/g' config.h sed -i '' 's/HAVE_LRINTF 0/HAVE_LRINTF 1/g' config.h sed -i '' 's/HAVE_ROUND 0/HAVE_ROUND 1/g' config.h sed -i '' 's/HAVE_ROUNDF 0/HAVE_ROUNDF 1/g' config.h sed -i '' 's/HAVE_TRUNC 0/HAVE_TRUNC 1/g' config.h sed -i '' 's/HAVE_TRUNCF 0/HAVE_TRUNCF 1/g' config.h sed -i '' 's/HAVE_CBRT 0/HAVE_CBRT 1/g' config.h sed -i '' 's/HAVE_RINT 0/HAVE_RINT 1/g' config.h make clean make -j8 make install }## 运行前面创建的编译 FFmpeg 的方法 build_one 复制代码

  • 5.不出意外的话,我们会在 project/android/arm 看见了 include 和 lib这两个文件夹。
    • 1.include:了解 c/c++ 的同学知道,include 文件是 c/c++ 的接口定义文件,可以比作 java 中的接口,用来将内部 api 暴露给外部。
    • 2.lib:这里里面就是 android 中可以使用的 so 文件了。
    • 3.我们可以根据 include 文件中提供的函数定义,来调用 so 文件中被暴露到外部的 api。
  • 6.上面就是我们整个 FFmpeg 的编译过程。
2.修改FFmpeg源码
本小节我们来聊聊怎么修改 FFmpeg 的源码,然后自动化的在我们的 android 项目中编译和打包。
在Clion 中编辑 FFmpeg 源码:
  • 1.首先我们在上面一节已经得 FFmpeg 的源码了,此时我们需要打开 Clion,然后点击 import project from sources 选择 project 文件夹,按 Clion 的默认设置将源码导入。
  • 2.这个时候我们会看见 Clion 会自动生成 CmakeLists.txt 的文件,里面引入了源码中所有可编译的文件。
  • 3.为了有一个干净的 git 项目,所以需要在 .gitignore 里面加上一些文件的过滤。如代码块3
----代码块3,本文发自简书、掘金:何时夕-----*.version *.ptx *.ptx.c /config.asm /config.h .idea /.idea /cmake-build-debug /android *.log 复制代码

  • 4.导入完成之后,大家会发现很多文件里面会报红,然后一些被 include 的头文件都找不到。这个是正常现象,因为我们有专门的脚本来编译代码,Clion只是作为一个编辑器来使用,所以报红的地方不影响我们接下来的操作。如果你实在看不顺眼的话,可以尝试用 Clion 的 Auto Import 快捷键来看见一个就纠正一个。
  • 5.现在我们就能愉快的编辑 FFmpeg 的源码了。我们在 project/libavcodec/allcodecs.c/avcodec_register_all 这个方法里面加一行初学者的标配 av_log(NULL, AV_LOG_DEBUG, "hello world");
  • 6.现在可以修改源码了,也有脚本能编译源码了,一个简单的将 so 文件引入 android 项目的方法就是手动编译然后拷贝 so 文件到 android 项目中。但我们是程序员,我们需要方便一点的方式来构建这个流程。
    • 1.首先我们在 从零开始仿写一个抖音App——音视频开篇 这篇文章中介绍了怎样将 so 文件引入 android 项目然后在 jni 层调用,这里我就不一一赘述了。
    • 2.那么此时我们只需要在我们需要的时候编译 FFmpeg 的源码,然后将生成的 so 文件替换老的 so 文件就行了。如代码块4
    • 3.现在有了自动编译拷贝的脚本了,我们需要将这个脚本在 gradle 编译项目的时候运行。如代码块5,我们将里面的代码放到 app moudle 的 build.gradle 文件中。
    • 4.现在只要点击一下 run,就会发现 Gradle Console 里面会输出 FFmpeg 编译时的输出 log。至此我们就能愉快的修改和使用 FFmpeg 的源码了。
    ----代码块4,本文发自简书、掘金:何时夕-----#!/usr/bin/env bash # exit 不注释的时候,表示 android 项目编译的时候不需要编译 ffmepg,注释的时候,表示 android 项目编译的时候要编译 ffmpeg。 # exit# 执行 FFmpeg 源码项目中的编译脚本 sh /Users/whensunset/AndroidStudioProjects/KSVideoProject/ffmpeg/build_android.sh# 当前项目的 so 文件的存放目录,需要改成自己的 so_path="/Users/whensunset/AndroidStudioProjects/KSVideoProject/FFmpeglearning/app/src/main/jni/ffmpeg/armeabi/"# 所有 so 文件编译生成后的默认命名 libavcodec_name="libavcodec-57.so" libavdeivce_name="libavdevice-57.so" libavfilter_name="libavfilter-6.so" libavformat_name="libavformat-57.so" libavutil_name="libavutil-55.so" libpostproc_name="libpostproc-54.so" libswresample_name="libswresample-2.so" libseacale_name="libswscale-4.so"# 删除当前项目中的老的 so 文件删除 rm ${so_path}${libavcodec_name} rm ${so_path}${libavdeivce_name} rm ${so_path}${libavfilter_name} rm ${so_path}${libavformat_name} rm ${so_path}${libavutil_name} rm ${so_path}${libpostproc_name} rm ${so_path}${libswresample_name} rm ${so_path}${libseacale_name}# FFmpeg 源码项目中,编译好的 so 文件的路径,需要改成自己的 build_so_path="/Users/whensunset/AndroidStudioProjects/KSVideoProject/ffmpeg/android/arm/lib/"# 将新编译的 so 文件拷贝到当前项目的 so 目录下 cd /Users/whensunset/AndroidStudioProjects/KSVideoProject/FFmpeglearning/app cp ${build_so_path}${libavcodec_name} ${so_path}${libavcodec_name} cp ${build_so_path}${libavdeivce_name} ${so_path}${libavdeivce_name} cp ${build_so_path}${libavfilter_name} ${so_path}${libavfilter_name} cp ${build_so_path}${libavformat_name} ${so_path}${libavformat_name} cp ${build_so_path}${libavutil_name} ${so_path}${libavutil_name} cp ${build_so_path}${libpostproc_name} ${so_path}${libpostproc_name} cp ${build_so_path}${libswresample_name} ${so_path}${libswresample_name} cp ${build_so_path}${libseacale_name} ${so_path}${libseacale_name} 复制代码

----代码块5,本文发自简书、掘金:何时夕----- // 创建一个 build_ffmpeg 的 task,其负责运行shell 脚本 task build_ffmpeg { doLast { exec { commandLine 'sh', '/Users/whensunset/AndroidStudioProjects/KSVideoProject/FFmpeglearning/app/build_ffmpeg.sh' } } }// 将 build_ffmpeg 这个 task 作为编译的前置任务来执行。 tasks.whenTaskAdded { task -> task.dependsOn 'build_ffmpeg' }复制代码

二、FFmpeg 解码
上篇文章中我们简单分析了一个 FFmpeg 的官方 demo。几周过去了,目前项目中已经有五个移植成功的官方 demo了,而且都是可以运行的。所以这一章我就来分析解码 demo。为最后一章写一个简单的 android 视频播放器打基础。
FFmpeg-learing:本章示例项目。
从零开始仿写一个抖音App——音视频开篇:上一篇文章。
1.开始
  • 1.首先项目比较简单,入口是 MainActivity,里面有很多按钮,每一个功能都由一个按钮触发。
  • 2.点击按钮之后,会开启一个线程来执行相应的代码,这里的代码最终会进入到 c++ 代码中使用 FFmpeg 的 Api 来进行视频文件的处理。
  • 3.FFmpegPlayer 这个 java 类是用来调用 c++ 代码的类。
  • 4.player.cpp 是 native 代码的入口。
  • 5.同学们应该还没忘记上一章中我们在 FFmpeg 中添加的 log 吧。可能有些人会问,那个 log 到底在哪里可以看见呢?在 c/c++ 中会有一个标准输出流的概念,Ffmpeg 的 log 都是向标准输出流中输出的,这个标准输出流一般会向控制台之类的东西里面上面打印数据,我们可以将这里 log 的输出流重定向到 android 的日志里面,这样我们就能在 Android Studio 中的 Logcat 里面看见它了。
    • 1.首先大家看 player.cpp 文件中有代码块6中的代码,这里我们先定义了两个宏,宏里面分别是 ndk 中提供的 android 的日志打印方法,我们将日志的 TAG 设置为 “FFmpeg”。后面我们只需要在 AS 的控制台中过滤这个字段就能看见 FFmpeg 内部输出的日志了。
    • 2.然后我们定义了一个方法,这个方法我们期望能在 FFmpeg 打印 log 之后调用,然后将 FFmpeg 打印的 log 交给这个方法,从而将 log 输出到 android 的日志中。
    • 3.再看代码块7,这个代码在 player.cpp 中,这里 FFmpeg 提供了 av_log_set_callback 方法,他会将我们刚刚定义的方法作为一个函数指针传入 FFmpeg 中进行持有,只要 FFmpeg 进行了 log 调用,那么就会触发我们在2中定义的方法,从而将 FFmpeg 的日志输出流,重定向到我们的 android 日志系统中。
    • 4.当然我们需要在 FFmpegPlayer 中定义 native 方法,然后在 MainActivity 中进行初始化调用。
-----代码块6,本文发自简书、掘金:何时夕----------- #ifndef LOG_TAG #defineLOG_TAG"FFMPEG" #endif#defineXLOGD(...)__android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__) #defineXLOGE(...)__android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)static void log_callback_null(void *ptr, int level, const char *fmt, va_list vl) { static int print_prefix = 1; static char prev[1024]; char line[1024]; av_log_format_line(ptr, level, fmt, vl, line, sizeof(line), &print_prefix); strcpy(prev, line); if (level <= AV_LOG_WARNING) { XLOGE("%s", line); } else { XLOGD("%s", line); } }复制代码

-----代码块7,本文发自简书、掘金:何时夕----------- extern "C" JNIEXPORT void JNICALL Java_com_example_whensunset_ffmpeg_1learning_FFmpegPlayer_initFfmpegLog(JNIEnv *env, jobject instance) { av_log_set_callback(log_callback_null); } 复制代码

2.解码
  • 1.下面的代码就是解码的代码,大家可以在示例项目中找到 FFMPEG_纯净的解码器 按钮点击触发这个功能。
  • 2.注意在运行之前需要在将示例项目中的 c.mpeg4 文件拷贝到手机中的 **/storage/emulated/0/av_test/ **这个目录下。
  • 3.有个前提知识我们需要了解,一个 MP4 文件解析到屏幕上需要下面这些步骤:
    • 1.解封装:解析 Mp4 文件的结构,然后读取文件中的数据流。
    • 2.解码:1中的数据流是经过编码算法压缩的,一般有 h264、mpeg4等等编码方式。这一步需要将数据流的每一帧都解码成类似图片的形式。
    • 3.显示:将2中解码出来的图像绘制到屏幕上。
  • 4.下面的代码主要用途是将我们传入的 c.mpeg4 文件直接解码成 c.yuv 这种原始图像数据,并没有解封装的过程。
----代码块8,本文发自简书、掘金:何时夕----- #include #include #include extern "C" { #include "libavcodec/avcodec.h" }#define INBUF_SIZE 4096static void pgm_save(unsigned char *buf, int wrap, int xsize, int ysize, const char *filename) { FILE *f; int i; f = fopen(filename, "w"); fprintf(f, "P5\n%d %d\n%d\n", xsize, ysize, 255); for (i = 0; i < ysize; i++) fwrite(buf + i * wrap, 1, xsize, f); fclose(f); }static int decode(AVCodecContext *dec_ctx, AVFrame *frame, AVPacket *pkt, const char *filename) { char buf[1024]; int ret; // 将一帧压缩图像传入解码器中 ret = avcodec_send_packet(dec_ctx, pkt); if (ret < 0) { return ret; }while (ret >= 0) { // 从解码器中取出刚刚传入的压缩图像被解码出来的图像,avcodec_send_packet 和 avcodec_receive_frame 一般是对应的。取出数据成功后,再去取时 ret 会小于0 ret = avcodec_receive_frame(dec_ctx, frame); if (ret < 0) { return ret; }av_log(NULL, AV_LOG_DEBUG, "saving frame %3d\n", dec_ctx->frame_number); fflush(stdout); /* the picture is allocated by the decoder. no need to free it */ snprintf(buf, sizeof(buf), "%s-%d", filename, dec_ctx->frame_number); // ........** // ........** // ........** // ........** // ........** // ........** // ........** // 如上所示,点就是我们平时看见的一帧图像,*是无用数据。一般来说:width指的是一行点的数量,height指的是一列点的数量,linesize[0]指的是 width + *的数量。 // data[0]中存放数据的方式则是这样:........**........**........**........**........**........**........**将一帧图像平铺。 // 最终我们存到文件中的数据就是这样:........ ........ ........ ........ ........ ........ ........ 中间的空格文件中不存在,只是为了好看一点 pgm_save(frame->data[0], frame->linesize[0], frame->width, frame->height, filename); } return 0; }char *decode_video(char **argv) { const char *filename, *outfilename; const AVCodec *codec; AVCodecParserContext *parser; AVCodecContext *c = NULL; FILE *f; AVFrame *frame; uint8_t inbuf[INBUF_SIZE + AV_INPUT_BUFFER_PADDING_SIZE]; uint8_t *data; size_t data_size; int ret; AVPacket *pkt; // 输入和输出文件的名称,输入文件是 c.mpeg4,输出文件是 c.yuv。 filename = argv[0]; outfilename = argv[1]; // 注册所有的编解码器 avcodec_register_all(); // 为 AVPacket 进行初始化,AVPacket 用于一帧压缩后的图像的数据结构 pkt = av_packet_alloc(); if (!pkt) exit(1); // 将 inbuf 从 INBUF_SIZE 到INBUF_SIZE + AV_INPUT_BUFFER_PADDING_SIZE 这一段的数据都设置为0(这确保了对损坏的MPEG流不会发生过读) /* set end of buffer to 0 (this ensures that no overreading happens for damaged MPEG streams) */ memset(inbuf + INBUF_SIZE, 0, AV_INPUT_BUFFER_PADDING_SIZE); // 根据名称来查找某个编解码器,这里我们使用输入文件的编解码器 mpeg4 codec = avcodec_find_decoder_by_name("mpeg4"); if (!codec) { ret = -1111; goto end; }// 根据编解码器的id,来找到一个 解析器,这个解析器可以用来解析出 mpeg4 文件流中的一帧压缩后的数据 parser = av_parser_init(codec->id); if (!parser) { ret = -1112; goto end; }// 根据编解码器初始化 编码器的上下文 数据结构。 c = avcodec_alloc_context3(codec); if (!c) { ret = -1113; goto end; }// 打来编解码器 if ((ret = avcodec_open2(c, codec, NULL)) < 0) { goto end; }// 打开文件 f = fopen(filename, "rb"); if (!f) { ret = -1114; goto end; }// 初始化 AV_Frame 这个数据结构,它是用来储存一帧解码后的图像的数据结构 frame = av_frame_alloc(); if (!frame) { ret = -1115; goto end; }// 一直循环,直到输入文件被读到了最后 while (!feof(f)) { // 从原文件中读取4096个字节 data_size = fread(inbuf, 1, INBUF_SIZE, f); if (!data_size) break; // 4096 的字节中可能会包含多帧压缩后的图像,所以这里每次解析出一帧压缩图像数据,然后解码成一帧解码后图像数据,然后再循环,直至4096个字节被读取完毕。 data = https://www.it610.com/article/inbuf; while (data_size> 0) { // 从4096个字节中以 data 作为起点,解析出一帧压缩图像数据到 AV_Packet 中。返回值是压缩帧的byte大小 if ((ret = av_parser_parse2(parser, c, &pkt->data, &pkt->size, data, data_size, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0)) < 0) { goto end; } // 将 data 移动到新的起点 data += ret; // 记录 4096 字节中剩下的可用字节大小 data_size -= ret; // 如果 size 大于0表示刚刚读取数据成功 if (pkt->size) { // 将一个 pkt 包解析成一个 frame decode(c, frame, pkt, outfilename); }} }/* flush the decoder */ decode(c, frame, NULL, outfilename); fclose(f); end: av_parser_close(parser); avcodec_free_context(&c); av_frame_free(&frame); av_packet_free(&pkt); if (ret < 0) { char buf2[500] = {0}; if (ret == -1111) { return (char *) "codec not found"; } else if (ret == -1112) { return (char *) "parser not found"; } else if (ret == -1113) { return (char *) "could not allocate video codec context"; } else if (ret == -1114) { return (char *) "could not open input file"; } else if (ret == -1115) { return (char *) "could not allocate video frame"; } av_strerror(ret, buf2, 1024); return buf2; } else { return (char *) "解码成功"; } } 复制代码

三、极简视频播放器
最后一章就来介绍一个用 FFmpeg 解码的极简视频播放器。
  • 1.首先这个视频播放器非常简单,简单到啥也没有,只是将从文件中解码出来的图像绘制到 surface上面。
  • 2.示例程序的使用方法是:将需要播放的视频以 /storage/emulated/0/av_test/b.mp4,这个命名拷贝到手机中去。
  • 3.更多的信息,大家可以看代码,里面都有注释。写的有点累了,这篇文章就到这吧:)
----代码块9,本文发自简书、掘金:何时夕----- extern "C" { #include #include #include "libavcodec/avcodec.h" #include "libavformat/avformat.h" #include "libswscale/swscale.h" #include "libavutil/imgutils.h"}; #include #include #include static AVFormatContext *pFormatCtx; static AVCodecContext *pCodecCtx; static int video_stream_index = -1; static AVCodec *pCodec; static int64_t last_pts = AV_NOPTS_VALUE; static long getCurrentTime() { struct timeval tv; gettimeofday(&tv,NULL); return tv.tv_sec * 1000 + tv.tv_usec / 1000; }struct timeval now; struct timespec outtime; pthread_cond_t cond; pthread_mutex_t mutex; static void sleep(int nHm) { gettimeofday(&now, NULL); now.tv_usec += 1000 * nHm; if (now.tv_usec > 1000000) { now.tv_sec += now.tv_usec / 1000000; now.tv_usec %= 1000000; }outtime.tv_sec = now.tv_sec; outtime.tv_nsec = now.tv_usec * 1000; pthread_cond_timedwait(&cond, &mutex, &outtime); }static int open_input_file(const char *filename) {int ret; // 打开文件,确认文件的封装格式,然后将文件的信息写入 AVFormatContext 中 if ((ret = avformat_open_input(&pFormatCtx, filename, NULL, NULL)) < 0) { av_log(NULL, AV_LOG_ERROR, "Cannot open input file\n"); return ret; }// 从 AVFormatContext 中解析文件中的各种流的信息,比如音频流、视频流、字幕流等等 if ((ret = avformat_find_stream_info(pFormatCtx, NULL)) < 0) { av_log(NULL, AV_LOG_ERROR, "Cannot find stream information\n"); return ret; }// 找到根据传入参数,找到最适合的数据流,和该数据流的编解码器,这里传入 AVMEDIA_TYPE_VIDEO 表示需要找到视频流 ret = av_find_best_stream(pFormatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, &pCodec, 0); if (ret < 0) { av_log(NULL, AV_LOG_ERROR, "Cannot find a video stream in the input file\n"); return ret; } // 将找到的视频流,的 index 暂存 video_stream_index = ret; // 根据前面找到的视频流的编解码器,构造编解码器上下文 pCodecCtx = avcodec_alloc_context3(pCodec); if (!pCodecCtx) return AVERROR(ENOMEM); // 使用视频流的信息来编解码器上下文的参数 avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[video_stream_index]->codecpar); // 打开编解码器 if ((ret = avcodec_open2(pCodecCtx, pCodec, NULL)) < 0) { av_log(NULL, AV_LOG_ERROR, "Cannot open video decoder\n"); return ret; }return 0; }int play(JNIEnv *env, jobject surface) { int ret; char filepath[] = "/storage/emulated/0/av_test/b.mp4"; // 初始化 libavformat 然后 注册所有的 封装器,解封装器 和 协议。 av_register_all(); if (open_input_file(filepath) < 0) { av_log(NULL, AV_LOG_ERROR, "can not open file"); return 0; }// 初始化两个 储存解码后视频帧 的数据结构,pFrame 表示解码后的视频帧,pFrameRGBA 表示将 pFrame 转换成 RGBA 格式的 视频帧 AVFrame *pFrame = av_frame_alloc(); AVFrame *pFrameRGBA = av_frame_alloc(); // 计算格式为 RGBA 的视频帧的 byte 大小,视频帧的长和宽在解封装的时候就确定了 int numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGBA, pCodecCtx->width, pCodecCtx->height, 1); // 初始化一块内存,内存大小就是 格式为 RGBA 的视频帧的大小 uint8_t *buffer = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t)); // 填充 buffer av_image_fill_arrays(pFrameRGBA->data, pFrameRGBA->linesize, buffer, AV_PIX_FMT_RGBA, pCodecCtx->width, pCodecCtx->height, 1); // 由于解码出来的帧格式不是RGBA的,在渲染之前需要进行格式转换 struct SwsContext *sws_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_RGBA, SWS_BILINEAR, NULL, NULL, NULL); // 获取native window,即surface ANativeWindow *nativeWindow = ANativeWindow_fromSurface(env, surface); // 获取视频宽高 int videoWidth = pCodecCtx->width; int videoHeight = pCodecCtx->height; // 设置native window的buffer大小,可自动拉伸 ANativeWindow_setBuffersGeometry(nativeWindow, videoWidth, videoHeight, WINDOW_FORMAT_RGBA_8888); ANativeWindow_Buffer windowBuffer; av_dump_format(pFormatCtx, 0, filepath, 0); // 初始化 压缩视频帧 的数据结构 AVPacket *packet = (AVPacket *) av_malloc(sizeof(AVPacket)); while (1) { long start_time = getCurrentTime(); // 从视频流中读取出一帧 压缩帧 if ((ret = av_read_frame(pFormatCtx, packet)) < 0) { av_log(NULL, AV_LOG_DEBUG, "can not read frame"); break; }// 如果 压缩帧 是从是 视频流中读出来的,那么就可以被解码 if (packet->stream_index == video_stream_index) { // 解码 ret = avcodec_send_packet(pCodecCtx, packet); if (ret < 0) { av_log(NULL, AV_LOG_ERROR, "Error while sending a packet to the decoder\n"); break; }while (ret >= 0) { // 解码 ret = avcodec_receive_frame(pCodecCtx, pFrame); if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { break; } else if (ret < 0) { av_log(NULL, AV_LOG_ERROR, "Error while receiving a frame from the decoder\n"); }ANativeWindow_lock(nativeWindow, &windowBuffer, 0); // 将 YUV 格式的数据转换为 RGBA 格式的数据 sws_scale(sws_ctx, (uint8_t const *const *) pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameRGBA->data, pFrameRGBA->linesize); // 获取stride uint8_t *dst = (uint8_t *) windowBuffer.bits; int dstStride = windowBuffer.stride * 4; uint8_t *src = https://www.it610.com/article/pFrameRGBA->data[0]; int srcStride = pFrameRGBA->linesize[0]; // 由于window的stride和帧的stride不同,因此需要逐行复制,逐行将图像帧的数据拷贝到 Surface 的缓冲流中。 int h; for (h = 0; h < videoHeight; h++) { memcpy(dst + h * dstStride, src + h * srcStride, srcStride); }// 为了保持 40毫秒一帧,如果解码时间很快,那么就 sleep一会儿 int sleep_time = 40 - (getCurrentTime() - start_time); if (sleep_time > 0) { sleep(sleep_time); }ANativeWindow_unlockAndPost(nativeWindow); } }av_packet_unref(packet); }if (sws_ctx) sws_freeContext(sws_ctx); av_frame_free(&pFrameRGBA); if (pFrame) av_frame_free(&pFrame); if (pCodecCtx) avcodec_close(pCodecCtx); if (pFormatCtx) avformat_close_input(&pFormatCtx); if (buffer) av_free(buffer); return 0; }复制代码

四、尾巴
又是一篇文章结尾,最近公司加班太多了,很多计划都没有如期进行,希望过了这个月会好一点。不需要打赏,只希望大家能多评论点赞关注,也算是对我的支持和鼓励。下篇文章见!
不贩卖焦虑,也不标题党。分享一些这个世界上有意思的事情。题材包括且不限于:科幻、科学、科技、互联网、程序员、计算机编程。下面是我的微信公众号:世界上有意思的事,干货多多等你来看。
【ffmpeg|从零开始仿写一个抖音App——基于FFmpeg的极简视频播放器】

    推荐阅读