Compare commits
8 Commits
7ba50976cf
...
370e056a2b
| Author | SHA1 | Date |
|---|---|---|
|
|
370e056a2b | |
|
|
55e5136e88 | |
|
|
2586e60f0c | |
|
|
7a7e99337f | |
|
|
f52b9a9c53 | |
|
|
9350346b4c | |
|
|
728f2d67d0 | |
|
|
5f81ed1756 |
|
|
@ -1,3 +1,4 @@
|
||||||
*/.env
|
*/.env
|
||||||
test
|
test
|
||||||
*/.DS_STORE
|
*/.DS_STORE
|
||||||
|
.DS_STORE
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
# watchtogether
|
||||||
|
|
||||||
|
This was a project that I created over 7 days of working while having covid :)
|
||||||
|
|
||||||
|
I probably won't do much with the codebase, but enjoy how I basically figured out how to sync video streams with multiple clients
|
||||||
|
|
||||||
|
## The Stack
|
||||||
|
|
||||||
|
- frontend
|
||||||
|
|
||||||
|
- next js
|
||||||
|
- websockets
|
||||||
|
- chakra-ui
|
||||||
|
- react-player
|
||||||
|
|
||||||
|
- backend
|
||||||
|
|
||||||
|
- go
|
||||||
|
- gorilla/websockets
|
||||||
|
|
||||||
|
## how 2 deploy?
|
||||||
|
|
||||||
|
you don't
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
cmd/watchtogether/watchtogether
|
||||||
|
|
@ -5,14 +5,8 @@
|
||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="8a64704d-5500-41a6-aa4c-e275933fc58c" name="Changes" comment="">
|
<list default="true" id="8a64704d-5500-41a6-aa4c-e275933fc58c" name="Changes" comment="">
|
||||||
<change beforePath="$PROJECT_DIR$/../.vscode/settings.json" beforeDir="false" afterPath="$PROJECT_DIR$/../.vscode/settings.json" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/internal/ws/handlers.go" beforeDir="false" afterPath="$PROJECT_DIR$/internal/ws/handlers.go" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/../frontend/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/../frontend/package.json" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/../frontend/src/hooks/useWS.ts" beforeDir="false" afterPath="$PROJECT_DIR$/../frontend/src/hooks/useWS.ts" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/../frontend/src/pages/player.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/../frontend/src/pages/player.tsx" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/../frontend/src/pages/player.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/../frontend/src/pages/player.tsx" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/../frontend/src/ws/websocket.ts" beforeDir="false" afterPath="$PROJECT_DIR$/../frontend/src/ws/websocket.ts" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/../frontend/yarn.lock" beforeDir="false" afterPath="$PROJECT_DIR$/../frontend/yarn.lock" afterDir="false" />
|
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
# backend
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
#!/bin/bash
|
||||||
|
VERSION=v1.0.0
|
||||||
|
COMMIT_HASH=$(git rev-parse --short HEAD)
|
||||||
|
ENVIRONMENT=$ENV
|
||||||
|
BUILDSTRING="$VERSION-$COMMIT_HASH-$ENVIRONMENT"
|
||||||
|
echo The environment is "$ENVIRONMENT"
|
||||||
|
echo Building watch together backend "$VERSION-$COMMIT_HASH"
|
||||||
|
if [[ "$ENVIRONMENT" == "dev" ]]; then
|
||||||
|
go build -gcflags="all=-N -l" -ldflags "-X github.com/qpixel/watchtogether/cmd/watchtogether/main.VERSION=${BUILDSTRING} -X github.com/qpixel/watchtogether/cmd/watchtogether/main.ENVIRONMENT=${ENVIRONMENT}";
|
||||||
|
else
|
||||||
|
go build -ldflags "-X github.com/qpixel/watchtogether/cmd/watchtogether/main.VERSION=${BUILDSTRING} -X github.com/ubergeek77/uberbot/v2/core.ENVIRONMENT=${ENVIRONMENT}";
|
||||||
|
fi
|
||||||
|
|
@ -10,6 +10,11 @@ import (
|
||||||
|
|
||||||
var log = tlog.NewTaggedLogger("Logger", tlog.NewColor("38;5;111"))
|
var log = tlog.NewTaggedLogger("Logger", tlog.NewColor("38;5;111"))
|
||||||
|
|
||||||
|
var (
|
||||||
|
VERSION string
|
||||||
|
ENVIRONMENT string
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
hub := ws.NewHub()
|
hub := ws.NewHub()
|
||||||
|
|
@ -47,7 +52,8 @@ func main() {
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
})
|
})
|
||||||
|
log.Infof("starting backend")
|
||||||
|
log.Infof("running version %s. environment: %s", VERSION, ENVIRONMENT)
|
||||||
err := http.ListenAndServe(":8080", r)
|
err := http.ListenAndServe(":8080", r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("%s", err.Error())
|
log.Errorf("%s", err.Error())
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,20 @@ func handleIdentifyEvent(message *Message) {
|
||||||
if id, ok := d["clientID"]; ok {
|
if id, ok := d["clientID"]; ok {
|
||||||
log.Infof("Client %s has sent identify event", id.(string))
|
log.Infof("Client %s has sent identify event", id.(string))
|
||||||
}
|
}
|
||||||
|
user := d["user"].(map[string]interface{})
|
||||||
|
userId := user["id"].(string)
|
||||||
|
playhead := message.hub.State.playhead
|
||||||
|
paused := message.hub.State.paused
|
||||||
m := Message{
|
m := Message{
|
||||||
MessageData: MessageData{
|
MessageData: MessageData{
|
||||||
Type: Identify,
|
Type: Identify,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"admin": true,
|
"admin": message.hub.State.IsAdmin(userId),
|
||||||
"playlist": "http://cdnapi.kaltura.com/p/1878761/sp/187876100/playManifest/entryId/1_usagz19w/flavorIds/1_5spqkazq,1_nslowvhp,1_boih5aji,1_qahc37ag/format/applehttp/protocol/http/a.m3u8",
|
"playlist": "http://localhost:8081/BelleOpening.m3u8",
|
||||||
"playhead": 0,
|
"hasController": message.hub.State.IsController(userId),
|
||||||
"user": d["user"],
|
"playhead": playhead,
|
||||||
|
"paused": paused,
|
||||||
|
"user": d["user"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -42,5 +48,29 @@ func handleGetPlayhead(message *Message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleSetPlayhead(message *Message) {
|
func handleSetPlayhead(message *Message) {
|
||||||
|
d := message.Data.(map[string]interface{})
|
||||||
|
m := Message{
|
||||||
|
MessageData: MessageData{
|
||||||
|
Type: SetPlayhead,
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"playhead": d["playhead"],
|
||||||
|
"paused": d["paused"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
log.Infof("Received SetPlayhead event. playhead is at %s", d["playhead"])
|
||||||
|
err := message.hub.State.setPlayhead(d["playhead"].(float64))
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("unable to set playhead. %s", err)
|
||||||
|
}
|
||||||
|
err = message.hub.State.setPaused(d["paused"].(bool))
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("unable to set paused. %s", err)
|
||||||
|
}
|
||||||
|
for client := range message.Client.hub.Clients {
|
||||||
|
if client == message.Client {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
client.send <- m.SerializeMessage().Data
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@ type Hub struct {
|
||||||
// Registered Clients
|
// Registered Clients
|
||||||
Clients map[*Client]bool
|
Clients map[*Client]bool
|
||||||
|
|
||||||
|
// State
|
||||||
|
State *State
|
||||||
|
|
||||||
// Inbound messages from the Clients
|
// Inbound messages from the Clients
|
||||||
broadcast chan RawMessage
|
broadcast chan RawMessage
|
||||||
|
|
||||||
|
|
@ -20,6 +23,7 @@ func NewHub() *Hub {
|
||||||
register: make(chan *Client),
|
register: make(chan *Client),
|
||||||
unregister: make(chan *Client),
|
unregister: make(chan *Client),
|
||||||
Clients: make(map[*Client]bool),
|
Clients: make(map[*Client]bool),
|
||||||
|
State: NewState(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -30,9 +34,9 @@ func (h *Hub) handleMessage(rm RawMessage) {
|
||||||
handleIdentifyEvent(&m)
|
handleIdentifyEvent(&m)
|
||||||
case Ping:
|
case Ping:
|
||||||
handlePingEvent(&m)
|
handlePingEvent(&m)
|
||||||
case Position:
|
case GetPlayhead:
|
||||||
handleGetPlayhead(&m)
|
handleGetPlayhead(&m)
|
||||||
case SetPosition:
|
case SetPlayhead:
|
||||||
handleSetPlayhead(&m)
|
handleSetPlayhead(&m)
|
||||||
default:
|
default:
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ const (
|
||||||
Ping MessageTypes = iota
|
Ping MessageTypes = iota
|
||||||
Pong
|
Pong
|
||||||
Identify
|
Identify
|
||||||
Position
|
GetPlayhead
|
||||||
SetPosition
|
SetPlayhead
|
||||||
)
|
)
|
||||||
|
|
||||||
type MessageData struct {
|
type MessageData struct {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
package ws
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type State struct {
|
||||||
|
sync.RWMutex
|
||||||
|
|
||||||
|
playhead float64
|
||||||
|
controllerUserId string
|
||||||
|
adminUserId string
|
||||||
|
paused bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewState() *State {
|
||||||
|
return &State{
|
||||||
|
playhead: 0,
|
||||||
|
controllerUserId: "218072060923084802",
|
||||||
|
adminUserId: "218072060923084802",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *State) setPlayhead(playhead float64) error {
|
||||||
|
if s == nil {
|
||||||
|
return errors.New("unable to find state")
|
||||||
|
}
|
||||||
|
s.Lock()
|
||||||
|
defer s.Unlock()
|
||||||
|
s.playhead = playhead
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (s *State) setPaused(paused bool) error {
|
||||||
|
if s == nil {
|
||||||
|
return errors.New("unable to find state")
|
||||||
|
}
|
||||||
|
s.Lock()
|
||||||
|
defer s.Unlock()
|
||||||
|
s.paused = paused
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (s *State) IsController(userID string) bool {
|
||||||
|
if userID == s.controllerUserId {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
func (s *State) IsAdmin(userID string) bool {
|
||||||
|
if userID == s.adminUserId {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
@ -1,39 +1 @@
|
||||||
# Example app with [chakra-ui](https://github.com/chakra-ui/chakra-ui) and TypeScript
|
# frontend
|
||||||
|
|
||||||
This example features how to use [chakra-ui](https://github.com/chakra-ui/chakra-ui) as the component library within a Next.js app with TypeScript.
|
|
||||||
|
|
||||||
Next.js and chakra-ui have built-in TypeScript declarations, so we'll get autocompletion for their modules straight away.
|
|
||||||
|
|
||||||
We are connecting the Next.js `_app.js` with `chakra-ui`'s Provider and theme so the pages can have app-wide dark/light mode. We are also creating some components which shows the usage of `chakra-ui`'s style props.
|
|
||||||
|
|
||||||
## Preview
|
|
||||||
|
|
||||||
Preview the example live on [StackBlitz](http://stackblitz.com/):
|
|
||||||
|
|
||||||
[](https://stackblitz.com/github/vercel/next.js/tree/canary/examples/with-chakra-ui-typescript)
|
|
||||||
|
|
||||||
## Deploy your own
|
|
||||||
|
|
||||||
Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example):
|
|
||||||
|
|
||||||
[](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-chakra-ui-typescript&project-name=with-chakra-ui-typescript&repository-name=with-chakra-ui-typescript)
|
|
||||||
|
|
||||||
## How to use
|
|
||||||
|
|
||||||
### Using `create-next-app`
|
|
||||||
|
|
||||||
Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx create-next-app --example with-chakra-ui-typescript with-chakra-ui-typescript-app
|
|
||||||
# or
|
|
||||||
yarn create next-app --example with-chakra-ui-typescript with-chakra-ui-typescript-app
|
|
||||||
```
|
|
||||||
|
|
||||||
Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
Chakra has supported Gradients and RTL in `v1.1`. To utilize RTL, [add RTL direction and swap](https://chakra-ui.com/docs/features/rtl-support).
|
|
||||||
|
|
||||||
If you don't have multi-direction app, you should make `<Html lang="ar" dir="rtl">` inside `_document.ts`.
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,23 @@
|
||||||
import { Flex, useColorMode, FlexProps } from '@chakra-ui/react'
|
import React, { FC } from "react";
|
||||||
|
import { Flex, useColorMode, FlexProps } from "@chakra-ui/react";
|
||||||
|
|
||||||
export const Container = (props: FlexProps) => {
|
export const Container: FC<FlexProps> = (props: FlexProps) => {
|
||||||
const { colorMode } = useColorMode()
|
const { colorMode } = useColorMode();
|
||||||
|
|
||||||
const bgColor = { light: 'gray.50', dark: 'gray.900' }
|
const bgColor = { light: "gray.50", dark: "gray.900" };
|
||||||
|
|
||||||
const color = { light: 'black', dark: 'white' }
|
const color = { light: "black", dark: "white" };
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
direction="column"
|
direction="column"
|
||||||
alignItems="center"
|
|
||||||
justifyContent="flex-start"
|
justifyContent="flex-start"
|
||||||
bg={bgColor[colorMode]}
|
bg={bgColor[colorMode]}
|
||||||
color={color[colorMode]}
|
color={color[colorMode]}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
Container.defaultProps = {
|
||||||
|
alignItems: "center",
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,83 @@
|
||||||
import React, { FC } from "react";
|
import { Box } from "@chakra-ui/react";
|
||||||
|
import React, { forwardRef } from "react";
|
||||||
import ReactPlayer, { Config, ReactPlayerProps } from "react-player";
|
import ReactPlayer, { Config, ReactPlayerProps } from "react-player";
|
||||||
|
|
||||||
type PlayerProps = { id: string } & ReactPlayerProps;
|
const Player = forwardRef<ReactPlayer, ReactPlayerProps>((props, ref) => {
|
||||||
|
|
||||||
const Player: FC<PlayerProps> = (props) => {
|
|
||||||
const config: Config = {
|
const config: Config = {
|
||||||
file: {
|
file: {
|
||||||
forceHLS: true,
|
forceHLS: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return <ReactPlayer url={props.id} config={config} {...props} />;
|
// useEffect(() => {
|
||||||
};
|
// if (playerRef.current && typeof props.identity !== "undefined") {
|
||||||
|
// console.log(props.identity.playhead);
|
||||||
|
// playerRef.current.seekTo(props.identity.playhead);
|
||||||
|
// setPaused(props.identity.paused);
|
||||||
|
// }
|
||||||
|
// }, []);
|
||||||
|
// socket.emitter.once(SocketEvents.SetPlayhead, (e) => {
|
||||||
|
// console.log(e);
|
||||||
|
// playerRef.current.seekTo(e.playhead);
|
||||||
|
// setPaused(e.paused);
|
||||||
|
// });
|
||||||
|
// const onSeek = (playedSeconds: number) => {
|
||||||
|
// if (!props.identity.admin) return;
|
||||||
|
// if (paused) {
|
||||||
|
// socket?.send(
|
||||||
|
// MessageUtil.encode(
|
||||||
|
// new Message(MessageTypes.SetPlayhead, {
|
||||||
|
// playhead: playedSeconds,
|
||||||
|
// paused: true,
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
// );
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// socket?.send(
|
||||||
|
// MessageUtil.encode(
|
||||||
|
// new Message(MessageTypes.SetPlayhead, {
|
||||||
|
// playhead: playedSeconds,
|
||||||
|
// paused: false,
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
// const onPause = () => {
|
||||||
|
// if (!props.identity.admin) return;
|
||||||
|
// setPaused(true);
|
||||||
|
// socket?.send(
|
||||||
|
// MessageUtil.encode(
|
||||||
|
// new Message(MessageTypes.SetPlayhead, {
|
||||||
|
// playhead: playerRef.current.getCurrentTime(),
|
||||||
|
// paused: true,
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
// const onPlay = () => {
|
||||||
|
// if (!props.identity.admin) return;
|
||||||
|
// setPaused(false);
|
||||||
|
// socket?.send(
|
||||||
|
// MessageUtil.encode(
|
||||||
|
// new Message(MessageTypes.SetPlayhead, {
|
||||||
|
// playhead: playerRef.current.getCurrentTime(),
|
||||||
|
// paused: false,
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
return (
|
||||||
|
<Box height="100vh" width="100vw">
|
||||||
|
<ReactPlayer
|
||||||
|
url={props.url}
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
config={config}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export default Player;
|
export default Player;
|
||||||
|
|
|
||||||
|
|
@ -7,17 +7,22 @@ interface useWSProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo write websocket reconnector
|
// todo write websocket reconnector
|
||||||
const useWS = ({ user }: useWSProps) => {
|
const useWS = ({ user }: useWSProps): PlayerSocket | null => {
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// todo checkout usecallback
|
// todo checkout usecallback
|
||||||
const [socket, setSocket] = useState<PlayerSocket>();
|
const [socket, setSocket] = useState<PlayerSocket>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (socket !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let internalSocket = new PlayerSocket(user);
|
let internalSocket = new PlayerSocket(user);
|
||||||
setSocket(internalSocket);
|
setSocket(internalSocket);
|
||||||
return () => {
|
return () => {
|
||||||
return internalSocket.close();
|
if (internalSocket.readyState !== WebSocket.OPEN) {
|
||||||
|
return internalSocket.close();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
import IUser from "./IUser";
|
import IUser from "./IUser";
|
||||||
|
|
||||||
interface IdentityData {
|
interface IdentityData {
|
||||||
|
admin?: boolean;
|
||||||
|
hasController?: boolean;
|
||||||
clientID?: string;
|
clientID?: string;
|
||||||
playlist?: string;
|
playlist?: string;
|
||||||
playHead?: number;
|
playhead?: number;
|
||||||
|
paused?: boolean;
|
||||||
user: IUser;
|
user: IUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
export default interface SetPlayheadEvent {
|
||||||
|
playhead: number;
|
||||||
|
paused: boolean;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
enum SocketEvents {
|
||||||
|
Identify = "Identify",
|
||||||
|
SetPlayhead = "SetPlayhead",
|
||||||
|
GetPlayhead = "GetPlayhead",
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SocketEvents;
|
||||||
|
|
@ -8,6 +8,7 @@ import { Container } from "../components/Container";
|
||||||
import { Footer } from "../components/Footer";
|
import { Footer } from "../components/Footer";
|
||||||
import { Hero } from "../components/Hero";
|
import { Hero } from "../components/Hero";
|
||||||
import { Main } from "../components/Main";
|
import { Main } from "../components/Main";
|
||||||
|
import { isDev } from "../util";
|
||||||
|
|
||||||
const Index: NextPage = () => {
|
const Index: NextPage = () => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -22,6 +23,7 @@ const Index: NextPage = () => {
|
||||||
maxWidth="200"
|
maxWidth="200"
|
||||||
alignSelf="center"
|
alignSelf="center"
|
||||||
onClick={() => signIn("discord")}
|
onClick={() => signIn("discord")}
|
||||||
|
disabled={!isDev()}
|
||||||
>
|
>
|
||||||
Login With Discord
|
Login With Discord
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,98 @@
|
||||||
import { GetServerSideProps, NextPage } from "next";
|
import { GetServerSideProps, NextPage } from "next";
|
||||||
import { User } from "next-auth";
|
import { User } from "next-auth";
|
||||||
import { getSession } from "next-auth/react";
|
import { getSession } from "next-auth/react";
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import React, { useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import ReactPlayer from "react-player";
|
import ReactPlayer from "react-player";
|
||||||
import { Container } from "../components/Container";
|
import { Container } from "../components/Container";
|
||||||
|
import Player from "../components/Player";
|
||||||
import useWS from "../hooks/useWS";
|
import useWS from "../hooks/useWS";
|
||||||
import IdentityData from "../interfaces/Identity";
|
import IdentityData from "../interfaces/Identity";
|
||||||
import { MessageTypes } from "../interfaces/IMessage";
|
import { MessageTypes } from "../interfaces/IMessage";
|
||||||
import isBrowser from "../util/isBrowser";
|
import SetPlayheadEvent from "../interfaces/Playhead";
|
||||||
|
import SocketEvents from "../interfaces/SocketEvents";
|
||||||
const Player = dynamic(() => import("../components/Player"), { ssr: false });
|
import { isBrowser } from "../util";
|
||||||
|
import Message from "../util/Message";
|
||||||
|
import MessageUtil from "../util/MessageUtil";
|
||||||
|
|
||||||
interface PlayerPageProps {
|
interface PlayerPageProps {
|
||||||
URI: string;
|
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
// types for the function
|
||||||
|
|
||||||
const PlayerPage: NextPage<PlayerPageProps> = ({ URI, user }) => {
|
const PlayerPage: NextPage<PlayerPageProps> = ({ user }) => {
|
||||||
const playerRef = useRef<ReactPlayer>();
|
|
||||||
const socket = useWS({ user });
|
const socket = useWS({ user });
|
||||||
|
const playerRef = useRef<ReactPlayer>();
|
||||||
const [id, setID] = useState<string>("");
|
const [id, setID] = useState<string>("");
|
||||||
if (isBrowser() && typeof socket !== "undefined") {
|
const [identity, setIdentity] = useState<IdentityData>();
|
||||||
socket.emitter.on("Identify", (e: IdentityData) => {
|
const [paused, setPaused] = useState<boolean>(true);
|
||||||
console.log(e);
|
|
||||||
setID(e.playlist);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isBrowser() && typeof socket !== "undefined") {
|
||||||
|
socket?.emitter.once(SocketEvents.Identify, (e: IdentityData) => {
|
||||||
|
setID(e.playlist);
|
||||||
|
setIdentity(e);
|
||||||
|
playerRef?.current.seekTo(e.playhead);
|
||||||
|
setPaused(e.paused);
|
||||||
|
});
|
||||||
|
socket?.emitter.on(SocketEvents.SetPlayhead, (e: SetPlayheadEvent) => {
|
||||||
|
console.log(e.paused);
|
||||||
|
setPaused(e.paused);
|
||||||
|
playerRef.current.seekTo(e.playhead);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [socket]);
|
||||||
|
const onPlay = () => {
|
||||||
|
if (!identity.admin) return;
|
||||||
|
setPaused(false);
|
||||||
|
socket?.send(
|
||||||
|
MessageUtil.encode(
|
||||||
|
new Message(MessageTypes.SetPlayhead, {
|
||||||
|
playhead: playerRef.current.getCurrentTime(),
|
||||||
|
paused: false,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const onSeek = (playedSeconds: number) => {
|
||||||
|
if (!identity.admin) return;
|
||||||
|
socket.send(
|
||||||
|
MessageUtil.encode(
|
||||||
|
new Message(MessageTypes.SetPlayhead, {
|
||||||
|
playhead: playedSeconds,
|
||||||
|
paused: paused,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const onPause = () => {
|
||||||
|
console.log("running now");
|
||||||
|
if (!identity.admin) return;
|
||||||
|
setPaused(true);
|
||||||
|
socket?.send(
|
||||||
|
MessageUtil.encode(
|
||||||
|
new Message(MessageTypes.SetPlayhead, {
|
||||||
|
playhead: playerRef.current.getCurrentTime(),
|
||||||
|
paused: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Watch Together</title>
|
<title>Watch Together</title>
|
||||||
</Head>
|
</Head>
|
||||||
<Container height="100vh">
|
<Container height="100vh" background={"#000"}>
|
||||||
<Player id={id} ref={playerRef} />
|
<Player
|
||||||
|
url={id}
|
||||||
|
onPlay={onPlay}
|
||||||
|
onPause={onPause}
|
||||||
|
onSeek={onSeek}
|
||||||
|
controls={identity?.hasController}
|
||||||
|
playing={!paused}
|
||||||
|
ref={playerRef}
|
||||||
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export default class Handler {}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
const useAPI = () => {};
|
|
||||||
|
|
||||||
export default useAPI;
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export function isDev() {
|
||||||
|
return process.env.NODE_ENV === "development";
|
||||||
|
}
|
||||||
|
export function isBrowser() {
|
||||||
|
return typeof window !== "undefined";
|
||||||
|
}
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export default function isBrowser() {
|
|
||||||
return typeof window !== "undefined";
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// nice and easy way to get types for the
|
|
||||||
|
|
||||||
import { User } from "next-auth";
|
import { User } from "next-auth";
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import IdentityData from "../interfaces/Identity";
|
import IdentityData from "../interfaces/Identity";
|
||||||
|
|
@ -8,6 +6,7 @@ import Message from "../util/Message";
|
||||||
import MessageUtil from "../util/MessageUtil";
|
import MessageUtil from "../util/MessageUtil";
|
||||||
|
|
||||||
// browser socket
|
// browser socket
|
||||||
|
// todo: write a shim for this
|
||||||
let Websocket: typeof WebSocket;
|
let Websocket: typeof WebSocket;
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
Websocket = window.WebSocket;
|
Websocket = window.WebSocket;
|
||||||
|
|
@ -26,6 +25,10 @@ export default class PlayerSocket extends Websocket {
|
||||||
this.onmessage = this.onMessage;
|
this.onmessage = this.onMessage;
|
||||||
this.onclose = this.onClose;
|
this.onclose = this.onClose;
|
||||||
}
|
}
|
||||||
|
close(code?: number, reason?: string): void {
|
||||||
|
this.emitter.removeAllListeners();
|
||||||
|
super.close(code, reason);
|
||||||
|
}
|
||||||
onMessage(evt: MessageEvent<any>) {
|
onMessage(evt: MessageEvent<any>) {
|
||||||
let message = MessageUtil.decode(evt.data);
|
let message = MessageUtil.decode(evt.data);
|
||||||
if (message.type === MessageTypes["Ping"]) {
|
if (message.type === MessageTypes["Ping"]) {
|
||||||
|
|
@ -66,7 +69,6 @@ export default class PlayerSocket extends Websocket {
|
||||||
}
|
}
|
||||||
onClose(event: CloseEvent) {
|
onClose(event: CloseEvent) {
|
||||||
console.log("[WS] socket connection closed");
|
console.log("[WS] socket connection closed");
|
||||||
console.log(event);
|
|
||||||
this.emitter.emit("closed");
|
this.emitter.emit("closed");
|
||||||
}
|
}
|
||||||
get open() {
|
get open() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue