本文首发于微信公众号——世界上有意思的事,搬运转载请注明出处,否则将追究版权责任。微信号:a1018998632,交流qq群:859640274
1.从零开始仿写一个抖音app——开始
4.从零开始仿写一个抖音App——日志和埋点以及后端初步架构
5.从零开始仿写一个抖音App——app架构更新与网络层定制
7.从零开始仿写一个抖音App——基于FFmpeg的极简视频播放器
8.从零开始仿写一个抖音App——跨平台视频编辑SDK项目搭建
9.从零开始仿写一个抖音App——Android绘制机制以及Surface家族源码全解析
好久不见,最近加班比较多所以第二篇音视频方面的文章 delay 了一周,大家多包涵哈。本文预计阅读时间二十分钟。
本文分为以下章节,读者可以按需阅读
1.FFmpeg源码食用——Clion中编译、修改、引用FFmpeg源码
2.FFmpeg Api食用——FFmpeg 数据结构以及官方 demo 解析
3.极简视频播放器——写一个基于 FFmpeg 的极简 Android 视频播放器
注意事项:
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 装)
拿到一个项目,我们一般有两种方式可以使用它:一个是使用它编译打包后的产物,一个是自己引用他的项目集成到自己的项目中。我们在这一章就来讲讲如何食用 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 的编译过程。
本小节我们来聊聊怎么修改 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/WebRTC/RTMP/NDK/Android音视频流媒体高级开发
【文章福利】:免费领取更多音视频学习资料包、大厂面试题、技术视频和学习路线图,资料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以点击1079654574加群领取哦~
上篇文章中我们简单分析了一个 FFmpeg 的官方 demo。几周过去了,目前项目中已经有五个移植成功的官方 demo了,而且都是可以运行的。所以这一章我就来分析解码 demo。为最后一章写一个简单的 android 视频播放器打基础。
FFmpeg-learing:本章示例项目。
从零开始仿写一个抖音App——音视频开篇:上一篇文章。
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
#define LOG_TAG "FFMPEG"
#endif
#define XLOGD(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define XLOGE(...) __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);
}
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 4096
static 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 = 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 = 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;
}
又是一篇文章结尾,最近公司加班太多了,很多计划都没有如期进行,希望过了这个月会好一点。不需要打赏,只希望大家能多评论点赞关注,也算是对我的支持和鼓励。下篇文章见!
不贩卖焦虑,也不标题党。分享一些这个世界上有意思的事情。题材包括且不限于:科幻、科学、科技、互联网、程序员、计算机编程。下面是我的微信公众号:世界上有意思的事,干货多多等你来看。
作者:何时夕 原文链接:从零开始仿写一个抖音App——基于FFmpeg的极简视频播放器 - 简书