背景
在 iOS 26 上,视频播放器使用的 libass 字幕渲染器遭遇了严重的兼容性问题。当字幕指定的字体在系统中找不到时,libass 的 CoreText 后端会尝试 fallback 到系统字体路径:
sql
/System/Library/PrivateFrameworks/FontServices.framework/CorePrivate/PingFangUI.ttc
然而,这个路径在 iOS 26 的沙盒机制下被系统拦截,导致 fallback 失败,内嵌 ASS/SSA 字幕中的中文字符完全无法渲染。用户看到的只是一片空白或乱码。
问题分析
1. 问题表现
| 字幕类型 | 问题描述 |
|---|---|
| 内嵌 ASS/SSA | 中文字符完全不显示,字体 fallback 失败 |
| 外挂 ASS/SSA | 某些字体无法渲染,fallback 到被拦截的路径 |
| SRT(内嵌提取后) | 带有 <font face="xxx"> 标签的 SRT,freetype 尝试加载指定字体失败 |
2. 根本原因
iOS 26 引入了一个沙盒安全限制,阻止了 libass 对系统字体的访问。libass 的字体回退机制无法获取 PingFang 字体,导致整个字幕渲染失败。
解决方案
方案概述
markdown
视频播放
↓
禁用内嵌字幕(避免 libass 走系统字体路径)
↓
提取内嵌字幕流为 SRT(通过 FFmpeg)
↓
SRT 无字体定义,通过 freetype 渲染器 + 指定中文字体显示
↓
同时对外挂 ASS 字幕做字体名替换(指向 CoreText 已注册的字体)
核心实现
1. 内嵌字幕提取
通过 FFmpeg 提取视频中的字幕流,转换为无字体定义的 SRT 格式:
objc
// 使用 FFmpeg 提取字幕流
FFmpegWrapperAPI *ffAPI = [[FFmpegWrapperAPI alloc] init];
ffAPI.inputPath = videoPath;
// -map 0:s:N 选择特定字幕流
NSString *command = [NSString stringWithFormat:@"-map 0:s:%d", trackIndex];
[ffAPI runFFmpegAPI:videoPath
outputPath:srtOutputPath
prefix:nil
command:command
async:YES];
2. 字体名替换(正则方案)
ASS 字幕中的字体名出现在两处:
[V4+ Styles]定义行:Style: Name,Fontname,Fontsize,...[Events]Dialogue 行内覆盖标签:{\fn字体名}
objc
// 替换 Dialogue 行内的 {\fn任意字体名} 覆盖标签
NSRegularExpression *fnTagRegex = [NSRegularExpression
regularExpressionWithPattern:@"\\{\\\\fn[^}\\\\]+"
options:NSRegularExpressionCaseInsensitive
error:®exError];
modifiedText = [fnTagRegex stringByReplacingMatchesInString:modifiedText
options:0
range:NSMakeRange(0, modifiedText.length)
withTemplate:[NSString stringWithFormat:@"{\\fn%@", kTargetFontName]];
// 替换 Style 定义行的 Fontname 字段
NSRegularExpression *styleLineRegex = [NSRegularExpression
regularExpressionWithPattern:@"^(Style\\s*:\\s*[^,]+,)([^,]+)(,.*)$"
options:0
error:®exError];
// ...
SRT 字幕中的字体名出现在 HTML 标签中:
objc
// 替换 <font face="任意内容">
NSRegularExpression *fontFaceRegex = [NSRegularExpression
regularExpressionWithPattern:@"<font\\s+face\\s*=\\s*([\"'])[^\"']*\\1"
options:NSRegularExpressionCaseInsensitive
error:®exError];
🔴 踩坑实录
坑一:VLC 索引与 FFmpeg 索引的映射错误
问题描述:用户选择中文内嵌字幕,但实际显示的是英文字幕。
根因分析:
- VLC 的
videoSubTitlesIndexes数组索引 0 是"Disable" - 内嵌字幕从索引 1 开始:索引 1 → 第一条字幕,索引 2 → 第二条字幕
- FFmpeg 的字幕流索引从 0 开始:第一条字幕流是
0,第二条是1
错误的映射:
用户选择 VLC 索引 1(第一条字幕)→ 错误地映射为 FFmpeg 索引 1 → 提取了第二条字幕
代码修复:
objc
// 修复前(错误)
int ffmpegTrackIndex = trackIndex + 1;
// 修复后(正确)
int ffmpegTrackIndex = (int)subtitleIndex - 1;
// VLC 索引 1 → FFmpeg 索引 0
// VLC 索引 2 → FFmpeg 索引 1
日志验证:
ini
[updateSubtitleUrl] iOS 26 拦截内嵌字幕: VLC subtitleIndex=1,
→ FFmpeg trackIndex=0 // 修复后正确映射到第一条字幕流
坑二:SRT 字幕的 <font> 标签问题
问题描述:内嵌字幕提取为 SRT 后,部分 SRT 仍无法显示中文。
日志分析:
ini
[ExtractSub] SRT 前 200 字:
1
00:00:00,000 --> 00:00:03,018
<font face="方正准圆简体" size="21"><b>...
根因分析 :虽然 FFmpeg 提取时没有字体定义,但某些视频的字幕流本身已包含 <font face="xxx"> 标签。这些标签导致 VLC 的 freetype 渲染器尝试加载指定字体,同样失败并 fallback 到被拦截的路径。
修复 :在加载 SRT 前,批量替换所有 <font face="xxx"> 标签:
objc
NSString *srtText = [NSString stringWithContentsOfFile:srtUrl.path encoding:NSUTF8StringEncoding error:nil];
NSString *replaced = [self replaceSrtFontNamesInText:srtText];
[replaced writeToFile:srtUrl.path atomically:YES encoding:NSUTF8StringEncoding error:nil];
坑三:字符串匹配无法覆盖所有字体
问题描述:硬编码的字体名列表无法覆盖所有可能出现的字体名。
原方案:
objc
NSArray *fontNamesToReplace = @[
@"微软雅黑", @"微软雅黑", @"SimHei", @"SimSun",
@"黑体", @"宋体", @"楷体", // ...
];
问题 :总有漏网之鱼,如 方正准圆简体、Noto Sans CJK SC 等。
改进方案:正则 + 字符串匹配兜底
正则覆盖任意字体名,字符串匹配处理边缘情况:
objc
// 正则:替换所有 {\fn任意字体名} → {\fnSource Han Sans CN}
// 兜底:字符串匹配常见字体名
modifiedText = [self fallbackReplaceFontNamesInText:modifiedText];
完整架构图
swift
┌─────────────────────────────────────────────────────────────────┐
│ iOS 26 字幕兼容架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ handleiOS26 │ │ updateSubtitleUrl │ │convertSubtitle│ │
│ │ SubtitleOn │ │ (内嵌拦截) │ │ (外挂处理) │ │
│ │ Playing │ │ │ │ │ │
│ └──────┬──────┘ └──────┬────────┘ └──────┬────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ @available(iOS 26.0, *) 守卫 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 禁用内嵌字幕 │ │ FFmpeg 提取 │ │ 字体名替换 │ │
│ │ (libass) │ │ SRT │ │ (正则+兜底) │ │
│ └──────────────┘ └──────┬───────┘ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ freetype 渲染 │ │
│ │ + 指定中文字体 │ │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