Editing and Sharing Home Videos with MediaGoblin

Michael Lynch

使用 MediaGoblin 編輯與分享家庭影片

原文由 Michael Lynch 于 發布,訂閱此部落格

目標

本教學將示範如何把數位化的影片擷取檔剪輯成較小的片段,並發布到你自行架設、受密碼保護的 MediaGoblin 伺服器上。你將使用免費的 Heroku dyno,因此執行這個私人媒體伺服器唯一的持續性成本,就是 Google Cloud Storage 的儲存費用,每 GB 只要 2.3 美分。

我就是用這個流程來剪輯並分享家人的家庭影片,每月成本只要 0.77 美元。想了解詳細的背景故事,請參考部落格文章「My Eight-Year Quest to Digitize 45 Videotapes」。這個流程適用於任何包含大量子片段、需要擷取並分享的影片檔案。

前置需求

  • Python 3
  • virtualenv
  • Git
  • Docker
  • ffmpeg
  • 影片播放器
    • 最好選擇有剪輯時間軸功能的播放器,例如 Adobe Premiere Elements 或 OpenShot。
  • 免費的 Heroku 帳號
  • Google Cloud Platform 帳號(已啟用付款功能)

示範影片

以下是在本教學中作為範例使用的影片,這是一支來自 1950 年代德州家庭的公眾領域家庭影片:

本教學用來作為原始素材進行剪輯與分享的公眾領域影片(請注意,這支影片沒有音訊)

如要下載這支影片,請執行以下指令:

VIDEO_URL="https://github.com/mtlynch/free-usage-videos/blob/master/texas-farm-family-1952/TexasFar1952.mp4?raw=true"RAW_VIDEOS_DIR="${HOME}/videos-raw"RAW_VIDEO="${RAW_VIDEOS_DIR}/TexasFar1952.mp4"mkdir -p "$RAW_VIDEOS_DIR"wget "$VIDEO_URL" -O "$RAW_VIDEO"

標註

影片檔案是由一連串的影格所組成。要找出影片檔案中片段的起點與終點,你需要一種方法來對應播放位置所對應的影格編號。為此,你可以使用 ffmpeg 製作一份帶有影格標註的原始影片副本:

ANNOTATED_VIDEO=TexasFar1952-annotated.mp4ffmpeg \  -i "$RAW_VIDEO" \  -vf "drawtext=fontfile=Arial.ttf: text='%{frame_num}': start_number=1: x=(w-tw)/2: y=h-(2*lh): fontcolor=black: fontsize=20: box=1: boxcolor=white: boxborderw=5" \  -c:a copy \  "$ANNOTATED_VIDEO"

這樣會產生一份看起來像這樣的影片副本:

加上影格數浮水印的公眾領域影片

建立目錄

接下來,該為錄影帶上的素材建立目錄了。你將瀏覽影片,辨識出各個場景、出現的人物以及發生的事件。

打開你慣用的影片播放器。我認為 Adobe Premiere Elements 最適合這項工作,但你也可以試試免費的 OpenShot 編輯器。技術上任何影片播放器都能用,但如果你選擇支援逐格播放和可縮放時間軸的播放器,會節省很多時間。

接著,建立一份試算表。你可以從我的試算表開始。我會在下面說明各個欄位:

我的 Google 試算表截圖

請用與我的試算表相同格式的試算表來整理所有詮釋資料。

  • tape_id:這是影片檔案的檔名(不含副檔名)。
  • tape_shortname:這是你想用來識別來自此原始檔案的短名稱,可以與 tape_id 相同。
  • tape_friendly_name:當 MediaGoblin 顯示此片段來源錄影帶時,會使用這個欄位。如果有比檔案名稱更具描述性的名稱,請填在這裡。
  • scene_start_frame:片段開始的第一個影格編號。
    • 以德州農場的範例影片來說,開頭有幾格是空白的,所以第一個片段從第 28 格開始。
  • title:此場景的標題。這將作為輸出檔名以及 MediaGoblin 縮圖中的標題。
    • 若要讓 render_scenes 忽略某段素材,請將標題命名為 junk。
  • description:(選填)描述此場景中發生的內容。這會顯示在 MediaGoblin 觀看頁面中影片下方的說明。
  • hashtags:在 description 欄之後,你可以建立以井字號開頭的標籤欄位。任何在標籤欄位中填有 y(或任何值)的片段,都會在 MediaGoblin 中被加上該標籤。
    • 例如:我的試算表中第 9 列在 #adam 和 #archie 欄位下填了 y。當 publish_to_mediagoblin 將片段加入 MediaGoblin 時,就會為該片段加上 adam 和 archie 標籤。
    • 這些欄位的用途是,如果你有想在影片中標記的家人或寵物,可以為他們新增 hashtag 欄位,並在他們出現的每個片段對應欄位中填入 y。
  • date:片段錄製的日期。
    • 可以是以下其中一種格式:
      • YYYY-MM-DD
      • YYYY-MM
      • YYYY
  • other_tags:以逗號分隔的清單,加入你想套用到該片段的其他標籤。

