jtvrtmp.py
· 3.6 KiB · Python
Raw
#!/usr/bin/env python2
# rtmpdump parameter generator for justin.tv/twitch.tv streams v6
# * Usage: jtvrtmp.py channelname [quality]
# where channelname is the channel name ([twitch/justin].tv/channelname)
# and quality is an optional parameter with a valid quality setting
# (360p, 480p, 720p, etc) If quality isn't present, it selects 'live'.
# * if quality is 'live', it picks the quality setting with a +, which
# is a restream of the video being sent to jtv, no transcoding involved.
#
# Changelog:
# v6: Added documentation!
# v5: Changed to default to 'live' quality instead of 360p, as some streams
# don't have 360p transcoding; made the code in main() slightly cleaner,
# possibly shorter.
# v4: Add shebang line
# v3: Removed unicode copyright symbol
# v2: Added exception catching for when a stream is offline or the specified
# quality setting is unavailable.
# v1: Initial release
#
# Copyright 2013 Steven Smith (blha303). All Rights Reserved.
# New BSD license
# http://www.opensource.org/licenses/BSD-3-Clause
from urllib2 import urlopen
import json
from sys import argv, exit, stderr
import traceback
def getswfaddr(channelname):
""" Follows the redirect to get the channel player url, strips out the useragent """
return urlopen("http://www.justin.tv/widgets/live_embed_player.swf?channel=" + channelname).geturl().split("&userAgent")[0]
def getstreaminfo(channelname):
""" returns a dict of {quality: data}. Example output in streaminfooutput.txt """
data = json.loads(urlopen("http://usher.justin.tv/find/%s.json?type=any" % channelname.lower()).read())
newd = {}
for i in data:
newd[i['display']] = i
return newd
def buildcmdline(channelname, data=None, quality="live"):
""" Builds and returns rtmpdump parameter string for loading a twitch/justin.tv channel.
Usage: buildcmdline(channelname as string, data dict from usher.justin.tv, formatted as {quality: data}
Check getstreaminfo() for how to create this dict, or let buildcmdline call it by not setting data. """
if not data:
data = getstreaminfo(channelname)
if quality == "live":
for i in data:
if "Source" in i:
quality = i
try:
data = data[quality]
except KeyError:
return '-q; echo "-------------"; echo "Couldn\'t find stream info for %s, maybe the stream is offline?"; echo "-------------" #' % quality
try:
if not "live-cdn" in data["connect"] and not "justintvlivefs" in data["connect"]:
justinlegacyparams = '-j "%s" ' % data["token"].replace('"', r'\"')
else:
justinlegacyparams = ""
except KeyError:
qualities = []
for i in data.keys():
stderr.write(str(i))
stderr.write(str(data))
if "connect" in data[i]:
qualities.append(i)
return '-q; echo "-------------"; echo "Couldn\'t find stream info for %s, maybe you need to be subscribed to watch that resolution? Try one of these: ' + ", ".join(qualities) + '"; echo "-------------" #' % quality
out = '-r "%s/%s" %s--swfVfy "%s" -v -o -' % (data["connect"], data["play"], justinlegacyparams, getswfaddr(channelname))
return out
def main(args):
""" Returns rtmpdump parameter string for channelname in specified list;
argv is a list, either [channelname] or [channelname, quality] """
if len(args) < 2:
return "Usage: %s channelname [quality]" % args[0]
elif len(args) > 2:
return buildcmdline(args[1], quality=args[2])
else:
return buildcmdline(args[1])
if __name__ == "__main__":
try:
out = main(argv)
print out
if "Usage:" in out:
exit(2)
except:
print '-q; echo "-------------"; echo "Error:"#'
traceback.print_exc()
| 1 | #!/usr/bin/env python2 |
| 2 | |
| 3 | # rtmpdump parameter generator for justin.tv/twitch.tv streams v6 |
| 4 | # * Usage: jtvrtmp.py channelname [quality] |
| 5 | # where channelname is the channel name ([twitch/justin].tv/channelname) |
| 6 | # and quality is an optional parameter with a valid quality setting |
| 7 | # (360p, 480p, 720p, etc) If quality isn't present, it selects 'live'. |
| 8 | # * if quality is 'live', it picks the quality setting with a +, which |
| 9 | # is a restream of the video being sent to jtv, no transcoding involved. |
| 10 | # |
| 11 | # Changelog: |
| 12 | # v6: Added documentation! |
| 13 | # v5: Changed to default to 'live' quality instead of 360p, as some streams |
| 14 | # don't have 360p transcoding; made the code in main() slightly cleaner, |
| 15 | # possibly shorter. |
| 16 | # v4: Add shebang line |
| 17 | # v3: Removed unicode copyright symbol |
| 18 | # v2: Added exception catching for when a stream is offline or the specified |
| 19 | # quality setting is unavailable. |
| 20 | # v1: Initial release |
| 21 | # |
| 22 | # Copyright 2013 Steven Smith (blha303). All Rights Reserved. |
| 23 | # New BSD license |
| 24 | # http://www.opensource.org/licenses/BSD-3-Clause |
| 25 | |
| 26 | from urllib2 import urlopen |
| 27 | import json |
| 28 | from sys import argv, exit, stderr |
| 29 | import traceback |
| 30 | |
| 31 | def getswfaddr(channelname): |
| 32 | """ Follows the redirect to get the channel player url, strips out the useragent """ |
| 33 | return urlopen("http://www.justin.tv/widgets/live_embed_player.swf?channel=" + channelname).geturl().split("&userAgent")[0] |
| 34 | |
| 35 | def getstreaminfo(channelname): |
| 36 | """ returns a dict of {quality: data}. Example output in streaminfooutput.txt """ |
| 37 | data = json.loads(urlopen("http://usher.justin.tv/find/%s.json?type=any" % channelname.lower()).read()) |
| 38 | newd = {} |
| 39 | for i in data: |
| 40 | newd[i['display']] = i |
| 41 | return newd |
| 42 | |
| 43 | def buildcmdline(channelname, data=None, quality="live"): |
| 44 | """ Builds and returns rtmpdump parameter string for loading a twitch/justin.tv channel. |
| 45 | Usage: buildcmdline(channelname as string, data dict from usher.justin.tv, formatted as {quality: data} |
| 46 | Check getstreaminfo() for how to create this dict, or let buildcmdline call it by not setting data. """ |
| 47 | if not data: |
| 48 | data = getstreaminfo(channelname) |
| 49 | if quality == "live": |
| 50 | for i in data: |
| 51 | if "Source" in i: |
| 52 | quality = i |
| 53 | try: |
| 54 | data = data[quality] |
| 55 | except KeyError: |
| 56 | return '-q; echo "-------------"; echo "Couldn\'t find stream info for %s, maybe the stream is offline?"; echo "-------------" #' % quality |
| 57 | try: |
| 58 | if not "live-cdn" in data["connect"] and not "justintvlivefs" in data["connect"]: |
| 59 | justinlegacyparams = '-j "%s" ' % data["token"].replace('"', r'\"') |
| 60 | else: |
| 61 | justinlegacyparams = "" |
| 62 | except KeyError: |
| 63 | qualities = [] |
| 64 | for i in data.keys(): |
| 65 | stderr.write(str(i)) |
| 66 | stderr.write(str(data)) |
| 67 | if "connect" in data[i]: |
| 68 | qualities.append(i) |
| 69 | return '-q; echo "-------------"; echo "Couldn\'t find stream info for %s, maybe you need to be subscribed to watch that resolution? Try one of these: ' + ", ".join(qualities) + '"; echo "-------------" #' % quality |
| 70 | out = '-r "%s/%s" %s--swfVfy "%s" -v -o -' % (data["connect"], data["play"], justinlegacyparams, getswfaddr(channelname)) |
| 71 | return out |
| 72 | |
| 73 | def main(args): |
| 74 | """ Returns rtmpdump parameter string for channelname in specified list; |
| 75 | argv is a list, either [channelname] or [channelname, quality] """ |
| 76 | if len(args) < 2: |
| 77 | return "Usage: %s channelname [quality]" % args[0] |
| 78 | elif len(args) > 2: |
| 79 | return buildcmdline(args[1], quality=args[2]) |
| 80 | else: |
| 81 | return buildcmdline(args[1]) |
| 82 | |
| 83 | if __name__ == "__main__": |
| 84 | try: |
| 85 | out = main(argv) |
| 86 | print out |
| 87 | if "Usage:" in out: |
| 88 | exit(2) |
| 89 | except: |
| 90 | print '-q; echo "-------------"; echo "Error:"#' |
| 91 | traceback.print_exc() |
jtvrtmp.sh
· 486 B · Bash
Raw
#!/bin/bash
# Quality setting in this file doesn't work at the moment. I'm open to suggestions.
# Copyright 2013 Steven Smith (blha303). All Rights Reserved.
# New BSD license
# http://www.opensource.org/licenses/BSD-3-Clause
die () {
echo >&2 "$@"
exit 1
}
(( $# == 1 )) || die "Usage: jtvrtmp.sh channelname [quality]"
(( $# == 2 )) || echo "rtmpdump $(python jtvrtmp.py $1) | vlc -" | sh; exit
(( $# == 3 )) || echo "rtmpdump $(python jtvrtmp.py $1 $2) | vlc -" | sh; exit
| 1 | #!/bin/bash |
| 2 | # Quality setting in this file doesn't work at the moment. I'm open to suggestions. |
| 3 | # Copyright 2013 Steven Smith (blha303). All Rights Reserved. |
| 4 | # New BSD license |
| 5 | # http://www.opensource.org/licenses/BSD-3-Clause |
| 6 | |
| 7 | die () { |
| 8 | echo >&2 "$@" |
| 9 | exit 1 |
| 10 | } |
| 11 | |
| 12 | (( $# == 1 )) || die "Usage: jtvrtmp.sh channelname [quality]" |
| 13 | (( $# == 2 )) || echo "rtmpdump $(python jtvrtmp.py $1) | vlc -" | sh; exit |
| 14 | (( $# == 3 )) || echo "rtmpdump $(python jtvrtmp.py $1 $2) | vlc -" | sh; exit |
| 15 |
streaminfooutput.txt
· 2.4 KiB · Text
Raw
# Output for python -c "import jtvrtmp; print jtvrtmp.getstreaminfo('Ludendi')" 2013-07-22
{u'360p':
{u'node': u'video8-2.lax01',
u'needed_info': u'',
u'play': u'jtv_SAz4Mud24V7aKSw0',
u'meta_game': u'The Legend of Zelda: Ocarina of Time',
u'cluster': u'lax01',
u'type': u'360p',
u'broadcast_part': 1,
u'persistent': u'true',
u'video_height': 360,
u'token': u'26b8cee84453fd5e370d0ba13108a1ef59c9e58b:{"swfDomains": ["justin.tv", "jtvx.com", "xarth.com","twitchtv.com", "twitch.tv","newjtv.com", "jtvnw.net", "wdtinc.com", "imapweather.com", "facebook.com", "starcrafting.com"], "streamName": "jtv_SAz4Mud24V7aKSw0", "expiration": 1374431364, "geo_ip": "124.169.218.124", "server": "video8-2.lax01"}',
u'connect': u'rtmp://199.9.254.164/app',
u'broadcast_id': 6230376768L,
u'bitrate': 512,
u'display': u'360p',
u'find_type': u'dist'
},
u'480p':
{u'node': u'video2-2.lax01',
u'needed_info': u'',
u'play': u'jtv_yv5WRBFg0ftTWUEO',
u'meta_game': u'The Legend of Zelda: Ocarina of Time',
u'cluster': u'lax01',
u'type': u'480p',
u'broadcast_part': 1,
u'persistent': u'true',
u'video_height': 480,
u'token': u'5e7a6465c72d71b455d0d78249ecf50e8cb64462:{"swfDomains": ["justin.tv", "jtvx.com", "xarth.com", "twitchtv.com", "twitch.tv", "newjtv.com", "jtvnw.net", "wdtinc.com", "imapweather.com", "facebook.com", "starcrafting.com"], "streamName": "jtv_yv5WRBFg0ftTWUEO", "expiration": 1374431364, "geo_ip": "124.169.218.124", "server": "video2-2.lax01"}',
u'connect': u'rtmp://199.9.254.158/app',
u'broadcast_id': 6230376752L,
u'bitrate': 768,
u'display': u'480p',
u'find_type': u'dist'
},
u'720p+':
{u'node': u'video6-1.lax01',
u'needed_info': u'',
u'play': u'jtv_fGEW0H_C_0Dfy2bI',
u'meta_game': u'The Legend of Zelda: Ocarina of Time',
u'cluster': u'lax01',
u'type': u'live',
u'broadcast_part': 6,
u'persistent': u'true',
u'video_height': 720,
u'token': u'88830ed1f5bab63eb512e01d45698106ecb500b1:{"swfDomains": ["justin.tv", "jtvx.com", "xarth.com", "twitchtv.com", "twitch.tv", "newjtv.com", "jtvnw.net", "wdtinc.com", "imapweather.com", "facebook.com", "starcrafting.com"], "streamName": "jtv_fGEW0H_C_0Dfy2bI", "expiration": 1374431364, "geo_ip": "124.169.218.124", "server": "video6-1.lax01"}',
u'connect': u'rtmp://199.9.254.138/app',
u'broadcast_id': 6230375632L,
u'bitrate': 723.296875,
u'display': u'720p+',
u'find_type': u'dist'
}
}
| 1 | # Output for python -c "import jtvrtmp; print jtvrtmp.getstreaminfo('Ludendi')" 2013-07-22 |
| 2 | {u'360p': |
| 3 | {u'node': u'video8-2.lax01', |
| 4 | u'needed_info': u'', |
| 5 | u'play': u'jtv_SAz4Mud24V7aKSw0', |
| 6 | u'meta_game': u'The Legend of Zelda: Ocarina of Time', |
| 7 | u'cluster': u'lax01', |
| 8 | u'type': u'360p', |
| 9 | u'broadcast_part': 1, |
| 10 | u'persistent': u'true', |
| 11 | u'video_height': 360, |
| 12 | u'token': u'26b8cee84453fd5e370d0ba13108a1ef59c9e58b:{"swfDomains": ["justin.tv", "jtvx.com", "xarth.com","twitchtv.com", "twitch.tv","newjtv.com", "jtvnw.net", "wdtinc.com", "imapweather.com", "facebook.com", "starcrafting.com"], "streamName": "jtv_SAz4Mud24V7aKSw0", "expiration": 1374431364, "geo_ip": "124.169.218.124", "server": "video8-2.lax01"}', |
| 13 | u'connect': u'rtmp://199.9.254.164/app', |
| 14 | u'broadcast_id': 6230376768L, |
| 15 | u'bitrate': 512, |
| 16 | u'display': u'360p', |
| 17 | u'find_type': u'dist' |
| 18 | }, |
| 19 | u'480p': |
| 20 | {u'node': u'video2-2.lax01', |
| 21 | u'needed_info': u'', |
| 22 | u'play': u'jtv_yv5WRBFg0ftTWUEO', |
| 23 | u'meta_game': u'The Legend of Zelda: Ocarina of Time', |
| 24 | u'cluster': u'lax01', |
| 25 | u'type': u'480p', |
| 26 | u'broadcast_part': 1, |
| 27 | u'persistent': u'true', |
| 28 | u'video_height': 480, |
| 29 | u'token': u'5e7a6465c72d71b455d0d78249ecf50e8cb64462:{"swfDomains": ["justin.tv", "jtvx.com", "xarth.com", "twitchtv.com", "twitch.tv", "newjtv.com", "jtvnw.net", "wdtinc.com", "imapweather.com", "facebook.com", "starcrafting.com"], "streamName": "jtv_yv5WRBFg0ftTWUEO", "expiration": 1374431364, "geo_ip": "124.169.218.124", "server": "video2-2.lax01"}', |
| 30 | u'connect': u'rtmp://199.9.254.158/app', |
| 31 | u'broadcast_id': 6230376752L, |
| 32 | u'bitrate': 768, |
| 33 | u'display': u'480p', |
| 34 | u'find_type': u'dist' |
| 35 | }, |
| 36 | u'720p+': |
| 37 | {u'node': u'video6-1.lax01', |
| 38 | u'needed_info': u'', |
| 39 | u'play': u'jtv_fGEW0H_C_0Dfy2bI', |
| 40 | u'meta_game': u'The Legend of Zelda: Ocarina of Time', |
| 41 | u'cluster': u'lax01', |
| 42 | u'type': u'live', |
| 43 | u'broadcast_part': 6, |
| 44 | u'persistent': u'true', |
| 45 | u'video_height': 720, |
| 46 | u'token': u'88830ed1f5bab63eb512e01d45698106ecb500b1:{"swfDomains": ["justin.tv", "jtvx.com", "xarth.com", "twitchtv.com", "twitch.tv", "newjtv.com", "jtvnw.net", "wdtinc.com", "imapweather.com", "facebook.com", "starcrafting.com"], "streamName": "jtv_fGEW0H_C_0Dfy2bI", "expiration": 1374431364, "geo_ip": "124.169.218.124", "server": "video6-1.lax01"}', |
| 47 | u'connect': u'rtmp://199.9.254.138/app', |
| 48 | u'broadcast_id': 6230375632L, |
| 49 | u'bitrate': 723.296875, |
| 50 | u'display': u'720p+', |
| 51 | u'find_type': u'dist' |
| 52 | } |
| 53 | } |