123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645 |
- import { makeMessage, Resolver } from './websocketResolver.js';
- import { ListOperationTypes, MediaAction, MessageTypes, PlayerState, YoutubeResourceType } from "./enums.js";
- window.onYouTubeIframeAPIReady = onYouTubeIframeAPIReady
- const youtubeApiScript = document.createElement('script');
- youtubeApiScript.src = "https://www.youtube.com/iframe_api";
- const firstScriptTag = document.getElementsByTagName('script')[0];
- firstScriptTag.parentNode.insertBefore(youtubeApiScript, firstScriptTag);
- const RTT_ESTIMATE = 1
- const ALLOWED_AHEAD = 5
- const PLAYER_WIDTH = 640
- const PLAYER_HEIGHT = 360
- let videos = null;
- let videosPlayed = null;
- let videoPlaying = null;
- let state = null;
- let isLeader = null;
- let repeat = false;
- let player = null;
- let playerActive = false;
- document.getElementById('player').append(mockPlayer('360', '640'));
- const codeInfoMap = new Map()
- function onYouTubeIframeAPIReady() {
- onYTDone();
- }
- function onPlayerReady(event) {
- event.target.seekTo(0);
- if (state === PlayerState.PLAYING) {
- event.target.playVideo();
- } else if (state === PlayerState.PAUSED) {
- event.target.pauseVideo();
- } else if (state === PlayerState.LIST_END) {
- event.target.stopVideo();
- }
- }
- function onPlayerStateChange(event) {
- console.log(event.data, YT.PlayerState, state, videoPlaying, videos);
- if (event.data === 0 /*Ended*/) {
- if (state !== PlayerState.PLAYING) {
- console.warn("Player ended up in state ENDED while it is was not playing.")
- return
- }
- if (isLeader && videoPlaying !== null) {
- socket.send(makeMessage(MessageTypes.SONG_END, { id: videoPlaying.id }))
- }
- const vid = popVideo()
- if (vid !== undefined) {
- playVideo(vid);
- } else {
- videoPlaying = null;
- state = PlayerState.LIST_END;
- }
- }
- }
- function onAddVideo() {
- console.log(state, videoPlaying, videos)
- switch (state) {
- case PlayerState.LIST_END:
- if (videoPlaying === null) {
- playVideo(popVideo());
- } else {
- console.error(`Invalid state: state=${state}; videoPlaying=${videoPlaying}`)
- return
- }
- break
- case PlayerState.PAUSED:
- if (videoPlaying === null) {
- loadVideo(popVideo())
- }
- break
- case PlayerState.PLAYING:
- if (videoPlaying === null) {
- playVideo(popVideo())
- }
- break
- default:
- console.error("Unknown state", state)
- break
- }
- }
- function addVideo(code, id) {
- videos.push({ code, id });
- makeQueueLine(code, id);
- onAddVideo();
- }
- function popVideo() {
- console.log("pop", videos)
- if (videos.length <= 0) {
- if (videosPlayed) {
- videos = []
- for (const { code, id } of videosPlayed) {
- videos.push({code, id});
- makeQueueLine(code, id);
- }
- videosPlayed = []
- } else {
- state = PlayerState.LIST_END
- return undefined;
- }
- }
- const vid = videos.shift();
- queueElement.removeChild(queueElement.querySelector(".videoListCard"));
- if (videosPlayed) {
- videosPlayed.push(vid);
- }
- return vid;
- }
- function findVideoIndex(id) {
- return videos.findIndex(function (vid) {
- return vid.id === id
- })
- }
- function delVideo(id) {
- videos.splice(findVideoIndex(id), 1)
- removeQueueLine(id)
- }
- function moveVideo(id, displacement) {
- const i = findVideoIndex(id)
- const [vid] = videos.splice(i, 1)
- const new_i = i + displacement
- videos.splice(new_i, 0, vid)
- removeQueueLine(id)
- if (new_i + 1 === videos.length) {
- makeQueueLine(vid.code, id)
- } else {
- console.log(videos, new_i)
- makeQueueLine(vid.code, id, videos[new_i + 1].id)
- }
- }
- function playVideo(vid) {
- videoPlaying = vid;
- state = PlayerState.PLAYING
- if (!playerActive) {
- return;
- }
- if (player === null) {
- buildPlayer(PLAYER_HEIGHT, PLAYER_WIDTH, vid.code);
- } else {
- player.loadVideoById(vid.code, 0);
- }
- }
- function loadVideo(vid) {
- videoPlaying = vid;
- if (!playerActive) {
- return
- }
- if (player === null) {
- buildPlayer(PLAYER_HEIGHT, PLAYER_WIDTH, vid.code);
- } else {
- player.cueVideoById(vid.code, 0);
- }
- }
- function setLeader(b) {
- console.log(isLeader, b)
- if (isLeader !== b) {
- let btn = document.getElementById("leader-button");
- btn.innerText = b ? "Leader" : "Follower";
- btn.classList.remove(b ? "btn-outline-success" : "btn-success")
- btn.classList.add(b ? "btn-success" : "btn-outline-success")
- if (isLeader === null) {
- btn.classList.remove("disabled", "btn-outline-secondary")
- }
- isLeader = b;
- }
- }
- window.setLeader = setLeader
- function mockPlayer(height, width) {
- const rect = document.createElement('div');
- rect.setAttribute('style', `height:${height}px;width:${width}px;background:black`);
- return rect
- }
- function buildPlayer(height, width, id) {
- player = new YT.Player('player', {
- height: height + 48,
- width: width,
- videoId: id,
- playerVars: {},
- events: {
- 'onReady': onPlayerReady,
- 'onStateChange': onPlayerStateChange,
- }
- });
- window.player = player
- }
- const queueElement = document.getElementById('videoList')
- const queueLine = document.getElementById('videoListCardTemplate')
- function makeQueueLine(code, id, before_id) {
- let newQueueLine = queueLine.cloneNode(true);
- newQueueLine.id = "";
- newQueueLine.hidden = false;
- newQueueLine.setAttribute("data-id", id)
- if (codeInfoMap.has(code)) {
- const { thumbnails, title, channelTitle, publishTime, description } = codeInfoMap.get(code)
- const thumbnail = newQueueLine.getElementsByClassName("videoListCardThumbnail")[0]
- const img = document.createElement('img')
- thumbnail.appendChild(img)
- img.setAttribute('src', thumbnails.default.url)
- img.setAttribute('width', thumbnails.default.width)
- img.setAttribute('height', thumbnails.default.height)
- img.setAttribute('alt', "")
- newQueueLine.getElementsByClassName("videoListCardTitle")[0].innerText = title
- newQueueLine.getElementsByClassName("videoListCardChannel")[0].innerText = channelTitle
- newQueueLine.getElementsByClassName("videoListCardDescription")[0].innerText = description.replace(/\n/g, " ")
- }
- newQueueLine.setAttribute("data-youtubeId", code)
- function delHandler(event) {
- onDeleteClick(event, id)
- }
- const delButton = newQueueLine.querySelector('.videoListCardDelete')
- delButton.addEventListener("click", delHandler)
- delButton.addEventListener("keydown", delHandler)
- function moveUpHandler(event) {
- onMoveClick(event, id, -1)
- }
- const upButton = newQueueLine.querySelector('.videoListCardMoveUp')
- upButton.addEventListener("click", moveUpHandler)
- upButton.addEventListener("keydown", moveUpHandler)
- function moveDownHandler(event) {
- onMoveClick(event, id, 1)
- }
- const downButton = newQueueLine.querySelector('.videoListCardMoveDown')
- downButton.addEventListener("click", moveDownHandler)
- downButton.addEventListener("keydown", moveDownHandler)
- if (before_id == null) {
- queueLine.before(newQueueLine);
- } else {
- queueElement.querySelector(`[data-id='${before_id}']`).before(newQueueLine)
- }
- }
- function removeQueueLine(id) {
- const card = queueElement.querySelector(`div[data-id='${id}']`);
- queueElement.removeChild(card);
- }
- function onSubmit(event) {
- event.preventDefault();
- socket.send(makeMessage(MessageTypes.LIST_OPERATION, {
- op: ListOperationTypes.ADD,
- code: event.target[0].value
- }))
- }
- function onSearch(event) {
- event.preventDefault();
- const q = event.target[0].value
- if (q !== "") {
- socket.send(makeMessage(MessageTypes.SEARCH, { q }))
- }
- }
- function onDeleteClick(event, id) {
- event.preventDefault();
- socket.send(makeMessage(MessageTypes.LIST_OPERATION, { op: ListOperationTypes.DEL, id }))
- }
- function onMoveClick(event, id, displacement) {
- event.preventDefault();
- socket.send(makeMessage(MessageTypes.LIST_OPERATION, { op: ListOperationTypes.MOVE, id, displacement }))
- }
- function onLeaderbutton(event) {
- if (isLeader) {
- socket.send(makeMessage(MessageTypes.RELEASE_CONTROL))
- } else {
- socket.send(makeMessage(MessageTypes.OBTAIN_CONTROL))
- }
- }
- const playerPlaceholder = document.getElementById('playerPlaceholder');
- const playerPlaceholderParent = playerPlaceholder.parentElement;
- const playerContainer = document.getElementById('playerContainer')
- const showPlaceholderButton = document.getElementById('showPlayerPlaceholder');
- function onPlayerStart(event) {
- event.preventDefault();
- playerActive = true;
- playerPlaceholderParent.removeChild(playerPlaceholder)
- playerContainer.toggleAttribute("hidden")
- switch (state) {
- case PlayerState.LIST_END:
- break;
- case PlayerState.PAUSED:
- if (videoPlaying !== null) {
- loadVideo(videoPlaying);
- } else {
- console.error(`Invalid state: state=${state}; videoPlaying=${videoPlaying}`)
- return
- }
- break;
- case PlayerState.PLAYING:
- if (videoPlaying !== null) {
- playVideo(videoPlaying);
- } else {
- console.error(`Invalid state: state=${state}; videoPlaying=${videoPlaying}`)
- return
- }
- break;
- }
- socket.send(makeMessage(MessageTypes.PLAYER_ENABLED, { enabled: true }))
- }
- function onPlayerClose(event) {
- event.preventDefault();
- playerActive = false;
- playerContainer.toggleAttribute("hidden")
- playerPlaceholderParent.appendChild(playerPlaceholder)
- if (player !== null) {
- player.pauseVideo();
- }
- socket.send(makeMessage(MessageTypes.PLAYER_ENABLED, { enabled: false }))
- }
- function hidePlayerPlaceholder(event) {
- event.preventDefault();
- playerPlaceholderParent.removeChild(playerPlaceholder);
- showPlaceholderButton.toggleAttribute("hidden")
- }
- function showPlayerPlaceholder(event) {
- event.preventDefault();
- playerPlaceholderParent.appendChild(playerPlaceholder);
- showPlaceholderButton.toggleAttribute("hidden")
- }
- function onPlayButton(event) {
- event.preventDefault();
- socket.send(makeMessage(MessageTypes.MEDIA_ACTION, { action: MediaAction.PLAY }))
- }
- function onPauseButton(event) {
- event.preventDefault();
- socket.send(makeMessage(MessageTypes.MEDIA_ACTION, { action: MediaAction.PAUSE }))
- }
- function onNextButton(event) {
- event.preventDefault();
- if (videoPlaying !== null) {
- socket.send(makeMessage(MessageTypes.MEDIA_ACTION, { action: MediaAction.NEXT, current_id: videoPlaying.id }))
- }
- }
- const repeatButton = document.getElementById('repeat-button');
- function onRepeatButton(event) {
- event.preventDefault();
- socket.send(makeMessage(MessageTypes.MEDIA_ACTION, { action: MediaAction.REPEAT, enable: !repeat }))
- }
- function stateProcessor(ws, data) {
- const { playing, state: newState, lists } = data;
- const { next, previous } = lists
- videos = []
- videosPlayed = previous
- repeat = !!previous
- state = newState
- videoPlaying = playing
- const codes = []
- for (const song of next) {
- const { code, id } = song
- addVideo(code, id)
- if (!(codes.includes(code))) {
- codes.push(code)
- }
- }
- for (const song of previous || []) {
- const { code, id } = song;
- if (!(codes.includes(code))) {
- codes.push(code)
- }
- }
- if (codes.length > 0) {
- socket.send(makeMessage(MessageTypes.SEARCH_ID, { id: codes }))
- }
- if (videoPlaying !== null) {
- if (state === PlayerState.PLAYING) {
- playVideo(videoPlaying)
- } else {
- loadVideo(videoPlaying)
- }
- }
- if (isLeader === null) {
- setLeader(false)
- }
- if (repeat) {
- repeatButton.classList.toggle('btn-outline-secondary');
- repeatButton.classList.toggle('btn-secondary');
- }
- afterStateInit()
- }
- function listOperationProcessor(ws, data) {
- const { op, items } = data;
- if (op === ListOperationTypes.ADD) {
- const noCodeInfo = []
- for (const { code, id, snippet } of items) {
- if (snippet !== undefined) {
- codeInfoMap.set(code, snippet)
- } else if (!codeInfoMap.has(code)) {
- noCodeInfo.push(code)
- }
- addVideo(code, id);
- }
- if (noCodeInfo.length > 0) {
- socket.send(makeMessage(MessageTypes.SEARCH_ID, { id: noCodeInfo.join(',') }))
- }
- } else if (op === ListOperationTypes.DEL) {
- for (const { id } of items) {
- delVideo(id);
- }
- } else if (op === ListOperationTypes.MOVE) {
- for (const { id, displacement } of items) {
- moveVideo(id, displacement)
- }
- }
- }
- function mediaActionProcessor(ws, data) {
- const { action, ended_id, current_id, enable } = data;
- if (action === MediaAction.PLAY && state === PlayerState.PAUSED) {
- state = PlayerState.PLAYING
- player.playVideo();
- } else if (action === MediaAction.PAUSE && state === PlayerState.PLAYING) {
- state = PlayerState.PAUSED
- player.pauseVideo();
- } else if (action === MediaAction.NEXT) {
- if (videoPlaying !== null && videoPlaying.id === ended_id) {
- const vid = popVideo()
- if (vid !== undefined) {
- playVideo(vid)
- } else {
- videoPlaying = null;
- state = PlayerState.LIST_END;
- if (player) {
- player.stopVideo();
- }
- }
- }
- } else if (action === MediaAction.REPEAT) {
- if (enable !== repeat) {
- repeat = enable;
- repeatButton.classList.toggle('btn-outline-secondary');
- repeatButton.classList.toggle('btn-secondary');
- videosPlayed = repeat ? (videoPlaying ? [videoPlaying] : []) : null;
- }
- }
- }
- function songEndProcessor(ws, data) {
- const { ended_id, current_id } = data;
- console.log(ended_id, current_id)
- if (videoPlaying === null) {
- // Do nothing
- } else if (ended_id === videoPlaying.id) {
- const vid = popVideo()
- console.log(vid)
- if (vid !== undefined) {
- playVideo(vid);
- } else {
- videoPlaying = null;
- state = PlayerState.LIST_END
- // TODO SEEK TO END
- }
- } else if (current_id === videoPlaying.id) {
- if (!isLeader && player.getCurrentTime() - RTT_ESTIMATE - ALLOWED_AHEAD > 0) {
- player.seekTo(RTT_ESTIMATE + ALLOWED_AHEAD, true)
- }
- } else {
- console.error("Difficult state reached. Reset protocol not implemented. Either to far ahead, behind or state inconsistency", ended_id, current_id, videoPlaying)
- }
- }
- const searchResultTemplate = document.getElementById("searchResultTemplate")
- const searchResultList = searchResultTemplate.parentElement
- searchResultList.removeChild(searchResultTemplate)
- searchResultTemplate.id = ""
- function makeSearchResult(item) {
- const { id, snippet } = item
- const { kind, videoId, channelId, playlistId } = id
- const { thumbnails, title, channelTitle, publishTime, description } = snippet
- const code = kind === YoutubeResourceType.CHANNEL ? channelId :
- kind === YoutubeResourceType.PLAYLIST ? playlistId :
- kind === YoutubeResourceType.VIDEO ? videoId :
- console.error(`Unknown kind ${kind}`)
- if (!code) return;
- const searchResult = searchResultTemplate.cloneNode(true)
- searchResult.setAttribute('data-youtubeID', code)
- function onClickHandler() {
- socket.send(makeMessage(MessageTypes.LIST_OPERATION, {
- op: ListOperationTypes.ADD,
- kind,
- code
- }));
- if (kind === YoutubeResourceType.VIDEO) {
- codeInfoMap.set(videoId, snippet)
- }
- }
- searchResult.addEventListener("click", onClickHandler)
- searchResult.addEventListener("keydown", onClickHandler)
- const thumbnailContainer = searchResult.getElementsByClassName("searchResultThumbnail")[0]
- const thumbnailImage = thumbnailContainer.getElementsByClassName("thumbnailImage")[0]
- const img = document.createElement('img')
- thumbnailImage.appendChild(img)
- const { url, width, height } = (thumbnails ? thumbnails["default"] : {
- url: "/img/no_thumbnail.png",
- width: 120,
- height: 90
- })
- img.setAttribute('src', url)
- img.setAttribute('width', width)
- img.setAttribute('height', height)
- img.setAttribute('alt', "")
- if (kind !== YoutubeResourceType.PLAYLIST) {
- const playlistOverlay = thumbnailContainer.getElementsByClassName("thumbnailPlaylistOverlay")[0]
- playlistOverlay.parentElement.removeChild(playlistOverlay);
- }
- searchResult.getElementsByClassName("searchResultTitle")[0].innerText = title
- searchResult.getElementsByClassName("searchResultChannel")[0].innerText = channelTitle
- searchResult.getElementsByClassName("searchResultDescription")[0].innerText = description
- searchResult.removeAttribute('hidden')
- searchResultList.appendChild(searchResult)
- }
- function searchResultProcessor(_, data) {
- const { items } = data;
- searchResultList.innerHTML = '';
- for (const item of items) {
- makeSearchResult(item)
- }
- }
- function searchIdResultProcessor(_, data) {
- const { items } = data;
- for (const { id: code, snippet } of items) {
- codeInfoMap.set(code, snippet);
- const lines = queueElement.querySelectorAll(`[data-youtubeID='${code}`)
- for (const line of lines) {
- if (line !== null) {
- const id = parseInt(line.getAttribute("data-id"))
- makeQueueLine(code, id, id)
- line.parentElement.removeChild(line)
- }
- }
- }
- }
- let socket;
- function onYTDone() {
- const resolver = new Resolver()
- resolver.register(MessageTypes.STATE, stateProcessor)
- resolver.register(MessageTypes.LIST_OPERATION, listOperationProcessor)
- resolver.register(MessageTypes.MEDIA_ACTION, mediaActionProcessor)
- resolver.register(MessageTypes.OBTAIN_CONTROL, () => setLeader(true))
- resolver.register(MessageTypes.RELEASE_CONTROL, () => setLeader(false))
- resolver.register(MessageTypes.SONG_END, songEndProcessor)
- resolver.register(MessageTypes.SEARCH, searchResultProcessor)
- resolver.register(MessageTypes.SEARCH_ID, searchIdResultProcessor)
- socket = resolver.connectSocket()
- socket.addEventListener("open", function () {
- socket.send(makeMessage(MessageTypes.STATE, null))
- })
- }
- function afterStateInit() {
- // document.getElementById('addVideoForm').addEventListener('submit', onSubmit);
- document.getElementById('searchVideoForm').addEventListener('submit', onSearch)
- document.getElementById('leader-button').addEventListener('click', onLeaderbutton)
- document.getElementById('startPlayerButton').addEventListener('click', onPlayerStart)
- document.getElementById('closePlayer').addEventListener('click', onPlayerClose)
- document.getElementById('hidePlayerPlaceholder').addEventListener('click', hidePlayerPlaceholder)
- document.getElementById('showPlayerPlaceholder').addEventListener('click', showPlayerPlaceholder)
- document.getElementById('play-button').addEventListener('click', onPlayButton)
- document.getElementById('pause-button').addEventListener('click', onPauseButton)
- document.getElementById('next-button').addEventListener('click', onNextButton)
- document.getElementById('repeat-button').addEventListener('click', onRepeatButton)
- }
|