Add examples/play-from-disk

Using IVFReader demonstrate how users can stream a video
from hard disk to browser.

Relates to #636
This commit is contained in:
Sean DuBois
2019-08-07 23:16:14 -07:00
committed by Sean DuBois
parent d6b9303410
commit 0d585106c0
13 changed files with 264 additions and 51 deletions

View File

@@ -8,7 +8,8 @@ For more full featured examples that use 3rd party libraries see our **[example-
### Overview ### Overview
#### Media API #### Media API
* [Echo](echo): The echo example demonstrates how to have Pion send back to the user exactly what it receives using the same PeerConnection . * [Echo](echo): The echo 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.
* [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.
* [SFU Minimal](sfu-minimal): The SFU 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. * [SFU Minimal](sfu-minimal): The SFU 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.

View File

@@ -30,9 +30,9 @@ func main() {
panic(err) panic(err)
} }
preferredCodec, err := mediaEngine.FirstCodecOfKind(webrtc.RTPCodecTypeVideo) videoCodecs := mediaEngine.GetCodecsByKind(webrtc.RTPCodecTypeVideo)
if err != nil { if len(videoCodecs) == 0 {
panic("no video codec in offer sdp") panic("Offer contained no video codecs")
} }
api := webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine)) api := webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine))
@@ -57,7 +57,7 @@ func main() {
} }
// Create Track that we send video back to browser on // Create Track that we send video back to browser on
outputTrack, err := peerConnection.NewTrack(preferredCodec.PayloadType, rand.Uint32(), "video", "pion") outputTrack, err := peerConnection.NewTrack(videoCodecs[0].PayloadType, rand.Uint32(), "video", "pion")
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@@ -35,6 +35,12 @@
"description": "Example pion-to-pion is an example of two pion instances communicating directly! It therefore has no corresponding web page.", "description": "Example pion-to-pion is an example of two pion instances communicating directly! It therefore has no corresponding web page.",
"type": "browser" "type": "browser"
}, },
{
"title": "Play from Disk",
"link": "play-from-disk",
"description": "The play-from-disk example demonstrates how to send video to your browser from a file saved to disk.",
"type": "browser"
},
{ {
"title": "Save to Disk", "title": "Save to Disk",
"link": "save-to-disk", "link": "save-to-disk",

View File

@@ -0,0 +1,33 @@
# play-from-disk
play-from-disk demonstrates how to send video to your browser from a file saved to disk.
## Instructions
### Create IVF named `output.ivf` that contains a VP8 track
```
ffmpeg -i $INPUT_FILE -g 30 output.ivf
```
### Download play-from-disk
```
go get github.com/pion/webrtc/examples/play-from-disk
```
### Open play-from-disk example page
[jsfiddle.net](https://jsfiddle.net/z7ms3u5r/) you should see two text-areas and a 'Start Session' button
### Run play-from-disk with your browsers SessionDescription as stdin
The `output.ivf` you created should be in the same directory as `play-from-disk`. In the jsfiddle the top textarea is your browser, copy that and:
#### Linux/macOS
Run `echo $BROWSER_SDP | play-from-disk`
#### Windows
1. Paste the SessionDescription into a file.
1. Run `play-from-disk < my_file`
### Input play-from-disk's SessionDescription into your browser
Copy the text that `play-from-disk` 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. `play-from-disk` will exit when the file reaches the end
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,14 @@
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><br />
<br />
Video<br />
<div id="remoteVideos"></div> <br />
Logs<br />
<div id="div"></div>

View File

@@ -0,0 +1,39 @@
/* eslint-env browser */
let pc = new RTCPeerConnection()
let log = msg => {
document.getElementById('div').innerHTML += msg + '<br>'
}
pc.ontrack = function (event) {
var el = document.createElement(event.track.kind)
el.srcObject = event.streams[0]
el.autoplay = true
el.controls = true
document.getElementById('remoteVideos').appendChild(el)
}
pc.oniceconnectionstatechange = e => log(pc.iceConnectionState)
pc.onicecandidate = event => {
if (event.candidate === null) {
document.getElementById('localSessionDescription').value = btoa(JSON.stringify(pc.localDescription))
}
}
// Offer to receive 1 audio, and 2 video tracks
pc.addTransceiver('video', {'direction': 'sendrecv'})
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)
}
}

View File

@@ -0,0 +1,119 @@
package main
import (
"fmt"
"math/rand"
"os"
"time"
"github.com/pion/webrtc/v2"
"github.com/pion/webrtc/v2/pkg/media"
"github.com/pion/webrtc/v2/pkg/media/ivfreader"
"github.com/pion/webrtc/v2/examples/internal/signal"
)
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)
}
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)
}
// 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 != nil {
panic(ivfErr)
}
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())
})
// 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 {}
}

View File

