0xAA55 发表于 2025-6-12 06:32:00

【C】教你手撕 AVI:提取其中的视频流和音频流

# 教你手撕 AVI:提取其中的视频流和音频流

我想在嵌入式环境下,利用 MCU 的 JPEG 硬解码功能,以及 DAC 输出模拟信号的功能,实现一个视频播放器。这个过程中需要手搓 AVI 文件解析器。

以前尝试过使用 F1C200S 作为 MCU,从 SPIFLASH 加载启动 Buildroot。我编译的 Buildroot 使 MCU 超频到 900 MHz(我没搞定 F1C200S 的视频硬解码驱动,于是通过超频来让 MCU 达到能够流畅软解的条件)。我配置 Buildroot 集成 FFmpeg,使用 FFmpeg 直接软解播放 AVI 文件(指定其视频画面输出到 fbdev,然后音频流走 `stdout` 出去,再搭建管道把 FFmpeg 的 `stdout` 接到 alsa 音频驱动的用户态播放器 tinyalsa 库的 tinyplay 播放器的 `stdin`,音视频的播放就解决了。

后来厂子卖给我的 F1C200S 开发板全是残次件,超频超不了一点,播放过程会出现 Kernel panic,于是我准备更换方案,用 STM32H750,不超频,跑 480 MHz 使用 JPEG 外设硬解来播放视频,不使用 Buildroot + FFmpeg,使用 STM32 HAL,手搓 AVI parser。

## (https://learn.microsoft.com/en-us/windows/win32/directshow/avi-riff-file-reference)

AVI 文件和 WAV 文件一样,使用 Chunk 来组织数据,每个 Chunk 都符合以下规则:
- FourCC 开头,比如 `RIFF`、`LIST`、`data` 等,每一种都对应各自的内容格式。
- 4 字节的 Chunk 内容长度(不包括 Chunk 头部)
- Chunk 内容

与 WAV 不同的是,WAV 使用 `data` Chunk 来存储全部的音频数据,但是 AVI 则需要同时存储音频流和视频流。WAV 很少有 Chunk 嵌套 Chunk 的情况(基本上大多数你关心的 Chunk 都是 `RIFF` Chunk 的子 Chunk),但 AVI 则不同,它有很多的 Chunk 嵌套的 Chunk。

AVI 把视频流拆成一个个的包(packet),音频流也拆成一个个的包,然后交错存储视频和音频的包。AVI 文件会在头部存储一个字段,指示它有多少个流;然后每个流都有个“流头部”数据,指示这个流是视频还是音频,它是什么格式,视频的话它有帧率计算的部分;音频的话它有采样率等。音频流头有 `WAVEFORMATEX` 结构。头部之后,就是一个个的 packet 了,每个 packet 都是一个 Chunk,它的 FourCC 使用两个数值字符描述它的流 ID,然后再使用两个字母来描述它是视频还是音频还是别的。到最后,AVI 会有一个 `idx1` Chunk(这个 Chunk 不一定有,它是可选的),这个 Chunk 可以帮助你快速 seek 你的 AVI 文件,它存储每个 packet 的 FourCC 和文件内位置、长度。

常见的 AVI 都是具有两个流的:一个视频流,一个音频流。然而:

- 一个 AVI 既可以只有一个流(比如只有视频流),也可以有很多个流,比如视频流有两个,按用途分为成人版和儿童版;音频流有六个,按语言分为中文普通话版,粤语版,以及英文版,同时也分成人版和儿童版。像这样具有很多个流的 AVI **虽然很少见,但是有**。
- **不同的流可以有不同的长度**,如果音频流提前播放完了,后面就是静音的视频播放;如果视频流提前播放完了,它就停在最后一帧,然后继续播放音频。
- 有些播放器能智能匹配成人版视频流和成人版音频流;儿童版视频流和儿童版音频流;
        - 它可能根据机器所在公网 IP 判断地区,根据地区判断选择普通话版的音频流还是粤语版、英文版的音频流;同时提供 UI 界面供你挑选要用的视频流和音频流等。
- 通常情况下,一般的播放器只会选择它遇到的第一个视频流和第一个音频流。

AVI 的音频部分,因为头部用的是 `WAVEFORMATEX` 结构存储的音频格式,这块和 WAV 文件具有一些相同的特征:音频格式按 `format_tag` 区分,采样率、声道数等都在 `WAVEFORMATEX` 里。而 AVI 的流头里面的 `dwRate` 和 `dwScale` 成员也同时给你指示音频的采样率数据。

音频也不一定是双声道,就像 WAV 一样,它可能不止具有两个声道,而是会有 2.1、2.2、3.1、4.1、5.1、6.1 等各种布局方式,每个声道都有对应的扬声器位置。而针对这种情况,对于通常只有双声道的 PC,有对应的下混器算法(Downmixer)使这种多声道布局被下混为双声道。

从[微软的 AVI 文档](https://learn.microsoft.com/en-us/windows/win32/directshow/avi-riff-file-reference)抄来它的大致结构:

```
RIFF ('AVI '
      LIST ('hdrl'
            'avih'(<Main AVI Header>)
            LIST ('strl'
                  'strh'(<Stream header>)
                  'strf'(<Stream format>)
                  [ 'strd'(<Additional header data>) ]
                  [ 'strn'(<Stream name>) ]
                  ...
               )
             ...
         )
      LIST ('movi'
            {SubChunk | LIST ('rec '
                              SubChunk1
                              SubChunk2
                              ...
                           )
               ...
            }
            ...
         )
      ['idx1' (<AVI Index>) ]
   )
```
需要这样去理解它的结构:
- 所有带括弧的都是 Chunk,比如 `RIFF`、`LIST` 这些都是 Chunk,符合 Chunk 的存储方式。
- 用单引号括起来的,则仅仅是个 flag,比如 `AVI `、`hdrl`,用来区分这个 Chunk 里面的数据是干啥的。
- 用单引号括起来,但是右边又有括弧,括弧里面又用尖括号告诉你头部结构的,那么它还是个 Chunk,得按读取 Chunk 的方式来读取。

需要注意:
- FFmpeg 生成的 AVI 有很多地方加了 `JUNK` Chunk,正确的处理方式就是忽略之。
- 有很多 Chunk 嵌套的子 Chunk 里面也有 `JUNK` Chunk,也要忽略之。

### 小结:

AVI 的格式分为三大部分:
- 第一个 `LIST` Chunk:存储 AVI 的头部,包括 AVI 的主头部,以及每个流的流头部。每个流头部都是一个子 `LIST` Chunk。
- 第二个 `LIST` Chunk:开头有 `movi` 标识,存储的是所有流的包裹。这里又有两种情况:
        - 紧随 `movi` 标识的,是一个个的流包裹。也就是,整个 AVI 只包含一个音视频片段。
        - 紧随 `movi` 标识的,是子 `LIST` Chunk,这个 Chunk 的开头是一个 `rec ` 标识,然后里面是一个个的流包裹。每个子 `LIST` Chunk 是一段音视频片段。这样的 AVI 包含多个音视频片段。
        - 一般的播放器会无视这种多片段的结构,直接把子 `LIST` Chunk 里面的流包裹拿出来连续播放。
- 一个可能有也可能没有的 `idx1` Chunk:这是整个 AVI 文件的索引器,负责帮你快速定位你要的包裹。

### AVI 的流包裹 Chunk 结构

每个流包裹都是一个 Chunk,而 Chunk 头部的 flag 则是两个数字 + 两个字母的形式构成,两个数字是十进制的流 ID,比如 `00`,而两个字母则描述这个包裹的内容:
- `db`:未压缩视频帧。说白了就是 BMP 位图格式。有这种包裹的 AVI 头部会有 `BITMAPINFO`。
- `dc`:压缩视频帧。具体压缩格式要看 AVI 流头的 `fccHandler` 字段,比如 `MJPG` 就是 JPEG 帧。除此以外还有其它各种各样的格式比如 H264 等。你如果有对应格式的解码器库,你就可以解码成 RGB 位图,没有就拉倒。
- `pc`:调色板。调色板是拿来干啥的呢?假设你的未压缩视频帧是使用索引颜色的 BMP 格式,那么当你遇到这种调色板包裹的时候,你就需要更新你的 BMP 调色板。基本上,遇到这种包裹的概率为零。都什么年代了还使用索引颜色?
- `wb`:一段音频。

举个例子,假设你的 AVI 文件有两个流,第 `0` 个流是 MJPEG 视频流,第 `1` 个流是 PCM 音频流,那么在 `LIST(movi)` 里面,你会遇到的 Chunk 序列的 FourCC 就像这样:
`00dc` 视频帧
`01wb` 音频片段
`00dc` 视频帧
`01wb` 音频片段
`00dc` 视频帧
`01wb` 音频片段
...

但是 AVI 没有规定说一定要均匀分布每个流的数据,它通常往往是不均匀的,比如:
`00dc` `00dc` `00dc` `00dc` `01wb` `01wb` `01wb` `01wb` `01wb` `01wb`

而且大多数情况下,它都是不均匀的,有时候可能一下子给你好几分钟的视频流,然后才开始给你音频流。你的播放器如果采取的策略是缓存「时机未到」的视频帧或者音频帧的话,那你会遇到最坏情况:**一个 AVI 文件前半段全是视频流,后半段全是音频流**。

因此显然不能采取缓存策略,而是要针对你要播放的视频流和音频流,分别存储对应的当前播放位置的文件内偏移量。

但如果你只用一个文件对象/文件描述符/文件句柄用于播放 AVI 的话,你就要不停地 `seek()` 到视频帧的位置读取视频,再 `seek()` 到音频帧的位置读取音频,不断地往复地使用 `seek()`。虽然确实不用缓存音视频包裹了,但是这样做依然低效,容易卡存储器 IO 带宽。这是因为如果你 `seek()` 的距离太远了,那么你的文件对象/文件描述符/文件句柄就需要重新缓存文件片段。

解决办法就是使用多个文件对象/文件描述符/文件句柄来打开同一个 AVI 文件,然后针对每个你要播放的流,使用它专用的文件对象/文件描述符/文件句柄。这样的话,每个文件对象/文件描述符/文件句柄的缓存都可以得到利用,能减少对存储器 IO 带宽的占用。

## 我的代码仓库——示例代码

(https://github.com/0xAA55/avi_read)
(https://gitee.com/a5k3rn3l/avi_read)
注意以上两个仓库是同步更新的,你没有条件使用 GitHub 的话就注册一个 Gitee 账号吧。

### AVI 头部 Parser 大致逻辑
以下代码摘自我的代码仓库,是用来读取 AVI 的流头的,凑合看吧。看不懂就请直接去仓库里看。
```
int avi_reader_init
(
        avi_reader *r,
        void *userdata,
        read_cb f_read,
        seek_cb f_seek,
        tell_cb f_tell,
        logprintf_cb f_logprintf,
        avi_logprintf_level log_level
)
{
        if (!f_logprintf) f_logprintf = default_logprintf;
#if AVI_ROBUSTINESS
        if (!r)
        {
                avi_reader fake_r = create_only_for_printf(f_logprintf, log_level, userdata);
                r = &fake_r;
                FATAL_PRINTF(r, "Invalid parameter: `avi_reader *r` must not be NULL." NL);
                r = NULL;
                goto ErrRet;
        }
#endif

        memset(r, 0, sizeof*r);
        r->userdata = userdata;
        r->f_read = f_read;
        r->f_seek = f_seek;
        r->f_tell = f_tell;
        r->f_logprintf = f_logprintf;
        r->log_level = log_level;

#if AVI_ROBUSTINESS
        if (!f_read)
        {
                FATAL_PRINTF(r, "Invalid parameter: must provide your `read_cb` implementation." NL);
                goto ErrRet;
        }
        if (!f_seek)
        {
                FATAL_PRINTF(r, "Invalid parameter: must provide your `seek_cb` implementation." NL);
                goto ErrRet;
        }
        if (!f_tell)
        {
                FATAL_PRINTF(r, "Invalid parameter: must provide your `tell_cb` implementation." NL);
                goto ErrRet;
        }
#endif
        uint32_t riff_len;
        if (!must_match(r, "RIFF")) goto ErrRet;
        if (!must_read(r, &riff_len, 4)) goto ErrRet;

        fsize_t avi_start;
        if (!must_tell(r, &avi_start)) goto ErrRet;
        if (!must_match(r, "AVI ")) goto ErrRet;

        r->end_of_file = (size_t)avi_start + riff_len;
        char fourcc_buf = { 0 };
        uint32_t chunk_size;
        fsize_t end_of_chunk;
        int got_all_we_need = 0;
        int has_index = 0;

        // https://learn.microsoft.com/en-us/windows/win32/directshow/avi-riff-file-reference
        while (!got_all_we_need)
        {
                fsize_t cur_chunk_pos;
                if (!must_read(r, fourcc_buf, 4)) goto ErrRet;
                if (!must_read(r, &chunk_size, 4)) goto ErrRet;
                if (!must_tell(r, &cur_chunk_pos)) goto ErrRet;
                end_of_chunk = cur_chunk_pos + chunk_size;
                switch (MATCH4CC(fourcc_buf))
                {
                default:
                case FCC_JUNK:
                case FCC_JUNK_:
                        INFO_PRINTF(r, "Skipping chunk \"%s\"" NL, fourcc_buf);
                        break;
                case FCC_LIST:
                case FCC_LIST_:
                        if (!must_read(r, fourcc_buf, 4)) goto ErrRet;
                        switch (MATCH4CC(fourcc_buf))
                        {
                        case FCC_hdrl:
                        case FCC_hdrl_:
                                INFO_PRINTF(r, "Reading toplevel LIST chunk \"hdrl\"" NL);
                                do
                                {
                                        fsize_t end_of_hdrl = end_of_chunk;
                                        int avih_read = 0;
                                        int strl_read = 0;

                                        char hdrl_fourcc_buf = { 0 };
                                        uint32_t hdrl_chunk_size;
                                        fsize_t hdrl_chunk_pos;
                                        fsize_t hdrl_end_of_chunk;
                                        do
                                        {
                                                if (!must_read(r, hdrl_fourcc_buf, 4)) goto ErrRet;
                                                if (!must_read(r, &hdrl_chunk_size, 4)) goto ErrRet;
                                                if (!must_tell(r, &hdrl_chunk_pos)) goto ErrRet;
                                                hdrl_end_of_chunk = hdrl_chunk_pos + hdrl_chunk_size;
                                                switch (MATCH4CC(hdrl_fourcc_buf))
                                                {
                                                case FCC_avih:
                                                case FCC_avih_:
                                                        if (avih_read)
                                                        {
                                                                FATAL_PRINTF(r, "AVI file format corrupted: duplicated main AVI header \"avih\"" NL);
                                                                goto ErrRet;
                                                        }
                                                        INFO_PRINTF(r, "Reading the main AVI header \"avih\"" NL);
                                                        r->avih.cb = hdrl_chunk_size;
                                                        if (!must_read(r, &(&(r->avih.cb)), r->avih.cb)) goto ErrRet;
                                                        if (r->avih.dwStreams > AVI_MAX_STREAMS)
                                                        {
                                                                FATAL_PRINTF(r, "The AVI file contains too many streams (%u) exceeded the limit %d" NL, r->avih.dwStreams, AVI_MAX_STREAMS);
                                                                goto ErrRet;
                                                        }
                                                        has_index = (r->avih.dwFlags & AVIF_HASINDEX) == AVIF_HASINDEX;
                                                        avih_read = 1;
                                                        break;
                                                case FCC_LIST:
                                                case FCC_LIST_:
                                                        INFO_PRINTF(r, "Reading the stream list" NL);
                                                        if (!must_match(r, "strl")) goto ErrRet;

                                                        fsize_t string_len = 0;
                                                        uint32_t stream_id = r->num_streams;
                                                        if (stream_id >= AVI_MAX_STREAMS)
                                                        {
                                                                FATAL_PRINTF(r, "Too many streams in the AVI file, max supported streams is %d" NL, AVI_MAX_STREAMS);
                                                                goto ErrRet;
                                                        }
                                                        avi_stream_info *stream_data = &r->avi_stream_info;
                                                        const fsize_t max_string_len = AVI_MAX_STREAM_NAME - 1;

                                                        char strl_fourcc = { 0 };
                                                        uint32_t strl_chunk_size = 0;
                                                        fsize_t strl_chunk_pos;
                                                        fsize_t strl_end_of_chunk;
                                                        do
                                                        {
                                                                if (!must_read(r, strl_fourcc, 4)) goto ErrRet;
                                                                if (!must_read(r, &strl_chunk_size, 4)) goto ErrRet;
                                                                if (!must_tell(r, &strl_chunk_pos)) goto ErrRet;
                                                                strl_end_of_chunk = strl_chunk_pos + strl_chunk_size;
                                                                switch (MATCH4CC(strl_fourcc))
                                                                {
                                                                default:
                                                                case FCC_JUNK:
                                                                case FCC_JUNK_:
                                                                        INFO_PRINTF(r, "Skipping chunk \"%s\"" NL, strl_fourcc);
                                                                        break;
                                                                case FCC_strh:
                                                                case FCC_strh_:
                                                                        INFO_PRINTF(r, "Reading the stream header for stream id %u" NL, stream_id);
                                                                        if (!must_read(r, &stream_data->stream_header, strl_chunk_size)) goto ErrRet;
                                                                        string_len = (sizeof stream_data->stream_name) - 1;
                                                                        break;
                                                                case FCC_strf:
                                                                case FCC_strf_:
                                                                        INFO_PRINTF(r, "Reading the stream format for stream id %u" NL, stream_id);
                                                                        if (!must_tell(r, &stream_data->stream_format_offset)) goto ErrRet;
                                                                        stream_data->stream_format_len = strl_chunk_size;
                                                                        break;
                                                                case FCC_strd:
                                                                case FCC_strd_:
                                                                        INFO_PRINTF(r, "Reading the stream additional header data for stream id %u" NL, stream_id);
                                                                        if (!must_tell(r, &stream_data->stream_additional_header_data_offset)) goto ErrRet;
                                                                        stream_data->stream_additional_header_data_len = strl_chunk_size;
                                                                        break;
                                                                case FCC_strn:
                                                                case FCC_strn_:
                                                                        INFO_PRINTF(r, "Reading the stream name for stream id %u" NL, stream_id);
                                                                        string_len = strl_chunk_size;
                                                                        if (string_len > max_string_len) string_len = max_string_len;
                                                                        if (!must_read(r, &stream_data->stream_name, string_len)) goto ErrRet;
                                                                        break;
                                                                }
                                                                if (!must_seek(r, strl_end_of_chunk)) goto ErrRet;
                                                        } while (strl_end_of_chunk < hdrl_end_of_chunk);

                                                        if (avi_stream_is_audio(stream_data))
                                                        {
                                                                size_t max_read = sizeof stream_data->audio_format;
                                                                size_t min_read = max_read - 2;
                                                                if (stream_data->stream_format_len >= min_read)
                                                                {
                                                                        if (!must_seek(r, stream_data->stream_format_offset)) goto ErrRet;
                                                                        if (!must_read(r, &stream_data->audio_format, max_read)) goto ErrRet;
                                                                        switch (stream_data->audio_format.wFormatTag)
                                                                        {
                                                                        case 2:
                                                                                break;
                                                                        default:
                                                                                stream_data->audio_format.cbSize = 0;
                                                                                break;
                                                                        }
                                                                }
                                                        }

                                                        char fourcc_type = { 0 };
                                                        char fourcc_handler = { 0 };
                                                        *(uint32_t *)fourcc_type = stream_data->stream_header.fccType;
                                                        *(uint32_t *)fourcc_handler = stream_data->stream_header.fccHandler;
                                                        if (!string_len)
                                                        {
                                                                INFO_PRINTF(r, "Stream %u: Type: \"%s\", Handler: \"%s\"" NL, stream_id, fourcc_type, fourcc_handler);
                                                        }
                                                        else
                                                        {
                                                                INFO_PRINTF(r, "Stream %u: Type: \"%s\", Handler: \"%s\", Name: %s" NL, stream_id, fourcc_type, fourcc_handler, stream_data->stream_name);
                                                        }

                                                        strl_read++;
                                                        break;
                                                default:
                                                case FCC_JUNK:
                                                case FCC_JUNK_:
                                                        INFO_PRINTF(r, "Skipping chunk \"%s\"" NL, hdrl_fourcc_buf);
                                                        break;
                                                }
                                                if (!must_seek(r, hdrl_end_of_chunk)) goto ErrRet;
                                        } while (hdrl_end_of_chunk < end_of_hdrl);
                                        if (!must_seek(r, end_of_hdrl)) goto ErrRet;
                                        if (!avih_read)
                                        {
                                                FATAL_PRINTF(r, "Missing main AVI header \"avih\"" NL);
                                                goto ErrRet;
                                        }
                                        if (!strl_read)
                                        {
                                                FATAL_PRINTF(r, "No stream found in the AVI file." NL);
                                                goto ErrRet;
                                        }
                                } while (0);

                                break;
                        case FCC_movi:
                        case FCC_movi_:
                                INFO_PRINTF(r, "Reading toplevel LIST chunk \"movi\"" NL);
                                if (!must_tell(r, &r->stream_data_offset)) goto ErrRet;

                                // Check if the AVI file uses LIST(rec) pattern to store the packets
                                if (!must_read(r, fourcc_buf, 4)) goto ErrRet;
                                if (!memcmp(fourcc_buf, "LIST", 4))
                                {
                                        if (!must_read(r, fourcc_buf, 4)) goto ErrRet;
                                        if (!memcmp(fourcc_buf, "rec ", 4))
                                        {
                                                INFO_PRINTF(r, "This AVI file uses `LIST(rec)` structure to store packets." NL);
                                        }
                                        else
                                        {
                                                FATAL_PRINTF(r, "Inside LIST(movi): expected LIST(rec), got LIST(%s)." NL, fourcc_buf);
                                                goto ErrRet;
                                        }
                                }
                                break;
                        }
                        break;
                case FCC_idx1:
                case FCC_idx1_:
                        INFO_PRINTF(r, "Reading toplevel chunk \"idx1\"" NL);
                        if (!must_tell(r, &r->idx_offset)) goto ErrRet;
                        r->num_indices = chunk_size / sizeof(avi_index_entry);
                        break;
                }
                // Skip the current chunk
                if (!must_seek(r, end_of_chunk)) goto ErrRet;
                got_all_we_need = r->num_streams && r->stream_data_offset && ((has_index && r->idx_offset) || !has_index);
                if (end_of_chunk == r->end_of_file) break;
        }

        if (!r->idx_offset)
        {
                WARN_PRINTF(r, "No AVI index: per-stream seeking requires per-packet file traversal." NL);
        }

        return 1;
ErrRet:
        if (r) FATAL_PRINTF(r, "Reading AVI file failed." NL);
        return 0;
}
```

嗷嗷叫的老马 发表于 2025-6-12 19:04:24

6666

居然是在嵌入式里折腾,还以为是上位机
页: [1]
查看完整版本: 【C】教你手撕 AVI:提取其中的视频流和音频流