youtubeVideos.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  3. * You can obtain one at http:// mozilla.org/MPL/2.0/. */
  4. define(["jquery", "util", "session", "elementFinder"],
  5. function ($, util, session, elementFinder) {
  6. // constant var to indicate whether two players are too far apart in sync
  7. var TOO_FAR_APART = 3000;
  8. // embedded youtube iframes
  9. var youTubeIframes = [];
  10. // youtube API load delay
  11. var API_LOADING_DELAY = 2000;
  12. session.on("reinitialize", function () {
  13. if (TogetherJS.config.get("youtube")) {
  14. prepareYouTube();
  15. }
  16. });
  17. session.on("close", function () {
  18. $(youTubeIframes).each(function (i, iframe) {
  19. // detach players from iframes
  20. $(iframe).removeData("togetherjs-player");
  21. $(iframe).removeData("dontPublish");
  22. $(iframe).removeData("currentVideoId");
  23. // disable iframeAPI
  24. $(iframe).removeAttr("enablejsapi");
  25. // remove unique youtube iframe indicators
  26. var id = $(iframe).attr("id") || "";
  27. if (id.indexOf("youtube-player") === 0) {
  28. // An id we added
  29. $(iframe).removeAttr("id");
  30. }
  31. youTubeIframes = [];
  32. });
  33. });
  34. $(function() {
  35. TogetherJS.config.track("youtube", function (track, previous) {
  36. if (track && ! previous) {
  37. prepareYouTube();
  38. // You can enable youtube dynamically, but can't turn it off:
  39. TogetherJS.config.close("youtube");
  40. }
  41. });
  42. });
  43. var youtubeHooked = false;
  44. function prepareYouTube() {
  45. // setup iframes first
  46. setupYouTubeIframes();
  47. // this function should be global so it can be called when API is loaded
  48. if (!youtubeHooked) {
  49. youtubeHooked = true;
  50. window.onYouTubeIframeAPIReady = (function(oldf) {
  51. return function() {
  52. // YouTube API is ready
  53. $(youTubeIframes).each(function (i, iframe) {
  54. var player = new YT.Player(iframe.id, { // get the reference to the already existing iframe
  55. events: {
  56. 'onReady': insertPlayer,
  57. 'onStateChange': publishPlayerStateChange
  58. }
  59. });
  60. });
  61. if (oldf) {
  62. return oldf();
  63. }
  64. };
  65. })(window.onYouTubeIframeAPIReady);
  66. }
  67. if (window.YT === undefined) {
  68. // load necessary API
  69. // it calls onYouTubeIframeAPIReady automatically when the API finishes loading
  70. var tag = document.createElement('script');
  71. tag.src = "https://www.youtube.com/iframe_api";
  72. var firstScriptTag = document.getElementsByTagName('script')[0];
  73. firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
  74. } else {
  75. // manually invoke APIReady function when the API was already loaded by user
  76. onYouTubeIframeAPIReady();
  77. }
  78. // give each youtube iframe a unique id and set its enablejsapi param to true
  79. function setupYouTubeIframes() {
  80. var iframes = $('iframe');
  81. iframes.each(function (i, iframe) {
  82. // if the iframe's unique id is already set, skip it
  83. // FIXME: what if the user manually sets an iframe's id (i.e. "#my-youtube")?
  84. // maybe we should set iframes everytime togetherjs is reinitialized?
  85. var osrc = $(iframe).attr("src"), src = osrc;
  86. if ((src || "").indexOf("youtube") != -1 && !$(iframe).attr("id")) {
  87. $(iframe).attr("id", "youtube-player"+i);
  88. $(iframe).attr("enablejsapi", 1);
  89. // we also need to add ?enablejsapi to the iframe src.
  90. if (!/[?&]enablejsapi=1(&|$)/.test(src)) {
  91. src += (/[?]/.test(src)) ? '&' : '?';
  92. src += 'enablejsapi=1';
  93. }
  94. // the youtube API seems to be unhappy unless the URL starts
  95. // with https
  96. if (!/^https[:]\/\//.test(src)) {
  97. src = 'https://' + src.replace(/^(\w+[:])?\/\//, '');
  98. }
  99. if (src !== osrc) {
  100. $(iframe).attr("src", src);
  101. }
  102. youTubeIframes[i] = iframe;
  103. }
  104. });
  105. } // iframes are ready
  106. function insertPlayer(event) {
  107. // only when it is READY, attach a player to its iframe
  108. var currentPlayer = event.target;
  109. var currentIframe = currentPlayer.getIframe();
  110. // check if a player is already attached in case of being reinitialized
  111. if (!$(currentIframe).data("togetherjs-player")) {
  112. $(currentIframe).data("togetherjs-player", currentPlayer);
  113. // initialize its dontPublish flag as well
  114. $(currentIframe).data("dontPublish", false);
  115. // store its current video's id
  116. var currentVideoId = getVideoIdFromUrl(currentPlayer.getVideoUrl());
  117. $(currentIframe).data("currentVideoId", currentVideoId);
  118. }
  119. }
  120. } // end of prepareYouTube
  121. function publishPlayerStateChange(event) {
  122. var target = event.target;
  123. var currentIframe = target.getIframe();
  124. //var currentPlayer = $(currentIframe).data("togetherjs-player");
  125. var currentPlayer = target;
  126. var currentTime = currentPlayer.getCurrentTime();
  127. //var currentTime = target.k.currentTime;
  128. var iframeLocation = elementFinder.elementLocation(currentIframe);
  129. if ($(currentPlayer).data("seek")) {
  130. $(currentPlayer).removeData("seek");
  131. return;
  132. }
  133. // do not publish if playerState was changed by other users
  134. if ($(currentIframe).data("dontPublish")) {
  135. // make it false again so it can start publishing events of its own state changes
  136. $(currentIframe).data("dontPublish", false);
  137. return;
  138. }
  139. // notify other people that I changed the player state
  140. if (event.data == YT.PlayerState.PLAYING) {
  141. var currentVideoId = isDifferentVideoLoaded(currentIframe);
  142. if (currentVideoId) {
  143. // notify that I just loaded another video
  144. publishDifferentVideoLoaded(iframeLocation, currentVideoId);
  145. // update current video id
  146. $(currentIframe).data("currentVideoId", currentVideoId);
  147. } else {
  148. session.send({
  149. type: "playerStateChange",
  150. element: iframeLocation,
  151. playerState: 1,
  152. playerTime: currentTime
  153. });
  154. }
  155. } else if (event.data == YT.PlayerState.PAUSED) {
  156. session.send({
  157. type: "playerStateChange",
  158. element: iframeLocation,
  159. playerState: 2,
  160. playerTime: currentTime
  161. });
  162. } else {
  163. // do nothing when the state is buffering, cued, or ended
  164. return;
  165. }
  166. }
  167. function publishDifferentVideoLoaded(iframeLocation, videoId) {
  168. session.send({
  169. type: "differentVideoLoaded",
  170. videoId: videoId,
  171. element: iframeLocation
  172. });
  173. }
  174. session.hub.on('playerStateChange', function (msg) {
  175. var iframe = elementFinder.findElement(msg.element);
  176. var player = $(iframe).data("togetherjs-player");
  177. var currentTime = player.getCurrentTime();
  178. var currentState = player.getPlayerState();
  179. if (currentState != msg.playerState) {
  180. $(iframe).data("dontPublish", true);
  181. }
  182. if (msg.playerState == 1) {
  183. player.playVideo();
  184. // seekTo() updates the video's time and plays it if it was already playing
  185. // and pauses it if it was already paused
  186. if (areTooFarApart(currentTime, msg.playerTime)) {
  187. player.seekTo(msg.playerTime, true);
  188. }
  189. } else if (msg.playerState == 2) {
  190. // When YouTube videos are advanced while playing,
  191. // Chrome: pause -> pause -> play (onStateChange is called even when it is from pause to pause)
  192. // FireFox: buffering -> play -> buffering -> play
  193. // We must prevent advanced videos from going out of sync
  194. player.pauseVideo();
  195. if (areTooFarApart(currentTime, msg.playerTime)) {
  196. // "seek" flag will help supress publishing unwanted state changes
  197. $(player).data("seek", true);
  198. player.seekTo(msg.playerTime, true);
  199. }
  200. }
  201. });
  202. // if a late user joins a channel, synchronize his videos
  203. session.hub.on('hello', function () {
  204. // wait a couple seconds to make sure the late user has finished loading API
  205. setTimeout(synchronizeVideosOfLateGuest, API_LOADING_DELAY);
  206. });
  207. session.hub.on('synchronizeVideosOfLateGuest', function (msg) {
  208. // XXX can this message arrive before we're initialized?
  209. var iframe = elementFinder.findElement(msg.element);
  210. var player = $(iframe).data("togetherjs-player");
  211. // check if another video had been loaded to an existing iframe before I joined
  212. var currentVideoId = getVideoIdFromUrl(player.getVideoUrl());
  213. if (msg.videoId != currentVideoId) {
  214. $(iframe).data("currentVideoId", msg.videoId);
  215. player.loadVideoById(msg.videoId, msg.playerTime, 'default');
  216. } else {
  217. // if the video is only cued, I do not have to do anything to sync
  218. if (msg.playerState != 5) {
  219. player.seekTo(msg.playerTime, true).playVideo();
  220. }
  221. }
  222. });
  223. session.hub.on('differentVideoLoaded', function (msg) {
  224. // load a new video if the host has loaded one
  225. var iframe = elementFinder.findElement(msg.element);
  226. var player = $(iframe).data("togetherjs-player");
  227. player.loadVideoById(msg.videoId, 0, 'default');
  228. $(iframe).data("currentVideoId", msg.videoId);
  229. });
  230. function synchronizeVideosOfLateGuest() {
  231. youTubeIframes.forEach(function (iframe) {
  232. var currentPlayer = $(iframe).data("togetherjs-player");
  233. var currentVideoId = getVideoIdFromUrl(currentPlayer.getVideoUrl());
  234. var currentState = currentPlayer.getPlayerState();
  235. var currentTime = currentPlayer.getCurrentTime();
  236. var iframeLocation = elementFinder.elementLocation(iframe);
  237. session.send({
  238. type: "synchronizeVideosOfLateGuest",
  239. element: iframeLocation,
  240. videoId: currentVideoId,
  241. playerState: currentState, //this might be necessary later
  242. playerTime: currentTime
  243. });
  244. });
  245. }
  246. function isDifferentVideoLoaded(iframe) {
  247. var lastVideoId = $(iframe).data("currentVideoId");
  248. var currentPlayer = $(iframe).data("togetherjs-player");
  249. var currentVideoId = getVideoIdFromUrl(currentPlayer.getVideoUrl());
  250. // since url forms of iframe src and player's video url are different,
  251. // I have to compare the video ids
  252. if (currentVideoId != lastVideoId) {
  253. return currentVideoId;
  254. } else {
  255. return false;
  256. }
  257. }
  258. // parses videoId from the url returned by getVideoUrl function
  259. function getVideoIdFromUrl(videoUrl) {
  260. var videoId = videoUrl.split('v=')[1];
  261. //Chrome and Firefox have different positions for parameters
  262. var ampersandIndex = videoId.indexOf('&');
  263. if (ampersandIndex != -1) {
  264. videoId = videoId.substring(0, ampersandIndex);
  265. }
  266. return videoId;
  267. }
  268. function areTooFarApart(myTime, theirTime) {
  269. var secDiff = Math.abs(myTime - theirTime);
  270. var milliDiff = secDiff * 1000;
  271. return milliDiff > TOO_FAR_APART;
  272. }
  273. });