跳转到内容

MP4 - MP4 解封装

MP4 解封装库把一个 MP4(ISO base media file)文件里的 H.264 视频轨和 AAC 音频轨的样本表读进内存,然后按解码顺序一个一个给出样本在文件中的位置、大小、时间戳和是否关键帧。它不解码,也不读样本内容:拿到位置和大小之后,调用者自己去文件里读,再交给视频引擎 vdec 或 AAC 解码器。

库的源码是 zephyr/lib/mp4/mp4.c,头文件是 zephyr/include/zephyr/mp4.h,应用里 #include <zephyr/mp4.h>。Kconfig 在 zephyr/lib/mp4/Kconfig(选项 CONFIG_MP4_DEMUX),由 zephyr/lib/Kconfig 引入,zephyr/lib/CMakeLists.txt 在选项打开时编译它。库只用 malloc()/free() 和 <zephyr/sys/byteorder.h>,不依赖文件系统和任何设备。

文件通过调用者给的读回调 mp4_read_t 访问,可以是内存数组、SD 卡上的 FAT 文件,或者其他能按偏移读的东西。库认识第一条 H.264 轨道(样本描述 avc1 或 avc3)和第一条 AAC 轨道(mp4a),其余轨道和编码忽略。样本表整个读进堆里,大小约 4×样本数 + 4×块数 + 8×(stsc + stts + ctts 项数) + 4×关键帧数 字节,下面用的 3 秒片段两条轨道加起来约 3 KB。

本文只讲这个库本身。用它做的完整播放器见 MP4 播放器。

库只有一个源文件、六个对外函数。上层把文件抽象成读回调,库内部是递归的盒(box)解析器,把解析结果放进 struct mp4_track;迭代器 struct mp4_iter 只是一个游标,不拥有任何内存。

flowchart TB
    APP["应用<br/>mp4_open / mp4_next / mp4_seek_sync / mp4_seek_time / mp4_close"] --> API["zephyr/mp4.h<br/>struct mp4、mp4_track、mp4_iter、mp4_sample"]
    API --> PARSE["mp4.c 盒解析器<br/>box_at / parse_children<br/>递归:moov → trak → mdia → minf → stbl"]
    API --> ITER["mp4.c 迭代器<br/>mp4_iter_init / mp4_next<br/>mp4_seek_sync / mp4_seek_time<br/>只读样本表,不碰文件"]
    PARSE --> TABLES["堆里的样本表<br/>sizes · chunk_offset · stsc · stts · ctts · stss · extra"]
    ITER --> TABLES
    PARSE -->|mp4_read_t| FILE["文件<br/>内存数组 / FAT 文件 / 其他"]
    APP -->|"用 sample.offset 和 size 自己读"| FILE

mp4_open() 只读 moov(影片盒)里的样本表,不读 mdat(媒体数据)。播放时每个样本的流程是:迭代器从内存里的表算出样本的位置,调用者按这个位置读文件,再把数据交给解码器。

flowchart LR
    F["MP4 文件"] -->|"mp4_open:只读 moov"| T["内存里的样本表"]
    T -->|"mp4_next"| S["struct mp4_sample<br/>offset · size · dts · pts · sync"]
    S -->|"读回调 read(ctx, offset, buf, size)"| F
    F -->|样本数据| D{"轨道"}
    D -->|"H.264:长度前缀 NAL"| V["vdec 视频引擎"]
    D -->|"AAC:裸 raw_data_block"| A["AAC 解码器"]
    X["extra:avcC / AudioSpecificConfig"] -.->|解码器初始化| V
    X -.-> A

MP4 是一棵盒树,每个盒的头是 4 字节大小加 4 字节类型(大小为 1 时后面跟 8 字节的 64 位大小,为 0 时盒延伸到父盒结束)。库只关心下面这些盒,其余的盒读到头就跳过。图里的偏移和大小是本文演示用的 clip.mp4(301652 字节)的真实值:

flowchart TB
    FILE["文件"] --> FTYP["ftyp @0,32 字节"]
    FILE --> MOOV["moov @32,4581 字节<br/>库只进入这个盒"]
    FILE --> MDAT["mdat @4621,297031 字节<br/>样本数据,库不读"]
    MOOV --> TRAKV["trak(视频)@148,2006 字节"]
    MOOV --> TRAKA["trak(音频)@2154,2361 字节"]
    TRAKV --> EDTS["edts → elst<br/>第一个非空编辑的 media_time → media_start"]
    TRAKV --> MDIA["mdia"]
    MDIA --> MDHD["mdhd<br/>timescale、duration"]
    MDIA --> MINF["minf → stbl @433,1721 字节"]
    MINF --> STBL["stbl 样本表"]
    STBL --> STSD["stsd<br/>avc1/avc3:宽高、avcC<br/>mp4a:声道、采样率、esds"]
    STBL --> STTS["stts<br/>解码时间差"]
    STBL --> CTTS["ctts(可选)<br/>呈现时间偏移"]
    STBL --> STSS["stss(可选)<br/>关键帧样本号"]
    STBL --> STSC["stsc<br/>每个块的样本数"]
    STBL --> STSZ["stsz<br/>每个样本的大小"]
    STBL --> STCO["stco / co64<br/>每个块的文件偏移"]

音频轨的结构相同,只是 stbl 里没有 stss、ctts(所有样本都是关键帧,呈现时间等于解码时间),多了库不看的 sgpd、sbgp。库读的盒和对应的字段:

盒位置读到的字段
moov文件顶层找到后只解析它,之后的盒不再看(moov 在 mdat 前后都行,库是按盒头一个个跳过去的)
trakmoov 下每条轨道先解析到一个临时的 mp4_track,是第一条 H.264 或第一条 AAC 就留下,否则释放
edts/elsttrak 下第一个 media_time >= 0 的编辑项 → media_start(版本 0 和 1 都支持)
mdhdmdia 下timescale、duration(版本 0 和 1 都支持)
stsdstbl 下H.264:width、height、extra(avcC 记录);AAC:channels、sample_rate、extra(AudioSpecificConfig,来自 esds)。只读 stsd 的前 1024 字节
sttsstbl 下stts[]、stts_count
cttsstbl 下ctts[]、ctts_count;没有这个盒时 pts == dts
stszstbl 下sample_count、fixed_size(非 0 表示所有样本一样大)、sizes[]、max_sample
stco、co64stbl 下chunk_offset[]、chunk_count;co64 的偏移超过 32 位返回 -EFBIG
stscstbl 下stsc[](只保留 first_chunk 和 samples_per_chunk)、stsc_count
stssstbl 下stss[]、stss_count;没有这个盒时所有样本都是关键帧(音频就是这样)

一个样本在文件里的位置怎么算

Section titled “一个样本在文件里的位置怎么算”

样本表是为了从“第 n 个样本”算出位置而设计的。mp4_next() 不是每次查表,而是在迭代器里递推,每一步 O(1):

