Parcourir la source

Implement the ability to disable the player

Clients with a disabled player will not be elected leader.
Clients start with the player disabled.

Also widen the container for wider screens
niels il y a 4 ans
Parent
commit
ada1ead54e

+ 24 - 11
channel.py

@@ -1,21 +1,34 @@
-from typing import List
+from typing import Dict
+
+from websockets import WebSocketServerProtocol
+
+
+class Subscriber:
+    player_enabled = False
+    ws: WebSocketServerProtocol
+
+    def __init__(self, ws):
+        self.ws = ws
 
 
 class Channel:
-    subscribers: List
+    subscribers: Dict[WebSocketServerProtocol, Subscriber]
 
     def __init__(self):
-        self.subscribers = []
+        self.subscribers = dict()
+
+    def subscribe(self, ws: WebSocketServerProtocol):
+        self.subscribers[ws] = (Subscriber(ws))
 
-    def subscribe(self, ws):
-        self.subscribers.append(ws)
+    def unsubscribe(self, ws: WebSocketServerProtocol):
+        self.subscribers.pop(ws)
 
-    def unsubscribe(self, ws):
-        self.subscribers.remove(ws)
+    def get_player_enabled_subscribers(self):
+        return filter(lambda s: s.player_enabled, self.subscribers.values())
 
     async def send(self, message):
-        for ws in self.subscribers:
-            if ws.open:
-                await ws.send(message)
+        for sub in list(self.subscribers.values()):
+            if sub.ws.open:
+                await sub.ws.send(message)
             else:
-                self.unsubscribe(ws)
+                self.unsubscribe(sub.ws)

+ 28 - 26
chube.py

@@ -6,7 +6,7 @@ from itertools import cycle
 from websockets import WebSocketServerProtocol
 
 import chube_search
-from channel import Channel
+from channel import Channel, Subscriber
 from chube_enums import *
 from chube_ws import Resolver, Message, start_server, make_message
 
@@ -111,7 +111,7 @@ class Playback:
 class Room:
     chueue: Chueue
     channel: Channel
-    controller: Optional[WebSocketServerProtocol]
+    controller: Optional[Subscriber]
     controller_lock: RLock
     playback: Playback
 
@@ -123,8 +123,7 @@ class Room:
         self.controller = None
 
 
-
-rooms = {}
+rooms: Dict[str, Room] = dict()
 
 
 async def request_state_processor(ws, data, path):
@@ -183,7 +182,7 @@ async def song_end_processor(ws, data, path):
     room = rooms[path]
     old_song_id = data["id"]
     with room.controller_lock, room.playback.lock:
-        if ws is room.controller and old_song_id == room.playback.get_song_id():
+        if room.controller is not None and ws is room.controller.ws and old_song_id == room.playback.get_song_id():
             new_song = room.chueue.pop()
             room.playback.set_song(new_song)
             if new_song is None:
@@ -191,33 +190,42 @@ async def song_end_processor(ws, data, path):
                 new_song_id = None
             else:
                 new_song_id = new_song["id"]
-            await room.channel.send(make_message(Message.SONG_END, {"ended_id": old_song_id, "current_id": new_song_id}))
+            await room.channel.send(
+                make_message(Message.SONG_END, {"ended_id": old_song_id, "current_id": new_song_id}))
 
 
 async def playback_processor(ws, data, path):
     room = rooms[path]
 
 
+async def player_enabled_processor(ws, data, path):
+    room = rooms[path]
+    room.channel.subscribers[ws].player_enabled = data["enabled"]
+    if data["enabled"]:
+        with room.controller_lock:
+            if room.controller is None:
+                await obtain_control(ws, room)
+    else:
+        await release_control(ws, room)
+
+
 # TODO There is some potential concurrent bug here, when the controller loses/releases control right before a song end.
-async def obtain_control(ws, room):
+async def obtain_control(ws, room: Room):
     with room.controller_lock:
-        if room.controller is not ws:
+        if room.controller is None or room.controller.ws is not ws:
             prev_controller = room.controller
-            room.controller = ws
+            room.controller = room.channel.subscribers[ws]
             await ws.send(make_message(Message.OBTAIN_CONTROL))
             if prev_controller is not None:
-                await prev_controller.send(make_message(Message.RELEASE_CONTROL))
+                await prev_controller.ws.send(make_message(Message.RELEASE_CONTROL))
 
 
-async def release_control(ws, room):
+async def release_control(ws, room: Room):
     with room.controller_lock:
-        if room.controller is ws:
-            subs = room.channel.subscribers
-            if len(subs) > 0:
-                room.controller = subs[0]
-                await room.controller.send(make_message(Message.OBTAIN_CONTROL))
-            else:
-                room.controller = None
+        if room.controller is not None and room.controller.ws is ws:
+            room.controller = next(room.channel.get_player_enabled_subscribers(), None)
+            if room.controller is not None:
+                await room.controller.ws.send(make_message(Message.OBTAIN_CONTROL))
             await ws.send(make_message(Message.RELEASE_CONTROL))
 
 
