FFmpeg and SDL Tutorial

在網路上看到一些教學,雖然好像有點過時了
但我覺得還是可以參考

就在這邊做翻譯邊記錄下來
原本的連結  http://dranger.com/ffmpeg/

前言:

前面先介紹了一些  FFmpeg  的特色跟優點
當我們跟著指示完成時,就會有一隻少於一千行的視訊播放器
而在製作撥放器的時候,會用到SDL來輸出

第一步:Making Screencaps

Overview

影片的檔案有一些基本成份,檔案的本身叫做container
container的例子有 AVI 跟 Quicktime
再來檔案通常會有 video stream 跟 audio stream,而 stream 的元素稱為 frame
每一個 stream,都被編碼成各種不同類型的codec,而codec 決定了怎麼編碼跟解碼
codec 的例子有 DivX 跟 MP3
Packets 是從 stream 讀出來,再解碼成 raw frames,是我們最終拿來應用的資料集合

簡單來說
10 OPEN video_stream FROM video.avi
20 READ packet FROM video_stream INTO frame
30 IF frame NOT COMPLETE GOTO 20
40 DO SOMETHING WITH frame
50 GOTO 20

用 FFmpeg 處理多媒體大部份就像這樣的流程,雖然有些程式 "DO SOMETHING" 非常複雜

Opening the File

首先我們要開啟檔案,開啟之前我們要先初始化 library

#include <avcodec.h>
#include <avformat.h>

int main(int argc, charg *argv[]){
av_register_all();

這會註冊所有在 library 裡面所有可用的 file formats and codecs
當檔案有對應的 format/codec 被開啟,他們就會被自動使用
要注意的是,註冊只需要做一次就可以了,所以我們在 main() 裡面呼叫

接下來就要開啟檔案了

AVFormatContext *pFormatCtx;

// Open video file
if(av_open_input_file(&pFormatCtx, argv[1], NULL, 0, NULL)  !=  0)
  return -1; // Couldn't open file

第一個引數會得到我們的檔案名稱
然後這個函式會讀取標頭檔以及儲存檔案格式 AVFormatContext 的結構
最後三個引數用來指定 file format, buffer size, and format options
如果設定成 0 或 NULL,libavformat 會自動檢測
這個函式只看標頭檔,接下來我們要檢查 stream 的資訊

// Retrieve stream information
if(av_find_stream_info(pFormatCtx)  <  0)
  return -1;  //  Couldn't find stream information

這個函式用適當資訊填充 pFormatCtx -> streams
我們可以用一個手動偵錯函式來看裡面有什麼

// Dump information about file onto standard error
dump_format(pFormatCtx, 0, argv[1], 0);

現在 pFormatCtx -> streams 是一個指標陣列
而 pFormatCtx -> nb_streams 是陣列大小
所以我們可以走訪這個陣列,直到我們發現 video stream

int i;
AVCodecContext *pCodeCtx;

// Find the first video stream
videoStream = -1;
for(i = 0 ; i < pFormatCtx->nb_streams ; i++)
  if(pFormatCtx->streams[i]->codec->codec_type==CODEC_TYPE_VIDEO) {
    videoStream = i;
    break;
  }
if(videoStream==-1)
  return -1;  // Didn't find a video stream

// Get a pointer to the codec context for the video stream
pCodeCtx=pFormatCtx->streams[videoStream]->codec;

關於 codec 的 stream 資訊,我們稱為 "codec context"
這包含了所有我們使用的 stream 資訊
但我們仍然必須找真正的 codec 並且開啟他

AVCodec *pCodec;

// Find the decoder for the video stream
pCodec = avcodec_find_decoder(pCodeCtx->codec_id);
if( pCodec==NULL )  {
  fprintf(stderr, "Unsupported codec!\n");
 return -1; // Codec not found
}
// Open codec
if(avcodec_open(pCodecCtx, pCodec) < 0 )
  return -1; // Could not open codec

Storing the Data

現在我們要找地方來存 frame

AVFrame *pFrame;

// Allocate video frame
pFrame = avcodec_alloc_frame();

因為我們計畫要輸出 PPM 的檔案,所以我們要把原生的 frame 轉成 RGB

// Allocate an AVFrame structure
pFrameRGB = avcodec_alloc_frame();
if( pFrameRGB == NULL )
  return -1;

雖然我們分配了 frame,但我們還是需要地方來放轉換好的 raw data
我們用 avpicture_get_size 來取得我們要多大的空間,並手動產生

uint8_t *buffer;
int numBytes;
// Determine required buffer size and allocate buffer
numBytes = avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width, pCodecCtx->height);
buffer = (uint8_t *)av_malloc(numBytes*sizeof(uint8_t));

