github.com/pion/webrtc/v4@v4.0.1/examples/bandwidth-estimation-from-disk/main.go (about) 1 // SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly> 2 // SPDX-License-Identifier: MIT 3 4 //go:build !js 5 // +build !js 6 7 // bandwidth-estimation-from-disk demonstrates how to use Pion's Bandwidth Estimation APIs. 8 package main 9 10 import ( 11 "bufio" 12 "encoding/base64" 13 "encoding/json" 14 "errors" 15 "fmt" 16 "io" 17 "os" 18 "strings" 19 "time" 20 21 "github.com/pion/interceptor" 22 "github.com/pion/interceptor/pkg/cc" 23 "github.com/pion/interceptor/pkg/gcc" 24 "github.com/pion/webrtc/v4" 25 "github.com/pion/webrtc/v4/pkg/media" 26 "github.com/pion/webrtc/v4/pkg/media/ivfreader" 27 ) 28 29 const ( 30 lowFile = "low.ivf" 31 lowBitrate = 300_000 32 33 medFile = "med.ivf" 34 medBitrate = 1_000_000 35 36 highFile = "high.ivf" 37 highBitrate = 2_500_000 38 39 ivfHeaderSize = 32 40 ) 41 42 // nolint: gocognit 43 func main() { 44 qualityLevels := []struct { 45 fileName string 46 bitrate int 47 }{ 48 {lowFile, lowBitrate}, 49 {medFile, medBitrate}, 50 {highFile, highBitrate}, 51 } 52 currentQuality := 0 53 54 for _, level := range qualityLevels { 55 _, err := os.Stat(level.fileName) 56 if os.IsNotExist(err) { 57 panic(fmt.Sprintf("File %s was not found", level.fileName)) 58 } 59 } 60 61 i := &interceptor.Registry{} 62 m := &webrtc.MediaEngine{} 63 if err := m.RegisterDefaultCodecs(); err != nil { 64 panic(err) 65 } 66 67 // Create a Congestion Controller. This analyzes inbound and outbound data and provides 68 // suggestions on how much we should be sending. 69 // 70 // Passing `nil` means we use the default Estimation Algorithm which is Google Congestion Control. 71 // You can use the other ones that Pion provides, or write your own! 72 congestionController, err := cc.NewInterceptor(func() (cc.BandwidthEstimator, error) { 73 return gcc.NewSendSideBWE(gcc.SendSideBWEInitialBitrate(lowBitrate)) 74 }) 75 if err != nil { 76 panic(err) 77 } 78 79 estimatorChan := make(chan cc.BandwidthEstimator, 1) 80 congestionController.OnNewPeerConnection(func(id string, estimator cc.BandwidthEstimator) { //nolint: revive 81 estimatorChan <- estimator 82 }) 83 84 i.Add(congestionController) 85 if err = webrtc.ConfigureTWCCHeaderExtensionSender(m, i); err != nil { 86 panic(err) 87 } 88 89 if err = webrtc.RegisterDefaultInterceptors(m, i); err != nil { 90 panic(err) 91 } 92 93 // Create a new RTCPeerConnection 94 peerConnection, err := webrtc.NewAPI(webrtc.WithInterceptorRegistry(i), webrtc.WithMediaEngine(m)).NewPeerConnection(webrtc.Configuration{ 95 ICEServers: []webrtc.ICEServer{ 96 { 97 URLs: []string{"stun:stun.l.google.com:19302"}, 98 }, 99 }, 100 }) 101 if err != nil { 102 panic(err) 103 } 104 defer func() { 105 if cErr := peerConnection.Close(); cErr != nil { 106 fmt.Printf("cannot close peerConnection: %v\n", cErr) 107 } 108 }() 109 110 // Wait until our Bandwidth Estimator has been created 111 estimator := <-estimatorChan 112 113 // Create a video track 114 videoTrack, err := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8}, "video", "pion") 115 if err != nil { 116 panic(err) 117 } 118 119 rtpSender, err := peerConnection.AddTrack(videoTrack) 120 if err != nil { 121 panic(err) 122 } 123 124 // Read incoming RTCP packets 125 // Before these packets are returned they are processed by interceptors. For things 126 // like NACK this needs to be called. 127 go func() { 128 rtcpBuf := make([]byte, 1500) 129 for { 130 if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil { 131 return 132 } 133 } 134 }() 135 136 // Set the handler for ICE connection state 137 // This will notify you when the peer has connected/disconnected 138 peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { 139 fmt.Printf("Connection State has changed %s \n", connectionState.String()) 140 }) 141 142 // Set the handler for Peer connection state 143 // This will notify you when the peer has connected/disconnected 144 peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { 145 fmt.Printf("Peer Connection State has changed: %s\n", s.String()) 146 }) 147 148 // Wait for the offer to be pasted 149 offer := webrtc.SessionDescription{} 150 decode(readUntilNewline(), &offer) 151 152 // Set the remote SessionDescription 153 if err = peerConnection.SetRemoteDescription(offer); err != nil { 154 panic(err) 155 } 156 157 // Create answer 158 answer, err := peerConnection.CreateAnswer(nil) 159 if err != nil { 160 panic(err) 161 } 162 163 // Create channel that is blocked until ICE Gathering is complete 164 gatherComplete := webrtc.GatheringCompletePromise(peerConnection) 165 166 // Sets the LocalDescription, and starts our UDP listeners 167 if err = peerConnection.SetLocalDescription(answer); err != nil { 168 panic(err) 169 } 170 171 // Block until ICE Gathering is complete, disabling trickle ICE 172 // we do this because we only can exchange one signaling message 173 // in a production application you should exchange ICE Candidates via OnICECandidate 174 <-gatherComplete 175 176 // Output the answer in base64 so we can paste it in browser 177 fmt.Println(encode(peerConnection.LocalDescription())) 178 179 // Open a IVF file and start reading using our IVFReader 180 file, err := os.Open(qualityLevels[currentQuality].fileName) 181 if err != nil { 182 panic(err) 183 } 184 185 ivf, header, err := ivfreader.NewWith(file) 186 if err != nil { 187 panic(err) 188 } 189 190 // 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. 191 // This isn't required since the video is timestamped, but we will such much higher loss if we send all at once. 192 // 193 // It is important to use a time.Ticker instead of time.Sleep because 194 // * avoids accumulating skew, just calling time.Sleep didn't compensate for the time spent parsing the data 195 // * works around latency issues with Sleep (see https://github.com/golang/go/issues/44343) 196 ticker := time.NewTicker(time.Millisecond * time.Duration((float32(header.TimebaseNumerator)/float32(header.TimebaseDenominator))*1000)) 197 defer ticker.Stop() 198 frame := []byte{} 199 frameHeader := &ivfreader.IVFFrameHeader{} 200 currentTimestamp := uint64(0) 201 202 switchQualityLevel := func(newQualityLevel int) { 203 fmt.Printf("Switching from %s to %s \n", qualityLevels[currentQuality].fileName, qualityLevels[newQualityLevel].fileName) 204 currentQuality = newQualityLevel 205 ivf.ResetReader(setReaderFile(qualityLevels[currentQuality].fileName)) 206 for { 207 if frame, frameHeader, err = ivf.ParseNextFrame(); err != nil { 208 break 209 } else if frameHeader.Timestamp >= currentTimestamp && frame[0]&0x1 == 0 { 210 break 211 } 212 } 213 } 214 215 for ; true; <-ticker.C { 216 targetBitrate := estimator.GetTargetBitrate() 217 switch { 218 // If current quality level is below target bitrate drop to level below 219 case currentQuality != 0 && targetBitrate < qualityLevels[currentQuality].bitrate: 220 switchQualityLevel(currentQuality - 1) 221 222 // If next quality level is above target bitrate move to next level 223 case len(qualityLevels) > (currentQuality+1) && targetBitrate > qualityLevels[currentQuality+1].bitrate: 224 switchQualityLevel(currentQuality + 1) 225 226 // Adjust outbound bandwidth for probing 227 default: 228 frame, _, err = ivf.ParseNextFrame() 229 } 230 231 switch { 232 // If we have reached the end of the file start again 233 case errors.Is(err, io.EOF): 234 ivf.ResetReader(setReaderFile(qualityLevels[currentQuality].fileName)) 235 236 // No error write the video frame 237 case err == nil: 238 currentTimestamp = frameHeader.Timestamp 239 if err = videoTrack.WriteSample(media.Sample{Data: frame, Duration: time.Second}); err != nil { 240 panic(err) 241 } 242 // Error besides io.EOF that we dont know how to handle 243 default: 244 panic(err) 245 } 246 } 247 } 248 249 func setReaderFile(filename string) func(_ int64) io.Reader { 250 return func(_ int64) io.Reader { 251 file, err := os.Open(filename) // nolint 252 if err != nil { 253 panic(err) 254 } 255 if _, err = file.Seek(ivfHeaderSize, io.SeekStart); err != nil { 256 panic(err) 257 } 258 return file 259 } 260 } 261 262 // Read from stdin until we get a newline 263 func readUntilNewline() (in string) { 264 var err error 265 266 r := bufio.NewReader(os.Stdin) 267 for { 268 in, err = r.ReadString('\n') 269 if err != nil && !errors.Is(err, io.EOF) { 270 panic(err) 271 } 272 273 if in = strings.TrimSpace(in); len(in) > 0 { 274 break 275 } 276 } 277 278 fmt.Println("") 279 return 280 } 281 282 // JSON encode + base64 a SessionDescription 283 func encode(obj *webrtc.SessionDescription) string { 284 b, err := json.Marshal(obj) 285 if err != nil { 286 panic(err) 287 } 288 289 return base64.StdEncoding.EncodeToString(b) 290 } 291 292 // Decode a base64 and unmarshal JSON into a SessionDescription 293 func decode(in string, obj *webrtc.SessionDescription) { 294 b, err := base64.StdEncoding.DecodeString(in) 295 if err != nil { 296 panic(err) 297 } 298 299 if err = json.Unmarshal(b, obj); err != nil { 300 panic(err) 301 } 302 }