當你執行下方的 render_scenes 時,它會使用以下命名規則將影片切成較短的片段:

  • [tape_shortname] - [clip index] - [title].mp4

例如:

  • Texas Family - 1952 - 05 - Archie in the Corn Fields.mp4

建立 scenes YAML 檔

既然已經整理好片段目錄,你就可以用我的指令碼將 CSV 轉換成 YAML 檔,讓其他指令碼知道如何分割影片並匯入至 MediaGoblin。

git clone https://github.com/mtlynch/process-home-videos.gitcd process-home-videosmkdir -p ./venvvirtualenv --python python3 ./venv. venv/bin/activatepip install --requirement requirements.txt

接下來,你需要建立一個設定檔。請將 process-home-videos 中的 config.example.yaml 複製為 config.yaml,並根據檔案中的說明,針對你的片段進行編輯。

cp config.example.yaml config.yaml

現在,建立 CSV 與 YAML 詮釋資料檔案的路徑:

MEDIAGOBLIN_METADATA="${HOME}/mediagoblin-meta"mkdir -p "$MEDIAGOBLIN_METADATA"# Path to the CSV you created.SCENES_CSV="${MEDIAGOBLIN_METADATA}/scenes.csv"# Path to output file to create.SCENES_YAML="${MEDIAGOBLIN_METADATA}/scenes.yaml"

如果你是跟著我的試算表範例操作,可以將它下載為 CSV:

CSV_URL="https://docs.google.com/spreadsheets/d/1kuamVFEYBrOI097IWBQ8sB0q37ZRACYe2o389Ag92zI/export?format=csv&id=1kuamVFEYBrOI097IWBQ8sB0q37ZRACYe2o389Ag92zI&gid=401061703"wget "$CSV_URL" -O "$SCENES_CSV"

執行下列指令碼,將你的 CSV 轉換為 YAML 檔:

app/csv_to_yaml.py \  --config config.yaml \  "$SCENES_CSV" \  > "$SCENES_YAML"

這會產生一個像這樣的 YAML 檔:

$ head -n 19 "$SCENES_YAML"- description: 'Abigail and Adam stand outside with Abigail primping Adam''s coat.    Adam is 12.    Recorded between June 1, 1952 and June 30, 1952.    Came from tape "Texas Farm Family - 1952," scene #01.'  duration_frames: 457  raw_source_filename: TexasFar1952.mp4  rendered_filename: Texas Family - 1952 - 01 - Abigail Primping Adam.mp4  tags:  - abigail  - adam  - best of  timecode_start: '0:00:00.934267'  title: Abigail Primping Adam
這算是我當初一個有點蠢的設計選擇。其實其他指令碼可以直接讀取 CSV,但我當時就是這樣做的。

分割影片片段

現在你已經準備好所有詮釋資料,該把大型影片檔切成一系列較小的片段了:

# Specify the directory where the script should write out the processed clips.PROCESSED_CLIPS_DIR="${HOME}/videos-processed"mkdir -p "$RAW_VIDEOS_DIR"app/render_scenes.py \  --metadata "$SCENES_YAML" \  --raw_videos_dir "$RAW_VIDEOS_DIR" \  --output_clips_dir "$PROCESSED_CLIPS_DIR"

當指令碼執行完畢,你應該會看到一個像這樣的資料夾:

