視頻縮放及像素格式轉換ffmpg(十二)


前言

1、視頻縮放是指將視頻放大或者縮小,放大或者縮小對應着不同的縮放算法,每一種算法性能和效果也不一致。視頻縮小也是很常見的需求,各個點播平台基本上都會提供不同分辨率(超清1080P,高清720P,標清360P)的視頻資源以適應用戶不同網絡條件的需求。
2、視頻像素格式轉換;安卓平台碎片化的特性,安卓手機錄制的視頻可能有多種不同像素格式,比如NV12,NV21等等,雖然他們都是YUV顏色空間,但是轉換成RGB的方式和方法卻不一致。

視頻縮放及像素格式轉換流程

 
image.png

視頻縮放及像素格式轉換相關命令行介紹

  • 1、MP4文件中提取視頻並轉換為YUV

ffmpeg -i test_1280x720.MP4 -ss 00:00:00 -t 00:00:05 -pixel_format yuv420p -vf scale=640:360 -f rawvideo test_640x360_yuv420p.yuv

備注:pixel_format指定輸出的YUV的格式。vf scale=640:360表示壓縮視頻濾鏡,最終轉換后的視頻分辨率為640x360

  • 2、播放YUV

ffplay -i test.yuv -f rawvideo -pixel_format nv12 -video_size 320x180 -framerate 50

備注:-pixel_format代表yuv的格式,如果不指定默認yuv420p;framerate代表播放時視頻的幀率,如果不指定默認25

視頻縮放及像素格式轉換流程相關函數介紹

1、SwsContext

視頻進行縮放和格式轉換的上下文

2、struct SwsContext *sws_getContext(
int srcW, int srcH, enum AVPixelFormat srcFormat,
int dstW, int dstH, enum AVPixelFormat dstFormat,
int flags, SwsFilter *srcFilter,SwsFilter *dstFilter,
const double *param);

創建SwsContext上下文,並將函數中參數賦值到上下文中
1、參數1 2 3:轉換源數據寬,高,像素格式
2、參數4 5 6:轉換后數據寬,高,像素格式
3、參數7:如果進行縮放,所采用的的縮放算法
4、參數8:轉換源數據的 SwsFilter 參數
5、參數9:轉換后數據的 SwsFilter 參數
6、參數10:轉換縮放算法的相關參數 const double* 類型數組
7、8 9 10三個參數一般傳遞NULL,即采用默認值即可
8、SWS_BICUBIC性能比較好,SWS_FAST_BILINEAR在性能和速度之間有一個比好好的平衡,SWS_POINT的效果比較差。

struct SwsContext *sws_alloc_context(void)

創建SwsContext上下文,並賦值默認的參數

av_opt_set_xxx()給SwsContext上下文設置對應的參數

int sws_init_context(struct SwsContext *sws_context, SwsFilter *srcFilter, SwsFilter *dstFilter);

初始化SwsContext上下文

int sws_scale(struct SwsContext *c, const uint8_t *const srcSlice[],
const int srcStride[], int srcSliceY, int srcSliceH,
uint8_t *const dst[], const int dstStride[]);

進行縮放和格式轉換

ffmpeg實現代碼

頭文件

#ifndef videoresample_hpp
#define videoresample_hpp

#include <stdio.h>
#include <string>
#include "cppcommon/CLog.h"

extern "C" {
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
#include <libavutil/opt.h>
}
using namespace std;

typedef enum
{
    FormatTypeYUV420P,
    FormattypeRGB24
}FormatType;

class VideoScale {
public:
    VideoScale();
    ~VideoScale();
    
    /** sws_scale()函數主要用來進行視頻的縮放和視頻數據的格式轉換。對於縮放,有不同的算法,每個算法的性能不一樣
     */
    void doScale();
};


#endif /* videoresample_hpp */

實現文件

VideoScale::VideoScale(){
    
}

VideoScale::~VideoScale()
{
    
}