flowchart LR
    N["样本号 n"] --> STSC["stsc<br/>n 落在第几个块,<br/>块里有几个样本"]
    STSC --> STCO["stco<br/>块的起始偏移"]
    STCO --> ADD["加上块内前面样本的 stsz 之和"]
    ADD --> OFF["offset"]
    N --> STSZ["stsz[n]"] --> SIZE["size"]
    N --> STTS["stts 累加"] --> DTS["dts"]
    DTS --> PTS["pts = dts + ctts"]
    N --> STSS["stss 里有 n+1 吗"] --> SYNC["sync"]

mp4_next() 里:一个块里的样本用完就换下一个块,偏移取 chunk_offset[chunk];块内的下一个样本偏移是前一个的偏移加大小;stts、ctts 各有一个“当前项还剩几个”的计数;stss 的下标只往前走。

不涉及。这个库是纯软件,不需要设备树节点,也没有绑定。

选项默认含义
CONFIG_MP4_DEMUXn,应用里设为 y编译 MP4 解封装库(lib/mp4/mp4.c)。没有依赖项
CONFIG_COMMON_LIBC_MALLOC_ARENA_SIZE来自 libc库用 malloc() 放样本表和 extra,需要 libc 堆;演示里设为 -1,把剩余内存都给 libc 堆
CONFIG_MAIN_STACK_SIZE来自内核库自己的栈使用很小(stsd 解析用一个 1 KiB 的局部缓冲区,盒的递归最多 6 层),演示里设为 4096

mp4_next() 给出的一个样本:

字段含义
offset样本在文件中的位置(64 位)
size样本字节数
dts解码时间,单位是轨道的 timescale
pts呈现时间,dts 加 ctts 偏移,单位同上;要得到从 0 开始的播放时间还要减 media_start
index样本序号,从 0 开始,按解码顺序
sync1 表示关键帧(音频恒为 1)

一条轨道,struct mp4 里有两条:video 和 audio。轨道不存在时 codec 是 MP4_CODEC_NONE,其余字段都是 0。

字段含义
codecMP4_CODEC_NONE、MP4_CODEC_H264(extra 是 avcC 记录)、MP4_CODEC_AAC(extra 是 AudioSpecificConfig)
timescale时间单位:每秒多少个 tick
duration媒体时长,tick(来自 mdhd,不扣除编辑列表)
media_start编辑列表给出的起始媒体时间;播放时间 = pts - media_start,没有编辑列表为 0
sample_count样本总数
max_sample最大样本的字节数,可以用来一次分配好读缓冲区
width、height视频的宽高
channels、sample_rate音频的声道数和采样率(Hz,只取 16.16 定点数的整数部分)
extra、extra_len解码器初始化数据,库分配,mp4_close() 释放
sizes、fixed_sizestsz:逐样本大小,或所有样本相同的大小
chunk_count、chunk_offsetstco/co64:每个块的文件偏移
stsc_count、stscstsc:(first_chunk,samples_per_chunk)成对
stts_count、sttsstts:(count,delta)成对
ctts_count、cttsctts:(count,offset)成对,没有为 0
stss_count、stssstss:关键帧的样本号(从 1 起),没有则 stss 为 NULL

sizes 以后的字段是库内部用的,应用只读不改。

样本游标。必须用 mp4_iter_init() 初始化,不要自己填。它是普通结构体,赋值就是保存和恢复位置(mp4_seek_sync() 内部就是这样做的)。next 字段是下一个样本的序号,其余字段是内部状态。

一个打开的文件:read、ctx、size 是 mp4_open() 记下的读回调、回调参数和文件大小(调用者也可以直接用 m->read(m->ctx, ...) 读样本数据),video、audio 是两条轨道。

演示的数据是一个 3 秒的片段 clip.mp4(从 video540p.mp4 截出来,视频重新编码成每 15 帧一个关键帧,保留 B 帧;音频直接复制),编进固件,读回调从内存数组里读。每个接口一个小标题,下面是它的原型、行为、一个完整的示例函数和板子上的输出。

演示里重复用到的数据和辅助函数:

static const uint8_t clip[] = {
#include "clip.inc"
};
static struct mp4 mp4;
/* Track time units to milliseconds */
static int32_t to_ms(int64_t ticks, uint32_t timescale)
{
return (int32_t)(ticks * 1000 / timescale);
}
static const char *codec_name(enum mp4_codec c)
{
switch (c) {
case MP4_CODEC_H264:
return "H264";
case MP4_CODEC_AAC:
return "AAC";
default:
return "NONE";
}
}
static void print_track(const char *name, const struct mp4_track *t)
{
printk("%s: codec=%s timescale=%u duration=%u (%d ms) media_start=%d\n", name,
codec_name(t->codec), t->timescale, (uint32_t)t->duration,
to_ms(t->duration, t->timescale ? t->timescale : 1), (int32_t)t->media_start);
printk(" samples=%u max_sample=%u chunks=%u stsc=%u stts=%u ctts=%u stss=%u\n",
t->sample_count, t->max_sample, t->chunk_count, t->stsc_count, t->stts_count,
t->ctts_count, t->stss_count);
if (t->codec == MP4_CODEC_H264) {
printk(" %ux%u avcC(%u bytes): profile_idc=%u level_idc=%u\n", t->width,
t->height, t->extra_len, t->extra[1], t->extra[3]);
} else if (t->codec == MP4_CODEC_AAC) {
printk(" %u ch %u Hz AudioSpecificConfig(%u bytes): %02x %02x\n", t->channels,
t->sample_rate, t->extra_len, t->extra[0], t->extra[1]);
}
}
typedef int (*mp4_read_t)(void *ctx, uint64_t offset, void *buf, size_t len);

读回调。从文件 offset 处读 len 字节到 buf,成功返回 0,失败返回负的 errno;库不会重试,也原样把这个错误码传回给调用者。ctx 是 mp4_open() 时给的指针。读不到足够的数据(超出文件)也要返回错误,不能默默返回短读。

演示用的回调从一个内存数组读,另有一个总是失败的回调用来看错误传递:

/* Read callback: the file is an array in memory */
static int mem_read(void *ctx, uint64_t offset, void *buf, size_t len)
{
const uint8_t *data = ctx;
if (offset + len > sizeof(clip)) {
return -EIO;
}
memcpy(buf, data + offset, len);
return 0;
}
/* A read callback that always fails */
static int bad_read(void *ctx, uint64_t offset, void *buf, size_t len)
{
ARG_UNUSED(ctx);
ARG_UNUSED(offset);
ARG_UNUSED(buf);
ARG_UNUSED(len);
return -EIO;
}

这两个回调的效果在后面各节的输出里:mem_read 让 mp4_open() 成功,bad_read 让它返回 -5(-EIO)。

int mp4_open(struct mp4 *m, mp4_read_t read, void *ctx, uint64_t file_size);

先把 *m 清零,再从文件顶层找 moov,解析里面的轨道,读入样本表。第一条 H.264 轨道放进 m->video,第一条 AAC 轨道放进 m->audio,其他轨道忽略。

  • 成功返回 0。两条轨道至少有一条;只有视频或只有音频的文件也能打开,另一条的 codec 是 MP4_CODEC_NONE。
  • 找不到 moov,或者 moov 里没有 H.264/AAC 轨道,返回 -ENOENT。
  • 盒的大小超出父盒、样本表缺项(样本数、块数、stsc、stts 为 0,timescale 为 0,比如分片 MP4)、stsd 太短,返回 -EINVAL。
  • 内存不够返回 -ENOMEM,co64 偏移超过 32 位返回 -EFBIG,读回调的错误原样返回。
  • 失败时库已经自己调用了 mp4_close(),不需要再调。成功后用完要调 mp4_close()。
