From 29fc1baa51bf0f45b188b0863c5cd889155626fc Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Thu, 23 Apr 2015 02:12:12 +0200 Subject: [PATCH 1/8] Initial code to support running YouTube player in a sandboxed iframe. --- static/js/directives/youtubevideo.js | 155 ++++++++++++------- static/partials/youtubevideo.html | 2 +- static/partials/youtubevideo_sandbox.html | 175 ++++++++++++++++++++++ 3 files changed, 277 insertions(+), 55 deletions(-) create mode 100644 static/partials/youtubevideo_sandbox.html diff --git a/static/js/directives/youtubevideo.js b/static/js/directives/youtubevideo.js index 706570b9..986cb1ed 100644 --- a/static/js/directives/youtubevideo.js +++ b/static/js/directives/youtubevideo.js @@ -20,20 +20,65 @@ */ "use strict"; -define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], function($, _, template, BigScreen) { +define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'text!partials/youtubevideo_sandbox.html', 'bigscreen'], function($, _, template, sandboxTemplate, BigScreen) { return ["$window", "$document", "mediaStream", "alertify", "translation", "safeApply", "appData", "$q", function($window, $document, mediaStream, alertify, translation, safeApply, appData, $q) { 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 Sandbox = function(iframe) { + this.iframe = iframe; + this.iframe.src = "data:text/html;charset=utf-8," + encodeURI(sandboxTemplate); + this.target = this.iframe.contentWindow; + }; + + Sandbox.prototype.postMessage = function(type, message) { + var msg = {"type": type} + msg[type] = message; + this.target.postMessage(msg, "*"); + }; + + var SandboxPlayer = function(sandbox, params) { + this.sandbox = sandbox; + 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.getCurrentTime = function() { + // TODO(fancycode): implement me + return 0; + }; var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) { @@ -47,14 +92,49 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], var prevNow = null; var initialState = null; - var stateEvents = { - "-1": "youtube.unstarted", - "0": "youtube.ended", - "1": "youtube.playing", - "2": "youtube.paused", - "3": "youtube.buffering", - "5": "youtube.videocued" + var sandbox = new Sandbox($("#youtubeplayer", $element)[0]); + + var isYouTubeIframeAPIReadyDefer = $q.defer(); + var isYouTubeIframeAPIReady = isYouTubeIframeAPIReadyDefer.promise; + + var onPostMessage = function(event) { + var msg = event.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.event); + scope.$emit(data.event); + }); + break; + default: + console.log("Unknown message received", event); + break; + } }; + + $window.addEventListener("message", onPostMessage, false); + + $scope.$on("$destroy", function() { + $window.removeEventListener("message", onPostMessage, false); + }); + var errorIds = { "2": "invalidParameter", "5": "htmlPlayerError", @@ -79,13 +159,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 +166,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: @@ -131,6 +191,8 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], } var startDetectSeek = function() { + // TODO(fancycode): seek detection should be implemented in the sandbox + /* var checkSeek = function() { if (!player) { return; @@ -160,6 +222,7 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], }, 1000); } checkSeek(); + */ }; var stopDetectSeek = function() { @@ -278,7 +341,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(sandbox, { height: "390", width: "640", playerVars: { @@ -291,10 +354,6 @@ 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(); @@ -475,19 +534,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; - } + sandbox.postMessage("loadApi", {"url": $window.location.protocol + YOUTUBE_IFRAME_API_URL}); }; $scope.showYouTubeVideo = function() { 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..3b434ed8 --- /dev/null +++ b/static/partials/youtubevideo_sandbox.html @@ -0,0 +1,175 @@ + + + + YouTube Player Sandbox + + + +
+ + + From c8ad76005b830768e36dcfdbfd97adbfdd503099 Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Thu, 23 Apr 2015 12:21:04 +0200 Subject: [PATCH 2/8] Added seek and volume support, check origins in postMessage receivers. --- static/js/directives/youtubevideo.js | 89 +++++++---------------- static/partials/youtubevideo_sandbox.html | 82 ++++++++++++++++++++- 2 files changed, 104 insertions(+), 67 deletions(-) diff --git a/static/js/directives/youtubevideo.js b/static/js/directives/youtubevideo.js index 986cb1ed..6290f1aa 100644 --- a/static/js/directives/youtubevideo.js +++ b/static/js/directives/youtubevideo.js @@ -26,9 +26,13 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'text!partial var YOUTUBE_IFRAME_API_URL = "//www.youtube.com/iframe_api"; + var origin = $window.location.protocol + "//" + $window.location.host; + var Sandbox = function(iframe) { this.iframe = iframe; - this.iframe.src = "data:text/html;charset=utf-8," + encodeURI(sandboxTemplate); + var template = sandboxTemplate; + template = template.replace(/__PARENT__ORIGIN__/g, origin); + this.iframe.src = "data:text/html;charset=utf-8," + encodeURI(template); this.target = this.iframe.contentWindow; }; @@ -75,21 +79,28 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'text!partial this.sandbox.postMessage("seekTo", msg); }; + SandboxPlayer.prototype.setVolume = function(volume) { + this.sandbox.postMessage("setVolume", {"volume": volume}); + + }; + SandboxPlayer.prototype.getCurrentTime = function() { // TODO(fancycode): implement me return 0; }; + SandboxPlayer.prototype.getPlayerState = function() { + // TODO(fancycode): implement me + return null; + } + var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) { var addedIframeScript = false; 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 sandbox = new Sandbox($("#youtubeplayer", $element)[0]); @@ -98,6 +109,10 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'text!partial var isYouTubeIframeAPIReady = isYouTubeIframeAPIReadyDefer.promise; var onPostMessage = function(event) { + if (event.origin !== "null" || event.source !== sandbox.target) { + // the sandboxed data-url iframe has "null" as origin + return; + } var msg = event.data; var data = msg[msg.type] || {}; switch (msg.type) { @@ -119,8 +134,8 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'text!partial break; case "youtube.event": $scope.$apply(function(scope) { - console.log("State change", data.event); - scope.$emit(data.event); + console.log("State change", data); + scope.$emit(data.event, data.position); }); break; default: @@ -190,82 +205,34 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'text!partial return null; } - var startDetectSeek = function() { - // TODO(fancycode): seek detection should be implemented in the sandbox - /* - 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; } @@ -276,7 +243,7 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'text!partial mediaStreamSendYouTubeVideo(peercall, currentToken, { Type: "Pause", Pause: { - position: player.getCurrentTime() + position: position } }); }); @@ -284,7 +251,6 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'text!partial }); $scope.$on("youtube.ended", function() { - stopDetectSeek(); }); $scope.$on("youtube.seeked", function($event, position) { @@ -306,8 +272,6 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'text!partial playerReady.done(function() { $("#youtubeplayer").show(); $scope.playbackActive = true; - prevTime = null; - prevNow = null; isPaused = null; if (playReceivedNow) { var delta = ((new Date()) - playReceivedNow) * 0.001; @@ -585,7 +549,6 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'text!partial $scope.currentVideoUrl = null; $scope.currentVideoId = null; peers = {}; - stopDetectSeek(); playerReady = null; initialState = null; mediaStream.webrtc.e.off("statechange", updater); diff --git a/static/partials/youtubevideo_sandbox.html b/static/partials/youtubevideo_sandbox.html index 3b434ed8..bba0e6f1 100644 --- a/static/partials/youtubevideo_sandbox.html +++ b/static/partials/youtubevideo_sandbox.html @@ -24,17 +24,22 @@ - + + From 02bda0367de1cd89d1736048077b3f7c7f768d7e Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Thu, 23 Apr 2015 16:48:29 +0200 Subject: [PATCH 7/8] Merge rule (found by Hound). --- src/styles/components/_youtubevideo.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/styles/components/_youtubevideo.scss b/src/styles/components/_youtubevideo.scss index aebde5ff..75645690 100644 --- a/src/styles/components/_youtubevideo.scss +++ b/src/styles/components/_youtubevideo.scss @@ -92,10 +92,10 @@ #youtubecontainer { // scss-lint:disable IdSelector position: relative; -} -#youtubecontainer.fullscreen { // scss-lint:disable IdSelector - width: 100%; + &.fullscreen { + width: 100%; + } } #youtubeplayerinfo { // scss-lint:disable IdSelector From cbe9c0b51fb9733762388857fa8aa261e5dada0f Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Fri, 24 Apr 2015 11:34:18 +0200 Subject: [PATCH 8/8] Don't use global "encodeURI". --- static/js/services/sandbox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/services/sandbox.js b/static/js/services/sandbox.js index 0bc5e9ef..d3f65a4a 100644 --- a/static/js/services/sandbox.js +++ b/static/js/services/sandbox.js @@ -26,7 +26,7 @@ define(["jquery", "underscore"], function($, _) { var Sandbox = function(iframe, template) { this.iframe = iframe; - this.iframe.src = "data:text/html;charset=utf-8," + encodeURI(template); + 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);