├── src
├── styles
│ ├── App.scss
│ ├── Viewer.scss
│ ├── index.scss
│ └── Generator.scss
├── vite-env.d.ts
├── main.tsx
├── App.tsx
├── utils
│ └── logger.tsx
├── Generator.tsx
└── Viewer.tsx
├── .prettierrc
├── public
├── images
│ └── viewer_example_background.png
└── logo.svg
├── tsconfig.node.json
├── .gitignore
├── .eslintrc.cjs
├── .github
├── dependabot.yml
├── workflows
│ └── deploy.yml
└── ISSUE_TEMPLATE
│ └── bug_report.md
├── index.html
├── vite.config.ts
├── tsconfig.json
├── package.json
└── README.md
/src/styles/App.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "useTabs": false,
4 | "printWidth": 180,
5 | "singleQuote": false,
6 | "semi": true
7 | }
8 |
--------------------------------------------------------------------------------
/public/images/viewer_example_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DerTyp7/teamspeak-obs-overlay/HEAD/public/images/viewer_example_background.png
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from "react-dom/client";
2 | import App from "./App.tsx";
3 | import "@styles/index.scss";
4 | import { HashRouter } from "react-router-dom";
5 |
6 | ReactDOM.createRoot(document.getElementById("root")!).render(
7 |
8 |
9 |
10 | );
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: { browser: true, es2020: true },
3 | extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react-hooks/recommended"],
4 | parser: "@typescript-eslint/parser",
5 | parserOptions: { ecmaVersion: "latest", sourceType: "module" },
6 | plugins: ["react-refresh"],
7 | rules: {
8 | "react-refresh/only-export-components": "warn",
9 | "react-hooks/exhaustive-deps": "ignore",
10 | },
11 | };
12 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 | version: 2
6 | updates:
7 | - package-ecosystem: "npm" # See documentation for possible values
8 | directory: "/" # Location of package manifests
9 | schedule:
10 | interval: "monthly"
11 | target-branch: "dev"
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | TS5-OBS-Overlay
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Deploy to GitHub Pages on Release
2 |
3 | on:
4 | release:
5 | types:
6 | - created
7 |
8 | jobs:
9 | deploy:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout code
14 | uses: actions/checkout@v2
15 |
16 | - name: Setup Node.js
17 | uses: actions/setup-node@v2
18 | with:
19 | node-version: '18'
20 |
21 | - name: Install dependencies
22 | run: npm install
23 |
24 | - name: Build
25 | run: npm run build
26 |
27 | - name: Deploy to GitHub Pages
28 | uses: peaceiris/actions-gh-pages@v3.9.3
29 | with:
30 | github_token: ${{ secrets.GITHUB_TOKEN }}
31 | publish_dir: ./dist
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Something isn't working for you
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | > Screenshots are welcome
11 | ## Describe the bug
12 | A clear and concise description of what the bug is.
13 |
14 | ## To Reproduce
15 | Example or explanation to reproduce the issue.
16 |
17 | ## Expected behavior
18 | A clear and concise description of what you expected to happen.
19 |
20 | **Do you use the local or online version?**
21 | - [x] Online version with URL: `https://dertyp7.github.io/ts5-obs-overlay`
22 | - [ ] The version named below locally downloaded on my device
23 |
24 | **Your environment:**
25 | - OS: [e.g. Windows 11]
26 | - OBS version: [e.g. 29.1.3]
27 | - Overlay version [e.g. v1.1.0]
28 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react-swc'
3 | import path from 'path';
4 |
5 | // https://vitejs.dev/config/
6 | export default defineConfig({
7 | base: "./",
8 | resolve: {
9 | alias: {
10 | '@': path.resolve(__dirname, './src'),
11 | '@assets': path.resolve(__dirname, './src/assets'),
12 | '@components': path.resolve(__dirname, './src/components'),
13 | '@handlers': path.resolve(__dirname, './src/handlers'),
14 | '@interfaces': path.resolve(__dirname, './src/interfaces'),
15 | '@utils': path.resolve(__dirname, './src/utils'),
16 | '@styles': path.resolve(__dirname, './src/styles'),
17 | },
18 | },
19 | build: {
20 | outDir: 'dist',
21 | emptyOutDir: true,
22 | },
23 | plugins: [react()],
24 | })
25 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import "@styles/App.scss";
2 |
3 | import { Route, Routes, useSearchParams } from "react-router-dom";
4 | import Viewer from "./Viewer";
5 | import Generator from "./Generator";
6 |
7 | export default function App() {
8 | const [searchParams] = useSearchParams();
9 |
10 | return (
11 |
12 |
21 | }
22 | />
23 | } />
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | "baseUrl": ".",
10 | "paths": {
11 | "@/*": ["src/*"],
12 | "@components/*": ["src/components/*"],
13 | "@assets/*": ["src/assets/*"],
14 | "@styles/*": ["src/styles/*"],
15 | "@utils/*": ["src/utils/*"],
16 | "@interfaces/*": ["src/interfaces/*"],
17 | "@handlers/*": ["src/handlers/*"]
18 | },
19 |
20 | /* Bundler mode */
21 | "moduleResolution": "Node",
22 | "allowSyntheticDefaultImports": true,
23 | "allowImportingTsExtensions": true,
24 | "resolveJsonModule": true,
25 | "isolatedModules": true,
26 | "noEmit": true,
27 | "jsx": "react-jsx",
28 |
29 | /* Linting */
30 | "strict": true,
31 | "noUnusedLocals": false,
32 | "noUnusedParameters": true,
33 | "noFallthroughCasesInSwitch": true
34 | },
35 | "include": ["src"],
36 | "references": [{ "path": "./tsconfig.node.json" }]
37 | }
38 |
--------------------------------------------------------------------------------
/src/styles/Viewer.scss:
--------------------------------------------------------------------------------
1 | //* Viewer styles
2 | // this file contains styles for the viewer component
3 | // styles for the viewer component should not be modified somewhere else
4 |
5 | .viewer {
6 | display: flex;
7 | flex-direction: column;
8 | padding: 0.5rem;
9 |
10 | h1 {
11 | font-size: 5vw;
12 | }
13 |
14 | h1,
15 | p {
16 | background-color: #2f313680;
17 | padding: 0.25rem 0.5rem;
18 | border-radius: 0.25rem;
19 | white-space: nowrap;
20 | overflow: hidden;
21 | text-overflow: ellipsis;
22 | max-width: 20ch;
23 | user-select: none;
24 | }
25 |
26 | .channelNameContainer {
27 | display: flex;
28 | align-items: center;
29 | margin-bottom: 20px;
30 | }
31 |
32 | .client {
33 | display: flex;
34 | flex-direction: row;
35 | gap: 0 0;
36 | align-items: center;
37 | margin: 0.5rem 0;
38 |
39 | // icon styles
40 | svg {
41 | width: 5vw;
42 | aspect-ratio: 1/1;
43 | margin-right: 0.5rem;
44 | filter: drop-shadow(0 0 0.75rem rgba(15, 15, 15, 0.1));
45 | }
46 |
47 | // client name styles
48 | p {
49 | font-size: 5vw;
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/utils/logger.tsx:
--------------------------------------------------------------------------------
1 | export default class Logger {
2 | // Log message to the console
3 | public static log(message: string, data: object | null = null): void {
4 | console.log(`[Log] %c${message}`.trim(), "color: gray", data ?? "");
5 | }
6 |
7 | // Log warning to the console
8 | public static warn(message: string, data: object | null = null): void {
9 | console.warn(`%c${message}`.trim(), data ?? "");
10 | }
11 |
12 | // Log error to the console
13 | public static error(message: string, data: object | null = null): void {
14 | console.error(`%c${message}`.trim(), data ?? "");
15 | }
16 |
17 | // Log message received from the websocket to the console
18 | public static wsReceived(data: object, message: string | undefined = undefined): void {
19 | console.log(`%c[WS Recieved] ${message ?? ""}`.trim(), "color: #8258c7", data);
20 | }
21 |
22 | // Log message sent to the websocket to the console
23 | public static wsSent(data: object, message: string | undefined = undefined): void {
24 | console.log(`%c[WS Sent] ${message ?? ""}`.trim(), "color: #4eb570", data);
25 | }
26 |
27 | // Log message to the console with a timestamp
28 | public static ts(message: string, data: object | null = null): void {
29 | console.log(`%c[TS] ${message}`.trim(), "color: #2e6bc7", data ?? "");
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "teamspeak-obs-overlay",
3 | "private": false,
4 | "version": "2.2.0",
5 | "description": "Overlay for OBS to show the current talking clients in your TeamSpeak Channel",
6 | "author": "DerTyp7",
7 | "homepage": "https://dertyp7.github.io/teamspeak-obs-overlay/#",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/DerTyp7/teamspeak-obs-overlay"
11 | },
12 | "keywords": [
13 | "ts5",
14 | "ts6",
15 | "teamspeak6",
16 | "teamspeak",
17 | "teamspeak5",
18 | "overlay",
19 | "remote app",
20 | "obs",
21 | "typescript",
22 | "react",
23 | "vite"
24 | ],
25 | "bugs": {
26 | "url": "https://github.com/DerTyp7/teamspeak-obs-overlay/issues"
27 | },
28 | "type": "module",
29 | "scripts": {
30 | "dev": "vite",
31 | "build": "tsc && vite build",
32 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
33 | "preview": "vite preview"
34 | },
35 | "dependencies": {
36 | "@types/node": "^22.10.10",
37 | "react": "^19.0.0",
38 | "react-dom": "^19.0.0",
39 | "react-router-dom": "^7.1.3",
40 | "react-teamspeak-remote-app-api": "^2.0.0",
41 | "sass": "^1.83.4"
42 | },
43 | "devDependencies": {
44 | "@types/jest": "^29.5.14",
45 | "@types/react": "^19.0.8",
46 | "@types/react-dom": "^19.0.3",
47 | "@typescript-eslint/eslint-plugin": "^8.21.0",
48 | "@typescript-eslint/parser": "^8.21.0",
49 | "@vitejs/plugin-react-swc": "^3.7.2",
50 | "eslint": "^9.18.0",
51 | "eslint-plugin-react-hooks": "^5.1.0",
52 | "eslint-plugin-react-refresh": "^0.4.18",
53 | "typescript": "^5.7.3",
54 | "vite": "^6.0.11"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/styles/index.scss:
--------------------------------------------------------------------------------
1 | // Reset styles for all elements
2 | * {
3 | font-family: Arial, Helvetica, sans-serif;
4 | font-weight: bold;
5 | font-size: 1rem;
6 | margin: 0;
7 | padding: 0;
8 | box-sizing: border-box;
9 | }
10 |
11 | // Set up basic styles for the entire page
12 | body,
13 | html {
14 | min-height: 100vh;
15 | min-width: 100%;
16 | display: flex;
17 | overflow-x: hidden;
18 | color: #fff;
19 | }
20 |
21 | // Ensure the root element takes up the full viewport
22 | #root {
23 | height: 100%;
24 | min-width: 100%;
25 | }
26 |
27 | // Headline styles
28 | h1 {
29 | font-size: 1.8rem;
30 | }
31 |
32 | h2 {
33 | font-size: 1.5rem;
34 | }
35 |
36 | h3 {
37 | font-size: 1.2rem;
38 | }
39 |
40 | h4 {
41 | font-size: 1.1rem;
42 | }
43 |
44 | h5 {
45 | font-size: 1rem;
46 | }
47 |
48 | h6 {
49 | font-size: 0.9rem;
50 | }
51 |
52 | // Common styles for heading elements
53 | h1,
54 | h2,
55 | h3,
56 | h4,
57 | h5,
58 | h6 {
59 | font-weight: bold;
60 | }
61 |
62 | // Text styles
63 | a {
64 | color: #3abe78;
65 | font-weight: bold;
66 | text-decoration: none;
67 | transition: all 100ms ease-in-out;
68 |
69 | &:hover {
70 | color: #31f399;
71 | }
72 | }
73 |
74 | // Button styles
75 | button {
76 | background-color: #202024;
77 | color: #fff;
78 | font-weight: bold;
79 | border: 2px solid #31f399;
80 | border-radius: 5px;
81 | padding: 10px 20px;
82 | cursor: pointer;
83 | transition: all 300ms ease-in-out;
84 |
85 | &:hover {
86 | background-color: #42d486;
87 | color: #202024;
88 | }
89 | }
90 |
91 | // Custom dark-themed scrollbar
92 | ::-webkit-scrollbar {
93 | width: 5px;
94 | height: 5px;
95 | }
96 |
97 | ::-webkit-scrollbar-track {
98 | background: #363638;
99 | border-radius: 10px;
100 | }
101 |
102 | ::-webkit-scrollbar-thumb {
103 | background: #31f39973;
104 | border-radius: 10px;
105 |
106 | &:hover {
107 | background: #48ee95;
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TeamSpeak-OBS-Overlay
2 |
3 | This is an overlay for OBS to show the current talking clients in your TeamSpeak5/6 Channel.
4 | This App uses the new "Remote Apps" feature of TeamSpeak5/6.
5 |
6 | This overlay uses the [TeamSpeak Remote App API](https://github.com/DerTyp7/react-teamspeak-remote-app-api).
7 |
8 | 
9 |
10 | - [TeamSpeak-OBS-Overlay](#teamspeak-obs-overlay)
11 | - [Usage](#usage)
12 | - [Quick instructions](#quick-instructions)
13 | - [Detailed instructions](#detailed-instructions)
14 | - [Common Issues](#common-issues)
15 | - [The overlay is empty, but i'm connected to a TeamSpeak5 server](#the-overlay-is-empty-but-im-connected-to-a-teamspeak5-server)
16 | - [OBS doesn't show the latest version of the overlay](#obs-doesnt-show-the-latest-version-of-the-overlay)
17 | - [Setup (Developer)](#setup-developer)
18 |
19 | ## Usage
20 |
21 | ### Quick instructions
22 |
23 | 1. Open this link in your Browser: [https://teamspeak-overlay.tealfire.de/#/generate](https://teamspeak-overlay.tealfire.de/#/generate)
24 | 2. Follow the instructions on the website
25 | 3. Accept overlay inside TeamSpeak5/6
26 | 
27 |
28 | ### Detailed instructions
29 |
30 | Try this instruction if you have problems with the quick instructions above.
31 |
32 | 1. Open this link in your Browser: [https://teamspeak-overlay.tealfire.de/#/generate](https://teamspeak-overlay.tealfire.de/#/generate)
33 |
34 | 2. Follow the instructions on the website
35 |
36 | 3. Go into the TeamSpeak5 Settings and enable "Remote Apps"
37 | 
38 |
39 | 4. Add a new Browser Source to your OBS Scene
40 | 
41 | 
42 |
43 | 5. Enter the in step 1 generated URL into the URL field of the Browser Source
44 | 
45 |
46 | 6. Set the width and height to your desired size. Recommended is a width of 1000px and the height is determined of how many clients are expected (play around with these values)
47 |
48 | 7. You should now receive a notification in TeamSpeak5 that the app is allowed to connect to your TeamSpeak5 client. Allow it. (If you don't get a notification, restart TeamSpeak5 and OBS -> try again)
49 | 
50 |
51 | ## Common Issues
52 |
53 | ### The overlay is empty, but i'm connected to a TeamSpeak5 server
54 |
55 | **Fix 1**
56 | Make sure you accepted the notification in your TeamSpeak Client.
57 |
58 | **Fix 2**
59 | Sadly TeamSpeak5/6 does not give us any information about the current active server tab.
60 | 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 client’s microphone.
61 |
62 | However this workaround is not 100% accurate and can fail in some cases.
63 |
64 | Possible fixes:
65 |
66 | - Unmute and mute yourself in the active server tab (Just a normal unmute and mute, not the hardware mute)
67 | - Reconnect to the TS5/6 server while the overlay is open
68 |
69 | ### OBS doesn't show the latest version of the overlay
70 |
71 | This can happen if the OBS Browser Source is caching the overlay.
72 | To fix this, open the Browser Source settings and click on "Refresh cache of current page".
73 |
74 | ## Setup (Developer)
75 |
76 | 1. Clone this repository
77 | 2. Run `npm install`
78 | 3. To start the development server run `npm run dev`
79 |
80 | > **Note:** Pull requests are welcome, but please be consistent with the code style.
81 | > This project uses [Prettier](https://prettier.io/) to format the code.
82 | > Pull requests always in the `dev` branch.
83 |
--------------------------------------------------------------------------------
/src/Generator.tsx:
--------------------------------------------------------------------------------
1 | import { ChangeEvent, useRef, useState, useEffect } from "react";
2 | import "@styles/Generator.scss";
3 | import Viewer from "./Viewer";
4 |
5 | export default function Generator() {
6 | // State variables
7 | const [outputUrl, setOutputUrl] = useState(() => new URL(window.location.href).toString());
8 | const copiedTooltipRef = useRef(null);
9 |
10 | const [remoteAppPort, setRemoteAppPort] = useState(5899);
11 | const [showChannelName, setShowChannelName] = useState(true);
12 | const [hideNonTalking, setHideNonTalking] = useState(false);
13 | const [clientLimit, setClientLimit] = useState(0);
14 |
15 | // Effect to generate URL when dependencies change
16 | useEffect(() => {
17 | generateUrl();
18 | }, [remoteAppPort, showChannelName, hideNonTalking, clientLimit]);
19 |
20 | // Function to generate the output URL
21 | function generateUrl() {
22 | const url = new URL(window.location.href);
23 | url.hash = "";
24 |
25 | url.searchParams.set("remoteAppPort", remoteAppPort.toString());
26 | url.searchParams.set("showChannelName", showChannelName.toString());
27 | url.searchParams.set("hideNonTalking", hideNonTalking.toString());
28 | url.searchParams.set("clientLimit", clientLimit.toString());
29 |
30 | // url.hash function always sets the hash to the end of the URL, so we have to replace the question mark with a hash
31 | // gh-pages needs the hash to be between the base URL and the search params
32 | setOutputUrl(url.toString().replace("?", "#/?"));
33 | }
34 |
35 | // Function to copy URL to clipboard
36 | function copy() {
37 | navigator.clipboard.writeText(outputUrl);
38 |
39 | if (copiedTooltipRef.current) {
40 | copiedTooltipRef.current.style.animation = "tooltipAnimation 200ms";
41 | copiedTooltipRef.current.style.opacity = "1";
42 |
43 | setTimeout(() => {
44 | if (copiedTooltipRef.current) {
45 | copiedTooltipRef.current.style.opacity = "0";
46 | copiedTooltipRef.current.style.animation = "";
47 | }
48 | }, 1000);
49 | }
50 | }
51 |
52 | return (
53 |
54 | {/* Header */}
55 |
56 |
TeamSpeak-OBS-Overlay Generator
57 | by DerTyp7
58 |
59 |
60 | {/* Instructions */}
61 |
67 |
68 | {/* Output Section */}
69 |
70 |
71 | {outputUrl}
72 |
73 |
76 |
77 | Copied!
78 |
79 |
80 |
81 | {/* Generator Content */}
82 |
83 | {/* Configurations */}
84 |
85 |
Configurations
86 |
87 |
88 | {/* Option Sections */}
89 |
102 |
103 |
116 |
117 |
118 |
119 | {/* Preview */}
120 |
121 |
122 |
123 |
124 |
125 | );
126 | }
127 |
--------------------------------------------------------------------------------
/src/styles/Generator.scss:
--------------------------------------------------------------------------------
1 | // Breakpoints
2 | $breakpoint-1: 1200px;
3 | $breakpoint-2: 790px;
4 | $breakpoint-3: 600px;
5 |
6 | // Tooltip animation keyframes
7 | @keyframes tooltipAnimation {
8 | 0% {
9 | opacity: 0;
10 | transform: translateY(-10px);
11 | }
12 | 100% {
13 | opacity: 1;
14 | transform: translateY(0);
15 | }
16 | }
17 |
18 | .generator {
19 | background-color: #232528;
20 | color: #fff;
21 | width: 100%;
22 | display: flex;
23 | height: 100%;
24 | flex-direction: column;
25 | align-items: center;
26 | gap: 50px;
27 | padding: 50px 0;
28 |
29 | .headline {
30 | text-align: center;
31 | letter-spacing: 1.8px;
32 | }
33 |
34 | .instructions {
35 | border-bottom: 2px solid #75797773;
36 | display: flex;
37 | flex-direction: row;
38 | align-items: center;
39 | justify-content: center;
40 | gap: 10px 50px;
41 | flex-wrap: wrap;
42 | padding-bottom: 10px;
43 |
44 | p,
45 | a {
46 | font-size: 0.8rem;
47 | }
48 |
49 | p {
50 | color: #b4b4b4;
51 | }
52 | }
53 |
54 | .output {
55 | display: flex;
56 | flex-direction: row;
57 | align-items: center;
58 | justify-content: center;
59 | column-gap: 20px;
60 | position: relative;
61 | height: 50px;
62 |
63 | .url {
64 | white-space: nowrap;
65 | overflow: auto;
66 | font-weight: bold;
67 | width: 500px;
68 | padding: 5px 10px;
69 | background-color: #313136;
70 | border-radius: 5px;
71 | text-align: center;
72 | }
73 |
74 | .copy {
75 | padding: 5px 10px;
76 | border: 2px solid #31f39973;
77 | transition: all 100ms ease-in-out;
78 |
79 | &:hover {
80 | border-color: #42d486;
81 | background-color: transparent;
82 | color: #fff;
83 | }
84 | }
85 |
86 | .copiedTooltip {
87 | opacity: 0;
88 | position: absolute;
89 | right: -90px;
90 | background-color: #31f399;
91 | color: #202024;
92 | padding: 5px 10px;
93 | border-radius: 5px;
94 | font-size: 0.8rem;
95 | font-weight: bold;
96 | }
97 | }
98 |
99 | .generatorContent {
100 | display: flex;
101 | flex-direction: row;
102 | align-items: center;
103 | justify-content: center;
104 | padding: 0 50px;
105 | gap: 30px;
106 | width: 100%;
107 |
108 | .configurations {
109 | display: flex;
110 | flex-direction: column;
111 | align-items: center;
112 | height: 100%;
113 | flex: 1;
114 | gap: 50px;
115 |
116 | .options {
117 | display: flex;
118 | flex-direction: row;
119 | align-items: center;
120 | gap: 100px;
121 | user-select: none;
122 |
123 | section {
124 | display: flex;
125 | flex-direction: column;
126 | align-items: center;
127 | justify-content: left;
128 | gap: 10px;
129 | }
130 |
131 | .option {
132 | display: flex;
133 | flex-direction: row;
134 | align-items: center;
135 | justify-content: left;
136 | column-gap: 10px;
137 | width: 100%;
138 |
139 | input {
140 | -webkit-appearance: none;
141 | -moz-appearance: none;
142 | appearance: none;
143 | width: 20px;
144 | height: 20px;
145 | border: 2px solid #31f399;
146 | border-radius: 5px;
147 | background-color: #202024;
148 | outline: none;
149 | transition: all 200ms ease-in-out;
150 | position: relative;
151 | color: #fff;
152 | text-align: center;
153 |
154 | &::-webkit-outer-spin-button,
155 | &::-webkit-inner-spin-button {
156 | -webkit-appearance: none;
157 | margin: 0;
158 | }
159 |
160 | // Cross when checked styles
161 | &:checked {
162 | &:after {
163 | content: "";
164 | position: absolute;
165 | width: 10px;
166 | height: 2px;
167 | background-color: #31f399;
168 | transform: rotate(45deg);
169 | }
170 | &:before {
171 | content: "";
172 | position: absolute;
173 | width: 10px;
174 | height: 2px;
175 | background-color: #31f399;
176 | transform: rotate(-45deg);
177 | }
178 |
179 | &:after,
180 | &:before {
181 | top: 7px;
182 | left: 3px;
183 | }
184 | }
185 | cursor: pointer;
186 | }
187 |
188 | input[type="number"] {
189 | width: 50px;
190 | height: 25px;
191 | cursor: text;
192 | -moz-appearance: textfield;
193 | appearance: textfield;
194 | }
195 |
196 | label {
197 | cursor: pointer;
198 | }
199 | }
200 | }
201 | }
202 |
203 | // Preview styles
204 | .preview {
205 | flex: 1;
206 | border: 2px solid #31f39973;
207 |
208 | // Viewer styles (see src/styles/Viewer.scss)
209 | .viewer {
210 | background-image: url("/images/viewer_example_background.png");
211 | background-repeat: no-repeat;
212 | background-size: cover;
213 | min-height: 500px;
214 | h1 {
215 | font-size: 2rem;
216 | }
217 | .client {
218 | svg {
219 | width: 2rem;
220 | }
221 |
222 | p {
223 | font-size: 2rem;
224 | }
225 | }
226 | }
227 | }
228 | }
229 |
230 | // Responsive styles
231 | @media screen and (max-width: $breakpoint-1) {
232 | .generatorContent {
233 | flex-direction: column;
234 |
235 | .preview {
236 | width: 80%;
237 | }
238 | }
239 | }
240 |
241 | @media screen and (max-width: $breakpoint-2) {
242 | .output {
243 | .url {
244 | width: 300px;
245 | }
246 | }
247 |
248 | .generatorContent {
249 | .preview {
250 | width: 100%;
251 | }
252 | }
253 | }
254 |
255 | @media screen and (max-width: $breakpoint-3) {
256 | .output {
257 | .url {
258 | width: 200px;
259 | }
260 |
261 | .generatorContent {
262 | .configurations {
263 | .options {
264 | flex-direction: column;
265 | gap: 30px;
266 | }
267 | }
268 | }
269 | }
270 | }
271 | }
272 |
--------------------------------------------------------------------------------
/src/Viewer.tsx:
--------------------------------------------------------------------------------
1 | import "@styles/Viewer.scss";
2 | import useTSRemoteApp, { IClient } from "react-teamspeak-remote-app-api";
3 |
4 | export default function Viewer({
5 | remoteAppPort = 5899,
6 | showChannelName = false,
7 | hideNonTalking = false,
8 | clientLimit = 0,
9 | }: {
10 | remoteAppPort?: number;
11 | showChannelName?: boolean;
12 | hideNonTalking?: boolean;
13 | clientLimit?: number;
14 | }) {
15 | const { clients, activeConnectionId, currentChannel } = useTSRemoteApp({
16 | remoteAppPort: remoteAppPort,
17 | auth: {
18 | identifier: "de.tealfire.obs",
19 | version: "2.2.0",
20 | name: "TeamSpeak OBS Overlay",
21 | description: "A OBS overlay for TeamSpeak by DerTyp7",
22 | },
23 | logging: true,
24 | });
25 |
26 | const currentClients = clients.map((client) => {
27 | if (client.channel?.id === currentChannel?.id && client.channel.connection.id === activeConnectionId) {
28 | return client;
29 | }
30 | }) as IClient[];
31 |
32 | return (
33 |
34 | {showChannelName && currentChannel ? (
35 |
36 |
{currentChannel?.properties.name}
37 |
38 | ) : null}
39 | {currentClients?.map((client, i) => {
40 | //* Client limit
41 | if (clientLimit != 0 && i >= clientLimit) {
42 | return null;
43 | }
44 |
45 | if (client) {
46 | //* Non-talking client
47 | if (hideNonTalking && (client.properties.inputMuted || client.properties.outputMuted || client.talkStatus == 0)) {
48 | return null;
49 | }
50 |
51 | //* Normal client
52 | return (
53 |
54 | {client.properties.outputHardware == false ? (
55 |
80 | ) : client.properties.inputHardware == false ? (
81 |
91 | ) : client.properties.outputMuted ? (
92 |
117 | ) : client.properties.inputMuted ? (
118 |
127 | ) : client.talkStatus == 1 ? (
128 |
134 | ) : (
135 |
141 | )}
142 |
{client.properties.nickname}
143 |
144 | );
145 | } else {
146 | return
;
147 | }
148 | })}
149 | {currentChannel == null ? (
150 | <>
151 |
Overlay couldn't connect to the client:
152 |
153 |
154 |
1. Make sure to accept the overlay in your TeamSpeak-Client via the notifications
155 |
156 |
2. Enable remote apps inside the the TeamSpeak-Settings
157 |
158 |
3. Make sure to match the configuration port with the port in the TeamSpeak remote app settings
159 |
160 |
4. Refresh this page/BrowserSource (Select BrowserSource & click "Refresh" in OBS)
161 |
162 |
If non of this worked refer to the GitHub and write an issue with your problem
163 | >
164 | ) : (
165 | ""
166 | )}
167 |
168 | );
169 | }
170 |
--------------------------------------------------------------------------------