NDK--利用FFmpeg进行视频解码

对于普通人而言,视频文件有许多格式,如mp4,rmvb,avi等,这存在的一个误区,让人们谈到视频就想到这些文件格式,而在音视频开发中,我们把这些文件格式称为封装格式,这些封装格式中,不仅仅包含着视频流,还包含着音频流,字幕流等,而视频流都按照视频编码标准进行编码,下面是视频编码标准

名称 推出机构 推出时间 目前使用领域
HEVC(H.265) MPEG/ITU-T 2013 研发中
H.264 MPEG/ITU-T 2003 各个领域
MPEG4 MPEG 2001 不温不火
MPEG2 MPEG 1994 数字电视
VP9 Google 2013 研发中
VP8 Google 2008 不普及
VC-1 Microsoft Inc. 2006 微软平台

那么为什么要编码呢?我们知道所有的颜色都可以用三原色(Red,Green,Blue)来合成,任何一个图像都可以由RGB组成,那么一个像素点的RGB该如何表示呢?对于一幅图像,一般使用整数表示方法来进行描述:

·整数表示:取值范围为0~255或者00~FF,8个比特表示一个子像素,32个比特表示一个像素,这就是类似于某些平台上表示图像格式的 RGBA_8888数据格式。比如,Android平台上RGB_565的表示方法为16比特模式表示一个像素,R用5个比特来表示,G用6个比特来表示,B用5个比特来表示。

那么,计算一张 1280×720的RGBA_8888图像的大小,可采用如下方式:

1280 * 720 * 4 = 3.516MB

很明显如果一个视频使用RGB来存储,那么它的文件大小将非常庞大,如果fps(1秒的视频帧数目)是24,按照一般电影的长度90分钟来计算,RGB数据格式来表示的话,它的文件大小为:

3.516MB * 24fps * 90min * 60s = 455.7GB

后来,生物学家利用人眼的特性,推出了YUV,YUV主要应用于优化彩色视频信号的传输,使其向后兼容老式黑白电视。与RGB视频信号传输相比,它最大的优点在于只需要占用极少的频宽(RGB要求三个独立的视频信号同时传输)。

“Y”表示明亮度 (Luminance或Luma),也称灰阶值;而“U”和“V”表示的则是色度 (Chrominance或Chroma),它们的作用是描述影像的色彩及饱和度, 用于指定像素的颜色。

YUV最常用的采样格式是4:2:0
NDK--利用FFmpeg进行视频解码_第1张图片

如果某一行是4:2:0,那么其下一行就是4:0:2,再下一行是 4:2:0,以此类推。对于每个色度分量来说,水平方向和竖直方向的 抽样率都是2:1,所以可以说色度的抽样率是4:1。对非压缩的8比特量化的视频来说,8×4的一张图片需要占用48字节的内存

相较于RGB,我们可以计算一帧为1280×720的视频帧,用 YUV420P的格式来表示,其数据量的大小如下:

1280 * 720 * 1 + 1280 * 720 * 0.5 = 1.318MB

如果fps(1秒的视频帧数目)是24,按照一般电影的长度90分钟来 计算,那么这部电影用YUV420P的数据格式来表示的话,其数据量的 大小就是:

1.318MB * 24fps * 90min * 60s = 166.8GB

虽然相比于RGB而言,文件体积已经小了很多,但仅用这种方式来存储电影肯定是不可行的,更别说在网络上进行流媒体播放了,那么如何对电影进行存储以及流媒体播放呢?答案是 需要进行视频编码,即视频压缩
压缩分为有损压缩和无损压缩,无损压缩比2:1 ~ 3:1,有损压缩比10:1 ~ 20:1,一般都为有损压缩。主要针对以下几点进行压缩:

○ 空间冗余:图像相邻像素之间有较强的相关性
○ 时间冗余:视频序列的相邻图像之间内容相似
○ 编码冗余:不同像素值出现的概率不同
○ 视觉冗余:人的视觉系统对某些细节不敏感
○ 知识冗余:规律性的结构可由先验知识和背景知识得到

因此有了我们开始所说的视频编码标准,对视频进行压缩(编码)来保存文件,播放时对视频解压缩(解码)成YUV格式进行播放

接下来,利用之前编译集成好FFmpeg的as项目,初探视频解码:将视频转码成yuv格式

在手机中传入一个MP4视频:
NDK--利用FFmpeg进行视频解码_第2张图片
在java中传入原始视频路径和转码视频路径:
package com.aruba.ffmpegapplication;

import android.os.Bundle;
import android.os.Environment;
import android.view.View;

import androidx.appcompat.app.AppCompatActivity;

import java.io.File;

public class MainActivity extends AppCompatActivity {

    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

    }

    public native void decode(String inputFilePath, String outputFilePath);

    public void click(View view) {
        File input = new File(Environment.getExternalStorageDirectory(), "input.mp4");
        File output = new File(Environment.getExternalStorageDirectory(), "output.yuv");
        decode(input.getAbsolutePath(), output.getAbsolutePath());
    }
}

编写相应的native方法:
#include 
#include 
#include 

extern "C" {
//编码
#include "libavcodec/avcodec.h"
//封装格式处理
#include "libavformat/avformat.h"
//像素处理
#include "libswscale/swscale.h"
}

#define  LOG_TAG    "aruba"
#define  LOGE(...)  __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)

