github.com/pion/webrtc/v4@v4.0.1/examples/play-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 // play-from-disk demonstrates how to send video and/or audio to your browser from files saved to disk. 8 package main 9 10 import ( 11 "bufio" 12 "context" 13 "encoding/base64" 14 "encoding/json" 15 "errors" 16 "fmt" 17 "io" 18 "os" 19 "strings" 20 "time" 21 22 "github.com/pion/webrtc/v4" 23 "github.com/pion/webrtc/v4/pkg/media" 24 "github.com/pion/webrtc/v4/pkg/media/ivfreader" 25 "github.com/pion/webrtc/v4/pkg/media/oggreader" 26 ) 27 28 const ( 29 audioFileName = "output.ogg" 30 videoFileName = "output.ivf" 31 oggPageDuration = time.Millisecond * 20 32 ) 33 34 // nolint:gocognit 35 func main() { 36 // Assert that we have an audio or video file 37 _, err := os.Stat(videoFileName) 38 haveVideoFile := !os.IsNotExist(err) 39 40 _, err = os.Stat(audioFileName) 41 haveAudioFile := !os.IsNotExist(err) 42 43 if !haveAudioFile && !haveVideoFile { 44 panic("Could not find `" + audioFileName + "` or `" + videoFileName + "`") 45 } 46 47 // Create a new RTCPeerConnection 48 peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{ 49 ICEServers: []webrtc.ICEServer{ 50 { 51 URLs: []string{"stun:stun.l.google.com:19302"}, 52 }, 53 }, 54 }) 55 if err != nil { 56 panic(err) 57 } 58 defer func() { 59 if cErr := peerConnection.Close(); cErr != nil { 60 fmt.Printf("cannot close peerConnection: %v\n", cErr) 61 } 62 }() 63 64 iceConnectedCtx, iceConnectedCtxCancel := context.WithCancel(context.Background()) 65 66 if haveVideoFile { 67 file, openErr := os.Open(videoFileName) 68 if openErr != nil { 69 panic(openErr) 70 } 71 72 _, header, openErr := ivfreader.NewWith(file) 73 if openErr != nil { 74 panic(openErr) 75 } 76 77 // Determine video codec 78 var trackCodec string 79 switch header.FourCC { 80 case "AV01": 81 trackCodec = webrtc.MimeTypeAV1 82 case "VP90": 83 trackCodec = webrtc.MimeTypeVP9 84 case "VP80": 85 trackCodec = webrtc.MimeTypeVP8 86 default: 87 panic(fmt.Sprintf("Unable to handle FourCC %s", header.FourCC)) 88 } 89 90 // Create a video track 91 videoTrack, videoTrackErr := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: trackCodec}, "video", "pion") 92 if videoTrackErr != nil { 93 panic(videoTrackErr) 94 } 95 96 rtpSender, videoTrackErr := peerConnection.AddTrack(videoTrack) 97 if videoTrackErr != nil { 98 panic(videoTrackErr) 99 } 100 101 // Read incoming RTCP packets 102 // Before these packets are returned they are processed by interceptors. For things 103 // like NACK this needs to be called. 104 go func() { 105 rtcpBuf := make([]byte, 1500) 106 for { 107 if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil { 108 return 109 } 110 } 111 }() 112 113 go func() { 114 // Open a IVF file and start reading using our IVFReader 115 file, ivfErr := os.Open(videoFileName) 116 if ivfErr != nil { 117 panic(ivfErr) 118 } 119 120 ivf, header, ivfErr := ivfreader.NewWith(file) 121 if ivfErr != nil { 122 panic(ivfErr) 123 } 124 125 // Wait for connection established 126 <-iceConnectedCtx.Done() 127 128 // 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. 129 // This isn't required since the video is timestamped, but we will such much higher loss if we send all at once. 130 // 131 // It is important to use a time.Ticker instead of time.Sleep because 132 // * avoids accumulating skew, just calling time.Sleep didn't compensate for the time spent parsing the data 133 // * works around latency issues with Sleep (see https://github.com/golang/go/issues/44343) 134 ticker := time.NewTicker(time.Millisecond * time.Duration((float32(header.TimebaseNumerator)/float32(header.TimebaseDenominator))*1000)) 135 defer ticker.Stop() 136 for ; true; <-ticker.C { 137 frame, _, ivfErr := ivf.ParseNextFrame() 138 if errors.Is(ivfErr, io.EOF) { 139 fmt.Printf("All video frames parsed and sent") 140 os.Exit(0) 141 } 142 143 if ivfErr != nil { 144 panic(ivfErr) 145 } 146 147 if ivfErr = videoTrack.WriteSample(media.Sample{Data: frame, Duration: time.Second}); ivfErr != nil { 148 panic(ivfErr) 149 } 150 } 151 }() 152 } 153 154 if haveAudioFile { 155 // Create a audio track 156 audioTrack, audioTrackErr := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus}, "audio", "pion") 157 if audioTrackErr != nil { 158 panic(audioTrackErr) 159 } 160 161 rtpSender, audioTrackErr := peerConnection.AddTrack(audioTrack) 162 if audioTrackErr != nil { 163 panic(audioTrackErr) 164 } 165 166 // Read incoming RTCP packets 167 // Before these packets are returned they are processed by interceptors. For things 168 // like NACK this needs to be called. 169 go func() { 170 rtcpBuf := make([]byte, 1500) 171 for { 172 if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil { 173 return 174 } 175 } 176 }() 177 178 go func() { 179 // Open a OGG file and start reading using our OGGReader 180 file, oggErr := os.Open(audioFileName) 181 if oggErr != nil { 182 panic(oggErr) 183 } 184 185 // Open on oggfile in non-checksum mode. 186 ogg, _, oggErr := oggreader.NewWith(file) 187 if oggErr != nil { 188 panic(oggErr) 189 } 190 191 // Wait for connection established 192 <-iceConnectedCtx.Done() 193 194 // Keep track of last granule, the difference is the amount of samples in the buffer 195 var lastGranule uint64 196 197 // It is important to use a time.Ticker instead of time.Sleep because 198 // * avoids accumulating skew, just calling time.Sleep didn't compensate for the time spent parsing the data 199 // * works around latency issues with Sleep (see https://github.com/golang/go/issues/44343) 200 ticker := time.NewTicker(oggPageDuration) 201 defer ticker.Stop() 202 for ; true; <-ticker.C { 203 pageData, pageHeader, oggErr := ogg.ParseNextPage() 204 if errors.Is(oggErr, io.EOF) { 205 fmt.Printf("All audio pages parsed and sent") 206 os.Exit(0) 207 } 208 209 if oggErr != nil { 210 panic(oggErr) 211 } 212 213 // The amount of samples is the difference between the last and current timestamp 214 sampleCount := float64(pageHeader.GranulePosition - lastGranule) 215 lastGranule = pageHeader.GranulePosition 216 sampleDuration := time.Duration((sampleCount/48000)*1000) * time.Millisecond 217 218 if oggErr = audioTrack.WriteSample(media.Sample{Data: pageData, Duration: sampleDuration}); oggErr != nil { 219 panic(oggErr) 220 } 221 } 222 }() 223 } 224 225 // Set the handler for ICE connection state 226 // This will notify you when the peer has connected/disconnected 227 peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { 228 fmt.Printf("Connection State has changed %s \n", connectionState.String()) 229 if connectionState == webrtc.ICEConnectionStateConnected { 230 iceConnectedCtxCancel() 231 } 232 }) 233 234 // Set the handler for Peer connection state 235 // This will notify you when the peer has connected/disconnected 236 peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { 237 fmt.Printf("Peer Connection State has changed: %s\n", s.String()) 238 239 if s == webrtc.PeerConnectionStateFailed { 240 // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. 241 // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. 242 // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. 243 fmt.Println("Peer Connection has gone to failed exiting") 244 os.Exit(0) 245 } 246 247 if s == webrtc.PeerConnectionStateClosed { 248 // PeerConnection was explicitly closed. This usually happens from a DTLS CloseNotify 249 fmt.Println("Peer Connection has gone to closed exiting") 250 os.Exit(0) 251 } 252 }) 253 254 // Wait for the offer to be pasted 255 offer := webrtc.SessionDescription{} 256 decode(readUntilNewline(), &offer) 257 258 // Set the remote SessionDescription 259 if err = peerConnection.SetRemoteDescription(offer); err != nil { 260 panic(err) 261 } 262 263 // Create answer 264 answer, err := peerConnection.CreateAnswer(nil) 265 if err != nil { 266 panic(err) 267 } 268 269 // Create channel that is blocked until ICE Gathering is complete 270 gatherComplete := webrtc.GatheringCompletePromise(peerConnection) 271 272 // Sets the LocalDescription, and starts our UDP listeners 273 if err = peerConnection.SetLocalDescription(answer); err != nil { 274 panic(err) 275 } 276 277 // Block until ICE Gathering is complete, disabling trickle ICE 278 // we do this because we only can exchange one signaling message 279 // in a production application you should exchange ICE Candidates via OnICECandidate 280 <-gatherComplete 281 282 // Output the answer in base64 so we can paste it in browser 283 fmt.Println(encode(peerConnection.LocalDescription())) 284 285 // Block forever 286 select {} 287 } 288 289 // Read from stdin until we get a newline 290 func readUntilNewline() (in string) { 291 var err error 292 293 r := bufio.NewReader(os.Stdin) 294 for { 295 in, err = r.ReadString('\n') 296 if err != nil && !errors.Is(err, io.EOF) { 297 panic(err) 298 } 299 300 if in = strings.TrimSpace(in); len(in) > 0 { 301 break 302 } 303 } 304 305 fmt.Println("") 306 return 307 } 308 309 // JSON encode + base64 a SessionDescription 310 func encode(obj *webrtc.SessionDescription) string { 311 b, err := json.Marshal(obj) 312 if err != nil { 313 panic(err) 314 } 315 316 return base64.StdEncoding.EncodeToString(b) 317 } 318 319 // Decode a base64 and unmarshal JSON into a SessionDescription 320 func decode(in string, obj *webrtc.SessionDescription) { 321 b, err := base64.StdEncoding.DecodeString(in) 322 if err != nil { 323 panic(err) 324 } 325 326 if err = json.Unmarshal(b, obj); err != nil { 327 panic(err) 328 } 329 }