Skip to content

Commit c501b73

Browse files
committed
fix(edge): sync Caddy HLS ladder playback routes
1 parent a7d3366 commit c501b73

4 files changed

Lines changed: 201 additions & 12 deletions

File tree

scripts/deploy-release-over-ssh.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ if [[ "$role" == "control-plane" ]]; then
172172
else
173173
echo "Deploying edge on $target"
174174
remote_sudo \
175-
"cd $remote_dir_q && scripts/preflight-edge-host.sh --manifest release-manifest.json --expected-platform $(shell_quote "$expected_platform") --skip-bind-port-check && scripts/deploy-edge-host.sh --manifest release-manifest.json --expected-platform $(shell_quote "$expected_platform") --dry-run && scripts/deploy-edge-host.sh --manifest release-manifest.json --expected-platform $(shell_quote "$expected_platform") && curl -fsS http://127.0.0.1:4100/readyz && curl -fsS http://127.0.0.1:4100/healthz"
175+
"cd $remote_dir_q && scripts/sync-edge-caddy-playback-routes.sh && scripts/preflight-edge-host.sh --manifest release-manifest.json --expected-platform $(shell_quote "$expected_platform") --skip-bind-port-check && scripts/deploy-edge-host.sh --manifest release-manifest.json --expected-platform $(shell_quote "$expected_platform") --dry-run && scripts/deploy-edge-host.sh --manifest release-manifest.json --expected-platform $(shell_quote "$expected_platform") && curl -fsS http://127.0.0.1:4100/readyz && curl -fsS http://127.0.0.1:4100/healthz"
176176
fi
177177

178178
echo "$role deploy completed on $target"

scripts/playback-readiness-gate.mjs

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -607,35 +607,58 @@ function artifactPathFromUrl(urlValue, assetId) {
607607
return parts.slice(2).join("/");
608608
}
609609

