使用 MediaGoblin 编辑和分享家庭视频
目标
本教程将介绍如何把数字化的视频采集文件编辑成较短的片段,然后发布到由你自己运行、受密码保护的 MediaGoblin 服务器上。你将使用免费的 Heroku dyno(运行单元),因此运行这个私有媒体服务器的唯一持续成本,就是 Google Cloud Storage 的存储费用,即每 GB 2.3 美分。
我使用这套流程编辑并分享家人的家庭视频,每月成本仅为 0.77 美元。想了解详细背景,请参阅博客文章“My Eight-Year Quest to Digitize 45 Videotapes(《我历时八年将 45 盘录像带数字化的探索》)”。你也可以将这套流程用于任何包含大量子片段、需要剪切出来并分享的视频文件。
前置要求
- Python 3
- virtualenv
- Git
- Docker
- ffmpeg
- 视频播放器
- 最好使用带有编辑时间线的视频播放器,例如 Adobe Premiere Elements 或 OpenShot。
- 免费的 Heroku 账户
- 以及 Heroku CLI
- Google Cloud Platform 账户(已启用结算)
演示视频
作为示例,本教程中始终使用的视频是一段来自 20 世纪 50 年代德州某家庭的公有领域家庭视频:
本教程将其作为原始视频进行编辑和分享的公有领域视频(注意:该视频不含音频)
要下载该视频,请运行以下命令:
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 编辑器。从技术上讲,任何视频播放器都能使用,但如果选择支持逐帧查看并带有可缩放时间线的播放器,你可以节省时间。
接下来,创建一个电子表格。你可以从我的表格开始。下面我会解释各个字段:

在电子表格中记录所有元数据,格式与我的表格相同。
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 时,会为片段添加adam和archie标签。 - 设置这些列的用途是:如果你想在视频中标记家人或宠物,可以用他们的井号标签创建列,并在他们出现的每个片段对应的列中填入
y。
- 例如:我的电子表格第 9 行在
date:片段的录制日期。- 可以使用以下格式之一:
YYYY-MM-DDYYYY-MMYYYY
- 可以使用以下格式之一:
other_tags:以逗号分隔的列表形式,添加你想应用于片段的其他标签。
运行 render_scenes(见下文)时,它会按照以下命名格式将视频剪切成较短的片段:
[tape_shortname] - [clip index] - [title].mp4
例如:
Texas Family - 1952 - 05 - Archie in the Corn Fields.mp4
创建场景 YAML 文件
现在你已经完成片段编目,可以使用我的脚本将 CSV 转换为 YAML 文件。该 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接下来,需要创建配置文件。将 config.example.yaml 从 process-home-videos 复制为 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剪切片段
现在所有元数据都准备好了,是时候将大型视频文件剪切成一系列较小的片段了:
# 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.ini,也就是 MediaGoblin 的配置文件。
MG_ 前缀的路径指主机,带有 MGC_ 前缀的路径指 Docker 容器内的路径。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"脚本完成后,你应该会在 MediaGoblin 的 Web 界面 http://localhost:6543 中看到所有片段。

你应该会在 MediaGoblin 的 Web 界面中看到全部八个片段的缩略图。
点击某个视频后,你无法在普通视频播放器中观看它,因为 MediaGoblin 使用的 Web 播放器太旧,不支持现代的流媒体优化视频格式。不过,点击“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 存储桶。我创建的存储桶会将所有对象设为公开,但依赖随机存储桶名称的难以猜测性来提供保护。
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/额外内容:分享单个视频
如果你使用这套流程存储家庭视频,你可能希望家人能够浏览和访问所有视频,但有时又想在不让他人访问整个视频集合的情况下,与他们分享单个视频。
如果想这样做,请点击 MediaGoblin 界面中的“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,你可以观看这一个视频,但无法浏览存储桶来查找其他视频。
源代码
| 仓库 | 说明 |
|---|---|
| MediaGoblin | MediaGoblin 核心仓库的镜像,以及 Circle CI 配置。 mtlynch-custom 分支包含针对我的实例的自定义修复(替换了其旧版视频播放器,并删减了我不需要的部分界面)。 |
| mediagoblin-docker | 为 MediaGoblin 构建 Docker 镜像。 mtlynch-custom 分支会使用我的自定义设置进行构建,其中包括指向 Google Cloud Storage 存储桶,以及添加 HTTP Basic Authentication。 |
| process-home-videos | 用于将原始视频文件剪切成片段,然后将这些片段发布到 MediaGoblin 的 Python 脚本。 |
随机一篇博客

