1.利用FFMpeg進行MP4視頻轉YUV格式,2.ffmpeg解碼MP4後用surfaceview播放

FFMpegDemo

ffmpeg play video from android

1 利用FFMpeg進行MP3視頻轉YUV格式

理論:

YUV,是一種顏色編碼方法

詳細看這裏

https://blog.csdn.net/junzia/article/details/76315120

爲什麼需要轉yuv格式

現在絕大多數視頻解碼後播放的格式都是YUV 所以需要做下YUV格式

一個通道.
前面放Y 後面放UV 比例是 4:1:1

視頻yuv 音頻 是pcm

YUV

  • “Y”表示明亮度(Luminance、Luma),“U”和“V”則是色度、濃度(Chrominance、Chroma)。

封裝格式: MP4 rmvb等
編碼格式: 對應響應的編碼 MPEG2,MPEG4,H.264

視頻播放一般有兩個 流 音頻流 視頻流, 有時還有個一流 是字幕流

下面開始預先操作

  1. 把ffmpeg 的so庫 還有頭文件導入到android studio 工程中去.
  2. 在native-lib中導入頭文件.
extern "C" {
//編碼
#include "libavcodec/avcodec.h"
//封裝格式處理
#include "libavformat/avformat.h"
//像素處理
#include "libswscale/swscale.h"
}
  1. 在手機根目錄放個mp4視頻

視頻信息
image

可以看到:

  • 封裝格式mp4
  • 編碼格式avc1格式

大致流程圖:

image

代碼

註釋都有

#include <jni.h>
#include <string>
#include <jni.h>
#include <string>
#include <android/log.h>

//extern "C" 主要作用就是爲了能夠正確實現C++代碼調用其他C語言代碼 加上extern "C"後,會指示編譯器這部分代碼按C語言的進行編譯,而不是C++的。
extern "C" {
//編碼
#include "libavcodec/avcodec.h"
//封裝格式處理
#include "libavformat/avformat.h"
//像素處理
#include "libswscale/swscale.h"
}

