Compare commits
3 Commits
f06ebc1ec1
...
2510cfaceb
| Author | SHA1 | Date | |
|---|---|---|---|
| 2510cfaceb | |||
| f17e57db63 | |||
| 94db824243 |
@@ -18,3 +18,5 @@ MAIN_HOST=media.example.com
|
|||||||
LIVE_WEBHOOK_URL=
|
LIVE_WEBHOOK_URL=
|
||||||
BOT_REQUIRE_GROUP=bot-users
|
BOT_REQUIRE_GROUP=bot-users
|
||||||
LIVE_REQUIRE_GROUP=
|
LIVE_REQUIRE_GROUP=
|
||||||
|
APEX_HOST=
|
||||||
|
APEX_REDIRECT=
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ Source of truth: this repo, `techy/Media-Server-Setup` on the stack's own Gitea
|
|||||||
## Verified behaviours worth remembering
|
## Verified behaviours worth remembering
|
||||||
- **Never scp generated configs** (`mediamtx.yml`, `copyparty.conf`, `Caddyfile`, `dump-users.txt`, `.env`): a template copy wiped all streamers once. They are gitignored now; `*.example.*` are reference only.
|
- **Never scp generated configs** (`mediamtx.yml`, `copyparty.conf`, `Caddyfile`, `dump-users.txt`, `.env`): a template copy wiped all streamers once. They are gitignored now; `*.example.*` are reference only.
|
||||||
- Caddy runs in a container: anything it serves from disk must be a mount (`/srv/live` was missing → 404).
|
- Caddy runs in a container: anything it serves from disk must be a mount (`/srv/live` was missing → 404).
|
||||||
|
- MediaMTX stamps recording filenames in its container clock: without `TZ` it is UTC and the plugin reads names as Eastern (4 h off). `TZ=America/New_York` is set on the container.
|
||||||
- MediaMTX 1.21 refuses to start without `webrtcAdditionalHosts` or `webrtcIPsFromInterfaces`; configure.py writes the public host in.
|
- MediaMTX 1.21 refuses to start without `webrtcAdditionalHosts` or `webrtcIPsFromInterfaces`; configure.py writes the public host in.
|
||||||
- Jellyfin honours provider-supplied season numbers on a Shows library (day seasons appeared). Next check: Monday 04:10 ET should fold them into a week season.
|
- Jellyfin honours provider-supplied season numbers on a Shows library (day seasons appeared). Next check: Monday 04:10 ET should fold them into a week season.
|
||||||
- A stream indexed by Jellyfin while still recording has no duration and skips to the next episode on seek — fixed by recording into `.live/` so nothing sees a file until it is finished and flat.
|
- A stream indexed by Jellyfin while still recording has no duration and skips to the next episode on seek — fixed by recording into `.live/` so nothing sees a file until it is finished and flat.
|
||||||
|
|||||||
@@ -52,10 +52,12 @@ probe() {
|
|||||||
local head; head=$(head -c 2097152 "$f" | grep -aob -m3 -E 'moov|mdat|moof' | tr '\n' ' ')
|
local head; head=$(head -c 2097152 "$f" | grep -aob -m3 -E 'moov|mdat|moof' | tr '\n' ' ')
|
||||||
if grep -q moof <<<"$head"; then echo "remux fragmented"; return; fi
|
if grep -q moof <<<"$head"; then echo "remux fragmented"; return; fi
|
||||||
# real VFR test: do the gaps between the first ~400 frames vary? (dts is monotonic even with B-frames)
|
# real VFR test: do the gaps between the first ~400 frames vary? (dts is monotonic even with B-frames)
|
||||||
|
# portable awk (Debian's mawk has no asort): deltas -> sort -n -> median + spread
|
||||||
local vfr; vfr=$(ffprobe -v error -select_streams v:0 -show_entries packet=dts_time -of csv=p=0 -read_intervals '%+#400' "$f" 2>/dev/null |
|
local vfr; vfr=$(ffprobe -v error -select_streams v:0 -show_entries packet=dts_time -of csv=p=0 -read_intervals '%+#400' "$f" 2>/dev/null |
|
||||||
awk 'NF && $1!="N/A" { if (p!="") d[n++]=$1-p; p=$1 }
|
awk 'NF && $1!="N/A" { if (p!="") print $1-p; p=$1 }' | sort -n |
|
||||||
|
awk '{ d[n++]=$1 }
|
||||||
END { if (n<20) { print "cfr"; exit }
|
END { if (n<20) { print "cfr"; exit }
|
||||||
for(i=0;i<n;i++) a[i]=d[i]; asort(a); med=a[int(n/2)]; if (med<=0) { print "cfr"; exit }
|
med=d[int(n/2)]; if (med<=0) { print "cfr"; exit }
|
||||||
bad=0; for(i=0;i<n;i++){ x=d[i]-med; if(x<0)x=-x; if (x > med*0.25) bad++ }
|
bad=0; for(i=0;i<n;i++){ x=d[i]-med; if(x<0)x=-x; if (x > med*0.25) bad++ }
|
||||||
print (bad > n*0.02) ? "vfr" : "cfr" }')
|
print (bad > n*0.02) ? "vfr" : "cfr" }')
|
||||||
[[ $vfr == vfr ]] && { echo "encode vfr"; return; }
|
[[ $vfr == vfr ]] && { echo "encode vfr"; return; }
|
||||||
|
|||||||
19
configure.py
19
configure.py
@@ -152,6 +152,14 @@ def gate(required_group=""):
|
|||||||
def write_caddy(domain, auth_host="", watch_host="", git_host="", bot_host="", live_host=""):
|
def write_caddy(domain, auth_host="", watch_host="", git_host="", bot_host="", live_host=""):
|
||||||
env = read_env()
|
env = read_env()
|
||||||
extra = ""
|
extra = ""
|
||||||
|
apex, target = env.get("APEX_HOST", ""), env.get("APEX_REDIRECT", "")
|
||||||
|
if apex and target:
|
||||||
|
extra += f"""
|
||||||
|
# vanguard:apex
|
||||||
|
{apex} {{
|
||||||
|
redir {target} 302
|
||||||
|
}}
|
||||||
|
"""
|
||||||
if live_host:
|
if live_host:
|
||||||
extra += f"""
|
extra += f"""
|
||||||
# vanguard:live
|
# vanguard:live
|
||||||
@@ -253,6 +261,8 @@ def write_env(watch_host, discord_token=None, discord_guild=None, git_host="", b
|
|||||||
if main_host: env["MAIN_HOST"] = main_host
|
if main_host: env["MAIN_HOST"] = main_host
|
||||||
if live_webhook is not None: env["LIVE_WEBHOOK_URL"] = live_webhook
|
if live_webhook is not None: env["LIVE_WEBHOOK_URL"] = live_webhook
|
||||||
env.setdefault("LIVE_WEBHOOK_URL", "")
|
env.setdefault("LIVE_WEBHOOK_URL", "")
|
||||||
|
env.setdefault("APEX_HOST", "") # bare domain to serve (A record -> this box); empty = skip
|
||||||
|
env.setdefault("APEX_REDIRECT", "") # where the bare domain sends people, e.g. https://watch.HOST/
|
||||||
env.setdefault("BOT_REQUIRE_GROUP", "bot-users") # Caddy 403s anyone not in this Authentik group; empty = any logged-in member
|
env.setdefault("BOT_REQUIRE_GROUP", "bot-users") # Caddy 403s anyone not in this Authentik group; empty = any logged-in member
|
||||||
env.setdefault("LIVE_REQUIRE_GROUP", "")
|
env.setdefault("LIVE_REQUIRE_GROUP", "")
|
||||||
env["GIT_HOST"] = git_host or env.get("GIT_HOST", "git.example.com")
|
env["GIT_HOST"] = git_host or env.get("GIT_HOST", "git.example.com")
|
||||||
@@ -438,6 +448,8 @@ def apply(domain, admin_pw, streamers, editors, auth_host="", watch_host="", dis
|
|||||||
|
|
||||||
def show(domain, admin_pw, streamers, editors, auth_host="", watch_host="", git_host="", bot_host="", live_host=""):
|
def show(domain, admin_pw, streamers, editors, auth_host="", watch_host="", git_host="", bot_host="", live_host=""):
|
||||||
print(f"\n== domain : {domain or '(not set)'}")
|
print(f"\n== domain : {domain or '(not set)'}")
|
||||||
|
_e = read_env()
|
||||||
|
if _e.get("APEX_HOST") and _e.get("APEX_REDIRECT"): print(f"== apex : https://{_e['APEX_HOST']}/ -> {_e['APEX_REDIRECT']}")
|
||||||
print(f"== live : https://{live_host}/ (behind Authentik forward-auth; deploy live/dist -> /srv/live; WebRTC needs 8189 open)" if live_host else "== live : (not set — run --full)")
|
print(f"== live : https://{live_host}/ (behind Authentik forward-auth; deploy live/dist -> /srv/live; WebRTC needs 8189 open)" if live_host else "== live : (not set — run --full)")
|
||||||
print(f"== bot web : https://{bot_host}/ (Authentik forward-auth; Caddy requires group '{read_env().get('BOT_REQUIRE_GROUP','bot-users') or 'any'}')" if bot_host else "== bot web : (not set — run --full)")
|
print(f"== bot web : https://{bot_host}/ (Authentik forward-auth; Caddy requires group '{read_env().get('BOT_REQUIRE_GROUP','bot-users') or 'any'}')" if bot_host else "== bot web : (not set — run --full)")
|
||||||
print(f"== gitea : https://{git_host}/ ssh port 2222 (runner: ./registerGiteaRunner.sh)" if git_host else "== gitea : (not set — run --full)")
|
print(f"== gitea : https://{git_host}/ ssh port 2222 (runner: ./registerGiteaRunner.sh)" if git_host else "== gitea : (not set — run --full)")
|
||||||
@@ -492,6 +504,9 @@ def full(domain, admin_pw, streamers, editors, auth_host="", watch_host="", git_
|
|||||||
git_host = ask("Gitea hostname (CNAME to the above)", git_host or default_sub(domain, "git"))
|
git_host = ask("Gitea hostname (CNAME to the above)", git_host or default_sub(domain, "git"))
|
||||||
bot_host = ask("Bot web UI hostname (CNAME to the above)", bot_host or default_sub(domain, "bot"))
|
bot_host = ask("Bot web UI hostname (CNAME to the above)", bot_host or default_sub(domain, "bot"))
|
||||||
live_host = ask("Live portal hostname (CNAME to the above)", live_host or default_sub(domain, "live"))
|
live_host = ask("Live portal hostname (CNAME to the above)", live_host or default_sub(domain, "live"))
|
||||||
|
env0 = read_env(); parent = default_sub(domain, "x")[2:] if domain else ""
|
||||||
|
apex = ask("Bare domain to serve too (A record must point here; blank = skip)", env0.get("APEX_HOST", parent))
|
||||||
|
apex_to = ask(" bare domain redirects to", env0.get("APEX_REDIRECT") or (f"https://{watch_host}/" if watch_host else "")) if apex else ""
|
||||||
print("\n-- Discord bot (optional; the Bot added to the Authentik Discord application). Enter keeps the current value.")
|
print("\n-- Discord bot (optional; the Bot added to the Authentik Discord application). Enter keeps the current value.")
|
||||||
discord_token, discord_guild = ask_discord()
|
discord_token, discord_guild = ask_discord()
|
||||||
live_webhook = ask(" Discord webhook URL for 'is live' notices (optional)", read_env().get("LIVE_WEBHOOK_URL", ""))
|
live_webhook = ask(" Discord webhook URL for 'is live' notices (optional)", read_env().get("LIVE_WEBHOOK_URL", ""))
|
||||||
@@ -517,6 +532,10 @@ def full(domain, admin_pw, streamers, editors, auth_host="", watch_host="", git_
|
|||||||
n = ask_name("editor", editors)
|
n = ask_name("editor", editors)
|
||||||
if not n: break
|
if not n: break
|
||||||
editors[n] = ask_pw(n)
|
editors[n] = ask_pw(n)
|
||||||
|
e = read_env(); e["APEX_HOST"] = apex; e["APEX_REDIRECT"] = apex_to
|
||||||
|
Path(".env").write_text("".join(f"{k}={v}\n" for k, v in e.items())) if Path(".env").exists() else None
|
||||||
|
if not Path(".env").exists():
|
||||||
|
Path(".env").write_text(f"APEX_HOST={apex}\nAPEX_REDIRECT={apex_to}\n"); os.chmod(".env", 0o600)
|
||||||
return domain, admin_pw, streamers, editors, auth_host, watch_host, discord_token, discord_guild, git_host, bot_host, live_host, live_webhook
|
return domain, admin_pw, streamers, editors, auth_host, watch_host, discord_token, discord_guild, git_host, bot_host, live_host, live_webhook
|
||||||
|
|
||||||
def ask_discord():
|
def ask_discord():
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ services:
|
|||||||
container_name: mediamtx
|
container_name: mediamtx
|
||||||
user: "1000:1000" # recordings owned by the debian user, so the archive pull can delete them
|
user: "1000:1000" # recordings owned by the debian user, so the archive pull can delete them
|
||||||
environment:
|
environment:
|
||||||
|
TZ: America/New_York # recording filenames are stamped in the container's clock; must match the host
|
||||||
LIVE_WEBHOOK_URL: ${LIVE_WEBHOOK_URL:-}
|
LIVE_WEBHOOK_URL: ${LIVE_WEBHOOK_URL:-}
|
||||||
LIVE_HOST: ${LIVE_HOST:-live.example.com}
|
LIVE_HOST: ${LIVE_HOST:-live.example.com}
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
Reference in New Issue
Block a user