|
|
import { useState, useEffect, useRef } from "react";
|
|
|
import { Howl, Howler } from 'howler';
|
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
|
import { listen } from '@tauri-apps/api/event';
|
|
|
import "./App.css";
|
|
|
import { OSC_ADDRESS } from "./utils/constant";
|
|
|
import { Light } from "./utils/light";
|
|
|
|
|
|
|
|
|
const DefaultFadeDuration = 3; // 1 second
|
|
|
const CLIENT_COUNT = 13;
|
|
|
const CUE_FILE = 'cuelist_1009.json';
|
|
|
const OSC_BROADCAST_IP='192.168.51.255:8000';
|
|
|
const SCS_IP='192.168.51.100:58200';
|
|
|
// const OSC_BROADCAST_IP = '192.168.234.255:8000';
|
|
|
const UNITY_BROADCAST_IP = '192.168.234.255:9000';
|
|
|
// const SCS_IP = '192.168.234.41:58200';
|
|
|
|
|
|
|
|
|
const CueType = {
|
|
|
Bg: 'bg',
|
|
|
Announce: 'announce',
|
|
|
Light: 'light'
|
|
|
}
|
|
|
const EmojiType = {
|
|
|
bg: '🎵',
|
|
|
announce: '📢',
|
|
|
light: '💡'
|
|
|
}
|
|
|
|
|
|
function App() {
|
|
|
|
|
|
const [cuelist, setCuelist] = useState([]);
|
|
|
const [currentCue, setCurrentCue] = useState(null);
|
|
|
const [fadeDuration, setFadeDuration] = useState(DefaultFadeDuration); // Default fade duration in seconds
|
|
|
const [clientStatus, setClientStatus] = useState({});
|
|
|
|
|
|
const refCue = useRef(null);
|
|
|
// const refNextCue = useRef(null);
|
|
|
const refLight = useRef();
|
|
|
const refDuration = useRef();
|
|
|
const refTotalTime=useRef();
|
|
|
const refTimer = useRef();
|
|
|
|
|
|
|
|
|
function sendOsc(addr, message, id) {
|
|
|
|
|
|
invoke('send_osc_message', {
|
|
|
key: addr,
|
|
|
message: `${id}#${message}`,
|
|
|
host: '0.0.0.0:0',
|
|
|
target: OSC_BROADCAST_IP,
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
function sendOscToSound(addr, message) {
|
|
|
|
|
|
invoke('send_osc_message', {
|
|
|
key: addr,
|
|
|
message,
|
|
|
host: '0.0.0.0:0',
|
|
|
target: SCS_IP,
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
function onOsc(message) {
|
|
|
|
|
|
const addr = message.addr;
|
|
|
const [id, status, name] = message.args[0]?.split('#');
|
|
|
// console.log('receive osc:', id, status,name);
|
|
|
|
|
|
switch (addr) {
|
|
|
case OSC_ADDRESS.CLIENT_STATUS:
|
|
|
|
|
|
setClientStatus(prev => ({
|
|
|
...prev,
|
|
|
[id]: {
|
|
|
status,
|
|
|
name,
|
|
|
timestamp: new Date().toLocaleTimeString(),
|
|
|
}
|
|
|
}));
|
|
|
break;
|
|
|
case OSC_ADDRESS.CLIENT_DURATION:
|
|
|
setClientStatus(prev => ({
|
|
|
...prev,
|
|
|
[id]: {
|
|
|
...prev[id],
|
|
|
duration: status,
|
|
|
timestamp: new Date().toLocaleTimeString(),
|
|
|
}
|
|
|
}));
|
|
|
break;
|
|
|
case OSC_ADDRESS.CLIENT_INPUT:
|
|
|
setClientStatus(prev => ({
|
|
|
...prev,
|
|
|
[id]: {
|
|
|
...prev[id],
|
|
|
input: status,
|
|
|
timestamp: new Date().toLocaleTimeString(),
|
|
|
}
|
|
|
}));
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function getTotalLeftTime(id){
|
|
|
const cue=cuelist.find(el=>el.id==id);
|
|
|
const index=cuelist.indexOf(cue);
|
|
|
if(index==-1) return;
|
|
|
|
|
|
let sum=0;
|
|
|
for(var i=index;i<cuelist.length;++i){
|
|
|
if(cuelist[i].duration) sum+=cuelist[i].duration;
|
|
|
}
|
|
|
|
|
|
return sum;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
function playCue({ id, name, description, type, auto, audioFile, ...props }) {
|
|
|
|
|
|
console.log('Playing cue:', { id, name, description, type, auto, audioFile, ...props });
|
|
|
setCurrentCue({ id, name, description, type, auto, audioFile, ...props });
|
|
|
|
|
|
// Handle other cue types and properties here
|
|
|
if (props.audioCue) {
|
|
|
sendOscToSound(OSC_ADDRESS.SCS_GO_CUE, props.audioCue);
|
|
|
}
|
|
|
|
|
|
if (props.clientCue) {
|
|
|
sendOsc(OSC_ADDRESS.PLAYCUE, props.clientCue, 'all');
|
|
|
}
|
|
|
|
|
|
if (props.lightCue) {
|
|
|
// sendOsc(OSC_ADDRESS.CLIENT_INPUT, props.lightCue);
|
|
|
|
|
|
if (props.lightCue == 'fade_in_light'){
|
|
|
refLight.current.fadeIn(props.lightDuration, props.syncUnityLight?UNITY_BROADCAST_IP:null); // Fade in light for conversation start
|
|
|
}
|
|
|
if (props.lightCue == 'fade_out_light'){
|
|
|
refLight.current.fadeOut(props.lightDuration, props.syncUnityLight?UNITY_BROADCAST_IP:null); // Fade out light for conversation end
|
|
|
}
|
|
|
|
|
|
}
|
|
|
if (props.reset) {
|
|
|
resetLight();
|
|
|
}
|
|
|
|
|
|
if (refTimer.current) {
|
|
|
clearTimeout(refTimer.current);
|
|
|
}
|
|
|
|
|
|
if (props.duration) {
|
|
|
|
|
|
// calculate last time
|
|
|
let totalLefTime=getTotalLeftTime(id);
|
|
|
|
|
|
// Clear any existing timer
|
|
|
|
|
|
// Store the end time of the timer
|
|
|
const totalEndTime=new Date().getTime()+totalLefTime*1000;
|
|
|
const endTime = new Date().getTime() + (props.duration * 1000);
|
|
|
|
|
|
// Function to update the timer
|
|
|
const tick = () => {
|
|
|
const now = new Date().getTime();
|
|
|
const timeLeft = endTime - now;
|
|
|
const totalTimeLeft=totalEndTime-now;
|
|
|
//console.log(totalTimeLeft);
|
|
|
|
|
|
if (timeLeft <= 0) {
|
|
|
// Timer has finished
|
|
|
clearTimeout(refTimer.current);
|
|
|
refTimer.current = null;
|
|
|
|
|
|
if (auto) {
|
|
|
// const next = cuelist.find(c => c.id === id + 1);
|
|
|
const index = cuelist.findIndex(c => c.id === id);
|
|
|
const next = cuelist[index + 1];
|
|
|
if (!next) return;
|
|
|
|
|
|
console.log('Auto play next cue:', next);
|
|
|
playCue(next);
|
|
|
}
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// Update the displayed duration
|
|
|
refDuration.current.innerText = secondToTime(timeLeft / 1000);
|
|
|
refTotalTime.current.innerText=`${secondToTime(totalTimeLeft/1000)} / ${secondToTime(totalLefTime)}`;
|
|
|
|
|
|
|
|
|
|
|
|
// Call the next tick
|
|
|
refTimer.current = setTimeout(tick, 100);
|
|
|
};
|
|
|
|
|
|
// Start the timer
|
|
|
console.log('Start timer:', props.duration);
|
|
|
refTimer.current = setTimeout(tick, 100);
|
|
|
}
|
|
|
|
|
|
}
|
|
|
function stop() {
|
|
|
console.log('Stop all');
|
|
|
|
|
|
sendOsc(OSC_ADDRESS.STOPCUE, '', 'all');
|
|
|
sendOscToSound(OSC_ADDRESS.SCS_FADE_ALL, '');
|
|
|
|
|
|
// cear timer
|
|
|
if (refTimer.current) clearInterval(refTimer.current);
|
|
|
}
|
|
|
function reset() {
|
|
|
console.log('Reset all');
|
|
|
|
|
|
sendOsc(OSC_ADDRESS.RESETCUE, '', 'all');
|
|
|
sendOscToSound(OSC_ADDRESS.SCS_STOP_ALL, '');
|
|
|
resetLight();
|
|
|
}
|
|
|
function resetLight(){
|
|
|
refLight.current.set(1);
|
|
|
invoke('send_osc_message', {
|
|
|
key: '/amplitude',
|
|
|
message: 1.0.toString(),
|
|
|
host:`0.0.0.0:0`,
|
|
|
target: UNITY_BROADCAST_IP,
|
|
|
});
|
|
|
|
|
|
}
|
|
|
function secondToTime(seconds) {
|
|
|
const minutes = Math.floor(seconds / 60);
|
|
|
const secs = Math.floor(seconds % 60);
|
|
|
return `${minutes.toString().padStart(2,'0')}:${secs < 10 ? '0' : ''}${secs}`;
|
|
|
}
|
|
|
function getAudioDuration() {
|
|
|
|
|
|
const type = refCue.current?.type;
|
|
|
// console.log('getAudioDuration', type, refCue.current);
|
|
|
// switch(type) {
|
|
|
// case CueType.Bg:
|
|
|
if (refAudioBg.current) {
|
|
|
return `${secondToTime(refAudioBg.current.seek())} / ${secondToTime(refAudioBg.current.duration())} ${refAudioBg.current?.volume()}`;
|
|
|
} else {
|
|
|
return 'N/A';
|
|
|
}
|
|
|
// case CueType.Announce:
|
|
|
// return refAudioAnnounce.current ? `${secondToTime(refAudioAnnounce.current.seek())} / ${secondToTime(refAudioAnnounce.current.duration())}` : 'N/A';
|
|
|
// default:
|
|
|
// return 'N/A';
|
|
|
// }
|
|
|
|
|
|
|
|
|
}
|
|
|
useEffect(() => {
|
|
|
if (currentCue) {
|
|
|
refCue.current = currentCue;
|
|
|
// console.log('Current Cue:', currentCue);
|
|
|
}
|
|
|
}, [currentCue]);
|
|
|
useEffect(() => {
|
|
|
fetch(`/${CUE_FILE}`)
|
|
|
.then(response => response.json())
|
|
|
.then(data => {
|
|
|
console.log('Cuelist data:', data);
|
|
|
setCuelist(data.cuelist);
|
|
|
})
|
|
|
.catch(error => {
|
|
|
console.error('Error fetching cuelist:', error);
|
|
|
});
|
|
|
|
|
|
|
|
|
listen('osc_message', (event) => {
|
|
|
// console.log(`Received OSC message: ${JSON.stringify(event.payload)}`);
|
|
|
onOsc(event.payload);
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
return (
|
|
|
<main className="overflow-y-auto flex flex-row gap-8 p-4 min-h-screen">
|
|
|
<section className="flex-1 flex flex-col gap-4">
|
|
|
<section className="grid grid-cols-4 items-center gap-2">
|
|
|
<div className="grid grid-cols-1 font-bold items-stretch justify-between bg-pink-300 gap-4 p-2">
|
|
|
<div className="text-4xl">{currentCue ? `${currentCue.name}` : 'None'}</div>
|
|
|
<div className="text-4xl" ref={refDuration}></div>
|
|
|
</div>
|
|
|
<button className="text-4xl self-stretch" onClick={stop}>stop all</button>
|
|
|
<button className="text-4xl self-stretch" onClick={reset}>reset all</button>
|
|
|
<Light ref={refLight} />
|
|
|
<span className="flex flex-col gap-2 items-stretch hidden">
|
|
|
<label htmlFor="fade_duration">Fade Duration</label>
|
|
|
<input type="range" id="fade_duration" min="0" max="30" step="0.1" defaultValue={DefaultFadeDuration} className="slider"
|
|
|
onChange={(e) => {
|
|
|
const value = parseFloat(e.target.value);
|
|
|
setFadeDuration(value);
|
|
|
}}></input>
|
|
|
<span className="text-2xl">{fadeDuration}s</span>
|
|
|
</span>
|
|
|
</section>
|
|
|
<table className="border-collapse w-full **:px-1 border-green-500 border-4">
|
|
|
<thead className="bg-green-500">
|
|
|
<tr className="text-left lowercase font-[900]">
|
|
|
{/* <th>ID</th> */}
|
|
|
<th></th>
|
|
|
<th>Name</th>
|
|
|
<th>Description</th>
|
|
|
<th>Type</th>
|
|
|
<th>Auto</th>
|
|
|
<th>Due</th>
|
|
|
<th>Light</th>
|
|
|
<th>Audio</th>
|
|
|
<th>Client</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
{cuelist?.map(({ id, name, description, type, auto, ...props }, index) => (
|
|
|
<tr key={id} className={currentCue?.id === id ? 'bg-green-200' : `${props.debug && 'text-red-500'}`}>
|
|
|
<td className="flex flex-row gap-2">
|
|
|
<button
|
|
|
onClick={() => {
|
|
|
playCue({ id, name, description, type, auto, ...props });
|
|
|
}}>go</button>
|
|
|
</td>
|
|
|
<td>{name}</td>
|
|
|
<td className="text-sm">{description}</td>
|
|
|
<td>{EmojiType[type]}</td>
|
|
|
<td>{auto ? '⤵️' : ''}</td>
|
|
|
<td>{props.duration}</td>
|
|
|
<td className="text-sm">{props.lightCue && `L${props.lightCue}`}</td>
|
|
|
<td className="text-sm">{props.audioCue && `S${props.audioCue}`}</td>
|
|
|
<td className={`text-sm ${props.clientCue && 'bg-green-300 rounded-full text-center font-[900]'}`}>{props.clientCue || ''}</td>
|
|
|
</tr>
|
|
|
))}
|
|
|
</tbody>
|
|
|
</table>
|
|
|
<div className="text-6xl text-center font-bold bg-blue-500 text-white p-4" ref={refTotalTime}></div>
|
|
|
|
|
|
</section>
|
|
|
<table className="flex-1 text-sm">
|
|
|
<thead className="bg-blue-500">
|
|
|
<tr className="text-center lowercase font-[900]">
|
|
|
<th>id</th>
|
|
|
<th>status</th>
|
|
|
<th>cue</th>
|
|
|
<th>input</th>
|
|
|
<th>due</th>
|
|
|
<th>timestamp</th>
|
|
|
<th></th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
{Array.from(Array(CLIENT_COUNT).keys()).map((i) => {
|
|
|
const id = (i + 1).toString();//.padStart(2,'0');
|
|
|
const log = clientStatus[id.toString()];
|
|
|
return (
|
|
|
<tr key={id} className="text-left lowercase">
|
|
|
<td className="font-[900]">{id}</td>
|
|
|
{<>
|
|
|
<td>{log?.status}</td>
|
|
|
<td>
|
|
|
<span className="bg-green-200 rounded-full p-2">{log?.name}</span>
|
|
|
</td>
|
|
|
<td className="flex-1 text-[9px] min-w-[80px]">
|
|
|
{log?.input?.substr(log.input.length - 20)?.split('').map((ch, i) => i % 3 < 1 ? ch : '*').join('')}
|
|
|
</td>
|
|
|
<td>{log?.duration}</td>
|
|
|
<td className="text-[12px]">{log?.timestamp}</td>
|
|
|
</>}
|
|
|
<td className="flex flex-row justify-end gap-1 p-1">
|
|
|
<button className="!bg-pink-300" onClick={() => sendOsc(OSC_ADDRESS.TESTPRINT, '', id)}>print</button>
|
|
|
<button onClick={() => sendOsc(OSC_ADDRESS.STOPCUE, '', id)}>stop</button>
|
|
|
<button onClick={() => sendOsc(OSC_ADDRESS.RESETCUE, '', id)}>reset</button>
|
|
|
{cuelist?.filter(c => c.clientCue && !c.debug).map((c, index) => (
|
|
|
<button key={`${id}${c.clientCue}${index}`} onClick={() => {
|
|
|
sendOsc(OSC_ADDRESS.PLAYCUE, c.clientCue, id);
|
|
|
}}>{c.clientCue}</button>
|
|
|
))}
|
|
|
</td>
|
|
|
</tr>
|
|
|
);
|
|
|
})}
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</main>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
export default App;
|
|
|
|