动态

详情 返回 返回

《ESP32-S3使用指南—IDF版 V1.6》第四十三章視頻播放器實驗 - 动态 详情

第四十三章視頻播放器實驗

1)實驗平台:正點原子DNESP32S3開發板

2)章節摘自【正點原子】ESP32-S3使用指南—IDF版 V1.6

3)購買鏈接:https://detail.tmall.com/item.htm?&id=768499342659

4)全套實驗源碼+手冊+視頻下載地址:http://www.openedv.com/docs/boards/esp32/ATK-DNESP32S3.html

5)正點原子官方B站:https://space.bilibili.com/394620890

6)正點原子DNESP32S3開發板技術交流羣:132780729

155537c2odj87vz1z9vj6l

155537nfqovl2gg9faaol9

DNESP32S3的處理能力,不僅可以軟解碼音頻,還可以用來播放視頻!本章,我們將使用DNESP32S3開發板來播放AVI視頻,本章我們將實現一個簡單的視頻播放器。
本章分為如下幾個小節:
43.1 ES8388錄音簡介
43.2 硬件設計
43.3 程序設計
43.4 下載驗證

43.1 AVI&libjpeg簡介
本章,我們將使用libjepg(由IJG提供),來實現MJPEG編碼的AVI格式視頻播放,我們先來簡單介紹一下AVI和libjpeg。
43.1.1 AVI簡介
AVI是音頻視頻交錯(Audio Video Interleaved)的英文縮寫,它是微軟開發的一種符合RIFF文件規範的數字音頻與視頻文件格式,原先用於Microsoft Video forWindows (簡稱VFW)環境,現在已被多數操作系統直接支持。
AVI格式允許視頻和音頻交錯在一起同步播放,支持256色和RLE壓縮,但AVI文件並未限定壓縮標準,AVI僅僅是一個容器,用不同壓縮算法生成的AVI文件,必須使用相應的解壓縮算法才能播放出來。比如本章,我們使用的AVI,其音頻數據採用16位線性PCM格式(未壓縮),而視頻數據,則採用MJPEG編碼方式。
在介紹AVI文件前,我們要先來看看RIFF文件結構。AVI文件採用的是RIFF文件結構方式,RIFF(Resource Interchange File Format,資源互換文件格式)是微軟定義的一種用於管理WINDOWS環境中多媒體數據的文件格式,波形音頻WAVE,MIDI和數字視頻AVI都採用這種格式存儲。構造RIFF文件的基本單元叫做數據塊(Chunk),每個數據塊包含3個部分:
l 4字節的數據塊標記(或者叫做數據塊的ID)
l 數據塊的大小
l 數據
整個RIFF文件可以看成一個數據塊,其數據塊ID為RIFF,稱為RIFF塊。一個RIFF文件中只允許存在一個RIFF塊。RIFF塊中包含一系列的子塊,其中有一種子塊的ID為"LIST",稱為LIST塊,LIST塊中可以再包含一系列的子塊,但除了LIST塊外的其他所有的子塊都不能再包含子塊。
RIFF和LIST塊分別比普通的數據塊多一個被稱為形式類型(Form Type)和列表類型(List Type)的數據域,其組成如下:
l 4字節的數據塊標記(Chunk ID)
l 數據塊的大小
l 4字節的形式類型或者列表類型(ID)
l 數據
下面我們看看AVI文件的結構。AVI文件是目前使用的最複雜的RIFF文件,它能同時存儲同步表現的音頻視頻數據。AVI的RIFF塊的形式類型(Form Type)是AVI,它一般包含3個子塊,如下所述:
l 信息塊,一個ID為"hdrl"的LIST塊,定義AVI文件的數據格式。
l 數據塊,一個ID為 "movi"的LIST塊,包含AVI的音視頻序列數據。
l 索引塊,ID為"idxl"的子塊,定義"movi"LIST塊的索引數據,是可選塊(不一定有)。
接下來,我們詳細介紹下AVI文件的各子塊構造,AVI文件的結構如圖43.1.1所示:

image002

圖43.1.1 AVI文件結構圖
從上圖可以看出(注意‘AVI ’,是帶了一個空格的),AVI文件,由:信息塊(HeaderList)、數據塊(MovieList)和索引塊(Index Chunk)等三部分組成,下面,我們分別介紹這幾個部分。
1,信息塊(HeaderList)
信息塊,即ID為“hdrl”的LIST塊,它包含文件的通用信息,定義數據格式,所用的壓縮算法等參數等。hdrl塊還包括了一系列的子塊,首先是:avih塊,用於記錄AVI的全局信息,比如數據流的數量,視頻圖像的寬度和高度等信息,avih塊(結構體都有把BlockID和BlockSize包含進來,下同)的定義如下:

/* avih 子塊信息 */
typedef struct
{
    uint32_t BlockID;                 /* 塊標誌:avih==0X61766968 */
    uint32_t BlockSize; /* 塊大小(不包含最初8字節,也就是BlockID和BlockSize不在內*/
    uint32_t SecPerFrame;            /* 視頻幀間隔時間(單位為us) */
    uint32_t MaxByteSec;             /* 最大數據傳輸率,字節/秒 */
    uint32_tPaddingGranularity;    /* 數據填充的粒度 */
    uint32_t Flags;                   /* AVI文件的全局標記,比如是否含有索引塊等 */
    uint32_t TotalFrame;                 /* 文件總幀數 */
    uint32_t InitFrames;             /* 為交互格式指定初始幀數(非交互格式應該指定為0)*/
    uint32_t Streams;                 /* 包含的數據流種類個數,通常為2 */
    uint32_t RefBufSize;/* 建議讀取本文件的緩存大小(應能容納最大的塊)默認是1M字節*/
    uint32_t Width;                   /* 圖像寬 */
    uint32_t Height;                  /* 圖像高 */
    uint32_t Reserved[4];                /* 保留 */
}AVIH_HEADER;

這裏有很多我們要用到的信息,比如SecPerFrame,通過該參數,我們可以知道每秒鐘的幀率,也就知道了每秒鐘需要解碼多少幀圖片,才能正常播放。TotalFrame告訴我們整個視頻有多少幀,結合SecPerFrame參數,就可以很方便計算整個視頻的時間了。Streams告訴我們數據流的種類數,一般是2,即包含視頻數據流和音頻數據流。
在avih塊之後,是一個或者多個strl子列表,文件中有多少種數據流(即前面的Streams),就有多少個strl子列表。每個strl子列表,至少包括一個strh(Stream Header)塊和一個strf(Stream Format)塊,還有一個可選的strn(Stream Name)塊(未列出)。注意:strl子列表出現的順序與媒體流的編號(比如:00dc,前面的00,即媒體流編號00)是對應的,比如第一個strl子列表説明的是第一個流(Stream 0),假設是視頻流,則表徵視頻數據塊的四字符碼為“00dc”,第二個strl子列表説明的是第二個流(Stream 1),假設是音頻流,則表徵音頻數據塊的四字符碼為“01dw”,以此類推。
先看strh子塊,該塊用於説明這個流的頭信息,定義如下:

