Android FFmpeg + OpenGL ES YUV Player

一、FFmpeg解出YUV帧数据

方法介绍

打开封装格式上下文

1)[avformat.h] avformat_open_input
打开媒体流(本地或者网络资源)得到封装格式上下文AVFormatContext
AVFormatContext包含了媒体的基本信息,如时长等

初始化解码器

2)[avformat.h] avformat_find_stream_info
从封装格式上下文中找到所有的媒体数据流AVStream,如音频流、视频流、字幕流等
AVStream中则包含了这些数据流的解码信息,如分辨率、采样率、解码器参数、帧率等等

3)[avcodec.h] avcodec_find_decoder
根据AVStream中的解码器id找到对应的解码器AVCodec,FFmpeg可以指定编译编解码器,因此实际编译的FFmpeg包中可能不包含流所对应的解码器;注意此时解码器还未打开,缺少相关的上下文信息

4)[avcodec.h] avcodec_alloc_context3、avcodec_parameters_to_context
avcodec_alloc_context3创建一个空的codec上下文结构体AVCodecContext
avcodec_parameters_to_context将AVStream中的codec参数信息导入到创建出来的AVCodecContext中

5)[avcodec.h] avcodec_open2
根据解码器上下文中所描述的解码器参数信息打开指定的解码器

6)[avformat.h] av_read_frame
从媒体上下文中读取一个数据包,这是一个压缩后的数据包AVPacket,需要经过解码器解码才能得到原始的媒体数据

解码

6)[avcodec.h] avcodec_send_packet
将av_read_frame读取到的数据压缩包送入解码器进行解码

7)[avcodec.h] avcodec_receive_frame
从解码去读取一帧媒体原始数据,注意(6)、(7)并不是一一对应的调用关系

重裁剪

8)[swscale.h] sws_getContext
由于我们送显时一般都是固定像素格式或者分辨率的,因此需要对解码出的视频原始数据进行重裁剪,将分辨率或者像素格式统一,这个方法主要是初始化重裁剪相关的上下文

9)[swscale.h] sws_scale
将(7)中的AVFrame视频原始数据进行重裁剪

二、Demo实现

2.1 流程简介

FFmpegVideoDecoder.jpg

2.2 代码示例

1)打开封装格式上下文

// 1、创建1个空的上下文对象
AVFormatContext* formatCtx = avformat_alloc_context();
// 2、打开指定资源地址的封装格式上下文
avformat_open_input(&formatCtx, sourcePath.c_str(), nullptr, nullptr);

2)找到视频流

// 3、从封装格式中解析出所有的流信息
avformat_find_stream_info(formatCtx, nullptr);
// 4、遍历流找到视频流
AVStream *videoStream = nullptr;
  for (int i = 0; i < formatCtx->nb_streams; i++) {
    if (formatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
      videoStream = formatCtx->streams[i];
      videoStreamIndex = i;
    }
}

3)根据流信息打开对应的解码器

// 5、找到解码器,这个时候解码器对象还没有打开,不能用
AVCodec *codec = avcodec_find_decoder(videoStream->codecpar->codec_id);
// 6、为该codec创建1个对应的空上下文
AVCodecContext* codecCtx = avcodec_alloc_context3(codec);
// 7、将指定流中限定的解码器参数输入解码器上下文中
avcodec_parameters_to_context(codecCtx, videoStream->codecpar);
// 8、通过解码器上下文打开解码器
avcodec_open2(codecCtx, codec, NULL);

// 以下为视频相关的信息

// 帧率
// 优先通过 avg_frame_rate 计算帧率,若 avg_frame_rate 为{x, 0} or {0, 1} 那么就使用 r_frame_rate
// av_q2d 就是 avg_frame_rate.num(分子) / avg_frame_rate.den(分母)
if (stream->avg_frame_rate.den == 0 || 
      (stream->avg_frame_rate.num == 0 && stream->avg_frame_rate.den == 1)) {
  frameRate = av_q2d(stream->r_frame_rate);
} else {
  frameRate = av_q2d(stream->avg_frame_rate);
}

