playerControl.js 18 KB

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