/* strh 流頭子塊信息(strh∈strl) */
typedef struct
{
uint32_tBlockID;        /* 塊標誌:strh==0X73747268*/
/* 塊大小(不包含最初的8字節,也就是BlockID和BlockSize不計算在內) */
uint32_tBlockSize;
uint32_tStreamType;
/* 數據流種類,vids(0X73646976):視頻;auds(0X73647561):音頻 */
uint32_t Handler;  
/* 指定流的處理者,對於音視頻來説就是解碼器,比如MJPEG/H264之類的 */
    uint32_t Flags;          /* 標記:是否允許這個流輸出?調色板是否變化? */
    uint16_t Priority;       /* 流的優先級(當有多個相同類型的流時優先級最高的為默認流)*/
    uint16_t Language;       /* 音頻的語言代號 */
    uint32_t InitFrames;    /* 為交互格式指定初始幀數 */
    uint32_t Scale;          /* 數據量, 視頻每幀的大小或者音頻的採樣大小 */
    uint32_t Rate;           /*Scale/Rate=每秒採樣數 */
    uint32_t Start;          /* 數據流開始播放的位置,單位為Scale */
    uint32_t Length;         /* 數據流的數據量,單位為Scale */
    uint32_t RefBufSize;    /* 建議使用的緩衝區大小 */
    uint32_t Quality;        /* 解壓縮質量參數,值越大,質量越好 */
    uint32_t SampleSize;    /* 音頻的樣本大小 */
    struct                     /* 視頻幀所佔的矩形 */
    {
        short Left;
        short Top;
        short Right;
        shortBottom;
    } Frame;
}STRH_HEADER;

這裏面,對我們最有用的即StreamType 和Handler這兩個參數了,StreamType用於告訴我們此strl描述的是音頻流(“auds”),還是視頻流(“vids”)。而Handler則告訴我們所使用的解碼器,比如MJPEG/H264等(實際以strf塊為準)。
然後是strf子塊,不過strf子塊,需要根據strh子塊的類型而定。
如果strh子塊是視頻數據流(StreamType=“vids”),則strf子塊的內容定義如下:

/* BMP結構體 */
typedef struct
{
    uint32_t BmpSize;         /*bmp結構體大小,包含(BmpSize在內) */
    long Width;                /* 圖像寬 */
    long Height;               /* 圖像高 */
    uint16_t  Planes;         /* 平面數,必須為1 */
    uint16_t  BitCount;       /* 像素位數,0X0018表示24位 */
    uint32_t  Compression;   /* 壓縮類型,比如:MJPEG/H264等 */
    uint32_t  SizeImage;     /* 圖像大小 */
    long XpixPerMeter;       /* 水平分辨率 */
    long YpixPerMeter;       /* 垂直分辨率 */
    uint32_t  ClrUsed;       /* 實際使用了調色板中的顏色數,壓縮格式中不使用 */
    uint32_t  ClrImportant;     /* 重要的顏色 */
}BMP_HEADER;

/* 顏色表 */
typedef struct
{
    uint8_t  rgbBlue;        /* 藍色的亮度(值範圍為0-255) */
    uint8_t  rgbGreen;       /* 綠色的亮度(值範圍為0-255) */
    uint8_t  rgbRed;          /* 紅色的亮度(值範圍為0-255) */
    uint8_t  rgbReserved;        /* 保留,必須為0 */
}AVIRGBQUAD;

/* 對於strh,如果是視頻流,strf(流格式)使STRH_BMPHEADER塊 */
typedef struct
{
    uint32_t BlockID;         /* 塊標誌,strf==0X73747266 */
uint32_tBlockSize;      /* 塊大小(不包含最初的8字節,也就是BlockID
和本BlockSize不計算在內) */
    BMP_HEADER bmiHeader;    /* 位圖信息頭 */
    AVIRGBQUAD bmColors[1];     /* 顏色表 */
}STRF_BMPHEADER;

這裏有3個結構體,strf子塊完整內容即:STRF_BMPHEADER結構體,不過對我們有用的信息,都存放在BMP_HEADER結構體裏面,本結構體對視頻數據的解碼起決定性的作用,它告訴我們視頻的分辨率(Width和Height),以及視頻所用的編碼器(Compression),因此它決定了視頻的解碼。本章例程僅支持解碼視頻分辨率小於屏幕分辨率,且編解碼器必須是MJPEG的視頻格式。
如果strh子塊是音頻數據流(StreamType=“auds”),則strf子塊的內容定義如下:

/* 對於strh,如果是音頻流,strf(流格式)使STRH_WAVHEADER塊 */
typedef struct
{
    uint32_t BlockID;        /* 塊標誌,strf==0X73747266 */
uint32_tBlockSize;      /* 塊大小(不包含最初的8字節,也就是BlockID
和本BlockSize不計算在內) */
    uint16_t FormatTag;      /* 格式標誌:0X0001=PCM,0X0055=MP3 */
    uint16_t Channels;       /* 聲道數,一般為2,表示立體聲 */
    uint32_t SampleRate;     /* 音頻採樣率 */
    uint32_t BaudRate;       /* 波特率 */
    uint16_t BlockAlign;        /* 數據塊對齊標誌 */
    uint16_t Size;            /* 該結構大小 */
}STRF_WAVHEADER;

本結構體對音頻數據解碼起決定性的作用,他告訴我們音頻信號的編碼方式(FormatTag)、聲道數(Channels)和採樣率(SampleRate)等重要信息。本章例程僅支持PCM格式(FormatTag=0X0001)的音頻數據解碼。
2、數據塊(MovieList)
信息塊,即ID為“movi”的LIST塊,它包含AVI的音視頻序列數據,是這個AVI文件的主體部分。音視頻數據塊交錯的嵌入在“movi”LIST塊裏面,通過標準類型碼進行區分,標準類型碼有如下4種:
1)“##db”(非壓縮視頻幀)
2)“##dc”(壓縮視頻幀)
3)“##pc”(改用新的調色板)
4)“##wb”(音頻幀)
其中##是編號,得根據我們的數據流順序來確定,也就是前面的strl塊。比如,如果第一個strl塊是視頻數據,那麼對於壓縮的視頻幀,標準類型碼就是:00dc。第二個strl塊是音頻數據,那麼對於音頻幀,標準類型碼就是:01wb。
緊跟着標準類型碼的是4個字節的數據長度(不包含類型碼和長度參數本身,也就是總長度必須要加8才對),該長度必須是偶數,如果讀到為奇數,則加1即可。我們讀數據的時候,一般一次性要讀完一個標準類型碼所表徵的數據,方便解碼。
3、索引塊(Index Chunk)
最後,緊跟在‘hdrl’列表和‘movi’列表之後的,就是AVI文件可選的索引塊。這個索引塊為AVI文件中每一個媒體數據塊進行索引,並且記錄它們在文件中的偏移(可能相對於‘movi’列表,也可能相對於AVI文件開頭)。本章我們用不到索引塊,這裏就不詳細介紹了。
關於AVI文件,我們就介紹到這,有興趣的朋友,可以再看看光盤:6,軟件資料à3,AVI學習資料.zip裏面的相關文檔。
43.1.2 libjpeg簡介
libjpeg是一個完全用C語言編寫的庫,包含了廣泛使用的JPEG解碼、JPEG編碼和其他的JPEG功能的實現。這個IJG庫由組織(Independent JPEG Group(獨立JPEG小組))提供並維護。libjepg,目前最新版本為v9f,可以在https://www.ijg.org/files/這個網站下載。libjpeg具有穩定、兼容性強和解碼速度較快等優點。
本章,我們使用libjpeg來實現MJPEG數據流的解碼,MJPEG數據流,其實就是一張張的JPEG圖片拼起來的圖片視頻流,只要能快速解碼JPEG圖片,就可以實現視頻播放。
前面的圖片顯示實驗我們使用了TJPGD實現JPEG解碼,大家可能會問,為什麼不直接用TJPGD來解碼呢?因為TJPG的特點是:佔用資源少,但是解碼速度慢。在DNESP32S3上,同樣一張320240的JPG圖片,用TJPGD來解碼,需要120多毫秒,而用libjpeg,則只需要50ms左右即可完成解碼,libjpeg的解碼速度明顯比TJPGD快了不少,使得解碼視頻成為可能。實際上,經過優化後的libjpeg,使用DNESP32S3不超頻的情況下,可以流暢播放480272@ 10幀的MJPEG視頻(帶音頻)。
關於libjpeg的移植和使用,在下載的libjpeg源碼裏面還有很多介紹,可以重點看:readme.txt、filelist.txt、install.txt和libjpeg.txt等,也可以參考光盤源碼進行移植與使用。
我們主要講解一下如何使用libjpeg來實現一個jpeg圖片的解碼,這個在libjpeg源碼裏面:example.c,這個文件裏面有簡單的示例,在libjpeg.txt裏面也有相關內容的介紹。我們主要簡要介紹一下example.c裏面標準解碼流程(示例代碼):

