yt-dlp/yt-dlp · error · ExtractorError

Stream is offline

Error message

Stream is offline

What it means

PicartoIE (yt_dlp/extractor/picarto.py:53) extracts LIVE streams from picarto.tv channels. It queries the Picarto GraphQL API at https://ptvintern.picarto.tv/ptvapi for channel metadata, and if the returned channel.online field equals 0 the channel is not broadcasting, so extraction stops immediately with this expected error. There is no live feed to download in this state; this code path never returns formats for an offline channel.

Source

Thrown at yt_dlp/extractor/picarto.py:53

        data = self._download_json(
            'https://ptvintern.picarto.tv/ptvapi', channel_id, query={
                'query': '''{
  channel(name: "%s") {
    adult
    id
    online
    stream_name
    title
  }
  getLoadBalancerUrl(channel_name: "%s") {
    url
  }
}''' % (channel_id, channel_id),  # noqa: UP031
            }, headers={'Accept': '*/*', 'Content-Type': 'application/json'})['data']
        metadata = data['channel']

        if metadata.get('online') == 0:
            raise ExtractorError('Stream is offline', expected=True)

        cdn_data = self._download_json(''.join((
            update_url(data['getLoadBalancerUrl']['url'], scheme='https'),
            '/stream/json_', metadata['stream_name'], '.js')),
            channel_id, 'Downloading load balancing info')

        formats = []
        for source in (cdn_data.get('source') or []):
            source_url = source.get('url')
            if not source_url:
                continue
            source_type = source.get('type')
            if source_type == 'html5/application/vnd.apple.mpegurl':
                formats.extend(self._extract_m3u8_formats(
                    source_url, channel_id, 'mp4', m3u8_id='hls', fatal=False))
            elif source_type == 'html5/video/mp4':
                formats.append({
                    'url': source_url,

View on GitHub (pinned to 81ecd58b13)

Solutions

  1. Open https://picarto.tv/<channel> in a browser and confirm the channel is actually live before downloading.
  2. If you want past broadcasts, use a VOD URL handled by PicartoVodIE (e.g. https://picarto.tv/<channel>/videos/<id> or https://picarto.tv/videopopout/<file>) instead of the live channel URL.
  3. If you automate, poll the same GraphQL field (channel(name: ...) { online }) yourself and only invoke yt-dlp when it is 1.
  4. Note yt-dlp's own test for this extractor is marked skip: 'Stream is offline' — hitting it is normal behaviour, not a bug.

Example fix

# before: live-channel extraction while the streamer is offline
yt-dlp https://picarto.tv/SomeChannel
# after: fetch a recording via the VOD extractor instead
yt-dlp https://picarto.tv/SomeChannel/videos/771008
Defensive patterns

Strategy: validation

Validate before calling

import requests

def picarto_is_live(channel_id):
    query = '{ channel(name: "%s") { online } }' % channel_id
    res = requests.get(
        'https://ptvintern.picarto.tv/ptvapi',
        params={'query': query},
        headers={'Accept': '*/*', 'Content-Type': 'application/json'},
        timeout=10).json()
    return res['data']['channel']['online'] == 1

# only hand the URL to yt-dlp when the channel is actually broadcasting
if picarto_is_live('SomeChannel'):
    ...  # run YoutubeDL here

Type guard

from yt_dlp.utils import ExtractorError

def is_stream_offline(e: BaseException) -> bool:
    """True when e is the expected Picarto 'Stream is offline' failure."""
    return isinstance(e, ExtractorError) and e.expected and 'Stream is offline' in str(e)

Try / catch

from yt_dlp.utils import ExtractorError

try:
    info = ydl.extract_info('https://picarto.tv/SomeChannel', download=True)
except ExtractorError as e:
    if e.expected and 'Stream is offline' in str(e):
        logger.info('channel not broadcasting; skipping')
    else:
        raise

Prevention

When it happens

Trigger: Running yt-dlp on a https://picarto.tv/<channel> URL (matched by PicartoIE, which defers to PicartoVodIE for /videos/ and /videopopout/ URLs) at a moment when the GraphQL response has channel.online == 0, i.e. the streamer is offline. Automated channel watchers typically hit this constantly.

Common situations: Monitoring scripts polling channels that are rarely live; following a scheduled-stream announcement page before the stream starts; a channel that was renamed so the slug points at a dead (offline) channel.

Related errors


AI-assisted analysis of yt-dlp/yt-dlp@81ecd58b13 (2026-08-22). Data as JSON: /api/errors/a4e327100c72f523. Report an issue: GitHub.