snapshot of https://github.com/pion/webrtc.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
REUSE Compliance Check / lint (push) Failing after 0s
Browser E2E / Test (push) Failing after 3s
Test / test (1.24) (push) Failing after 0s
Test / test-i386 (1.25) (push) Failing after 0s
Test / test-i386 (1.24) (push) Failing after 0s
Test / test-windows (1.25) (push) Failing after 0s
Test / test-windows (1.24) (push) Failing after 0s
Test / test (1.25) (push) Failing after 0s
Examples Tests / Test (push) Failing after 3s
Test / test-macos (1.25) (push) Failing after 0s
Test / test-macos (1.24) (push) Failing after 0s
Go mod tidy / tidy (push) Failing after 0s
Test / test-wasm (push) Failing after 0s
CodeQL / analyze (push) Failing after 1s
Fuzz / fuzz (push) Failing after 0s

This commit is contained in:
upstream-mirror
2026-09-15 20:06:07 +05:00
commit 8acb1a3a29
394 changed files with 78746 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
# save-to-disk
save-to-disk is a simple application that shows how to record your webcam/microphone using Pion WebRTC and save VP8/Opus to disk.
If you wish to save VP9 instead of VP8 you can just replace all occurences of VP8 with VP9 in [main.go](https://github.com/pion/example-webrtc-applications/tree/main/save-to-disk/main.go).
If you wish to save VP8/Opus inside the same file see [save-to-webm](https://github.com/pion/example-webrtc-applications/tree/main/save-to-webm)
If you wish to save AV1 instead see [save-to-disk-av1](https://github.com/pion/webrtc/tree/main/examples/save-to-disk-av1)
You can then send this video back to your browser using [play-from-disk](https://github.com/pion/webrtc/tree/main/examples/play-from-disk)
## Instructions
### Download save-to-disk
```
go install github.com/pion/webrtc/v4/examples/save-to-disk@latest
```
### Open save-to-disk example page
[jsfiddle.net](https://jsfiddle.net/2nwt1vjq/) you should see your Webcam, two text-areas and two buttons: `Copy browser SDP to clipboard`, `Start Session`.
### Run save-to-disk, with your browsers SessionDescription as stdin
In the jsfiddle the top textarea is your browser's Session Description. Press `Copy browser SDP to clipboard` or copy the base64 string manually.
We will use this value in the next step.
#### Linux/macOS
Run `echo $BROWSER_SDP | save-to-disk`
#### Windows
1. Paste the SessionDescription into a file.
1. Run `save-to-disk < my_file`
### Input save-to-disk's SessionDescription into your browser
Copy the text that `save-to-disk` just emitted and copy into second text area
### Hit 'Start Session' in jsfiddle, wait, close jsfiddle, enjoy your video!
In the folder you ran `save-to-disk` you should now have a file `output-1.ivf` play with your video player of choice!
> Note: In order to correctly create the files, the remote client (JSFiddle) should be closed. The Go example will automatically close itself.
Congrats, you have used Pion WebRTC! Now start building something cool

View File

@@ -0,0 +1,8 @@
/*
SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
SPDX-License-Identifier: MIT
*/
textarea {
width: 500px;
min-height: 75px;
}

View File

@@ -0,0 +1,8 @@
---
# SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
# SPDX-License-Identifier: MIT
name: save-to-disk
description: Example of using Pion WebRTC to save video to disk in an IVF container
authors:
- Sean DuBois

View File

@@ -0,0 +1,23 @@
<!--
SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
SPDX-License-Identifier: MIT
-->
Browser base64 Session Description<br />
<textarea id="localSessionDescription" readonly="true"></textarea> <br />
<button onclick="window.copySDP()">
Copy browser SDP to clipboard
</button>
<br />
<br />
Golang base64 Session Description<br />
<textarea id="remoteSessionDescription"></textarea> <br/>
<button onclick="window.startSession()"> Start Session </button><br />
<br />
Video<br />
<video id="video1" width="160" height="120" autoplay muted></video> <br />
Logs<br />
<div id="logs"></div>

View File

@@ -0,0 +1,56 @@
/* eslint-env browser */
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
const pc = new RTCPeerConnection({
iceServers: [
{
urls: 'stun:stun.l.google.com:19302'
}
]
})
const log = msg => {
document.getElementById('logs').innerHTML += msg + '<br>'
}
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(stream => {
document.getElementById('video1').srcObject = stream
stream.getTracks().forEach(track => pc.addTrack(track, stream))
pc.createOffer().then(d => pc.setLocalDescription(d)).catch(log)
}).catch(log)
pc.oniceconnectionstatechange = e => log(pc.iceConnectionState)
pc.onicecandidate = event => {
document.getElementById('localSessionDescription').value = btoa(JSON.stringify(pc.localDescription))
}
window.startSession = () => {
const sd = document.getElementById('remoteSessionDescription').value
if (sd === '') {
return alert('Session Description must not be empty')
}
try {
pc.setRemoteDescription(JSON.parse(atob(sd)))
} catch (e) {
alert(e)
}
}
window.copySDP = () => {
const browserSDP = document.getElementById('localSessionDescription')
browserSDP.focus()
browserSDP.select()
try {
const successful = document.execCommand('copy')
const msg = successful ? 'successful' : 'unsuccessful'
log('Copying SDP was ' + msg)
} catch (err) {
log('Unable to copy SDP ' + err)
}
}

View File

@@ -0,0 +1,250 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
//go:build !js
// save-to-disk is a simple application that shows how to record your webcam/microphone using
// Pion WebRTC and save VP8/Opus to disk.
package main
import (
"bufio"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"strings"
"github.com/pion/interceptor"
"github.com/pion/interceptor/pkg/intervalpli"
"github.com/pion/webrtc/v4"
"github.com/pion/webrtc/v4/pkg/media"
"github.com/pion/webrtc/v4/pkg/media/ivfwriter"
"github.com/pion/webrtc/v4/pkg/media/oggwriter"
)
func saveToDisk(writer media.Writer, track *webrtc.TrackRemote) {
defer func() {
if err := writer.Close(); err != nil {
panic(err)
}
}()
for {
rtpPacket, _, err := track.ReadRTP()
if err != nil {
fmt.Println(err)
return
}
if err := writer.WriteRTP(rtpPacket); err != nil {
fmt.Println(err)
return
}
}
}
// nolint:gocognit, cyclop
func main() {
// Everything below is the Pion WebRTC API! Thanks for using it ❤️.
// Create a MediaEngine object to configure the supported codec
mediaEngine := &webrtc.MediaEngine{}
// Setup the codecs you want to use.
// We'll use a VP8 and Opus but you can also define your own
if err := mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP8, ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil,
},
PayloadType: 96,
}, webrtc.RTPCodecTypeVideo); err != nil {
panic(err)
}
if err := mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeOpus, ClockRate: 48000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil,
},
PayloadType: 111,
}, webrtc.RTPCodecTypeAudio); err != nil {
panic(err)
}
// Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline.
// This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection`
// this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry
// for each PeerConnection.
interceptorRegistry := &interceptor.Registry{}
// Register a intervalpli factory
// This interceptor sends a PLI every 3 seconds. A PLI causes a video keyframe to be generated by the sender.
// This makes our video seekable and more error resilent, but at a cost of lower picture quality and higher bitrates
// A real world application should process incoming RTCP packets from viewers and forward them to senders
intervalPliFactory, err := intervalpli.NewReceiverInterceptor()
if err != nil {
panic(err)
}
interceptorRegistry.Add(intervalPliFactory)
// Use the default set of Interceptors
if err = webrtc.RegisterDefaultInterceptors(mediaEngine, interceptorRegistry); err != nil {
panic(err)
}
// Create the API object with the MediaEngine
api := webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine), webrtc.WithInterceptorRegistry(interceptorRegistry))
// Prepare the configuration
config := webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
{
URLs: []string{"stun:stun.l.google.com:19302"},
},
},
}
// Create a new RTCPeerConnection
peerConnection, err := api.NewPeerConnection(config)
if err != nil {
panic(err)
}
// Allow us to receive 1 audio track, and 1 video track
if _, err = peerConnection.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio); err != nil {
panic(err)
} else if _, err = peerConnection.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo); err != nil {
panic(err)
}
oggFile, err := oggwriter.New("output.ogg", 48000, 2)
if err != nil {
panic(err)
}
ivfFile, err := ivfwriter.New("output.ivf", ivfwriter.WithCodec("video/VP8"))
if err != nil {
panic(err)
}
// Set a handler for when a new remote track starts, this handler saves buffers to disk as
// an ivf file, since we could have multiple video tracks we provide a counter.
// In your application this is where you would handle/process video
peerConnection.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { //nolint: revive
codec := track.Codec()
if strings.EqualFold(codec.MimeType, webrtc.MimeTypeOpus) {
fmt.Println("Got Opus track, saving to disk as output.opus (48 kHz, 2 channels)")
saveToDisk(oggFile, track)
} else if strings.EqualFold(codec.MimeType, webrtc.MimeTypeVP8) {
fmt.Println("Got VP8 track, saving to disk as output.ivf")
saveToDisk(ivfFile, track)
}
})
// 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())
if connectionState == webrtc.ICEConnectionStateConnected {
fmt.Println("Ctrl+C the remote client to stop the demo")
} else if connectionState == webrtc.ICEConnectionStateFailed || connectionState == webrtc.ICEConnectionStateClosed {
if closeErr := oggFile.Close(); closeErr != nil {
panic(closeErr)
}
if closeErr := ivfFile.Close(); closeErr != nil {
panic(closeErr)
}
fmt.Println("Done writing media files")
// Gracefully shutdown the peer connection
if closeErr := peerConnection.Close(); closeErr != nil {
panic(closeErr)
}
os.Exit(0)
}
})
// Wait for the offer to be pasted
offer := webrtc.SessionDescription{}
decode(readUntilNewline(), &offer)
// Set the remote SessionDescription
err = peerConnection.SetRemoteDescription(offer)
if 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
err = peerConnection.SetLocalDescription(answer)
if 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()))
// Block forever
select {}
}
// 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)
}
}