main.js 11 KB

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