610+
function isHlsRenditionManifestPath(artifactPath) {
611+
return /^hls\/(?:720p|1080p|2k|4k)\/index\.m3u8$/.test(artifactPath);
612+
}
613+
614+
function isHlsSegmentPath(artifactPath) {
615+
return /^hls\/(?:(?:720p|1080p|2k|4k)\/)?segment_[0-9]+\.ts$/.test(artifactPath);
616+
}
617+
610618
function extractBootstrapArtifacts(bootstrap, assetId) {
611619
const hints = Array.isArray(bootstrap.prefetch_hints) ? bootstrap.prefetch_hints : [];
612-
const firstSegment = hints.find((hint) => typeof hint.artifact_path === "string" && hint.artifact_path.endsWith(".ts"));
613-
if (!bootstrap.opener_url || !bootstrap.manifest_url || !firstSegment?.url) {
614-
throw new SafeError("playback bootstrap did not include opener, manifest, and first segment hints");
620+
const renditionManifest = hints.find((hint) =>
621+
typeof hint.artifact_path === "string" && isHlsRenditionManifestPath(hint.artifact_path) && hint.url
622+
);
623+
const firstSegment = hints.find((hint) =>
624+
typeof hint.artifact_path === "string" && isHlsSegmentPath(hint.artifact_path) && hint.url
625+
);
626+
if (!bootstrap.manifest_url || !renditionManifest?.url || !firstSegment?.url) {
627+
throw new SafeError("playback bootstrap did not include HLS master, startup playlist, and first segment hints");
615628
}
616629
const artifacts = [
617-
{
618-
label: "opener",
619-
artifact_path: artifactPathFromUrl(bootstrap.opener_url, assetId),
620-
content_type: bootstrap.opener_content_type || "video/mp4",
621-
},
622630
{
623631
label: "manifest",
624632
artifact_path: artifactPathFromUrl(bootstrap.manifest_url, assetId),
625633
content_type: bootstrap.manifest_content_type || "application/vnd.apple.mpegurl",
626634
},
635+
{
636+
label: "variant_manifest",
637+
artifact_path: artifactPathFromUrl(renditionManifest.url, assetId),
638+
content_type: renditionManifest.content_type || "application/vnd.apple.mpegurl",
639+
},
627640
{
628641
label: "segment",
629642
artifact_path: artifactPathFromUrl(firstSegment.url, assetId),
630643
content_type: firstSegment.content_type || "video/mp2t",
631644
},
632645
];
633-
if (artifacts[0].artifact_path !== "opener.mp4") {
634-
throw new SafeError("playback bootstrap opener did not use opener.mp4");
646+
if (bootstrap.opener_url) {
647+
artifacts.push({
648+
label: "opener",
649+
artifact_path: artifactPathFromUrl(bootstrap.opener_url, assetId),
650+
content_type: bootstrap.opener_content_type || "video/mp4",
651+
});
635652
}
636-
if (artifacts[1].artifact_path !== "hls/master.m3u8") {
653+
if (artifacts[0].artifact_path !== "hls/master.m3u8") {
637654
throw new SafeError("playback bootstrap manifest did not use hls/master.m3u8");
638655
}
656+
if (!isHlsRenditionManifestPath(artifacts[1].artifact_path)) {
657+
throw new SafeError("playback bootstrap startup playlist did not use a supported HLS rendition path");
658+
}
659+
if (!isHlsSegmentPath(artifacts[2].artifact_path)) {
660+
throw new SafeError("playback bootstrap first segment did not use a supported HLS segment path");
661+
}
639662
return artifacts;
640663
}
641664

@@ -1176,6 +1199,7 @@ async function runFixture(config, fixtureName, thresholds, warnings, failures, c
11761199
timings: {
11771200
opener: summarizeArtifactTimings(edgeResults, "opener"),
11781201
manifest: summarizeArtifactTimings(edgeResults, "manifest"),
1202+
variant_manifest: summarizeArtifactTimings(edgeResults, "variant_manifest"),
11791203
segment: summarizeArtifactTimings(edgeResults, "segment"),
11801204
},
11811205
cache_mix: aggregateCacheMix(edgeResults),
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
caddyfile="${REND_CADDYFILE:-/etc/caddy/Caddyfile}"
5+
dry_run=false
6+
skip_reload=false
7+
8+
usage() {
9+
cat <<'EOF'
10+
Usage: scripts/sync-edge-caddy-playback-routes.sh [options]
11+
12+
Patch the edge Caddy public playback matcher so HLS ladder playlists and
13+
segments reach rend-edge. The script edits only the signed playback regexp,
14+
backs up the Caddyfile, validates the result, and reloads Caddy.
15+
16+
Options:
17+
--caddyfile FILE Caddyfile path. Default: /etc/caddy/Caddyfile.
18+
--dry-run Print whether the file would change without writing.
19+
--skip-reload Skip caddy fmt/validate and systemctl reload.
20+
-h, --help Show this help.
21+
EOF
22+
}
23+
24+
die() {
25+
echo "error: $*" >&2
26+
exit 1
27+
}
28+
29+
require_command() {
30+
command -v "$1" >/dev/null 2>&1 || die "$1 is required"
31+
}
32+
33+
while [[ $# -gt 0 ]]; do
34+
case "$1" in
35+
--caddyfile)
36+
caddyfile="${2:?missing value for $1}"
37+
shift 2
38+
;;
39+
--dry-run)
40+
dry_run=true
41+
shift
42+
;;
43+
--skip-reload)
44+
skip_reload=true
45+
shift
46+
;;
47+
-h | --help)
48+
usage
49+
exit 0
50+
;;
51+
*)
52+
die "unknown argument: $1"
53+
;;
54+
esac
55+
done
56+
57+
[[ -f "$caddyfile" ]] || die "missing Caddyfile: $caddyfile"
58+
require_command python3
59+
60+
tmp="$(mktemp "${TMPDIR:-/tmp}/rend-edge-caddy.XXXXXX")"
61+
cleanup() {
62+
rm -f "$tmp"
63+
}
64+
trap cleanup EXIT
65+
66+
patch_status="$(
67+
python3 - "$caddyfile" "$tmp" <<'PY'
68+
import sys
69+
from pathlib import Path
70+
71+
source = Path(sys.argv[1])
72+
target = Path(sys.argv[2])
73+
ladder = r"hls/(720p|1080p|2k|4k)/(index\.m3u8|segment_[0-9]+\.ts)"
74+
strict_pattern = (
75+
r"^/v/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-"
76+
r"[0-9a-f]{4}-[0-9a-f]{12}/"
77+
r"(opener\.mp4|hls/master\.m3u8|hls/segment_[0-9]+\.ts|"
78+
+ ladder
79+
+ r")$"
80+
)
81+
lines = source.read_text(encoding="utf-8").splitlines(keepends=True)
82+
found = False
83+
changed = False
84+
output = []
85+
86+
for line in lines:
87+
stripped = line.rstrip("\n")
88+
newline = "\n" if line.endswith("\n") else ""
89+
leading = stripped[: len(stripped) - len(stripped.lstrip())]
90+
tokens = stripped.strip().split()
91+
if (
92+
"path_regexp" in line
93+
and "^/v/" in line
94+
and "hls/master\\.m3u8" in line
95+
and "hls/segment_[0-9]+\\.ts" in line
96+
):
97+
found = True
98+
if ladder in stripped:
99+
output.append(line)
100+
continue
101+
if not stripped.endswith(")$"):
102+
raise SystemExit("signed playback regexp has an unsupported shape")
103+
output.append(f"{stripped[:-2]}|{ladder})${newline}")
104+
changed = True
105+
elif (
106+
len(tokens) >= 2
107+
and "/v/" in line
108+
and "hls/master.m3u8" in line
109+
and "hls/segment_" in line
110+
and ("path" in {tokens[0], tokens[1]})
111+
):
112+
found = True
113+
if tokens[0].startswith("@") and tokens[1] == "path":
114+
output.append(f"{leading}{tokens[0]} path_regexp canonical_playback {strict_pattern}{newline}")
115+
elif tokens[0] == "path":
116+
output.append(f"{leading}path_regexp canonical_playback {strict_pattern}{newline}")
117+
else:
118+
raise SystemExit("signed playback path matcher has an unsupported shape")
119+
changed = True
120+
else:
121+
output.append(line)
122+
123+
if not found:
124+
raise SystemExit("signed playback regexp was not found")
125+
126+
target.write_text("".join(output), encoding="utf-8")
127+
print("changed" if changed else "unchanged")
128+
PY
129+
)"
130+
131+
case "$patch_status" in
132+
changed | unchanged) ;;
133+
*) die "unexpected patch status: $patch_status" ;;
134+
esac
135+
136+
if [[ "$patch_status" == "unchanged" ]]; then
137+
echo "Edge Caddy playback routes already include HLS ladder paths"
138+
exit 0
139+
fi
140+
141+
if [[ "$dry_run" == "true" ]]; then
142+
echo "Edge Caddy playback routes would be updated in $caddyfile"
143+
exit 0
144+
fi
145+
146+
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
147+
cp -p "$caddyfile" "${caddyfile}.bak.${stamp}"
148+
cat "$tmp" > "$caddyfile"
149+
echo "Updated edge Caddy playback routes in $caddyfile"
150+
151+
if [[ "$skip_reload" == "true" ]]; then
152+
echo "Skipped Caddy validate/reload"
153+
exit 0
154+
fi
155+
156+
require_command caddy
157+
require_command systemctl
158+
159+
caddy fmt --overwrite "$caddyfile"
160+
caddy validate --config "$caddyfile"
161+
systemctl reload caddy
162+
echo "Caddy reload completed"

