<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: MehdiHttp</title>
    <description>The latest articles on DEV Community by MehdiHttp (@mehdi_alemehdihttp_a91).</description>
    <link>https://dev.to/mehdi_alemehdihttp_a91</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1588086%2Ff388658e-0d83-4a3e-a188-29c960cc20d7.jpg</url>
      <title>DEV Community: MehdiHttp</title>
      <link>https://dev.to/mehdi_alemehdihttp_a91</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mehdi_alemehdihttp_a91"/>
    <language>en</language>
    <item>
      <title>Building a Discord-style media chat inside TeamSpeak 3 with Qt and no extra server</title>
      <dc:creator>MehdiHttp</dc:creator>
      <pubDate>Mon, 12 Oct 2026 11:21:00 +0000</pubDate>
      <link>https://dev.to/mehdi_alemehdihttp_a91/building-a-discord-style-media-chat-inside-teamspeak-3-with-qt-and-no-extra-server-2m8e</link>
      <guid>https://dev.to/mehdi_alemehdihttp_a91/building-a-discord-style-media-chat-inside-teamspeak-3-with-qt-and-no-extra-server-2m8e</guid>
      <description>&lt;p&gt;TeamSpeak 3 is old, but many gaming communities still run it, and its voice is great. Its chat is not: a plain-text log with BBCode. To share a screenshot, you upload it to the channel's file browser, post a link, and everyone downloads it and opens it somewhere else.&lt;/p&gt;

&lt;p&gt;I wanted a Discord-style chat inside the TeamSpeak 3 client: pictures and GIFs inline, a video player, voice messages, replies, reactions, polls. With one hard rule: &lt;strong&gt;no extra server, no cloud, no account.&lt;/strong&gt; The server a community already runs had to be enough.&lt;/p&gt;

&lt;p&gt;The result is &lt;a href="https://github.com/Metihttp/Teamspeak_Media_chat" rel="noopener noreferrer"&gt;TS Media chat&lt;/a&gt;, a free plugin for the TeamSpeak 3.6 client on Windows, written in C++17 with Qt 5.15 Widgets and now at v2.3.1. (The source is public on GitHub; it's source-available rather than open source.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy3zw7t0lg6dosmzkulaw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy3zw7t0lg6dosmzkulaw.png" alt="The same messages with and without the plugin: a plain file link with a grey note, and the same chat with the photo shown inline" width="799" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What's in this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;storing media in the channel's own file browser&lt;/li&gt;
&lt;li&gt;drawing previews into TeamSpeak's chat documents, reversibly&lt;/li&gt;
&lt;li&gt;three ways my plugin crashed TeamSpeak, one way it quietly broke, and the fixes&lt;/li&gt;
&lt;li&gt;measured anti-flood numbers and a tiny protocol over plugin commands&lt;/li&gt;
&lt;li&gt;anonymous polls with a hash chain, and live music through your own voice&lt;/li&gt;
&lt;li&gt;auto-updates from GitHub without signing keys&lt;/li&gt;
&lt;li&gt;testing a plugin for a closed-source app&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The core trick: the server already has storage
&lt;/h2&gt;

&lt;p&gt;Every TeamSpeak channel has a file browser. Drag a file from it into the chat and TeamSpeak posts a BBCode link with a &lt;code&gt;ts3file://&lt;/code&gt; URL. TS Media does exactly that: it uploads the file with TeamSpeak's own file transfer (into &lt;code&gt;/tsmedia&lt;/code&gt;, previews into &lt;code&gt;/tsmedia/previews&lt;/code&gt;) and posts an ordinary &lt;code&gt;ts3file&lt;/code&gt; link with a few extra parameters (a video link here):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[URL=ts3file://&amp;lt;host&amp;gt;?port=..&amp;amp;serverUID=..&amp;amp;channel=..&amp;amp;path=..&amp;amp;filename=..&amp;amp;isDir=0
  &amp;amp;size=..&amp;amp;fileDateTime=..&amp;amp;tsm=2&amp;amp;w=..&amp;amp;h=..&amp;amp;d=..&amp;amp;bh=..&amp;amp;pv=..&amp;amp;sha=..]&amp;lt;file name&amp;gt;[/URL]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Everything from &lt;code&gt;tsm=&lt;/code&gt; on is TS Media's own, and TeamSpeak ignores parameters it doesn't know. Without the plugin you see a normal download link and a short grey note, &lt;em&gt;TS Media chat plugin required to view this in chat&lt;/em&gt;. With the plugin, the extras (width and height, duration, a BlurHash placeholder, a preview path, the SHA-256, spoiler, album and voice-message flags) are enough to draw the media in place and hide the note.&lt;/p&gt;