av_malloc 是 FFmpeg 的 malloc

現在我們要使用 avpicture_fill 來連結我們的 frame 跟新的 buffer

// Assign appropriate parts of buffer to image planes in pFrameRGB
// Note that pFrameRGB ia an AVFrame, but AVFrame is a superset of AVPicture
avpicture_fill( (AVPicture *)pFrameRGB, buffer, PIX_FMT_RGB24, pCodecCtx->width,
                     pCodecCtx->height);

終於,我們已經準備好讀取 stream

Reading the Data

現在我們要用讀取 packet 的方式來讀取完整的 video stream
再解碼到我們的 frame,一旦完成之後,就去轉換並存檔

int frameFinished;
AVPacket packet;

i=0;
while( av_read_frame( pFormatCtx, &packet) >=0 ){
  // Is this a packet from the video stream?
  if( packet.stream_index == videoStream ) {
    // Decode video frame
    avcodec_decode_video(pCodeCtx, pFrame, &frameFinished, packet.data, packet.size );
 
    // Did we get a video frame?
    if(frameFinished) {
      // Convert the image from its native format to RGB
      img_convert( (AVPicture *)pFrameRGB, PIX_FMT_RGB24, (AVPicture*)pFrame,
                         pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height);
      // Save the frame to disk
      if( ++i <= 5 )
        SaveFrame(pFrameRGB, pCodecCtx->width, pCodecCtx->height, i);
    }//if

  }//if

  // Free the packet that was allocated by av_read_frame
  av_free_packet(&packet);

}//while

av_read_frame() 讀取 packet 並儲存到 AVPacket 結構裡
要注意我們只有分配 packet 結構
FFmpeg 為我們分配內部的資料指向 packet.data
而這等一下會被 av_free_packet() 給 free掉

avcodec_decode_video() 幫我們從 packet 轉換到 frame
總之在做完解碼 packet 後,我們可能沒有我們所需 frame 的所有資訊
所以當我們有下一張 frame 時,avcodec_decode_video()可以幫我們設定 frameFinished

再來我們使用 img_convert() 把原生 format 轉換成 RGB (pCodecCtx->pix_fmt)
要記得我們可以把 AVFrame 轉換成 AVPicture
最後我們就把 frame 的寬跟高存進我們的 SaveFrame 函式

現在我們只要讓 SaveFrame 函式把 RGB 的資訊輸出成 PPM

video SaveFrame( AVFrame *pFrame, int width, int height, intframe) {
  File *pFile;
  char szFilename[32];
  int y;

  // Open file
  sprintf(szFilename, "frame%d.ppm", iFrame );
  pFile = fopen(szFilename, "wb");
  if( pFile == NULL )
    return;

  // Write header
  fprintf(pFile, "P6\n%d %d\n225\n", width, height);

  // Write pixel data
  for( y = 0 ; y < height ; y++)
    fwrite(pFrame->data[0]+y*pFrame->linesize[0], 1, width*3, pFile);

  // Close file
  fclose(pFile);
}

我們做了一些標準的檔案開啟,然後寫入 RGB
我們一次在檔案上面寫一行
因為 PPM 檔案其實就是用非常長的字串來表示 RGB 資訊
標頭檔指出了圖案的長跟寬還有 RGB 值的最大尺寸

現在回到main()
當我們讀取完 video stream,我們必須清除所有東西

// Free the RGB image
av_free(buffer);
av_free(pFrameRGB);

// Free the YUV frame
av_free(pFrame)

// Close the codec
avcodec_close(pCodeCtx);

// Close the video file
av_close_input_file(pFormatCtx);

return 0;

可以用以下的命令來執行

gcc -o tutorial01 tutorial01.c -lavutil -lavformat -lavcodec -lz -lavutil -lm

留言

這個網誌中的熱門文章

FF7 練功賺錢

鳥雞研究團

閃之軌跡 2 第一部 (3)