streamer-node 1.0.31 → 1.0.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package.json +6 -2
- package/dist/scripts/README.md +670 -0
- package/dist/scripts/install-streamer-node.sh +117 -0
- package/dist/scripts/reframe_ball.py +354 -0
- package/dist/src/application/interfaces/StartStreamUseCase.types.d.ts +3 -1
- package/dist/src/application/interfaces/StartStreamUseCase.types.d.ts.map +1 -1
- package/dist/src/application/interfaces/StartStreamUseCase.types.js.map +1 -1
- package/dist/src/application/services/HttpClient.d.ts +21 -0
- package/dist/src/application/services/HttpClient.d.ts.map +1 -1
- package/dist/src/application/services/HttpClient.js +37 -5
- package/dist/src/application/services/HttpClient.js.map +1 -1
- package/dist/src/application/services/StreamManagerService.d.ts.map +1 -1
- package/dist/src/application/services/StreamManagerService.js +2 -0
- package/dist/src/application/services/StreamManagerService.js.map +1 -1
- package/dist/src/application/use-cases/CaptureHighlightUseCase.d.ts +52 -0
- package/dist/src/application/use-cases/CaptureHighlightUseCase.d.ts.map +1 -0
- package/dist/src/application/use-cases/CaptureHighlightUseCase.js +169 -0
- package/dist/src/application/use-cases/CaptureHighlightUseCase.js.map +1 -0
- package/dist/src/application/use-cases/StartStreamUseCase.d.ts +9 -1
- package/dist/src/application/use-cases/StartStreamUseCase.d.ts.map +1 -1
- package/dist/src/application/use-cases/StartStreamUseCase.js +125 -2
- package/dist/src/application/use-cases/StartStreamUseCase.js.map +1 -1
- package/dist/src/application/use-cases/StopStreamUseCase.d.ts +5 -1
- package/dist/src/application/use-cases/StopStreamUseCase.d.ts.map +1 -1
- package/dist/src/application/use-cases/StopStreamUseCase.js +7 -1
- package/dist/src/application/use-cases/StopStreamUseCase.js.map +1 -1
- package/dist/src/domain/events/StreamEvent.d.ts +5 -0
- package/dist/src/domain/events/StreamEvent.d.ts.map +1 -1
- package/dist/src/domain/services/BallReframer.d.ts +35 -0
- package/dist/src/domain/services/BallReframer.d.ts.map +1 -0
- package/dist/src/domain/services/BallReframer.js +3 -0
- package/dist/src/domain/services/BallReframer.js.map +1 -0
- package/dist/src/domain/services/FFmpegService.d.ts +6 -2
- package/dist/src/domain/services/FFmpegService.d.ts.map +1 -1
- package/dist/src/domain/services/HighlightSignalSource.d.ts +113 -0
- package/dist/src/domain/services/HighlightSignalSource.d.ts.map +1 -0
- package/dist/src/domain/services/HighlightSignalSource.js +3 -0
- package/dist/src/domain/services/HighlightSignalSource.js.map +1 -0
- package/dist/src/domain/services/SSEService.d.ts +1 -0
- package/dist/src/domain/services/SSEService.d.ts.map +1 -1
- package/dist/src/infrastructure/config/Config.d.ts +32 -0
- package/dist/src/infrastructure/config/Config.d.ts.map +1 -1
- package/dist/src/infrastructure/config/Config.js +60 -0
- package/dist/src/infrastructure/config/Config.js.map +1 -1
- package/dist/src/infrastructure/listeners/SupabaseListener.d.ts +5 -0
- package/dist/src/infrastructure/listeners/SupabaseListener.d.ts.map +1 -1
- package/dist/src/infrastructure/listeners/SupabaseListener.js +43 -5
- package/dist/src/infrastructure/listeners/SupabaseListener.js.map +1 -1
- package/dist/src/infrastructure/logging/LogShippingService.d.ts +3 -1
- package/dist/src/infrastructure/logging/LogShippingService.d.ts.map +1 -1
- package/dist/src/infrastructure/logging/LogShippingService.js +13 -4
- package/dist/src/infrastructure/logging/LogShippingService.js.map +1 -1
- package/dist/src/infrastructure/logging/RemoteLogger.d.ts +1 -0
- package/dist/src/infrastructure/logging/RemoteLogger.d.ts.map +1 -1
- package/dist/src/infrastructure/logging/RemoteLogger.js +1 -1
- package/dist/src/infrastructure/logging/RemoteLogger.js.map +1 -1
- package/dist/src/infrastructure/services/AdDownloaderService.d.ts +85 -0
- package/dist/src/infrastructure/services/AdDownloaderService.d.ts.map +1 -0
- package/dist/src/infrastructure/services/AdDownloaderService.js +530 -0
- package/dist/src/infrastructure/services/AdDownloaderService.js.map +1 -0
- package/dist/src/infrastructure/services/AdRotator.d.ts +63 -0
- package/dist/src/infrastructure/services/AdRotator.d.ts.map +1 -0
- package/dist/src/infrastructure/services/AdRotator.js +223 -0
- package/dist/src/infrastructure/services/AdRotator.js.map +1 -0
- package/dist/src/infrastructure/services/HighlightBufferManager.d.ts +65 -0
- package/dist/src/infrastructure/services/HighlightBufferManager.d.ts.map +1 -0
- package/dist/src/infrastructure/services/HighlightBufferManager.js +203 -0
- package/dist/src/infrastructure/services/HighlightBufferManager.js.map +1 -0
- package/dist/src/infrastructure/services/HighlightBufferRegistry.d.ts +23 -0
- package/dist/src/infrastructure/services/HighlightBufferRegistry.d.ts.map +1 -0
- package/dist/src/infrastructure/services/HighlightBufferRegistry.js +44 -0
- package/dist/src/infrastructure/services/HighlightBufferRegistry.js.map +1 -0
- package/dist/src/infrastructure/services/HighlightExtractorService.d.ts +65 -0
- package/dist/src/infrastructure/services/HighlightExtractorService.d.ts.map +1 -0
- package/dist/src/infrastructure/services/HighlightExtractorService.js +264 -0
- package/dist/src/infrastructure/services/HighlightExtractorService.js.map +1 -0
- package/dist/src/infrastructure/services/HighlightRendererService.d.ts +38 -0
- package/dist/src/infrastructure/services/HighlightRendererService.d.ts.map +1 -0
- package/dist/src/infrastructure/services/HighlightRendererService.js +182 -0
- package/dist/src/infrastructure/services/HighlightRendererService.js.map +1 -0
- package/dist/src/infrastructure/services/NodeFFmpegService.d.ts +6 -4
- package/dist/src/infrastructure/services/NodeFFmpegService.d.ts.map +1 -1
- package/dist/src/infrastructure/services/NodeFFmpegService.js +154 -32
- package/dist/src/infrastructure/services/NodeFFmpegService.js.map +1 -1
- package/dist/src/infrastructure/services/NodeSSEService.d.ts +8 -0
- package/dist/src/infrastructure/services/NodeSSEService.d.ts.map +1 -1
- package/dist/src/infrastructure/services/NodeSSEService.js +71 -27
- package/dist/src/infrastructure/services/NodeSSEService.js.map +1 -1
- package/dist/src/infrastructure/services/NullBallReframer.d.ts +11 -0
- package/dist/src/infrastructure/services/NullBallReframer.d.ts.map +1 -0
- package/dist/src/infrastructure/services/NullBallReframer.js +16 -0
- package/dist/src/infrastructure/services/NullBallReframer.js.map +1 -0
- package/dist/src/infrastructure/services/PythonBallReframer.d.ts +35 -0
- package/dist/src/infrastructure/services/PythonBallReframer.d.ts.map +1 -0
- package/dist/src/infrastructure/services/PythonBallReframer.js +238 -0
- package/dist/src/infrastructure/services/PythonBallReframer.js.map +1 -0
- package/dist/src/infrastructure/services/SerialHighlightListener.d.ts +45 -0
- package/dist/src/infrastructure/services/SerialHighlightListener.d.ts.map +1 -0
- package/dist/src/infrastructure/services/SerialHighlightListener.js +440 -0
- package/dist/src/infrastructure/services/SerialHighlightListener.js.map +1 -0
- package/dist/src/infrastructure/services/YouTubeUploadService.d.ts +36 -0
- package/dist/src/infrastructure/services/YouTubeUploadService.d.ts.map +1 -0
- package/dist/src/infrastructure/services/YouTubeUploadService.js +402 -0
- package/dist/src/infrastructure/services/YouTubeUploadService.js.map +1 -0
- package/dist/src/infrastructure/utils/ResourceSampler.d.ts +47 -0
- package/dist/src/infrastructure/utils/ResourceSampler.d.ts.map +1 -0
- package/dist/src/infrastructure/utils/ResourceSampler.js +102 -0
- package/dist/src/infrastructure/utils/ResourceSampler.js.map +1 -0
- package/dist/src/infrastructure/utils/lowPriority.d.ts +29 -0
- package/dist/src/infrastructure/utils/lowPriority.d.ts.map +1 -0
- package/dist/src/infrastructure/utils/lowPriority.js +31 -0
- package/dist/src/infrastructure/utils/lowPriority.js.map +1 -0
- package/dist/src/infrastructure/utils/paths.d.ts +27 -0
- package/dist/src/infrastructure/utils/paths.d.ts.map +1 -0
- package/dist/src/infrastructure/utils/paths.js +69 -0
- package/dist/src/infrastructure/utils/paths.js.map +1 -0
- package/dist/src/infrastructure/utils/spawnToFile.d.ts +20 -0
- package/dist/src/infrastructure/utils/spawnToFile.d.ts.map +1 -0
- package/dist/src/infrastructure/utils/spawnToFile.js +98 -0
- package/dist/src/infrastructure/utils/spawnToFile.js.map +1 -0
- package/dist/src/main.js +228 -2
- package/dist/src/main.js.map +1 -1
- package/package.json +6 -2
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
echo "=================================="
|
|
6
|
+
echo "Streamer Node Installation"
|
|
7
|
+
echo "=================================="
|
|
8
|
+
echo ""
|
|
9
|
+
|
|
10
|
+
# Check if running as root
|
|
11
|
+
if [ "$EUID" -eq 0 ]; then
|
|
12
|
+
echo "Please run as regular user (with sudo privileges), not as root"
|
|
13
|
+
exit 1
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# Prompt for configuration
|
|
17
|
+
read -p "Enter GROUND_ID: " GROUND_ID
|
|
18
|
+
if [ -z "$GROUND_ID" ]; then
|
|
19
|
+
echo "Error: GROUND_ID is required"
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
read -p "Enter installation path [/opt/streamer-node]: " INSTALL_PATH
|
|
24
|
+
INSTALL_PATH=${INSTALL_PATH:-/opt/streamer-node}
|
|
25
|
+
|
|
26
|
+
# Install Node.js if not present
|
|
27
|
+
if ! command -v node &> /dev/null; then
|
|
28
|
+
echo "Installing Node.js..."
|
|
29
|
+
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
|
|
30
|
+
sudo apt-get install -y nodejs
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# Install FFmpeg if not present
|
|
34
|
+
if ! command -v ffmpeg &> /dev/null; then
|
|
35
|
+
echo "Installing FFmpeg..."
|
|
36
|
+
sudo apt-get update
|
|
37
|
+
sudo apt-get install -y ffmpeg
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# Create directories
|
|
41
|
+
echo "Creating directories..."
|
|
42
|
+
sudo mkdir -p $INSTALL_PATH/public
|
|
43
|
+
sudo mkdir -p /var/tmp/stream_registry
|
|
44
|
+
sudo mkdir -p /tmp/stream_registry
|
|
45
|
+
sudo chown -R $USER:$USER $INSTALL_PATH
|
|
46
|
+
sudo chown -R $USER:$USER /var/tmp/stream_registry
|
|
47
|
+
|
|
48
|
+
# Set global environment variables
|
|
49
|
+
echo "Setting up environment variables..."
|
|
50
|
+
if ! grep -q "GROUND_ID" ~/.bashrc; then
|
|
51
|
+
cat >> ~/.bashrc << EOF
|
|
52
|
+
|
|
53
|
+
# Streamer Node Configuration
|
|
54
|
+
export GROUND_ID="$GROUND_ID"
|
|
55
|
+
export CLIENT_IMAGES_PATH="./public/client.png"
|
|
56
|
+
EOF
|
|
57
|
+
source ~/.bashrc
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Download setup script
|
|
61
|
+
echo "Downloading setup script..."
|
|
62
|
+
curl -sL https://raw.githubusercontent.com/MHA10/drop-shot-streaming-scripts-ubuntu/master/streamer-node-env-setup.sh -o /tmp/streamer-node-env-setup.sh
|
|
63
|
+
sudo mv /tmp/streamer-node-env-setup.sh /usr/local/bin/
|
|
64
|
+
sudo chmod +x /usr/local/bin/streamer-node-env-setup.sh
|
|
65
|
+
|
|
66
|
+
# Create systemd service
|
|
67
|
+
echo "Creating systemd service..."
|
|
68
|
+
sudo tee /etc/systemd/system/streamer-node.service > /dev/null << EOF
|
|
69
|
+
[Unit]
|
|
70
|
+
Description=Streamer Node Service
|
|
71
|
+
After=network.target network-online.target
|
|
72
|
+
Wants=network-online.target
|
|
73
|
+
|
|
74
|
+
[Service]
|
|
75
|
+
Type=simple
|
|
76
|
+
User=$USER
|
|
77
|
+
Group=$USER
|
|
78
|
+
Environment="GROUND_ID=$GROUND_ID"
|
|
79
|
+
Environment="CLIENT_IMAGES_PATH=./public/client.png"
|
|
80
|
+
ExecStart=/usr/local/bin/streamer-node-env-setup.sh --path $INSTALL_PATH
|
|
81
|
+
Restart=always
|
|
82
|
+
RestartSec=10
|
|
83
|
+
StandardOutput=journal
|
|
84
|
+
StandardError=journal
|
|
85
|
+
SyslogIdentifier=streamer-node
|
|
86
|
+
|
|
87
|
+
[Install]
|
|
88
|
+
WantedBy=multi-user.target
|
|
89
|
+
EOF
|
|
90
|
+
|
|
91
|
+
# Reload systemd
|
|
92
|
+
echo "Configuring systemd..."
|
|
93
|
+
sudo systemctl daemon-reload
|
|
94
|
+
sudo systemctl enable streamer-node
|
|
95
|
+
|
|
96
|
+
echo ""
|
|
97
|
+
echo "=================================="
|
|
98
|
+
echo "Installation Complete!"
|
|
99
|
+
echo "=================================="
|
|
100
|
+
echo ""
|
|
101
|
+
echo "Configuration:"
|
|
102
|
+
echo " GROUND_ID: $GROUND_ID"
|
|
103
|
+
echo " Installation Path: $INSTALL_PATH"
|
|
104
|
+
echo " Environment Variables: ~/.bashrc"
|
|
105
|
+
echo ""
|
|
106
|
+
echo "Commands:"
|
|
107
|
+
echo " Start: sudo systemctl start streamer-node"
|
|
108
|
+
echo " Stop: sudo systemctl stop streamer-node"
|
|
109
|
+
echo " Status: sudo systemctl status streamer-node"
|
|
110
|
+
echo " Logs: sudo journalctl -u streamer-node -f"
|
|
111
|
+
echo ""
|
|
112
|
+
echo "Manual run:"
|
|
113
|
+
echo " streamer-node-env-setup.sh --path $INSTALL_PATH"
|
|
114
|
+
echo ""
|
|
115
|
+
echo "To apply environment variables in current session:"
|
|
116
|
+
echo " source ~/.bashrc"
|
|
117
|
+
echo ""
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
reframe_ball.py — crop a padel highlight clip to a reel aspect that follows the
|
|
4
|
+
play, using classical CV (no ML model).
|
|
5
|
+
|
|
6
|
+
MODES (--mode)
|
|
7
|
+
action (default) — follow the PLAYERS. Background subtraction (MOG2) isolates
|
|
8
|
+
the moving people; the crop centre is their area-weighted horizontal
|
|
9
|
+
centre, so it sits on wherever the rally is. Robust: players are big,
|
|
10
|
+
stable targets (unlike the tiny ball, which color/motion can't separate
|
|
11
|
+
from yellow-green plants and orange paddles on this footage).
|
|
12
|
+
ball — follow the BALL via an HSV colour gate ∩ motion ∩ small-round shape.
|
|
13
|
+
Kept for footage where the ball is cleanly separable; unreliable on dim,
|
|
14
|
+
cluttered courts (documented limitation).
|
|
15
|
+
|
|
16
|
+
SMOOTH PANNING (two passes)
|
|
17
|
+
Detect a target x for every frame, then build ONE smoothed pan path
|
|
18
|
+
(interpolate gaps → median filter → wide moving average → hard max-velocity
|
|
19
|
+
clamp) and render the crop along it. Smoothness is prioritized.
|
|
20
|
+
|
|
21
|
+
DEBUG / DEMO
|
|
22
|
+
--debug-overlay <path> writes a full-frame annotated video: detected boxes
|
|
23
|
+
(green), the chosen crop-centre (red dot), and the crop window (yellow).
|
|
24
|
+
|
|
25
|
+
Off by default in the streamer; falls back to full frame on any failure.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import argparse
|
|
29
|
+
import sys
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
import cv2
|
|
33
|
+
import numpy as np
|
|
34
|
+
except Exception as exc: # noqa: BLE001
|
|
35
|
+
sys.stderr.write(f"reframe_ball: dependencies unavailable: {exc}\n")
|
|
36
|
+
sys.exit(2)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def parse_aspect(aspect):
|
|
40
|
+
try:
|
|
41
|
+
w, h = aspect.split(":")
|
|
42
|
+
r = float(w) / float(h)
|
|
43
|
+
return r if r > 0 else 9 / 16
|
|
44
|
+
except Exception: # noqa: BLE001
|
|
45
|
+
return 9 / 16
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def parse_hsv(s, default):
|
|
49
|
+
try:
|
|
50
|
+
p = [int(v) for v in s.split(",")]
|
|
51
|
+
return np.array(p[:3], np.uint8) if len(p) >= 3 else np.array(default, np.uint8)
|
|
52
|
+
except Exception: # noqa: BLE001
|
|
53
|
+
return np.array(default, np.uint8)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def moving_average(xs, win):
|
|
57
|
+
if win < 2:
|
|
58
|
+
return xs
|
|
59
|
+
win = int(win) | 1
|
|
60
|
+
pad = win // 2
|
|
61
|
+
return np.convolve(np.pad(xs, pad, mode="edge"), np.ones(win) / win, mode="valid")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def median_filter(xs, win):
|
|
65
|
+
if win < 3:
|
|
66
|
+
return xs
|
|
67
|
+
win = int(win) | 1
|
|
68
|
+
pad = win // 2
|
|
69
|
+
padded = np.pad(xs, pad, mode="edge")
|
|
70
|
+
out = np.empty_like(xs)
|
|
71
|
+
for i in range(len(xs)):
|
|
72
|
+
out[i] = np.median(padded[i:i + win])
|
|
73
|
+
return out
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def main():
|
|
77
|
+
ap = argparse.ArgumentParser()
|
|
78
|
+
ap.add_argument("--input", required=True)
|
|
79
|
+
ap.add_argument("--output", required=True)
|
|
80
|
+
ap.add_argument("--aspect", default="4:5")
|
|
81
|
+
ap.add_argument("--mode", choices=["action", "ball"], default="action")
|
|
82
|
+
# action-mode (players)
|
|
83
|
+
ap.add_argument("--min-player-frac", type=float, default=0.0015)
|
|
84
|
+
ap.add_argument("--max-player-frac", type=float, default=0.20)
|
|
85
|
+
# ball-mode
|
|
86
|
+
ap.add_argument("--hsv-lower", default="20,70,120")
|
|
87
|
+
ap.add_argument("--hsv-upper", default="45,255,255")
|
|
88
|
+
ap.add_argument("--diff-thresh", type=int, default=18)
|
|
89
|
+
ap.add_argument("--min-area-frac", type=float, default=0.000008)
|
|
90
|
+
ap.add_argument("--max-area-frac", type=float, default=0.0006)
|
|
91
|
+
ap.add_argument("--min-aspect", type=float, default=0.5)
|
|
92
|
+
ap.add_argument("--max-jump-frac", type=float, default=0.30)
|
|
93
|
+
# smoothing
|
|
94
|
+
ap.add_argument("--median-sec", type=float, default=0.5)
|
|
95
|
+
ap.add_argument("--smooth-sec", type=float, default=2.0)
|
|
96
|
+
ap.add_argument("--max-vel-frac", type=float, default=0.004)
|
|
97
|
+
# performance: detection runs on a downscaled, frame-skipped copy so the
|
|
98
|
+
# reframe finishes well under timeout on low-power streamer boxes.
|
|
99
|
+
ap.add_argument("--proc-width", type=int, default=640)
|
|
100
|
+
ap.add_argument("--detect-fps", type=float, default=12.0)
|
|
101
|
+
ap.add_argument("--debug-overlay", default="")
|
|
102
|
+
ap.add_argument("--debug", action="store_true")
|
|
103
|
+
args = ap.parse_args()
|
|
104
|
+
|
|
105
|
+
cap = cv2.VideoCapture(args.input)
|
|
106
|
+
if not cap.isOpened():
|
|
107
|
+
sys.stderr.write("reframe_ball: cannot open input\n")
|
|
108
|
+
return 3
|
|
109
|
+
fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
|
|
110
|
+
src_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
111
|
+
src_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
112
|
+
if src_w <= 0 or src_h <= 0:
|
|
113
|
+
sys.stderr.write("reframe_ball: bad source dimensions\n")
|
|
114
|
+
cap.release()
|
|
115
|
+
return 3
|
|
116
|
+
|
|
117
|
+
ratio = parse_aspect(args.aspect)
|
|
118
|
+
crop_w = min(src_w, int(round(src_h * ratio)))
|
|
119
|
+
crop_w -= crop_w % 2
|
|
120
|
+
crop_h = src_h - (src_h % 2)
|
|
121
|
+
half = crop_w // 2
|
|
122
|
+
|
|
123
|
+
# ── DETECTION runs downscaled + frame-skipped (perf) ──
|
|
124
|
+
# MOG2 + contours on every full-res 1080p frame is far too slow on
|
|
125
|
+
# low-power streamer boxes — it blew the reframe timeout and starved the
|
|
126
|
+
# live encoder. We only need approximate player positions (the pan path is
|
|
127
|
+
# heavily smoothed afterwards), so detect on a ~proc_width-wide frame, only
|
|
128
|
+
# every `step` frames (~detect_fps), and scale results back to source
|
|
129
|
+
# coordinates. Output resolution/quality is unchanged — only detection is
|
|
130
|
+
# cheapened.
|
|
131
|
+
proc_w = min(src_w, max(160, args.proc_width))
|
|
132
|
+
det_scale = proc_w / float(src_w) # <= 1.0
|
|
133
|
+
det_w = proc_w
|
|
134
|
+
det_h = max(2, int(round(src_h * det_scale)))
|
|
135
|
+
det_area = float(det_w * det_h)
|
|
136
|
+
inv_scale = src_w / float(det_w) # det coords → source coords
|
|
137
|
+
step = max(1, int(round(fps / max(1.0, args.detect_fps))))
|
|
138
|
+
|
|
139
|
+
# detector state (all area/jump thresholds measured on the DOWNSCALED frame)
|
|
140
|
+
lower = parse_hsv(args.hsv_lower, [20, 70, 120])
|
|
141
|
+
upper = parse_hsv(args.hsv_upper, [45, 255, 255])
|
|
142
|
+
min_area = det_area * args.min_area_frac
|
|
143
|
+
max_area = det_area * args.max_area_frac
|
|
144
|
+
max_jump = det_w * args.max_jump_frac
|
|
145
|
+
min_player = det_area * args.min_player_frac
|
|
146
|
+
max_player = det_area * args.max_player_frac
|
|
147
|
+
bg = cv2.createBackgroundSubtractorMOG2(history=300, varThreshold=40, detectShadows=False) \
|
|
148
|
+
if args.mode == "action" else None
|
|
149
|
+
|
|
150
|
+
xs_raw, boxes_per_frame = [], []
|
|
151
|
+
prev_gray = None
|
|
152
|
+
last_x = det_w / 2.0
|
|
153
|
+
idx = 0
|
|
154
|
+
last_pos_msec = 0.0
|
|
155
|
+
|
|
156
|
+
while True:
|
|
157
|
+
# grab() advances the decoder without fully decoding; only retrieve()
|
|
158
|
+
# (decode) the frames we actually analyse, so skipped frames are cheap.
|
|
159
|
+
if not cap.grab():
|
|
160
|
+
break
|
|
161
|
+
# Track the real presentation time of the last frame. The DECLARED fps
|
|
162
|
+
# cannot be trusted: RTSP sub-streams routinely announce 25fps while
|
|
163
|
+
# delivering ~8, and writing N frames at the declared rate then produces
|
|
164
|
+
# a clip several times shorter than the footage (a 30s window came out
|
|
165
|
+
# as 10.28s). We rebuild the true rate from these timestamps below.
|
|
166
|
+
pos = cap.get(cv2.CAP_PROP_POS_MSEC)
|
|
167
|
+
if pos and pos > last_pos_msec:
|
|
168
|
+
last_pos_msec = pos
|
|
169
|
+
found_x = np.nan
|
|
170
|
+
frame_boxes = []
|
|
171
|
+
|
|
172
|
+
if idx % step == 0:
|
|
173
|
+
ok, frame = cap.retrieve()
|
|
174
|
+
if not ok:
|
|
175
|
+
break
|
|
176
|
+
frame_s = cv2.resize(frame, (det_w, det_h), interpolation=cv2.INTER_AREA) \
|
|
177
|
+
if det_scale < 1.0 else frame
|
|
178
|
+
gray = cv2.cvtColor(frame_s, cv2.COLOR_BGR2GRAY)
|
|
179
|
+
|
|
180
|
+
if args.mode == "action":
|
|
181
|
+
fg = bg.apply(frame_s)
|
|
182
|
+
_, fg = cv2.threshold(fg, 200, 255, cv2.THRESH_BINARY) # drop shadows/soft
|
|
183
|
+
fg = cv2.morphologyEx(fg, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8))
|
|
184
|
+
# kernels sized for the downscaled frame (were 9x9 at full res)
|
|
185
|
+
fg = cv2.dilate(fg, np.ones((5, 5), np.uint8), iterations=1)
|
|
186
|
+
contours, _ = cv2.findContours(fg, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
|
187
|
+
wsum = 0.0
|
|
188
|
+
xsum = 0.0
|
|
189
|
+
for c in contours:
|
|
190
|
+
area = cv2.contourArea(c)
|
|
191
|
+
if area < min_player or area > max_player:
|
|
192
|
+
continue # too small (noise/plants) or too big (light flash)
|
|
193
|
+
x, y, w, h = cv2.boundingRect(c)
|
|
194
|
+
# Reject only extreme horizontal streaks (reflections / banner
|
|
195
|
+
# glints). Keep near-square and wide blobs — two clustered
|
|
196
|
+
# players often merge into a wide box at the key rally moments,
|
|
197
|
+
# and dropping those was hurting tracking accuracy.
|
|
198
|
+
if w > h * 3:
|
|
199
|
+
continue
|
|
200
|
+
cx = x + w / 2.0
|
|
201
|
+
wsum += area
|
|
202
|
+
xsum += area * cx
|
|
203
|
+
frame_boxes.append((x, y, w, h))
|
|
204
|
+
if wsum > 0:
|
|
205
|
+
found_x = xsum / wsum
|
|
206
|
+
else: # ball mode
|
|
207
|
+
hsv = cv2.cvtColor(frame_s, cv2.COLOR_BGR2HSV)
|
|
208
|
+
color = cv2.inRange(hsv, lower, upper)
|
|
209
|
+
if prev_gray is not None:
|
|
210
|
+
diff = cv2.absdiff(prev_gray, gray)
|
|
211
|
+
_, motion = cv2.threshold(diff, args.diff_thresh, 255, cv2.THRESH_BINARY)
|
|
212
|
+
motion = cv2.dilate(motion, np.ones((7, 7), np.uint8), iterations=1)
|
|
213
|
+
mask = cv2.bitwise_and(color, motion)
|
|
214
|
+
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
|
215
|
+
best = -1.0
|
|
216
|
+
for c in contours:
|
|
217
|
+
area = cv2.contourArea(c)
|
|
218
|
+
if area < min_area or area > max_area:
|
|
219
|
+
continue
|
|
220
|
+
x, y, w, h = cv2.boundingRect(c)
|
|
221
|
+
if w == 0 or h == 0 or min(w, h) / max(w, h) < args.min_aspect:
|
|
222
|
+
continue
|
|
223
|
+
cx = x + w / 2.0
|
|
224
|
+
dist = abs(cx - last_x)
|
|
225
|
+
if dist > max_jump:
|
|
226
|
+
continue
|
|
227
|
+
score = (min(w, h) / max(w, h)) - dist / det_w
|
|
228
|
+
if score > best:
|
|
229
|
+
best = score
|
|
230
|
+
found_x = cx
|
|
231
|
+
frame_boxes = [(x, y, w, h)]
|
|
232
|
+
if not np.isnan(found_x):
|
|
233
|
+
last_x = found_x
|
|
234
|
+
prev_gray = gray
|
|
235
|
+
|
|
236
|
+
# Map detection-space results up to source coordinates so all the
|
|
237
|
+
# downstream path math stays in real (source) pixels.
|
|
238
|
+
if not np.isnan(found_x):
|
|
239
|
+
found_x *= inv_scale
|
|
240
|
+
if inv_scale != 1.0 and frame_boxes:
|
|
241
|
+
frame_boxes = [
|
|
242
|
+
(int(x * inv_scale), int(y * inv_scale),
|
|
243
|
+
int(w * inv_scale), int(h * inv_scale))
|
|
244
|
+
for (x, y, w, h) in frame_boxes
|
|
245
|
+
]
|
|
246
|
+
|
|
247
|
+
xs_raw.append(found_x)
|
|
248
|
+
boxes_per_frame.append(frame_boxes)
|
|
249
|
+
idx += 1
|
|
250
|
+
cap.release()
|
|
251
|
+
|
|
252
|
+
n = len(xs_raw)
|
|
253
|
+
if n == 0:
|
|
254
|
+
sys.stderr.write("reframe_ball: no frames read\n")
|
|
255
|
+
return 4
|
|
256
|
+
|
|
257
|
+
# ── True output frame rate ──
|
|
258
|
+
# Write N frames at the rate the footage ACTUALLY ran at, not the rate the
|
|
259
|
+
# container claims. A camera sub-stream that declares 25fps but delivers ~8
|
|
260
|
+
# would otherwise yield a clip ~3x too short and playing ~3x too fast.
|
|
261
|
+
# Derived from the last frame's presentation time; falls back to the
|
|
262
|
+
# declared fps when timestamps are unavailable or implausible.
|
|
263
|
+
out_fps = fps
|
|
264
|
+
if last_pos_msec > 0 and n > 1:
|
|
265
|
+
measured = n / (last_pos_msec / 1000.0)
|
|
266
|
+
if 1.0 <= measured <= 120.0:
|
|
267
|
+
out_fps = measured
|
|
268
|
+
else:
|
|
269
|
+
sys.stderr.write(
|
|
270
|
+
f"reframe_ball: measured fps {measured:.2f} implausible; "
|
|
271
|
+
f"keeping declared {fps:.2f}\n"
|
|
272
|
+
)
|
|
273
|
+
xs = np.array(xs_raw, dtype=float)
|
|
274
|
+
valid = ~np.isnan(xs)
|
|
275
|
+
# hit_rate is over the frames we actually analysed (not skipped ones)
|
|
276
|
+
processed = max(1, (n + step - 1) // step)
|
|
277
|
+
hit_rate = valid.sum() / processed
|
|
278
|
+
|
|
279
|
+
if valid.sum() < max(5, n * 0.03):
|
|
280
|
+
path = np.full(n, src_w / 2.0)
|
|
281
|
+
else:
|
|
282
|
+
idx = np.arange(n)
|
|
283
|
+
xs[~valid] = np.interp(idx[~valid], idx[valid], xs[valid])
|
|
284
|
+
xs = median_filter(xs, round(args.median_sec * fps))
|
|
285
|
+
xs = moving_average(xs, round(args.smooth_sec * fps))
|
|
286
|
+
max_vel = max(1.0, src_w * args.max_vel_frac)
|
|
287
|
+
path = np.empty(n)
|
|
288
|
+
path[0] = xs[0]
|
|
289
|
+
for i in range(1, n):
|
|
290
|
+
dx = np.clip(xs[i] - path[i - 1], -max_vel, max_vel)
|
|
291
|
+
path[i] = path[i - 1] + dx
|
|
292
|
+
path = np.clip(np.round(path), half, src_w - half).astype(int)
|
|
293
|
+
|
|
294
|
+
if args.debug:
|
|
295
|
+
vel = np.abs(np.diff(path)) if n > 1 else np.array([0])
|
|
296
|
+
sys.stderr.write(
|
|
297
|
+
f"reframe_ball: mode={args.mode} frames={n} hit_rate={hit_rate:.2f} "
|
|
298
|
+
f"fps_declared={fps:.2f} fps_out={out_fps:.2f} dur={n/out_fps:.1f}s "
|
|
299
|
+
f"max_step_px={vel.max():.0f} mean_step_px={vel.mean():.2f}\n"
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
cap = cv2.VideoCapture(args.input)
|
|
303
|
+
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
|
304
|
+
writer = cv2.VideoWriter(args.output, fourcc, out_fps, (crop_w, crop_h))
|
|
305
|
+
if not writer.isOpened():
|
|
306
|
+
sys.stderr.write("reframe_ball: cannot open output writer\n")
|
|
307
|
+
cap.release()
|
|
308
|
+
return 3
|
|
309
|
+
ov_writer = cv2.VideoWriter(args.debug_overlay, fourcc, out_fps, (src_w, src_h)) \
|
|
310
|
+
if args.debug_overlay else None
|
|
311
|
+
|
|
312
|
+
i = written = 0
|
|
313
|
+
try:
|
|
314
|
+
while True:
|
|
315
|
+
ok, frame = cap.read()
|
|
316
|
+
if not ok:
|
|
317
|
+
break
|
|
318
|
+
cx = int(path[min(i, n - 1)])
|
|
319
|
+
x0 = cx - half
|
|
320
|
+
crop = frame[0:crop_h, x0:x0 + crop_w]
|
|
321
|
+
if crop.shape[1] != crop_w or crop.shape[0] != crop_h:
|
|
322
|
+
crop = cv2.resize(crop, (crop_w, crop_h))
|
|
323
|
+
writer.write(crop)
|
|
324
|
+
written += 1
|
|
325
|
+
if ov_writer is not None:
|
|
326
|
+
ov = frame.copy()
|
|
327
|
+
cv2.rectangle(ov, (x0, 0), (x0 + crop_w, crop_h), (0, 255, 255), 3)
|
|
328
|
+
for (bx, by, bw, bh) in boxes_per_frame[min(i, n - 1)]:
|
|
329
|
+
cv2.rectangle(ov, (bx, by), (bx + bw, by + bh), (0, 255, 0), 2)
|
|
330
|
+
cv2.circle(ov, (cx, crop_h // 2), 8, (0, 0, 255), -1)
|
|
331
|
+
cv2.putText(ov, f"{args.mode} center x={cx}", (x0 + 8, 30),
|
|
332
|
+
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
|
|
333
|
+
ov_writer.write(ov)
|
|
334
|
+
i += 1
|
|
335
|
+
finally:
|
|
336
|
+
# Always release so an error mid-render never leaks handles or leaves a
|
|
337
|
+
# locked/partial file (the caller ffprobe-validates and falls back).
|
|
338
|
+
cap.release()
|
|
339
|
+
writer.release()
|
|
340
|
+
if ov_writer is not None:
|
|
341
|
+
ov_writer.release()
|
|
342
|
+
|
|
343
|
+
if written == 0:
|
|
344
|
+
sys.stderr.write("reframe_ball: no frames written\n")
|
|
345
|
+
return 4
|
|
346
|
+
return 0
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
if __name__ == "__main__":
|
|
350
|
+
try:
|
|
351
|
+
sys.exit(main())
|
|
352
|
+
except Exception as exc: # noqa: BLE001
|
|
353
|
+
sys.stderr.write(f"reframe_ball: unexpected error: {exc}\n")
|
|
354
|
+
sys.exit(1)
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { Stream } from "../../domain/entities/Stream";
|
|
2
|
+
import { AdSpec } from "../../domain/events/StreamEvent";
|
|
3
|
+
export { AdSpec };
|
|
2
4
|
export interface StartStreamRequest {
|
|
3
5
|
cameraUrl: string;
|
|
4
6
|
streamKey: string;
|
|
5
7
|
courtId: string;
|
|
6
8
|
detectAudio?: boolean;
|
|
7
9
|
isScorecardActivated?: boolean;
|
|
10
|
+
ads?: AdSpec[];
|
|
8
11
|
}
|
|
9
12
|
export interface StartStreamResponse {
|
|
10
13
|
streamId: string;
|
|
@@ -33,5 +36,4 @@ type TrueOrFalse<T> = {
|
|
|
33
36
|
};
|
|
34
37
|
export type ValidationEvent = TrueOrFalse<StartStreamEvent>;
|
|
35
38
|
export type ShouldStartStream = TrueOrFalse<StartStreamResponse>;
|
|
36
|
-
export {};
|
|
37
39
|
//# sourceMappingURL=StartStreamUseCase.types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StartStreamUseCase.types.d.ts","sourceRoot":"","sources":["../../../../src/application/interfaces/StartStreamUseCase.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"StartStreamUseCase.types.d.ts","sourceRoot":"","sources":["../../../../src/application/interfaces/StartStreamUseCase.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,8BAA8B,CAAC;AACtD,OAAO,EAAE,MAAM,EAAE,MAAM,iCAAiC,CAAC;AACzD,OAAO,EAAE,MAAM,EAAE,CAAC;AAElB,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,oBAAY,YAAY;IACtB,wBAAwB,6BAA6B;IACrD,0BAA0B,+BAA+B;IACzD,eAAe,oBAAoB;IACnC,0BAA0B,+BAA+B;IACzD,qBAAqB,0BAA0B;CAChD;AAED,MAAM,MAAM,gBAAgB,GACxB;IACE,MAAM,EAAE,YAAY,CAAC,wBAAwB,CAAC;IAC9C,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB,GACD;IACE,MAAM,EACF,YAAY,CAAC,0BAA0B,GACvC,YAAY,CAAC,eAAe,GAC5B,YAAY,CAAC,0BAA0B,GACvC,YAAY,CAAC,qBAAqB,CAAC;IACvC,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEN,KAAK,WAAW,CAAC,CAAC,IAAI;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,CAAC,CAAA;CAAE,CAAC;AAEtE,MAAM,MAAM,eAAe,GAAG,WAAW,CAAC,gBAAgB,CAAC,CAAC;AAE5D,MAAM,MAAM,iBAAiB,GAAG,WAAW,CAAC,mBAAmB,CAAC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StartStreamUseCase.types.js","sourceRoot":"","sources":["../../../../src/application/interfaces/StartStreamUseCase.types.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"StartStreamUseCase.types.js","sourceRoot":"","sources":["../../../../src/application/interfaces/StartStreamUseCase.types.ts"],"names":[],"mappings":";;;AAmBA,IAAY,YAMX;AAND,WAAY,YAAY;IACtB,qEAAqD,CAAA;IACrD,yEAAyD,CAAA;IACzD,mDAAmC,CAAA;IACnC,yEAAyD,CAAA;IACzD,+DAA+C,CAAA;AACjD,CAAC,EANW,YAAY,4BAAZ,YAAY,QAMvB"}
|
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
export declare class HttpClient {
|
|
2
2
|
private readonly config;
|
|
3
|
+
private backendHeaders;
|
|
3
4
|
goLiveYouTube(groundId: string, courtId: string, streamKey: string): Promise<Response>;
|
|
5
|
+
/**
|
|
6
|
+
* Forward a court's current scoreboard state to the backend, which writes it
|
|
7
|
+
* to Supabase (the single source of truth the overlay already reads).
|
|
8
|
+
*
|
|
9
|
+
* ── WHY VIA THE BACKEND AND NOT SUPABASE DIRECTLY ──
|
|
10
|
+
* Writing Supabase from the box would need write credentials on every court
|
|
11
|
+
* machine. This reuses the x-streaming-api-key every box already has, and the
|
|
12
|
+
* backend validates the court actually belongs to this ground — a check the
|
|
13
|
+
* shared device key cannot make on its own.
|
|
14
|
+
*
|
|
15
|
+
* The state is ABSOLUTE, never a delta: mesh delivery is best-effort with no
|
|
16
|
+
* retry, so a dropped packet must be self-healing on the next update.
|
|
17
|
+
*/
|
|
18
|
+
postScoreboard(groundId: string, courtId: string, body: {
|
|
19
|
+
mode?: string;
|
|
20
|
+
redScore: string;
|
|
21
|
+
blueScore: string;
|
|
22
|
+
redGames?: number;
|
|
23
|
+
blueGames?: number;
|
|
24
|
+
}): Promise<Response>;
|
|
4
25
|
sendHeartbeat(groundId: string): Promise<Response>;
|
|
5
26
|
}
|
|
6
27
|
//# sourceMappingURL=HttpClient.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HttpClient.d.ts","sourceRoot":"","sources":["../../../../src/application/services/HttpClient.ts"],"names":[],"mappings":"AAEA,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA8B;
|
|
1
|
+
{"version":3,"file":"HttpClient.d.ts","sourceRoot":"","sources":["../../../../src/application/services/HttpClient.ts"],"names":[],"mappings":"AAEA,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA8B;IAKrD,OAAO,CAAC,cAAc;IAOhB,aAAa,CACjB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,QAAQ,CAAC;IAwDpB;;;;;;;;;;;;OAYG;IACG,cAAc,CAClB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;QACJ,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,GACA,OAAO,CAAC,QAAQ,CAAC;IAYd,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;CAyBzD"}
|
|
@@ -6,6 +6,16 @@ class HttpClient {
|
|
|
6
6
|
constructor() {
|
|
7
7
|
this.config = Config_1.Config.getInstance().get();
|
|
8
8
|
}
|
|
9
|
+
// Base headers for DropShot backend calls, including the server-to-server
|
|
10
|
+
// auth key when configured. Sent only when STREAMING_API_KEY is set, so this
|
|
11
|
+
// stays backward-compatible with a backend that doesn't yet require it.
|
|
12
|
+
backendHeaders(extra = {}) {
|
|
13
|
+
const headers = { ...extra };
|
|
14
|
+
const key = this.config.server.streamingApiKey;
|
|
15
|
+
if (key)
|
|
16
|
+
headers["x-streaming-api-key"] = key;
|
|
17
|
+
return headers;
|
|
18
|
+
}
|
|
9
19
|
async goLiveYouTube(groundId, courtId, streamKey) {
|
|
10
20
|
// GET request to notify YouTube to go live
|
|
11
21
|
const url = `${this.config.server.baseUrl}/api/v1/padel-grounds/${groundId}/courts/${courtId}/go-live/${streamKey}`;
|
|
@@ -17,9 +27,7 @@ class HttpClient {
|
|
|
17
27
|
try {
|
|
18
28
|
const response = await fetch(url, {
|
|
19
29
|
method: "GET",
|
|
20
|
-
headers: {
|
|
21
|
-
"Content-Type": "application/json",
|
|
22
|
-
},
|
|
30
|
+
headers: this.backendHeaders({ "Content-Type": "application/json" }),
|
|
23
31
|
});
|
|
24
32
|
// If we get a non-5xx response, return it (success or non-retryable error)
|
|
25
33
|
if (response.status < 500) {
|
|
@@ -55,16 +63,40 @@ class HttpClient {
|
|
|
55
63
|
}
|
|
56
64
|
}
|
|
57
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Forward a court's current scoreboard state to the backend, which writes it
|
|
68
|
+
* to Supabase (the single source of truth the overlay already reads).
|
|
69
|
+
*
|
|
70
|
+
* ── WHY VIA THE BACKEND AND NOT SUPABASE DIRECTLY ──
|
|
71
|
+
* Writing Supabase from the box would need write credentials on every court
|
|
72
|
+
* machine. This reuses the x-streaming-api-key every box already has, and the
|
|
73
|
+
* backend validates the court actually belongs to this ground — a check the
|
|
74
|
+
* shared device key cannot make on its own.
|
|
75
|
+
*
|
|
76
|
+
* The state is ABSOLUTE, never a delta: mesh delivery is best-effort with no
|
|
77
|
+
* retry, so a dropped packet must be self-healing on the next update.
|
|
78
|
+
*/
|
|
79
|
+
async postScoreboard(groundId, courtId, body) {
|
|
80
|
+
const url = `${this.config.server.baseUrl}/api/v1/padel-grounds/${groundId}/courts/${courtId}/scoreboard`;
|
|
81
|
+
return fetch(url, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: this.backendHeaders({
|
|
84
|
+
accept: "application/json",
|
|
85
|
+
"Content-Type": "application/json",
|
|
86
|
+
}),
|
|
87
|
+
body: JSON.stringify(body),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
58
90
|
async sendHeartbeat(groundId) {
|
|
59
91
|
// POST request to send heartbeat
|
|
60
92
|
const url = `${this.config.server.baseUrl}/api/v1/padel-grounds/heartbeat`;
|
|
61
93
|
try {
|
|
62
94
|
const response = await fetch(url, {
|
|
63
95
|
method: "POST",
|
|
64
|
-
headers: {
|
|
96
|
+
headers: this.backendHeaders({
|
|
65
97
|
"accept": "application/json",
|
|
66
98
|
"Content-Type": "application/json",
|
|
67
|
-
},
|
|
99
|
+
}),
|
|
68
100
|
body: JSON.stringify({
|
|
69
101
|
groundId: groundId
|
|
70
102
|
}),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HttpClient.js","sourceRoot":"","sources":["../../../../src/application/services/HttpClient.ts"],"names":[],"mappings":";;;AAAA,+DAA4D;AAE5D,MAAa,UAAU;IAAvB;QACmB,WAAM,GAAG,eAAM,CAAC,WAAW,EAAE,CAAC,GAAG,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"HttpClient.js","sourceRoot":"","sources":["../../../../src/application/services/HttpClient.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StreamManagerService.d.ts","sourceRoot":"","sources":["../../../../src/application/services/StreamManagerService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,4CAA4C,CAAC;AAC9E,OAAO,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,MAAM,kCAAkC,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAEnE,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,oCAAoC,CAAC;AAE5D,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAI1C,OAAO,EAAE,gBAAgB,EAAE,MAAM,iDAAiD,CAAC;AAGnF,qBAAa,oBAAoB;IAK7B,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,kBAAkB;IACnC,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC;IAZpC,OAAO,CAAC,mBAAmB,CAAC,CAAiB;IAC7C,OAAO,CAAC,SAAS,CAAS;gBAGP,gBAAgB,EAAE,gBAAgB,EAClC,aAAa,EAAE,aAAa,EAC5B,UAAU,EAAE,UAAU,EACtB,kBAAkB,EAAE,kBAAkB,EACtC,iBAAiB,EAAE,iBAAiB,EACpC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,UAAU,EACtB,gBAAgB,CAAC,EAAE,gBAAgB,YAAA;IAGzC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"StreamManagerService.d.ts","sourceRoot":"","sources":["../../../../src/application/services/StreamManagerService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,4CAA4C,CAAC;AAC9E,OAAO,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,MAAM,kCAAkC,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAEnE,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,oCAAoC,CAAC;AAE5D,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAI1C,OAAO,EAAE,gBAAgB,EAAE,MAAM,iDAAiD,CAAC;AAGnF,qBAAa,oBAAoB;IAK7B,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,kBAAkB;IACnC,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC;IAZpC,OAAO,CAAC,mBAAmB,CAAC,CAAiB;IAC7C,OAAO,CAAC,SAAS,CAAS;gBAGP,gBAAgB,EAAE,gBAAgB,EAClC,aAAa,EAAE,aAAa,EAC5B,UAAU,EAAE,UAAU,EACtB,kBAAkB,EAAE,kBAAkB,EACtC,iBAAiB,EAAE,iBAAiB,EACpC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,UAAU,EACtB,gBAAgB,CAAC,EAAE,gBAAgB,YAAA;IAGzC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAuCtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YA8BpB,gBAAgB;YAsBhB,cAAc;YA2Dd,wBAAwB;IA8BtC,OAAO,CAAC,qBAAqB;YAQf,iBAAiB;YAsBjB,gBAAgB;YAuBhB,eAAe;IA8B7B,OAAO,CAAC,gBAAgB;YAsBV,kBAAkB;YAuDlB,cAAc;YAiCd,sBAAsB;YAwFtB,YAAY;IAiCb,SAAS,IAAI,OAAO,CAAC;QAChC,SAAS,EAAE,OAAO,CAAC;QACnB,YAAY,EAAE,OAAO,CAAC;QACtB,cAAc,EAAE,MAAM,CAAC;QACvB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;CAWH"}
|
|
@@ -73,6 +73,7 @@ class StreamManagerService {
|
|
|
73
73
|
...sseConfig,
|
|
74
74
|
groundId: this.config.get().groundInfo.groundId,
|
|
75
75
|
baseUrl: this.config.get().server.baseUrl,
|
|
76
|
+
streamingApiKey: this.config.get().server.streamingApiKey,
|
|
76
77
|
});
|
|
77
78
|
this.logger.info("Stream Manager Service started successfully");
|
|
78
79
|
}
|
|
@@ -236,6 +237,7 @@ class StreamManagerService {
|
|
|
236
237
|
courtId: event.courtId,
|
|
237
238
|
detectAudio: true,
|
|
238
239
|
isScorecardActivated: event.isScorecardActivated,
|
|
240
|
+
ads: event.ads,
|
|
239
241
|
}, this.stopStreamUseCase);
|
|
240
242
|
// If stream started successfully (has a streamId), subscribe to updates
|
|
241
243
|
if (result && result.streamId && this.supabaseListener && event.isScorecardActivated) {
|