/* * 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", "modernizr", "injectCSS"], function($, _, Modernizr) { var renderers = {}; var defaultSize = { width: 640, height: 360 }; var defaultAspectRatio = defaultSize.width/defaultSize.height; var getRemoteVideoSize = function(videos, streams) { var size = { width: defaultSize.width, height: defaultSize.height } if (videos.length) { if (videos.length === 1) { var remoteVideo = streams[videos[0]].element.find("video")[0]; if (remoteVideo) { size.width = remoteVideo.videoWidth; size.height = remoteVideo.videoHeight; console.log("Remote video size: ", size); } } } return size; }; var dynamicCSSContainer = "audiovideo-dynamic"; var injectCSS = function(css) { $.injectCSS(css, { containerName: dynamicCSSContainer, truncateFirst: true, useRawValues: true }); }; var objectFitSupport = Modernizr["object-fit"] && true; // videoLayout return ["$window", function($window) { // Invisible layout (essentially shows nothing). var Invisible = function(container, scope, controller) {}; Invisible.prototype.name = "invisible"; Invisible.prototype.render = function() {}; Invisible.prototype.close = function() {}; // Video layout with all videos rendered the same size. var OnePeople = function(container, scope, controller) {}; OnePeople.prototype.name = "onepeople"; OnePeople.prototype.render = function(container, size, scope, videos, streams) { if (this.closed) { return; } var videoWidth; var videoHeight; if (videos.length) { var remoteSize = getRemoteVideoSize(videos, streams); videoWidth = remoteSize.width; videoHeight = remoteSize.height; } if (!videoWidth) { // XXX(longsleep): Improve this condition - its crap to compare style opacity (tm)! if (scope.localVideo.style.opacity === '1') { videoWidth = scope.localVideo.videoWidth; videoHeight = scope.localVideo.videoHeight; videos = [null]; } } if (!videos.length) { return; } if (!videoWidth) { videoWidth = defaultSize.width; } if (!videoHeight) { videoHeight = defaultSize.height; } if (this.countSelfAsRemote) { videos.unshift(null); } var innerHeight = size.height; var innerWidth = size.width; // We use the same aspect ratio to make all videos look the same. var aspectRatio = defaultAspectRatio; //console.log("resize", innerHeight, innerWidth); //console.log("resize", container, videos.length, aspectRatio, innerHeight, innerWidth); var extraCSS = {}; // Always set size of mini video. extraCSS[".renderer-"+this.name+" .miniVideo"] = { width: ($(scope.mini).height() * defaultAspectRatio) + "px" }; var space = innerHeight * innerWidth; // square pixels var videoSpace = space / videos.length; var singleVideoWidthOptimal = Math.pow(videoSpace * aspectRatio, 0.5); var videosPerRow = Math.ceil(innerWidth / singleVideoWidthOptimal); if (videosPerRow > videos.length) { videosPerRow = videos.length; } var singleVideoWidth = Math.ceil(innerWidth / videosPerRow); var singleVideoHeight = Math.ceil(singleVideoWidth / aspectRatio); var newContainerWidth = (videosPerRow * singleVideoWidth); var newContainerHeight = Math.ceil(videos.length / videosPerRow) * singleVideoHeight; if (newContainerHeight > innerHeight) { var tooHigh = (newContainerHeight - innerHeight) / Math.ceil(videos.length / videosPerRow); singleVideoHeight -= tooHigh; singleVideoWidth = singleVideoHeight * aspectRatio; } /* console.log("space", space); console.log("videospace", videoSpace); console.log("singleVideoWidthOptimal", singleVideoWidthOptimal); console.log("videosPerRow", videosPerRow); console.log("singleVideoWidth", singleVideoWidth); console.log("singleVideoHeight", singleVideoHeight); */ container.style.width = newContainerWidth + "px"; container.style.left = ((innerWidth - newContainerWidth) / 2) + 'px'; extraCSS[".renderer-"+this.name+" .remoteVideos"] = { ">div": { width: singleVideoWidth + "px", height: singleVideoHeight + "px" } }; injectCSS(extraCSS); }; OnePeople.prototype.close = function(container, scope, controller) { this.closed = true; }; // Smally inherits from OnePeople var Smally = function(container, scope, controller) { // Call super. OnePeople.call(this, container, scope, controller); } Smally.prototype = Object.create(OnePeople.prototype); Smally.prototype.constructor = Smally; Smally.prototype.name = "smally"; // Democrazy inherits from OnePeople var Democrazy = function(container, scope, controller) { // Call super. OnePeople.call(this, container, scope, controller); // Move mini video into remoteVideos. var $mini = $(scope.mini); this.miniParent = $mini.parent(); $mini.prependTo(scope.remoteVideos); $mini.find("video")[0].play(); this.countSelfAsRemote = true; } Democrazy.prototype = Object.create(OnePeople.prototype); Democrazy.prototype.constructor = Democrazy; Democrazy.prototype.name = "democrazy"; Democrazy.prototype.close = function(container, scope, controller) { OnePeople.prototype.close.call(this, container, scope, controller); var $mini = $(scope.mini); $mini.appendTo(this.miniParent); $mini.find("video")[0].play(); this.miniParent = null; }; // A view with one selectable large video. The others are small. var ConferenceKiosk = function(container, scope, controller) { this.remoteVideos = $(container).find(".remoteVideos"); this.bigVideo = $("
").addClass("bigVideo")[0]; this.remoteVideos.before(this.bigVideo); this.big = null; this.remoteVideos.on("click", ".remoteVideo", _.bind(function(event) { if ($(event.currentTarget).hasClass("remoteVideo")) { event.stopPropagation(); this.makeBig($(event.currentTarget)); } }, this)); }; ConferenceKiosk.prototype.name = "conferencekiosk"; ConferenceKiosk.prototype.makeBig = function(remoteVideo) { if (this.big === remoteVideo) { return; } if (this.big) { // Add old video back. this.big.insertAfter(remoteVideo); this.big.find("video")[0].play(); } this.big = remoteVideo; remoteVideo.appendTo(this.bigVideo); remoteVideo.find("video")[0].play(); }; ConferenceKiosk.prototype.render = function(container, size, scope, videos, streams) { var big = this.big; if (big) { var currentbigpeerid = this.big.data("peerid"); if (!streams[currentbigpeerid]) { console.log("Current big peer is no longer there", currentbigpeerid); this.big = big = null; } } if (!big) { if (videos.length) { this.makeBig(streams[videos[0]].element); this.bigVideo.style.opacity = 1; } } var innerHeight = size.height - 110; var innerWidth = size.width; var extraCSS = {}; // Use the same aspect ratio for all videos. var aspectRatio = defaultAspectRatio; var bigVideoWidth = innerWidth < aspectRatio * innerHeight ? innerWidth : aspectRatio * innerHeight; var bigVideoHeight = innerHeight < innerWidth / aspectRatio ? innerHeight : innerWidth / aspectRatio; // Make space for own video on the right if width goes low. if (((size.width - (videos.length - 1) * 192) / 2) < 192) { extraCSS[".renderer-"+this.name+" .remoteVideos"] = { "margin-right": "192px", "overflow-x": "auto", "overflow-y": "hidden" }; } // Big video size. extraCSS[".renderer-"+this.name+" .bigVideo .remoteVideo"] = { "height": bigVideoHeight + 'px', "width": bigVideoWidth + 'px', "margin": "auto", "display": "block" }; injectCSS(extraCSS); }; ConferenceKiosk.prototype.close = function(container, scope, controller) { this.closed = true; if (this.big) { this.remoteVideos.append(this.big); this.big.find("video")[0].play(); } this.big = null; this.bigVideo.remove() this.bigVideo = null; this.remoteVideos = null; }; // Auditorium inherits from ConferenceKiosk var Auditorium = function(container, scope, controller) { // Call super. ConferenceKiosk.call(this, container, scope, controller); } Auditorium.prototype = Object.create(ConferenceKiosk.prototype); Auditorium.prototype.constructor = Auditorium; Auditorium.prototype.name = "auditorium"; Auditorium.prototype.render = function(container, size, scope, videos, streams) { var big = this.big; if (big) { var currentbigpeerid = this.big.data("peerid"); if (!streams[currentbigpeerid]) { console.log("Current big peer is no longer there", currentbigpeerid); this.big = big = null; } } if (!big) { if (videos.length) { this.makeBig(streams[videos[0]].element); this.bigVideo.style.opacity = 1; } } var extraCSS = {}; // Always set size of mini video. extraCSS[".renderer-"+this.name+" .miniVideo"] = { width: ($(scope.mini).height() * defaultAspectRatio) + "px" }; injectCSS(extraCSS); }; // Register renderers. renderers[Invisible.prototype.name] = Invisible; renderers[OnePeople.prototype.name] = OnePeople; renderers[Smally.prototype.name] = Smally; renderers[Democrazy.prototype.name] = Democrazy; renderers[ConferenceKiosk.prototype.name] = ConferenceKiosk; renderers[Auditorium.prototype.name] = Auditorium; // Helper for class name generation. var makeName = function(prefix, n, camel) { var r = prefix; if (camel) { r = r + n.charAt(0).toUpperCase() + n.slice(1); } else { r = r + "-" + n; } return r; }; // Public api. var current = null; var body = $("body"); return { update: function(name, size, scope, controller) { var videos = _.keys(controller.streams); var streams = controller.streams; var container = scope.container; var layoutparent = scope.layoutparent; if (!current) { current = new renderers[name](container, scope, controller) console.log("Created new video layout renderer", name, current); $(layoutparent).addClass(makeName("renderer", name)); body.addClass(makeName("videolayout", name, true)); return true; } else if (current && current.name !== name) { current.close(container, scope, controller); $(container).removeAttr("style"); $(layoutparent).removeClass(makeName("renderer", current.name)); body.removeClass(makeName("videolayout", current.name, true)); current = new renderers[name](container, scope, controller) $(layoutparent).addClass(makeName("renderer", name)); body.addClass(makeName("videolayout", name, true)); console.log("Switched to new video layout renderer", name, current); return true; } return current.render(container, size, scope, videos, streams); }, register: function(name, impl) { renderers[name] = impl; }, layouts: function() { return _.keys(renderers); } } }]; });