From e409a1d9cc10b09d5ea519eec516ef00b59ced58 Mon Sep 17 00:00:00 2001 From: Sean DuBois Date: Sun, 17 Sep 2023 00:10:16 -0400 Subject: [PATCH] Add ORTC Media examples Resolves #379 --- examples/ortc-media/README.md | 46 +++++++ examples/ortc-media/main.go | 246 ++++++++++++++++++++++++++++++++++ examples/ortc/main.go | 24 ++-- 3 files changed, 301 insertions(+), 15 deletions(-) create mode 100644 examples/ortc-media/README.md create mode 100644 examples/ortc-media/main.go diff --git a/examples/ortc-media/README.md b/examples/ortc-media/README.md new file mode 100644 index 00000000000..76c2454814c --- /dev/null +++ b/examples/ortc-media/README.md @@ -0,0 +1,46 @@ +# ortc-media +ortc demonstrates Pion WebRTC's [ORTC](https://ortc.org/) capabilities. Instead of using the Session Description Protocol +to configure and communicate ORTC provides APIs. Users then can implement signaling with whatever protocol they wish. +ORTC can then be used to implement WebRTC. A ORTC implementation can parse/emit Session Description and act as a WebRTC +implementation. + +In this example we have defined a simple JSON based signaling protocol. + +## Instructions +### Create IVF named `output.ivf` that contains a VP8/VP9/AV1 track +``` +ffmpeg -i $INPUT_FILE -g 30 -b:v 2M output.ivf +``` + +**Note**: In the `ffmpeg` command which produces the .ivf file, the argument `-b:v 2M` specifies the video bitrate to be 2 megabits per second. We provide this default value to produce decent video quality, but if you experience problems with this configuration (such as dropped frames etc.), you can decrease this. See the [ffmpeg documentation](https://ffmpeg.org/ffmpeg.html#Options) for more information on the format of the value. + + +### Download ortc-media +``` +go install github.com/pion/webrtc/v4/examples/ortc-media@latest +``` + +### Run first client as offerer +`ortc-media -offer` this will emit a base64 message. Copy this message to your clipboard. + +## Run the second client as answerer +Run the second client. This should be launched with the message you copied in the previous step as stdin. + +`echo BASE64_MESSAGE_YOU_COPIED | ortc-media` + +This will emit another base64 message. Copy this new message. + +## Send base64 message to first client via CURL + +* Run `curl localhost:8080 -d "BASE64_MESSAGE_YOU_COPIED"`. `BASE64_MESSAGE_YOU_COPIED` is the value you copied in the last step. + +### Enjoy +The client that accepts media will print when it gets the first media packet. The SSRC will be different every run. + +``` +Got RTP Packet with SSRC 3097857772 +``` + +Media packets will continue to flow until the end of the file has been reached. + +Congrats, you have used Pion WebRTC! Now start building something cool diff --git a/examples/ortc-media/main.go b/examples/ortc-media/main.go new file mode 100644 index 00000000000..39155024210 --- /dev/null +++ b/examples/ortc-media/main.go @@ -0,0 +1,246 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +//go:build !js +// +build !js + +// ortc demonstrates Pion WebRTC's ORTC capabilities. +package main + +import ( + "errors" + "flag" + "fmt" + "io" + "os" + "time" + + "github.com/pion/webrtc/v4" + "github.com/pion/webrtc/v4/examples/internal/signal" + "github.com/pion/webrtc/v4/pkg/media" + "github.com/pion/webrtc/v4/pkg/media/ivfreader" +) + +const ( + videoFileName = "output.ivf" +) + +func main() { + isOffer := flag.Bool("offer", false, "Act as the offerer if set") + port := flag.Int("port", 8080, "http server port") + flag.Parse() + + // Everything below is the Pion WebRTC (ORTC) API! Thanks for using it ❤️. + + // Prepare ICE gathering options + iceOptions := webrtc.ICEGatherOptions{ + ICEServers: []webrtc.ICEServer{ + {URLs: []string{"stun:stun.l.google.com:19302"}}, + }, + } + + // Use default Codecs + m := &webrtc.MediaEngine{} + if err := m.RegisterDefaultCodecs(); err != nil { + panic(err) + } + + // Create an API object + api := webrtc.NewAPI(webrtc.WithMediaEngine(m)) + + // Create the ICE gatherer + gatherer, err := api.NewICEGatherer(iceOptions) + if err != nil { + panic(err) + } + + // Construct the ICE transport + ice := api.NewICETransport(gatherer) + + // Construct the DTLS transport + dtls, err := api.NewDTLSTransport(ice, nil) + if err != nil { + panic(err) + } + + // Create a RTPSender or RTPReceiver + var ( + rtpReceiver *webrtc.RTPReceiver + rtpSendParameters webrtc.RTPSendParameters + ) + + if *isOffer { + // Open the video file + file, fileErr := os.Open(videoFileName) + if fileErr != nil { + panic(fileErr) + } + + // Read the header of the video file + ivf, header, fileErr := ivfreader.NewWith(file) + if fileErr != nil { + panic(fileErr) + } + + trackLocal := fourCCToTrack(header.FourCC) + + // Create RTPSender to send our video file + rtpSender, fileErr := api.NewRTPSender(trackLocal, dtls) + if fileErr != nil { + panic(fileErr) + } + + rtpSendParameters = rtpSender.GetParameters() + + if fileErr = rtpSender.Send(rtpSendParameters); fileErr != nil { + panic(fileErr) + } + + go writeFileToTrack(ivf, header, trackLocal) + } else { + if rtpReceiver, err = api.NewRTPReceiver(webrtc.RTPCodecTypeVideo, dtls); err != nil { + panic(err) + } + } + + gatherFinished := make(chan struct{}) + gatherer.OnLocalCandidate(func(i *webrtc.ICECandidate) { + if i == nil { + close(gatherFinished) + } + }) + + // Gather candidates + if err = gatherer.Gather(); err != nil { + panic(err) + } + + <-gatherFinished + + iceCandidates, err := gatherer.GetLocalCandidates() + if err != nil { + panic(err) + } + + iceParams, err := gatherer.GetLocalParameters() + if err != nil { + panic(err) + } + + dtlsParams, err := dtls.GetLocalParameters() + if err != nil { + panic(err) + } + + s := Signal{ + ICECandidates: iceCandidates, + ICEParameters: iceParams, + DTLSParameters: dtlsParams, + RTPSendParameters: rtpSendParameters, + } + + iceRole := webrtc.ICERoleControlled + + // Exchange the information + fmt.Println(signal.Encode(s)) + remoteSignal := Signal{} + + if *isOffer { + signalingChan := signal.HTTPSDPServer(*port) + signal.Decode(<-signalingChan, &remoteSignal) + + iceRole = webrtc.ICERoleControlling + } else { + signal.Decode(signal.MustReadStdin(), &remoteSignal) + } + + if err = ice.SetRemoteCandidates(remoteSignal.ICECandidates); err != nil { + panic(err) + } + + // Start the ICE transport + if err = ice.Start(nil, remoteSignal.ICEParameters, &iceRole); err != nil { + panic(err) + } + + // Start the DTLS transport + if err = dtls.Start(remoteSignal.DTLSParameters); err != nil { + panic(err) + } + + if !*isOffer { + if err = rtpReceiver.Receive(webrtc.RTPReceiveParameters{ + Encodings: []webrtc.RTPDecodingParameters{ + { + RTPCodingParameters: remoteSignal.RTPSendParameters.Encodings[0].RTPCodingParameters, + }, + }, + }); err != nil { + panic(err) + } + + remoteTrack := rtpReceiver.Track() + pkt, _, err := remoteTrack.ReadRTP() + if err != nil { + panic(err) + } + + fmt.Printf("Got RTP Packet with SSRC %d \n", pkt.SSRC) + } + + select {} +} + +// Given a FourCC value return a Track +func fourCCToTrack(fourCC string) *webrtc.TrackLocalStaticSample { + // Determine video codec + var trackCodec string + switch 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", fourCC)) + } + + // Create a video Track with the codec of the file + trackLocal, err := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: trackCodec}, "video", "pion") + if err != nil { + panic(err) + } + + return trackLocal +} + +// Write a file to Track +func writeFileToTrack(ivf *ivfreader.IVFReader, header *ivfreader.IVFFileHeader, track *webrtc.TrackLocalStaticSample) { + ticker := time.NewTicker(time.Millisecond * time.Duration((float32(header.TimebaseNumerator)/float32(header.TimebaseDenominator))*1000)) + for ; true; <-ticker.C { + frame, _, err := ivf.ParseNextFrame() + if errors.Is(err, io.EOF) { + fmt.Printf("All video frames parsed and sent") + os.Exit(0) + } + + if err != nil { + panic(err) + } + + if err = track.WriteSample(media.Sample{Data: frame, Duration: time.Second}); err != nil { + panic(err) + } + } +} + +// Signal is used to exchange signaling info. +// This is not part of the ORTC spec. You are free +// to exchange this information any way you want. +type Signal struct { + ICECandidates []webrtc.ICECandidate `json:"iceCandidates"` + ICEParameters webrtc.ICEParameters `json:"iceParameters"` + DTLSParameters webrtc.DTLSParameters `json:"dtlsParameters"` + RTPSendParameters webrtc.RTPSendParameters `json:"rtpSendParameters"` +} diff --git a/examples/ortc/main.go b/examples/ortc/main.go index 0d1de6bf551..60ad6c83f3c 100644 --- a/examples/ortc/main.go +++ b/examples/ortc/main.go @@ -70,8 +70,7 @@ func main() { }) // Gather candidates - err = gatherer.Gather() - if err != nil { + if err = gatherer.Gather(); err != nil { panic(err) } @@ -101,6 +100,8 @@ func main() { SCTPCapabilities: sctpCapabilities, } + iceRole := webrtc.ICERoleControlled + // Exchange the information fmt.Println(signal.Encode(s)) remoteSignal := Signal{} @@ -108,17 +109,13 @@ func main() { if *isOffer { signalingChan := signal.HTTPSDPServer(*port) signal.Decode(<-signalingChan, &remoteSignal) - } else { - signal.Decode(signal.MustReadStdin(), &remoteSignal) - } - iceRole := webrtc.ICERoleControlled - if *isOffer { iceRole = webrtc.ICERoleControlling + } else { + signal.Decode(signal.MustReadStdin(), &remoteSignal) } - err = ice.SetRemoteCandidates(remoteSignal.ICECandidates) - if err != nil { + if err = ice.SetRemoteCandidates(remoteSignal.ICECandidates); err != nil { panic(err) } @@ -129,14 +126,12 @@ func main() { } // Start the DTLS transport - err = dtls.Start(remoteSignal.DTLSParameters) - if err != nil { + if err = dtls.Start(remoteSignal.DTLSParameters); err != nil { panic(err) } // Start the SCTP transport - err = sctp.Start(remoteSignal.SCTPCapabilities) - if err != nil { + if err = sctp.Start(remoteSignal.SCTPCapabilities); err != nil { panic(err) } @@ -183,8 +178,7 @@ func handleOnOpen(channel *webrtc.DataChannel) func() { message := signal.RandSeq(15) fmt.Printf("Sending '%s' \n", message) - err := channel.SendText(message) - if err != nil { + if err := channel.SendText(message); err != nil { panic(err) } }