/* mp4_open(): parse the movie box of the clip */
static int demo_open(void)
{
int ret = mp4_open(&mp4, mem_read, (void *)clip, sizeof(clip));
if (ret != 0) {
printk("mp4_open failed: %d\n", ret);
return ret;
}
printk("file size %u bytes\n", (uint32_t)mp4.size);
print_track("video", &mp4.video);
print_track("audio", &mp4.audio);
return 0;
}
file size 301652 bytes
video: codec=H264 timescale=15360 duration=46080 (3000 ms) media_start=1024
samples=90 max_sample=11636 chunks=89 stsc=2 stts=1 ctts=82 stss=6
960x540 avcC(39 bytes): profile_idc=77 level_idc=31
audio: codec=AAC timescale=44100 duration=134144 (3041 ms) media_start=1024
samples=131 max_sample=430 chunks=89 stsc=78 stts=1 ctts=0 stss=0
2 ch 44100 Hz AudioSpecificConfig(5 bytes): 12 10

profile_idc=77 是 Main,level_idc=31 是 3.1;AudioSpecificConfig 的 12 10 是 AAC-LC、44100 Hz、双声道。视频有 ctts(有 B 帧),所以 media_start 是 1024:第一个样本的 pts 是 1024,要减掉才是播放时间 0。

/* mp4_open() on input it cannot use: the error codes */
static void demo_open_errors(void)
{
struct mp4 m;
int ret;
/* too short to hold a box header: no movie box */
ret = mp4_open(&m, mem_read, (void *)clip, 4);
printk("size 4: ret=%d\n", ret);
/* the first box (ftyp) claims to be longer than the file */
ret = mp4_open(&m, mem_read, (void *)clip, 16);
printk("size 16: ret=%d\n", ret);
/* a read callback that fails: its error is passed through */
ret = mp4_open(&m, bad_read, NULL, sizeof(clip));
printk("bad_read: ret=%d\n", ret);
/* not an MP4 at all: the 4 byte box size is 0x00000000 so it spans the file,
* but there is no moov in it
*/
static const uint8_t zeros[64];
ret = mp4_open(&m, mem_read, (void *)zeros, sizeof(zeros));
printk("zeros: ret=%d\n", ret);
}
size 4: ret=-2
size 16: ret=-22
bad_read: ret=-5
zeros: ret=-2

长度 4 放不下一个盒头,没有 moov,是 -ENOENT(-2);长度 16 时 ftyp 盒声称有 32 字节,超出文件,是 -EINVAL(-22);读回调失败的 -EIO(-5)原样返回;全零数据没有 moov,是 -ENOENT。

void mp4_close(struct mp4 *m);

释放两条轨道的 extra 和全部样本表,并把轨道清零。不关闭文件(文件是调用者的)。关闭之后可以再关一次,没有副作用;mp4_open() 失败时已经自动关闭过。关闭后,之前 mp4_next() 给出的 struct mp4_sample 里的数值仍然有效(它们是拷贝),但迭代器不能再用。

/* mp4_close(): free the tables, the struct is zeroed and can be closed again */
static void demo_close(void)
{
struct mp4 m;
int ret = mp4_open(&m, mem_read, (void *)clip, sizeof(clip));
if (ret != 0) {
printk("mp4_open failed: %d\n", ret);
return;
}
printk("before close: video sizes=%s samples=%u\n", m.video.sizes ? "set" : "NULL",
m.video.sample_count);
mp4_close(&m);
printk("after close: video sizes=%s samples=%u codec=%s\n",
m.video.sizes ? "set" : "NULL", m.video.sample_count, codec_name(m.video.codec));
mp4_close(&m);
printk("closed twice without trouble\n");
}
before close: video sizes=set samples=90
after close: video sizes=NULL samples=0 codec=NONE
closed twice without trouble
int mp4_iter_init(const struct mp4_track *t, struct mp4_iter *it);

把迭代器放到轨道的第一个样本前,成功返回 0。轨道不存在(t 为 NULL、codec 是 MP4_CODEC_NONE,包括已经 mp4_close() 的轨道)或样本表不完整时返回 -EINVAL,迭代器被清零,不能再交给 mp4_next()。所以文件里可能没有的那条轨道,用之前不必先查 codec,检查返回值就行。

/* mp4_iter_init(): an iterator starts at the first sample of a track */
static void demo_iter_init(const struct mp4_track *t)
{
struct mp4_iter it;
int ret = mp4_iter_init(t, &it);
if (ret != 0) {
printk("mp4_iter_init: %d\n", ret);
return;
}
printk("next=%u chunk=%u in_chunk=%u chunk_pos=%u stts_left=%u dts=%d\n", it.next,
it.chunk, it.in_chunk, (uint32_t)it.chunk_pos, it.stts_left, (int32_t)it.dts);
}
next=0 chunk=0 in_chunk=0 chunk_pos=4629 stts_left=90 dts=0

第一个样本在文件偏移 4629,在 mdat(@4621)的数据区里;stts_left=90 是 stts 第一项的计数,这条轨道只有一项(每帧间隔相同,下一节里 dts 每次加 512)。

/* mp4_iter_init() on a track the file does not have */
static void demo_iter_init_errors(void)
{
struct mp4 m;
struct mp4_iter it;
struct mp4_track none = {0};
/* a track after mp4_close(), and one that was never filled in */
mp4_open(&m, mem_read, (void *)clip, sizeof(clip));
mp4_close(&m);
printk("track closed: ret=%d\n", mp4_iter_init(&m.audio, &it));
printk("empty track: ret=%d\n", mp4_iter_init(&none, &it));
printk("NULL track: ret=%d\n", mp4_iter_init(NULL, &it));
}
track closed: ret=-22
empty track: ret=-22
NULL track: ret=-22

三种情况都是 -EINVAL(-22)。

int mp4_next(const struct mp4_track *t, struct mp4_iter *it, struct mp4_sample *s);

取出迭代器当前的样本,填进 *s,然后把迭代器推进到下一个。按解码顺序,index 依次是 0、1、2…。成功返回 0;样本取完返回 -ENODATA(-61),这时 *s 不变。它只算位置,不读文件。

要得到样本数据,用 s.offset 和 s.size 调读回调。H.264 的样本是长度前缀的 NAL(每个 NAL 前面 4 字节大端长度,不是 Annex B 起始码),AAC 的样本是裸的 raw_data_block,没有 ADTS 头。

