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 時,就會為該片段加上 adamarchie 標籤。
    • 這些欄位的用途是,如果你有想在影片中標記的家人或寵物,可以為他們新增 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 進行翻譯

留言