1. FFmpeg视频解码

代码工程

1.1. 一、视频解码流程

1.1.1. 第一步:注册组件

av_register_all:例如:编码器、解码器等等。

// 第一步:注册组件
av_register_all();

1.1.2. 第二步:打开封装格式

avformat_open_input:例如:打开.mp4、.mov、.wmv文件等等。

// 第二步:打开封装格式
// 参数一:封装格式上下文
// 作用:保存整个视频信息(解码器、编码器等等...)
// 信息:码率、帧率等...
AVFormatContext* avformat_context = avformat_alloc_context();
// 参数二:视频路径
// 在我们iOS里面
// NSString* path = @"test.mov";
// const char *url = [path UTF8String]
const char *url = env->GetStringUTFChars(in_file_path, NULL);
// 参数三:指定输入的格式
// 参数四:设置默认参数
int avformat_open_input_result = avformat_open_input(&avformat_context, url, NULL, NULL);
if (avformat_open_input_result != 0){
    // 安卓平台下log
    __android_log_print(ANDROID_LOG_INFO, "main", "打开文件失败");
    // iOS平台下log
    // NSLog("打开文件失败");
    // 不同的平台替换不同平台log日志
    return;
}

1.1.3. 第三步:查找视频基本信息

avformat_find_stream_info:如果是视频解码,那么查找视频流,如果是音频解码,那么就查找音频流。

// 第三步:查找视频流,拿到视频信息
// 参数一:封装格式上下文
// 参数二:指定默认配置
int avformat_find_stream_info_result = avformat_find_stream_info(avformat_context, NULL);
if (avformat_find_stream_info_result < 0){
    __android_log_print(ANDROID_LOG_INFO, "main", "查找失败");
    return;
}

1.1.4. 第四步:查找视频解码器

avcodec_find_decoder:查找解码器。

1. 查找视频流索引位置

// 第四步:查找视频解码器
// 4.1 查找视频流索引位置
int av_stream_index = -1;
for (int i = 0; i < avformat_context->nb_streams; ++i) {
    // 判断流类型:视频流、音频流、字母流等等...
    if (avformat_context->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO){
        av_stream_index = i;
        break;
    }
}

2. 获取解码器上下文

根据视频流索引,获取解码器上下文。

// 4.2 根据视频流索引,获取解码器上下文
AVCodecContext *avcodec_context = avformat_context->streams[av_stream_index]->codec;

3. 获得解码器ID

根据解码器上下文,获得解码器ID,然后查找解码器。

// 4.3 根据解码器上下文,获得解码器ID,然后查找解码器
AVCodec *avcodec = avcodec_find_decoder(avcodec_context->codec_id);

1.1.5. 第五步:打开解码器

avcodec_open2:打开解码器。

// 第五步:打开解码器
int avcodec_open2_result = avcodec_open2(avcodec_context, avcodec, NULL);
if (avcodec_open2_result != 0){
    __android_log_print(ANDROID_LOG_INFO, "main", "打开解码器失败");
    return;
}
// 测试一下
// 打印信息
__android_log_print(ANDROID_LOG_INFO, "main", "解码器名称:%s", avcodec->name);

1.1.6. 第六步:定义类型转换参数

用于sws_scale(),进行音频采样数据转换操作。

1. 创建视频采样数据上下文

// 第六步:定义类型转换参数
// 6.1 创建视频采样数据帧上下文
// 参数一:源文件->原始视频像素数据格式宽
// 参数二:源文件->原始视频像素数据格式高
// 参数三:源文件->原始视频像素数据格式类型
// 参数四:目标文件->目标视频像素数据格式宽
// 参数五:目标文件->目标视频像素数据格式高
// 参数六:目标文件->目标视频像素数据格式类型
SwsContext *swscontext = sws_getContext(avcodec_context->width,
                avcodec_context->height,
                avcodec_context->pix_fmt,
                avcodec_context->width,
                avcodec_context->height,
                AV_PIX_FMT_YUV420P,
                SWS_BICUBIC,
                NULL,
                NULL,
                NULL);

2. 创建视频压缩数据帧

// 6.2 创建视频压缩数据帧
// 视频压缩数据:H264
AVFrame* avframe_in = av_frame_alloc();
// 定义解码结果
int decode_result = 0;

3. 创建视频采样数据帧

// 6.3 创建视频采样数据帧
// 视频采样数据:YUV格式
AVFrame* avframe_yuv420p = av_frame_alloc();
// 给缓冲区设置类型->yuv420类型
// 得到YUV420P缓冲区大小
// 参数一:视频像素数据格式类型->YUV420P格式
// 参数二:一帧视频像素数据宽 = 视频宽
// 参数三:一帧视频像素数据高 = 视频高
// 参数四:字节对齐方式->默认是1
int buffer_size = av_image_get_buffer_size(AV_PIX_FMT_YUV420P,
                     avcodec_context->width,
                     avcodec_context->height,
                     1);
// 开辟一块内存空间
uint8_t *out_buffer = (uint8_t *)av_malloc(buffer_size);
// 向avframe_yuv420p填充数据
// 参数一:目标->填充数据(avframe_yuv420p)
// 参数二:目标->每一行大小
// 参数三:原始数据
// 参数四:目标->格式类型
// 参数五:宽
// 参数六:高
// 参数七:字节对齐方式
av_image_fill_arrays(avframe_yuv420p->data,
                 avframe_yuv420p->linesize,
                 out_buffer,
                 AV_PIX_FMT_YUV420P,
                 avcodec_context->width,
                 avcodec_context->height,
                 1);

1.1.7. 第七步:打开.yuv文件

// 第七步:打开.yuv文件
const char *outfile = env->GetStringUTFChars(out_file_path, NULL);
FILE* file_yuv420p = fopen(outfile, "wb+");
if (file_yuv420p == NULL){
    __android_log_print(ANDROID_LOG_INFO, "main", "输出文件打开失败");
    return;
}

1.1.8. 第八步:读取视频压缩数据帧

av_read_frame:读取视频压缩数据帧。

// 第八步:读取视频压缩数据帧
int current_index = 0;
// 写入时yuv数据位置
int y_size, u_size, v_size;
// 分析av_read_frame参数。
// 参数一:封装格式上下文
// 参数二:一帧压缩数据
// 如果是解码视频流,是视频压缩帧数据,例如H264
AVPacket* packet = (AVPacket*)av_malloc(sizeof(AVPacket));
while (av_read_frame(avformat_context, packet) >= 0) {
    // >=:读取到了
    // <0:读取错误或者读取完毕
    // 是否是我们的视频流
    if (packet->stream_index == av_stream_index) {
        // 第九步:开始视频解码
        // ...
        current_index++;
        __android_log_print(ANDROID_LOG_INFO, "main", "当前解码第%d帧", current_index);
    }
}

1.1.9. 第九步:开始视频解码

注意:代码位置在第八步。

avcodec_send_packet:发送一帧视频压缩数据。

avcodec_receive_frame:解码一帧视频数据。

// 第九步:开始视频解码
// 发送一帧视频压缩数据
avcodec_send_packet(avcodec_context, packet);
// 解码一帧视频数据
decode_result = avcodec_receive_frame(avcodec_context, avframe_in);
if (decode_result == 0) {

    // 视频解码成功

    // 第十步:开始类型转换
    // ...

    // 第十一步:写入.yuv文件
    // ...

}

1.1.10. 第十步:开始类型转换

注意:代码位置在第九步。

// 第十步:开始类型转换
// 将解码出来的视频像素点数据格式统一转类型为yuv420P
// 参数一:视频像素数据格式上下文
// 参数二:原来的视频像素数据格式->输入数据
// 参数三:原来的视频像素数据格式->输入画面每一行大小
// 参数四:原来的视频像素数据格式->输入画面每一行开始位置(填写:0->表示从原点开始读取)
// 参数五:原来的视频像素数据格式->输入数据行数
// 参数六:转换类型后视频像素数据格式->输出数据
// 参数七:转换类型后视频像素数据格式->输出画面每一行大小
sws_scale(swscontext,
          (const uint8_t *const *)avframe_in->data,
          avframe_in->linesize,
          0,
          avcodec_context->height,
          avframe_yuv420p->data,
          avframe_yuv420p->linesize);

1.1.11. 第十一步:写入.yuv文件

注意:代码位置在第九步。

// 第十一步:写入.yuv文件
// 计算YUV大小
// Y表示:亮度
// UV表示:色度
// YUV420P格式规范一:Y结构表示一个像素(一个像素对应一个Y)
// YUV420P格式规范二:4个像素点对应一个(U和V: 4Y = U = V)
y_size = avcodec_context->width * avcodec_context->height;
u_size = y_size / 4;
v_size = y_size / 4;
// 首先->Y数据
fwrite(avframe_yuv420p->data[0], 1, y_size, file_yuv420p);
// 其次->U数据
fwrite(avframe_yuv420p->data[1], 1, u_size, file_yuv420p);
// 再其次->V数据
fwrite(avframe_yuv420p->data[2], 1, v_size, file_yuv420p);

