Browse Source

Added support for sharing YouTube videos in a call.

pull/83/head
Joachim Bauch 11 years ago
parent
commit
da4bae5875
  1. 4
      html/main.html
  2. 102
      src/styles/components/_youtubevideo.scss
  3. 1
      src/styles/main.scss
  4. 6
      static/js/directives/directives.js
  5. 483
      static/js/directives/youtubevideo.js
  6. 20
      static/js/mediastream/api.js
  7. 62
      static/partials/youtubevideo.html

4
html/main.html

@ -22,6 +22,7 @@ @@ -22,6 +22,7 @@
<div class="navbar-collapse collapse" collapse="isCollapsed">
<ul class="nav navbar-nav navbar-right right">
<li class="ng-cloak">
<button title="{{_('Share a YouTube video')}}" class="btn aenablebtn btn-youtubevideo" ng-show="status=='connected' || status=='conference' || layout.youtubevideo" ng-model="layout.youtubevideo" btn-checkbox><i class="fa fa-youtube"></i></button>
<button title="{{_('Share a file as presentation')}}" class="btn aenablebtn btn-presentation" ng-show="status=='connected' || status=='conference' || layout.presentation" ng-model="layout.presentation" btn-checkbox><i class="fa fa-folder-open-o"></i></button>
<button title="{{_('Share your screen')}}" class="btn aenablebtn btn-screenshare" ng-disabled="!supported.screensharing" ng-show="status=='connected' || status=='conference' || layout.screenshare" ng-model="layout.screenshare" btn-checkbox><i class="fa fa-desktop"></i></button>
<button title="{{_('Chat')}}" class="btn btn-chat" ng-class="{messagesunseen: chatMessagesUnseen>0}" ng-model="layout.chat" btn-checkbox btn-checkbox-true="true" btn-checkbox-false="false"><i class="fa fa-comments-o"></i><span class="badge" ng-show="chatMessagesUnseen" ng-bind="chatMessagesUnseen"></span></button>
@ -51,6 +52,9 @@ @@ -51,6 +52,9 @@
<div id="presentation" class="ng-cloak mainview">
<presentation/>
</div>
<div id="youtubevideo" class="ng-cloak mainview">
<youtubevideo/>
</div>
<div class="ng-cloak" id="rightslide">
<div class="rightslidepane">
<div id="buddylist"><buddy-list/></div>

102
src/styles/components/_youtubevideo.scss

@ -0,0 +1,102 @@ @@ -0,0 +1,102 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 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 <http://www.gnu.org/licenses/>.
*
*/
.youtubevideo {
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 0;
}
.youtubevideopane {
bottom: 0;
left: 0;
overflow: auto;
position: absolute;
right: 0;
top: 0;
}
#youtubecontainer {
position: relative;
}
#youtubeplayerinfo {
position: absolute;
left: 0;
right: 0;
bottom: 10%;
opacity: 0;
pointer-events: auto;
text-align: center;
transition-property: opacity;
transition-duration: .2s;
z-index: 10;
&:hover {
opacity: .8;
}
}
#youtubeplayerinfo div {
background-color: #f9f2f4;
border-radius: 10px;
padding: 20px 40px;
display: inline-block;
font-size: 2em;
}
.youtubevideo .welcome {
max-width: 700px;
}
.youtubevideo .welcome-container {
max-width: 700px;
}
.youtubevideo .welcome-logo {
font-size: 10em;
background: transparent;
}
.mainYoutubevideo #youtubevideo {
display: block;
}
.youtubevideo .row {
margin-bottom: 15px;
}
.youtubevideo label {
color: white;
text-shadow: 1px 1px 1px $text-color;
}
.youtubevideo .overlaybar {
bottom: 0;
left: 0;
right: 0;
}
.youtubevideo .overlaybar-content {
width: 100%;
max-width: 100%;
}

1
src/styles/main.scss

@ -50,5 +50,6 @@ @@ -50,5 +50,6 @@
@import "components/social";
@import "components/contactsmanager";
@import "components/presentation";
@import "components/youtubevideo";
@import "shame";

6
static/js/directives/directives.js

@ -40,7 +40,8 @@ define([ @@ -40,7 +40,8 @@ define([
'directives/defaultdialog',
'directives/pdfcanvas',
'directives/odfcanvas',
'directives/presentation'], function(_, onEnter, onEscape, statusMessage, buddyList, buddyPicture, settings, chat, audioVideo, usability, audioLevel, fileInfo, screenshare, roomBar, socialShare, page, contactRequest, defaultDialog, pdfcanvas, odfcanvas, presentation) {
'directives/presentation',
'directives/youtubevideo',], function(_, onEnter, onEscape, statusMessage, buddyList, buddyPicture, settings, chat, audioVideo, usability, audioLevel, fileInfo, screenshare, roomBar, socialShare, page, contactRequest, defaultDialog, pdfcanvas, odfcanvas, presentation, youtubevideo) {
var directives = {
onEnter: onEnter,
@ -62,7 +63,8 @@ define([ @@ -62,7 +63,8 @@ define([
defaultDialog: defaultDialog,
pdfcanvas: pdfcanvas,
odfcanvas: odfcanvas,
presentation: presentation
presentation: presentation,
youtubevideo: youtubevideo
};
var initialize = function(angModule) {

483
static/js/directives/youtubevideo.js

@ -0,0 +1,483 @@ @@ -0,0 +1,483 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 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 <http://www.gnu.org/licenses/>.
*
*/
define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], function($, _, template, BigScreen) {
return ["$window", "mediaStream", "alertify", "translation", "safeApply", "appData", function($window, mediaStream, alertify, translation, safeApply, appData) {
var isYouTubeIframeAPIReady = $.Deferred();
$window.onYouTubeIframeAPIReady = function() {
console.log("YouTube IFrame ready");
isYouTubeIframeAPIReady.resolve();
};
var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
var addedIframeScript = false;
var player = null;
var playerReady = null;
var isPublisher = null;
var isPaused = null;
var seekDetector = null;
var prevTime = null;
var prevNow = 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",
"100": "videoNotFound",
"101": "notAllowedEmbedded",
"150": "notAllowedEmbedded"
};
$scope.playbackActive = false;
$scope.hideControlsBar = true;
$scope.currentVideoUrl = null;
$scope.youtubeurl = "http://www.youtube.com/watch?v=_C92v6uKCIU";
var onPlayerReady = function(event) {
$scope.$apply(function(scope) {
playerReady.resolve();
});
};
var onPlayerError = function(event) {
var error = errorIds[event.data] || "unknownError";
$scope.$apply(function(scope) {
scope.$emit("youtube.error", error);
});
};
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:
* http://www.youtube.com/watch?v=0zM3nApSvMg&feature=feedrec_grec_index
* http://www.youtube.com/user/IngridMichaelsonVEVO#p/a/u/1/QdK8U-VIH_o
* http://www.youtube.com/v/0zM3nApSvMg?fs=1&amp;hl=en_US&amp;rel=0
* http://www.youtube.com/watch?v=0zM3nApSvMg#t=0m10s
* http://www.youtube.com/embed/0zM3nApSvMg?rel=0
* http://www.youtube.com/watch?v=0zM3nApSvMg
* http://youtu.be/0zM3nApSvMg
*
* Source: http://lasnv.net/foro/839/Javascript_parsear_URL_de_YouTube
*/
if (!url) {
return null;
}
var regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?]*).*/;
var match = url.match(regExp);
if (match && match[7].length == 11) {
return match[7];
}
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() {
prevTime = null;
startDetectSeek();
if (isPaused) {
isPaused = false;
mediaStream.webrtc.callForEachCall(function(peercall) {
mediaStreamSendYouTubeVideo(peercall, currentToken, {
Type: "Resume",
Resume: true
});
});
}
});
$scope.$on("youtube.buffering", function() {
startDetectSeek();
});
$scope.$on("youtube.paused", function() {
stopDetectSeek();
if (!isPublisher || !currentToken) {
return;
}
if (!isPaused) {
isPaused = true;
mediaStream.webrtc.callForEachCall(function(peercall) {
mediaStreamSendYouTubeVideo(peercall, currentToken, {
Type: "Pause",
Pause: true
});
});
}
});
$scope.$on("youtube.ended", function() {
stopDetectSeek();
});
$scope.$on("youtube.seeked", function($event, position) {
if (!isPublisher || !currentToken) {
return;
}
mediaStream.webrtc.callForEachCall(function(peercall) {
mediaStreamSendYouTubeVideo(peercall, currentToken, {
Type: "Seek",
Seek: {
"position": position
}
});
});
});
var playVideo = function(id) {
playerReady.done(function() {
$("#youtubeplayer").show();
$scope.playbackActive = true;
prevTime = null;
prevNow = null;
isPaused = null;
player.loadVideoById(id);
});
};
var createVideoPlayer = function(with_controls) {
if (player && isPublisher !== with_controls) {
player.destroy();
player = null;
playerReady = null;
}
if (!playerReady) {
playerReady = $.Deferred();
}
isYouTubeIframeAPIReady.done(function() {
if (!player) {
var origin = $window.location.protocol + "//" + $window.location.host;
player = new YT.Player("youtubeplayer", {
height: "390",
width: "640",
playerVars: {
"enablejsapi": "1",
"hl": appData.language || "en",
"autohide": "1", // hide all controls on playback
"rel": "0", // don't show related videos on end
"showinfo": "0", // don't show title/uploader before start
"playsinline": "1", // play inline on iOS if possible
"controls": with_controls ? "2" : "0",
"disablekb": with_controls ? "0" : "1",
"origin": origin
},
events: {
"onReady": onPlayerReady,
"onStateChange": onPlayerStateChange
}
});
isPublisher = with_controls;
}
});
};
$scope.shareVideo = function(url) {
var id = getYouTubeId(url);
if (!id) {
alertify.dialog.alert(translation._("Unknown URL format. Please make sure to enter a valid YouTube URL."));
return;
}
mediaStream.webrtc.callForEachCall(function(peercall) {
mediaStreamSendYouTubeVideo(peercall, currentToken, {
Type: "Play",
Play: {
"url": url,
"id": id
}
});
});
createVideoPlayer(true);
$scope.youtubeurl = "";
$scope.currentVideoUrl = url;
playVideo(id);
};
mediaStream.api.e.on("received.youtubevideo", function(event, id, from, data, p2p) {
if (!p2p) {
console.warn("Received YouTubeVideo info without p2p. This should not happen!");
return;
}
if (data.Type) {
switch (data.Type) {
case "Show":
console.log("Received YouTubeVideo show request", data);
$scope.$apply(function(scope) {
scope.layout.youtubevideo = true;
});
break;
case "Hide":
console.log("Received YouTubeVideo hide request", data);
$scope.$apply(function(scope) {
scope.layout.youtubevideo = false;
});
break;
case "Play":
console.log("Received YouTubeVideo play request", data);
$scope.$apply(function(scope) {
createVideoPlayer(false);
scope.currentVideoUrl = data.Play.url;
playVideo(data.Play.id);
});
break;
case "Pause":
console.log("Received YouTubeVideo pause request", data);
$scope.$apply(function(scope) {
if (player) {
player.pauseVideo();
}
});
break;
case "Resume":
console.log("Received YouTubeVideo resume request", data);
$scope.$apply(function(scope) {
if (player) {
player.playVideo();
}
});
break;
case "Seek":
console.log("Received YouTubeVideo seek request", data);
$scope.$apply(function(scope) {
if (player) {
player.seekTo(data.Seek.position);
}
});
break;
default:
console.log("Received unknown YouTubeVideo event", data);
}
}
});
var peers = {};
var youtubevideos = [];
var youtubevideoCount = 0;
var currentToken = null;
var tokenHandler = null;
var mediaStreamSendYouTubeVideo = function(peercall, token, params) {
mediaStream.api.apply("sendYouTubeVideo", {
send: function(type, data) {
if (!peercall.peerconnection.datachannelReady) {
return peercall.e.one("dataReady", function() {
peercall.peerconnection.send(data);
});
} else {
return peercall.peerconnection.send(data);
}
}
})(peercall.id, token, params);
};
var connector = function(token, peercall) {
if (peers.hasOwnProperty(peercall.id)) {
// Already got a connection.
return;
}
peers[peercall.id] = true;
mediaStreamSendYouTubeVideo(peercall, token, {
Type: "Show",
Show: true
});
};
// Updater function to bring in new calls.
var updater = function(event, state, currentcall) {
switch (state) {
case "completed":
case "connected":
connector(currentToken, currentcall);
break;
case "closed":
delete peers[currentcall.id];
if (_.isEmpty(peers)) {
console.log("All peers disconnected, stopping youtubevideo");
$scope.$apply(function(scope) {
scope.hideYouTubeVideo();
});
}
break;
}
};
$scope.showYouTubeVideo = function() {
if (!addedIframeScript) {
$element.append($('<script src="https://www.youtube.com/iframe_api"></script>'));
addedIframeScript = true;
}
$scope.layout.youtubevideo = true;
$scope.$emit("mainview", "youtubevideo", true);
if (currentToken) {
mediaStream.tokens.off(currentToken, tokenHandler);
}
// Create token to register with us and send token out to all peers.
// Peers when connect to us with the token and we answer.
currentToken = "youtubevideo_" + $scope.id + "_" + (youtubevideoCount++);
// Create callbacks are called for each incoming connections.
tokenHandler = mediaStream.tokens.create(currentToken, function(event, currenttoken, to, data, type, to2, from, peer) {
console.log("YouTubeVideo create", currenttoken, data, type, peer);
youtubevideos.push(peer);
}, "youtubevideo");
// Connect all current calls.
mediaStream.webrtc.callForEachCall(function(peercall) {
connector(currentToken, peercall);
});
// Catch later calls too.
mediaStream.webrtc.e.on("statechange", updater);
};
$scope.hideYouTubeVideo = function() {
$scope.$emit("mainview", "youtubevideo", false);
$scope.layout.youtubevideo = false;
if (currentToken) {
mediaStream.webrtc.callForEachCall(function(peercall) {
mediaStreamSendYouTubeVideo(peercall, currentToken, {
Type: "Hide",
Hide: true
});
});
mediaStream.tokens.off(currentToken, tokenHandler);
currentToken = null;
}
if (player) {
player.destroy();
player = null;
}
isPublisher = null;
$scope.playbackActive = false;
peers = {};
stopDetectSeek();
playerReady = null;
mediaStream.webrtc.e.off("statechange", updater);
};
$scope.$watch("layout.youtubevideo", function(newval, oldval) {
if (newval && !oldval) {
$scope.showYouTubeVideo();
} else if (!newval && oldval) {
$scope.hideYouTubeVideo();
}
});
$scope.$watch("layout.main", function(newval, oldval) {
if (newval && newval !== "youtubevideo") {
$scope.hideYouTubeVideo();
}
});
}];
var compile = function(tElement, tAttr) {
return function(scope, iElement, iAttrs, controller) {
$(iElement).on("dblclick", ".videoContainer", _.debounce(function(event) {
scope.toggleFullscreen(event.delegateTarget);
}, 100, true));
}
};
return {
restrict: 'E',
replace: true,
scope: true,
template: template,
controller: controller,
compile: compile
};
}];
});

20
static/js/mediastream/api.js

@ -192,6 +192,9 @@ define(['jquery', 'underscore'], function($, _) { @@ -192,6 +192,9 @@ define(['jquery', 'underscore'], function($, _) {
case "Presentation":
this.e.triggerHandler("received.presentation", [d.To, d.From, data.Presentation, d.p2p]);
break;
case "YouTubeVideo":
this.e.triggerHandler("received.youtubevideo", [d.To, d.From, data.YouTubeVideo, d.p2p]);
break;
case "Alive":
// Do nothing.
//console.log("Alive response received.");
@ -360,6 +363,23 @@ define(['jquery', 'underscore'], function($, _) { @@ -360,6 +363,23 @@ define(['jquery', 'underscore'], function($, _) {
};
Api.prototype.sendYouTubeVideo = function(id, video_id, video_data) {
var data = {
Id: id,
Type: "YouTubeVideo",
YouTubeVideo: {
id: video_id
}
}
if (video_data) {
data.YouTubeVideo = _.extend(data.YouTubeVideo, video_data);
}
return this.send("YouTubeVideo", data);
};
Api.prototype.sendAlive = function(timestamp) {
var data = {

62
static/partials/youtubevideo.html

@ -0,0 +1,62 @@ @@ -0,0 +1,62 @@
<div class="youtubevideo">
<div class="youtubevideopane nicescroll">
<form class="container-fluid form" role="form">
<div class="welcome container-fluid" ng-show="!playbackActive">
<div class="welcome-logo fa fa-youtube"></div>
<h1>{{_("Share a video with everyone in the call")}}</h1>
<div class="welcome-container">
<p>
<div class="form-group welcome-input">
<input type="text" class="form-control input-lg" ng-model="youtubeurl" placeholder="{{_('YouTube URL')}}">
<div class="welcome-input-buttons">
<button class="btn btn-primary" type="button" ng-disabled="!youtubeurl" ng-click="shareVideo(youtubeurl)">{{_("Share")}}</button>
</div>
</div>
</p>
<center>
<p ng-show="roomdata.name"><i class="fa fa-external-link"></i> <a href="{{roomdata.link}}" ng-click="changeRoomToId(roomdata.name);$event.preventDefault()">{{roomdata.link}}</a></p>
</center>
</div>
</div>
<!--
<div class="row welcome" ng-show="!playbackActive">
<h1 class="fa fa-youtube"></h1>
<div class="center-block">
<p>{{_('Videos are shared with everyone in this call.')}}</p>
</div>
</div>
-->
<div ng-show="playbackActive">
<div class="row" id="youtubecontainer">
<div class="embed-responsive embed-responsive-16by9">
<div id="youtubeplayer"></div>
</div>
<div id="youtubeplayerinfo">
<div>{{_('Currently playing')}}<br>{{ currentVideoUrl }}</div>
</div>
</div>
</div>
</form>
</div>
<div class="overlaybar form-horizontal" ng-class="{notvisible: hideControlsBar}">
<a class="overlaybar-button" ng-model="hideControlsBar" btn-checkbox btn-checkbox btn-checkbox-true="0" btn-checkbox-false="1" title="{{_('YouTube controls')}}"><i class="fa fa-cogs"></i></a>
<div class="overlaybar-content">
<div class="container-fluid form-horizontal">
<div class="form-group">
<label class="col-sm-2" for="youtubeurl">YouTube URL to share</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="youtubeurl" ng-model="youtubeurl" required placeholder="{{_('YouTube URL')}}">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="button" class="btn btn-primary" ng-click="shareVideo(youtubeurl)" ng-disabled="youtubeurl === ''">{{_('Share')}}</button>
</div>
</div>
</div>
</div>
</div>
</div>
Loading…
Cancel
Save