Fragmented MP4(fMP4)是一种基于MPEG-4 Part 12标准的音视频容器格式,专为优化互联网流媒体传输而设计。与传统的MP4格式相比,fMP4通过将音视频数据分割成多个独立可解码的片段,显著提升了流媒体的效率和灵活性。

fMP4的基本概念

fMP4的定义

Fragmented MP4(fMP4)是标准MP4(MPEG-4 Part 14,ISO/IEC 14496-14)格式的扩展,基于ISO Base Media File Format(ISOBMFF)。与传统MP4将音视频数据存储为单一连续文件不同,fMP4将数据分割成多个独立片段(fragments),每个片段包含可独立解码的音视频数据和元数据。这种结构使其特别适合自适应流媒体传输,如HTTP Live Streaming(HLS)和Dynamic Adaptive Streaming over HTTP(DASH)。

fMP4与标准MP4的区别

标准MP4和fMP4的主要区别在于文件结构和传输机制:

  • 标准MP4:所有音视频数据存储在一个mdat(Movie Data)原子中,元数据存储在单一的moov(Movie)原子中,通常位于文件开头或结尾。播放器需下载整个moov原子后才能开始播放,这在流媒体场景中会导致启动延迟。
  • fMP4:将音视频数据分割为多个moof(Movie Fragment)和mdat原子对,并包含一个独立的初始化片段(init.mp4),存储moov原子。这种结构允许播放器在下载初始化片段和第一个片段后立即开始播放,支持低延迟流媒体和自适应比特率切换。

fMP4的文件结构

fMP4 基于盒子(Box)结构,每个盒子由长度 + 类型 + 数据组成,常见盒子如下:

  1. ftyp(File Type Box)
    • 文件类型标识
    • 指明兼容性,如 isomiso6avc1
  2. moov(Movie Box)
    • 全局描述信息
    • 包含轨道信息(trak)、媒体信息(mdia)、采样描述(stsd)
    • 在直播场景中通常只传一次,后续片段只发送 moof+mdat
  3. moof(Movie Fragment Box)
    • 每个片段的局部描述
    • 包含 mfhd(片段头)、traf(轨道片段)
    • 指定本片段的时间戳、帧数量等信息
  4. mdat(Media Data Box)
    • 存储音视频实际编码数据
    • 与 moof 配对,形成可独立播放的片段

示例结构:

ftyp
moov
moof + mdat  <-- fragment 1
moof + mdat  <-- fragment 2
...

fMP4 时间戳和同步

  • 时间戳类型
    • baseMediaDecodeTime:解码起始时间
    • compositionTimeOffset:PTS-Corrected 时间(保证帧显示顺序)
  • 同步原则
    • 音视频同片段内同步(mdat 对应 moof)
    • 通过 tfdt(Track Fragment Decode Time)记录基准解码时间
    • 在多轨道(音频/视频)中,利用 tfhdtrun 盒子确保帧对应关系

fMP4的技术原理

视频封装流程

  1. NALU 分割
    • H.264/H.265 裸流按 NALU 切分
    • 提取关键帧(IDR/关键帧)作为片段起始点
  2. 采样表构建
    • 通过 trun 盒子记录每帧长度、偏移、时间戳
    • 可快速定位和解码
  3. moof 构建
    • 构建 mfhd(片段序号)
    • 构建 traf
      • tfhd:轨道标识
      • tfdt:解码起始时间
      • trun:帧信息
  4. mdat 拼接帧数据
    • 按帧顺序写入视频 NALU
    • 与 moof 对应形成 fragment
  5. 片段传输
    • 每个 fragment 独立,可被 CDN 分发
    • 播放端边下载边解析 moof+mdat

音频封装流程

  • AAC/Opus 音频流按帧封装
  • 构建 trun 记录每帧长度和时间戳
  • 音频 fragment 与视频 fragment 对齐(或单独传输)

