main.js 20 KB

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