github.com/pion/webrtc/v4@v4.0.1/examples/play-from-disk/main.go (about)

     1  // SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
     2  // SPDX-License-Identifier: MIT
     3  
     4  //go:build !js
     5  // +build !js
     6  
     7  // play-from-disk demonstrates how to send video and/or audio to your browser from files saved to disk.
     8  package main
     9  
    10  import (
    11  	"bufio"
    12  	"context"
    13  	"encoding/base64"
    14  	"encoding/json"
    15  	"errors"
    16  	"fmt"
    17  	"io"
    18  	"os"
    19  	"strings"
    20  	"time"
    21  
    22  	"github.com/pion/webrtc/v4"
    23  	"github.com/pion/webrtc/v4/pkg/media"
    24  	"github.com/pion/webrtc/v4/pkg/media/ivfreader"
    25  	"github.com/pion/webrtc/v4/pkg/media/oggreader"
    26  )
    27  
    28  const (
    29  	audioFileName   = "output.ogg"
    30  	videoFileName   = "output.ivf"
    31  	oggPageDuration = time.Millisecond * 20
    32  )
    33  
    34  // nolint:gocognit
    35  func main() {
    36  	// Assert that we have an audio or video file
    37  	_, err := os.Stat(videoFileName)
    38  	haveVideoFile := !os.IsNotExist(err)
    39  
    40  	_, err = os.Stat(audioFileName)
    41  	haveAudioFile := !os.IsNotExist(err)
    42  
    43  	if !haveAudioFile && !haveVideoFile {
    44  		panic("Could not find `" + audioFileName + "` or `" + videoFileName + "`")
    45  	}
    46  
    47  	// Create a new RTCPeerConnection
    48  	peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{
    49  		ICEServers: []webrtc.ICEServer{
    50  			{
    51  				URLs: []string{"stun:stun.l.google.com:19302"},
    52  			},
    53  		},
    54  	})
    55  	if err != nil {
    56  		panic(err)
    57  	}
    58  	defer func() {
    59  		if cErr := peerConnection.Close(); cErr != nil {
    60  			fmt.Printf("cannot close peerConnection: %v\n", cErr)
    61  		}
    62  	}()
    63  
    64  	iceConnectedCtx, iceConnectedCtxCancel := context.WithCancel(context.Background())
    65  
    66  	if haveVideoFile {
    67  		file, openErr := os.Open(videoFileName)
    68  		if openErr != nil {
    69  			panic(openErr)
    70  		}
    71  
    72  		_, header, openErr := ivfreader.NewWith(file)
    73  		if openErr != nil {
    74  			panic(openErr)
    75  		}
    76  
    77  		// Determine video codec
    78  		var trackCodec string
    79  		switch header.FourCC {
    80  		case "AV01":
    81  			trackCodec = webrtc.MimeTypeAV1
    82  		case "VP90":
    83  			trackCodec = webrtc.MimeTypeVP9
    84  		case "VP80":
    85  			trackCodec = webrtc.MimeTypeVP8
    86  		default:
    87  			panic(fmt.Sprintf("Unable to handle FourCC %s", header.FourCC))
    88  		}
    89  
    90  		// Create a video track
    91  		videoTrack, videoTrackErr := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: trackCodec}, "video", "pion")
    92  		if videoTrackErr != nil {
    93  			panic(videoTrackErr)
    94  		}
    95  
    96  		rtpSender, videoTrackErr := peerConnection.AddTrack(videoTrack)
    97  		if videoTrackErr != nil {
    98  			panic(videoTrackErr)
    99  		}
   100  
   101  		// Read incoming RTCP packets
   102  		// Before these packets are returned they are processed by interceptors. For things
   103  		// like NACK this needs to be called.
   104  		go func() {
   105  			rtcpBuf := make([]byte, 1500)
   106  			for {
   107  				if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil {
   108  					return
   109  				}
   110  			}
   111  		}()
   112  
   113  		go func() {
   114  			// Open a IVF file and start reading using our IVFReader
   115  			file, ivfErr := os.Open(videoFileName)
   116  			if ivfErr != nil {
   117  				panic(ivfErr)
   118  			}
   119  
   120  			ivf, header, ivfErr := ivfreader.NewWith(file)
   121  			if ivfErr != nil {
   122  				panic(ivfErr)
   123  			}
   124  
   125  			// Wait for connection established
   126  			<-iceConnectedCtx.Done()
   127  
   128  			// 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.
   129  			// This isn't required since the video is timestamped, but we will such much higher loss if we send all at once.
   130  			//
   131  			// It is important to use a time.Ticker instead of time.Sleep because
   132  			// * avoids accumulating skew, just calling time.Sleep didn't compensate for the time spent parsing the data
   133  			// * works around latency issues with Sleep (see https://github.com/golang/go/issues/44343)
   134  			ticker := time.NewTicker(time.Millisecond * time.Duration((float32(header.TimebaseNumerator)/float32(header.TimebaseDenominator))*1000))
   135  			defer ticker.Stop()
   136  			for ; true; <-ticker.C {
   137  				frame, _, ivfErr := ivf.ParseNextFrame()
   138  				if errors.Is(ivfErr, io.EOF) {
   139  					fmt.Printf("All video frames parsed and sent")
   140  					os.Exit(0)
   141  				}
   142  
   143  				if ivfErr != nil {
   144  					panic(ivfErr)
   145  				}
   146  
   147  				if ivfErr = videoTrack.WriteSample(media.Sample{Data: frame, Duration: time.Second}); ivfErr != nil {
   148  					panic(ivfErr)
   149  				}
   150  			}
   151  		}()
   152  	}
   153  
   154  	if haveAudioFile {
   155  		// Create a audio track
   156  		audioTrack, audioTrackErr := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus}, "audio", "pion")
   157  		if audioTrackErr != nil {
   158  			panic(audioTrackErr)
   159  		}
   160  
   161  		rtpSender, audioTrackErr := peerConnection.AddTrack(audioTrack)
   162  		if audioTrackErr != nil {
   163  			panic(audioTrackErr)
   164  		}
   165  
   166  		// Read incoming RTCP packets
   167  		// Before these packets are returned they are processed by interceptors. For things
   168  		// like NACK this needs to be called.
   169  		go func() {
   170  			rtcpBuf := make([]byte, 1500)
   171  			for {
   172  				if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil {
   173  					return
   174  				}
   175  			}
   176  		}()
   177  
   178  		go func() {
   179  			// Open a OGG file and start reading using our OGGReader
   180  			file, oggErr := os.Open(audioFileName)
   181  			if oggErr != nil {
   182  				panic(oggErr)
   183  			}
   184  
   185  			// Open on oggfile in non-checksum mode.
   186  			ogg, _, oggErr := oggreader.NewWith(file)
   187  			if oggErr != nil {
   188  				panic(oggErr)
   189  			}
   190  
   191  			// Wait for connection established
   192  			<-iceConnectedCtx.Done()
   193  
   194  			// Keep track of last granule, the difference is the amount of samples in the buffer
   195  			var lastGranule uint64
   196  
   197  			// It is important to use a time.Ticker instead of time.Sleep because
   198  			// * avoids accumulating skew, just calling time.Sleep didn't compensate for the time spent parsing the data
   199  			// * works around latency issues with Sleep (see https://github.com/golang/go/issues/44343)
   200  			ticker := time.NewTicker(oggPageDuration)
   201  			defer ticker.Stop()
   202  			for ; true; <-ticker.C {
   203  				pageData, pageHeader, oggErr := ogg.ParseNextPage()
   204  				if errors.Is(oggErr, io.EOF) {
   205  					fmt.Printf("All audio pages parsed and sent")
   206  					os.Exit(0)
   207  				}
   208  
   209  				if oggErr != nil {
   210  					panic(oggErr)
   211  				}
   212  
   213  				// The amount of samples is the difference between the last and current timestamp
   214  				sampleCount := float64(pageHeader.GranulePosition - lastGranule)
   215  				lastGranule = pageHeader.GranulePosition
   216  				sampleDuration := time.Duration((sampleCount/48000)*1000) * time.Millisecond
   217  
   218  				if oggErr = audioTrack.WriteSample(media.Sample{Data: pageData, Duration: sampleDuration}); oggErr != nil {
   219  					panic(oggErr)
   220  				}
   221  			}
   222  		}()
   223  	}
   224  
   225  	// Set the handler for ICE connection state
   226  	// This will notify you when the peer has connected/disconnected
   227  	peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
   228  		fmt.Printf("Connection State has changed %s \n", connectionState.String())
   229  		if connectionState == webrtc.ICEConnectionStateConnected {
   230  			iceConnectedCtxCancel()
   231  		}
   232  	})
   233  
   234  	// Set the handler for Peer connection state
   235  	// This will notify you when the peer has connected/disconnected
   236  	peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
   237  		fmt.Printf("Peer Connection State has changed: %s\n", s.String())
   238  
   239  		if s == webrtc.PeerConnectionStateFailed {
   240  			// Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart.
   241  			// Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout.
   242  			// Note that the PeerConnection may come back from PeerConnectionStateDisconnected.
   243  			fmt.Println("Peer Connection has gone to failed exiting")
   244  			os.Exit(0)
   245  		}
   246  
   247  		if s == webrtc.PeerConnectionStateClosed {
   248  			// PeerConnection was explicitly closed. This usually happens from a DTLS CloseNotify
   249  			fmt.Println("Peer Connection has gone to closed exiting")
   250  			os.Exit(0)
   251  		}
   252  	})
   253  
   254  	// Wait for the offer to be pasted
   255  	offer := webrtc.SessionDescription{}
   256  	decode(readUntilNewline(), &offer)
   257  
   258  	// Set the remote SessionDescription
   259  	if err = peerConnection.SetRemoteDescription(offer); err != nil {
   260  		panic(err)
   261  	}
   262  
   263  	// Create answer
   264  	answer, err := peerConnection.CreateAnswer(nil)
   265  	if err != nil {
   266  		panic(err)
   267  	}
   268  
   269  	// Create channel that is blocked until ICE Gathering is complete
   270  	gatherComplete := webrtc.GatheringCompletePromise(peerConnection)
   271  
   272  	// Sets the LocalDescription, and starts our UDP listeners
   273  	if err = peerConnection.SetLocalDescription(answer); err != nil {
   274  		panic(err)
   275  	}
   276  
   277  	// Block until ICE Gathering is complete, disabling trickle ICE
   278  	// we do this because we only can exchange one signaling message
   279  	// in a production application you should exchange ICE Candidates via OnICECandidate
   280  	<-gatherComplete
   281  
   282  	// Output the answer in base64 so we can paste it in browser
   283  	fmt.Println(encode(peerConnection.LocalDescription()))
   284  
   285  	// Block forever
   286  	select {}
   287  }
   288  
   289  // Read from stdin until we get a newline
   290  func readUntilNewline() (in string) {
   291  	var err error
   292  
   293  	r := bufio.NewReader(os.Stdin)
   294  	for {
   295  		in, err = r.ReadString('\n')
   296  		if err != nil && !errors.Is(err, io.EOF) {
   297  			panic(err)
   298  		}
   299  
   300  		if in = strings.TrimSpace(in); len(in) > 0 {
   301  			break
   302  		}
   303  	}
   304  
   305  	fmt.Println("")
   306  	return
   307  }
   308  
   309  // JSON encode + base64 a SessionDescription
   310  func encode(obj *webrtc.SessionDescription) string {
   311  	b, err := json.Marshal(obj)
   312  	if err != nil {
   313  		panic(err)
   314  	}
   315  
   316  	return base64.StdEncoding.EncodeToString(b)
   317  }
   318  
   319  // Decode a base64 and unmarshal JSON into a SessionDescription
   320  func decode(in string, obj *webrtc.SessionDescription) {
   321  	b, err := base64.StdEncoding.DecodeString(in)
   322  	if err != nil {
   323  		panic(err)
   324  	}
   325  
   326  	if err = json.Unmarshal(b, obj); err != nil {
   327  		panic(err)
   328  	}
   329  }