const char* nameForOptionType(AVOptionType type) {
    const char *names[19] = {
        "AV_OPT_TYPE_FLAGS",
        "AV_OPT_TYPE_INT",
        "AV_OPT_TYPE_INT64",
        "AV_OPT_TYPE_DOUBLE",
        "AV_OPT_TYPE_FLOAT",
        "AV_OPT_TYPE_STRING",
        "AV_OPT_TYPE_RATIONAL",
        "AV_OPT_TYPE_BINARY",
        "AV_OPT_TYPE_DICT",
        "AV_OPT_TYPE_UINT64",
        "AV_OPT_TYPE_CONST",
        "AV_OPT_TYPE_IMAGE_SIZE",
        "AV_OPT_TYPE_PIXEL_FMT",
        "AV_OPT_TYPE_SAMPLE_FMT",
        "AV_OPT_TYPE_VIDEO_RATE",
        "AV_OPT_TYPE_DURATION",
        "AV_OPT_TYPE_COLOR",
        "AV_OPT_TYPE_CHANNEL_LAYOUT",
        "AV_OPT_TYPE_BOOL"
    };
    
    return names[type];
}


/** 視頻像素格式存儲方式
 *  YUV420P:三個plane,按照 YYYY........U........V..........分別存儲於各個plane通道
 *  RGB24:一個plane,按照RGBRGB....順序存儲在一個planne中
 *  NV12:兩個plane,一個存儲YYYYYY,另一個按照UV的順序交叉存儲
 *  NV21:兩個plane,一個存儲YYYYYY,另一個按照VU的順序交叉存儲
 */
/** 播放yuv的ffplay 命令
 *  ffplay -i  test_320x180_nv12.yuv -f rawvideo --pixel_format nv12 -video_size 320x180 -framerate 50
 */
/** 遇到問題:源MP4文件視頻的幀率為50fps,解碼成yuv之后用ffplay播放rawvideo速度變慢
 *  原因分析:ffplay播放rawvideo的默認幀率為25fps;如上添加-framerate 50 代表以50的fps播放
 */
