10、MediaMuxer实战:添加字幕轨道(Subtitle Track)——将外部字幕文件嵌入视频
字幕嵌入,说白了就是把外挂字幕变成内嵌字幕。很多视频编辑App都有这个功能——用户导入一个SRT文件,然后字幕就“焊死”在视频里了。今天我们就来聊聊,如何用MediaMuxer实现这个能力。
我个人习惯把字幕轨道看作“第四种轨道类型”。视频有视频轨道,音频有音频轨道,字幕嘛,在Android里其实也是通过MediaMuxer写入的。不过这里有个坑——MediaMuxer对字幕的支持并不像视频、音频那么完善。嗯,我们得绕点路。
10.1 字幕轨道的本质
先搞清楚一件事:MediaMuxer支持的字幕格式,其实是“时间戳+文本”的样本数据。它不像MP4里那种标准的字幕轨道(比如tx3g格式),而是需要我们手动把字幕数据打包成MediaCodec能识别的格式。
我在项目中遇到过这样一个场景:用户上传了一个SRT文件,要求嵌入到视频里。当时我第一反应是“直接用MediaMuxer加个字幕轨道不就行了?”结果发现,事情没那么简单。
MediaMuxer的addTrack方法确实可以添加字幕轨道,但前提是——你得提供一个MediaFormat对象,并且这个格式必须是“application/x-subrip”或者类似的MIME类型。说白了,Android原生支持的字幕格式非常有限。
核心要点:MediaMuxer添加字幕轨道,本质上是把字幕文本作为“样本数据”写入,而不是渲染成图像。字幕的渲染工作,需要播放器端支持。
10.2 准备工作:解析SRT文件
SRT文件的结构很简单:序号、时间范围、字幕文本。我们得先把它解析成程序能用的数据结构。
// 字幕条目数据结构
public class SubtitleItem {
public long startTimeUs; // 开始时间,微秒
public long endTimeUs; // 结束时间,微秒
public String text; // 字幕文本
}
// 解析SRT文件
public List<SubtitleItem> parseSrt(String filePath) {
List<SubtitleItem> items = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
String line;
while ((line = reader.readLine()) != null) {
// 跳过序号行
if (line.trim().isEmpty()) continue;
// 读取时间行
String timeLine = reader.readLine();
if (timeLine == null) break;
// 解析时间
String[] times = timeLine.split(" --> ");
long startUs = parseTimeToUs(times[0].trim());
long endUs = parseTimeToUs(times[1].trim());
// 读取字幕文本(可能有多行)
StringBuilder text = new StringBuilder();
while ((line = reader.readLine()) != null && !line.trim().isEmpty()) {
text.append(line).append("\n");
}
SubtitleItem item = new SubtitleItem();
item.startTimeUs = startUs;
item.endTimeUs = endUs;
item.text = text.toString().trim();
items.add(item);
}
} catch (IOException e) {
e.printStackTrace();
}
return items;
}
// 时间格式:00:00:01,234 → 微秒
private long parseTimeToUs(String timeStr) {
String[] parts = timeStr.split("[:,]");
long hours = Long.parseLong(parts[0]);
long minutes = Long.parseLong(parts[1]);
long seconds = Long.parseLong(parts[2]);
long millis = Long.parseLong(parts[3]);
return (hours * 3600 + minutes * 60 + seconds) * 1000000 + millis * 1000;
}
小技巧:解析时间时,注意SRT文件的时间分隔符是“-->”,前后各有一个空格。我曾经因为没处理空格,解析出来的时间全是0,排查了半天才发现是格式问题。
10.3 创建字幕轨道并写入
解析完SRT文件后,我们就可以创建字幕轨道了。这里要注意,MediaMuxer的字幕轨道格式需要手动指定。
// 创建字幕轨道的MediaFormat
MediaFormat subtitleFormat = MediaFormat.createSubtitleFormat("application/x-subrip", "en");
// 或者使用更通用的方式
// MediaFormat subtitleFormat = new MediaFormat();
// subtitleFormat.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_TEXT_SUBRIP);
// 添加到MediaMuxer
int subtitleTrackIndex = muxer.addTrack(subtitleFormat);
muxer.start();
// 写入字幕样本
for (SubtitleItem item : subtitleItems) {
byte[] data = item.text.getBytes("UTF-8");
ByteBuffer buffer = ByteBuffer.wrap(data);
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
info.offset = 0;
info.size = data.length;
info.presentationTimeUs = item.startTimeUs;
info.flags = MediaCodec.BUFFER_FLAG_KEY_FRAME; // 字幕样本通常标记为关键帧
muxer.writeSampleData(subtitleTrackIndex, buffer, info);
}
你想想看,这里有个关键点——每个字幕样本的presentationTimeUs,对应的是字幕开始显示的时间。那结束时间呢?其实MediaMuxer不关心结束时间,它只负责把样本写进去。字幕什么时候消失,是播放器根据下一个字幕样本的时间戳来决定的。
注意:MediaMuxer写入字幕样本时,必须保证时间戳是递增的。如果你乱序写入,生成的视频文件在播放时可能会出现字幕错乱。我曾经犯过这个错——解析SRT时没排序,结果字幕在播放器里跳来跳去。
10.4 完整的嵌入流程
把上面这些串起来,就是一个完整的字幕嵌入流程。我画了一张图,帮你理清思路:
10.5 完整代码示例
下面是一个完整的字幕嵌入方法,把视频、音频和字幕合并到一个MP4文件里:
public void muxWithSubtitle(String videoPath, String audioPath,
String srtPath, String outputPath) {
try {
MediaExtractor videoExtractor = new MediaExtractor();
videoExtractor.setDataSource(videoPath);
MediaExtractor audioExtractor = new MediaExtractor();
audioExtractor.setDataSource(audioPath);
MediaMuxer muxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
// 添加视频轨道
int videoTrackIndex = -1;
for (int i = 0; i < videoExtractor.getTrackCount(); i++) {
MediaFormat format = videoExtractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("video/")) {
videoExtractor.selectTrack(i);
videoTrackIndex = muxer.addTrack(format);
break;
}
}
// 添加音频轨道
int audioTrackIndex = -1;
for (int i = 0; i < audioExtractor.getTrackCount(); i++) {
MediaFormat format = audioExtractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("audio/")) {
audioExtractor.selectTrack(i);
audioTrackIndex = muxer.addTrack(format);
break;
}
}
// 解析字幕并添加轨道
List<SubtitleItem> subtitles = parseSrt(srtPath);
MediaFormat subtitleFormat = MediaFormat.createSubtitleFormat(
"application/x-subrip", "zh");
int subtitleTrackIndex = muxer.addTrack(subtitleFormat);
muxer.start();
// 写入视频样本
// ...(省略视频写入代码,与之前章节相同)
// 写入音频样本
// ...(省略音频写入代码)
// 写入字幕样本
for (SubtitleItem item : subtitles) {
byte[] data = item.text.getBytes("UTF-8");
ByteBuffer buffer = ByteBuffer.wrap(data);
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
info.offset = 0;
info.size = data.length;
info.presentationTimeUs = item.startTimeUs;
info.flags = MediaCodec.BUFFER_FLAG_KEY_FRAME;
muxer.writeSampleData(subtitleTrackIndex, buffer, info);
}
muxer.stop();
muxer.release();
videoExtractor.release();
audioExtractor.release();
} catch (IOException e) {
e.printStackTrace();
}
}
经验之谈:写入字幕样本时,我建议把字幕文本的编码统一为UTF-8。有些SRT文件是ANSI编码,直接读取会出现乱码。另外,字幕文本里如果包含换行符,记得保留——播放器会根据换行符自动分行显示。
10.6 避坑指南
做字幕嵌入时,有几个坑我踩过,分享给你:
- 时间戳精度问题:SRT文件的时间单位是毫秒,但MediaMuxer要求微秒。转换时别漏了乘以1000。我曾经因为单位搞错,字幕全部提前或延迟显示。
- 字幕轨道索引:addTrack返回的索引,在写入样本时一定要用对。如果你有视频、音频、字幕三个轨道,索引分别是0、1、2。写错索引会导致数据写入错误的轨道。
- 播放器兼容性:不是所有播放器都支持内嵌字幕。系统自带的播放器可能直接忽略字幕轨道。建议用VLC或MX Player测试。
- 字幕文本长度:单个字幕样本的文本不要太大。如果一行字幕有几百个字,播放器可能显示不全。我一般建议每行不超过40个字符。
重要提醒:MediaMuxer写入字幕轨道后,生成的MP4文件在PC上可能正常播放,但在手机上可能看不到字幕。这是因为Android系统播放器对字幕轨道的支持有限。如果你需要确保所有设备都能显示字幕,可以考虑把字幕渲染到视频画面上——那就是另一套方案了。
好了,关于MediaMuxer添加字幕轨道的内容就聊到这里。说白了,这个功能的核心就是“解析SRT → 创建轨道 → 写入样本”三步走。虽然Android对字幕的支持不算完美,但掌握了这套方法,至少能应对大部分需求。