// 每一帧的时间(毫秒)
frameMillions = 1000 / frameRate;

4)创建重采样上下文

swsContext = sws_getContext(
      decoder->Width(),         // 输入源宽度
      decoder->Height(),        // 输入源高度
      decoder->PixelFormat(),   // 输入源格式 YUV42 NV12 NV32...
      decoder->Width(),         // 输出源宽度
      decoder->Height(),        // 输出源高度
      AV_PIX_FMT_YUV420P,       // 输出源格式
      SWS_BICUBIC, nullptr, nullptr, nullptr);

if (!swsContext) {
    return false;
}

// 分配重采样的输出Frame
swsAVFrame = av_frame_alloc();
swsAVFrame->width = decoder->Width();
swsAVFrame->height = decoder->Height();
swsAVFrame->format = AV_PIX_FMT_YUV420P;

// 按照1字节对齐,给AVFrame创建缓冲区,重采样的数据送入此swsAVFrame中
int ret = av_frame_get_buffer(swsAVFrame, 1);

5)从AVFormatContext中读取数据包,放入到缓存队列中

while (1) {
  AVPacket *packet = av_packet_alloc();
  int ret = av_read_frame(formatCtx, packet);
  if (ret == AVERROR_EOF) {
    break;
  } else if (ret) {
    LOGE("%s av_read_frame error", __func__);
    av_packet_free(&packet);
    av_free(packet);
    packet = nullptr;
    break;
  }

  // 读取到一个视频数据包,方璐packet队列中
  if (packet->stream_index == videoStreamIndex) {
    packetQueue->PushPacket(packet);
  }
}

6)从缓存队列中取出AVPacket,送入解码器解码

AVFrame * Decode() {
  // 先从缓存中获取,avcodec_receive_frame 与 avcodec_send_packet 不是一一对应的
  AVFrame *avFrame = av_frame_alloc();
  if (avcodec_receive_frame(codecCtx, av_frame) == 0) {
    return avFrame;
  }

  while (1) {
    AVPacket *packet = packetQueue->GetTopOne();
    if (!packet) {
      av_frame_free(&avFrame);
      av_free(avFrame);
      avFrame = NULL;
      break;
    }

    // 向解码器发送压缩数据包
    int ret = avcodec_send_packet(codecCtx, packet);
    if (ret) {
      char buffer[128];
      av_make_error_string(buffer, 128, ret);
      LOGE("%s avcodec_send_packet error %s", __func__, buffer);
      av_frame_free(&avFrame);
      av_free(avFrame);
      avFrame = NULL;

      av_packet_free(&packet);
      av_free(packet);
      break;
    }

    // 从解码器获取解压缩后的原始包
    int code = avcodec_receive_frame(codecCtx, avFrame);

    av_packet_free(&packet);
    av_free(packet);

    if (code == 0) {
      break;
    } else if (code == AVERROR_EOF) {
      av_frame_free(&avFrame);
      av_free(avFrame);
      avFrame = NULL;
      break;
    }
  }

  return avFrame;
}

7)对解出的AVFrame进行重裁剪

sws_scale(swsContext, avFrame->data, avFrame->linesize, 0,
                avFrame->height, swsAVFrame->data, swsAVFrame->linesize);

8)将重采样后的AVFrame数据送入OpenGL显示

// 送显示速度控制,按照视频的帧率进行控制,确保流畅播放
long currentMillions = base::CurrentMillions();
long diff = currentMillions - lastMillions;

if (diff >= frameMillions) {
  lastMillions = currentMillions;
} else {
  long sleepMillions = frameMillions - diff;
  sleepMillions = sleepMillions > frameMillions ? frameMillions : sleepMillions;
  usleep(sleepMillions * 1000); // usleep 时微秒 1ms = 1000微秒
  lastMillions = currentMillions + sleepMillions;
}

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

推荐阅读更多精彩内容