Skip to content

從影片第一格圖片生成器開發學到的事

事情源於如下圖的情況,每篇文章中的影片在尚未播放前是一片空白:

雖說可以用 <video preload="metadata"> 僅預載影片元數據以顯示影片的圖片,但實測後發現就算僅載入 metadata,單個影片大約也要載入約 1MB 的資料量,甚至有單個影片 5MB 的情況,於是打算開發一個自動生成影片第一張圖片的功能,只要在每一個影片網址的路徑開頭加上 /ff 就會自動生成第一張圖片,例如:

取得影片的第一格並轉存成圖片,可以用 ffmpeg -i <影片> -q:v 1 -vframes 1 <輸出檔名> 指令來完成,其中 -q:v 1 是設定品質 1~31 越小越佳,檔案也越大。

整個實作的結果大致如下:

  1. 使用 Cloudflare Worker 處理網址路徑開頭為 /ff 的請求。
  2. Cloudflare Worker 檢查是否有快取,有的話直接回傳快取的圖片;沒有的話進入下一步。
  3. 將請求網址的中的 /ff 去除(即為影片網址),轉發到 AWS Lambda。
  4. AWS Lambda 收到影片的網址,檢查 S3 是否有先前處理過相同影片所產生的圖片,有的話直接回傳; 沒有的話下載影片,執行 ffmpeg 回傳圖片同時將其上傳至 S3。
  5. Cloudflare Worker 將圖片回傳,並將請求快取下來,之後如有相同的請求不再進 AWS Lambda。

Cloudflare Worker 無法處理太複雜的工作

本來打算完全使用 Cloudflare Worker 完成整個需求,實作過程中才發現 CF Worker 僅能處理一些簡單的工作,參考官方的 Worker Examples 所能做的事情大概是網路請求的轉發、代理、轉址...等,Worker 的整包部署檔案上限為 1MB,意即不可能在 Worker 上跑 ffmpeg,因為光是 ffmpeg 及 ffprobe 兩個執行檔加起來就 100MB。

本來還不死心的嘗試了 ffmpeg.wasm,它可以在瀏覽器上跑 ffmpeg,套件本身檔案很小,是在運行階段用 Web Worker 下載 ffmpeg.wasm 大檔案,但 Cloudflare Worker 的運行環境是專為邊緣運算設計 JS 執行環境,不是所有的 Web API 都能使用,而 ffmpeg.wasm 使用到 CF Worker 不支援的 Web Worker API,在執行到 load 調用 Worker 時就會報錯。

所以最後不得不將影片處理的部分交給 AWS Lambda 來處理,CF Worker 只負責轉發請求及快取的部分。

AWS Lambda 檔案太大需要使用 S3 的方式來部署

AWS Lambda 相對於 Cloudflare Worker 就自由多了,想執行什麼都不是問題,所以我將 ffmpeg 及 ffprobe 都一起 bundle 進 Lambda 的 .zip 檔案中,順帶一提我用是 FFmpeg Static Builds

過往我使用 Lambda 都是使用 .zip 檔案部署,因為以前都是純腳本的在使用沒有遇過檔案大小問題,這次因為 ffmpeg 的關係撞到了 50MB 的限制,如果超過這個大小就必須改用 S3 的方式來部署。

sh
aws s3 cp lambda_function.zip s3://your-bucket/lambda_function.zip
aws lambda update-function-code --function-name YourFunctionName \
  --s3-bucket your-bucket \
  --s3-key lambda_function.zip \
  --region ap-northeast-1

HTTP Range header

ffmpeg 的 -i 是可以使用 URL 作為 input,好處是不用另外寫下載程式,可以直接將影片網址作為參數使用。 但我使用的 ffmpeg 版本在使用 URL 作為 input 時會產生 Failed to resolve hostname 的錯誤,原因如下:

A limitation of statically linking glibc is the loss of DNS resolution. Installing nscd through your package manager will fix this.

出自於作者的 README,需要安裝 nscd 來解決 DNS 解析問題。 我並不打算解決這個問題,因為不想讓 ffmpeg 幫我下載整個檔案,接下來會說為什麼。

功能完成後進行實測,剛好測試的第一個影片沒有問題,但第二個測試影片執行 Lambda 最後 timeout 了,發生 timeout 沒有很意外,畢竟只為了取第一格圖片就下載整個影片,這樣的做法實在太暴力。

我對於影片檔案本身沒有研究,猜想有沒有辦法使用影片的前幾 MB 來作為 ffmpeg 的 input,所以打算先實驗下載某個檔案的一部分來試試看。

HTTP Range header,可用來指定下載檔案的範圍,例如 Range: bytes=0-1023 會下載檔案的前 1024 bytes,這是我第一次使用到的 header,對應到 curl 指令可以適用 -r 選項:

sh
# 下載影片的前 1MB
curl -r 0-1048575 -O <video_url>
# 效果等同
curl -H "Range: bytes=0-1048575" -O <video_url>

嘗試先在本地端預覽影片確實是可以播放的,只不過會停在第 n 秒,畢竟只有下載了前 1MB 的影片,執行 ffmpeg -i <video> -q:v 1 -vframes 1 <output> 也確實成功生成了第一格圖片。

所以我將 AWS Lambda 程式當中影片下載的部分從原先的「下載整個影片檔案」改成「下載影片的前 1MB」,此時 Lambda 的執行時間大幅縮短,不再 timeout 了。

ffmpege-movflags 選項

乍看之下一切都很順利,但在實際使用時有一個影片無法順利產生第一格圖片的問題,將該影片的 1MB 部分下載到本地做測試,這個影片不像其他影片可以播放,使用 ffmpeg 產生圖片時得到 moov atom not found 錯誤。

關於 moov atom 的解釋,以下出自於 Google Code Archive

In H.264-based video formats (mp4, m4v) the metadata is called a "moov atom". The moov atom is a part of the file that holds the index information for the whole file.

Many encoding software programs such as FFmpeg will insert this moov atom information at the end of the video file. This is bad. The moov atom needs to be located at the beginning of the file, or else the entire file will have to be downloaded before it begins playing.

大概理解為是一個影片的 metadata,這個資訊存放在影片的越前面越好,否則整個影片必須完整下載才能播放。 也就是說我下載的 1MB 影片,它的 moov atom 並不在檔案開頭,所以是一個連開頭都無法播放的影片。

所以需借助 ffmpeg 的 -movflags 選項:

sh
ffmpeg -i <video> -c copy -movflags faststart <output>

將「完整影片檔案」的 moov atom 移到檔案開頭,再將完成的影片檔案重新上傳。 對於這個影片再取一次前 1MB 部分確實就能播放了,並且也能順利生成第一格圖片。

Written By
YI FENG XIE
YI FENG XIE

Creative Problem Solver