mirror of
https://github.com/pion/webrtc.git
synced 2025-09-27 03:25:58 +08:00

In Go 1.22 and earlier, a ticker needs to be explicitly stopped when it's no longer useful in order to avoid a resource leak. In Go 1.23 and later, an orphaned ticker will eventually be garbage collected, but it's still more thrifty to stop it early.
330 lines
9.1 KiB
Go
330 lines
9.1 KiB
Go
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
//go:build !js
|
|
// +build !js
|
|
|
|
// play-from-disk demonstrates how to send video and/or audio to your browser from files saved to disk.
|
|
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/pion/webrtc/v4"
|
|
"github.com/pion/webrtc/v4/pkg/media"
|
|
"github.com/pion/webrtc/v4/pkg/media/ivfreader"
|
|
"github.com/pion/webrtc/v4/pkg/media/oggreader"
|
|
)
|
|
|
|
const (
|
|
audioFileName = "output.ogg"
|
|
videoFileName = "output.ivf"
|
|
oggPageDuration = time.Millisecond * 20
|
|
)
|
|
|
|
// nolint:gocognit
|
|
func main() {
|
|
// Assert that we have an audio or video file
|
|
_, err := os.Stat(videoFileName)
|
|
haveVideoFile := !os.IsNotExist(err)
|
|
|
|
_, err = os.Stat(audioFileName)
|
|
haveAudioFile := !os.IsNotExist(err)
|
|
|
|
if !haveAudioFile && !haveVideoFile {
|
|
panic("Could not find `" + audioFileName + "` or `" + videoFileName + "`")
|
|
}
|
|
|
|
// Create a new RTCPeerConnection
|
|
peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{
|
|
ICEServers: []webrtc.ICEServer{
|
|
{
|
|
URLs: []string{"stun:stun.l.google.com:19302"},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer func() {
|
|
if cErr := peerConnection.Close(); cErr != nil {
|
|
fmt.Printf("cannot close peerConnection: %v\n", cErr)
|
|
}
|
|
}()
|
|
|
|
iceConnectedCtx, iceConnectedCtxCancel := context.WithCancel(context.Background())
|
|
|
|
if haveVideoFile {
|
|
file, openErr := os.Open(videoFileName)
|
|
if openErr != nil {
|
|
panic(openErr)
|
|
}
|
|
|
|
_, header, openErr := ivfreader.NewWith(file)
|
|
if openErr != nil {
|
|
panic(openErr)
|
|
}
|
|
|
|
// Determine video codec
|
|
var trackCodec string
|
|
switch header.FourCC {
|
|
case "AV01":
|
|
trackCodec = webrtc.MimeTypeAV1
|
|
case "VP90":
|
|
trackCodec = webrtc.MimeTypeVP9
|
|
case "VP80":
|
|
trackCodec = webrtc.MimeTypeVP8
|
|
default:
|
|
panic(fmt.Sprintf("Unable to handle FourCC %s", header.FourCC))
|
|
}
|
|
|
|
// Create a video track
|
|
videoTrack, videoTrackErr := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: trackCodec}, "video", "pion")
|
|
if videoTrackErr != nil {
|
|
panic(videoTrackErr)
|
|
}
|
|
|
|
rtpSender, videoTrackErr := peerConnection.AddTrack(videoTrack)
|
|
if videoTrackErr != nil {
|
|
panic(videoTrackErr)
|
|
}
|
|
|
|
// Read incoming RTCP packets
|
|
// Before these packets are returned they are processed by interceptors. For things
|
|
// like NACK this needs to be called.
|
|
go func() {
|
|
rtcpBuf := make([]byte, 1500)
|
|
for {
|
|
if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
// Open a IVF file and start reading using our IVFReader
|
|
file, ivfErr := os.Open(videoFileName)
|
|
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.
|
|
//
|
|
// It is important to use a time.Ticker instead of time.Sleep because
|
|
// * avoids accumulating skew, just calling time.Sleep didn't compensate for the time spent parsing the data
|
|
// * works around latency issues with Sleep (see https://github.com/golang/go/issues/44343)
|
|
ticker := time.NewTicker(time.Millisecond * time.Duration((float32(header.TimebaseNumerator)/float32(header.TimebaseDenominator))*1000))
|
|
defer ticker.Stop()
|
|
for ; true; <-ticker.C {
|
|
frame, _, ivfErr := ivf.ParseNextFrame()
|
|
if errors.Is(ivfErr, io.EOF) {
|
|
fmt.Printf("All video frames parsed and sent")
|
|
os.Exit(0)
|
|
}
|
|
|
|
if ivfErr != nil {
|
|
panic(ivfErr)
|
|
}
|
|
|
|
if ivfErr = videoTrack.WriteSample(media.Sample{Data: frame, Duration: time.Second}); ivfErr != nil {
|
|
panic(ivfErr)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
if haveAudioFile {
|
|
// Create a audio track
|
|
audioTrack, audioTrackErr := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus}, "audio", "pion")
|
|
if audioTrackErr != nil {
|
|
panic(audioTrackErr)
|
|
}
|
|
|
|
rtpSender, audioTrackErr := peerConnection.AddTrack(audioTrack)
|
|
if audioTrackErr != nil {
|
|
panic(audioTrackErr)
|
|
}
|
|
|
|
// Read incoming RTCP packets
|
|
// Before these packets are returned they are processed by interceptors. For things
|
|
// like NACK this needs to be called.
|
|
go func() {
|
|
rtcpBuf := make([]byte, 1500)
|
|
for {
|
|
if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
// Open a OGG file and start reading using our OGGReader
|
|
file, oggErr := os.Open(audioFileName)
|
|
if oggErr != nil {
|
|
panic(oggErr)
|
|
}
|
|
|
|
// Open on oggfile in non-checksum mode.
|
|
ogg, _, oggErr := oggreader.NewWith(file)
|
|
if oggErr != nil {
|
|
panic(oggErr)
|
|
}
|
|
|
|
// Wait for connection established
|
|
<-iceConnectedCtx.Done()
|
|
|
|
// Keep track of last granule, the difference is the amount of samples in the buffer
|
|
var lastGranule uint64
|
|
|
|
// It is important to use a time.Ticker instead of time.Sleep because
|
|
// * avoids accumulating skew, just calling time.Sleep didn't compensate for the time spent parsing the data
|
|
// * works around latency issues with Sleep (see https://github.com/golang/go/issues/44343)
|
|
ticker := time.NewTicker(oggPageDuration)
|
|
defer ticker.Stop()
|
|
for ; true; <-ticker.C {
|
|
pageData, pageHeader, oggErr := ogg.ParseNextPage()
|
|
if errors.Is(oggErr, io.EOF) {
|
|
fmt.Printf("All audio pages parsed and sent")
|
|
os.Exit(0)
|
|
}
|
|
|
|
if oggErr != nil {
|
|
panic(oggErr)
|
|
}
|
|
|
|
// The amount of samples is the difference between the last and current timestamp
|
|
sampleCount := float64(pageHeader.GranulePosition - lastGranule)
|
|
lastGranule = pageHeader.GranulePosition
|
|
sampleDuration := time.Duration((sampleCount/48000)*1000) * time.Millisecond
|
|
|
|
if oggErr = audioTrack.WriteSample(media.Sample{Data: pageData, Duration: sampleDuration}); oggErr != nil {
|
|
panic(oggErr)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// 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 handler for Peer connection state
|
|
// This will notify you when the peer has connected/disconnected
|
|
peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
|
|
fmt.Printf("Peer Connection State has changed: %s\n", s.String())
|
|
|
|
if s == webrtc.PeerConnectionStateFailed {
|
|
// Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart.
|
|
// Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout.
|
|
// Note that the PeerConnection may come back from PeerConnectionStateDisconnected.
|
|
fmt.Println("Peer Connection has gone to failed exiting")
|
|
os.Exit(0)
|
|
}
|
|
|
|
if s == webrtc.PeerConnectionStateClosed {
|
|
// PeerConnection was explicitly closed. This usually happens from a DTLS CloseNotify
|
|
fmt.Println("Peer Connection has gone to closed exiting")
|
|
os.Exit(0)
|
|
}
|
|
})
|
|
|
|
// Wait for the offer to be pasted
|
|
offer := webrtc.SessionDescription{}
|
|
decode(readUntilNewline(), &offer)
|
|
|
|
// 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)
|
|
}
|
|
|
|
// Create channel that is blocked until ICE Gathering is complete
|
|
gatherComplete := webrtc.GatheringCompletePromise(peerConnection)
|
|
|
|
// Sets the LocalDescription, and starts our UDP listeners
|
|
if err = peerConnection.SetLocalDescription(answer); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Block until ICE Gathering is complete, disabling trickle ICE
|
|
// we do this because we only can exchange one signaling message
|
|
// in a production application you should exchange ICE Candidates via OnICECandidate
|
|
<-gatherComplete
|
|
|
|
// Output the answer in base64 so we can paste it in browser
|
|
fmt.Println(encode(peerConnection.LocalDescription()))
|
|
|
|
// Block forever
|
|
select {}
|
|
}
|
|
|
|
// Read from stdin until we get a newline
|
|
func readUntilNewline() (in string) {
|
|
var err error
|
|
|
|
r := bufio.NewReader(os.Stdin)
|
|
for {
|
|
in, err = r.ReadString('\n')
|
|
if err != nil && !errors.Is(err, io.EOF) {
|
|
panic(err)
|
|
}
|
|
|
|
if in = strings.TrimSpace(in); len(in) > 0 {
|
|
break
|
|
}
|
|
}
|
|
|
|
fmt.Println("")
|
|
return
|
|
}
|
|
|
|
// JSON encode + base64 a SessionDescription
|
|
func encode(obj *webrtc.SessionDescription) string {
|
|
b, err := json.Marshal(obj)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return base64.StdEncoding.EncodeToString(b)
|
|
}
|
|
|
|
// Decode a base64 and unmarshal JSON into a SessionDescription
|
|
func decode(in string, obj *webrtc.SessionDescription) {
|
|
b, err := base64.StdEncoding.DecodeString(in)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
if err = json.Unmarshal(b, obj); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|