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 ( -
+
logo

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`);