playerControl.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682
  1. $(function() {
  2. /**
  3. * Ws is a wrapper of the WebSocket API
  4. * @class Ws
  5. */
  6. var Ws = function(options, managerOptions) {
  7. options = options || {};
  8. managerOptions = managerOptions || {};
  9. if (!options.url)
  10. throw "Cannot open a socket without a url";
  11. //@TODO: add try catch & retry
  12. this.onMessageCallbacks = [];
  13. this.socket = new WebSocket(options.url);
  14. this.url = options.url;
  15. this.maxTry = 4;
  16. this.recoTry = 0;
  17. };
  18. /**
  19. * @method init
  20. */
  21. Ws.prototype.init = function(fn) {
  22. var self = this;
  23. this.init = true;
  24. this.socket.onopen = function() {
  25. self._onOpen(this);
  26. if (self.init && typeof fn === 'function') {
  27. //Should be called only once
  28. fn(this);
  29. }
  30. self.init = false;
  31. };
  32. this.socket.onmessage = function(e) {
  33. self.onMessage(e);
  34. };
  35. this.socket.onclose = function() {
  36. self._onClose();
  37. };
  38. this.socket.onerror = function(error) {
  39. self._onError(error);
  40. };
  41. };
  42. /**
  43. * @method _onOpen
  44. * @param {Object} e
  45. * @private
  46. */
  47. Ws.prototype._onOpen = function(e) {
  48. console.log(e);
  49. this.recoTry = 0;
  50. };
  51. /**
  52. * On message call message callbacks
  53. * @method onMessage
  54. * @param {Object} e
  55. */
  56. Ws.prototype.onMessage = function(e) {
  57. //@TODO: is json?
  58. var message = $.parseJSON(e.data);
  59. for (var i = 0, length = this.onMessageCallbacks.length; i < length; i++) {
  60. var cb = this.onMessageCallbacks[i];
  61. if (typeof cb === 'function') {
  62. cb(message);
  63. }
  64. }
  65. };
  66. /**
  67. * @method _onError
  68. * @param {Object} e
  69. * @private
  70. */
  71. Ws.prototype._onError = function(e) {
  72. console.log(e);
  73. };
  74. /**
  75. * @method _onClose
  76. * @param {Object} e
  77. * @private
  78. */
  79. Ws.prototype._onClose = function(e) {
  80. console.log(e);
  81. //try to reco?
  82. if (this.maxTry > this.recoTry) {
  83. return;
  84. }
  85. this.socket = new WebSocket(this.url);
  86. this.init();
  87. this.recoTry++;
  88. };
  89. /**
  90. * Send stringify & send message to the socket
  91. * @method sendMessage
  92. * @param message
  93. */
  94. Ws.prototype.sendMessage = function(message) {
  95. message = JSON.stringify(message);
  96. this.socket.send(message);
  97. };
  98. /**
  99. * @class PlayerControl
  100. */
  101. var PlayerControl = function(options) {
  102. options = options || {};
  103. if (!options.socket instanceof Ws)
  104. throw "You need to provide a socket instance";
  105. if (!options.element || !options.element.length)
  106. throw "Element not found";
  107. this._playing = false;
  108. this._vClicked = false;
  109. this._progDragged = false;
  110. this._progClicked = false;
  111. this.element = options.element;
  112. this.socket = options.socket;
  113. this.duration = 0;
  114. this.currentTime = 0;
  115. this.volume = 0.5;
  116. this._previousVolume = 1;
  117. this.timeInterval = null;
  118. this.enableInterval = false;
  119. this.volumeStyleMap = {
  120. 0: 'mute',
  121. 1: 'very-low',
  122. 2: 'very-low',
  123. 3: 'low',
  124. 4: 'low',
  125. 5: 'medium',
  126. 6: 'medium',
  127. 7: 'high',
  128. 8: 'high',
  129. 9: 'very-high',
  130. 10: 'very-high'
  131. };
  132. };
  133. /**
  134. * @method init
  135. */
  136. PlayerControl.prototype.init = function() {
  137. //Add event listener
  138. var self = this;
  139. //Update time
  140. self.updateTime();
  141. var buttonHeight = self.element.find('.volume-button').height();
  142. var y = buttonHeight * this.volume * 10;
  143. self.updateVolume(y);
  144. var progWidth = this.element.find('.progress').width();
  145. var x = self.duration ? (progWidth / self.duration) * self.currentTime : 0;
  146. self.updateProgress(x);
  147. $(window).bind('mouseup', function(e) {
  148. self._progClicked = false;
  149. self._progDragged = false;
  150. self._vClicked = false;
  151. self._dragVolume = false;
  152. });
  153. // Shortcuts
  154. $(window).bind('keydown', function(e) {
  155. switch (e.which) {
  156. case 32:
  157. e.preventDefault();
  158. self.playPause();
  159. break;
  160. case 37: // left
  161. var time = self.currentTime - (self.duration / 100);
  162. self.seekTo({
  163. send: true,
  164. currentTime: time
  165. });
  166. break;
  167. case 39: // right
  168. var time = self.currentTime + (self.duration / 100);
  169. self.seekTo({
  170. send: true,
  171. currentTime: time
  172. });
  173. break;
  174. case 38: // up
  175. e.preventDefault();
  176. var buttonHeight = self.element.find('.volume-button').height();
  177. var y = buttonHeight * (self.volume + 0.1) * 10;
  178. self.updateVolume(y);
  179. break;
  180. case 40: // down
  181. e.preventDefault();
  182. var buttonHeight = self.element.find('.volume-button').height();
  183. var y = buttonHeight * (self.volume - 0.1) * 10;
  184. self.updateVolume(y);
  185. break;
  186. default:
  187. return; // exit this handler for other keys
  188. }
  189. });
  190. this.element.find('.play-pause').bind('click', function() {
  191. self.playPause();
  192. });
  193. this.element.find('.progress').bind('mousedown', function(e) {
  194. self.mousedownProgress(e);
  195. });
  196. this.element.bind('mousemove', function(e) {
  197. if (self._progClicked) {
  198. self.dragProgress(e);
  199. }
  200. if (self._vClicked) {
  201. self.dragVolume(e);
  202. }
  203. });
  204. this.element.find('.volume-bar-holder').bind('mousedown', function(e) {
  205. self._vClicked = true;
  206. var y = self.element.find('.volume-bar-holder').height() - (e.pageY - self.element.find('.volume-bar-holder').offset().top);
  207. self.updateVolume(y);
  208. });
  209. this.element.find('.volume-icon').bind('mousedown', function() {
  210. if (self.volume) {
  211. self._previousVolume = self.volume;
  212. self.volume = 0;
  213. } else {
  214. self.volume = self._previousVolume;
  215. }
  216. var buttonHeight = self.element.find('.volume-button').height();
  217. var y = buttonHeight * self.volume * 10;
  218. self.updateVolume(y);
  219. });
  220. };
  221. /**
  222. * @method updateVolume
  223. * @param {number} y
  224. */
  225. PlayerControl.prototype.updateVolume = function(y) {
  226. var buttonHeight = this.element.find('.volume-button').height();
  227. var barHolderHeight = this.element.find('.volume-bar-holder').height();
  228. if (y > barHolderHeight) {
  229. y = barHolderHeight;
  230. }
  231. this.element.find('.volume-bar').css({
  232. height: y + 'px'
  233. });
  234. //between 1 and 0
  235. this.volume = this.element.find('.volume-bar').height() / barHolderHeight;
  236. this.animateVolume();
  237. };
  238. /**
  239. * @method playPause
  240. */
  241. PlayerControl.prototype.playPause = function() {
  242. if (this.isEnded()) {
  243. return this.pause();
  244. }
  245. if (this._playing) {
  246. this.pause({
  247. send: true
  248. });
  249. }
  250. else {
  251. this.play({
  252. send: true
  253. });
  254. }
  255. };
  256. /**
  257. * @method animateVolume
  258. */
  259. PlayerControl.prototype.animateVolume = function() {
  260. var volumeIcon = this.element.find('.volume-icon');
  261. volumeIcon.removeClass().addClass('volume-icon v-' + this.volumeStyleMap[parseInt(this.volume * 10)]);
  262. };
  263. /**
  264. * @method getMouseProgressPosition
  265. * @param {Object} e - DOM event
  266. * @returns {number}
  267. */
  268. PlayerControl.prototype.getMouseProgressPosition = function(e) {
  269. return e.pageX - this.element.find('.progress').offset().left;
  270. };
  271. /**
  272. * @method mousedownProgress
  273. * @param {Object} e - DOM event
  274. */
  275. PlayerControl.prototype.mousedownProgress = function(e) {
  276. var progWidth = this.element.find('.progress').width();
  277. this._progClicked = true;
  278. var x = this.getMouseProgressPosition(e);
  279. this.currentTime = Math.round((x / progWidth) * this.duration);
  280. this.seekTo({
  281. send: true,
  282. currentTime: this.currentTime
  283. });
  284. };
  285. /**
  286. * @method dragProgress
  287. * @param {Object} e - DOM event
  288. */
  289. PlayerControl.prototype.dragProgress = function(e) {
  290. this._progDragged = true;
  291. var progMove = 0;
  292. var x = this.getMouseProgressPosition(e);
  293. var progWidth = this.element.find('.progress').width();
  294. if (this._playing && (this.currentTime < this.duration)) {
  295. this.play();
  296. }
  297. if (x <= 0) {
  298. progMove = 0;
  299. this.currentTime = 0;
  300. }
  301. else if (x > progWidth) {
  302. this.currentTime = this.duration;
  303. progMove = progWidth;
  304. }
  305. else {
  306. progMove = x;
  307. this.currentTime = Math.round((x / progWidth) * this.duration);
  308. }
  309. this.seekTo({
  310. send: true,
  311. currentTime: this.currentTime
  312. });
  313. };
  314. /**
  315. * @method dragVolume
  316. * @param {Object} e - DOM event
  317. */
  318. PlayerControl.prototype.dragVolume = function(e) {
  319. this._dragVolume = true;
  320. var volHeight = this.element.find('.volume-bar-holder').height();
  321. var y = volHeight - (e.pageY - this.element.find('.volume-bar-holder').offset().top);
  322. var volMove = 0;
  323. if (y <= 0) {
  324. volMove = 0;
  325. } else if (y > this.element.find('.volume-bar-holder').height() ||
  326. (y / volHeight) === 1) {
  327. volMove = volHeight;
  328. } else {
  329. volMove = y;
  330. }
  331. this.updateVolume(volMove);
  332. };
  333. PlayerControl.prototype.formatTime = function(ms) {
  334. var timeMatch = new Date(ms).toUTCString().match(/(\d\d:\d\d:\d\d)/);
  335. return timeMatch.length ? timeMatch[0].replace('00:', '') : '00:00';
  336. };
  337. /**
  338. * @method updateTime
  339. * @param {number} currentTime
  340. */
  341. PlayerControl.prototype.updateTime = function(currentTime) {
  342. var progWidth, cTime, tTime;
  343. progWidth = this.element.find('.progress').width();
  344. currentTime = currentTime ||
  345. Math.round(($('.progress-bar').width() / progWidth) * this.duration);
  346. cTime = this.formatTime(currentTime);
  347. tTime = this.formatTime(this.duration);
  348. this.element.find('.ctime').text(cTime);
  349. this.element.find('.ttime').text(tTime);
  350. };
  351. /**
  352. * @method updateProgress
  353. * @param {number} x
  354. */
  355. PlayerControl.prototype.updateProgress = function(x) {
  356. var buttonWidth = this.element.find('.progress-button').width();
  357. this.element.find('.progress-bar').css({
  358. width: x
  359. });
  360. this.element.find('.progress-button').css({
  361. left: x - (2 * buttonWidth) - (buttonWidth / 2) + 'px'
  362. });
  363. };
  364. /**
  365. * @method isEnded
  366. * @returns {boolean}
  367. */
  368. PlayerControl.prototype.isEnded = function() {
  369. return this.currentTime >= this.duration;
  370. };
  371. /**
  372. * @method play
  373. * @param {boolean} send
  374. */
  375. PlayerControl.prototype.play = function(options) {
  376. options = options || {};
  377. var self = this,
  378. send = options.send,
  379. currentTime = options.currentTime,
  380. delay = 1000,
  381. progWidth = this.element.find('.progress').width();
  382. //update currentTime
  383. this.currentTime = typeof currentTime !== 'undefined' ? currentTime : this.currentTime;
  384. if (this.timeInterval) {
  385. clearInterval(this.timeInterval);
  386. }
  387. //@TODO: use requestFrame instead
  388. this._playing = true;
  389. this.element
  390. .find('.play-pause')
  391. .addClass('pause')
  392. .removeClass('play');
  393. if (send) {
  394. this.socket.sendMessage({
  395. type: 'pause'
  396. });
  397. }
  398. if (!this.enableInterval) {
  399. return;
  400. }
  401. this.timeInterval = setInterval(function() {
  402. if (self.isEnded()) {
  403. self.currentTime = self.duration;
  404. self.pause();
  405. return clearInterval(this.timeInterval);
  406. }
  407. self.currentTime += 1;
  408. self.updateTime(self.currentTime);
  409. var x = (progWidth / self.duration) * self.currentTime;
  410. self.updateProgress(x);
  411. }, delay);
  412. };
  413. /**
  414. * @method pause
  415. * @param {boolean} send
  416. */
  417. PlayerControl.prototype.pause = function(options) {
  418. options = options || {};
  419. var send = options.send,
  420. currentTime = options.currentTime;
  421. //update currentTime
  422. this.currentTime = typeof currentTime !== 'undefined' ? currentTime : this.currentTime;
  423. if (this.timeInterval) {
  424. clearInterval(this.timeInterval);
  425. }
  426. this._playing = false;
  427. this.element
  428. .find('.play-pause')
  429. .addClass('play')
  430. .removeClass('pause');
  431. if (send) {
  432. this.socket.sendMessage({
  433. type: 'pause',
  434. currentTime: this.currentTime,
  435. media: {
  436. id: this.id
  437. }
  438. });
  439. }
  440. };
  441. /**
  442. * @method seekTo
  443. * @param {boolean} send
  444. * @param {number} time
  445. */
  446. PlayerControl.prototype.seekTo = function(options) {
  447. options = options || {};
  448. var currentTime = options.currentTime,
  449. send = options.send;
  450. //@TODO: make a debounce when dragging & key
  451. var progWidth = this.element.find('.progress').width();
  452. if (currentTime < 0) {
  453. currentTime = 0;
  454. }
  455. if (currentTime > this.duration) {
  456. currentTime = this.duration;
  457. }
  458. this.currentTime = currentTime;
  459. if (this._playing) {
  460. this.play();
  461. }
  462. var x = (progWidth / this.duration) * this.currentTime;
  463. this.updateProgress(x);
  464. this.updateTime(this.currentTime);
  465. if (send) {
  466. this.socket.sendMessage({
  467. type: 'seekTo',
  468. currentTime: currentTime
  469. });
  470. }
  471. };
  472. /**
  473. * @method getPlaying
  474. */
  475. PlayerControl.prototype.getPlaying = function() {
  476. //Trigger playing event (from server)
  477. this.socket.sendMessage({
  478. type: 'playing'
  479. });
  480. };
  481. /**
  482. * @method playing
  483. * @param {Object} message
  484. */
  485. PlayerControl.prototype.playing = function(options) {
  486. if (!options) {
  487. return;
  488. }
  489. options = options || {};
  490. this.currentTime = options.currentTime;
  491. this.duration = options.duration;
  492. this.title = options.title;
  493. this.id = options.id;
  494. var titleElement = this.element.find('.title');
  495. titleElement.text(this.title);
  496. this.play();
  497. }
  498. PlayerControl.prototype.openURL = function(options) {
  499. options = options || {};
  500. this.socket.sendMessage({
  501. type: 'openURL',
  502. url: options.url
  503. });
  504. };
  505. /**
  506. * Instanciation of the Ws class
  507. */
  508. var URL = 'ws://' + location.host;
  509. var socket = new Ws({
  510. url: URL
  511. });
  512. var playerControl = new PlayerControl({
  513. socket: socket,
  514. element: $('.player-control')
  515. });
  516. playerControl.init();
  517. socket.init(function() {
  518. playerControl.getPlaying();
  519. });
  520. /**
  521. * Map which method is allowed by event type
  522. */
  523. var TYPE_MAP = {
  524. play: function(message) {
  525. playerControl.play({
  526. currentTime: message.currentTime
  527. });
  528. },
  529. pause: function(message) {
  530. playerControl.pause({
  531. currentTime: message.currentTime
  532. });
  533. },
  534. playing: function(message) {
  535. playerControl.playing({
  536. currentTime: message.currentTime,
  537. duration: message.media.duration,
  538. title: message.media.title,
  539. id: message.media.id
  540. });
  541. },
  542. seekTo: function(message) {
  543. playerControl.seekTo({
  544. currentTime: message.currentTime
  545. });
  546. }
  547. };
  548. /**
  549. * Manage incoming messages
  550. */
  551. socket.onMessageCallbacks.push(function(message) {
  552. var key = 'type';
  553. var type = message[key];
  554. if (!type || typeof TYPE_MAP[type] !== 'function')
  555. return;
  556. TYPE_MAP[type](message);
  557. });
  558. $('form.open-url').on('submit', function(e) {
  559. e.preventDefault();
  560. var url = $(this).find('input').val();
  561. var localesURL = LOCALES ? (LOCALES.PLAYER_CONTROL ? LOCALES.PLAYER_CONTROL.URL : {}) : {};
  562. if (!url) {
  563. return displayMessage(localesURL.EMPTY);
  564. } else if (!isURL(url)) {
  565. return displayMessage(localesURL.NOT_VALID);
  566. }
  567. displayMessage(localesURL.SENT_SUCCESSFULLY);
  568. playerControl.openURL({
  569. url: url
  570. });
  571. //clear the form
  572. $(this).find('input').val('');
  573. });
  574. /**
  575. * Check if a given string is a URL
  576. * Regex from https://gist.github.com/searls/1033143
  577. * @param {string} str
  578. * @returns {boolean}
  579. */
  580. function isURL(str) {
  581. var p = /\b((?:https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/i;
  582. return p.test(str);
  583. }
  584. //Display message to the user
  585. var TIMEOUT = null;
  586. var DELAY = 5000;
  587. function displayMessage(message) {
  588. if (!message) {
  589. return;
  590. }
  591. $('.display-message').addClass('show');
  592. $('.display-message').text(message);
  593. if (TIMEOUT) {
  594. clearTimeout(TIMEOUT);
  595. }
  596. TIMEOUT = setTimeout(function() {
  597. clearMessage();
  598. }, DELAY);
  599. }
  600. function clearMessage() {
  601. if (TIMEOUT) {
  602. clearTimeout(TIMEOUT);
  603. }
  604. $('.display-message').removeClass('show');
  605. }
  606. });