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