/* mp4_next(): list the first samples of a track and read the first one */
static void demo_next(const struct mp4_track *t, const char *name, int count)
{
struct mp4_iter it;
struct mp4_sample s;
uint8_t head[8];
int ret;
mp4_iter_init(t, &it);
for (int i = 0; i < count; i++) {
ret = mp4_next(t, &it, &s);
if (ret != 0) {
printk("mp4_next: %d\n", ret);
return;
}
printk("%s #%u off=%u size=%u dts=%d pts=%d (%d ms) sync=%u\n", name, s.index,
(uint32_t)s.offset, s.size, (int32_t)s.dts, (int32_t)s.pts,
to_ms(s.pts - t->media_start, t->timescale), s.sync);
if (i == 0) {
ret = mp4.read(mp4.ctx, s.offset, head, sizeof(head));
if (ret != 0) {
printk("read sample: %d\n", ret);
return;
}
printk(" first bytes: %02x %02x %02x %02x %02x %02x %02x %02x\n", head[0],
head[1], head[2], head[3], head[4], head[5], head[6], head[7]);
}
}
}
video #0 off=4629 size=7604 dts=0 pts=1024 (0 ms) sync=1
first bytes: 00 00 02 bc 06 05 ff ff
video #1 off=12233 size=2733 dts=512 pts=2560 (100 ms) sync=0
video #2 off=15304 size=1542 dts=1024 pts=1536 (33 ms) sync=0
video #3 off=17587 size=1138 dts=1536 pts=2048 (66 ms) sync=0
video #4 off=19087 size=2503 dts=2048 pts=4096 (200 ms) sync=0
video #5 off=22308 size=1294 dts=2560 pts=3072 (133 ms) sync=0
video #6 off=23961 size=1109 dts=3072 pts=3584 (166 ms) sync=0
video #7 off=25811 size=2641 dts=3584 pts=5632 (300 ms) sync=0
audio #0 off=14966 size=338 dts=0 pts=0 (-23 ms) sync=1
first bytes: de 02 00 4c 61 76 63 36
audio #1 off=16846 size=408 dts=1024 pts=1024 (0 ms) sync=1
audio #2 off=17254 size=333 dts=2048 pts=2048 (23 ms) sync=1
audio #3 off=18725 size=362 dts=3072 pts=3072 (46 ms) sync=1

看点:

  • 视频 dts 单调(每帧加 512),pts 不单调(1024、2560、1536、2048…):这是 B 帧,样本 #1 是 P 帧,要等后面的 B 帧(#2、#3)显示完才轮到它。呈现顺序要按 pts 排。
  • 第一个视频样本开头 00 00 02 bc 是 NAL 长度 700,06 是 SEI NAL(nal_unit_type 6)。
  • 视频和音频在文件里是交错的:视频 #0 在 4629,音频 #0 在 14966,视频 #2 在 15304。
  • 音频 #0 的呈现时间是 -23 ms:AAC 编码器延迟,编辑列表把前 1024 个采样跳过,所以 pts - media_start 为负的样本不用播放。
/* mp4_next() until it returns -ENODATA: totals of a whole track */
static void demo_next_all(const struct mp4_track *t, const char *name)
{
struct mp4_iter it;
struct mp4_sample s = {0};
uint32_t n = 0, sync = 0;
uint64_t bytes = 0;
int ret;
mp4_iter_init(t, &it);
while ((ret = mp4_next(t, &it, &s)) == 0) {
n++;
bytes += s.size;
sync += s.sync;
}
printk("%s: ret=%d samples=%u sync=%u bytes=%u last dts=%d pts=%d\n", name, ret, n, sync,
(uint32_t)bytes, (int32_t)s.dts, (int32_t)s.pts);
}
video: ret=-61 samples=90 sync=6 bytes=248350 last dts=45568 pts=46080
audio: ret=-61 samples=131 sync=131 bytes=48673 last dts=133120 pts=133120

最后返回的是 -ENODATA(-61)。视频 90 个样本、6 个关键帧、共 248350 字节;音频 131 个样本、全是关键帧。

int mp4_seek_sync(const struct mp4_track *t, struct mp4_iter *it, uint32_t index);

把迭代器放到“样本序号 index 或之前最近的一个关键帧”上,之后 mp4_next() 返回的第一个样本就是这个关键帧。解码器必须从关键帧开始,所以 seek 到某个画面时要从这里开始解码,把不要显示的帧丢掉。成功返回 0;index 超出范围就落在最后一个关键帧上;轨道不存在返回 -EINVAL。函数从轨道开头往后扫(O(n),只扫内存里的表,不读文件)。

/* mp4_seek_sync(): the key frame at or before a sample, then read on from there */
static void demo_seek_sync(const struct mp4_track *t, uint32_t index)
{
struct mp4_iter it;
struct mp4_sample s;
int ret = mp4_seek_sync(t, &it, index);
printk("seek sample %u: ret=%d, restart from sample %u\n", index, ret, it.next);
for (int i = 0; i < 3 && mp4_next(t, &it, &s) == 0; i++) {
printk(" #%u off=%u size=%u pts=%d (%d ms) sync=%u\n", s.index,
(uint32_t)s.offset, s.size, (int32_t)s.pts,
to_ms(s.pts - t->media_start, t->timescale), s.sync);
}
}
seek sample 0: ret=0, restart from sample 0
#0 off=4629 size=7604 pts=1024 (0 ms) sync=1
#1 off=12233 size=2733 pts=2560 (100 ms) sync=0
#2 off=15304 size=1542 pts=1536 (33 ms) sync=0
seek sample 47: ret=0, restart from sample 45
#45 off=134516 size=9763 pts=24064 (1500 ms) sync=1
#46 off=145039 size=4830 pts=26112 (1633 ms) sync=0
#47 off=150246 size=2400 pts=25088 (1566 ms) sync=0
seek sample 89: ret=0, restart from sample 75
#75 off=245325 size=11636 pts=39424 (2500 ms) sync=1
#76 off=257704 size=5481 pts=41472 (2633 ms) sync=0
#77 off=263536 size=2140 pts=40448 (2566 ms) sync=0
seek sample 1000: ret=0, restart from sample 75
#75 off=245325 size=11636 pts=39424 (2500 ms) sync=1
#76 off=257704 size=5481 pts=41472 (2633 ms) sync=0
#77 off=263536 size=2140 pts=40448 (2566 ms) sync=0
seek sample 66: ret=0, restart from sample 66
#66 off=152646 size=372 pts=67584 (1509 ms) sync=1
#67 off=153018 size=376 pts=68608 (1532 ms) sync=1
#68 off=155115 size=376 pts=69632 (1555 ms) sync=1

视频的关键帧在样本 0、15、30、45、60、75。样本 47 往回退到 45,样本 89 退到 75,1000 超出范围也落在 75。音频每个样本都是关键帧,样本 66 就停在 66。

int mp4_seek_time(const struct mp4_track *t, struct mp4_iter *it, int64_t time);

按播放时间 seek。time 的单位是轨道的 timescale,0 是播放开始(函数内部加上 media_start,调用者不用管编辑列表)。函数找出 pts 不早于目标的样本里 pts 最小的那个,也就是要显示的那一帧,再像 mp4_seek_sync() 一样退到它前面最近的关键帧。有 B 帧时解码顺序和显示顺序不同,所以是按 pts 的最小值找,不是解码顺序里第一个超过目标的样本。

  • 成功返回 0,迭代器在要开始解码的关键帧上;解码后 pts - media_start 小于 time 的帧要丢掉,不显示。
  • time 超过最后一个样本返回 -ERANGE,迭代器在最后一个关键帧上。
  • 轨道不存在返回 -EINVAL。
  • 函数扫一遍全部样本,O(n),不读文件。
