diff --git a/examples/README.md b/examples/README.md
index 7adf6633..d869c1f8 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -9,7 +9,9 @@ For more full featured examples that use 3rd party libraries see our **[example-
### Overview
#### Media API
* [Reflect](reflect): The reflect example demonstrates how to have Pion send back to the user exactly what it receives using the same PeerConnection.
-* [Play from disk](play-from-disk): The play-from-disk example demonstrates how to send video to your browser from a file saved to disk.
+* [Play from Disk](play-from-disk): The play-from-disk example demonstrates how to send video to your browser from a file saved to disk.
+* [Play from Disk Renegotation](play-from-disk-renegotation): The play-from-disk-renegotation example is an extension of the play-from-disk example, but demonstrates how you can add/remove video tracks from an already negotiated PeerConnection.
+* [Insertable Streams](insertable-streams): The insertable-streams example demonstrates how Pion can be used to send E2E encrypted video and decrypt via insertable streams in the browser.
* [Save to Disk](save-to-disk): The save-to-disk example shows how to record your webcam and save the footage to disk on the server side.
* [Broadcast](broadcast): The broadcast example demonstrates how to broadcast a video to multiple peers. A broadcaster uploads the video once and the server forwards it to all other peers.
* [RTP Forwarder](rtp-forwarder): The rtp-forwarder example demonstrates how to forward your audio/video streams using RTP.
diff --git a/examples/examples.json b/examples/examples.json
index 896cb911..79e9223f 100644
--- a/examples/examples.json
+++ b/examples/examples.json
@@ -41,6 +41,18 @@
"description": "The play-from-disk example demonstrates how to send video to your browser from a file saved to disk.",
"type": "browser"
},
+ {
+ "title": "Play from Disk Renegotation",
+ "link": "play-from-disk",
+ "description": "The play-from-disk-renegotation example is an extension of the play-from-disk example, but demonstrates how you can add/remove video tracks from an already negotiated PeerConnection.",
+ "type": "browser"
+ },
+ {
+ "title": "Insertable Streams",
+ "link": "insertable-streams",
+ "description": "The insertable-streams example demonstrates how Pion can be used to send E2E encrypted video and decrypt via insertable streams in the browser.",
+ "type": "browser"
+ },
{
"title": "Save to Disk",
"link": "save-to-disk",
diff --git a/examples/insertable-streams/README.md b/examples/insertable-streams/README.md
new file mode 100644
index 00000000..ac54b574
--- /dev/null
+++ b/examples/insertable-streams/README.md
@@ -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 encyption, 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 get github.com/pion/webrtc/v2/examples/insertable-streams
+```
+
+### Open insertable-streams example page
+[jsfiddle.net](https://jsfiddle.net/z7ms3u5r/) 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
diff --git a/examples/insertable-streams/jsfiddle/demo.css b/examples/insertable-streams/jsfiddle/demo.css
new file mode 100644
index 00000000..9e43d340
--- /dev/null
+++ b/examples/insertable-streams/jsfiddle/demo.css
@@ -0,0 +1,4 @@
+textarea {
+ width: 500px;
+ min-height: 75px;
+}
\ No newline at end of file
diff --git a/examples/insertable-streams/jsfiddle/demo.details b/examples/insertable-streams/jsfiddle/demo.details
new file mode 100644
index 00000000..0d23cfb1
--- /dev/null
+++ b/examples/insertable-streams/jsfiddle/demo.details
@@ -0,0 +1,5 @@
+---
+ 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
diff --git a/examples/insertable-streams/jsfiddle/demo.html b/examples/insertable-streams/jsfiddle/demo.html
new file mode 100644
index 00000000..f8f9001d
--- /dev/null
+++ b/examples/insertable-streams/jsfiddle/demo.html
@@ -0,0 +1,18 @@
+
+
Browser does not support insertable streams
+
+
+Browser base64 Session Description
+
+
+Golang base64 Session Description
+
+ Decrypt Video
+
+
+
+Video
+
+
+Logs
+
diff --git a/examples/insertable-streams/jsfiddle/demo.js b/examples/insertable-streams/jsfiddle/demo.js
new file mode 100644
index 00000000..953d9fb6
--- /dev/null
+++ b/examples/insertable-streams/jsfiddle/demo.js
@@ -0,0 +1,95 @@
+/* eslint-env browser */
+
+// cipherKey that video is encrypted with
+const cipherKey = 0xAA
+
+let pc = new RTCPeerConnection({encodedInsertableStreams: true, forceEncodedVideoInsertableStreams: true})
+let log = msg => {
+ document.getElementById('div').innerHTML += msg + ' '
+}
+
+// Offer to receive 1 video
+let transceiver = pc.addTransceiver('video')
+
+// The API has seen two iterations, support both
+// In the future this will just be `createEncodedStreams`
+let 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.readableStream.getReader()
+const writer = receiverStreams.writableStream.getWriter()
+reader.read().then(function processVideo({ done, value }) {
+ let 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 = () => {
+ let sd = document.getElementById('remoteSessionDescription').value
+ if (sd === '') {
+ return alert('Session Description must not be empty')
+ }
+
+ try {
+ pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(atob(sd))))
+ } catch (e) {
+ alert(e)
+ }
+}
+
+// DOM code to show banner if insertable streams not supported
+let insertableStreamsSupported = true
+let updateSupportBanner = () => {
+ let 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 'Insertable Streams are not supported'
+ }
+
+ return insertableStreams
+}
diff --git a/examples/insertable-streams/main.go b/examples/insertable-streams/main.go
new file mode 100644
index 00000000..2fcd51da
--- /dev/null
+++ b/examples/insertable-streams/main.go
@@ -0,0 +1,139 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "math/rand"
+ "os"
+ "time"
+
+ "github.com/pion/webrtc/v2"
+ "github.com/pion/webrtc/v2/examples/internal/signal"
+ "github.com/pion/webrtc/v2/pkg/media"
+ "github.com/pion/webrtc/v2/pkg/media/ivfreader"
+)
+
+const cipherKey = 0xAA
+
+func main() {
+ // Wait for the offer to be pasted
+ offer := webrtc.SessionDescription{}
+ signal.Decode(signal.MustReadStdin(), &offer)
+
+ // We make our own mediaEngine so we can place the sender's codecs in it. This because we must use the
+ // dynamic media type from the sender in our answer. This is not required if we are the offerer
+ mediaEngine := webrtc.MediaEngine{}
+ err := mediaEngine.PopulateFromSDP(offer)
+ if err != nil {
+ panic(err)
+ }
+
+ // Search for VP8 Payload type. If the offer doesn't support VP8 exit since
+ // since they won't be able to decode anything we send them
+ var payloadType uint8
+ for _, videoCodec := range mediaEngine.GetCodecsByKind(webrtc.RTPCodecTypeVideo) {
+ if videoCodec.Name == "VP8" {
+ payloadType = videoCodec.PayloadType
+ break
+ }
+ }
+ if payloadType == 0 {
+ panic("Remote peer does not support VP8")
+ }
+
+ // Create a new RTCPeerConnection
+ api := webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine))
+ peerConnection, err := api.NewPeerConnection(webrtc.Configuration{
+ ICEServers: []webrtc.ICEServer{
+ {
+ URLs: []string{"stun:stun.l.google.com:19302"},
+ },
+ },
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ // Create a video track
+ videoTrack, err := peerConnection.NewTrack(payloadType, rand.Uint32(), "video", "pion")
+ if err != nil {
+ panic(err)
+ }
+ if _, err = peerConnection.AddTrack(videoTrack); err != nil {
+ panic(err)
+ }
+
+ 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.
+ sleepTime := time.Millisecond * time.Duration((float32(header.TimebaseNumerator)/float32(header.TimebaseDenominator))*1000)
+ for {
+ frame, _, ivfErr := ivf.ParseNextFrame()
+ if 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
+ }
+
+ time.Sleep(sleepTime)
+ if ivfErr = videoTrack.WriteSample(media.Sample{Data: frame, Samples: 90000}); 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 remote SessionDescription
+ if err = peerConnection.SetRemoteDescription(offer); err != nil {
+ panic(err)
+ }
+
+ // Create answer
+ answer, err := peerConnection.CreateAnswer(nil)
+ if err != nil {
+ panic(err)
+ }
+
+ // Sets the LocalDescription, and starts our UDP listeners
+ if err = peerConnection.SetLocalDescription(answer); err != nil {
+ panic(err)
+ }
+
+ // Output the answer in base64 so we can paste it in browser
+ fmt.Println(signal.Encode(answer))
+
+ // Block forever
+ select {}
+}