main.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. import { makeMessage, Resolver } from './websocketResolver.js';
  2. import { ListOperationTypes, MessageTypes, PlayerState } 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. let videos = null;
  11. let videoPlaying = null;
  12. let state = null;
  13. let isLeader = null;
  14. let player = null;
  15. document.getElementById('player').append(mockPlayer('360', '640'));
  16. const codeInfoMap = new Map()
  17. function onYouTubeIframeAPIReady() {
  18. onYTDone();
  19. }
  20. function onPlayerReady(event) {
  21. event.target.seekTo(0);
  22. if (state === PlayerState.PLAYING) {
  23. event.target.playVideo();
  24. }
  25. }
  26. function onPlayerStateChange(event) {
  27. console.log(event.data, YT.PlayerState, state, videoPlaying, videos);
  28. if (event.data === 0 /*Ended*/) {
  29. if (state !== PlayerState.PLAYING) {
  30. console.warn("Player ended up in state ENDED while it is was not playing.")
  31. return
  32. }
  33. if (isLeader && videoPlaying !== null) {
  34. socket.send(makeMessage(MessageTypes.SONG_END, { id: videoPlaying.id }))
  35. }
  36. const vid = popVideo()
  37. if (vid !== undefined) {
  38. playVideo(vid);
  39. } else {
  40. videoPlaying = null;
  41. state = PlayerState.LIST_END;
  42. }
  43. }
  44. }
  45. function onAddVideo() {
  46. console.log(state, videoPlaying, videos)
  47. switch (state) {
  48. case PlayerState.LIST_END:
  49. if (videoPlaying === null) {
  50. playVideo(popVideo());
  51. } else {
  52. console.error(`Invalid state: state=${state}; videoPlaying=${videoPlaying}`)
  53. return
  54. }
  55. break
  56. case PlayerState.PAUSED:
  57. if (videoPlaying === null) {
  58. loadVideo(popVideo())
  59. }
  60. break
  61. case PlayerState.PLAYING:
  62. if (videoPlaying === null) {
  63. playVideo(popVideo())
  64. }
  65. break
  66. default:
  67. console.error("Unknown state", state)
  68. break
  69. }
  70. }
  71. function addVideo(code, id) {
  72. videos.push({ code, id });
  73. makeQueueLine(code, id);
  74. onAddVideo();
  75. }
  76. function popVideo() {
  77. console.log("pop", videos)
  78. const vid = videos.shift();
  79. if (vid !== undefined) {
  80. queueElement.removeChild(queueElement.querySelector(".videoListCard"));
  81. } else {
  82. state = PlayerState.LIST_END
  83. }
  84. return vid;
  85. }
  86. function findVideoIndex(id) {
  87. return videos.findIndex(function (vid) {
  88. return vid.id === id
  89. })
  90. }
  91. function delVideo(id) {
  92. videos.splice(findVideoIndex(id), 1)
  93. removeQueueLine(id)
  94. }
  95. function moveVideo(id, displacement) {
  96. const i = findVideoIndex(id)
  97. const [vid] = videos.splice(i, 1)
  98. const new_i = i + displacement
  99. videos.splice(new_i, 0, vid)
  100. removeQueueLine(id)
  101. if (new_i + 1 === videos.length) {
  102. makeQueueLine(vid.code, id)
  103. } else {
  104. console.log(videos, new_i)
  105. makeQueueLine(vid.code, id, videos[new_i + 1].id)
  106. }
  107. }
  108. function playVideo(vid) {
  109. videoPlaying = vid;
  110. state = PlayerState.PLAYING
  111. if (player === null) {
  112. buildPlayer('360', '640', vid.code);
  113. } else {
  114. player.loadVideoById(vid.code, 0);
  115. }
  116. }
  117. function loadVideo(vid) {
  118. videoPlaying = vid;
  119. if (player === null) {
  120. buildPlayer('360', '640', vid.code);
  121. } else {
  122. player.cueVideoById(vid.code, 0);
  123. }
  124. }
  125. function setLeader(b) {
  126. console.log(isLeader, b)
  127. if (isLeader !== b) {
  128. let btn = document.getElementById("leader-button");
  129. btn.innerText = b ? "Leader" : "Follower";
  130. btn.classList.remove(b ? "btn-outline-success" : "btn-success")
  131. btn.classList.add(b ? "btn-success" : "btn-outline-success")
  132. if (isLeader === null) {
  133. btn.classList.remove("disabled", "btn-outline-secondary")
  134. }
  135. isLeader = b;
  136. }
  137. }
  138. window.setLeader = setLeader
  139. function mockPlayer(height, width) {
  140. const rect = document.createElement('div');
  141. rect.setAttribute('style', `height:${height}px;width:${width}px;background:black`);
  142. return rect
  143. }
  144. function buildPlayer(height, width, id) {
  145. player = new YT.Player('player', {
  146. height: parseInt(height) + 48,
  147. width: width,
  148. videoId: id,
  149. playerVars: {},
  150. events: {
  151. 'onReady': onPlayerReady,
  152. 'onStateChange': onPlayerStateChange,
  153. }
  154. });
  155. window.player = player
  156. }
  157. const queueElement = document.getElementById('videoList')
  158. const queueLine = document.getElementById('videoListCardTemplate')
  159. function makeQueueLine(code, id, before_id) {
  160. let newQueueLine = queueLine.cloneNode(true);
  161. newQueueLine.id = "";
  162. newQueueLine.hidden = false;
  163. newQueueLine.setAttribute("data-id", id)
  164. if (codeInfoMap.has(code)) {
  165. const { id, snippet } = codeInfoMap.get(code)
  166. const { thumbnails, title, channelTitle, publishTime, description } = snippet
  167. const thumbnail = newQueueLine.getElementsByClassName("videoListCardThumbnail")[0]
  168. const img = document.createElement('img')
  169. thumbnail.appendChild(img)
  170. img.setAttribute('src', thumbnails.default.url)
  171. img.setAttribute('width', thumbnails.default.width)
  172. img.setAttribute('height', thumbnails.default.height)
  173. img.setAttribute('alt', "")
  174. newQueueLine.getElementsByClassName("videoListCardTitle")[0].innerText = title
  175. newQueueLine.getElementsByClassName("videoListCardChannel")[0].innerText = channelTitle
  176. newQueueLine.getElementsByClassName("videoListCardDescription")[0].innerText = description.replace(/\n/g, " ")
  177. }
  178. newQueueLine.setAttribute("data-youtubeId", code)
  179. function delHandler(event) {
  180. onDeleteClick(event, id)
  181. }
  182. const delButton = newQueueLine.querySelector('.videoListCardDelete')
  183. delButton.addEventListener("click", delHandler)
  184. delButton.addEventListener("keydown", delHandler)
  185. function moveUpHandler(event) {
  186. onMoveClick(event, id, -1)
  187. }
  188. const upButton = newQueueLine.querySelector('.videoListCardMoveUp')
  189. upButton.addEventListener("click", moveUpHandler)
  190. upButton.addEventListener("keydown", moveUpHandler)
  191. function moveDownHandler(event) {
  192. onMoveClick(event, id, 1)
  193. }
  194. const downButton = newQueueLine.querySelector('.videoListCardMoveDown')
  195. downButton.addEventListener("click", moveDownHandler)
  196. downButton.addEventListener("keydown", moveDownHandler)
  197. if (before_id == null) {
  198. queueLine.before(newQueueLine);
  199. } else {
  200. queueElement.querySelector(`[data-id='${before_id}']`).before(newQueueLine)
  201. }
  202. }
  203. function removeQueueLine(id) {
  204. const card = queueElement.querySelector(`div[data-id='${id}']`);
  205. queueElement.removeChild(card);
  206. }
  207. function onSubmit(event) {
  208. event.preventDefault();
  209. socket.send(makeMessage(MessageTypes.LIST_OPERATION, {
  210. op: ListOperationTypes.ADD,
  211. code: event.target[0].value
  212. }))
  213. }
  214. function onSearch(event) {
  215. event.preventDefault();
  216. const q = event.target[0].value
  217. if (q !== "") {
  218. socket.send(makeMessage(MessageTypes.SEARCH, {q}))
  219. }
  220. }
  221. function onDeleteClick(event, id) {
  222. event.preventDefault();
  223. socket.send(makeMessage(MessageTypes.LIST_OPERATION, { op: ListOperationTypes.DEL, id }))
  224. }
  225. function onMoveClick(event, id, displacement) {
  226. event.preventDefault();
  227. socket.send(makeMessage(MessageTypes.LIST_OPERATION, { op: ListOperationTypes.MOVE, id, displacement }))
  228. }
  229. function onLeaderbutton(event) {
  230. if (isLeader) {
  231. socket.send(makeMessage(MessageTypes.RELEASE_CONTROL))
  232. } else {
  233. socket.send(makeMessage(MessageTypes.OBTAIN_CONTROL))
  234. }
  235. }
  236. function stateProcessor(ws, data) {
  237. const { playing, state: newState, list } = data;
  238. videos = []
  239. state = newState
  240. videoPlaying = playing
  241. const codes = []
  242. for (const song of list) {
  243. const {code, id} = song
  244. addVideo(code, id)
  245. if (!(codes.includes(code))) {
  246. codes.push(code)
  247. }
  248. }
  249. socket.send(makeMessage(MessageTypes.SEARCH_ID, {id: codes}))
  250. if (videoPlaying !== null) {
  251. if (state === PlayerState.PLAYING) {
  252. playVideo(videoPlaying)
  253. } else {
  254. loadVideo(videoPlaying)
  255. }
  256. }
  257. if (isLeader === null) {
  258. setLeader(false)
  259. }
  260. afterStateInit()
  261. }
  262. function listOperationProcessor(ws, data) {
  263. const { op, id } = data;
  264. if (op === ListOperationTypes.ADD) {
  265. const { code } = data;
  266. addVideo(code, id);
  267. if (!codeInfoMap.has(code)) {
  268. socket.send(makeMessage(MessageTypes.SEARCH_ID, {id: code}))
  269. }
  270. } else if (op === ListOperationTypes.DEL) {
  271. delVideo(id);
  272. } else if (op === ListOperationTypes.MOVE) {
  273. const { displacement } = data
  274. moveVideo(id, displacement)
  275. }
  276. }
  277. function songEndProcessor(ws, data) {
  278. const { ended_id, current_id } = data;
  279. console.log(ended_id, current_id)
  280. if (videoPlaying === null) {
  281. // Do nothing
  282. } else if (ended_id === videoPlaying.id) {
  283. const vid = popVideo()
  284. console.log(vid)
  285. if (vid !== undefined) {
  286. playVideo(vid);
  287. } else {
  288. videoPlaying = null;
  289. state = PlayerState.LIST_END
  290. // TODO SEEK TO END
  291. }
  292. } else if (current_id === videoPlaying.id) {
  293. if (!isLeader && player.getCurrentTime() - RTT_ESTIMATE - ALLOWED_AHEAD > 0) {
  294. player.seekTo(RTT_ESTIMATE + ALLOWED_AHEAD, true)
  295. }
  296. } else {
  297. console.error("Difficult state reached. Reset protocol not implemented. Either to far ahead, behind or state inconsistency", ended_id, current_id, videoPlaying)
  298. }
  299. }
  300. const searchResultTemplate = document.getElementById("searchResultTemplate")
  301. const searchResultList = searchResultTemplate.parentElement
  302. searchResultList.removeChild(searchResultTemplate)
  303. searchResultTemplate.id = ""
  304. function makeSearchResult(item) {
  305. const { id, snippet } = item
  306. const { videoId } = id
  307. const { thumbnails, title, channelTitle, publishTime, description } = snippet
  308. const searchResult = searchResultTemplate.cloneNode(true)
  309. searchResult.setAttribute('data-youtubeID', videoId)
  310. function onClickHandler() {
  311. socket.send(makeMessage(MessageTypes.LIST_OPERATION, {
  312. op: ListOperationTypes.ADD,
  313. code: videoId
  314. }));
  315. codeInfoMap.set(videoId, item)
  316. }
  317. searchResult.addEventListener("click", onClickHandler)
  318. searchResult.addEventListener("keydown", onClickHandler)
  319. const thumbnail = searchResult.getElementsByClassName("searchResultThumbnail")[0]
  320. const img = document.createElement('img')
  321. thumbnail.appendChild(img)
  322. img.setAttribute('src', thumbnails.default.url)
  323. img.setAttribute('width', thumbnails.default.width)
  324. img.setAttribute('height', thumbnails.default.height)
  325. img.setAttribute('alt', "")
  326. searchResult.getElementsByClassName("searchResultTitle")[0].innerText = title
  327. searchResult.getElementsByClassName("searchResultChannel")[0].innerText = channelTitle
  328. searchResult.getElementsByClassName("searchResultDescription")[0].innerText = description
  329. searchResult.removeAttribute('hidden')
  330. searchResultList.appendChild(searchResult)
  331. }
  332. function searchResultProcessor(_, data) {
  333. const { items } = data;
  334. searchResultList.innerHTML = '';
  335. for (const item of items) {
  336. makeSearchResult(item)
  337. }
  338. }
  339. function searchIdResultProcessor(_, data) {
  340. const { items } = data;
  341. for (const item of items) {
  342. console.log(item)
  343. const code = item.id;
  344. codeInfoMap.set(code, item);
  345. const lines = queueElement.querySelectorAll(`[data-youtubeID='${code}`)
  346. for (const line of lines) {
  347. if (line !== null) {
  348. const id = parseInt(line.getAttribute("data-id"))
  349. makeQueueLine(code, id, id)
  350. line.parentElement.removeChild(line)
  351. }
  352. }
  353. }
  354. }
  355. let socket;
  356. function onYTDone() {
  357. const resolver = new Resolver()
  358. resolver.register(MessageTypes.STATE, stateProcessor)
  359. resolver.register(MessageTypes.LIST_OPERATION, listOperationProcessor)
  360. resolver.register(MessageTypes.OBTAIN_CONTROL, () => setLeader(true))
  361. resolver.register(MessageTypes.RELEASE_CONTROL, () => setLeader(false))
  362. resolver.register(MessageTypes.SONG_END, songEndProcessor)
  363. resolver.register(MessageTypes.SEARCH, searchResultProcessor)
  364. resolver.register(MessageTypes.SEARCH_ID, searchIdResultProcessor)
  365. socket = resolver.connectSocket()
  366. socket.addEventListener("open", function () {
  367. socket.send(makeMessage(MessageTypes.STATE, null))
  368. })
  369. }
  370. function afterStateInit() {
  371. // document.getElementById('addVideoForm').addEventListener('submit', onSubmit);
  372. document.getElementById('searchVideoForm').addEventListener('submit', onSearch)
  373. document.getElementById('leader-button').addEventListener('click', onLeaderbutton)
  374. }