$ ls "$PROCESSED_CLIPS_DIR"'Texas Family - 1952 - 01 - Abigail Primping Adam.mp4''Texas Family - 1952 - 02 - Abigail Standing on the Stairs.mp4''Texas Family - 1952 - 03 - Adam Plays with Cars on the Sidewalk.mp4''Texas Family - 1952 - 04 - Trudy Working at the Loom.mp4''Texas Family - 1952 - 05 - Archie in the Corn Fields.mp4''Texas Family - 1952 - 06 - Tending to the Cows, Bessie Nursing.mp4''Texas Family - 1952 - 07 - Giving Pigs Belly-Rubs.mp4''Texas Family - 1952 - 08 - Checking on the Chickens and Hens.mp4'

建立 MediaGoblin 檔案

處理好片段後,就可以準備上傳檔案到 MediaGoblin 了。你會先將影片和詮釋資料匯入本機上的 MediaGoblin 實例。這能讓 MediaGoblin 將你的檔案轉換為內部的資料庫與檔案結構,之後就能重複用於在雲端執行 MediaGoblin 實例。

首先,將你的環境資訊填入 MediaGoblin 的設定檔 mediagoblin.ini 中。

注意:為了區分主機上的路徑與 MediaGoblin Docker 容器內的路徑,我採用了 MG_ 前綴代表主機、MGC_ 前綴代表容器內路徑的命名慣例。
MG_CONFIG=$(mktemp)wget https://mtlynch.io/digitizing-home-videos-walkthrough/mediagoblin.ini \  -O "$MG_CONFIG"# Path to MediaGoblin's home directory within the container.MGC_HOME="/var/lib/mediagoblin"MGC_DB_PATH="${MGC_HOME}/mediagoblin.db"# Update the relevant lines in the config file.sed \  --in-place \  "s@.*sql_engine = .*@sql_engine = sqlite:///${MGC_DB_PATH}@" \  "$MG_CONFIG"

現在啟動容器:

# This is a path on the host machine that will receive the files the MediaGoblin# container generates.MG_SERVING_DIR=$(mktemp --directory)# These are paths within the MediaGoblin container.MGC_INPUT_VOLUME="/opt/input-videos"MGC_APP_DIR="/srv/mediagoblin.example.org/mediagoblin"# Name for MediaGoblin Docker container.MGC_NAME=mediaglindocker run \  --tty \  --detach \  --publish 6543:6543 \  --volume "${PROCESSED_CLIPS_DIR}:${MGC_INPUT_VOLUME}" \  --volume "${MG_CONFIG}:${MGC_APP_DIR}/mediagoblin_host.ini" \  --volume "${MG_SERVING_DIR}:${MGC_HOME}" \  --name "$MGC_NAME" \  mtlynch/mediagoblin

上面的指令會將主機上的幾個路徑掛載到 MediaGoblin 容器中,讓你在主機與 MediaGoblin 之間共享檔案:

  • PROCESSED_CLIPS_DIR 讓 MediaGoblin 能讀取你上面建立的片段。
  • MG_CONFIG 指定 MediaGoblin 要使用的設定檔。
  • MG_SERVING_DIR 用於接收 MediaGoblin 放置於本機檔案系統上的影片與縮圖檔案。

執行容器後,MediaGoblin 需要幾秒鐘才能啟動。你可以執行下列指令來監看進度

docker logs "$MGC_NAME" -f

當伺服器就緒時,你會看到類似以下的訊息:

Starting server in PID 26.Serving on http://0.0.0.0:6543

你可以前往 http://localhost:6543 來確認 MediaGoblin 伺服器是否已正常運作。

注意:如果你想登入,可以使用帳號 admin、密碼 admin,不過登入並非必要。

執行下列指令碼,將你的片段與詮釋資料匯入 MediaGoblin:

MEDIAGOBLIN_PUBLISH_HISTORY=$(mktemp)app/publish_to_mediagoblin.py \  "$MGC_INPUT_VOLUME" \  --metadata "$SCENES_YAML" \  --publish_history "$MEDIAGOBLIN_PUBLISH_HISTORY" \  --container_name "$MGC_NAME"

當指令碼執行完畢,你應該會在 http://localhost:6543 的 MediaGoblin 網頁介面上看到所有片段。