#define LOGI(FORMAT, ...) __android_log_print(ANDROID_LOG_INFO,"jnilib",FORMAT,##__VA_ARGS__);
#define LOGE(FORMAT, ...) __android_log_print(ANDROID_LOG_ERROR,"jnilib",FORMAT,##__VA_ARGS__);
JNICALL extern "C"
JNIEXPORT void JNICALL
Java_androidrn_ffmpegdemo_MainActivity_openVideo(JNIEnv *env, jobject instance, jstring inputStr_,
                                                 jstring outStr_) {
    const char *inputStr = env->GetStringUTFChars(inputStr_, 0);
    const char *outStr = env->GetStringUTFChars(outStr_, 0);

    //無論編碼還是解碼 都要調用這個 註冊各大組件
    av_register_all();

    //獲取AVFormatContext  比特率 時長 文件路徑 流的信息(nustream) 都封裝在這裏面
    AVFormatContext *pContext = avformat_alloc_context();

    //AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options
    //上下文  文件名  打開文件格式 獲取信息(AVDictionary)  凡是AVDictionary字典 都是獲取視頻文件信息
    if (avformat_open_input(&pContext, inputStr, NULL, NULL) < 0) {
        LOGE("打開失敗");
        return;
    }

    //給nbstram填充信息
    if (avformat_find_stream_info(pContext, NULL) < 0) {
        LOGE("獲取信息失敗");
        return;
    }

    //找到視頻流
    int video_stream_ids = -1;
    for (int i = 0; i < pContext->nb_streams; ++i) {
        LOGE("循環 %d", i);
        //如果填充的視頻流信息 -> 編解碼,解碼器 -> 流的類型 == 視頻的類型
        //codec 每一個流 對應的解碼上下文 codec_type 流的類型
        if (pContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
            video_stream_ids = i;
        }
    }
    // 獲取到解碼器上下文
    AVCodecContext *pCodecCtx = pContext->streams[video_stream_ids]->codec;
    //解碼器
    AVCodec *pCodex = avcodec_find_decoder(pCodecCtx->codec_id);
    //打開解碼器  爲什麼avcodec_open2 版本升級的原因
    if (avcodec_open2(pCodecCtx, pCodex, NULL) < 0) {
        LOGE("解碼失敗");
        return;
    }

    //得到解封裝 讀取 解封每一幀 讀取每一幀的壓縮數據
    //初始化avpacket 分配內存  FFMpeg 沒有自動分配內存 必須手動分匹配手動釋放  不過有分配函數
    AVPacket *packet = (AVPacket *) av_malloc(sizeof(AVPacket));

    //初始化packet  每一個包是一個完整的數據幀,來暫存解複用之後、解碼之前的媒體數據(一個音/視頻幀、一個字幕包等)及附加信息(解碼時間戳、顯示時間戳、時長等)
    av_init_packet(packet);

    //初始化AVFrame
    AVFrame *frame = av_frame_alloc();

    //目的數據 YUV的frame 聲明yuvFrame
    AVFrame *yuvFrame = av_frame_alloc();
    //給yuv 緩衝區初始化
    //avpicture_get_size 計算給定寬度和高度的圖片的字節大小 如果以給定的圖片格式存儲。
    // uint8_t 8位無符號整型數(int)
    uint8_t *out_buffer = (uint8_t *) av_malloc(
            avpicture_get_size((AV_PIX_FMT_YUV420P), pCodecCtx->width, pCodecCtx->height));

    //填充YUM緩衝區
    int re = avpicture_fill(reinterpret_cast<AVPicture *>(yuvFrame), out_buffer, AV_PIX_FMT_YUV420P,
                            pCodecCtx->width, pCodecCtx->height);

    LOGE("寬 %d  高 %d",pCodecCtx->width,pCodecCtx->height);

    //獲取轉化的上下文   參數 通過解碼器的上下文獲取到   pCodecCtx->pix_fmt 是mp4的上下文
    SwsContext *swsContext = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,
                                            pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P,
                                            SWS_BILINEAR, NULL, NULL, NULL);

    int frame_count = 0;

    //目的轉化成YUV  寫到一個文件裏面去 聲明文件
    FILE *fp_yuv = fopen(outStr, "wb");


    //AVFormatContext *s, AVPacket *pkt  上下文,avpacket 是數據包的意思
    //packet 入參
    int got_frame;
    //解碼每一幀圖片
    while (av_read_frame(pContext, packet) >= 0) {
//        解封裝
        //參數
        //AVCodecContext *avctx, 解碼器的上下文
        // AVFrame *picture, 已經解封裝的frame,是直接能在手機上播放的frame
//        int *got_picture_ptr, 入參出參對象, 可以根據出參判斷是否解析完成
//        const AVPacket *avpkt 壓縮的packet數據


        //把packet壓縮的數據 解壓 賦給frame
        avcodec_decode_video2(pCodecCtx, frame, &got_frame, packet);

        //判斷是否讀完
        if (got_frame > 0) {
            LOGE("解碼第 %d 個 frame ",frame_count++);
            // 獲取到了frame數據  視頻像素的數據
            // 目的轉化爲yuv格式    sws_scale 函數
            sws_scale(swsContext, reinterpret_cast<const uint8_t *const *>(frame->data),
                      frame->linesize, 0, frame->height, yuvFrame->data, yuvFrame->linesize);

            //得到所有的大小 寬度乘以高度  解釋: 在R G B 中有多少像素 就是寬度乘以高度, 在YUV中 有多少像素是由Y決定的 如果只有Y 那麼只有亮度 就是黑白的
            int y_size = pCodecCtx->width * pCodecCtx->height;
            //Y亮度信息
            fwrite(yuvFrame->data[0], 1, y_size, fp_yuv);
            //色度
            fwrite(yuvFrame->data[1], 1, y_size / 4, fp_yuv);
            //濃度
            fwrite(yuvFrame->data[2], 1, y_size / 4, fp_yuv);
        }
        //釋放
        av_free_packet(packet);

    }
    fclose(fp_yuv);
    av_frame_free(&yuvFrame);
    av_frame_free(&frame);
    avcodec_close(pCodecCtx);
    avformat_free_context(pContext);

    env->ReleaseStringUTFChars(inputStr_, inputStr);
    env->ReleaseStringUTFChars(outStr_, outStr);
}








