FFmpeg視頻播放(解封裝)

ffmpeg 快速瀏覽

​ 視頻編解碼一般有分軟編解碼ffmpeg,以及硬編碼MediaCodec,硬編效率高、速度快但兼容性很差,這裏咱們選擇FFmpeg。FFmpeg還能夠集成其它的編解碼庫,好比x264, faac, lamc, fdkaac等,市面上大多數視頻網站編解碼也都是採用對FFmpeg進行封裝軟編嗎,下面介紹FFmpeg主要的模塊及功能:java

libavformat

用於各類音視頻封裝格式的生成和解析,包括獲取解碼所需信息以生成解碼上下文結構和讀取音視頻幀等功能;音視頻的格式解析協議,爲 libavcodec 分析碼流提供獨立的音頻或視頻碼流源。網絡

libavcodec

用於各類類型聲音/圖像編解碼;該庫是音視頻編解碼核心,實現了市面上可見的絕大部分解碼器的功能,libavcodec 庫被其餘各大解碼器 ffdshow,Mplayer 等所包含或應用。ide

libavfilter

filter(FileIO、FPS、DrawText)音視頻濾波器的開發,如水印、倍速播放等。函數

libavtutil

包含一些公共的工具函數的使用庫,包括算數運算 字符操做;工具

libswreasmple

原始音頻格式轉碼post

libswscale

​(原始視頻格式轉換)用於視頻場景比例縮放、色彩映射轉換;圖像顏色空間或格式轉換,如 rgb565,rgb888 等與 yuv420 等之間轉換。網站

libpostproc+libavcodecui

視頻播放流程

Java層

這裏簡單說一下,詳細可看源碼,PlayActivity,Player等類。this

Datasource:播放源編碼

TinaPlayer, 控制視頻的開始,中止等狀態。

SurfaceView/TexureView: 用於視頻的顯示,提供Surface給Player

Native層

初始化

首先拿到Java層給過來的視頻源,傳給視頻流程處理的 TinaFFmpeg類

extern "C"
JNIEXPORT void JNICALL Java_tina_com_player_TinaPlayer_native_1prepare(JNIEnv *env, jobject instance, jstring dataSource_) {
    const char *dataSource = env->GetStringUTFChars(dataSource_, 0);
    callHelper = new JavaCallHelper(javaVM, env, instance);
    ffmpeg = new TinaFFmpeg(callHelper, dataSource);
    ffmpeg->setRenderFrameCallback(render);
    ffmpeg->prepare();
    env->ReleaseStringUTFChars(dataSource_, dataSource);
}

//經過構造方法 保存到 TinaFFmpeg
TinaFFmpeg::TinaFFmpeg(JavaCallHelper *callHelper, const char *dataSource) {
    //須要內存拷貝,不然會形成懸空指針
// this->dataSource = const_cast<char *>(dataSource);
    this->callHelper = callHelper;
    //strlen 得到字符串的長度,不包括\0
    this->dataSource = new char[strlen(dataSource) + 1];
    stpcpy(this->dataSource, dataSource);
}
複製代碼

TinaFFmpeg做爲處理整個視頻的解封裝,解碼,渲染,音視頻同步處理的類,封裝面向對象:

class TinaFFmpeg {
public:
    TinaFFmpeg(JavaCallHelper *javaCallHelper, const char *dataSource);
    ~TinaFFmpeg();
    void prepare();
    void _prepare();
    void start();
    void _start();
    void setRenderFrameCallback(RenderFrameCallback callback);
    void stop();
public:
    char *dataSource;
    pthread_t pid;
    pthread_t pid_play;
    AVFormatContext *formatContext = 0;
    JavaCallHelper *callHelper;
    AudioChannel *audioChannel = 0;//指針初始化最好賦值爲null
    VideoChannel *videoChannel = 0;
    bool isPlaying;
    RenderFrameCallback callback;
    pthread_t pid_stop;
};
複製代碼
解碼流程

TinaFFmpeg 解碼流程過程 ,調用FFmpeg中的函數,其中av_register_all()新版本中再也不調用。

解封裝(prepare)

prepare:解封裝,分離出視頻中的Video跟Audio信息,開啓線程處理:

void TinaFFmpeg::prepare() {
    //建立線程,task_prepare做爲處理線程的函數,this爲函數的參數
    pthread_create(&pid, 0, task_prepare, this);
}

//調用真正的處理函數_prepare中
void *task_prepare(void *arges) {
    TinaFFmpeg *ffmpeg = static_cast<TinaFFmpeg *>(arges);
    ffmpeg->_prepare();
    return 0;
}
複製代碼

調用_prepare,在TinaFFmpeg.h中建立AVFormatContext *formatContext = 0;從它身上拿到Vedio,Audio。獲取的方法是avformat_open_input(&formatContext, dataSource, 0, &options); 這個方法時耗時操做,有可能失敗,因此須要回調錯誤信息到Java中,須要調用JavaCallHelper來發射調用Java方法。

void TinaFFmpeg::_prepare() {

    //初始化網絡 讓ffmpeg 可以
    avformat_network_init();

    //1. 打開播放的媒體地址(文件、直播地址)
    //AVFormatContext 包含了視頻的信息(寬、高)
    //文件路徑不對、手機沒網
    //第三個參數:指示咱們打開媒體的格式(傳NUll, ffmpeg會推導是MP4仍是flv)
    //第四個參數:
    AVDictionary *options = 0;
    //設置超時時間 微秒
    av_dict_set(&options, "timeout", "5000000", 0);
    //耗時操做
    int ret = avformat_open_input(&formatContext, dataSource, 0, &options);

    av_dict_free(&options);

    //ret 不爲0表示 打開媒體失敗
    if (ret != 0) {
        LOGE("打開媒體失敗:%s", av_err2str(ret));
        callHelper->onError(THREAD_CHILD, FFMPEG_CAN_NOT_OPEN_URL);
        return;
    }

    //2. 查找音視頻中的流
    ret = avformat_find_stream_info(formatContext, 0);
    if (ret < 0) {
        LOGE("查找流失敗:%s", av_err2str(ret));
        callHelper->onError(THREAD_CHILD, FFMPEG_CAN_NOT_FIND_STREAMS);
        return;
    }
。。。。。。。。。。。。
  
}
複製代碼