structmy_error_mgr
{
    struct jpeg_error_mgrpub;   /* 公共”字段 */
    jmp_buf setjmp_buffer;       /* 用於返回 */
};

typedef structmy_error_mgr * my_error_ptr;

/**
* @brief   JPEG解碼錯誤處理函數
* @param   無
* @retval  無
*/

METHODDEF(void)my_error_exit (j_common_ptr cinfo)
{
    my_error_ptr myerr = (my_error_ptr) cinfo->err;     /* 指向cinfo->err */
    (*cinfo->err->output_message) (cinfo);               /* 顯示錯誤信息 */
    longjmp(myerr->setjmp_buffer, 1);                  /* 跳轉到setjmp處 */
}

/**
* @brief   JPEG解碼函數
* @param   filename : 解碼文件
* @retval  無
*/
GLOBAL(int)read_JPEG_file(char* filename)
{
    structjpeg_decompress_struct cinfo;
    struct my_error_mgrjerr;       /* 錯誤處理結構體 */
    FILE *infile;                /* 輸入源文件 */
    JSAMPARRAY buffer;           /* 輸出緩存 */
    int row_stride;               /*physical row width in output buffer */

    if ((infile= fopen(filename, "rb")) == NULL) /* 嘗試打開文件 */
    {
        fprintf(stderr, "can'topen %s\n", filename);
        return 0;
    }

    /* 第一步,設置錯誤管理,初始化JPEG解碼對象 */
    cinfo.err =jpeg_std_error(&jerr.pub);  /* 建立JPEG錯誤處理流程 */
    jerr.pub.error_exit= my_error_exit;    /* 處理函數指向 my_error_exit */
    /*
     * 建立my_error_exit函數使用的返回上下文,當其他地方
     * 調用longjmp函數時,可以返回到這裏進行錯誤處理
     */
    if (setjmp(jerr.setjmp_buffer))
    {
        jpeg_destroy_decompress(&cinfo);   /* 釋放解碼對象資源 */
        fclose(infile);                      /* 關閉文件 */
        return 0;
    }
    jpeg_create_decompress(&cinfo);     /* 初始化解碼對象cinfo */
   
    /* 第二步,指定數據源(比如一個文件) */
    jpeg_stdio_src(&cinfo,infile);
   
    /* 第三步,讀取文件參數(通過jpeg_read_header函數) */
    (void)jpeg_read_header(&cinfo, TRUE);
   
    /* 第四步,設置解碼參數(這裏使用jpeg_read_header確認的默認參數),故無處理 */
   
    /* 第五步,開始解碼 */
    (void)jpeg_start_decompress(&cinfo);
   
    /* 在讀取數據之前,可以做一些處理,比如設定LCD窗口,設定LCD起始座標等 */
   
    /* 確定一樣有多少樣本 */
    row_stride = cinfo.output_width* cinfo.output_components;
    /* 確保buffer至少可以保存一行的數據樣本,併為其申請內存 */
    buffer = (*cinfo.mem->alloc_sarray)
        ((j_common_ptr) &cinfo,JPOOL_IMAGE, row_stride, 1);

    /* 第六步,循環讀取數據 */
   
    /* 每次讀取一樣,直到讀完整個文件 */      
    while (cinfo.output_scanline< cinfo.output_height)
    {
       (void)jpeg_read_scanlines(&cinfo,buffer, 1);/* 解碼一行數據 */
        put_scanline_someplace(buffer[0],row_stride);/* 將解碼後的數據輸出到某處 */
    }

    /* 第七步,結束解碼 */
    (void)jpeg_finish_decompress(&cinfo);

    /* 第八步,釋放解碼對象資源 */
    jpeg_destroy_decompress(&cinfo);  /* 釋放解碼申請的資源(SRAM內存) */
    fclose(infile);                     /* 關閉文件 */
    return 1;
}

以上代碼,將一個jpeg解碼分成了8個步驟,我們結合本例程代碼簡單講解下這幾個步驟。我們先來看一下一個很重要的結構體數據類型:structjpeg_decompress_struct,定義成cinfo結構體變量,該變量保存着jpeg數據的詳細信息,也保存着解碼之後輸出數據的詳細信息。一般情況下,每次調用libjpeg庫API的時候都需要把這個變量作為第一個參數傳入。另外,用户也可以通過修改這個變量來修改libjpeg行為,比如輸出數據格式,libjpeg庫可用的最大內存等。
因為DNESP32S3堆棧有限,不能按照示例來定義cinfo和jerr結構體,cinfo和jerr都比較大(均超過400字節),很容易出現堆棧溢出的情況。在開發板源碼中,我們使用的是全局指針變量,通過內存管理分配內存。
接下來看解碼步驟,第一步是分配,並初始化解碼對象結構體。做了兩件事:1,錯誤管理;2,初始化解碼對象。首先,錯誤管理使用setjmp和longjmp機制(不懂請百度)來實現類似C++的異常處理功能,外部代碼可以調用longjmp來跳轉到setjmp位置,執行錯誤管理(釋放內存,關閉文件等)。這裏註冊了一個my_error_exit函數,來執行錯誤退出處理,在例程源碼中,還實現了輸出警告信息函數:my_emit_message,方便調試代碼。然後,初始化解碼對象cinfo,就是通過jpeg_create_decompress函數實現。
第二步,解壓縮操作。示例代碼使用的是jpeg_stdio_src函數。這個函數的作用是指定數據源,我們用另外一個函數實現:

/**
* @brief   設置JPEG解壓縮的源數據
* @param   cinfo : 結構體指針
              Inbuffer :內存位置和大小所儲存在結構體中的指針
              Insize : 數據大小
* @retval  無
*/
GLOBAL(void)
jpeg_mem_src (j_decompress_ptrcinfo,
                 const unsigned char *inbuffer,
              size_t insize)
{
  struct jpeg_source_mgr * src;

  if (inbuffer== NULL || insize == 0)/* 將空輸入視為致命錯誤*/
    ERREXIT(cinfo,JERR_INPUT_EMPTY);

  /* 源對象是永久性的,
     因此可以通過只在第一個之前調用jpeg_mem_src從同一個緩衝區讀取一系列JPEG圖像 */
  if (cinfo->src == NULL) { /*first time for this JPEG object? */
    cinfo->src = (structjpeg_source_mgr *) (*cinfo->mem->alloc_small)
      ((j_common_ptr) cinfo,JPOOL_PERMANENT, SIZEOF(struct jpeg_source_mgr));
  }
  src = cinfo->src;
  src->init_source =init_mem_source;
  src->fill_input_buffer= fill_mem_input_buffer;
  src->skip_input_data =skip_input_data;
  src->resync_to_restart= jpeg_resync_to_restart; /* 使用默認方法 */
  src->term_source =term_source;
  src->bytes_in_buffer =insize;
  src->next_input_byte = (constJOCTET *) inbuffer;
}

