github.com/pion/webrtc/v4@v4.0.1/examples/save-to-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 // save-to-disk is a simple application that shows how to record your webcam/microphone using Pion WebRTC and save VP8/Opus to disk. 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 20 "github.com/pion/interceptor" 21 "github.com/pion/interceptor/pkg/intervalpli" 22 "github.com/pion/webrtc/v4" 23 "github.com/pion/webrtc/v4/pkg/media" 24 "github.com/pion/webrtc/v4/pkg/media/ivfwriter" 25 "github.com/pion/webrtc/v4/pkg/media/oggwriter" 26 ) 27 28 func saveToDisk(i media.Writer, track *webrtc.TrackRemote) { 29 defer func() { 30 if err := i.Close(); err != nil { 31 panic(err) 32 } 33 }() 34 35 for { 36 rtpPacket, _, err := track.ReadRTP() 37 if err != nil { 38 fmt.Println(err) 39 return 40 } 41 if err := i.WriteRTP(rtpPacket); err != nil { 42 fmt.Println(err) 43 return 44 } 45 } 46 } 47 48 // nolint:gocognit 49 func main() { 50 // Everything below is the Pion WebRTC API! Thanks for using it ❤️. 51 52 // Create a MediaEngine object to configure the supported codec 53 m := &webrtc.MediaEngine{} 54 55 // Setup the codecs you want to use. 56 // We'll use a VP8 and Opus but you can also define your own 57 if err := m.RegisterCodec(webrtc.RTPCodecParameters{ 58 RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8, ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil}, 59 PayloadType: 96, 60 }, webrtc.RTPCodecTypeVideo); err != nil { 61 panic(err) 62 } 63 if err := m.RegisterCodec(webrtc.RTPCodecParameters{ 64 RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus, ClockRate: 48000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil}, 65 PayloadType: 111, 66 }, webrtc.RTPCodecTypeAudio); err != nil { 67 panic(err) 68 } 69 70 // Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline. 71 // This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection` 72 // this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry 73 // for each PeerConnection. 74 i := &interceptor.Registry{} 75 76 // Register a intervalpli factory 77 // This interceptor sends a PLI every 3 seconds. A PLI causes a video keyframe to be generated by the sender. 78 // This makes our video seekable and more error resilent, but at a cost of lower picture quality and higher bitrates 79 // A real world application should process incoming RTCP packets from viewers and forward them to senders 80 intervalPliFactory, err := intervalpli.NewReceiverInterceptor() 81 if err != nil { 82 panic(err) 83 } 84 i.Add(intervalPliFactory) 85 86 // Use the default set of Interceptors 87 if err = webrtc.RegisterDefaultInterceptors(m, i); err != nil { 88 panic(err) 89 } 90 91 // Create the API object with the MediaEngine 92 api := webrtc.NewAPI(webrtc.WithMediaEngine(m), webrtc.WithInterceptorRegistry(i)) 93 94 // Prepare the configuration 95 config := webrtc.Configuration{ 96 ICEServers: []webrtc.ICEServer{ 97 { 98 URLs: []string{"stun:stun.l.google.com:19302"}, 99 }, 100 }, 101 } 102 103 // Create a new RTCPeerConnection 104 peerConnection, err := api.NewPeerConnection(config) 105 if err != nil { 106 panic(err) 107 } 108 109 // Allow us to receive 1 audio track, and 1 video track 110 if _, err = peerConnection.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio); err != nil { 111 panic(err) 112 } else if _, err = peerConnection.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo); err != nil { 113 panic(err) 114 } 115 116 oggFile, err := oggwriter.New("output.ogg", 48000, 2) 117 if err != nil { 118 panic(err) 119 } 120 ivfFile, err := ivfwriter.New("output.ivf") 121 if err != nil { 122 panic(err) 123 } 124 125 // Set a handler for when a new remote track starts, this handler saves buffers to disk as 126 // an ivf file, since we could have multiple video tracks we provide a counter. 127 // In your application this is where you would handle/process video 128 peerConnection.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { //nolint: revive 129 codec := track.Codec() 130 if strings.EqualFold(codec.MimeType, webrtc.MimeTypeOpus) { 131 fmt.Println("Got Opus track, saving to disk as output.opus (48 kHz, 2 channels)") 132 saveToDisk(oggFile, track) 133 } else if strings.EqualFold(codec.MimeType, webrtc.MimeTypeVP8) { 134 fmt.Println("Got VP8 track, saving to disk as output.ivf") 135 saveToDisk(ivfFile, track) 136 } 137 }) 138 139 // Set the handler for ICE connection state 140 // This will notify you when the peer has connected/disconnected 141 peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { 142 fmt.Printf("Connection State has changed %s \n", connectionState.String()) 143 144 if connectionState == webrtc.ICEConnectionStateConnected { 145 fmt.Println("Ctrl+C the remote client to stop the demo") 146 } else if connectionState == webrtc.ICEConnectionStateFailed || connectionState == webrtc.ICEConnectionStateClosed { 147 if closeErr := oggFile.Close(); closeErr != nil { 148 panic(closeErr) 149 } 150 151 if closeErr := ivfFile.Close(); closeErr != nil { 152 panic(closeErr) 153 } 154 155 fmt.Println("Done writing media files") 156 157 // Gracefully shutdown the peer connection 158 if closeErr := peerConnection.Close(); closeErr != nil { 159 panic(closeErr) 160 } 161 162 os.Exit(0) 163 } 164 }) 165 166 // Wait for the offer to be pasted 167 offer := webrtc.SessionDescription{} 168 decode(readUntilNewline(), &offer) 169 170 // Set the remote SessionDescription 171 err = peerConnection.SetRemoteDescription(offer) 172 if err != nil { 173 panic(err) 174 } 175 176 // Create answer 177 answer, err := peerConnection.CreateAnswer(nil) 178 if err != nil { 179 panic(err) 180 } 181 182 // Create channel that is blocked until ICE Gathering is complete 183 gatherComplete := webrtc.GatheringCompletePromise(peerConnection) 184 185 // Sets the LocalDescription, and starts our UDP listeners 186 err = peerConnection.SetLocalDescription(answer) 187 if err != nil { 188 panic(err) 189 } 190 191 // Block until ICE Gathering is complete, disabling trickle ICE 192 // we do this because we only can exchange one signaling message 193 // in a production application you should exchange ICE Candidates via OnICECandidate 194 <-gatherComplete 195 196 // Output the answer in base64 so we can paste it in browser 197 fmt.Println(encode(peerConnection.LocalDescription())) 198 199 // Block forever 200 select {} 201 } 202 203 // Read from stdin until we get a newline 204 func readUntilNewline() (in string) { 205 var err error 206 207 r := bufio.NewReader(os.Stdin) 208 for { 209 in, err = r.ReadString('\n') 210 if err != nil && !errors.Is(err, io.EOF) { 211 panic(err) 212 } 213 214 if in = strings.TrimSpace(in); len(in) > 0 { 215 break 216 } 217 } 218 219 fmt.Println("") 220 return 221 } 222 223 // JSON encode + base64 a SessionDescription 224 func encode(obj *webrtc.SessionDescription) string { 225 b, err := json.Marshal(obj) 226 if err != nil { 227 panic(err) 228 } 229 230 return base64.StdEncoding.EncodeToString(b) 231 } 232 233 // Decode a base64 and unmarshal JSON into a SessionDescription 234 func decode(in string, obj *webrtc.SessionDescription) { 235 b, err := base64.StdEncoding.DecodeString(in) 236 if err != nil { 237 panic(err) 238 } 239 240 if err = json.Unmarshal(b, obj); err != nil { 241 panic(err) 242 } 243 }