A lot of people keep videos in Telegram: lectures from a course group, clips from family chats, talks a friend forwarded, recordings from a club channel. Telegram's own apps are made for a phone or a desktop. On a television, the usual answer is casting from a phone, which ties up the phone and breaks the moment it goes to sleep.
TMPlayer is my attempt at the TV side of that. It is a free, open source (GPL-3.0-or-later) player for the videos already in your own Telegram chats. You sign in on the TV by scanning a QR code with your phone, pick a chat, and only its playable videos show up, as a library you browse with a remote. The same app runs on Android phones, Windows, macOS and Linux. It is an unofficial client and is not affiliated with Telegram.
This post is about how it is put together and the problems that took the most work.
TDLib does the Telegram part
Telegram publishes TDLib, its own client library, and TMPlayer uses it for everything account related: sign in, the chat list, message search and file downloads. I use it through tdl-coroutines, which ships prebuilt native libraries for every Android ABI and the desktop platforms, so TDLib is never compiled in the repository. That alone saves a build that would otherwise take longer than the rest of the project put together.
The app has no server of its own. The TV talks to Telegram directly, under the user's own account, and the app shows nothing the user's account could not already see. Telegram's own restrictions stay on: messages that Telegram marks as restricted are skipped.
Playing a file that has not finished downloading
The part of the code I am most careful with is small. On Android the player is Media3's ExoPlayer, and TMPlayer gives it a custom DataSource that maps ExoPlayer's reads onto TDLib's offset based partial downloads.
When playback starts, TDLib is asked for the bytes at the start of the file, and playback begins as soon as there is enough to decode, usually within seconds, while the rest keeps downloading. When you seek to the middle of a two hour file, the data source points TDLib at that byte instead of waiting for the gigabytes in between. The window arithmetic (what to request, what is already on disk, when to wait and when to give up) lives in one pure Kotlin class, and the awkward cases are pinned down by unit tests, because that is exactly where a streaming player either works or does not.
A 1 GB TV stick is the target
The reference device is a cheap Android TV stick with 1 GB of RAM and 8 GB of storage. That decision shaped a lot:
- The app keeps one video in its watch cache at a time instead of filling the disk.
- Memory is watched during playback, and heavier features (like seek preview thumbnails) turn themselves off on a low RAM device and say why in Settings.
- Audio that a cheap stick cannot decode in hardware (DTS, TrueHD, E-AC3) falls back to software decoders from NextLib's FFmpeg build, while the video stays on the hardware decoder.
Testing on an emulator with a 1 GB profile, not just on my own phone, is what kept this honest.
One codebase, phone and TV in the same APK
The Android app is one APK for both the phone and the TV. A television gets a single row of controls walked with the D-pad; a phone gets the same layout plus a top bar, a centre transport and gestures. The library screens are Compose, with a Material 3 layout on the phone and a TV layout built for focus and a remote.
Shared logic (the chat model, series grouping, resume points, watched state, settings) lives in a core module, and shared UI in a ui module that both Android and the desktop use.
The desktop app
The desktop version is Compose Multiplatform on the JVM. Playback there goes through mediamp, which wraps libmpv, so the desktop plays the same range of formats, subtitles and audio tracks. It ships as an MSI for Windows, an AppImage and a tarball for Linux, and a DMG for Apple Silicon Macs on macOS 26 (still a preview, not notarized yet).
Getting each desktop package right was its own small project: the Windows installer, a self updater per platform, the AppImage on window managers like sway, and a macOS build that took several attempts before it signed in and played properly.
Series, resume and the next episode
A Telegram chat is a list of messages, not a library. TMPlayer reads the file names and captions to group episodes into series and seasons, remembers where you stopped in each video, and starts the next episode on its own with a short countdown. Optional artwork and overviews can come from TMDB, TVmaze and AniList if the user turns that on. None of it is required to watch.
Casting, in the latest release
Version 1.27.1 was mostly about casting from the phone to a TV: subtitles now show on the TV, a soundtrack the TV cannot play is converted on the phone so it is still heard, and when the connection to the TV drops, the video waits with Reconnect and Play here instead of jumping back onto the phone.
Releases are a single workflow
One GitHub Actions workflow builds the Android APK, the Windows MSI, the macOS DMG and the Linux AppImage and tarball from a tag. The same run publishes an update feed that the installed apps read, regenerates the changelog pages on the website from the annotated tag message, posts the release to the project's Telegram channel and pings search engines through IndexNow. The tag message is the release note everywhere, so I write it once.
Where it is now
TMPlayer is at version 1.27.1, released roughly weekly, in 21 languages. It has no ads and no account of its own. It is launching on Product Hunt today.
- Site and downloads: https://tmplayer.org
- Source: https://github.com/dracu-lah/TMPlayer
- Legal note: https://tmplayer.org/legal/
If you have built something on TDLib, or fought ExoPlayer over partial files, I would like to hear how you handled it.
This post was drafted by an AI assistant from the project's own docs and code, and published on my behalf.
Top comments (0)