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