&lt;p&gt;Two lessons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every link is hostile.&lt;/strong&gt; Anyone can type a &lt;code&gt;ts3file&lt;/code&gt; link with any parameters, so sizes are clamped, previews must be in the same channel, and images above 80 megapixels are never decoded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why the link carries a SHA-256.&lt;/strong&gt; TeamSpeak reports a download as complete &lt;em&gt;before&lt;/em&gt; it has finished writing the preallocated file, so the end of the file can still be zeros. The plugin waits until the file is really written, then checks the hash. A file that was swapped on the server after it was posted is deleted and marked &lt;em&gt;File doesn't match what was sent&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Living inside someone else's Qt app
&lt;/h2&gt;

&lt;p&gt;A TeamSpeak plugin is a DLL with C exports (&lt;code&gt;ts3plugin_init&lt;/code&gt;, &lt;code&gt;ts3plugin_onTextMessageEvent&lt;/code&gt; and so on; plugin API 26). TeamSpeak's UI is Qt, so a plugin built against the same Qt can work with its widgets, under a few rules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exactly the same Qt, and only part of it.&lt;/strong&gt; TeamSpeak 3.6.2 ships Qt 5.15.2, so I build against 5.15.2, and only with QtCore, QtGui and QtWidgets: there's no QtMultimedia and no QtConcurrent. The build script checks every imported Qt symbol against the exports of TeamSpeak's own Qt DLLs: a plugin that imports one symbol they don't export fails to load on users' PCs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Previews live in TeamSpeak's chat documents.&lt;/strong&gt; Each chat view is a &lt;code&gt;QTextBrowser&lt;/code&gt; (&lt;code&gt;ChatTab : TextBrowser : QTextBrowser&lt;/code&gt;) with the stock &lt;code&gt;QTextDocumentLayout&lt;/code&gt;. One message is one &lt;code&gt;QTextBlock&lt;/code&gt;; a &lt;code&gt;\n&lt;/code&gt; becomes U+2028 inside that block. A preview is an image resource in that document, placed under the link. Follow-up messages that only carry more album links are hidden with &lt;code&gt;QTextBlock::setVisible(false)&lt;/code&gt;; the layout honours it but doesn't keep the view pinned to the bottom, so the plugin does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every edit must be reversible.&lt;/strong&gt; TeamSpeak can unload a plugin while TeamSpeak itself keeps running, so every change the plugin makes to its documents must be undone byte for byte. For HD emoji, each emoji (a whole grapheme cluster, such as a ZWJ sequence or a flag) is replaced by one image whose format remembers what was there:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// c: one emoji found in the chat document (position, length, text, format)&lt;/span&gt;
&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;QTextCursor&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;KeepAnchor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;QTextImageFormat&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setObjectType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QTextFormat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ImageObject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kOriginalText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                  &lt;span class="c1"&gt;// UserProperty + 0x7460&lt;/span&gt;
&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kOriginalFormat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;QTextFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// format before&lt;/span&gt;
&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resourceName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;                     &lt;span class="c1"&gt;// e.g. "tsmemoji:1f602/22"&lt;/span&gt;
&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;insertText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QChar&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ObjectReplacementCharacter&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;On unload, &lt;code&gt;restoreInto()&lt;/code&gt; puts back the original text and format of every fragment with those properties. Reply lines, reaction rows, poll cards and the restyled layout carry their own &lt;code&gt;UserProperty&lt;/code&gt; markers the same way. The Cozy layout below is TeamSpeak's own chat document, restyled in place; on unload it goes back to TeamSpeak's plain look.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjddsfoxwarvwxotndu69.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjddsfoxwarvwxotndu69.png" alt="TS Media's Cozy layout with bubbles, avatars, day dividers, centred system rows and your own messages on the right, next to the Compact layout" width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One chat input for every server tab.&lt;/strong&gt; TeamSpeak 3.6.2 has a single &lt;code&gt;ChatLineEdit&lt;/code&gt; (a &lt;code&gt;QTextEdit&lt;/code&gt; with &lt;code&gt;acceptRichText=false&lt;/code&gt;) shared by all server tabs, so everything attached to it must follow the tab in front. And its "Enter Chat Message..." placeholder isn't one: it's real grey text that TeamSpeak removes on focus-in. An early emoji picker inserted 😀 into an empty, unfocused input, and the message went out as &lt;code&gt;😀Enter Chat Message...&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Things that crashed (or quietly broke) TeamSpeak
&lt;/h2&gt;