這裏面重點是兩個函數:fill_mem_input_buffer和skip_input_data,前者用於從內存填充數據給libjpeg,後者用於跳過一定字節的數據。這兩個函數請看本例程源碼(在mjpeg.c裏面)。
第三步,讀取文件參數。通過jpeg_read_header函數實現,該函數將讀取JPEG的各個參數,必須在解碼之前調用。
第四步,開始解碼。示例代碼首先調用jpeg_start_decompress函數,然後計算樣本輸出buffer大小,併為其申請內存,為後續讀取解碼後的數據做準備。
第五步,循環讀取數據。通過jpeg_read_scanlines函數,循環解碼並讀取jpeg圖片數據,實現jpeg解碼。
第六步,解碼結束。解碼完成後,通過jepg_finish_decompress函數,結束jpeg解碼。
第七步,釋放解碼對象資源。在所有操作完成後,通過jpeg_destroy_decompress,釋放解碼過程中用到的資源(比如釋放內存)。
這樣,我們就完成了一張jpeg圖片的解碼。詳細的代碼,請大家參考光盤本例程源碼mjpeg.c。
最後,我們看看要實現avi視頻文件的播放,主要有哪些步驟,如下:
l 初始化各外設
要解碼視頻,相關外設肯定要先初始化好,比如:SDIO(驅動SD卡用)、SAI、DMA、ES8388、LCD和按鍵等。這些具體初始化過程,在前面的例程都有介紹,大同小異,這裏就不再細説了。
l 讀取AVI文件,並解析
要解碼,得先讀取avi文件,按43.1.1節的介紹,讀取出音視頻關鍵信息,音頻參數:編碼方式、採樣率、位數和音頻流類型碼(01wb/00wb)等;視頻參數:編碼方式、幀間隔、圖片尺寸和視頻流類型碼(00dc/01dc)等;共同的:數據流起始地址。有了這些參數,我們便可以初始化音視頻解碼,為後續解碼做好準備。
l 根據解析結果,設置相關參數
根據第2步解析的結果,設置I²S的音頻採樣率和位數,同時要讓視頻顯示在LCD中間區域,得根據圖片尺寸,設置LCD開窗時x,y方向的偏移量。
l 讀取數據流,開始解碼
前面三步完成,就可以正式開始播放視頻了。讀取視頻流數據(movi塊),根據類型碼,執行音頻/視頻解碼。對於音頻數據(01wb/00wb),本例程只支持未壓縮的PCM數據。對於視頻數據(00dc/01dc),本例程只支持MJPEG,通過libjpeg解碼,所以將視頻數據按前面所説的幾個步驟解碼即可。然後,利用定時器來控制幀間隔,以正常速度播放視頻,從而實現音視頻解碼。
l 解碼完成,釋放資源
最後在文件讀取完後(或者出錯了),需要釋放申請的內存、恢復LCD窗口、關閉定時器、停止SAI播放音樂和關閉文件等一系列操作,等待下一次解碼。
43.2 硬件設計
43.2.1例程功能
1、本實驗開機後,先初始化各外設,然後檢測字庫是否存在,如果檢測無問題,則開始播放SD卡VIDEO文件夾裏面的視頻(.avi格式)。
注意:自備SD卡一張,並在SD卡根目錄建立一個VIDEO文件夾,存放AVI視頻(僅支持MJPEG視頻,音頻必須是PCM,且視頻分辨率必須小於等於屏幕分辨率)在裏面。例程所需視頻,可以通過:狸窩全能視頻轉換器,轉換後得到,具體步驟見<<DNESP32S3開發指南>>)。
視頻播放時,LCD上會顯示視頻名字、當前視頻編號、總視頻數、聲道數、音頻採樣率、幀率、播放時間和總時間等信息。KEY0用於選擇下一個視頻,KEY1用於選擇上一個視頻,KEY_UP可以快進,KEY1可以快退。
2、LED閃爍,提示程序運行。
43.2.2硬件資源

  1. LED燈
    LED -IO0
    2.獨立按鍵
    KEY0(XL9555) - IO1_7
    KEY1(XL9555) - IO1_6
    KEY2(XL9555) - IO1_5
    KEY3(XL9555) - IO1_4
  2. XL9555
    IIC_SDA-IO41
    IIC_SCL-IO42
  3. SPILCD
    CS-IO21
    SCK-IO12
    SDA-IO11
    DC-IO40(在P5端口,使用跳線帽將IO_SET和LCD_DC相連)
    PWR- IO1_3(XL9555)
    RST- IO1_2(XL9555)
  4. SD
    CS-IO2
    SCK-IO12
    MOSI-IO11
    MISO-IO13
  5. ES8388音頻CODEC芯片(IIC端口0)
    IIC_SDA-IO41
    IIC_SCL-IO42
    I2S_BCK_IO-IO46
    I2S_WS_IO-IO9
    I2S_DO_IO-IO10
    I2S_DI_IO-IO14
    IS2_MCLK_IO-IO3
  6. 開發闆闆載的咪頭或自備麥克風輸入
  7. 喇叭或耳機
  8. MJPEG解碼庫
  9. 高分辨率定時器(ESP定時器)
    43.2.3原理圖
    本實驗相關的原理圖同上一章節。
    43.3 程序設計
    43.3.1 程序流程圖
    程序流程圖能幫助我們更好的理解一個工程的功能和實現的過程,對學習和設計工程有很好的主導作用。下面看看本實驗的程序流程圖:

image004

圖43.3.1.1錄音實驗程序流程圖
43.3.2 視頻播放實驗函數解析
本章實驗所使用ESP32-S3的API函數在第四十一章節已經講述過了,在此不再贅述。
43.3.3 視頻播放實驗驅動解析
在IDF版的32_videoplayer例程中,作者在32_videoplayer\components\BSP路徑下新增了一個ESPTIM文件夾,分別用於存放esptim.c、esptim.h這兩個文件。同時,在32_videoplayer\components路徑下新增了MJPEG驅動文件。
1,MJPEG驅動
這裏我們只講解核心代碼,詳細的源碼請大家參考光盤本實驗對應源碼。MJPEG驅動源碼包括四個文件:avi.c、avi.h、mjpeg.c和mjpeg.h。
avi.h頭文件在43.1小節部分講過,具體請看源碼。下面來看到avi.c文件,這裏總共有三個函數都很重要,首先介紹AVI解碼初始化函數,該函數定義如下:

/* avi文件相關信息 */
AVI_INFO g_avix;
/* 視頻編碼標誌字符串,00dc/01dc */
char *constAVI_VIDS_FLAG_TBL[2] = {"00dc", "01dc"};
/* 音頻編碼標誌字符串,00wb/01wb */
char *constAVI_AUDS_FLAG_TBL[2] = {"00wb", "01wb"};