結果

image
image
image

  • sws_getContext 函數解析
// 初始化sws_scale
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);
參數int srcW, int srcH, enum AVPixelFormat srcFormat定義輸入圖像信息(寬、高、顏色空間(像素格式))
參數int dstW, int dstH, enum AVPixelFormat dstFormat定義輸出圖像信息寬、高、顏色空間(像素格式))。
參數int flags選擇縮放算法(只有當輸入輸出圖像大小不同時有效)
參數SwsFilter *srcFilter, SwsFilter *dstFilter分別定義輸入/輸出圖像濾波器信息,如果不做前後圖像濾波,輸入NULL
參數const double *param定義特定縮放算法需要的參數(?),默認爲NULL
函數返回SwsContext結構體,定義了基本變換信息。
如果是對一個序列的所有幀做相同的處理,函數sws_getContext只需要調用一次就可以了。
sws_getContext(w, h, YV12, w, h, NV12, 0, NULL, NULL, NULL);      // YV12->NV12 色彩空間轉換
sws_getContext(w, h, YV12, w/2, h/2, YV12, 0, NULL, NULL, NULL);  // YV12圖像縮小到原圖1/4
sws_getContext(w, h, YV12, 2w, 2h, YN12, 0, NULL, NULL, NULL);    // YV12圖像放大到原圖4倍,並轉換爲NV12結構

 sws_scale
 縮放SRCPLACE中的圖像切片並將其縮放

需要注意的是第四個參數srcSliceY 這個代表的是第一列要處理的位置,如果要從頭開始處理,直接填0即可
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[]);
*

 -C以前創建的縮放上下文

* SWSYGETCONTRONTHECT()

*@ PARAM SRCPLACE包含數組指向平面的指針

*源切片

*@ PARAM SrcReST包含每個平面的步幅的數組

*源圖像

*@ PARAM SRCLSICY在切片的源圖像中的位置

*進程,即數字(從開始計數)

*0)在切片的第一行的圖像中

*@ PARAM SRCLSICH源數組的高度,即

*切片中的行

*@ PARAM DST包含指向平面的指針的數組

*目的地形象

*@ PARAM-DSSTRIDE包含每個平面的步長的數組

*目的地形象

*@返回輸出條的高度


利用surfaceview播放

理論

解碼部分和上面的代碼基本上差不多.解碼出來會得到了rgbframe (和yuvframe一樣 不過格式不是YUV格式,因爲surfaceview不能識別YUV,只能識別RGB.).
主要問題是解碼後如何給surfaceview播放. ? c++ 裏面是利用 ANativeWindow 播放. ANativeWindow 播放需要一個緩衝區buffer.
這個buffer 可以通過rgbframe 一行一行的寫入緩衝區(ANativeWindow的緩衝區).

導入頭文件:

#include <android/native_window_jni.h> 導入頭文件後需要導入so庫實現, 在cmakelists裏面加上 可以和log 庫導入一樣,使用find_library,也可以直接在target_link_libraries加上android

#include <jni.h>
#include <string>
#include <jni.h>
#include <string>
#include <android/log.h>

//extern "C" 主要作用就是爲了能夠正確實現C++代碼調用其他C語言代碼 加上extern "C"後,會指示編譯器這部分代碼按C語言的進行編譯,而不是C++的。
extern "C" {
//編碼
#include "libavcodec/avcodec.h"
//封裝格式處理
#include "libavformat/avformat.h"
//像素處理
#include "libswscale/swscale.h"
//用於android 繪製圖像的
#include <android/native_window_jni.h>
#include <unistd.h>
}

#define LOGI(FORMAT, ...) __android_log_print(ANDROID_LOG_INFO,"jnilib",FORMAT,##__VA_ARGS__);
#define LOGE(FORMAT, ...) __android_log_print(ANDROID_LOG_ERROR,"jnilib",FORMAT,##__VA_ARGS__);

