天天看點

Android FFmpeg + OpenGL ES YUV Player

作者:音視訊開發T哥

1、FFmpeg解出YUV幀資料

1.1 方法介紹

打開封裝格式上下文

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視訊原始資料進行重裁剪

2、Demo實作

2.1 流程簡介

Android FFmpeg + OpenGL ES YUV Player

C++音視訊學習資料免費擷取方法:關注音視訊開發T哥,點選「連結」即可免費擷取2023年最新C++音視訊開發進階獨家免費學習大禮包!

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

原文連結:Android FFmpeg + OpenGL ES YUV Player - 鎺橀噾