本文首发于微信公众号——世界上有意思的事,搬运转载请注明出处,不然将追究版权责任。微信号:a1018998632,交流qq群:859640274java
很久不见,最近加班比较多因此第二篇音视频方面的文章 delay 了一周,你们多包涵哈。本文预计阅读时间二十分钟。linux
本文分为如下章节,读者能够按需阅读android
注意事项:c++
拿到一个项目,咱们通常有两种方式可使用它:一个是使用它编译打包后的产物,一个是本身引用他的项目集成到本身的项目中。咱们在这一章就来说讲如何食用 FFmpeg 的源码,将咱们的代码写入 FFmpeg项目中,而后编译到 android 项目中。 FFmpeg-learing,强烈建议你们依照项目代码进行文章的阅读。git
-----代码块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
复制代码
本小节咱们来聊聊怎么修改 FFmpeg 的源码,而后自动化的在咱们的 android 项目中编译和打包。程序员
在Clion 中编辑 FFmpeg 源码:github
----代码块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");shell
6.如今能够修改源码了,也有脚本能编译源码了,一个简单的将 so 文件引入 android 项目的方法就是手动编译而后拷贝 so 文件到 android 项目中。但咱们是程序员,咱们须要方便一点的方式来构建这个流程。编程
----代码块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 的官方 demo。几周过去了,目前项目中已经有五个移植成功的官方 demo了,并且都是能够运行的。因此这一章我就来分析解码 demo。为最后一章写一个简单的 android 视频播放器打基础。
FFmpeg-learing:本章示例项目。
从零开始仿写一个抖音App——音视频开篇:上一篇文章。
-----代码块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);
}
复制代码
----代码块8,本文发自简书、掘金:什么时候夕-----
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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 解码的极简视频播放器。
----代码块9,本文发自简书、掘金:什么时候夕-----
extern "C"
{
#include <android/native_window.h>
#include <android/native_window_jni.h>
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
};
#include <sys/time.h>
#include <unistd.h>
#include <pthread.h>
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;
}
复制代码
又是一篇文章结尾,最近公司加班太多了,不少计划都没有如期进行,但愿过了这个月会好一点。不须要打赏,只但愿你们能多评论点赞关注,也算是对个人支持和鼓励。下篇文章见!
不贩卖焦虑,也不标题党。分享一些这个世界上有意思的事情。题材包括且不限于:科幻、科学、科技、互联网、程序员、计算机编程。下面是个人微信公众号:世界上有意思的事,干货多多等你来看。