How to Find the Video URL of Twitch.tv Streams
extract video URLs to watch live streams through VLC for reduced lagging or to download entire past broadcastsTwitch.tv is a great source for all things esport. Unfortunately, they suffer from inexplicable video stuttering which can be extremely bothersome. One of the more helpful remedies is playing live streams with VLC. For past broadcasts an obvious solution is to download the video before watching. Both measures require the video url, which can be retrieved by tools like livestreamer and websites like Twitchtools.
Live Video
For the impatient, here’s how to list all Video urls for channel gamesdonequick
(only works if the channel is live of course):
user@users-desktop:~$ mkdir live && cd $_
user@users-desktop:~$ git clone https://gist.github.com/8593472.git .
user@users-desktop:~$ pip install m3u8
user@users-desktop:~$ python twitch_live_url.py gamesdonequick
Video URLs (sorted by quality):
3362 kbit/s (Source), resolution=(1280, 720)
---------------------------------------------
http://video15.ams01.hls.twitch.tv/hls25/gamesdonequick_12509293952_185809391/chunked/py-index-live.m3u8?token=id=1418093999473934365,bid=12509293952,exp=1420554598,node=video15-1.ams01.hls.justin.tv,nname=video15.ams01,fmt=chunked&sig=395f5905fb1a1d67f0c43ff16c87a992ff0d5a56
1718 kbit/s (High), resolution=?
---------------------------------
http://video15.ams01.hls.twitch.tv/hls25/gamesdonequick_12509293952_185809391/high/py-index-live.m3u8?token=id=1418093999473934365,bid=12509293952,exp=1420554598,node=video15-1.ams01.hls.justin.tv,nname=video15.ams01,fmt=high&sig=43fb62091c5177ed25fa1ee58c3880adc828a30c
906 kbit/s (Medium), resolution=?
----------------------------------
http://video15.ams01.hls.twitch.tv/hls25/gamesdonequick_12509293952_185809391/medium/py-index-live.m3u8?token=id=1418093999473934365,bid=12509293952,exp=1420554598,node=video15-1.ams01.hls.justin.tv,nname=video15.ams01,fmt=medium&sig=a771f42f0bc2a1743558a83f4a9c9679a0bfe29a
582 kbit/s (Low), resolution=?
-------------------------------
http://video15.ams01.hls.twitch.tv/hls25/gamesdonequick_12509293952_185809391/low/py-index-live.m3u8?token=id=1418093999473934365,bid=12509293952,exp=1420554598,node=video15-1.ams01.hls.justin.tv,nname=video15.ams01,fmt=low&sig=cc2e22fbd1a0a0d34a8f9b56162482fe974d3bcd
160 kbit/s (Mobile), resolution=?
----------------------------------
http://video15.ams01.hls.twitch.tv/hls25/gamesdonequick_12509293952_185809391/mobile/py-index-li
Open the live stream of channel <code>riotgames</code> in VLC:
The Python script performs three steps to get to the urls:
Step 1: Request a token
The API call to request a token for channel channel
is:
http://api.twitch.tv/api/channels/{channel}/access_token
The request should return a JSON formatted data with two fields:
- token: A JSON text with the following fields:
- user_id: should be
null
since we are making anonymous API calls - channel: should echo the requested channel name
- expires: a UNIX time stamp giving the expiry date of the token. Tokens seem to be valid 15 minutes. (You only need the token to get the stream url, once you have to url, you don’t have to send the token again for the entire duration of the live broadcast.)
- chansup: again a JSON text with two fields:
- view_until: Unix timestamp, usually set to December 31, 2030.
- restricted_bitrates: The token can probably be restricted to certain bitrates, but should be an empty array in general.
- private: a JSON text with just one field: allowed_to_view, should be
true
of course. - privileged: usually false, might be related to you subscription status.
- source_restricted: should be false, if true then the
source
quality might be unavailable
- user_id: should be
- sig: The 20 bytes hex-string representing the signature
- mobile_restricted: should be false, might restrict the stream to non mobile devices.
Here is an example for the stream gamesdonequick
: http://api.twitch.tv/api/channels/gamesdonequick/access_token
The response should look similar to this:
{
"token":
"{\"user_id\":null,
\"channel\":\"gamesdonequick\",
\"expires\":1420470744,
\"chansub\": {
\"view_until\":1924905600,
\"restricted_bitrates\":[]
},
\"private\": {
\"allowed_to_view\":true
},
\"privileged\":false,
\"source_restricted\":false
}",
"sig":"c176d91c9216b84fd3717c3fb4efcc755c46f3c6",
"mobile_restricted":false
}
Note that the value of token
is text that contains JSON formatted data, therefore all quotes are escaped.
Step 2: Request the Playlist
All video urls for a live streams are packed in a m3u playlist. You can request this file with the following API call:
http://usher.twitch.tv/api/channel/hls/{channel}.m3u8? →
player=twitchweb&&token={token}&sig={sig}& →
allow_audio_only=true&allow_source=true&type=any&p={random}'
where you need to fill in the following values:
{channel}
: the channel name, e.g.,gamesdonequick
{token}
: the value oftoken
from step 1, e.g.,{\"user_id\":null,...}
{sig}
: the value ofsig
from step 1, e.g.,c176d91c9216b8...
{random}
: a random integer, up to 6 digits?
For example,
http://usher.twitch.tv/api/channel/hls/gamesdonequick.m3u8? →
player=twitchweb&token={"user_id":null,"channel":"gamesdonequick","expires":1420472431, →
"chansub":{"view_until":1924905600,"restricted_bitrates":[]}, →
"private":{"allowed_to_view":true},"privileged":false,"source_restricted":false}& →
sig=19b3425a3cf937e6c15e35717c7e25baab083a70&allow_audio_only=true&allow_source=true&type=any&p=9333029
This API call should return a m3u file like this:
#EXTM3U
#EXT-X-TWITCH-INFO:NODE="video62.ams01",MANIFEST-NODE="video62.ams01",SERVER-TIME="1420471407.91",USER-IP="83.228.241.174",CLUSTER="ams01",MANIFEST-CLUSTER="ams01"
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="chunked",NAME="Source",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=3287062,RESOLUTION=1280x720,CODECS="avc1.4D4029,mp4a.40.2",VIDEO="chunked"
http://video62.ams01.hls.twitch.tv/hls25/gamesdonequick_12509293952_185809391/chunked/py-index-live.m3u8?token=id=6986430544396636668,bid=12509293952,exp=1420557807,node=video62-1.ams01.hls.justin.tv,nname=video62.ams01,fmt=chunked&sig=51f7f401342979dd9af3a1f366a339ab7dceb4f9
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="high",NAME="High",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1760000,CODECS="avc1.66.31,mp4a.40.2",VIDEO="high"
http://video62.ams01.hls.twitch.tv/hls25/gamesdonequick_12509293952_185809391/high/py-index-live.m3u8?token=id=6986430544396636668,bid=12509293952,exp=1420557807,node=video62-1.ams01.hls.justin.tv,nname=video62.ams01,fmt=high&sig=017bf5f8c9aace7e6a4f169e06188a086a5fd298
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="medium",NAME="Medium",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=928000,CODECS="avc1.66.31,mp4a.40.2",VIDEO="medium"
http://video62.ams01.hls.twitch.tv/hls25/gamesdonequick_12509293952_185809391/medium/py-index-live.m3u8?token=id=6986430544396636668,bid=12509293952,exp=1420557807,node=video62-1.ams01.hls.justin.tv,nname=video62.ams01,fmt=medium&sig=00f249f9c89938b747115f0f1f383900d06c58a7
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Low",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=596000,CODECS="avc1.66.31,mp4a.40.2",VIDEO="low"
http://video62.ams01.hls.twitch.tv/hls25/gamesdonequick_12509293952_185809391/low/py-index-live.m3u8?token=id=6986430544396636668,bid=12509293952,exp=1420557807,node=video62-1.ams01.hls.justin.tv,nname=video62.ams01,fmt=low&sig=f61c7b7821aa281974bbb5a1e5bb8bac4d7e3ab2
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mobile",NAME="Mobile",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=164000,CODECS="avc1.66.31,mp4a.40.2",VIDEO="mobile"
http://video62.ams01.hls.twitch.tv/hls25/gamesdonequick_12509293952_185809391/mobile/py-index-live.m3u8?token=id=6986430544396636668,bid=12509293952,exp=1420557807,node=video62-1.ams01.hls.justin.tv,nname=video62.ams01,fmt=mobile&sig=78597f44e32af0900f4526d4c31d9befffcbb798
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="audio_only",NAME="Audio Only",AUTOSELECT=NO,DEFAULT=NO
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=128000,CODECS="mp4a.40.2",VIDEO="audio_only"
http://video62.ams01.hls.twitch.tv/hls25/gamesdonequick_12509293952_185809391/audio_only/py-index-live.m3u8?token=id=6986430544396636668,bid=12509293952,exp=1420557807,node=video62-1.ams01.hls.justin.tv,nname=video62.ams01,fmt=audio_only&sig=53fac453e3a02c47bcd9132ff6991b8f78c229b4
Step 3: Parse the m3u file
M3U is simple text-based file format and easy to parse. For example, in Python you can use the package m3u8
:
m3u8_obj = m3u8.loads(m3u8_text)
for p in m3u8_obj.playlists:
print(p.stream_info.bandwidth, p.uri)
Past Broadcasts
Download the past broadcast at url http://www.twitch.tv/riotgames/b/577357806
user@users-desktop:~$ mkdir past && cd $_
user@users-desktop:~$ git clone https://gist.github.com/8340312.git .
user@users-desktop:~$ python twitch_past_broadcast_downloader.py 577357806
downloading 577357806_00.flv
146.43 MB
For past broadcasts we need to know the video id. You see the video id as the digits of the url of the broadcast. The broadcast http://www.twitch.tv/riotgames/b/577357806, for example, has the video id 577357806. You can plug this video id in the following API call
https://api.twitch.tv/api/videos/{videoid}
In our example this would be:
https://api.twitch.tv/api/videos/a577357806
The API should return actual JSON (no escaped text) this time that represents an array of objects, where each object corresponds to a 30 minute part of the broadcast (the last part obviously can be shorter). So in contrast to live streams the video file is split into 30 minute chunks with different urls. Here’s an example of the response (only the first two objects are shown in full):
{
"api_id": "a577357806",
"broadcaster_software": "fme",
"channel": "riotgames",
"chunks": {
"240p": [
{
"length": 1800,
"upkeep": null,
"url": "http://media-cdn.twitch.tv/store89.media66/archives/2014-10-12/format_240p_577357806.flv",
"vod_count_url": "http://countess.twitch.tv/ping.gif?u=%7B%22type%22:%22vod%22,%22id%22:5773578060%7D"
},
{
"length": 1802,
"upkeep": "pass",
"url": "http://media-cdn.twitch.tv/store63.media54/archives/2014-10-12/format_240p_577363096.flv",
"vod_count_url": "http://countess.twitch.tv/ping.gif?u=%7B%22type%22:%22vod%22,%22id%22:5773630960%7D"
}
...
"360p": [
...
"480p": [
...
"end_offset": 20900,
"increment_view_count_url": "http://countess.twitch.tv/ping.gif?u=%7B%22type%22:%22archive%22,%22id%22:577357806%7D",
"muted_segments": null,
"path": "/riotgames/b/577357806",
"play_offset": 0,
"preview": "http://static-cdn.jtvnw.net/jtv.thumbs/archive-577357806-320x240.jpg",
"preview_small": "http://static-cdn.jtvnw.net/jtv.thumbs/archive-577357806-150x113.jpg",
"restrictions": {},
"start_offset": 0,
"vod_ad_frequency": "1200",
"vod_ad_length": "30"
}
The objects should already be sorted, meaning the first 30 minutes of the video come first.
You can find a small script in Python on GitHub that performs the API calls to download a past broadcast. The script accepts video_ids of past broadcasts and downloads the individual parts to numbered files, where the filename is derived from the id of the broadcast:
python twitch_past_broadcast_downloader.py 577357806
downloading 577357806_00.flv
195.85 MB done
downloading 577357806_01.flv
137.84 MB
Archived Comments
Note: I removed the Disqus integration in an effort to cut down on bloat. The following comments were retrieved with the export functionality of Disqus. If you have comments, please reach out to me by Twitter or email.