media: publish to our own origin instead of mirroring a file host
A 167 MB screen recording uploaded to catbox.moe hit fetch-media.sh's 64 MB
MAX_BYTES, so curl refused it, the entry kept its third-party URL, and e2e
failed "/posts media origin" on a file we had on disk the whole time. Raising
the cap would have papered over it: the mirror step still depends on someone
else's server being up, fast, and still holding the file.
Invert it. tools/publish-media.sh uploads a recording to the media mount under
its content hash BEFORE the post exists and prints the URL to post, and
fetch-media.sh adopts an own-origin URL by rewriting it to /media/<hash> with no
request at all — no size cap, no third party in the build.
Also here, because publishing exposed them:
* Rotation. Phones record 1920x1080 and attach a display matrix rather than
rotating pixels, so an untouched file reports landscape while playing
portrait and width/height reserve exactly the wrong box. publish-media.sh
bakes rotation into the frames; fetch-media.sh swaps the dimensions on a
quarter-turn matrix for anything mirrored straight from a phone.
* Posters. pict-rs will not thumbnail AV1, so a self-hosted video usually
arrives with no poster. publish-media.sh uploads <hash>.poster.webp beside
the video and fetch-media.sh falls back to it — a real thumbnail still wins.
* The workflow comment claiming a file on the mount is never downloaded again
was wrong: the name is the hash of the bytes, so third-party media is
re-fetched every build and only the write is skipped.
Transcoding to AV1 is what makes self-hosting cheap: that clip was 78 s of a
dark room at 17 Mbps, and denoise + AV1 gives the same picture in 15 MB. Note
<video> carries a single src with no fallback, so AV1 excludes Safari < 17;
--raw skips the transcode when that matters.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 01:16:17 +02:00
|
|
|
#!/bin/sh
|
2026-08-18 21:21:41 +02:00
|
|
|
# Publish a recording, screenshot or audio file to catcrafts.net and print the
|
|
|
|
|
# URL to put in the fediverse post.
|
media: publish to our own origin instead of mirroring a file host
A 167 MB screen recording uploaded to catbox.moe hit fetch-media.sh's 64 MB
MAX_BYTES, so curl refused it, the entry kept its third-party URL, and e2e
failed "/posts media origin" on a file we had on disk the whole time. Raising
the cap would have papered over it: the mirror step still depends on someone
else's server being up, fast, and still holding the file.
Invert it. tools/publish-media.sh uploads a recording to the media mount under
its content hash BEFORE the post exists and prints the URL to post, and
fetch-media.sh adopts an own-origin URL by rewriting it to /media/<hash> with no
request at all — no size cap, no third party in the build.
Also here, because publishing exposed them:
* Rotation. Phones record 1920x1080 and attach a display matrix rather than
rotating pixels, so an untouched file reports landscape while playing
portrait and width/height reserve exactly the wrong box. publish-media.sh
bakes rotation into the frames; fetch-media.sh swaps the dimensions on a
quarter-turn matrix for anything mirrored straight from a phone.
* Posters. pict-rs will not thumbnail AV1, so a self-hosted video usually
arrives with no poster. publish-media.sh uploads <hash>.poster.webp beside
the video and fetch-media.sh falls back to it — a real thumbnail still wins.
* The workflow comment claiming a file on the mount is never downloaded again
was wrong: the name is the hash of the bytes, so third-party media is
re-fetched every build and only the write is skipped.
Transcoding to AV1 is what makes self-hosting cheap: that clip was 78 s of a
dark room at 17 Mbps, and denoise + AV1 gives the same picture in 15 MB. Note
<video> carries a single src with no fallback, so AV1 excludes Safari < 17;
--raw skips the transcode when that matters.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 01:16:17 +02:00
|
|
|
#
|
|
|
|
|
# usage: tools/publish-media.sh [-r] [-c CRF] <file>...
|
|
|
|
|
#
|
|
|
|
|
# -r, --raw upload the file unchanged (no transcode, no poster)
|
|
|
|
|
# -c, --crf N AV1 quality, lower is better (default 32)
|
|
|
|
|
#
|
|
|
|
|
# WHY THIS EXISTS: the media used to be uploaded to a third-party file host and
|
|
|
|
|
# then mirrored back at build time by tools/fetch-media.sh. That worked until a
|
|
|
|
|
# 167 MB recording hit the mirror's size cap, and the deploy failed on a file we
|
|
|
|
|
# had sitting on disk the whole time. Publishing to our own origin FIRST inverts
|
|
|
|
|
# it — the URL in the post is already the URL the page wants, so there is nothing
|
|
|
|
|
# to download, no size cap to clear, and no third party who can be slow, rate-
|
|
|
|
|
# limit us, or delete the file that IS the post.
|
|
|
|
|
#
|
|
|
|
|
# The name is the content hash, matching what fetch-media.sh produces, which is
|
|
|
|
|
# what makes Caddy's one-year immutable cache on /media honest: different bytes
|
|
|
|
|
# can never appear under a name someone already has cached.
|
|
|
|
|
#
|
|
|
|
|
# TRANSCODE: phone recordings are enormous for what they show — the one that
|
|
|
|
|
# started this was 78 s of a dark room at 17 Mbps, 167 MB, where denoising and
|
|
|
|
|
# AV1 give the same picture in 15 MB. Dark handheld footage is mostly sensor
|
|
|
|
|
# noise, which is expensive to encode and worth nothing, so hqdn3d earns its
|
|
|
|
|
# place before the encoder rather than after it.
|
|
|
|
|
#
|
|
|
|
|
# ROTATION: phones record landscape and attach a display matrix instead of
|
|
|
|
|
# rotating pixels. Transcoding bakes the rotation into the frames, so what every
|
|
|
|
|
# downstream consumer measures is what the viewer actually sees — see the
|
|
|
|
|
# rotation note in fetch-media.sh for what goes wrong when it does not.
|
|
|
|
|
#
|
media: H.264 fallback beside every AV1, and the post links the H.264
The AV1-only publish lasted four hours in the wild: the first viewer on
mobile Safari got "bad media error" from the raw file, because a post's
link is fetched raw — Lemmy apps and browsers play that exact URL, with
no <source> negotiation in front of it. The previous commit's note
("--raw ... if that audience matters") had it backwards: the audience
that cannot play AV1 is not a per-post judgement call, it is whoever
happens to open the thread on an iPhone.
So publish-media.sh now emits two encodings and points the post at the
compatible one:
* Every video transcode also produces <hash>.h264.mp4 (x264 crf 23,
same denoise, same frames), uploaded beside the AV1 under the AV1's
hash — the poster's sibling-naming trick, reused, so fetch-media.sh
finds it by name with nothing to look up.
* The printed URL to paste into the post is the H.264 one. pict-rs
can thumbnail it too, so instance thumbnails come back as a bonus.
fetch-media.sh adopting an own-origin .h264.mp4 URL swaps the AV1 back
in as the page's primary when it is on the mount, and records the H.264
as `fallback` in posts.json. The renderer turns a non-empty fallback
into a <source> pair: the AV1 first with an explicit codecs parameter —
both files are video/mp4, so the parameter is the only thing that lets
a non-AV1 browser skip to the file it can play — and the H.264 second.
Browsers with AV1 keep downloading the small file; Safari before 17
gets one that plays instead of an element that will not.
e2e gains the matching conditional gate: a page offering an av01
<source> must offer an .h264.mp4 one, so an AV1 video can never again
ship without its fallback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 03:14:51 +02:00
|
|
|
# FALLBACK: AV1 alone locks out Safari before 17 and Apple hardware older than
|
|
|
|
|
# A17/M3 — which surfaced the first time an AV1 link was posted and an iPhone
|
|
|
|
|
# viewer got "bad media error" from the raw file. So every transcode also emits
|
|
|
|
|
# an H.264 sibling, uploaded as <hash>.h264.mp4 next to the AV1 (same trick as
|
|
|
|
|
# the poster: named after the AV1's hash so fetch-media.sh finds it with nothing
|
|
|
|
|
# to look up). The URL this script prints for the post is the H.264 one, because
|
|
|
|
|
# a post's link is fetched raw — Lemmy apps and browsers play that exact file,
|
|
|
|
|
# with no <source> negotiation in front of it — and it must be the encoding
|
|
|
|
|
# everything can play. Our own pages recognise the .h264.mp4 name, serve the AV1
|
|
|
|
|
# to browsers that can take it, and keep the H.264 as the <source> fallback.
|
2026-08-18 21:21:41 +02:00
|
|
|
#
|
|
|
|
|
# AUDIO: the same two rails, for the same reason. Opus in WebM is the efficient
|
|
|
|
|
# one — the first file through here, 93 s of speech, went from a 1.5 MB 128k MP3
|
|
|
|
|
# to 553 KB — and an MP3 sibling is the one that plays everywhere, so the MP3 is
|
|
|
|
|
# the URL printed for the post. The primary does not have to be universal
|
|
|
|
|
# precisely because the sibling is.
|
|
|
|
|
#
|
|
|
|
|
# Two ways the audio path deliberately differs from the video path above. It does
|
|
|
|
|
# not fold everything to mono: here the audio IS the content rather than a
|
|
|
|
|
# rounding error next to a picture, so the source's channel layout survives and
|
|
|
|
|
# sets the bitrate. And a source that is ALREADY an MP3 is copied to the
|
|
|
|
|
# compatibility rail untouched rather than re-encoded — MP3 to MP3 spends a
|
|
|
|
|
# second generation of lossy artefacts to arrive at the same rail, and speech,
|
|
|
|
|
# which is most of what gets published here, is where that is most audible.
|
|
|
|
|
#
|
|
|
|
|
# Nothing downstream builds an <audio> element yet: fetch-media.sh does not match
|
|
|
|
|
# these names and the post renderer has no audio case. So the printed URL is for
|
|
|
|
|
# posting and for linking by hand, not yet for embedding in a post body.
|
media: publish to our own origin instead of mirroring a file host
A 167 MB screen recording uploaded to catbox.moe hit fetch-media.sh's 64 MB
MAX_BYTES, so curl refused it, the entry kept its third-party URL, and e2e
failed "/posts media origin" on a file we had on disk the whole time. Raising
the cap would have papered over it: the mirror step still depends on someone
else's server being up, fast, and still holding the file.
Invert it. tools/publish-media.sh uploads a recording to the media mount under
its content hash BEFORE the post exists and prints the URL to post, and
fetch-media.sh adopts an own-origin URL by rewriting it to /media/<hash> with no
request at all — no size cap, no third party in the build.
Also here, because publishing exposed them:
* Rotation. Phones record 1920x1080 and attach a display matrix rather than
rotating pixels, so an untouched file reports landscape while playing
portrait and width/height reserve exactly the wrong box. publish-media.sh
bakes rotation into the frames; fetch-media.sh swaps the dimensions on a
quarter-turn matrix for anything mirrored straight from a phone.
* Posters. pict-rs will not thumbnail AV1, so a self-hosted video usually
arrives with no poster. publish-media.sh uploads <hash>.poster.webp beside
the video and fetch-media.sh falls back to it — a real thumbnail still wins.
* The workflow comment claiming a file on the mount is never downloaded again
was wrong: the name is the hash of the bytes, so third-party media is
re-fetched every build and only the write is skipped.
Transcoding to AV1 is what makes self-hosting cheap: that clip was 78 s of a
dark room at 17 Mbps, and denoise + AV1 gives the same picture in 15 MB. Note
<video> carries a single src with no fallback, so AV1 excludes Safari < 17;
--raw skips the transcode when that matters.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 01:16:17 +02:00
|
|
|
|
|
|
|
|
set -eu
|
|
|
|
|
|
|
|
|
|
MEDIA_HOST="${MEDIA_HOST:-root@catcrafts.net}"
|
|
|
|
|
MEDIA_PATH="${MEDIA_PATH:-/srv/catcrafts-app/media}"
|
|
|
|
|
SITE_ORIGIN="${SITE_ORIGIN:-https://catcrafts.net}"
|
|
|
|
|
CRF="${CRF:-32}"
|
|
|
|
|
RAW=0
|
|
|
|
|
|
|
|
|
|
for c in ffmpeg ffprobe sha256sum ssh scp; do
|
|
|
|
|
command -v "$c" >/dev/null 2>&1 || { echo "publish-media: $c not found" >&2; exit 1; }
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
FILES=""
|
|
|
|
|
while [ $# -gt 0 ]; do
|
|
|
|
|
case "$1" in
|
|
|
|
|
-r|--raw) RAW=1; shift ;;
|
|
|
|
|
-c|--crf) CRF="$2"; shift 2 ;;
|
|
|
|
|
-h|--help) sed -n '2,10p' "$0"; exit 0 ;;
|
|
|
|
|
-*) echo "publish-media: unknown option: $1" >&2; exit 1 ;;
|
|
|
|
|
*) FILES="$FILES $1"; shift ;;
|
|
|
|
|
esac
|
|
|
|
|
done
|
|
|
|
|
[ -n "$FILES" ] || { echo "publish-media: no input file. See --help." >&2; exit 1; }
|
|
|
|
|
|
|
|
|
|
WORK="$(mktemp -d)"
|
2026-08-17 11:04:03 +02:00
|
|
|
|
|
|
|
|
# One SSH connection for the whole run, not one per operation. The origin runs
|
|
|
|
|
# `ufw limit 22/tcp`, which rejects the sixth new connection from a source inside
|
|
|
|
|
# 30 s — and a video publishes three artifacts at three connections each (probe,
|
|
|
|
|
# copy, rename), so a run used to trip the limit on the poster's rename and die
|
|
|
|
|
# with the H.264 fallback still unsent. The failure does not even look like rate
|
|
|
|
|
# limiting: ufw REJECTs with ICMP port-unreachable, ssh moves on to the AAAA
|
|
|
|
|
# record, and reports the errno of the last address it tried — so a refused IPv4
|
|
|
|
|
# connection surfaces as "Network is unreachable" from a host with no IPv6 route.
|
|
|
|
|
# Multiplexing over one master socket keeps a run at a single connection however
|
|
|
|
|
# many artifacts it produces, and leaves the limit in place doing its real job.
|
|
|
|
|
SSH_CTL="$WORK/cm"
|
|
|
|
|
SSH_OPTS="-o BatchMode=yes -o ControlMaster=auto -o ControlPath=$SSH_CTL -o ControlPersist=60"
|
|
|
|
|
cleanup() {
|
|
|
|
|
if [ -S "$SSH_CTL" ]; then
|
|
|
|
|
ssh -O exit $SSH_OPTS "$MEDIA_HOST" >/dev/null 2>&1 || true
|
|
|
|
|
fi
|
|
|
|
|
rm -rf "$WORK"
|
|
|
|
|
}
|
|
|
|
|
trap cleanup EXIT
|
media: publish to our own origin instead of mirroring a file host
A 167 MB screen recording uploaded to catbox.moe hit fetch-media.sh's 64 MB
MAX_BYTES, so curl refused it, the entry kept its third-party URL, and e2e
failed "/posts media origin" on a file we had on disk the whole time. Raising
the cap would have papered over it: the mirror step still depends on someone
else's server being up, fast, and still holding the file.
Invert it. tools/publish-media.sh uploads a recording to the media mount under
its content hash BEFORE the post exists and prints the URL to post, and
fetch-media.sh adopts an own-origin URL by rewriting it to /media/<hash> with no
request at all — no size cap, no third party in the build.
Also here, because publishing exposed them:
* Rotation. Phones record 1920x1080 and attach a display matrix rather than
rotating pixels, so an untouched file reports landscape while playing
portrait and width/height reserve exactly the wrong box. publish-media.sh
bakes rotation into the frames; fetch-media.sh swaps the dimensions on a
quarter-turn matrix for anything mirrored straight from a phone.
* Posters. pict-rs will not thumbnail AV1, so a self-hosted video usually
arrives with no poster. publish-media.sh uploads <hash>.poster.webp beside
the video and fetch-media.sh falls back to it — a real thumbnail still wins.
* The workflow comment claiming a file on the mount is never downloaded again
was wrong: the name is the hash of the bytes, so third-party media is
re-fetched every build and only the write is skipped.
Transcoding to AV1 is what makes self-hosting cheap: that clip was 78 s of a
dark room at 17 Mbps, and denoise + AV1 gives the same picture in 15 MB. Note
<video> carries a single src with no fallback, so AV1 excludes Safari < 17;
--raw skips the transcode when that matters.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 01:16:17 +02:00
|
|
|
|
|
|
|
|
# Upload only what is not already there. The name is the content hash, so a name
|
|
|
|
|
# that exists on the mount holds these exact bytes and re-sending them would
|
|
|
|
|
# change nothing — which makes re-running this script on the same file free, and
|
|
|
|
|
# makes a half-finished batch safe to just run again.
|
|
|
|
|
publish_file() {
|
|
|
|
|
local_file=$1
|
|
|
|
|
remote_name=$2
|
2026-08-17 11:04:03 +02:00
|
|
|
if ssh $SSH_OPTS "$MEDIA_HOST" "test -f '$MEDIA_PATH/$remote_name'"; then
|
media: publish to our own origin instead of mirroring a file host
A 167 MB screen recording uploaded to catbox.moe hit fetch-media.sh's 64 MB
MAX_BYTES, so curl refused it, the entry kept its third-party URL, and e2e
failed "/posts media origin" on a file we had on disk the whole time. Raising
the cap would have papered over it: the mirror step still depends on someone
else's server being up, fast, and still holding the file.
Invert it. tools/publish-media.sh uploads a recording to the media mount under
its content hash BEFORE the post exists and prints the URL to post, and
fetch-media.sh adopts an own-origin URL by rewriting it to /media/<hash> with no
request at all — no size cap, no third party in the build.
Also here, because publishing exposed them:
* Rotation. Phones record 1920x1080 and attach a display matrix rather than
rotating pixels, so an untouched file reports landscape while playing
portrait and width/height reserve exactly the wrong box. publish-media.sh
bakes rotation into the frames; fetch-media.sh swaps the dimensions on a
quarter-turn matrix for anything mirrored straight from a phone.
* Posters. pict-rs will not thumbnail AV1, so a self-hosted video usually
arrives with no poster. publish-media.sh uploads <hash>.poster.webp beside
the video and fetch-media.sh falls back to it — a real thumbnail still wins.
* The workflow comment claiming a file on the mount is never downloaded again
was wrong: the name is the hash of the bytes, so third-party media is
re-fetched every build and only the write is skipped.
Transcoding to AV1 is what makes self-hosting cheap: that clip was 78 s of a
dark room at 17 Mbps, and denoise + AV1 gives the same picture in 15 MB. Note
<video> carries a single src with no fallback, so AV1 excludes Safari < 17;
--raw skips the transcode when that matters.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 01:16:17 +02:00
|
|
|
echo " already published: $remote_name"
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
# Land it under a temporary name and move it into place, so a reader can
|
|
|
|
|
# never see a partial file under the name its hash promises.
|
2026-08-17 11:04:03 +02:00
|
|
|
scp -q $SSH_OPTS "$local_file" "$MEDIA_HOST:$MEDIA_PATH/.$remote_name.part"
|
|
|
|
|
ssh $SSH_OPTS "$MEDIA_HOST" \
|
media: publish to our own origin instead of mirroring a file host
A 167 MB screen recording uploaded to catbox.moe hit fetch-media.sh's 64 MB
MAX_BYTES, so curl refused it, the entry kept its third-party URL, and e2e
failed "/posts media origin" on a file we had on disk the whole time. Raising
the cap would have papered over it: the mirror step still depends on someone
else's server being up, fast, and still holding the file.
Invert it. tools/publish-media.sh uploads a recording to the media mount under
its content hash BEFORE the post exists and prints the URL to post, and
fetch-media.sh adopts an own-origin URL by rewriting it to /media/<hash> with no
request at all — no size cap, no third party in the build.
Also here, because publishing exposed them:
* Rotation. Phones record 1920x1080 and attach a display matrix rather than
rotating pixels, so an untouched file reports landscape while playing
portrait and width/height reserve exactly the wrong box. publish-media.sh
bakes rotation into the frames; fetch-media.sh swaps the dimensions on a
quarter-turn matrix for anything mirrored straight from a phone.
* Posters. pict-rs will not thumbnail AV1, so a self-hosted video usually
arrives with no poster. publish-media.sh uploads <hash>.poster.webp beside
the video and fetch-media.sh falls back to it — a real thumbnail still wins.
* The workflow comment claiming a file on the mount is never downloaded again
was wrong: the name is the hash of the bytes, so third-party media is
re-fetched every build and only the write is skipped.
Transcoding to AV1 is what makes self-hosting cheap: that clip was 78 s of a
dark room at 17 Mbps, and denoise + AV1 gives the same picture in 15 MB. Note
<video> carries a single src with no fallback, so AV1 excludes Safari < 17;
--raw skips the transcode when that matters.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 01:16:17 +02:00
|
|
|
"chmod 0644 '$MEDIA_PATH/.$remote_name.part' && \
|
|
|
|
|
mv -f '$MEDIA_PATH/.$remote_name.part' '$MEDIA_PATH/$remote_name'"
|
|
|
|
|
echo " uploaded: $remote_name"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for src in $FILES; do
|
|
|
|
|
[ -f "$src" ] || { echo "publish-media: no such file: $src" >&2; exit 1; }
|
|
|
|
|
echo "$src"
|
|
|
|
|
|
|
|
|
|
ext=$(printf '%s' "$src" | sed -E 's/.*\.([A-Za-z0-9]+)$/\1/' | tr 'A-Z' 'a-z')
|
|
|
|
|
kind=other
|
|
|
|
|
case "$ext" in
|
|
|
|
|
mp4|webm|mov|mkv|avi) kind=video ;;
|
|
|
|
|
png|jpg|jpeg|webp|gif|avif) kind=image ;;
|
2026-08-18 21:21:41 +02:00
|
|
|
mp3|m4a|aac|opus|oga|ogg|flac|wav|wma|aiff) kind=audio ;;
|
media: publish to our own origin instead of mirroring a file host
A 167 MB screen recording uploaded to catbox.moe hit fetch-media.sh's 64 MB
MAX_BYTES, so curl refused it, the entry kept its third-party URL, and e2e
failed "/posts media origin" on a file we had on disk the whole time. Raising
the cap would have papered over it: the mirror step still depends on someone
else's server being up, fast, and still holding the file.
Invert it. tools/publish-media.sh uploads a recording to the media mount under
its content hash BEFORE the post exists and prints the URL to post, and
fetch-media.sh adopts an own-origin URL by rewriting it to /media/<hash> with no
request at all — no size cap, no third party in the build.
Also here, because publishing exposed them:
* Rotation. Phones record 1920x1080 and attach a display matrix rather than
rotating pixels, so an untouched file reports landscape while playing
portrait and width/height reserve exactly the wrong box. publish-media.sh
bakes rotation into the frames; fetch-media.sh swaps the dimensions on a
quarter-turn matrix for anything mirrored straight from a phone.
* Posters. pict-rs will not thumbnail AV1, so a self-hosted video usually
arrives with no poster. publish-media.sh uploads <hash>.poster.webp beside
the video and fetch-media.sh falls back to it — a real thumbnail still wins.
* The workflow comment claiming a file on the mount is never downloaded again
was wrong: the name is the hash of the bytes, so third-party media is
re-fetched every build and only the write is skipped.
Transcoding to AV1 is what makes self-hosting cheap: that clip was 78 s of a
dark room at 17 Mbps, and denoise + AV1 gives the same picture in 15 MB. Note
<video> carries a single src with no fallback, so AV1 excludes Safari < 17;
--raw skips the transcode when that matters.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 01:16:17 +02:00
|
|
|
esac
|
|
|
|
|
|
|
|
|
|
poster=""
|
media: H.264 fallback beside every AV1, and the post links the H.264
The AV1-only publish lasted four hours in the wild: the first viewer on
mobile Safari got "bad media error" from the raw file, because a post's
link is fetched raw — Lemmy apps and browsers play that exact URL, with
no <source> negotiation in front of it. The previous commit's note
("--raw ... if that audience matters") had it backwards: the audience
that cannot play AV1 is not a per-post judgement call, it is whoever
happens to open the thread on an iPhone.
So publish-media.sh now emits two encodings and points the post at the
compatible one:
* Every video transcode also produces <hash>.h264.mp4 (x264 crf 23,
same denoise, same frames), uploaded beside the AV1 under the AV1's
hash — the poster's sibling-naming trick, reused, so fetch-media.sh
finds it by name with nothing to look up.
* The printed URL to paste into the post is the H.264 one. pict-rs
can thumbnail it too, so instance thumbnails come back as a bonus.
fetch-media.sh adopting an own-origin .h264.mp4 URL swaps the AV1 back
in as the page's primary when it is on the mount, and records the H.264
as `fallback` in posts.json. The renderer turns a non-empty fallback
into a <source> pair: the AV1 first with an explicit codecs parameter —
both files are video/mp4, so the parameter is the only thing that lets
a non-AV1 browser skip to the file it can play — and the H.264 second.
Browsers with AV1 keep downloading the small file; Safari before 17
gets one that plays instead of an element that will not.
e2e gains the matching conditional gate: a page offering an av01
<source> must offer an .h264.mp4 one, so an AV1 video can never again
ship without its fallback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 03:14:51 +02:00
|
|
|
fallback=""
|
2026-08-18 21:21:41 +02:00
|
|
|
fallback_suffix=""
|
media: publish to our own origin instead of mirroring a file host
A 167 MB screen recording uploaded to catbox.moe hit fetch-media.sh's 64 MB
MAX_BYTES, so curl refused it, the entry kept its third-party URL, and e2e
failed "/posts media origin" on a file we had on disk the whole time. Raising
the cap would have papered over it: the mirror step still depends on someone
else's server being up, fast, and still holding the file.
Invert it. tools/publish-media.sh uploads a recording to the media mount under
its content hash BEFORE the post exists and prints the URL to post, and
fetch-media.sh adopts an own-origin URL by rewriting it to /media/<hash> with no
request at all — no size cap, no third party in the build.
Also here, because publishing exposed them:
* Rotation. Phones record 1920x1080 and attach a display matrix rather than
rotating pixels, so an untouched file reports landscape while playing
portrait and width/height reserve exactly the wrong box. publish-media.sh
bakes rotation into the frames; fetch-media.sh swaps the dimensions on a
quarter-turn matrix for anything mirrored straight from a phone.
* Posters. pict-rs will not thumbnail AV1, so a self-hosted video usually
arrives with no poster. publish-media.sh uploads <hash>.poster.webp beside
the video and fetch-media.sh falls back to it — a real thumbnail still wins.
* The workflow comment claiming a file on the mount is never downloaded again
was wrong: the name is the hash of the bytes, so third-party media is
re-fetched every build and only the write is skipped.
Transcoding to AV1 is what makes self-hosting cheap: that clip was 78 s of a
dark room at 17 Mbps, and denoise + AV1 gives the same picture in 15 MB. Note
<video> carries a single src with no fallback, so AV1 excludes Safari < 17;
--raw skips the transcode when that matters.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 01:16:17 +02:00
|
|
|
if [ "$RAW" = 1 ] || [ "$kind" = other ]; then
|
|
|
|
|
payload="$src"
|
|
|
|
|
out_ext="$ext"
|
|
|
|
|
elif [ "$kind" = video ]; then
|
|
|
|
|
payload="$WORK/out.mp4"
|
|
|
|
|
out_ext=mp4
|
|
|
|
|
echo " transcoding to AV1 (crf $CRF)…"
|
|
|
|
|
ffmpeg -nostdin -v error -i "$src" \
|
|
|
|
|
-vf "hqdn3d=4:3:6:4.5" \
|
|
|
|
|
-c:v libsvtav1 -preset 4 -crf "$CRF" -pix_fmt yuv420p -g 240 \
|
|
|
|
|
-movflags +faststart \
|
|
|
|
|
-c:a aac -b:a 96k -ac 1 \
|
|
|
|
|
"$payload" -y
|
|
|
|
|
# AAC rather than Opus on purpose: Opus-in-MP4 is still uneven in exactly
|
|
|
|
|
# the players most likely to be shaky about AV1 anyway, and the audio is
|
|
|
|
|
# a rounding error next to the video either way.
|
media: H.264 fallback beside every AV1, and the post links the H.264
The AV1-only publish lasted four hours in the wild: the first viewer on
mobile Safari got "bad media error" from the raw file, because a post's
link is fetched raw — Lemmy apps and browsers play that exact URL, with
no <source> negotiation in front of it. The previous commit's note
("--raw ... if that audience matters") had it backwards: the audience
that cannot play AV1 is not a per-post judgement call, it is whoever
happens to open the thread on an iPhone.
So publish-media.sh now emits two encodings and points the post at the
compatible one:
* Every video transcode also produces <hash>.h264.mp4 (x264 crf 23,
same denoise, same frames), uploaded beside the AV1 under the AV1's
hash — the poster's sibling-naming trick, reused, so fetch-media.sh
finds it by name with nothing to look up.
* The printed URL to paste into the post is the H.264 one. pict-rs
can thumbnail it too, so instance thumbnails come back as a bonus.
fetch-media.sh adopting an own-origin .h264.mp4 URL swaps the AV1 back
in as the page's primary when it is on the mount, and records the H.264
as `fallback` in posts.json. The renderer turns a non-empty fallback
into a <source> pair: the AV1 first with an explicit codecs parameter —
both files are video/mp4, so the parameter is the only thing that lets
a non-AV1 browser skip to the file it can play — and the H.264 second.
Browsers with AV1 keep downloading the small file; Safari before 17
gets one that plays instead of an element that will not.
e2e gains the matching conditional gate: a page offering an av01
<source> must offer an .h264.mp4 one, so an AV1 video can never again
ship without its fallback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 03:14:51 +02:00
|
|
|
fallback="$WORK/h264.mp4"
|
2026-08-18 21:21:41 +02:00
|
|
|
fallback_suffix=h264.mp4
|
media: H.264 fallback beside every AV1, and the post links the H.264
The AV1-only publish lasted four hours in the wild: the first viewer on
mobile Safari got "bad media error" from the raw file, because a post's
link is fetched raw — Lemmy apps and browsers play that exact URL, with
no <source> negotiation in front of it. The previous commit's note
("--raw ... if that audience matters") had it backwards: the audience
that cannot play AV1 is not a per-post judgement call, it is whoever
happens to open the thread on an iPhone.
So publish-media.sh now emits two encodings and points the post at the
compatible one:
* Every video transcode also produces <hash>.h264.mp4 (x264 crf 23,
same denoise, same frames), uploaded beside the AV1 under the AV1's
hash — the poster's sibling-naming trick, reused, so fetch-media.sh
finds it by name with nothing to look up.
* The printed URL to paste into the post is the H.264 one. pict-rs
can thumbnail it too, so instance thumbnails come back as a bonus.
fetch-media.sh adopting an own-origin .h264.mp4 URL swaps the AV1 back
in as the page's primary when it is on the mount, and records the H.264
as `fallback` in posts.json. The renderer turns a non-empty fallback
into a <source> pair: the AV1 first with an explicit codecs parameter —
both files are video/mp4, so the parameter is the only thing that lets
a non-AV1 browser skip to the file it can play — and the H.264 second.
Browsers with AV1 keep downloading the small file; Safari before 17
gets one that plays instead of an element that will not.
e2e gains the matching conditional gate: a page offering an av01
<source> must offer an .h264.mp4 one, so an AV1 video can never again
ship without its fallback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 03:14:51 +02:00
|
|
|
echo " transcoding H.264 fallback (crf 23)…"
|
|
|
|
|
# Same source and same denoise as the AV1, so the two encodings show the
|
|
|
|
|
# same frames. CRF is x264's own scale (not comparable to SVT-AV1's);
|
|
|
|
|
# 23 is its "looks the same" default, and this file is the compatibility
|
|
|
|
|
# rail, not the one most visitors download — bigger is acceptable here.
|
|
|
|
|
ffmpeg -nostdin -v error -i "$src" \
|
|
|
|
|
-vf "hqdn3d=4:3:6:4.5" \
|
|
|
|
|
-c:v libx264 -preset slow -crf 23 -pix_fmt yuv420p \
|
|
|
|
|
-movflags +faststart \
|
|
|
|
|
-c:a aac -b:a 96k -ac 1 \
|
|
|
|
|
"$fallback" -y
|
media: publish to our own origin instead of mirroring a file host
A 167 MB screen recording uploaded to catbox.moe hit fetch-media.sh's 64 MB
MAX_BYTES, so curl refused it, the entry kept its third-party URL, and e2e
failed "/posts media origin" on a file we had on disk the whole time. Raising
the cap would have papered over it: the mirror step still depends on someone
else's server being up, fast, and still holding the file.
Invert it. tools/publish-media.sh uploads a recording to the media mount under
its content hash BEFORE the post exists and prints the URL to post, and
fetch-media.sh adopts an own-origin URL by rewriting it to /media/<hash> with no
request at all — no size cap, no third party in the build.
Also here, because publishing exposed them:
* Rotation. Phones record 1920x1080 and attach a display matrix rather than
rotating pixels, so an untouched file reports landscape while playing
portrait and width/height reserve exactly the wrong box. publish-media.sh
bakes rotation into the frames; fetch-media.sh swaps the dimensions on a
quarter-turn matrix for anything mirrored straight from a phone.
* Posters. pict-rs will not thumbnail AV1, so a self-hosted video usually
arrives with no poster. publish-media.sh uploads <hash>.poster.webp beside
the video and fetch-media.sh falls back to it — a real thumbnail still wins.
* The workflow comment claiming a file on the mount is never downloaded again
was wrong: the name is the hash of the bytes, so third-party media is
re-fetched every build and only the write is skipped.
Transcoding to AV1 is what makes self-hosting cheap: that clip was 78 s of a
dark room at 17 Mbps, and denoise + AV1 gives the same picture in 15 MB. Note
<video> carries a single src with no fallback, so AV1 excludes Safari < 17;
--raw skips the transcode when that matters.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 01:16:17 +02:00
|
|
|
poster="$WORK/poster.webp"
|
|
|
|
|
# A frame a third of the way in, which beats frame 0 — recordings tend to
|
|
|
|
|
# open on a lock screen, a hand moving into place, or a fade.
|
|
|
|
|
dur=$(ffprobe -v error -show_entries format=duration \
|
|
|
|
|
-of default=nw=1:nk=1 "$payload" 2>/dev/null || echo 0)
|
|
|
|
|
at=$(awk -v d="$dur" 'BEGIN { printf "%.2f", (d > 3 ? d / 3 : 0) }')
|
|
|
|
|
ffmpeg -nostdin -v error -ss "$at" -i "$payload" -frames:v 1 \
|
|
|
|
|
-vf "scale=720:-2" -c:v libwebp -quality 80 "$poster" -y
|
2026-08-18 21:21:41 +02:00
|
|
|
elif [ "$kind" = audio ]; then
|
|
|
|
|
payload="$WORK/out.webm"
|
|
|
|
|
out_ext=webm
|
|
|
|
|
fallback_suffix=mp3
|
|
|
|
|
# Channel count sets the bitrate instead of being flattened by it: 48k of
|
|
|
|
|
# mono Opus is comfortable for speech, and stereo music at that figure
|
|
|
|
|
# would not be. These are VBR targets, not ceilings, and libopus does not
|
|
|
|
|
# track them evenly — on the first file through here the 48k target came
|
|
|
|
|
# out at 45 kbps while 64k ran away to 76, which is why the mono rail sits
|
|
|
|
|
# at the lower figure rather than splitting the difference.
|
|
|
|
|
ch=$(ffprobe -v error -select_streams a:0 -show_entries stream=channels \
|
|
|
|
|
-of default=nw=1:nk=1 "$src" </dev/null 2>/dev/null \
|
|
|
|
|
| head -n1 || true)
|
|
|
|
|
case "$ch" in ''|*[!0-9]*) ch=1 ;; esac
|
|
|
|
|
if [ "$ch" -gt 1 ]; then ab=96k; else ab=48k; fi
|
|
|
|
|
echo " transcoding to Opus ($ab, ${ch}ch)…"
|
|
|
|
|
# -vn drops cover art, which arrives as a still video stream and belongs
|
|
|
|
|
# in neither output — without it an MP3 carrying album art fails the
|
|
|
|
|
# encode outright. -map_metadata -1 drops the ID3 tags, which on a file
|
|
|
|
|
# from someone else carry their tooling and their file naming. 48 kHz is
|
|
|
|
|
# the rate Opus works at internally, so saying it makes the resample of
|
|
|
|
|
# the usual 44.1 kHz source explicit rather than implied.
|
|
|
|
|
ffmpeg -nostdin -v error -i "$src" -vn -map_metadata -1 \
|
|
|
|
|
-c:a libopus -b:a "$ab" -ar 48000 \
|
|
|
|
|
"$payload" -y
|
|
|
|
|
fallback="$WORK/fallback.mp3"
|
|
|
|
|
if [ "$ext" = mp3 ]; then
|
|
|
|
|
echo " MP3 fallback: source is already MP3, copied unchanged"
|
|
|
|
|
cp "$src" "$fallback"
|
|
|
|
|
else
|
|
|
|
|
echo " transcoding MP3 fallback (128k)…"
|
|
|
|
|
# Fixed 128k, decodable by anything, and the same "bigger is
|
|
|
|
|
# acceptable on the compatibility rail" trade the H.264 above makes.
|
|
|
|
|
ffmpeg -nostdin -v error -i "$src" -vn -map_metadata -1 \
|
|
|
|
|
-c:a libmp3lame -b:a 128k "$fallback" -y
|
|
|
|
|
fi
|
media: publish to our own origin instead of mirroring a file host
A 167 MB screen recording uploaded to catbox.moe hit fetch-media.sh's 64 MB
MAX_BYTES, so curl refused it, the entry kept its third-party URL, and e2e
failed "/posts media origin" on a file we had on disk the whole time. Raising
the cap would have papered over it: the mirror step still depends on someone
else's server being up, fast, and still holding the file.
Invert it. tools/publish-media.sh uploads a recording to the media mount under
its content hash BEFORE the post exists and prints the URL to post, and
fetch-media.sh adopts an own-origin URL by rewriting it to /media/<hash> with no
request at all — no size cap, no third party in the build.
Also here, because publishing exposed them:
* Rotation. Phones record 1920x1080 and attach a display matrix rather than
rotating pixels, so an untouched file reports landscape while playing
portrait and width/height reserve exactly the wrong box. publish-media.sh
bakes rotation into the frames; fetch-media.sh swaps the dimensions on a
quarter-turn matrix for anything mirrored straight from a phone.
* Posters. pict-rs will not thumbnail AV1, so a self-hosted video usually
arrives with no poster. publish-media.sh uploads <hash>.poster.webp beside
the video and fetch-media.sh falls back to it — a real thumbnail still wins.
* The workflow comment claiming a file on the mount is never downloaded again
was wrong: the name is the hash of the bytes, so third-party media is
re-fetched every build and only the write is skipped.
Transcoding to AV1 is what makes self-hosting cheap: that clip was 78 s of a
dark room at 17 Mbps, and denoise + AV1 gives the same picture in 15 MB. Note
<video> carries a single src with no fallback, so AV1 excludes Safari < 17;
--raw skips the transcode when that matters.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 01:16:17 +02:00
|
|
|
else
|
|
|
|
|
payload="$WORK/out.webp"
|
|
|
|
|
out_ext=webp
|
|
|
|
|
echo " converting to webp…"
|
|
|
|
|
ffmpeg -nostdin -v error -i "$src" -c:v libwebp -quality 82 "$payload" -y
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
hash=$(sha256sum "$payload" | cut -c1-16)
|
|
|
|
|
name="$hash.$out_ext"
|
|
|
|
|
publish_file "$payload" "$name"
|
|
|
|
|
|
media: H.264 fallback beside every AV1, and the post links the H.264
The AV1-only publish lasted four hours in the wild: the first viewer on
mobile Safari got "bad media error" from the raw file, because a post's
link is fetched raw — Lemmy apps and browsers play that exact URL, with
no <source> negotiation in front of it. The previous commit's note
("--raw ... if that audience matters") had it backwards: the audience
that cannot play AV1 is not a per-post judgement call, it is whoever
happens to open the thread on an iPhone.
So publish-media.sh now emits two encodings and points the post at the
compatible one:
* Every video transcode also produces <hash>.h264.mp4 (x264 crf 23,
same denoise, same frames), uploaded beside the AV1 under the AV1's
hash — the poster's sibling-naming trick, reused, so fetch-media.sh
finds it by name with nothing to look up.
* The printed URL to paste into the post is the H.264 one. pict-rs
can thumbnail it too, so instance thumbnails come back as a bonus.
fetch-media.sh adopting an own-origin .h264.mp4 URL swaps the AV1 back
in as the page's primary when it is on the mount, and records the H.264
as `fallback` in posts.json. The renderer turns a non-empty fallback
into a <source> pair: the AV1 first with an explicit codecs parameter —
both files are video/mp4, so the parameter is the only thing that lets
a non-AV1 browser skip to the file it can play — and the H.264 second.
Browsers with AV1 keep downloading the small file; Safari before 17
gets one that plays instead of an element that will not.
e2e gains the matching conditional gate: a page offering an av01
<source> must offer an .h264.mp4 one, so an AV1 video can never again
ship without its fallback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 03:14:51 +02:00
|
|
|
# The poster and the H.264 fallback are named after the AV1's hash, not
|
|
|
|
|
# their own. That is the whole point: fetch-media.sh finds both by name,
|
|
|
|
|
# with nothing to look up — the poster when the instance made no thumbnail
|
|
|
|
|
# (pict-rs will not read AV1, so for these uploads that is the normal
|
2026-08-18 21:21:41 +02:00
|
|
|
# case), the fallback for every browser that cannot decode AV1. Audio's MP3
|
|
|
|
|
# sibling is named the same way, after the Opus file's hash.
|
media: publish to our own origin instead of mirroring a file host
A 167 MB screen recording uploaded to catbox.moe hit fetch-media.sh's 64 MB
MAX_BYTES, so curl refused it, the entry kept its third-party URL, and e2e
failed "/posts media origin" on a file we had on disk the whole time. Raising
the cap would have papered over it: the mirror step still depends on someone
else's server being up, fast, and still holding the file.
Invert it. tools/publish-media.sh uploads a recording to the media mount under
its content hash BEFORE the post exists and prints the URL to post, and
fetch-media.sh adopts an own-origin URL by rewriting it to /media/<hash> with no
request at all — no size cap, no third party in the build.
Also here, because publishing exposed them:
* Rotation. Phones record 1920x1080 and attach a display matrix rather than
rotating pixels, so an untouched file reports landscape while playing
portrait and width/height reserve exactly the wrong box. publish-media.sh
bakes rotation into the frames; fetch-media.sh swaps the dimensions on a
quarter-turn matrix for anything mirrored straight from a phone.
* Posters. pict-rs will not thumbnail AV1, so a self-hosted video usually
arrives with no poster. publish-media.sh uploads <hash>.poster.webp beside
the video and fetch-media.sh falls back to it — a real thumbnail still wins.
* The workflow comment claiming a file on the mount is never downloaded again
was wrong: the name is the hash of the bytes, so third-party media is
re-fetched every build and only the write is skipped.
Transcoding to AV1 is what makes self-hosting cheap: that clip was 78 s of a
dark room at 17 Mbps, and denoise + AV1 gives the same picture in 15 MB. Note
<video> carries a single src with no fallback, so AV1 excludes Safari < 17;
--raw skips the transcode when that matters.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 01:16:17 +02:00
|
|
|
if [ -n "$poster" ] && [ -f "$poster" ]; then
|
|
|
|
|
publish_file "$poster" "$hash.poster.webp"
|
|
|
|
|
fi
|
media: H.264 fallback beside every AV1, and the post links the H.264
The AV1-only publish lasted four hours in the wild: the first viewer on
mobile Safari got "bad media error" from the raw file, because a post's
link is fetched raw — Lemmy apps and browsers play that exact URL, with
no <source> negotiation in front of it. The previous commit's note
("--raw ... if that audience matters") had it backwards: the audience
that cannot play AV1 is not a per-post judgement call, it is whoever
happens to open the thread on an iPhone.
So publish-media.sh now emits two encodings and points the post at the
compatible one:
* Every video transcode also produces <hash>.h264.mp4 (x264 crf 23,
same denoise, same frames), uploaded beside the AV1 under the AV1's
hash — the poster's sibling-naming trick, reused, so fetch-media.sh
finds it by name with nothing to look up.
* The printed URL to paste into the post is the H.264 one. pict-rs
can thumbnail it too, so instance thumbnails come back as a bonus.
fetch-media.sh adopting an own-origin .h264.mp4 URL swaps the AV1 back
in as the page's primary when it is on the mount, and records the H.264
as `fallback` in posts.json. The renderer turns a non-empty fallback
into a <source> pair: the AV1 first with an explicit codecs parameter —
both files are video/mp4, so the parameter is the only thing that lets
a non-AV1 browser skip to the file it can play — and the H.264 second.
Browsers with AV1 keep downloading the small file; Safari before 17
gets one that plays instead of an element that will not.
e2e gains the matching conditional gate: a page offering an av01
<source> must offer an .h264.mp4 one, so an AV1 video can never again
ship without its fallback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 03:14:51 +02:00
|
|
|
post_name="$name"
|
|
|
|
|
if [ -n "$fallback" ] && [ -f "$fallback" ]; then
|
2026-08-18 21:21:41 +02:00
|
|
|
post_name="$hash.$fallback_suffix"
|
media: H.264 fallback beside every AV1, and the post links the H.264
The AV1-only publish lasted four hours in the wild: the first viewer on
mobile Safari got "bad media error" from the raw file, because a post's
link is fetched raw — Lemmy apps and browsers play that exact URL, with
no <source> negotiation in front of it. The previous commit's note
("--raw ... if that audience matters") had it backwards: the audience
that cannot play AV1 is not a per-post judgement call, it is whoever
happens to open the thread on an iPhone.
So publish-media.sh now emits two encodings and points the post at the
compatible one:
* Every video transcode also produces <hash>.h264.mp4 (x264 crf 23,
same denoise, same frames), uploaded beside the AV1 under the AV1's
hash — the poster's sibling-naming trick, reused, so fetch-media.sh
finds it by name with nothing to look up.
* The printed URL to paste into the post is the H.264 one. pict-rs
can thumbnail it too, so instance thumbnails come back as a bonus.
fetch-media.sh adopting an own-origin .h264.mp4 URL swaps the AV1 back
in as the page's primary when it is on the mount, and records the H.264
as `fallback` in posts.json. The renderer turns a non-empty fallback
into a <source> pair: the AV1 first with an explicit codecs parameter —
both files are video/mp4, so the parameter is the only thing that lets
a non-AV1 browser skip to the file it can play — and the H.264 second.
Browsers with AV1 keep downloading the small file; Safari before 17
gets one that plays instead of an element that will not.
e2e gains the matching conditional gate: a page offering an av01
<source> must offer an .h264.mp4 one, so an AV1 video can never again
ship without its fallback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 03:14:51 +02:00
|
|
|
publish_file "$fallback" "$post_name"
|
|
|
|
|
fi
|
media: publish to our own origin instead of mirroring a file host
A 167 MB screen recording uploaded to catbox.moe hit fetch-media.sh's 64 MB
MAX_BYTES, so curl refused it, the entry kept its third-party URL, and e2e
failed "/posts media origin" on a file we had on disk the whole time. Raising
the cap would have papered over it: the mirror step still depends on someone
else's server being up, fast, and still holding the file.
Invert it. tools/publish-media.sh uploads a recording to the media mount under
its content hash BEFORE the post exists and prints the URL to post, and
fetch-media.sh adopts an own-origin URL by rewriting it to /media/<hash> with no
request at all — no size cap, no third party in the build.
Also here, because publishing exposed them:
* Rotation. Phones record 1920x1080 and attach a display matrix rather than
rotating pixels, so an untouched file reports landscape while playing
portrait and width/height reserve exactly the wrong box. publish-media.sh
bakes rotation into the frames; fetch-media.sh swaps the dimensions on a
quarter-turn matrix for anything mirrored straight from a phone.
* Posters. pict-rs will not thumbnail AV1, so a self-hosted video usually
arrives with no poster. publish-media.sh uploads <hash>.poster.webp beside
the video and fetch-media.sh falls back to it — a real thumbnail still wins.
* The workflow comment claiming a file on the mount is never downloaded again
was wrong: the name is the hash of the bytes, so third-party media is
re-fetched every build and only the write is skipped.
Transcoding to AV1 is what makes self-hosting cheap: that clip was 78 s of a
dark room at 17 Mbps, and denoise + AV1 gives the same picture in 15 MB. Note
<video> carries a single src with no fallback, so AV1 excludes Safari < 17;
--raw skips the transcode when that matters.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 01:16:17 +02:00
|
|
|
|
2026-08-18 21:21:41 +02:00
|
|
|
# The postable URL is the compatibility rail when there is one: the H.264
|
|
|
|
|
# video or the MP3 audio — see FALLBACK and AUDIO above.
|
media: H.264 fallback beside every AV1, and the post links the H.264
The AV1-only publish lasted four hours in the wild: the first viewer on
mobile Safari got "bad media error" from the raw file, because a post's
link is fetched raw — Lemmy apps and browsers play that exact URL, with
no <source> negotiation in front of it. The previous commit's note
("--raw ... if that audience matters") had it backwards: the audience
that cannot play AV1 is not a per-post judgement call, it is whoever
happens to open the thread on an iPhone.
So publish-media.sh now emits two encodings and points the post at the
compatible one:
* Every video transcode also produces <hash>.h264.mp4 (x264 crf 23,
same denoise, same frames), uploaded beside the AV1 under the AV1's
hash — the poster's sibling-naming trick, reused, so fetch-media.sh
finds it by name with nothing to look up.
* The printed URL to paste into the post is the H.264 one. pict-rs
can thumbnail it too, so instance thumbnails come back as a bonus.
fetch-media.sh adopting an own-origin .h264.mp4 URL swaps the AV1 back
in as the page's primary when it is on the mount, and records the H.264
as `fallback` in posts.json. The renderer turns a non-empty fallback
into a <source> pair: the AV1 first with an explicit codecs parameter —
both files are video/mp4, so the parameter is the only thing that lets
a non-AV1 browser skip to the file it can play — and the H.264 second.
Browsers with AV1 keep downloading the small file; Safari before 17
gets one that plays instead of an element that will not.
e2e gains the matching conditional gate: a page offering an av01
<source> must offer an .h264.mp4 one, so an AV1 video can never again
ship without its fallback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 03:14:51 +02:00
|
|
|
url="$SITE_ORIGIN/media/$post_name"
|
media: publish to our own origin instead of mirroring a file host
A 167 MB screen recording uploaded to catbox.moe hit fetch-media.sh's 64 MB
MAX_BYTES, so curl refused it, the entry kept its third-party URL, and e2e
failed "/posts media origin" on a file we had on disk the whole time. Raising
the cap would have papered over it: the mirror step still depends on someone
else's server being up, fast, and still holding the file.
Invert it. tools/publish-media.sh uploads a recording to the media mount under
its content hash BEFORE the post exists and prints the URL to post, and
fetch-media.sh adopts an own-origin URL by rewriting it to /media/<hash> with no
request at all — no size cap, no third party in the build.
Also here, because publishing exposed them:
* Rotation. Phones record 1920x1080 and attach a display matrix rather than
rotating pixels, so an untouched file reports landscape while playing
portrait and width/height reserve exactly the wrong box. publish-media.sh
bakes rotation into the frames; fetch-media.sh swaps the dimensions on a
quarter-turn matrix for anything mirrored straight from a phone.
* Posters. pict-rs will not thumbnail AV1, so a self-hosted video usually
arrives with no poster. publish-media.sh uploads <hash>.poster.webp beside
the video and fetch-media.sh falls back to it — a real thumbnail still wins.
* The workflow comment claiming a file on the mount is never downloaded again
was wrong: the name is the hash of the bytes, so third-party media is
re-fetched every build and only the write is skipped.
Transcoding to AV1 is what makes self-hosting cheap: that clip was 78 s of a
dark room at 17 Mbps, and denoise + AV1 gives the same picture in 15 MB. Note
<video> carries a single src with no fallback, so AV1 excludes Safari < 17;
--raw skips the transcode when that matters.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 01:16:17 +02:00
|
|
|
# Confirm it is actually being served before handing over a URL that is about
|
|
|
|
|
# to be pasted into a post, where a 404 is public and permanent.
|
|
|
|
|
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 30 "$url" || echo 000)
|
|
|
|
|
if [ "$code" = 200 ]; then
|
media: H.264 fallback beside every AV1, and the post links the H.264
The AV1-only publish lasted four hours in the wild: the first viewer on
mobile Safari got "bad media error" from the raw file, because a post's
link is fetched raw — Lemmy apps and browsers play that exact URL, with
no <source> negotiation in front of it. The previous commit's note
("--raw ... if that audience matters") had it backwards: the audience
that cannot play AV1 is not a per-post judgement call, it is whoever
happens to open the thread on an iPhone.
So publish-media.sh now emits two encodings and points the post at the
compatible one:
* Every video transcode also produces <hash>.h264.mp4 (x264 crf 23,
same denoise, same frames), uploaded beside the AV1 under the AV1's
hash — the poster's sibling-naming trick, reused, so fetch-media.sh
finds it by name with nothing to look up.
* The printed URL to paste into the post is the H.264 one. pict-rs
can thumbnail it too, so instance thumbnails come back as a bonus.
fetch-media.sh adopting an own-origin .h264.mp4 URL swaps the AV1 back
in as the page's primary when it is on the mount, and records the H.264
as `fallback` in posts.json. The renderer turns a non-empty fallback
into a <source> pair: the AV1 first with an explicit codecs parameter —
both files are video/mp4, so the parameter is the only thing that lets
a non-AV1 browser skip to the file it can play — and the H.264 second.
Browsers with AV1 keep downloading the small file; Safari before 17
gets one that plays instead of an element that will not.
e2e gains the matching conditional gate: a page offering an av01
<source> must offer an .h264.mp4 one, so an AV1 video can never again
ship without its fallback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 03:14:51 +02:00
|
|
|
printf ' post this: %s\n' "$url"
|
media: publish to our own origin instead of mirroring a file host
A 167 MB screen recording uploaded to catbox.moe hit fetch-media.sh's 64 MB
MAX_BYTES, so curl refused it, the entry kept its third-party URL, and e2e
failed "/posts media origin" on a file we had on disk the whole time. Raising
the cap would have papered over it: the mirror step still depends on someone
else's server being up, fast, and still holding the file.
Invert it. tools/publish-media.sh uploads a recording to the media mount under
its content hash BEFORE the post exists and prints the URL to post, and
fetch-media.sh adopts an own-origin URL by rewriting it to /media/<hash> with no
request at all — no size cap, no third party in the build.
Also here, because publishing exposed them:
* Rotation. Phones record 1920x1080 and attach a display matrix rather than
rotating pixels, so an untouched file reports landscape while playing
portrait and width/height reserve exactly the wrong box. publish-media.sh
bakes rotation into the frames; fetch-media.sh swaps the dimensions on a
quarter-turn matrix for anything mirrored straight from a phone.
* Posters. pict-rs will not thumbnail AV1, so a self-hosted video usually
arrives with no poster. publish-media.sh uploads <hash>.poster.webp beside
the video and fetch-media.sh falls back to it — a real thumbnail still wins.
* The workflow comment claiming a file on the mount is never downloaded again
was wrong: the name is the hash of the bytes, so third-party media is
re-fetched every build and only the write is skipped.
Transcoding to AV1 is what makes self-hosting cheap: that clip was 78 s of a
dark room at 17 Mbps, and denoise + AV1 gives the same picture in 15 MB. Note
<video> carries a single src with no fallback, so AV1 excludes Safari < 17;
--raw skips the transcode when that matters.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 01:16:17 +02:00
|
|
|
else
|
|
|
|
|
echo " WARNING: $url answered HTTP $code — do not post this link yet" >&2
|
|
|
|
|
fi
|
|
|
|
|
printf ' %s -> %s (%s)\n' \
|
|
|
|
|
"$(du -h "$src" | cut -f1)" "$(du -h "$payload" | cut -f1)" "$name"
|
media: H.264 fallback beside every AV1, and the post links the H.264
The AV1-only publish lasted four hours in the wild: the first viewer on
mobile Safari got "bad media error" from the raw file, because a post's
link is fetched raw — Lemmy apps and browsers play that exact URL, with
no <source> negotiation in front of it. The previous commit's note
("--raw ... if that audience matters") had it backwards: the audience
that cannot play AV1 is not a per-post judgement call, it is whoever
happens to open the thread on an iPhone.
So publish-media.sh now emits two encodings and points the post at the
compatible one:
* Every video transcode also produces <hash>.h264.mp4 (x264 crf 23,
same denoise, same frames), uploaded beside the AV1 under the AV1's
hash — the poster's sibling-naming trick, reused, so fetch-media.sh
finds it by name with nothing to look up.
* The printed URL to paste into the post is the H.264 one. pict-rs
can thumbnail it too, so instance thumbnails come back as a bonus.
fetch-media.sh adopting an own-origin .h264.mp4 URL swaps the AV1 back
in as the page's primary when it is on the mount, and records the H.264
as `fallback` in posts.json. The renderer turns a non-empty fallback
into a <source> pair: the AV1 first with an explicit codecs parameter —
both files are video/mp4, so the parameter is the only thing that lets
a non-AV1 browser skip to the file it can play — and the H.264 second.
Browsers with AV1 keep downloading the small file; Safari before 17
gets one that plays instead of an element that will not.
e2e gains the matching conditional gate: a page offering an av01
<source> must offer an .h264.mp4 one, so an AV1 video can never again
ship without its fallback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 03:14:51 +02:00
|
|
|
[ -z "$fallback" ] || printf ' fallback: %s (%s)\n' \
|
|
|
|
|
"$(du -h "$fallback" | cut -f1)" "$post_name"
|
media: publish to our own origin instead of mirroring a file host
A 167 MB screen recording uploaded to catbox.moe hit fetch-media.sh's 64 MB
MAX_BYTES, so curl refused it, the entry kept its third-party URL, and e2e
failed "/posts media origin" on a file we had on disk the whole time. Raising
the cap would have papered over it: the mirror step still depends on someone
else's server being up, fast, and still holding the file.
Invert it. tools/publish-media.sh uploads a recording to the media mount under
its content hash BEFORE the post exists and prints the URL to post, and
fetch-media.sh adopts an own-origin URL by rewriting it to /media/<hash> with no
request at all — no size cap, no third party in the build.
Also here, because publishing exposed them:
* Rotation. Phones record 1920x1080 and attach a display matrix rather than
rotating pixels, so an untouched file reports landscape while playing
portrait and width/height reserve exactly the wrong box. publish-media.sh
bakes rotation into the frames; fetch-media.sh swaps the dimensions on a
quarter-turn matrix for anything mirrored straight from a phone.
* Posters. pict-rs will not thumbnail AV1, so a self-hosted video usually
arrives with no poster. publish-media.sh uploads <hash>.poster.webp beside
the video and fetch-media.sh falls back to it — a real thumbnail still wins.
* The workflow comment claiming a file on the mount is never downloaded again
was wrong: the name is the hash of the bytes, so third-party media is
re-fetched every build and only the write is skipped.
Transcoding to AV1 is what makes self-hosting cheap: that clip was 78 s of a
dark room at 17 Mbps, and denoise + AV1 gives the same picture in 15 MB. Note
<video> carries a single src with no fallback, so AV1 excludes Safari < 17;
--raw skips the transcode when that matters.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 01:16:17 +02:00
|
|
|
done
|