FFmpeg视频播放(解封装)

2,083 阅读5分钟

ffmpeg 快速浏览

​ 视频编解码通常有分软编解码ffmpeg,以及硬编码MediaCodec,硬编效率高、速度快但兼容性不好,这里我们选择FFmpeg。FFmpeg还可以集成其它的编解码库,比如x264, faac, lamc, fdkaac等,市面上大多数视频网站编解码也都是采用对FFmpeg进行封装软编吗,下面介绍FFmpeg主要的模块及功能:

libavformat

用于各种音视频封装格式的生成和解析,包括获取解码所需信息以生成解码上下文结构和读取音视频帧等功能;音视频的格式解析协议,为 libavcodec 分析码流提供独立的音频或视频码流源。

libavcodec

用于各种类型声音/图像编解码;该库是音视频编解码核心,实现了市面上可见的绝大部分解码器的功能,libavcodec 库被其他各大解码器 ffdshow,Mplayer 等所包含或应用。

libavfilter

filter(FileIO、FPS、DrawText)音视频滤波器的开发,如水印、倍速播放等。

libavtutil

包含一些公共的工具函数的使用库,包括算数运算 字符操作;

libswreasmple

原始音频格式转码

libswscale

​(原始视频格式转换)用于视频场景比例缩放、色彩映射转换;图像颜色空间或格式转换,如 rgb565,rgb888 等与 yuv420 等之间转换。

libpostproc+libavcodec

视频播放流程

Java层

这里简单说一下,详细可看源码,PlayActivity,Player等类。

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);
}

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