前言 前幾天 Leko 輾轉偶然知道 Jellyfin 的線上播放器的字幕渲染居然是用 SubtitlesOctopus (目前瀏覽器唯一可用的 libass 相容 ASS 字幕渲染),真是驚為天人。 Leko 馬上架了一個來用,還因此不辭辛勞手動標記了動畫庫內六千多部動畫的 metadata (花了我整整三天,要得腱鞘炎了XDD)
一開始播放影音的時候會發現,奇怪我的中文字幕怎麼都變豆腐?
原來是因為他的 SubtitlesOctopus 預設沒有載入中文字體。
在官方的 Github Issues 內就有解法了 (載入 NotoCJK),但 Leko 覺得還不夠 – 既然都可以讓他載入 NotoCJK 了,那我應該也可以讓他跟一般播放器一樣按需載入需要的字體才對。
沒想到居然滿容易的,不到半小時就解決了,在這裡紀錄一下解法。
2021/6/9 更新: 本篇僅適用於 JellyFin 1.6.4 與其之前版本
前置
字體庫 (推薦 超级字体整合包 X)
JellyFin (假設使用 Docker 架設)
Windows 環境 (僅在設置時需要)
FontLoaderSub (用來產生字體索引)
node.js (小腳本我用 Node.js 撰寫,因為很簡單,有能力的讀者可以用其他程式語言實現,或是小小修改後在瀏覽器內執行。)
內文 1. 掛載字幕庫 首先我們將字體庫 bind 到 Jellyfin 內的 /jellyfin/jellyfin-web/libraries/fonts/Pack
。
2. 產生字幕索引 我們需要一個 {字幕名稱: 字幕檔案名稱}
的 JSON,用來配合接下來步驟解析的 ASS 字幕字體。
首先在字幕庫的根目錄放入 FontLoaderSub 並執行,讓它解析完後會產生一個 fc-subs.db
,此即字幕索引
觀察可以得知它是一個 UTF-16LE 的文字檔案,格式是
1 2 3 4 5 6 7 檔案相對路徑 t:字幕格式 v:字幕版本 字幕名稱 1 字幕名稱 2 ... 空白行
的循環,我們可以寫一個簡單的腳本來把它變成我們需要的 JSON。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 const fs = require ("fs" )const SUBTITLE_DATABASE = "fc-subs.db" const OUTPUT = "font.json" const db = fs.readFileSync (SUBTITLE_DATABASE , 'utf16le' ).split ("\n" ).map (x => x.slice (0 , -1 ))const result = {};let stage = 0 ;let fontPath = null ;for (const line of db) { switch (stage) { case 0 : if (!line.startsWith ("超级字体整合包X" )) { stage = -1 ; fontPath = null ; } else { stage += 1 ; fontPath = line.slice ("超级字体整合包X\\" .length ) } continue ; case 1 : case 2 : stage += 1 ; continue ; case 3 : if (line.length ) { result[line] = fontPath; } else { stage = 0 ; } continue ; case -1 : if (line.length !== 0 ) continue ; stage = 0 ; continue ; } } fs.writeFile (OUTPUT , JSON .stringify (result), 'utf8' , function (err ) { if (err) { console .log ("An error occured while writing JSON Object to File." ); return console .log (err); } console .log ("JSON file has been saved." ); });
執行後得到我想要的 font.json
,會在第三步驟用到。
3. Plugin.js Jellyfin 內的 SubtitlesOctopus 主要是在 /jellyfin/jellyfin-web/plugins/htmlVideoPlayer/plugins.js
內 Init 的,我們將他複製出來並 Prettify。
觀察到 renderSsaAss()
內 options
在初始化時有一個 fonts
欄位可以填入 []FontURL
,其預設值是
1 2 3 attachments.map (function (i ) { return apiClient.getUrl (i.DeliveryUrl ); })
簡單的觀察就可以得知我們可以用 getTextTrackUrl(track, item)
來拿到 ASS 字幕的 URL。
因此我們寫幾個簡單的函式來解析 ASS 字幕需要的字體並按需把他加入那個 fonts
欄位。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 async function _lekoGetFontNames (url ) { const rawAssSubtitle = await (await fetch (url)).text (); let isReachedStyleLine = false let formatIndex = null ; let result = new Set (); for (const lineRaw of rawAssSubtitle.split ("\n" )) { const line = lineRaw.trim (); if (!isReachedStyleLine) { if (line === "[V4+ Styles]" ) { isReachedStyleLine = true ; } continue ; } else { if (line.startsWith ("Format:" )) { formatIndex = line.slice ("Format:" .length ).split ("," ).findIndex (x => x.includes ("Fontname" )); } else if (line.startsWith ("Style:" )) { const fontName = line.slice ("Style:" .length ).split ("," )[formatIndex].trim () result.add (fontName) } else if (line.startsWith ("[" )) { break ; } } } return [...result]; } var _lekoFontListP = fetch (`${appRouter.baseUrl()} /libraries/fonts/font.json` ).then (x => x.json ())async function _lekoFontnameToFilename (fontname ) { const fontList = await _lekoFontListP; return fontList[fontname]; } function _lekoGetFontUrl (filename, isBasicFont=false ){ if (isBasicFont) { return `${appRouter.baseUrl()} /libraries/fonts/${filename} ` } else { return `${appRouter.baseUrl()} /libraries/fonts/Pack/字体/超级字体整合包X/${filename} ` } }
其中 _lekoFontListP
的 font.json
路徑可以自己調整 (我是把它掛到 /jellyfin/jellyfin-web/libraries/fonts/font.json
)
_lekoGetFontUrl
內的 else
區塊可以看到 fonts/Pack...
,那個就是我們第一步驟掛進去的字體。
然後我們應用這些函式 – 在 renderSsaAss()
初始化 var options
前加入:
1 2 3 4 5 6 let _lekoFonts = await _lekoGetFontNames (getTextTrackUrl (track, item));for (let i = 0 ; i < _lekoFonts.length ; i++) { _lekoFonts[i] = await _lekoFontnameToFilename (_lekoFonts[i]); if (_lekoFonts[i]) _lekoFonts[i] = _lekoGetFontUrl (_lekoFonts[i]); } _lekoFonts = _lekoFonts.filter (x => x);
然後 options
內 fonts
改為
1 2 3 fonts : attachments.map (function (i ) { return apiClient.getUrl (i.DeliveryUrl ); }).concat (_lekoFonts)
也可以在此時順便加入一些預設的 CJK 字體。
最後我們將 function renderSsaAss
改為 async
,並在有調用它的函數 await
它並適當的把一些函式改為 async
就大功告成。
最後的 plugin.js
供參。
清除瀏覽器快取後應該就能看到 JellyFin 動態按需載入所需字體了,yay!