#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" STREAMER_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" BUILD_DIR="${STREAMER_ROOT}/build" BUILD_BIN_DIR="${BUILD_DIR}/bin" EVIDENCE_ROOT="${STREAMER_ROOT}/.sisyphus/evidence" TASK_EVIDENCE_DIR="${EVIDENCE_ROOT}/task-14-acceptance" SUMMARY_HELPER="${SCRIPT_DIR}/acceptance_summary_helper.py" RUN_ID="" RUN_DIR="" MANIFEST_TSV="" SUMMARY_JSON="" LATEST_SUMMARY_JSON="${EVIDENCE_ROOT}/task-14-acceptance-summary.json" EVIDENCE_TEXT="${EVIDENCE_ROOT}/task-14-acceptance.txt" STARTED_AT_UTC="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" mkdir -p "${TASK_EVIDENCE_DIR}" allocate_run_dir() { local attempts=0 while (( attempts < 50 )); do local candidate_id candidate_id="$(date +"%Y%m%dT%H%M%S")-$(date +"%N")-p$$-$RANDOM" local candidate_dir="${TASK_EVIDENCE_DIR}/${candidate_id}" if mkdir "${candidate_dir}" 2>/dev/null; then RUN_ID="${candidate_id}" RUN_DIR="${candidate_dir}" MANIFEST_TSV="${RUN_DIR}/rows.tsv" SUMMARY_JSON="${RUN_DIR}/summary.json" return 0 fi attempts=$((attempts + 1)) sleep 0.01 done echo "failed to allocate unique acceptance run directory" >&2 return 1 } allocate_run_dir || exit 1 RUN_HASH="$(printf '%s' "${RUN_ID}" | cksum | awk '{print $1}')" PORT_OFFSET="$((RUN_HASH % 1000))" RTP_PORT_BASE="$((51040 + PORT_OFFSET))" RTMP_PORT_BASE="$((19360 + PORT_OFFSET))" echo -e "order\trow_id\tname\tprotocol\tcodec\ttransport\tstatus\treason\tduration_ms\temitter_rc\treceiver_rc\temitter_log\treceiver_log\tsdp_path" > "${MANIFEST_TSV}" cleanup_pids=() cleanup_all() { for pid in "${cleanup_pids[@]:-}"; do if [[ -n "${pid}" ]] && kill -0 "${pid}" 2>/dev/null; then kill "${pid}" 2>/dev/null || true wait "${pid}" 2>/dev/null || true fi done } trap cleanup_all EXIT binary_exists() { local path="$1" [[ -x "${path}" ]] } binary_candidates() { local name="$1" printf '%s\n' "${BUILD_BIN_DIR}/${name}" "${BUILD_DIR}/${name}" } resolve_binary() { local name="$1" local candidate="" while IFS= read -r candidate; do if binary_exists "${candidate}"; then printf '%s\n' "${candidate}" return 0 fi done < <(binary_candidates "${name}") return 1 } wait_pid() { local pid="$1" local timeout_s="$2" local elapsed=0 while kill -0 "${pid}" 2>/dev/null; do if (( elapsed >= timeout_s )); then kill "${pid}" 2>/dev/null || true wait "${pid}" 2>/dev/null || true return 124 fi sleep 1 elapsed=$((elapsed + 1)) done wait "${pid}" 2>/dev/null return $? } append_manifest_row() { local order="$1" local row_id="$2" local name="$3" local protocol="$4" local codec="$5" local transport="$6" local status="$7" local reason="$8" local duration_ms="$9" local emitter_rc="${10}" local receiver_rc="${11}" local emitter_log="${12}" local receiver_log="${13}" local sdp_path="${14}" echo -e "${order}\t${row_id}\t${name}\t${protocol}\t${codec}\t${transport}\t${status}\t${reason}\t${duration_ms}\t${emitter_rc}\t${receiver_rc}\t${emitter_log}\t${receiver_log}\t${sdp_path}" >> "${MANIFEST_TSV}" } run_rtp_row() { local order="$1" local row_id="$2" local codec="$3" local row_dir="${RUN_DIR}/${order}-${row_id}" mkdir -p "${row_dir}" local emitter_log="${row_dir}/rtp_output.log" local receiver_log="${row_dir}/rtp_receiver.log" local sdp_path="${row_dir}/stream.sdp" local port port="$((RTP_PORT_BASE + (order - 1) * 2))" local payload_type=96 if [[ "${codec}" == "h265" ]]; then payload_type=98 fi local row_start_ms row_start_ms="$(date +%s%3N)" "${RTP_RECEIVER_TESTER}" \ --port "${port}" \ --expect-pt "${payload_type}" \ --packet-threshold 1 \ --timeout-ms 12000 >"${receiver_log}" 2>&1 & local receiver_pid=$! cleanup_pids+=("${receiver_pid}") sleep 1 set +e "${RTP_OUTPUT_TESTER}" \ --host 127.0.0.1 \ --port "${port}" \ --payload-type "${payload_type}" \ --codec "${codec}" \ --encoder-device software \ --sdp-path "${sdp_path}" \ --frames 48 \ --width 320 \ --height 240 \ --frame-interval-ms 20 >"${emitter_log}" 2>&1 local emitter_rc=$? set -e set +e wait_pid "${receiver_pid}" 20 local receiver_rc=$? set -e local row_end_ms row_end_ms="$(date +%s%3N)" local duration_ms=$((row_end_ms - row_start_ms)) local status="PASS" local reason="all-processes-ok" if (( emitter_rc != 0 || receiver_rc != 0 )); then status="FAIL" reason="emitter_rc=${emitter_rc},receiver_rc=${receiver_rc}" if (( receiver_rc == 0 )) && grep -Eq "Broken pipe|Connection reset by peer" "${emitter_log}"; then status="PASS" reason="receiver exited cleanly after threshold; emitter observed peer close" fi fi append_manifest_row \ "${order}" \ "${row_id}" \ "RTP + ${codec}" \ "rtp" \ "${codec}" \ "udp" \ "${status}" \ "${reason}" \ "${duration_ms}" \ "${emitter_rc}" \ "${receiver_rc}" \ "${emitter_log}" \ "${receiver_log}" \ "${sdp_path}" printf "[%s] RTP + %s => %s (%s)\n" "${row_id}" "${codec}" "${status}" "${reason}" } run_rtmp_row() { local order="$1" local row_id="$2" local codec="$3" local transport="$4" local row_dir="${RUN_DIR}/${order}-${row_id}" mkdir -p "${row_dir}" local emitter_log="${row_dir}/rtmp_output.log" local receiver_log="${row_dir}/rtmp_stub.log" local port port="$((RTMP_PORT_BASE + (order - 3) * 2))" local mode="h264" if [[ "${codec}" == "h265" ]]; then mode="h265-enhanced" fi local row_start_ms row_start_ms="$(date +%s%3N)" "${RTMP_STUB_TESTER}" \ --mode "${mode}" \ --listen-host 127.0.0.1 \ --listen-port "${port}" \ --video-threshold 4 \ --timeout-ms 12000 >"${receiver_log}" 2>&1 & local receiver_pid=$! cleanup_pids+=("${receiver_pid}") sleep 1 set +e "${RTMP_OUTPUT_TESTER}" \ --rtmp-url "rtmp://127.0.0.1:${port}/live/${row_id}" \ --transport "${transport}" \ --codec "${codec}" \ --encoder-device software \ --frames 32 \ --width 320 \ --height 240 \ --frame-interval-ms 20 \ --linger-ms 200 >"${emitter_log}" 2>&1 local emitter_rc=$? set -e set +e wait_pid "${receiver_pid}" 20 local receiver_rc=$? set -e local row_end_ms row_end_ms="$(date +%s%3N)" local duration_ms=$((row_end_ms - row_start_ms)) local status="PASS" local reason="all-processes-ok" if (( emitter_rc != 0 || receiver_rc != 0 )); then status="FAIL" reason="emitter_rc=${emitter_rc},receiver_rc=${receiver_rc}" if (( receiver_rc == 0 )) && grep -Eq "Broken pipe|Connection reset by peer" "${emitter_log}"; then status="PASS" reason="receiver exited cleanly after threshold; emitter observed peer close" fi fi append_manifest_row \ "${order}" \ "${row_id}" \ "RTMP + ${codec} + ${transport}" \ "rtmp" \ "${codec}" \ "${transport}" \ "${status}" \ "${reason}" \ "${duration_ms}" \ "${emitter_rc}" \ "${receiver_rc}" \ "${emitter_log}" \ "${receiver_log}" \ "" printf "[%s] RTMP + %s + %s => %s (%s)\n" "${row_id}" "${codec}" "${transport}" "${status}" "${reason}" } main() { local required=( "rtp_output_tester" "rtp_receiver_tester" "rtmp_output_tester" "rtmp_stub_tester" ) local missing=() for bin in "${required[@]}"; do if ! resolve_binary "${bin}" >/dev/null; then missing+=("${BUILD_BIN_DIR}/${bin} or ${BUILD_DIR}/${bin}") fi done if (( ${#missing[@]} > 0 )); then { echo "task=14" echo "run_id=${RUN_ID}" echo "run_dir=${RUN_DIR}" echo "manifest=${MANIFEST_TSV}" echo "missing_binaries=${missing[*]}" } > "${EVIDENCE_TEXT}" echo "missing binaries: ${missing[*]}" >&2 return 1 fi RTP_OUTPUT_TESTER="$(resolve_binary rtp_output_tester)" RTP_RECEIVER_TESTER="$(resolve_binary rtp_receiver_tester)" RTMP_OUTPUT_TESTER="$(resolve_binary rtmp_output_tester)" RTMP_STUB_TESTER="$(resolve_binary rtmp_stub_tester)" run_rtp_row 1 "rtp_h264" "h264" run_rtp_row 2 "rtp_h265" "h265" run_rtmp_row 3 "rtmp_h264_libavformat" "h264" "libavformat" run_rtmp_row 4 "rtmp_h265_libavformat" "h265" "libavformat" run_rtmp_row 5 "rtmp_h264_ffmpeg_process" "h264" "ffmpeg_process" run_rtmp_row 6 "rtmp_h265_ffmpeg_process" "h265" "ffmpeg_process" local finished_at_utc finished_at_utc="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" python3 "${SUMMARY_HELPER}" \ --manifest "${MANIFEST_TSV}" \ --output "${SUMMARY_JSON}" \ --run-id "${RUN_ID}" \ --run-dir "${RUN_DIR}" \ --started-at "${STARTED_AT_UTC}" \ --finished-at "${finished_at_utc}" cp -f "${SUMMARY_JSON}" "${LATEST_SUMMARY_JSON}" 2>/dev/null || true local total_count pass_count fail_count all_pass total_count="$(python3 - <<'PY' "${SUMMARY_JSON}" import json, sys data = json.load(open(sys.argv[1], "r", encoding="utf-8")) counts = data.get("counts", {}) print(counts.get("total", 0)) PY )" pass_count="$(python3 - <<'PY' "${SUMMARY_JSON}" import json, sys data = json.load(open(sys.argv[1], "r", encoding="utf-8")) counts = data.get("counts", {}) print(counts.get("pass", 0)) PY )" fail_count="$(python3 - <<'PY' "${SUMMARY_JSON}" import json, sys data = json.load(open(sys.argv[1], "r", encoding="utf-8")) counts = data.get("counts", {}) print(counts.get("fail", 0)) PY )" all_pass="$(python3 - <<'PY' "${SUMMARY_JSON}" import json, sys data = json.load(open(sys.argv[1], "r", encoding="utf-8")) print("true" if data.get("all_pass", False) else "false") PY )" { echo "task=14" echo "run_id=${RUN_ID}" echo "run_dir=${RUN_DIR}" echo "manifest=${MANIFEST_TSV}" echo "summary_json=${SUMMARY_JSON}" echo "started_at=${STARTED_AT_UTC}" echo "finished_at=${finished_at_utc}" echo "counts_total=${total_count}" echo "counts_pass=${pass_count}" echo "counts_fail=${fail_count}" echo "all_pass=${all_pass}" echo "matrix_rows=rtp_h264,rtp_h265,rtmp_h264_libavformat,rtmp_h265_libavformat,rtmp_h264_ffmpeg_process,rtmp_h265_ffmpeg_process" } > "${EVIDENCE_TEXT}" if [[ "${all_pass}" == "true" ]]; then echo "acceptance matrix PASS (${pass_count}/${total_count})" echo "summary: ${SUMMARY_JSON}" return 0 fi echo "acceptance matrix FAIL (${pass_count}/${total_count})" >&2 echo "summary: ${SUMMARY_JSON}" >&2 return 1 } main "$@"