视频播放器之解码

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

解码

解码一般分成以下几步:

解码.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方法。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容