MediaGoblin 中顯示的所有片段

你應該會在 MediaGoblin 網頁介面上看到全部八個片段的縮圖。

如果你點擊影片,會發現無法在一般的影片播放器中觀看,因為 MediaGoblin 使用的網頁播放器過於老舊,不支援現代經過串流優化的影片格式。不過你仍然可以點擊「Original file」連結來觀看。

單支影片檢視畫面的截圖

在本機 MediaGoblin 實例中檢視單支影片的畫面

別擔心,我們會在下一個步驟修正這個問題,讓你之後能在內嵌播放器中正常觀看所有影片。

本機容器的任務已經完成,所以將它移除。

docker rm --force "$MGC_NAME"

而 Docker 接管了你共享資料夾的控制權。現在是時候把它取回來了。

sudo chown "${USER}:${USER}" "$MG_SERVING_DIR" --recursive

如果你檢視該目錄,應該會看到類似這樣的內容:

find "$MG_SERVING_DIR" -type f/tmp/tmp.bq7GAbNNNW/media/public/media_entries/8/Texas_Family_-_1952_-_08_-_Checking_on_the_Chickens_and_Hens.mp4/tmp/tmp.bq7GAbNNNW/media/public/media_entries/8/Texas_Family_-_1952_-_08_-_Checking_on_the_Chickens_and_Hens.thumbnail.jpg/tmp/tmp.bq7GAbNNNW/media/public/media_entries/5/Texas_Family_-_1952_-_05_-_Archie_in_the_Corn_Fields.mp4/tmp/tmp.bq7GAbNNNW/media/public/media_entries/5/Texas_Family_-_1952_-_05_-_Archie_in_the_Corn_Fields.thumbnail.jpg/tmp/tmp.bq7GAbNNNW/media/public/media_entries/1/Texas_Family_-_1952_-_01_-_Abigail_Primping_Adam.thumbnail.jpg/tmp/tmp.bq7GAbNNNW/media/public/media_entries/1/Texas_Family_-_1952_-_01_-_Abigail_Primping_Adam.mp4/tmp/tmp.bq7GAbNNNW/media/public/media_entries/4/Texas_Family_-_1952_-_04_-_Trudy_Working_at_the_Loom.thumbnail.jpg/tmp/tmp.bq7GAbNNNW/media/public/media_entries/4/Texas_Family_-_1952_-_04_-_Trudy_Working_at_the_Loom.mp4/tmp/tmp.bq7GAbNNNW/media/public/media_entries/7/Texas_Family_-_1952_-_07_-_Giving_Pigs_Belly-Rubs.mp4/tmp/tmp.bq7GAbNNNW/media/public/media_entries/7/Texas_Family_-_1952_-_07_-_Giving_Pigs_Belly-Rubs.thumbnail.jpg/tmp/tmp.bq7GAbNNNW/media/public/media_entries/6/Texas_Family_-_1952_-_06_-_Tending_to_the_Cows_Bessie_Nursing.thumbnail.jpg/tmp/tmp.bq7GAbNNNW/media/public/media_entries/6/Texas_Family_-_1952_-_06_-_Tending_to_the_Cows_Bessie_Nursing.mp4/tmp/tmp.bq7GAbNNNW/media/public/media_entries/3/Texas_Family_-_1952_-_03_-_Adam_Plays_with_Cars_on_the_Sidewalk.thumbnail.jpg/tmp/tmp.bq7GAbNNNW/media/public/media_entries/3/Texas_Family_-_1952_-_03_-_Adam_Plays_with_Cars_on_the_Sidewalk.mp4/tmp/tmp.bq7GAbNNNW/media/public/media_entries/2/Texas_Family_-_1952_-_02_-_Abigail_Standing_on_the_Stairs.mp4/tmp/tmp.bq7GAbNNNW/media/public/media_entries/2/Texas_Family_-_1952_-_02_-_Abigail_Standing_on_the_Stairs.thumbnail.jpg/tmp/tmp.bq7GAbNNNW/mediagoblin.db/tmp/tmp.bq7GAbNNNW/.cache/gstreamer-1.0/registry.x86_64.bin

設定 Google Cloud Storage

