', {id: 'scVolumeSlider'})
+ ),
+ );
+
+ //grab slider since it's the source of volume
+ this.slider = parent.find('#scVolumeSlider');
+ this.slider.slider({
+ orientation:'vertical',
+ range:'min',
+ value: volume,
+ min: 0,
+ max: 100,
+ stop: (_, ui) => {
+ this.player.setVolume(ui.value);
+ this.event(Event.Volume, {volume: ui.value});
+ }
+ });
+
+ return parent.find('#scPlayer');
+ }
+
+ ready(cb) {
+ if (this.status === Status.READY) {
+ return cb();
+ }
+ }
+
+ event(event, data) {
+ switch (event) {
+ case Event.Seek: data.time = data.currentPosition / 1000; break;
+ }
+
+ super.event(event, data);
+ }
+
+ loadPlayer(id, timestamp, volume) {
+ this.video = {id, timestamp};
+
+ this.iframe = this.ui(window.$(super.frame()), volume);
+ this.iframe.attr(
+ 'src',
+ `${this.source}${id.substr(2)}?${encodeURIComponent(this.parameters.join('&'))}`
+ );
+ this.player = window.SC.Widget(this.iframe[0]);
+
+ //keep Events.READY separate
+ this.player.bind(window.SC.Widget.Events.READY, () => {
+ this.status = Status.READY;
+
+ this.player.setVolume(volume);
+ this.delay(timestamp);
+ });
+
+ //bind the rest of the events
+ for (const [key, event] of this.events) {
+ this.player.bind(key, (data) => this.event(event, data));
+ }
+ }
+
+ playVideo(id) {
+ this.player.load(
+ `https://api.soundcloud.com/tracks/${id.substr(2)}`
+ );
+ this.player.play();
+ }
+
+ pause() {
+ this.ready(() => this.player.pause());
+ }
+
+ play() {
+ this.ready(() => this.player.play());
+ }
+
+ seek(to) {
+ this.video.timestamp = to;
+ this.ready(() => {
+ this.player.seekTo(to * 1000);
+ });
+ }
+
+ getTime(cb) {
+ this.ready(() => this.player.getPosition(time => cb(time / 1000.0)));
+ }
+
+ getVolume(cb) {
+ this.ready(() => this.player.getVolume(cb));
+ }
+
+ getVideoState() {
+ return State.PLAYING;
+ }
+
+ destroy() {
+ this.status = Status.UNREADY;
+
+ for (const key of this.events.keys()) {
+ this.player.unbind(key);
+ }
+
+ window.$(super.frame()).empty();
+ }
+}
diff --git a/web/js/modules/players/twitch.js b/web/js/modules/players/twitch.js
new file mode 100644
index 00000000..275a5018
--- /dev/null
+++ b/web/js/modules/players/twitch.js
@@ -0,0 +1,112 @@
+import { Event, Base, State, Status } from "./base.js";
+
+function parseTwitchSource(src) {
+ const parts = src.split('/');
+
+ if (parts[0] === 'videos') {
+ return ['video', parts[1]];
+ } else {
+ return ['channel', parts[0]];
+ }
+}
+
+export class Twitch extends Base {
+ constructor() {
+ super();
+
+ this.player = null;
+ this.events = new Map([
+ [window.Twitch.Player.SEEK, Event.Seek],
+ [window.Twitch.Player.PLAYING, Event.Play],
+ [window.Twitch.Player.READY, Event.Ready],
+ ]);
+ }
+
+ ready(cb) {
+ if (this.status === Status.READY) {
+ return cb();
+ }
+ }
+
+ event(event, data) {
+ switch (event) {
+ case Event.Ready: {
+ this.status = Status.READY;
+ this.player.setVolume(this.video.volume);
+ this.delay(this.video.timestamp);
+ break;
+ }
+ case Event.Seek: {
+ this.video.timestamp = data.position;
+ data = {time: data.position};
+ break;
+ }
+ }
+
+ super.event(event, data);
+ }
+
+ loadPlayer(id, timestamp, volume, length) {
+ this.video = {id, timestamp, volume, length, sync: length > 0};
+
+ const parts = parseTwitchSource(id);
+ const options = {
+ [parts[0]]: parts[1],
+ muted: volume === 0,
+ width: this.width,
+ height: this.height,
+ autoplay: timestamp >= 0,
+ };
+
+ window.$(this.frame()).empty();
+
+ this.player = new window.Twitch.Player(this.frame().id, options);
+ this.events.forEach((value, key) => {
+ this.player.addEventListener(key, (data) => this.event(value, data));
+ });
+ }
+
+ playVideo(id, timestamp, volume, length) {
+ this.video = {id, timestamp, volume, length, sync: length > 0};
+ this.ready(() => {
+ const parts = id.split('/');
+ if (parts[0] === 'videos') {
+ this.player.setVideo(parts[1], timestamp);
+ } else {
+ this.player.setChannel(parts[0]);
+ }
+ });
+ }
+
+ pause() {
+ this.ready(() => this.player.pause());
+ }
+
+ play() {
+ this.ready(() => this.player.play());
+ }
+
+ seek(to) {
+ this.video.timestamp = to;
+ this.ready(() => this.player.seek(to));
+ }
+
+ getTime(cb) {
+ this.ready(() => {
+ cb(this.player.getDuration() === Infinity ? -1 : this.player.getCurrentTime());
+ });
+ }
+
+ getVolume(cb) {
+ this.ready(() => cb(this.player.getVolume()));
+ }
+
+ getVideoState() {
+ return State.PLAYING;
+ }
+
+ destroy() {
+ this.status = Status.UNREADY;
+ window.$(this.frame()).empty();
+ }
+}
\ No newline at end of file
diff --git a/web/js/modules/players/twitchclip.js b/web/js/modules/players/twitchclip.js
new file mode 100644
index 00000000..42a9eab1
--- /dev/null
+++ b/web/js/modules/players/twitchclip.js
@@ -0,0 +1,39 @@
+import { Base, State } from "./base.js";
+
+export class Twitchclip extends Base {
+ constructor() {
+ super();
+ }
+
+ loadPlayer(src, _, volume) {
+ this.video = {id: src};
+
+ const parameters = [
+ `clip=${src}`,
+ `parent=${document.location.hostname}`,
+ `autoplay=true`,
+ `muted=${volume === 0}`
+ ];
+
+ window.$(super.frame()).empty().append(
+ window.$('