diff --git a/Makefile.am b/Makefile.am index 80b82790..6af530b1 100644 --- a/Makefile.am +++ b/Makefile.am @@ -131,6 +131,7 @@ install: $(INSTALL) -d $(SHARE)/www/static/translation $(INSTALL) -d $(SHARE)/www/static/css $(INSTALL) -d $(SHARE)/www/static/js/libs/pdf + $(INSTALL) -d $(SHARE)/www/static/js/sandboxes $(INSTALL) bin/$(EXENAME) $(BIN) $(INSTALL) html/* $(SHARE)/www/html $(INSTALL) static/img/* $(SHARE)/www/static/img @@ -142,6 +143,7 @@ install: $(INSTALL) $(OUTPUT_JS)/*.js $(SHARE)/www/static/js $(INSTALL) $(OUTPUT_JS)/libs/pdf/*.js $(SHARE)/www/static/js/libs/pdf $(INSTALL) -D static/js/libs/webodf.js $(SHARE)/www/static/js/libs/webodf.js + $(INSTALL) $(OUTPUT_JS)/sandboxes/*.js $(SHARE)/www/static/js/sandboxes clean: $(GO) clean -i -r app/... 2>/dev/null || true diff --git a/build/build.js b/build/build.js index ec6c1eb9..69ce4508 100644 --- a/build/build.js +++ b/build/build.js @@ -76,6 +76,13 @@ override: { skipModuleInsertion: true } + }, + { + name: 'sandboxes/youtube', + dir: './out/sandboxes', + override: { + skipModuleInsertion: true + } } ] }) diff --git a/server.conf.in b/server.conf.in index e6b143d3..a02df90c 100644 --- a/server.conf.in +++ b/server.conf.in @@ -109,6 +109,7 @@ serverRealm = local ; data: URL for images. ; The currently recommended CSP is: ; default-src 'self'; +; frame-src 'self' data:; ; style-src 'self' 'unsafe-inline'; ; img-src 'self' data: blob:; ; connect-src 'self' wss://server:port/ws blob:; diff --git a/src/styles/components/_youtubevideo.scss b/src/styles/components/_youtubevideo.scss index 398a2a53..75645690 100644 --- a/src/styles/components/_youtubevideo.scss +++ b/src/styles/components/_youtubevideo.scss @@ -92,6 +92,10 @@ #youtubecontainer { // scss-lint:disable IdSelector position: relative; + + &.fullscreen { + width: 100%; + } } #youtubeplayerinfo { // scss-lint:disable IdSelector diff --git a/static/js/directives/youtubevideo.js b/static/js/directives/youtubevideo.js index 706570b9..9b8b8b6d 100644 --- a/static/js/directives/youtubevideo.js +++ b/static/js/directives/youtubevideo.js @@ -20,20 +20,81 @@ */ "use strict"; -define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], function($, _, template, BigScreen) { +define(['require', 'jquery', 'underscore', 'moment', 'text!partials/youtubevideo.html', 'text!partials/youtubevideo_sandbox.html', 'bigscreen'], function(require, $, _, moment, template, sandboxTemplate, BigScreen) { - return ["$window", "$document", "mediaStream", "alertify", "translation", "safeApply", "appData", "$q", function($window, $document, mediaStream, alertify, translation, safeApply, appData, $q) { + return ["$window", "$document", "mediaStream", "alertify", "translation", "safeApply", "appData", "$q", "restURL", "sandbox", function($window, $document, mediaStream, alertify, translation, safeApply, appData, $q, restURL, sandbox) { var YOUTUBE_IFRAME_API_URL = "//www.youtube.com/iframe_api"; - var isYouTubeIframeAPIReady = (function() { - var d = $q.defer(); - $window.onYouTubeIframeAPIReady = function() { - console.log("YouTube IFrame ready"); - d.resolve(); - }; - return d.promise; - })(); + var isYouTubeIframeAPIReadyDefer = $q.defer(); + var isYouTubeIframeAPIReady = isYouTubeIframeAPIReadyDefer.promise; + + var SandboxPlayer = function(sandbox, params) { + this.sandbox = sandbox; + this.state = -1; + this.position = 0; + this.lastPositionUpdate = null; + this.sandbox.postMessage("loadPlayer", params); + }; + + SandboxPlayer.prototype.destroy = function() { + this.sandbox.postMessage("destroyPlayer", {"destroy": true}); + }; + + SandboxPlayer.prototype.loadVideoById = function(id, position) { + var msg = {"id": id}; + if (typeof(position) !== "undefined") { + msg.position = position; + } + this.sandbox.postMessage("loadVideo", msg); + }; + + SandboxPlayer.prototype.playVideo = function() { + this.sandbox.postMessage("playVideo", {"play": true}); + }; + + SandboxPlayer.prototype.pauseVideo = function() { + this.sandbox.postMessage("pauseVideo", {"pause": true}); + }; + + SandboxPlayer.prototype.stopVideo = function() { + this.sandbox.postMessage("stopVideo", {"stop": true}); + }; + + SandboxPlayer.prototype.seekTo = function(position, allowSeekAhead) { + var msg = {"position": position}; + if (typeof(allowSeekAhead) !== "undefined") { + msg.allowSeekAhead = allowSeekAhead; + } + this.sandbox.postMessage("seekTo", msg); + }; + + SandboxPlayer.prototype.setVolume = function(volume) { + this.sandbox.postMessage("setVolume", {"volume": volume}); + }; + + SandboxPlayer.prototype.setCurrentTime = function(time) { + this.position = time; + this.lastPositionUpdate = moment(); + }; + + SandboxPlayer.prototype.getCurrentTime = function() { + if (!this.lastPositionUpdate) { + return this.position; + } + + var now = moment(); + var deltaTime = now.diff(this.lastPositionUpdate, 'seconds', true); + return this.position + deltaTime; + }; + + SandboxPlayer.prototype.setPlayerState = function(state) { + this.state = state; + }; + + SandboxPlayer.prototype.getPlayerState = function() { + return this.state; + }; var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) { @@ -41,20 +102,63 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], var player = null; var playerReady = null; var isPaused = null; - var seekDetector = null; var playReceivedNow = null; - var prevTime = null; - var prevNow = null; var initialState = null; + var sandboxFrame = $("#youtubeplayer", $element)[0]; + + var template = sandboxTemplate; + template = template.replace(/__PARENT_ORIGIN__/g, $window.location.protocol + "//" + $window.location.host); + template = template.replace(/__YOUTUBE_SANDBOX_JS_URL__/g, restURL.createAbsoluteUrl(require.toUrl('sandboxes/youtube') + ".js")); + var sandboxApi = sandbox.createSandbox(sandboxFrame, template); + + sandboxApi.e.on("message", function(event, message) { + var msg = message.data; + var data = msg[msg.type] || {}; + switch (msg.type) { + case "youtube.apiReady": + $scope.$apply(function() { + console.log("YouTube IFrame ready"); + isYouTubeIframeAPIReadyDefer.resolve(); + }); + break; + case "youtube.playerReady": + $scope.$apply(function() { + playerReady.resolve(); + }); + break; + case "youtube.volume": + $scope.$apply(function(scope) { + scope.volume = data.volume; + }); + break; + case "youtube.event": + $scope.$apply(function(scope) { + console.log("State change", data); + if (player) { + player.setPlayerState(data.state); + } + scope.$emit(data.event, data.position); + }); + break; + case "youtube.position": + if (player) { + player.setCurrentTime(data.position); + } + break; + default: + console.log("Unknown message received", message); + break; + } + }); + + $scope.$on("$destroy", function() { + if (player) { + player.destroy(); + } + sandboxApi.destroy(); + sandboxApi = null; + }); - var stateEvents = { - "-1": "youtube.unstarted", - "0": "youtube.ended", - "1": "youtube.playing", - "2": "youtube.paused", - "3": "youtube.buffering", - "5": "youtube.videocued" - }; var errorIds = { "2": "invalidParameter", "5": "htmlPlayerError", @@ -79,13 +183,6 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], }); }); - var onPlayerReady = function(event) { - $scope.$apply(function(scope) { - scope.volume = player.getVolume(); - playerReady.resolve(); - }); - }; - var onPlayerError = function(event) { var error = errorIds[event.data] || "unknownError"; $scope.$apply(function(scope) { @@ -93,19 +190,6 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], }); }; - var onPlayerStateChange = function(event) { - var msg = stateEvents[event.data]; - if (typeof msg === "undefined") { - console.warn("Unknown YouTube player state", event) - return; - } - - $scope.$apply(function(scope) { - console.log("State change", msg, event.target); - scope.$emit(msg, event.target); - }); - }; - var getYouTubeId = function(url) { /* * Supported URLs: @@ -130,79 +214,34 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], return null; } - var startDetectSeek = function() { - var checkSeek = function() { - if (!player) { - return; - } - var now = new Date(); - var time = player.getCurrentTime(); - if (prevTime === null) { - prevTime = time; - } - if (prevNow === null) { - prevNow = now; - } - var deltaTime = Math.abs(time - prevTime); - var deltaNow = (now - prevNow) * 0.001; - if (deltaTime > deltaNow * 1.1) { - safeApply($scope, function(scope) { - scope.$emit("youtube.seeked", time); - }); - } - prevNow = now; - prevTime = time; - }; - - if (!seekDetector) { - seekDetector = $window.setInterval(function() { - checkSeek(); - }, 1000); - } - checkSeek(); - }; - - var stopDetectSeek = function() { - if (seekDetector) { - $window.clearInterval(seekDetector); - seekDetector = null; - } - prevNow = null; - }; - - $scope.$on("youtube.playing", function() { + $scope.$on("youtube.playing", function(event, position) { if (initialState === 2) { initialState = null; player.pauseVideo(); return; } - prevTime = null; - startDetectSeek(); if (isPaused) { isPaused = false; mediaStream.webrtc.callForEachCall(function(peercall) { mediaStreamSendYouTubeVideo(peercall, currentToken, { Type: "Resume", Resume: { - position: player.getCurrentTime() + position: position } }); }); } }); - $scope.$on("youtube.buffering", function() { + $scope.$on("youtube.buffering", function(event, position) { if (initialState === 2) { initialState = null; player.pauseVideo(); } - - startDetectSeek(); }); - $scope.$on("youtube.paused", function() { - stopDetectSeek(); + $scope.$on("youtube.paused", function(event, position) { if (!$scope.isPublisher || !currentToken) { return; } @@ -213,7 +252,7 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], mediaStreamSendYouTubeVideo(peercall, currentToken, { Type: "Pause", Pause: { - position: player.getCurrentTime() + position: position } }); }); @@ -221,7 +260,6 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], }); $scope.$on("youtube.ended", function() { - stopDetectSeek(); }); $scope.$on("youtube.seeked", function($event, position) { @@ -241,10 +279,7 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], var playVideo = function(id, position, state) { playerReady.done(function() { - $("#youtubeplayer").show(); $scope.playbackActive = true; - prevTime = null; - prevNow = null; isPaused = null; if (playReceivedNow) { var delta = ((new Date()) - playReceivedNow) * 0.001; @@ -278,7 +313,7 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], isYouTubeIframeAPIReady.then(function() { if (!player) { var origin = $window.location.protocol + "//" + $window.location.host; - player = new $window.YT.Player("youtubeplayer", { + player = new SandboxPlayer(sandboxApi, { height: "390", width: "640", playerVars: { @@ -291,13 +326,8 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], "controls": with_controls ? "2" : "0", "disablekb": with_controls ? "0" : "1", "origin": origin - }, - events: { - "onReady": onPlayerReady, - "onStateChange": onPlayerStateChange } }); - $("#youtubeplayer").show(); safeApply($scope, function(scope) { // YT player events don't fire in Firefox if // player is not visible, so show while loading @@ -475,19 +505,7 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], }; $scope.loadYouTubeAPI = function() { - if (!addedIframeScript) { - var head = $document[0].getElementsByTagName('head')[0]; - var script = $document[0].createElement('script'); - script.type = "text/javascript"; - script.src = YOUTUBE_IFRAME_API_URL; - script.onerror = function(event) { - alertify.dialog.alert(translation._("Could not load YouTube player API, please check your network / firewall settings.")); - head.removeChild(script); - addedIframeScript = false; - }; - head.appendChild(script); - addedIframeScript = true; - } + sandboxApi.postMessage("loadApi", {"url": $window.location.protocol + YOUTUBE_IFRAME_API_URL}); }; $scope.showYouTubeVideo = function() { @@ -538,7 +556,6 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], $scope.currentVideoUrl = null; $scope.currentVideoId = null; peers = {}; - stopDetectSeek(); playerReady = null; initialState = null; mediaStream.webrtc.e.off("statechange", updater); @@ -568,7 +585,11 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], $scope.toggleFullscreen = function(elem) { if (BigScreen.enabled) { - BigScreen.toggle(elem); + BigScreen.toggle(elem, function() { + $(elem).addClass("fullscreen"); + }, function() { + $(elem).removeClass("fullscreen"); + }); } }; diff --git a/static/js/sandboxes/youtube.js b/static/js/sandboxes/youtube.js new file mode 100644 index 00000000..b09e7cbf --- /dev/null +++ b/static/js/sandboxes/youtube.js @@ -0,0 +1,248 @@ +/* + * Spreed WebRTC. + * Copyright (C) 2013-2015 struktur AG + * + * This file is part of Spreed WebRTC. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +"use strict"; +(function () { + + var script = document.getElementsByTagName("script")[0]; + var PARENT_ORIGIN = script.getAttribute("data-parent-origin"); + + var YouTubeSandbox = function(window) { + this.head = document.getElementsByTagName('head')[0]; + this.window = window; + this.addedIframeScript = false; + this.player = null; + this.seekDetector = null; + this.prevTime = null; + this.prevNow = null; + }; + + YouTubeSandbox.prototype.postMessage = function(type, message) { + var msg = {"type": type}; + msg[type] = message; + this.window.parent.postMessage(msg, PARENT_ORIGIN); + }; + + YouTubeSandbox.prototype.onYouTubeIframeAPIReady = function() { + this.postMessage("youtube.apiReady", {"apiReady": true}); + }; + + YouTubeSandbox.prototype.loadApi = function(url) { + if (!this.addedIframeScript) { + var that = this; + var script = document.createElement('script'); + script.type = "text/javascript"; + script.src = url; + script.onerror = function(evt) { + that.postMessage("youtube.error", {"msgid": "loadScriptFailed"}); + that.head.removeChild(script); + that.addedIframeScript = false; + }; + this.head.appendChild(script); + this.addedIframeScript = true; + } + }; + + YouTubeSandbox.prototype.loadPlayer = function(params) { + if (!this.player) { + var that = this; + var stateEvents = { + "-1": "youtube.unstarted", + "0": "youtube.ended", + "1": "youtube.playing", + "2": "youtube.paused", + "3": "youtube.buffering", + "5": "youtube.videocued" + }; + + var playerVars = params.playerVars || {}; + delete playerVars.origin; + this.player = new this.window.YT.Player("youtubeplayer", { + height: params.height || "390", + width: params.width || "640", + playerVars: playerVars, + events: { + "onReady": function(event) { + that.postMessage("youtube.volume", {"volume": that.player.getVolume()}); + that.postMessage("youtube.playerReady", {"playerReady": true}); + }, + "onStateChange": function(event) { + var msg = stateEvents[event.data]; + if (typeof msg === "undefined") { + console.warn("Unknown YouTube player state", event) + return; + } + + switch (msg) { + case "youtube.playing": + that.prevTime = null; + that.startDetectSeek(); + break; + case "youtube.buffering": + that.startDetectSeek(); + break; + case "youtube.paused": + that.stopDetectSeek(); + break; + case "youtube.ended": + that.stopDetectSeek(); + break; + } + + that.postMessage("youtube.event", {"event": msg, "state": event.data, "position": that.player.getCurrentTime()}); + } + } + }); + } + }; + + YouTubeSandbox.prototype.destroyPlayer = function() { + this.stopDetectSeek(); + if (this.player) { + this.player.destroy(); + this.player = null; + } + }; + + YouTubeSandbox.prototype.loadVideo = function(id, position) { + this.prevTime = null; + this.prevNow = null; + if (typeof(position) !== "undefined") { + this.player.loadVideoById(id, position); + } else { + this.player.loadVideoById(id); + } + }; + + YouTubeSandbox.prototype.playVideo = function() { + this.player.playVideo(); + }; + + YouTubeSandbox.prototype.pauseVideo = function() { + this.player.pauseVideo(); + }; + + YouTubeSandbox.prototype.stopVideo = function() { + this.player.stopVideo(); + }; + + YouTubeSandbox.prototype.seekTo = function(position, allowSeekAhead) { + if (typeof(allowSeekAhead) !== "undefined") { + this.player.seekTo(position, allowSeekAhead); + } else { + this.player.seekTo(position); + } + }; + + YouTubeSandbox.prototype.setVolume = function(volume) { + this.player.setVolume(volume); + }; + + YouTubeSandbox.prototype.startDetectSeek = function() { + var that = this; + var checkSeek = function() { + if (!that.player) { + return; + } + var now = new Date(); + var time = that.player.getCurrentTime(); + that.postMessage("youtube.position", {"position": time}); + if (that.prevTime === null) { + that.prevTime = time; + } + if (that.prevNow === null) { + that.prevNow = now; + } + var deltaTime = Math.abs(time - that.prevTime); + var deltaNow = (now - that.prevNow) * 0.001; + if (deltaTime > deltaNow * 1.1) { + that.postMessage("youtube.event", {"event": "youtube.seeked", "position": time}); + } + that.prevNow = now; + that.prevTime = time; + }; + + if (!this.seekDetector) { + this.seekDetector = this.window.setInterval(function() { + checkSeek(); + }, 1000); + } + checkSeek(); + }; + + YouTubeSandbox.prototype.stopDetectSeek = function() { + if (this.seekDetector) { + this.window.clearInterval(this.seekDetector); + this.seekDetector = null; + } + this.prevNow = null; + }; + + var sandbox = new YouTubeSandbox(window); + + window.onYouTubeIframeAPIReady = function() { + sandbox.onYouTubeIframeAPIReady(); + }; + + window.addEventListener("message", function(event) { + if (event.origin !== PARENT_ORIGIN) { + // only accept messages from spreed-webrtc + return; + } + var msg = event.data; + var data = msg[msg.type] || {}; + switch (msg.type) { + case "loadApi": + sandbox.loadApi(data.url); + break; + case "loadPlayer": + sandbox.loadPlayer(data); + break; + case "destroyPlayer": + sandbox.destroyPlayer(); + break; + case "loadVideo": + sandbox.loadVideo(data.id, data.position); + break; + case "playVideo": + sandbox.playVideo(); + break; + case "pauseVideo": + sandbox.pauseVideo(); + break; + case "stopVideo": + sandbox.stopVideo(); + break; + case "seekTo": + sandbox.seekTo(data.position, data.allowSeekAhead); + break; + case "setVolume": + sandbox.setVolume(data.volume); + break; + default: + console.log("Unknown message received", event); + break; + } + }, false); + + console.log("YouTube sandbox ready."); + +})(); diff --git a/static/js/services/resturl.js b/static/js/services/resturl.js index 4ba4ad73..3ff406ee 100644 --- a/static/js/services/resturl.js +++ b/static/js/services/resturl.js @@ -64,6 +64,11 @@ define(["underscore"], function(_) { } return url.join("/"); }; + RestURL.prototype.createAbsoluteUrl = function(url) { + var link = $window.document.createElement("a"); + link.href = url; + return link.href; + }; return new RestURL(); }]; }); diff --git a/static/js/services/sandbox.js b/static/js/services/sandbox.js new file mode 100644 index 00000000..d3f65a4a --- /dev/null +++ b/static/js/services/sandbox.js @@ -0,0 +1,66 @@ +/* + * Spreed WebRTC. + * Copyright (C) 2013-2015 struktur AG + * + * This file is part of Spreed WebRTC. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +"use strict"; +define(["jquery", "underscore"], function($, _) { + + return ["$window", function($window) { + + var Sandbox = function(iframe, template) { + this.iframe = iframe; + this.iframe.src = "data:text/html;charset=utf-8," + $window.encodeURI(template); + this.target = this.iframe.contentWindow; + this.e = $({}); + this.handler = _.bind(this.onPostMessageReceived, this); + $window.addEventListener("message", this.handler, false); + }; + + Sandbox.prototype.destroy = function() { + if (this.handler) { + $window.removeEventListener("message", this.handler, false); + this.handler = null; + } + }; + + Sandbox.prototype.onPostMessageReceived = function(event) { + if (event.origin !== "null" || event.source !== this.target) { + // the sandboxed data-url iframe has "null" as origin + return; + } + + this.e.triggerHandler("message", [event]); + }; + + Sandbox.prototype.postMessage = function(type, message) { + var msg = {"type": type} + msg[type] = message; + this.target.postMessage(msg, "*"); + }; + + return { + createSandbox: function(iframe, template) { + return new Sandbox(iframe, template); + } + }; + + }]; + +}); diff --git a/static/js/services/services.js b/static/js/services/services.js index b95c52fd..8e7dc20f 100644 --- a/static/js/services/services.js +++ b/static/js/services/services.js @@ -67,7 +67,8 @@ define([ 'services/roompin', 'services/constraints', 'services/modules', - 'services/mediadevices'], function(_, + 'services/mediadevices', + 'services/sandbox'], function(_, desktopNotify, playSound, safeApply, @@ -112,7 +113,8 @@ restURL, roompin, constraints, modules, -mediaDevices) { +mediaDevices, +sandbox) { var services = { desktopNotify: desktopNotify, @@ -159,7 +161,8 @@ mediaDevices) { roompin: roompin, constraints: constraints, modules: modules, - mediaDevices: mediaDevices + mediaDevices: mediaDevices, + sandbox: sandbox }; var initialize = function(angModule) { diff --git a/static/partials/youtubevideo.html b/static/partials/youtubevideo.html index 5fe5e3d3..b7c8350f 100644 --- a/static/partials/youtubevideo.html +++ b/static/partials/youtubevideo.html @@ -31,7 +31,7 @@
-
+
{{_('Currently playing')}}
{{ currentVideoUrl }}
diff --git a/static/partials/youtubevideo_sandbox.html b/static/partials/youtubevideo_sandbox.html new file mode 100644 index 00000000..b178d01d --- /dev/null +++ b/static/partials/youtubevideo_sandbox.html @@ -0,0 +1,26 @@ + + + + YouTube Player Sandbox + + + +
+ + +