diff --git a/player/soundcloud.coffee b/player/soundcloud.coffee new file mode 100644 index 00000000..eb45ec5f --- /dev/null +++ b/player/soundcloud.coffee @@ -0,0 +1,105 @@ +window.SoundCloudPlayer = class SoundCloudPlayer extends Player + constructor: (data) -> + if not (this instanceof SoundCloudPlayer) + return new SoundCloudPlayer(data) + + @setMediaProperties(data) + + waitUntilDefined(window, 'SC', => + removeOld() + + # For tracks that are private, but embeddable, the API returns a + # special URL to load into the player. + # TODO: rename scuri? + if data.meta.scuri + soundUrl = data.meta.scuri + else + soundUrl = data.id + + widget = $('').appendTo($('#ytapiplayer')) + widget.attr( + id: 'scplayer' + src: "https://w.soundcloud.com/player/?url=#{soundUrl}" + ) + + # Soundcloud embed widget doesn't have a volume control. + volumeSlider = $('
').attr('id', 'widget-volume') + .css('top', '170px') + .insertAfter(widget) + .slider( + range: 'min' + value: VOLUME * 100 + stop: (event, ui) => + @setVolume(ui.value / 100) + ) + + @soundcloud = SC.Widget(widget[0]) + @soundcloud.bind(SC.Widget.Events.READY, => + @soundcloud.ready = true + @setVolume(VOLUME) + @play() + + @soundcloud.bind(SC.Widget.Events.PAUSE, => + @paused = true + if CLIENT.leader + sendVideoUpdate() + ) + @soundcloud.bind(SC.Widget.Events.PLAY, => + @paused = false + if CLIENT.leader + sendVideoUpdate() + ) + @soundcloud.bind(SC.Widget.Events.FINISH, => + if CLIENT.leader + socket.emit('playNext') + ) + ) + ) + + load: (data) -> + @setMediaProperties(data) + if @soundcloud and @soundcloud.ready + if data.meta.scuri + soundUrl = data.meta.scuri + else + soundUrl = data.id + @soundcloud.load(soundUrl, auto_play: true) + else + console.error('SoundCloudPlayer::load() called but soundcloud is not ready') + + play: -> + @paused = false + if @soundcloud and @soundcloud.ready + @soundcloud.play() + + pause: -> + @paused = true + if @soundcloud and @soundcloud.ready + @soundcloud.pause() + + seekTo: (time) -> + if @soundcloud and @soundcloud.ready + # SoundCloud measures time in milliseconds while CyTube uses seconds. + @soundcloud.seekTo(time * 1000) + + setVolume: (volume) -> + # NOTE: SoundCloud's documentation claims that setVolume() accepts + # volumes in the range [0, 100], however it *actually* accepts volumes + # in the range [0, 1] (anything larger than 1 is treated as 1). I + # emailed them about this 2 years ago and they still haven't fixed + # their documentation. + if @soundcloud and @soundcloud.ready + @soundcloud.setVolume(volume) + + getTime: (cb) -> + if @soundcloud and @soundcloud.ready + # Returned time is in milliseconds; CyTube expects seconds + @soundcloud.getPosition((time) -> cb(time / 1000)) + else + cb(0) + + getVolume: (cb) -> + if @soundcloud and @soundcloud.ready + @soundcloud.getVolume(cb) + else + cb(VOLUME) diff --git a/player/update.coffee b/player/update.coffee index 912d7e87..810e6162 100644 --- a/player/update.coffee +++ b/player/update.coffee @@ -4,6 +4,7 @@ TYPE_MAP = dm: DailymotionPlayer gd: VideoJSPlayer gp: VideoJSPlayer + sc: SoundCloudPlayer window.loadMediaPlayer = (data) -> if data.type of TYPE_MAP diff --git a/www/js/player-new.js b/www/js/player-new.js index f991b912..2bd6cc4b 100644 --- a/www/js/player-new.js +++ b/www/js/player-new.js @@ -1,5 +1,5 @@ (function() { - var DailymotionPlayer, Player, TYPE_MAP, VideoJSPlayer, VimeoPlayer, YouTubePlayer, sortSources, + var DailymotionPlayer, Player, SoundCloudPlayer, TYPE_MAP, VideoJSPlayer, VimeoPlayer, YouTubePlayer, sortSources, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; @@ -593,12 +593,134 @@ })(Player); + window.SoundCloudPlayer = SoundCloudPlayer = (function(superClass) { + extend(SoundCloudPlayer, superClass); + + function SoundCloudPlayer(data) { + if (!(this instanceof SoundCloudPlayer)) { + return new SoundCloudPlayer(data); + } + this.setMediaProperties(data); + waitUntilDefined(window, 'SC', (function(_this) { + return function() { + var soundUrl, volumeSlider, widget; + removeOld(); + if (data.meta.scuri) { + soundUrl = data.meta.scuri; + } else { + soundUrl = data.id; + } + widget = $('').appendTo($('#ytapiplayer')); + widget.attr({ + id: 'scplayer', + src: "https://w.soundcloud.com/player/?url=" + soundUrl + }); + volumeSlider = $('').attr('id', 'widget-volume').css('top', '170px').insertAfter(widget).slider({ + range: 'min', + value: VOLUME * 100, + stop: function(event, ui) { + return _this.setVolume(ui.value / 100); + } + }); + _this.soundcloud = SC.Widget(widget[0]); + return _this.soundcloud.bind(SC.Widget.Events.READY, function() { + _this.soundcloud.ready = true; + _this.setVolume(VOLUME); + _this.play(); + _this.soundcloud.bind(SC.Widget.Events.PAUSE, function() { + _this.paused = true; + if (CLIENT.leader) { + return sendVideoUpdate(); + } + }); + _this.soundcloud.bind(SC.Widget.Events.PLAY, function() { + _this.paused = false; + if (CLIENT.leader) { + return sendVideoUpdate(); + } + }); + return _this.soundcloud.bind(SC.Widget.Events.FINISH, function() { + if (CLIENT.leader) { + return socket.emit('playNext'); + } + }); + }); + }; + })(this)); + } + + SoundCloudPlayer.prototype.load = function(data) { + var soundUrl; + this.setMediaProperties(data); + if (this.soundcloud && this.soundcloud.ready) { + if (data.meta.scuri) { + soundUrl = data.meta.scuri; + } else { + soundUrl = data.id; + } + return this.soundcloud.load(soundUrl, { + auto_play: true + }); + } else { + return console.error('SoundCloudPlayer::load() called but soundcloud is not ready'); + } + }; + + SoundCloudPlayer.prototype.play = function() { + this.paused = false; + if (this.soundcloud && this.soundcloud.ready) { + return this.soundcloud.play(); + } + }; + + SoundCloudPlayer.prototype.pause = function() { + this.paused = true; + if (this.soundcloud && this.soundcloud.ready) { + return this.soundcloud.pause(); + } + }; + + SoundCloudPlayer.prototype.seekTo = function(time) { + if (this.soundcloud && this.soundcloud.ready) { + return this.soundcloud.seekTo(time * 1000); + } + }; + + SoundCloudPlayer.prototype.setVolume = function(volume) { + if (this.soundcloud && this.soundcloud.ready) { + return this.soundcloud.setVolume(volume); + } + }; + + SoundCloudPlayer.prototype.getTime = function(cb) { + if (this.soundcloud && this.soundcloud.ready) { + return this.soundcloud.getPosition(function(time) { + return cb(time / 1000); + }); + } else { + return cb(0); + } + }; + + SoundCloudPlayer.prototype.getVolume = function(cb) { + if (this.soundcloud && this.soundcloud.ready) { + return this.soundcloud.getVolume(cb); + } else { + return cb(VOLUME); + } + }; + + return SoundCloudPlayer; + + })(Player); + TYPE_MAP = { yt: YouTubePlayer, vi: VimeoPlayer, dm: DailymotionPlayer, gd: VideoJSPlayer, - gp: VideoJSPlayer + gp: VideoJSPlayer, + sc: SoundCloudPlayer }; window.loadMediaPlayer = function(data) { diff --git a/www/js/sc.js b/www/js/sc.js index 3a7c3c08..34b51a85 100644 --- a/www/js/sc.js +++ b/www/js/sc.js @@ -1,330 +1 @@ -(function () { - var requirejs, require, define, __inflate; - (function (e) { - function a(e, t) { - var n = t && t.split("/"), - i = r.map, - s = i && i["*"] || {}, o, u, a, f, l, c, h; - if (e && e.charAt(0) === "." && t) { - n = n.slice(0, n.length - 1), e = n.concat(e.split("/")); - for (l = 0; h = e[l]; l++) if (h === ".") e.splice(l, 1), l -= 1; - else if (h === "..") { - if (l === 1 && (e[2] === ".." || e[0] === "..")) return !0; - l > 0 && (e.splice(l - 1, 2), l -= 2) - } - e = e.join("/") - } - if ((n || s) && i) { - o = e.split("/"); - for (l = o.length; l > 0; l -= 1) { - u = o.slice(0, l).join("/"); - if (n) for (c = n.length; c > 0; c -= 1) { - a = i[n.slice(0, c).join("/")]; - if (a) { - a = a[u]; - if (a) { - f = a; - break - } - } - } - f = f || s[u]; - if (f) { - o.splice(0, l, f), e = o.join("/"); - break - } - } - } - return e - } - function f(t, n) { - return function () { - return u.apply(e, s.call(arguments, 0).concat([t, n])) - } - } - function l(e) { - return function (t) { - return a(t, e) - } - } - function c(e) { - return function (n) { - t[e] = n - } - } - function h(r) { - if (n.hasOwnProperty(r)) { - var s = n[r]; - delete n[r], i[r] = !0, o.apply(e, s) - } - if (!t.hasOwnProperty(r)) throw new Error("No " + r); - return t[r] - } - function p(e, t) { - var n, r, i = e.indexOf("!"); - return i !== -1 ? (n = a(e.slice(0, i), t), e = e.slice(i + 1), r = h(n), r && r.normalize ? e = r.normalize(e, l(t)) : e = a(e, t)) : e = a(e, t), { - f: n ? n + "!" + e : e, - n: e, - p: r - } - } - function d(e) { - return function () { - return r && r.config && r.config[e] || {} - } - } - var t = {}, n = {}, r = {}, i = {}, s = [].slice, - o, u; - o = function (r, s, o, u) { - var a = [], - l, v, m, g, y, b; - u = u || r, typeof o == "string" && (o = __inflate(r, o)); - if (typeof o == "function") { - s = !s.length && o.length ? ["require", "exports", "module"] : s; - for (b = 0; b < s.length; b++) { - y = p(s[b], u), m = y.f; - if (m === "require") a[b] = f(r); - else if (m === "exports") a[b] = t[r] = {}, l = !0; - else if (m === "module") v = a[b] = { - id: r, - uri: "", - exports: t[r], - config: d(r) - }; - else if (t.hasOwnProperty(m) || n.hasOwnProperty(m)) a[b] = h(m); - else if (y.p) y.p.load(y.n, f(u, !0), c(m), {}), a[b] = t[m]; - else if (!i[m]) throw new Error(r + " missing " + m) - } - g = o.apply(t[r], a); - if (r) if (v && v.exports !== e && v.exports !== t[r]) t[r] = v.exports; - else if (g !== e || !l) t[r] = g - } else r && (t[r] = o) - }, requirejs = require = u = function (t, n, i, s) { - return typeof t == "string" ? h(p(t, n).f) : (t.splice || (r = t, n.splice ? (t = n, n = i, i = null) : t = e), n = n || function () {}, s ? o(e, t, n, i) : setTimeout(function () { - o(e, t, n, i) - }, 15), u) - }, u.config = function (e) { - return r = e, u - }, define = function (e, t, r) { - t.splice || (r = t, t = []), n[e] = [e, t, r] - }, define.amd = { - jQuery: !0 - } - })(), __inflate = function (name, src) { - var r; - return eval(["r = function(a,b,c){", "\n};\n//@ sourceURL=" + name + "\n"].join(src)), r - }, define("lib/api/events", ["require", "exports", "module"], function (e, t, n) { - t.api = { - LOAD_PROGRESS: "loadProgres", - PLAY_PROGRESS: "playProgress", - PLAY: "play", - PAUSE: "pause", - FINISH: "finish", - SEEK: "seek", - READY: "ready", - OPEN_SHARE_PANEL: "sharePanelOpened", - SHARE: "share", - CLICK_DOWNLOAD: "downloadClicked", - CLICK_BUY: "buyClicked" - }, t.bridge = { - REMOVE_LISTENER: "removeEventListener", - ADD_LISTENER: "addEventListener" - } - }), define("lib/api/getters", ["require", "exports", "module"], function (e, t, n) { - n.exports = { - GET_VOLUME: "getVolume", - GET_DURATION: "getDuration", - GET_POSITION: "getPosition", - GET_SOUNDS: "getSounds", - GET_CURRENT_SOUND: "getCurrentSound", - GET_CURRENT_SOUND_INDEX: "getCurrentSoundIndex", - IS_PAUSED: "isPaused" - } - }), define("lib/api/setters", ["require", "exports", "module"], function (e, t, n) { - n.exports = { - PLAY: "play", - PAUSE: "pause", - TOGGLE: "toggle", - SEEK_TO: "seekTo", - SET_VOLUME: "setVolume", - NEXT: "next", - PREV: "prev", - SKIP: "skip" - } - }), define("lib/api/api", ["require", "exports", "module", "lib/api/events", "lib/api/getters", "lib/api/setters"], function (e, t, n) { - function m(e) { - return !!(e === "" || e && e.charCodeAt && e.substr) - } - function g(e) { - return !!(e && e.constructor && e.call && e.apply) - } - function y(e) { - return !!e && e.nodeType === 1 && e.nodeName.toUpperCase() === "IFRAME" - } - function b(e) { - var t = !1, - n; - for (n in i) if (i.hasOwnProperty(n) && i[n] === e) { - t = !0; - break - } - return t - } - function w(e) { - var t, n, r; - for (t = 0, n = f.length; t < n; t++) { - r = e(f[t]); - if (r === !1) break - } - } - function E(e) { - var t = "", - n, r, i; - e.substr(0, 2) === "//" && (e = window.location.protocol + e), i = e.split("/"); - for (n = 0, r = i.length; n < r; n++) { - if (!(n < 3)) break; - t += i[n], n < 2 && (t += "/") - } - return t - } - function S(e) { - return e.contentWindow ? e.contentWindow : e.contentDocument && "parentWindow" in e.contentDocument ? e.contentDocument.parentWindow : null - } - function x(e) { - var t = [], - n; - for (n in e) e.hasOwnProperty(n) && t.push(e[n]); - return t - } - function T(e, t, n) { - n.callbacks[e] = n.callbacks[e] || [], n.callbacks[e].push(t) - } - function N(e, t) { - var n = !0, - r; - return t.callbacks[e] = [], w(function (t) { - r = t.callbacks[e] || []; - if (r.length) return n = !1, !1 - }), n - } - function C(e, t, n) { - var r = S(n), - i, s; - if (!r.postMessage) return !1; - i = n.getAttribute("src").split("?")[0], s = JSON.stringify({ - method: e, - value: t - }), i.substr(0, 2) === "//" && (i = window.location.protocol + i), i = i.replace(/http:\/\/(w|wt).soundcloud.com/, "https://$1.soundcloud.com"), r.postMessage(s, i) - } - function k(e) { - var t; - return w(function (n) { - if (n.instance === e) return t = n, !1 - }), t - } - function L(e) { - var t; - return w(function (n) { - if (S(n.element) === e) return t = n, !1 - }), t - } - function A(e, t) { - return function (n) { - var r = g(n), - i = k(this), - s = !r && t ? n : null, - o = r && !t ? n : null; - return o && T(e, o, i), C(e, s, i.element), this - } - } - function O(e, t, n) { - var r, i, s; - for (r = 0, i = t.length; r < i; r++) s = t[r], e[s] = A(s, n) - } - function M(e, t, n) { - return e + "?url=" + t + "&" + _(n) - } - function _(e) { - var t, n, r = []; - for (t in e) e.hasOwnProperty(t) && (n = e[t], r.push(t + "=" + (t === "start_track" ? parseInt(n, 10) : n ? "true" : "false"))); - return r.join("&") - } - function D(e, t, n) { - var r = e.callbacks[t] || [], - i, s; - for (i = 0, s = r.length; i < s; i++) r[i].apply(e.instance, n); - if (b(t) || t === o.READY) e.callbacks[t] = [] - } - function P(e) { - var t, n, r, i, s; - try { - n = JSON.parse(e.data) - } catch (u) {} - t = L(e.source), r = n.method, i = n.value, r === o.READY && (t ? (t.isReady = !0, D(t, l), N(l, t)) : a.push(e.source)), r === o.PLAY && !t.playEventFired && (t.playEventFired = !0), r === o.PLAY_PROGRESS && !t.playEventFired && (t.playEventFired = !0, D(t, o.PLAY, [i])); - if (!t || H(e.origin) !== H(t.domain)) return !1; - s = [], i !== undefined && s.push(i), D(t, r, s) - } - // Hack - function H(e) { - return e.replace(h, "") - } - var r = e("lib/api/events"), - i = e("lib/api/getters"), - s = e("lib/api/setters"), - o = r.api, - u = r.bridge, - a = [], - f = [], - l = "__LATE_BINDING__", - c = "http://wt.soundcloud.dev:9200/", - h = /^http(?:s?)/, - p, d, v; - window.scwtf = P; - window.fixSoundcloudShit = function() { - window.removeEventListener ? window.removeEventListener("message", window.scwtf, !1) : window.detachEvent("onmessage", window.scwtf); - } - window.unfixSoundcloudShit = function() { - window.addEventListener ? window.addEventListener("message", scwtf, !1) : window.attachEvent("onmessage", scwtf); - } - n.exports = v = function (e, t, n) { - m(e) && (e = document.getElementById(e)); - if (!y(e)) throw new Error("SC.Widget function should be given either iframe element or a string specifying id attribute of iframe element."); - t && (n = n || {}, e.src = M(c, t, n)); - var r = L(S(e)), - i, s; - return r && r.instance ? r.instance : (i = a.indexOf(S(e)) > -1, s = new p(e), f.push(new d(s, e, i)), s) - }, v.Events = o, window.SC = window.SC || {}, window.SC.Widget = v, d = function (e, t, n) { - this.instance = e, this.element = t, this.domain = E(t.getAttribute("src")), this.isReady = !! n, this.callbacks = {} - }, p = function () {}, p.prototype = { - constructor: p, - load: function (e, t) { - if (!e) return; - t = t || {}; - var n = this, - r = k(this), - i = r.element, - s = i.src, - a = s.substr(0, s.indexOf("?")); - r.isReady = !1, r.playEventFired = !1, i.onload = function () { - n.bind(o.READY, function () { - var e, n = r.callbacks; - for (e in n) n.hasOwnProperty(e) && e !== o.READY && C(u.ADD_LISTENER, e, r.element); - t.callback && t.callback() - }) - }, i.src = M(a, e, t) - }, - bind: function (e, t) { - var n = this, - r = k(this); - return r && r.element && (e === o.READY && r.isReady ? setTimeout(t, 1) : r.isReady ? (T(e, t, r), C(u.ADD_LISTENER, e, r.element)) : T(l, function () { - n.bind(e, t) - }, r)), this - }, - unbind: function (e) { - var t = k(this), - n; - t && t.element && (n = N(e, t), e !== o.READY && n && C(u.REMOVE_LISTENER, e, t.element)) - } - }, O(p.prototype, x(i)), O(p.prototype, x(s), !0) - }), window.SC = window.SC || {}, window.SC.Widget = require("lib/api/api") -})() +(function(){var requirejs,require,define,__inflate;(function(e){function a(e,t){var n=t&&t.split("/"),i=r.map,s=i&&i["*"]||{},o,u,a,f,l,c,h;if(e&&e.charAt(0)==="."&&t){n=n.slice(0,n.length-1),e=n.concat(e.split("/"));for(l=0;h=e[l];l++)if(h===".")e.splice(l,1),l-=1;else if(h===".."){if(l===1&&(e[2]===".."||e[0]===".."))return!0;l>0&&(e.splice(l-1,2),l-=2)}e=e.join("/")}if((n||s)&&i){o=e.split("/");for(l=o.length;l>0;l-=1){u=o.slice(0,l).join("/");if(n)for(c=n.length;c>0;c-=1){a=i[n.slice(0,c).join("/")];if(a){a=a[u];if(a){f=a;break}}}f=f||s[u];if(f){o.splice(0,l,f),e=o.join("/");break}}}return e}function f(t,n){return function(){return u.apply(e,s.call(arguments,0).concat([t,n]))}}function l(e){return function(t){return a(t,e)}}function c(e){return function(n){t[e]=n}}function h(r){if(n.hasOwnProperty(r)){var s=n[r];delete n[r],i[r]=!0,o.apply(e,s)}if(!t.hasOwnProperty(r))throw new Error("No "+r);return t[r]}function p(e,t){var n,r,i=e.indexOf("!");return i!==-1?(n=a(e.slice(0,i),t),e=e.slice(i+1),r=h(n),r&&r.normalize?e=r.normalize(e,l(t)):e=a(e,t)):e=a(e,t),{f:n?n+"!"+e:e,n:e,p:r}}function d(e){return function(){return r&&r.config&&r.config[e]||{}}}var t={},n={},r={},i={},s=[].slice,o,u;o=function(r,s,o,u){var a=[],l,v,m,g,y,b;u=u||r,typeof o=="string"&&(o=__inflate(r,o));if(typeof o=="function"){s=!s.length&&o.length?["require","exports","module"]:s;for(b=0;b