/* mp4_seek_time(): the key frame to start from to show the frame at a playback time */
static void demo_seek_time(const struct mp4_track *t, int32_t target_ms)
{
struct mp4_iter it;
struct mp4_sample s;
int64_t ticks = (int64_t)target_ms * t->timescale / 1000;
int ret = mp4_seek_time(t, &it, ticks);
printk("seek %d ms (%d ticks): ret=%d, restart from sample %u\n", target_ms,
(int32_t)ticks, ret, it.next);
for (int i = 0; i < 2 && mp4_next(t, &it, &s) == 0; i++) {
printk(" #%u off=%u size=%u pts=%d (%d ms, ffprobe pts %d) sync=%u\n", s.index,
(uint32_t)s.offset, s.size, (int32_t)s.pts,
to_ms(s.pts - t->media_start, t->timescale),
(int32_t)(s.pts - t->media_start), s.sync);
}
}
seek 0 ms (0 ticks): ret=0, restart from sample 0
#0 off=4629 size=7604 pts=1024 (0 ms, ffprobe pts 0) sync=1
#1 off=12233 size=2733 pts=2560 (100 ms, ffprobe pts 1536) sync=0
seek 1500 ms (23040 ticks): ret=0, restart from sample 45
#45 off=134516 size=9763 pts=24064 (1500 ms, ffprobe pts 23040) sync=1
#46 off=145039 size=4830 pts=26112 (1633 ms, ffprobe pts 25088) sync=0
seek 2000 ms (30720 ticks): ret=0, restart from sample 60
#60 off=188864 size=11184 pts=31744 (2000 ms, ffprobe pts 30720) sync=1
#61 off=200418 size=5786 pts=33792 (2133 ms, ffprobe pts 32768) sync=0
seek 2900 ms (44544 ticks): ret=0, restart from sample 75
#75 off=245325 size=11636 pts=39424 (2500 ms, ffprobe pts 38400) sync=1
#76 off=257704 size=5481 pts=41472 (2633 ms, ffprobe pts 40448) sync=0
seek 5000 ms (76800 ticks): ret=-34, restart from sample 75
#75 off=245325 size=11636 pts=39424 (2500 ms, ffprobe pts 38400) sync=1
#76 off=257704 size=5481 pts=41472 (2633 ms, ffprobe pts 40448) sync=0
seek 1500 ms (66150 ticks): ret=0, restart from sample 66
#66 off=152646 size=372 pts=67584 (1509 ms, ffprobe pts 66560) sync=1
#67 off=153018 size=376 pts=68608 (1532 ms, ffprobe pts 67584) sync=1

ret=-34 是 -ERANGE。每一条的“ffprobe pts”是 pts - media_start,用来和主机的 ffprobe 对,见下面的验证。

库不改 dts、pts,原样给出 mdhd/ctts 里的值,同时把第一个非空编辑项的 media_start 单独放在轨道里。原因有两个:解码器和 B 帧重排需要原始的 dts/pts 顺序,改了反而要再还原;而减掉 media_start 只是一行 pts - t->media_start。需要“从 0 开始的播放时间”的地方(比如 mp4_seek_time())库自己处理,其余由调用者减。库只用编辑列表的第一个非空项,多段编辑(拼接、静音段)不处理。

下面每一项写验证了什么、怎么验证、板子上看到什么。数据是上面的 clip.mp4,主机侧的参照是 ffprobe(ffprobe -show_entries stream=...、-show_entries packet=...,以及一个脚本 mp4-seekcheck.py 从 ffprobe 的包表算出 seek 应该落在哪个关键帧),输出在最后给出。

验证 mp4_open() 读出的编码、分辨率、时长、样本数和 ffprobe 一致。板子上 demo_open 打印:视频 H264、960x540、timescale 15360、duration 46080(3000 ms)、90 个样本、profile_idc 77(Main)、level_idc 31;音频 AAC、2 声道、44100 Hz、131 个样本、AudioSpecificConfig 12 10。ffprobe 给出 h264 Main level 31、960x540、time_base 1/15360、duration_ts 46080、nb_frames 90,以及 aac LC、44100 Hz、2 声道、nb_frames 131。一致。

音频时长不同:库给的是 mdhd 里的 134144(3041 ms),ffprobe 的 duration_ts 是 133094,它按编辑列表算实际呈现的时长。这不是错,mp4_track.duration 就是 mdhd 的值。

验证 mp4_next() 算出的 offset、size。板子上视频 #0..#7 是 4629/7604、12233/2733、15304/1542、17587/1138、19087/2503、22308/1294、23961/1109、25811/2641;音频 #0..#3 是 14966/338、16846/408、17254/333、18725/362。ffprobe 的 pos/size 完全相同。另外 demo_next 用读回调读出视频 #0 的前 8 字节:00 00 02 bc 06 ...,是 4 字节长度加一个 SEI NAL,符合 MP4 里 H.264 的长度前缀格式。

验证走完整条轨道(demo_next_all)。板子上视频 90 个样本、6 个关键帧、248350 字节,音频 131 个样本、48673 字节,最后都返回 -ENODATA。ffprobe 的合计一样:视频 90/6/248350,音频 131/48673;关键帧在包序号 0 15 30 45 60 75。

验证 dts、pts、media_start。板子上视频 #0 dts=0 pts=1024,#1 pts=2560,#2 pts=1536,media_start=1024;ffprobe 的是 dts=-1024 pts=0、pts=1536、pts=512。差都是 1024,也就是 media_start。视频 dts 每帧加 512,pts 不单调,是 B 帧。音频 #0 板子 pts=0,ffprobe 是 -1024,同样差 1024,换算成 -23 ms,是编码器延迟。

验证 mp4_open() 和 mp4_iter_init() 的返回值,用 demo_open_errors 和 demo_iter_init_errors。板子上:长度 4 返回 -2,长度 16 返回 -22,读回调失败返回 -5,全零数据返回 -2;已关闭的轨道、全零的轨道、NULL 轨道调用 mp4_iter_init() 都返回 -22。这些是库代码的预期返回值,没有 ffprobe 可比的部分。mp4_close() 关闭两次也没有问题(demo_close)。

验证 mp4_seek_sync() 和 mp4_seek_time() 落在 ffprobe 算出的关键帧上。mp4-seekcheck.py 对每个目标时间,在 ffprobe 的包表里找 pts 不早于目标的最小者,再往前找最近的关键帧。板子上 mp4_seek_time() 的结果:0 ms 到样本 0、1500 ms 到 45、2000 ms 到 60、2900 ms 到 75、5000 ms 返回 -ERANGE 并停在 75;音频 1500 ms 到 66。脚本给出的:0、45、60、75、越界、音频 66。完全一致,找到的关键帧的 pts - media_start 也和 ffprobe 的 pts 相同(23040、30720、……)。mp4_seek_sync() 的样本 47、89、1000、音频 66 分别落在 45、75、75、66,和关键帧列表 0 15 30 45 60 75 一致。