上一個步驟的目的是讓 MediaGoblin 為你所有的片段產生資料庫、檔案配置與影片縮圖。現在,該將 MediaGoblin 部署到雲端伺服器上,這樣你就能從任何地方存取,而不只是在本機上。

建立新的 Google Cloud Platform 專案(選用)

如果你想為 MediaGoblin 檔案建立一個乾淨的 Google Cloud Platform 專案,可以使用以下指令來建立。若你想使用現有的專案,請將 GCP_PROJECT_ID 設為你想使用的專案 ID,並跳過此節。

GCP_PROJECT_NAME="mediagoblin"RANDOM_SUFFIX=$(head /dev/urandom | tr -dc 'a-z0-9' | head -c 16 ; echo '')GCP_PROJECT_ID="${GCP_PROJECT_NAME}-${RANDOM_SUFFIX}"gcloud projects create \  "$GCP_PROJECT_ID" \  --name "$GCP_PROJECT_NAME" \  --set-as-default

你需要將新專案連結到一個付款帳號。若要列出你的付款帳號,請執行:

gcloud beta billing accounts list

接著將 BILLING_ACCOUNT 替換為你想用來支付這個儲存空間的帳號:

BILLING_ACCOUNT="0X0X0X-0X0X0X-0X0X0X" # Replace with account from the listgcloud beta billing projects link \  "$GCP_PROJECT_ID" \  --billing-account "$BILLING_ACCOUNT"

建立儲存空間

接著,為你的檔案建立一個 Google Cloud Storage 儲存空間。我建立了一個讓所有物件皆可公開存取的儲存空間,但我靠的是隨機產生的儲存空間名稱本身難以被猜中來確保安全。

24 個字元的隨機後綴代表有 3624 = 2.24 x 1037 種可能的組合。換句話說,攻擊者需要猜測約 1037 次儲存空間名稱才有可能找到你的。這相當於猜測一把 128 位元的私鑰。
RANDOM_SUFFIX=$(head /dev/urandom | tr -dc 'a-z0-9' | head -c 24 ; echo '')BUCKET_NAME="mediagoblin-${RANDOM_SUFFIX}"# All the videos need to be public for users to access them directly from the# browser, but we're adding enough entropy to the bucket name to make it# infeasible for unauthorized users to access the files by guessing or# enumerating URLs.UNIFORM_ACCESS="on"# Bucket storage properties. You can adjust these depending on your preferences.STORAGE_CLASS="Standard"BUCKET_LOCATION="US-EAST1"# Create the GCS bucket.gsutil mb \  -p "$GCP_PROJECT_ID" \  -c "$STORAGE_CLASS" \  -l "$BUCKET_LOCATION" \  -b "$UNIFORM_ACCESS" \  "gs://${BUCKET_NAME}/"# Set uniform access policy for the bucket.# We use legacyObjectReader because it grants read access to individual files but# prevents clients from exploring the bucket's contents.gsutil iam ch allUsers:roles/storage.legacyObjectReader "gs://${BUCKET_NAME}"

將檔案複製到 Google Cloud Storage

還記得 MediaGoblin 容器產生的那些位於 MG_SERVING_DIR 目錄中的檔案嗎?將它們搬移到你剛建立的 Google Cloud Storage 儲存空間中:

gsutil -m cp -r "${MG_SERVING_DIR}/media/public/*" "gs://${BUCKET_NAME}/"RANDOM_FOLDER=$(head /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 16 ; echo '')gsutil cp "${MG_SERVING_DIR}/mediagoblin.db" "gs://${BUCKET_NAME}/${RANDOM_FOLDER}/"# Save the public URL of the mediagoblin.db file.MEDIAGOBLIN_DB_URL="https://storage.googleapis.com/${BUCKET_NAME}/${RANDOM_FOLDER}/mediagoblin.db"
注意:我為 mediagoblin.db 的存放位置多加了一層隨機性,因為否則任何知道儲存空間名稱的人都能找到資料庫檔案。透過這種方式增加亂度,你就能在分享單支影片時,不會同時暴露整個收藏。

部署至 Heroku

