// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build !js // +build !js // bandwidth-estimation-from-disk demonstrates how to use Pion's Bandwidth Estimation APIs. package main import ( "bufio" "encoding/base64" "encoding/json" "errors" "fmt" "io" "os" "strings" "time" "github.com/pion/interceptor" "github.com/pion/interceptor/pkg/cc" "github.com/pion/interceptor/pkg/gcc" "github.com/pion/webrtc/v4" "github.com/pion/webrtc/v4/pkg/media" "github.com/pion/webrtc/v4/pkg/media/ivfreader" ) const ( lowFile = "low.ivf" lowBitrate = 300_000 medFile = "med.ivf" medBitrate = 1_000_000 highFile = "high.ivf" highBitrate = 2_500_000 ivfHeaderSize = 32 ) func main() { //nolint:gocognit,cyclop,maintidx qualityLevels := []struct { fileName string bitrate int }{ {lowFile, lowBitrate}, {medFile, medBitrate}, {highFile, highBitrate}, } currentQuality := 0 for _, level := range qualityLevels { _, err := os.Stat(level.fileName) if os.IsNotExist(err) { panic(fmt.Sprintf("File %s was not found", level.fileName)) } } interceptorRegistry := &interceptor.Registry{} mediaEngine := &webrtc.MediaEngine{} if err := mediaEngine.RegisterDefaultCodecs(); err != nil { panic(err) } // Create a Congestion Controller. This analyzes inbound and outbound data and provides // suggestions on how much we should be sending. // // Passing `nil` means we use the default Estimation Algorithm which is Google Congestion Control. // You can use the other ones that Pion provides, or write your own! congestionController, err := cc.NewInterceptor(func() (cc.BandwidthEstimator, error) { return gcc.NewSendSideBWE(gcc.SendSideBWEInitialBitrate(lowBitrate)) }) if err != nil { panic(err) } estimatorChan := make(chan cc.BandwidthEstimator, 1) congestionController.OnNewPeerConnection(func(id string, estimator cc.BandwidthEstimator) { //nolint: revive estimatorChan <- estimator }) interceptorRegistry.Add(congestionController) if err = webrtc.ConfigureTWCCHeaderExtensionSender(mediaEngine, interceptorRegistry); err != nil { panic(err) } if err = webrtc.RegisterDefaultInterceptors(mediaEngine, interceptorRegistry); err != nil { panic(err) } // Create a new RTCPeerConnection peerConnection, err := webrtc.NewAPI( webrtc.WithInterceptorRegistry(interceptorRegistry), webrtc.WithMediaEngine(mediaEngine), ).NewPeerConnection(webrtc.Configuration{ ICEServers: []webrtc.ICEServer{ { URLs: []string{"stun:stun.l.google.com:19302"}, }, }, }) if err != nil { panic(err) } defer func() { if cErr := peerConnection.Close(); cErr != nil { fmt.Printf("cannot close peerConnection: %v\n", cErr) } }() // Wait until our Bandwidth Estimator has been created estimator := <-estimatorChan // Create a video track videoTrack, err := webrtc.NewTrackLocalStaticSample( webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8}, "video", "pion", ) 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 } } }() // Set the handler for ICE connection state // This will notify you when the peer has connected/disconnected peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { fmt.Printf("Connection State has changed %s \n", connectionState.String()) }) // Set the handler for Peer connection state // This will notify you when the peer has connected/disconnected peerConnection.OnConnectionStateChange(func(state webrtc.PeerConnectionState) { fmt.Printf("Peer Connection State has changed: %s\n", state.String()) }) // Wait for the offer to be pasted offer := webrtc.SessionDescription{} decode(readUntilNewline(), &offer) // Set the remote SessionDescription if err = peerConnection.SetRemoteDescription(offer); err != nil { panic(err) } // Create answer answer, err := peerConnection.CreateAnswer(nil) if err != nil { panic(err) } // Create channel that is blocked until ICE Gathering is complete gatherComplete := webrtc.GatheringCompletePromise(peerConnection) // Sets the LocalDescription, and starts our UDP listeners 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 // Output the answer in base64 so we can paste it in browser fmt.Println(encode(peerConnection.LocalDescription())) // Open a IVF file and start reading using our IVFReader file, err := os.Open(qualityLevels[currentQuality].fileName) 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), ) defer ticker.Stop() frame := []byte{} frameHeader := &ivfreader.IVFFrameHeader{} currentTimestamp := uint64(0) switchQualityLevel := func(newQualityLevel int) { fmt.Printf( "Switching from %s to %s \n", qualityLevels[currentQuality].fileName, qualityLevels[newQualityLevel].fileName, ) currentQuality = newQualityLevel ivf.ResetReader(setReaderFile(qualityLevels[currentQuality].fileName)) for { if frame, frameHeader, err = ivf.ParseNextFrame(); err != nil { break } else if frameHeader.Timestamp >= currentTimestamp && frame[0]&0x1 == 0 { break } } } for ; true; <-ticker.C { targetBitrate := estimator.GetTargetBitrate() switch { // If current quality level is below target bitrate drop to level below case currentQuality != 0 && targetBitrate < qualityLevels[currentQuality].bitrate: switchQualityLevel(currentQuality - 1) // If next quality level is above target bitrate move to next level case len(qualityLevels) > (currentQuality+1) && targetBitrate > qualityLevels[currentQuality+1].bitrate: switchQualityLevel(currentQuality + 1) // Adjust outbound bandwidth for probing default: frame, frameHeader, err = ivf.ParseNextFrame() } switch { // If we have reached the end of the file start again case errors.Is(err, io.EOF): ivf.ResetReader(setReaderFile(qualityLevels[currentQuality].fileName)) // No error write the video frame case err == nil: currentTimestamp = frameHeader.Timestamp if err = videoTrack.WriteSample(media.Sample{Data: frame, Duration: time.Second}); err != nil { panic(err) } // Error besides io.EOF that we dont know how to handle default: panic(err) } } } func setReaderFile(filename string) func(_ int64) io.Reader { return func(_ int64) io.Reader { file, err := os.Open(filename) // nolint if err != nil { panic(err) } if _, err = file.Seek(ivfHeaderSize, io.SeekStart); err != nil { panic(err) } return file } } // Read from stdin until we get a newline. func readUntilNewline() (in string) { var err error r := bufio.NewReader(os.Stdin) for { in, err = r.ReadString('\n') if err != nil && !errors.Is(err, io.EOF) { panic(err) } if in = strings.TrimSpace(in); len(in) > 0 { break } } fmt.Println("") return } // JSON encode + base64 a SessionDescription. func encode(obj *webrtc.SessionDescription) string { b, err := json.Marshal(obj) if err != nil { panic(err) } return base64.StdEncoding.EncodeToString(b) } // Decode a base64 and unmarshal JSON into a SessionDescription. func decode(in string, obj *webrtc.SessionDescription) { b, err := base64.StdEncoding.DecodeString(in) if err != nil { panic(err) } if err = json.Unmarshal(b, obj); err != nil { panic(err) } }