JavaCallHelper,專門處理 Native層反射調用Java層方法,這裏只處理了Java中 傳入對象 instance(TinaPlayer)中的 onError、onPrepare方法。JNIEnv *env處理的是Java跟Native在同一個線程中,而不一樣線程時能夠經過JavaVM *vm來獲取。傳過來的instance對象須要經過 env->NewGlobalRef(instace)建立全局引用。

JavaCallHelper::JavaCallHelper(JavaVM *vm, JNIEnv *env, jobject instace) {
    this->vm = vm;
    //若是在主線程 回調
    this->env = env;
    // 一旦涉及到jobject 跨方法 跨線程 就須要建立全局引用
    this->instance = env->NewGlobalRef(instace);
    jclass clazz = env->GetObjectClass(instace);
    onErrorId = env->GetMethodID(clazz, "onError", "(I)V");
    onPrepareId = env->GetMethodID(clazz, "onPrepare", "()V");
}

JavaCallHelper::~JavaCallHelper() {
    env->DeleteGlobalRef(instance);
}

void JavaCallHelper::onError(int thread, int error) {
    //主線程
    if (thread == THREAD_MAIN) {
        env->CallVoidMethod(instance, onErrorId, error);
    } else {
        //子線程
        JNIEnv *env;
        //得到屬於我這一個線程的jnienv
        vm->AttachCurrentThread(&env, 0);
        env->CallVoidMethod(instance, onErrorId, error);
        vm->DetachCurrentThread();
    }
}
void JavaCallHelper::onPrepare(int thread) {
   。。。
}
複製代碼

解碼音視頻

經過 AVFormatContext *formatContext拿到相應的視頻、音頻流,獲取解碼器AVCodec,同時把解碼器上下文交AVCodecContext給對應的VideoChannel,AudioChannel來處理相應的解碼工做

void TinaFFmpeg::_prepare() {
    //耗時操做
    int ret = avformat_open_input(&formatContext, dataSource, 0, &options);
    av_dict_free(&options);
	。。。
    //2. 查找音視頻中的流
    ret = avformat_find_stream_info(formatContext, 0);
    if (ret < 0) {
        LOGE("查找流失敗:%s", av_err2str(ret));
        callHelper->onError(THREAD_CHILD, FFMPEG_CAN_NOT_FIND_STREAMS);
        return;
    }
    //nb_streams; 幾個流(幾段視頻/音頻)
    for (int i = 0; i < formatContext->nb_streams; ++i) {
        //可能表明是一個視頻,也能夠表明是一個音頻
        AVStream *stream = formatContext->streams[i];
        //包含 解碼這段流的工種參數信息
        AVCodecParameters *codecpar = stream->codecpar;
        //不管音頻、視頻,須要作的事情(得到解碼器)
        AVCodec *dec = avcodec_find_decoder(codecpar->codec_id);
        if (dec == NULL) {
            LOGE("查找解碼器失敗:%s", av_err2str(ret));
            callHelper->onError(THREAD_CHILD, FFMPEG_FIND_DECODER_FAIL);
            return;
        }
        //得到解碼器上下文
        AVCodecContext *context = avcodec_alloc_context3(dec);
        if (context == NULL) {
            LOGE("建立解碼上下文失敗:%s", av_err2str(ret));
            callHelper->onError(THREAD_CHILD, FFMPEG_ALLOC_CODEC_CONTEXT_FAIL);
            return;
        }
        //3. 設置上下文內的一些參數
        ret = avcodec_parameters_to_context(context, codecpar);
        if (ret < 0) {
            LOGE("設置解碼上下文參數失敗:%s", av_err2str(ret));
            callHelper->onError(THREAD_CHILD, FFMPEG_OPEN_DECODER_FAIL);
            return;
        }
        //4. 打開解碼器
        ret = avcodec_open2(context, dec, 0);
        if (ret != 0) {
            LOGE("打開解碼器失敗:%s", av_err2str(ret));
            callHelper->onError(THREAD_CHILD, FFMPEG_ALLOC_CODEC_CONTEXT_FAIL);
            return;
        }
        //單位
        AVRational time_base = stream->time_base;
        //音頻
        if (codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {//0
            audioChannel = new AudioChannel(i, context, time_base);
        } else if (codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {//視頻
            //幀率:單位時間內,須要顯示多少個圖片
            AVRational frame_rate = stream->avg_frame_rate;
            int fps = av_q2d(frame_rate);
            videoChannel = new VideoChannel(i, context, time_base, fps);
            videoChannel->setRenderFrameCallback(callback);
        }
    }
    if (!audioChannel && !videoChannel) {
        LOGE("沒有音視頻");
        callHelper->onError(THREAD_CHILD, FFMPEG_NOMEDIA);
        return;
    }
    LOGE("native prepare流程準備完畢");
    // 準備完了 通知java 你隨時能夠開始播放
    callHelper->onPrepare(THREAD_CHILD);
}
複製代碼

下篇進入視頻、音頻解碼,音視頻同步處理等流程,並附上源碼地址。

相關文章
相關標籤/搜索