Skip to content

Commit

Permalink
Added support for Twitch
Browse files Browse the repository at this point in the history
  • Loading branch information
gurupras committed Jun 21, 2019
1 parent dfc09b8 commit 7aa0026
Show file tree
Hide file tree
Showing 6 changed files with 337 additions and 3 deletions.
6 changes: 6 additions & 0 deletions src/js/config/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,12 @@ const defaults = {
googleIMA: {
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
},
twitch: {
api: 'https://player.twitch.tv/js/embed/v1.js',
},
twitch: {
api: 'https://player.twitch.tv/js/embed/v1.js',
},
},

// Custom control listeners
Expand Down
1 change: 1 addition & 0 deletions src/js/config/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const providers = {
html5: 'html5',
youtube: 'youtube',
vimeo: 'vimeo',
twitch: 'twitch',
};

export const types = {
Expand Down
3 changes: 3 additions & 0 deletions src/js/media.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import html5 from './html5';
import vimeo from './plugins/vimeo';
import youtube from './plugins/youtube';
import twitch from './plugins/twitch';
import { createElement, toggleClass, wrap } from './utils/elements';

const media = {
Expand Down Expand Up @@ -52,6 +53,8 @@ const media = {
youtube.setup.call(this);
} else if (this.isVimeo) {
vimeo.setup.call(this);
} else if (this.isTwitch) {
twitch.setup.call(this);
}
},
};
Expand Down
300 changes: 300 additions & 0 deletions src/js/plugins/twitch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
// ==========================================================================
// Twitch plugin
// ==========================================================================
import controls from '../controls';
import ui from '../ui';
import { createElement, replaceElement, toggleClass } from '../utils/elements';
import { triggerEvent } from '../utils/events';
import is from '../utils/is';
import loadScript from '../utils/load-script';
import { generateId } from '../utils/strings';
import { setAspectRatio } from '../utils/style';

