Skip to content

Commit ccd8bef

Browse files
committed
feat(karaoke): Add standard photobooth capabilities
1 parent 70b70a8 commit ccd8bef

File tree

5 files changed

+223
-39
lines changed

5 files changed

+223
-39
lines changed

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ reset-db:
1313
sqlite3 0_DATA/partyhall.db < sql/fixtures.sql
1414

1515
take-picture:
16-
docker compose exec mosquitto mosquitto_pub -h 127.0.0.1 -t partyhall/button_press -m "TAKE_PICTURE"
16+
docker compose exec mosquitto mosquitto_pub -h 127.0.0.1 -t partyhall/button_press -m "partyhall/photobooth/take_picture"
1717

1818
show-debug:
1919
docker compose exec mosquitto mosquitto_pub -h 127.0.0.1 -t partyhall/button_press -m "DISPLAY_DEBUG"

gui/src/assets/css/photobooth.scss

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.photobooth{
1+
.photobooth, .karaoke{
22
position: relative;
33

44
width: 100%;
+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { useEffect, useRef } from "react";
2+
import { useBoothSocket } from "../../../hooks/boothSocket";
3+
4+
import '../../../assets/css/karaoke.scss';
5+
import CDGPlayer from "./cdgplayer";
6+
import Webcam from "react-webcam";
7+
import { Stack, Typography } from "@mui/material";
8+
import { songTitle } from "../../../utils/songs";
9+
import OsdSong from "./osd_song";
10+
import VideoPlayer from "./videoplayer";
11+
import { b64ImageToBlob } from "../../../utils/files";
12+
import { useTranslation } from "react-i18next";
13+
14+
/**
15+
* This is a temp file
16+
* Currently, the karaoke module in index.tsx is a clone of the photobooth
17+
* With the capability to play songs
18+
* Later this probably will be merged as only one
19+
* Or something else, IDK yet
20+
*
21+
* But the idea is that the photobooth thing should still work when in Karaoke
22+
*/
23+
export default function Karaoke() {
24+
const {t} = useTranslation();
25+
const { appState, lastMessage, sendMessage } = useBoothSocket();
26+
const webcamRef = useRef<Webcam>(null);
27+
const module = appState.modules.karaoke;
28+
29+
const takePicture = async () => {
30+
if (!webcamRef || !webcamRef.current) {
31+
return;
32+
}
33+
34+
const imageSrc = webcamRef.current.getScreenshot();
35+
if (imageSrc) {
36+
let form = new FormData();
37+
38+
form.append('image', b64ImageToBlob(imageSrc));
39+
form.append('unattended', 'true')
40+
form.append('event', ''+appState?.app_state?.current_event?.id)
41+
42+
try {
43+
await fetch('/api/picture', {
44+
method: 'POST',
45+
body: form,
46+
});
47+
} catch {
48+
}
49+
}
50+
};
51+
52+
useEffect(() => {
53+
if (!lastMessage) {
54+
return;
55+
}
56+
57+
if (lastMessage.type == 'UNATTENDED_PICTURE') {
58+
takePicture();
59+
}
60+
}, [lastMessage]);
61+
62+
return <div className="karaoke">
63+
<Webcam
64+
forceScreenshotSourceSize
65+
ref={webcamRef}
66+
width={appState.modules.photobooth.webcam_resolution.width}
67+
height={appState.modules.photobooth.webcam_resolution.height}
68+
screenshotFormat="image/jpeg"
69+
videoConstraints={{ facingMode: 'user', ...appState.modules.photobooth.webcam_resolution }}
70+
className='karaoke__webcam'
71+
/>
72+
{
73+
module.currentSong && module.preplayTimer == 0 &&
74+
<>
75+
{
76+
module.currentSong.format.toLowerCase() === 'cdg' &&
77+
<CDGPlayer
78+
cdgAlpha={.8}
79+
cdgSize={window.innerHeight / 2}
80+
width={window.innerWidth/2}
81+
height={window.innerHeight / 2}
82+
isPlaying={module.started}
83+
song={module.currentSong}
84+
onEnd={() => sendMessage('karaoke/PLAYING_ENDED')}
85+
onError={() => {}}
86+
onLoad={() => {}}
87+
onPlay={() => {}}
88+
onStatus={(x: any) => sendMessage('karaoke/PLAYING_STATUS', {'current': x.position, 'total': x.total})}
89+
/>
90+
}
91+
{
92+
module.currentSong.format.toLowerCase() !== 'cdg' && <VideoPlayer
93+
isPlaying={module.started}
94+
song={module.currentSong}
95+
onEnd={() => sendMessage('karaoke/PLAYING_ENDED')}
96+
onStatus={(x: any) => sendMessage('karaoke/PLAYING_STATUS', {'current': x.position, 'total': x.total})}
97+
/>
98+
}
99+
</>
100+
}
101+
{
102+
module.currentSong && module.preplayTimer > 0 &&
103+
<Stack display="column" className="karaoke__no_song">
104+
<Typography variant="h1">{t('karaoke.now_playing')}:</Typography>
105+
<Typography variant="h2">{songTitle(module.currentSong)}</Typography>
106+
<Typography variant="h3">{module.preplayTimer}</Typography>
107+
{
108+
module.currentSong.sung_by && module.currentSong.sung_by.length > 0 &&
109+
<Typography variant="h2">{t('karaoke.sung_by')} {module.currentSong.sung_by}</Typography>
110+
}
111+
</Stack>
112+
}
113+
{
114+
!module.currentSong &&
115+
<Stack display="column" className="karaoke__no_song">
116+
<Typography variant="h1">{t('karaoke.no_song_playing')}</Typography>
117+
</Stack>
118+
}
119+
{
120+
module.queue.length > 0 &&
121+
<Stack className="karaoke__next_song" gap={1}>
122+
<Typography variant="h3">{t('karaoke.next_up')}:</Typography>
123+
<OsdSong song={module.queue[0]} />
124+
</Stack>
125+
}
126+
</div>;
127+
}

gui/src/pages/booth/karaoke/index.tsx

+92-37
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,36 @@
1-
import { useEffect, useRef } from "react";
1+
import { useEffect, useRef, useState } from "react";
2+
import Webcam from "react-webcam";
3+
import LockedModal from "../../../components/locked_modal";
24
import { useBoothSocket } from "../../../hooks/boothSocket";
35

6+
import '../../../assets/css/photobooth.scss';
47
import '../../../assets/css/karaoke.scss';
8+
import { b64ImageToBlob } from "../../../utils/files";
59
import CDGPlayer from "./cdgplayer";
6-
import Webcam from "react-webcam";
10+
import VideoPlayer from "./videoplayer";
711
import { Stack, Typography } from "@mui/material";
8-
import { songTitle } from "../../../utils/songs";
912
import OsdSong from "./osd_song";
10-
import VideoPlayer from "./videoplayer";
11-
import { b64ImageToBlob } from "../../../utils/files";
1213
import { useTranslation } from "react-i18next";
14+
import { songTitle } from "../../../utils/songs";
15+
16+
type LastPicture = {
17+
url: string;
18+
loaded: boolean;
19+
};
1320

14-
export default function Karaoke() {
21+
export default function Photobooth() {
22+
const webcamRef = useRef<Webcam>(null);
1523
const {t} = useTranslation();
1624
const { appState, lastMessage, sendMessage } = useBoothSocket();
17-
const webcamRef = useRef<Webcam>(null);
18-
const module = appState.modules.karaoke;
25+
const [timer, setTimer] = useState(-1);
26+
const [flash, setFlash] = useState<boolean>(false);
27+
const [lastPicture, setLastPicture] = useState<LastPicture|null>(null);
1928

20-
const takePicture = async () => {
29+
const module = appState.modules.photobooth;
30+
const modulek = appState.modules.karaoke;
31+
const resolution = module.webcam_resolution;
32+
33+
const takePicture = async (unattended: boolean) => {
2134
if (!webcamRef || !webcamRef.current) {
2235
return;
2336
}
@@ -27,15 +40,26 @@ export default function Karaoke() {
2740
let form = new FormData();
2841

2942
form.append('image', b64ImageToBlob(imageSrc));
30-
form.append('unattended', 'true')
43+
form.append('unattended', unattended ? 'true' : 'false')
3144
form.append('event', ''+appState?.app_state?.current_event?.id)
3245

3346
try {
34-
await fetch('/api/picture', {
47+
const resp = await fetch('/api/picture', {
3548
method: 'POST',
3649
body: form,
3750
});
51+
52+
setTimer(-1);
53+
54+
if (!unattended) {
55+
const image = await resp.blob();
56+
const url = URL.createObjectURL(image);
57+
58+
setLastPicture({ url, loaded: false});
59+
setTimeout(() => setLastPicture(null), 3500);
60+
}
3861
} catch {
62+
setTimer(-1);
3963
}
4064
}
4165
};
@@ -45,33 +69,60 @@ export default function Karaoke() {
4569
return;
4670
}
4771

72+
if (lastMessage.type == 'TAKE_PICTURE' && timer === -1) {
73+
if (appState.current_mode === 'DISABLED') {
74+
return;
75+
}
76+
77+
setTimer(module.default_timer)
78+
return
79+
}
80+
4881
if (lastMessage.type == 'UNATTENDED_PICTURE') {
49-
takePicture();
82+
takePicture(true);
5083
}
5184
}, [lastMessage]);
5285

86+
useEffect(() => {
87+
if (timer > 0) {
88+
setTimeout(() => {
89+
setTimer(timer-1);
90+
}, 1000);
91+
}
92+
93+
if (timer == 0) {
94+
setFlash(true);
95+
setTimeout(() => {
96+
takePicture(false);
97+
setFlash(false);
98+
setTimer(-1);
99+
}, 500);
100+
}
101+
}, [timer]);
102+
53103
return <div className="karaoke">
54104
<Webcam
55105
forceScreenshotSourceSize
56106
ref={webcamRef}
57-
width={appState.modules.photobooth.webcam_resolution.width}
58-
height={appState.modules.photobooth.webcam_resolution.height}
107+
width={resolution.width}
108+
height={resolution.height}
109+
onClick={() => appState.current_mode !== 'DISABLED' && sendMessage('photobooth/TAKE_PICTURE')}
59110
screenshotFormat="image/jpeg"
60-
videoConstraints={{ facingMode: 'user', ...appState.modules.photobooth.webcam_resolution }}
61-
className='karaoke__webcam'
111+
videoConstraints={{ facingMode: 'user', ...resolution }}
62112
/>
113+
63114
{
64-
module.currentSong && module.preplayTimer == 0 &&
115+
modulek.currentSong && modulek.preplayTimer == 0 &&
65116
<>
66117
{
67-
module.currentSong.format.toLowerCase() === 'cdg' &&
118+
modulek.currentSong.format.toLowerCase() === 'cdg' &&
68119
<CDGPlayer
69120
cdgAlpha={.8}
70121
cdgSize={window.innerHeight / 2}
71122
width={window.innerWidth/2}
72123
height={window.innerHeight / 2}
73-
isPlaying={module.started}
74-
song={module.currentSong}
124+
isPlaying={modulek.started}
125+
song={modulek.currentSong}
75126
onEnd={() => sendMessage('karaoke/PLAYING_ENDED')}
76127
onError={() => {}}
77128
onLoad={() => {}}
@@ -80,39 +131,43 @@ export default function Karaoke() {
80131
/>
81132
}
82133
{
83-
module.currentSong.format.toLowerCase() !== 'cdg' && <VideoPlayer
84-
isPlaying={module.started}
85-
song={module.currentSong}
134+
modulek.currentSong.format.toLowerCase() !== 'cdg' && <VideoPlayer
135+
isPlaying={modulek.started}
136+
song={modulek.currentSong}
86137
onEnd={() => sendMessage('karaoke/PLAYING_ENDED')}
87138
onStatus={(x: any) => sendMessage('karaoke/PLAYING_STATUS', {'current': x.position, 'total': x.total})}
88139
/>
89140
}
90141
</>
91142
}
92143
{
93-
module.currentSong && module.preplayTimer > 0 &&
144+
modulek.currentSong && modulek.preplayTimer > 0 &&
94145
<Stack display="column" className="karaoke__no_song">
95146
<Typography variant="h1">{t('karaoke.now_playing')}:</Typography>
96-
<Typography variant="h2">{songTitle(module.currentSong)}</Typography>
97-
<Typography variant="h3">{module.preplayTimer}</Typography>
147+
<Typography variant="h2">{songTitle(modulek.currentSong)}</Typography>
148+
<Typography variant="h3">{modulek.preplayTimer}</Typography>
98149
{
99-
module.currentSong.sung_by && module.currentSong.sung_by.length > 0 &&
100-
<Typography variant="h2">{t('karaoke.sung_by')} {module.currentSong.sung_by}</Typography>
150+
modulek.currentSong.sung_by && modulek.currentSong.sung_by.length > 0 &&
151+
<Typography variant="h2">{t('karaoke.sung_by')} {modulek.currentSong.sung_by}</Typography>
101152
}
102153
</Stack>
103154
}
104155
{
105-
!module.currentSong &&
106-
<Stack display="column" className="karaoke__no_song">
107-
<Typography variant="h1">{t('karaoke.no_song_playing')}</Typography>
108-
</Stack>
109-
}
110-
{
111-
module.queue.length > 0 &&
156+
modulek.queue.length > 0 &&
112157
<Stack className="karaoke__next_song" gap={1}>
113158
<Typography variant="h3">{t('karaoke.next_up')}:</Typography>
114-
<OsdSong song={module.queue[0]} />
159+
<OsdSong song={modulek.queue[0]} />
115160
</Stack>
116161
}
117-
</div>;
162+
163+
{ timer >= 0 && <div className={`timer`}>{timer > 0 && timer}</div> }
164+
{ flash && <div className="timer flash"></div> }
165+
{ appState.current_mode === 'DISABLED' && <LockedModal /> }
166+
167+
{
168+
lastPicture && <div className="picture_frame" style={lastPicture.loaded ? {} : {display: 'none'}}>
169+
<img src={lastPicture.url} onLoad={() => setLastPicture({...lastPicture, loaded: true})} alt="Last picture" />
170+
</div>
171+
}
172+
</div>
118173
}

hwhandler/hwhandler.go

+2
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ func (hh *HardwareHandler) processSerialMessage(msg string) {
109109

110110
hx := hex.EncodeToString([]byte(msg))
111111

112+
// @TODO: Send raw BTN_XXX to the main software
113+
// So that its customizable in the admin
112114
if strings.HasPrefix(msg, "BTN_") {
113115
msg = strings.Trim(msg, " \t")
114116
val, ok := config.GET.HardwareHandler.Mappings[msg]

0 commit comments

Comments
 (0)