Example of using Insertable Streams with Pion

Use simple XOR Cipher on both sides. In the Web UI
you can flip a checkbox and watch decoding fail when
decryption is disabled.
This commit is contained in:
Sean DuBois
2020-06-12 00:09:13 -07:00
committed by Sean DuBois
parent 1e68320bab
commit 7041e196bc
8 changed files with 317 additions and 1 deletions

View File

@@ -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.

View File

@@ -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",

View 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 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

View File

@@ -0,0 +1,4 @@
textarea {
width: 500px;
min-height: 75px;
}

View File

@@ -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

View File

@@ -0,0 +1,18 @@
<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>

View File

@@ -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 + '<br>'
}
// 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
}

View File

@@ -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 {}
}