這次來分享一下最近的靈感和成果吧。
平時用 OBS 開遊戲實況的時候,常常遇上觀眾在聊天室問:「豬腳,可以問一下現在播的歌名是什麼嗎?」
每次問、每次回答,這樣一直下去也不是辦法。要是安裝了其他 YouTube 相關的 OBS 外掛,似乎又沒有辦法符合我想要的簡單跑馬燈風格,而且也不能隨心所欲地修改它。
既然要把目前播放中的 YouTube 影片標題放到跑馬燈裡面,那一定要有辦法取得網頁裡面的內容。然而,由於瀏覽器對 JavaScript 的限制,導致要利用 JavaScript 把影片標題寫入 OBS 裡跑馬燈指定的 txt 檔案會變成一件很麻煩的事情,於是我把腦筋動到了 HTTP server 和 Golang 上面。
做法是這樣的:首先我們利用 Golang 寫出來的程式(當然你要用 Python 或其他語言也可以)建立一個 HTTP server,這個程式負責用來接應、刷新 OBS 跑馬燈指定的 txt 檔案;至於網頁瀏覽器那端,則是使用 Tampermonkey 搭配 JavaScript 來抓取歌曲名稱,並且在需要更新跑馬燈的時候,將歌曲名稱送到 Golang 那邊建立起來的 server。
先來談談用 JavaScript 抓影片名稱。如果是對於一個平常有寫爬蟲經驗的人來說,對於 selector 已經有足夠的理解,那想利用 JavaScript 取得網頁特定元素內容,自然是很簡單的事情。只要對影片標題區塊右鍵、檢測元素:
一點開就可以抓到了,依照 tag[.class][#id] 的規則,它的 selector 寫作:
yt-formatted-string.style-scope.ytd-video-primary-info-renderer
後來我研究了一下,發現這麼做會出問題,因為符合這個 selector 的元素一共有三個,如果要取得真正的標題,就只能固定抓其中的第二個元素:
document.querySelectorAll("yt-formatted-string.style-scope.ytd-video-primary-info-renderer")[1].innerText;
寫爬蟲最擔心的事情就是 query 選出來的東西不唯一,要是遇上了例外情況,抓到的資料就可能不正確。所以為了保險起見,我把它往上追溯,找到它的 parentNode:
h1.title.style-scope.ytd-video-primary-info-renderer
利用 F12 打開 console,抓抓看它的 innerText:
document.querySelector("h1.title.style-scope.ytd-video-primary-info-renderer").innerText;
確定可以透過這個 selector 取得標題內容以後,就可以動手寫 JavaScript 的 code 了。在這之前我們需要在瀏覽器上安裝 Tampermonkey 這個擴充功能。
先來解釋一下這段 JavaScript。其實起初我測試的時候,發現 YouTube 網頁的運作機制比較特別,當你從某一首歌切換到另一首歌的時候,網頁應該是只有把裡面的播放器等等元素動態切換成別首歌,而不是直接將整個網頁重載。也就是說,只有當你第一次點進該網頁的時候,才會觸發一次 document-end 的事件,之後切換歌曲都不會觸發。
這意味著,如果我 code 寫的內容設定在 document-end 的時候才捕捉影片名稱、送出影片名稱的話,我就只能夠抓到第一次播放的影片名稱,這個 code 之後就會起不了作用。
於是我設了一個解決方案:每 0.5 秒(500 毫秒)偵測一次影片標題,如果發現影片標題有變更的話,我就送出影片名稱。
所以,你會在我的 code 裡面看到 setInterval 的函數,它就是被我用來反覆確認歌曲有沒有切換,如果歌曲名稱變了,那就把這個歌曲名稱資訊重抓一次,利用當初剛載入網頁,觸發 document-end 的時候建立的 iframe,把歌曲名稱送去我用 Golang 建的 server。
這時候一樣開著 F12 的 console 頁面,去 YouTube 隨機點個幾首歌,觀察一下 console 上面的文字,會發現我寫的腳本成功透過 console.log(encodeURIComponent(nowTitle)); 顯示出來了:
當然,上面這些只不過是整個外掛的一半,最重要的還是負責接收影片名稱,以及將名稱寫入文字檔的 server 端。
一方面是因為自己最近剛學 Golang,一方面也是因為用 Golang 建 HTTP server 還蠻方便的,所以就選擇它了。
原理很簡單。我們在本地端隨便抓一個 port 來用,只要 server 這邊收到了就寫入 title.txt。這份 code 使用的是 port 8763,你想要把它改成其他的 port 也可以(只要沒被其他程式佔用),但請注意,若想修改的話,JavaScript 指定的送達地點也要跟著改成一樣的。
這樣的 code 不到 100 行就可以解決了,相信光是看字面也能看得懂程式的運作。
之後,只要把 YouTubeListener.exe 開著,接著再打開 YouTube,隨便點一首歌,就會發現影片名稱會被送到那程式上面,並且由那程式把名字寫進同目錄下的 title.txt 裡面了。
所以,我們這時候打開 OBS,在場景裡面新增一個文字(GDI+),接著右鍵 > 濾鏡,新增捲動濾鏡,再憑自己喜好設定寬度,再右鍵 > 屬性,把檔案來源設成 title.txt,由於 OBS 本身對於跑馬燈的內容也是即時更新的,所以隨著 YouTube 播放的歌曲而變動的跑馬燈就完成了:
這裡有些小細節要強調。
第一點是,JavaScript 裡面送出的歌曲名稱是用 encodeURIComponent() 函式(URL 編碼)轉換過的,這是用了避免歌曲名稱出現「&」的時候,網址的參數會被切開來的情形。
第二點是,你會在那段 JavaScript 腳本跟 Golang 的 code 發現裡面設了一組 accessKey,這是為了安全考量。如果今天有個外人試圖偽造連線請求,想要惡意地把一些假資訊送到你的電腦裡面,害你的實況台出現「某些文字」,那你就可以使用這個 accessKey 來避免,因為只有你才知道這個 accessKey 的內容。雖然把 Golang 的 ListenAndServe 設在 127.0.0.1 已經夠安全了,但保險起見還是寫一下。
舉個例子,我在 Golang 裡面寫的是這樣:先讀取 accessKey.upk 檔案裡面的內容(並且將之設為 key),接著建立 HTTP server。
這個同樣的 key 我也設在 JavaScript 的腳本裡面(兩串是一樣的),例如我預設的 key 內容是「952d9c43(太長了,中間省略)3ad20b0e」,那我在網頁抓到新歌曲名稱的時候,就會把剛開始在網頁裡面塞好的 iframe 網址跳轉到這個地方。
Golang 這邊建立的 HTTP server 在收到這次連線請求的時候,就會去對照這組 key 是不是跟在 accessKey.upk 一樣,確認一樣的話才把名稱寫進去。
如果你有實驗精神的話,可以試試看自己用瀏覽器訪問這個網址,看看會不會成功送出一筆資料過去,這樣它的原理也就會變得很好理解了。
這個 key 當然你也可以自己定義,總之只要確保 accessKey.upk 的內容跟 JavaScript 腳本裡面設的 accessKey 相同,那就可以了。
以上是這次利用 Golang、JavaScript 聯合起來做的跑馬燈外掛原理。如果把兩份 code 稍微改造一下,甚至還可以把 YouTube 影片網頁裡面的瀏覽人次等其他資訊一起寫到文件檔裡面,並且顯示在實況畫面呢。
感謝大家把滑鼠滾輪滾到這,我是豬腳,我們下次再會。