@@ -226,14 +234,6 @@ async def on_connect(ws, path):
         rooms[path] = Room()
     room = rooms[path]
     room.channel.subscribe(ws)
-    with room.controller_lock:
-        if room.controller is None:
-            await obtain_control(ws, room)
-            # with room.playback.lock:
-            #     if room.playback.get_state() == PlayerState.WAITING_FOR_CLIENTS:
-            #         room.playback.set_state(PlayerState.PLAYING)
-            #         # TODO maybe send a play message on channel.
-            #         # await ws.send(make_message(Message.MEDIA_ACTION, {"action": MediaAction.PLAY}))
 
 
 async def on_disconnect(ws, path):
@@ -249,6 +249,7 @@ def make_resolver():
     resolver = Resolver()
     resolver.register(Message.STATE, request_state_processor)
     resolver.register(Message.LIST_OPERATION, request_list_operation_processor)
+    resolver.register(Message.PLAYER_ENABLED, player_enabled_processor)
     resolver.register(Message.OBTAIN_CONTROL, obtain_control_processor)
     resolver.register(Message.RELEASE_CONTROL, release_control_processor)
     resolver.register(Message.SONG_END, song_end_processor)
@@ -264,6 +265,7 @@ def init_rooms():
     # rooms["main"] = Room()
     pass
 
+
 if __name__ == "__main__":
     player_resolver = make_resolver()
 

+ 1 - 0
chube_enums.py

@@ -12,6 +12,7 @@ class Message(Enum):
     # CONTROL
     OBTAIN_CONTROL = "OBTAIN_CONTROL"
     RELEASE_CONTROL = "RELEASE_CONTROL"
+    PLAYER_ENABLED = "PLAYER_ENABLED"
     # SEARCH
     SEARCH = "SEARCH"
     SEARCH_ID = "SEARCH_ID"

+ 0 - 0
static/css/bootstrap.css → static/css/bootstrap/bootstrap.css


+ 0 - 0
static/css/bootstrap.css.map → static/css/bootstrap/bootstrap.css.map


+ 0 - 0
static/css/bootstrap.min.css → static/css/bootstrap/bootstrap.min.css


+ 0 - 0
static/css/bootstrap.min.css.map → static/css/bootstrap/bootstrap.min.css.map


+ 20 - 1
static/css/main.css

@@ -3,12 +3,31 @@
     src: url("../webfonts/kenyc.ttf");
 }
 
+@media (min-width: 1500px) {
+    .container, .container-sm, .container-md, .container-lg, .container-xl {
+        max-width: 1440px;
+    }
+}
+
 body {
     font-family: "Open Sans", sans-serif;
 }
 
 input, select, textarea, button{font-family:inherit;}
 
+#playerPlaceholder {
+    width: 640px;
+    height: 422px;
+    border: dashed lightgray;
+    border-radius: 5%;
+    padding-top: 192px;
+}
+
+#closePlayer {
+    font-size: 18pt;
+    color: black;
+}
+
 .searchResultChannel, .videoListCardChannel {
     font-style: italic;
 }
@@ -31,7 +50,7 @@ input, select, textarea, button{font-family:inherit;}
     text-overflow: ellipsis;
 }
 
