「ffmpeg」三 简单的YUV解码器

2019-08-08  本文已影响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
就说明成功了。
上一篇 下一篇

猜你喜欢

热点阅读