// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build !js // +build !js // play-from-disk-renegotation demonstrates Pion WebRTC's renegotiation abilities. package main import ( "encoding/json" "fmt" "math/rand" "net/http" "os" "time" "github.com/pion/randutil" "github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3/pkg/media" "github.com/pion/webrtc/v3/pkg/media/ivfreader" ) var peerConnection *webrtc.PeerConnection //nolint // doSignaling exchanges all state of the local PeerConnection and is called // every time a video is added or removed func doSignaling(w http.ResponseWriter, r *http.Request) { var offer webrtc.SessionDescription if err := json.NewDecoder(r.Body).Decode(&offer); err != nil { panic(err) } if err := peerConnection.SetRemoteDescription(offer); err != nil { panic(err) } // Create channel that is blocked until ICE Gathering is complete gatherComplete := webrtc.GatheringCompletePromise(peerConnection) answer, err := peerConnection.CreateAnswer(nil) if err != nil { panic(err) } else if err = peerConnection.SetLocalDescription(answer); err != nil { panic(err) } // Block until ICE Gathering is complete, disabling trickle ICE // we do this because we only can exchange one signaling message // in a production application you should exchange ICE Candidates via OnICECandidate <-gatherComplete response, err := json.Marshal(*peerConnection.LocalDescription()) if err != nil { panic(err) } w.Header().Set("Content-Type", "application/json") if _, err := w.Write(response); err != nil { panic(err) } } // Add a single video track func createPeerConnection(w http.ResponseWriter, r *http.Request) { if peerConnection.ConnectionState() != webrtc.PeerConnectionStateNew { panic(fmt.Sprintf("createPeerConnection called in non-new state (%s)", peerConnection.ConnectionState())) } doSignaling(w, r) fmt.Println("PeerConnection has been created") } // Add a single video track func addVideo(w http.ResponseWriter, r *http.Request) { videoTrack, err := webrtc.NewTrackLocalStaticSample( webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8}, fmt.Sprintf("video-%d", randutil.NewMathRandomGenerator().Uint32()), fmt.Sprintf("video-%d", randutil.NewMathRandomGenerator().Uint32()), ) if err != nil { panic(err) } rtpSender, err := peerConnection.AddTrack(videoTrack) if err != nil { panic(err) } // Read incoming RTCP packets // Before these packets are returned they are processed by interceptors. For things // like NACK this needs to be called. go func() { rtcpBuf := make([]byte, 1500) for { if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil { return } } }() go writeVideoToTrack(videoTrack) doSignaling(w, r) fmt.Println("Video track has been added") } // Remove a single sender func removeVideo(w http.ResponseWriter, r *http.Request) { if senders := peerConnection.GetSenders(); len(senders) != 0 { if err := peerConnection.RemoveTrack(senders[0]); err != nil { panic(err) } } doSignaling(w, r) fmt.Println("Video track has been removed") } func main() { rand.Seed(time.Now().UTC().UnixNano()) var err error if peerConnection, err = webrtc.NewPeerConnection(webrtc.Configuration{}); err != nil { panic(err) } defer func() { if cErr := peerConnection.Close(); cErr != nil { fmt.Printf("cannot close peerConnection: %v\n", cErr) } }() // Set the handler for Peer connection state // This will notify you when the peer has connected/disconnected peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { fmt.Printf("Peer Connection State has changed: %s\n", s.String()) if s == webrtc.PeerConnectionStateFailed { // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. fmt.Println("Peer Connection has gone to failed exiting") os.Exit(0) } }) http.Handle("/", http.FileServer(http.Dir("."))) http.HandleFunc("/createPeerConnection", createPeerConnection) http.HandleFunc("/addVideo", addVideo) http.HandleFunc("/removeVideo", removeVideo) go func() { fmt.Println("Open http://localhost:8080 to access this demo") // nolint: gosec panic(http.ListenAndServe(":8080", nil)) }() // Block forever select {} } // Read a video file from disk and write it to a webrtc.Track // When the video has been completely read this exits without error func writeVideoToTrack(t *webrtc.TrackLocalStaticSample) { // Open a IVF file and start reading using our IVFReader file, err := os.Open("output.ivf") if err != nil { panic(err) } ivf, header, err := ivfreader.NewWith(file) if err != nil { panic(err) } // Send our video file frame at a time. Pace our sending so we send it at the same speed it should be played back as. // This isn't required since the video is timestamped, but we will such much higher loss if we send all at once. // // It is important to use a time.Ticker instead of time.Sleep because // * avoids accumulating skew, just calling time.Sleep didn't compensate for the time spent parsing the data // * works around latency issues with Sleep (see https://github.com/golang/go/issues/44343) ticker := time.NewTicker(time.Millisecond * time.Duration((float32(header.TimebaseNumerator)/float32(header.TimebaseDenominator))*1000)) for ; true; <-ticker.C { frame, _, err := ivf.ParseNextFrame() if err != nil { fmt.Printf("Finish writing video track: %s ", err) return } if err = t.WriteSample(media.Sample{Data: frame, Duration: time.Second}); err != nil { fmt.Printf("Finish writing video track: %s ", err) return } } }