美文网首页
NDK--利用FFmpeg进行视频解码

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

作者: aruba | 来源:发表于2020-06-28 16:09 被阅读0次

    对于普通人而言,视频文件有许多格式,如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

    如果某一行是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视频:
    在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 <jni.h>
    #include <string>
    #include <android/log.h>
    
    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文件:
    很明显,yuv文件体积较原视频大很多,视频解码转码的课题就到此结束了,之后的文章会介绍如何原生绘制视频。
    项目地址:https://gitee.com/aruba/FFmpegApplication.git

    相关文章

      网友评论

          本文标题:NDK--利用FFmpeg进行视频解码

          本文链接:https://www.haomeiwen.com/subject/grlifktx.html