/**
* @brief      avi解碼初始化
* @param      buf  : 輸入緩衝區
* @param      size : 緩衝區大小
* @retval     res
* @arg         OK,avi文件解析成功
* @arg         其他,錯誤代碼
*/
AVISTATUSavi_init(uint8_t *buf, uint32_t size)
{
    uint16_t offset;
    uint8_t *tbuf;
    AVISTATUS res =AVI_OK;
    AVI_HEADER *aviheader;
    LIST_HEADER *listheader;
    AVIH_HEADER *avihheader;
    STRH_HEADER *strhheader;

    STRF_BMPHEADER *bmpheader;
    STRF_WAVHEADER *wavheader;

    tbuf = buf;
    aviheader = (AVI_HEADER*)buf;
    if (aviheader->RiffID!= AVI_RIFF_ID)
    {
        returnAVI_RIFF_ERR;                          /* RIFF ID錯誤 */
    }

    if (aviheader->AviID !=AVI_AVI_ID)
    {
        returnAVI_AVI_ERR;                           /* AVI ID錯誤 */
    }

    buf += sizeof(AVI_HEADER);                       /* 偏移 */
    listheader = (LIST_HEADER*)(buf);
    if (listheader->ListID!= AVI_LIST_ID)
    {
        returnAVI_LIST_ERR;                          /* LIST ID錯誤 */
    }

    if (listheader->ListType!= AVI_HDRL_ID)
    {
        returnAVI_HDRL_ERR;                          /* HDRL ID錯誤 */
    }

    buf += sizeof(LIST_HEADER);                      /* 偏移 */
    avihheader = (AVIH_HEADER*)(buf);
    if (avihheader->BlockID!= AVI_AVIH_ID)
    {
        returnAVI_AVIH_ERR;                          /* AVIH ID錯誤 */
    }

    g_avix.SecPerFrame= avihheader->SecPerFrame;      /* 得到幀間隔時間 */
    g_avix.TotalFrame= avihheader->TotalFrame;      /* 得到總幀數 */
    buf +=avihheader->BlockSize + 8;                 /* 偏移 */
    listheader = (LIST_HEADER*)(buf);
    if (listheader->ListID!= AVI_LIST_ID)
    {
        returnAVI_LIST_ERR;                          /* LIST ID錯誤 */
    }

    if (listheader->ListType!= AVI_STRL_ID)
    {
        returnAVI_STRL_ERR;                          /* STRL ID錯誤 */
    }

    strhheader = (STRH_HEADER*)(buf + 12);
    if (strhheader->BlockID!= AVI_STRH_ID)
    {
        returnAVI_STRH_ERR;                          /*STRH ID錯誤 */
    }

    if (strhheader->StreamType== AVI_VIDS_STREAM)     /* 視頻幀在前 */
    {
        if (strhheader->Handler!= AVI_FORMAT_MJPG)
        {
            returnAVI_FORMAT_ERR;                   /* 非MJPG視頻流,不支持 */
        }

        g_avix.VideoFLAG= AVI_VIDS_FLAG_TBL[0];     /* 視頻流標記 "00dc" */
        g_avix.AudioFLAG= AVI_AUDS_FLAG_TBL[1];     /* 音頻流標記 "01wb" */
        bmpheader = (STRF_BMPHEADER*)(buf + 12 +strhheader->BlockSize + 8);   
        if (bmpheader->BlockID!= AVI_STRF_ID)
        {
            returnAVI_STRF_ERR;                      /* STRF ID錯誤 */
        }

        g_avix.Width =bmpheader->bmiHeader.Width;
        g_avix.Height= bmpheader->bmiHeader.Height;
        buf +=listheader->BlockSize + 8;            /* 偏移 */
        listheader = (LIST_HEADER*)(buf);
        if (listheader->ListID!= AVI_LIST_ID)      /* 是不含有音頻幀的視頻文件 */
        {
            g_avix.SampleRate= 0;                     /* 音頻採樣率 */
            g_avix.Channels= 0;                       /* 音頻通道數 */
            g_avix.AudioType= 0;                      /* 音頻格式 */

        }
        else
        {
            if (listheader->ListType!= AVI_STRL_ID)
            {
                returnAVI_STRL_ERR;    /* STRL ID錯誤 */
            }

            strhheader = (STRH_HEADER*)(buf + 12);
            if (strhheader->BlockID!= AVI_STRH_ID)
            {
                returnAVI_STRH_ERR;    /* STRH ID錯誤 */
            }

            if (strhheader->StreamType!= AVI_AUDS_STREAM)
            {
                returnAVI_FORMAT_ERR;  /* 格式錯誤 */
            }

            wavheader = (STRF_WAVHEADER*)(buf + 12 +strhheader->BlockSize+8);   
            if (wavheader->BlockID!= AVI_STRF_ID)
            {
                returnAVI_STRF_ERR;    /* STRF ID錯誤 */
            }

            g_avix.SampleRate= wavheader->SampleRate;       /* 音頻採樣率 */
            g_avix.Channels= wavheader->Channels;       /* 音頻通道數 */
            g_avix.AudioType= wavheader->FormatTag;      /* 音頻格式 */
        }
    }
    else if (strhheader->StreamType== AVI_AUDS_STREAM)/* 音頻幀在前 */
    {
        g_avix.VideoFLAG= AVI_VIDS_FLAG_TBL[1];         /* 視頻流標記 "01dc" */
        g_avix.AudioFLAG= AVI_AUDS_FLAG_TBL[0];         /* 音頻流標記 "00wb" */
        wavheader = (STRF_WAVHEADER*)(buf + 12 +strhheader->BlockSize + 8);
        if (wavheader->BlockID!= AVI_STRF_ID)
        {
            returnAVI_STRF_ERR;                          /*STRF ID錯誤 */
        }

        g_avix.SampleRate= wavheader->SampleRate;           /* 音頻採樣率 */
        g_avix.Channels= wavheader->Channels;            /* 音頻通道數 */
        g_avix.AudioType= wavheader->FormatTag;         /* 音頻格式 */
        buf +=listheader->BlockSize + 8;                  /* 偏移 */
        listheader = (LIST_HEADER*)(buf);
        if (listheader->ListID!= AVI_LIST_ID)
        {
            returnAVI_LIST_ERR;                          /* LIST ID錯誤 */
        }

        if (listheader->ListType!= AVI_STRL_ID)
        {
            returnAVI_STRL_ERR;    /* STRL ID錯誤 */
        }

        strhheader = (STRH_HEADER*)(buf + 12);
        if (strhheader->BlockID!= AVI_STRH_ID)
        {
            returnAVI_STRH_ERR;    /* STRH ID錯誤 */
        }

        if (strhheader->StreamType!= AVI_VIDS_STREAM)
        {
            returnAVI_FORMAT_ERR;  /* 格式錯誤 */
        }

        bmpheader = (STRF_BMPHEADER*)(buf + 12 +strhheader->BlockSize + 8);   
        if (bmpheader->BlockID!= AVI_STRF_ID)
        {
            returnAVI_STRF_ERR;    /* STRF ID錯誤 */
        }

        if (bmpheader->bmiHeader.Compression!= AVI_FORMAT_MJPG)
        {
            returnAVI_FORMAT_ERR;  /* 格式錯誤 */
        }

        g_avix.Width =bmpheader->bmiHeader.Width;
        g_avix.Height= bmpheader->bmiHeader.Height;
    }

    offset =avi_srarch_id(tbuf, size, "movi");     /* 查找moviID */
    if (offset== 0)
    {
        returnAVI_MOVI_ERR;        /*MOVI ID錯誤 */
    }

    if (g_avix.SampleRate)          /* 有音頻流,才查找 */
    {
        tbuf +=offset;
        offset =avi_srarch_id(tbuf, size,g_avix.AudioFLAG);   /* 查找音頻流標記 */
        if (offset== 0)
        {
            returnAVI_STREAM_ERR;  /* 流錯誤 */
        }
        tbuf +=offset + 4;
        g_avix.AudioBufSize= *((uint16_t *)tbuf);  /* 得到音頻流buf大小 */
    }

    printf("aviinit ok\r\n");
    printf("g_avix.SecPerFrame:%ld\r\n",g_avix.SecPerFrame);
    printf("g_avix.TotalFrame:%ld\r\n",g_avix.TotalFrame);
    printf("g_avix.Width:%ld\r\n",g_avix.Width);
    printf("g_avix.Height:%ld\r\n",g_avix.Height);
    printf("g_avix.AudioType:%d\r\n",g_avix.AudioType);
    printf("g_avix.SampleRate:%ld\r\n",g_avix.SampleRate);
    printf("g_avix.Channels:%d\r\n",g_avix.Channels);
    printf("g_avix.AudioBufSize:%d\r\n",g_avix.AudioBufSize);
    printf("g_avix.VideoFLAG:%s\r\n",g_avix.VideoFLAG);
    printf("g_avix.AudioFLAG:%s\r\n",g_avix.AudioFLAG);

    return res;
}

