main.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  1. import { makeMessage, Resolver } from './websocketResolver.js';
  2. import { ListOperationTypes, MediaAction, MessageTypes, PlayerState, YoutubeResourceType } from "./enums.js";
  3. window.onYouTubeIframeAPIReady = onYouTubeIframeAPIReady
  4. const youtubeApiScript = document.createElement('script');
  5. youtubeApiScript.src = "https://www.youtube.com/iframe_api";
  6. const firstScriptTag = document.getElementsByTagName('script')[0];
  7. firstScriptTag.parentNode.insertBefore(youtubeApiScript, firstScriptTag);
  8. const RTT_ESTIMATE = 1
  9. const ALLOWED_AHEAD = 5
  10. const PLAYER_WIDTH = 640
  11. const PLAYER_HEIGHT = 360
  12. let videos = null;
  13. let videosPlayed = null;
  14. let videoPlaying = null;
  15. let state = null;
  16. let isLeader = null;
  17. let repeat = false;
  18. let player = null;
  19. let playerActive = false;
  20. document.getElementById('player').append(mockPlayer('360', '640'));
  21. const codeInfoMap = new Map()
  22. function onYouTubeIframeAPIReady() {
  23. onYTDone();
  24. }
  25. function onPlayerReady(event) {
  26. event.target.seekTo(0);
  27. if (state === PlayerState.PLAYING) {
  28. event.target.playVideo();
  29. } else if (state === PlayerState.PAUSED) {
  30. event.target.pauseVideo();
  31. } else if (state === PlayerState.LIST_END) {
  32. event.target.stopVideo();
  33. }
  34. }
  35. function onPlayerStateChange(event) {
  36. console.log(event.data, YT.PlayerState, state, videoPlaying, videos);
  37. if (event.data === 0 /*Ended*/) {
  38. if (state !== PlayerState.PLAYING) {
  39. console.warn("Player ended up in state ENDED while it is was not playing.")
  40. return
  41. }
  42. if (isLeader && videoPlaying !== null) {
  43. socket.send(makeMessage(MessageTypes.SONG_END, { id: videoPlaying.id }))
  44. }
  45. const vid = popVideo()
  46. if (vid !== undefined) {
  47. playVideo(vid);
  48. } else {
  49. videoPlaying = null;
  50. state = PlayerState.LIST_END;
  51. }
  52. }
  53. }
  54. function onAddVideo() {
  55. console.log(state, videoPlaying, videos)
  56. switch (state) {
  57. case PlayerState.LIST_END:
  58. if (videoPlaying === null) {
  59. playVideo(popVideo());
  60. } else {
  61. console.error(`Invalid state: state=${state}; videoPlaying=${videoPlaying}`)
  62. return
  63. }
  64. break
  65. case PlayerState.PAUSED:
  66. if (videoPlaying === null) {
  67. loadVideo(popVideo())
  68. }
  69. break
  70. case PlayerState.PLAYING:
  71. if (videoPlaying === null) {
  72. playVideo(popVideo())
  73. }
  74. break
  75. default:
  76. console.error("Unknown state", state)
  77. break
  78. }
  79. }
  80. function addVideo(code, id) {
  81. videos.push({ code, id });
  82. makeQueueLine(code, id);
  83. onAddVideo();
  84. }
  85. function popVideo() {
  86. console.log("pop", videos)
  87. if (videos.length <= 0) {
  88. if (videosPlayed) {
  89. videos = []
  90. for (const { code, id } of videosPlayed) {
  91. videos.push({code, id});
  92. makeQueueLine(code, id);
  93. }
  94. videosPlayed = []
  95. } else {
  96. state = PlayerState.LIST_END
  97. return undefined;
  98. }
  99. }
  100. const vid = videos.shift();
  101. queueElement.removeChild(queueElement.querySelector(".videoListCard"));
  102. if (videosPlayed) {
  103. videosPlayed.push(vid);
  104. }
  105. return vid;
  106. }
  107. function findVideoIndex(id) {
  108. return videos.findIndex(function (vid) {
  109. return vid.id === id
  110. })
  111. }
  112. function delVideo(id) {
  113. videos.splice(findVideoIndex(id), 1)
  114. removeQueueLine(id)
  115. }
  116. function moveVideo(id, displacement) {
  117. const i = findVideoIndex(id)
  118. const [vid] = videos.splice(i, 1)
  119. const new_i = i + displacement
  120. videos.splice(new_i, 0, vid)
  121. removeQueueLine(id)
  122. if (new_i + 1 === videos.length) {
  123. makeQueueLine(vid.code, id)
  124. } else {
  125. console.log(videos, new_i)
  126. makeQueueLine(vid.code, id, videos[new_i + 1].id)
  127. }
  128. }
  129. function playVideo(vid) {
  130. videoPlaying = vid;
  131. state = PlayerState.PLAYING
  132. if (!playerActive) {
  133. return;
  134. }
  135. if (player === null) {
  136. buildPlayer(PLAYER_HEIGHT, PLAYER_WIDTH, vid.code);
  137. } else {
  138. player.loadVideoById(vid.code, 0);
  139. }
  140. }
  141. function loadVideo(vid) {
  142. videoPlaying = vid;
  143. if (!playerActive) {
  144. return
  145. }
  146. if (player === null) {
  147. buildPlayer(PLAYER_HEIGHT, PLAYER_WIDTH, vid.code);
  148. } else {
  149. player.cueVideoById(vid.code, 0);
  150. }
  151. }
  152. function setLeader(b) {
  153. console.log(isLeader, b)
  154. if (isLeader !== b) {
  155. let btn = document.getElementById("leader-button");
  156. btn.innerText = b ? "Leader" : "Follower";
  157. btn.classList.remove(b ? "btn-outline-success" : "btn-success")
  158. btn.classList.add(b ? "btn-success" : "btn-outline-success")
  159. if (isLeader === null) {
  160. btn.classList.remove("disabled", "btn-outline-secondary")
  161. }
  162. isLeader = b;
  163. }
  164. }
  165. window.setLeader = setLeader
  166. function mockPlayer(height, width) {
  167. const rect = document.createElement('div');
  168. rect.setAttribute('style', `height:${height}px;width:${width}px;background:black`);
  169. return rect
  170. }
  171. function buildPlayer(height, width, id) {
  172. player = new YT.Player('player', {
  173. height: height + 48,
  174. width: width,
  175. videoId: id,
  176. playerVars: {},
  177. events: {
  178. 'onReady': onPlayerReady,
  179. 'onStateChange': onPlayerStateChange,
  180. }
  181. });
  182. window.player = player
  183. }
  184. const queueElement = document.getElementById('videoList')
  185. const queueLine = document.getElementById('videoListCardTemplate')
  186. function makeQueueLine(code, id, before_id) {
  187. let newQueueLine = queueLine.cloneNode(true);
  188. newQueueLine.id = "";
  189. newQueueLine.hidden = false;
  190. newQueueLine.setAttribute("data-id", id)
  191. if (codeInfoMap.has(code)) {
  192. const { thumbnails, title, channelTitle, publishTime, description } = codeInfoMap.get(code)
  193. const thumbnail = newQueueLine.getElementsByClassName("videoListCardThumbnail")[0]
  194. const img = document.createElement('img')
  195. thumbnail.appendChild(img)
  196. img.setAttribute('src', thumbnails.default.url)
  197. img.setAttribute('width', thumbnails.default.width)
  198. img.setAttribute('height', thumbnails.default.height)
  199. img.setAttribute('alt', "")
  200. newQueueLine.getElementsByClassName("videoListCardTitle")[0].innerText = title
  201. newQueueLine.getElementsByClassName("videoListCardChannel")[0].innerText = channelTitle
  202. newQueueLine.getElementsByClassName("videoListCardDescription")[0].innerText = description.replace(/\n/g, " ")
  203. }
  204. newQueueLine.setAttribute("data-youtubeId", code)
  205. function delHandler(event) {
  206. onDeleteClick(event, id)
  207. }
  208. const delButton = newQueueLine.querySelector('.videoListCardDelete')
  209. delButton.addEventListener("click", delHandler)
  210. delButton.addEventListener("keydown", delHandler)
  211. function moveUpHandler(event) {
  212. onMoveClick(event, id, -1)
  213. }
  214. const upButton = newQueueLine.querySelector('.videoListCardMoveUp')
  215. upButton.addEventListener("click", moveUpHandler)
  216. upButton.addEventListener("keydown", moveUpHandler)
  217. function moveDownHandler(event) {
  218. onMoveClick(event, id, 1)
  219. }
  220. const downButton = newQueueLine.querySelector('.videoListCardMoveDown')
  221. downButton.addEventListener("click", moveDownHandler)
  222. downButton.addEventListener("keydown", moveDownHandler)
  223. if (before_id == null) {
  224. queueLine.before(newQueueLine);
  225. } else {
  226. queueElement.querySelector(`[data-id='${before_id}']`).before(newQueueLine)
  227. }
  228. }
  229. function removeQueueLine(id) {
  230. const card = queueElement.querySelector(`div[data-id='${id}']`);
  231. queueElement.removeChild(card);
  232. }
  233. function onSubmit(event) {
  234. event.preventDefault();
  235. socket.send(makeMessage(MessageTypes.LIST_OPERATION, {
  236. op: ListOperationTypes.ADD,
  237. code: event.target[0].value
  238. }))
  239. }
  240. function onSearch(event) {
  241. event.preventDefault();
  242. const q = event.target[0].value
  243. if (q !== "") {
  244. socket.send(makeMessage(MessageTypes.SEARCH, { q }))
  245. }
  246. }
  247. function onDeleteClick(event, id) {
  248. event.preventDefault();
  249. socket.send(makeMessage(MessageTypes.LIST_OPERATION, { op: ListOperationTypes.DEL, id }))
  250. }
  251. function onMoveClick(event, id, displacement) {
  252. event.preventDefault();
  253. socket.send(makeMessage(MessageTypes.LIST_OPERATION, { op: ListOperationTypes.MOVE, id, displacement }))
  254. }
  255. function onLeaderbutton(event) {
  256. if (isLeader) {
  257. socket.send(makeMessage(MessageTypes.RELEASE_CONTROL))
  258. } else {
  259. socket.send(makeMessage(MessageTypes.OBTAIN_CONTROL))
  260. }
  261. }
  262. const playerPlaceholder = document.getElementById('playerPlaceholder');
  263. const playerPlaceholderParent = playerPlaceholder.parentElement;
  264. const playerContainer = document.getElementById('playerContainer')
  265. const showPlaceholderButton = document.getElementById('showPlayerPlaceholder');
  266. function onPlayerStart(event) {
  267. event.preventDefault();
  268. playerActive = true;
  269. playerPlaceholderParent.removeChild(playerPlaceholder)
  270. playerContainer.toggleAttribute("hidden")
  271. switch (state) {
  272. case PlayerState.LIST_END:
  273. break;
  274. case PlayerState.PAUSED:
  275. if (videoPlaying !== null) {
  276. loadVideo(videoPlaying);
  277. } else {
  278. console.error(`Invalid state: state=${state}; videoPlaying=${videoPlaying}`)
  279. return
  280. }
  281. break;
  282. case PlayerState.PLAYING:
  283. if (videoPlaying !== null) {
  284. playVideo(videoPlaying);
  285. } else {
  286. console.error(`Invalid state: state=${state}; videoPlaying=${videoPlaying}`)
  287. return
  288. }
  289. break;
  290. }
  291. socket.send(makeMessage(MessageTypes.PLAYER_ENABLED, { enabled: true }))
  292. }
  293. function onPlayerClose(event) {
  294. event.preventDefault();
  295. playerActive = false;
  296. playerContainer.toggleAttribute("hidden")
  297. playerPlaceholderParent.appendChild(playerPlaceholder)
  298. if (player !== null) {
  299. player.pauseVideo();
  300. }
  301. socket.send(makeMessage(MessageTypes.PLAYER_ENABLED, { enabled: false }))
  302. }
  303. function hidePlayerPlaceholder(event) {
  304. event.preventDefault();
  305. playerPlaceholderParent.removeChild(playerPlaceholder);
  306. showPlaceholderButton.toggleAttribute("hidden")
  307. }
  308. function showPlayerPlaceholder(event) {
  309. event.preventDefault();
  310. playerPlaceholderParent.appendChild(playerPlaceholder);
  311. showPlaceholderButton.toggleAttribute("hidden")
  312. }
  313. function onPlayButton(event) {
  314. event.preventDefault();
  315. socket.send(makeMessage(MessageTypes.MEDIA_ACTION, { action: MediaAction.PLAY }))
  316. }
  317. function onPauseButton(event) {
  318. event.preventDefault();
  319. socket.send(makeMessage(MessageTypes.MEDIA_ACTION, { action: MediaAction.PAUSE }))
  320. }
  321. function onNextButton(event) {
  322. event.preventDefault();
  323. if (videoPlaying !== null) {
  324. socket.send(makeMessage(MessageTypes.MEDIA_ACTION, { action: MediaAction.NEXT, current_id: videoPlaying.id }))
  325. }
  326. }
  327. const repeatButton = document.getElementById('repeat-button');
  328. function onRepeatButton(event) {
  329. event.preventDefault();
  330. socket.send(makeMessage(MessageTypes.MEDIA_ACTION, { action: MediaAction.REPEAT, enable: !repeat }))
  331. }
  332. function stateProcessor(ws, data) {
  333. const { playing, state: newState, lists } = data;
  334. const { next, previous } = lists
  335. videos = []
  336. videosPlayed = previous
  337. repeat = !!previous
  338. state = newState
  339. videoPlaying = playing
  340. const codes = []
  341. for (const song of next) {
  342. const { code, id } = song
  343. addVideo(code, id)
  344. if (!(codes.includes(code))) {
  345. codes.push(code)
  346. }
  347. }
  348. for (const song of previous || []) {
  349. const { code, id } = song;
  350. if (!(codes.includes(code))) {
  351. codes.push(code)
  352. }
  353. }
  354. if (codes.length > 0) {
  355. socket.send(makeMessage(MessageTypes.SEARCH_ID, { id: codes }))
  356. }
  357. if (videoPlaying !== null) {
  358. if (state === PlayerState.PLAYING) {
  359. playVideo(videoPlaying)
  360. } else {
  361. loadVideo(videoPlaying)
  362. }
  363. }
  364. if (isLeader === null) {
  365. setLeader(false)
  366. }
  367. if (repeat) {
  368. repeatButton.classList.toggle('btn-outline-secondary');
  369. repeatButton.classList.toggle('btn-secondary');
  370. }
  371. afterStateInit()
  372. }
  373. function listOperationProcessor(ws, data) {
  374. const { op, items } = data;
  375. if (op === ListOperationTypes.ADD) {
  376. const noCodeInfo = []
  377. for (const { code, id, snippet } of items) {
  378. if (snippet !== undefined) {
  379. codeInfoMap.set(code, snippet)
  380. } else if (!codeInfoMap.has(code)) {
  381. noCodeInfo.push(code)
  382. }
  383. addVideo(code, id);
  384. }
  385. if (noCodeInfo.length > 0) {
  386. socket.send(makeMessage(MessageTypes.SEARCH_ID, { id: noCodeInfo.join(',') }))
  387. }
  388. } else if (op === ListOperationTypes.DEL) {
  389. for (const { id } of items) {
  390. delVideo(id);
  391. }
  392. } else if (op === ListOperationTypes.MOVE) {
  393. for (const { id, displacement } of items) {
  394. moveVideo(id, displacement)
  395. }
  396. }
  397. }
  398. function mediaActionProcessor(ws, data) {
  399. const { action, ended_id, current_id, enable } = data;
  400. if (action === MediaAction.PLAY && state === PlayerState.PAUSED) {
  401. state = PlayerState.PLAYING
  402. if (playerActive) {
  403. player.playVideo();
  404. }
  405. } else if (action === MediaAction.PAUSE && state === PlayerState.PLAYING) {
  406. state = PlayerState.PAUSED
  407. if (playerActive) {
  408. player.pauseVideo();
  409. }
  410. } else if (action === MediaAction.NEXT) {
  411. if (videoPlaying !== null && videoPlaying.id === ended_id) {
  412. const vid = popVideo()
  413. if (vid !== undefined) {
  414. playVideo(vid)
  415. } else {
  416. videoPlaying = null;
  417. state = PlayerState.LIST_END;
  418. if (player) {
  419. player.stopVideo();
  420. }
  421. }
  422. }
  423. } else if (action === MediaAction.REPEAT) {
  424. if (enable !== repeat) {
  425. repeat = enable;
  426. repeatButton.classList.toggle('btn-outline-secondary');
  427. repeatButton.classList.toggle('btn-secondary');
  428. videosPlayed = repeat ? (videoPlaying ? [videoPlaying] : []) : null;
  429. }
  430. }
  431. }
  432. function songEndProcessor(ws, data) {
  433. const { ended_id, current_id } = data;
  434. console.log(ended_id, current_id)
  435. if (videoPlaying === null) {
  436. // Do nothing
  437. } else if (ended_id === videoPlaying.id) {
  438. const vid = popVideo()
  439. console.log(vid)
  440. if (vid !== undefined) {
  441. playVideo(vid);
  442. } else {
  443. videoPlaying = null;
  444. state = PlayerState.LIST_END
  445. // TODO SEEK TO END
  446. }
  447. } else if (current_id === videoPlaying.id) {
  448. if (!isLeader && player.getCurrentTime() - RTT_ESTIMATE - ALLOWED_AHEAD > 0) {
  449. player.seekTo(RTT_ESTIMATE + ALLOWED_AHEAD, true)
  450. }
  451. } else {
  452. console.error("Difficult state reached. Reset protocol not implemented. Either to far ahead, behind or state inconsistency", ended_id, current_id, videoPlaying)
  453. }
  454. }
  455. const searchResultTemplate = document.getElementById("searchResultTemplate")
  456. const searchResultList = searchResultTemplate.parentElement
  457. searchResultList.removeChild(searchResultTemplate)
  458. searchResultTemplate.id = ""
  459. function makeSearchResult(item) {
  460. const { id, snippet } = item
  461. const { kind, videoId, channelId, playlistId } = id
  462. const { thumbnails, title, channelTitle, publishTime, description } = snippet
  463. const code = kind === YoutubeResourceType.CHANNEL ? channelId :
  464. kind === YoutubeResourceType.PLAYLIST ? playlistId :
  465. kind === YoutubeResourceType.VIDEO ? videoId :
  466. console.error(`Unknown kind ${kind}`)
  467. if (!code) return;
  468. const searchResult = searchResultTemplate.cloneNode(true)
  469. searchResult.setAttribute('data-youtubeID', code)
  470. function onClickHandler() {
  471. socket.send(makeMessage(MessageTypes.LIST_OPERATION, {
  472. op: ListOperationTypes.ADD,
  473. kind,
  474. code
  475. }));
  476. if (kind === YoutubeResourceType.VIDEO) {
  477. codeInfoMap.set(videoId, snippet)
  478. }
  479. }
  480. searchResult.addEventListener("click", onClickHandler)
  481. searchResult.addEventListener("keydown", onClickHandler)
  482. const thumbnailContainer = searchResult.getElementsByClassName("searchResultThumbnail")[0]
  483. const thumbnailImage = thumbnailContainer.getElementsByClassName("thumbnailImage")[0]
  484. const img = document.createElement('img')
  485. thumbnailImage.appendChild(img)
  486. const { url, width, height } = (thumbnails ? thumbnails["default"] : {
  487. url: "/img/no_thumbnail.png",
  488. width: 120,
  489. height: 90
  490. })
  491. img.setAttribute('src', url)
  492. img.setAttribute('width', width)
  493. img.setAttribute('height', height)
  494. img.setAttribute('alt', "")
  495. if (kind !== YoutubeResourceType.PLAYLIST) {
  496. const playlistOverlay = thumbnailContainer.getElementsByClassName("thumbnailPlaylistOverlay")[0]
  497. playlistOverlay.parentElement.removeChild(playlistOverlay);
  498. }
  499. searchResult.getElementsByClassName("searchResultTitle")[0].innerText = title
  500. searchResult.getElementsByClassName("searchResultChannel")[0].innerText = channelTitle
  501. searchResult.getElementsByClassName("searchResultDescription")[0].innerText = description
  502. searchResult.removeAttribute('hidden')
  503. searchResultList.appendChild(searchResult)
  504. }
  505. function searchResultProcessor(_, data) {
  506. const { items } = data;
  507. searchResultList.innerHTML = '';
  508. for (const item of items) {
  509. makeSearchResult(item)
  510. }
  511. }
  512. function searchIdResultProcessor(_, data) {
  513. const { items } = data;
  514. for (const { id: code, snippet } of items) {
  515. codeInfoMap.set(code, snippet);
  516. const lines = queueElement.querySelectorAll(`[data-youtubeID='${code}`)
  517. for (const line of lines) {
  518. if (line !== null) {
  519. const id = parseInt(line.getAttribute("data-id"))
  520. makeQueueLine(code, id, id)
  521. line.parentElement.removeChild(line)
  522. }
  523. }
  524. }
  525. }
  526. let socket;
  527. function onYTDone() {
  528. const resolver = new Resolver()
  529. resolver.register(MessageTypes.STATE, stateProcessor)
  530. resolver.register(MessageTypes.LIST_OPERATION, listOperationProcessor)
  531. resolver.register(MessageTypes.MEDIA_ACTION, mediaActionProcessor)
  532. resolver.register(MessageTypes.OBTAIN_CONTROL, () => setLeader(true))
  533. resolver.register(MessageTypes.RELEASE_CONTROL, () => setLeader(false))
  534. resolver.register(MessageTypes.SONG_END, songEndProcessor)
  535. resolver.register(MessageTypes.SEARCH, searchResultProcessor)
  536. resolver.register(MessageTypes.SEARCH_ID, searchIdResultProcessor)
  537. socket = resolver.connectSocket()
  538. socket.addEventListener("open", function () {
  539. socket.send(makeMessage(MessageTypes.STATE, null))
  540. })
  541. }
  542. function afterStateInit() {
  543. // document.getElementById('addVideoForm').addEventListener('submit', onSubmit);
  544. document.getElementById('searchVideoForm').addEventListener('submit', onSearch)
  545. document.getElementById('leader-button').addEventListener('click', onLeaderbutton)
  546. document.getElementById('startPlayerButton').addEventListener('click', onPlayerStart)
  547. document.getElementById('closePlayer').addEventListener('click', onPlayerClose)
  548. document.getElementById('hidePlayerPlaceholder').addEventListener('click', hidePlayerPlaceholder)
  549. document.getElementById('showPlayerPlaceholder').addEventListener('click', showPlayerPlaceholder)
  550. document.getElementById('play-button').addEventListener('click', onPlayButton)
  551. document.getElementById('pause-button').addEventListener('click', onPauseButton)
  552. document.getElementById('next-button').addEventListener('click', onNextButton)
  553. document.getElementById('repeat-button').addEventListener('click', onRepeatButton)
  554. }