aboutsummaryrefslogtreecommitdiff
path: root/main.go
diff options
context:
space:
mode:
Diffstat (limited to 'main.go')
-rw-r--r--main.go236
1 files changed, 236 insertions, 0 deletions
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..8e6befc
--- /dev/null
+++ b/main.go
@@ -0,0 +1,236 @@
+package main
+
+import (
+ "encoding/json"
+ "flag"
+ "fmt"
+ "log"
+ "net/http"
+ "strconv"
+
+ "github.com/gorilla/websocket"
+ "github.com/pion/rtwatch/gst"
+ "github.com/pion/webrtc/v2"
+)
+
+const homeHTML = `<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>synced-playback</title>
+ </head>
+ <body id="body">
+ <video id="video1" autoplay playsinline></video>
+
+ <div>
+ <input type="number" id="seekTime" value="30">
+ <button type="button" onClick="seekClick()">Seek</button>
+ <button type="button" onClick="playClick()">Play</button>
+ <button type="button" onClick="pauseClick()">Pause</button>
+ </div>
+
+ <script>
+ let conn = new WebSocket('ws://' + window.location.host + '/ws')
+ let pc = new RTCPeerConnection()
+
+ window.seekClick = () => {
+ conn.send(JSON.stringify({event: 'seek', data: document.getElementById('seekTime').value}))
+ }
+ window.playClick = () => {
+ conn.send(JSON.stringify({event: 'play', data: ''}))
+ }
+ window.pauseClick = () => {
+ conn.send(JSON.stringify({event: 'pause', data: ''}))
+ }
+
+ pc.ontrack = function (event) {
+ if (event.track.kind === 'audio') {
+ return
+ }
+ var el = document.getElementById('video1')
+ el.srcObject = event.streams[0]
+ el.autoplay = true
+ el.controls = true
+ }
+
+ conn.onopen = () => {
+ pc.createOffer({offerToReceiveVideo: true, offerToReceiveAudio: true}).then(offer => {
+ pc.setLocalDescription(offer)
+ conn.send(JSON.stringify({event: 'offer', data: JSON.stringify(offer)}))
+ })
+ }
+ conn.onclose = evt => {
+ console.log('Connection closed')
+ }
+ conn.onmessage = evt => {
+ let msg = JSON.parse(evt.data)
+ if (!msg) {
+ return console.log('failed to parse msg')
+ }
+
+ switch (msg.event) {
+ case 'answer':
+ answer = JSON.parse(msg.data)
+ if (!answer) {
+ return console.log('failed to parse answer')
+ }
+ pc.setRemoteDescription(answer)
+ }
+ }
+ window.conn = conn
+ </script>
+ </body>
+</html>
+`
+
+var (
+ upgrader = websocket.Upgrader{
+ ReadBufferSize: 1024,
+ WriteBufferSize: 1024,
+ }
+
+ peerConnectionConfig = webrtc.Configuration{}
+
+ audioTrack = &webrtc.Track{}
+ videoTrack = &webrtc.Track{}
+ pipeline = &gst.Pipeline{}
+)
+
+type websocketMessage struct {
+ Event string `json:"event"`
+ Data string `json:"data"`
+}
+
+func main() {
+ containerPath := ""
+ httpListenAddress := ""
+ flag.StringVar(&containerPath, "container-path", "", "path to the media file you want to playback")
+ flag.StringVar(&httpListenAddress, "http-listen-address", ":8080", "address for HTTP server to listen on")
+ flag.Parse()
+
+ if containerPath == "" {
+ panic("-container-path must be specified")
+ }
+
+ pc, err := webrtc.NewPeerConnection(webrtc.Configuration{
+ ICEServers: []webrtc.ICEServer{
+ {
+ URLs: []string{"stun:stun.l.google.com:19302"},
+ },
+ },
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ videoTrack, err = pc.NewTrack(webrtc.DefaultPayloadTypeH264, 5000, "synced-video", "synced-video")
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ audioTrack, err = pc.NewTrack(webrtc.DefaultPayloadTypeOpus, 5001, "synced-video", "synced-video")
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ pipeline = gst.CreatePipeline(containerPath, audioTrack, videoTrack)
+ pipeline.Start()
+
+ http.HandleFunc("/", serveHome)
+ http.HandleFunc("/ws", serveWs)
+
+ fmt.Println(fmt.Sprintf("Video file '%s' is now available on '%s', have fun!", containerPath, httpListenAddress))
+ log.Fatal(http.ListenAndServe(httpListenAddress, nil))
+}
+
+func handleWebsocketMessage(pc *webrtc.PeerConnection, ws *websocket.Conn, message *websocketMessage) error {
+ switch message.Event {
+ case "play":
+ pipeline.Play()
+ case "pause":
+ pipeline.Pause()
+ case "seek":
+ i, err := strconv.ParseInt(message.Data, 0, 64)
+ if err != nil {
+ log.Print(err)
+ }
+ pipeline.SeekToTime(i)
+ case "offer":
+ offer := webrtc.SessionDescription{}
+ if err := json.Unmarshal([]byte(message.Data), &offer); err != nil {
+ return err
+ }
+
+ if err := pc.SetRemoteDescription(offer); err != nil {
+ return err
+ }
+
+ answer, err := pc.CreateAnswer(nil)
+ if err != nil {
+ return err
+ }
+ if err := pc.SetLocalDescription(answer); err != nil {
+ return err
+ }
+
+ answerString, err := json.Marshal(answer)
+ if err != nil {
+ return err
+ }
+
+ if err = ws.WriteJSON(&websocketMessage{
+ Event: "answer",
+ Data: string(answerString),
+ }); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func serveWs(w http.ResponseWriter, r *http.Request) {
+ ws, err := upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ if _, ok := err.(websocket.HandshakeError); !ok {
+ log.Println(err)
+ }
+ return
+ }
+
+ peerConnection, err := webrtc.NewPeerConnection(peerConnectionConfig)
+ if err != nil {
+ log.Print(err)
+ return
+ } else if _, err = peerConnection.AddTrack(audioTrack); err != nil {
+ log.Print(err)
+ return
+ } else if _, err = peerConnection.AddTrack(videoTrack); err != nil {
+ log.Print(err)
+ return
+ }
+
+ defer func() {
+ if err := peerConnection.Close(); err != nil {
+ log.Println(err)
+ }
+ }()
+
+ message := &websocketMessage{}
+ for {
+ _, msg, err := ws.ReadMessage()
+ if err != nil {
+ break
+ } else if err := json.Unmarshal(msg, &message); err != nil {
+ log.Print(err)
+ return
+ }
+
+ if err := handleWebsocketMessage(peerConnection, ws, message); err != nil {
+ log.Print(err)
+ }
+ }
+}
+
+func serveHome(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ fmt.Fprintf(w, homeHTML)
+}