Skip to content

Instantly share code, notes, and snippets.

@BtbN
Last active March 10, 2016 13:16
Show Gist options
  • Save BtbN/378340fc2372e5b96163 to your computer and use it in GitHub Desktop.
Save BtbN/378340fc2372e5b96163 to your computer and use it in GitHub Desktop.
Replace Twitch.tv Video player with HTML5
// ==UserScript==
// @name Twitch hls.js
// @namespace http://btbn.de/
// @version 6.7
// @license MIT
// @description Twitch hls.js replacer
// @author BtbN
// @include *.twitch.tv/*
// @exclude api.twitch.tv/*
// @exclude blog.twitch.tv/*
// @exclude help.twitch.tv/*
// @exclude *.www.twitch.tv/*
// @require https://raw.githubusercontent.com/dailymotion/hls.js/master/dist/hls.min.js
// @updateURL https://gist.githubusercontent.com/BtbN/378340fc2372e5b96163/raw/twitchhtml5.user.js
// @downloadURL https://gist.githubusercontent.com/BtbN/378340fc2372e5b96163/raw/twitchhtml5.user.js
// @grant none
// ==/UserScript==
'use strict';
var hls_error_counter = 0;
if(window.top == window.self)
pagereload();
function pagereload()
{
if(!Hls.isSupported())
return;
decErrorCounter();
document.documentElement.addEventListener('DOMSubtreeModified', checkload, false);
}
function decErrorCounter()
{
if(hls_error_counter > 0)
hls_error_counter -= 1;
setTimeout(decErrorCounter, 5000);
}
function checkNeedReplace()
{
// Not a simple stream page, prevent breaking VOD player and stuff
if(window.location.pathname.split('/').length != 2)
return false;
return !document.getElementById('html5player');
}
function getStreamName()
{
if (document.getElementById("hostmode"))
{
var hostnode = document.querySelector("a[data-tt_content=host_channel]");
if(!hostnode)
return false;
var prof = hostnode.href.split('/');
return prof[prof.length - 1];
}
else
{
var profilenode = document.getElementsByClassName('channel-name')[0];
if(!profilenode)
return false;
var prof = profilenode.href.split('/');
return prof[prof.length - 2];
}
}
function killVideo()
{
var video = document.getElementById('html5player');
if(!video)
return;
//This triggers a reload because DOM modified
video.parentNode.removeChild(video);
}
function removeJunk()
{
var i;
if(window.location.pathname.split('/').length != 2)
return;
var player_errors = document.getElementsByClassName('player-error');
for(i = 0; i < player_errors.length; i++) {
var player_error = player_errors[i];
setTimeout(function() { player_error.parentNode.removeChild(player_error); }, 0);
}
var player_objs = document.querySelectorAll('[id^=swfobject]');
for(i = 0; i < player_objs.length; i++) {
var player_obj = player_objs[i];
setTimeout(function() { player_obj.parentNode.removeChild(player_obj); }, 0);
}
var player_ctrls = document.getElementsByClassName('player-controls-bottom');
for(i = 0; i < player_ctrls.length; i++) {
var player_ctrl = player_ctrls[i];
setTimeout(function() { player_ctrl.parentNode.removeChild(player_ctrl); }, 0);
}
}
function checkload()
{
var officialPlayer = true; // CHANGE THIS to true, if you want to iframe the official HTML5 player instead
if(!document.getElementById('oldtheamodebtn'))
{
document.documentElement.removeEventListener('DOMSubtreeModified', checkload, false);
var chan_actions = document.getElementsByClassName('channel-actions')[0];
if(chan_actions) {
var thea_button = document.createElement('span');
thea_button.className='ember-view';
var thea_sub_button = document.createElement('span');
thea_sub_button.id = 'oldtheamodebtn';
thea_sub_button.className = 'button action js-control-tip';
thea_sub_button.innerHTML = 'Theatre';
thea_sub_button.onclick = function() { App.__container__.lookup('controller:channel').send('toggleTheatre'); }
thea_button.appendChild(thea_sub_button);
chan_actions.appendChild(thea_button);
}
document.documentElement.addEventListener('DOMSubtreeModified', checkload, false);
}
removeJunk();
if(checkNeedReplace())
{
var video = 0;
var streamer = getStreamName();
if(!streamer)
return;
document.documentElement.removeEventListener('DOMSubtreeModified', checkload, false);
if(!officialPlayer) {
var player = document.getElementsByClassName('dynamic-player')[0];
if(!player)
return;
player.innerHTML = '<video id="html5player" width="100%" height="100%"></video>';
video = document.getElementById('html5player');
document.documentElement.addEventListener('DOMSubtreeModified', checkload, false);
} else {
var player = document.getElementById('video-1');
if(!player)
return;
player.innerHTML = '<iframe id="html5player" src="https://player.twitch.tv/?branding=false&html5&showInfo=false&channel=' + streamer + '" width="100%" height="100%" allowfullscreen="allowfullscreen" webkitallowfullscreen="webkitallowfullscreen" mozallowfullscreen="mozallowfullscreen"></iframe>';
document.documentElement.addEventListener('DOMSubtreeModified', checkload, false);
return;
}
get_stream_url(streamer, function(url) {
var hls = new Hls();
hls.on(Hls.Events.MANIFEST_PARSED,function() {
video.play();
});
hls.on(Hls.Events.ERROR, function(event,data) {
if(data.type == Hls.ErrorTypes.NETWORK_ERROR)
hls_error_counter += 1;
if(hls_error_counter >= 3 || data.fatal) {
video.pause();
hls.destroy();
killVideo();
}
});
hls.loadSource(url);
hls.attachMedia(video);
});
}
}
function get_stream_url(streamer, cb)
{
var auth_url = "https://api.twitch.tv/api/channels/" + streamer + "/access_token";
$.get(auth_url, function(data) {
var token = data['token'];
var sig = data['sig'];
var params = {
player: "twitchweb",
token: token,
sig: sig,
allow_audio_only: "false",
allow_source: "true",
type: "any"
};
var pp = $.param(params)
var usher_url = "https://usher.ttvnw.net/api/channel/hls/" + streamer + ".m3u8?" + pp
get_source_url(usher_url, cb);
});
}
function get_source_url(usher_url, cb)
{
var streamId = 0; ///CHANGE THIS, if you want a diffrent quality level.
/// -1 is auto-select, 0 is source, 1 is high, 2 medium, 3 low.
/// Falls back to auto-select in case of invalid ID
var localStreamId = 0;
if(streamId < 0) {
cb(usher_url);
return;
}
$.get(usher_url, function(data) {
data.split("\n").some(function(l) {
if(stringStartsWith(l, "http://") || stringStartsWith(l, "https://")) {
localStreamId += 1;
if(localStreamId <= streamId)
return false;
cb(l);
return true;
}
return false;
});
if(localStreamId <= streamId)
cb(usher_url);
}).fail(function() {
setTimeout(killVideo, 5000);
});
}
function stringStartsWith(string, prefix) {
return string.slice(0, prefix.length) == prefix;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment