Compare commits

..

1 Commits

Author SHA1 Message Date
DerTyp7
6d95ec8f4c Add option to align right 2025-01-24 14:36:50 +01:00
7 changed files with 78 additions and 24 deletions

View File

@@ -1,7 +1,7 @@
{ {
"tabWidth": 2, "tabWidth": 2,
"useTabs": false, "useTabs": false,
"printWidth": 180, "printWidth": 120,
"singleQuote": false, "singleQuote": false,
"semi": true "semi": true
} }

View File

@@ -1,13 +1,13 @@
# TeamSpeak-OBS-Overlay # TeamSpeak5-OBS-Overlay
This is an overlay for OBS to show the current talking clients in your TeamSpeak5/6 Channel. This is an overlay for OBS to show the current talking clients in your TeamSpeak5 Channel.
This App uses the new "Remote Apps" feature of TeamSpeak5/6. This App uses the new "Remote Apps" feature of TeamSpeak5.
This overlay uses the [TeamSpeak Remote App API](https://github.com/DerTyp7/react-teamspeak-remote-app-api). This overlay uses the [TeamSpeak Remote App API](https://github.com/DerTyp7/react-teamspeak-remote-app-api).
![image](https://github.com/DerTyp7/ts5-obs-overlay/assets/76851529/d0ab06f2-1a36-479d-826f-bd4bd3d405b7) ![image](https://github.com/DerTyp7/ts5-obs-overlay/assets/76851529/d0ab06f2-1a36-479d-826f-bd4bd3d405b7)
- [TeamSpeak-OBS-Overlay](#teamspeak-obs-overlay) - [TeamSpeak5-OBS-Overlay](#teamspeak5-obs-overlay)
- [Usage](#usage) - [Usage](#usage)
- [Quick instructions](#quick-instructions) - [Quick instructions](#quick-instructions)
- [Detailed instructions](#detailed-instructions) - [Detailed instructions](#detailed-instructions)
@@ -20,16 +20,16 @@ This overlay uses the [TeamSpeak Remote App API](https://github.com/DerTyp7/reac
### Quick instructions ### Quick instructions
1. Open this link in your Browser: [https://teamspeak-overlay.tealfire.de/#/generate](https://teamspeak-overlay.tealfire.de/#/generate) 1. Open this link in your Browser: [https://ts5-overlay.tealfire.de/#/generate](https://ts5-overlay.tealfire.de/#/generate)
2. Follow the instructions on the website 2. Follow the instructions on the website
3. Accept overlay inside TeamSpeak5/6 3. Accept overlay inside TeamSpeak5
![image](https://github.com/DerTyp7/teamspeak-overlay.tealfire.de/assets/76851529/aa83b07d-3dea-461f-9487-f9e6a299f2f3) ![image](https://github.com/DerTyp7/ts5-obs-overlay/assets/76851529/aa83b07d-3dea-461f-9487-f9e6a299f2f3)
### Detailed instructions ### Detailed instructions
Try this instruction if you have problems with the quick instructions above. Try this instruction if you have problems with the quick instructions above.
1. Open this link in your Browser: [https://teamspeak-overlay.tealfire.de/#/generate](https://teamspeak-overlay.tealfire.de/#/generate) 1. Open this link in your Browser: [https://ts5-overlay.tealfire.de/#/generate](https://ts5-overlay.tealfire.de/#/generate)
2. Follow the instructions on the website 2. Follow the instructions on the website
@@ -56,7 +56,7 @@ Try this instruction if you have problems with the quick instructions above.
Make sure you accepted the notification in your TeamSpeak Client. Make sure you accepted the notification in your TeamSpeak Client.
**Fix 2** **Fix 2**
Sadly TeamSpeak5/6 does not give us any information about the current active server tab. Sadly TeamSpeak5 does not give us any information about the current active server tab.
So we try currently use a workaround, where the active server tab is determined by looking on which server the your hardware input was unmuted the latest, since the non-active server tabs in TS5 usually mute the clients microphone. So we try currently use a workaround, where the active server tab is determined by looking on which server the your hardware input was unmuted the latest, since the non-active server tabs in TS5 usually mute the clients microphone.
However this workaround is not 100% accurate and can fail in some cases. However this workaround is not 100% accurate and can fail in some cases.
@@ -64,7 +64,7 @@ However this workaround is not 100% accurate and can fail in some cases.
Possible fixes: Possible fixes:
- Unmute and mute yourself in the active server tab (Just a normal unmute and mute, not the hardware mute) - Unmute and mute yourself in the active server tab (Just a normal unmute and mute, not the hardware mute)
- Reconnect to the TS5/6 server while the overlay is open - Reconnect to the TS5 server while the overlay is open
### OBS doesn't show the latest version of the overlay ### OBS doesn't show the latest version of the overlay

View File

@@ -3,10 +3,13 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="An OBS overlay for TeamSpeak5, so your audience can see who is talking. https://github.com/DerTyp7/ts5-obs-overlay" /> <meta
name="description"
content="An OBS overlay for TeamSpeak5/6, so your audience can see who is talking. https://github.com/DerTyp7/teamspeak-obs-overlay"
/>
<meta name="author" content="DerTyp7" /> <meta name="author" content="DerTyp7" />
<link rel="icon" href="/logo.svg" type="image/x-icon" /> <link rel="icon" href="/logo.svg" type="image/x-icon" />
<title>TS5-OBS-Overlay</title> <title>TeamSpeak-OBS-Overlay</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -17,6 +17,7 @@ export default function App() {
showChannelName={searchParams.get("showChannelName") === "true"} showChannelName={searchParams.get("showChannelName") === "true"}
hideNonTalking={searchParams.get("hideNonTalking") === "true"} hideNonTalking={searchParams.get("hideNonTalking") === "true"}
clientLimit={searchParams.get("clientLimit") ? parseInt(searchParams.get("clientLimit") ?? "0") : 0} clientLimit={searchParams.get("clientLimit") ? parseInt(searchParams.get("clientLimit") ?? "0") : 0}
alignRight={searchParams.get("alignRight") === "true"}
/> />
} }
/> />

View File

@@ -9,13 +9,14 @@ export default function Generator() {
const [remoteAppPort, setRemoteAppPort] = useState(5899); const [remoteAppPort, setRemoteAppPort] = useState(5899);
const [showChannelName, setShowChannelName] = useState(true); const [showChannelName, setShowChannelName] = useState(true);
const [alignRight, setAlignRight] = useState(false);
const [hideNonTalking, setHideNonTalking] = useState(false); const [hideNonTalking, setHideNonTalking] = useState(false);
const [clientLimit, setClientLimit] = useState(0); const [clientLimit, setClientLimit] = useState(0);
// Effect to generate URL when dependencies change // Effect to generate URL when dependencies change
useEffect(() => { useEffect(() => {
generateUrl(); generateUrl();
}, [remoteAppPort, showChannelName, hideNonTalking, clientLimit]); }, [remoteAppPort, showChannelName, hideNonTalking, clientLimit, alignRight]);
// Function to generate the output URL // Function to generate the output URL
function generateUrl() { function generateUrl() {
@@ -26,6 +27,7 @@ export default function Generator() {
url.searchParams.set("showChannelName", showChannelName.toString()); url.searchParams.set("showChannelName", showChannelName.toString());
url.searchParams.set("hideNonTalking", hideNonTalking.toString()); url.searchParams.set("hideNonTalking", hideNonTalking.toString());
url.searchParams.set("clientLimit", clientLimit.toString()); url.searchParams.set("clientLimit", clientLimit.toString());
url.searchParams.set("alignRight", alignRight.toString());
// url.hash function always sets the hash to the end of the URL, so we have to replace the question mark with a hash // url.hash function always sets the hash to the end of the URL, so we have to replace the question mark with a hash
// gh-pages needs the hash to be between the base URL and the search params // gh-pages needs the hash to be between the base URL and the search params
@@ -62,7 +64,9 @@ export default function Generator() {
<p>1. Customize your settings</p> <p>1. Customize your settings</p>
<p>2. Copy the generated URL</p> <p>2. Copy the generated URL</p>
<p>3. Paste the URL into the BrowserSource URL field in OBS</p> <p>3. Paste the URL into the BrowserSource URL field in OBS</p>
<a href="https://github.com/DerTyp7/ts5-obs-overlay#detailed-instructions">Click here for detailed instructions</a> <a href="https://github.com/DerTyp7/ts5-obs-overlay#detailed-instructions">
Click here for detailed instructions
</a>
</div> </div>
{/* Output Section */} {/* Output Section */}
@@ -98,18 +102,34 @@ export default function Generator() {
<input type="checkbox" checked={hideNonTalking} /> <input type="checkbox" checked={hideNonTalking} />
<label>Hide non talking clients</label> <label>Hide non talking clients</label>
</div> </div>
{/* Alignment Option */}
<div className="option" onClick={() => setAlignRight(!alignRight)}>
<input type="checkbox" checked={alignRight} />
<label>Align Right</label>
</div>
</section> </section>
<section> <section>
{/* Client Limit Option */} {/* Client Limit Option */}
<div className="option"> <div className="option">
<input type="number" value={clientLimit} min={0} onChange={(e: ChangeEvent<HTMLInputElement>) => setClientLimit(parseInt(e.target.value))} /> <input
type="number"
value={clientLimit}
min={0}
onChange={(e: ChangeEvent<HTMLInputElement>) => setClientLimit(parseInt(e.target.value))}
/>
<label>Client Limit</label> <label>Client Limit</label>
</div> </div>
{/* RemoteApp-Port Option */} {/* RemoteApp-Port Option */}
<div className="option"> <div className="option">
<input type="number" value={remoteAppPort} min={0} onChange={(e: ChangeEvent<HTMLInputElement>) => setRemoteAppPort(parseInt(e.target.value))} /> <input
type="number"
value={remoteAppPort}
min={0}
onChange={(e: ChangeEvent<HTMLInputElement>) => setRemoteAppPort(parseInt(e.target.value))}
/>
<label>RemoteApp-Port</label> <label>RemoteApp-Port</label>
</div> </div>
</section> </section>
@@ -118,7 +138,13 @@ export default function Generator() {
{/* Preview */} {/* Preview */}
<div className="preview"> <div className="preview">
<Viewer remoteAppPort={remoteAppPort} showChannelName={showChannelName} hideNonTalking={hideNonTalking} clientLimit={clientLimit} /> <Viewer
remoteAppPort={remoteAppPort}
showChannelName={showChannelName}
hideNonTalking={hideNonTalking}
clientLimit={clientLimit}
alignRight={alignRight}
/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -6,11 +6,13 @@ export default function Viewer({
showChannelName = false, showChannelName = false,
hideNonTalking = false, hideNonTalking = false,
clientLimit = 0, clientLimit = 0,
alignRight = false,
}: { }: {
remoteAppPort?: number; remoteAppPort?: number;
showChannelName?: boolean; showChannelName?: boolean;
hideNonTalking?: boolean; hideNonTalking?: boolean;
clientLimit?: number; clientLimit?: number;
alignRight?: boolean;
}) { }) {
const { clients, activeConnectionId, currentChannel } = useTSRemoteApp({ const { clients, activeConnectionId, currentChannel } = useTSRemoteApp({
remoteAppPort: remoteAppPort, remoteAppPort: remoteAppPort,
@@ -30,7 +32,7 @@ export default function Viewer({
}) as IClient[]; }) as IClient[];
return ( return (
<div className="viewer"> <div className={`viewer ${alignRight ? "viewer--align-right" : ""}`}>
{showChannelName && currentChannel ? ( {showChannelName && currentChannel ? (
<div className="channelNameContainer"> <div className="channelNameContainer">
<h1>{currentChannel?.properties.name}</h1> <h1>{currentChannel?.properties.name}</h1>
@@ -44,13 +46,19 @@ export default function Viewer({
if (client) { if (client) {
//* Non-talking client //* Non-talking client
if (hideNonTalking && (client.properties.inputMuted || client.properties.outputMuted || client.talkStatus == 0)) { if (
hideNonTalking &&
(client.properties.inputMuted || client.properties.outputMuted || client.talkStatus == 0)
) {
return null; return null;
} }
//* Normal client //* Normal client
return ( return (
<div className="client" key={`${client.id}-${client.channel?.connection.id}`}> <div
className={`client ${alignRight ? "client--align-right" : ""}`}
key={`${client.id}-${client.channel?.connection.id}`}
>
{client.properties.outputHardware == false ? ( {client.properties.outputHardware == false ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<title>muted_hardware_output</title> <title>muted_hardware_output</title>
@@ -85,7 +93,16 @@ export default function Viewer({
d="M88.62,54.15V64A24.69,24.69,0,0,1,64,88.62a25.26,25.26,0,0,1-8.38-1.46l-7.39,7.39A34,34,0,0,0,64,98.46,34.5,34.5,0,0,0,98.46,64V54.15a4.92,4.92,0,1,1,9.85,0V64a44.31,44.31,0,0,1-39.38,44v10.15H88.62a4.92,4.92,0,0,1,0,9.85H39.38a4.92,4.92,0,1,1,0-9.85H59.08V108A43.3,43.3,0,0,1,41,101.77L21.46,121.31a2.46,2.46,0,0,1-3.54,0L11.62,115a2.46,2.46,0,0,1,0-3.54l94.92-94.92a2.46,2.46,0,0,1,3.54,0l6.31,6.31a2.46,2.46,0,0,1,0,3.54ZM22.92,80.46A43.3,43.3,0,0,1,19.69,64V54.15a4.92,4.92,0,1,1,9.85,0V64a35.94,35.94,0,0,0,1.15,8.69ZM39.38,64V24.62a24.62,24.62,0,0,1,47.77-8.38Z" d="M88.62,54.15V64A24.69,24.69,0,0,1,64,88.62a25.26,25.26,0,0,1-8.38-1.46l-7.39,7.39A34,34,0,0,0,64,98.46,34.5,34.5,0,0,0,98.46,64V54.15a4.92,4.92,0,1,1,9.85,0V64a44.31,44.31,0,0,1-39.38,44v10.15H88.62a4.92,4.92,0,0,1,0,9.85H39.38a4.92,4.92,0,1,1,0-9.85H59.08V108A43.3,43.3,0,0,1,41,101.77L21.46,121.31a2.46,2.46,0,0,1-3.54,0L11.62,115a2.46,2.46,0,0,1,0-3.54l94.92-94.92a2.46,2.46,0,0,1,3.54,0l6.31,6.31a2.46,2.46,0,0,1,0,3.54ZM22.92,80.46A43.3,43.3,0,0,1,19.69,64V54.15a4.92,4.92,0,1,1,9.85,0V64a35.94,35.94,0,0,0,1.15,8.69ZM39.38,64V24.62a24.62,24.62,0,0,1,47.77-8.38Z"
fill="#d8d8d8" fill="#d8d8d8"
/> />
<rect x="-5.93" y="61.89" width="139.87" height="14.02" rx="2.87" ry="2.87" transform="translate(-29.97 65.43) rotate(-45)" fill="#c9070a" /> <rect
x="-5.93"
y="61.89"
width="139.87"
height="14.02"
rx="2.87"
ry="2.87"
transform="translate(-29.97 65.43) rotate(-45)"
fill="#c9070a"
/>
</g> </g>
</svg> </svg>
) : client.properties.outputMuted ? ( ) : client.properties.outputMuted ? (

View File

@@ -7,6 +7,10 @@
flex-direction: column; flex-direction: column;
padding: 0.5rem; padding: 0.5rem;
&--align-right {
align-items: flex-end;
}
h1 { h1 {
font-size: 5vw; font-size: 5vw;
} }
@@ -32,9 +36,12 @@
.client { .client {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 0 0; gap: 0.5rem 0.5rem;
align-items: center; align-items: center;
margin: 0.5rem 0;
&--align-right {
flex-direction: row-reverse;
}
// icon styles // icon styles
svg { svg {