Android多媒体容器格式深度解析:从MP4到Fragmented MP4,打造高效流媒体体验
·
多媒体容器格式是音视频开发的基石,MP4作为最流行的容器格式,其标准版本和碎片化版本在流媒体中扮演不同角色。本文将深入解析MP4和Fragmented MP4的结构差异、工作原理,以及在Android中如何利用它们实现高效流媒体播放。
一、多媒体容器格式基础
1. 容器格式的核心作用
// 容器格式的核心功能抽象
interface MediaContainer {
// 封装功能
fun encapsulateStreams(
videoStream: VideoStream,
audioStream: AudioStream,
subtitleStream: SubtitleStream? = null
): ByteArray
// 解封装功能
fun demultiplex(data: ByteArray): List<MediaStream>
// 元数据管理
fun getMetadata(): MediaMetadata
// 随机访问支持
fun seekToTimestamp(timestamp: Long): SeekResult
// 流式传输支持
fun supportsStreaming(): Boolean
}
// 容器格式关键概念
data class MediaStream(
val type: StreamType,
val codec: CodecInfo,
val timescale: Long, // 时间基准
val duration: Long, // 时长(按时间基准)
val bitrate: Int, // 比特率
val extradata: ByteArray? = null // 编解码器特定数据
)
enum class StreamType {
VIDEO, AUDIO, SUBTITLE, DATA
}
2. 常见容器格式对比
| 格式 | 文件扩展名 | 编码格式支持 | 流媒体支持 | 应用场景 |
|---|---|---|---|---|
| MP4 | .mp4, .m4v | H.264/5, AAC, MP3 | 有限(需moov前置) | 本地存储,点播 |
| Fragmented MP4 | .mp4, .m4s | H.264/5, AAC, Opus | 优秀(DASH, HLS | ) 流媒体,直播 |
| WebM | .webm | VP8/9, AV1, Opus, Vorbis | 良好(WebRTC) | Web视频,开源项目 |
| MKV | .mkv | 几乎所有格式 | 良好 | 高清存储,多字幕 |
| FLV | .flv | H.264, AAC, MP3 | 优秀(传统直播 | ) 传统直播流 |
| TS | .ts | H.264, AAC | 优秀(传统HLS) | 广播电视,传统HLS |
3. ISO BMFF标准家族
ISO Base Media File Format (ISO/IEC 14496-12)
├── MP4 (MPEG-4 Part 14)
├── 3GP (3GPP)
├── MOV (QuickTime)
├── Fragmented MP4
└── CMAF (Common Media Application Format)
二、MP4容器格式深度解析
1. MP4文件结构详解
// MP4文件结构解析器
class MP4Parser {
// MP4盒子(Box)结构
data class Box(
val size: Long, // 盒子大小(包含header)
val type: String, // 4字符类型标识
val largeSize: Long? = null, // 当size=1时使用
val userType: ByteArray? = null, // 'uuid'类型时使用
val data: ByteArray, // 盒子数据
val children: List<Box> = emptyList() // 子盒子
) {
companion object {
// 标准盒子类型
const val TYPE_FTYP = "ftyp"
const val TYPE_MOOV = "moov"
const val TYPE_MDAT = "mdat"
const val TYPE_MVHD = "mvhd" // Movie Header
const val TYPE_TRAK = "trak" // Track
const val TYPE_MDIA = "mdia" // Media
const val TYPE_MINF = "minf" // Media Information
const val TYPE_STBL = "stbl" // Sample Table
const val TYPE_STSD = "stsd" // Sample Description
const val TYPE_STTS = "stts" // Time-to-Sample
const val TYPE_STSC = "stsc" // Sample-to-Chunk
const val TYPE_STSZ = "stsz" // Sample Size
const val TYPE_STCO = "stco" // Chunk Offset (32-bit)
const val TYPE_CO64 = "co64" // Chunk Offset (64-bit)
const val TYPE_CTTS = "ctts" // Composition Time Offset
}
}
// 解析MP4文件
fun parseMP4(file: File): MP4File {
val boxes = mutableListOf<Box>()
val input = FileInputStream(file).channel
var position = 0L
while (position < input.size()) {
val box = parseBox(input, position)
boxes.add(box)
position += box.size
}
return MP4File(boxes)
}
private fun parseBox(channel: FileChannel, position: Long): Box {
// 读取盒子header
val buffer = ByteBuffer.allocate(8)
channel.read(buffer, position)
buffer.flip()
val size = buffer.int.toLong() and 0xFFFFFFFFL
val type = String(ByteArray(4).apply {
buffer.get(this, 0, 4)
})
// 处理扩展大小
val largeSize = if (size == 1L) {
val largeBuffer = ByteBuffer.allocate(8)
channel.read(largeBuffer, position + 8)
largeBuffer.flip()
largeBuffer.long
} else null
// 处理uuid类型
val userType = if (type == "uuid") {
val uuidBuffer = ByteBuffer.allocate(16)
channel.read(uuidBuffer, position + 8)
uuidBuffer.array()
} else null
// 计算数据位置和大小
val headerSize = when {
largeSize != null -> 16
userType != null -> 24
else -> 8
}
val dataSize = (largeSize ?: size) - headerSize
val dataBuffer = ByteBuffer.allocate(dataSize.toInt())
channel.read(dataBuffer, position + headerSize)
// 解析子盒子(如果是容器盒子)
val children = if (isContainerBox(type)) {
parseChildBoxes(dataBuffer.array())
} else emptyList()
return Box(
size = largeSize ?: size,
type = type,
largeSize = largeSize,
userType = userType,
data = dataBuffer.array(),
children = children
)
}
private fun isContainerBox(type: String): Boolean {
return when (type) {
"moov", "trak", "mdia", "minf", "stbl", "edts" -> true
else -> false
}
}
}
// MP4文件结构表示
data class MP4File(
val boxes: List<MP4Parser.Box>,
val ftyp: FtypBox? = null,
val moov: MoovBox? = null,
val mdat: MdatBox? = null
) {
init {
// 提取关键盒子
boxes.forEach { box ->
when (box.type) {
MP4Parser.Box.TYPE_FTYP -> ftyp = parseFtyp(box)
MP4Parser.Box.TYPE_MOOV -> moov = parseMoov(box)
MP4Parser.Box.TYPE_MDAT -> mdat = parseMdat(box)
}
}
}
// 获取媒体信息
fun getMediaInfo(): MediaInfo {
return moov?.let { moov ->
MediaInfo(
duration = moov.mvhd.duration / moov.mvhd.timescale,
tracks = moov.traks.map { track ->
TrackInfo(
type = track.mdia.hdlr.handlerType,
codec = track.mdia.minf.stbl.stsd.codecType,
duration = track.mdia.mdhd.duration / track.mdia.mdhd.timescale,
bitrate = calculateBitrate(track)
)
}
)
} ?: throw IllegalStateException("moov box not found")
}
}
2. MP4关键盒子详解
ftyp(File Type)盒子
data class FtypBox(
val majorBrand: String, // 主要品牌(如:isom, mp41, avc1)
val minorVersion: Int, // 次要版本
val compatibleBrands: List<String> // 兼容品牌
) {
companion object {
// 常见品牌
const val BRAND_ISOM = "isom" // ISO Base Media
const val BRAND_MP41 = "mp41" // MP4 v1
const val BRAND_MP42 = "mp42" // MP4 v2
const val BRAND_AVC1 = "avc1" // AVC (H.264)
const val BRAND_HEV1 = "hev1" // HEVC (H.265)
const val BRAND_DASH = "dash" // DASH流媒体
const val BRAND_MSDH = "msdh" // Microsoft Smooth Streaming
}
}
moov(Movie)盒子结构
data class MoovBox(
val mvhd: MvhdBox, // Movie Header
val traks: List<TrakBox> // Tracks
)
data class MvhdBox(
val version: Int, // 版本(0或1)
val creationTime: Long, // 创建时间
val modificationTime: Long, // 修改时间
val timescale: Long, // 时间基准(每秒刻度数)
val duration: Long, // 时长(按时间基准)
val rate: Int, // 播放速率(16.16定点数)
val volume: Short, // 音量(8.8定点数)
val matrix: IntArray, // 变换矩阵(9个32位定点数)
val nextTrackID: Int // 下一个轨道ID
)
data class TrakBox(
val tkhd: TkhdBox, // Track Header
val mdia: MdiaBox // Media
)
data class TkhdBox(
val version: Int,
val flags: Int, // 轨道标志
val creationTime: Long,
val modificationTime: Long,
val trackID: Int, // 轨道ID
val duration: Long,
val layer: Short, // 图层顺序
val alternateGroup: Short, // 轨道组
val volume: Short,
val width: Int, // 宽度(16.16定点数)
val height: Int, // 高度(16.16定点数)
val matrix: IntArray
)
data class MdiaBox(
val mdhd: MdhdBox, // Media Header
val hdlr: HdlrBox, // Handler Reference
val minf: MinfBox // Media Information
)
data class MdhdBox(
val version: Int,
val creationTime: Long,
val modificationTime: Long,
val timescale: Long, // 媒体时间基准
val duration: Long,
val language: Short // ISO-639-2/T语言代码
)
stbl(Sample Table)关键结构
data class StblBox(
val stsd: StsdBox, // Sample Description
val stts: SttsBox, // Decoding Time to Sample
val stsc: StscBox, // Sample To Chunk
val stsz: StszBox, // Sample Size
val stco: StcoBox, // Chunk Offset (32-bit)
val co64: Co64Box? = null, // Chunk Offset (64-bit)
val ctts: CttsBox? = null // Composition Time Offset
)
// STTS:解码时间戳映射
data class SttsBox(
val entries: List<SttsEntry> // 时间戳映射表
) {
data class SttsEntry(
val sampleCount: Int, // 连续样本数
val sampleDelta: Int // 样本时间间隔
)
// 根据样本索引获取解码时间
fun getDecodeTime(sampleIndex: Int): Long {
var remaining = sampleIndex
var time = 0L
for (entry in entries) {
if (remaining < entry.sampleCount) {
return time + remaining * entry.sampleDelta
}
time += entry.sampleCount * entry.sampleDelta
remaining -= entry.sampleCount
}
throw IndexOutOfBoundsException("Sample index out of range")
}
}
// STSC:样本到块的映射
data class StscBox(
val entries: List<StscEntry>
) {
data class StscEntry(
val firstChunk: Int, // 第一个块号
val samplesPerChunk: Int, // 每块样本数
val sampleDescriptionID: Int // 样本描述ID
)
}
// STCO/CO64:块偏移
data class StcoBox(
val chunkOffsets: List<Long> // 块在文件中的偏移
)
data class Co64Box(
val chunkOffsets: List<Long> // 64位块偏移
)
// CTTS:合成时间偏移(B帧相关)
data class CttsBox(
val entries: List<CttsEntry>
) {
data class CttsEntry(
val sampleCount: Int, // 连续样本数
val sampleOffset: Int // 合成时间偏移
)
// 计算PTS(Presentation Time Stamp)
fun getPresentationTime(sampleIndex: Int, decodeTime: Long): Long {
var remaining = sampleIndex
var offset = 0L
for (entry in entries) {
if (remaining < entry.sampleCount) {
offset = entry.sampleOffset.toLong()
break
}
remaining -= entry.sampleCount
}
return decodeTime + offset
}
}
3. MP4的局限性分析
class MP4LimitationsAnalyzer {
// moov前置问题
class MoovFirstProblem {
companion object {
// moov位置检测
fun findMoovPosition(file: File): MoovPosition {
val parser = MP4Parser()
val mp4File = parser.parseMP4(file)
var moovOffset = 0L
var currentOffset = 0L
for (box in mp4File.boxes) {
when (box.type) {
"moov" -> return MoovPosition(currentOffset, MoovLocation.MIDDLE)
"ftyp" -> currentOffset += box.size
"mdat" -> {
// 如果先遇到mdat,moov在末尾
if (moovOffset == 0L) {
return MoovPosition(-1, MoovLocation.END)
}
}
}
}
return MoovPosition(moovOffset, MoovLocation.START)
}
// 修复moov前置(优化网络播放)
fun moveMoovToFront(input: File, output: File) {
val parser = MP4Parser()
val mp4File = parser.parseMP4(input)
// 重新排列盒子顺序:ftyp -> moov -> mdat
val outputStream = FileOutputStream(output)
val writer = MP4BoxWriter(outputStream.channel())
// 写入ftyp
mp4File.ftyp?.let { writer.writeBox(it) }
// 写入moov
mp4File.moov?.let { writer.writeBox(it) }
// 写入mdat
mp4File.mdat?.let { writer.writeBox(it) }
writer.close()
}
}
data class MoovPosition(
val offset: Long,
val location: MoovLocation
)
enum class MoovLocation {
START, MIDDLE, END
}
}
// 流式传输问题
class StreamingLimitations {
// MP4不适合流式传输的原因
val limitations = listOf(
Limitation(
name = "索引集中",
description = "stbl中的所有索引都集中在文件开头或结尾",
impact = Impact.HIGH,
solution = "使用Fragmented MP4将索引分散"
),
Limitation(
name = "不支持动态更新",
description = "无法在播放过程中添加新内容",
impact = Impact.HIGH,
solution = "使用Fragmented MP4或流媒体协议"
),
Limitation(
name = "随机访问成本高",
description = "跳转时需要加载整个stbl",
impact = Impact.MEDIUM,
solution = "使用Fragmented MP4的moof索引"
)
)
data class Limitation(
val name: String,
val description: String,
val impact: Impact,
val solution: String
)
enum class Impact {
LOW, MEDIUM, HIGH, CRITICAL
}
}
}
三、Fragmented MP4(fMP4)深度解析
1. fMP4设计哲学
// fMP4设计原理
class FragmentedMP4Design {
// 关键设计原则
data class DesignPrinciple(
val principle: String,
val description: String,
val benefit: String
)
companion object {
val PRINCIPLES = listOf(
DesignPrinciple(
principle = "分片独立",
description = "每个媒体片段(fragment)包含完整的解码和渲染信息",
benefit = "支持流式传输和随机访问"
),
DesignPrinciple(
principle = "索引分散",
description = "媒体样本索引分散在各个moof盒子中",
benefit = "无需等待完整文件即可播放"
),
DesignPrinciple(
principle = "动态更新",
description = "支持在播放过程中追加新的媒体片段",
benefit = "适合直播和动态内容"
),
DesignPrinciple(
principle = "轨道片段化",
description = "每个轨道可以独立分片,支持选择性下载",
benefit = "节省带宽,支持自适应流"
)
)
}
// fMP4与MP4结构对比
class StructureComparison {
data class Comparison(
val component: String,
val mp4Structure: String,
val fmp4Structure: String,
val difference: String
)
val comparisons = listOf(
Comparison(
component = "索引结构",
mp4Structure = "集中式stbl",
fmp4Structure = "分布式trun",
difference = "fMP4在每个moof中都有局部索引"
),
Comparison(
component = "媒体数据",
mp4Structure = "单个mdat",
fmp4Structure = "多个mdat(每个片段一个)",
difference = "fMP4数据分段存储"
),
Comparison(
component = "随机访问",
mp4Structure = "依赖完整stbl",
fmp4Structure = "依赖sidx和moof",
difference = "fMP4支持基于片段的随机访问"
),
Comparison(
component = "动态更新",
mp4Structure = "不支持",
fmp4Structure = "支持追加片段",
difference = "fMP4适合直播和动态内容"
)
)
}
}
2. fMP4文件结构
// fMP4文件结构解析
class FragmentedMP4Parser : MP4Parser() {
// fMP4特定盒子类型
companion object {
const val TYPE_MOOF = "moof" // Movie Fragment
const val TYPE_MFRA = "mfra" // Movie Fragment Random Access
const val TYPE_TRAF = "traf" // Track Fragment
const val TYPE_TFHD = "tfhd" // Track Fragment Header
const val TYPE_TRUN = "trun" // Track Fragment Run
const val TYPE_TFDT = "tfdt" // Track Fragment Decode Time
const val TYPE_SIDX = "sidx" // Segment Index
const val TYPE_SSIX = "ssix" // Subsegment Index
}
// 解析fMP4文件
fun parseFragmentedMP4(file: File): FragmentedMP4File {
val boxes = parseMP4(file).boxes
val fragments = mutableListOf<MovieFragment>()
var currentFragment: MovieFragment? = null
boxes.forEach { box ->
when (box.type) {
TYPE_MOOF -> {
currentFragment?.let { fragments.add(it) }
currentFragment = MovieFragment().apply {
moof = parseMoof(box)
}
}
TYPE_MDAT -> {
currentFragment?.mdat = parseMdat(box)
}
TYPE_SIDX -> {
// 处理分片索引
}
}
}
currentFragment?.let { fragments.add(it) }
return FragmentedMP4File(
ftyp = boxes.find { it.type == TYPE_FTYP }?.let { parseFtyp(it) },
moov = boxes.find { it.type == TYPE_MOOV }?.let { parseMoov(it) },
fragments = fragments,
sidx = boxes.find { it.type == TYPE_SIDX }?.let { parseSidx(it) }
)
}
// Movie Fragment结构
data class MovieFragment(
var moof: MoofBox? = null,
var mdat: MdatBox? = null
)
// moof(Movie Fragment)盒子
data class MoofBox(
val mfhd: MfhdBox, // Movie Fragment Header
val trafs: List<TrafBox> // Track Fragments
)
data class MfhdBox(
val sequenceNumber: Int // 片段序列号
)
// traf(Track Fragment)盒子
data class TrafBox(
val tfhd: TfhdBox, // Track Fragment Header
val tfdt: TfdtBox? = null, // Track Fragment Decode Time
val trun: TrunBox? = null, // Track Fragment Run
val sdtp: SdtpBox? = null // Independent and Disposable Samples
)
data class TfhdBox(
val trackID: Int, // 引用的轨道ID
val baseDataOffset: Long? = null,
val sampleDescriptionIndex: Int? = null,
val defaultSampleDuration: Int? = null,
val defaultSampleSize: Int? = null,
val defaultSampleFlags: Int? = null
)
data class TfdtBox(
val version: Int, // 0或1
val baseMediaDecodeTime: Long // 基础媒体解码时间
)
// trun(Track Fragment Run)盒子 - fMP4核心
data class TrunBox(
val version: Int,
val flags: Int,
val sampleCount: Int,
val dataOffset: Int? = null,
val firstSampleFlags: Int? = null,
val samples: List<TrunSample>
) {
data class TrunSample(
val sampleDuration: Int? = null,
val sampleSize: Int? = null,
val sampleFlags: Int? = null,
val sampleCompositionTimeOffset: Int? = null
)
// 计算样本在文件中的位置
fun calculateSamplePositions(startOffset: Long): List<Long> {
val positions = mutableListOf<Long>()
var currentOffset = startOffset + (dataOffset ?: 0)
samples.forEach { sample ->
positions.add(currentOffset)
currentOffset += (sample.sampleSize ?: 0).toLong()
}
return positions
}
}
// sidx(Segment Index)盒子
data class SidxBox(
val version: Int,
val referenceID: Int,
val timescale: Int,
val earliestPresentationTime: Long,
val firstOffset: Long,
val references: List<SidxReference>
) {
data class SidxReference(
val referenceType: Int, // 0=媒体数据,1=索引数据
val referencedSize: Long,
val subsegmentDuration: Long,
val startsWithSAP: Boolean, // 是否以SAP(流访问点)开始
val sapType: Int,
val sapDeltaTime: Int
)
// 根据时间查找分片
fun findSegmentForTime(timeMs: Long): Int? {
var accumulatedTime = 0L
references.forEachIndexed { index, ref ->
val segmentDurationMs = (ref.subsegmentDuration * 1000) / timescale
accumulatedTime += segmentDurationMs
if (timeMs < accumulatedTime) {
return index
}
}
return null
}
}
}
3. fMP4在流媒体协议中的应用
// fMP4在DASH中的应用
class DASHWithFMP4 {
// MPD(Media Presentation Description)结构
data class MPD(
val type: MPDType, // 静态或动态
val availabilityStartTime: String? = null,
val availabilityEndTime: String? = null,
val mediaPresentationDuration: String? = null,
val minBufferTime: String,
val periods: List<Period>
) {
enum class MPDType {
STATIC, DYNAMIC
}
}
data class Period(
val start: String? = null,
val duration: String? = null,
val adaptationSets: List<AdaptationSet>
)
data class AdaptationSet(
val mimeType: String, // 如:video/mp4
val codecs: String, // 如:avc1.640028
val segmentAlignment: Boolean = true,
val bitstreamSwitching: Boolean = false,
val representations: List<Representation>
)
data class Representation(
val id: String,
val bandwidth: Int, // 比特率(bps)
val width: Int? = null,
val height: Int? = null,
val frameRate: String? = null,
val segmentTemplate: SegmentTemplate? = null,
val baseURL: String? = null
)
data class SegmentTemplate(
val media: String, // 媒体URL模板
val initialization: String? = null, // 初始化段
val duration: Int, // 分片时长(时间基准单位)
val startNumber: Int, // 起始编号
val timescale: Int // 时间基准
)
// DASH分片命名模式
class SegmentNamingPattern {
companion object {
// URL模板示例
// $RepresentationID$ - 表示ID
// $Number$ - 分片编号
// $Time$ - 分片起始时间
// $Bandwidth$ - 比特率
fun generateSegmentURL(
template: String,
representationID: String,
number: Int,
time: Long,
bandwidth: Int
): String {
return template
.replace("\$RepresentationID\$", representationID)
.replace("\$Number\$", number.toString())
.replace("\$Time\$", time.toString())
.replace("\$Bandwidth\$", bandwidth.toString())
}
// 初始化段:包含moov
val INIT_SEGMENT_PATTERN = "\$RepresentationID\$-init.mp4"
// 媒体段:包含moof+mdat
val MEDIA_SEGMENT_PATTERN = "\$RepresentationID\$-\$Number\$.m4s"
}
}
}
// fMP4在HLS中的应用
class HLSWithFMP4 {
// HLS播放列表结构
data class HLSPlaylist(
val version: Int = 6, // HLS版本,6+支持fMP4
val targetDuration: Int, // 目标分片时长(秒)
val mediaSequence: Long, // 媒体序列号
val playlistType: PlaylistType? = null,
val segments: List<HLSSegment>,
val renditions: List<Rendition> = emptyList()
) {
enum class PlaylistType {
VOD, EVENT, LIVE
}
}
data class HLSSegment(
val duration: Double, // 分片时长(秒)
val uri: String, // 分片URI
val byteRange: ByteRange? = null, // 字节范围(对于fMP4打包在单个文件)
val discontinuity: Boolean = false, // 不连续标志
val programDateTime: String? = null // 节目日期时间
) {
data class ByteRange(
val offset: Long, // 起始偏移
val length: Long // 长度
)
}
data class Rendition(
val type: RenditionType,
val groupID: String,
val name: String,
val uri: String,
val default: Boolean = false,
val autoselect: Boolean = false,
val forced: Boolean = false
) {
enum class RenditionType {
AUDIO, VIDEO, SUBTITLES, CLOSED_CAPTIONS
}
}
// HLS fMP4分片生成
class FMP4Segmenter {
fun createInitSegment(moovData: ByteArray): ByteArray {
// 初始化段 = ftyp + moov
return concatenateBoxes(
createFtypBox("mp42", listOf("mp42", "iso6", "dash")),
moovData
)
}
fun createMediaSegment(
moofData: ByteArray,
mdatData: ByteArray
): ByteArray {
// 媒体段 = moof + mdat
return concatenateBoxes(moofData, mdatData)
}
// 创建HLS播放列表条目
fun createHLSEntry(
segmentData: ByteArray,
segmentIndex: Long,
duration: Double,
isIndependent: Boolean = true
): HLSSegment {
return HLSSegment(
duration = duration,
uri = "segment_$segmentIndex.m4s",
byteRange = if (isIndependent) null else {
// 如果多个分片打包在一个文件
calculateByteRange(segmentIndex, segmentData.size)
},
discontinuity = segmentIndex == 0L // 第一个分片标记不连续
)
}
}
}
4. CMAF(Common Media Application Format)
// CMAF规范实现
class CMAFImplementation {
// CMAF Header(CMAF头)
data class CMAFHeader(
val tracks: List<CMAFTrack>,
val fileType: CMAFFileType = CMAFFileType.CMAF
) {
enum class CMAFFileType {
CMAF, // 标准CMAF
CMAF_TRACK, // CMAF轨道
CMAF_SWITCHING // CMAF切换
}
}
data class CMAFTrack(
val trackID: Int,
val type: TrackType,
val codec: String, // RFC 6381编码字符串
val timescale: Long,
val duration: Long,
val trackWidth: Int? = null,
val trackHeight: Int? = null,
val sampleEntry: CMAFSampleEntry
) {
enum class TrackType {
VIDEO, AUDIO, SUBTITLE, TIMED_METADATA
}
}
// CMAF Chunk(CMAF块)
data class CMAFChunk(
val header: CMAFChunkHeader,
val samples: List<CMAFSample>
) {
data class CMAFChunkHeader(
val chunkIndex: Int,
val trackID: Int,
val baseDecodeTime: Long,
val sampleCount: Int,
val chunkDuration: Long
)
data class CMAFSample(
val sampleSize: Int,
val sampleDuration: Int,
val sampleFlags: CMAFSampleFlags,
val compositionTimeOffset: Int? = null,
val data: ByteArray
) {
data class CMAFSampleFlags(
val isLeading: Int, // 前导样本
val dependsOn: Int, // 依赖关系
val isDependedOn: Int, // 被依赖关系
val hasRedundancy: Int, // 冗余
val paddingValue: Int, // 填充值
val isNonSyncSample: Boolean // 是否非关键帧
)
}
}
// CMAF Switching Set(切换集)
class CMAFSwitchingSet {
// 同一内容的不同表示形式
val representations = mutableListOf<CMAFRepresentation>()
fun addRepresentation(rep: CMAFRepresentation) {
representations.add(rep)
}
// 查找最佳表示形式(基于带宽)
fun findBestRepresentation(availableBandwidth: Int): CMAFRepresentation? {
return representations
.filter { it.bandwidth <= availableBandwidth }
.maxByOrNull { it.bandwidth }
}
}
data class CMAFRepresentation(
val id: String,
val bandwidth: Int,
val width: Int? = null,
val height: Int? = null,
val frameRate: String? = null,
val codecs: String,
val initializationSegment: ByteArray,
val mediaSegments: List<ByteArray>
)
// CMAF生成器
class CMAFGenerator {
fun createCMAFTrack(
mediaData: ByteArray,
trackInfo: TrackInfo
): CMAFTrack {
return CMAFTrack(
trackID = trackInfo.trackID,
type = when (trackInfo.mimeType.substringBefore("/")) {
"video" -> TrackType.VIDEO
"audio" -> TrackType.AUDIO
else -> TrackType.TIMED_METADATA
},
codec = parseCodecString(trackInfo.codecSpecificData),
timescale = trackInfo.timescale,
duration = trackInfo.duration,
trackWidth = trackInfo.width,
trackHeight = trackInfo.height,
sampleEntry = createSampleEntry(trackInfo)
)
}
fun createCMAFChunk(
samples: List<MediaSample>,
chunkIndex: Int,
trackID: Int
): CMAFChunk {
val cmafSamples = samples.map { sample ->
CMAFChunk.CMAFSample(
sampleSize = sample.size,
sampleDuration = sample.duration,
sampleFlags = CMAFChunk.CMAFSample.CMAFSampleFlags(
isLeading = 0,
dependsOn = if (sample.isKeyFrame) 2 else 1,
isDependedOn = 1,
hasRedundancy = 0,
paddingValue = 0,
isNonSyncSample = !sample.isKeyFrame
),
compositionTimeOffset = sample.compositionOffset,
data = sample.data
)
}
return CMAFChunk(
header = CMAFChunk.CMAFChunkHeader(
chunkIndex = chunkIndex,
trackID = trackID,
baseDecodeTime = samples.first().decodeTime,
sampleCount = samples.size,
chunkDuration = samples.sumOf { it.duration }
),
samples = cmafSamples
)
}
}
}
四、Android中的MP4与fMP4实践
1. 使用MediaExtractor解析容器
class MediaContainerAnalyzer(private val context: Context) {
// 分析MP4文件
fun analyzeMP4File(filePath: String): ContainerAnalysis {
val extractor = MediaExtractor()
return try {
extractor.setDataSource(filePath)
val trackCount = extractor.trackCount
val tracks = mutableListOf<TrackAnalysis>()
for (i in 0 until trackCount) {
val format = extractor.getTrackFormat(i)
val trackAnalysis = analyzeTrack(format, i)
tracks.add(trackAnalysis)
}
// 获取容器级信息
val containerInfo = extractContainerInfo(extractor, filePath)
ContainerAnalysis(
filePath = filePath,
containerFormat = containerInfo.format,
tracks = tracks,
containerInfo = containerInfo,
isFragmented = isFragmentedMP4(filePath)
)
} finally {
extractor.release()
}
}
private fun analyzeTrack(format: MediaFormat, trackIndex: Int): TrackAnalysis {
val mime = format.getString(MediaFormat.KEY_MIME) ?: "unknown"
return TrackAnalysis(
trackIndex = trackIndex,
mimeType = mime,
durationUs = format.getLong(MediaFormat.KEY_DURATION),
bitrate = format.getInteger(MediaFormat.KEY_BIT_RATE),
codec = parseCodecFromMime(mime),
sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE, 0),
channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT, 0),
width = format.getInteger(MediaFormat.KEY_WIDTH, 0),
height = format.getInteger(MediaFormat.KEY_HEIGHT, 0),
frameRate = format.getInteger(MediaFormat.KEY_FRAME_RATE, 0),
iFrameInterval = format.getInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 0),
csd = extractCodecSpecificData(format)
)
}
// 检测是否为fMP4
private fun isFragmentedMP4(filePath: String): Boolean {
return try {
val input = FileInputStream(filePath).channel
val buffer = ByteBuffer.allocate(1024)
// 读取文件前1KB,查找moof盒子
input.read(buffer, 0)
buffer.flip()
val data = ByteArray(buffer.remaining())
buffer.get(data)
// 简单查找moof标记
String(data).contains("moof")
} catch (e: Exception) {
false
}
}
// 提取编解码器特定数据
private fun extractCodecSpecificData(format: MediaFormat): List<ByteArray> {
val csdList = mutableListOf<ByteArray>()
// 尝试获取CSD-0, CSD-1, CSD-2
for (i in 0..2) {
val key = "csd-$i"
if (format.containsKey(key)) {
val byteBuffer = format.getByteBuffer(key)
byteBuffer?.let {
val csd = ByteArray(it.remaining())
it.get(csd)
csdList.add(csd)
}
}
}
return csdList
}
data class ContainerAnalysis(
val filePath: String,
val containerFormat: String,
val tracks: List<TrackAnalysis>,
val containerInfo: ContainerInfo,
val isFragmented: Boolean
)
data class TrackAnalysis(
val trackIndex: Int,
val mimeType: String,
val durationUs: Long,
val bitrate: Int,
val codec: String,
val sampleRate: Int,
val channelCount: Int,
val width: Int,
val height: Int,
val frameRate: Int,
val iFrameInterval: Int,
val csd: List<ByteArray>
)
data class ContainerInfo(
val format: String,
val durationMs: Long,
val fileSize: Long,
val moovPosition: String,
val hasFragments: Boolean
)
}
2. 使用MediaMuxer生成MP4
class MP4Muxer(private val outputPath: String) {
private lateinit var muxer: MediaMuxer
private var videoTrackIndex = -1
private var audioTrackIndex = -1
private var isStarted = false
// 初始化Muxer
fun initialize(
videoFormat: MediaFormat? = null,
audioFormat: MediaFormat? = null
) {
muxer = MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
videoFormat?.let {
videoTrackIndex = muxer.addTrack(it)
}
audioFormat?.let {
audioTrackIndex = muxer.addTrack(it)
}
}
// 开始混合
fun start() {
if (!isStarted) {
muxer.start()
isStarted = true
}
}
// 写入视频样本
fun writeVideoSample(
buffer: ByteBuffer,
bufferInfo: MediaCodec.BufferInfo
) {
if (videoTrackIndex >= 0 && isStarted) {
muxer.writeSampleData(videoTrackIndex, buffer, bufferInfo)
}
}
// 写入音频样本
fun writeAudioSample(
buffer: ByteBuffer,
bufferInfo: MediaCodec.BufferInfo
) {
if (audioTrackIndex >= 0 && isStarted) {
muxer.writeSampleData(audioTrackIndex, buffer, bufferInfo)
}
}
// 停止并释放资源
fun release() {
if (isStarted) {
muxer.stop()
}
muxer.release()
}
// 生成MP4文件示例
companion object {
fun createMP4FromFrames(
outputPath: String,
videoFrames: List<VideoFrame>,
audioSamples: List<AudioSample>? = null
) {
val muxer = MP4Muxer(outputPath)
// 创建视频格式
val videoFormat = MediaFormat.createVideoFormat(
MediaFormat.MIMETYPE_VIDEO_AVC,
videoFrames.first().width,
videoFrames.first().height
).apply {
setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible)
setInteger(MediaFormat.KEY_BIT_RATE, 2_000_000)
setInteger(MediaFormat.KEY_FRAME_RATE, 30)
setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
// 设置SPS和PPS
videoFrames.first().sps?.let { sps ->
val spsBuffer = ByteBuffer.wrap(sps)
setByteBuffer("csd-0", spsBuffer)
}
videoFrames.first().pps?.let { pps ->
val ppsBuffer = ByteBuffer.wrap(pps)
setByteBuffer("csd-1", ppsBuffer)
}
}
// 创建音频格式(如果存在音频)
val audioFormat = audioSamples?.firstOrNull()?.let { firstSample ->
MediaFormat.createAudioFormat(
MediaFormat.MIMETYPE_AUDIO_AAC,
firstSample.sampleRate,
firstSample.channelCount
).apply {
setInteger(MediaFormat.KEY_BIT_RATE, 128_000)
setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC)
firstSample.config?.let { config ->
val configBuffer = ByteBuffer.wrap(config)
setByteBuffer("csd-0", configBuffer)
}
}
}
// 初始化Muxer
muxer.initialize(videoFormat, audioFormat)
muxer.start()
// 写入视频帧
var presentationTimeUs = 0L
videoFrames.forEach { frame ->
val bufferInfo = MediaCodec.BufferInfo().apply {
offset = 0
size = frame.data.size
presentationTimeUs = presentationTimeUs
flags = if (frame.isKeyFrame) MediaCodec.BUFFER_FLAG_KEY_FRAME else 0
}
val buffer = ByteBuffer.wrap(frame.data)
muxer.writeVideoSample(buffer, bufferInfo)
presentationTimeUs += 33_333 // 30fps,每帧约33.3ms
}
// 写入音频样本(如果存在)
audioSamples?.let { samples ->
var audioTimeUs = 0L
samples.forEach { sample ->
val bufferInfo = MediaCodec.BufferInfo().apply {
offset = 0
size = sample.data.size
presentationTimeUs = audioTimeUs
flags = 0
}
val buffer = ByteBuffer.wrap(sample.data)
muxer.writeAudioSample(buffer, bufferInfo)
audioTimeUs += (sample.sampleCount * 1_000_000L) / sample.sampleRate
}
}
muxer.release()
}
}
data class VideoFrame(
val data: ByteArray,
val width: Int,
val height: Int,
val isKeyFrame: Boolean,
val sps: ByteArray? = null,
val pps: ByteArray? = null
)
data class AudioSample(
val data: ByteArray,
val sampleRate: Int,
val channelCount: Int,
val sampleCount: Int,
val config: ByteArray? = null
)
}
3. 使用ExoPlayer播放fMP4流
class FragmentedMP4Player(
private val context: Context,
private val playerView: PlayerView
) {
private lateinit var player: ExoPlayer
private lateinit var mediaSource: MediaSource
// 初始化播放器
fun initialize(streamUrl: String, isLive: Boolean = false) {
// 创建播放器
player = ExoPlayer.Builder(context)
.setSeekBackIncrementMs(5000)
.setSeekForwardIncrementMs(15000)
.build()
playerView.player = player
// 创建媒体源
mediaSource = buildMediaSource(Uri.parse(streamUrl), isLive)
// 设置播放器监听
player.addListener(createPlayerListener())
}
// 构建媒体源
private fun buildMediaSource(uri: Uri, isLive: Boolean): MediaSource {
val dataSourceFactory = DefaultHttpDataSource.Factory()
.setUserAgent("ExoPlayerDemo")
.setConnectTimeoutMs(8000)
.setReadTimeoutMs(15000)
.setAllowCrossProtocolRedirects(true)
return if (isLive) {
// 直播流 - 使用HLS或DASH
if (uri.toString().contains(".m3u8")) {
HlsMediaSource.Factory(dataSourceFactory)
.setAllowChunklessPreparation(true)
.createMediaSource(MediaItem.fromUri(uri))
} else {
// DASH流
DashMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri(uri))
}
} else {
// 点播流 - 使用渐进式或fMP4
ProgressiveMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri(uri))
}
}
// 开始播放
fun play() {
player.setMediaSource(mediaSource)
player.prepare()
player.playWhenReady = true
}
// 处理fMP4特定功能
class FMP4Features {
// 自适应比特率切换
fun setupAdaptiveBitrate(player: ExoPlayer) {
player.addAnalyticsListener(object : AnalyticsListener {
override fun onBandwidthEstimate(
eventTime: EventTime,
totalLoadTimeMs: Long,
totalBytesLoaded: Long,
bitrateEstimate: Long
) {
// 基于带宽估计调整播放策略
adjustPlaybackBasedOnBandwidth(bitrateEstimate)
}
})
}
// 分片加载监控
fun monitorSegmentLoading(player: ExoPlayer) {
player.addAnalyticsListener(object : AnalyticsListener {
override fun onLoadCompleted(
eventTime: EventTime,
loadEventInfo: LoadEventInfo,
mediaLoadData: MediaLoadData
) {
// 分片加载完成
mediaLoadData.dataSpec?.uri?.let { uri ->
Log.d("SegmentLoad", "Loaded segment: $uri")
}
}
override fun onLoadCanceled(
eventTime: EventTime,
loadEventInfo: LoadEventInfo,
mediaLoadData: MediaLoadData
) {
// 分片加载取消(可能由于比特率切换)
}
})
}
// 低延迟优化
fun optimizeForLowLatency(player: ExoPlayer) {
// 减少缓冲区
val loadControl = DefaultLoadControl.Builder()
.setBufferDurationsMs(1000, 5000, 500, 2000)
.build()
// 设置轨道选择器偏好低延迟
val trackSelector = DefaultTrackSelector(context).apply {
parameters = buildUponParameters()
.setMaxVideoBitrate(1_500_000)
.setMaxVideoSize(1280, 720)
.build()
}
}
}
private fun createPlayerListener(): Player.Listener {
return object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
when (playbackState) {
Player.STATE_READY -> {
Log.d("Player", "播放器准备就绪")
}
Player.STATE_BUFFERING -> {
Log.d("Player", "播放器缓冲中")
}
Player.STATE_ENDED -> {
Log.d("Player", "播放结束")
}
}
}
override fun onPlayerError(error: PlaybackException) {
Log.e("Player", "播放错误: ${error.message}")
// 处理错误,如切换到低码率流
handlePlaybackError(error)
}
override fun onVideoSizeChanged(videoSize: VideoSize) {
Log.d("Player", "视频尺寸变化: ${videoSize.width}x${videoSize.height}")
}
}
}
private fun handlePlaybackError(error: PlaybackException) {
// 根据错误类型采取不同策略
when (error.errorCode) {
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT -> {
// 网络错误,尝试重连
scheduleReconnect()
}
PlaybackException.ERROR_CODE_PARSING_CONTAINER_MALFORMED -> {
// 容器格式错误,可能是fMP4分片损坏
skipToNextSegment()
}
PlaybackException.ERROR_CODE_DECODER_INIT_FAILED -> {
// 解码器初始化失败,尝试软解码
switchToSoftwareDecoding()
}
}
}
}
4. 生成fMP4分片
class FMP4SegmentGenerator {
// 生成初始化段(包含moov)
fun generateInitSegment(
videoConfig: VideoConfig,
audioConfig: AudioConfig? = null
): ByteArray {
val output = ByteArrayOutputStream()
val writer = MP4BoxWriter(output)
// 1. 写入ftyp盒子
writer.writeBox(createFtypBox())
// 2. 写入moov盒子
val moovBox = createMoovBox(videoConfig, audioConfig)
writer.writeBox(moovBox)
return output.toByteArray()
}
// 生成媒体段(包含moof+mdat)
fun generateMediaSegment(
segmentIndex: Int,
videoSamples: List<VideoSample>,
audioSamples: List<AudioSample>? = null,
isLastSegment: Boolean = false
): ByteArray {
val output = ByteArrayOutputStream()
val writer = MP4BoxWriter(output)
// 1. 写入moof盒子
val moofBox = createMoofBox(segmentIndex, videoSamples, audioSamples)
writer.writeBox(moofBox)
// 2. 写入mdat盒子
val mdatData = createMdatData(videoSamples, audioSamples)
writer.writeBox(createMdatBox(mdatData))
// 3. 如果是最后一个分片,写入mfra盒子(可选)
if (isLastSegment) {
val mfraBox = createMfraBox(segmentIndex)
writer.writeBox(mfraBox)
}
return output.toByteArray()
}
// 创建moof盒子
private fun createMoofBox(
segmentIndex: Int,
videoSamples: List<VideoSample>,
audioSamples: List<AudioSample>?
): Box {
// 创建mfhd(Movie Fragment Header)
val mfhd = Box(
type = "mfhd",
data = createMfhdData(segmentIndex)
)
// 创建traf(Track Fragment)列表
val trafs = mutableListOf<Box>()
// 视频轨道fragment
trafs.add(createTrafBox(
trackID = 1,
baseDecodeTime = calculateBaseDecodeTime(videoSamples),
samples = videoSamples.map { it.toTrunSample() }
))
// 音频轨道fragment(如果存在)
audioSamples?.let { samples ->
trafs.add(createTrafBox(
trackID = 2,
baseDecodeTime = calculateBaseDecodeTime(samples),
samples = samples.map { it.toTrunSample() }
))
}
return Box(
type = "moof",
children = listOf(mfhd) + trafs
)
}
// 创建traf盒子
private fun createTrafBox(
trackID: Int,
baseDecodeTime: Long,
samples: List<TrunSample>
): Box {
// tfhd(Track Fragment Header)
val tfhd = Box(
type = "tfhd",
data = createTfhdData(trackID)
)
// tfdt(Track Fragment Decode Time)
val tfdt = Box(
type = "tfdt",
data = createTfdtData(baseDecodeTime)
)
// trun(Track Fragment Run)
val trun = Box(
type = "trun",
data = createTrunData(samples)
)
return Box(
type = "traf",
children = listOf(tfhd, tfdt, trun)
)
}
// 创建sidx盒子(分片索引)
fun createSidxBox(
references: List<SidxReference>,
timescale: Int = 1000
): Box {
val data = ByteArrayOutputStream().apply {
// version(1) + flags(3)
writeBytes(byteArrayOf(0, 0, 0, 0))
// reference ID
writeInt(1)
// timescale
writeInt(timescale)
// earliest presentation time
writeLong(0)
// first offset
writeLong(0)
// reserved
writeShort(0)
// reference count
writeShort(references.size.toShort())
// references
references.forEach { ref ->
// reference type (1 bit) + referenced size (31 bits)
val firstWord = (if (ref.isContained) 1 else 0 shl 31) or
(ref.size and 0x7FFFFFFF).toInt()
writeInt(firstWord)
// subsegment duration
writeInt(ref.duration)
// starts with SAP (1) + SAP type (3) + SAP delta time (28)
val sapInfo = (1 shl 31) or
(ref.sapType shl 28) or
(ref.sapDeltaTime and 0x0FFFFFFF)
writeInt(sapInfo)
}
}
return Box(type = "sidx", data = data.toByteArray())
}
data class VideoSample(
val data: ByteArray,
val timestamp: Long,
val duration: Int,
val isKeyFrame: Boolean,
val compositionOffset: Int = 0
) {
fun toTrunSample(): TrunSample {
return TrunSample(
duration = duration,
size = data.size,
flags = if (isKeyFrame) 0x02000000 else 0x01010000,
compositionOffset = compositionOffset
)
}
}
data class AudioSample(
val data: ByteArray,
val timestamp: Long,
val duration: Int
) {
fun toTrunSample(): TrunSample {
return TrunSample(
duration = duration,
size = data.size,
flags = 0x02000000,
compositionOffset = null
)
}
}
data class TrunSample(
val duration: Int,
val size: Int,
val flags: Int,
val compositionOffset: Int? = null
)
data class SidxReference(
val isContained: Boolean, // 是否包含在同一个文件中
val size: Int, // 引用大小
val duration: Int, // 子段时长
val sapType: Int, // 流访问点类型
val sapDeltaTime: Int // 流访问点时间偏移
)
data class Box(
val type: String,
val data: ByteArray = ByteArray(0),
val children: List<Box> = emptyList()
)
// MP4盒子写入器
class MP4BoxWriter(private val output: OutputStream) {
fun writeBox(box: Box) {
// 计算总大小(包括子盒子)
val totalSize = calculateBoxSize(box)
// 写入大小(32位)
output.writeInt(totalSize.toInt())
// 写入类型(4字符)
output.write(box.type.toByteArray())
// 写入数据
if (box.data.isNotEmpty()) {
output.write(box.data)
}
// 写入子盒子
box.children.forEach { child ->
writeBox(child)
}
}
private fun calculateBoxSize(box: Box): Long {
var size = 8L // header大小
size += box.data.size
size += box.children.sumOf { calculateBoxSize(it) }
return size
}
}
}
五、性能优化与最佳实践
1. 容器格式选择策略
class ContainerFormatSelector {
// 根据场景选择容器格式
fun selectFormat(requirements: PlaybackRequirements): SelectedFormat {
return when {
// 低延迟直播
requirements.latency < 3000 && requirements.isLive -> {
SelectedFormat(
format = ContainerFormat.FRAGMENTED_MP4,
protocol = StreamingProtocol.DASH_LOW_LATENCY,
segmentDuration = 1, // 1秒分片
optimization = listOf(
"启用分块传输编码",
"使用CMAF低延迟模式",
"启用前向纠错"
)
)
}
// VOD点播
!requirements.isLive && requirements.quality == Quality.HIGH -> {
SelectedFormat(
format = ContainerFormat.MP4,
protocol = StreamingProtocol.PROGRESSIVE_DOWNLOAD,
segmentDuration = 0, // 不分片
optimization = listOf(
"moov前置优化",
"启用HTTP范围请求",
"视频轨道优先"
)
)
}
// 自适应流媒体
requirements.adaptiveBitrate -> {
SelectedFormat(
format = ContainerFormat.FRAGMENTED_MP4,
protocol = StreamingProtocol.DASH_ADAPTIVE,
segmentDuration = 4, // 4秒分片
optimization = listOf(
"多码率编码",
"分片级索引",
"带宽自适应逻辑"
)
)
}
// 移动端优化
requirements.deviceType == DeviceType.MOBILE -> {
SelectedFormat(
format = ContainerFormat.FRAGMENTED_MP4,
protocol = StreamingProtocol.HLS,
segmentDuration = 6, // 6秒分片
optimization = listOf(
"目标时长6秒",
"启用字节范围请求",
"移动网络优化"
)
)
}
else -> {
SelectedFormat(
format = ContainerFormat.FRAGMENTED_MP4,
protocol = StreamingProtocol.DASH,
segmentDuration = 2,
optimization = listOf("标准配置")
)
}
}
}
data class PlaybackRequirements(
val isLive: Boolean,
val latency: Int, // 目标延迟(毫秒)
val adaptiveBitrate: Boolean,
val quality: Quality,
val deviceType: DeviceType,
val networkType: NetworkType
)
data class SelectedFormat(
val format: ContainerFormat,
val protocol: StreamingProtocol,
val segmentDuration: Int, // 分片时长(秒)
val optimization: List<String>
)
enum class ContainerFormat {
MP4, FRAGMENTED_MP4, WEBM, TS
}
enum class StreamingProtocol {
DASH, HLS, DASH_LOW_LATENCY, DASH_ADAPTIVE,
HLS_FMP4, PROGRESSIVE_DOWNLOAD
}
enum class Quality {
LOW, MEDIUM, HIGH, ULTRA
}
enum class DeviceType {
MOBILE, TABLET, TV, DESKTOP
}
enum class NetworkType {
WIFI, CELLULAR_4G, CELLULAR_5G, ETHERNET
}
}
2. 编码与封装优化
class EncodingAndMuxingOptimizer {
// GOP(Group of Pictures)结构优化
class GOPOptimizer {
fun optimizeGOPStructure(
videoConfig: VideoConfig,
useCase: UseCase
): GOPStructure {
return when (useCase) {
UseCase.VOD -> {
// 点播:较长的GOP,高效压缩
GOPStructure(
gopSize = 120, // 4秒(30fps)
bFrames = 2, // 适量B帧
closedGOP = true, // 封闭式GOP
sceneChangeThreshold = 40
)
}
UseCase.LIVE -> {
// 直播:较短的GOP,低延迟
GOPStructure(
gopSize = 30, // 1秒(30fps)
bFrames = 0, // 无B帧以降低延迟
closedGOP = false, // 开放式GOP
sceneChangeThreshold = 30
)
}
UseCase.LOW_LATENCY_LIVE -> {
// 低延迟直播:超短GOP
GOPStructure(
gopSize = 15, // 0.5秒(30fps)
bFrames = 0,
closedGOP = false,
sceneChangeThreshold = 20
)
}
UseCase.ADAPTIVE_STREAMING -> {
// 自适应流:平衡的GOP
GOPStructure(
gopSize = 60, // 2秒(30fps)
bFrames = 1,
closedGOP = true,
sceneChangeThreshold = 35
)
}
}
}
data class GOPStructure(
val gopSize: Int, // GOP大小(帧数)
val bFrames: Int, // B帧数量
val closedGOP: Boolean, // 是否封闭式GOP
val sceneChangeThreshold: Int // 场景切换阈值
)
enum class UseCase {
VOD, LIVE, LOW_LATENCY_LIVE, ADAPTIVE_STREAMING
}
}
// 分片大小优化
class SegmentSizeOptimizer {
fun calculateOptimalSegmentSize(
bitrate: Int, // 比特率(bps)
targetDuration: Int, // 目标时长(秒)
networkConditions: NetworkConditions
): SegmentSize {
val targetBytes = (bitrate * targetDuration / 8).toLong()
// 根据网络条件调整
val adjustedSize = when (networkConditions.stability) {
NetworkStability.EXCELLENT -> targetBytes
NetworkStability.GOOD -> (targetBytes * 0.9).toLong()
NetworkStability.FAIR -> (targetBytes * 0.8).toLong()
NetworkStability.POOR -> (targetBytes * 0.6).toLong()
}
// 确保在合理范围内
val minSize = 100 * 1024L // 100KB
val maxSize = 10 * 1024 * 1024L // 10MB
val finalSize = adjustedSize.coerceIn(minSize, maxSize)
return SegmentSize(
targetBytes = finalSize,
targetDuration = targetDuration,
minSize = minSize,
maxSize = maxSize,
recommendation = generateRecommendation(finalSize, networkConditions)
)
}
data class SegmentSize(
val targetBytes: Long,
val targetDuration: Int,
val minSize: Long,
val maxSize: Long,
val recommendation: String
)
data class NetworkConditions(
val bandwidth: Int, // 带宽(bps)
val rtt: Int, // 往返时间(ms)
val packetLoss: Double, // 丢包率
val stability: NetworkStability
)
enum class NetworkStability {
EXCELLENT, GOOD, FAIR, POOR
}
}
// 缓存策略优化
class CacheOptimizer {
fun optimizeCacheStrategy(
format: ContainerFormat,
segmentSize: Long,
availableMemory: Long
): CacheStrategy {
val memoryPerSegment = (availableMemory * 0.3).toLong() // 30%内存用于缓存
return when (format) {
ContainerFormat.FRAGMENTED_MP4 -> {
// fMP4:缓存多个分片
val segmentsToCache = (memoryPerSegment / segmentSize).toInt().coerceAtLeast(3)
CacheStrategy(
type = CacheType.SEGMENT_CACHE,
size = segmentsToCache,
preloadSegments = 2,
evictionPolicy = EvictionPolicy.LRU
)
}
ContainerFormat.MP4 -> {
// MP4:缓存关键数据(moov,索引)
CacheStrategy(
type = CacheType.METADATA_CACHE,
size = (segmentSize * 0.1).toLong().coerceAtMost(10 * 1024 * 1024),
preloadSegments = 0,
evictionPolicy = EvictionPolicy.FIFO
)
}
else -> {
CacheStrategy(
type = CacheType.SEGMENT_CACHE,
size = 5,
preloadSegments = 1,
evictionPolicy = EvictionPolicy.LRU
)
}
}
}
data class CacheStrategy(
val type: CacheType,
val size: Any, // 缓存大小或数量
val preloadSegments: Int, // 预加载分片数
val evictionPolicy: EvictionPolicy
)
enum class CacheType {
SEGMENT_CACHE, // 分片缓存
METADATA_CACHE, // 元数据缓存
FRAME_CACHE // 帧缓存
}
enum class EvictionPolicy {
LRU, // 最近最少使用
FIFO, // 先进先出
LFU // 最不经常使用
}
}
}
3. 兼容性与降级策略
class CompatibilityManager {
// 设备兼容性检测
fun detectContainerSupport(context: Context): ContainerSupport {
val codecCapabilities = detectCodecCapabilities()
val exoPlayerCapabilities = detectExoPlayerCapabilities()
val systemCapabilities = detectSystemCapabilities(context)
return ContainerSupport(
mp4 = ContainerCapability(
supported = true,
h264 = codecCapabilities.h264,
h265 = codecCapabilities.h265,
aac = true,
dolbyDigital = systemCapabilities.dolbyDigital
),
fragmentedMp4 = ContainerCapability(
supported = exoPlayerCapabilities.fragmentedMp4,
h264 = codecCapabilities.h264,
h265 = codecCapabilities.h265 && exoPlayerCapabilities.hevcDash,
aac = true,
dolbyDigital = false
),
webm = ContainerCapability(
supported = codecCapabilities.vp9 || codecCapabilities.av1,
vp8 = codecCapabilities.vp8,
vp9 = codecCapabilities.vp9,
av1 = codecCapabilities.av1,
opus = codecCapabilities.opus
),
recommendation = generateRecommendation(
codecCapabilities,
exoPlayerCapabilities,
systemCapabilities
)
)
}
// 降级策略
fun getFallbackStrategy(
requestedFormat: ContainerFormat,
deviceSupport: ContainerSupport
): FallbackStrategy {
return when (requestedFormat) {
ContainerFormat.FRAGMENTED_MP4 -> {
if (deviceSupport.fragmentedMp4.supported) {
// 支持fMP4,无需降级
FallbackStrategy(
targetFormat = ContainerFormat.FRAGMENTED_MP4,
codecFallback = getCodecFallback(
deviceSupport.fragmentedMp4
),
protocolFallback = null
)
} else {
// 不支持fMP4,降级到MP4
FallbackStrategy(
targetFormat = ContainerFormat.MP4,
codecFallback = getCodecFallback(deviceSupport.mp4),
protocolFallback = StreamingProtocol.PROGRESSIVE_DOWNLOAD
)
}
}
ContainerFormat.WEBM -> {
// WebM降级策略
FallbackStrategy(
targetFormat = if (deviceSupport.webm.supported) {
ContainerFormat.WEBM
} else {
ContainerFormat.MP4
},
codecFallback = getWebmCodecFallback(deviceSupport.webm),
protocolFallback = null
)
}
else -> {
FallbackStrategy(
targetFormat = requestedFormat,
codecFallback = null,
protocolFallback = null
)
}
}
}
data class ContainerSupport(
val mp4: ContainerCapability,
val fragmentedMp4: ContainerCapability,
val webm: ContainerCapability,
val recommendation: String
)
data class ContainerCapability(
val supported: Boolean,
val h264: Boolean = false,
val h265: Boolean = false,
val aac: Boolean = false,
val dolbyDigital: Boolean = false,
val vp8: Boolean = false,
val vp9: Boolean = false,
val av1: Boolean = false,
val opus: Boolean = false
)
data class FallbackStrategy(
val targetFormat: ContainerFormat,
val codecFallback: CodecFallback?,
val protocolFallback: StreamingProtocol?
)
data class CodecFallback(
val primary: String, // 首选编解码器
val fallback: String, // 降级编解码器
val rationale: String // 降级原因
)
// 编解码器能力检测
private fun detectCodecCapabilities(): CodecCapabilities {
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
val allCodecs = codecList.codecInfos
return CodecCapabilities(
h264 = allCodecs.any { it.isEncoder && it.supportsMimeType("video/avc") },
h265 = allCodecs.any { it.isEncoder && it.supportsMimeType("video/hevc") },
vp8 = allCodecs.any { it.isEncoder && it.supportsMimeType("video/x-vnd.on2.vp8") },
vp9 = allCodecs.any { it.isEncoder && it.supportsMimeType("video/x-vnd.on2.vp9") },
av1 = allCodecs.any { it.isEncoder && it.supportsMimeType("video/av01") },
opus = allCodecs.any { it.isEncoder && it.supportsMimeType("audio/opus") }
)
}
data class CodecCapabilities(
val h264: Boolean,
val h265: Boolean,
val vp8: Boolean,
val vp9: Boolean,
val av1: Boolean,
val opus: Boolean
)
}
六、未来趋势与发展方向
1. CMAF的演进
class CMAFFutureDevelopments {
// CMAF 2.0新特性
data class CMAF20Features(
// 低延迟增强
val ultraLowLatency: UltraLowLatencyMode,
// 新编码格式支持
val vvcSupport: Boolean, // VVC (H.266)
val evcSupport: Boolean, // EVC
val lc3Support: Boolean, // LC3音频
// 交互功能
val interactiveFeatures: InteractiveFeatures,
// 元数据增强
val enhancedMetadata: EnhancedMetadataSupport,
// 安全性增强
val securityFeatures: SecurityFeatures
)
data class UltraLowLatencyMode(
val targetLatency: Int, // 目标延迟(毫秒)
val chunkedTransfer: Boolean, // 分块传输
val cmafChunk: Boolean, // CMAF分块
val pushMode: Boolean // 推送模式
)
data class InteractiveFeatures(
val branching: Boolean, // 分支叙事
val personalizedAds: Boolean, // 个性化广告
val chooseYourOwnAdventure: Boolean, // 交互式故事
val multiEnding: Boolean // 多结局
)
// 边缘计算与容器格式
class EdgeComputingIntegration {
fun optimizeForEdge(
edgeNodeCapabilities: EdgeNodeCapabilities
): EdgeOptimization {
return EdgeOptimization(
containerFormat = if (edgeNodeCapabilities.supportsCMAF) {
ContainerFormat.CMAF
} else {
ContainerFormat.FRAGMENTED_MP4
},
transcodingStrategy = if (edgeNodeCapabilities.gpuAvailable) {
TranscodingStrategy.GPU_ACCELERATED
} else {
TranscodingStrategy.CPU_EFFICIENT
},
cachingStrategy = CachingStrategy(
type = CacheType.EDGE_CACHE,
ttl = edgeNodeCapabilities.cacheTtl,
prefetchEnabled = true
),
cdnIntegration = CDNIntegration(
supportsChunkedTransfer = true,
supportsByteRange = true,
supportsManifestManipulation = edgeNodeCapabilities.supportsManifestManipulation
)
)
}
}
}
2. 新技术整合
class EmergingTechnologies {
// AV1在容器中的支持
class AV1InContainers {
fun createAV1MP4Configuration(): AV1Configuration {
return AV1Configuration(
container = ContainerFormat.FRAGMENTED_MP4,
codecString = "av01.0.04M.08", // AV1 Codec String
profile = AV1Profile.MAIN,
level = AV1Level.LEVEL_4_0,
tier = AV1Tier.MAIN,
colorConfig = AV1ColorConfig(
bitDepth = 8,
chromaSubsampling = ChromaSubsampling.YUV420,
colorSpace = ColorSpace.BT709,
transferCharacteristics = TransferCharacteristics.BT709,
matrixCoefficients = MatrixCoefficients.BT709
),
optimization = listOf(
"使用AV1 Annex B格式",
"启用film grain合成",
"使用CDEF和LR滤波"
)
)
}
data class AV1Configuration(
val container: ContainerFormat,
val codecString: String,
val profile: AV1Profile,
val level: AV1Level,
val tier: AV1Tier,
val colorConfig: AV1ColorConfig,
val optimization: List<String>
)
}
// 沉浸式媒体(VR/360°)
class ImmersiveMediaContainers {
fun createVRMP4Configuration(): VRConfiguration {
return VRConfiguration(
container = ContainerFormat.FRAGMENTED_MP4,
projection = Projection.EQUIRECTANGULAR,
stereoMode = StereoMode.TOP_BOTTOM,
spatialAudio = true,
metadata = VRMetadata(
initialViewHeading = 0.0,
initialViewPitch = 0.0,
initialViewRoll = 0.0,
initialViewFov = 100.0,
timestampSubsamples = true
),
optimization = listOf(
"分片级视口自适应",
"多分辨率编码",
"运动预测预加载"
)
)
}
}
// 机器学习与容器优化
class MLEnhancedContainers {
fun applyMLOptimizations(
playbackPattern: PlaybackPattern,
networkConditions: NetworkConditions
): MLOptimization {
val model = loadMLModel("container_optimization_model")
return MLOptimization(
segmentSizePrediction = model.predictSegmentSize(
bitrate = playbackPattern.averageBitrate,
networkStability = networkConditions.stability
),
prefetchStrategy = model.predictPrefetch(
userBehavior = playbackPattern.seekingPattern,
contentType = playbackPattern.contentType
),
cacheOptimization = model.optimizeCache(
availableMemory = playbackPattern.availableMemory,
playbackHistory = playbackPattern.history
),
codecSelection = model.selectOptimalCodec(
deviceCapabilities = playbackPattern.deviceCapabilities,
contentComplexity = playbackPattern.contentComplexity
)
)
}
}
}
七、总结与最佳实践
✅ 容器格式选择指南
| 场景 | 推荐格式 | 分片大小 | 关键优化 | 适用协议 |
|---|---|---|---|---|
| 点播视频 | MP4 | 不分片 | moov前置,HTTP范围请求 | 渐进式下载 |
| 直播流 | fMP4 | 2-4秒 | 低GOP,无B帧,分片索引 | DASH,HLS+fMP4 |
| 低延迟直播 | fMP4 | 0.5-1秒 | CMAF低延迟,分块传输 | DASH-LL,LHLS |
| 自适应流 | fMP4 | 2-6秒 | 多码率,分片对齐,带宽自适应 | DASH,HLS |
| 移动端 | fMP4 | 4-10秒 | 字节范围请求,网络优化 | HLS |
| 超高清 | fMP4/MP4 | 4-8秒 | HEVC,HDR元数据,多层编码 | DASH |
| VR/360° | fMP4 | 2-4秒 | 视口自适应,空间音频 | DASH-OMAF |
🎯 性能指标目标
data class PerformanceTargets(
// 容器级指标
val moovSizePercentage: Double = 0.01, // moov不超过文件1%
val fragmentOverhead: Double = 0.05, // 分片开销不超过5%
val seekTime: Int = 200, // 跳转时间<200ms
// 流媒体指标
val startupTime: Int = 1500, // 起播时间<1.5s
val rebufferRatio: Double = 0.01, // 卡顿率<1%
val bitrateSwitchTime: Int = 500, // 码率切换<500ms
// 编码效率
val compressionRatio: Double = 100.0, // 压缩比>100:1
val keyFrameInterval: Int = 2, // 关键帧间隔2秒
// 兼容性
val deviceCoverage: Double = 0.98, // 设备覆盖率>98%
val formatSupport: Double = 0.95 // 格式支持率>95%
)
📊 监控指标建议
class ContainerMetricsMonitor {
val keyMetrics = listOf(
Metric(
name = "容器解析时间",
description = "解析容器头部和索引的时间",
unit = "ms",
target = "< 50ms",
alertThreshold = 100
),
Metric(
name = "分片加载延迟",
description = "从请求到开始下载分片的时间",
unit = "ms",
target = "< 100ms",
alertThreshold = 500
),
Metric(
name = "索引效率",
description = "索引大小与媒体大小的比例",
unit = "%",
target = "< 3%",
alertThreshold = 10
),
Metric(
name = "GOP对齐率",
description = "分片边界与GOP对齐的比例",
unit = "%",
target = "> 95%",
alertThreshold = 80
),
Metric(
name = "字节范围请求效率",
description = "字节范围请求的命中率",
unit = "%",
target = "> 90%",
alertThreshold = 70
)
)
data class Metric(
val name: String,
val description: String,
val unit: String,
val target: String,
val alertThreshold: Int
)
}
🚀 推荐学习路径
-
基础掌握
- ISO BMFF标准文档
- MP4文件格式详解
- ExoPlayer源码分析
-
进阶实践
- 实现简单的MP4解析器
- 构建fMP4分片生成器
- 集成DASH/HLS播放
-
专业深入
- CMAF规范研究
- 低延迟流媒体优化
- 容器格式性能调优
-
前沿探索
- AV1/AV2容器支持
- 沉浸式媒体容器
- AI驱动的容器优化
实践建议: 在实际项目中,建议从理解现有的MP4文件开始,逐步深入到fMP4和流媒体协议。记得使用工具(如mp4info, bento4)分析现有文件结构,这对理解容器格式非常有帮助!
更多推荐

所有评论(0)