├── 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 | 7 | 8 | 10 | 22 | 23 | 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 | ![image](https://github.com/DerTyp7/ts5-obs-overlay/assets/76851529/d0ab06f2-1a36-479d-826f-bd4bd3d405b7) 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 | ![image](https://github.com/DerTyp7/teamspeak-overlay.tealfire.de/assets/76851529/aa83b07d-3dea-461f-9487-f9e6a299f2f3) 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 | ![image](https://github.com/DerTyp7/ts5-obs-overlay/assets/76851529/b31bc553-fde2-46ab-b07c-d3c81339cc7d) 38 | 39 | 4. Add a new Browser Source to your OBS Scene 40 | ![image](https://github.com/DerTyp7/ts5-obs-overlay/assets/76851529/0198b468-bb96-4b65-bdd4-3d6bb3ef7d25) 41 | ![image](https://github.com/DerTyp7/ts5-obs-overlay/assets/76851529/58ad399f-5344-456f-b243-6e267b489fd5) 42 | 43 | 5. Enter the in step 1 generated URL into the URL field of the Browser Source 44 | ![image](https://github.com/DerTyp7/ts5-obs-overlay/assets/76851529/e8fd4a1b-be70-4123-8d28-4dc7ebc8c2bd) 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 | ![image](https://github.com/DerTyp7/ts5-obs-overlay/assets/76851529/40faa435-e128-415f-98eb-a9e8809e8f65) 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 |
62 |

1. Customize your settings

63 |

2. Copy the generated URL

64 |

3. Paste the URL into the BrowserSource URL field in OBS

65 | Click here for detailed instructions 66 |
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 |
90 | {/* Show Channel Name Option */} 91 |
setShowChannelName(!showChannelName)}> 92 | 93 | 94 |
95 | 96 | {/* Hide Non-Talking Clients Option */} 97 |
setHideNonTalking(!hideNonTalking)}> 98 | 99 | 100 |
101 |
102 | 103 |
104 | {/* Client Limit Option */} 105 |
106 | ) => setClientLimit(parseInt(e.target.value))} /> 107 | 108 |
109 | 110 | {/* RemoteApp-Port Option */} 111 |
112 | ) => setRemoteAppPort(parseInt(e.target.value))} /> 113 | 114 |
115 |
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 | 56 | muted_hardware_output 57 | 58 | 62 | 66 | 70 | 74 | 78 | 79 | 80 | ) : client.properties.inputHardware == false ? ( 81 | 82 | muted_hardware_input 83 | 84 | 88 | 89 | 90 | 91 | ) : client.properties.outputMuted ? ( 92 | 93 | muted_output 94 | 95 | 99 | 103 | 107 | 111 | 115 | 116 | 117 | ) : client.properties.inputMuted ? ( 118 | 119 | muted_input 120 | 121 | 125 | 126 | 127 | ) : client.talkStatus == 1 ? ( 128 | 129 | player_on_v2 130 | 131 | 132 | 133 | 134 | ) : ( 135 | 136 | player_off_v2 137 | 138 | 139 | 140 | 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 | --------------------------------------------------------------------------------