iOS 26 libass字幕渲染问题兼容解决实践

31 阅读4分钟

背景

在 iOS 26 上,视频播放器使用的 libass 字幕渲染器遭遇了严重的兼容性问题。当字幕指定的字体在系统中找不到时,libass 的 CoreText 后端会尝试 fallback 到系统字体路径:

/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 字体,导致整个字幕渲染失败。


解决方案

方案概述

视频播放
    ↓
禁用内嵌字幕(避免 libass 走系统字体路径)
    ↓
提取内嵌字幕流为 SRT(通过 FFmpeg)
    ↓
SRT 无字体定义,通过 freetype 渲染器 + 指定中文字体显示
    ↓
同时对外挂 ASS 字幕做字体名替换(指向 CoreText 已注册的字体)

核心实现

1. 内嵌字幕提取

通过 FFmpeg 提取视频中的字幕流,转换为无字体定义的 SRT 格式:

// 使用 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字体名}
// 替换 Dialogue 行内的 {\fn任意字体名} 覆盖标签
NSRegularExpression *fnTagRegex = [NSRegularExpression
    regularExpressionWithPattern:@"\\{\\\\fn[^}\\\\]+"
    options:NSRegularExpressionCaseInsensitive
    error:&regexError];
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:&regexError];
// ...

SRT 字幕中的字体名出现在 HTML 标签中:

// 替换 <font face="任意内容">
NSRegularExpression *fontFaceRegex = [NSRegularExpression
    regularExpressionWithPattern:@"<font\\s+face\\s*=\\s*([\"'])[^\"']*\\1"
    options:NSRegularExpressionCaseInsensitive
    error:&regexError];

🔴 踩坑实录

坑一:VLC 索引与 FFmpeg 索引的映射错误

问题描述:用户选择中文内嵌字幕,但实际显示的是英文字幕。

根因分析

  • VLC 的 videoSubTitlesIndexes 数组索引 0 是 "Disable"
  • 内嵌字幕从索引 1 开始:索引 1 → 第一条字幕,索引 2 → 第二条字幕
  • FFmpeg 的字幕流索引从 0 开始:第一条字幕流是 0,第二条是 1

错误的映射:

用户选择 VLC 索引 1(第一条字幕)→ 错误地映射为 FFmpeg 索引 1 → 提取了第二条字幕

代码修复

// 修复前(错误)
int ffmpegTrackIndex = trackIndex + 1;

// 修复后(正确)
int ffmpegTrackIndex = (int)subtitleIndex - 1;

// VLC 索引 1 → FFmpeg 索引 0
// VLC 索引 2 → FFmpeg 索引 1

日志验证

[updateSubtitleUrl] iOS 26 拦截内嵌字幕: VLC subtitleIndex=1,
→ FFmpeg trackIndex=0  // 修复后正确映射到第一条字幕流

坑二:SRT 字幕的 <font> 标签问题

问题描述:内嵌字幕提取为 SRT 后,部分 SRT 仍无法显示中文。

日志分析

[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"> 标签:

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];

坑三:字符串匹配无法覆盖所有字体

问题描述:硬编码的字体名列表无法覆盖所有可能出现的字体名。

原方案

NSArray *fontNamesToReplace = @[
    @"微软雅黑", @"微软雅黑", @"SimHei", @"SimSun",
    @"黑体", @"宋体", @"楷体", // ...
];

问题:总有漏网之鱼,如 方正准圆简体Noto Sans CJK SC 等。

改进方案:正则 + 字符串匹配兜底

正则覆盖任意字体名,字符串匹配处理边缘情况:

// 正则:替换所有 {\fn任意字体名} → {\fnSource Han Sans CN}
// 兜底:字符串匹配常见字体名
modifiedText = [self fallbackReplaceFontNamesInText:modifiedText];

完整架构图

┌─────────────────────────────────────────────────────────────────┐
                    iOS 26 字幕兼容架构                            
├─────────────────────────────────────────────────────────────────┤
                                                                 
  ┌─────────────┐     ┌─────────────┐     ┌─────────────┐        
   handleiOS26       updateSubtitleUrl  convertSubtitle    
   SubtitleOn          (内嵌拦截)          (外挂处理)         
     Playing                                               
  └──────┬──────┘     └──────┬────────┘  └──────┬────────┘       
                                                              
                                                              
  ┌──────────────────────────────────────────────────────────┐   
                @available(iOS 26.0, *) 守卫                    
  └──────────────────────────────────────────────────────────┘   
                                                              
                                                              
  ┌──────────────┐   ┌──────────────┐   ┌──────────────┐         
   禁用内嵌字幕       FFmpeg 提取       字体名替换             
   (libass)         SRT              (正则+兜底)            
  └──────────────┘   └──────┬───────┘   └──────────────┘         
                                                                
                                                                
                    ┌──────────────┐                             
                     freetype 渲染                              
                     + 指定中文字体                              
                    └──────────────┘                             
                                                                 
└─────────────────────────────────────────────────────────────────┘

参考资料