diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/Types.hx | 8 | ||||
| -rw-r--r-- | src/client/IPlayer.hx | 12 | ||||
| -rw-r--r-- | src/client/Main.hx | 45 | ||||
| -rw-r--r-- | src/client/Player.hx | 218 | ||||
| -rw-r--r-- | src/client/players/Raw.hx | 72 | ||||
| -rw-r--r-- | src/server/Main.hx | 64 | ||||
| -rw-r--r-- | src/server/ServerState.hx | 1 |
7 files changed, 300 insertions, 120 deletions
diff --git a/src/Types.hx b/src/Types.hx index 5706111..b98a6c2 100644 --- a/src/Types.hx +++ b/src/Types.hx @@ -35,7 +35,8 @@ typedef VideoItem = { url:String, title:String, author:String, - duration:Float + duration:Float, + isTemp:Bool } typedef WsEvent = { @@ -47,6 +48,7 @@ typedef WsEvent = { isUnknownClient:Bool, clientName:String, videoList:Array<VideoItem>, + itemPos:Int, globalIp:String }, ?login:{ @@ -73,6 +75,9 @@ typedef WsEvent = { ?removeVideo:{ url:String }, + ?skipVideo:{ + url:String + }, ?pause:{ time:Float }, @@ -108,6 +113,7 @@ enum abstract WsEventType(String) { // var RemoveClient; var AddVideo; var RemoveVideo; + var SkipVideo; var VideoLoaded; var Pause; var Play; diff --git a/src/client/IPlayer.hx b/src/client/IPlayer.hx new file mode 100644 index 0000000..9f224a8 --- /dev/null +++ b/src/client/IPlayer.hx @@ -0,0 +1,12 @@ +package client; + +import Types.VideoItem; + +interface IPlayer { + function loadVideo(item:VideoItem):Void; + function removeVideo():Void; + function play():Void; + function pause():Void; + function getTime():Float; + function setTime(time:Float):Void; +} diff --git a/src/client/Main.hx b/src/client/Main.hx index b2e224d..19ca44b 100644 --- a/src/client/Main.hx +++ b/src/client/Main.hx @@ -90,6 +90,18 @@ class Main { } }); } + final voteSkip = ge("#voteskip"); + voteSkip.onclick = e -> { + if (player.isListEmpty()) return; + final items = player.getItems(); + final pos = player.getItemPos(); + send({ + type: SkipVideo, + skipVideo: { + url: items[pos].url + } + }); + } ge("#queue_next").onclick = e -> addVideoUrl(false); ge("#queue_end").onclick = e -> addVideoUrl(true); @@ -112,23 +124,31 @@ class Main { function addVideoUrl(atEnd:Bool):Void { final mediaUrl:InputElement = cast ge("#mediaurl"); + final checkbox:InputElement = cast ge("#addfromurl").querySelector(".add-temp"); + final isTemp = checkbox.checked; final url = mediaUrl.value; if (url.length == 0) return; mediaUrl.value = ""; final url = ~/,(https?)/g.replace(url, "|$1"); final links = url.split("|"); - // if videos added as next, we need to load it in reverse order - final link = (atEnd || player.isListEmpty()) ? links.shift() : links.pop(); - addVideo(link, atEnd, () -> addVideoArray(links, atEnd)); + // if videos added as next, we need to load them in reverse order + if (!atEnd) { + // except first item when list empty + var first:Null<String> = null; + if (player.isListEmpty()) first = links.shift(); + links.reverse(); + if (player.isListEmpty()) links.unshift(first); + } + addVideoArray(links, atEnd, isTemp); } - function addVideoArray(links:Array<String>, atEnd:Bool):Void { + function addVideoArray(links:Array<String>, atEnd:Bool, isTemp:Bool):Void { if (links.length == 0) return; - final link = atEnd ? links.shift() : links.pop(); - addVideo(link, atEnd, () -> addVideoArray(links, atEnd)); + final link = links.shift(); + addVideo(link, atEnd, isTemp, () -> addVideoArray(links, atEnd, isTemp)); } - function addVideo(url:String, atEnd:Bool, callback:()->Void):Void { + function addVideo(url:String, atEnd:Bool, isTemp:Bool, callback:()->Void):Void { final protocol = Browser.location.protocol; if (url.startsWith("/")) { final host = Browser.location.hostname; @@ -153,6 +173,7 @@ class Main { title: name, author: personal.name, duration: duration, + isTemp: isTemp }, atEnd: atEnd }}); @@ -163,7 +184,7 @@ class Main { public function toggleVideoElement():Bool { if (player.hasVideo()) player.removeVideo(); else if (!player.isListEmpty()) { - player.setVideo(player.getItems()[0]); + player.setVideo(player.getItemPos()); } return player.hasVideo(); } @@ -231,8 +252,8 @@ class Main { addMessage(data.message.clientName, data.message.text); case AddVideo: - if (player.isListEmpty()) player.setVideo(data.addVideo.item); player.addVideoItem(data.addVideo.item, data.addVideo.atEnd); + if (player.itemsLength() == 1) player.setVideo(0); case VideoLoaded: player.setTime(0); @@ -242,6 +263,10 @@ class Main { player.removeItem(data.removeVideo.url); if (player.isListEmpty()) player.pause(); + case SkipVideo: + player.skipItem(data.skipVideo.url); + if (player.isListEmpty()) player.pause(); + case Pause: if (isLeader()) return; player.pause(); @@ -318,7 +343,7 @@ class Main { for (message in connected.history) { addMessage(message.name, message.text, message.time); } - player.setItems(connected.videoList); + player.setItems(connected.videoList, connected.itemPos); } function setConfig(config:Config):Void { diff --git a/src/client/Player.hx b/src/client/Player.hx index d8e4cb4..2c8f62e 100644 --- a/src/client/Player.hx +++ b/src/client/Player.hx @@ -1,10 +1,9 @@ package client; -import haxe.Timer; import js.html.Element; -import js.html.VideoElement; import js.Browser.document; import client.Main.ge; +import client.players.Raw; import Types.VideoItem; using StringTools; using Lambda; @@ -14,76 +13,93 @@ class Player { final main:Main; final items:Array<VideoItem> = []; final videoItemsEl = ge("#queue"); - final player:Element = ge("#ytapiplayer"); + final playerEl:Element = ge("#ytapiplayer"); + var player:Null<IPlayer>; + var currentSrc = ""; + var itemPos = 0; var isLoaded = false; var skipSetTime = false; - var video:VideoElement; + final matchYoutube = ~/v=([A-z0-9_-]+)/; public function new(main:Main):Void { this.main = main; } - public function setVideo(item:VideoItem):Void { + function setPlayer(player:IPlayer):Void { + this.player = player; + } + + function isYoutube(url:String):Bool { + if (!url.contains("youtube.com/")) return false; + if (!url.contains("youtu.be/")) return false; + if (!matchYoutube.match(url)) return false; + return true; + } + + public function setVideo(i:Int):Void { + final item = items[i]; + if (isYoutube(item.url)) {} // setPlayer(new Youtube(main, this)); + else setPlayer(new Raw(main, this)); + + final childs = videoItemsEl.children; + if (childs[itemPos] != null) { + childs[itemPos].classList.remove("queue_active"); + } + itemPos = i; + childs[itemPos].classList.add("queue_active"); + + currentSrc = item.url; + playerEl.textContent = ""; isLoaded = false; - video = document.createVideoElement(); - video.id = "videoplayer"; - item.url = main.tryLocalIp(item.url); - video.src = item.url; - video.controls = true; - final isTouch = untyped __js__("'ontouchstart' in window"); - if (!isTouch) Timer.delay(() -> { - video.controls = false; - video.onmouseover = e -> { - video.controls = true; - video.onmouseover = null; - video.onmousemove = null; + player.loadVideo(item); + ge("#currenttitle").textContent = item.title; + } + + public function removeVideo():Void { + currentSrc = ""; + player.removeVideo(); + ge("#currenttitle").textContent = Lang.get("nothingPlaying"); + } + + public function onCanBePlayed():Void { + if (!isLoaded) main.send({type: VideoLoaded}); + isLoaded = true; + } + + public function onPlay():Void { + if (!main.isLeader()) return; + main.send({ + type: Play, play: { + time: getTime() } - video.onmousemove = video.onmouseover; - }, 3000); - video.oncanplaythrough = e -> { - if (!isLoaded) main.send({type: VideoLoaded}); - isLoaded = true; - } - video.onseeking = e -> { - if (skipSetTime) { - skipSetTime = false; - return; + }); + } + + public function onPause():Void { + if (!main.isLeader()) return; + main.send({ + type: Pause, pause: { + time: getTime() } - if (!main.isLeader()) return; - main.send({ - type: SetTime, - setTime: { - time: video.currentTime - } - }); - } - video.onpause = e -> { - if (!main.isLeader()) return; - main.send({ - type: Pause, - pause: { - time: video.currentTime - } - }); - } - video.onplay = e -> { - if (!main.isLeader()) return; - main.send({ - type: Play, - play: { - time: video.currentTime - } - }); + }); + } + + public function onSetTime():Void { + if (skipSetTime) { + skipSetTime = false; + return; } - player.textContent = ""; - player.appendChild(video); - ge("#currenttitle").textContent = item.title; + if (!main.isLeader()) return; + main.send({ + type: SetTime, setTime: { + time: getTime() + } + }); } public function addVideoItem(item:VideoItem, atEnd:Bool):Void { - items.push(item); final itemEl = nodeFromString( - '<li class="queue_entry pluid-0 queue_temp queue_active" title="${Lang.get("addedBy")}: ${item.author}"> + '<li class="queue_entry pluid-0" title="${Lang.get("addedBy")}: ${item.author}"> <a class="qe_title" href="${item.url}" target="_blank">${item.title.htmlEscape()}</a> <span class="qe_time">${duration(item.duration)}</span> <div class="qe_clear"></div> @@ -103,6 +119,7 @@ class Player { </div> </li>' ); + if (item.isTemp) itemEl.classList.add("queue_temp"); final deleteBtn = itemEl.querySelector("#btn-delete"); deleteBtn.onclick = e -> { main.send({ @@ -112,18 +129,13 @@ class Player { } }); } + if (atEnd) items.push(item); + else items.insert(itemPos + 1, item); if (atEnd) videoItemsEl.appendChild(itemEl); - else Utils.insertAtIndex(videoItemsEl, itemEl, 1); + else Utils.insertAtIndex(videoItemsEl, itemEl, itemPos + 1); updateCounters(); } - public function removeVideo():Void { - if (video == null) return; - player.removeChild(video); - video = null; - ge("#currenttitle").textContent = Lang.get("nothingPlaying"); - } - public function removeItem(url:String):Void { for (child in videoItemsEl.children) { if (child.querySelector(".qe_title").getAttribute("href") == url) { @@ -132,15 +144,32 @@ class Player { } } - items.remove( - items.find(item -> item.url == url) - ); + final item = items.find(item -> item.url == url); + if (item == null) return; + var index = items.indexOf(item); + items.remove(item); updateCounters(); - if (video == null) return; - if (video.src == url) { - if (items.length > 0) setVideo(items[0]); + if (index < itemPos) { + itemPos--; + return; + } + if (index != itemPos) return; + if (items.length == 0) return; + if (items[index] == null) index = 0; + setVideo(index); + } + + public function skipItem(url:String):Void { + final item = items.find(item -> item.url == url); + if (item == null) return; + if (item.isTemp) { + removeItem(url); + return; } + var index = items.indexOf(item) + 1; + if (index >= items.length) index = 0; + setVideo(index); } function updateCounters():Void { @@ -152,15 +181,12 @@ class Player { return items; } - public function setItems(list:Array<VideoItem>):Void { + public function setItems(list:Array<VideoItem>, ?pos:Int):Void { clearItems(); + if (pos != null) itemPos = pos; if (list.length == 0) return; - if (video == null || video.src != list[0].url) { - setVideo(list[0]); - } - for (video in list) { - addVideoItem(video, true); - } + for (video in list) addVideoItem(video, true); + if (currentSrc != items[itemPos].url) setVideo(itemPos); } public function clearItems():Void { @@ -172,7 +198,7 @@ class Player { public function refresh():Void { if (items.length == 0) return; removeVideo(); - setVideo(items[0]); + setVideo(itemPos); } function duration(time:Float):String { @@ -203,29 +229,37 @@ class Player { return items.length == 0; } - public function hasVideo():Bool { - return video != null; + public function itemsLength():Int { + return items.length; } - public function pause():Void { - if (video == null) return; - video.pause(); + public function getItemPos():Int { + return itemPos; + } + + public function hasVideo():Bool { + return player != null; } public function play():Void { - if (video == null) return; - video.play(); + if (player == null) return; + player.play(); } - public function setTime(time:Float, isLocal = true):Void { - if (video == null) return; - skipSetTime = isLocal; - video.currentTime = time; + public function pause():Void { + if (player == null) return; + player.pause(); } public function getTime():Float { - if (video == null) return 0; - return video.currentTime; + if (player == null) return 0; + return player.getTime(); + } + + public function setTime(time:Float, isLocal = true):Void { + if (player == null) return; + skipSetTime = isLocal; + player.setTime(time); } } diff --git a/src/client/players/Raw.hx b/src/client/players/Raw.hx new file mode 100644 index 0000000..41b421c --- /dev/null +++ b/src/client/players/Raw.hx @@ -0,0 +1,72 @@ +package client.players; + +import haxe.Timer; +import js.html.Element; +import js.html.VideoElement; +import js.Browser.document; +import client.Main.ge; +import Types.VideoItem; + +class Raw implements IPlayer { + + final main:Main; + final player:Player; + var video:VideoElement; + final playerEl:Element = ge("#ytapiplayer"); + + public function new(main:Main, player:Player) { + this.main = main; + this.player = player; + } + + public function loadVideo(item:VideoItem):Void { + video = document.createVideoElement(); + video.id = "videoplayer"; + final url = main.tryLocalIp(item.url); + video.src = url; + video.controls = true; + final isTouch = untyped __js__("'ontouchstart' in window"); + if (!isTouch) Timer.delay(() -> { + video.controls = false; + video.onmouseover = e -> { + video.controls = true; + video.onmouseover = null; + video.onmousemove = null; + } + video.onmousemove = video.onmouseover; + }, 3000); + video.oncanplaythrough = player.onCanBePlayed; + video.onseeking = player.onSetTime; + video.onplay = player.onPlay; + video.onpause = player.onPause; + playerEl.appendChild(video); + video.pause(); + } + + public function removeVideo():Void { + if (video == null) return; + playerEl.removeChild(video); + video = null; + } + + public function play():Void { + if (video == null) return; + video.play(); + } + + public function pause():Void { + if (video == null) return; + video.pause(); + } + + public function getTime():Float { + if (video == null) return 0; + return video.currentTime; + } + + public function setTime(time:Float):Void { + if (video == null) return; + video.currentTime = time; + } + +} diff --git a/src/server/Main.hx b/src/server/Main.hx index 5d82ef1..e7895ca 100644 --- a/src/server/Main.hx +++ b/src/server/Main.hx @@ -31,6 +31,7 @@ class Main { final videoList:Array<VideoItem> = []; final videoTimer = new VideoTimer(); final messages:Array<Message> = []; + var itemPos = 0; static function main():Void new Main(); @@ -99,6 +100,7 @@ class Main { trace("Saving state..."); final data:ServerState = { videoList: videoList, + itemPos: itemPos, messages: messages, timer: { time: videoTimer.getTime(), @@ -116,6 +118,7 @@ class Main { videoList.resize(0); messages.resize(0); for (item in data.videoList) videoList.push(item); + itemPos = data.itemPos; for (message in data.messages) messages.push(message); videoTimer.start(); videoTimer.setTime(data.timer.time); @@ -166,6 +169,7 @@ class Main { for (client in clients) client.getData() ], videoList: videoList, + itemPos: itemPos, globalIp: globalIp } }); @@ -244,13 +248,17 @@ class Main { case AddVideo: final item = data.addVideo.item; + final local = '$localIp:$port'; + if (item.url.contains(local)) { + item.url = item.url.replace(local, '$globalIp:$port'); + } item.author = client.name; - final localOrigin = '$localIp:$port'; - if (item.url.indexOf(localOrigin) != -1) { - item.url = item.url.replace(localOrigin, '$globalIp:$port'); + if (videoList.exists(i -> i.url == item.url)) { + // TODO send server message + return; } if (data.addVideo.atEnd) videoList.push(item); - else videoList.insert(1, item); + else videoList.insert(itemPos + 1, item); broadcast(data); // Initial timer start if VideoLoaded is not happen if (videoList.length == 1) restartWaitTimer(); @@ -262,12 +270,30 @@ class Main { case RemoveVideo: if (videoList.length == 0) return; final url = data.removeVideo.url; - final isFirst = videoList[0].url == url; - if (isFirst) videoTimer.stop(); - videoList.remove( - videoList.find(item -> item.url == url) - ); - if (isFirst && videoList.length > 0) restartWaitTimer(); + final item = videoList.find(item -> item.url == url); + if (item == null) return; + final index = videoList.indexOf(item); + final isCurrent = videoList[itemPos].url == url; + if (index < itemPos) itemPos--; + videoList.remove(item); + if (isCurrent) { + if (itemPos >= videoList.length) itemPos = 0; + videoTimer.stop(); + if (videoList.length > 0) restartWaitTimer(); + } + broadcast(data); + + case SkipVideo: + if (videoList.length == 0) return; + final item = videoList[itemPos]; + if (item.url != data.skipVideo.url) return; + + if (!item.isTemp) itemPos++; + else videoList.remove(item); + if (itemPos >= videoList.length) itemPos = 0; + + videoTimer.stop(); + if (videoList.length > 0) restartWaitTimer(); broadcast(data); case Pause: @@ -284,12 +310,12 @@ class Main { case GetTime: if (videoList.length == 0) return; - if (videoTimer.getTime() > videoList[0].duration) { + if (videoTimer.getTime() > videoList[itemPos].duration) { videoTimer.stop(); onMessage(client, { - type: RemoveVideo, - removeVideo: { - url: videoList[0].url + type: SkipVideo, + skipVideo: { + url: videoList[itemPos].url } }); return; @@ -338,14 +364,18 @@ class Main { case ClearPlaylist: videoTimer.stop(); videoList.resize(0); + itemPos = 0; broadcast(data); case ShufflePlaylist: if (videoList.length == 0) return; - final first = videoList.shift(); + final current = videoList[itemPos]; + videoList.remove(current); Utils.shuffle(videoList); - videoList.unshift(first); - broadcast({type: UpdatePlaylist, updatePlaylist: { + videoList.insert(itemPos, current); + broadcast({ + type: UpdatePlaylist, + updatePlaylist: { videoList: videoList }}); case UpdatePlaylist: // client-only diff --git a/src/server/ServerState.hx b/src/server/ServerState.hx index 06240e5..02cdc40 100644 --- a/src/server/ServerState.hx +++ b/src/server/ServerState.hx @@ -5,6 +5,7 @@ import Types.VideoItem; typedef ServerState = { videoList:Array<VideoItem>, + itemPos:Int, messages:Array<Message>, timer:{ time:Float, |
