mirror of
https://github.com/pion/webrtc.git
synced 2025-10-07 08:01:27 +08:00
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:
@@ -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.
|
||||
|
@@ -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",
|
||||
|
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 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
|
4
examples/insertable-streams/jsfiddle/demo.css
Normal file
4
examples/insertable-streams/jsfiddle/demo.css
Normal file
@@ -0,0 +1,4 @@
|
||||
textarea {
|
||||
width: 500px;
|
||||
min-height: 75px;
|
||||
}
|
5
examples/insertable-streams/jsfiddle/demo.details
Normal file
5
examples/insertable-streams/jsfiddle/demo.details
Normal 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
|
18
examples/insertable-streams/jsfiddle/demo.html
Normal file
18
examples/insertable-streams/jsfiddle/demo.html
Normal 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>
|
95
examples/insertable-streams/jsfiddle/demo.js
Normal file
95
examples/insertable-streams/jsfiddle/demo.js
Normal 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
|
||||
}
|
139
examples/insertable-streams/main.go
Normal file
139
examples/insertable-streams/main.go
Normal 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 {}
|
||||
}
|
Reference in New Issue
Block a user