美文网首页
「ffmpeg」三 简单的YUV解码器

「ffmpeg」三 简单的YUV解码器

作者: 叨码 | 来源:发表于2019-08-08 17:17 被阅读0次

    本系列文章由刀码旦编写,转载请注明出处

    引言

    本ffmpeg系列文章可以看作是基于雷神的博客的学习实践,由于雷神所使用的ffmpeg是老版本,一些地方我也会根据新版本的API做了一些更新,另外改为了Cmake构建方式,这也是区别于雷神博客的地方。

    本文记录一个安卓平台下基于FFmpeg的视频解码器,目的是将一段视频文件解码为YUV数据。
    至于YUV是什么,可以参考下这篇文章
    ,暂且简单理解就可以。

    准备工作

    1.本文继续沿用上一篇中的项目Android平台基于ffmpeg的Helloworld
    2.手动创建了一个解码操作页面DecodecActivity 以及用来实现解码功能的c文件simple_ffmpeg_decoder.c,并配置CmakeLists.txt脚本。
    3.准备一个mp4的测试视频,将视频导入到设备sdcard根目录下
    这里提供一个视频下载

    开始

    项目结构如下


    image.png

    有别于之前,这里手动创建了一个解码操作页面DecodecActivity 以及用来实现解码功能的c文件simple_ffmpeg_decoder.c,以及

    首先先用c语言实现解码功能,代码如下

    simple_ffmpeg_decoder.c
    //
    // Created by ing on 2019/7/31.
    //最简单的基于FFmpeg的视频解码器
    
    #include <stdio.h>
    #include <time.h>
    #include "libavcodec/avcodec.h"
    #include "libavformat/avformat.h"
    #include "libswscale/swscale.h"
    #include "libavutil/log.h"
    #include "libavutil/imgutils.h"
    
    #ifdef ANDROID
    
    #include <jni.h>
    #include <android/log.h>
    #include <libavformat/avformat.h>
    
    #define LOGE(format, ...) __android_log_print(ANDROID_LOG_ERROR,"(>_<)",format,##__VA_ARGS__)
    #define LOGI(format, ...) __android_log_print(ANDROID_LOG_INFO,"(^_^)",format,##__VA_ARGS__)
    #else
    #define LOGE(format, ...) printf("(>_<) " format "\n", ##__VA_ARGS__)
    #define LOGI(format, ...) printf("(^_^) " format "\n", ##__VA_ARGS__)
    #endif
    
    //Output FFmpeg's av_log()
    void custom_log(void *ptr, int level, const char *fmt, va_list vl) {
        FILE *fp = fopen("/storage/emulated/0/av_log.txt", "a+");
        if (fp) {
            vfprintf(fp, fmt, vl);
            fflush(fp);
            fclose(fp);
        }
    }
    
    JNIEXPORT jint JNICALL
    Java_com_ing_ffmpeg_DecodecActivity_decode(JNIEnv *env, jobject obj, jstring input_jstr,
                                               jstring output_jstr) {
        AVFormatContext *pFormatCtx;
        int i, videoindex;
        AVCodecContext *pCodecCtx;
        AVCodec *pCodec;
        AVFrame *pFrame, *pFrameYUV;
        uint8_t *out_buffer;
        AVPacket *packet;
        int y_size;
        int ret, got_picture;
        struct SwsContext *img_convert_ctx;
        FILE *fp_yuv;
        int frame_cnt;
    
        clock_t time_start, time_finish;
        double time_duration = 0.0;
    
        char input_str[500] = {0};
        char output_str[500] = {0};
        char info[1000] = {0};
        sprintf(input_str, "%s", (*env)->GetStringUTFChars(env, input_jstr, NULL));
        sprintf(output_str, "%s", (*env)->GetStringUTFChars(env, output_jstr, NULL));
    
        av_log_set_callback(custom_log);
        av_register_all();
        avformat_network_init();
        pFormatCtx = avformat_alloc_context();
        /**
         * 打开音视频文件 avformat_open_input 主要负责服务器的连接和码流头部信息的拉取
         * 函数读取媒体文件的文件头并将文件格式相关的信息存储AVFormatContext上下文中。
         * 第二参数 input_str 文件的路径
         * 第三参数 用于指定媒体文件格式
         * 第四参数 文件格式的相关选项
         * 后面两个参数如果传入的NULL,那么libavformat将自动探测文件格式
         **/
        if (avformat_open_input(&pFormatCtx, input_str, NULL, NULL) != 0) {
            LOGE("Couldn't open input stream.\n");
            return -1;
        }
        /**
         * 
         * 媒体信息的探测和分析
         * 函数会为pFormatCtx->streams填充对应的信息
         *
         *
         * AVFormatContext 里包含了下面这些跟媒体信息有关的成员:
    ----------AVFormatContext-------
    struct AVInputFormat *iformat; // 记录了封装格式信息
    unsigned int nb_streams; // 记录了该 URL 中包含有几路流
    AVStream **streams; // 一个结构体数组,每个对象记录了一路流的详细信息
    int64_t start_time; // 第一帧的时间戳
    int64_t duration; // 码流的总时长
    int64_t bit_rate; // 码流的总码率,bps
    AVDictionary *metadata; // 一些文件信息头,key/value 字符串
    pFormatCtx->streams是一个AVStream指针的数组,里面包含了媒体资源的每一路流信息,数组大小为pFromatCtx->nb_streams
    
    --------AVStream---------
    AVStream 结构体中关键的成员包括:
    
    AVCodecContext *codec; // 记录了该码流的编码信息
    int64_t start_time; // 第一帧的时间戳
    int64_t duration; // 该码流的时长
    int64_t nb_frames; // 该码流的总帧数
    AVDictionary *metadata; // 一些文件信息头,key/value 字符串
    AVRational avg_frame_rate; // 平均帧率
    
    ----------AVCodecContext---------
         AVCodecContext 则记录了一路流的具体编码信息,其中关键的成员包括:
    
    const struct AVCodec *codec; // 编码的详细信息
    enum AVCodecID codec_id; // 编码类型
    int bit_rate; // 平均码率
    video only:
    int width, height; // 图像的宽高尺寸,码流中不一定存在该信息,会由解码后覆盖
    enum AVPixelFormat pix_fmt; // 原始图像的格式,码流中不一定存在该信息,会由解码后覆盖
    audio only:
    int sample_rate; // 音频的采样率
    int channels; // 音频的通道数
    enum AVSampleFormat sample_fmt; // 音频的格式,位宽
    int frame_size; // 每个音频帧的 sample 个数
    
         */
        if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
            LOGE("Couldn't find stream information.\n");
            return -1;
        }
        videoindex = -1;
        for (int i = 0; i < pFormatCtx->nb_streams; ++i) {
            if (pFormatCtx->streams[i]/*音视频流*/->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)//查找视频流
            {
                videoindex = i;
                break;
            }
    
        }
        if (videoindex == -1) {
            LOGE("Couldn't find a video stream.\n");
            return -1;
        }
        /**
         * pCodecCtx = pFormatCtx->streams[videoindex]->codec;//指向AVCodecContext的指针 #已废弃,不赞成使用。
         */
        pCodecCtx = avcodec_alloc_context3(NULL);
        if (pCodecCtx == NULL) {
            printf("Could not allocate AVCodecContext\n");
            return -1;
        }
        avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[videoindex]->codecpar);
        pCodec = avcodec_find_decoder(pCodecCtx->codec_id);//指向AVCodec的指针,查找解码器
        if (pCodec == NULL) {
            LOGE("Couldn't find Codec.\n");
            return -1;
        }
        //打开解码器
        if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
            LOGE("Couldn't open codec.\n");
            return -1;
        }
    
        /**------存储数据 存储视频的帧 并转化格式------**/
        pFrame = av_frame_alloc();
        pFrameYUV = av_frame_alloc();
        //当转换格式时,我们需要一块内存来存储视频帧的原始数据。
        // 为已经分配空间的结构体AVPicture挂上一段用于保存数据的空间
        // AVFrame/AVPicture有一个data[4]的数据字段,buffer里面存放的只是yuv这样排列的数据,
        // 而经过fill 之后,会把buffer中的yuv分别放到data[0],data[1],data[2]中。av_image_get_buffer_size来获取需要的内存大小,然后手动分配这块内存。
        out_buffer = (unsigned char *) av_malloc(
                av_image_get_buffer_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height, 1));
        //关联frame和我们刚才分配的内存---存储视频帧的原始数据
        av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize, out_buffer, AV_PIX_FMT_YUV420P,
                             pCodecCtx->width, pCodecCtx->height, 1);
    
        /**----------------读取数据----------------**/
        packet = (AVPacket *) av_malloc(sizeof(AVPacket));
        //初始化一个SwsContext 图形裁剪
        //参数 源图像的宽,源图像的高,源图像的像素格式,目标图像的宽,目标图像的高,目标图像的像素格式,设定图像拉伸使用的算法
        img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,
                                         pCodecCtx->width, pCodecCtx->height,
                                         AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);
        sprintf(info, "[Input ]$s\n", input_str);
        sprintf(info, "%s[Output ]%s\n", info, output_str);
        sprintf(info, "%s[Format ]%s\n", info, pFormatCtx->iformat->name);
        sprintf(info, "%s[Codec ]%s]\n", info, pCodecCtx->codec->name);
        sprintf(info, "%s[Resolution ]%dx%d\n", info, pCodecCtx->width, pCodecCtx->height);
    
    
        fp_yuv = fopen(output_str, "wb+");
        if (fp_yuv == NULL) {
            printf("Cannot open output file.\n");
            return -1;
        }
        frame_cnt = 0;
        time_start = clock();
    
        while (av_read_frame(pFormatCtx, packet) >= 0) {
            if (packet->stream_index == videoindex) {
                //解码一帧视频数据,输入一个压缩编码的结构体AVPacket,输出一个解码后的结构体AVFrame
                ret = avcodec_send_packet(pCodecCtx, packet);
                if (ret < 0) {
                    LOGE("Decode Error.\n");
                    return -1;
                }
    
                got_picture = avcodec_receive_frame(pCodecCtx, pFrame);
                if (got_picture) {
                    //转换像素
                    //解码后yuv格式的视频像素数据保存在AVFrame的data[0]、data[1]、data[2]中。
                    // 但是这些像素值并不是连续存储的,每行有效像素之后存储了一些无效像素
                    // ,以高度Y数据为例,data[0]中一共包含了linesize[0]*height个数据。
                    // 但是出于优化等方面的考虑,linesize[0]实际上并不等于宽度width,而是一个比宽度大一些的值。
                    // 因此需要使用ses_scale()进行转换。转换后去除了无效数据,width和linesize[0]就取值相同了
    
                    sws_scale(img_convert_ctx, (const uint8_t *const *) pFrame->data, pFrame->linesize,
                              0,
                              pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize);
                    y_size = pCodecCtx->width * pCodecCtx->height;
                    //向文件写入一个数据块
                    fwrite(pFrameYUV->data[0], 1, y_size, fp_yuv);
                    fwrite(pFrameYUV->data[1], 1, y_size / 4, fp_yuv);
                    fwrite(pFrameYUV->data[1], 1, y_size / 4, fp_yuv);
                    //Output info
                    char pictype_str[10] = {0};
                    switch (pFrame->pict_type) {
                        case AV_PICTURE_TYPE_I:
                            sprintf(pictype_str, "I");
                            break;
                        case AV_PICTURE_TYPE_P:
                            sprintf(pictype_str, "p");
                            break;
                        case AV_PICTURE_TYPE_B:
                            sprintf(pictype_str, "B");
                            break;
                        default:
                            sprintf(pictype_str, "Other");
                            break;
                    }
                    LOGI("Frame Index : %5d.Type:%s", frame_cnt, pictype_str);
                    frame_cnt++;
                }
    
            }
    //        av_free_packet(packet);已废弃
            av_packet_unref(packet);
        }
        //flush_decoder
        //当av_read_frame()循环退出时,实际上解码器中可能还包含剩余的几帧数据,因此需要通过flush_decoder将这几帧数据输出。
        //flush_decoder功能简而言之即直接调用avcodec_send_packet()获得AVFrame,而不再向解码器传递AVPacket
        while (1) {
            ret = avcodec_send_packet(pCodecCtx, packet);
            if (ret < 0) {
                break;
            }
            if (!got_picture) {
                break;
            }
            sws_scale(img_convert_ctx, (const uint8_t *const *) pFrame->data, pFrame->linesize, 0,
                      pCodecCtx->height,
                      pFrameYUV->data, pFrameYUV->linesize);
            int y_size = pCodecCtx->width * pCodecCtx->height;
            fwrite(pFrameYUV->data[0], 1, y_size, fp_yuv);//y
            fwrite(pFrameYUV->data[1], 1, y_size / 4, fp_yuv);//u
            fwrite(pFrameYUV->data[2], 1, y_size / 4, fp_yuv);//v
            //Output info
            char pictype_str[10] = {0};
            switch (pFrame->pict_type) {
                case AV_PICTURE_TYPE_I:
                    sprintf(pictype_str, "I");
                    break;
                case AV_PICTURE_TYPE_P:
                    sprintf(pictype_str, "p");
                    break;
                case AV_PICTURE_TYPE_B:
                    sprintf(pictype_str, "B");
                    break;
                default:
                    sprintf(pictype_str, "Other");
                    break;
            }
            LOGI("Frame Index:%5d. Type:%s", frame_cnt, pictype_str);
            frame_cnt++;
        }
        time_finish = clock();
        time_duration = (double) (time_finish - time_start);
        sprintf(info, "%s[Time  ]%fms\n", info, time_duration);
        sprintf(info, "%s[Count ]%d\n", info, frame_cnt);
        fclose(fp_yuv);
        av_frame_free(&pFrameYUV);
        av_frame_free(&pFrame);
        avcodec_close(pCodecCtx);
        avformat_close_input(&pFormatCtx);
        LOGI("%s", "解码完成.");
    
        return 0;
    }
    

    代码中依据自己的理解以及资料做了一部分注释,自己也没有完全吃透,所以仅供参考,如有纰漏,欢迎指正。

    至于DecodecActivity 就直接贴代码了

    DecodecActivity
    package com.ing.ffmpeg;
    
    import android.os.Bundle;
    import android.os.Environment;
    import android.support.annotation.Nullable;
    import android.support.v7.app.AppCompatActivity;
    import android.util.Log;
    import android.view.View;
    import android.widget.Button;
    import android.widget.EditText;
    
    /**
     * Created by ing on 2019/8/1
     */
    public class DecodecActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_decodec);
            Button start = findViewById(R.id.start);
            final EditText edt_input = findViewById(R.id.edt_input);
            final EditText edt_output = findViewById(R.id.edt_output);
            start.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    String folderUrl = Environment.getExternalStorageDirectory().getPath();
                    String urlinput = folderUrl+"/"+edt_input.getText().toString();
                    String urloutput = folderUrl+"/"+edt_output.getText().toString();
                    Log.i("Url","input url ="+urlinput);
                    Log.i("Url","output url ="+urloutput);
                    decode(urlinput,urloutput);
                }
            });
        }
        //JNI
        public native int decode(String inputurl, String outputurl);
    }
    
    

    对应的xml布局文件activit_main.xml

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:orientation="vertical">
    
        <TextView
            android:id="@+id/tv1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Input Bitstream" />
    
        <EditText
            android:id="@+id/edt_input"
            android:layout_width="match_parent"
            app:layout_constraintTop_toBottomOf="@id/tv1"
            android:layout_marginTop="10dp"
            android:hint="原视频文件名"
            android:layout_height="wrap_content"
            android:layout_marginLeft="20dp"
            android:layout_marginRight="20dp"/>
    
        <TextView
            android:id="@+id/tv2"
            app:layout_constraintTop_toBottomOf="@+id/edt_input"
            android:layout_width="wrap_content"
            android:layout_marginTop="10dp"
            android:layout_height="wrap_content"
            android:text="Output Raw YUV" />
    
        <EditText
            android:id="@+id/edt_output"
            app:layout_constraintTop_toBottomOf="@id/tv2"
            android:layout_width="match_parent"
            android:hint="解码后的存储文件名"
            android:layout_height="wrap_content"
            android:layout_margin="20dp"/>
        <Button
            android:id="@+id/start"
            app:layout_constraintTop_toBottomOf="@+id/edt_output"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="start"/>
       
    </android.support.constraint.ConstraintLayout>
    

    另外就是MainActivity里增加一个按钮,简单实现跳转即可


    image.png

    当然别忘了AndroidManifest.xml里注册下DecodecActivity

    <activity android:name=".DecodecActivity" android:screenOrientation="portrait"/>
    

    最最最后,千万不要忘了配置CMakeLists.txt,将新加的c源码添加到库native-lib中

    CMakeLists.txt修改部分.png
    然后【build】--【make project】完成so的编译,编译成功也就说明native-lib.so已经存在解码功能了。
    运行顺利的话,进入MainActivity点击【解码示例】按钮,进入DecodecActivity
    device-2019-08-08-170943.png
    输入你的视频名,解码后保存的文件名,然后【start】即可,可能需要等待一会,解码完成时,日志会打印"(_):解码完成."此时sdcard下会生成一个xxx.yuv文件
    image.png
    就说明成功了。

    相关文章

      网友评论

          本文标题:「ffmpeg」三 简单的YUV解码器

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