视频播放器之解码

作者: arvinljw | 来源:发表于2019-02-01 11:19 被阅读42次

    上一篇中解封装之后能得到每一帧的数据,这个数据如果是原始数据没有编码的,那么可以直接使用,音频和视频都是,但是往往都编码过的,不然数据量太大了,所以数据的解码就不可缺少了。

    解码

    解码一般分成以下几步:

    解码.jpg

    准备工作

    因为初始化解码器需要解封装提供解码器id,发送数据包需要解封装的帧数据,所以需要保存解封装中音视频信息的参数以及解封装之后的帧数据。

    这里我们创建一个类去保存参数信息,虽然目前就一个值,AVCodecParameters,但是之后音频还需要通道数和采样率,所以先把参数类封装一下,之后就添加属性就好了,例如叫做FFPrameters吧,如下:

    struct AVCodecParameters;
    
    class FFParameters {
    public:
        AVCodecParameters *params = 0;
    };
    

    然后修改Demux中的获取音视频参数的方法。核心方法就是params.params = ic->streams[re]->codecpar;

    其中re表示音频或视频流的索引。整体方法修改如下:

    定义:

    virtual FFParameters getVideoParams();
    virtual FFParameters getAudioParams();
    

    实现:

    FFParameters Demux::getVideoParams() {
        if (!ic) {
            return FFParameters();
        }
        int re = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO, -1, -1, 0, 0);
        if (re < 0) {
            LOG_E("av_find_best_stream video failed");
            return FFParameters();
        }
        videoStream = re;
        FFParameters params;
        params.params = ic->streams[re]->codecpar;
        return params;
    }
    
    FFParameters Demux::getAudioParams() {
        if (!ic) {
            return FFParameters();
        }
        int re = av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO, -1, -1, 0, 0);
        if (re < 0) {
            LOG_E("av_find_best_stream audio failed");
            return FFParameters();
        }
        audioStream = re;
        FFParameters params;
        params.params = ic->streams[re]->codecpar;
        return params;
    }
    

    然后帧数据可以定义一个FFData类,用来存储读取出来的每一帧数据,因为在解码时会用到解封装出来的帧数据,然后还需要保存这个数据是音频还是视频。如下:

    class FFData {
    
    public:
        bool isAudio = false;
        //保存解封装packet和解码frame的数据
        unsigned char *data = 0;
    }
    

    然后我们还需要在解封装的部分,把解封装的数据返回,以便解码时能获取到,也就是修改解封装的read方法,修改后如下:

    FFData Demux::read() {
        if (!ic) {
            return FFData();
        }
        AVPacket *pkt = av_packet_alloc();
        int re = av_read_frame(ic, pkt);
        if (re != 0) {
            av_packet_free(&pkt);
            return FFData();
        }
        FFData data;
        data.data = (unsigned char *) pkt;
        pkt->pts = (long long) (pkt->pts * (1000 * r2d(ic->streams[pkt->stream_index]->time_base)));
        if (pkt->stream_index == audioStream) {
            data.isAudio = true;
    //        LOG_I("read audio size = %d,pts = %lld", pkt->size, pkt->pts);
        } else if (pkt->stream_index == videoStream) {
            data.isAudio = false;
    //        LOG_I("read video size = %d,pts = %lld", pkt->size, pkt->pts);
        } else {
            av_packet_free(&pkt);
            return FFData();
        }
        return data;
    }
    

    其中返回数据类型变了,然后在返回之前先不把解封装数据清理掉,这一步会留到解码完这一帧之后去清理,所以我们的FFData类还需要增加一个清理的方法:

    extern "C" {
    #include <libavcodec/avcodec.h>
    }
    void FFData::clear() {
        if (!data) {
            return;
        }
        av_packet_free((AVPacket **) &data);
        data = 0;
    }
    

    说到清理,我们在解封装完成后,也应该关闭解封装上下文,所以在Demux中增加:

    void Demux::close() {
        if (ic) {
            avformat_close_input(&ic);
        }
    }
    

    然后再在cpp文件下创建Decode类,并在CMakeLists中申明,当然前边新定义的FFParameters和FFData都需要在CMakeLists中申明。解码按照流程图可以定义三个方法:初始化,发送数据包,接收数据包,如下:

    public:
        virtual void init(FFParameters params);
    
        virtual void sendPacket(FFData data);
    
        virtual FFData receivePacket();
    

    这样准备工作基本完成。

    初始化解码器

    初始化解码器,又可以分为:

    • 查找解码器
    • 创建解码上下文,并复制参数
    • 打开解码器

    对应着几个核心方法:

    AVCodec *avcodec_find_decoder(enum AVCodecID id);//查找解码器
    AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);//创建解码器上下文
    int avcodec_parameters_to_context(AVCodecContext *codec,
                                      const AVCodecParameters *par);//复制参数到解码器上下文
    int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);//打开解码器                                  
    

    其中我们解码器上下文会在发送数据包和接收数据包的时候也会用到,所以会把它作为属性。

    这三个方法的具体实现如下:

    void Decode::init(FFParameters params) {
        avcodec_register_all();
        if (!params.params) {
            LOG_E("Decode init params is empty");
            return;
        }
        AVCodecParameters *p = params.params;
        //查找解码器
        AVCodec *codec = avcodec_find_decoder(p->codec_id);
        if (!codec) {
            LOG_E("avcodec_find_decoder %d failed", p->codec_id);
            return;
        }
        LOG_I("avcodec_find_decoder %d success", p->codec_id);
    
        //创建解码器上下文,并复制参数
        codecContext = avcodec_alloc_context3(codec);
        avcodec_parameters_to_context(codecContext, p);
        //解码线程数量
        codecContext->thread_count = 8;
    
        //打开解码器
        int re = avcodec_open2(codecContext, 0, 0);
        if (re != 0) {
            char buff[1024] = {0};
            av_strerror(re, buff, sizeof(buff));
            LOG_E("%s", buff);
            return;
        }
        LOG_I("avcodec_open2 success");
    }
    

    这样就初始化好解码器了。

    发送数据包

    这一步就是使用解封装得到的包数据,发送给解码队列即可,核心方法就是avcodec_send_packet,参数需要解码器上下文和包数据,实现如下:

    void Decode::sendPacket(FFData data) {
        if (!data.data) {
            return;
        }
        if (!codecContext) {
            return;
        }
    
        int re = avcodec_send_packet(codecContext, (AVPacket *) data.data);
        if (re != 0) {
            LOG_E("avcodec_send_packet failed");
            return;
        }
    }
    

    这样发送数据包就完成了。

    接收数据包

    这一步需要注意一点就是发送一个数据包给解码器队列,可能需要调用多次接收数据包才能获取完成,核心函数是avcodec_receive_frame,参数是解码器上下文和解码后的到的帧数据,这个帧数据,需要手动分配空间,当然也需要手动清理空间,清理稍后再说,因为帧数据每次都会覆盖上一次的数据,所以可以重复利用,没必要每次都申请空间,所以可以作为属性,具体实现如下:

    FFData Decode::receivePacket() {
        if (!codecContext) {
            return FFData();
        }
        if (!frame) {
            frame = av_frame_alloc();
        }
        int re = avcodec_receive_frame(codecContext, frame);
        if (re != 0) {
            return FFData();
        }
        FFData data;
    
        data.data = (unsigned char *) frame;
        data.format = frame->format;
        data.pts = frame->pts;
        memcpy(data.decodeData, frame->data, sizeof(data.decodeData));
        if (codecContext->codec_type == AVMEDIA_TYPE_VIDEO) {
            data.size = (frame->linesize[0] + frame->linesize[1] + frame->linesize[2]) * frame->height;
            data.width = frame->width;
            data.height = frame->height;
        } else {
            data.size = av_get_bytes_per_sample((AVSampleFormat) frame->format) * frame->nb_samples +
                        frame->channels;
        }
        LOG_I("receive frame data size = %d,pts = %lld", data.size, data.pts);
        return data;
    }
    

    可以注意到,FFData再次多添加了一些属性,用来存储解码之后的数据,包括数据类型,pts,解码数据,数据大小,视频数据的宽高等。因为这在之后的音视频显示和播放中会使用到。其中frame是接收到的解码数据,可以重复使用,所以可以作为解码的属性,在最后的清理中再清除空间。

    其中数据的大小计算方式音频和视频不一样,而且即使这样算出来的大小也可能有错,因为视频数据对齐也很关键,会在之后的适配中处理不同视频类型的数据对齐问题。

    最后也需要在解码完把数据清理:

    void Decode::close() {
        if (frame) {
            av_frame_free(&frame);
        }
        if (codecContext) {
            avcodec_flush_buffers(codecContext);
            avcodec_close(codecContext);
            avcodec_free_context(&codecContext);
        }
    }
    

    这样解码部分的代码也基本完成,然后再把解码和解封装联系起来。

    关联解封装和解码

    关联部分,暂时为了方便还是在native-lib.cpp文件中写,修改如下:

    void decodeData(Decode *decode, FFData data);
    
    extern "C"
    JNIEXPORT void JNICALL
    Java_net_arvin_ffmpegtest_FFmpegUtil_init(JNIEnv *env, jclass type) {
        if (!demux) {
            demux = new Demux();
            demux->init();
        }
    }
    
    extern "C"
    JNIEXPORT void JNICALL
    Java_net_arvin_ffmpegtest_FFmpegUtil_open(JNIEnv *env, jclass type, jstring url_) {
        const char *url = env->GetStringUTFChars(url_, 0);
    
        if (demux) {
            demux->open(url);
        }
        if (!audioDecode) {
            audioDecode = new Decode();
            audioDecode->init(demux->getAudioParams());
        }
        if (!videoDecode) {
            videoDecode = new Decode();
            videoDecode->init(demux->getVideoParams());
        }
    
        env->ReleaseStringUTFChars(url_, url);
    }
    
    
    extern "C"
    JNIEXPORT void JNICALL
    Java_net_arvin_ffmpegtest_FFmpegUtil_read(JNIEnv *env, jclass type) {
        if (!demux) {
            return;
        }
        bool re = true;
        while (re) {
            FFData data = demux->read();
            re = data.data != 0;
            if (re) {
                if (data.isAudio) {
                    decodeData(audioDecode, data);
                } else {
                    decodeData(videoDecode, data);
                }
            }
        }
    }
    
    void decodeData(Decode *decode, FFData data) {
        if (!decode) {
            return;
        }
    
        decode->sendPacket(data);
        while (true) {
            FFData frame = decode->receivePacket();
            if (frame.data == 0) {
                break;
            }
        }
        data.clear();
    }
    
    extern "C"
    JNIEXPORT void JNICALL
    Java_net_arvin_ffmpegtest_FFmpegUtil_close(JNIEnv *env, jclass type) {
        if (demux) {
            demux->close();
        }
        if (audioDecode) {
            audioDecode->close();
        }
        if (videoDecode) {
            videoDecode->close();
        }
    }
    

    这部分是我的native-lib的代码,注意不要直接全部拷贝过去,因为对应的类名不一致,会找不到方法的。其中在解封装打开完成之后,初始化解码器,然后在解封装读取到数据的时候,判断音频还是视频交给不同的解码器去处理数据,具体数据的打印在解码器的接收数据函数中。

    然后在界面中,需要新加一个清除资源按钮,然后在FFmpegUtil中增加一个close方法,点击清楚资源按钮时调用FFmpegUtil的close方法。

    这样解码部分也就基本完成。

    相关文章

      网友评论

        本文标题:视频播放器之解码

        本文链接:https://www.haomeiwen.com/subject/blarsqtx.html