-.pageCenter {
+.absoluteCenter {
     position: absolute;
     top:0;
     bottom: 0;

+ 2 - 3
static/index.html

@@ -13,10 +13,9 @@
     <meta property="og:image" content="">
 
     <link rel="stylesheet" href="css/normalize.css">
-    <link rel="stylesheet" href="css/main.css">
     <link rel="stylesheet" href="css/fontawesome.css">
     <link rel="stylesheet" href="css/solid.css">
-    <link rel="stylesheet" href="css/bootstrap.css">
+    <link rel="stylesheet" href="css/bootstrap/bootstrap.css">
     <link rel="stylesheet" href="css/main.css">
 
 
@@ -24,7 +23,7 @@
 </head>
 
 <body>
-<div class="container pageCenter" style="height: 135px">
+<div class="container absoluteCenter" style="height: 135px">
     <div style="margin-top: -100px" >
         <div class="row mb-4">
             <img class="mx-auto" alt="ChuChube logo" src="img/logo-100.png"/>

+ 1 - 0
static/js/enums.js

@@ -3,6 +3,7 @@ export const MessageTypes = {
     LIST_OPERATION: "LIST_OPERATION",
     MEDIA_ACTION: "MEDIA_ACTION",
     SONG_END: "SONG_END",
+    PLAYER_ENABLED: "PLAYER_ENABLED",
     OBTAIN_CONTROL: "OBTAIN_CONTROL",
     RELEASE_CONTROL: "RELEASE_CONTROL",
     SEARCH: "SEARCH",

+ 77 - 3
static/js/main.js

@@ -13,6 +13,9 @@ 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 videoPlaying = null;
@@ -21,6 +24,7 @@ let state = null;
 let isLeader = null;
 
 let player = null;
+let playerActive = false;
 document.getElementById('player').append(mockPlayer('360', '640'));
 
 const codeInfoMap = new Map()
@@ -133,8 +137,12 @@ function playVideo(vid) {
     videoPlaying = vid;
     state = PlayerState.PLAYING
 
+    if (!playerActive) {
+        return;
+    }
+
     if (player === null) {
-        buildPlayer('360', '640', vid.code);
+        buildPlayer(PLAYER_HEIGHT, PLAYER_WIDTH, vid.code);
     } else {
         player.loadVideoById(vid.code, 0);
     }
@@ -143,8 +151,12 @@ function playVideo(vid) {
 function loadVideo(vid) {
     videoPlaying = vid;
 
+    if (!playerActive) {
+        return
+    }
+
     if (player === null) {
-        buildPlayer('360', '640', vid.code);
+        buildPlayer(PLAYER_WIDTH, PLAYER_HEIGHT, vid.code);
     } else {
         player.cueVideoById(vid.code, 0);
     }
@@ -174,7 +186,7 @@ function mockPlayer(height, width) {
 
 function buildPlayer(height, width, id) {
     player = new YT.Player('player', {
-        height: parseInt(height) + 48,
+        height: height + 48,
         width: width,
         videoId: id,
         playerVars: {},
@@ -283,6 +295,64 @@ function onLeaderbutton(event) {
     }
 }
 
+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 stateProcessor(ws, data) {
     const { playing, state: newState, list } = data;
 
@@ -439,4 +509,8 @@ 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)
 }

+ 41 - 18
static/player.html

@@ -13,10 +13,9 @@
     <meta property="og:image" content="">
 
     <link rel="stylesheet" href="css/normalize.css">
-    <link rel="stylesheet" href="css/main.css">
     <link rel="stylesheet" href="css/fontawesome.css">
     <link rel="stylesheet" href="css/solid.css">
-    <link rel="stylesheet" href="css/bootstrap.css">
+    <link rel="stylesheet" href="css/bootstrap/bootstrap.css">
     <link rel="stylesheet" href="css/main.css">
 
 
@@ -57,24 +56,48 @@
             </div>
         </div>
         <div class="col-xl-6 my-4 col-sm-12">
-            <span class="row"><button id="leader-button"
-                                      class="btn btn-outline-secondary disabled">No connection</button></span>
-            <div id="player" class="row py-4"></div>
-            <div id="videoList" class="list-group">
-                <div id="videoListCardTemplate" class="videoListCard list-group-item" hidden>
-                    <div class="row">
-                        <div class="col-1">
-                            <div class="videoListCardMoveUp"><i class="fa fa-caret-up"></i></div>
-                            <div class="videoListCardMoveDown"><i class="fa fa-caret-down"></i></div>
+            <div class="row">
+                <div class="col-12">
+                    <button class="btn btn-link btn-sm text-secondary" id="showPlayerPlaceholder" hidden>Show</button>
+                    <div id="playerPlaceholder" class="d-flex justify-content-center my-4">
+                        <div>
+                            <button class="btn btn-outline-secondary" id="startPlayerButton">Start Player</button><br/>
+                            <button class="btn btn-link btn-sm small text-secondary" id="hidePlayerPlaceholder" style="width:100%">Hide</button>
                         </div>
-                        <div class="videoListCardThumbnail col-3"></div>
-                        <div class="col-7 videoListCardTextContainer">
-                            <span class="h4 videoListCardTitle"></span><br/>
-                            <span class="videoListCardChannel pr-3"></span>
-                            <span class="videoListCardDescription"></span>
+                    </div>
+                    <div id="playerContainer" class="py-4" hidden>
+                        <div class="row justify-content-between">
+                            <span class="col-2"><button id="leader-button"
+                                      class="btn btn-outline-secondary disabled">No connection</button></span>
+                            <span class="col-1">
+                                <a href="#" id="closePlayer">
+                                    <i class="fa fa-times"></i>
+                                </a>
+                            </span>
                         </div>
-                        <div class="col-1">
-                            <button class="videoListCardDelete btn btn-outline-danger"><i class="fa fa-trash-alt"></i></button>
+                        <div id="player" class="pt-4"></div>
+                    </div>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col-12">
+                    <div id="videoList" class="list-group">
+                        <div id="videoListCardTemplate" class="videoListCard list-group-item" hidden>
+                            <div class="row">
+                                <div class="col-1">
+                                    <div class="videoListCardMoveUp"><i class="fa fa-caret-up"></i></div>
+                                    <div class="videoListCardMoveDown"><i class="fa fa-caret-down"></i></div>
+                                </div>
+                                <div class="videoListCardThumbnail col-3"></div>
+                                <div class="col-7 videoListCardTextContainer">
+                                    <span class="h4 videoListCardTitle"></span><br/>
+                                    <span class="videoListCardChannel pr-3"></span>
+                                    <span class="videoListCardDescription"></span>
+                                </div>
+                                <div class="col-1">
+                                    <button class="videoListCardDelete btn btn-outline-danger"><i class="fa fa-trash-alt"></i></button>
+                                </div>
+                            </div>
                         </div>
                     </div>
                 </div>