canToggleGroup compared a group's needed member add/remove power with our own
i_group_needed_member_*_power, which the server never reports for us: our
power is i_group_member_add_power and i_group_member_remove_power. An admin
was only ever offered groups that need no power at all.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Move to channel…" closes the sheet and marks the client, and the next
channel tapped is where they go; a bar above the tree names them and cancels.
"Move to own channel" shows while they are elsewhere.
Both show only with some i_client_move_power. The server's needed
permissions never carry it, even for an admin, so core asks for it with
permget on connect and whenever our own groups change; a refusal (guests may
not ask) counts as none. Whether it beats the client's needed move power only
the server knows, and it refuses when not.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Core's Details says what the info box tells, as TeamSpeak's info frame does:
labelled facts, then a BBCode description. The desktop info panel now lays it
out instead of building its own, and selecting the server row shows the
server: address, version, clients, channels, creation date and its welcome
and host messages, from the virtualserver_* properties kept since initserver.
On Android the channel and client sheets show the same details, asking the
server for a missing channel description or a client's variables; tapping
the server row opens a server sheet. Descriptions render as TeamSpeak's do:
BBCode now knows [hr], [left]/[right]/[justify], numbered and lettered lists
and tables, which Android's HTML parser approximates — rules as dividers,
table rows as lines, list items numbered, font sizes as big/small. Links are
blue whatever the wallpaper.
Push-to-talk in the bottom bar follows whichever server holds the microphone,
and a server without it keeps its button to take it in a fixed slot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The server name in the top bar opens the servers the app is on, "Connect…" for
another one and "Disconnect from current". Only one server captures with the
microphone at a time, handed over by core's Sessions as on the desktop; a
server without it shows a button to bring it there. Push-to-talk, the floating
button and the level meter follow the server holding it.
Each server has its own controller (session, tree, chats, network recovery);
the app-wide one keeps what they share. Private message notifications carry
their server, as client ids repeat across servers.
The layout follows TeamSpeak's own: 26 dp tree rows with smaller icons and
text, a lower top bar, and no tabs — the chat is a button in the bottom bar,
whose buttons keep fixed places around the push-to-talk button's space.
Holding the mute button switches between push-to-talk and voice activation.
A server that drops the connection now lets go of the microphone too, so a
reconnect cannot capture alongside the server that took it over meanwhile.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A contact on two servers at once was marked seen by each in turn: a sighting
announced a contact change, every connection re-applied its contacts, and
each re-application recorded the contact on its own server — another change,
round and round, rewriting contacts.txt every time. Re-applying now only marks
a contact that was never seen; the last sighting moves when the client appears.
Sightings no longer write the file on their own either: they reach it in one
save half a minute later, or with the next edit's save, and are flushed when
the client closes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings -> Identities lists them and creates, imports (an identity file or a
TeamSpeak 3 settings.db), exports, renames and removes them, picks the
default and improves an identity's security level; the search runs on until
reached or stopped, whichever page is on screen. The Connect screen and each
bookmark choose the identity to connect with.
An identity keeps a nickname and phonetic nickname, in TeamSpeak's own INI
keys, on both clients. A connect goes by the bookmark's nickname, else the
identity's, else the global one; the phonetic one is sent in clientinit.
Imported bookmarks no longer copy their identity's nickname.
IdentityStore is now safe to share with the thread a connect resolves its
identity on, and imports and exports through streams for Android's document
picker.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The build encodes TeamSpeak's packs to Ogg Opus at 64 kbps before bundling
them, which takes the sounds from 21.3 MB to 2.0 MB, in the APK and again
once unpacked on the phone. The encoding runs the desktop code's libopus
on the build machine, through a build-only :desktop module compiled from
the same sources as the Maven one.
Both clients play Opus sounds: the sound player (now PackSoundPlayer, as it
no longer plays only wave files) decodes Ogg Opus with the platform's
libopus, and a pack's play("x.wav") finds x.opus, so converted packs keep
their scripts. SoundPackConverter turns any pack folder into Opus.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While none of the client's windows has the focus, a poke or a private
message shows a desktop notification, which the desktop's notification
centre keeps. A private conversation keeps one notification, updated as
messages come; clicking it brings the window forward with that chat open,
and they all go once a window of the client is in front again.
On Linux they go to the freedesktop.org notification service, spoken to
over the session bus by a small D-Bus client of our own on Java's Unix
sockets, so no library or native code is involved; Plasma also takes a
reply typed into the notification. Elsewhere they are AWT tray messages,
which Windows and macOS put in their notification centres. The options
can turn them off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While the app is out of sight, a poke or a private message posts a
notification; a message's one gathers the conversation, takes a reply
inline and opens its chat. The notifications are silent, as the sound
pack already plays TeamSpeak's own sound for either.
The build bundles the sound packs of the TeamSpeak 3 client found next
to the checkout (or at ts3.client.dir / TS3_CLIENT_DIR), unpacked into
the profile once per app version. Sound Settings picks the pack and its
volume, and speech packs talk through the phone's text-to-speech.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Docking waits for the panel to adopt the icon, which held up the main
window; wait at most half a second for it before falling back to AWT.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each contact shows just the date it was last seen, without the client
dot and at half the usual nesting; the full time and server move under
the phonetic nickname as "Last seen … on …".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The contacts window drops its tabs for one column of short groups beside
the list: contact type, display and ignore, where whisper becomes a
"Deny whisper" box, since allowing is the default. The names get their
labels on a line of their own, and the window is never shorter than the
options, leaving the list the only thing that scrolls.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rest of default_colored_2014's keys get Material counterparts:
talk power, whisper, recording, text formatting, identities, add-ons,
permissions and the like. Ping levels are drawn as signal bars, since
Material's own tell their levels apart by a hairline, and the default
group icons as lettered badges.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Android app's Material icons become a TeamSpeak-format pack shipped in
core, which the desktop offers next to the TeamSpeak packs. Android now
draws its icons from packs too and imports TeamSpeak's own zips, falling
back to the Material pack for what they lack.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The myTeamSpeak client protocol and its end-to-end encryption are
reverse-engineered (docs/myteamspeak): an HTTP POST of a framed protobuf,
a PBKDF2-HMAC-SHA512 login token and account key, an AES-GCM wrapped
item key and AES-CTR items with a SHA-512 trailer.
Options on desktop and Settings on Android gain a myTeamSpeak page: sign
in and out, and the account's bookmarks and identities, each picked for
import and marked when already here. The client stays signed in the way
the official one does, keeping the derived token and key rather than the
password; each read signs in afresh, and a sign-in the server no longer
takes signs out. Nothing is written to the account.
Items are the same Item_Data as the local TeamSpeak store, so they go
through the existing decoder and importer; a bookmark chosen alone brings
the identity it connects with, found by UUID or, as some name it, by
the identity's name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AGC2 won't amplify the background noise above -50 dBFS, which also caps
the gain for a voice barely above that noise: a distant talker ended up
5 dB quieter than a near one with noise suppression on, 12 dB with it
off. A "Boost quiet speech" setting (0-20 dB, default 0) raises the cap,
live, on desktop and Android.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Disconnecting only acted once connected, so a connect that was stuck or
retrying could not be called off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new input device used to wait for the next connect, and the shared mixer
kept its line on the old output until it went idle for good. The microphone
now reopens on the new device as the settings are applied, and the mixer
moves its line over when the output device changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A failed connect keeps its reason in the status instead of a bare
"Disconnected", as does a disconnect the server gave a reason for.
- The server never announces the subscription to the channel we start
out in, although it keeps us subscribed after we leave it, so the
channel we enter counts as subscribed.
- A client connecting or leaving the server is logged as that, not as
switching from or to "channel #0": the server gives both the same
reason, only with no channel on one side.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Android client needs them too: which state a client's icon shows,
how spacer channels are named, and the HTML a chat line is written as.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Files.readString/writeString (API 36.1), Path.of, ByteBuffer.slice(int, int)
and Matcher.appendReplacement(StringBuilder) (API 34) threw NoSuchMethodError
on Android; connecting crashed in ChatLogStore. Each replacement keeps the
original desktop code in a comment above it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The architecture tree predated the session, chatlog, contacts, text,
file transfer and voice pipeline packages, and named a backend that no
longer exists. The feature list still described the first speech
detector and processing chain rather than the WebRTC ports, and knew
nothing of the jitter buffer; the limitations listed file transfers,
avatars and administration as missing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing in the desktop client needs more than the FFM API, final since
Java 22. It builds, passes its tests and runs on OpenJDK 25, the current
LTS, which reaches more distributions. CI now tests on that minimum.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- the tree's sibling chain: arrival order, broken links, removal,
reordering and reparenting, orphans, client order, path lookup
- the event handler, fed events parsed from the server's own wording:
joins, moves, leaves, partial and clearing updates, server groups,
and a channel's life from creation to deletion
- settings: saving and loading back, unknown values, and the one-time
conversion of the old dBFS voice threshold
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Which server captures with the microphone, handing it over, push-to-talk
and key presses reaching the right microphones, and away state and
nickname applied everywhere were all worked out in MainFrame. They move
to a Sessions class in core, next to ServerSession, where another
frontend can use them; the window keeps only the tabs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every tree rebuild, which any join, leave or talk-power change causes,
expanded every row again, so a collapsed channel reopened within
seconds. The tree now remembers which channels were collapsed and
opens everything else; reopening one restores what is below it. While
the find bar filters, everything opens so every match shows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The data socket had no read timeout, so a file server that went quiet
left the transfer, and its thread, waiting until the user cancelled.
Thirty seconds without data now fails it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings, bookmarks (server passwords) and identities (private keys)
were written in place with the default permissions, readable by every
user on the machine, and a crash mid-write left a truncated file that
the next start silently replaced with defaults.
Every profile file is now written to a private temporary file, synced
and moved over the old one in one step. The profile folder itself is
made accessible to its owner only, which also covers files written
before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chat logs always went into the TeamSpeak 3 client's own folder. A new
Chat page in Options can turn them off, or point them at another
folder; left empty, they are still shared with the TeamSpeak 3 client.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
appendMessage and appendPrivateMessage built the same entry twice; they
now differ only in which log they pick.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chat logs grow forever, and every connect and every private chat opened
read the whole log to keep its last 1000 entries. History now reads the
last 4 MiB, which holds far more than that; searching still reads the
whole log.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The text log has a message's line breaks in it, but history was read one
line at a time, so every line after the first failed to parse and was
dropped. A line that starts no entry of its own now continues the
message before it, which also recovers what earlier versions wrote.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
stop() closed the encoder and cleared the transmit state while the
capture loop could still be in its last frame, which could turn the
transmit indicator back on, or overlap the loop of a quick restart on
the same speech detector and processing chain. It now closes the line
to wake the loop and waits for it to finish before tearing down.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both desktop backends dropped writes to a line whose device had gone
away, and the per-speaker playout ignored any failure, so that speaker
stayed silent for the rest of the session. A write to a dead line now
throws; the playout drops that line and the speaker's next packet
opens a fresh one. Sound effects and the microphone test's loopback
reopen their line the same way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every client that ever spoke kept an open playback line and a thread
until it left the server. A speaker silent for 30 s now has both
released, and its next talk burst opens them again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each speaker's stream held a fixed 60 ms and gave up after 200 ms of
concealment, so a jittery connection concealed constantly and a stall
ended the burst. It now runs through a port of the libspeex jitter
buffer the official client links, configured and driven as it does:
- the delay adapts to the measured jitter, aiming for at most 1% of
packets arriving late
- a speaker keeps one timeline, never re-anchored between talk bursts,
so what the buffer learned carries over; only 5 s without packets
drops it
- the talker's stop marker never enters the buffer; playback ends when
it reaches it, and its slot is played as silence, not concealed
A pull now always hands out one 20 ms frame: what a longer packet or a
delay increase produces beyond that is played on the following pulls,
which Android's mixer used to cut off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Client and channel updates were only applied when the field had a
value, since ts3j reads a missing field back as an empty string. A
removed avatar, an emptied description or a cleared away message
arrives as the field without a value, so they stayed as they were. Such
fields are now applied whenever the update carries them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
selfPermissionValue scanned every permission the server lists, several
times per context menu. The model now keeps a name index next to the
id one. Also drops primaryServerGroupName, which nothing calls.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
buildTree() cleared and refilled the children and client lists inside
the shared channel nodes, so a UI still walking the previous tree could
see them change under it. It now builds new lists and publishes them
unmodifiable. The node fields, written by the event and audio threads
and read by the UI, are volatile.
Whether a channel is full is now counted from the model, rather than
from a tree that may not have been rebuilt since the last join.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each hand-over ran on a fresh thread, so two quick tab switches could
start the new tab's capture before the old one stopped, or stop it after
it started. They now run one after another on a single thread.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hotkeys.all() returned its synchronised list, and every reader had to
lock it while iterating: a lock owned by one class and taken by four
others. It now returns a copy, and the lock stays inside Hotkeys.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The id came from initserver, which never carries it, then from
serverinfo and servergetvariables, which a guest may not use; chat logs
only worked with admin rights. ts3j now derives it from the server's
handshake key, as the official client does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sound for a refused action was picked by looking for "permission"
or "insufficient" in the message, which also matched errors such as an
invalid permission id. The server's error ids for insufficient client,
group and permission power say it exactly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A connect on an instance that was still connected, or still
connecting, replaced the socket and audio pipelines without closing
them. The fields shared between the connect, event, action and UI
threads are now volatile, and read once where they are used.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each action started a thread that read the socket field once it ran; a
disconnect in between cleared it, and the user saw "Could not join
channel: NullPointerException". Actions now capture the socket when
they are issued and run on a pool that lives as long as the connection.
What fails because the connection went away is no longer reported as an
error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>