@@ -88,15 +88,6 @@ func (m *MediaEngine) PopulateFromSDP(sd SessionDescription) error {
return nil return nil
} }
// FirstCodecOfKind returns the first codec of a chosen kind in the codecs list
func (m *MediaEngine) FirstCodecOfKind(kind RTPCodecType) (*RTPCodec, error) {
foundCodecs := m.getCodecsByKind(kind)
if len(foundCodecs) == 0 {
return nil, fmt.Errorf("none found")
}
return foundCodecs[0], nil
}
func (m *MediaEngine) getCodec(payloadType uint8) (*RTPCodec, error) { func (m *MediaEngine) getCodec(payloadType uint8) (*RTPCodec, error) {
for _, codec := range m.codecs { for _, codec := range m.codecs {
if codec.PayloadType == payloadType { if codec.PayloadType == payloadType {
@@ -119,7 +110,8 @@ func (m *MediaEngine) getCodecSDP(sdpCodec sdp.Codec) (*RTPCodec, error) {
return nil, ErrCodecNotFound return nil, ErrCodecNotFound
} }
func (m *MediaEngine) getCodecsByKind(kind RTPCodecType) []*RTPCodec { // GetCodecsByKind returns all codecs of a chosen kind in the codecs list
func (m *MediaEngine) GetCodecsByKind(kind RTPCodecType) []*RTPCodec {
var codecs []*RTPCodec var codecs []*RTPCodec
for _, codec := range m.codecs { for _, codec := range m.codecs {
if codec.Type == kind { if codec.Type == kind {

View File

@@ -1364,7 +1364,7 @@ func (pc *PeerConnection) AddTransceiverFromKind(kind RTPCodecType, init ...RtpT
return nil, err return nil, err
} }
codecs := pc.api.mediaEngine.getCodecsByKind(kind) codecs := pc.api.mediaEngine.GetCodecsByKind(kind)
if len(codecs) == 0 { if len(codecs) == 0 {
return nil, fmt.Errorf("no %s codecs found", kind.String()) return nil, fmt.Errorf("no %s codecs found", kind.String())
} }
@@ -1662,7 +1662,7 @@ func (pc *PeerConnection) addTransceiverSDP(d *sdp.SessionDescription, midValue
WithPropertyAttribute(sdp.AttrKeyRTCPMux). WithPropertyAttribute(sdp.AttrKeyRTCPMux).
WithPropertyAttribute(sdp.AttrKeyRTCPRsize) WithPropertyAttribute(sdp.AttrKeyRTCPRsize)
codecs := pc.api.mediaEngine.getCodecsByKind(t.kind) codecs := pc.api.mediaEngine.GetCodecsByKind(t.kind)
for _, codec := range codecs { for _, codec := range codecs {
media.WithCodec(codec.PayloadType, codec.Name, codec.ClockRate, codec.Channels, codec.SDPFmtpLine) media.WithCodec(codec.PayloadType, codec.Name, codec.ClockRate, codec.Channels, codec.SDPFmtpLine)

View File

@@ -15,23 +15,23 @@ const (
// IVFFileHeader 32-byte header for IVF files // IVFFileHeader 32-byte header for IVF files
// https://wiki.multimedia.cx/index.php/IVF // https://wiki.multimedia.cx/index.php/IVF
type IVFFileHeader struct { type IVFFileHeader struct {
signature string // 0-3 signature string // 0-3
version uint16 // 4-5 version uint16 // 4-5
headerSize uint16 // 6-7 headerSize uint16 // 6-7
fourcc string // 8-11 FourCC string // 8-11
width uint16 // 12-13 Width uint16 // 12-13
height uint16 // 14-15 Height uint16 // 14-15
timebaseDenum uint32 // 16-19 TimebaseDenominator uint32 // 16-19
timebaseNum uint32 // 20-23 TimebaseNumerator uint32 // 20-23
numFrames uint32 // 24-27 NumFrames uint32 // 24-27
unused uint32 // 28-31 unused uint32 // 28-31
} }
// IVFFrameHeader 12-byte header for IVF frames // IVFFrameHeader 12-byte header for IVF frames
// https://wiki.multimedia.cx/index.php/IVF // https://wiki.multimedia.cx/index.php/IVF
type IVFFrameHeader struct { type IVFFrameHeader struct {
frameSize uint32 // 0-3 FrameSize uint32 // 0-3
timestamp uint64 // 4-11 Timestamp uint64 // 4-11
} }
// IVFReader is used to read IVF files and return frame payloads // IVFReader is used to read IVF files and return frame payloads
@@ -75,15 +75,15 @@ func (i *IVFReader) ParseNextFrame() ([]byte, *IVFFrameHeader, error) {
} }
header = &IVFFrameHeader{ header = &IVFFrameHeader{
frameSize: binary.LittleEndian.Uint32(buffer[:4]), FrameSize: binary.LittleEndian.Uint32(buffer[:4]),
timestamp: binary.LittleEndian.Uint64(buffer[4:12]), Timestamp: binary.LittleEndian.Uint64(buffer[4:12]),
} }
payload := make([]byte, header.frameSize) payload := make([]byte, header.FrameSize)
bytesRead, err = i.stream.Read(payload) bytesRead, err = i.stream.Read(payload)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} else if bytesRead != int(header.frameSize) { } else if bytesRead != int(header.FrameSize) {
return nil, nil, fmt.Errorf("incomplete frame data") return nil, nil, fmt.Errorf("incomplete frame data")
} }
return payload, header, nil return payload, header, nil
@@ -102,16 +102,16 @@ func (i *IVFReader) parseFileHeader() (*IVFFileHeader, error) {
} }
header := &IVFFileHeader{ header := &IVFFileHeader{
signature: string(buffer[:4]), signature: string(buffer[:4]),
version: binary.LittleEndian.Uint16(buffer[4:6]), version: binary.LittleEndian.Uint16(buffer[4:6]),
headerSize: binary.LittleEndian.Uint16(buffer[6:8]), headerSize: binary.LittleEndian.Uint16(buffer[6:8]),
fourcc: string(buffer[8:12]), FourCC: string(buffer[8:12]),
width: binary.LittleEndian.Uint16(buffer[12:14]), Width: binary.LittleEndian.Uint16(buffer[12:14]),
height: binary.LittleEndian.Uint16(buffer[14:16]), Height: binary.LittleEndian.Uint16(buffer[14:16]),
timebaseDenum: binary.LittleEndian.Uint32(buffer[16:20]), TimebaseDenominator: binary.LittleEndian.Uint32(buffer[16:20]),
timebaseNum: binary.LittleEndian.Uint32(buffer[20:24]), TimebaseNumerator: binary.LittleEndian.Uint32(buffer[20:24]),
numFrames: binary.LittleEndian.Uint32(buffer[24:28]), NumFrames: binary.LittleEndian.Uint32(buffer[24:28]),
unused: binary.LittleEndian.Uint32(buffer[28:32]), unused: binary.LittleEndian.Uint32(buffer[28:32]),
} }
if header.signature != ivfFileHeaderSignature { if header.signature != ivfFileHeaderSignature {

View File

@@ -43,12 +43,12 @@ func TestIVFReader_ParseValidFileHeader(t *testing.T) {
assert.Equal("DKIF", header.signature, "signature is 'DKIF'") assert.Equal("DKIF", header.signature, "signature is 'DKIF'")
assert.Equal(uint16(0), header.version, "version should be 0") assert.Equal(uint16(0), header.version, "version should be 0")
assert.Equal("VP80", header.fourcc, "FourCC should be 'VP80'") assert.Equal("VP80", header.FourCC, "FourCC should be 'VP80'")
assert.Equal(uint16(176), header.width, "width should be 176") assert.Equal(uint16(176), header.Width, "width should be 176")
assert.Equal(uint16(144), header.height, "height should be 144") assert.Equal(uint16(144), header.Height, "height should be 144")
assert.Equal(uint32(30000), header.timebaseDenum, "timebase denominator should be 30000") assert.Equal(uint32(30000), header.TimebaseDenominator, "timebase denominator should be 30000")
assert.Equal(uint32(1000), header.timebaseNum, "timebase numerator should be 1000") assert.Equal(uint32(1000), header.TimebaseNumerator, "timebase numerator should be 1000")
assert.Equal(uint32(29), header.numFrames, "number of frames should be 29") assert.Equal(uint32(29), header.NumFrames, "number of frames should be 29")
assert.Equal(uint32(0), header.unused, "bytes should be unused") assert.Equal(uint32(0), header.unused, "bytes should be unused")
} }
@@ -81,7 +81,7 @@ func TestIVFReader_ParseValidFrames(t *testing.T) {
payload, header, err := reader.ParseNextFrame() payload, header, err := reader.ParseNextFrame()
assert.Nil(err, "Should have parsed frame #1 without error") assert.Nil(err, "Should have parsed frame #1 without error")
assert.Equal(uint32(4), header.frameSize, "Frame header frameSize should be 4") assert.Equal(uint32(4), header.FrameSize, "Frame header frameSize should be 4")
assert.Equal(4, len(payload), "Payload should be length 4") assert.Equal(4, len(payload), "Payload should be length 4")
assert.Equal( assert.Equal(
payload, payload,
@@ -94,7 +94,7 @@ func TestIVFReader_ParseValidFrames(t *testing.T) {
payload, header, err = reader.ParseNextFrame() payload, header, err = reader.ParseNextFrame()
assert.Nil(err, "Should have parsed frame #2 without error") assert.Nil(err, "Should have parsed frame #2 without error")
assert.Equal(uint32(12), header.frameSize, "Frame header frameSize should be 4") assert.Equal(uint32(12), header.FrameSize, "Frame header frameSize should be 4")
assert.Equal(12, len(payload), "Payload should be length 12") assert.Equal(12, len(payload), "Payload should be length 12")
assert.Equal( assert.Equal(
payload, payload,