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 Sheets 电子表格截图

请按照与我的表格相同的格式,在电子表格中整理所有元数据。

  • tape_id:视频文件的文件名(不含扩展名)。
  • tape_shortname:用于标识来自该原始文件的所有文件的简称,可以与 tape_id 相同。
  • tape_friendly_name:MediaGoblin 显示片段来源磁带时会使用该字段。如果有比文件名更具描述性的名称,请在此填写。
  • scene_start_frame:片段起始帧的编号。
    • 在示例的 Texas Farm 素材中,最开始的几帧是空白的,因此第一个片段从第 28 帧开始。
  • title:该场景的标题,将用于输出文件名和 MediaGoblin 的缩略图标题。
    • 若要让 render_scenes 忽略某段素材,请将其标题设为 junk
  • description:(可选)对场景内容的描述,将显示在 MediaGoblin 播放页面的片段下方。
  • hashtags:在 description 列之后,你可以创建以井号开头的标签列。只要某片段在某个标签列中填入 y(或任意值),该标签就会被添加到 MediaGoblin 中的对应片段上。
    • 例如:在我的表格第 9 行,#adam#archie 列中填了 y。当 publish_to_mediagoblin 将该片段添加到 MediaGoblin 时,就会自动为其加上 adamarchie 标签。
    • 这些列的用途是,如果你想在视频中标记家人或宠物,可以为他们各添加一列标签,并在他们出现的每个片段对应列中填入 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

创建场景 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 实例 URL:

Your app is live at https://mediagoblin-v5lmqis51k.herokuapp.com/
注意:由于你使用的是 Heroku 的免费套餐,Heroku 会在你在浏览器中请求时按需启动 MediaGoblin 实例。这意味着首次加载页面需要 30-60 秒,你可能会看到 HTTP 502 网关超时错误。这是正常现象。应用启动后,在你让它闲置数小时之前,它都会快速响应。

额外福利:分享单个视频

如果你使用这套流程来存储家庭视频,你可能希望家人能够浏览和访问所有视频,但有时你只想与他人分享单个视频,而不让他们访问整个合集。

如果想这样做,请在 MediaGoblin 界面中点击“Original file”链接:

指向“Original file”链接的截图

这会让访客获得对该单个文件的访问权限,但由于你配置 Google Cloud Storage 存储桶权限的方式,他们将无法浏览媒体库或访问其它视频。

“Original file”链接会给你一个如下所示的 URL:

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

如你所见,通过上述 URL,你可以观看该单个视频,但无法通过浏览存储桶来找到其它视频。


源代码

仓库描述
MediaGoblinMediaGoblin 核心仓库的镜像 + CircleCI 配置。

分支 mtlynch-custom 包含了针对我个人实例的自定义修复(替换了旧的视频播放器并精简了 UI 中不需要的部分)。
mediagoblin-docker为 MediaGoblin 构建 Docker 镜像。

分支 mtlynch-custom 使用了我的自定义配置进行构建,包括指向 Google Cloud Storage 存储桶以及添加 HTTP Basic Authentication
process-home-videos用于将原始视频文件切成片段并发布到 MediaGoblin 的 Python 脚本。

本文章由 muse-spark-1.2-contributor 进行翻译

评论