&lt;p&gt;A plugin shares the host's process: when it crashes, TeamSpeak crashes. Each of these really happened; I count crash dumps before and after every live test.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. A pending &lt;code&gt;QTimer::singleShot&lt;/code&gt; at unload
&lt;/h3&gt;

&lt;p&gt;Up to 2.0.7, and in my 2.1.0 development build, TeamSpeak crashed on exit if you quit within 15 seconds of starting it (or within about 30 seconds of sending a file). The dump pointed at module base + &lt;code&gt;0x1CC40&lt;/code&gt;, which the linker map resolved to &lt;code&gt;QFunctorSlotObject&amp;lt;Core::start()::lambda_4&amp;gt;::impl&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// kStartupCacheCheckMs = 15000&lt;/span&gt;
&lt;span class="n"&gt;QTimer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;singleShot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kStartupCacheCheckMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;enforceCacheLimit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In Qt 5.15 the functor overload creates an internal timer whose parent is the &lt;strong&gt;event dispatcher&lt;/strong&gt;, not your context object. The context is only watched, so deleting it neither stops the timer nor frees the functor.&lt;/p&gt;

&lt;p&gt;The functor gets destroyed later (when the timer fires, or when &lt;code&gt;QApplication&lt;/code&gt; goes) by code in my DLL, which by then is unloaded.&lt;/p&gt;