const twitch = {
setup() {
// Add embed class for responsive
toggleClass(this.elements.wrapper, this.config.classNames.embed, true);

// Set aspect ratio
setAspectRatio.call(this);

// Setup API
if (is.object(window.Twitch) && is.function(window.Twitch.Player)) {
twitch.ready.call(this);
} else {
// Load the API
loadScript(this.config.urls.twitch.api).catch(error => {
this.debug.warn('Twitch API failed to load', error);
});
// Load the API
loadScript(this.config.urls.twitch.api);
const interval = window.setInterval(() => {
if (window.Twitch) {
window.clearInterval(interval);
twitch.ready.call(this);
}
}, 100);
}
},

// API ready
ready() {
const player = this;
// Get the source URL or ID
let source = player.media.getAttribute('src');

// Get from <div> if needed
if (is.empty(source)) {
source = player.media.getAttribute(this.config.attributes.embed.id);
}

const opts = {
autoplay: player.config.autoplay,
muted: player.config.muted,
};

// Parse whether video, channel or collection
// then assign it to opts accordingly
const videoId = twitch.parseTwitchId(source);
opts[videoId.type] = videoId.src;

const id = generateId(player.provider);
const { poster } = player;
// This was taken from youtube.js
const container = createElement('div', { id, poster, class: player.config.classNames.embedContainer });
player.media = replaceElement(container, player.media);

player.embed = new window.Twitch.Player(id, opts);

// Get the instance
const instance = player.embed;
instance.plyrProps = {
videoId,
};

instance.addEventListener(window.Twitch.Player.READY, () => {
// TODO: Get the title

// Create a faux HTML5 API using the YouTube API
player.media.play = () => {
instance.play();
};

player.media.pause = () => {
instance.pause();
};

player.media.stop = () => {
player.pause();
player.currentTime = 0;
};

// XXX: instance.getDuration() is 0 for some reason
// Despite the API reporting that it's ready to be used
// Live streams thankfully respond with a duration of Infinity
// Therefore, we should be OK using the condition of !== 0 && !isNan()
player.media.duration = 0;
const durationInterval = setInterval(() => {
const duration = instance.getDuration();
if (duration !== 0 && is.number(duration)) {
if (instance.plyrProps.type !== 'channel' && duration === Infinity) {
// Only channels are allowed to have an infinite duration
return;
}
clearInterval(durationInterval);
player.media.duration = duration;
player.media.paused = true;
triggerEvent.call(player, player.media, 'durationchange');
}
}, 100);

// Seeking
player.media.currentTime = 0;
Object.defineProperty(player.media, 'currentTime', {
get() {
return Number(instance.getCurrentTime());
},
set(time) {
// Vimeo will automatically play on seek
const { paused } = player.media;

// Set seeking flag
player.media.seeking = true;

// Trigger seeking
triggerEvent.call(player, player.media, 'seeking');

// Seek after events sent
instance.seek(time);

// Restore pause state
if (paused) {
player.pause();
}
},
});

// Playback speed
// Twitch does not allow changing playbackRate
Object.defineProperty(player.media, 'playbackRate', {
get() {
return 1.0;
},
set() {
// XXX: Fail silently?
},
});

// Quality

// instance.getQualities is of the format
// [
// {
// "name": "720p60",
// "group": "720p60",
// "codecs": "avc1.4D401F,mp4a.40.2",
// "bitrate": 3074863,
// "width": 1280,
// "height": 720,
// "framerate": 0,
// "isDefault": false,
// "bandwidth": 3074863
// },
// ...
// ]
// TODO: Should we calculate every time?
Object.defineProperty(player.media, 'quality', {
get() {
const qualities = instance.getQualities();
// instance.getQuality() always returns 'group'
const entry = qualities.filter(v => v.group === instance.getQuality())[0];

if (entry) {
return entry.height;
}
return undefined;
},
set(input) {
const qualities = instance.getQualities();
const quality = input;
// Find the entry whose 'height' equals quality
const entry = qualities.filter(v => v.height === quality)[0] || qualities[0];
instance.setQuality(entry.group);
},
});

// Volume
let { volume } = player.config;
Object.defineProperty(player.media, 'volume', {
get() {
return volume;
},
set(input) {
volume = input;
instance.setVolume(volume);
triggerEvent.call(player, player.media, 'volumechange');
},
});

// Muted
let { muted } = player.config;
Object.defineProperty(player.media, 'muted', {
get() {
return muted;
},
set(input) {
const toggle = is.boolean(input) ? input : muted;
muted = toggle;
instance.setMuted(muted);
triggerEvent.call(player, player.media, 'volumechange');
},
});

let { currentSrc } = player.media;
currentSrc = source;
// Source
Object.defineProperty(player.media, 'currentSrc', {
get() {
return currentSrc;
},
});

// Ended
Object.defineProperty(player.media, 'ended', {
get() {
return player.currentTime === player.duration;
},
});

// Twitch recommended is 4:3
player.embed.ratio = [16, 9];
setAspectRatio.call(this);

// Get available speeds
player.options.speed = [];

// Set the tabindex to avoid focus entering iframe
if (player.supported.ui) {
player.media.setAttribute('tabindex', -1);
}

triggerEvent.call(player, player.media, 'timeupdate');
triggerEvent.call(player, player.media, 'durationchange');

// Rebuild UI
setTimeout(() => ui.build.call(player), 50);

instance.addEventListener(window.Twitch.Player.ENDED, () => {
if(player.config.loop.active) {
instance.seek(0);
instance.play();
return;
}
player.media.paused = true;
triggerEvent.call(player, player.media, 'ended');
});

instance.addEventListener(window.Twitch.Player.PLAY, () => {
player.media.paused = false;
player.media.seeking = false;

triggerEvent.call(player, player.media, 'play');
triggerEvent.call(player, player.media, 'playing');

// Poll to get playback progress
player.timers.playing = window.setInterval(() => {
triggerEvent.call(player, player.media, 'timeupdate');
}, 50);

// Get quality
controls.setQualityMenu.call(player, instance.getQualities().map(v => v.height));
});

instance.addEventListener(window.Twitch.Player.PAUSE, () => {
player.media.paused = true;
triggerEvent.call(player, player.media, 'pause');
});
});
},
parseTwitchId(source) {
// By default, we treat things as videos
// If it contains a prefix of 'channel:', 'video:', 'collection:'
// then it's treated accordingly
const colonIndex = source.indexOf(':');
const hasPrefix = colonIndex !== -1;
if (hasPrefix) {
const idType = source.substr(0, colonIndex);
const id = source.substr(colonIndex + 1);
return {
type: idType,
src: id,
};
}
return {
type: 'video',
src: source,
};
},
};

export default twitch;
13 changes: 10 additions & 3 deletions src/js/plyr.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ class Plyr {
this.elements.original = clone;

// Set media type based on tag or data attribute
// Supported: video, audio, vimeo, youtube
// Supported: video, audio, vimeo, youtube, twitch
const type = this.media.tagName.toLowerCase();
// Embed properties
let iframe = null;
Expand Down Expand Up @@ -327,7 +327,7 @@ class Plyr {
}

get isEmbed() {
return this.isYouTube || this.isVimeo;
return this.isYouTube || this.isVimeo || this.isTwitch;
}

get isYouTube() {
Expand All @@ -338,6 +338,10 @@ class Plyr {
return this.provider === providers.vimeo;
}

get isTwitch() {
return this.provider === providers.twitch;
}

get isVideo() {
return this.type === types.video;
}
Expand Down Expand Up @@ -1193,6 +1197,9 @@ class Plyr {

// Vimeo does not always return
setTimeout(done, 200);
} else if (this.isTwitch) {
clearInterval(this.timers.playing);
done();
}
}

Expand All @@ -1207,7 +1214,7 @@ class Plyr {
/**
* Check for support
* @param {String} type - Player type (audio/video)
* @param {String} provider - Provider (html5/youtube/vimeo)
* @param {String} provider - Provider (html5/youtube/vimeo/twitch)
* @param {Boolean} inline - Where player has `playsinline` sttribute
*/
static supported(type, provider, inline) {
Expand Down
Loading

0 comments on commit 7aa0026

Please sign in to comment.