extern "C"
JNIEXPORT void JNICALL
Java_com_aruba_ffmpegapplication_MainActivity_decode(JNIEnv *env, jobject instance,
                                                     jstring inputFilePath_,
                                                     jstring outputFilePath_) {
    const char *inputFilePath = env->GetStringUTFChars(inputFilePath_, 0);
    const char *outputFilePath = env->GetStringUTFChars(outputFilePath_, 0);

    //注册FFmpeg中各大组件
    av_register_all();

    //打开文件
    AVFormatContext *formatContext = avformat_alloc_context();
    if (avformat_open_input(&formatContext, inputFilePath, NULL, NULL) != 0) {
        LOGE("打开失败");
        avformat_free_context(formatContext);
        env->ReleaseStringUTFChars(inputFilePath_, inputFilePath);
        env->ReleaseStringUTFChars(outputFilePath_, outputFilePath);
        return;
    }

    //将文件信息填充进AVFormatContext
    if (avformat_find_stream_info(formatContext, NULL) < 0) {
        LOGE("获取文件信息失败");
        avformat_free_context(formatContext);
        env->ReleaseStringUTFChars(inputFilePath_, inputFilePath);
        env->ReleaseStringUTFChars(outputFilePath_, outputFilePath);
        return;
    }

    //获取视频流的编解码器上下文
    AVCodecContext *codecContext = NULL;
    for (int i = 0; i < formatContext->nb_streams; ++i) {
        if (formatContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {//如果是视频流
            codecContext = formatContext->streams[i]->codec;
            break;
        }
    }

    if (codecContext == NULL) {
        avformat_free_context(formatContext);
        env->ReleaseStringUTFChars(inputFilePath_, inputFilePath);
        env->ReleaseStringUTFChars(outputFilePath_, outputFilePath);
        return;
    }

    //根据编解码器上下文的id获取视频流解码器
    AVCodec *codec = avcodec_find_decoder(codecContext->codec_id);
    //打开解码器
    if (avcodec_open2(codecContext, codec, NULL) < 0) {
        LOGE("解码失败");
        avformat_free_context(formatContext);
        env->ReleaseStringUTFChars(inputFilePath_, inputFilePath);
        env->ReleaseStringUTFChars(outputFilePath_, outputFilePath);
        return;
    }

    //开始读每一帧
    //存放压缩数据
    AVPacket *pkt = (AVPacket *) (av_malloc(sizeof(AVPacket)));
    av_init_packet(pkt);

    //存放解压数据
    AVFrame *picture = av_frame_alloc();

    //存放转码数据
    AVFrame *picture_yuv = av_frame_alloc();
    //为转码数据分配内存
    uint8_t *data_size = (uint8_t *) (av_malloc(
            (size_t) avpicture_get_size(AV_PIX_FMT_YUV420P, codecContext->width,
                                        codecContext->height)));
    avpicture_fill((AVPicture *) picture_yuv, data_size, AV_PIX_FMT_YUV420P, codecContext->width,
                   codecContext->height);

    int picture_ptr = 0;

    //转码组件上下文,前三个参数为原视频的宽高和编码,后三个为转码后的视频宽高和编码,还可以传入过滤器对视频做处理,这边不做处理
    SwsContext *swsContext = sws_getContext(codecContext->width, codecContext->height,
                                            codecContext->pix_fmt,
                                            codecContext->width, codecContext->height,
                                            AV_PIX_FMT_YUV420P, SWS_BILINEAR, NULL, NULL, NULL
    );

    //文件
    FILE *output_file = fopen(outputFilePath, "wb");

    while (av_read_frame(formatContext, pkt) == 0) {//读到每一帧的压缩数据存放在AVPacket

        //解码
        avcodec_decode_video2(codecContext, picture, &picture_ptr, pkt);

        if (picture_ptr > 0) {
            LOGE("picture_ptr %d", picture_ptr);

            //转码 data中存放着真实数据,linesize为一行的数据,0为转码起始位置,高度为整个画面高
            sws_scale(swsContext, picture->data, picture->linesize, 0, picture->height,
                      picture_yuv->data, picture_yuv->linesize);

            //一帧的数据大小
            size_t size_y = codecContext->width * codecContext->height;
            //写文件 y:u:v为4:1:1
            //写y
            fwrite(picture_yuv->data[0], sizeof(uint8_t), size_y, output_file);
            //写u
            fwrite(picture_yuv->data[1], sizeof(uint8_t), size_y / 4, output_file);
            //写v
            fwrite(picture_yuv->data[2], sizeof(uint8_t), size_y / 4, output_file);
        }

        av_free_packet(pkt);
    }

    //关闭文件
    fclose(output_file);
    //释放资源
    sws_freeContext(swsContext);
    av_frame_free(&picture_yuv);
    av_frame_free(&picture);
    avcodec_close(codecContext);
    avformat_free_context(formatContext);
    env->ReleaseStringUTFChars(inputFilePath_, inputFilePath);
    env->ReleaseStringUTFChars(outputFilePath_, outputFilePath);
}
最后授予APP读写权限,生成的yuv文件:
NDK--利用FFmpeg进行视频解码_第3张图片
很明显,yuv文件体积较原视频大很多,视频解码转码的课题就到此结束了,之后的文章会介绍如何原生绘制视频。
项目地址:https://gitee.com/aruba/FFmpegApplication.git

你可能感兴趣的:(NDK--利用FFmpeg进行视频解码)