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- 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
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.
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.
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- 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]| 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)
# 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.mp4qast "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.
qast video.mp4
qast ~/Videos/*.mp4MP4, MKV, AVI, WebM, FLV, OGG, WMV — anything ffmpeg can read.
qast screen # primary monitor
qast screen --no-cursor # hide mouse cursor
qast screen@5m # capture for 5 minutesWorks if your TV doesn't support Miracast or AirPlay.
qast window # click to select
qast window:Grafana # by title
qast window:Grafana@1m # by title, 1 minuteqast browser:https://grafana.example.com/dashboard # render any URL
qast browser:https://example.com@5m # stop after 5 minutesRenders 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).
qast webcam # default camera
qast webcam@2m # capture for 2 minutesIf you have a security camera on your network, it mostly likely outputs RTSP. You can cast it to your TV:
qast "rtsp://192.168.1.100:554/stream"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"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 -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"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 overrideThe @duration can be a separate argument, useful for URLs containing @:
qast "https://[email protected]/video" @5m # separate — unambiguousSource 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
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 --repeatComments 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 -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 indexqast [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
Full API reference: api.md
qast can be given detailed instructions via custom Python code.
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")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"]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)- 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.
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).
[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.
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 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.
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.
- 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)
MIT
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 and a Raspberry Pi for each TV. 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? If so, you could curate your own "channel" of content and render it on practically any TV. Additionally, you could have a single machine casting customized streams to multiple TVs for free.
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.)
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
- 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