diff --git a/src/mixer/audio.ts b/src/mixer/audio.ts index 37bbbe2..a29b716 100644 --- a/src/mixer/audio.ts +++ b/src/mixer/audio.ts @@ -125,6 +125,10 @@ class Player extends ((PlayerEmitter as unknown) as { new (): EventEmitter }) { } } + getVolume() { + return this.volume; + } + setVolume(val: number) { this.volume = val; this._applyVolume(); diff --git a/src/mixer/state.ts b/src/mixer/state.ts index 7b3df58..4fff95d 100644 --- a/src/mixer/state.ts +++ b/src/mixer/state.ts @@ -509,7 +509,12 @@ export const load = ( const itsChannel = getState() .showplan.plan!.filter((x) => x.channel === item.channel) .sort((x, y) => x.weight - y.weight); - const itsIndex = itsChannel.indexOf(item); + // Sadly, we can't just do .indexOf() item directly, + // since the player's idea of an item may be changed over it's lifecycle (setting played,intro/cue/outro etc.) + // Therefore we'll find the updated item from the plan and match that. + const itsIndex = itsChannel.findIndex( + (x) => itemId(x) === itemId(item) + ); if (itsIndex > -1 && itsIndex !== itsChannel.length - 1) { dispatch(load(player, itsChannel[itsIndex + 1])); } @@ -596,14 +601,12 @@ export const stop = (player: number): AppThunk => (dispatch, getState) => { let cueTime = 0; - console.log(Math.round(playerInstance.currentTime)); if ( state.loadedItem && "cue" in state.loadedItem && Math.round(playerInstance.currentTime) !== Math.round(state.loadedItem.cue) ) { cueTime = state.loadedItem.cue; - console.log(cueTime); } playerInstance.stop(); @@ -677,7 +680,14 @@ export const setVolume = ( const state = getState().mixer.players[player]; const currentLevel = state.volume; - const currentGain = state.gain; + let currentGain = state.gain; + + // If we can, use the engine's 'real' volume gain. + // This helps when we've interupted a previous fade, so the state gain won't be correct. + if (typeof audioEngine.players[player] !== "undefined") { + currentGain = audioEngine.players[player]!.getVolume(); + } + const volumeTween = new Between(currentLevel, uiLevel) .time(FADE_TIME_SECONDS * 1000) .on("update", (val: number) => { diff --git a/src/showplanner/ImporterModal.tsx b/src/showplanner/ImporterModal.tsx index 8d6946d..19583a6 100644 --- a/src/showplanner/ImporterModal.tsx +++ b/src/showplanner/ImporterModal.tsx @@ -14,18 +14,19 @@ export function ImporterModal(props: ImporterProps) { // Add support for closing the modal when the importer wants to reload the show plan. // There is a similar listener in showplanner/index.tsx to actually reload the show plan. useEffect(() => { - window.addEventListener( - "message", - (event) => { - if (!event.origin.includes("ury.org.uk")) { - return; - } - if (event.data === "reload_showplan") { - props.close(); - } - }, - false - ); + function reloadListener(event: MessageEvent) { + if (!event.origin.includes("ury.org.uk")) { + return; + } + if (event.data === "reload_showplan") { + props.close(); + } + } + + window.addEventListener("message", reloadListener); + return () => { + window.removeEventListener("message", reloadListener); + }; }); return ( diff --git a/src/showplanner/Player.tsx b/src/showplanner/Player.tsx index bc463e2..e1ed3e0 100644 --- a/src/showplanner/Player.tsx +++ b/src/showplanner/Player.tsx @@ -67,6 +67,8 @@ const setTrackIntro = ( player: number ): AppThunk => async (dispatch, getState) => { try { + // Api only deals with whole seconds. + secs = Math.round(secs); dispatch(MixerState.setLoadedItemIntro(player, secs)); if (getState().settings.saveShowPlanChanges) { await api.setTrackIntro(track.trackid, secs); @@ -84,6 +86,8 @@ const setTrackOutro = ( player: number ): AppThunk => async (dispatch, getState) => { try { + // Api only deals with whole seconds. + secs = Math.round(secs); dispatch(MixerState.setLoadedItemOutro(player, secs)); if (getState().settings.saveShowPlanChanges) { await api.setTrackOutro(track.trackid, secs); @@ -101,6 +105,8 @@ const setTrackCue = ( player: number ): AppThunk => async (dispatch, getState) => { try { + // Api only deals with whole seconds. + secs = Math.round(secs); dispatch(MixerState.setLoadedItemCue(player, secs)); if (getState().settings.saveShowPlanChanges) { await api.setTimeslotItemCue(item.timeslotitemid, secs); @@ -192,9 +198,17 @@ function TimingButtons({ id }: { id: number }) { } export function Player({ id }: { id: number }) { + // Define time remaining (secs) when the play icon should flash. + const SECS_REMAINING_WARNING = 20; + + // We want to force update the selector when we pass the SECS_REMAINING_WARNING barrier. const playerState = useSelector( (state: RootState) => state.mixer.players[id], (a, b) => + !( + a.timeRemaining <= SECS_REMAINING_WARNING && + b.timeRemaining > SECS_REMAINING_WARNING + ) && shallowEqual( omit(a, "timeCurrent", "timeRemaining"), omit(b, "timeCurrent", "timeRemaining") @@ -222,11 +236,15 @@ export function Player({ id }: { id: number }) { } }; - var duration: number = 0; + let channelDuration = 0; + let channelUnplayed = 0; const plan = useSelector((state: RootState) => state.showplan.plan); plan?.forEach((pItem) => { if (pItem.channel === id) { - duration += HHMMTosec(pItem.length); + channelDuration += HHMMTosec(pItem.length); + if (!pItem.played) { + channelUnplayed += HHMMTosec(pItem.length); + } } }); @@ -239,6 +257,14 @@ export function Player({ id }: { id: number }) { } >
+
+ + Total: {secToHHMM(channelDuration)} + + + Unplayed: {secToHHMM(channelUnplayed)} + +
Channel Controls -
Total Time: {secToHHMM(duration)}
{proMode && }
@@ -319,7 +344,7 @@ export function Player({ id }: { id: number }) { onClick={() => dispatch(MixerState.play(id))} className={ playerState.state === "playing" - ? playerState.timeRemaining <= 15 + ? playerState.timeRemaining <= SECS_REMAINING_WARNING ? "sp-state-playing sp-ending-soon" : "sp-state-playing" : "" diff --git a/src/showplanner/ProModeButtons.tsx b/src/showplanner/ProModeButtons.tsx index 9bcb496..fcbe60c 100644 --- a/src/showplanner/ProModeButtons.tsx +++ b/src/showplanner/ProModeButtons.tsx @@ -20,8 +20,14 @@ export default function ProModeButtons({ channel }: { channel: number }) { <>
Pro Mode™ -