&lt;p&gt;Before the fix, the 2.1 code had 19 such calls, including a 30-minute timer after a failed upload. The fix is a timer the object owns:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;template&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Fn&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="n"&gt;QTimer&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;singleShotOwned&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;QObject&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Fn&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;auto&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;QTimer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;setSingleShot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;QObject&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;QTimer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;)]()&lt;/span&gt; &lt;span class="k"&gt;mutable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;deleteLater&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Shutdown deletes the owners while the DLL is still loaded, and the timers and functors go with them. (A zero-delay &lt;code&gt;singleShot&lt;/code&gt; is fine: it's a posted event, removed with its receiver.)&lt;/p&gt;
&lt;h3&gt;
  
  
  2. &lt;code&gt;qApp-&amp;gt;quit()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The updater needs to restart TeamSpeak. My first fallback was &lt;code&gt;QTimer::singleShot(0, qApp, SLOT(quit()))&lt;/code&gt;. TeamSpeak crashed at once inside its own code, &lt;code&gt;ts3plugin_shutdown&lt;/code&gt; never ran, and the process lived on without a window until I killed it. What works (4 runs out of 4): find TeamSpeak's own Quit action (the one with Ctrl+Q) and trigger it through a queued call.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;QAction&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;findQuitAction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;QKeySequence&lt;/span&gt; &lt;span class="n"&gt;quit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Qt&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CTRL&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Qt&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Key_Q&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QWidget&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;QApplication&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;topLevelWidgets&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;qobject_cast&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;QMainWindow&lt;/span&gt;&lt;span class="o"&gt;*&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QAction&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;findChildren&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;QAction&lt;/span&gt;&lt;span class="o"&gt;*&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;isSeparator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;menu&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;shortcut&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;QKeySequence&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ExactMatch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;nullptr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// then, from the updater:&lt;/span&gt;
&lt;span class="n"&gt;QMetaObject&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;invokeMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"trigger"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Qt&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;QueuedConnection&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  3. &lt;code&gt;QStringLiteral&lt;/code&gt; in long-lived Qt state
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;QStringLiteral&lt;/code&gt; builds the string at compile time. Its characters live in the DLL's read-only data, and copies share them. If one lands in Qt state that outlives the DLL (style sheets, &lt;code&gt;QSettings&lt;/code&gt;, tooltips, &lt;code&gt;QFont&lt;/code&gt; families, statics), Qt later reads memory that is gone. Long-lived strings get heap data:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Bad: the characters stay in the DLL image,&lt;/span&gt;
&lt;span class="c1"&gt;// and Qt's font cache can outlive the DLL&lt;/span&gt;
&lt;span class="n"&gt;QFont&lt;/span&gt; &lt;span class="nf"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QStringLiteral&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Segoe UI Emoji"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// Good: the characters are copied to the heap&lt;/span&gt;
&lt;span class="n"&gt;QFont&lt;/span&gt; &lt;span class="nf"&gt;good&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QString&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;fromLatin1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Segoe UI Emoji"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  4. &lt;code&gt;[noparse]&lt;/code&gt; protects nothing
&lt;/h3&gt;

&lt;p&gt;Not a crash, but just as surprising. A caption containing &lt;code&gt;[URL=...]&lt;/code&gt; must not become a link. The usual BBCode escape is &lt;code&gt;[noparse]&lt;/code&gt;, but TeamSpeak 3.6 drops those tags and &lt;strong&gt;still parses what's inside&lt;/strong&gt;: a wrapped &lt;code&gt;ts3file&lt;/code&gt; link became a real link (my own plugin drew a file card for it). A backslash before each bracket works, in messages and in plugin-printed lines:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QLatin1Char&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'['&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;QLatin1String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;["&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QLatin1Char&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;']'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;QLatin1String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;]"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The receiving side skips &lt;code&gt;\[URL=&lt;/code&gt; too, so a caption can't smuggle in a fake file link for the plugin to prefetch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One precaution:&lt;/strong&gt; binaries from VS 2022 17.10+ can crash in &lt;code&gt;std::mutex&lt;/code&gt; on an older &lt;code&gt;msvcp140.dll&lt;/code&gt;, and the Visual C++ runtime on users' PCs comes with TeamSpeak and may be older than your compiler. Microsoft's documented workaround is to define &lt;code&gt;_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Measure first
&lt;/h2&gt;

&lt;p&gt;My 2.1 code assumed a 1024-byte message limit. Before 2.2, I wrote a measurement spike: a test-only build that ran probes from inside a TeamSpeak 3.6.2 client against a local 3.13.7 server (default anti-flood, Guest group). Its numbers shaped everything after:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Measured&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Text message limit&lt;/td&gt;
&lt;td&gt;8192 UTF-8 bytes (error &lt;code&gt;0x0605&lt;/code&gt; above that)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Silent drop&lt;/td&gt;
&lt;td&gt;an &lt;em&gt;escaped&lt;/em&gt; command over ~9.1 KB gets no answer and is never delivered (backslash, slash, space, pipe and control characters count double)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text flood cost&lt;/td&gt;
&lt;td&gt;15 of 150 points, 5 back per second: 10 in a row, then "retry in ~5.7 s"; 1 per 3.3 s sustained&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugin command cost&lt;/td&gt;
&lt;td&gt;5 points on a &lt;strong&gt;separate&lt;/strong&gt; counter: ~30 in a row, then ~1 per second&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rejected attempts&lt;/td&gt;
&lt;td&gt;charged too: retrying every 2 s while blocked never recovered in 120 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Channel or server plugin commands&lt;/td&gt;
&lt;td&gt;echoed back to the sender&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A list of target clients&lt;/td&gt;
&lt;td&gt;one stale client id fails the whole command (&lt;code&gt;0x0200&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugin id&lt;/td&gt;
&lt;td&gt;a random GUID on every load; receivers see the DLL name without its suffix (&lt;code&gt;tsmedia&lt;/code&gt; for &lt;code&gt;tsmedia_win64.dll&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File transfers&lt;/td&gt;
&lt;td&gt;never flood-blocked (60 uploads at 150 ms intervals); each start still costs ~3 points on the text counter&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So messages are sized on their escaped length (at most 8168 bytes; a 10-item album still fits). A &lt;code&gt;FloodGovernor&lt;/code&gt; per connection models both counters, keeps 30 points for the user's own typing, and after &lt;code&gt;0x020c&lt;/code&gt; waits for the server's &lt;code&gt;retry in &amp;lt;n&amp;gt;ms&lt;/code&gt; plus 250 ms, then sends exactly one command. Plugin commands carry signalling only; bulk data goes through file transfer.&lt;/p&gt;
&lt;h2&gt;
  
  
  A tiny protocol over plugin commands
&lt;/h2&gt;

&lt;p&gt;Copies of the plugin talk through &lt;code&gt;sendPluginCommand&lt;/code&gt; / &lt;code&gt;onPluginCommandEvent&lt;/code&gt; in a format I called &lt;code&gt;tsm1&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tsm1 HELLO pv=1 v=2.2.0 caps=p,r rr=1   I'm here (to the channel)
tsm1 HI pv=1 v=2.2.0 caps=p,r           the answer, only to whoever asked
tsm1 BYE                                presence off, or the plugin unloads
tsm1 R s=c i=&amp;lt;media key&amp;gt;:up.heart       my complete reaction set on that item
tsm1 SYNC s=c m=&amp;lt;media key&amp;gt;,...         a late joiner asks for reactions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;What kept it robust:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strict and small.&lt;/strong&gt; ASCII only, at most 32 tokens, values limited to &lt;code&gt;[A-Za-z0-9._,:-]&lt;/code&gt;. Outgoing commands are at most 900 bytes; incoming ones over 2048 bytes are dropped before they're even copied.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forward compatible.&lt;/strong&gt; Unknown types and fields are ignored, and so is a future &lt;code&gt;tsm2&lt;/code&gt;. &lt;code&gt;e=&lt;/code&gt; carries reactions with any emoji, while &lt;code&gt;i=&lt;/code&gt; keeps the six classic codes (👍 ❤️ 😂 😮 😢 🔥), so a client that doesn't know &lt;code&gt;e=&lt;/code&gt; still sees those. 2.2 simply ignores 2.3's poll and text-reaction types.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity from the server, never the payload.&lt;/strong&gt; The server fills in the invoker's id, name and unique id. A payload claiming &lt;code&gt;uid=FAKEUID invokerid=999&lt;/code&gt; arrived with the real invoker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match by name, send with the GUID.&lt;/strong&gt; Receivers see the plugin as &lt;code&gt;tsmedia&lt;/code&gt; (the DLL name without its suffix), but sending needs the random GUID TeamSpeak hands out on each load. Channel commands echo back to you, so drop your own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotent state.&lt;/strong&gt; Each &lt;code&gt;R&lt;/code&gt; carries the sender's &lt;em&gt;whole&lt;/em&gt; set for an item, so a lost command heals with the next one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Presence keeps the send window honest: one &lt;code&gt;HELLO&lt;/code&gt; 800 ms after you enter a channel, one &lt;code&gt;HI&lt;/code&gt; back from each member running the plugin after a random 150 to 900 ms, and the window can say &lt;em&gt;3 of 5 people here will see it in the chat&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;TeamSpeak messages have no ids, so a reaction on a text message names it by a hash of the sender and the normalized text, plus "received N seconds ago". Clocks and time zones don't matter.&lt;/p&gt;
&lt;h2&gt;
  
  
  Anonymous polls without a server
&lt;/h2&gt;

&lt;p&gt;A poll is an ordinary chat message, readable without the plugin; plugin users get a card and vote over plugin commands. No server counts votes: every plugin in the channel keeps the ballots it has seen and answers late joiners. So how can a vote be anonymous yet impossible for others to change?&lt;/p&gt;

&lt;p&gt;A hash chain, the idea behind Lamport's one-time passwords:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On your first vote, your plugin makes a random 16-byte secret for that poll. &lt;code&gt;h&lt;/code&gt; is the first 16 bytes of SHA-256 over &lt;code&gt;"tsmpoll1|" + poll + "|" + value&lt;/code&gt;. Write hⁿ for &lt;code&gt;h&lt;/code&gt; applied n times: &lt;code&gt;h¹²⁰(secret)&lt;/code&gt; is the ballot id. Nothing in it says who you are.&lt;/li&gt;
&lt;li&gt;Your vote reveals &lt;code&gt;h¹¹⁹(secret)&lt;/code&gt;; anyone can hash it once and get the id.&lt;/li&gt;
&lt;li&gt;Each change (another option, a retraction) reveals the next link back. SHA-256 can't be inverted, so only the secret's owner can do that. The newest state wins.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;Ballot&lt;/span&gt; &lt;span class="nf"&gt;makeBallot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;QString&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;QByteArray&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quint16&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Ballot&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;qBound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kChainLength&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// kChainLength = 120&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;proof&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chainAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kChainLength&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chainAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                          &lt;span class="c1"&gt;// the chosen options; 0 = retracted&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The honest limits are in &lt;a href="https://github.com/Metihttp/Teamspeak_Media_chat/blob/main/docs/POLLS.md#honest-limits" rel="noopener noreferrer"&gt;the poll docs&lt;/a&gt;: the server tells every receiver who sent each plugin command. TS Media discards that, but a modified client could log it and tie a live vote to a person, or invent ballots. The counts are only as trustworthy as the plugin users in the channel.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fql56r53frw8kbxhwd8f8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fql56r53frw8kbxhwd8f8.png" alt="An anonymous poll card with percentages, the paperclip menu, and the live music bar above the chat input" width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Live music through your own voice
&lt;/h2&gt;

&lt;p&gt;Live music plays a file to your channel &lt;em&gt;through your own voice&lt;/em&gt;, and you can keep talking over it. Everyone hears it, plugin or not.&lt;/p&gt;

&lt;p&gt;TeamSpeak hands the plugin each block of captured voice as 16-bit PCM at 48 kHz in &lt;code&gt;ts3plugin_onEditCapturedVoiceDataEvent(sch, samples, sampleCount, channels, int* edited)&lt;/code&gt;. Media Foundation decodes the file into a 48 kHz ring buffer on a worker thread, and the callback mixes it in with a soft knee above 0.75, so music plus voice never clips hard.&lt;/p&gt;

&lt;p&gt;Once you export a voice callback, TeamSpeak calls it for every audio block, music or not. So the hot path is a few atomic operations (a counter and one load), with no locks and no allocation, and swapping the engine waits until no callback still holds the old one:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onCaptured&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quint64&lt;/span&gt; &lt;span class="n"&gt;sch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;short&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;channels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;edited&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="n"&gt;g_inside&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;Engine&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;g_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;sch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;captured&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;edited&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;g_inside&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// swapping engines&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;setActive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Engine&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Engine&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;g_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// a callback that took the previous engine finishes with it first&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g_inside&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;this_thread&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;yield&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;*edited&lt;/code&gt; holds two flags. Flag 1 tells TeamSpeak the plugin changed the samples. Flag 2 is set on entry when TeamSpeak is going to send the block, and setting it yourself has the block sent anyway. With music playing the plugin sets both, and keeps the microphone samples only in blocks TeamSpeak would have sent, so background noise between your sentences isn't sent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The voice activation surprise.&lt;/strong&gt; In a live test, nobody heard the music until I spoke. With voice activation, TeamSpeak keeps a transmission going on the plugin's send flag, but never &lt;em&gt;starts&lt;/em&gt; one while it hears silence.&lt;/p&gt;

&lt;p&gt;The fix: when music should go out and my own &lt;code&gt;CLIENT_FLAG_TALKING&lt;/code&gt; is 0, the plugin briefly turns voice activation off with &lt;code&gt;setPreProcessorConfigValue(sch, "vad", "false")&lt;/code&gt;, with the microphone samples silenced, and turns it back on once TeamSpeak is sending. It's rate-limited, always undone (pause, stop, tab change, unload) and never touches the user's capture profile.&lt;/p&gt;

&lt;p&gt;Push-to-talk is a documented limit: the plugin can't see the key, so with push-to-talk you can't talk over the music; the channel hears only the music.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Only the tab in front.&lt;/strong&gt; TeamSpeak sends voice only from the server tab in front, so the music pauses while its tab is in the background. The plugin API can't bring a tab to the front, so &lt;strong&gt;Play&lt;/strong&gt; in the compact music bar finds TeamSpeak's &lt;code&gt;QTabBar&lt;/code&gt; and selects the right tab.&lt;/p&gt;
&lt;h2&gt;
  
  
  Video with Media Foundation, emoji with DirectWrite
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Video&lt;/strong&gt; plays through &lt;code&gt;IMFMediaEngine&lt;/code&gt; in frame-server mode on D3D11 (WARP as a fallback), with frames handed over as &lt;code&gt;QImage&lt;/code&gt;s. &lt;code&gt;mfplat.dll&lt;/code&gt; and &lt;code&gt;mfreadwrite.dll&lt;/code&gt; are delay-loaded and checked for, so the plugin still loads on Windows N editions without the Media Feature Pack, just without video, audio playback, voice messages and video compression. Videos over 25 MB are transcoded to H.264/AAC before upload, and the result is checked before it's sent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HD emoji&lt;/strong&gt; are drawn with DirectWrite and Direct2D (loaded at run time; Segoe UI Emoji with &lt;code&gt;D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT&lt;/code&gt;) on one low-priority worker thread: about 3 ms per new emoji on Windows 11, then cached. The chat document changes in small steps, so a wall of emoji can't stall TeamSpeak.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsaofcbk09fvj5021sx6w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsaofcbk09fvj5021sx6w.png" alt="HD emoji in TeamSpeak's chat, reply lines above replies, and the emoji picker open above TeamSpeak's own emoji button" width="800" height="509"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Updating from GitHub without signing keys
&lt;/h2&gt;

&lt;p&gt;The official addon site isn't an option for new TeamSpeak 3 addons, so releases live on GitHub. I wanted updates without a key to guard: publishing a release should be enough. In official builds, once the user agrees:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The check:&lt;/strong&gt; WinHTTP (loaded only while it runs), HTTPS only, the repository name compiled into the DLL, at most 5 redirects, each only to &lt;code&gt;github.com&lt;/code&gt; or &lt;code&gt;*.githubusercontent.com&lt;/code&gt;. The plugin builds every URL itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The manifest:&lt;/strong&gt; an unsigned &lt;code&gt;tsmedia-update-v2.json&lt;/code&gt; on the latest release lists each file's size and SHA-256. Every download must match, its PE machine must match (x64 or x86), and all of the new DLL's imports must resolve in the running TeamSpeak. Only strictly newer versions count. The trade-off: whoever controls the GitHub repository can ship an update to everyone who turned updates on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The install:&lt;/strong&gt; a loaded DLL can't be overwritten or deleted, but on NTFS it &lt;em&gt;can&lt;/em&gt; be renamed. TeamSpeak only loads &lt;code&gt;*_win64.dll&lt;/code&gt; (&lt;code&gt;*_win32.dll&lt;/code&gt; on 32-bit) from the top of &lt;code&gt;plugins/&lt;/code&gt;, never subfolders, so staging and rollback copies are safe there:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// simplified: the running plugin swaps itself&lt;/span&gt;
&lt;span class="n"&gt;MoveFileExW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;L"plugins&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;tsmedia_win64.dll"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;L"plugins&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;tsmedia&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;update&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;rollback&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;old&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;tsmedia_win64.dll.bak"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;MOVEFILE_WRITE_THROUGH&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;MOVEFILE_REPLACE_EXISTING&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;MoveFileExW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;L"plugins&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;tsmedia&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;update&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;staging&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;new&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;tsmedia_win64.dll.new"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;L"plugins&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;tsmedia_win64.dll"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MOVEFILE_WRITE_THROUGH&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The safety net:&lt;/strong&gt; a boot counter at the top of &lt;code&gt;ts3plugin_init&lt;/code&gt;. If a new version never finished starting, the next start restores the previous DLL and returns 1, so TeamSpeak unloads the plugin and keeps working.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The bridge:&lt;/strong&gt; 2.2.0 only trusts a signed manifest (ECDSA P-256), so every release still carries the one signed manifest of 2.2.1. 2.2.0 users hop to 2.2.1, then to the latest; no key is needed again.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Testing a plugin for a closed-source app
&lt;/h2&gt;

&lt;p&gt;You can't unit test TeamSpeak, but most of your code doesn't have to need it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pure logic first.&lt;/strong&gt; Link parsing, the protocol, the flood governor (with an injected clock), ballots, the mixing math and the update manifest have no TeamSpeak in them and run in one Qt Test executable. The upload engine runs against a fake TeamSpeak function table: the test writes what "arrives", then calls the engine as TeamSpeak would.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always &lt;code&gt;-platform offscreen&lt;/code&gt;.&lt;/strong&gt; Test windows that took the focus once swallowed my keystrokes. Set &lt;code&gt;QT_QPA_FONTDIR&lt;/code&gt;, or the offscreen platform draws text as boxes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A driver inside TeamSpeak.&lt;/strong&gt; Test builds (never released) run a script of JSON commands inside TeamSpeak against a local test server, and only while the current tab is connected to &lt;code&gt;127.0.0.1&lt;/code&gt;, &lt;code&gt;localhost&lt;/code&gt; or &lt;code&gt;::1&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"send"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"files"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"cat.png"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="nl"&gt;"caption"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"hello"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"waitidle"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;20000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"snap"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"after-send"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"quit"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never the real microphone.&lt;/strong&gt; In one live test, a click on the mic button sent 2.5 seconds of real audio to the local server. Since then, test builds record generated sound, and live tests mute TeamSpeak's own capture and play nothing audible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quit on purpose.&lt;/strong&gt; Unload tests quit within seconds of an upload, to catch the timer bug above, then count crash dumps.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;If you write a plugin for someone else's app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Measure the host before you design. My message limit was off by a factor of 8.&lt;/li&gt;
&lt;li&gt;Own every timer, string and widget you hand to the host, and make every change reversible.&lt;/li&gt;
&lt;li&gt;Treat whatever other clients send as hostile, and keep identity out of payloads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;TS Media chat is free &lt;a href="https://github.com/Metihttp/Teamspeak_Media_chat" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt; (v2.3.1 at the time of writing). From the &lt;a href="https://github.com/Metihttp/Teamspeak_Media_chat/releases/latest" rel="noopener noreferrer"&gt;latest release&lt;/a&gt; you only need the &lt;code&gt;.ts3_plugin&lt;/code&gt; file: double-click it with TeamSpeak closed (TeamSpeak 3.6.x, Windows 10 and 11). The &lt;a href="https://github.com/Metihttp/Teamspeak_Media_chat/blob/main/docs/ARCHITECTURE.md" rel="noopener noreferrer"&gt;architecture notes&lt;/a&gt; go much deeper.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Metihttp" rel="noopener noreferrer"&gt;
        Metihttp
      &lt;/a&gt; / &lt;a href="https://github.com/Metihttp/Teamspeak_Media_chat" rel="noopener noreferrer"&gt;
        Teamspeak_Media_chat
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      TeamSpeak 3 plugin: Discord-style inline images, GIFs, videos and file cards. Files stay on your own TeamSpeak server.
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;If your community still runs TeamSpeak 3, give it a try. If this post saved you a crash or two, a star on the repo helps other TeamSpeak users find it. Bugs go to &lt;a href="https://github.com/Metihttp/Teamspeak_Media_chat/issues" rel="noopener noreferrer"&gt;GitHub issues&lt;/a&gt;; questions and ideas are welcome in the comments or on Telegram, &lt;a href="https://t.me/metii" rel="noopener noreferrer"&gt;@metii&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What would you build on TeamSpeak's three pipes: voice, file transfer and plugin commands?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I wrote this post with help from an AI assistant and checked the technical details against the project's code.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cpp</category>
      <category>qt</category>
      <category>showdev</category>
      <category>windows</category>
    </item>
  </channel>
</rss>