关键技术点

  1. 随机访问支持
    • 每个 fragment 自带 moof,可直接解码
    • 支持 seek 功能和多码率切换
  2. 低延迟流
    • 通过小片段(1~2s)减少播放延迟
    • 支持 LL-HLS(Low-Latency HLS)
  3. 时间戳精度与对齐
    • 视频帧可能存在 B 帧延迟,需要使用 CTS/PTS 计算显示顺序
    • 音视频同步依赖 moof 中 tfdttrun
  4. 多码率和自适应流
    • fMP4 可与 HLS/DASH 分段配合
    • 多码率 fragment 可通过 M3U8 或 MPD 文件动态切换

fMP4 实践应用

HTTP Live Streaming(HLS)

  • HLS 传统使用 TS 切片
  • LL-HLS 和 HLS fMP4 支持小片段传输,减少播放延迟
  • 支持 DRM 和多轨音频

MPEG-DASH

  • DASH 强调自适应码率
  • fMP4 作为默认分片格式,支持 MPD 文件描述
  • 支持边播边下载、动态码率切换

WebRTC & CMAF

  • CMAF(Common Media Application Format)基于 fMP4
  • WebRTC 与低延迟流结合
  • 支持实时播放和 VOD 内容统一封装

fMP4 优化与注意事项

  1. 片段长度选择
    • 小片段:低延迟,增大请求次数
    • 大片段:网络利用率高,但延迟大
    • 实践中常取 1~4 秒
  2. 关键帧切片
    • 每个 fragment 开头必须是关键帧
    • 避免解码器依赖前片段
  3. 初始化 moov 优化
    • 将 moov 放在文件前部,支持快速启动
    • 可使用 “fast-start” 模式
  4. PTS/DTS 对齐
    • 确保音视频同步
    • 使用 90 kHz 时间基或采样率时间基
  5. 网络传输优化
    • CDN 分发 fragment
    • 支持 HTTP/2 或 QUIC 提高并发性能

fMP4的优势

fMP4在音视频流媒体中的广泛应用得益于以下优势:

快速播放启动

由于只需下载初始化片段和第一个媒体片段,fMP4显著减少了播放启动时间,特别适合实时流媒体和点播服务。

自适应流媒体

fMP4是HLS和DASH等协议的理想容器格式,支持动态比特率切换,确保在不同网络条件下提供最佳观看体验。

高效存储与传输

通过分段存储和字节范围请求,fMP4优化了带宽使用,减少了服务器和客户端的存储需求。

跨平台兼容性

fMP4基于标准MP4格式,兼容大多数现代媒体播放器和设备,包括智能手机、平板电脑和智能电视。

支持高级功能

fMP4支持交互式内容、多语言音轨、字幕和音频描述等高级功能,满足多样化的流媒体需求。

示例

#include <iostream>
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>

extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/time.h>
#include <libavutil/opt.h>
#include <libavutil/mathematics.h>
}

// 线程安全队列
template<typename T>
class SafeQueue {
public:
    void push(T value) {
        std::lock_guard<std::mutex> lock(mtx);
        q.push(value);
        cv.notify_one();
    }
    T pop() {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [this](){ return !q.empty(); });
        T val = q.front();
        q.pop();
        return val;
    }
private:
    std::queue<T> q;
    std::mutex mtx;
    std::condition_variable cv;
};

// 帧数据结构
struct FramePacket {
    std::vector<uint8_t> data;
    int64_t pts;
    int64_t dts;
    bool keyFrame;
    int streamIndex; // 0=video,1=audio
};

// 全局队列
SafeQueue<FramePacket> g_frameQueue;

// 判断关键帧
bool isKeyFrame(const uint8_t* data, int size, bool isH265) {
    if (isH265) {
        int nalType = (data[0] >> 1) & 0x3F;
        return nalType >= 16 && nalType <= 21;
    } else {
        int nalType = data[0] & 0x1F;
        return nalType == 5;
    }
}

// 将 AVPacket 写入 fMP4 内存 buffer
bool writePacketToBuffer(AVFormatContext* outCtx, AVPacket* pkt, std::vector<uint8_t>& buffer) {
    int ret = av_interleaved_write_frame(outCtx, pkt);
    if (ret < 0) {
        std::cerr << "Error writing frame: " << ret << "\n";
        return false;
    }
    // 获取 dyn_buf 数据
    uint8_t* data = nullptr;
    int size = avio_close_dyn_buf(outCtx->pb, &data);
    if (size > 0) {
        buffer.insert(buffer.end(), data, data + size);
        av_free(data);
        // 重新初始化 dyn_buf
        avio_open_dyn_buf(&outCtx->pb);
    }
    return true;
}

