最近帮朋友搞了个在线视频平台,叫Club365,主要面向海外华人,需求很简单:视频播放要有中文字幕,但真干起来才发现,这事比想象中复杂多了,字幕同步、编码转换、实时流处理……每个环节都可能出幺蛾子,今天就用我实际写的Golang代码,聊聊怎么搞定“在线视频Club365中文字幕”这个需求。
为什么选Golang?三个字:快、稳、省
一开始想过用Python,但视频转码太吃CPU,Python那点并发能力根本扛不住,后来换成Go,协程+原生并发,一下子就舒坦了,举个例子,处理字幕文件解析时,我开了10个goroutine同时干活,CPU跑满但内存占用才几十MB。
// 实际代码片段:并发解析字幕文件
func parseSubtitles(files []string) []Subtitle {
ch := make(chan Subtitle, len(files))
for _, f := range files {
go func(path string) {
sub, err := parseSRT(path)
if err != nil {
log.Printf("解析字幕文件 %s 失败: %v", path, err)
return
}
ch <- sub
}(f)
}
var subs []Subtitle
for i := 0; i < len(files); i++ {
sub := <-ch
subs = append(subs, sub)
}
return subs
}
字幕处理最头疼的三件事
编码混乱:GBK、UTF-8、BOM混在一起
用户上传的字幕文件,编码五花八门,有的带BOM头,有的繁体UTF-8,有的干脆是GB2312,我写了个自动检测函数,用Golang的golang.org/x/text/encoding包做转换。
| 编码类型 | 检测方法 | 处理方式 |
|---|---|---|
| UTF-8 BOM | 前3字节EF BB BF | 去掉BOM直接解码 |
| UTF-8无BOM | 字节序列验证 | 直接解码 |
| GBK | 字节范围判断 | 转为UTF-8 |
| 繁体Big5 | 字节特征检测 | 转为UTF-8再繁转简 |
时间轴对齐:差100毫秒都是灾难
视频流和字幕流必须精确同步,我用FFmpeg的Golang绑定库github.com/asticode/go-astits,解析视频的PTS(显示时间戳),再和字幕的起止时间做映射,有个坑:不同编码器生成的视频,时间基准可能不一样,有的是90000,有的是1000,得统一转成毫秒级。
实时流字幕:HLS+DASH双协议
Club365要求支持直播场景,字幕得跟着视频流走,我用了HLS的fMP4分段,把字幕封装成独立的TS轨,和视频一起打包,Golang这边用github.com/grafov/m3u8库生成播放列表,字幕轨的BANDWIDTH参数设得比视频低很多,确保老电视也能播。
// 生成带字幕轨的HLS播放列表
func generateHLSWithSubtitles(videoURL, subtitleURL string) string {
playlist := `#EXTM3U
#EXT-X-VERSION:6
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",LANGUAGE="zh",NAME="中文",DEFAULT=YES,AUTOSELECT=YES,URI="` + subtitleURL + `"
#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS="avc1.64001e,mp4a.40.2",SUBTITLES="subs"
` + videoURL
return playlist
}
中文字幕渲染:从SRT到ASS再到WebVTT
用户上传的.SRT文件最多,但浏览器原生支持的是WebVTT,我写了个转换器,用正则表达式解析SRT的序号、时间码、文本,再拼接成VTT格式,这里有个细节:SRT的时间码格式是00:00:01,234,VTT要求00:00:01.234,就一个逗号变点号,但忘记转就会整段字幕不显示。
func srtToVTT(srtContent string) string {
re := regexp.MustCompile(`(\d{2}:\d{2}:\d{2}),(\d{3})`)
vttContent := re.ReplaceAllString(srtContent, "${1}.${2}")
vttContent = "WEBVTT\n\n" + vttContent
return vttContent
}
ASS字幕的高级特性
Club365有些原创视频用ASS字幕,带特效和彩色字,Go处理ASS很麻烦,我干脆调用了FFmpeg的子进程,把ASS硬编码到视频里,虽然增加了转码时间,但效果确实好,观众能看到渐变色的片头字幕。
缓存策略:别再让用户等加载了
视频字幕文件可能很大,我用了两级缓存,第一级是内存LRU缓存,用github.com/hashicorp/golang-lru实现,存最近1小时访问的字幕文件,第二级是Redis,存全量字幕元数据,热点数据命中率能到95%,用户切换字幕基本秒开。

cache, _ := lru.New2Q(1000) // 存1000个字幕对象
func getSubtitleCached(videoID string) (*Subtitle, bool) {
if sub, ok := cache.Get(videoID); ok {
return sub.(*Subtitle), true
}
// 从数据库加载
sub := loadFromDB(videoID)
cache.Add(videoID, sub)
return sub, false
}
部署时的坑:中文字体不能少
用Golang渲染字幕时,如果系统没有中文字体,会显示一堆方框,我的Docker镜像里必须安装fonts-wqy-microhei这个包。FFmpeg的滤镜需要指定字体路径,我写了个配置函数自动检测系统字体。
func getChineseFont() string {
fontPaths := []string{
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
}
for _, path := range fontPaths {
if _, err := os.Stat(path); err == nil {
return path
}
}
return "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" // 退而求其次
}
压力测试:500人同时看视频会怎样?
用wrk工具测了一下,我的Golang服务能扛住500并发连接,字幕请求响应时间平均15ms,但有一个瓶颈:FFmpeg转码是单进程的,并发高了会排队,后来改成用gocv库(OpenCV的Go绑定)做软解,但效果不如FFmpeg,最后折中方案:字幕转码用异步任务队列,用户提交后轮询结果。
踩过的其他小坑
- 时间戳溢出:32位int存毫秒时间戳会溢出,用int64
- Unicode转义:从数据库读出的\uXXXX字符串要转成中文
- 字幕换行:SRT用空行分隔字幕块,解析时别吞掉
- 流媒体服务器选择:Nginx-RTMP模块比Go自己的rtmp库稳定
现在Club365的在线视频中文字幕系统已经跑了大半年,用户反馈字幕延迟控制在200毫秒以内,基本没出过同步问题,虽然代码里还有几百行TODO注释没处理,但已经能用了,Go这门语言确实适合干这种实时的脏活累活,写完就编译成二进制扔服务器上跑,省心。
本文来自作者[kyadmin]投稿,不代表be365立场,如若转载,请注明出处:http://www.hljbesthome.com/tiyu/311.html
评论列表(4条)
我是be365的签约作者“kyadmin”!
希望本篇文章《从零开始搭建在线视频Club365中文字幕系统—我用Golang踩过的那些坑》能对你有所帮助!
本站[be365]内容主要涵盖:be365,be365网站,be365网址,ball365体育,Be365排名
本文概览:最近帮朋友搞了个在线视频平台,叫Club365,主要面向海外华人,需求很简单:视频播放要有中文字幕,但真干起来才发现,这事比想象中复杂多...