Cast anything to any TV from the command line
Project description
qast
qast casts anything to any TV from the command line.
qast video.mov # Cast local file
qast "https://dropbox.com/abc123/video.mp4" # Cast video located somewhere on web
qast "https://youtube.com/watch?v=..." # Cast YouTube video
qast screen # Cast your computer desktop
qast window # Cast a window on your desktop (select via mouse click)
qast "browser:https://grafana.example.com" # Cast a webpage (via headless Chromium)
qast webcam # Cast your webcam
cat stream.ts | qast - # Cast generic piped data
qast url1 url2 url3 --repeat # Cast varied content, queued, and looped
Contents
- The problem
- The solution
- Install
- Quick start
- What can you cast?
- Queue mode
- Supported devices
- CLI reference
- Python API
- Use cases
- FAQ
- How it works
- YouTube notes
- Upcoming features
- License
- How did this come about?
- Related projects
- See also
The problem
Almost every TV made in the last decade can receive cast streams. But what they'll accept varies:
- Chromecast handles YouTube natively, but won't take an arbitrary URL
- Most DLNA TVs play MP4 files but won't play MKV or WebM
- Roku has varied mechanisms for streaming depending on version/vendor
- Screen mirroring exists on some platforms, not others
In other words, TV's streaming features differ. Straightforward "play this stream" reveals inconsistencies — content that plays fine on a Samsung may fail on an LG. Codec mismatches (VP9, H.265, DivX/Xvid), uncommon containers (MKV, WebM, FLV, AVI, OGG), and unsupported audio formats (FLAC, Opus, DTS) are common causes of "format not supported" errors. Even when a TV claims to support a format, it may only handle specific codec profiles or resolutions.
The solution
qast sidesteps the compatibility problem entirely. Practically all TVs accept either MPEG transport stream or fragmented MP4, so qast transcodes everything — URLs, files, screen captures, windows, webcams, piped data — into a single H.264/AAC stream. Input can be anything ffmpeg understands, which is practically every media format in existence. Because everything is transcoded to a common format, qast can play varied content (different sources, formats, and resolutions) back-to-back seamlessly. The TV sees one continuous stream with consistent format, resolution and bitrate throughout — content is added dynamically to a continuously-running mux, so there are no gaps or format switches between items. qast basically creates your own TV station from the command line.
Install
pip install qast[all] # recommended: includes yt-dlp, Chromecast, and browser capture
pip install qast # core only: local files, screen/webcam/window capture, piped data
Requirements
- Python 3.8+
- ffmpeg — transcoding and capture
- yt-dlp — for YouTube and 1000+ sites (strongly recommended)
# Ubuntu/Debian
sudo apt install ffmpeg
pip install yt-dlp # or: included in qast[all]
Optional extras
| Extra | What it adds |
|---|---|
qast[ytdlp] |
yt-dlp — YouTube and 1000+ sites |
qast[chromecast] |
pychromecast — Chromecast/Google TV support |
qast[browser] |
Playwright — browser: capture (also run playwright install chromium) |
qast[all] |
All of the above |
Optional system packages:
- xdotool — for
windowsource on Linux (apt install xdotool)
Quick start
# Cast a YouTube video
qast "https://youtube.com/watch?v=dQw4w9WgXcQ"
# Cast a local file
qast video.mov
# Cast your screen
qast screen
# Pick a device by name
qast -d "Samsung" video.mp4
What can you cast?
Anything on the internet
qast "https://youtube.com/watch?v=..."
qast "https://vimeo.com/..."
qast "https://twitch.tv/..."
YouTube, Vimeo, Twitch, TikTok, Twitter/X, Dropbox, Google Drive, PBS, BBC, and 1000+ sites via yt-dlp.
Any file on your computer
qast video.mp4
qast ~/Videos/*.mp4
MP4, MKV, AVI, WebM, FLV, OGG, WMV — anything ffmpeg can read.
Your screen/desktop
qast screen # primary monitor
qast screen --no-cursor # hide mouse cursor
qast screen@5m # capture for 5 minutes
Works if your TV doesn't support Miracast or AirPlay.
A single window
qast window # click to select
qast window:Grafana # by title
qast window:Grafana@1m # by title, 1 minute
A webpage
qast browser:https://grafana.example.com/dashboard # render any URL
qast browser:https://example.com@5m # stop after 5 minutes
Renders a URL in headless Chromium and casts the result to your TV. Good for dashboards, status pages, or any content that's best viewed as a live webpage rather than a video. Requires Playwright (pip install playwright && playwright install chromium).
Your webcam
qast webcam # default camera
qast webcam@2m # capture for 2 minutes
Live TV streams
HLS and IPTV streams work directly. Many international broadcasters stream free online but getting those streams onto your TV is a pain and sometimes requires a paid app. qast handles the HLS fetching and transcoding — you just give it the URL. See iptv-org for a directory of free streams. Note, many of these streams are geo-blocked. Also note, aspect ratio often needs to be tweaked, see --aspect arg.
# HLS streams (URLs are examples — check broadcaster sites for current links)
qast "https://tv-trtworld.medya.trt.com.tr/master.m3u8"
qast "https://cbsn-us.cbsnstream.cbsnews.com/out/v1/55a8648e8f134e82a470f83d562deeca/master.m3u8"
Piped data
cat stream.ts | qast -
ffmpeg -i input.avi -f mpegts - | qast -
The - tells qast to read from stdin. This makes qast composable with any tool that outputs video.
Audio visualizer from system audio:
ffmpeg -loglevel quiet -f pulse -i $(pactl get-default-sink).monitor \
-filter_complex "[0:a]showcqt=s=1920x1080:axis_h=0:bar_g=2:count=6[v]" \
-map "[v]" -f mpegts - | qast -
Displays your music as a real-time frequency-based waterfall graph on your TV. Note, ffmpeg has lots and lots of these kinds of visualizations. (But we're still waiting on --flight-simulator and --play-chess... C'mon ffmpeg!)
Security cam grid:
ffmpeg -loglevel quiet -i rtsp://cam1 -i rtsp://cam2 -i rtsp://cam3 -i rtsp://cam4 \
-filter_complex "[0:v][1:v]hstack[top];[2:v][3:v]hstack[bottom];[top][bottom]vstack" \
-f mpegts - | qast -
4 cameras on 1 TV, no NVR needed.
Test pattern:
ffmpeg -loglevel quiet -f lavfi -i "testsrc2=size=1920x1080:rate=30" -f mpegts - | qast -
Generative art from Python:
python my_visualizer.py | ffmpeg -f rawvideo -pix_fmt rgb24 -s 1920x1080 -r 30 -i - \
-f mpegts - | qast -
Queue mode
Pass multiple sources to play them back-to-back as one continuous stream:
qast \
"https://youtube.com/watch?v=morning-news" \
~/Videos/workout.mp4 \
"https://youtube.com/watch?v=lofi-beats"
Per-item duration
Append @duration to any source to limit how long it plays:
qast video.mp4@5m # play for 5 minutes
qast screen@30s video.mp4@1m webcam@20s # mixed sources with durations
qast "browser:https://grafana.example.com"@5m video.mp4 # browser capture then video
qast --duration 1m video1.mp4 video2.mp4@30s # global default + per-item override
The @duration can be a separate argument, useful for URLs containing @:
qast "https://user@host.com/video" @5m # separate — unambiguous
Source syntax:
screen[@duration]— screen capturewebcam[@duration]— webcam capturebrowser:<url>[@duration]— headless browser capturewindow:<title>[@duration]— window capture by title<url-or-file>[@duration]— URL or local file
Playlist files
Plain text, one source per line. Supports the same source syntax including @duration:
# morning.txt
https://youtube.com/watch?v=VIDEO1
https://youtube.com/watch?v=VIDEO2@10m
~/Videos/workout.mp4
screen@30s
browser:https://grafana.example.com@5m
qast --playlist morning.txt
qast --playlist morning.txt --repeat
Comments start with #. Blank lines are ignored.
# Play a YouTube playlist
yt-dlp --flat-playlist --print url "https://youtube.com/playlist?list=PLxyz" | qast --playlist -
# Random files from a directory
ls ~/Videos/*.mp4 | shuf | qast --playlist -
Supported devices
Chromecast — Chromecast, Chromecast with Google TV, Android TV
DLNA — Samsung, LG, Sony, and most smart TVs
Roku — Requires the free Media Assistant app, but conveniently, this app only needs to be installed -- it doesn't need to be selected and "running" for qast to stream and render to your Roku device/TV.
qast auto-discovers devices on your network. If multiple are found, it presents a menu:
$ qast video.mp4
Scanning for devices...
[0] Living Room TV (Chromecast)
[1] Bedroom Samsung (DLNA)
[2] Kitchen Roku (Roku)
Select device:
Or specify directly:
qast -d "Samsung" video.mp4 # by name (substring match)
qast -d 0 video.mp4 # by index
CLI reference
qast [OPTIONS] [SOURCE...]
Sources:
<file>[@duration] Local video file
<url>[@duration] YouTube, Vimeo, etc. (via yt-dlp)
screen[@duration] Capture primary screen
webcam[@duration] Capture default webcam
browser:<url>[@duration] Render a URL in headless Chromium and cast
window:<title>[@duration] Capture a window by title
- Read from stdin
@duration can be attached (video.mp4@5m) or separate (video.mp4 @5m).
Device:
-d, --device NAME|INDEX Select device by name (substring) or index
Queue:
--playlist FILE Load sources from a file (- for stdin)
--repeat Loop the queue indefinitely
--shuffle Shuffle queue order
--no-placeholder Disable "up next" placeholder screens
--preroll TIME Preroll video by the specified time. This is useful for some
TVs that either cut off the beginning of the first segment
or show wait icon because of insufficient buffering.
--placeholder-time TIME Specify amount of time to show placeholders (2s default)
--duration TIME Default duration for sources without @duration
(e.g., 30s, 5m, 1h, 5m30s)
Capture:
--no-cursor Hide mouse cursor in screen capture
Other:
--youtube-default Use YouTube's default muxed stream instead of DASH
(lower latency, may be lower quality)
--aspect Squish or stretch content. 1.0 default, >1.0 stretches,
<1.0 squishes
--cookies-from-browser B Extract cookies from browser (B=chrome, firefox, brave, edge,
or safari) — helps when YouTube blocks yt-dlp extraction
(uses your logged-in session)
--save-stream FILE Save the served stream to a file (fMP4 or TS, matching device
format)
-v, --verbose Debug logging
-h, --help Show help
During playback, you can type commands into a simple text prompt >:
<source[@duration]> add a source to the queue (URLs, screen, webcam, etc.)
s skip current item
r <N> remove item by index
? show queue status
q quit
Python API
Full API reference: api.md
qast can be given detailed instructions via custom Python code.
One-shot casting
For simple cases — cast something and block until it finishes:
from qast import discover, cast
# Discover devices on the network
devices = discover()
for i, d in enumerate(devices):
print(f" [{i}] {d.name} ({d.protocol})")
# Cast a file (blocks until done or Ctrl+C)
cast("video.mp4", device="Living Room TV")
# Select by index
cast("https://youtube.com/watch?v=...", device=0)
# Cast your screen
cast(screen=True, device="Samsung")
Queue-based playback
Build a queue, control playback, add and remove items on the fly:
from qast import Qast
q = Qast(device="Living Room TV")
q.add("https://youtube.com/watch?v=VIDEO1")
q.add("https://youtube.com/watch?v=VIDEO2")
q.add("~/Videos/workout.mp4", placeholder=False)
q.add_screen(duration=30)
q.add_window("Grafana", duration=60)
q.add_browser("https://grafana.example.com/dashboard", duration=60)
q.add_webcam(duration=120)
q.play() # starts casting (non-blocking)
q.add("another.mp4", duration=300) # add with 5-minute limit
q.remove(2) # remove item by index
q.skip() # skip to next item
q.stop() # stop and disconnect
s = q.status()
s.state # "playing" | "stopped" | "idle"
s.now_playing # "Never Gonna Give You Up"
s.duration # 212.0 (seconds, None for live)
s.position # 45.3 (seconds elapsed)
s.queue # ["workout.mp4", "another.mp4"]
Example: scheduled casting
from qast import Qast
import schedule, time
q = Qast(device="Office TV")
def morning():
q.stop()
q.add("https://youtube.com/watch?v=morning-news")
q.add("https://youtube.com/watch?v=lofi-beats")
q.play(repeat=True)
def afternoon():
q.stop()
q.add("screen") # cast screen/desktop, not sure why...
q.play()
schedule.every().day.at("08:00").do(morning)
schedule.every().day.at("13:00").do(afternoon)
while True:
schedule.run_pending()
time.sleep(60)
Use cases
- Screen share to any TV — works even if your TV doesn't support Miracast or AirPlay.
- Security cam grid — compose RTSP feeds with ffmpeg, pipe to TV.
- Social gathering — queue up varied sources from Youtube, Vimeo, Google Drive, Slideshare, and play on a loop.
- Movie marathon — e.g. queue up the LOTR trilogy.
- Curated kids content — queue up appropriate kid content -- YouTube Kids, PBS, etc.
- Digital signage — Show "live" data, sales figures, number of users, company news, promotions, etc.
- MagicMirror — cast your MagicMirror screen wherever.
- Etc — pipe frames from your custom video source -- art, AI generated content, etc.
FAQ
Why transcode everything?
Compatibility. TVs are picky about codecs, containers, and parameters. A Samsung might play your MKV; an LG might not. By normalizing to H.264 + AAC in MPEG-TS (or fragmented MP4 for Chromecast), qast hits the lowest common denominator that every TV accepts. It also enables seamless queue transitions — uniform codec parameters mean no discontinuities between sources. Modern PCs typically have hardware encode support, so CPU usage is kept reasonably low.
Can I seek within a video?
No. qast streams forward-only — it's designed for lean-back viewing. If you need seeking, consider using a casting app such as YouTube, which is supported on most TVs.
What about DRM content?
If yt-dlp can't extract it, qast can't play it. Netflix, Disney+, etc. use DRM that prevents this.
My TV isn't discovered. What do I do?
Make sure your TV and computer are on the same network/VLAN. Try qast -v to see discovery traffic. Some TVs need DLNA/casting enabled in settings. Roku requires "Control by mobile apps" to be enabled under Settings > System > Advanced.
Does Roku require anything extra?
Yes — install the free Media Assistant app from the Roku Channel Store.
My TV cuts off the beginning of the stream
Some DLNA TVs consume and discard the first chunk of data when they connect — probing the format before they start rendering. This means the first few seconds of your video get eaten, and it can also disrupt the audio/video sync that follows. The --preroll flag works around this by inserting a placeholder video (a title card) at the start of the stream. The TV chews through the placeholder instead of your content. Start with --preroll 5 and increase until you see the placeholder appear on screen — that means the TV is past its probe phase and your real content will play from the beginning. Some TVs need 30 seconds or more. Once you know how much preroll your TV needs, you can add the preroll amount to your qast calls.
qast --preroll 30 "https://youtube.com/watch?v=..."
Why am I seeing several seconds of latency?
Practically all TVs want to buffer a few seconds of data before starting to render frames, which leads to latencies. For live streams such as webcam or computer desktop where latency matters most, you might see up to a 10 second lag from when you move your mouse and when you see it on your TV (for example).
How it works
[source] → [yt-dlp resolve] → [ffmpeg transcode] → [TS rewriter] → [muxer] → [ring buffer] → [HTTP server] → [TV]
- Resolve — yt-dlp extracts direct video URLs from YouTube etc. Local files and pipes skip this step.
- Transcode — ffmpeg normalizes everything to H.264/AAC in MPEG-TS. This is the lowest common denominator that every TV accepts.
- Rewrite — A TS rewriter ensures PTS/DTS continuity across segment boundaries, so the TV sees one seamless stream even when sources change.
- Mux — A continuously-running muxer accepts rewritten TS segments and produces the output format. For DLNA and Roku, the rewritten MPEG-TS is used directly. For Chromecast, the master muxer remuxes to fragmented MP4.
- Buffer — An in-memory ring buffer decouples the muxer from the HTTP server, absorbing bitrate variations.
- Cast — Protocol-specific signaling (DLNA SOAP, Roku ECP, or Chromecast protobuf) tells the TV to stream from a local URL which points to qast's HTTP server.
- Serve — The TV connects and qast streams the buffer contents over HTTP.
See architecture.md for details.
YouTube notes
By default, qast asks yt-dlp for separate DASH video and audio streams when resolving YouTube URLs. DASH streams are higher quality — YouTube serves its best resolutions and bitrates this way, while muxed (combined) streams typically cap at 720p. The downside is that ffmpeg receives two HTTP inputs (one video, one audio), and there's a long-standing ffmpeg bug where multiple HTTP inputs can cause audio truncation or desync.
To work around this, qast downloads the audio stream to a small temp file (~1-2 MB) before handing it to ffmpeg. This way ffmpeg only has one HTTP input (video) and one local file (audio), which avoids the bug. The audio download is fast and the temp file is cleaned up automatically. If the download times out or fails, qast falls back to a single muxed stream automatically.
If you'd rather skip the DASH path entirely and use YouTube's default muxed stream (lower latency, simpler, but potentially lower resolution), use:
qast --youtube-default "https://youtube.com/watch?v=..."
Or via the Python API:
cast("https://youtube.com/watch?v=...", device=0, youtube_default=True)
YouTube blocking yt-dlp
YouTube periodically changes its player internals to break yt-dlp extraction. When this happens you'll see errors like "Sign in to confirm you're not a bot" or "Unable to extract" in yt-dlp's output. Two things help:
- Update yt-dlp — the yt-dlp maintainers typically push fixes within days. Run
pip install -U yt-dlp. - Use browser cookies — passing
--cookies-from-browser chrome(orfirefox,brave,edge,safari) lets yt-dlp use your logged-in YouTube session, which bypasses most bot detection. This is often the only fix until yt-dlp pushes an update.
qast --cookies-from-browser chrome "https://youtube.com/watch?v=..."
Note that cookie extraction reads from your browser's cookie store — it does not modify anything.
Non-YouTube sites
yt-dlp supports 1000+ sites. For most of these, qast receives a single muxed URL and no audio download is needed. The DASH splitting behavior is specific to YouTube (and a few other sites that use DASH). If yt-dlp fails entirely for a given URL, qast passes the raw URL directly to ffmpeg as a last resort — this works surprisingly often for direct video links.
Upcoming features
- Multi-device casting — cast the same stream to multiple TVs simultaneously (
qast -d "Living Room" -d "Kitchen" video.mp4) - Subtitles — burn subtitles into the video stream via ffmpeg
- Scripting — a simple script format for automated playback sequences with loops, durations, and mixed sources (
qast --script morning-tv.qast) - Audio with visualization — render audio only files with graphical visualization
- Overlay/watermark — add a visible overlay (aka watermark) to the video stream
- Windows support
- macOS support (screen capture to come later)
License
MIT
How did this come about?
Our office has TVs of various types. During the Winter Olympics I had mixed results casting live feeds from my browser — sometimes it would work, sometimes not, and some TVs were invisible to Chrome despite being capable of streaming. In the past our business has sought ways to display live numbers on TVs — user counts, sales figures, that sort of thing. We have Raspberry Pis, and that's a solution, but the pain factor is high.
Why can't I just "play this video" or "cast this window" to a given TV from the command line (and most importantly expect it to work)?
Looking into it more, I found that screen casting is often a paid service for businesses (Yodeck, Screenly, UPshow, many more). These solutions typically use Raspberry Pis coupled to a cloud backend. The technical hurdles are solved but it requires a paid subscription. Being a big ol nerd, it got me thinking -- could you make a streamer that's agnostic to both the video source/format and the TV type? And thus qast was born. I hope others find this tool useful.
qast is pronounced "cast". The q is for queue -- play a queue of varied content back-to-back. (And everyone knows replacing a c with q makes anything sound cooler.)
Related projects
qast leans heavily on existing projects.
- ffmpeg — transcoding, muxing, screen capture, window capture, audio visualization, placeholder video encoding
- yt-dlp — video extraction
- pychromecast — Chromecast protocol
- Playwright — headless Chromium (browser) capture
See also
- Mkchromecast — Chromecast CLI utility, casts audio and video files
- catt — Chromecast CLI utility, casts urls and web pages
- go2tv — DLNA casting (single files)
- MagicMirror — Configurable/programmable smart information display
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file qast-0.1.1.tar.gz.
File metadata
- Download URL: qast-0.1.1.tar.gz
- Upload date:
- Size: 67.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
172022715819fdadaf8f0efa39e8115e64b6368f77836376d45ca87c6ab4508a
|
|
| MD5 |
99d5ff8ec56c8dae1f8f8a27eb7ed141
|
|
| BLAKE2b-256 |
07466a781a7e12f72bf1fdb085539f141659273769f7f83e1f590a5e01fea8a3
|
File details
Details for the file qast-0.1.1-py3-none-any.whl.
File metadata
- Download URL: qast-0.1.1-py3-none-any.whl
- Upload date:
- Size: 69.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3c0d16e30330275f14267ad2e503d0ca4e92bccddf590abd9d0967da397b085a
|
|
| MD5 |
63a90ed39b58b131bc21506ee00c011a
|
|
| BLAKE2b-256 |
efb41c141d3fabde529ffed6adecb3f40613d2320d57c99243d77a1f9d4a049a
|