scripts/validate-edge-deploy-templates.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ require_file scripts/deploy-edge-host.sh
186186
require_file scripts/quarantine-telemetry-spool-lines.sh
187187
require_file scripts/verify-first-host-deploy.sh
188188
require_file scripts/sync-edge-deploy-env-over-ssh.sh
189+
require_file scripts/sync-edge-caddy-playback-routes.sh
189190

190191
require_compose_service compose.yml rend-api
191192
require_compose_service compose.yml rend-media-worker
@@ -269,6 +270,8 @@ require_contains scripts/verify-first-host-deploy.sh "operator_psql_database_url
269270
require_contains scripts/verify-first-host-deploy.sh "/v/not-a-uuid/hls/master.m3u8"
270271
require_contains scripts/verify-first-host-deploy.sh "rend_edge_telemetry_spool_bytes"
271272
require_contains scripts/sync-edge-deploy-env-over-ssh.sh "REND_EDGE_CORS_ALLOWED_ORIGINS"
273+
require_contains scripts/sync-edge-caddy-playback-routes.sh "hls/(720p|1080p|2k|4k)/(index\\.m3u8|segment_[0-9]+\\.ts)"
274+
require_contains scripts/deploy-release-over-ssh.sh "scripts/sync-edge-caddy-playback-routes.sh"
272275
require_contains .github/workflows/release-deploy.yml "REND_EDGE_CORS_ALLOWED_ORIGINS"
273276
require_contains .github/workflows/release-deploy.yml "scripts/sync-edge-deploy-env-over-ssh.sh"
274277

0 commit comments

Comments
 (0)