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
|
### Overview
|
||||||
#### Media API
|
#### 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.
|
* [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.
|
* [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.
|
* [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.
|
* [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.",
|
"description": "The play-from-disk example demonstrates how to send video to your browser from a file saved to disk.",
|
||||||
"type": "browser"
|
"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",
|
"title": "Save to Disk",
|
||||||
"link": "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