Add lab restream pipeline with pirate_drift logo and lib/env.sh.

Encode-once/fanout scripts, HLS output, and single overloadable env source.
Commits assets/pirate_drift.png as default watermark for cluster runs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Anton Afanasyeu
2026-07-11 15:27:24 +02:00
parent d990862b64
commit 5ca8a9f539
27 changed files with 911 additions and 1 deletions

82
bin/encode-once.sh Executable file
View File

@@ -0,0 +1,82 @@
#!/usr/bin/env bash
# Single encode: ingest → filter (logo/delogo) → tee → local file + named pipe (+ optional HLS).
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck disable=SC1091
source "${ROOT}/lib/common.sh"
# shellcheck disable=SC1091
source "${ROOT}/lib/source.sh"
# shellcheck disable=SC1091
source "${ROOT}/lib/filters.sh"
# shellcheck disable=SC1091
source "${ROOT}/lib/outputs.sh"
resolve_source "${1:-}"
require_cmd "${FFMPEG_BIN}"
require_logo
ensure_storage
ensure_pipe
compute_layout() {
local idx=0
VIDEO_INPUT="${idx}:v"
if [[ "${USE_YTDLP}" == "1" ]]; then
idx=0
else
idx=0
fi
if [[ ${#AUDIO_INPUT_ARGS[@]} -gt 0 ]]; then
AUDIO_MAP_INPUT=$((idx + 1))
LOGO_INPUT=$((idx + 2))
else
AUDIO_MAP_INPUT="${idx}"
LOGO_INPUT=$((idx + 1))
fi
}
compute_layout
RECORD_PATH="$(next_record_path)"
LOG_FILE="${STORAGE_DIR}/encode.log"
build_filter_complex "${VIDEO_INPUT}" "${LOGO_INPUT}:v"
TEE_SPEC="$(build_encode_tee_spec "${RECORD_PATH}" "${PIPE_PATH}")"
MAP_V="$(map_video_out)"
MAP_A="$(map_audio_out)"
run_ffmpeg() {
"${FFMPEG_BIN}" -hide_banner -loglevel info \
-rtbufsize 256M -probesize 100M -analyzeduration 250M \
"$@" \
-i "${LOGO_PATH}" \
"${FFMPEG_TS_FLAGS[@]}" \
-filter_complex "${FILTER_COMPLEX}" \
-map "${MAP_V}" -map "${MAP_A}" \
-threads "${THREADS}" -framerate "${FPS}" -r "${FPS}" \
-s "${RES}" ${VCODEC} ${PRESET} ${BITRATE} ${ACODEC} -bufsize "${BUFSIZE}" \
-f tee "${TEE_SPEC}" >> "${LOG_FILE}" 2>&1
}
cleanup() {
remove_pipe
}
trap cleanup EXIT
log "recording → ${RECORD_PATH}"
log "pipe → ${PIPE_PATH}"
[[ -n "${HLS_OUTPUT_DIR}" ]] && log "HLS → ${HLS_OUTPUT_DIR}"
if [[ "${USE_YTDLP}" == "1" ]]; then
require_cmd "${YTDLP_BIN}"
mapfile -d '' -t YTDLP_CMD < <(build_ytdlp_pipeline)
"${YTDLP_CMD[@]}" 2>>"${LOG_FILE}" | \
run_ffmpeg -thread_queue_size "${THREAD_QUEUE_SIZE}" -i pipe:0 \
"${AUDIO_INPUT_ARGS[@]}"
else
read_rate_flag
run_ffmpeg \
"${RE_FLAG[@]}" \
-thread_queue_size "${THREAD_QUEUE_SIZE}" \
-reconnect_on_network_error 1 -reconnect_streamed 1 -reconnect_delay_max 5 \
-i "${SOURCE_URL}" \
"${AUDIO_INPUT_ARGS[@]}"
fi

81
bin/encode-only.sh Executable file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# Encode once → local recording (+ optional HLS). No named-pipe fanout stage.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck disable=SC1091
source "${ROOT}/lib/common.sh"
# shellcheck disable=SC1091
source "${ROOT}/lib/source.sh"
# shellcheck disable=SC1091
source "${ROOT}/lib/filters.sh"
# shellcheck disable=SC1091
source "${ROOT}/lib/outputs.sh"
resolve_source "${1:-}"
require_cmd "${FFMPEG_BIN}"
require_logo
ensure_storage
compute_layout() {
VIDEO_INPUT="0:v"
if [[ ${#AUDIO_INPUT_ARGS[@]} -gt 0 ]]; then
AUDIO_MAP_INPUT=1
LOGO_INPUT=2
else
AUDIO_MAP_INPUT=0
LOGO_INPUT=1
fi
}
compute_layout
RECORD_PATH="$(next_record_path)"
LOG_FILE="${STORAGE_DIR}/encode.log"
build_filter_complex "${VIDEO_INPUT}" "${LOGO_INPUT}:v"
MAP_V="$(map_video_out)"
MAP_A="$(map_audio_out)"
build_record_tee() {
local record="$1"
local parts=()
parts+=("$(file_tee_entry "${record}")")
if [[ -n "${HLS_OUTPUT_DIR}" ]]; then
parts+=("$(hls_tee_entry "${HLS_OUTPUT_DIR}")")
fi
local IFS='|'
echo "${parts[*]}"
}
TEE_SPEC="$(build_record_tee "${RECORD_PATH}")"
run_ffmpeg() {
"${FFMPEG_BIN}" -hide_banner -loglevel info \
-rtbufsize 256M -probesize 100M -analyzeduration 250M \
"$@" \
-i "${LOGO_PATH}" \
"${FFMPEG_TS_FLAGS[@]}" \
-filter_complex "${FILTER_COMPLEX}" \
-map "${MAP_V}" -map "${MAP_A}" \
-threads "${THREADS}" -framerate "${FPS}" -r "${FPS}" \
-s "${RES}" ${VCODEC} ${PRESET} ${BITRATE} ${ACODEC} -bufsize "${BUFSIZE}" \
-f tee "${TEE_SPEC}" >> "${LOG_FILE}" 2>&1
}
log "encode-only → ${RECORD_PATH}"
[[ -n "${HLS_OUTPUT_DIR}" ]] && log "HLS → ${HLS_OUTPUT_DIR}"
if [[ "${USE_YTDLP}" == "1" ]]; then
require_cmd "${YTDLP_BIN}"
mapfile -d '' -t YTDLP_CMD < <(build_ytdlp_pipeline)
"${YTDLP_CMD[@]}" 2>>"${LOG_FILE}" | \
run_ffmpeg -thread_queue_size "${THREAD_QUEUE_SIZE}" -i pipe:0 \
"${AUDIO_INPUT_ARGS[@]}"
else
read_rate_flag
run_ffmpeg \
"${RE_FLAG[@]}" \
-thread_queue_size "${THREAD_QUEUE_SIZE}" \
-reconnect_on_network_error 1 -reconnect_streamed 1 -reconnect_delay_max 5 \
-i "${SOURCE_URL}" \
"${AUDIO_INPUT_ARGS[@]}"
fi

36
bin/fanout-copy.sh Executable file
View File

@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Fan-out: read pre-encoded FLV from pipe → RTMP sinks (stream copy, no re-encode).
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck disable=SC1091
source "${ROOT}/lib/common.sh"
# shellcheck disable=SC1091
source "${ROOT}/lib/outputs.sh"
require_cmd "${FFMPEG_BIN}"
ensure_storage
ensure_pipe
TEE_SPEC="$(build_fanout_tee_spec)"
LOG_FILE="${STORAGE_DIR}/fanout.log"
MAX_RETRIES="${FANOUT_RETRIES:-5}"
attempt=0
log "fanout → ${#SINKS[@]} sink(s)"
while [[ "${attempt}" -le "${MAX_RETRIES}" ]]; do
attempt=$((attempt + 1))
log "fanout attempt ${attempt}/${MAX_RETRIES}"
if "${FFMPEG_BIN}" -hide_banner -loglevel info \
-thread_queue_size "${THREAD_QUEUE_SIZE}" \
-i "${PIPE_PATH}" \
-map 0 \
-c copy \
-f tee "${TEE_SPEC}" >> "${LOG_FILE}" 2>&1; then
log "fanout finished normally"
break
fi
warn "fanout ffmpeg exited — retrying after pipe flush"
sleep 1
done

8
bin/install-nginx-hls.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env bash
# Lab helper: print nginx fragment with HLS_ROOT substituted.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
HLS_ROOT="${1:-${ROOT}/storage/hls}"
sed "s|@HLS_ROOT@|${HLS_ROOT}|g" "${ROOT}/nginx/hls-location.conf"

51
bin/probe-source.sh Executable file
View File

@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# ffprobe → normalized source profile (draft SPEC-1 preview).
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck disable=SC1091
source "${ROOT}/lib/common.sh"
# shellcheck disable=SC1091
source "${ROOT}/lib/source.sh"
resolve_source "${1:-}"
require_cmd "${FFPROBE_BIN}"
probe_target="${SOURCE_URL}"
if [[ "${USE_YTDLP}" == "1" ]]; then
warn "page URL — ffprobe may fail; use a direct m3u8/mp4 URL or run encode-once"
fi
log "probing ${probe_target}"
"${FFPROBE_BIN}" -hide_banner -loglevel error \
-show_format -show_streams -print_format json \
"${probe_target}" | python3 -c '
import json, sys
data = json.load(sys.stdin)
fmt = data.get("format", {})
streams = data.get("streams", [])
v = next((s for s in streams if s.get("codec_type") == "video"), None)
a = next((s for s in streams if s.get("codec_type") == "audio"), None)
out = {
"duration": fmt.get("duration"),
"bitrate": fmt.get("bit_rate"),
"format": fmt.get("format_name"),
"video": None,
"audio": None,
}
if v:
out["video"] = {
"codec": v.get("codec_name"),
"width": v.get("width"),
"height": v.get("height"),
"fps": v.get("avg_frame_rate"),
"profile": v.get("profile"),
}
if a:
out["audio"] = {
"codec": a.get("codec_name"),
"channels": a.get("channels"),
"sample_rate": a.get("sample_rate"),
}
print(json.dumps(out, indent=2))
'

23
bin/run-encode-only.sh Executable file
View File

@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck disable=SC1091
source "${ROOT}/lib/common.sh"
SRC_ARG="${1:-1}"
PID_ENCODE="${RUN_DIR}/encode.pid"
LOG_RUN="${STORAGE_DIR}/run-encode-only.log"
ensure_storage
mkdir -p "${RUN_DIR}"
if [[ -f "${PID_ENCODE}" ]] && kill -0 "$(cat "${PID_ENCODE}")" 2>/dev/null; then
die "encode-only already running (pid $(cat "${PID_ENCODE}")). use bin/stop.sh first"
fi
chmod +x "${ROOT}/bin/"*.sh 2>/dev/null || true
log "starting encode-only source=${SRC_ARG}"
nohup "${ROOT}/bin/encode-only.sh" "${SRC_ARG}" >> "${LOG_RUN}" 2>&1 &
echo $! > "${PID_ENCODE}"
log "encode pid $(cat "${PID_ENCODE}")"

42
bin/run.sh Executable file
View File

@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# Start encode-once + fanout-copy (two-stage pipeline from v0).
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck disable=SC1091
source "${ROOT}/lib/common.sh"
SRC_ARG="${1:-1}"
PID_ENCODE="${RUN_DIR}/encode.pid"
PID_FANOUT="${RUN_DIR}/fanout.pid"
LOG_RUN="${STORAGE_DIR}/run.log"
ensure_storage
mkdir -p "${RUN_DIR}"
if [[ -f "${PID_ENCODE}" ]] && kill -0 "$(cat "${PID_ENCODE}")" 2>/dev/null; then
die "pipeline already running (encode pid $(cat "${PID_ENCODE}")). use bin/stop.sh first"
fi
chmod +x "${ROOT}/bin/"*.sh 2>/dev/null || true
# shellcheck disable=SC1091
source "${ROOT}/lib/outputs.sh"
read_sinks
log "starting pipeline source=${SRC_ARG}"
nohup "${ROOT}/bin/encode-once.sh" "${SRC_ARG}" >> "${LOG_RUN}" 2>&1 &
echo $! > "${PID_ENCODE}"
sleep 1
if [[ ${#SINKS[@]} -gt 0 ]]; then
nohup "${ROOT}/bin/fanout-copy.sh" >> "${LOG_RUN}" 2>&1 &
echo $! > "${PID_FANOUT}"
log "fanout pid $(cat "${PID_FANOUT}")"
else
log "no RTMP sinks configured — fanout stage skipped"
rm -f "${PID_FANOUT}" 2>/dev/null || true
fi
log "encode pid $(cat "${PID_ENCODE}")"
log "logs: ${LOG_RUN}, ${STORAGE_DIR}/encode.log, ${STORAGE_DIR}/fanout.log"

27
bin/stop.sh Executable file
View File

@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck disable=SC1091
source "${ROOT}/lib/common.sh"
stop_pidfile() {
local pf="$1"
[[ -f "${pf}" ]] || return 0
local pid
pid="$(cat "${pf}")"
if kill -0 "${pid}" 2>/dev/null; then
kill "${pid}" 2>/dev/null || true
sleep 1
kill -9 "${pid}" 2>/dev/null || true
fi
rm -f "${pf}"
}
stop_pidfile "${RUN_DIR}/fanout.pid"
stop_pidfile "${RUN_DIR}/encode.pid"
pkill -f "${ROOT}/bin/fanout-copy.sh" 2>/dev/null || true
pkill -f "${ROOT}/bin/encode-once.sh" 2>/dev/null || true
remove_pipe
rm -f "${ROOT}/.intro_shown" "${ROOT}/.current_source" 2>/dev/null || true
log "pipeline stopped"

50
bin/switch-source.sh Executable file
View File

@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# Stop pipeline, optionally rotate source index, restart (v0 switch_quality.sh cleanup).
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck disable=SC1091
source "${ROOT}/lib/common.sh"
VARIANT="${1:-}"
SRC="${SRC:-}"
if [[ "${VARIANT}" == "stop" ]]; then
exec "${ROOT}/bin/stop.sh"
fi
case "${VARIANT}" in
blur|blurred) export FILTER_PROFILE=blurred ;;
nologo) export FILTER_PROFILE=nologo ;;
default|'') export FILTER_PROFILE=default ;;
encode-only|record)
export RUN_ENCODE_ONLY=1
;;
[0-9]*)
SRC="${VARIANT}"
;;
esac
if [[ -z "${SRC}" && -f "${ROOT}/.current_source" ]]; then
# rotate index in sources.list
if [[ -f "${SOURCES_LIST}" ]]; then
count="$(grep -vc '^[[:space:]]*#' "${SOURCES_LIST}" || true)"
cur=1
[[ -f "${ROOT}/.current_index" ]] && cur="$(cat "${ROOT}/.current_index")"
cur=$((cur + 1))
[[ "${count}" -gt 0 && "${cur}" -gt "${count}" ]] && cur=1
echo "${cur}" > "${ROOT}/.current_index"
SRC="${cur}"
fi
fi
SRC="${SRC:-1}"
"${ROOT}/bin/stop.sh"
sleep 1
log "switch → source index ${SRC} profile ${FILTER_PROFILE}"
if [[ -n "${RUN_ENCODE_ONLY:-}" || "${VARIANT}" == "encode-only" || "${VARIANT}" == "record" ]]; then
exec "${ROOT}/bin/run-encode-only.sh" "${SRC}"
fi
exec "${ROOT}/bin/run.sh" "${SRC}"