既然你的媒體檔案已經放在 Google Cloud Storage 上可透過網際網路存取的位置,剩下的就是部署你的 MediaGoblin 伺服器了。我使用 Heroku 是因為它為 Docker 容器提供了相當大方的免費額度,而且設定起來很簡單。

如果這台機器上尚未完成 Heroku CLI 的驗證,請使用你的 Heroku 帳號資訊登入:

heroku login --interactive

接著,驗證 Heroku 的容器登錄庫並建立一個新的 Heroku 應用程式。預設情況下,建立的應用程式會是一個免費的 Heroku dyno:

heroku container:loginRANDOM_SUFFIX=$(head /dev/urandom | tr -dc 'a-z0-9' | head -c 10 ; echo '')HEROKU_APP_NAME="mediagoblin-${RANDOM_SUFFIX}"heroku apps:create "$HEROKU_APP_NAME"

最後一個步驟是使用你的驗證設定與標題來自訂 Docker 建置,然後將其推送至 Heroku 以進行部署:

pushd $(mktemp -d)REPO="https://github.com/mtlynch/mediagoblin-docker.git"# This is a special branch I created that adds authentication and adds a few# other features.TARGET_BRANCH="mtlynch-custom"git clone --single-branch --branch "$TARGET_BRANCH" "$REPO" .HTTP_AUTH_USER="mediagoblin" # change this to a username you chooseHTTP_AUTH_PASS="goblinmedia" # change this to a password you choose# Title you want for your MediaGoblin server.HTML_TITLE="My Demo MediaGoblin Server"heroku container:push web \    --app "$HEROKU_APP_NAME" \    --arg "HTTP_AUTH_USER=${HTTP_AUTH_USER},HTTP_AUTH_PASS=${HTTP_AUTH_PASS},GCS_BUCKET=${BUCKET_NAME},MEDIAGOBLIN_DB_URL=${MEDIAGOBLIN_DB_URL},HTML_TITLE=${HTML_TITLE}"heroku container:release --app "$HEROKU_APP_NAME" web && \  printf "Your app is live at https://${HEROKU_APP_NAME}.herokuapp.com/\n"

如果一切順利,你應該會看到像這樣的新 MediaGoblin 實例網址:

Your app is live at https://mediagoblin-v5lmqis51k.herokuapp.com/
注意:由於你使用的是 Heroku 的免費方案,當你在瀏覽器中發出請求時,Heroku 才會按需啟動你的 MediaGoblin 實例。這表示首次載入頁面會需要 30 至 60 秒,你可能會看到 HTTP 502 Gateway timeout 錯誤。這是正常現象。應用程式開始運作後,在閒置數小時之前都會保持快速且靈敏。

額外技巧:分享單支影片

如果你是用這個流程來存放家庭影片,你大概會希望家人能夠瀏覽並存取所有影片,但有時你只想與他人分享單支影片,而不讓他們存取整個收藏。

若想這麼做,請點擊 MediaGoblin 介面中的「Original file」連結:

標示「Original file」連結的截圖

這會讓訪客取得該單一檔案的存取權,但由於你設定 Google Cloud Storage 儲存空間權限的方式,他們將無法探索整個媒體庫並存取其他影片。

「Original file」連結會產生一個像這樣的網址:

  • https://storage.googleapis.com/mediagoblin-39dpduhfz1wstbprmyk5ak29/media_entries/4/Texas_Family_-_1952_-_04_-_Trudy_Working_at_the_Loom.mp4

如你所見,有了上面的網址,你可以觀看該單支影片,但無法透過探索儲存空間來找到其他影片。


原始碼

儲存庫說明
MediaGoblinMediaGoblin 核心儲存庫的鏡像,加上 Circle CI 設定。

分支 mtlynch-custom 包含了針對我個人實例的自訂修正(替換了舊的影片播放器,並精簡了部分我不需要的介面)。
mediagoblin-docker為 MediaGoblin 建置 Docker 映像檔。

分支 mtlynch-custom 以我的客製化設定進行建置,包含指向 Google Cloud Storage 儲存空間以及加入 HTTP Basic Authentication。
process-home-videos用於將原始影片檔分割成片段,並將這些片段發布至 MediaGoblin 的 Python 指令碼。

本文章由 muse-spark-1.2-contributor 進行翻譯

留言