FFmpeg/Fraps v2 视频解码

从符号频次重建 Fraps 的 Huffman 解码表

FFmpeg 根据 Fraps 码流中的符号频次构造二叉前缀树,再生成实际使用的 VLC 解码表。

n7.1音视频处理核验 2026-10-12
相关概念

工程问题#

Fraps 压缩平面携带 256 个符号的频次,后续位流使用由这些频次决定的可变长编码。解码器必须重建一致的码字及符号对应关系,频次相同时也要遵守格式约定的顺序。

实际实现#

fraps2_decode_plane 读取频次并调用 ff_huff_build_tree。构建器以候选按权重正确排序为前提,反复合并最小的两个节点并重新插入父节点。父节点引用相邻的两个孩子;递归访问左、右孩子时分别追加 0 和 1,叶节点保存符号,最后将码字转换为 VLC 查表结构。

处理流程#

  1. 读取压缩平面携带的 256 个符号频次
  2. 按权重及规定的同权顺序排序待合并节点
  3. 反复合并最小两个节点,将父节点按新权重插回序列
  4. 沿二叉树生成码字与码长,再建立 VLC 解码表
  5. 使用 get_vlc2 解码符号,并恢复行间预测差值

源码片段#

固定版本 · 原文摘录
libavcodec/fraps.cn7.1 · L99–L117
查看原文件
99    for (i = 0; i < 256; i++)100        nodes[i].count = bytestream_get_le32(&src);101    size -= 1024;102    if ((ret = ff_huff_build_tree(s->avctx, &vlc, 256, VLC_BITS,103                                  nodes, huff_cmp,104                                  FF_HUFFMAN_FLAG_ZERO_COUNT)) < 0)105        return ret;106    /* we have built Huffman table and are ready to decode plane */107108    /* convert bits so they may be used by standard bitreader */109    s->bdsp.bswap_buf((uint32_t *) s->tmpbuf,110                      (const uint32_t *) src, size >> 2);111112    if ((ret = init_get_bits8(&gb, s->tmpbuf, size)) < 0)113        return ret;114115    for (j = 0; j < h; j++) {116        for (i = 0; i < w*step; i += step) {117            dst[i] = get_vlc2(&gb, vlc.table, VLC_BITS, 3);
fraps2_decode_plane

从码流读取 256 个频次并调用 Huffman 建表,后续用 get_vlc2 取符号,建立实际视频解码路径的证据。

libavcodec/huffman.cn7.1 · L178–L196
查看原文件
178    AV_QSORT(nodes, nb_codes, Node, cmp);179    cur_node = nb_codes;180    nodes[nb_codes*2-1].count = 0;181    for (i = 0; i < nb_codes * 2 - 1; i += 2) {182        uint32_t cur_count = nodes[i].count + nodes[i+1].count;183        // find correct place to insert new node, and184        // make space for the new node while at it185        for(j = cur_node; j > i + 2; j--){186            if(cur_count > nodes[j-1].count ||187               (cur_count == nodes[j-1].count &&188                !(flags & FF_HUFFMAN_FLAG_HNODE_FIRST)))189                break;190            nodes[j] = nodes[j - 1];191        }192        nodes[j].sym = HNODE;193        nodes[j].count = cur_count;194        nodes[j].n0 = i;195        cur_node++;196    }
ff_huff_build_tree

在候选正确按权重排序时,每轮把最小两项合成父节点,再将父节点插回;n0 与 n0 + 1 是该父节点的两个孩子。Fraps 比较器的数值限制在后续片段单独说明。

libavcodec/huffman.cn7.1 · L124–L139
查看原文件
124    s = nodes[node].sym;125    if (s != HNODE || (no_zero_count && !nodes[node].count)) {126        bits[*pos] = pfx;127        lens[*pos] = pl;128        xlat[*pos] = s;129        (*pos)++;130    } else {131        pfx <<= 1;132        pl++;133        get_tree_codes(bits, lens, xlat, nodes, nodes[node].n0, pfx, pl,134                       pos, no_zero_count);135        pfx |= 1;136        get_tree_codes(bits, lens, xlat, nodes, nodes[node].n0 + 1, pfx, pl,137                       pos, no_zero_count);138    }139}
get_tree_codes

内部节点递归访问两个孩子,左分支追加 0,右分支追加 1;叶节点写出码字、码长与符号,不是按键比较的搜索树。

libavcodec/fraps.cn7.1 · L77–L85
查看原文件
77/**78 * Comparator - our nodes should ascend by count79 * but with preserved symbol order80 */81static int huff_cmp(const void *va, const void *vb)82{83    const Node *a = va, *b = vb;84    return (a->count - b->count)*256 + a->sym - b->sym;85}
huff_cmp

Fraps 比较器希望按频次排序并用符号打破平局,但减法和乘法有整数范围限制。因此解释后续 Huffman 合并时,必须保留频次排序正确这一前提。

片段展示频次表进入树构建器、二叉节点合并、递归生成码字及 Fraps 比较器。实际像素符号通过 VLC 表解码;比较器的数值边界不能从树的形状推出。

收益与取舍#

工程分析

带来的收益

  • 每个符号只出现在叶节点,根到叶的二进制路径自然形成前缀码
  • 构建完成后转换成 VLC 查表结构,使解码过程无需每个比特都访问一个树节点

付出的代价

  • 每个编码平面需保存频次并重建解码表;对于固定的 256 符号表,这是一笔明确的初始化成本
  • 此实现用有序数组插入合并节点;若把符号数视为变量,构建过程含二次级移动成本,不能直接套用最小堆实现的 O(n log n)
  • 频次比较器的整数算术有范围限制;源文件的总频次检查不等同于证明比较器在任意 32 位输入上都正确。

可比较的选择

直接遍历 Huffman 二叉树解码可复用同一套码字规则,表空间较小,但每个符号需要逐位推进;当前实现用 VLC 表换取批量位查找。

版本与适用范围#

对应 FFmpeg n7.1 的 Fraps v2 压缩平面路径。二叉树用于生成前缀码,节点不满足 BST 的按键排序约束;实际解码使用 VLC 表。该版 huff_cmp 对 32 位频次数值做减法和乘法,不能保证任意输入范围下的比较顺序;Huffman 的最优性要求候选按真实权重正确排序,不能仅凭调用了建树函数就作无条件保证。

参考来源#

源码、官方文档与原始论文
01
ffmpeg · fraps2_decode_plane

libavcodec/fraps.c · fraps2_decode_plane

源码n7.1
02
ffmpeg · ff_huff_build_tree

libavcodec/huffman.c · ff_huff_build_tree

源码n7.1
03
ffmpeg · get_tree_codes

libavcodec/huffman.c · get_tree_codes

源码n7.1
04
Fraps 的频次比较器及整数算术边界

libavcodec/fraps.c · huff_cmp

源码n7.1
4 个匹配案例