main.js 18 KB

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