#!/usr/bin/env bash # Stream playback helper — takes BEX stream JSON output and plays with mpv/ffplay # # Usage: # ./play-stream.sh # echo '{"manifest_url":"...", "headers":[...]}' | ./play-stream.sh # ./play-stream.sh --from-file stream_output.json # # Supports: # - HLS (.m3u8) streams # - Direct MP4/MKV URLs # - Custom headers (Referer, User-Agent, etc.) # - mpv (preferred) or ffplay (fallback) # # The script extracts the stream URL and headers from the BEX engine's # resolve-stream JSON output and constructs the correct player command. set -euo pipefail # Read JSON from stdin, argument, or file if [ "${1:-}" = "--from-file" ] && [ -f "${2:-}" ]; then JSON=$(cat "$2") elif [ -n "${1:-}" ] && [ "${1:-}" != "-" ]; then JSON="$1" else JSON=$(cat) fi if [ -z "$JSON" ]; then echo "Error: No stream JSON provided" echo "Usage: ./play-stream.sh '' or pipe JSON to stdin" exit 1 fi # Parse JSON with jq (or python fallback) if command -v jq &>/dev/null; then STREAM_URL=$(echo "$JSON" | jq -r '.manifest_url // .videos[0].url // empty') # Extract headers as key: value pairs HEADERS=$(echo "$JSON" | jq -r '.headers[]? | "\(.key): \(.value)"') FORMAT=$(echo "$JSON" | jq -r '.format // "hls"') LABEL=$(echo "$JSON" | jq -r '.label // "Stream"') elif command -v python3 &>/dev/null; then STREAM_URL=$(echo "$JSON" | python3 -c " import json, sys d = json.load(sys.stdin) url = d.get('manifest_url') or (d.get('videos', [{}])[0].get('url', '')) print(url) ") HEADERS=$(echo "$JSON" | python3 -c " import json, sys d = json.load(sys.stdin) for h in d.get('headers', []): print(f\"{h['key']}: {h['value']}\") ") FORMAT=$(echo "$JSON" | python3 -c " import json, sys d = json.load(sys.stdin) print(d.get('format', 'hls')) ") LABEL="Stream" else echo "Error: requires 'jq' or 'python3' to parse JSON" exit 1 fi if [ -z "$STREAM_URL" ]; then echo "Error: Could not extract stream URL from JSON" echo "JSON received:" echo "$JSON" | head -20 exit 1 fi echo "=== BEX Stream Player ===" echo "URL: $STREAM_URL" echo "Format: $FORMAT" echo "Label: $LABEL" echo "" # Build header arguments MPV_HEADERS="" FFPLAY_HEADERS="" while IFS= read -r header; do if [ -n "$header" ]; then KEY=$(echo "$header" | cut -d: -f1 | xargs) VALUE=$(echo "$header" | cut -d: -f2- | xargs) MPV_HEADERS="${MPV_HEADERS}${KEY}: ${VALUE}\r\n" FFPLAY_HEADERS="${FFPLAY_HEADERS}-headers \"${KEY}: ${VALUE}\r\n\" " echo "Header: $KEY: $VALUE" fi done <<< "$HEADERS" echo "" # Try mpv first (best experience) if command -v mpv &>/dev/null; then echo "Playing with mpv..." MPV_ARGS=( "$STREAM_URL" "--title=BEX: $LABEL" "--force-media-title=$LABEL" ) if [ -n "$MPV_HEADERS" ]; then MPV_ARGS+=("--http-header-fields=${MPV_HEADERS}") fi # HLS-specific options if [ "$FORMAT" = "hls" ] || echo "$STREAM_URL" | grep -q "m3u8"; then MPV_ARGS+=( "--demuxer-max-bytes=100MiB" "--demuxer-max-back-bytes=50MiB" "--cache=yes" "--cache-secs=30" ) fi exec mpv "${MPV_ARGS[@]}" # Fallback to ffplay elif command -v ffplay &>/dev/null; then echo "Playing with ffplay..." FFPLAY_ARGS=( -i "$STREAM_URL" -window_title "BEX: $LABEL" -loglevel warning -autoexit ) if [ -n "$MPV_HEADERS" ]; then FFPLAY_ARGS+=(-headers "$MPV_HEADERS") fi exec ffplay "${FFPLAY_ARGS[@]}" # Last resort: just print the URL for manual use else echo "No player found (mpv or ffplay required)" echo "" echo "Manual playback commands:" echo "" echo " mpv \"$STREAM_URL\" \\" if [ -n "$MPV_HEADERS" ]; then echo " --http-header-fields=\"${MPV_HEADERS}\" \\" fi echo " --cache=yes" echo "" echo " ffplay -i \"$STREAM_URL\" \\" if [ -n "$MPV_HEADERS" ]; then echo " -headers \"${MPV_HEADERS}\"" fi echo "" echo " curl -L -H 'User-Agent: Mozilla/5.0' \"$STREAM_URL\" | ffplay -" exit 1 fi