該函數用於解析AVI文件,獲取音視頻流數據的詳細信息,為後續解碼做準備。
接下來介紹的是查找 ID函數,其定義如下:

/**
* @brief      查找 ID
* @param      buf  : 待查緩存區
* @param      size : 緩存大小
* @param      id   : 要查找的id,必須是4字節長度
* @retval     0,接收應答失敗
*              其他:movi ID偏移量
*/
uint16_tavi_srarch_id(uint8_t *buf,uint32_t size, char *id)
{
uint32_ti;
uint32_tidsize = 0;
    size -= 4;
    for (i = 0; i < size; i++)
    {
        if ((buf == id[0])&&
            (buf[i + 1] == id[1])&&
            (buf[i + 2] == id[2])&&
            (buf[i + 3] == id[3]))   
                {
                    idsize = MAKEDWORD(buf + i + 4);   
/* 得到幀大小,必須大於16字節,才返回,否則不是有效數據 */
                    if (idsize > 0X10)return i; /* 找到"id"所在的位置 */
              }
            }
         }
    }
    return 0;
}

該函數用於查找某個ID,可以是4個字節長度的ID,比如00dc,01wb,movi之類的,在解析數據以及快進快退的時候,有用到。
接下來介紹的是得到stream流信息函數,其定義如下:

/**
* @brief      得到stream流信息
* @param      buf  : 流開始地址(必須是01wb/00wb/01dc/00dc開頭)
* @retval     執行結果
*  @arg       AVI_OK, AVI文件解析成功
*  @arg       其他  , 錯誤代碼
*/
AVISTATUSavi_get_streaminfo(uint8_t *buf)
{
    g_avix.StreamID= MAKEWORD(buf + 2);        /* 得到流類型 */
    g_avix.StreamSize= MAKEDWORD(buf + 4);         /* 得到流大小 */
if (g_avix.StreamSize > AVI_MAX_FRAME_SIZE)    /* 幀大小太大了,直接返回錯誤 */
    {
        printf("FRAME SIZEOVER:%d\r\n", g_avix.StreamSize);
        g_avix.StreamSize = 0;
        returnAVI_STREAM_ERR;
    }
    if (g_avix.StreamSize% 2)
    {
        g_avix.StreamSize++;    /* 奇數加1(g_avix.StreamSize,必須是偶數) */
    }
    if (g_avix.StreamID== AVI_VIDS_FLAG || g_avix.StreamID== AVI_AUDS_FLAG)
    {
        returnAVI_OK;
    }
    return AVI_STREAM_ERR;
}

該函數用來獲取當前數據流信息,重點是取得流類型和流大小,方便解碼和讀取下一個數據流。
mjpeg.h文件只有一些函數和變量聲明,接下來,介紹mjpeg.c裏面的幾個函數,首先是初始化MJPEG解碼數據源的函數,其定義如下:

