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);
}
复制代码

下篇进入视频、音频解码,音视频同步处理等流程,并附上源码地址。

相关文章
相关标签/搜索