如何检测分段原始MP4数据的音视频轨道并构建MP4Parser的Movie对象?
哈哈,这个问题我之前帮人排查过好几次——用范围请求拉的部分MP4数据,没拿到关键的元数据就想解析轨道,可不就报无轨道嘛!咱们一步一步来解决:
MP4文件是由一个个叫box的单元组成的,其中moov box是核心——它包含了所有音视频轨道的元数据(比如轨道类型、编码格式、时长这些)。MP4Parser构建Movie对象时必须读取到完整的moov box,如果你的范围请求只拉了媒体数据部分(也就是mdat box),或者只拉了一半moov,那它肯定找不到轨道,直接抛异常。
要检测轨道,本质就是看片段里有没有完整的moov box,以及moov里有没有trak子box(每个trak对应一个轨道)。这里给你写个实用的Java代码片段,用MP4Parser的工具来遍历解析:
import org.mp4parser.boxes.iso14496.part12.*; import org.mp4parser.DefaultBoxParser; import java.io.File; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.charset.StandardCharsets; import java.util.List; public class PartialMp4Checker { public static void main(String[] args) throws Exception { File partialFile = new File("你的部分MP4文件路径"); FileChannel fc = new RandomAccessFile(partialFile, "r").getChannel(); DefaultBoxParser parser = new DefaultBoxParser(); boolean foundMoov = false; ByteBuffer headerBuffer = ByteBuffer.allocate(8); while (fc.read(headerBuffer) == 8) { headerBuffer.flip(); // 读取box大小(转成无符号长整型) long boxSize = headerBuffer.getInt() & 0xFFFFFFFFL; String boxType = new String(headerBuffer.array(), headerBuffer.position(), 4, StandardCharsets.US_ASCII); headerBuffer.clear(); if ("moov".equals(boxType)) { // 回退到box开头,尝试完整解析moov fc.position(fc.position() - 8); Box moovBox = parser.parseBox(fc, null, null); if (moovBox instanceof MoovBox) { MoovBox moov = (MoovBox) moovBox; List<TrakBox> tracks = moov.getTraks(); System.out.println("找到" + tracks.size() + "个轨道"); // 区分音视频轨道 for (TrakBox trak : tracks) { MdiaBox mdia = trak.getMdia(); MinfBox minf = mdia.getMinf(); StblBox stbl = minf.getStbl(); StsdBox stsd = stbl.getStsd(); for (Box entry : stsd.getBoxes()) { if (entry instanceof Avc1Box) { System.out.println("→ 视频轨道(H.264编码)"); } else if (entry instanceof Mp4aBox) { System.out.println("→ 音频轨道(AAC编码)"); } else { System.out.println("→ 未知轨道类型:" + entry.getType()); } } } } foundMoov = true; break; } else { // 跳过当前box,移动到下一个box的开头 if (boxSize == 0) { break; // 文件结束 } else if (boxSize == 1) { // 超大box,读取8字节的实际大小 ByteBuffer bigSizeBuffer = ByteBuffer.allocate(8); fc.read(bigSizeBuffer); boxSize = bigSizeBuffer.getLong(); bigSizeBuffer.clear(); } fc.position(fc.position() + boxSize - 8); } } if (!foundMoov) { System.out.println("当前片段中没有找到moov box,无法检测轨道"); } fc.close(); } }
运行这段代码后,你就能清楚知道:
- 片段里有没有moov box
- 有多少个轨道,以及每个轨道是音频还是视频
分两种情况来处理:
情况1:片段包含完整的moov box
如果上面的检测显示有轨道,那直接用MovieCreator构建就行——MP4Parser能识别包含完整moov和部分mdat的文件:
import org.mp4parser.Movie; import org.mp4parser.MovieCreator; import java.io.File; public class PartialMp4Loader { public static void main(String[] args) throws Exception { File partialFile = new File("你的部分MP4文件路径"); Movie movie = MovieCreator.build(partialFile.getAbsolutePath()); // 验证轨道是否加载成功 System.out.println("成功加载" + movie.getTracks().size() + "个轨道"); // 后续可以对轨道做处理,比如提取样本、转码等 } }
注意:这种情况下,Movie对象能正常构建,但媒体数据是不完整的,播放时只能播放到你下载的部分。
情况2:片段只有mdat,没有moov
这时候你得先拿到完整的moov box才行。MP4的moov一般在两个位置:
- 普通MP4:大多在文件开头
- 流媒体MP4(比如fMP4):可能在文件结尾,或者每个分段有自己的moof(类似迷你moov)
你可以先发起两次范围请求:
- 拉取文件开头的1MB数据,检查有没有moov
- 如果没有,拉取文件结尾的1MB数据,检查有没有moov
找到moov的起始和结束位置后,单独下载完整的moov,然后把moov和你之前下载的mdat按原始文件的顺序拼接成一个临时文件,再用MovieCreator解析。
举个例子,假设原始文件是mdat在前、moov在后,拼接代码如下:
import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; public class Mp4SplitterMerger { public static void main(String[] args) throws Exception { File mdatFile = new File("你下载的mdat片段"); File moovFile = new File("你单独下载的完整moov"); File tempMp4 = new File("临时拼接文件.mp4"); try (FileOutputStream fos = new FileOutputStream(tempMp4); FileInputStream mdatIn = new FileInputStream(mdatFile); FileInputStream moovIn = new FileInputStream(moovFile)) { byte[] buffer = new byte[8192]; int bytesRead; // 先写mdat while ((bytesRead = mdatIn.read(buffer)) != -1) { fos.write(buffer, 0, bytesRead); } // 再写moov while ((bytesRead = moovIn.read(buffer)) != -1) { fos.write(buffer, 0, bytesRead); } } // 解析临时文件 Movie movie = MovieCreator.build(tempMp4.getAbsolutePath()); System.out.println("成功构建Movie,轨道数:" + movie.getTracks().size()); // 用完临时文件可以删除 tempMp4.deleteOnExit(); } }
额外提醒:如果是流媒体分段MP4(fMP4)
如果你的片段是fMP4(带moof和mdat的分段文件),那上面的方法不适用,得用MP4Parser的FragmentedMp4Parser来解析,示例代码如下:
import org.mp4parser.fragmented.FragmentedMp4Parser; import org.mp4parser.muxer.Movie; import java.io.File; public class FragmentedMp4Loader { public static void main(String[] args) throws Exception { File fmp4File = new File("你的fMP4片段"); Movie movie = FragmentedMp4Parser.parse(fmp4File); System.out.println("成功加载fMP4,轨道数:" + movie.getTracks().size()); } }
内容的提问来源于stack exchange,提问作者UVM