/**
* @brief      mjpeg 解碼初始化
* @param      offx,offy:x,y方向的偏移
* @retval     0,成功; 1,失敗
*/
charmjpegdec_init(uint16_t offx, uint16_t offy)
{
cinfo = (structjpeg_decompress_struct *)
malloc(sizeof(structjpeg_decompress_struct));
    jerr = (structmy_error_mgr *)malloc(sizeof(structmy_error_mgr));
    if (cinfo == NULL || jerr == NULL)
    {
        printf("[E][mjpeg.cpp]mjpegdec_init():
malloc failed to apply for memory\r\n");
        mjpegdec_free();
        return -1;
    }
    /* 保存圖像在x,y方向的偏移量 */
    imgoffx = offx;
    imgoffy = offy;
    return 0;
}

該函數用於初始化jpeg解碼,主要是申請內存,然後確定視頻在液晶上面的偏移(讓視頻顯示在SPILCD中央)。
下面介紹的是MJPEG釋放所有申請的內存函數,其定義如下:

/**
* @brief      mjpeg結束,釋放內存
* @param      無
* @retval     無
*/
voidmjpegdec_free(void)
{
    free(cinfo);
    free(jerr);
}

該函數用於釋放內存,解碼結束後調用。
下面介紹的是解碼一副JPEG圖片函數,其定義如下:

/**
* @brief      解碼一副JPEG圖片
* @param      buf: jpeg數據流數組
* @param      bsize: 數組大小
* @retval     0,成功; 1,失敗
*/
uint8_tmjpegdec_decode(uint8_t* buf, uint32_t bsize)
{
    JSAMPARRAY buffer;
    if (bsize == 0) return 1;
    int row_stride = 0;
    int j = 0;                             /* 記錄當前解碼的行數 */
    int lineR = 0;                         /* 每一行R分量的起始位置 */
    cinfo->err =jpeg_std_error(&jerr->pub);
    jerr->pub.error_exit= my_error_exit;
    jerr->pub.emit_message= my_emit_message;
    cinfo->out_color_space= JCS_RGB;
    if (setjmp(jerr->setjmp_buffer))    /* 錯誤處理 */
    {
        jpeg_abort_decompress(cinfo);
        jpeg_destroy_decompress(cinfo);
        return 2;
    }
    jpeg_create_decompress(cinfo);
    jpeg_mem_src(cinfo, buf, bsize);    /* 測試正常 */
    jpeg_read_header(cinfo, TRUE);
    jpeg_start_decompress(cinfo);
    row_stride = cinfo->output_width* cinfo->output_components;
    /* 計算buffer大小並申請相應空間 */
    buffer = (*cinfo->mem->alloc_sarray)
        ((j_common_ptr)cinfo,JPOOL_IMAGE, row_stride, 1);
   
    while (cinfo->output_scanline< cinfo->output_height)
    {
        int i = 0;
        jpeg_read_scanlines(cinfo,buffer, 1);
        unsigned shorttmp_color565;
        /* 為上述圖像數據賦值 */
        for (int k = 0; k <Windows_Width * 2; k += 2)
        {
            tmp_color565 =rgb565(buffer[0],
                                      buffer[0][i + 1],
                                      buffer[0][i + 2]);
                                    
            lcd_buf[lineR + k] = (tmp_color565& 0xFF00) >> 8;
            lcd_buf[lineR + k + 1] =  tmp_color565 & 0x00FF;
            i += 3;
        }
        
        j++;
        lineR = j *Windows_Width * 2;
    }
    lcd_set_window(imgoffx,
                      imgoffy - 30,
                      imgoffx + cinfo->output_width- 1,
                      imgoffy - 30 + cinfo->output_height- 1);
    taskENTER_CRITICAL(&my_spinlock);
    /* 例如:96*96*2/1536= 12;分12次發送RGB數據 */
    for(int x = 0;
            x < (cinfo->output_width* cinfo->output_height * 2 /LCD_BUF_SIZE);
            x++)
    {
        /*&lcd_buf[j * LCD_BUF_SIZE] 偏移地址發送數據 */
        lcd_write_data(&lcd_buf[x *LCD_BUF_SIZE] , LCD_BUF_SIZE);
    }
   
    taskEXIT_CRITICAL(&my_spinlock);
    lcd_set_window(0, 0,lcd_self.width, lcd_self.height);  /* 恢復窗口 */
    jpeg_finish_decompress(cinfo);
    jpeg_destroy_decompress(cinfo);
    return 0;

}
該函數是解碼jpeg的主要函數,通過前面43.1.2節介紹過的步驟進行解碼,該函數的參數buf指向內存裏面的一幀jpeg數據,bsize是數據大小。
2,APP驅動
videoplayer.h頭文件有兩個宏定義和函數聲明,具體請看源碼。下面來看到videoplayer.c文件中,播放一個MJPEG文件函數,其定義如下:

/**
* @brief      播放MJPEG視頻
* @param      pname: 視頻文件名
* @retval     按鍵鍵值
*             KEY2_PRES: 上一個視頻
*             KEY0_PRES: 下一個視頻
*             其他值   : 錯誤代碼
*/
static uint8_tvideo_play_mjpeg(uint8_t *pname)
{
    uint8_t *framebuf;
    uint8_t *pbuf;
    uint8_t res = 0;
    uint16_t offset;
    uint32_t nr;
    uint8_t key;
    FIL *favi;
   
    /* 申請內存 */
    framebuf = (uint8_t *)malloc(AVI_VIDEO_BUF_SIZE);
    favi = (FIL *)malloc(sizeof(FIL));
    if ((framebuf== NULL) || (favi == NULL))
    {
        printf("memoryerror!\r\n");
        res = 0xFF;
    }
    memset(framebuf, 0,AVI_VIDEO_BUF_SIZE);
   
    while (res == 0)
    {
        /* 打開文件 */
        res = (uint8_t)f_open(favi, (const TCHAR*)pname, FA_READ);
        
        if (res == 0)
        {
            pbuf =framebuf;
            
            /* 開始讀取 */
            res = (uint8_t)f_read(favi, pbuf,AVI_VIDEO_BUF_SIZE, (UINT*)&nr);
            
            if (res != 0)
            {
                printf("freaderror:%d\r\n", res);
                break;
            }
            
            /* AVI解析 */
            res =avi_init(pbuf, AVI_VIDEO_BUF_SIZE);
            
            if (res != 0)
            {
                printf("avierror:%d\r\n", res);
                break;
            }
            
            video_info_show(&g_avix);
            esptim_int_init(g_avix.SecPerFrame/ 1000, 1000);
            
            /* 尋找movi ID */
            offset =avi_srarch_id(pbuf, AVI_VIDEO_BUF_SIZE, "movi");
            
            /* 獲取流信息 */
            avi_get_streaminfo(pbuf +offset + 4);
            
            /* 跳過標誌ID,讀地址偏移到流數據開始處 */
            f_lseek(favi,offset + 12);
            
            /* 初始化JPG解碼 */
            res =mjpegdec_init((lcd_self.width -g_avix.Width) / 2,
                                    110+(lcd_self.height-110-g_avix.Height)/2);
            
            /* 定義圖像的寬高 */
            Windows_Width =g_avix.Width;
            Windows_Height =g_avix.Height;
            /* 有音頻信息,才初始化 */
            if (g_avix.SampleRate)
            {
                printf("g_avix.SampleRate:%ld\r\n",g_avix.SampleRate);
               
                /* 飛利浦標準,16位數據長度 */
                es8388_sai_cfg(0, 3);
               
                /* 設置採樣率 */
                i2s_set_samplerate_bits_sample(g_avix.SampleRate,
                                              I2S_BITS_PER_SAMPLE_16BIT);
                                                i2s_start(I2S_NUM);
            }
            while (1)
            {
               
                /* 視頻流 */
                if (g_avix.StreamID== AVI_VIDS_FLAG)
                {
                    pbuf =framebuf;
                    
                    /* 讀取整幀+下一幀數據流ID信息 */
                    f_read(favi, pbuf,g_avix.StreamSize + 8, (UINT*)&nr);
                    res =mjpegdec_decode(pbuf, g_avix.StreamSize);
                    
                    if (res != 0)
                    {
                        printf("decodeerror!\r\n");
                    }
                    /* 等待播放時間到達 */
                    while (frameup== 0);
                    frameup = 0;
                }
                else
                {
                    /* 顯示當前播放時間 */
                    video_time_show(favi, &g_avix);
                    
                    /* 填充psaibuf */
                    f_read(favi,framebuf, g_avix.StreamSize + 8, &nr);
                    pbuf =framebuf;
                    
                    /* 數據轉換+發送給DAC */
                    i2s_tx_write(framebuf,g_avix.StreamSize);
                }
               
                key =xl9555_key_scan(0);
                /* KEY0/KEY2按下,播放下一個/上一個視頻 */
                if (key ==KEY0_PRES || key == KEY2_PRES)
                {
                    res = key;
                    break;
                }
                else if (key ==KEY1_PRES || key == KEY3_PRES)
                {
                    /* 關閉音頻 */
                    i2s_stop(I2S_NUM);
                    video_seek(favi, &g_avix,framebuf);
                    pbuf =framebuf;
                    
                    /* 開啓DMA播放 */
                    i2s_start(I2S_NUM);
                }
                /* 讀取下一幀流標誌 */
                if (avi_get_streaminfo(pbuf +g_avix.StreamSize) != 0)
                {
                    printf("g_frameerror\r\n");
                    res =KEY0_PRES;
                    break;
                }
            }
            i2s_stop(I2S_NUM);
            esp_timer_stop(esp_tim_handle);
            
            /* 恢復窗口 */
            lcd_set_window(0, 0,lcd_self.width, lcd_self.height);
            
            /* 釋放內存 */
            mjpegdec_free();
            
            /* 關閉文件 */
            f_close(favi);
        }
    }
    i2s_zero_dma_buffer(I2S_NUM);
    free(framebuf);
    free(favi);
   
    return res;
}

該函數用來播放一個avi視頻文件(MJPEG編碼),解碼過程就是根據前面我們在43.1節最後所介紹的步驟進行。其他代碼,我們就不介紹了,請大家參考本例程源碼。
43.3.4 CMakeLists.txt文件
打開本實驗BSP下的CMakeLists.txt文件,其內容如下所示:

set(src_dirs
            IIC
            LCD
            LED
            SDIO
            SPI
            XL9555
            ESPTIM
            ES8388
            I2S)
set(include_dirs
            IIC
            LCD
            LED
            SDIO
            SPI
            XL9555
            ESPTIM
            ES8388
            I2S)
set(requires
            driver
            fatfs
            esp_timer)
idf_component_register(SRC_DIRS${src_dirs}
INCLUDE_DIRS ${include_dirs}REQUIRES ${requires})
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)

上述的紅色ESPTIM驅動以及esp_timer依賴庫需要由開發者自行添加,以確保視頻播放驅動能夠順利集成到構建系統中。這一步驟是必不可少的,它確保了視頻播放驅動的正確性和可用性,為後續的開發工作提供了堅實的基礎。
打開本實驗main文件下的CMakeLists.txt文件,其內容如下所示:

idf_component_register(
    SRC_DIRS
        "."
        "app"
    INCLUDE_DIRS
        "."
        "app")

上述的紅色app驅動需要由開發者自行添加,在此便不做贅述了。
43.3.5 實驗應用代碼
打開main/main.c文件,該文件定義了工程入口函數,名為app_main。該函數代碼如下。