$ ffprobe -hide_banner clip.mp4 (streams)
Duration: 00:00:03.02, start: 0.000000, bitrate: 799 kb/s
Stream #0:0[0x1](und): Video: h264 (Main) (avc1 / 0x31637661), yuv420p(progressive), 960x540 [SAR 1:1 DAR 16:9], 662 kb/s, 30 fps, 30 tbr, 15360 tbn (default)
Stream #0:1[0x2](und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 128 kb/s (default)
$ ffprobe -show_entries stream=...
stream|codec_name=h264|profile=Main|width=960|height=540|level=31|time_base=1/15360|duration_ts=46080|nb_frames=90
stream|codec_name=aac|profile=LC|sample_rate=44100|channels=2|time_base=1/44100|duration_ts=133094|nb_frames=131
$ ffprobe -select_streams v -show_entries packet=pos,size,dts,pts,flags -of csv (前 8 个包,列依次是 pts,dts,size,pos,flags)
0,-1024,7604,4629,K__
1536,-512,2733,12233,___
512,0,1542,15304,___
1024,512,1138,17587,___
3072,1024,2503,19087,___
2048,1536,1294,22308,___
2560,2048,1109,23961,___
4608,2560,2641,25811,___
$ 视频:关键帧的包序号(0 起)
0 15 30 45 60 75
$ 视频合计
samples=90 sync=6 bytes=248350
$ ffprobe -select_streams a -show_entries packet=pos,size,dts,pts -of csv (前 4 个包,列依次是 pts,dts,size,pos)
-1024,-1024,338,14966,
0,0,408,16846
1024,1024,333,17254
2048,2048,362,18725
$ 音频合计
samples=131 bytes=48673
$ ffprobe -show_entries format=size,duration
format|duration=3.018005|size=301652
$ python3 seekcheck.py clip.mp4 (ffprobe 的包表:目标时间之后 pts 最小的帧,以及它所属的关键帧;序号按解码顺序从 0 起)
video 0 ms (0 ticks): frame 0 pts 0, key frame 0
video 1500 ms (23040 ticks): frame 45 pts 23040, key frame 45
video 2000 ms (30720 ticks): frame 60 pts 30720, key frame 60
video 2900 ms (44544 ticks): frame 84 pts 44544, key frame 75
video 5000 ms (76800 ticks): past the end
audio 1500 ms (66150 ticks): frame 66 pts 66560, key frame 66

这些项没有对应的测试文件,或者本文的演示没有覆盖,所以没有验证,库代码里有对应的分支:

项没有验证的原因
分片 MP4(moof)库不支持:影片盒里没有样本表,mp4_open() 返回 -EINVAL。没有用分片文件试过
co64,偏移超过 32 位库只存 32 位块偏移,高 32 位非 0 返回 -EFBIG;演示片段用的是 stco,没有 co64 文件
avc3、mdhd/elst 版本 1、stsd 超过 1024 字节演示片段是 avc1、版本 0、stsd 185 字节
多条同类轨道、其他编码的轨道演示片段只有一条视频一条音频;“忽略其余轨道”没有用多轨道文件试过
只有视频或只有音频的文件同上,没有这样的片段
从 SD 卡的文件读演示的读回调读内存数组,没有接 fs_read();播放器用 SD 卡文件,见 MP4 播放器
样本内容的解码库不解码,板子上只核对了 NAL 长度前缀
多段编辑列表库只用第一个非空编辑项,没有多段编辑的文件

上面所有示例和日志都来自这一个程序。建一个目录,放进下面的文件,就可以像其他 Zephyr 应用一样编译。clip.mp4 是被编进固件的片段,用这条命令从任意 MP4 截一段(视频重新编码成每 15 帧一个关键帧,音频直接复制):

终端窗口
ffmpeg -t 3 -i video540p.mp4 -c:v libx264 -profile:v main -g 15 -b:v 600k \
-c:a copy -movflags +faststart clip.mp4
mp4_api/
├── CMakeLists.txt
├── prj.conf
├── clip.mp4
└── src/
└── main.c

CMake 的 generate_inc_file_for_target() 把 clip.mp4 变成一个逗号分隔的字节列表 clip.inc,main.c 把它 #include 进一个 const uint8_t 数组里。演示不需要 SD 卡和文件系统。要读 SD 卡上的文件,把读回调换成 fs_seek() 加 fs_read() 即可,做法见 MP4 播放器。

cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(mp4_api)
# clip.mp4 is turned into clip.inc (a comma separated list of bytes) that
# main.c includes into a const array, so the demo needs no file system
set(CLIP_INC ${ZEPHYR_BINARY_DIR}/include/generated/clip.inc)
generate_inc_file_for_target(app ${CMAKE_CURRENT_SOURCE_DIR}/clip.mp4 ${CLIP_INC})
target_sources(app PRIVATE src/main.c)
CONFIG_MP4_DEMUX=y
CONFIG_PRINTK=y
CONFIG_LOG=y
CONFIG_LOG_PRINTK=n
CONFIG_MAIN_STACK_SIZE=4096
CONFIG_COMMON_LIBC_MALLOC_ARENA_SIZE=-1
CONFIG_SHELL=n
CONFIG_DEVMEM_SHELL=n
/*
* MP4 demultiplexer demo: the clip is linked into the firmware, mp4_open()
* reads it through a callback, then every function of <zephyr/mp4.h> is shown.
*/
#include <errno.h>
#include <stdint.h>
#include <string.h>
#include <zephyr/kernel.h>
#include <zephyr/mp4.h>
#include <zephyr/sys/printk.h>
static const uint8_t clip[] = {
#include "clip.inc"
};
static struct mp4 mp4;
/* Read callback: the file is an array in memory */
static int mem_read(void *ctx, uint64_t offset, void *buf, size_t len)
{
const uint8_t *data = ctx;
if (offset + len > sizeof(clip)) {
return -EIO;
}
memcpy(buf, data + offset, len);
return 0;
}
/* A read callback that always fails */
static int bad_read(void *ctx, uint64_t offset, void *buf, size_t len)
{
ARG_UNUSED(ctx);
ARG_UNUSED(offset);
ARG_UNUSED(buf);
ARG_UNUSED(len);
return -EIO;
}
/* Track time units to milliseconds */
static int32_t to_ms(int64_t ticks, uint32_t timescale)
{
return (int32_t)(ticks * 1000 / timescale);
}
static const char *codec_name(enum mp4_codec c)
{
switch (c) {
case MP4_CODEC_H264:
return "H264";
case MP4_CODEC_AAC:
return "AAC";
default:
return "NONE";
}
}
static void print_track(const char *name, const struct mp4_track *t)
{
printk("%s: codec=%s timescale=%u duration=%u (%d ms) media_start=%d\n", name,
codec_name(t->codec), t->timescale, (uint32_t)t->duration,
to_ms(t->duration, t->timescale ? t->timescale : 1), (int32_t)t->media_start);
printk(" samples=%u max_sample=%u chunks=%u stsc=%u stts=%u ctts=%u stss=%u\n",
t->sample_count, t->max_sample, t->chunk_count, t->stsc_count, t->stts_count,
t->ctts_count, t->stss_count);
if (t->codec == MP4_CODEC_H264) {
printk(" %ux%u avcC(%u bytes): profile_idc=%u level_idc=%u\n", t->width,
t->height, t->extra_len, t->extra[1], t->extra[3]);
} else if (t->codec == MP4_CODEC_AAC) {
printk(" %u ch %u Hz AudioSpecificConfig(%u bytes): %02x %02x\n", t->channels,
t->sample_rate, t->extra_len, t->extra[0], t->extra[1]);
}
}
/* mp4_open(): parse the movie box of the clip */
static int demo_open(void)
{
int ret = mp4_open(&mp4, mem_read, (void *)clip, sizeof(clip));
if (ret != 0) {
printk("mp4_open failed: %d\n", ret);
return ret;
}
printk("file size %u bytes\n", (uint32_t)mp4.size);
print_track("video", &mp4.video);
print_track("audio", &mp4.audio);
return 0;
}
/* mp4_open() on input it cannot use: the error codes */
static void demo_open_errors(void)
{
struct mp4 m;
int ret;
/* too short to hold a box header: no movie box */
ret = mp4_open(&m, mem_read, (void *)clip, 4);
printk("size 4: ret=%d\n", ret);
/* the first box (ftyp) claims to be longer than the file */
ret = mp4_open(&m, mem_read, (void *)clip, 16);
printk("size 16: ret=%d\n", ret);
/* a read callback that fails: its error is passed through */
ret = mp4_open(&m, bad_read, NULL, sizeof(clip));
printk("bad_read: ret=%d\n", ret);
/* not an MP4 at all: the 4 byte box size is 0x00000000 so it spans the file,
* but there is no moov in it
*/
static const uint8_t zeros[64];
ret = mp4_open(&m, mem_read, (void *)zeros, sizeof(zeros));
printk("zeros: ret=%d\n", ret);
}
/* mp4_close(): free the tables, the struct is zeroed and can be closed again */
static void demo_close(void)
{
struct mp4 m;
int ret = mp4_open(&m, mem_read, (void *)clip, sizeof(clip));
if (ret != 0) {
printk("mp4_open failed: %d\n", ret);
return;
}
printk("before close: video sizes=%s samples=%u\n", m.video.sizes ? "set" : "NULL",
m.video.sample_count);
mp4_close(&m);
printk("after close: video sizes=%s samples=%u codec=%s\n",
m.video.sizes ? "set" : "NULL", m.video.sample_count, codec_name(m.video.codec));
mp4_close(&m);
printk("closed twice without trouble\n");
}
/* mp4_iter_init(): an iterator starts at the first sample of a track */
static void demo_iter_init(const struct mp4_track *t)
{
struct mp4_iter it;
int ret = mp4_iter_init(t, &it);
if (ret != 0) {
printk("mp4_iter_init: %d\n", ret);
return;
}
printk("next=%u chunk=%u in_chunk=%u chunk_pos=%u stts_left=%u dts=%d\n", it.next,
it.chunk, it.in_chunk, (uint32_t)it.chunk_pos, it.stts_left, (int32_t)it.dts);
}
/* mp4_iter_init() on a track the file does not have */
static void demo_iter_init_errors(void)
{
struct mp4 m;
struct mp4_iter it;
struct mp4_track none = {0};
/* a track after mp4_close(), and one that was never filled in */
mp4_open(&m, mem_read, (void *)clip, sizeof(clip));
mp4_close(&m);
printk("track closed: ret=%d\n", mp4_iter_init(&m.audio, &it));
printk("empty track: ret=%d\n", mp4_iter_init(&none, &it));
printk("NULL track: ret=%d\n", mp4_iter_init(NULL, &it));
}
/* mp4_next(): list the first samples of a track and read the first one */
static void demo_next(const struct mp4_track *t, const char *name, int count)
{
struct mp4_iter it;
struct mp4_sample s;
uint8_t head[8];
int ret;
mp4_iter_init(t, &it);
for (int i = 0; i < count; i++) {
ret = mp4_next(t, &it, &s);
if (ret != 0) {
printk("mp4_next: %d\n", ret);
return;
}
printk("%s #%u off=%u size=%u dts=%d pts=%d (%d ms) sync=%u\n", name, s.index,
(uint32_t)s.offset, s.size, (int32_t)s.dts, (int32_t)s.pts,
to_ms(s.pts - t->media_start, t->timescale), s.sync);
if (i == 0) {
ret = mp4.read(mp4.ctx, s.offset, head, sizeof(head));
if (ret != 0) {
printk("read sample: %d\n", ret);
return;
}
printk(" first bytes: %02x %02x %02x %02x %02x %02x %02x %02x\n", head[0],
head[1], head[2], head[3], head[4], head[5], head[6], head[7]);
}
}
}
/* mp4_next() until it returns -ENODATA: totals of a whole track */
static void demo_next_all(const struct mp4_track *t, const char *name)
{
struct mp4_iter it;
struct mp4_sample s = {0};
uint32_t n = 0, sync = 0;
uint64_t bytes = 0;
int ret;
mp4_iter_init(t, &it);
while ((ret = mp4_next(t, &it, &s)) == 0) {
n++;
bytes += s.size;
sync += s.sync;
}
printk("%s: ret=%d samples=%u sync=%u bytes=%u last dts=%d pts=%d\n", name, ret, n, sync,
(uint32_t)bytes, (int32_t)s.dts, (int32_t)s.pts);
}
/* mp4_seek_sync(): the key frame at or before a sample, then read on from there */
static void demo_seek_sync(const struct mp4_track *t, uint32_t index)
{
struct mp4_iter it;
struct mp4_sample s;
int ret = mp4_seek_sync(t, &it, index);
printk("seek sample %u: ret=%d, restart from sample %u\n", index, ret, it.next);
for (int i = 0; i < 3 && mp4_next(t, &it, &s) == 0; i++) {
printk(" #%u off=%u size=%u pts=%d (%d ms) sync=%u\n", s.index,
(uint32_t)s.offset, s.size, (int32_t)s.pts,
to_ms(s.pts - t->media_start, t->timescale), s.sync);
}
}
/* mp4_seek_time(): the key frame to start from to show the frame at a playback time */
static void demo_seek_time(const struct mp4_track *t, int32_t target_ms)
{
struct mp4_iter it;
struct mp4_sample s;
int64_t ticks = (int64_t)target_ms * t->timescale / 1000;
int ret = mp4_seek_time(t, &it, ticks);
printk("seek %d ms (%d ticks): ret=%d, restart from sample %u\n", target_ms,
(int32_t)ticks, ret, it.next);
for (int i = 0; i < 2 && mp4_next(t, &it, &s) == 0; i++) {
printk(" #%u off=%u size=%u pts=%d (%d ms, ffprobe pts %d) sync=%u\n", s.index,
(uint32_t)s.offset, s.size, (int32_t)s.pts,
to_ms(s.pts - t->media_start, t->timescale),
(int32_t)(s.pts - t->media_start), s.sync);
}
}
int main(void)
{
printk("\n== mp4_open ==\n");
if (demo_open() != 0) {
return 0;
}
printk("\n== mp4_open errors ==\n");
demo_open_errors();
printk("\n== mp4_close ==\n");
demo_close();
printk("\n== mp4_iter_init ==\n");
demo_iter_init(&mp4.video);
demo_iter_init_errors();
printk("\n== mp4_next ==\n");
demo_next(&mp4.video, "video", 8);
demo_next(&mp4.audio, "audio", 4);
demo_next_all(&mp4.video, "video");
demo_next_all(&mp4.audio, "audio");
printk("\n== mp4_seek_sync ==\n");
demo_seek_sync(&mp4.video, 0);
demo_seek_sync(&mp4.video, 47);
demo_seek_sync(&mp4.video, 89);
demo_seek_sync(&mp4.video, 1000);
demo_seek_sync(&mp4.audio, 66);
printk("\n== mp4_seek_time ==\n");
demo_seek_time(&mp4.video, 0);
demo_seek_time(&mp4.video, 1500);
demo_seek_time(&mp4.video, 2000);
demo_seek_time(&mp4.video, 2900);
demo_seek_time(&mp4.video, 5000);
demo_seek_time(&mp4.audio, 1500);
mp4_close(&mp4);
printk("\nmp4_api done\n");
return 0;
}
终端窗口
. ~/f101-zephyr/env.sh
cd ~/f101-zephyr
west build -b yuzukineko -d build/mp4-api examples/mp4_api