void VideoScale::doScale()
{
    string curFile(__FILE__);
    unsigned long pos = curFile.find("1-video_encode_decode");
    if (pos == string::npos) {
        LOGD("1-video_encode_decode file fail");
        return;
    }
    string resourceDir = curFile.substr(0,pos)+"filesources/";
    string srcyuvPath = resourceDir + "test_640x360_yuv420p.yuv";
    string dstyuvPath = "test.yuv";
    enum AVPixelFormat src_pix_fmt = AV_PIX_FMT_YUV420P;
    enum AVPixelFormat dst_pix_fmt = AV_PIX_FMT_NV12;
    int src_w = 640,src_h = 360;
    int dst_w = 320,dst_h = 180;
    
    /** 1、因為視頻的planner個數最多不超過4個,所以這里的src_data數組和linesize數組長度都是4
     *  2、linesize數組存儲的是各個planner的寬,對應"視頻的寬",由於字節對齊可能會大於等於對應"視頻的寬"
    */
    uint8_t *src_buffer[4],*dst_buffer[4];
    int     src_linesize[4],dst_linesize[4];
    const AVPixFmtDescriptor *pixfmtDest = av_pix_fmt_desc_get(src_pix_fmt);
    
    // 一、獲取視頻轉換上下文
    /**
     * 方式一:
     *  1、參數1 2 3:轉換源數據寬,高,像素格式
     *  2、參數4 5 6:轉換后數據寬,高,像素格式
     *  3、參數7:如果進行縮放,所采用的的縮放算法
     *  4、參數8:轉換源數據的 SwsFilter 參數
     *  5、參數9:轉換后數據的 SwsFilter 參數
     *  6、參數10:轉換縮放算法的相關參數 const double* 類型數組
     *  7、8 9 10三個參數一般傳遞NULL,即采用默認值即可
     *  8、SWS_BICUBIC性能比較好,SWS_FAST_BILINEAR在性能和速度之間有一個比好好的平衡,SWS_POINT的效果比較差。
     */
    SwsContext *swsCtx = NULL;
#if 1
    swsCtx = sws_getContext(src_w,src_h,src_pix_fmt,
                                dst_w,dst_h,dst_pix_fmt,
                                SWS_BICUBIC,
                                NULL,NULL,
                                NULL
                                );
    if (swsCtx == NULL) {
        LOGD("sws_getContext fail");
        return;
    }
#else
    // 方式二;此方式可以設置更多的轉換屬性,比如設置YUV的顏色取值范圍是MPEG還是JPEG
    swsCtx = sws_alloc_context();
//    const AVOption *opt = NULL;
//    LOGD("輸出 AVOption的所有值===>");
    /** 基於AVClass的結構體。
     *  1、所有基於AVClass的結構體都可以用av_opt_set_xxx()函數和av_opt_get_xxx()函數來設置和獲取結構體對應的屬性,注意:AVClass
     *  必須是該結構體的第一個屬性。這個結構體的每一個屬性是AVOption結構體,包含了屬性名,屬性值等等。存儲於AVClass的
     *  const struct AVOption *option中
     *  2、ffmpeg的常用結構體都是基於AVClass的,比如AVCodec,SwsContext等等。這些結構體對應屬性的屬性名在各個目錄的
     *  options.c中,比如SwsContext的就在libswscale目錄下的options.c文件中定義,並且有默認值
     *  3、av_opt_next()可以遍歷基於AVClass的結構體的屬性對應的AVOption結構體
     */
//    while ((opt = av_opt_next(swsCtx, opt)) != NULL) {
//        LOGD("name:%s help:%s type %s",opt->name,opt->help,nameForOptionType(opt->type));
//    }
    
    /** 遇到問題:視頻進行壓縮后變形
     *  原因分析:因為下面dstw的參數寫成了src_w導致變形,設置爭取即可
     */
    av_opt_set_pixel_fmt(swsCtx,"src_format",src_pix_fmt,AV_OPT_SEARCH_CHILDREN);
    av_opt_set_int(swsCtx, "srcw", src_w, AV_OPT_SEARCH_CHILDREN);
    av_opt_set_int(swsCtx,"srch",src_h,AV_OPT_SEARCH_CHILDREN);
    /** 設置源的AVColorRange
     *  AVCOL_RANGE_JPEG:以JPEG為代表的標准中,Y、U、V的取值范圍都是0-255。FFmpeg中稱之為“jpeg” 范圍。
     *  AVCOL_RANGE_MPEG:一般用於以Rec.601為代表(還包括BT.709 / BT.2020)的廣播電視標准Y的取值范圍是16-235,
     *  U、V的取值范圍是16-240。FFmpeg中稱之為“mpeg”范圍。
     *  默認是MPEG范圍,對於源來說 要與實際情況一致,這里源的實際范圍是MPEG范圍
     */
    av_opt_set_int(swsCtx,"src_range",0,AV_OPT_SEARCH_CHILDREN);
    
    av_opt_set_pixel_fmt(swsCtx,"dst_format",dst_pix_fmt,AV_OPT_SEARCH_CHILDREN);
    av_opt_set_int(swsCtx,"dstw",dst_w,AV_OPT_SEARCH_CHILDREN);
    av_opt_set_int(swsCtx,"dsth",dst_h,AV_OPT_SEARCH_CHILDREN);
    av_opt_set_int(swsCtx,"dst_range",1,AV_OPT_SEARCH_CHILDREN);
    av_opt_set_int(swsCtx,"sws_flags",SWS_BICUBIC,AV_OPT_SEARCH_CHILDREN);
    if (sws_init_context(swsCtx, NULL, NULL) < 0) {
        LOGD("sws_init_context fail");
        sws_freeContext(swsCtx);
        return;
    }
#endif
    
    LOGD("src_fmt %s dst_fmt %s",av_get_pix_fmt_name(src_pix_fmt),av_get_pix_fmt_name(dst_pix_fmt));
    
    // 二、分配內存 轉換前和轉換后的
    /** av_image_alloc()返回分配內存的大小
    *  1、該大小大於等於視頻數據實際占用的大小,當最后一個參數為1時或者視頻長寬剛好滿足字節對齊時
    * 兩者會相等。
    *  2、linesize對應視頻的寬,它的值會大於等於視頻的寬,在將視頻數據讀入這個內存或者從這個內存取出數據時都要用實際的寬的值
    *  3、該函數分配的內存實際上一塊連續的內存,只不過src_data數組的指針分別指向了這塊連續內存上對應各個planner
    *  上的首地址
    */
    int src_size = av_image_alloc(src_buffer,src_linesize,src_w,src_h,src_pix_fmt,16);
    LOGD("size %d linesize[0] %d linesize[1] %d linesize[2] %d log2_chroma_h %d",src_size,src_linesize[0],src_linesize[1],src_linesize[2],pixfmtDest->log2_chroma_h);
    if (src_size < 0) {
        LOGD("src image_alloc <0 %d",src_size);
        return;
    }
    int dst_size = av_image_alloc(dst_buffer,dst_linesize,dst_w,dst_h,dst_pix_fmt,16);
    if (dst_size < 0) {
        LOGD("dst_size < 0 %d",dst_size);
        return;
    }
    
    // 三、進行轉換
    FILE *srcFile = fopen(srcyuvPath.c_str(), "rb");
    if (srcFile == NULL) {
        LOGD("srcFile is NULL");
        return;
    }
    FILE *dstFile = fopen(dstyuvPath.c_str(), "wb+");
    if (dstFile == NULL) {
        LOGD("dstFile is NULL");
        return;
    }
    
    while (true) {
        
        // 讀取YUV420P的方式;注意這里用的是src_w而非linesize中的值;yuv中uv的寬高為實際寬高的一半
        if (src_pix_fmt == AV_PIX_FMT_YUV420P) {
            size_t size_y = fread(src_buffer[0],1,src_w*src_h,srcFile);
            size_t size_u = fread(src_buffer[1],1,src_w/2*src_h/2,srcFile);
            size_t size_v = fread(src_buffer[2],1,src_w/2*src_h/2,srcFile);
            if (size_y <= 0 || size_u <= 0 || size_v <= 0) {
                LOGD("size_y %d size_u %d size_v %d",size_y,size_u,size_v);
                break;
            }
        } else if (src_pix_fmt == AV_PIX_FMT_RGB24) {
            size_t size = fread(src_buffer[0],1,src_w*src_h*3,srcFile);
            if (size <= 0) {
                LOGD("size %d",size);
                break;
            }
        } else if (src_pix_fmt == AV_PIX_FMT_NV12) {
            size_t size_y = fread(src_buffer[0],1,src_w * src_h,srcFile);
            size_t size_uv = fread(src_buffer[1],1,src_w * src_h/2,srcFile);
            if (size_y <= 0 || size_uv <= 0) {
                LOGD("size_y %d size_uv %d",size_y,size_uv);
                break;
            }
        } else if (src_pix_fmt == AV_PIX_FMT_NV21) {
            size_t size_y = fread(src_buffer[0],1,src_w * src_h,srcFile);
            size_t size_vu = fread(src_buffer[1],1,src_w * src_h/2,srcFile);
            if (size_y <= 0 || size_vu <= 0) {
                LOGD("size_y %d size_vu %d",size_y,size_vu);
                break;
            }
        }
        
        /** 參數1:轉換上下文
        *  參數2:轉換源數據內存地址
        *  參數3:轉換源的linesize數組
        *  參數4:從源數據的撒位置起開始處理數據,一般是處理整個數據,傳0
        *  參數5:換換源數據的height(視頻的高)
        *  參數6:轉換目的數據內存地址
        *  參數7:轉換目的linesize數組
        *  返回:轉換后的目的數據的height(轉換后的視頻的實際高)
        */
        int size = sws_scale(swsCtx,src_buffer,src_linesize,0,src_h,
                  dst_buffer,dst_linesize);
        LOGD("size == %d",size);
        
        if (dst_pix_fmt == AV_PIX_FMT_YUV420P) {
            fwrite(dst_buffer[0],1,dst_w*dst_h,dstFile);
            fwrite(dst_buffer[1],1,dst_w/2*dst_h/2,dstFile);
            fwrite(dst_buffer[2],1,dst_w/2*dst_h/2,dstFile);
        } else if (dst_pix_fmt == AV_PIX_FMT_RGB24) {
            fwrite(dst_buffer[0],1,dst_w*dst_h*3,dstFile);
        } else if (dst_pix_fmt == AV_PIX_FMT_NV12) {
            fwrite(dst_buffer[0],1,dst_w*dst_h,dstFile);
            fwrite(dst_buffer[1],1,dst_w*dst_h/2,dstFile);
        } else if (dst_pix_fmt == AV_PIX_FMT_NV21) {
           fwrite(dst_buffer[0],1,dst_w*dst_h,dstFile);
           fwrite(dst_buffer[1],1,dst_w*dst_h/2,dstFile);
        }
    }
    
    
    sws_freeContext(swsCtx);
    fclose(srcFile);
    fclose(dstFile);
    av_freep(&src_buffer[0]);
    av_freep(&dst_buffer[0]);
}

項目代碼:
示例地址

示例代碼位於cppsrc目錄下文件
videoresample.hpp
videoresample.cpp

項目下示例可運行於iOS/android/mac平台,工程分別位於demo-ios/demo-android/demo-mac三個目錄下,可根據需要選擇不同平台

from:https://www.jianshu.com/p/7ddff595a390


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM