mirror of
https://github.com/pion/webrtc.git
synced 2025-10-29 01:42:59 +08:00
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:
@@ -8,7 +8,8 @@ For more full featured examples that use 3rd party libraries see our **[example-
|
||||
|
||||
### Overview
|
||||
#### 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.
|
||||
* [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.
|
||||
|
||||
|
||||
@@ -30,9 +30,9 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
preferredCodec, err := mediaEngine.FirstCodecOfKind(webrtc.RTPCodecTypeVideo)
|
||||
if err != nil {
|
||||
panic("no video codec in offer sdp")
|
||||
videoCodecs := mediaEngine.GetCodecsByKind(webrtc.RTPCodecTypeVideo)
|
||||
if len(videoCodecs) == 0 {
|
||||
panic("Offer contained no video codecs")
|
||||
}
|
||||
|
||||
api := webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine))
|
||||
@@ -57,7 +57,7 @@ func main() {
|
||||
}
|
||||
|
||||
// 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 {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -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.",
|
||||
"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",
|
||||
"link": "save-to-disk",
|
||||
|
||||
33
examples/play-from-disk/README.md
Normal file
33
examples/play-from-disk/README.md
Normal 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
|
||||
4
examples/play-from-disk/jsfiddle/demo.css
Normal file
4
examples/play-from-disk/jsfiddle/demo.css
Normal file
@@ -0,0 +1,4 @@
|
||||
textarea {
|
||||
width: 500px;
|
||||
min-height: 75px;
|
||||
}
|
||||
5
examples/play-from-disk/jsfiddle/demo.details
Normal file
5
examples/play-from-disk/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
|
||||
14
examples/play-from-disk/jsfiddle/demo.html
Normal file
14
examples/play-from-disk/jsfiddle/demo.html
Normal 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>
|
||||
39
examples/play-from-disk/jsfiddle/demo.js
Normal file
39
examples/play-from-disk/jsfiddle/demo.js
Normal 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)
|
||||
}
|
||||
}
|
||||
119
examples/play-from-disk/main.go
Normal file
119
examples/play-from-disk/main.go
Normal 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 {}
|
||||
}
|
||||
@@ -88,15 +88,6 @@ func (m *MediaEngine) PopulateFromSDP(sd SessionDescription) error {
|
||||
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) {
|
||||
for _, codec := range m.codecs {
|
||||
if codec.PayloadType == payloadType {
|
||||
@@ -119,7 +110,8 @@ func (m *MediaEngine) getCodecSDP(sdpCodec sdp.Codec) (*RTPCodec, error) {
|
||||
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
|
||||
for _, codec := range m.codecs {
|
||||
if codec.Type == kind {
|
||||
|
||||
@@ -1364,7 +1364,7 @@ func (pc *PeerConnection) AddTransceiverFromKind(kind RTPCodecType, init ...RtpT
|
||||
return nil, err
|
||||
}
|
||||
|
||||
codecs := pc.api.mediaEngine.getCodecsByKind(kind)
|
||||
codecs := pc.api.mediaEngine.GetCodecsByKind(kind)
|
||||
if len(codecs) == 0 {
|
||||
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.AttrKeyRTCPRsize)
|
||||
|
||||
codecs := pc.api.mediaEngine.getCodecsByKind(t.kind)
|
||||
codecs := pc.api.mediaEngine.GetCodecsByKind(t.kind)
|
||||
for _, codec := range codecs {
|
||||
media.WithCodec(codec.PayloadType, codec.Name, codec.ClockRate, codec.Channels, codec.SDPFmtpLine)
|
||||
|
||||
|
||||
@@ -15,23 +15,23 @@ const (
|
||||
// IVFFileHeader 32-byte header for IVF files
|
||||
// https://wiki.multimedia.cx/index.php/IVF
|
||||
type IVFFileHeader struct {
|
||||
signature string // 0-3
|
||||
version uint16 // 4-5
|
||||
headerSize uint16 // 6-7
|
||||
fourcc string // 8-11
|
||||
width uint16 // 12-13
|
||||
height uint16 // 14-15
|
||||
timebaseDenum uint32 // 16-19
|
||||
timebaseNum uint32 // 20-23
|
||||
numFrames uint32 // 24-27
|
||||
unused uint32 // 28-31
|
||||
signature string // 0-3
|
||||
version uint16 // 4-5
|
||||
headerSize uint16 // 6-7
|
||||
FourCC string // 8-11
|
||||
Width uint16 // 12-13
|
||||
Height uint16 // 14-15
|
||||
TimebaseDenominator uint32 // 16-19
|
||||
TimebaseNumerator uint32 // 20-23
|
||||
NumFrames uint32 // 24-27
|
||||
unused uint32 // 28-31
|
||||
}
|
||||
|
||||
// IVFFrameHeader 12-byte header for IVF frames
|
||||
// https://wiki.multimedia.cx/index.php/IVF
|
||||
type IVFFrameHeader struct {
|
||||
frameSize uint32 // 0-3
|
||||
timestamp uint64 // 4-11
|
||||
FrameSize uint32 // 0-3
|
||||
Timestamp uint64 // 4-11
|
||||
}
|
||||
|
||||
// IVFReader is used to read IVF files and return frame payloads
|
||||
@@ -75,15 +75,15 @@ func (i *IVFReader) ParseNextFrame() ([]byte, *IVFFrameHeader, error) {
|
||||
}
|
||||
|
||||
header = &IVFFrameHeader{
|
||||
frameSize: binary.LittleEndian.Uint32(buffer[:4]),
|
||||
timestamp: binary.LittleEndian.Uint64(buffer[4:12]),
|
||||
FrameSize: binary.LittleEndian.Uint32(buffer[:4]),
|
||||
Timestamp: binary.LittleEndian.Uint64(buffer[4:12]),
|
||||
}
|
||||
|
||||
payload := make([]byte, header.frameSize)
|
||||
payload := make([]byte, header.FrameSize)
|
||||
bytesRead, err = i.stream.Read(payload)
|
||||
if err != nil {
|
||||
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 payload, header, nil
|
||||
@@ -102,16 +102,16 @@ func (i *IVFReader) parseFileHeader() (*IVFFileHeader, error) {
|
||||
}
|
||||
|
||||
header := &IVFFileHeader{
|
||||
signature: string(buffer[:4]),
|
||||
version: binary.LittleEndian.Uint16(buffer[4:6]),
|
||||
headerSize: binary.LittleEndian.Uint16(buffer[6:8]),
|
||||
fourcc: string(buffer[8:12]),
|
||||
width: binary.LittleEndian.Uint16(buffer[12:14]),
|
||||
height: binary.LittleEndian.Uint16(buffer[14:16]),
|
||||
timebaseDenum: binary.LittleEndian.Uint32(buffer[16:20]),
|
||||
timebaseNum: binary.LittleEndian.Uint32(buffer[20:24]),
|
||||
numFrames: binary.LittleEndian.Uint32(buffer[24:28]),
|
||||
unused: binary.LittleEndian.Uint32(buffer[28:32]),
|
||||
signature: string(buffer[:4]),
|
||||
version: binary.LittleEndian.Uint16(buffer[4:6]),
|
||||
headerSize: binary.LittleEndian.Uint16(buffer[6:8]),
|
||||
FourCC: string(buffer[8:12]),
|
||||
Width: binary.LittleEndian.Uint16(buffer[12:14]),
|
||||
Height: binary.LittleEndian.Uint16(buffer[14:16]),
|
||||
TimebaseDenominator: binary.LittleEndian.Uint32(buffer[16:20]),
|
||||
TimebaseNumerator: binary.LittleEndian.Uint32(buffer[20:24]),
|
||||
NumFrames: binary.LittleEndian.Uint32(buffer[24:28]),
|
||||
unused: binary.LittleEndian.Uint32(buffer[28:32]),
|
||||
}
|
||||
|
||||
if header.signature != ivfFileHeaderSignature {
|
||||
|
||||
@@ -43,12 +43,12 @@ func TestIVFReader_ParseValidFileHeader(t *testing.T) {
|
||||
|
||||
assert.Equal("DKIF", header.signature, "signature is 'DKIF'")
|
||||
assert.Equal(uint16(0), header.version, "version should be 0")
|
||||
assert.Equal("VP80", header.fourcc, "FourCC should be 'VP80'")
|
||||
assert.Equal(uint16(176), header.width, "width should be 176")
|
||||
assert.Equal(uint16(144), header.height, "height should be 144")
|
||||
assert.Equal(uint32(30000), header.timebaseDenum, "timebase denominator should be 30000")
|
||||
assert.Equal(uint32(1000), header.timebaseNum, "timebase numerator should be 1000")
|
||||
assert.Equal(uint32(29), header.numFrames, "number of frames should be 29")
|
||||
assert.Equal("VP80", header.FourCC, "FourCC should be 'VP80'")
|
||||
assert.Equal(uint16(176), header.Width, "width should be 176")
|
||||
assert.Equal(uint16(144), header.Height, "height should be 144")
|
||||
assert.Equal(uint32(30000), header.TimebaseDenominator, "timebase denominator should be 30000")
|
||||
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(0), header.unused, "bytes should be unused")
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ func TestIVFReader_ParseValidFrames(t *testing.T) {
|
||||
payload, header, err := reader.ParseNextFrame()
|
||||
|
||||
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(
|
||||
payload,
|
||||
@@ -94,7 +94,7 @@ func TestIVFReader_ParseValidFrames(t *testing.T) {
|
||||
payload, header, err = reader.ParseNextFrame()
|
||||
|
||||
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(
|
||||
payload,
|
||||
|
||||
Reference in New Issue
Block a user