extern "C"
JNIEXPORT void JNICALL
Java_androidrn_ffmpegdemo_MyVideoView_render(JNIEnv *env, jobject instance, jstring input_,
                                             jobject surface) {
    const char *input = env->GetStringUTFChars(input_, 0);

    // TODO
    //無論編碼還是解碼 都要調用這個 註冊各大組件
    av_register_all();

    //獲取AVFormatContext  比特率 時長 文件路徑 流的信息(nustream) 都封裝在這裏面
    AVFormatContext *pContext = avformat_alloc_context();

    //AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options
    //上下文  文件名  打開文件格式 獲取信息(AVDictionary)  凡是AVDictionary字典 都是獲取視頻文件信息
    if (avformat_open_input(&pContext, input, NULL, NULL) < 0) {
        LOGE("打開失敗");
        return;
    }

    //給nbstram填充信息
    if (avformat_find_stream_info(pContext, NULL) < 0) {
        LOGE("獲取信息失敗");
        return;
    }

    //找到視頻流
    int video_stream_ids = -1;
    for (int i = 0; i < pContext->nb_streams; ++i) {
        LOGE("循環 %d", i);
        //如果填充的視頻流信息 -> 編解碼,解碼器 -> 流的類型 == 視頻的類型
        //codec 每一個流 對應的解碼上下文 codec_type 流的類型
        if (pContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
            video_stream_ids = i;
            break;
        }
    }
    // 獲取到解碼器上下文 獲取視頻編解碼器
    AVCodecContext *pCodecCtx = pContext->streams[video_stream_ids]->codec;
    //解碼器
    AVCodec *pCodex = avcodec_find_decoder(pCodecCtx->codec_id);
    //打開解碼器  爲什麼avcodec_open2 版本升級的原因
    if (avcodec_open2(pCodecCtx, pCodex, NULL) < 0) {
        LOGE("解碼失敗");
        return;
    }

    //得到解封裝 讀取 解封每一幀 讀取每一幀的壓縮數據
    //初始化avpacket 分配內存  FFMpeg 沒有自動分配內存 必須手動分匹配手動釋放  不過有分配函數
    AVPacket *packet = (AVPacket *) av_malloc(sizeof(AVPacket));

    //初始化packet  每一個包是一個完整的數據幀,來暫存解複用之後、解碼之前的媒體數據(一個音/視頻幀、一個字幕包等)及附加信息(解碼時間戳、顯示時間戳、時長等)
//    av_init_packet(packet);

    //初始化AVFrame
    AVFrame *frame = av_frame_alloc();

    //目的數據 RGB的frame 聲明RGBFrame
    AVFrame *rgbFrame = av_frame_alloc();
    //給RGB 緩衝區初始化 分配內存
    //avpicture_get_size 計算給定寬度和高度的圖片的字節大小 如果以給定的圖片格式存儲。
    // uint8_t 8位無符號整型數(int)
    uint8_t *out_buffer = (uint8_t *) av_malloc(
            avpicture_get_size((AV_PIX_FMT_RGBA), pCodecCtx->width, pCodecCtx->height));

    LOGE("render 寬 %d  render 高 %d", pCodecCtx->width, pCodecCtx->height);

//    //填充rgb緩衝區
    int re = avpicture_fill(reinterpret_cast<AVPicture *>(rgbFrame), out_buffer, AV_PIX_FMT_RGBA,
                            pCodecCtx->width, pCodecCtx->height);
    LOGE("申請YUM內存%d   ", re);


    //獲取轉化的上下文   參數 通過解碼器的上下文獲取到   pCodecCtx->pix_fmt 是mp4的上下文  SWS_BICUBIC 是級別 越往下效率越高清晰度越低
    //AV_PIX_FMT_RGBA  這個是需要轉化的格式
    SwsContext *swsContext = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,
                                            pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_RGBA,
                                            SWS_BICUBIC, NULL, NULL, NULL);
    //AVFormatContext *s, AVPacket *pkt  上下文,avpacket 是數據包的意思
    //packet 入參
    int got_frame;

    int length = 0;

    //獲取ANtiviewindow  ANativeWindow_fromSurface
    ANativeWindow *nativeWindow = ANativeWindow_fromSurface(env, surface);
    //聲明buffer 視頻的緩衝區   rgbFrame 的緩衝區
    ANativeWindow_Buffer outBuffer;

    int frame_count = 0;
    //解碼每一幀圖片
    while (av_read_frame(pContext, packet) >= 0) {
//        解封裝
        //參數
        //AVCodecContext *avctx, 解碼器的上下文
        // AVFrame *picture, 已經解封裝的frame,是直接能在手機上播放的frame
//        int *got_picture_ptr, 入參出參對象, 可以根據出參判斷是否解析完成
//        const AVPacket *avpkt 壓縮的packet數據

        if (packet->stream_index == video_stream_ids) {

            //把packet壓縮的數據 解壓 賦給默認frame
            length = avcodec_decode_video2(pCodecCtx, frame, &got_frame, packet);
            LOGE(" 獲得長度   %d ", length);

            //判斷是否讀完
            if (got_frame) {
                LOGE("解碼第 %d 個 frame ", frame_count++);
                //繪製之前 需要配置一些信息. 寬高 輸出的格式  可以修改 pCodecCtx->width 和height
                ANativeWindow_setBuffersGeometry(nativeWindow, pCodecCtx->width, pCodecCtx->height,
                                                 WINDOW_FORMAT_RGBA_8888);//AV_PIX_FMT_RGBA  這個也是上面轉化的格式

                //因爲surfaceview 只支持RGB所以 這裏不轉成YUV, 電視機 機頂盒都是YUV
                //TODO buffer很重要
                //先鎖定當前的window  和surfaceview 類似 需要先鎖定 , 第一個參數就是上面獲取的ANtiviewindow, 第二個參數是buffer 這個buffer非常重要他是需要繪製的緩衝區.
                ANativeWindow_lock(nativeWindow, &outBuffer, NULL);

                // 調用轉化方式,目的轉化爲RGB格式    sws_scale 函數
                sws_scale(swsContext, reinterpret_cast<const uint8_t *const *>(frame->data),
                          frame->linesize, 0, pCodecCtx->height, rgbFrame->data,
                          rgbFrame->linesize);

                //獲取window 被輸出的畫面的首地址  window首地址
                uint8_t *dst = static_cast<uint8_t *>(outBuffer.bits);

                //拿到一行有多少字節  爲什麼需要×4  rgba
                int destStride = outBuffer.stride * 4;//這裏就是window的一行數據 字節

                //獲取rgbframe(像素數據)的 首地址
                uint8_t *src = rgbFrame->data[0];
                //實際內存一行的數量
                int srcStride = rgbFrame->linesize[0];
                LOGE("實際內存第一行一行的數量 = %d", srcStride);
                //把rgbframe拷貝到緩衝區  到解碼的高度
                for (int i = 0; i < pCodecCtx->height; ++i) {

                    //dest 目的地址 src 源地址    n 數量
                    //void *memcpy(void *dest, const void *src, size_t n);
                    //從源src所指的內存地址的起始位置 開始拷貝n個字節 到目標dest 所指的內存地址的起始位置中
                    //window首地址  變化的  真正拷貝的字節數 srcStride
                    memcpy(dst + i * destStride,
                           reinterpret_cast<const void *>(src + i * srcStride),
                           srcStride);
                }//這個循環完成就完成拷貝了.
                LOGE("解鎖畫布 睡眠16毫秒 ");
                //解鎖畫布
                ANativeWindow_unlockAndPost(nativeWindow);
                //畫面繪製完之後 畫面停止16毫秒   usleep在#include <unistd.h> 中
                usleep(1000 * 16);
            }
        }
        //釋放
        av_free_packet(packet);
    }

    ANativeWindow_release(nativeWindow);

    av_frame_free(&rgbFrame);
    av_frame_free(&frame);
    avcodec_close(pCodecCtx);
    avformat_free_context(pContext);


    env->ReleaseStringUTFChars(input_, input);
}

結果:

image

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章