i2c_obj_ti2c0_master;
/**
* @brief      程序入口
* @param      無
* @retval     無
*/
voidapp_main(void)
{
    esp_err_t ret = 0;
    uint8_t key = 0;
    /* 初始化NVS*/
    ret =nvs_flash_init();
if (ret ==ESP_ERR_NVS_NO_FREE_PAGES ||
ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
    {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret =nvs_flash_init();
    }
    /* 初始化LED*/
    led_init();
   
    /* 初始化IIC0*/
    i2c0_master =iic_init(I2C_NUM_0);
   
    /* 初始化SPI*/
    spi2_init();
   
    /* 初始化IO擴展芯片 */  
    xl9555_init(i2c0_master);
   
    /* 初始化LCD*/
    lcd_init();
    /* ES8388初始化 */
    es8388_init(i2c0_master);
   
    /* 開啓DAC關閉ADC */
    es8388_adda_cfg(1, 0);
    es8388_input_cfg(0);
   
    /* DAC選擇通道輸出 */
    es8388_output_cfg(1, 1);
   
    /* 設置耳機音量 */
    es8388_hpvol_set(20);
   
    /* 設置喇叭音量 */
    es8388_spkvol_set(20);
   
    /* I2S初始化 */
    i2s_init();
    vTaskDelay(1000);
   
    /* 打開喇叭 */
    xl9555_pin_write(SPK_EN_IO,0);
    /* 檢測不到SD卡 */
    while (sd_spi_init())
    {
        lcd_show_string(30, 110, 200, 16, 16, "SDCard Error!", RED);
        vTaskDelay(500);
        lcd_show_string(30, 130, 200, 16, 16, "PleaseCheck! ", RED);
        vTaskDelay(500);
    }
    /* 檢查字庫 */
    while (fonts_init())
    {
        /* 清屏 */
        lcd_clear(WHITE);
        lcd_show_string(30, 30, 200, 16, 16, "ESP32-S3", RED);
        
        /* 更新字庫 */
        key =fonts_update_font(30, 50, 16, (uint8_t *)"0:", RED);
   
        /* 更新失敗 */
        while (key)
        {
            lcd_show_string(30, 50, 200, 16, 16, "FontUpdate Failed!", RED);
            vTaskDelay(200);
            lcd_fill(20, 50, 200 + 20, 90 + 16, WHITE);
            vTaskDelay(200);
        }
        lcd_show_string(30, 50, 200, 16, 16, "FontUpdate Success!   ", RED);
        vTaskDelay(1500);
        
        /* 清屏 */
        lcd_clear(WHITE);
    }
    /* 為fatfs相關變量申請內存 */
    ret =exfuns_init();
    text_show_string(30, 30, 200, 16, "正點原子ESP32開發板", 16, 0, RED);
    text_show_string(30, 50, 200, 16, "視頻播放器實驗", 16, 0, RED);
    text_show_string(30, 70, 200, 16, "正點原子@ALIENTEK", 16, 0, RED);
    text_show_string(30, 90, 200, 16, "KEY0:NEXTKEY2:PREV ", 16, 0, RED);
    text_show_string(30, 110, 200, 16, "KEY_UP:FF   KEY1:REW", 16, 0, RED);
   
    /* 實驗信息顯示延時 */
    vTaskDelay(500);
    while (1)
    {
        video_play();
    }
}

main函數只是經過一系列的外設初始化後,檢查字庫是否已經更新,然後顯示實驗的信息,就通過調用video_play函數,執行視頻播放的程序了。
43.4 下載驗證
本章,我們例程僅支持MJPEG編碼的avi格式視頻,且音頻必須是PCM格式,另外視頻分辨率不能大於LCD分辨率。要滿足這些要求,現成的avi文件是很難找到的,所以我們需要用軟件,將通用視頻(任何視頻都可以)轉換為我們需要的格式,這裏我們通過:狸窩全能視頻轉換器,這款軟件來實現(路徑:光盤:6,軟件資料à1,軟件à7,其他軟件.zipà視頻轉換軟件à狸窩全能視頻轉換器.exe)。安裝完後,打開,然後進行相關設置,軟件設置如圖43.4.1和43.4.2所示:

image005

圖43.4.1 軟件啓動界面和設置

image007

圖43.4.2 高級設置
首先,如圖43.4.1所示,點擊1處,添加視頻,找到你要轉換的視頻,添加進來。有的視頻可能有獨立字幕,比如我們打開的這個視頻就有,所以在2處選擇下字幕(如果沒有的,可以忽略此步)。然後在3處,點擊▼圖標,選擇預製方案:AVI-Audio-Video Interleaved(.avi),即生成.avi文件,然後點擊4處的高級設置按鈕,進入43.4.2所示的界面,設置詳細參數如下:
視頻編碼器:選擇MJPEG。本例程僅支持MJPEG視頻解碼,所以選擇這個編碼器。
視頻尺寸:480x272。這裏得根據所用LCD分辨率來選擇,假設我們用800
480的4.3寸電容屏模塊,則這裏最大可以設置:480x272。PS:如果是2.8屏,最大寬度只能是240)。
比特率:1000。這裏設置越大,視頻質量越好,解碼就越慢(可能會卡),我們設置為1000,可以得到比較好的視頻質量,同時也不怎麼會卡。
幀率:10。即每秒鐘10幀。對於480*272的視頻,本例程最高能播放30幀左右的視頻,如果要想提高幀率,有幾個辦法:1,降低分辨率;2,降低比特率;3,降低音頻採樣率。
音頻編碼器:PCMS16LE。本例程只支持PCM音頻,所以選擇音頻編碼器為這個。
採樣率:這裏設置為110250,即11.025Khz的採樣率。這裏越高,聲音質量越好,不過,轉換後的文件就越大,而且視頻可能會卡。
其他設置,採用默認的即可。設置完以後,點擊確定,即可完成設置。
點擊圖43.4.1的5處的文件夾圖標,設置轉換後視頻的輸出路徑,這裏我們設置到了桌面,這樣轉換後的視頻,會保存在桌面。最後,點擊圖中6處的按鈕,即可開始轉換了,如圖43.4.3所示:

image009

圖43.4.3 正在轉換
等轉換完成後,將轉換後的.avi文件,拷貝到SD卡àVIDEO文件夾下,然後插入開發板的SD卡接口,就可以開始測試本章例程了。
將程序下載到開發板後,程序先檢測字庫,只有字庫已經更新才可以繼續執行後面的程序。字庫已更新,就可以看到LCD首先顯示一些實驗相關的信息,如圖43.4.4所示:

image011

圖43.4.4顯示實驗相關信息
顯示了上圖的信息後,檢測SD卡的VIDEO文件夾,並查找avi視頻文件,在找到有效視頻文件後,便開始播放視頻,如圖43.4.5所示:

image013

圖43.4.5 視頻播放中
可以看到,屏幕顯示了文件名、索引、聲道數、採樣率、幀率和播放時間等參數。然後,我們按KEY0/KEY2,可以切換到下一個/上一個視頻,按KEY_UP/KEY1,可以快進/快退。
至此,本例程介紹就結束了。本實驗,我們在開發板上實現了視頻播放,體現了DNESP32S3強大的處理能力。
附本實驗測試結果(視頻比特率:1000,音頻均為:110250,立體聲)
對 240160/240180分辨率,可達30幀
對 320240分辨率,可達20幀
對 480
272分辨率,可達10幀
最後提醒大家,轉換的視頻分辨率,一定要根據自己的SPILCD設置,不能超過SPILCD的尺寸!!否則無法播放(可能只聽到聲音,看不到圖像)。

user avatar SparkHo 头像 openeuler 头像 XY-Heruo 头像
点赞 3 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.