mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-11-15 03:02:17 -05:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cb7cc5bdc | ||
|
|
dbcb7f6a7a | ||
|
|
34936ab05c | ||
|
|
7231809140 | ||
|
|
8ca714cf1a | ||
|
|
ffd34de56c | ||
|
|
aeeff6fe1b | ||
|
|
846c9dae55 | ||
|
|
e750712a39 | ||
|
|
ff8b0c530e | ||
|
|
69c5bfe544 | ||
|
|
3ad95b376a | ||
|
|
bda740b08d | ||
|
|
3c8c5f5883 | ||
|
|
c35544e515 |
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Finite, CPU-only rover policy compute benchmark. No rover/server connections.
|
||||
|
||||
Run from the repo root on Linux:
|
||||
python3 -m venv ~/.venvs/rover-bench
|
||||
~/.venvs/rover-bench/bin/pip install torch --index-url https://download.pytorch.org/whl/cpu
|
||||
~/.venvs/rover-bench/bin/python perf/rover-model-benchmark.py
|
||||
|
||||
Quick run:
|
||||
.../python perf/rover-model-benchmark.py --threads 4 --rovers 1 --seconds 2
|
||||
|
||||
Random inputs/weights measure compute cost ONLY, not learned driving ability.
|
||||
This is a representative architecture, not a selected production policy.
|
||||
Video decoding, network delay, dataset I/O, and simultaneous training/inference
|
||||
are excluded. No packages are installed automatically and no files are written.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
import resource
|
||||
import statistics
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
def positive_int(value):
|
||||
number = int(value)
|
||||
if number < 1:
|
||||
raise argparse.ArgumentTypeError("must be positive")
|
||||
return number
|
||||
|
||||
|
||||
def positive_seconds(value):
|
||||
number = float(value)
|
||||
if not math.isfinite(number) or number <= 0:
|
||||
raise argparse.ArgumentTypeError("must be finite and positive")
|
||||
return number
|
||||
|
||||
|
||||
def arguments():
|
||||
parser = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument("--threads", type=positive_int, nargs="+", default=[4, 8, 16])
|
||||
parser.add_argument("--rovers", type=positive_int, nargs="+", default=[1, 2, 4])
|
||||
parser.add_argument("--seconds", type=positive_seconds, default=10,
|
||||
help="minimum measured seconds per case (default: 10)")
|
||||
parser.add_argument("--frames", type=positive_int, default=4)
|
||||
parser.add_argument("--height", type=positive_int, default=120)
|
||||
parser.add_argument("--width", type=positive_int, default=160)
|
||||
parser.add_argument("--batch-size", type=positive_int, default=16,
|
||||
help="training batch size")
|
||||
parser.add_argument("--hz", type=positive_int, default=10,
|
||||
help="target decision frequency per rover")
|
||||
parser.add_argument("--worker", choices=["inference", "training"],
|
||||
help=argparse.SUPPRESS)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def worker(args):
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
torch.set_num_threads(args.threads[0])
|
||||
torch.set_num_interop_threads(1)
|
||||
torch.manual_seed(42)
|
||||
|
||||
class Policy(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# Preserve coarse spatial layout: global pooling alone would discard
|
||||
# information about which side of the image contains an obstacle.
|
||||
self.vision = nn.Sequential(
|
||||
nn.Conv2d(3, 24, 5, stride=2, padding=2), nn.ReLU(),
|
||||
nn.Conv2d(24, 48, 3, stride=2, padding=1), nn.ReLU(),
|
||||
nn.Conv2d(48, 96, 3, stride=2, padding=1), nn.ReLU(),
|
||||
nn.Conv2d(96, 128, 3, stride=2, padding=1), nn.ReLU(),
|
||||
nn.AdaptiveAvgPool2d((3, 4)), nn.Flatten(),
|
||||
nn.Linear(128 * 3 * 4, 128), nn.ReLU(),
|
||||
)
|
||||
# 32 sensor values + 32 previous-control/state values per frame.
|
||||
self.history = nn.GRU(128 + 64, 128, batch_first=True)
|
||||
self.continuous = nn.Linear(128, 8)
|
||||
self.buttons = nn.Linear(128, 16)
|
||||
|
||||
def forward(self, images, state):
|
||||
batch, frames, channels, height, width = images.shape
|
||||
features = self.vision(images.reshape(-1, channels, height, width))
|
||||
features = features.reshape(batch, frames, -1)
|
||||
history, _ = self.history(torch.cat((features, state), dim=-1))
|
||||
last = history[:, -1]
|
||||
return self.continuous(last).tanh(), self.buttons(last)
|
||||
|
||||
model = Policy().cpu().float()
|
||||
training = args.worker == "training"
|
||||
model.train(training)
|
||||
batch = args.batch_size if training else args.rovers[0]
|
||||
# Keep several independent windows in RAM. Allocation/random generation is
|
||||
# outside timing; full frame-history encoding IS included on every decision.
|
||||
windows = [(
|
||||
torch.rand(batch, args.frames, 3, args.height, args.width),
|
||||
torch.rand(batch, args.frames, 64),
|
||||
) for _ in range(4)]
|
||||
targets = (torch.rand(batch, 8) * 2 - 1, torch.randint(0, 2, (batch, 16)).float())
|
||||
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4) if training else None
|
||||
|
||||
def step(index):
|
||||
if training:
|
||||
optimizer.zero_grad(set_to_none=True)
|
||||
continuous, buttons = model(*windows[index % len(windows)])
|
||||
if training:
|
||||
loss = (nn.functional.mse_loss(continuous, targets[0])
|
||||
+ nn.functional.binary_cross_entropy_with_logits(buttons, targets[1]))
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
|
||||
context = torch.enable_grad if training else torch.inference_mode
|
||||
with context():
|
||||
for index in range(5):
|
||||
step(index)
|
||||
timings = []
|
||||
cpu_start = time.process_time()
|
||||
start = time.perf_counter()
|
||||
# CPU execution is synchronous, so these durations include completion.
|
||||
while time.perf_counter() - start < args.seconds or len(timings) < 10:
|
||||
before = time.perf_counter()
|
||||
step(len(timings))
|
||||
timings.append(time.perf_counter() - before)
|
||||
elapsed = time.perf_counter() - start
|
||||
cpu_elapsed = time.process_time() - cpu_start
|
||||
|
||||
ordered = sorted(timings)
|
||||
print(json.dumps({
|
||||
"mode": args.worker, "threads": args.threads[0], "batch": batch,
|
||||
"parameters": sum(p.numel() for p in model.parameters()),
|
||||
"torch": torch.__version__, "iterations": len(timings),
|
||||
"p50_ms": statistics.median(timings) * 1000,
|
||||
"p95_ms": ordered[math.ceil(len(ordered) * .95) - 1] * 1000,
|
||||
"max_ms": max(timings) * 1000,
|
||||
"steps_s": len(timings) / elapsed,
|
||||
"examples_s": len(timings) * batch / elapsed,
|
||||
"cpu_cores": cpu_elapsed / elapsed,
|
||||
"peak_mib": resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024,
|
||||
}))
|
||||
|
||||
|
||||
def main():
|
||||
args = arguments()
|
||||
if args.worker:
|
||||
worker(args)
|
||||
return
|
||||
try:
|
||||
import torch
|
||||
except ImportError:
|
||||
sys.exit("PyTorch is required. Run the three setup/run commands shown by --help.")
|
||||
|
||||
print(f"CPU-only FP32 | {platform.platform()} | Python {platform.python_version()}"
|
||||
f" | PyTorch {torch.__version__}", flush=True)
|
||||
print(f"Logical CPUs: {os.cpu_count()} | initial load averages: {os.getloadavg()}")
|
||||
print(f"Input: {args.frames} RGB frames at {args.width}x{args.height},"
|
||||
" 32 sensors + 32 controls/state per frame")
|
||||
print("Output: 8 continuous controls + 16 button logits (placeholder dimensions)")
|
||||
print("Synthetic in-memory data; full history re-encoded each decision.")
|
||||
print("Excludes video decode/network/dataset I/O and concurrent training/inference.")
|
||||
print("Each case has 5 warmups, then at least 10 iterations and the requested duration.")
|
||||
print("CPU cores = process CPU seconds / wall seconds; peak MiB includes runtime/data.")
|
||||
print("Multi-rover inference batches one independent history per rover per step.\n")
|
||||
print("mode threads batch p50 ms p95 ms max ms steps/s examples/s CPU cores peak MiB", flush=True)
|
||||
results = []
|
||||
for mode in ("inference", "training"):
|
||||
batches = args.rovers if mode == "inference" else [args.batch_size]
|
||||
for threads in args.threads:
|
||||
for batch in batches:
|
||||
# Fresh processes isolate thread pools and peak memory per case.
|
||||
command = [sys.executable, os.path.abspath(__file__), "--worker", mode,
|
||||
"--threads", str(threads), "--rovers", str(batch),
|
||||
"--batch-size", str(args.batch_size), "--seconds", str(args.seconds),
|
||||
"--frames", str(args.frames), "--height", str(args.height),
|
||||
"--width", str(args.width)]
|
||||
process = subprocess.run(command, capture_output=True, text=True)
|
||||
if process.returncode:
|
||||
print(process.stderr, file=sys.stderr)
|
||||
sys.exit(f"Failed: {mode}, threads={threads}, batch={batch}"
|
||||
" (if killed for memory use, reduce --batch-size or --rovers).")
|
||||
result = json.loads(process.stdout)
|
||||
results.append(result)
|
||||
print(f"{mode:10} {threads:7} {batch:5}"
|
||||
f" {result['p50_ms']:7.1f} {result['p95_ms']:7.1f}"
|
||||
f" {result['max_ms']:7.1f} {result['steps_s']:8.2f}"
|
||||
f" {result['examples_s']:10.2f} {result['cpu_cores']:9.2f}"
|
||||
f" {result['peak_mib']:8.0f}", flush=True)
|
||||
|
||||
print(f"\nModel parameters: {results[0]['parameters']:,}")
|
||||
print(f"At {args.hz} Hz, each rover batch must finish every {1000 / args.hz:.1f} ms;")
|
||||
print("p95 needs headroom for the excluded pipeline. This is not a deployment pass/fail.")
|
||||
fastest = max((r for r in results if r['mode'] == 'training'), key=lambda r: r['examples_s'])
|
||||
examples = 3600 * args.hz
|
||||
minutes = examples / fastest['examples_s'] / 60
|
||||
print(f"Best measured training: {fastest['examples_s']:.1f} windows/s at"
|
||||
f" {fastest['threads']} threads.")
|
||||
print(f"One pass over one rover-hour sampled at {args.hz} Hz ({examples:,} windows):"
|
||||
f" about {minutes:.1f} compute-only minutes.")
|
||||
print("Multiply by dataset hours and training passes; this does not predict convergence.")
|
||||
print("Paste this complete output, along with the command/options used.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
sys.exit("\nBenchmark interrupted.")
|
||||
+3
-3
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
.configuration-tree{margin-top:.25rem}.configuration-tree>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.configuration-card{min-width:0px}.configuration-card-header{top:var(--configuration-sticky-top, 0px)}.configuration-card .configuration-card{margin-left:1rem;width:calc(100% - 1rem)}.configuration-card-body>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.configuration-card-body{padding:.125rem}.configuration-children>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.configuration-line{display:grid;min-width:0px;grid-template-columns:repeat(1,minmax(0,1fr));align-items:flex-start;gap:.125rem;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(38 38 38 / var(--tw-bg-opacity));padding:.125rem}@media(min-width:640px){.configuration-line{grid-template-columns:minmax(9rem,16rem) minmax(12rem,40rem)}}.configuration-line{justify-content:start}.configuration-key,.configuration-value{min-width:0px}.configuration-key-label{font-size:1rem;line-height:1.5rem;font-weight:600;line-height:1.375;--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity))}.configuration-root-description,.configuration-branch-description,.configuration-item-description,.configuration-value-description{display:block;font-size:.875rem;line-height:1.25rem;line-height:1.375;--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity));margin-top:.125rem}.configuration-root-description{margin-bottom:.25rem}.configuration-branch-description,.configuration-item-description{max-width:56rem}.configuration-value-description{margin-bottom:.125rem;max-width:40rem}.configuration-value input:not([type=checkbox]),.configuration-value select,.configuration-value textarea{width:100%;border-radius:.375rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(82 82 82 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(64 64 64 / var(--tw-bg-opacity));padding:.125rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.configuration-value input:not([type=checkbox])::-moz-placeholder,.configuration-value select::-moz-placeholder,.configuration-value textarea::-moz-placeholder{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.configuration-value input:not([type=checkbox])::placeholder,.configuration-value select::placeholder,.configuration-value textarea::placeholder{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.configuration-value input:not([type=checkbox]):focus,.configuration-value select:focus,.configuration-value textarea:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000);--tw-ring-opacity: 1;--tw-ring-color: rgb(14 165 233 / var(--tw-ring-opacity))}.configuration-value input[type=checkbox]{height:1rem;width:1rem;vertical-align:middle;accent-color:#0ea5e9}.configuration-value .checkbox label{display:flex;min-height:1.75rem;align-items:center;--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity))}.configuration-value .error-detail{margin-top:.125rem;font-size:.75rem;line-height:1rem;--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity))}.configuration-value .help-block{margin-top:.125rem;display:block;font-size:.7rem;--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity))}.configuration-secret{min-width:0px}.configuration-item-actions,.configuration-array-actions{display:flex;flex-wrap:wrap;gap:.125rem}
|
||||
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
||||
import{r as g,R as a}from"./index-dJH5nUWV.js";const d="64px",u={},k=({src:s,light:t,oEmbedUrl:o,onClickPreview:n,playIcon:b,previewTabIndex:_,previewAriaLabel:p})=>{const[f,r]=g.useState(null);g.useEffect(()=>{!s||!t||!o||x({src:s,light:t,oEmbedUrl:o})},[s,t,o]);const x=async({src:e,light:i,oEmbedUrl:E})=>{if(a.isValidElement(i))return;if(typeof i=="string"){r(i);return}if(u[e]){r(u[e]);return}r(null);const m=await(await fetch(E.replace("{url}",e))).json();if(m.thumbnail_url){const h=m.thumbnail_url.replace("height=100","height=480").replace("-d_295x166","-d_640");r(h),u[e]=h}},I=e=>{(e.key==="Enter"||e.key===" ")&&n?.(e)},v=e=>{n?.(e)},l=a.isValidElement(t),y={display:"flex",alignItems:"center",justifyContent:"center"},c={preview:{width:"100%",height:"100%",backgroundImage:f&&!l?`url(${f})`:void 0,backgroundSize:"cover",backgroundPosition:"center",cursor:"pointer",...y},shadow:{background:"radial-gradient(rgb(0, 0, 0, 0.3), rgba(0, 0, 0, 0) 60%)",borderRadius:d,width:d,height:d,position:l?"absolute":void 0,...y},playIcon:{borderStyle:"solid",borderWidth:"16px 0 16px 26px",borderColor:"transparent transparent transparent white",marginLeft:"7px"}},w=a.createElement("div",{style:c.shadow,className:"react-player__shadow"},a.createElement("div",{style:c.playIcon,className:"react-player__play-icon"}));return a.createElement("div",{style:c.preview,className:"react-player__preview",tabIndex:_,onClick:v,onKeyDown:I,...p?{"aria-label":p}:{}},l?t:null,b||w)};var C=k;export{C as default};
|
||||
//# sourceMappingURL=Preview-BBzOMZjy.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
||||
import{r as g,R as a}from"./index-5EWtcenj.js";const d="64px",u={},k=({src:s,light:t,oEmbedUrl:o,onClickPreview:n,playIcon:b,previewTabIndex:_,previewAriaLabel:p})=>{const[f,r]=g.useState(null);g.useEffect(()=>{!s||!t||!o||x({src:s,light:t,oEmbedUrl:o})},[s,t,o]);const x=async({src:e,light:i,oEmbedUrl:E})=>{if(a.isValidElement(i))return;if(typeof i=="string"){r(i);return}if(u[e]){r(u[e]);return}r(null);const m=await(await fetch(E.replace("{url}",e))).json();if(m.thumbnail_url){const h=m.thumbnail_url.replace("height=100","height=480").replace("-d_295x166","-d_640");r(h),u[e]=h}},I=e=>{(e.key==="Enter"||e.key===" ")&&n?.(e)},v=e=>{n?.(e)},l=a.isValidElement(t),y={display:"flex",alignItems:"center",justifyContent:"center"},c={preview:{width:"100%",height:"100%",backgroundImage:f&&!l?`url(${f})`:void 0,backgroundSize:"cover",backgroundPosition:"center",cursor:"pointer",...y},shadow:{background:"radial-gradient(rgb(0, 0, 0, 0.3), rgba(0, 0, 0, 0) 60%)",borderRadius:d,width:d,height:d,position:l?"absolute":void 0,...y},playIcon:{borderStyle:"solid",borderWidth:"16px 0 16px 26px",borderColor:"transparent transparent transparent white",marginLeft:"7px"}},w=a.createElement("div",{style:c.shadow,className:"react-player__shadow"},a.createElement("div",{style:c.playIcon,className:"react-player__play-icon"}));return a.createElement("div",{style:c.preview,className:"react-player__preview",tabIndex:_,onClick:v,onKeyDown:I,...p?{"aria-label":p}:{}},l?t:null,b||w)};var C=k;export{C as default};
|
||||
//# sourceMappingURL=Preview-CKTJqKe-.js.map
|
||||
File diff suppressed because one or more lines are too long
+2
-2
@@ -1,2 +1,2 @@
|
||||
import{e as F,r as s,j as e,S as P,C as c,L as q}from"./index-n0JxE1Mv.js";import{o as $,p as D,q as I}from"./api-B8GGCEeo.js";function R(){const l=F(),[a,m]=s.useState(null),[n,b]=s.useState(""),[d,C]=s.useState(""),[p,N]=s.useState(""),[r,S]=s.useState(""),[f,v]=s.useState(""),[o,w]=s.useState(null),[x,h]=s.useState(!1),[g,i]=s.useState("");s.useEffect(()=>{$(l).then(t=>m(t.required)).catch(t=>i(t.message))},[l]);async function j(t){h(!0),i("");try{await t(),m(!1),i("Setup completed. You can now open the administration application and log in.")}catch(u){const E=Array.isArray(u.validationErrors)?` ${u.validationErrors.map(y=>`${y.path}: ${y.message}`).join("; ")}`:"";i(`${u.message}${E}`)}finally{h(!1)}}function k(t){if(t.preventDefault(),r!==f){i("Passwords do not match.");return}j(()=>D(l,{setupCode:n,username:d,discordId:p,password:r}))}function A(t){t.preventDefault(),o&&j(async()=>I(l,{setupCode:n,fileName:o.name,yaml:await o.text()}))}return e.jsxs("div",{className:"min-h-screen bg-neutral-950 p-1 text-slate-100",children:[e.jsx(P,{}),e.jsxs("main",{className:"mx-auto flex min-h-screen w-full max-w-3xl flex-col justify-center gap-0.5",children:[e.jsxs(c,{title:"MultiRover setup",meta:a===null?"checking":a?"required":"complete",bodyClassName:"space-y-0.5 p-1 text-sm",children:[a?e.jsx("p",{children:"Enter the one-time code from setup-code.txt in the server data folder, then create the first lockdown administrator or import an existing configuration."}):null,a===!1?e.jsx(q,{className:"button-dark inline-block",to:"/admin",children:"Open administration"}):null,g?e.jsx("p",{className:"surface p-1 text-sm text-slate-200",children:g}):null]}),a?e.jsxs(e.Fragment,{children:[e.jsxs(c,{title:"Setup authorization",bodyClassName:"p-1",children:[e.jsx("label",{className:"block text-xs font-semibold text-slate-200",children:"One-time setup code"}),e.jsx("input",{className:"field-input mt-0.5 w-full font-mono",value:n,onChange:t=>b(t.target.value)})]}),e.jsx(c,{title:"Create first administrator",bodyClassName:"p-1",children:e.jsxs("form",{className:"grid gap-0.5 md:grid-cols-2",onSubmit:k,children:[e.jsx("input",{className:"field-input",placeholder:"Username",value:d,onChange:t=>C(t.target.value)}),e.jsx("input",{className:"field-input",placeholder:"Discord id (optional)",value:p,onChange:t=>N(t.target.value)}),e.jsx("input",{className:"field-input",type:"password",placeholder:"Password",value:r,onChange:t=>S(t.target.value)}),e.jsx("input",{className:"field-input",type:"password",placeholder:"Confirm password",value:f,onChange:t=>v(t.target.value)}),e.jsx("button",{className:"button-dark md:col-span-2",type:"submit",disabled:x||!n||!d||!r,children:"Create lockdown administrator"})]})}),e.jsxs(c,{title:"Import configuration file",bodyClassName:"space-y-0.5 p-1 text-sm",children:[e.jsx("p",{className:"text-xs text-slate-400",children:"Choose an existing YAML configuration explicitly. The server validates and imports it once, and its secrets are never displayed back in the browser."}),e.jsxs("form",{className:"flex flex-col gap-0.5 md:flex-row",onSubmit:A,children:[e.jsx("input",{className:"field-input flex-1",type:"file",accept:".yaml,.yml,text/yaml",onChange:t=>w(t.target.files?.[0]||null)}),e.jsx("button",{className:"button-dark",type:"submit",disabled:x||!n||!o,children:"Import selected YAML"})]})]})]}):null]})]})}export{R as default};
|
||||
//# sourceMappingURL=SetupApp-CaWv689i.js.map
|
||||
import{e as F,r as s,j as e,S as P,C as c,L as q}from"./index-5EWtcenj.js";import{o as $,p as D,q as I}from"./api-B8GGCEeo.js";function R(){const l=F(),[a,m]=s.useState(null),[n,b]=s.useState(""),[d,C]=s.useState(""),[p,N]=s.useState(""),[r,S]=s.useState(""),[f,v]=s.useState(""),[o,w]=s.useState(null),[x,h]=s.useState(!1),[g,i]=s.useState("");s.useEffect(()=>{$(l).then(t=>m(t.required)).catch(t=>i(t.message))},[l]);async function j(t){h(!0),i("");try{await t(),m(!1),i("Setup completed. You can now open the administration application and log in.")}catch(u){const E=Array.isArray(u.validationErrors)?` ${u.validationErrors.map(y=>`${y.path}: ${y.message}`).join("; ")}`:"";i(`${u.message}${E}`)}finally{h(!1)}}function k(t){if(t.preventDefault(),r!==f){i("Passwords do not match.");return}j(()=>D(l,{setupCode:n,username:d,discordId:p,password:r}))}function A(t){t.preventDefault(),o&&j(async()=>I(l,{setupCode:n,fileName:o.name,yaml:await o.text()}))}return e.jsxs("div",{className:"min-h-screen bg-neutral-950 p-1 text-slate-100",children:[e.jsx(P,{}),e.jsxs("main",{className:"mx-auto flex min-h-screen w-full max-w-3xl flex-col justify-center gap-0.5",children:[e.jsxs(c,{title:"MultiRover setup",meta:a===null?"checking":a?"required":"complete",bodyClassName:"space-y-0.5 p-1 text-sm",children:[a?e.jsx("p",{children:"Enter the one-time code from setup-code.txt in the server data folder, then create the first lockdown administrator or import an existing configuration."}):null,a===!1?e.jsx(q,{className:"button-dark inline-block",to:"/admin",children:"Open administration"}):null,g?e.jsx("p",{className:"surface p-1 text-sm text-slate-200",children:g}):null]}),a?e.jsxs(e.Fragment,{children:[e.jsxs(c,{title:"Setup authorization",bodyClassName:"p-1",children:[e.jsx("label",{className:"block text-xs font-semibold text-slate-200",children:"One-time setup code"}),e.jsx("input",{className:"field-input mt-0.5 w-full font-mono",value:n,onChange:t=>b(t.target.value)})]}),e.jsx(c,{title:"Create first administrator",bodyClassName:"p-1",children:e.jsxs("form",{className:"grid gap-0.5 md:grid-cols-2",onSubmit:k,children:[e.jsx("input",{className:"field-input",placeholder:"Username",value:d,onChange:t=>C(t.target.value)}),e.jsx("input",{className:"field-input",placeholder:"Discord id (optional)",value:p,onChange:t=>N(t.target.value)}),e.jsx("input",{className:"field-input",type:"password",placeholder:"Password",value:r,onChange:t=>S(t.target.value)}),e.jsx("input",{className:"field-input",type:"password",placeholder:"Confirm password",value:f,onChange:t=>v(t.target.value)}),e.jsx("button",{className:"button-dark md:col-span-2",type:"submit",disabled:x||!n||!d||!r,children:"Create lockdown administrator"})]})}),e.jsxs(c,{title:"Import configuration file",bodyClassName:"space-y-0.5 p-1 text-sm",children:[e.jsx("p",{className:"text-xs text-slate-400",children:"Choose an existing YAML configuration explicitly. The server validates and imports it once, and its secrets are never displayed back in the browser."}),e.jsxs("form",{className:"flex flex-col gap-0.5 md:flex-row",onSubmit:A,children:[e.jsx("input",{className:"field-input flex-1",type:"file",accept:".yaml,.yml,text/yaml",onChange:t=>w(t.target.files?.[0]||null)}),e.jsx("button",{className:"button-dark",type:"submit",disabled:x||!n||!o,children:"Import selected YAML"})]})]})]}):null]})]})}export{R as default};
|
||||
//# sourceMappingURL=SetupApp-DN7XBkOe.js.map
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
||||
import{e as F,r as s,j as e,S as P,C as c,L as q}from"./index-dJH5nUWV.js";import{o as $,p as D,q as I}from"./api-B8GGCEeo.js";function R(){const l=F(),[a,m]=s.useState(null),[n,b]=s.useState(""),[d,C]=s.useState(""),[p,N]=s.useState(""),[r,S]=s.useState(""),[f,v]=s.useState(""),[o,w]=s.useState(null),[x,h]=s.useState(!1),[g,i]=s.useState("");s.useEffect(()=>{$(l).then(t=>m(t.required)).catch(t=>i(t.message))},[l]);async function j(t){h(!0),i("");try{await t(),m(!1),i("Setup completed. You can now open the administration application and log in.")}catch(u){const E=Array.isArray(u.validationErrors)?` ${u.validationErrors.map(y=>`${y.path}: ${y.message}`).join("; ")}`:"";i(`${u.message}${E}`)}finally{h(!1)}}function k(t){if(t.preventDefault(),r!==f){i("Passwords do not match.");return}j(()=>D(l,{setupCode:n,username:d,discordId:p,password:r}))}function A(t){t.preventDefault(),o&&j(async()=>I(l,{setupCode:n,fileName:o.name,yaml:await o.text()}))}return e.jsxs("div",{className:"min-h-screen bg-neutral-950 p-1 text-slate-100",children:[e.jsx(P,{}),e.jsxs("main",{className:"mx-auto flex min-h-screen w-full max-w-3xl flex-col justify-center gap-0.5",children:[e.jsxs(c,{title:"MultiRover setup",meta:a===null?"checking":a?"required":"complete",bodyClassName:"space-y-0.5 p-1 text-sm",children:[a?e.jsx("p",{children:"Enter the one-time code from setup-code.txt in the server data folder, then create the first lockdown administrator or import an existing configuration."}):null,a===!1?e.jsx(q,{className:"button-dark inline-block",to:"/admin",children:"Open administration"}):null,g?e.jsx("p",{className:"surface p-1 text-sm text-slate-200",children:g}):null]}),a?e.jsxs(e.Fragment,{children:[e.jsxs(c,{title:"Setup authorization",bodyClassName:"p-1",children:[e.jsx("label",{className:"block text-xs font-semibold text-slate-200",children:"One-time setup code"}),e.jsx("input",{className:"field-input mt-0.5 w-full font-mono",value:n,onChange:t=>b(t.target.value)})]}),e.jsx(c,{title:"Create first administrator",bodyClassName:"p-1",children:e.jsxs("form",{className:"grid gap-0.5 md:grid-cols-2",onSubmit:k,children:[e.jsx("input",{className:"field-input",placeholder:"Username",value:d,onChange:t=>C(t.target.value)}),e.jsx("input",{className:"field-input",placeholder:"Discord id (optional)",value:p,onChange:t=>N(t.target.value)}),e.jsx("input",{className:"field-input",type:"password",placeholder:"Password",value:r,onChange:t=>S(t.target.value)}),e.jsx("input",{className:"field-input",type:"password",placeholder:"Confirm password",value:f,onChange:t=>v(t.target.value)}),e.jsx("button",{className:"button-dark md:col-span-2",type:"submit",disabled:x||!n||!d||!r,children:"Create lockdown administrator"})]})}),e.jsxs(c,{title:"Import configuration file",bodyClassName:"space-y-0.5 p-1 text-sm",children:[e.jsx("p",{className:"text-xs text-slate-400",children:"Choose an existing YAML configuration explicitly. The server validates and imports it once, and its secrets are never displayed back in the browser."}),e.jsxs("form",{className:"flex flex-col gap-0.5 md:flex-row",onSubmit:A,children:[e.jsx("input",{className:"field-input flex-1",type:"file",accept:".yaml,.yml,text/yaml",onChange:t=>w(t.target.files?.[0]||null)}),e.jsx("button",{className:"button-dark",type:"submit",disabled:x||!n||!o,children:"Import selected YAML"})]})]})]}):null]})]})}export{R as default};
|
||||
//# sourceMappingURL=SetupApp-Ya006OtX.js.map
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
||||
function u(i){return class extends i{_playedRanges=[];_currentPlayedRange=null;_RANGE_EPSILON=.5;_seeking=!1;connectedCallback(){super.connectedCallback&&super.connectedCallback(),this.addEventListener("play",()=>this.onPlaybackStart({time:this.currentTime})),this.addEventListener("pause",()=>this.onPlaybackStop({time:this.currentTime})),this.addEventListener("ended",()=>this.onPlaybackStop({time:this.currentTime})),this.addEventListener("seeking",()=>this.onSeeking()),this.addEventListener("seeked",()=>this.onSeeked({time:this.currentTime}))}onPlaybackStart(e={}){this._seeking=!1;const{time:t}=e,n=typeof t=="number"?t:this.currentTime;this._currentPlayedRange||(this._currentPlayedRange={start:n,end:n})}onSeeking(){this._seeking=!0,this._commitCurrentRange()}onSeeked(e={}){this._seeking=!1;const{time:t}=e,n=typeof t=="number"?t:this.currentTime;this._currentPlayedRange={start:n,end:n}}onPlaybackStop(e={}){const{time:t}=e,n=typeof t=="number"?t:this.currentTime;this._commitCurrentRange(n)}_commitCurrentRange(e){if(!this._currentPlayedRange)return;typeof e=="number"&&(this._currentPlayedRange.end=e);const{start:t,end:n}=this._currentPlayedRange;this._currentPlayedRange=null,this.addPlayedRange(t,n)}addPlayedRange(e,t){if(e>=t)return;const n=this._RANGE_EPSILON,c=[...this._playedRanges,{start:e,end:t}];c.sort((a,s)=>a.start-s.start);const r=[];for(const a of c){if(!r.length){r.push({...a});continue}const s=r[r.length-1];a.start<=s.end+n?(s.start=Math.min(s.start,a.start),s.end=Math.max(s.end,a.end)):r.push({...a})}this._playedRanges=r}get played(){const e=this.currentTime;return!this.paused&&!this._currentPlayedRange&&typeof e=="number"&&(this._currentPlayedRange={start:e,end:e}),this._currentPlayedRange&&typeof e=="number"&&(e>this._currentPlayedRange.end&&(this._currentPlayedRange.end=e),this.addPlayedRange(this._currentPlayedRange.start,this._currentPlayedRange.end)),this._playedRanges.length?l(this._playedRanges.map(t=>[t.start,t.end])):l([[0,0]])}}}function l(i){return Object.defineProperties(i,{start:{value:d=>i[d][0]},end:{value:d=>i[d][1]}}),i}export{u as M};
|
||||
//# sourceMappingURL=index-B4nclofW.js.map
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+20
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+7
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+22
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+7
@@ -0,0 +1,7 @@
|
||||
import{_ as N,R as A}from"./index-dJH5nUWV.js";import{M as D,C as S}from"./mixin-BY5xW4uI.js";class P extends D(S){static shadowRootOptions={...S.shadowRootOptions};static getTemplateHTML=i=>{const{src:n,...o}=i;return S.getTemplateHTML(o)};#t;attributeChangedCallback(i,n,o){i!=="src"&&super.attributeChangedCallback(i,n,o),i==="src"&&n!=o&&this.load()}async _initThumbnails(i){const n=async(d,m)=>{const v=[],g=i.timescale||1,h=i.startNumber||1,E=i.presentationTimeOffset?i.presentationTimeOffset/g:0,_=i.segmentDuration;for(let l=0;l<d;l++){const T=C({thIndex:l,thduration:m,ttiles:d,tduration:_,startNumber:h,pto:E}),w=T+m,M=new Promise((f,y)=>{this.api.provideThumbnail(T,({url:a,width:s,height:c,x:p,y:b})=>{try{const R=new VTTCue(T,w,`${a}#xywh=${p},${b},${s},${c}`);f(R)}catch(R){y(R)}})});v.push(M)}return await Promise.all(v).catch(l=>console.error("Error processing thumbnails",l))},{totalThumbnails:o,thumbnailDuration:e}=$(i),u=await n(o,e);let r=this.nativeEl.querySelector('track[label="thumbnails"]');if(!r){r=O(),this.nativeEl.appendChild(r);const d=x(u);r.src=d,r.dispatchEvent(new Event("change"))}}async load(){if(this.#t){this.api.attachSource(this.src);return}this.#t=!0;const i=await N(()=>import("./dash.all.min-C9gxBrL1.js"),[]);this.api=i.MediaPlayer().create(),this.api.initialize(this.nativeEl,this.src,this.autoplay),this.api.on(i.MediaPlayer.events.STREAM_INITIALIZED,()=>{const n=this.api.getRepresentationsByType("video");let o=this.videoTracks.getTrackById("main");o||(o=this.addVideoTrack("main"),o.id="main",o.selected=!0),n.forEach(e=>{const u=e.bandwidth??e.bitrate??(Number.isFinite(e.bitrateInKbit)?e.bitrateInKbit*1e3:void 0),r=o.addRendition(e.id,e.width,e.height,e.mimeType??e.codec,u);r.id=e.id}),this.videoRenditions.addEventListener("change",()=>{const e=this.videoRenditions[this.videoRenditions.selectedIndex];e?.id?(this.api.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:!1}}}}),this.api.setRepresentationForTypeById("video",e.id,!0)):this.api.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:!0}}}})}),this.api.isDynamic()||this.api.getRepresentationsByType("image").forEach(async(u,r)=>{r>0||this._initThumbnails(u)})})}}function $(t){var i,n;const o=t.essentialProperties[0],[e,u]=o.value.split("x").map(Number),r=e*u,d=((n=(i=t.adaptation)==null?void 0:i.period)==null?void 0:n.duration)||null,m=t.segmentDuration,v=t.timescale||1,h=m/v/r;return{totalThumbnails:d!=null?Math.ceil(d/h):Math.ceil(m/h),thumbnailDuration:h}}function C({thIndex:t,tduration:i,thduration:n,ttiles:o,startNumber:e,pto:u}){const r=Math.floor(t/o)+e,d=t%o+1,m=(r-1)*i-u,v=(d-1)*n;return m+v}function O(){const t=document.createElement("track");return t.kind="metadata",t.label="thumbnails",t.srclang="en",t.mode="hidden",t.default=!0,t}function x(t){let i=`WEBVTT
|
||||
|
||||
`;for(const e of t)i+=`${o(e.startTime)} --> ${o(e.endTime)}
|
||||
`,i+=`${e.text}
|
||||
|
||||
`;const n=new Blob([i],{type:"text/vtt"});return URL.createObjectURL(n);function o(e){const u=String(Math.floor(e/3600)).padStart(2,"0"),r=String(Math.floor(e%3600/60)).padStart(2,"0"),d=(e%60).toFixed(3).padStart(6,"0");return`${u}:${r}:${d}`}}globalThis.customElements&&!globalThis.customElements.get("dash-video")&&globalThis.customElements.define("dash-video",P);var B=P,H=new Set(["style","children","ref","key","suppressContentEditableWarning","suppressHydrationWarning","dangerouslySetInnerHTML"]),F={className:"class",htmlFor:"for"};function I(t){return t.toLowerCase()}function k(t){if(typeof t=="boolean")return t?"":void 0;if(typeof t!="function"&&!(typeof t=="object"&&t!==null))return t}function W({react:t,tagName:i,elementClass:n,events:o,displayName:e,defaultProps:u,toAttributeName:r=I,toAttributeValue:d=k}){const m=Number.parseInt(t.version)>=19,v=t.forwardRef((g,h)=>{var E,_;const l=t.useRef(null),T=t.useRef(new Map),w={},M={},f={},y={};for(const[a,s]of Object.entries(g)){if(H.has(a)){f[a]=s;continue}const c=r(F[a]??a);if(n.prototype&&a in n.prototype&&!(a in(((E=globalThis.HTMLElement)==null?void 0:E.prototype)??{}))&&!((_=n.observedAttributes)!=null&&_.some(b=>b===c))){y[a]=s;continue}if(a.startsWith("on")){w[a]=s;continue}const p=d(s);if(c&&p!=null&&(M[c]=String(p),m||(f[c]=p)),c&&m){const b=k(s);p!==b?f[c]=p:f[c]=s}}if(typeof window<"u"){for(const a in w){const s=w[a],c=a.endsWith("Capture"),p=(o?.[a]??a.slice(2).toLowerCase()).slice(0,c?-7:void 0);t.useLayoutEffect(()=>{const b=l?.current;if(!(!b||typeof s!="function"))return b.addEventListener(p,s,c),()=>{b.removeEventListener(p,s,c)}},[l?.current,s])}t.useLayoutEffect(()=>{if(l.current===null)return;const a=new Map;for(const s in y)L(l.current,s,y[s]),T.current.delete(s),a.set(s,y[s]);for(const[s,c]of T.current)L(l.current,s,void 0);T.current=a})}if(typeof window>"u"&&n?.getTemplateHTML&&n?.shadowRootOptions){const{mode:a,delegatesFocus:s}=n.shadowRootOptions,c=t.createElement("template",{shadowrootmode:a,shadowrootdelegatesfocus:s,dangerouslySetInnerHTML:{__html:n.getTemplateHTML(M,g)},key:"ce-la-react-ssr-template-shadow-root"});f.children=[c,f.children]}return t.createElement(i,{...u,...f,ref:t.useCallback(a=>{l.current=a,typeof h=="function"?h(a):h!==null&&(h.current=a)},[h])},f.children)});return v.displayName=e??n.name,v}function L(t,i,n){var o;t[i]=n,n==null&&i in(((o=globalThis.HTMLElement)==null?void 0:o.prototype)??{})&&t.removeAttribute(i)}var V=W({react:A,tagName:"dash-video",elementClass:B,toAttributeName(t){return t==="muted"?"":t==="defaultMuted"?"muted":I(t)}});export{V as default};
|
||||
//# sourceMappingURL=react-C61Q8sy9.js.map
|
||||
File diff suppressed because one or more lines are too long
+20
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+35
@@ -0,0 +1,35 @@
|
||||
import{R as W}from"./index-dJH5nUWV.js";const $=["abort","canplay","canplaythrough","durationchange","emptied","encrypted","ended","error","loadeddata","loadedmetadata","loadstart","pause","play","playing","progress","ratechange","seeked","seeking","stalled","suspend","timeupdate","volumechange","waiting","waitingforkey","resize","enterpictureinpicture","leavepictureinpicture","webkitbeginfullscreen","webkitendfullscreen","webkitpresentationmodechanged"],A=globalThis.document?.createElement("template");A&&(A.innerHTML=`
|
||||
<style>
|
||||
:host {
|
||||
display: inline-block;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
video,
|
||||
audio {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
</style>
|
||||
<slot></slot>
|
||||
`);const N=(i,{tag:t,is:s})=>{const d=globalThis.document?.createElement(t,{is:s}),f=d?S(d):[];return class g extends i{static Events=$;static template=A;static skipAttributes=[];static#t;static get observedAttributes(){return g.#l(),[...d?.constructor?.observedAttributes??[],"autopictureinpicture","disablepictureinpicture","disableremoteplayback","autoplay","controls","controlslist","crossorigin","loop","muted","playsinline","poster","preload","src"]}static#l(){if(this.#t)return;this.#t=!0;const e=new Set(this.observedAttributes);e.delete("muted");for(let o of f){if(o in this.prototype)continue;if(typeof d[o]=="function")this.prototype[o]=function(...r){this.#e();const a=()=>this.call?this.call(o,...r):this.nativeEl[o].apply(this.nativeEl,r);return this.loadComplete&&!this.isLoaded?this.loadComplete.then(a):a()};else{let r={get(){this.#e();let a=o.toLowerCase();if(e.has(a)){const p=this.getAttribute(a);return p===null?!1:p===""?!0:p}return this.get?.(o)??this.nativeEl?.[o]??this.#s[o]}};o!==o.toUpperCase()&&(r.set=async function(a){this.#e();let p=o.toLowerCase();if(e.has(p)){a===!0||a===!1||a==null?this.toggleAttribute(p,!!a):this.setAttribute(p,a);return}if(this.loadComplete&&!this.isLoaded&&await this.loadComplete,this.set){this.set(o,a);return}this.nativeEl[o]=a}),Object.defineProperty(this.prototype,o,r)}}}#o;#n;#r=!1;#i=!1;#a;#s;constructor(){super(),this.shadowRoot||(this.attachShadow({mode:"open"}),this.shadowRoot.append(this.constructor.template.content.cloneNode(!0))),this.load!==g.prototype.load&&(this.loadComplete=new O)}get loadComplete(){return this.#n}set loadComplete(e){this.#i=!1,this.#n=e,e?.then(()=>{this.#i=!0})}get isLoaded(){return this.#i}get nativeEl(){return this.#a??this.shadowRoot.querySelector(t)??this.querySelector(t)}set nativeEl(e){this.#a=e}get defaultMuted(){return this.hasAttribute("muted")}set defaultMuted(e){this.toggleAttribute("muted",!!e)}get src(){return this.getAttribute("src")}set src(e){this.setAttribute("src",`${e}`)}get preload(){return this.getAttribute("preload")??this.nativeEl?.preload}set preload(e){this.setAttribute("preload",`${e}`)}async#e(){if(this.#o)return;this.#o=!0,this.#u(),this.#c();for(let u of f)this.#d(u);const e=new Map,o=this.shadowRoot.querySelector("slot:not([name])");o?.addEventListener("slotchange",()=>{const u=new Map(e);o.assignedElements().filter(r=>["track","source"].includes(r.localName)).forEach(async r=>{u.delete(r);let a=e.get(r);a||(a=r.cloneNode(),e.set(r,a)),this.loadComplete&&!this.isLoaded&&await this.loadComplete,this.nativeEl.append?.(a)}),u.forEach(r=>r.remove())});for(let u of this.constructor.Events)this.shadowRoot.addEventListener?.(u,r=>{r.target===this.nativeEl&&this.dispatchEvent(new CustomEvent(r.type,{detail:r.detail}))},!0)}#d(e){if(Object.prototype.hasOwnProperty.call(this,e)){const o=this[e];delete this[e],this[e]=o}}#u(){const e=document.createElement(t,{is:s});e.muted=this.hasAttribute("muted");for(let{name:o,value:u}of this.attributes)e.setAttribute(o,u);this.#s={};for(let o of S(e))this.#s[o]=e[o];e.removeAttribute("src"),e.load()}async#c(){if(this.loadComplete&&!this.isLoaded&&await this.loadComplete,!this.nativeEl){const e=document.createElement(t,{is:s});e.part=t,this.shadowRoot.append(e)}this.nativeEl.muted=this.hasAttribute("muted")}attributeChangedCallback(e,o,u){this.#e(),e==="src"&&this.load!==g.prototype.load&&this.#h(),this.#p(e,o,u)}async#h(){this.#r&&(this.loadComplete=new O),this.#r=!0,await Promise.resolve(),await this.load(),this.loadComplete?.resolve(),await this.loadComplete}async#p(e,o,u){this.loadComplete&&!this.isLoaded&&await this.loadComplete,!["id","class",...this.constructor.skipAttributes].includes(e)&&(u===null?this.nativeEl.removeAttribute?.(e):this.nativeEl.setAttribute?.(e,u))}connectedCallback(){this.#e()}}};function S(i){let t=[];for(let s=Object.getPrototypeOf(i);s&&s!==HTMLElement.prototype;s=Object.getPrototypeOf(s))t.push(...Object.getOwnPropertyNames(s));return t}class O extends Promise{constructor(t=()=>{}){let s,d;super((f,c)=>{t(f,c),s=f,d=c}),this.resolve=s,this.reject=d}}const B=globalThis.document?N(HTMLElement,{tag:"video"}):class{};globalThis.document&&N(HTMLElement,{tag:"audio"});var H,R;const L=(H=globalThis.document)==null?void 0:H.createElement("template");L&&(L.innerHTML=`
|
||||
<div class="wistia_embed"></div>
|
||||
`);const P=(R=globalThis.document)==null?void 0:R.createElement("template");P&&(P.innerHTML=`
|
||||
<style>
|
||||
:host {
|
||||
display: inline-block;
|
||||
min-width: 300px;
|
||||
min-height: 150px;
|
||||
position: relative;
|
||||
}
|
||||
::slotted(.wistia_embed) {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<slot></slot>
|
||||
`);function D(i,t){if(Object.hasOwn(i,t)){const s=i[t];delete i[t],i[t]=s}}class q extends B{static template=P;static skipAttributes=["src"];#t=null;constructor(){super(),D(this,"config")}get config(){return this.#t}set config(t){this.#t=t}get nativeEl(){var t;return((t=this.api)==null?void 0:t.elem())??this.querySelector("video")}async load(){var t;if((t=this.querySelector(".wistia_embed"))==null||t.remove(),!this.src)return;await new Promise(o=>setTimeout(o,50));const s=/(?:wistia\.com|wi\.st)\/(?:medias|embed)\/(.*)$/i,d=this.src.match(s)[1],f={autoPlay:this.autoplay,silentAutoPlay:this.autoplay?"allow":void 0,preload:this.preload??"metadata",playsinline:this.playsInline,endVideoBehavior:this.loop?"loop":void 0,chromeless:!this.controls,playButton:this.controls,muted:this.defaultMuted,...this.#t??{}};this.append(L.content.cloneNode(!0));const c=this.querySelector(".wistia_embed");c.id||(c.id=z(d)),c.classList.add(`wistia_async_${d}`),await F("https://fast.wistia.com/assets/external/E-v1.js","Wistia"),this.api=await new Promise(o=>{globalThis._wq.push({id:c.id,onReady:o,options:f})})}async attributeChangedCallback(t,s,d){if(t==="controls"){switch(await this.loadComplete,t){case"controls":this.api.bigPlayButtonEnabled(this.controls),this.controls?this.api.releaseChromeless():this.api.requestChromeless();break}return}super.attributeChangedCallback(t,s,d)}get duration(){var t;return(t=this.api)==null?void 0:t.duration()}async play(){return await this.loadComplete,this.api.play(),new Promise(t=>this.addEventListener("playing",t))}}const T={};async function F(i,t){if(T[i])return T[i];if(self[t])return self[t];const s=new Promise((d,f)=>{const c=document.createElement("script");c.defer=!0,c.src=i,c.onload=()=>d(self[t]),c.onerror=f,document.head.append(c)});return T[i]=s,s}let U=0;function z(i){const t=++U;return`${i}${t}`}globalThis.customElements&&!globalThis.customElements.get("wistia-video")&&globalThis.customElements.define("wistia-video",q);var V=q,G=new Set(["style","children","ref","key","suppressContentEditableWarning","suppressHydrationWarning","dangerouslySetInnerHTML"]),J={className:"class",htmlFor:"for"};function I(i){return i.toLowerCase()}function j(i){if(typeof i=="boolean")return i?"":void 0;if(typeof i!="function"&&!(typeof i=="object"&&i!==null))return i}function K({react:i,tagName:t,elementClass:s,events:d,displayName:f,defaultProps:c,toAttributeName:e=I,toAttributeValue:o=j}){const u=Number.parseInt(i.version)>=19,r=i.forwardRef((a,p)=>{var M,k;const m=i.useRef(null),E=i.useRef(new Map),C={},_={},v={},w={};for(const[n,l]of Object.entries(a)){if(G.has(n)){v[n]=l;continue}const h=e(J[n]??n);if(s.prototype&&n in s.prototype&&!(n in(((M=globalThis.HTMLElement)==null?void 0:M.prototype)??{}))&&!((k=s.observedAttributes)!=null&&k.some(b=>b===h))){w[n]=l;continue}if(n.startsWith("on")){C[n]=l;continue}const y=o(l);if(h&&y!=null&&(_[h]=String(y),u||(v[h]=y)),h&&u){const b=j(l);y!==b?v[h]=y:v[h]=l}}if(typeof window<"u"){for(const n in C){const l=C[n],h=n.endsWith("Capture"),y=(d?.[n]??n.slice(2).toLowerCase()).slice(0,h?-7:void 0);i.useLayoutEffect(()=>{const b=m?.current;if(!(!b||typeof l!="function"))return b.addEventListener(y,l,h),()=>{b.removeEventListener(y,l,h)}},[m?.current,l])}i.useLayoutEffect(()=>{if(m.current===null)return;const n=new Map;for(const l in w)x(m.current,l,w[l]),E.current.delete(l),n.set(l,w[l]);for(const[l,h]of E.current)x(m.current,l,void 0);E.current=n})}if(typeof window>"u"&&s?.getTemplateHTML&&s?.shadowRootOptions){const{mode:n,delegatesFocus:l}=s.shadowRootOptions,h=i.createElement("template",{shadowrootmode:n,shadowrootdelegatesfocus:l,dangerouslySetInnerHTML:{__html:s.getTemplateHTML(_,a)},key:"ce-la-react-ssr-template-shadow-root"});v.children=[h,v.children]}return i.createElement(t,{...c,...v,ref:i.useCallback(n=>{m.current=n,typeof p=="function"?p(n):p!==null&&(p.current=n)},[p])},v.children)});return r.displayName=f??s.name,r}function x(i,t,s){var d;i[t]=s,s==null&&t in(((d=globalThis.HTMLElement)==null?void 0:d.prototype)??{})&&i.removeAttribute(t)}var X=K({react:W,tagName:"wistia-video",elementClass:V,toAttributeName(i){return i==="muted"?"":i==="defaultMuted"?"muted":I(i)}});export{X as default};
|
||||
//# sourceMappingURL=react-CVBBC_mw.js.map
|
||||
File diff suppressed because one or more lines are too long
+23
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+20
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+20
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+23
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+22
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+7
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+35
@@ -0,0 +1,35 @@
|
||||
import{R as W}from"./index-5EWtcenj.js";const $=["abort","canplay","canplaythrough","durationchange","emptied","encrypted","ended","error","loadeddata","loadedmetadata","loadstart","pause","play","playing","progress","ratechange","seeked","seeking","stalled","suspend","timeupdate","volumechange","waiting","waitingforkey","resize","enterpictureinpicture","leavepictureinpicture","webkitbeginfullscreen","webkitendfullscreen","webkitpresentationmodechanged"],A=globalThis.document?.createElement("template");A&&(A.innerHTML=`
|
||||
<style>
|
||||
:host {
|
||||
display: inline-block;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
video,
|
||||
audio {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
</style>
|
||||
<slot></slot>
|
||||
`);const N=(i,{tag:t,is:s})=>{const d=globalThis.document?.createElement(t,{is:s}),f=d?S(d):[];return class g extends i{static Events=$;static template=A;static skipAttributes=[];static#t;static get observedAttributes(){return g.#l(),[...d?.constructor?.observedAttributes??[],"autopictureinpicture","disablepictureinpicture","disableremoteplayback","autoplay","controls","controlslist","crossorigin","loop","muted","playsinline","poster","preload","src"]}static#l(){if(this.#t)return;this.#t=!0;const e=new Set(this.observedAttributes);e.delete("muted");for(let o of f){if(o in this.prototype)continue;if(typeof d[o]=="function")this.prototype[o]=function(...r){this.#e();const a=()=>this.call?this.call(o,...r):this.nativeEl[o].apply(this.nativeEl,r);return this.loadComplete&&!this.isLoaded?this.loadComplete.then(a):a()};else{let r={get(){this.#e();let a=o.toLowerCase();if(e.has(a)){const p=this.getAttribute(a);return p===null?!1:p===""?!0:p}return this.get?.(o)??this.nativeEl?.[o]??this.#s[o]}};o!==o.toUpperCase()&&(r.set=async function(a){this.#e();let p=o.toLowerCase();if(e.has(p)){a===!0||a===!1||a==null?this.toggleAttribute(p,!!a):this.setAttribute(p,a);return}if(this.loadComplete&&!this.isLoaded&&await this.loadComplete,this.set){this.set(o,a);return}this.nativeEl[o]=a}),Object.defineProperty(this.prototype,o,r)}}}#o;#n;#r=!1;#i=!1;#a;#s;constructor(){super(),this.shadowRoot||(this.attachShadow({mode:"open"}),this.shadowRoot.append(this.constructor.template.content.cloneNode(!0))),this.load!==g.prototype.load&&(this.loadComplete=new O)}get loadComplete(){return this.#n}set loadComplete(e){this.#i=!1,this.#n=e,e?.then(()=>{this.#i=!0})}get isLoaded(){return this.#i}get nativeEl(){return this.#a??this.shadowRoot.querySelector(t)??this.querySelector(t)}set nativeEl(e){this.#a=e}get defaultMuted(){return this.hasAttribute("muted")}set defaultMuted(e){this.toggleAttribute("muted",!!e)}get src(){return this.getAttribute("src")}set src(e){this.setAttribute("src",`${e}`)}get preload(){return this.getAttribute("preload")??this.nativeEl?.preload}set preload(e){this.setAttribute("preload",`${e}`)}async#e(){if(this.#o)return;this.#o=!0,this.#u(),this.#c();for(let u of f)this.#d(u);const e=new Map,o=this.shadowRoot.querySelector("slot:not([name])");o?.addEventListener("slotchange",()=>{const u=new Map(e);o.assignedElements().filter(r=>["track","source"].includes(r.localName)).forEach(async r=>{u.delete(r);let a=e.get(r);a||(a=r.cloneNode(),e.set(r,a)),this.loadComplete&&!this.isLoaded&&await this.loadComplete,this.nativeEl.append?.(a)}),u.forEach(r=>r.remove())});for(let u of this.constructor.Events)this.shadowRoot.addEventListener?.(u,r=>{r.target===this.nativeEl&&this.dispatchEvent(new CustomEvent(r.type,{detail:r.detail}))},!0)}#d(e){if(Object.prototype.hasOwnProperty.call(this,e)){const o=this[e];delete this[e],this[e]=o}}#u(){const e=document.createElement(t,{is:s});e.muted=this.hasAttribute("muted");for(let{name:o,value:u}of this.attributes)e.setAttribute(o,u);this.#s={};for(let o of S(e))this.#s[o]=e[o];e.removeAttribute("src"),e.load()}async#c(){if(this.loadComplete&&!this.isLoaded&&await this.loadComplete,!this.nativeEl){const e=document.createElement(t,{is:s});e.part=t,this.shadowRoot.append(e)}this.nativeEl.muted=this.hasAttribute("muted")}attributeChangedCallback(e,o,u){this.#e(),e==="src"&&this.load!==g.prototype.load&&this.#h(),this.#p(e,o,u)}async#h(){this.#r&&(this.loadComplete=new O),this.#r=!0,await Promise.resolve(),await this.load(),this.loadComplete?.resolve(),await this.loadComplete}async#p(e,o,u){this.loadComplete&&!this.isLoaded&&await this.loadComplete,!["id","class",...this.constructor.skipAttributes].includes(e)&&(u===null?this.nativeEl.removeAttribute?.(e):this.nativeEl.setAttribute?.(e,u))}connectedCallback(){this.#e()}}};function S(i){let t=[];for(let s=Object.getPrototypeOf(i);s&&s!==HTMLElement.prototype;s=Object.getPrototypeOf(s))t.push(...Object.getOwnPropertyNames(s));return t}class O extends Promise{constructor(t=()=>{}){let s,d;super((f,c)=>{t(f,c),s=f,d=c}),this.resolve=s,this.reject=d}}const B=globalThis.document?N(HTMLElement,{tag:"video"}):class{};globalThis.document&&N(HTMLElement,{tag:"audio"});var H,R;const L=(H=globalThis.document)==null?void 0:H.createElement("template");L&&(L.innerHTML=`
|
||||
<div class="wistia_embed"></div>
|
||||
`);const P=(R=globalThis.document)==null?void 0:R.createElement("template");P&&(P.innerHTML=`
|
||||
<style>
|
||||
:host {
|
||||
display: inline-block;
|
||||
min-width: 300px;
|
||||
min-height: 150px;
|
||||
position: relative;
|
||||
}
|
||||
::slotted(.wistia_embed) {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<slot></slot>
|
||||
`);function D(i,t){if(Object.hasOwn(i,t)){const s=i[t];delete i[t],i[t]=s}}class q extends B{static template=P;static skipAttributes=["src"];#t=null;constructor(){super(),D(this,"config")}get config(){return this.#t}set config(t){this.#t=t}get nativeEl(){var t;return((t=this.api)==null?void 0:t.elem())??this.querySelector("video")}async load(){var t;if((t=this.querySelector(".wistia_embed"))==null||t.remove(),!this.src)return;await new Promise(o=>setTimeout(o,50));const s=/(?:wistia\.com|wi\.st)\/(?:medias|embed)\/(.*)$/i,d=this.src.match(s)[1],f={autoPlay:this.autoplay,silentAutoPlay:this.autoplay?"allow":void 0,preload:this.preload??"metadata",playsinline:this.playsInline,endVideoBehavior:this.loop?"loop":void 0,chromeless:!this.controls,playButton:this.controls,muted:this.defaultMuted,...this.#t??{}};this.append(L.content.cloneNode(!0));const c=this.querySelector(".wistia_embed");c.id||(c.id=z(d)),c.classList.add(`wistia_async_${d}`),await F("https://fast.wistia.com/assets/external/E-v1.js","Wistia"),this.api=await new Promise(o=>{globalThis._wq.push({id:c.id,onReady:o,options:f})})}async attributeChangedCallback(t,s,d){if(t==="controls"){switch(await this.loadComplete,t){case"controls":this.api.bigPlayButtonEnabled(this.controls),this.controls?this.api.releaseChromeless():this.api.requestChromeless();break}return}super.attributeChangedCallback(t,s,d)}get duration(){var t;return(t=this.api)==null?void 0:t.duration()}async play(){return await this.loadComplete,this.api.play(),new Promise(t=>this.addEventListener("playing",t))}}const T={};async function F(i,t){if(T[i])return T[i];if(self[t])return self[t];const s=new Promise((d,f)=>{const c=document.createElement("script");c.defer=!0,c.src=i,c.onload=()=>d(self[t]),c.onerror=f,document.head.append(c)});return T[i]=s,s}let U=0;function z(i){const t=++U;return`${i}${t}`}globalThis.customElements&&!globalThis.customElements.get("wistia-video")&&globalThis.customElements.define("wistia-video",q);var V=q,G=new Set(["style","children","ref","key","suppressContentEditableWarning","suppressHydrationWarning","dangerouslySetInnerHTML"]),J={className:"class",htmlFor:"for"};function I(i){return i.toLowerCase()}function j(i){if(typeof i=="boolean")return i?"":void 0;if(typeof i!="function"&&!(typeof i=="object"&&i!==null))return i}function K({react:i,tagName:t,elementClass:s,events:d,displayName:f,defaultProps:c,toAttributeName:e=I,toAttributeValue:o=j}){const u=Number.parseInt(i.version)>=19,r=i.forwardRef((a,p)=>{var M,k;const m=i.useRef(null),E=i.useRef(new Map),C={},_={},v={},w={};for(const[n,l]of Object.entries(a)){if(G.has(n)){v[n]=l;continue}const h=e(J[n]??n);if(s.prototype&&n in s.prototype&&!(n in(((M=globalThis.HTMLElement)==null?void 0:M.prototype)??{}))&&!((k=s.observedAttributes)!=null&&k.some(b=>b===h))){w[n]=l;continue}if(n.startsWith("on")){C[n]=l;continue}const y=o(l);if(h&&y!=null&&(_[h]=String(y),u||(v[h]=y)),h&&u){const b=j(l);y!==b?v[h]=y:v[h]=l}}if(typeof window<"u"){for(const n in C){const l=C[n],h=n.endsWith("Capture"),y=(d?.[n]??n.slice(2).toLowerCase()).slice(0,h?-7:void 0);i.useLayoutEffect(()=>{const b=m?.current;if(!(!b||typeof l!="function"))return b.addEventListener(y,l,h),()=>{b.removeEventListener(y,l,h)}},[m?.current,l])}i.useLayoutEffect(()=>{if(m.current===null)return;const n=new Map;for(const l in w)x(m.current,l,w[l]),E.current.delete(l),n.set(l,w[l]);for(const[l,h]of E.current)x(m.current,l,void 0);E.current=n})}if(typeof window>"u"&&s?.getTemplateHTML&&s?.shadowRootOptions){const{mode:n,delegatesFocus:l}=s.shadowRootOptions,h=i.createElement("template",{shadowrootmode:n,shadowrootdelegatesfocus:l,dangerouslySetInnerHTML:{__html:s.getTemplateHTML(_,a)},key:"ce-la-react-ssr-template-shadow-root"});v.children=[h,v.children]}return i.createElement(t,{...c,...v,ref:i.useCallback(n=>{m.current=n,typeof p=="function"?p(n):p!==null&&(p.current=n)},[p])},v.children)});return r.displayName=f??s.name,r}function x(i,t,s){var d;i[t]=s,s==null&&t in(((d=globalThis.HTMLElement)==null?void 0:d.prototype)??{})&&i.removeAttribute(t)}var X=K({react:W,tagName:"wistia-video",elementClass:V,toAttributeName(i){return i==="muted"?"":i==="defaultMuted"?"muted":I(i)}});export{X as default};
|
||||
//# sourceMappingURL=react-DxdYv-AY.js.map
|
||||
File diff suppressed because one or more lines are too long
+18
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+18
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+7
@@ -0,0 +1,7 @@
|
||||
import{_ as N,R as A}from"./index-5EWtcenj.js";import{M as D,C as S}from"./mixin-BY5xW4uI.js";class P extends D(S){static shadowRootOptions={...S.shadowRootOptions};static getTemplateHTML=i=>{const{src:n,...o}=i;return S.getTemplateHTML(o)};#t;attributeChangedCallback(i,n,o){i!=="src"&&super.attributeChangedCallback(i,n,o),i==="src"&&n!=o&&this.load()}async _initThumbnails(i){const n=async(d,m)=>{const v=[],g=i.timescale||1,h=i.startNumber||1,E=i.presentationTimeOffset?i.presentationTimeOffset/g:0,_=i.segmentDuration;for(let l=0;l<d;l++){const T=C({thIndex:l,thduration:m,ttiles:d,tduration:_,startNumber:h,pto:E}),w=T+m,M=new Promise((f,y)=>{this.api.provideThumbnail(T,({url:a,width:s,height:c,x:p,y:b})=>{try{const R=new VTTCue(T,w,`${a}#xywh=${p},${b},${s},${c}`);f(R)}catch(R){y(R)}})});v.push(M)}return await Promise.all(v).catch(l=>console.error("Error processing thumbnails",l))},{totalThumbnails:o,thumbnailDuration:e}=$(i),u=await n(o,e);let r=this.nativeEl.querySelector('track[label="thumbnails"]');if(!r){r=O(),this.nativeEl.appendChild(r);const d=x(u);r.src=d,r.dispatchEvent(new Event("change"))}}async load(){if(this.#t){this.api.attachSource(this.src);return}this.#t=!0;const i=await N(()=>import("./dash.all.min-C9gxBrL1.js"),[]);this.api=i.MediaPlayer().create(),this.api.initialize(this.nativeEl,this.src,this.autoplay),this.api.on(i.MediaPlayer.events.STREAM_INITIALIZED,()=>{const n=this.api.getRepresentationsByType("video");let o=this.videoTracks.getTrackById("main");o||(o=this.addVideoTrack("main"),o.id="main",o.selected=!0),n.forEach(e=>{const u=e.bandwidth??e.bitrate??(Number.isFinite(e.bitrateInKbit)?e.bitrateInKbit*1e3:void 0),r=o.addRendition(e.id,e.width,e.height,e.mimeType??e.codec,u);r.id=e.id}),this.videoRenditions.addEventListener("change",()=>{const e=this.videoRenditions[this.videoRenditions.selectedIndex];e?.id?(this.api.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:!1}}}}),this.api.setRepresentationForTypeById("video",e.id,!0)):this.api.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:!0}}}})}),this.api.isDynamic()||this.api.getRepresentationsByType("image").forEach(async(u,r)=>{r>0||this._initThumbnails(u)})})}}function $(t){var i,n;const o=t.essentialProperties[0],[e,u]=o.value.split("x").map(Number),r=e*u,d=((n=(i=t.adaptation)==null?void 0:i.period)==null?void 0:n.duration)||null,m=t.segmentDuration,v=t.timescale||1,h=m/v/r;return{totalThumbnails:d!=null?Math.ceil(d/h):Math.ceil(m/h),thumbnailDuration:h}}function C({thIndex:t,tduration:i,thduration:n,ttiles:o,startNumber:e,pto:u}){const r=Math.floor(t/o)+e,d=t%o+1,m=(r-1)*i-u,v=(d-1)*n;return m+v}function O(){const t=document.createElement("track");return t.kind="metadata",t.label="thumbnails",t.srclang="en",t.mode="hidden",t.default=!0,t}function x(t){let i=`WEBVTT
|
||||
|
||||
`;for(const e of t)i+=`${o(e.startTime)} --> ${o(e.endTime)}
|
||||
`,i+=`${e.text}
|
||||
|
||||
`;const n=new Blob([i],{type:"text/vtt"});return URL.createObjectURL(n);function o(e){const u=String(Math.floor(e/3600)).padStart(2,"0"),r=String(Math.floor(e%3600/60)).padStart(2,"0"),d=(e%60).toFixed(3).padStart(6,"0");return`${u}:${r}:${d}`}}globalThis.customElements&&!globalThis.customElements.get("dash-video")&&globalThis.customElements.define("dash-video",P);var B=P,H=new Set(["style","children","ref","key","suppressContentEditableWarning","suppressHydrationWarning","dangerouslySetInnerHTML"]),F={className:"class",htmlFor:"for"};function I(t){return t.toLowerCase()}function k(t){if(typeof t=="boolean")return t?"":void 0;if(typeof t!="function"&&!(typeof t=="object"&&t!==null))return t}function W({react:t,tagName:i,elementClass:n,events:o,displayName:e,defaultProps:u,toAttributeName:r=I,toAttributeValue:d=k}){const m=Number.parseInt(t.version)>=19,v=t.forwardRef((g,h)=>{var E,_;const l=t.useRef(null),T=t.useRef(new Map),w={},M={},f={},y={};for(const[a,s]of Object.entries(g)){if(H.has(a)){f[a]=s;continue}const c=r(F[a]??a);if(n.prototype&&a in n.prototype&&!(a in(((E=globalThis.HTMLElement)==null?void 0:E.prototype)??{}))&&!((_=n.observedAttributes)!=null&&_.some(b=>b===c))){y[a]=s;continue}if(a.startsWith("on")){w[a]=s;continue}const p=d(s);if(c&&p!=null&&(M[c]=String(p),m||(f[c]=p)),c&&m){const b=k(s);p!==b?f[c]=p:f[c]=s}}if(typeof window<"u"){for(const a in w){const s=w[a],c=a.endsWith("Capture"),p=(o?.[a]??a.slice(2).toLowerCase()).slice(0,c?-7:void 0);t.useLayoutEffect(()=>{const b=l?.current;if(!(!b||typeof s!="function"))return b.addEventListener(p,s,c),()=>{b.removeEventListener(p,s,c)}},[l?.current,s])}t.useLayoutEffect(()=>{if(l.current===null)return;const a=new Map;for(const s in y)L(l.current,s,y[s]),T.current.delete(s),a.set(s,y[s]);for(const[s,c]of T.current)L(l.current,s,void 0);T.current=a})}if(typeof window>"u"&&n?.getTemplateHTML&&n?.shadowRootOptions){const{mode:a,delegatesFocus:s}=n.shadowRootOptions,c=t.createElement("template",{shadowrootmode:a,shadowrootdelegatesfocus:s,dangerouslySetInnerHTML:{__html:n.getTemplateHTML(M,g)},key:"ce-la-react-ssr-template-shadow-root"});f.children=[c,f.children]}return t.createElement(i,{...u,...f,ref:t.useCallback(a=>{l.current=a,typeof h=="function"?h(a):h!==null&&(h.current=a)},[h])},f.children)});return v.displayName=e??n.name,v}function L(t,i,n){var o;t[i]=n,n==null&&i in(((o=globalThis.HTMLElement)==null?void 0:o.prototype)??{})&&t.removeAttribute(i)}var V=W({react:A,tagName:"dash-video",elementClass:B,toAttributeName(t){return t==="muted"?"":t==="defaultMuted"?"muted":I(t)}});export{V as default};
|
||||
//# sourceMappingURL=react-gdx3v4jU.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -12,8 +12,8 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<!-- site-metadata:inject -->
|
||||
<!-- analytics:inject -->
|
||||
<script type="module" crossorigin src="/assets/index-n0JxE1Mv.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-xCRVLGjq.css">
|
||||
<script type="module" crossorigin src="/assets/index-dJH5nUWV.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DOmNOCX4.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -7,6 +7,7 @@ const logger = require('../../globals/logger').child('assignment');
|
||||
const { MODES, getMode, modeEvents } = require('../modeManager');
|
||||
const { roleEvents, getRole, isAdmin, isLockdownAdmin } = require('../roleService');
|
||||
const roverManager = require('../roverManager');
|
||||
const { getOperatingMode } = require('../operatingModeService');
|
||||
const { compareRoversForAssignment } = require('./roverRanking');
|
||||
|
||||
const socketRefs = new Map(); // socketId -> socket
|
||||
@@ -131,7 +132,7 @@ roverManager.managerEvents.on('switch', ({ socketId, roverId }) => {
|
||||
});
|
||||
|
||||
function assignSocket(socket, options = {}) {
|
||||
if (!socket || isAdmin(socket) || getRole(socket) !== 'user') {
|
||||
if (!socket || getOperatingMode(socket) !== 'rover' || isAdmin(socket) || getRole(socket) !== 'user') {
|
||||
return;
|
||||
}
|
||||
// avoid double assignment
|
||||
@@ -311,13 +312,22 @@ function describeAssignment(socketId) {
|
||||
|
||||
module.exports = {
|
||||
assignmentEvents,
|
||||
resumeAssignment: assignSocket,
|
||||
releaseForOperatingMode: (socket) => {
|
||||
// Include direct admin control as well as automatic assignment ownership.
|
||||
const roverIds = new Set(roverManager.getRoversForSocket(socket.id));
|
||||
const assigned = assignments.get(socket.id);
|
||||
if (assigned) roverIds.add(assigned);
|
||||
waiting.delete(socket.id);
|
||||
roverIds.forEach((roverId) => forceRelease(roverId, socket.id));
|
||||
},
|
||||
describeAssignment,
|
||||
forceRelease,
|
||||
forceReleaseWithNotice,
|
||||
rerollAssignments,
|
||||
getAssignedRover: (socketId) => assignments.get(socketId) || null,
|
||||
moveAssignment: (socket, roverId, { releasePrevious = true } = {}) => {
|
||||
if (!socket || !roverId) return;
|
||||
if (!socket || !roverId || getOperatingMode(socket) !== 'rover') return;
|
||||
const previous = assignments.get(socket.id);
|
||||
if (previous && previous !== roverId && releasePrevious) {
|
||||
roverManager.releaseControl(previous, socket);
|
||||
|
||||
@@ -2,20 +2,14 @@
|
||||
// Purpose: Builds normalized chat message/typing payloads and rover context snapshots.
|
||||
// Scope: Encapsulates chat DTO construction and rover/driver metadata extraction.
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const io = require('../../globals/io');
|
||||
const roverManager = require('../roverManager');
|
||||
const { getRole } = require('../roleService');
|
||||
const { getOperatingMode } = require('../operatingModeService');
|
||||
const { describeAssignment } = require('../assignmentService');
|
||||
const { getNickname } = require('../nicknameService');
|
||||
const ptzCameraService = require('../ptzCameraService');
|
||||
|
||||
function resolvePtzChatTarget(socketId) {
|
||||
return ptzCameraService.getChatTargetForSocket(socketId) || null;
|
||||
}
|
||||
|
||||
function resolveRoverId(socketId) {
|
||||
const ptzTarget = resolvePtzChatTarget(socketId);
|
||||
if (ptzTarget?.roverId) return ptzTarget.roverId;
|
||||
const primary = roverManager.getPrimaryRoverForSocket(socketId);
|
||||
if (primary) return primary;
|
||||
const assignment = describeAssignment(socketId);
|
||||
@@ -24,54 +18,22 @@ function resolveRoverId(socketId) {
|
||||
|
||||
function resolveRoverColor(roverId) {
|
||||
if (!roverId) return null;
|
||||
if (String(roverId) === ptzCameraService.PTZ_CAMERA_ID) {
|
||||
return ptzCameraService.getPublicState()?.color || null;
|
||||
}
|
||||
const record = roverManager.rovers.get(String(roverId));
|
||||
return record?.meta?.color || null;
|
||||
}
|
||||
|
||||
function resolveRoverName(roverId) {
|
||||
if (!roverId) return null;
|
||||
if (String(roverId) === ptzCameraService.PTZ_CAMERA_ID) {
|
||||
return ptzCameraService.getPublicState()?.name || null;
|
||||
}
|
||||
const record = roverManager.rovers.get(String(roverId));
|
||||
return record?.meta?.name || null;
|
||||
}
|
||||
|
||||
function isPtzChatTargetId(roverId) {
|
||||
/*
|
||||
PTZ is intentionally treated as a virtual rover for chat identity only. It
|
||||
does not live in roverManager.rovers because movement, video authorization,
|
||||
and queue ownership are PTZ-service concerns, but chat needs one stable
|
||||
"rover-like" id so the existing web UI, Discord bridge, and AI transcript
|
||||
code can all render the same badge without learning PTZ internals.
|
||||
*/
|
||||
return Boolean(roverId) && String(roverId) === ptzCameraService.PTZ_CAMERA_ID;
|
||||
}
|
||||
|
||||
function isPublicChatTargetId(roverId, socket = null) {
|
||||
if (!roverId) return false;
|
||||
/*
|
||||
Normal rovers remain governed by the existing replay visibility rule, which
|
||||
is also the rule chat historically used to avoid exposing closed private
|
||||
rover activity. PTZ gets an explicit allow-list entry here because it is a
|
||||
public chat target that deliberately pretends to be a rover, even though it
|
||||
is not a roverManager record.
|
||||
*/
|
||||
if (isPtzChatTargetId(roverId)) return true;
|
||||
return roverManager.canReplayRoverId(roverId, socket) === true;
|
||||
return Boolean(roverId) && roverManager.canReplayRoverId(roverId, socket) === true;
|
||||
}
|
||||
|
||||
function isPrivateClosedRoverId(roverId) {
|
||||
if (!roverId) return false;
|
||||
/*
|
||||
PTZ uses the existing rover badge fields so chat rows can reuse RoverLabel,
|
||||
but it is not a private rover. Let PTZ-badged messages broadcast normally
|
||||
instead of falling into the closed-private rover path for unknown ids.
|
||||
*/
|
||||
return !isPublicChatTargetId(roverId);
|
||||
return Boolean(roverId) && !isPublicChatTargetId(roverId);
|
||||
}
|
||||
|
||||
function normalizeProfileImageUrl(value) {
|
||||
@@ -170,6 +132,8 @@ function buildMessage(socket, text, meta = {}) {
|
||||
socketId: socket?.id || null,
|
||||
nickname: meta.nickname || getNickname(socket) || null,
|
||||
role: meta.role || getRole(socket),
|
||||
operatingMode: socket ? getOperatingMode(socket) : null,
|
||||
operatingModeDisplay: socket ? ptzCameraService.getOperatingModeDisplay(socket.id) : null,
|
||||
roverId,
|
||||
roverName,
|
||||
roverColor,
|
||||
@@ -215,6 +179,8 @@ function buildTypingPayload(socket, meta = {}) {
|
||||
socketId,
|
||||
nickname: meta.nickname || getNickname(socket) || null,
|
||||
role: meta.role || getRole(socket),
|
||||
operatingMode: socket ? getOperatingMode(socket) : null,
|
||||
operatingModeDisplay: socket ? ptzCameraService.getOperatingModeDisplay(socket.id) : null,
|
||||
roverId,
|
||||
roverName,
|
||||
roverColor,
|
||||
@@ -231,7 +197,6 @@ function buildTypingPayload(socket, meta = {}) {
|
||||
|
||||
module.exports = {
|
||||
resolveRoverId,
|
||||
isPtzChatTargetId,
|
||||
isPublicChatTargetId,
|
||||
isPrivateClosedRoverId,
|
||||
buildRoverCtxSnapshot,
|
||||
|
||||
@@ -19,13 +19,6 @@ const { getLastAccessNoticeAt, setLastAccessNoticeAt } = require('./state');
|
||||
|
||||
function playTypingNote(roverId, note, socketId) {
|
||||
if (!roverId) return;
|
||||
/*
|
||||
PTZ borrows the roverId field for chat badges, but it has no rover command
|
||||
channel. Skipping the song command here keeps PTZ chat from producing noisy
|
||||
"unknown rover" command attempts while still allowing the message itself to
|
||||
behave like rover chat everywhere else.
|
||||
*/
|
||||
if (String(roverId) === ptzCameraService.PTZ_CAMERA_ID) return;
|
||||
try {
|
||||
issueCommand(roverId, {
|
||||
type: 'song',
|
||||
@@ -90,8 +83,8 @@ function maybeSendAccessNotice(message, sendSystemMessage) {
|
||||
}
|
||||
|
||||
function maybeSpeak(socket, message, ttsOptions) {
|
||||
if (!ttsOptions || !message?.roverId) return;
|
||||
if (String(message.roverId) === ptzCameraService.PTZ_CAMERA_ID) {
|
||||
if (!ttsOptions || !message) return;
|
||||
if (message.operatingMode === 'ptz') {
|
||||
/*
|
||||
PTZ has no rover websocket, but it does have a real speaker behind the
|
||||
Reolink/neolink path. Keep PTZ routing here so chat remains the single
|
||||
@@ -107,6 +100,7 @@ function maybeSpeak(socket, message, ttsOptions) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!message.roverId) return;
|
||||
const record = roverManager.rovers.get(message.roverId);
|
||||
const ttsEnabled = Boolean(record?.meta?.audio?.ttsEnabled);
|
||||
if (!ttsEnabled) return;
|
||||
|
||||
@@ -59,12 +59,6 @@ function createChatBridgeHandlers(deps) {
|
||||
function handleChatBridgeOutbound(event) {
|
||||
const payload = event?.payload;
|
||||
if (!payload) return;
|
||||
/*
|
||||
Outbound bridge filtering must use chat visibility, not rover replay
|
||||
visibility. PTZ deliberately uses roverId: "ptz-camera" so the existing
|
||||
chat badge path can be reused, but that id is not a roverManager rover and
|
||||
would be dropped by canReplayRoverId().
|
||||
*/
|
||||
if (payload?.roverId && !isPublicChatTargetId(payload.roverId)) return;
|
||||
const guildConfigs = listGuildConfigs();
|
||||
if (!guildConfigs.length) return;
|
||||
|
||||
@@ -24,14 +24,8 @@ function formatWebhookUsername(payload) {
|
||||
const origin = payload.discordGuildName ? ` (From: ${payload.discordGuildName})` : '';
|
||||
return `${name}${origin}${botTag}${spectatorTag}${adminTag}`;
|
||||
}
|
||||
/*
|
||||
The chat payload already carries the resolved display name for rover-like
|
||||
targets. Prefer that name so PTZ, which is intentionally pretending to be a
|
||||
rover in chat, shows up as "PTZ Camera" instead of the internal id
|
||||
"ptz-camera"; fall back to the id for older payloads or missing metadata.
|
||||
*/
|
||||
const roverTagLabel = payload.roverName || payload.roverId;
|
||||
const roverTag = payload.roverId ? ` [${roverTagLabel}]` : '';
|
||||
const tagLabel = payload.operatingModeDisplay?.name || payload.roverName || payload.roverId;
|
||||
const roverTag = tagLabel ? ` [${tagLabel}]` : '';
|
||||
return `${name}${botTag}${spectatorTag}${adminTag}${roverTag}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ const { loadConfig, getFeatureFlags, registerConfigurationHandler } = require('.
|
||||
const { getConfiguredSocials } = require('../sessionService/configuration');
|
||||
const { getMode, MODES } = require('../modeManager');
|
||||
const roverManager = require('../roverManager');
|
||||
const { getTurnQueues } = require('../turnService');
|
||||
const { getTurnQueues, getActiveDrivers } = require('../turnService');
|
||||
const { buildRoverTurn } = require('../turnService/display');
|
||||
const { getRoomCameras, getRoomCameraState } = require('../roomCameraService');
|
||||
const { getRoverSnapshotState } = require('../roverSnapshotService');
|
||||
const { getRole } = require('../roleService');
|
||||
@@ -157,22 +158,6 @@ function publicRoverIdSet(roster = []) {
|
||||
return new Set(roster.map((rover) => String(rover?.id || '')).filter(Boolean));
|
||||
}
|
||||
|
||||
function filterPublicTurnQueues(turnQueues = {}, publicIds) {
|
||||
const visible = publicIds instanceof Set ? publicIds : new Set();
|
||||
const next = {};
|
||||
/*
|
||||
RoverQueuesPanel creates fallback rows for queue ids that are not present in
|
||||
the roster, so the public payload must filter queues with the exact same
|
||||
privacy boundary as the roster. Otherwise a closed-private rover can leak as
|
||||
an orphan queue row after a private access grant assigns someone to it.
|
||||
*/
|
||||
Object.entries(turnQueues || {}).forEach(([roverId, info]) => {
|
||||
if (!visible.has(String(roverId))) return;
|
||||
next[roverId] = info;
|
||||
});
|
||||
return next;
|
||||
}
|
||||
|
||||
function filterPublicUsers(users = [], publicIds) {
|
||||
const visible = publicIds instanceof Set ? publicIds : new Set();
|
||||
/*
|
||||
@@ -203,6 +188,8 @@ function buildLocalInfo() {
|
||||
const publicIds = publicRoverIdSet(publicRoster);
|
||||
const roster = publicRoster.map((rover) => (lockdown ? rover : addRoverSnapshotLinks(rover)));
|
||||
const users = filterPublicUsers(Array.from(io.sockets.sockets.values()).map(buildUserEntry), publicIds);
|
||||
const turnQueues = getTurnQueues();
|
||||
const activeDrivers = getActiveDrivers();
|
||||
const roomCameras = lockdown || !features.roomCameras ? [] : getRoomCameras().map(buildRoomCameraInfo);
|
||||
return {
|
||||
instance: {
|
||||
@@ -212,8 +199,12 @@ function buildLocalInfo() {
|
||||
features,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
roster,
|
||||
turnQueues: filterPublicTurnQueues(getTurnQueues(), publicIds),
|
||||
// Only public roster entries reach the builder; queue metadata follows the
|
||||
// same visibility boundary instead of creating separate orphan queue rows.
|
||||
roster: roster.map((rover) => ({
|
||||
...rover,
|
||||
turn: buildRoverTurn({ rover, mode, socketId: null, turnInfo: turnQueues[rover.id], activeDriverId: activeDrivers[rover.id], users }),
|
||||
})),
|
||||
users,
|
||||
roomCameras,
|
||||
socials: features.socials ? getConfiguredSocials(config) : [],
|
||||
@@ -349,7 +340,6 @@ function normalizeRemotePayload(entry, payload) {
|
||||
features: instance.features && typeof instance.features === 'object' ? instance.features : {},
|
||||
},
|
||||
roster: Array.isArray(payload?.roster) ? payload.roster : [],
|
||||
turnQueues: payload?.turnQueues && typeof payload.turnQueues === 'object' ? payload.turnQueues : {},
|
||||
users: Array.isArray(payload?.users) ? payload.users : [],
|
||||
roomCameras: Array.isArray(payload?.roomCameras) ? payload.roomCameras : [],
|
||||
socials: Array.isArray(payload?.socials) ? payload.socials : [],
|
||||
@@ -419,7 +409,6 @@ function markOffline(entry, error) {
|
||||
publicUrl: previous.instance?.publicUrl || entry.url,
|
||||
},
|
||||
roster: previous.roster || [],
|
||||
turnQueues: previous.turnQueues || {},
|
||||
users: previous.users || [],
|
||||
roomCameras: previous.roomCameras || [],
|
||||
socials: previous.socials || [],
|
||||
|
||||
@@ -370,11 +370,6 @@ function createSnapshotEngine(deps) {
|
||||
.filter((entry) => {
|
||||
const roverId = entry?.roverId ? String(entry.roverId) : null;
|
||||
if (!roverId) return true;
|
||||
/*
|
||||
This is a chat transcript filter, not a physical-rover filter. PTZ
|
||||
chat intentionally carries a rover-like id so transcript consumers can
|
||||
render it consistently, even though roverManager cannot replay that id.
|
||||
*/
|
||||
return isPublicChatTargetId(roverId);
|
||||
});
|
||||
const chatRecent = allRecentMessages
|
||||
@@ -430,6 +425,8 @@ function createSnapshotEngine(deps) {
|
||||
type: 'chat',
|
||||
nickname: entry.nickname || entry.socketId?.slice(0, 6) || 'unknown',
|
||||
text: entry.text || '',
|
||||
operating_mode: entry.operatingMode || null,
|
||||
operating_mode_name: entry.operatingModeDisplay?.name || null,
|
||||
rover_id: roverId,
|
||||
rover_ctx: { ...baseCtx, ...storedCtx },
|
||||
};
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
const EventEmitter = require('events');
|
||||
|
||||
const operatingModeEvents = new EventEmitter();
|
||||
const transitions = new WeakMap();
|
||||
|
||||
function getOperatingMode(socket) {
|
||||
// Handshake intent reserves the mode before role-driven assignment runs.
|
||||
// It grants no camera permissions; entry still checks access and readiness.
|
||||
return socket?.data?.operatingMode
|
||||
|| (socket?.handshake?.auth?.operatingMode === 'ptz' ? 'ptz' : 'rover');
|
||||
}
|
||||
|
||||
async function transition(socket, operatingMode) {
|
||||
// Resolve services at the action boundary to avoid a dependency cycle with
|
||||
// assignment and camera policy, which both read operating mode.
|
||||
const assignment = require('../assignmentService');
|
||||
const roverManager = require('../roverManager');
|
||||
const ptz = require('../ptzCameraService');
|
||||
if (operatingMode === 'rover' && getOperatingMode(socket) === 'rover') {
|
||||
return { operatingMode, state: ptz.getPublicState(socket) };
|
||||
}
|
||||
if (operatingMode === 'ptz') {
|
||||
await ptz.prepareParticipation(socket);
|
||||
if (socket.disconnected) throw new Error('Session disconnected');
|
||||
ptz.checkParticipationAccess(socket);
|
||||
const leave = roverManager.canLeaveCurrentRover(socket);
|
||||
if (!leave.ok) throw new Error(leave.message);
|
||||
|
||||
// Block new rover ownership before release events notify other services.
|
||||
socket.data.operatingMode = 'ptz';
|
||||
assignment.releaseForOperatingMode(socket);
|
||||
socket.data.ptzEntered = true;
|
||||
} else {
|
||||
ptz.releaseTurn(socket);
|
||||
socket.data.ptzEntered = false;
|
||||
socket.data.operatingMode = 'rover';
|
||||
assignment.resumeAssignment(socket);
|
||||
}
|
||||
operatingModeEvents.emit('change', { socket, operatingMode });
|
||||
return { operatingMode, state: ptz.getPublicState(socket) };
|
||||
}
|
||||
|
||||
function enqueueTransition(socket, action) {
|
||||
// Camera initialization can yield. Serialize switches and turn release per
|
||||
// socket so a late entry cannot undo a subsequent exit or release request.
|
||||
const previous = transitions.get(socket) || Promise.resolve();
|
||||
const next = previous.catch(() => {}).then(() => {
|
||||
if (socket.disconnected) throw new Error('Session disconnected');
|
||||
return action();
|
||||
});
|
||||
transitions.set(socket, next);
|
||||
return next;
|
||||
}
|
||||
|
||||
function setOperatingMode(socket, operatingMode) {
|
||||
if (!['rover', 'ptz'].includes(operatingMode)) {
|
||||
return Promise.reject(new Error('Invalid operating mode'));
|
||||
}
|
||||
return enqueueTransition(socket, () => transition(socket, operatingMode));
|
||||
}
|
||||
|
||||
function runPtzTurnAction(socket, action) {
|
||||
return enqueueTransition(socket, () => {
|
||||
const ptz = require('../ptzCameraService');
|
||||
if (action === 'release') return ptz.releaseTurn(socket);
|
||||
if (action !== 'request') throw new Error('Invalid PTZ turn action');
|
||||
if (getOperatingMode(socket) !== 'ptz' || !socket.data.ptzEntered) {
|
||||
throw new Error('Enter PTZ before requesting a turn');
|
||||
}
|
||||
return ptz.claimTurn(socket);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { getOperatingMode, setOperatingMode, runPtzTurnAction, operatingModeEvents };
|
||||
@@ -396,11 +396,6 @@ async function runDecision(triggerReason) {
|
||||
.filter((entry) => Number(entry?.ts || 0) >= runtime.contextResetAt)
|
||||
.filter((entry) => {
|
||||
if (!entry?.roverId) return true;
|
||||
/*
|
||||
Chat context should preserve every public chat target, including the
|
||||
PTZ virtual rover. Rover replay visibility alone would drop PTZ because
|
||||
it is owned by ptzCameraService instead of roverManager.
|
||||
*/
|
||||
return isPublicChatTargetId(entry.roverId);
|
||||
})
|
||||
.slice(-(MAX_CHAT_CONTEXT + MAX_BOT_CONTEXT));
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// Purpose: Generates server-side TTS files and sends them to the Reolink TrackMix speaker through neolink.
|
||||
// Scope: Owns file/cache/process details for PTZ speech only; PTZ ownership, chat identity, and camera motion stay in index.js.
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const fsp = require('fs/promises');
|
||||
const path = require('path');
|
||||
const { spawn } = require('child_process');
|
||||
@@ -78,6 +77,7 @@ function createPtzAudioPlayback(deps) {
|
||||
const volume = clampNumber(audioConfig.volume, DEFAULT_VOLUME, 0, 4);
|
||||
const fliteDefaultVoice = String(audioConfig.fliteDefaultVoice || 'kal').trim();
|
||||
|
||||
let disposed = false;
|
||||
let playbackProc = null;
|
||||
let playbackSeq = 0;
|
||||
|
||||
@@ -244,12 +244,16 @@ function createPtzAudioPlayback(deps) {
|
||||
}, 1200);
|
||||
}
|
||||
|
||||
async function playFile(filePath, context = {}) {
|
||||
if (!audioEnabled) throw new Error('PTZ audio disabled');
|
||||
async function playFile(filePath, context = {}, authorize = () => {}) {
|
||||
if (disposed || !audioEnabled) throw new Error('PTZ audio unavailable');
|
||||
authorize();
|
||||
const neolinkConfigPath = await ensureNeolinkConfig();
|
||||
const stat = await fsp.stat(filePath);
|
||||
if (!stat.isFile()) throw new Error(`PTZ TTS file is not a regular file: ${filePath}`);
|
||||
|
||||
// Rendering and config IO may outlive a turn or camera configuration.
|
||||
if (disposed) throw new Error('PTZ audio configuration changed');
|
||||
authorize();
|
||||
stopActivePlayback('new-playback');
|
||||
const seq = ++playbackSeq;
|
||||
const args = [
|
||||
@@ -302,13 +306,13 @@ function createPtzAudioPlayback(deps) {
|
||||
return { pid: proc.pid || null, seq };
|
||||
}
|
||||
|
||||
async function speakText(text, ttsOptions = {}, context = {}) {
|
||||
async function speakText(text, ttsOptions = {}, context = {}, authorize = () => {}) {
|
||||
const rendered = await ensureTtsFile(text, ttsOptions);
|
||||
await playFile(rendered.filePath, {
|
||||
...context,
|
||||
engine: rendered.engine,
|
||||
cached: rendered.cached,
|
||||
});
|
||||
}, authorize);
|
||||
return rendered;
|
||||
}
|
||||
|
||||
@@ -316,6 +320,10 @@ function createPtzAudioPlayback(deps) {
|
||||
getState,
|
||||
speakText,
|
||||
stopActivePlayback,
|
||||
dispose: () => {
|
||||
disposed = true;
|
||||
stopActivePlayback('configuration-change');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,98 @@
|
||||
// Owns PTZ turns only. Camera IO, permissions and operating-mode entry are supplied
|
||||
// by the service, so queue transitions cannot create physical rover ownership.
|
||||
function createParticipation({ io, events, emitChange, getTurnDurationMs, canParticipate, stopMotion, revokeVideo }) {
|
||||
const state = { operatorSocketId: null, queue: [], deadline: null, generation: 0 };
|
||||
let timer = null;
|
||||
|
||||
function remove(socketId) {
|
||||
state.queue = state.queue.filter((id) => id !== socketId);
|
||||
}
|
||||
|
||||
function stopOperator(reason) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
const previous = state.operatorSocketId;
|
||||
state.operatorSocketId = null;
|
||||
state.deadline = null;
|
||||
state.generation += 1;
|
||||
if (!previous) return;
|
||||
revokeVideo(previous);
|
||||
stopMotion(reason).catch(() => {});
|
||||
events.emit('operator', { socketId: previous, action: 'release', reason });
|
||||
}
|
||||
|
||||
function scheduleTurn() {
|
||||
// Uncontested control has no deadline. Starting a new wait gets a full turn,
|
||||
// while later queue changes must not reset an already running countdown.
|
||||
if (!state.operatorSocketId || !state.queue.length) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
state.deadline = null;
|
||||
return;
|
||||
}
|
||||
if (timer) return;
|
||||
const duration = getTurnDurationMs();
|
||||
state.deadline = Date.now() + duration;
|
||||
timer = setTimeout(() => {
|
||||
timer = null;
|
||||
state.queue = state.queue.filter(eligible);
|
||||
if (state.queue.length) {
|
||||
// Preserve PTZ's explicit request-to-rejoin rule after a handoff.
|
||||
stopOperator('turn-expired');
|
||||
advance('turn-expired');
|
||||
} else {
|
||||
scheduleTurn();
|
||||
emitChange('turn-uncontested');
|
||||
}
|
||||
}, duration);
|
||||
}
|
||||
|
||||
function eligible(socketId) {
|
||||
const socket = io.sockets.sockets.get(socketId);
|
||||
return socket && !socket.disconnected && canParticipate(socket);
|
||||
}
|
||||
|
||||
function advance(reason) {
|
||||
state.queue = state.queue.filter(eligible);
|
||||
while (!state.operatorSocketId && state.queue.length) {
|
||||
const next = state.queue.shift();
|
||||
if (!eligible(next)) continue;
|
||||
state.operatorSocketId = next;
|
||||
state.generation += 1;
|
||||
events.emit('operator', { socketId: next, action: 'active' });
|
||||
}
|
||||
scheduleTurn();
|
||||
emitChange(reason);
|
||||
}
|
||||
|
||||
function claim(socket) {
|
||||
if (!eligible(socket.id)) throw new Error('PTZ participation required');
|
||||
if (state.operatorSocketId === socket.id || state.queue.includes(socket.id)) return;
|
||||
state.queue.push(socket.id);
|
||||
advance('queue-join');
|
||||
}
|
||||
|
||||
function release(socketId, reason = 'turn-release') {
|
||||
remove(socketId);
|
||||
if (state.operatorSocketId === socketId) stopOperator(reason);
|
||||
advance(reason);
|
||||
}
|
||||
|
||||
function reconcile() {
|
||||
state.queue = state.queue.filter(eligible);
|
||||
if (state.operatorSocketId && !eligible(state.operatorSocketId)) stopOperator('access-change');
|
||||
advance('access-change');
|
||||
}
|
||||
|
||||
function clear(reason) {
|
||||
state.queue = [];
|
||||
stopOperator(reason);
|
||||
}
|
||||
|
||||
return {
|
||||
state, claim, release, reconcile, clear,
|
||||
revoke: (reason) => { stopOperator(reason); advance(reason); },
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { createParticipation };
|
||||
@@ -0,0 +1,123 @@
|
||||
// Socket transport delegates policy and lifecycle decisions to the PTZ service.
|
||||
function registerSocketGateway(deps) {
|
||||
const { io, isAdmin, participation, emitChange, normalizeSocketArgs,
|
||||
runPtzTurnAction, acceptMotionIntent, getStatus, setSpotlight, setIr,
|
||||
listPresets, gotoPreset, createPreset, removePreset, passesMode,
|
||||
normalizeSnapshotIds, addSnapshotSubscription, sendSnapshotFrame,
|
||||
getSnapshot, removeSnapshotSubscriptions } = deps;
|
||||
io.on('connection', (socket) => {
|
||||
socket.on('ptzCamera:revokeOperator', (firstArg, secondArg) => {
|
||||
const { cb } = normalizeSocketArgs(firstArg, secondArg);
|
||||
if (!isAdmin(socket)) return cb({ error: 'Admin required' });
|
||||
participation.revoke('admin-revoke');
|
||||
cb({ ok: true });
|
||||
});
|
||||
socket.on('ptzCamera:requestTurn', async (firstArg, secondArg) => {
|
||||
const { cb } = normalizeSocketArgs(firstArg, secondArg);
|
||||
try { cb({ ok: true, state: await runPtzTurnAction(socket, 'request') }); }
|
||||
catch (err) { cb({ error: err.message }); }
|
||||
});
|
||||
socket.on('ptzCamera:release', async (firstArg, secondArg) => {
|
||||
const { cb } = normalizeSocketArgs(firstArg, secondArg);
|
||||
try {
|
||||
cb({ ok: true, state: await runPtzTurnAction(socket, 'release') });
|
||||
} catch (err) {
|
||||
cb({ error: err.message });
|
||||
}
|
||||
});
|
||||
socket.on('ptzCamera:motion', (firstArg, secondArg) => {
|
||||
const { payload, cb } = normalizeSocketArgs(firstArg, secondArg);
|
||||
try {
|
||||
/*
|
||||
Acknowledge acceptance of the newest desired state immediately. The
|
||||
serialized ONVIF pump deliberately runs independently of Socket.IO
|
||||
request latency so browser heartbeats cannot accumulate while waiting
|
||||
for a camera SOAP response.
|
||||
*/
|
||||
cb(acceptMotionIntent(socket, payload));
|
||||
} catch (err) {
|
||||
cb({ error: err.message });
|
||||
}
|
||||
});
|
||||
socket.on('ptzCamera:status', async (firstArg, secondArg) => {
|
||||
const { cb } = normalizeSocketArgs(firstArg, secondArg);
|
||||
try {
|
||||
cb({ ok: true, status: await getStatus(socket) });
|
||||
} catch (err) {
|
||||
cb({ error: err.message });
|
||||
}
|
||||
});
|
||||
socket.on('ptzCamera:spotlight', async (firstArg, secondArg) => {
|
||||
const { payload, cb } = normalizeSocketArgs(firstArg, secondArg);
|
||||
try {
|
||||
cb({ ok: true, light: await setSpotlight(socket, payload) });
|
||||
} catch (err) {
|
||||
cb({ error: err.message });
|
||||
}
|
||||
});
|
||||
socket.on('ptzCamera:ir', async (firstArg, secondArg) => {
|
||||
const { payload, cb } = normalizeSocketArgs(firstArg, secondArg);
|
||||
try {
|
||||
cb({ ok: true, ir: await setIr(socket, payload) });
|
||||
} catch (err) {
|
||||
cb({ error: err.message });
|
||||
}
|
||||
});
|
||||
socket.on('ptzCamera:presets:list', async (firstArg, secondArg) => {
|
||||
const { cb } = normalizeSocketArgs(firstArg, secondArg);
|
||||
try {
|
||||
cb({ ok: true, presets: await listPresets(socket) });
|
||||
} catch (err) {
|
||||
cb({ error: err.message });
|
||||
}
|
||||
});
|
||||
socket.on('ptzCamera:preset:goto', async (firstArg, secondArg) => {
|
||||
const { payload, cb } = normalizeSocketArgs(firstArg, secondArg);
|
||||
try {
|
||||
cb(await gotoPreset(socket, payload));
|
||||
} catch (err) {
|
||||
cb({ error: err.message });
|
||||
}
|
||||
});
|
||||
socket.on('ptzCamera:preset:create', async (firstArg, secondArg) => {
|
||||
const { payload, cb } = normalizeSocketArgs(firstArg, secondArg);
|
||||
try {
|
||||
cb(await createPreset(socket, payload));
|
||||
} catch (err) {
|
||||
cb({ error: err.message });
|
||||
}
|
||||
});
|
||||
socket.on('ptzCamera:preset:remove', async (firstArg, secondArg) => {
|
||||
const { payload, cb } = normalizeSocketArgs(firstArg, secondArg);
|
||||
try {
|
||||
cb(await removePreset(socket, payload));
|
||||
} catch (err) {
|
||||
cb({ error: err.message });
|
||||
}
|
||||
});
|
||||
socket.on('ptzCamera:snapshotSubscribe', (firstArg, secondArg) => {
|
||||
const { payload, cb } = normalizeSocketArgs(firstArg, secondArg);
|
||||
try {
|
||||
if (!passesMode(socket)) throw new Error('Not authorized for PTZ snapshots');
|
||||
const ids = normalizeSnapshotIds(payload);
|
||||
addSnapshotSubscription(socket, ids);
|
||||
const lastSnapshotState = getSnapshot();
|
||||
if (lastSnapshotState?.frame) sendSnapshotFrame(socket, lastSnapshotState.frame, lastSnapshotState.ts);
|
||||
cb({ ok: true, subscribed: ids });
|
||||
} catch (err) {
|
||||
cb({ error: err.message });
|
||||
}
|
||||
});
|
||||
socket.on('ptzCamera:snapshotUnsubscribe', (firstArg, secondArg) => {
|
||||
const { payload } = normalizeSocketArgs(firstArg, secondArg);
|
||||
removeSnapshotSubscriptions(socket.id, normalizeSnapshotIds(payload));
|
||||
});
|
||||
socket.on('disconnect', () => {
|
||||
participation.release(socket.id, 'disconnect');
|
||||
removeSnapshotSubscriptions(socket.id);
|
||||
emitChange('disconnect');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { registerSocketGateway };
|
||||
@@ -0,0 +1,9 @@
|
||||
const labels = {
|
||||
...require('../turnService/labels'),
|
||||
waiting: 'Someone else has control',
|
||||
active: 'You control the camera',
|
||||
showName: 'Show camera name',
|
||||
hideName: 'Hide camera name',
|
||||
};
|
||||
|
||||
module.exports = labels;
|
||||
@@ -68,13 +68,13 @@ function getDefaultWebSources(assignment = {}, socket = null) {
|
||||
replay`, follows the camera currently controlled by that socket just as it
|
||||
follows an assigned rover below.
|
||||
|
||||
isOperator is deliberately stricter than PTZ access or queue membership:
|
||||
turn.ownsControl is deliberately stricter than PTZ access or queue membership:
|
||||
spectators and users waiting for a camera turn must not silently replay a
|
||||
camera they are not currently operating. Keeping this rule here also makes
|
||||
every web replay entry point share the same default instead of teaching the
|
||||
chat-command adapter about PTZ-specific state.
|
||||
*/
|
||||
if (ptzCameraService.getPublicState(socket).isOperator) {
|
||||
if (ptzCameraService.getPublicState(socket).turn.ownsControl) {
|
||||
const source = ptzCameraService.getReplaySource();
|
||||
if (!source) return [];
|
||||
return [{ type: source.type, id: String(source.id), label: source.label || source.id }];
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const { getOperatingMode } = require('../operatingModeService');
|
||||
// rover Manager private access
|
||||
// Purpose: Encapsulates private-rover visibility and control-authorization policy checks.
|
||||
// Scope: Keeps runtime behavior unchanged while isolating policy decisions from orchestration wiring.
|
||||
@@ -80,6 +81,7 @@ function createPrivateAccessPolicy(deps) {
|
||||
allowClosedPrivateGrantInLockdown = false,
|
||||
allowClosedPrivateCurrentDriver = false,
|
||||
} = options;
|
||||
if (getOperatingMode(socket) !== 'rover') return 'Leave PTZ before requesting rover control';
|
||||
if (!record) return 'Unknown rover';
|
||||
if (!allowUser && !isAdmin(socket)) return 'Only admins can request control';
|
||||
if (record.locked && !isAdmin(socket)) return 'Rover locked';
|
||||
|
||||
@@ -5,16 +5,18 @@ const io = require('../../globals/io');
|
||||
const logger = require('../../globals/logger').child('sessionService');
|
||||
const { getFeatureFlags, configurationEvents } = require('../../configuration');
|
||||
const { getRole, isAdmin, roleEvents } = require('../roleService');
|
||||
const { getOperatingMode, setOperatingMode, operatingModeEvents } = require('../operatingModeService');
|
||||
const { getMode, modeEvents } = require('../modeManager');
|
||||
const roverManager = require('../roverManager');
|
||||
const { managerEvents } = roverManager;
|
||||
const assignmentService = require('../assignmentService');
|
||||
const { getActiveDrivers, getTurnQueues, turnEvents } = require('../turnService');
|
||||
const { getActiveDrivers, getTurnQueues, canDrive, turnEvents } = require('../turnService');
|
||||
const { buildRoverTurn } = require('../turnService/display');
|
||||
const { getRoomCameras, roomCameraEvents } = require('../roomCameraService');
|
||||
const {
|
||||
getPublicState: getPtzCameraState,
|
||||
getChatTargetForSocket: getPtzChatTargetForSocket,
|
||||
PTZ_CAMERA_ID,
|
||||
getOperatingModeDisplay,
|
||||
getParticipantSocketIds,
|
||||
ptzCameraEvents,
|
||||
} = require('../ptzCameraService');
|
||||
const { getState: getHomeAssistantState, homeAssistantEvents } = require('../homeAssistantService');
|
||||
@@ -111,18 +113,13 @@ function buildBandwidthSavingsSessionState(socket, controllableUserCount = 0) {
|
||||
|
||||
function countControllableUsers(userEntries = []) {
|
||||
const ids = new Set();
|
||||
const ptzParticipants = new Set(getParticipantSocketIds());
|
||||
userEntries.forEach((entry) => {
|
||||
const role = String(entry?.role || '');
|
||||
if (role === 'spectator') return;
|
||||
const socketId = String(entry?.socketId || '').trim();
|
||||
const roverId = String(entry?.roverId || '').trim();
|
||||
/*
|
||||
buildUserEntry already maps PTZ queued/operators to the PTZ pseudo-rover
|
||||
id and normal drivers to their physical rover. Counting entries after that
|
||||
normalization gives the browser the same conceptual "controllable users"
|
||||
count it shows in the user/queue panels without duplicating PTZ UI logic.
|
||||
*/
|
||||
if (socketId && roverId) ids.add(socketId);
|
||||
if (socketId && (roverId || ptzParticipants.has(socketId))) ids.add(socketId);
|
||||
});
|
||||
return ids.size;
|
||||
}
|
||||
@@ -146,19 +143,14 @@ function buildUserEntry(socket) {
|
||||
&& roverManager.isDriver(assignment.roverId, socket)
|
||||
? assignment.roverId
|
||||
: null;
|
||||
const ptzChatTarget = getPtzChatTargetForSocket(socket.id);
|
||||
return {
|
||||
socketId: socket.id,
|
||||
userId: socket?.data?.userId || null,
|
||||
nickname: getNickname(socket) || null,
|
||||
role,
|
||||
/*
|
||||
PTZ is not inserted into the physical rover roster, but for chat and user
|
||||
presence it should read like the user moved to a rover-like target. Prefer
|
||||
the PTZ chat target while the socket is queued or operating so presence,
|
||||
queue lookup, and chat identity all agree.
|
||||
*/
|
||||
roverId: ptzChatTarget?.roverId || verifiedPrimaryRover || verifiedAssignmentRover || null,
|
||||
operatingMode: getOperatingMode(socket),
|
||||
operatingModeDisplay: getOperatingModeDisplay(socket.id),
|
||||
roverId: verifiedPrimaryRover || verifiedAssignmentRover || null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -171,15 +163,7 @@ function buildSession(socket) {
|
||||
const controllableUserCount = countControllableUsers(userEntries);
|
||||
const users = userEntries.map((entry) => ({
|
||||
...entry,
|
||||
/*
|
||||
PTZ is intentionally not a roverManager record, so the normal physical
|
||||
rover visibility filter would erase the user's PTZ chat target. Preserve
|
||||
it here because getPtzChatTargetForSocket already applied the PTZ access
|
||||
and queue/operator rules before buildUserEntry returned it.
|
||||
*/
|
||||
roverId: entry.roverId === PTZ_CAMERA_ID
|
||||
? entry.roverId
|
||||
: filterVisibleRoverId(socket, entry.roverId),
|
||||
roverId: filterVisibleRoverId(socket, entry.roverId),
|
||||
}));
|
||||
const roster = roverManager.getRosterForSocket(socket);
|
||||
const assignment = assignmentService.describeAssignment(socket?.id || '');
|
||||
@@ -201,6 +185,7 @@ function buildSession(socket) {
|
||||
return {
|
||||
socketId: socket?.id || null,
|
||||
role: getRole(socket),
|
||||
operatingMode: getOperatingMode(socket),
|
||||
mode: getMode(),
|
||||
isLocalNetwork: isLocalNetwork(getSocketIp(socket)),
|
||||
bandwidthSavings: buildBandwidthSavingsSessionState(socket, controllableUserCount),
|
||||
@@ -210,15 +195,24 @@ function buildSession(socket) {
|
||||
of navigation and layout without maintaining another feature registry.
|
||||
*/
|
||||
features,
|
||||
roster,
|
||||
roster: roster.map((rover) => ({
|
||||
...rover,
|
||||
permissions: { canControl: getRole(socket) !== 'spectator' && canDrive(rover.id, socket) },
|
||||
turn: buildRoverTurn({
|
||||
rover,
|
||||
mode: getMode(),
|
||||
socketId: socket?.id,
|
||||
turnInfo: turnQueues[rover.id],
|
||||
activeDriverId: activeDrivers[rover.id],
|
||||
users,
|
||||
}),
|
||||
})),
|
||||
odometers: roverManager.getOdometersForSocket(socket),
|
||||
assignment: {
|
||||
...assignment,
|
||||
roverId: assignmentRoverId,
|
||||
status: assignmentRoverId ? assignment.status : assignment.status === 'waiting' ? 'waiting' : null,
|
||||
},
|
||||
activeDrivers,
|
||||
turnQueues,
|
||||
roomCameras: getRoomCameras(),
|
||||
ptzCamera: getPtzCameraState(socket),
|
||||
homeAssistant: getHomeAssistantState(),
|
||||
@@ -280,10 +274,19 @@ function syncAll() {
|
||||
}
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
socket.on('session:setOperatingMode', async ({ operatingMode } = {}, cb = () => {}) => {
|
||||
try {
|
||||
cb({ ok: true, ...await setOperatingMode(socket, operatingMode) });
|
||||
} catch (err) {
|
||||
cb({ error: err.message });
|
||||
}
|
||||
});
|
||||
logger.info('New socket connected', socket.id);
|
||||
syncSocket(socket);
|
||||
});
|
||||
|
||||
operatingModeEvents.on('change', () => syncAll());
|
||||
|
||||
roleEvents.on('change', ({ socket }) => {
|
||||
if (!socket) return;
|
||||
logger.info('Role changed; syncing session', socket.id);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
const { describeQueue } = require('./queueDisplay');
|
||||
const { TURN_DURATION_MS, IDLE_TIMEOUT_MS } = require('./constants');
|
||||
|
||||
const labels = {
|
||||
...require('./labels'),
|
||||
waiting: 'Someone else is driving',
|
||||
active: 'You’re driving',
|
||||
showName: 'Show rover name',
|
||||
hideName: 'Hide rover name',
|
||||
};
|
||||
|
||||
// Build this from visibility-filtered session data so private rover state stays private.
|
||||
function buildRoverTurn({ rover, mode, socketId, turnInfo, activeDriverId, users }) {
|
||||
const queue = turnInfo?.queue || [];
|
||||
// Direct ownership can arrive before queue details on load/reconnect.
|
||||
const currentDriverId = activeDriverId || turnInfo?.current || null;
|
||||
const currentIndex = currentDriverId ? queue.indexOf(currentDriverId) : -1;
|
||||
const userIndex = socketId ? queue.indexOf(socketId) : -1;
|
||||
const enabled = mode === 'turns' && queue.length > 1 && currentIndex >= 0 && userIndex >= 0;
|
||||
const turnsAhead = enabled ? (userIndex - currentIndex + queue.length) % queue.length : null;
|
||||
const display = describeQueue(queue, currentDriverId, users);
|
||||
return {
|
||||
target: { id: rover.id, name: rover.name, color: rover.color, fallback: rover.id },
|
||||
...display,
|
||||
// Null distinguishes a rover that has never had a queue from an empty queue.
|
||||
queue: turnInfo ? display.queue : null,
|
||||
enabled,
|
||||
ownsControl: Boolean(socketId && currentDriverId === socketId),
|
||||
turnsAhead,
|
||||
queueLength: queue.length,
|
||||
deadline: turnInfo?.deadline || null,
|
||||
durationMs: TURN_DURATION_MS,
|
||||
idleDeadline: turnInfo?.idleDeadline || null,
|
||||
idleGraceMs: IDLE_TIMEOUT_MS,
|
||||
labels,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { buildRoverTurn };
|
||||
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
handoff: 'It’s your turn!',
|
||||
idleWarning: 'Start driving or your turn will be skipped',
|
||||
activeTimer: 'Your turn',
|
||||
waitingTimer: 'Waiting',
|
||||
untilTurn: 'until your turn',
|
||||
timeRemaining: 'left',
|
||||
showTimer: 'Show turn timer',
|
||||
hideTimer: 'Hide turn timer',
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
// Queue entries are complete display records. No browser-side user lookup or
|
||||
// naming fallback is needed, including when the queue comes from another server.
|
||||
function describeQueue(socketIds, currentId, users) {
|
||||
const byId = new Map(users.map((user) => [user.socketId, user]));
|
||||
const currentIndex = currentId ? socketIds.indexOf(currentId) : -1;
|
||||
return {
|
||||
queue: socketIds.map((socketId) => {
|
||||
const user = byId.get(socketId);
|
||||
return {
|
||||
socketId,
|
||||
name: user?.nickname || user?.name || socketId.slice(0, 6) || 'unknown',
|
||||
role: user?.role || null,
|
||||
};
|
||||
}),
|
||||
currentId,
|
||||
nextId: socketIds.length > 1 ? socketIds[(currentIndex + 1) % socketIds.length] : null,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { describeQueue };
|
||||
@@ -157,11 +157,11 @@ function identifySocket(socket, payload = {}) {
|
||||
}
|
||||
|
||||
/*
|
||||
Spectator-style pages carry identity too, but only the driver surface
|
||||
participates in duplicate-driver enforcement. This flag remains on the
|
||||
Spectator-style pages carry identity too, but only the driver and PTZ surfaces
|
||||
participate in duplicate-driver enforcement. This flag remains on the
|
||||
socket because it is connection-specific, not person-specific.
|
||||
*/
|
||||
socket.data.identitySurface = payload.identitySurface === 'driver' ? 'driver' : 'passive';
|
||||
socket.data.identitySurface = ['driver', 'ptz'].includes(payload.identitySurface) ? payload.identitySurface : 'passive';
|
||||
|
||||
const result = identifyCanonicalSocket(socket, {
|
||||
...payload,
|
||||
@@ -207,7 +207,7 @@ function enforceSingleDriverSocketPerIdentity(currentSocket) {
|
||||
if (
|
||||
!currentSocket?.id ||
|
||||
!currentUserId ||
|
||||
currentSocket.data?.identitySurface !== 'driver' ||
|
||||
!['driver', 'ptz'].includes(currentSocket.data?.identitySurface) ||
|
||||
!enforceForCurrentSocket
|
||||
) {
|
||||
return;
|
||||
@@ -215,7 +215,7 @@ function enforceSingleDriverSocketPerIdentity(currentSocket) {
|
||||
|
||||
const duplicates = Array.from(io.sockets.sockets.values()).filter((candidate) => {
|
||||
if (!candidate?.id || candidate.id === currentSocket.id || candidate.disconnected) return false;
|
||||
if (candidate?.data?.identitySurface !== 'driver') return false;
|
||||
if (!['driver', 'ptz'].includes(candidate?.data?.identitySurface)) return false;
|
||||
return getUserIdForSocket(candidate) === currentUserId;
|
||||
});
|
||||
|
||||
|
||||
Generated
+1121
-1256
File diff suppressed because it is too large
Load Diff
+8
-6
@@ -4,7 +4,7 @@
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "VITE_ROVERD_URL=https://rover.otter.land vite",
|
||||
"dev": "VITE_ROVERD_URL=http://192.168.0.86:8080 vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"test": "node --test src/controls/inputs/*.test.js src/components/GamepadMappingSettings/*.test.js",
|
||||
@@ -15,12 +15,15 @@
|
||||
"@rjsf/core": "^6.10.0",
|
||||
"@rjsf/validator-ajv8": "^6.10.0",
|
||||
"@thumbmarkjs/thumbmarkjs": "^1.10.0",
|
||||
"linkify-react": "^4.3.3",
|
||||
"linkifyjs": "^4.3.3",
|
||||
"midi-file": "^1.2.4",
|
||||
"papaparse": "^5.5.4",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-icons": "^5.7.0",
|
||||
"react-joystick-component": "^6.2.1",
|
||||
"react-player": "^3.4.0",
|
||||
"react-router-dom": "^7.9.6",
|
||||
"three": "^0.184.0",
|
||||
"uplot": "^1.6.32",
|
||||
@@ -28,18 +31,17 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@vitejs/plugin-react": "^5.1.0",
|
||||
"autoprefixer": "^10.4.22",
|
||||
"baseline-browser-mapping": "^2.11.25",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"postcss": "^8.5.6",
|
||||
"socket.io-client": "4.8.3",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"vite": "^7.2.2",
|
||||
"vitest": "^5.0.0"
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -137,7 +137,7 @@ export default function ConfigurationEditor({ snapshot, socket, runSensitive, on
|
||||
return (
|
||||
<div ref={editorRef} className="configuration-editor">
|
||||
<CardFrame title="Configuration" meta={`revision ${revision}`} clipOverflow={false} bodyClassName="p-0.5">
|
||||
<div ref={toolbarRef} className="configuration-toolbar sticky top-0 z-20 mb-0.5 space-y-0.5 border border-neutral-500/60 bg-neutral-900/95 p-0.5 backdrop-blur">
|
||||
<div ref={toolbarRef} className="configuration-toolbar sticky top-0 z-20 mb-0.5 space-y-0.5 border border-neutral-500/60 bg-neutral-900/95 p-0.5 backdrop-blur-sm">
|
||||
<p className="text-xs text-slate-400">Saving applies the complete revision immediately and reloads each affected service.</p>
|
||||
{/* All document actions stay together at the start of the toolbar. The
|
||||
editor may use a wide canvas, but width is never used to separate a
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user