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
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:
41
examples/insertable-streams/README.md
Normal file
41
examples/insertable-streams/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# insertable-streams
|
||||
insertable-streams demonstrates how to use insertable streams with Pion.
|
||||
This example modifies the video with a single-byte XOR cipher before sending, and then
|
||||
decrypts in Javascript.
|
||||
|
||||
insertable-streams allows the browser to process encoded video. You could implement
|
||||
E2E encryption, add metadata or insert a completely different video feed!
|
||||
|
||||
## Instructions
|
||||
### Create IVF named `output.ivf` that contains a VP8 track
|
||||
```
|
||||
ffmpeg -i $INPUT_FILE -g 30 output.ivf
|
||||
```
|
||||
|
||||
### Download insertable-streams
|
||||
```
|
||||
go install github.com/pion/webrtc/v4/examples/insertable-streams@latest
|
||||
```
|
||||
|
||||
### Open insertable-streams example page
|
||||
[jsfiddle.net](https://jsfiddle.net/t5xoaryc/) you should see two text-areas and a 'Start Session' button. You will also have a 'Decrypt' checkbox.
|
||||
When unchecked the browser will not decrypt the incoming video stream, so it will stop playing or display certificates.
|
||||
|
||||
### Run insertable-streams with your browsers SessionDescription as stdin
|
||||
The `output.ivf` you created should be in the same directory as `insertable-streams`. In the jsfiddle the top textarea is your browser, copy that and:
|
||||
|
||||
#### Linux/macOS
|
||||
Run `echo $BROWSER_SDP | insertable-streams`
|
||||
#### Windows
|
||||
1. Paste the SessionDescription into a file.
|
||||
1. Run `insertable-streams < my_file`
|
||||
|
||||
### Input insertable-streams's SessionDescription into your browser
|
||||
Copy the text that `insertable-streams` just emitted and copy into second text area
|
||||
|
||||
### Hit 'Start Session' in jsfiddle, enjoy your video!
|
||||
A video should start playing in your browser above the input boxes. `insertable-streams` will exit when the file reaches the end.
|
||||
|
||||
To stop decrypting the stream uncheck the box and the video will not be viewable.
|
||||
|
||||
Congrats, you have used Pion WebRTC! Now start building something cool
|
||||
8
examples/insertable-streams/jsfiddle/demo.css
Normal file
8
examples/insertable-streams/jsfiddle/demo.css
Normal file
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
|
||||
SPDX-License-Identifier: MIT
|
||||
*/
|
||||
textarea {
|
||||
width: 500px;
|
||||
min-height: 75px;
|
||||
}
|
||||
8
examples/insertable-streams/jsfiddle/demo.details
Normal file
8
examples/insertable-streams/jsfiddle/demo.details
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
# SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
name: play-from-disk
|
||||
description: play-from-disk demonstrates how to send video to your browser from a file saved to disk.
|
||||
authors:
|
||||
- Sean DuBois
|
||||
22
examples/insertable-streams/jsfiddle/demo.html
Normal file
22
examples/insertable-streams/jsfiddle/demo.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
|
||||
SPDX-License-Identifier: MIT
|
||||
-->
|
||||
<div id="no-support-banner" style="background-color: red">
|
||||
<h1> Browser does not support insertable streams </h1>
|
||||
</div>
|
||||
|
||||
Browser base64 Session Description<br />
|
||||
<textarea id="localSessionDescription" readonly="true"></textarea> <br />
|
||||
|
||||
Golang base64 Session Description<br />
|
||||
<textarea id="remoteSessionDescription"> </textarea> <br/>
|
||||
<button onclick="window.startSession()"> Start Session </button> Decrypt Video <input type="checkbox" checked="checked" onclick="window.toggleDecryption()"/> <br />
|
||||
|
||||
<br />
|
||||
|
||||
Video<br />
|
||||
<video id="remote-video" playsinline autoplay controls style="width: 640; height: 480"></video> <br />
|
||||
|
||||
Logs<br />
|
||||
<div id="div"></div>
|
||||
98
examples/insertable-streams/jsfiddle/demo.js
Normal file
98
examples/insertable-streams/jsfiddle/demo.js
Normal file
@@ -0,0 +1,98 @@
|
||||
/* eslint-env browser */
|
||||
|
||||
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// cipherKey that video is encrypted with
|
||||
const cipherKey = 0xAA
|
||||
|
||||
const pc = new RTCPeerConnection({ encodedInsertableStreams: true, forceEncodedVideoInsertableStreams: true })
|
||||
const log = msg => {
|
||||
document.getElementById('div').innerHTML += msg + '<br>'
|
||||
}
|
||||
|
||||
// Offer to receive 1 video
|
||||
const transceiver = pc.addTransceiver('video')
|
||||
|
||||
// The API has seen two iterations, support both
|
||||
// In the future this will just be `createEncodedStreams`
|
||||
const receiverStreams = getInsertableStream(transceiver)
|
||||
|
||||
// boolean controlled by checkbox to enable/disable encryption
|
||||
let applyDecryption = true
|
||||
window.toggleDecryption = () => {
|
||||
applyDecryption = !applyDecryption
|
||||
}
|
||||
|
||||
// Loop that is called for each video frame
|
||||
const reader = receiverStreams.readable.getReader()
|
||||
const writer = receiverStreams.writable.getWriter()
|
||||
reader.read().then(function processVideo ({ done, value }) {
|
||||
const decrypted = new DataView(value.data)
|
||||
|
||||
if (applyDecryption) {
|
||||
for (let i = 0; i < decrypted.buffer.byteLength; i++) {
|
||||
decrypted.setInt8(i, decrypted.getInt8(i) ^ cipherKey)
|
||||
}
|
||||
}
|
||||
|
||||
value.data = decrypted.buffer
|
||||
writer.write(value)
|
||||
return reader.read().then(processVideo)
|
||||
})
|
||||
|
||||
// Fire when remote video arrives
|
||||
pc.ontrack = function (event) {
|
||||
document.getElementById('remote-video').srcObject = event.streams[0]
|
||||
document.getElementById('remote-video').style = ''
|
||||
}
|
||||
|
||||
// Populate SDP field when finished gathering
|
||||
pc.oniceconnectionstatechange = e => log(pc.iceConnectionState)
|
||||
pc.onicecandidate = event => {
|
||||
if (event.candidate === null) {
|
||||
document.getElementById('localSessionDescription').value = btoa(JSON.stringify(pc.localDescription))
|
||||
}
|
||||
}
|
||||
pc.createOffer().then(d => pc.setLocalDescription(d)).catch(log)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// DOM code to show banner if insertable streams not supported
|
||||
let insertableStreamsSupported = true
|
||||
const updateSupportBanner = () => {
|
||||
const el = document.getElementById('no-support-banner')
|
||||
if (insertableStreamsSupported && el) {
|
||||
el.style = 'display: none'
|
||||
}
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', updateSupportBanner)
|
||||
|
||||
// Shim to support both versions of API
|
||||
function getInsertableStream (transceiver) {
|
||||
let insertableStreams = null
|
||||
if (transceiver.receiver.createEncodedVideoStreams) {
|
||||
insertableStreams = transceiver.receiver.createEncodedVideoStreams()
|
||||
} else if (transceiver.receiver.createEncodedStreams) {
|
||||
insertableStreams = transceiver.receiver.createEncodedStreams()
|
||||
}
|
||||
|
||||
if (!insertableStreams) {
|
||||
insertableStreamsSupported = false
|
||||
updateSupportBanner()
|
||||
throw new Error('Insertable Streams are not supported')
|
||||
}
|
||||
|
||||
return insertableStreams
|
||||
}
|
||||
224
examples/insertable-streams/main.go
Normal file
224
examples/insertable-streams/main.go
Normal file
@@ -0,0 +1,224 @@
|
||||
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !js
|
||||
|
||||
// insertable-streams demonstrates how to use insertable streams with Pion
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
"github.com/pion/webrtc/v4/pkg/media"
|
||||
"github.com/pion/webrtc/v4/pkg/media/ivfreader"
|
||||
)
|
||||
|
||||
const cipherKey = 0xAA
|
||||
|
||||
// nolint:gocognit, cyclop
|
||||
func main() {
|
||||
peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{
|
||||
ICEServers: []webrtc.ICEServer{
|
||||
{
|
||||
URLs: []string{"stun:stun.l.google.com:19302"},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer func() {
|
||||
if cErr := peerConnection.Close(); cErr != nil {
|
||||
fmt.Printf("cannot close peerConnection: %v\n", cErr)
|
||||
}
|
||||
}()
|
||||
|
||||
// Create a video track
|
||||
videoTrack, err := webrtc.NewTrackLocalStaticSample(
|
||||
webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8}, "video", "pion",
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
rtpSender, err := peerConnection.AddTrack(videoTrack)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Read incoming RTCP packets
|
||||
// Before these packets are returned they are processed by interceptors. For things
|
||||
// like NACK this needs to be called.
|
||||
go func() {
|
||||
rtcpBuf := make([]byte, 1500)
|
||||
for {
|
||||
if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
iceConnectedCtx, iceConnectedCtxCancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
// Open a IVF file and start reading using our IVFReader
|
||||
file, ivfErr := os.Open("output.ivf")
|
||||
if ivfErr != nil {
|
||||
panic(ivfErr)
|
||||
}
|
||||
|
||||
ivf, header, ivfErr := ivfreader.NewWith(file)
|
||||
if ivfErr != nil {
|
||||
panic(ivfErr)
|
||||
}
|
||||
|
||||
// Wait for connection established
|
||||
<-iceConnectedCtx.Done()
|
||||
|
||||
// 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.
|
||||
// This isn't required since the video is timestamped, but we will such much higher loss if we send all at once.
|
||||
//
|
||||
// It is important to use a time.Ticker instead of time.Sleep because
|
||||
// * avoids accumulating skew, just calling time.Sleep didn't compensate for the time spent parsing the data
|
||||
// * works around latency issues with Sleep (see https://github.com/golang/go/issues/44343)
|
||||
ticker := time.NewTicker(
|
||||
time.Millisecond * time.Duration((float32(header.TimebaseNumerator)/float32(header.TimebaseDenominator))*1000),
|
||||
)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
frame, _, ivfErr := ivf.ParseNextFrame()
|
||||
if errors.Is(ivfErr, io.EOF) {
|
||||
fmt.Printf("All frames parsed and sent")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if ivfErr != nil {
|
||||
panic(ivfErr)
|
||||
}
|
||||
|
||||
// Encrypt video using XOR Cipher
|
||||
for i := range frame {
|
||||
frame[i] ^= cipherKey
|
||||
}
|
||||
|
||||
if ivfErr = videoTrack.WriteSample(media.Sample{Data: frame, Duration: time.Second}); ivfErr != nil {
|
||||
panic(ivfErr)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// 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 {
|
||||
iceConnectedCtxCancel()
|
||||
}
|
||||
})
|
||||
|
||||
// Set the handler for Peer connection state
|
||||
// This will notify you when the peer has connected/disconnected
|
||||
peerConnection.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
|
||||
fmt.Printf("Peer Connection State has changed: %s\n", state.String())
|
||||
|
||||
if state == webrtc.PeerConnectionStateFailed {
|
||||
// Wait until PeerConnection has had no network activity for 30 seconds or another failure.
|
||||
// It may be reconnected using an ICE Restart.
|
||||
// Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout.
|
||||
// Note that the PeerConnection may come back from PeerConnectionStateDisconnected.
|
||||
fmt.Println("Peer Connection has gone to failed exiting")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if state == webrtc.PeerConnectionStateClosed {
|
||||
// PeerConnection was explicitly closed. This usually happens from a DTLS CloseNotify
|
||||
fmt.Println("Peer Connection has gone to closed exiting")
|
||||
os.Exit(0)
|
||||
}
|
||||
})
|
||||
|
||||
// Wait for the offer to be pasted
|
||||
offer := webrtc.SessionDescription{}
|
||||
decode(readUntilNewline(), &offer)
|
||||
|
||||
// Set the remote SessionDescription
|
||||
if err = peerConnection.SetRemoteDescription(offer); 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
|
||||
if err = peerConnection.SetLocalDescription(answer); 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user