int main() {
    const bool isH265 = false;
    const int videoWidth = 1280;
    const int videoHeight = 720;
    const int fps = 30;

    avformat_network_init();

    AVFormatContext* outCtx = nullptr;
    avformat_alloc_output_context2(&outCtx, nullptr, "mp4", nullptr);
    outCtx->oformat->flags |= AVFMT_FLAG_FRAGMENT; // fMP4

    // 视频轨
    AVStream* videoStream = avformat_new_stream(outCtx, nullptr);
    videoStream->id = 0;
    AVCodecParameters* vCodecpar = videoStream->codecpar;
    vCodecpar->codec_type = AVMEDIA_TYPE_VIDEO;
    vCodecpar->codec_id = isH265 ? AV_CODEC_ID_HEVC : AV_CODEC_ID_H264;
    vCodecpar->width = videoWidth;
    vCodecpar->height = videoHeight;
    vCodecpar->format = AV_PIX_FMT_YUV420P;

    // 音频轨
    AVStream* audioStream = avformat_new_stream(outCtx, nullptr);
    audioStream->id = 1;
    AVCodecParameters* aCodecpar = audioStream->codecpar;
    aCodecpar->codec_type = AVMEDIA_TYPE_AUDIO;
    aCodecpar->codec_id = AV_CODEC_ID_AAC;
    aCodecpar->sample_rate = 48000;
    aCodecpar->channels = 2;
    aCodecpar->channel_layout = AV_CH_LAYOUT_STEREO;
    aCodecpar->format = AV_SAMPLE_FMT_FLTP;

    // 内存输出
    avio_open_dyn_buf(&outCtx->pb);

    // 写头
    avformat_write_header(outCtx, nullptr);

    std::vector<uint8_t> fmp4Buffer;

    // 写帧线程
    std::thread writer([&]() {
        while (true) {
            FramePacket pkt = g_frameQueue.pop();
            AVPacket avpkt;
            av_init_packet(&avpkt);
            avpkt.data = pkt.data.data();
            avpkt.size = pkt.data.size();
            avpkt.stream_index = pkt.streamIndex;
            avpkt.pts = pkt.pts;
            avpkt.dts = pkt.dts;
            avpkt.duration = pkt.streamIndex == 0 ? 90000 / fps : 1024; // 视频帧/音频帧时长

            writePacketToBuffer(outCtx, &avpkt, fmp4Buffer);

            // 关键帧 flush fragment
            if (pkt.keyFrame && pkt.streamIndex == 0) {
                av_write_frame(outCtx, nullptr); // moof+mdat
                writePacketToBuffer(outCtx, nullptr, fmp4Buffer);
                // TODO: 可以立即通过 HTTP/WS 推送 fmp4Buffer
                fmp4Buffer.clear();
            }
        }
    });

    // 模拟帧生成
    int64_t pts = 0;
    while (pts < 300) {
        int naluSize = 1024;
        std::vector<uint8_t> nalu(naluSize, 0);
        bool keyFrame = (pts % 30 == 0);
        g_frameQueue.push({nalu, pts, pts, keyFrame, 0});
        pts++;
        std::this_thread::sleep_for(std::chrono::milliseconds(33));
    }

    writer.join();

    av_write_trailer(outCtx);

    avformat_free_context(outCtx);
    avformat_network_deinit();

    return 0;
}

总结

Fragmented MP4(fMP4)作为一种专为流媒体优化的音视频容器格式,通过分段结构、自适应比特率和高效传输机制,显著提升了互联网视频分发的性能。相较于标准MP4,fMP4在快速启动、低缓冲和跨平台兼容性方面具有显著优势,广泛应用于点播、直播和Web端播放等场景。尽管面临复杂性和兼容性等挑战,但随着5G、边缘计算和CMAF等技术的发展,fMP4将在音视频领域继续扮演关键角色。未来,fMP4有望在低延迟、高分辨率和交互式内容分发中进一步创新,为用户带来更优质的观看体验。

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