1.1.12. 第十二步:释放内存资源,关闭解码器

// 第十二步:释放内存资源,关闭解码器
av_packet_free(&packet);
fclose(file_yuv420p);
av_frame_free(&avframe_in);
av_frame_free(&avframe_yuv420p);
free(out_buffer);
avcodec_close(avcodec_context);
avformat_free_context(avformat_context);

1.2. 二、新建Android视频解码工程

1.2.1. 1. 新建工程

参考之前FFmpeg集成,新建ndk工程AndroidFFmpegDecodingVideo。

1.2.2. 2. 定义java方法

寻找MainActivity:app->src->main->java->MainActivity,增加代码如下:

public native void ffmepgDecodeVideo(String inFilePath, String outFilePath);

1.2.3. 3. 定义NDK方法

增加android打印。

#include <android/log.h>

在native-lib.cpp中,导入FFmpeg头文件。

extern "C" {
// 引入头文件
// 核心库->音视频编解码库
#include <libavcodec/avcodec.h>
// 封装格式处理库
#include "libavformat/avformat.h"
// 工具库
#include "libavutil/imgutils.h"
// 视频像素数据格式库
#include "libswscale/swscale.h"
}

在native-lib.cpp中新增java方法ffmepgDecodeVideo的C++实现,输入MainActivity.就会有代码提示,选择正确ffmepgDecodeVideo方法补全代码。

extern "C"
JNIEXPORT void JNICALL
Java_com_ccq_androidffmpegdecodingvideo_MainActivity_ffmepgDecodeVideo(JNIEnv *env, jobject thiz,
                                                                       jstring in_file_path,
                                                                       jstring out_file_path) {
    // 这里拷贝上面的视频解码流程的代码即可。
}

1.3. 三、测试Android视频解码工程

准备视频文件:test.mov

在AndroidManifest.xml增加SD卡的读写权限。

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

MainActivity增加测试代码。

注意:如果打开失败,可能读写存储设备的权限被禁用。

摩托罗拉·刀锋:设置->应用和通知->高级->权限管理器->隐私相关·读写存储设备->找到应用->如果禁用,则修改为允许。

import android.os.Environment;
import java.io.File;
import java.io.IOException;
import android.util.Log;

String rootPath = Environment.getExternalStorageDirectory().getAbsolutePath();
String downloadPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();
String inFilePath = downloadPath.concat("/test.mov");
String outFilePath = downloadPath.concat("/test.yuv");

// 文件不存在我创建一个文件
File file = new File(outFilePath);
    if (file.exists()) {
        Log.i("日志:","存在");
} else {
    try {
        file.createNewFile();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
ffmepgDecodeVideo(inFilePath, outFilePath);

出现问题,待解决:

I/main: 解码器名称:h264
A/libc: Fatal signal 6 (SIGABRT), code -1 (SI_QUEUE) in tid 5713 (egdecodingvideo), pid 5713 (egdecodingvideo)

增加打印pix_fmt代码:

 __android_log_print(ANDROID_LOG_INFO, "main", "avcodec_context->pix_fmt:%d", avcodec_context->pix_fmt);

发现avcodec_context->pix_fmt = -1,导致sws_getContext方法出错,修改sws_getContext的srcFormat参数。

SwsContext *swscontext = sws_getContext(avcodec_context->width,
                avcodec_context->height,
                AV_PIX_FMT_YUV420P,
                //avcodec_context->pix_fmt,
                avcodec_context->width,
                avcodec_context->height,
                AV_PIX_FMT_YUV420P,
                SWS_BICUBIC,
                NULL,
                NULL,
                NULL);

run工程代码,正确打印,同时正确生成yuv文件。

I/main: 解码器名称:h264
I/main: avcodec_context->width:640
I/main: avcodec_context->height:352
I/main: avcodec_context->pix_fmt:-1
I/main: 当前解码第1帧
.
.
.
I/main: 当前解码第600帧

yuv文件太大(202.1M),不方便上传。yuv播放:

ffplay -f rawvideo -video_size 640x352 /Users/chenchangqing/Documents/code/ffmpeg/resources/test.yuv

results matching ""

    No results matching ""