diff --git a/pkg/api/app.go b/pkg/api/app.go
index c69bcee..bd6f504 100644
--- a/pkg/api/app.go
+++ b/pkg/api/app.go
@@ -38,11 +38,14 @@ func NewApp(config utils.Config, hub *controllers.Hub) App {
router.HandleFunc("/api/channel/{id}", handlers.handleCreateChannel).Methods("POST")
router.HandleFunc("/api/channel/{id}/skip", handlers.handleNextVideo).Methods("POST")
router.HandleFunc("/api/channel/{id}/shuffle", handlers.handleShuffleVideo).Methods("POST")
+ router.HandleFunc("/api/channel/{id}/clear", handlers.handleClearVideo).Methods("POST")
router.HandleFunc("/api/channel/{id}/loop", handlers.handleLoopVideo).Methods("POST")
router.HandleFunc("/api/channel/{id}/play", handlers.handlePlayVideo).Methods("POST")
router.HandleFunc("/api/channel/{id}/pause", handlers.handlePauseVideo).Methods("POST")
router.HandleFunc("/api/channel/{id}/queue", handlers.handleUpateQueue).Methods("POST")
+ router.HandleFunc("/api/channel/{id}/seek", handlers.handleSeekVideo).Methods("POST")
router.HandleFunc("/api/channel/{id}/add", handlers.handleAddVideo).Methods("PUT")
+ router.HandleFunc("/api/channel/{id}/players", handlers.handleGetPlayers).Methods("GET")
router.HandleFunc("/api/channel/{id}/playlist", handlers.handleGetPlaylistsByChannel).Methods("GET")
router.HandleFunc("/api/channel/{id}/add/playlist/{playlist_id}", handlers.handleAddFromPlaylist).Methods("PUT")
diff --git a/pkg/api/handlers.go b/pkg/api/handlers.go
index 3e597cd..2a20918 100644
--- a/pkg/api/handlers.go
+++ b/pkg/api/handlers.go
@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"net/http"
+ "time"
"w2g/pkg/api/ui"
"w2g/pkg/controllers"
"w2g/pkg/media"
@@ -103,6 +104,25 @@ func (h *handler) handleShuffleVideo(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(controller.State())
}
+
+func (h *handler) handleClearVideo(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ id := vars["id"]
+ controller, err := h.Get(id)
+ if err != nil {
+ WriteError(w, channelNotFound)
+ return
+ }
+ user, err := h.getUser(r)
+ if err != nil {
+ WriteError(w, userNotFound)
+ return
+ }
+ controller.UpdateQueue([]media.Media{}, user.Username)
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(controller.State())
+}
+
func (h *handler) handleLoopVideo(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
@@ -137,6 +157,31 @@ func (h *handler) handlePlayVideo(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(controller.State())
}
+
+func (h *handler) handleSeekVideo(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ id := vars["id"]
+ var seconds time.Duration
+ decoder := json.NewDecoder(r.Body)
+ if decoder.Decode(&seconds) != nil {
+ return
+ }
+
+ controller, err := h.Get(id)
+ if err != nil {
+ WriteError(w, channelNotFound)
+ return
+ }
+ user, err := h.getUser(r)
+ if err != nil {
+ WriteError(w, userNotFound)
+ return
+ }
+ controller.Seek(seconds, user.Username)
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(controller.State())
+}
+
func (h *handler) handlePauseVideo(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
@@ -221,9 +266,8 @@ func (h *handler) notify() http.Handler {
w.Write([]byte("connection is not using the websocket protocol"))
return
}
- player := NewWebPlayer()
- controller.Join(player, user.Username)
- client := NewClient(socket, controller, player)
+ client := NewClient(socket, controller, user)
+ controller.Join(client, user.Username)
controller.AddListner(client.id, client)
})
}
@@ -339,3 +383,15 @@ func (h *handler) handleDeletePlaylist(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(id)
}
+
+func (h *handler) handleGetPlayers(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ id := vars["id"]
+ controller, err := h.Get(id)
+ if err != nil {
+ WriteError(w, playlistNotFound)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(controller.Players().GetProgress())
+}
diff --git a/pkg/api/webPlayer.go b/pkg/api/webPlayer.go
deleted file mode 100644
index 32d3f28..0000000
--- a/pkg/api/webPlayer.go
+++ /dev/null
@@ -1,58 +0,0 @@
-package api
-
-import (
- "w2g/pkg/controllers"
- "w2g/pkg/media"
-)
-
-const WEBPLAYER = controllers.PlayerType("WEB_PLAYER")
-
-type WebPlayer struct {
- done chan any
- progress media.MediaDuration
- running bool
-}
-
-func NewWebPlayer() *WebPlayer {
- return &WebPlayer{
- done: make(chan any),
- progress: media.MediaDuration{
- Progress: 0,
- },
- }
-}
-
-func (wb *WebPlayer) Type() controllers.PlayerType {
- return WEBPLAYER
-}
-
-func (wb *WebPlayer) Play(url string, start int) error {
- wb.running = true
- <-wb.done
- wb.running = false
- return nil
-}
-
-func (wb *WebPlayer) Progress() media.MediaDuration {
- return wb.progress
-}
-
-func (wb *WebPlayer) Pause() {}
-func (wb *WebPlayer) Unpause() {}
-
-func (wb *WebPlayer) Stop() {
- if wb.running {
- wb.done <- "STOP"
- }
-}
-func (wb *WebPlayer) Close() {
- wb.Stop()
-}
-
-func (wb *WebPlayer) Status() bool {
- return wb.running
-}
-
-func (wb *WebPlayer) UpdateDuration(duration media.MediaDuration) {
- wb.progress = duration
-}
diff --git a/pkg/api/websocket.go b/pkg/api/websocket.go
index 755a75f..3fcf69e 100644
--- a/pkg/api/websocket.go
+++ b/pkg/api/websocket.go
@@ -4,12 +4,16 @@ import (
"encoding/json"
"fmt"
"net/http"
+ "time"
"w2g/pkg/controllers"
+ "w2g/pkg/media"
"github.com/google/uuid"
"github.com/gorilla/websocket"
)
+const WEBPLAYER = controllers.PlayerType("WEB_PLAYER")
+
var Upgrader = &websocket.Upgrader{
ReadBufferSize: SocketBufferSize,
WriteBufferSize: SocketBufferSize,
@@ -21,12 +25,14 @@ var Upgrader = &websocket.Upgrader{
// client represents a single chatting user.
type Client struct {
id string
+ user User
contoller *controllers.Controller
- player *WebPlayer
- // socket is the web socket for this client.
- socket *websocket.Conn
- // send is a channel on which messages are sent.
- send chan []byte
+ socket *websocket.Conn
+ send chan []byte
+
+ done chan any
+ progress media.MediaDuration
+ running bool
}
const (
@@ -41,12 +47,13 @@ func (c *Client) Read() {
if err != nil {
fmt.Printf("ERROR decoding %v", err)
c.contoller.RemoveListner(c.id)
+ c.contoller.Leave(c.id, c.user.Username)
return
}
var event controllers.Event
err = json.Unmarshal(msg, &event)
if err == nil {
- c.player.UpdateDuration(event.State.Current.Progress)
+ c.UpdateDuration(event.State.Current.Progress)
}
}
}
@@ -69,13 +76,69 @@ func (c *Client) Send(event controllers.Event) {
}
}
-func NewClient(socket *websocket.Conn, contoller *controllers.Controller, player *WebPlayer) *Client {
+func (wb *Client) Type() controllers.PlayerType {
+ return WEBPLAYER
+}
+
+func (wb *Client) Id() string {
+ return wb.id
+}
+
+func (wb *Client) Play(url string, start int) error {
+ fmt.Println(WEBPLAYER + "_PLAY")
+ wb.progress = media.MediaDuration{
+ Progress: 0,
+ }
+ wb.running = true
+ <-wb.done
+ fmt.Println(WEBPLAYER + "_DONE")
+ return nil
+}
+
+func (wb *Client) Progress() media.MediaDuration {
+ return wb.progress
+}
+
+func (wb *Client) Pause() {
+ fmt.Println(WEBPLAYER + "_PAUSE")
+}
+func (wb *Client) Unpause() {
+ fmt.Println(WEBPLAYER + "_UNPAUSE")
+ wb.running = true
+}
+
+func (wb *Client) Stop() {
+ fmt.Println(WEBPLAYER + "_STOP")
+ if wb.running {
+ wb.running = false
+ wb.done <- "STOP"
+ }
+}
+func (wb *Client) Close() {
+ fmt.Println(WEBPLAYER + "_CLOSE")
+ wb.Stop()
+}
+
+func (wb *Client) Status() bool {
+ return wb.running
+}
+
+func (wb *Client) UpdateDuration(duration media.MediaDuration) {
+ wb.progress = duration
+}
+
+func (wb *Client) Seek(seconds time.Duration) {
+ wb.progress.Progress = seconds
+}
+
+func NewClient(socket *websocket.Conn, contoller *controllers.Controller, user User) *Client {
client := &Client{
id: uuid.NewString(),
+ user: user,
socket: socket,
send: make(chan []byte, MessageBufferSize),
+ done: make(chan any),
contoller: contoller,
- player: player,
}
go client.Read()
go client.Write()
diff --git a/pkg/controllers/controller.go b/pkg/controllers/controller.go
index 78ae422..d881660 100644
--- a/pkg/controllers/controller.go
+++ b/pkg/controllers/controller.go
@@ -1,6 +1,7 @@
package controllers
import (
+ "fmt"
"time"
"w2g/pkg/media"
@@ -86,6 +87,12 @@ func (c *Controller) Pause(user string) {
c.Notify(PAUSE_ACTION, user)
}
+func (c *Controller) Seek(seconds time.Duration, user string) {
+ c.players.Seek(seconds)
+ c.state.Current.Progress.Progress = c.players.Progress().Progress
+ c.Notify(SEEK, user)
+}
+
func (c *Controller) Add(url string, user string) error {
tracks, err := media.NewVideo(url, user)
if err != nil {
@@ -136,37 +143,41 @@ func (c *Controller) Update(state PlayerState, user string) {
}
func (c *Controller) Join(player Player, user string) {
- if _, ok := c.players.players[player.Type()]; !ok {
+ if _, ok := c.players.players[player.Id()]; !ok {
c.players.Add(player)
c.Notify(PLAYER_ACTION, user)
}
}
-func (c *Controller) Leave(pType PlayerType, user string) {
- c.players.Remvoe(pType)
+func (c *Controller) Leave(id string, user string) {
+ c.players.Remvoe(id)
c.Notify(LEAVE_ACTION, user)
}
-func (c *Controller) ContainsPlayer(pType PlayerType) bool {
- if _, ok := c.players.players[pType]; ok {
+func (c *Controller) ContainsPlayer(id string) bool {
+ if _, ok := c.players.players[id]; ok {
return true
}
return false
}
func (c *Controller) progress() {
- defer c.Stop(SYSTEM)
+ fmt.Println("START")
for {
- if len(c.state.Current.Url) == 0 || !c.running || c.players.Empty() {
- c.Stop(SYSTEM)
- return
- }
audio := c.state.Current.GetAudioUrl()
+ fmt.Println("START_PLAYING")
c.players.Play(audio, 0)
+ fmt.Println("STOP_PLAYING")
if !c.state.Loop {
c.state.Next()
c.Notify(UPDATE_QUEUE, SYSTEM)
}
+ if len(c.state.Current.Url) == 0 || c.players.Empty() {
+ c.Stop(SYSTEM)
+ fmt.Println("DONE")
+ return
+ }
+ fmt.Println("NEXT")
}
}
@@ -202,3 +213,7 @@ func (c *Controller) Notify(action ActionType, user string) {
State: state,
}
}
+
+func (c *Controller) Players() *Players {
+ return c.players
+}
diff --git a/pkg/controllers/notify.go b/pkg/controllers/notify.go
index 0baf804..e4429b3 100644
--- a/pkg/controllers/notify.go
+++ b/pkg/controllers/notify.go
@@ -12,6 +12,7 @@ var (
PLAY_ACTION = ActionType("PLAY")
PAUSE_ACTION = ActionType("PAUSE")
ADD_QUEUE = ActionType("ADD_QUEUE")
+ SEEK = ActionType("SEEK")
UPDATE_QUEUE = ActionType("UPDATE_QUEUE")
UPDATE = ActionType("UPDATE")
REMOVE_QUEUE = ActionType("REMOVE_QUEUE")
diff --git a/pkg/controllers/player.go b/pkg/controllers/player.go
index 4fc78ba..8faafc4 100644
--- a/pkg/controllers/player.go
+++ b/pkg/controllers/player.go
@@ -3,15 +3,22 @@ package controllers
import (
"fmt"
"sync"
+ "time"
"w2g/pkg/media"
)
type PlayerType string
-
+type PlayerMeta struct {
+ Progress media.MediaDuration `json:"progress"`
+ Type PlayerType `json:"type"`
+ Running bool `json:"running"`
+}
type Player interface {
Play(string, int) error
Progress() media.MediaDuration
+ Seek(time.Duration)
Type() PlayerType
+ Id() string
Pause()
Unpause()
Stop()
@@ -20,12 +27,12 @@ type Player interface {
}
type Players struct {
- players map[PlayerType]Player
+ players map[string]Player
}
func newPlayers() *Players {
return &Players{
- players: map[PlayerType]Player{},
+ players: map[string]Player{},
}
}
@@ -34,10 +41,28 @@ func (p *Players) Empty() bool {
}
func (p *Players) Add(player Player) {
- p.players[player.Type()] = player
+ p.players[player.Id()] = player
+}
+
+func (p *Players) Seek(seconds time.Duration) {
+ for _, player := range p.players {
+ player.Seek(seconds)
+ }
+}
+
+func (p *Players) GetProgress() map[string]PlayerMeta {
+ data := map[string]PlayerMeta{}
+ for _, player := range p.players {
+ data[player.Id()] = PlayerMeta{
+ Progress: player.Progress(),
+ Type: player.Type(),
+ Running: player.Status(),
+ }
+ }
+ return data
}
-func (p *Players) Remvoe(id PlayerType) {
+func (p *Players) Remvoe(id string) {
if player, ok := p.players[id]; ok {
player.Close()
delete(p.players, id)
diff --git a/pkg/discord/bot.go b/pkg/discord/bot.go
index fd58ee7..547171d 100644
--- a/pkg/discord/bot.go
+++ b/pkg/discord/bot.go
@@ -7,7 +7,6 @@ import (
"w2g/pkg/controllers"
"w2g/pkg/discord/commands"
"w2g/pkg/discord/components"
- "w2g/pkg/discord/players"
"w2g/pkg/discord/session"
"w2g/pkg/utils"
@@ -189,8 +188,8 @@ func (db *DiscordBot) AutoDisconnect() {
for range ticker.C {
for _, guild := range db.session.State.Guilds {
if controller, err := db.channels.Get(guild.ID); err == nil {
- if len(guild.VoiceStates) <= 1 && controller.ContainsPlayer(players.DISCORD) {
- controller.Leave(players.DISCORD, controllers.SYSTEM)
+ if len(guild.VoiceStates) <= 1 && controller.ContainsPlayer(guild.ID) {
+ controller.Leave(guild.ID, controllers.SYSTEM)
}
}
}
diff --git a/pkg/discord/commands/joinCmd.go b/pkg/discord/commands/joinCmd.go
index 9c9db26..977aac3 100644
--- a/pkg/discord/commands/joinCmd.go
+++ b/pkg/discord/commands/joinCmd.go
@@ -59,7 +59,7 @@ func join(ctx CommandCtx) error {
return fmt.Errorf("you are not connected to voice channel")
}
ctx.Controller.Stop(ctx.Member.User.Username)
- ctx.Controller.Join(players.NewDiscordPlayer(voice), ctx.Member.User.Username)
+ ctx.Controller.Join(players.NewDiscordPlayer(ctx.Guild.ID, voice), ctx.Member.User.Username)
return nil
}
@@ -71,6 +71,6 @@ func joinCmd(ctx CommandCtx) *discordgo.InteractionResponse {
}
func leave(ctx CommandCtx) *discordgo.InteractionResponse {
- ctx.Controller.Leave(players.DISCORD, ctx.Member.User.Username)
+ ctx.Controller.Leave(ctx.Guild.ID, ctx.Member.User.Username)
return ctx.Reply("👋 cheerio have a good day 🎩")
}
diff --git a/pkg/discord/commands/playCmd.go b/pkg/discord/commands/playCmd.go
index 9527693..dd09abd 100644
--- a/pkg/discord/commands/playCmd.go
+++ b/pkg/discord/commands/playCmd.go
@@ -2,7 +2,6 @@ package commands
import (
"net/url"
- "w2g/pkg/discord/players"
"github.com/bwmarrin/discordgo"
)
@@ -82,7 +81,7 @@ func addCmd(ctx CommandCtx) *discordgo.InteractionResponse {
}
func playCmd(ctx CommandCtx) *discordgo.InteractionResponse {
- if !ctx.Controller.ContainsPlayer(players.DISCORD) {
+ if !ctx.Controller.ContainsPlayer(ctx.Guild.ID) {
if join(ctx) != nil {
return ctx.Reply("User not connected to voice channel")
}
diff --git a/pkg/discord/components/controls.go b/pkg/discord/components/controls.go
index 9ff28ce..82049c3 100644
--- a/pkg/discord/components/controls.go
+++ b/pkg/discord/components/controls.go
@@ -37,7 +37,9 @@ func init() {
Name: SkipBtn,
Function: func(ctx HandlerCtx) *discordgo.InteractionResponse {
ctx.Controller.Skip(ctx.User.Username)
- return ctx.UpdateMessage(ControlCompontent(ctx.Controller.State()))
+ state := ctx.Controller.State()
+ state.Next()
+ return ctx.UpdateMessage(ControlCompontent(state))
},
},
)
@@ -54,7 +56,8 @@ func init() {
func ControlCompontent(state controllers.PlayerState) *discordgo.InteractionResponseData {
var actionButton discordgo.Button
- if state.State == controllers.PLAY {
+
+ if state.State == controllers.PLAY && state.Current.ID != "" {
actionButton = discordgo.Button{
CustomID: PauseBtn,
Emoji: discordgo.ComponentEmoji{
diff --git a/pkg/discord/components/list.go b/pkg/discord/components/list.go
index d1c83ad..5dcae94 100644
--- a/pkg/discord/components/list.go
+++ b/pkg/discord/components/list.go
@@ -2,6 +2,7 @@ package components
import (
"fmt"
+ "math"
"unicode"
"w2g/pkg/media"
@@ -128,8 +129,10 @@ func QueueCompontent(queue []media.Media, pageNum int) *discordgo.InteractionRes
pos := pageNum*pageSize + i + 1
queStr = queStr + fmt.Sprintf("`%d.` [%s](%s) \n", pos, truncate(video.Title, 40), video.Url)
}
+ totalPages := float64(len(queue)) / float64(pageSize)
+
embed.AddField(discordgo.MessageEmbedField{
- Name: fmt.Sprintf("Page %d of %d", pageNum+1, len(queue)/pageSize),
+ Name: fmt.Sprintf("Page %d of %d", pageNum+1, int(math.Ceil(totalPages))),
Value: queStr,
})
embed.Description = fmt.Sprintf("%d tracks in total in the queue", len(queue))
diff --git a/pkg/discord/players/discord.go b/pkg/discord/players/discord.go
index 1d4c829..ece8d3e 100644
--- a/pkg/discord/players/discord.go
+++ b/pkg/discord/players/discord.go
@@ -15,6 +15,7 @@ import (
const DISCORD = controllers.PlayerType("DISCORD")
type DiscordPlayer struct {
+ id string
done chan error
stream *dca.StreamingSession
session *dca.EncodeSession
@@ -24,8 +25,9 @@ type DiscordPlayer struct {
startTime int
}
-func NewDiscordPlayer(voice *discordgo.VoiceConnection) *DiscordPlayer {
+func NewDiscordPlayer(id string, voice *discordgo.VoiceConnection) *DiscordPlayer {
audio := &DiscordPlayer{
+ id: id,
done: make(chan error),
voice: voice,
}
@@ -85,6 +87,10 @@ func (player *DiscordPlayer) Type() controllers.PlayerType {
return DISCORD
}
+func (player *DiscordPlayer) Id() string {
+ return player.id
+}
+
func (player *DiscordPlayer) Status() bool {
return player.running
}
@@ -94,6 +100,9 @@ func (player *DiscordPlayer) Close() {
player.voice.Disconnect()
}
+func (player *DiscordPlayer) Seek(seconds time.Duration) {
+}
+
func (player *DiscordPlayer) Finish() {
player.session.Cleanup()
player.session.Truncate()
diff --git a/ui/package-lock.json b/ui/package-lock.json
index e206e70..0fba911 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -10,6 +10,7 @@
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-full-screen": "^1.1.1",
"react-hot-toast": "^2.4.1",
"react-hotkeys-hook": "^4.4.3",
"react-player": "2.13.0",
@@ -3999,6 +4000,11 @@
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true
},
+ "node_modules/fscreen": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/fscreen/-/fscreen-1.2.0.tgz",
+ "integrity": "sha512-hlq4+BU0hlPmwsFjwGGzZ+OZ9N/wq9Ljg/sq3pX+2CD7hrJsX9tJgWWK/wiNTFM212CLHWhicOoqwXyZGGetJg=="
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -5694,6 +5700,20 @@
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="
},
+ "node_modules/react-full-screen": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/react-full-screen/-/react-full-screen-1.1.1.tgz",
+ "integrity": "sha512-xoEgkoTiN0dw9cjYYGViiMCBYbkS97BYb4bHPhQVWXj1UnOs8PZ1rPzpX+2HMhuvQV1jA5AF9GaRbO3fA5aZtg==",
+ "dependencies": {
+ "fscreen": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8.0"
+ }
+ },
"node_modules/react-hot-toast": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz",
diff --git a/ui/package.json b/ui/package.json
index 5ec080d..bd02560 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -12,6 +12,7 @@
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-full-screen": "^1.1.1",
"react-hot-toast": "^2.4.1",
"react-hotkeys-hook": "^4.4.3",
"react-player": "2.13.0",
diff --git a/ui/src/pages/app/components/header.jsx b/ui/src/pages/app/components/header.jsx
index 72715e2..0a0dac2 100644
--- a/ui/src/pages/app/components/header.jsx
+++ b/ui/src/pages/app/components/header.jsx
@@ -1,5 +1,3 @@
-import { VideoPlayer } from "./videoPlayer"
-
export const Header = ({ state }) => {
return (
@@ -10,15 +8,4 @@ export const Header = ({ state }) => {
)
-}
-
-export const VideoHeader = ({ state, connection }) => {
- return (
-
-
-
-
-
{state.current.title}
-
- )
}
\ No newline at end of file
diff --git a/ui/src/pages/app/components/header/Controls.jsx b/ui/src/pages/app/components/header/Controls.jsx
new file mode 100644
index 0000000..42d9b86
--- /dev/null
+++ b/ui/src/pages/app/components/header/Controls.jsx
@@ -0,0 +1,48 @@
+import { useState } from "react";
+import { clearVideoController, shuffleVideoController } from "../../watch2gether";
+
+export const AddVideoCtrl = ({ onAddVideo, controls }) => {
+ const [video, setVideo] = useState("");
+ const addVideo = async () => {
+ if (video.length == 0) {
+ return
+ }
+ onAddVideo(video)
+ setVideo("")
+ }
+ const handleKeyPress = (e) => {
+ if (e.key == 'Enter') {
+ addVideo()
+ }
+ }
+ return (
+
+
+
setVideo(e.target.value)} className="block w-full p-4 pl-10 text-xl bg-transparent text-white focus:ring-0 focus:outline-none " placeholder="Add New video" required />
+
+ {controls&&
+
+
+
}
+
+ )
+}
\ No newline at end of file
diff --git a/ui/src/pages/app/components/nav.jsx b/ui/src/pages/app/components/nav.jsx
index 299ecd8..024a39c 100644
--- a/ui/src/pages/app/components/nav.jsx
+++ b/ui/src/pages/app/components/nav.jsx
@@ -34,7 +34,7 @@ export const Nav = ({user, bot}) => {
useOnClickOutside(ref, () => setModalOpen(false));
return (
-
+
Watch2Gether
diff --git a/ui/src/pages/app/components/player.jsx b/ui/src/pages/app/components/player.jsx
deleted file mode 100644
index 2f2be8c..0000000
--- a/ui/src/pages/app/components/player.jsx
+++ /dev/null
@@ -1,211 +0,0 @@
-import React, { useContext, useState } from "react";
-import { loopVideoController, pauseVideoController, playVideoController, shuffleVideoController, skipVideoController } from "../watch2gether";
-import { PlayerContext, VolumeContext } from "./providers";
-
-const Switch = () => {
- const { showVideo, setShowVideo } = useContext(PlayerContext)
- return (
-
- {showVideo ?
-
- :
-
}
-
- )
-}
-
-const VolumeControl = React.memo(() => {
- const { volume, setVolume } = useContext(VolumeContext)
-
- const handleChange = (event) => {
- setVolume(event.target.value);
- // Code to update the media player's volume based on the new value
- };
-
- const MuteBtn = ({ onClick }) => {
- return
- }
- const MaxVolBtn = ({ onClick }) => {
- return
- }
-
- return (
- <>
-
- setVolume(0)} />
-
- setVolume(100)} />
-
-
- {volume === 0 ? setVolume(100)} /> : setVolume(0)} />}
-
- >
- );
-});
-
-
-const Player = ({ state }) => {
- const formatTime = (seconds) => {
- if (seconds === undefined) {
- seconds = 0
- }
- let iso = new Date(seconds / 1000000).toISOString()
- return iso.substring(11, iso.length - 5);
- }
- const playerProgress = (current, total) => {
- if (total === -1){
- return 100
- }
- let pct = current / total * 100
- return Math.min(Math.max(pct, 0), 100)
- }
-
- const handleShuffle = () => {
- shuffleVideoController();
- }
- const handlePlay = () => {
- playVideoController();
- }
- const handlePause = () => {
- pauseVideoController();
- }
- const handleSkip = () => {
- skipVideoController();
- }
- const handleLoop = () => {
- loopVideoController();
- }
- return (
-
-
- {state.active ?
-
-
-
-
-
-
-
-
- {state.status === "PLAY" ?
-
- :
- }
-
-
-
-
-
- {state.current.id &&
-
-
{formatTime(state.current.time.progress)}
-
- {
- state.current.time.duration > -1 ?
-
{formatTime(state.current.time.duration)}
- :
-
- live
-
- }
-
- }
-
-
- :
-
-
Player is not active.
Join the bot to one of the voice channels
-
- }
-
- )
-}
-
-export default Player
\ No newline at end of file
diff --git a/ui/src/pages/app/components/player/AudioPlayer.jsx b/ui/src/pages/app/components/player/AudioPlayer.jsx
new file mode 100644
index 0000000..1847c8a
--- /dev/null
+++ b/ui/src/pages/app/components/player/AudioPlayer.jsx
@@ -0,0 +1,81 @@
+import React from "react";
+import { loopVideoController, pauseVideoController, playVideoController, skipVideoController } from "../../watch2gether";
+import { PlayBtn } from "./PlayBtn";
+import { PlayerSwitch } from "./PlayerSwitch";
+import { VolumeControl } from "./VolumeControl";
+
+export const AudioPlayer = ({ state }) => {
+ const formatTime = (seconds) => {
+ if (seconds === undefined) {
+ seconds = 0
+ }
+ let iso = new Date(seconds / 1000000).toISOString()
+ return iso.substring(11, iso.length - 5);
+ }
+
+ const progressPercentage = (current, total) => {
+ if (total === -1) {
+ return 100
+ }
+ let pct = current / total * 100
+ return Math.min(Math.max(pct, 0), 100)
+ }
+
+ const handlePlay = () => {
+ playVideoController();
+ }
+ const handlePause = () => {
+ pauseVideoController();
+ }
+ const handleSkip = () => {
+ skipVideoController();
+ }
+ const handleLoop = () => {
+ loopVideoController();
+ }
+
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {state.current.id &&
+
+
{formatTime(state.current.time.progress)}
+
+ {state.current.time.duration > -1 ?
+
{formatTime(state.current.time.duration)}
+ :
+
+ live
+
+ }
+
+ }
+
+ )
+}
diff --git a/ui/src/pages/app/components/player/FullScreenBtn.jsx b/ui/src/pages/app/components/player/FullScreenBtn.jsx
new file mode 100644
index 0000000..8f37175
--- /dev/null
+++ b/ui/src/pages/app/components/player/FullScreenBtn.jsx
@@ -0,0 +1,27 @@
+import React from "react";
+
+export const FullScreenBtn = ({ status, show, hide }) => {
+ return (status ?
+
+ :
+
+ )
+}
\ No newline at end of file
diff --git a/ui/src/pages/app/components/player/PlayBtn.jsx b/ui/src/pages/app/components/player/PlayBtn.jsx
new file mode 100644
index 0000000..6f67767
--- /dev/null
+++ b/ui/src/pages/app/components/player/PlayBtn.jsx
@@ -0,0 +1,20 @@
+export const PlayBtn = ({ status, play, pause }) => {
+ if (status === "PLAY") {
+ return (
+
+ )
+ }
+ return (
+
+ )
+}
\ No newline at end of file
diff --git a/ui/src/pages/app/components/player/PlayerSwitch.jsx b/ui/src/pages/app/components/player/PlayerSwitch.jsx
new file mode 100644
index 0000000..f98b59c
--- /dev/null
+++ b/ui/src/pages/app/components/player/PlayerSwitch.jsx
@@ -0,0 +1,33 @@
+import React, { useContext } from "react";
+import { PlayerContext } from "../providers";
+
+export const PlayerSwitch = () => {
+ const { showVideo, setShowVideo} = useContext(PlayerContext)
+ return (showVideo ?
+
+ :
+
+ )
+}
\ No newline at end of file
diff --git a/ui/src/pages/app/components/player/VideoPlayer.jsx b/ui/src/pages/app/components/player/VideoPlayer.jsx
new file mode 100644
index 0000000..50a4b10
--- /dev/null
+++ b/ui/src/pages/app/components/player/VideoPlayer.jsx
@@ -0,0 +1,203 @@
+import React, { useContext, useEffect, useState } from "react";
+import { getRoomId, loopVideoController, pauseVideoController, playVideoController, seekVideoController, skipVideoController } from "../../watch2gether";
+import { FullScreen, useFullScreenHandle } from "react-full-screen";
+import ReactPlayer from "react-player";
+import background from "./wave-signal.svg"
+import { toast } from "react-hot-toast";
+import { FullScreenBtn } from "./FullScreenBtn";
+import { PlayerSwitch } from "./PlayerSwitch";
+import { PlayBtn } from "./PlayBtn";
+import { VolumeControl } from "./VolumeControl";
+import { PlayerContext, VolumeContext } from "../providers";
+
+
+const microseconds = 1000000000;
+
+export const VideoPlayer = ({ state, connection }) => {
+ const playerRef = React.useRef(null);
+ const [playerProgress, setPlayerProgress] = useState(0)
+ const [updating, setUpdating] = useState(false)
+ const { progress } = useContext(PlayerContext)
+ const { volume } = useContext(VolumeContext)
+ const handle = useFullScreenHandle();
+
+
+ useEffect(() => {
+ if (!updating && (Math.abs((playerProgress - progress) / microseconds)) > 2) {
+ setPlayerProgress(progress)
+ playerRef.current.seekTo(progress / microseconds)
+ }
+ console.log("PRGORSS UPDSATED")
+ }, [progress])
+
+
+ useEffect(() => {
+ console.log("LOADING")
+ }, [playerRef])
+
+ const handleOnSeek = (evt) => {
+ setUpdating(false)
+ playerRef.current.seekTo(evt.target.value / microseconds);
+ }
+
+ const updateSeek = () => {
+ toast.success("Syncing everyone to your position")
+ seekVideoController(playerProgress)
+ }
+
+ const handleProgessChange = (evt) => {
+ setPlayerProgress(evt.target.value)
+ }
+
+ const formatTime = (seconds) => {
+ if (seconds === undefined) {
+ seconds = 0
+ }
+ let iso = new Date(seconds / 1000000).toISOString()
+ return iso.substring(11, iso.length - 5);
+ }
+
+ const progressPercentage = (current, total) => {
+ if (total === -1) {
+ return 100
+ }
+ let pct = current / total * 100
+ return Math.min(Math.max(pct, 0), 100)
+ }
+
+ const handlePlay = () => {
+ playVideoController();
+ }
+ const handlePause = () => {
+ pauseVideoController();
+ }
+ const handleSkip = () => {
+ skipVideoController();
+ }
+ const handleLoop = () => {
+ loopVideoController();
+ }
+ const onEnded = () => {
+ if (!state.loop) {
+ skipVideoController();
+ }
+ }
+ const onStart = () => {
+ playVideoController();
+ }
+ const onPlay = () => {
+ playVideoController();
+ }
+ const handleProgress = (video_state) => {
+ let s = Object.assign({}, state)
+ let seconds = Math.floor(video_state.playedSeconds) * microseconds;
+ s.current.time = {
+ duration: s.current.time.duration,
+ progress: seconds
+ }
+
+ const evt = {
+ id: getRoomId(),
+ action: {
+ type: "UPDATE_DURATION"
+ },
+ state: {
+ Current: s.current
+ }
+ }
+ connection.send(JSON.stringify(evt))
+ setPlayerProgress(seconds)
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
{handle.enter();}} hide={handle.exit} />
+
+
+
+
+
+
+
+
+
+
+ {state.current.id &&
+
+
{formatTime(playerProgress)}
+
{
+ setUpdating(true)
+ console.log("mouse_Down")
+ }}
+ onMouseUp={handleOnSeek}
+ onTouchStart={() => { setUpdating(true) }}
+ onTouchEnd={() => { setUpdating(false) }}
+ onChange={handleProgessChange}
+ className="w-full h-2 rounded-lg appearance-none cursor-pointer bg-gray-700 accent-purple-500"
+
+ />
+ {state.current.time.duration > -1 ?
+
{formatTime(state.current.time.duration)}
+ :
+
+ live
+
+ }
+
+ }
+
+
+ )
+}
\ No newline at end of file
diff --git a/ui/src/pages/app/components/player/VolumeControl.jsx b/ui/src/pages/app/components/player/VolumeControl.jsx
new file mode 100644
index 0000000..8ef2070
--- /dev/null
+++ b/ui/src/pages/app/components/player/VolumeControl.jsx
@@ -0,0 +1,69 @@
+import React, { useContext } from "react";
+import { VolumeContext } from "../providers";
+
+const MuteBtn = ({ onClick }) => {
+ return
+}
+
+
+const MaxVolBtn = ({ onClick }) => {
+ return
+}
+
+
+export const VolumeControl = React.memo(() => {
+ const { volume, setVolume } = useContext(VolumeContext)
+
+ const handleChange = (event) => {
+ setVolume(event.target.value);
+ // Code to update the media player's volume based on the new value
+ };
+ return (
+ <>
+
+ setVolume(0)} />
+
+ setVolume(100)} />
+
+
+ {volume === 0 ? setVolume(100)} /> : setVolume(0)} />}
+
+ >
+ );
+});
\ No newline at end of file
diff --git a/ui/src/pages/app/components/player/index.jsx b/ui/src/pages/app/components/player/index.jsx
new file mode 100644
index 0000000..594a826
--- /dev/null
+++ b/ui/src/pages/app/components/player/index.jsx
@@ -0,0 +1,9 @@
+import { useContext } from "react"
+import { AudioPlayer } from "./AudioPlayer"
+import { VideoPlayer } from "./VideoPlayer"
+import { PlayerContext } from "../providers"
+
+export const Player = ({state, connection}) => {
+ const { showVideo} = useContext(PlayerContext)
+ return showVideo ?
:
+}
\ No newline at end of file
diff --git a/ui/src/pages/app/components/wave-signal.svg b/ui/src/pages/app/components/player/wave-signal.svg
similarity index 100%
rename from ui/src/pages/app/components/wave-signal.svg
rename to ui/src/pages/app/components/player/wave-signal.svg
diff --git a/ui/src/pages/app/components/providers.jsx b/ui/src/pages/app/components/providers.jsx
index 6c4ed0d..0875a5c 100644
--- a/ui/src/pages/app/components/providers.jsx
+++ b/ui/src/pages/app/components/providers.jsx
@@ -13,14 +13,17 @@ const VolumeProvider = ({ children }) => {
export const PlayerContext = createContext();
const PlayerProvider = ({ children }) => {
const [showVideo, setShowVideo] = useState(false);
+ const [progress, setProgress] = useState(0);
return (
-
+
{children}
);
};
+
+
export const Provider = ({ children }) => {
return (
<>
diff --git a/ui/src/pages/app/components/videoPlayer.jsx b/ui/src/pages/app/components/videoPlayer.jsx
deleted file mode 100644
index c89d269..0000000
--- a/ui/src/pages/app/components/videoPlayer.jsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import React, { useContext, useEffect } from 'react';
-import ReactPlayer from 'react-player'
-import { getRoomId, playVideoController, skipVideoController } from '../watch2gether';
-import { VolumeContext } from './providers';
-import waveImge from "./wave-signal.svg"
-
-export const VideoPlayer = ({state, connection}) => {
- const playerRef = React.useRef(null);
- const { volume } = useContext(VolumeContext)
- const onEnded = () => {
- if (!state.loop){
- skipVideoController();
- }
- }
-
- const onStart = () => {
- playVideoController();
- }
-
-
- const handleProgress = (video_state) => {
- let s = Object.assign({}, state)
- s.current.time = {
- duration: s.current.time.duration,
- progress: Math.floor(video_state.playedSeconds)*1000000000
- }
-
- const evt = {
- id: getRoomId(),
- action:{
- type: "UPDATE_DURATION"
- },
- state: {
- Current: s.current
- }
- }
- connection.send(JSON.stringify(evt))
- };
-
-
- return(
-
-
-
-
- );
-}
diff --git a/ui/src/pages/app/controller.jsx b/ui/src/pages/app/controller.jsx
index aa4e9f1..b97f25c 100644
--- a/ui/src/pages/app/controller.jsx
+++ b/ui/src/pages/app/controller.jsx
@@ -1,43 +1,16 @@
import { useContext, useEffect, useRef, useState } from "react";
import Card from "./components/card";
import toast from 'react-hot-toast';
-import Player from "./components/player";
import { addVideoController, getChannelPlaylists, getSocket, updateQueueController, getController, createController } from "./watch2gether";
-import { Header, VideoHeader } from "./components/header";
+import { Header } from "./components/header";
import { useNavigate } from "react-router-dom";
import { PlaylistBtn } from "./playlist";
import { PlayerContext } from "./components/providers";
import { Loading } from "./components/loading";
import { useHotkeys } from 'react-hotkeys-hook'
+import { Player } from "./components/player";
+import { AddVideoCtrl } from "./components/header/Controls";
-export const AddVideoCtrl = ({ onAddVideo }) => {
- const [video, setVideo] = useState("");
- const addVideo = async () => {
- if (video.length == 0) {
- return
- }
- onAddVideo(video)
- setVideo("")
- }
- const handleKeyPress = (e) => {
- if (e.key == 'Enter') {
- addVideo()
- }
- }
- return (
-
-
-
-
setVideo(e.target.value)} className="block w-full p-4 pl-10 text-xl bg-transparent text-white focus:ring-0 focus:outline-none " placeholder="Add New video" required />
-
-
-
- )
-}
export const AppController = () => {
const navigate = useNavigate();
@@ -45,9 +18,16 @@ export const AppController = () => {
const [playlists, setPlaylists] = useState([])
const [notificationURL, setNotificationURL] = useState(null)
const [debug, setDebug] = useState(false)
-
- const { showVideo } = useContext(PlayerContext)
+ const { showVideo, setProgress } = useContext(PlayerContext)
+ const [state, setState] = useState({
+ id: "",
+ status: "STOPPED",
+ queue: [],
+ current: {}
+ })
+
+
useHotkeys('ctrl+shift+b', () => setDebug(!debug), [debug])
const updatePlaylists = async () => {
@@ -73,23 +53,8 @@ export const AppController = () => {
setLoading(false)
}
- const [state, setState] = useState({
- id: "",
- status: "STOPPED",
- queue: [],
- current: {
- id: "",
- user: "",
- url: "",
- audio_url: "",
- type: "",
- title: "",
- channel: "",
- duration: 0,
- progress: 0,
- thumbnail: ""
- }
- })
+
+
const connection = useRef(null)
useEffect(() => {
@@ -102,11 +67,12 @@ export const AppController = () => {
})
socket.addEventListener("message", (event) => {
let evt = JSON.parse(event.data)
+ if (evt.action.type === "SEEK"){
+ console.log("SEEK", evt)
+ setProgress(evt.state.current.time.progress)
+ toast.success(`${evt.action.user} has synced the track to their position`)
+ }
setState(evt.state)
- // if (evt.action.type !== "UPDATE_DURATION" && evt.action.user !== "system"){
- // toast.success(`${evt.action.user} ${NotificationMessages[evt.action.type]}`)
- // }
-
})
connection.current = socket
return () => socket.close()
@@ -121,7 +87,6 @@ export const AppController = () => {
const addVideo = async (video) => {
try {
await addVideoController(video)
- // toast.success("Video is being added to the queue please wait");
} catch (e) {
console.log("ADD VIDOE ERROR", e)
toast.error("Unable to add video: invalid video url");
@@ -142,14 +107,14 @@ export const AppController = () => {
:
-
+
- {state.current.id && (showVideo ?
:
)}
+ {state.current.id &&
}
-
+
diff --git a/ui/src/pages/app/playlist.jsx b/ui/src/pages/app/playlist.jsx
index ac387a2..85315e8 100644
--- a/ui/src/pages/app/playlist.jsx
+++ b/ui/src/pages/app/playlist.jsx
@@ -1,10 +1,10 @@
import { useEffect, useRef, useState } from "react"
import { toast } from "react-hot-toast"
import Card from "./components/card"
-import { AddVideoCtrl } from "./controller"
import { createPlaylist, deletePlaylist, getChannelPlaylists, getUser, loadFromPlaylist, updatePlaylist } from "./watch2gether"
import { useOnClickOutside } from "./components/nav"
import { Link } from "react-router-dom"
+import { AddVideoCtrl } from "./components/header/Controls"
const ManagePlaylist = ({ playlist, onUpdate }) => {
@@ -124,7 +124,7 @@ export const PlaylistBtn = ({ playlists }) => {
setShow(false)
}
- return
+ return
diff --git a/ui/src/pages/app/watch2gether.js b/ui/src/pages/app/watch2gether.js
index ab3f4a5..496d54c 100644
--- a/ui/src/pages/app/watch2gether.js
+++ b/ui/src/pages/app/watch2gether.js
@@ -97,11 +97,16 @@ export async function pauseVideoController(video) {
"Content-Type": "application/json",
},
});
- // const jsonData = await response.json();
- // if (!response.ok){
- // throw jsonData.message
- // }
- // return jsonData
+}
+
+export async function seekVideoController(seek) {
+ const response = await fetch(`/api/channel/${getRoomId()}/seek`, {
+ method: "POST", // or 'PUT'
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(Math.round(seek))
+ });
}
@@ -151,6 +156,14 @@ export async function shuffleVideoController(video) {
return jsonData
}
+export async function clearVideoController() {
+ await fetch(`/api/channel/${getRoomId()}/clear`, {
+ method: "POST", // or 'PUT'
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+}
export async function getSettings() {
const response = await fetch(`/api/settings`);