下载运行的方法见编译与运行。控制台 UART1 上的完整输出:

*** Booting Zephyr OS build b6f3dfe1a1f4 ***
== mp4_open ==
file size 301652 bytes
video: codec=H264 timescale=15360 duration=46080 (3000 ms) media_start=1024
samples=90 max_sample=11636 chunks=89 stsc=2 stts=1 ctts=82 stss=6
960x540 avcC(39 bytes): profile_idc=77 level_idc=31
audio: codec=AAC timescale=44100 duration=134144 (3041 ms) media_start=1024
samples=131 max_sample=430 chunks=89 stsc=78 stts=1 ctts=0 stss=0
2 ch 44100 Hz AudioSpecificConfig(5 bytes): 12 10
== mp4_open errors ==
size 4: ret=-2
size 16: ret=-22
bad_read: ret=-5
zeros: ret=-2
== mp4_close ==
before close: video sizes=set samples=90
after close: video sizes=NULL samples=0 codec=NONE
closed twice without trouble
== mp4_iter_init ==
next=0 chunk=0 in_chunk=0 chunk_pos=4629 stts_left=90 dts=0
track closed: ret=-22
empty track: ret=-22
NULL track: ret=-22
== mp4_next ==
video #0 off=4629 size=7604 dts=0 pts=1024 (0 ms) sync=1
first bytes: 00 00 02 bc 06 05 ff ff
video #1 off=12233 size=2733 dts=512 pts=2560 (100 ms) sync=0
video #2 off=15304 size=1542 dts=1024 pts=1536 (33 ms) sync=0
video #3 off=17587 size=1138 dts=1536 pts=2048 (66 ms) sync=0
video #4 off=19087 size=2503 dts=2048 pts=4096 (200 ms) sync=0
video #5 off=22308 size=1294 dts=2560 pts=3072 (133 ms) sync=0
video #6 off=23961 size=1109 dts=3072 pts=3584 (166 ms) sync=0
video #7 off=25811 size=2641 dts=3584 pts=5632 (300 ms) sync=0
audio #0 off=14966 size=338 dts=0 pts=0 (-23 ms) sync=1
first bytes: de 02 00 4c 61 76 63 36
audio #1 off=16846 size=408 dts=1024 pts=1024 (0 ms) sync=1
audio #2 off=17254 size=333 dts=2048 pts=2048 (23 ms) sync=1
audio #3 off=18725 size=362 dts=3072 pts=3072 (46 ms) sync=1
video: ret=-61 samples=90 sync=6 bytes=248350 last dts=45568 pts=46080
audio: ret=-61 samples=131 sync=131 bytes=48673 last dts=133120 pts=133120
== mp4_seek_sync ==
seek sample 0: ret=0, restart from sample 0
#0 off=4629 size=7604 pts=1024 (0 ms) sync=1
#1 off=12233 size=2733 pts=2560 (100 ms) sync=0
#2 off=15304 size=1542 pts=1536 (33 ms) sync=0
seek sample 47: ret=0, restart from sample 45
#45 off=134516 size=9763 pts=24064 (1500 ms) sync=1
#46 off=145039 size=4830 pts=26112 (1633 ms) sync=0
#47 off=150246 size=2400 pts=25088 (1566 ms) sync=0
seek sample 89: ret=0, restart from sample 75
#75 off=245325 size=11636 pts=39424 (2500 ms) sync=1
#76 off=257704 size=5481 pts=41472 (2633 ms) sync=0
#77 off=263536 size=2140 pts=40448 (2566 ms) sync=0
seek sample 1000: ret=0, restart from sample 75
#75 off=245325 size=11636 pts=39424 (2500 ms) sync=1
#76 off=257704 size=5481 pts=41472 (2633 ms) sync=0
#77 off=263536 size=2140 pts=40448 (2566 ms) sync=0
seek sample 66: ret=0, restart from sample 66
#66 off=152646 size=372 pts=67584 (1509 ms) sync=1
#67 off=153018 size=376 pts=68608 (1532 ms) sync=1
#68 off=155115 size=376 pts=69632 (1555 ms) sync=1
== mp4_seek_time ==
seek 0 ms (0 ticks): ret=0, restart from sample 0
#0 off=4629 size=7604 pts=1024 (0 ms, ffprobe pts 0) sync=1
#1 off=12233 size=2733 pts=2560 (100 ms, ffprobe pts 1536) sync=0
seek 1500 ms (23040 ticks): ret=0, restart from sample 45
#45 off=134516 size=9763 pts=24064 (1500 ms, ffprobe pts 23040) sync=1
#46 off=145039 size=4830 pts=26112 (1633 ms, ffprobe pts 25088) sync=0
seek 2000 ms (30720 ticks): ret=0, restart from sample 60
#60 off=188864 size=11184 pts=31744 (2000 ms, ffprobe pts 30720) sync=1
#61 off=200418 size=5786 pts=33792 (2133 ms, ffprobe pts 32768) sync=0
seek 2900 ms (44544 ticks): ret=0, restart from sample 75
#75 off=245325 size=11636 pts=39424 (2500 ms, ffprobe pts 38400) sync=1
#76 off=257704 size=5481 pts=41472 (2633 ms, ffprobe pts 40448) sync=0
seek 5000 ms (76800 ticks): ret=-34, restart from sample 75
#75 off=245325 size=11636 pts=39424 (2500 ms, ffprobe pts 38400) sync=1
#76 off=257704 size=5481 pts=41472 (2633 ms, ffprobe pts 40448) sync=0
seek 1500 ms (66150 ticks): ret=0, restart from sample 66
#66 off=152646 size=372 pts=67584 (1509 ms, ffprobe pts 66560) sync=1
#67 off=153018 size=376 pts=68608 (1532 ms, ffprobe pts 67584) sync=1
mp4_api done

输出里的 mp4_open errors 一节是故意传入打不开的输入,不是程序出错。