New Track API

The Pion WebRTC API has been dramatically redesigned. The design docs
are located here [0]

You can also read the release notes [1] on how to migrate your
application.

[0] https://github.com/pion/webrtc-v3-design
[1] https://github.com/pion/webrtc/wiki/Release-WebRTC@v3.0.0
This commit is contained in:
Sean DuBois
2020-11-11 12:03:08 -08:00
parent 159ba5aca3
commit 7edfb701e0
46 changed files with 1750 additions and 2059 deletions

View File

@@ -4,6 +4,8 @@ package webrtc
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewAPI(t *testing.T) {
@@ -22,7 +24,7 @@ func TestNewAPI_Options(t *testing.T) {
s := SettingEngine{}
s.DetachDataChannels()
m := MediaEngine{}
m.RegisterDefaultCodecs()
assert.NoError(t, m.RegisterDefaultCodecs())
api := NewAPI(
WithSettingEngine(s),
@@ -33,7 +35,7 @@ func TestNewAPI_Options(t *testing.T) {
t.Error("Failed to set settings engine")
}
if len(api.mediaEngine.codecs) == 0 {
if len(api.mediaEngine.audioCodecs) == 0 || len(api.mediaEngine.videoCodecs) == 0 {
t.Error("Failed to set media engine")
}
}

View File

@@ -5,7 +5,6 @@ package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"strconv"
@@ -13,11 +12,9 @@ import (
"testing"
"time"
"github.com/sclevine/agouti"
"github.com/pion/randutil"
"github.com/pion/webrtc/v3"
"github.com/pion/webrtc/v3/pkg/media"
"github.com/sclevine/agouti"
)
var silentOpusFrame = []byte{0xf8, 0xff, 0xfe} // 20ms, 8kHz, mono
@@ -103,7 +100,7 @@ func TestE2E_Audio(t *testing.T) {
go func() {
for {
if err := track.WriteSample(
media.Sample{Data: silentOpusFrame, Samples: 960},
media.Sample{Data: silentOpusFrame, Duration: time.Millisecond * 20},
); err != nil {
t.Errorf("Failed to WriteSample: %v", err)
return
@@ -330,28 +327,13 @@ func parseLog(log agouti.Log) (string, string, bool) {
return k, v, true
}
func createTrack(offer webrtc.SessionDescription) (*webrtc.PeerConnection, *webrtc.SessionDescription, *webrtc.Track, error) {
mediaEngine := webrtc.MediaEngine{}
if err := mediaEngine.PopulateFromSDP(offer); err != nil {
return nil, nil, nil, err
}
var payloadType uint8
for _, videoCodec := range mediaEngine.GetCodecsByKind(webrtc.RTPCodecTypeAudio) {
if videoCodec.Name == webrtc.Opus {
payloadType = videoCodec.PayloadType
break
}
}
if payloadType == 0 {
return nil, nil, nil, errors.New("Remote peer does not support VP8")
}
api := webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine))
pc, errPc := api.NewPeerConnection(webrtc.Configuration{})
func createTrack(offer webrtc.SessionDescription) (*webrtc.PeerConnection, *webrtc.SessionDescription, *webrtc.TrackLocalStaticSample, error) {
pc, errPc := webrtc.NewPeerConnection(webrtc.Configuration{})
if errPc != nil {
return nil, nil, nil, errPc
}
track, errTrack := pc.NewTrack(payloadType, randutil.NewMathRandomGenerator().Uint32(), "video", "pion")
track, errTrack := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: "audio/opus"}, "audio", "pion")
if errTrack != nil {
return nil, nil, nil, errTrack
}

View File

@@ -129,6 +129,18 @@ var (
// ErrFailedToGenerateCertificateFingerprint indicates that we failed to generate the fingerprint used for comparing certificates
ErrFailedToGenerateCertificateFingerprint = errors.New("failed to generate certificate fingerprint")
// ErrNoCodecsAvailable indicates that operation isn't possible because the MediaEngine has no codecs available
ErrNoCodecsAvailable = errors.New("operation failed no codecs are available")
// ErrUnsupportedCodec indicates the remote peer doesn't support the requested codec
ErrUnsupportedCodec = errors.New("unable to start track, codec is not supported by remote")
// ErrUnbindFailed indicates that a TrackLocal was not able to be unbind
ErrUnbindFailed = errors.New("failed to unbind TrackLocal from PeerConnection")
// ErrNoPayloaderForCodec indicates that the requested codec does not have a payloader
ErrNoPayloaderForCodec = errors.New("the requested codec does not have a payloader")
errDetachNotEnabled = errors.New("enable detaching by calling webrtc.DetachDataChannels()")
errDetachBeforeOpened = errors.New("datachannel not opened yet, try calling Detach from OnOpen")
errDtlsTransportNotStarted = errors.New("the DTLS transport has not started yet")
@@ -149,9 +161,7 @@ var (
errICEProtocolUnknown = errors.New("unknown protocol")
errICEGathererNotStarted = errors.New("gatherer not started")
errMediaEngineParseError = errors.New("format parse error")
errMediaEngineCodecNotFound = errors.New("could not find codec")
errNetworkTypeUnknown = errors.New("unknown network type")
errNetworkTypeUnknown = errors.New("unknown network type")
errSDPDoesNotMatchOffer = errors.New("new sdp does not match previous offer")
errSDPDoesNotMatchAnswer = errors.New("new sdp does not match previous answer")
@@ -163,16 +173,15 @@ var (
errPeerConnRemoteDescriptionNil = errors.New("remoteDescription has not been set yet")
errPeerConnSingleMediaSectionHasExplicitSSRC = errors.New("single media section has an explicit SSRC")
errPeerConnRemoteSSRCAddTransceiver = errors.New("could not add transceiver for remote SSRC")
errPeerConnSimulcastMidAndRidRTPExtensionRequired = errors.New("mid and rid RTP Extensions required for Simulcast")
errPeerConnSimulcastMidRTPExtensionRequired = errors.New("mid RTP Extensions required for Simulcast")
errPeerConnSimulcastStreamIDRTPExtensionRequired = errors.New("stream id RTP Extensions required for Simulcast")
errPeerConnSimulcastIncomingSSRCFailed = errors.New("incoming SSRC failed Simulcast probing")
errPeerConnAddTransceiverFromKindOnlyAcceptsOne = errors.New("AddTransceiverFromKind only accepts one RtpTransceiverInit")
errPeerConnAddTransceiverFromTrackOnlyAcceptsOne = errors.New("AddTransceiverFromTrack only accepts one RtpTransceiverInit")
errPeerConnCodecsNotFound = errors.New("no codecs found")
errPeerConnAddTransceiverFromKindSupport = errors.New("AddTransceiverFromKind currently only supports recvonly and sendrecv")
errPeerConnAddTransceiverFromTrackOneTransceiver = errors.New("AddTransceiverFromTrack only accepts one RtpTransceiverInit")
errPeerConnAddTransceiverFromKindSupport = errors.New("AddTransceiverFromKind currently only supports recvonly")
errPeerConnAddTransceiverFromTrackSupport = errors.New("AddTransceiverFromTrack currently only supports sendonly and sendrecv")
errPeerConnSetIdentityProviderNotImplemented = errors.New("TODO SetIdentityProvider")
errPeerConnWriteRTCPOpenWriteStream = errors.New("WriteRTCP failed to open WriteStream")
errPeerConnCodecPayloaderNotSet = errors.New("codec payloader not set")
errPeerConnTranscieverMidNil = errors.New("cannot find transceiver with mid")
errRTPReceiverDTLSTransportNil = errors.New("DTLSTransport must not be nil")
@@ -181,11 +190,9 @@ var (
errRTPReceiverForSSRCTrackStreamNotFound = errors.New("no trackStreams found for SSRC")
errRTPReceiverForRIDTrackStreamNotFound = errors.New("no trackStreams found for RID")
errRTPSenderTrackNil = errors.New("Track must not be nil")
errRTPSenderDTLSTransportNil = errors.New("DTLSTransport must not be nil")
errRTPSenderCannotConstructRemoteTrack = errors.New("RTPSender can not be constructed with remote track")
errRTPSenderSendAlreadyCalled = errors.New("Send has already been called")
errRTPSenderStopped = errors.New("RTPSender has been stopped")
errRTPSenderTrackNil = errors.New("Track must not be nil")
errRTPSenderDTLSTransportNil = errors.New("DTLSTransport must not be nil")
errRTPSenderSendAlreadyCalled = errors.New("Send has already been called")
errRTPTransceiverCannotChangeMid = errors.New("errRTPSenderTrackNil")
errRTPTransceiverSetSendingInvalidState = errors.New("invalid state change in RTPTransceiver.setSending")
@@ -195,8 +202,6 @@ var (
errSDPZeroTransceivers = errors.New("addTransceiverSDP() called with 0 transceivers")
errSDPMediaSectionMediaDataChanInvalid = errors.New("invalid Media Section. Media + DataChannel both enabled")
errSDPMediaSectionMultipleTrackInvalid = errors.New("invalid Media Section. Can not have multiple tracks in one MediaSection in UnifiedPlan")
errSDPParseExtMap = errors.New("failed to parse ExtMap")
errSDPRemoteDescriptionChangedExtMap = errors.New("RemoteDescription changed some extmaps values")
errSettingEngineSetAnsweringDTLSRole = errors.New("SetAnsweringDTLSRole must DTLSRoleClient or DTLSRoleServer")
@@ -204,8 +209,4 @@ var (
errSignalingStateProposedTransitionInvalid = errors.New("invalid proposed signaling state transition")
errStatsICECandidateStateInvalid = errors.New("cannot convert to StatsICECandidatePairStateSucceeded invalid ice candidate state")
errTrackLocalTrackRead = errors.New("this is a local track and must not be read from")
errTrackLocalTrackWrite = errors.New("this is a remote track and must not be written to")
errTrackSSRCNewTrackZero = errors.New("SSRC supplied to NewTrack() must be non-zero")
)

View File

@@ -23,16 +23,6 @@ func main() { // nolint:gocognit
signal.Decode(<-sdpChan, &offer)
fmt.Println("")
// Since we are answering use PayloadTypes declared by offerer
mediaEngine := webrtc.MediaEngine{}
err := mediaEngine.PopulateFromSDP(offer)
if err != nil {
panic(err)
}
// Create the API object with the MediaEngine
api := webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine))
peerConnectionConfig := webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
{
@@ -42,7 +32,7 @@ func main() { // nolint:gocognit
}
// Create a new RTCPeerConnection
peerConnection, err := api.NewPeerConnection(peerConnectionConfig)
peerConnection, err := webrtc.NewPeerConnection(peerConnectionConfig)
if err != nil {
panic(err)
}
@@ -52,23 +42,23 @@ func main() { // nolint:gocognit
panic(err)
}
localTrackChan := make(chan *webrtc.Track)
localTrackChan := make(chan *webrtc.TrackLocalStaticRTP)
// Set a handler for when a new remote track starts, this just distributes all our packets
// to connected peers
peerConnection.OnTrack(func(remoteTrack *webrtc.Track, receiver *webrtc.RTPReceiver) {
peerConnection.OnTrack(func(remoteTrack *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
// Send a PLI on an interval so that the publisher is pushing a keyframe every rtcpPLIInterval
// This can be less wasteful by processing incoming RTCP events, then we would emit a NACK/PLI when a viewer requests it
go func() {
ticker := time.NewTicker(rtcpPLIInterval)
for range ticker.C {
if rtcpSendErr := peerConnection.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: remoteTrack.SSRC()}}); rtcpSendErr != nil {
if rtcpSendErr := peerConnection.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: uint32(remoteTrack.SSRC())}}); rtcpSendErr != nil {
fmt.Println(rtcpSendErr)
}
}
}()
// Create a local track, all our SFU clients will be fed via this track
localTrack, newTrackErr := peerConnection.NewTrack(remoteTrack.PayloadType(), remoteTrack.SSRC(), "video", "pion")
localTrack, newTrackErr := webrtc.NewTrackLocalStaticRTP(remoteTrack.Codec().RTPCodecCapability, "video", "pion")
if newTrackErr != nil {
panic(newTrackErr)
}
@@ -126,7 +116,7 @@ func main() { // nolint:gocognit
signal.Decode(<-sdpChan, &recvOnlyOffer)
// Create a new PeerConnection
peerConnection, err := api.NewPeerConnection(peerConnectionConfig)
peerConnection, err := webrtc.NewPeerConnection(peerConnectionConfig)
if err != nil {
panic(err)
}

View File

@@ -61,6 +61,11 @@ func main() {
panic(err)
}
// We need a DataChannel so we can have ICE Candidates
if _, err = offerPeerConnection.CreateDataChannel("custom-logger", nil); err != nil {
panic(err)
}
// Create a new RTCPeerConnection
answerPeerConnection, err := api.NewPeerConnection(webrtc.Configuration{})
if err != nil {

View File

@@ -17,7 +17,9 @@ func doSignaling(w http.ResponseWriter, r *http.Request) {
if peerConnection == nil {
m := webrtc.MediaEngine{}
m.RegisterDefaultCodecs()
if err = m.RegisterDefaultCodecs(); err != nil {
panic(err)
}
settingEngine := webrtc.SettingEngine{}

View File

@@ -7,7 +7,6 @@ import (
"os"
"time"
"github.com/pion/randutil"
"github.com/pion/webrtc/v3"
"github.com/pion/webrtc/v3/examples/internal/signal"
"github.com/pion/webrtc/v3/pkg/media"
@@ -17,34 +16,7 @@ import (
const cipherKey = 0xAA
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{
peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
{
URLs: []string{"stun:stun.l.google.com:19302"},
@@ -56,7 +28,7 @@ func main() {
}
// Create a video track
videoTrack, err := peerConnection.NewTrack(payloadType, randutil.NewMathRandomGenerator().Uint32(), "video", "pion")
videoTrack, err := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
if err != nil {
panic(err)
}
@@ -100,7 +72,7 @@ func main() {
}
time.Sleep(sleepTime)
if ivfErr = videoTrack.WriteSample(media.Sample{Data: frame, Samples: 90000}); ivfErr != nil {
if ivfErr = videoTrack.WriteSample(media.Sample{Data: frame, Duration: time.Second}); ivfErr != nil {
panic(ivfErr)
}
}
@@ -115,6 +87,10 @@ func main() {
}
})
// Wait for the offer to be pasted
offer := webrtc.SessionDescription{}
signal.Decode(signal.MustReadStdin(), &offer)
// Set the remote SessionDescription
if err = peerConnection.SetRemoteDescription(offer); err != nil {
panic(err)

View File

@@ -17,7 +17,7 @@
<script>
let activeVideos = 0
let pc = new RTCPeerConnection()({
let pc = new RTCPeerConnection({
iceServers: [
{
urls: 'stun:stun.l.google.com:19302'

View File

@@ -66,9 +66,8 @@ func createPeerConnection(w http.ResponseWriter, r *http.Request) {
// Add a single video track
func addVideo(w http.ResponseWriter, r *http.Request) {
videoTrack, err := peerConnection.NewTrack(
webrtc.DefaultPayloadTypeVP8,
randutil.NewMathRandomGenerator().Uint32(),
videoTrack, err := webrtc.NewTrackLocalStaticSample(
webrtc.RTPCodecCapability{MimeType: "video/vp8"},
fmt.Sprintf("video-%d", randutil.NewMathRandomGenerator().Uint32()),
fmt.Sprintf("video-%d", randutil.NewMathRandomGenerator().Uint32()),
)
@@ -121,7 +120,7 @@ func main() {
// Read a video file from disk and write it to a webrtc.Track
// When the video has been completely read this exits without error
func writeVideoToTrack(t *webrtc.Track) {
func writeVideoToTrack(t *webrtc.TrackLocalStaticSample) {
// Open a IVF file and start reading using our IVFReader
file, err := os.Open("output.ivf")
if err != nil {
@@ -144,7 +143,7 @@ func writeVideoToTrack(t *webrtc.Track) {
}
time.Sleep(sleepTime)
if err = t.WriteSample(media.Sample{Data: frame, Samples: 90000}); err != nil {
if err = t.WriteSample(media.Sample{Data: frame, Duration: time.Second}); err != nil {
fmt.Printf("Finish writing video track: %s ", err)
return
}

View File

@@ -7,7 +7,6 @@ import (
"os"
"time"
"github.com/pion/randutil"
"github.com/pion/webrtc/v3"
"github.com/pion/webrtc/v3/examples/internal/signal"
"github.com/pion/webrtc/v3/pkg/media"
@@ -32,20 +31,8 @@ func main() {
panic("Could not find `" + audioFileName + "` or `" + videoFileName + "`")
}
// 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{}
if err = mediaEngine.PopulateFromSDP(offer); err != nil {
panic(err)
}
// Create a new RTCPeerConnection
api := webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine))
peerConnection, err := api.NewPeerConnection(webrtc.Configuration{
peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
{
URLs: []string{"stun:stun.l.google.com:19302"},
@@ -59,12 +46,13 @@ func main() {
if haveVideoFile {
// Create a video track
videoTrack, addTrackErr := peerConnection.NewTrack(getPayloadType(mediaEngine, webrtc.RTPCodecTypeVideo, "VP8"), randutil.NewMathRandomGenerator().Uint32(), "video", "pion")
if addTrackErr != nil {
panic(addTrackErr)
videoTrack, videoTrackErr := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
if videoTrackErr != nil {
panic(videoTrackErr)
}
if _, addTrackErr = peerConnection.AddTrack(videoTrack); addTrackErr != nil {
panic(addTrackErr)
if _, videoTrackErr = peerConnection.AddTrack(videoTrack); videoTrackErr != nil {
panic(videoTrackErr)
}
go func() {
@@ -97,7 +85,7 @@ func main() {
}
time.Sleep(sleepTime)
if ivfErr = videoTrack.WriteSample(media.Sample{Data: frame, Samples: 90000}); ivfErr != nil {
if ivfErr = videoTrack.WriteSample(media.Sample{Data: frame, Duration: time.Second}); ivfErr != nil {
panic(ivfErr)
}
}
@@ -106,12 +94,12 @@ func main() {
if haveAudioFile {
// Create a audio track
audioTrack, addTrackErr := peerConnection.NewTrack(getPayloadType(mediaEngine, webrtc.RTPCodecTypeAudio, "opus"), randutil.NewMathRandomGenerator().Uint32(), "audio", "pion")
if addTrackErr != nil {
panic(addTrackErr)
audioTrack, audioTrackErr := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: "audio/opus"}, "audio", "pion")
if audioTrackErr != nil {
panic(audioTrackErr)
}
if _, addTrackErr = peerConnection.AddTrack(audioTrack); addTrackErr != nil {
panic(addTrackErr)
if _, audioTrackErr = peerConnection.AddTrack(audioTrack); audioTrackErr != nil {
panic(audioTrackErr)
}
go func() {
@@ -146,13 +134,13 @@ func main() {
// 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, Samples: uint32(sampleCount)}); oggErr != nil {
if oggErr = audioTrack.WriteSample(media.Sample{Data: pageData, Duration: sampleDuration}); oggErr != nil {
panic(oggErr)
}
// Convert seconds to Milliseconds, Sleep doesn't accept floats
time.Sleep(time.Duration((sampleCount/48000)*1000) * time.Millisecond)
time.Sleep(sampleDuration)
}
}()
}
@@ -166,6 +154,10 @@ func main() {
}
})
// Wait for the offer to be pasted
offer := webrtc.SessionDescription{}
signal.Decode(signal.MustReadStdin(), &offer)
// Set the remote SessionDescription
if err = peerConnection.SetRemoteDescription(offer); err != nil {
panic(err)
@@ -196,15 +188,3 @@ func main() {
// Block forever
select {}
}
// Search for Codec PayloadType
//
// Since we are answering we need to match the remote PayloadType
func getPayloadType(m webrtc.MediaEngine, codecType webrtc.RTPCodecType, codecName string) uint8 {
for _, codec := range m.GetCodecsByKind(codecType) {
if codec.Name == codecName {
return codec.PayloadType
}
}
panic(fmt.Sprintf("Remote peer does not support %s", codecName))
}

View File

@@ -4,7 +4,6 @@ import (
"fmt"
"time"
"github.com/pion/randutil"
"github.com/pion/rtcp"
"github.com/pion/webrtc/v3"
"github.com/pion/webrtc/v3/examples/internal/signal"
@@ -13,29 +12,20 @@ import (
func main() {
// Everything below is the Pion WebRTC API! Thanks for using it ❤️.
// Wait for the offer to be pasted
offer := webrtc.SessionDescription{}
signal.Decode(signal.MustReadStdin(), &offer)
// Create a MediaEngine object to configure the supported codec
m := webrtc.MediaEngine{}
// We make our own mediaEngine so we can place the sender's codecs in it. Since we are echoing their RTP packet
// back to them we are actually codec agnostic - we can accept all their codecs. This also ensures that we use the
// dynamic media type from the sender in our answer.
mediaEngine := webrtc.MediaEngine{}
// Add codecs to the mediaEngine. Note that even though we are only going to echo back the sender's video we also
// add audio codecs. This is because createAnswer will create an audioTransceiver and associated SDP and we currently
// cannot tell it not to. The audio SDP must match the sender's codecs too...
err := mediaEngine.PopulateFromSDP(offer)
if err != nil {
// Setup the codecs you want to use.
// We'll use a VP8 and Opus but you can also define your own
if err := m.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "video/VP8", ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil},
PayloadType: 96,
}, webrtc.RTPCodecTypeVideo); err != nil {
panic(err)
}
videoCodecs := mediaEngine.GetCodecsByKind(webrtc.RTPCodecTypeVideo)
if len(videoCodecs) == 0 {
panic("Offer contained no video codecs")
}
api := webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine))
// Create the API object with the MediaEngine
api := webrtc.NewAPI(webrtc.WithMediaEngine(m))
// Prepare the configuration
config := webrtc.Configuration{
@@ -52,7 +42,7 @@ func main() {
}
// Create Track that we send video back to browser on
outputTrack, err := peerConnection.NewTrack(videoCodecs[0].PayloadType, randutil.NewMathRandomGenerator().Uint32(), "video", "pion")
outputTrack, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
if err != nil {
panic(err)
}
@@ -62,6 +52,10 @@ func main() {
panic(err)
}
// Wait for the offer to be pasted
offer := webrtc.SessionDescription{}
signal.Decode(signal.MustReadStdin(), &offer)
// Set the remote SessionDescription
err = peerConnection.SetRemoteDescription(offer)
if err != nil {
@@ -70,20 +64,20 @@ func main() {
// Set a handler for when a new remote track starts, this handler copies inbound RTP packets,
// replaces the SSRC and sends them back
peerConnection.OnTrack(func(track *webrtc.Track, receiver *webrtc.RTPReceiver) {
peerConnection.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
// Send a PLI on an interval so that the publisher is pushing a keyframe every rtcpPLIInterval
// This is a temporary fix until we implement incoming RTCP events, then we would push a PLI only when a viewer requests it
go func() {
ticker := time.NewTicker(time.Second * 3)
for range ticker.C {
errSend := peerConnection.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: track.SSRC()}})
errSend := peerConnection.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: uint32(track.SSRC())}})
if errSend != nil {
fmt.Println(errSend)
}
}
}()
fmt.Printf("Track has started, of type %d: %s \n", track.PayloadType(), track.Codec().Name)
fmt.Printf("Track has started, of type %d: %s \n", track.PayloadType(), track.Codec().MimeType)
for {
// Read RTP packets being sent to Pion
rtp, readErr := track.ReadRTP()
@@ -91,10 +85,6 @@ func main() {
panic(readErr)
}
// Replace the SSRC with the SSRC of the outbound track.
// The only change we are making replacing the SSRC, the RTP packets are unchanged otherwise
rtp.SSRC = outputTrack.SSRC()
if writeErr := outputTrack.WriteRTP(rtp); writeErr != nil {
panic(writeErr)
}
@@ -116,8 +106,7 @@ func main() {
gatherComplete := webrtc.GatheringCompletePromise(peerConnection)
// Sets the LocalDescription, and starts our UDP listeners
err = peerConnection.SetLocalDescription(answer)
if err != nil {
if err = peerConnection.SetLocalDescription(answer); err != nil {
panic(err)
}

View File

@@ -17,22 +17,29 @@ type udpConn struct {
}
func main() {
// Create context
ctx, cancel := context.WithCancel(context.Background())
// Everything below is the Pion WebRTC API! Thanks for using it ❤️.
// Create a MediaEngine object to configure the supported codec
m := webrtc.MediaEngine{}
// Setup the codecs you want to use.
// We'll use a VP8 codec but you can also define your own
m.RegisterCodec(webrtc.NewRTPOpusCodec(webrtc.DefaultPayloadTypeOpus, 48000))
m.RegisterCodec(webrtc.NewRTPVP8Codec(webrtc.DefaultPayloadTypeVP8, 90000))
// We'll use a VP8 and Opus but you can also define your own
if err := m.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "video/VP8", ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil},
PayloadType: 96,
}, webrtc.RTPCodecTypeVideo); err != nil {
panic(err)
}
if err := m.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "audio/opus", ClockRate: 48000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil},
PayloadType: 111,
}, webrtc.RTPCodecTypeAudio); err != nil {
panic(err)
}
// Create the API object with the MediaEngine
api := webrtc.NewAPI(webrtc.WithMediaEngine(m))
// Everything below is the Pion WebRTC API! Thanks for using it ❤️.
// Prepare the configuration
config := webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
@@ -87,7 +94,7 @@ func main() {
// Set a handler for when a new remote track starts, this handler will forward data to
// our UDP listeners.
// In your application this is where you would handle/process audio/video
peerConnection.OnTrack(func(track *webrtc.Track, receiver *webrtc.RTPReceiver) {
peerConnection.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
// Retrieve udp connection
c, ok := udpConns[track.Kind().String()]
if !ok {
@@ -98,7 +105,7 @@ func main() {
go func() {
ticker := time.NewTicker(time.Second * 2)
for range ticker.C {
if rtcpErr := peerConnection.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: track.SSRC()}}); rtcpErr != nil {
if rtcpErr := peerConnection.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: uint32(track.SSRC())}}); rtcpErr != nil {
fmt.Println(rtcpErr)
}
}
@@ -128,6 +135,9 @@ func main() {
}
})
// Create context
ctx, cancel := context.WithCancel(context.Background())
// Set the handler for ICE connection state
// This will notify you when the peer has connected/disconnected
peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {

View File

@@ -10,34 +10,7 @@ import (
)
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{
peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
{
URLs: []string{"stun:stun.l.google.com:19302"},
@@ -74,8 +47,8 @@ func main() {
panic(err)
}
// Create a video track, using the same SSRC as the incoming RTP Packet
videoTrack, err := peerConnection.NewTrack(payloadType, packet.SSRC, "video", "pion")
// Create a video track
videoTrack, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
if err != nil {
panic(err)
}
@@ -89,6 +62,10 @@ func main() {
fmt.Printf("Connection State has changed %s \n", connectionState.String())
})
// Wait for the offer to be pasted
offer := webrtc.SessionDescription{}
signal.Decode(signal.MustReadStdin(), &offer)
// Set the remote SessionDescription
if err = peerConnection.SetRemoteDescription(offer); err != nil {
panic(err)
@@ -124,13 +101,7 @@ func main() {
panic(err)
}
packet := &rtp.Packet{}
if err := packet.Unmarshal(inboundRTPPacket[:n]); err != nil {
panic(err)
}
packet.Header.PayloadType = payloadType
if writeErr := videoTrack.WriteRTP(packet); writeErr != nil {
if _, writeErr := videoTrack.Write(inboundRTPPacket[:n]); writeErr != nil {
panic(writeErr)
}
}

View File

@@ -13,7 +13,7 @@ import (
"github.com/pion/webrtc/v3/pkg/media/oggwriter"
)
func saveToDisk(i media.Writer, track *webrtc.Track) {
func saveToDisk(i media.Writer, track *webrtc.TrackRemote) {
defer func() {
if err := i.Close(); err != nil {
panic(err)
@@ -32,19 +32,29 @@ func saveToDisk(i media.Writer, track *webrtc.Track) {
}
func main() {
// Everything below is the Pion WebRTC API! Thanks for using it ❤️.
// Create a MediaEngine object to configure the supported codec
m := webrtc.MediaEngine{}
// Setup the codecs you want to use.
// We'll use a VP8 codec but you can also define your own
m.RegisterCodec(webrtc.NewRTPOpusCodec(webrtc.DefaultPayloadTypeOpus, 48000))
m.RegisterCodec(webrtc.NewRTPVP8Codec(webrtc.DefaultPayloadTypeVP8, 90000))
// We'll use a VP8 and Opus but you can also define your own
if err := m.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "video/VP8", ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil},
PayloadType: 96,
}, webrtc.RTPCodecTypeVideo); err != nil {
panic(err)
}
if err := m.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "audio/opus", ClockRate: 48000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil},
PayloadType: 111,
}, webrtc.RTPCodecTypeAudio); err != nil {
panic(err)
}
// Create the API object with the MediaEngine
api := webrtc.NewAPI(webrtc.WithMediaEngine(m))
// Everything below is the Pion WebRTC API! Thanks for using it ❤️.
// Prepare the configuration
config := webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
@@ -79,12 +89,12 @@ func main() {
// Set a handler for when a new remote track starts, this handler saves buffers to disk as
// an ivf file, since we could have multiple video tracks we provide a counter.
// In your application this is where you would handle/process video
peerConnection.OnTrack(func(track *webrtc.Track, receiver *webrtc.RTPReceiver) {
peerConnection.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
// Send a PLI on an interval so that the publisher is pushing a keyframe every rtcpPLIInterval
go func() {
ticker := time.NewTicker(time.Second * 3)
for range ticker.C {
errSend := peerConnection.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: track.SSRC()}})
errSend := peerConnection.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: uint32(track.SSRC())}})
if errSend != nil {
fmt.Println(errSend)
}
@@ -92,10 +102,10 @@ func main() {
}()
codec := track.Codec()
if codec.Name == webrtc.Opus {
if codec.MimeType == "audio/opus" {
fmt.Println("Got Opus track, saving to disk as output.opus (48 kHz, 2 channels)")
saveToDisk(oggFile, track)
} else if codec.Name == webrtc.VP8 {
} else if codec.MimeType == "video/VP8" {
fmt.Println("Got VP8 track, saving to disk as output.ivf")
saveToDisk(ivfFile, track)
}

View File

@@ -4,12 +4,9 @@ import (
"errors"
"fmt"
"io"
"net/url"
"time"
"github.com/pion/randutil"
"github.com/pion/rtcp"
"github.com/pion/sdp/v3"
"github.com/pion/webrtc/v3"
"github.com/pion/webrtc/v3/examples/internal/signal"
)
@@ -17,46 +14,6 @@ import (
func main() {
// Everything below is the Pion WebRTC API! Thanks for using it ❤️.
// 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. Since we are echoing their RTP packet
// back to them we are actually codec agnostic - we can accept all their codecs. This also ensures that we use the
// dynamic media type from the sender in our answer.
mediaEngine := webrtc.MediaEngine{}
// Add codecs to the mediaEngine. Note that even though we are only going to echo back the sender's video we also
// add audio codecs. This is because createAnswer will create an audioTransceiver and associated SDP and we currently
// cannot tell it not to. The audio SDP must match the sender's codecs too...
err := mediaEngine.PopulateFromSDP(offer)
if err != nil {
panic(err)
}
videoCodecs := mediaEngine.GetCodecsByKind(webrtc.RTPCodecTypeVideo)
if len(videoCodecs) == 0 {
panic("Offer contained no video codecs")
}
// Configure required extensions
sdes, _ := url.Parse(sdp.SDESRTPStreamIDURI)
sdedMid, _ := url.Parse(sdp.SDESMidURI)
exts := []sdp.ExtMap{
{
URI: sdes,
},
{
URI: sdedMid,
},
}
se := webrtc.SettingEngine{}
se.AddSDPExtensions(webrtc.SDPSectionVideo, exts)
api := webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine), webrtc.WithSettingEngine((se)))
// Prepare the configuration
config := webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
@@ -66,27 +23,27 @@ func main() {
},
}
// Create a new RTCPeerConnection
peerConnection, err := api.NewPeerConnection(config)
peerConnection, err := webrtc.NewPeerConnection(config)
if err != nil {
panic(err)
}
outputTracks := map[string]*webrtc.Track{}
outputTracks := map[string]*webrtc.TrackLocalStaticRTP{}
// Create Track that we send video back to browser on
outputTrack, err := peerConnection.NewTrack(videoCodecs[0].PayloadType, randutil.NewMathRandomGenerator().Uint32(), "video_q", "pion_q")
outputTrack, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: "video/vp8"}, "video_q", "pion_q")
if err != nil {
panic(err)
}
outputTracks["q"] = outputTrack
outputTrack, err = peerConnection.NewTrack(videoCodecs[0].PayloadType, randutil.NewMathRandomGenerator().Uint32(), "video_h", "pion_h")
outputTrack, err = webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: "video/vp8"}, "video_h", "pion_h")
if err != nil {
panic(err)
}
outputTracks["h"] = outputTrack
outputTrack, err = peerConnection.NewTrack(videoCodecs[0].PayloadType, randutil.NewMathRandomGenerator().Uint32(), "video_f", "pion_f")
outputTrack, err = webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: "video/vp8"}, "video_f", "pion_f")
if err != nil {
panic(err)
}
@@ -103,12 +60,16 @@ func main() {
panic(err)
}
// Wait for the offer to be pasted
offer := webrtc.SessionDescription{}
signal.Decode(signal.MustReadStdin(), &offer)
if err = peerConnection.SetRemoteDescription(offer); err != nil {
panic(err)
}
// Set a handler for when a new remote track starts
peerConnection.OnTrack(func(track *webrtc.Track, receiver *webrtc.RTPReceiver) {
peerConnection.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
fmt.Println("Track has started")
// Start reading from all the streams and sending them to the related output track
@@ -117,12 +78,12 @@ func main() {
ticker := time.NewTicker(3 * time.Second)
for range ticker.C {
fmt.Printf("Sending pli for stream with rid: %q, ssrc: %d\n", track.RID(), track.SSRC())
if writeErr := peerConnection.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: track.SSRC()}}); writeErr != nil {
if writeErr := peerConnection.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: uint32(track.SSRC())}}); writeErr != nil {
fmt.Println(writeErr)
}
// Send a remb message with a very high bandwidth to trigger chrome to send also the high bitrate stream
fmt.Printf("Sending remb for stream with rid: %q, ssrc: %d\n", track.RID(), track.SSRC())
if writeErr := peerConnection.WriteRTCP([]rtcp.Packet{&rtcp.ReceiverEstimatedMaximumBitrate{Bitrate: 10000000, SenderSSRC: track.SSRC()}}); writeErr != nil {
if writeErr := peerConnection.WriteRTCP([]rtcp.Packet{&rtcp.ReceiverEstimatedMaximumBitrate{Bitrate: 10000000, SenderSSRC: uint32(track.SSRC())}}); writeErr != nil {
fmt.Println(writeErr)
}
}
@@ -134,8 +95,6 @@ func main() {
panic(readErr)
}
packet.SSRC = outputTracks[rid].SSRC()
if writeErr := outputTracks[rid].WriteRTP(packet); writeErr != nil && !errors.Is(writeErr, io.ErrClosedPipe) {
panic(writeErr)
}

View File

@@ -6,7 +6,6 @@ import (
"io"
"time"
"github.com/pion/randutil"
"github.com/pion/rtcp"
"github.com/pion/rtp"
"github.com/pion/webrtc/v3"
@@ -16,30 +15,6 @@ import (
func main() { // nolint:gocognit
// Everything below is the Pion WebRTC API! Thanks for using it ❤️.
// 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. Since we are echoing their RTP packet
// back to them we are actually codec agnostic - we can accept all their codecs. This also ensures that we use the
// dynamic media type from the sender in our answer.
mediaEngine := webrtc.MediaEngine{}
// Add codecs to the mediaEngine. Note that even though we are only going to echo back the sender's video we also
// add audio codecs. This is because createAnswer will create an audioTransceiver and associated SDP and we currently
// cannot tell it not to. The audio SDP must match the sender's codecs too...
err := mediaEngine.PopulateFromSDP(offer)
if err != nil {
panic(err)
}
videoCodecs := mediaEngine.GetCodecsByKind(webrtc.RTPCodecTypeVideo)
if len(videoCodecs) == 0 {
panic("Offer contained no video codecs")
}
api := webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine))
// Prepare the configuration
config := webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
@@ -49,13 +24,13 @@ func main() { // nolint:gocognit
},
}
// Create a new RTCPeerConnection
peerConnection, err := api.NewPeerConnection(config)
peerConnection, err := webrtc.NewPeerConnection(config)
if err != nil {
panic(err)
}
// Create Track that we send video back to browser on
outputTrack, err := peerConnection.NewTrack(videoCodecs[0].PayloadType, randutil.NewMathRandomGenerator().Uint32(), "video", "pion")
outputTrack, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
if err != nil {
panic(err)
}
@@ -78,6 +53,10 @@ func main() { // nolint:gocognit
panic(err)
}
// Wait for the offer to be pasted
offer := webrtc.SessionDescription{}
signal.Decode(signal.MustReadStdin(), &offer)
// Set the remote SessionDescription
err = peerConnection.SetRemoteDescription(offer)
if err != nil {
@@ -92,8 +71,8 @@ func main() { // nolint:gocognit
packets := make(chan *rtp.Packet, 60)
// Set a handler for when a new remote track starts
peerConnection.OnTrack(func(track *webrtc.Track, receiver *webrtc.RTPReceiver) {
fmt.Printf("Track has started, of type %d: %s \n", track.PayloadType(), track.Codec().Name)
peerConnection.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
fmt.Printf("Track has started, of type %d: %s \n", track.PayloadType(), track.Codec().MimeType)
trackNum := trackCount
trackCount++
// The last timestamp so that we can change the packet to only be the delta
@@ -123,7 +102,7 @@ func main() { // nolint:gocognit
// If just switched to this track, send PLI to get picture refresh
if !isCurrTrack {
isCurrTrack = true
if writeErr := peerConnection.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: track.SSRC()}}); writeErr != nil {
if writeErr := peerConnection.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: uint32(track.SSRC())}}); writeErr != nil {
fmt.Println(writeErr)
}
}
@@ -170,8 +149,6 @@ func main() { // nolint:gocognit
// Timestamp on the packet is really a diff, so add it to current
currTimestamp += packet.Timestamp
packet.Timestamp = currTimestamp
// Set the output SSRC
packet.SSRC = outputTrack.SSRC()
// Keep an increasing sequence number
packet.SequenceNumber = i
// Write out the packet, ignoring closed pipe if nobody is listening

View File

@@ -13,422 +13,420 @@ import (
"github.com/pion/sdp/v3"
)
// PayloadTypes for the default codecs
const (
DefaultPayloadTypePCMU = 0
DefaultPayloadTypePCMA = 8
DefaultPayloadTypeG722 = 9
DefaultPayloadTypeOpus = 111
DefaultPayloadTypeVP8 = 96
DefaultPayloadTypeVP9 = 98
DefaultPayloadTypeH264 = 102
mediaNameAudio = "audio"
mediaNameVideo = "video"
mimeTypeH264 = "video/h264"
mimeTypeOpus = "audio/opus"
mimeTypeVP8 = "video/vp8"
mimeTypeVP9 = "video/vp9"
mimeTypeG722 = "audio/G722"
mimeTypePCMU = "audio/PCMU"
mimeTypePCMA = "audio/PCMA"
)
// A MediaEngine defines the codecs supported by a PeerConnection.
// MediaEngines populated using RegisterCodec (and RegisterDefaultCodecs)
// may be set up once and reused, including concurrently,
// as long as no other codecs are added subsequently.
// MediaEngines populated using PopulateFromSDP should be used
// only for that session.
type MediaEngine struct {
codecs []*RTPCodec
type mediaEngineHeaderExtension struct {
uri string
isAudio, isVideo bool
}
// RegisterCodec adds codec to m.
// RegisterCodec is not safe for concurrent use.
func (m *MediaEngine) RegisterCodec(codec *RTPCodec) uint8 {
// nolint:godox
// TODO: dynamically generate a payload type in the range 96-127 if one wasn't provided.
// See https://github.com/pion/webrtc/issues/43
m.codecs = append(m.codecs, codec)
return codec.PayloadType
// A MediaEngine defines the codecs supported by a PeerConnection, and the
// configuration of those codecs. A MediaEngine must not be shared between
// PeerConnections.
type MediaEngine struct {
// If we have attempted to negotiate a codec type yet.
negotiatedVideo, negotiatedAudio bool
videoCodecs, audioCodecs []RTPCodecParameters
negotiatedVideoCodecs, negotiatedAudioCodecs []RTPCodecParameters
headerExtensions []mediaEngineHeaderExtension
negotiatedHeaderExtensions map[int]mediaEngineHeaderExtension
}
// RegisterDefaultCodecs registers the default codecs supported by Pion WebRTC.
// RegisterDefaultCodecs is not safe for concurrent use.
func (m *MediaEngine) RegisterDefaultCodecs() {
// Audio Codecs in descending order of preference
m.RegisterCodec(NewRTPOpusCodec(DefaultPayloadTypeOpus, 48000))
m.RegisterCodec(NewRTPPCMUCodec(DefaultPayloadTypePCMU, 8000))
m.RegisterCodec(NewRTPPCMACodec(DefaultPayloadTypePCMA, 8000))
m.RegisterCodec(NewRTPG722Codec(DefaultPayloadTypeG722, 8000))
// Video Codecs in descending order of preference
m.RegisterCodec(NewRTPVP8Codec(DefaultPayloadTypeVP8, 90000))
m.RegisterCodec(NewRTPVP9Codec(DefaultPayloadTypeVP9, 90000))
m.RegisterCodec(NewRTPH264Codec(DefaultPayloadTypeH264, 90000))
}
// PopulateFromSDP finds all codecs in sd and adds them to m, using the dynamic
// payload types and parameters from sd.
// PopulateFromSDP is intended for use when answering a request.
// The offerer sets the PayloadTypes for the connection.
// PopulateFromSDP allows an answerer to properly match the PayloadTypes from the offerer.
// A MediaEngine populated by PopulateFromSDP should be used only for a single session.
func (m *MediaEngine) PopulateFromSDP(sd SessionDescription) error {
sdp := sdp.SessionDescription{}
if err := sdp.Unmarshal([]byte(sd.SDP)); err != nil {
return err
func (m *MediaEngine) RegisterDefaultCodecs() error {
// Default Pion Audio Codecs
for _, codec := range []RTPCodecParameters{
{
RTPCodecCapability: RTPCodecCapability{mimeTypeOpus, 48000, 2, "minptime=10;useinbandfec=1", nil},
PayloadType: 111,
},
{
RTPCodecCapability: RTPCodecCapability{mimeTypeG722, 8000, 0, "", nil},
PayloadType: 9,
},
{
RTPCodecCapability: RTPCodecCapability{mimeTypePCMU, 8000, 0, "", nil},
PayloadType: 0,
},
{
RTPCodecCapability: RTPCodecCapability{mimeTypePCMA, 8000, 0, "", nil},
PayloadType: 8,
},
} {
if err := m.RegisterCodec(codec, RTPCodecTypeAudio); err != nil {
return err
}
}
for _, md := range sdp.MediaDescriptions {
if md.MediaName.Media != mediaNameAudio && md.MediaName.Media != mediaNameVideo {
continue
// Default Pion Audio Header Extensions
for _, extension := range []string{
"urn:ietf:params:rtp-hdrext:sdes:mid",
"urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id",
"urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id",
} {
if err := m.RegisterHeaderExtension(RTPHeaderExtensionCapability{extension}, RTPCodecTypeAudio); err != nil {
return err
}
}
videoRTCPFeedback := []RTCPFeedback{{"goog-remb", ""}, {"ccm", "fir"}, {"nack", ""}, {"nack", "pli"}}
for _, codec := range []RTPCodecParameters{
{
RTPCodecCapability: RTPCodecCapability{mimeTypeVP8, 90000, 0, "", videoRTCPFeedback},
PayloadType: 96,
},
{
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=96", nil},
PayloadType: 97,
},
{
RTPCodecCapability: RTPCodecCapability{mimeTypeVP9, 90000, 0, "profile-id=0", videoRTCPFeedback},
PayloadType: 98,
},
{
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=98", nil},
PayloadType: 99,
},
{
RTPCodecCapability: RTPCodecCapability{mimeTypeVP9, 90000, 0, "profile-id=1", videoRTCPFeedback},
PayloadType: 100,
},
{
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=100", nil},
PayloadType: 101,
},
{
RTPCodecCapability: RTPCodecCapability{mimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f", videoRTCPFeedback},
PayloadType: 102,
},
{
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=102", nil},
PayloadType: 121,
},
{
RTPCodecCapability: RTPCodecCapability{mimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f", videoRTCPFeedback},
PayloadType: 127,
},
{
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=127", nil},
PayloadType: 120,
},
{
RTPCodecCapability: RTPCodecCapability{mimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", videoRTCPFeedback},
PayloadType: 125,
},
{
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=125", nil},
PayloadType: 107,
},
{
RTPCodecCapability: RTPCodecCapability{mimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f", videoRTCPFeedback},
PayloadType: 108,
},
{
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=108", nil},
PayloadType: 109,
},
{
RTPCodecCapability: RTPCodecCapability{mimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f", videoRTCPFeedback},
PayloadType: 127,
},
{
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=127", nil},
PayloadType: 120,
},
{
RTPCodecCapability: RTPCodecCapability{mimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032", videoRTCPFeedback},
PayloadType: 123,
},
{
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=123", nil},
PayloadType: 118,
},
{
RTPCodecCapability: RTPCodecCapability{"video/ulpfec", 90000, 0, "", nil},
PayloadType: 116,
},
} {
if err := m.RegisterCodec(codec, RTPCodecTypeVideo); err != nil {
return err
}
}
// Default Pion Video Header Extensions
for _, extension := range []string{
"urn:ietf:params:rtp-hdrext:sdes:mid",
"urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id",
"urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id",
} {
if err := m.RegisterHeaderExtension(RTPHeaderExtensionCapability{extension}, RTPCodecTypeVideo); err != nil {
return err
}
}
return nil
}
// RegisterCodec adds codec to the MediaEngine
// These are the list of codecs supported by this PeerConnection.
// RegisterCodec is not safe for concurrent use.
func (m *MediaEngine) RegisterCodec(codec RTPCodecParameters, typ RTPCodecType) error {
codec.statsID = fmt.Sprintf("RTPCodec-%d", time.Now().UnixNano())
switch typ {
case RTPCodecTypeAudio:
m.audioCodecs = append(m.audioCodecs, codec)
case RTPCodecTypeVideo:
m.videoCodecs = append(m.videoCodecs, codec)
default:
return ErrUnknownType
}
return nil
}
// RegisterHeaderExtension adds a header extension to the MediaEngine
// To determine the negotiated value use `GetHeaderExtensionID` after signaling is complete
func (m *MediaEngine) RegisterHeaderExtension(extension RTPHeaderExtensionCapability, typ RTPCodecType) error {
extensionIndex := -1
for i := range m.headerExtensions {
if extension.URI == m.headerExtensions[i].uri {
extensionIndex = i
}
}
if extensionIndex == -1 {
m.headerExtensions = append(m.headerExtensions, mediaEngineHeaderExtension{})
extensionIndex = len(m.headerExtensions) - 1
}
if typ == RTPCodecTypeAudio {
m.headerExtensions[extensionIndex].isAudio = true
} else if typ == RTPCodecTypeVideo {
m.headerExtensions[extensionIndex].isVideo = true
}
m.headerExtensions[extensionIndex].uri = extension.URI
return nil
}
// GetHeaderExtensionID returns the negotiated ID for a header extension.
// If the Header Extension isn't enabled ok will be false
func (m *MediaEngine) GetHeaderExtensionID(extension RTPHeaderExtensionCapability) (val int, audioNegotiated, videoNegotiated bool) {
if m.negotiatedHeaderExtensions == nil {
m.negotiatedHeaderExtensions = map[int]mediaEngineHeaderExtension{}
}
for id, h := range m.negotiatedHeaderExtensions {
if extension.URI == h.uri {
return id, h.isAudio, h.isVideo
}
}
return
}
func (m *MediaEngine) getCodecByPayload(payloadType PayloadType) (RTPCodecParameters, error) {
for _, codec := range m.negotiatedVideoCodecs {
if codec.PayloadType == payloadType {
return codec, nil
}
}
for _, codec := range m.negotiatedAudioCodecs {
if codec.PayloadType == payloadType {
return codec, nil
}
}
return RTPCodecParameters{}, ErrCodecNotFound
}
func (m *MediaEngine) collectStats(collector *statsReportCollector) {
statsLoop := func(codecs []RTPCodecParameters) {
for _, codec := range codecs {
collector.Collecting()
stats := CodecStats{
Timestamp: statsTimestampFrom(time.Now()),
Type: StatsTypeCodec,
ID: codec.statsID,
PayloadType: codec.PayloadType,
MimeType: codec.MimeType,
ClockRate: codec.ClockRate,
Channels: uint8(codec.Channels),
SDPFmtpLine: codec.SDPFmtpLine,
}
collector.Collect(stats.ID, stats)
}
}
statsLoop(m.videoCodecs)
statsLoop(m.audioCodecs)
}
// Look up a codec and enable if it exists
func (m *MediaEngine) updateCodecParameters(remoteCodec RTPCodecParameters, typ RTPCodecType) error {
codecs := m.videoCodecs
if typ == RTPCodecTypeAudio {
codecs = m.audioCodecs
}
pushCodec := func(codec RTPCodecParameters) error {
if typ == RTPCodecTypeAudio {
m.negotiatedAudioCodecs = append(m.negotiatedAudioCodecs, codec)
} else if typ == RTPCodecTypeVideo {
m.negotiatedVideoCodecs = append(m.negotiatedVideoCodecs, codec)
}
return nil
}
if strings.HasPrefix(remoteCodec.RTPCodecCapability.SDPFmtpLine, "apt=") {
payloadType, err := strconv.Atoi(strings.TrimPrefix(remoteCodec.RTPCodecCapability.SDPFmtpLine, "apt="))
if err != nil {
return err
}
for _, format := range md.MediaName.Formats {
pt, err := strconv.Atoi(format)
if err != nil {
return errMediaEngineParseError
if _, err = m.getCodecByPayload(PayloadType(payloadType)); err != nil {
return nil // not an error, we just ignore this codec we don't support
}
}
if _, err := codecParametersFuzzySearch(remoteCodec, codecs); err == nil {
return pushCodec(remoteCodec)
}
return nil
}
// Look up a header extension and enable if it exists
func (m *MediaEngine) updateHeaderExtension(id int, extension string, typ RTPCodecType) error {
if m.negotiatedHeaderExtensions == nil {
m.negotiatedHeaderExtensions = map[int]mediaEngineHeaderExtension{}
}
for _, localExtension := range m.headerExtensions {
if localExtension.uri == extension {
h := mediaEngineHeaderExtension{uri: extension}
if existingValue, ok := m.negotiatedHeaderExtensions[id]; ok {
h = existingValue
}
payloadType := uint8(pt)
payloadCodec, err := sdp.GetCodecForPayloadType(payloadType)
if err != nil {
return fmt.Errorf("%w: codec for payload type %d", errMediaEngineCodecNotFound, payloadType)
}
var codec *RTPCodec
switch {
case strings.EqualFold(payloadCodec.Name, PCMA):
codec = NewRTPPCMACodec(payloadType, payloadCodec.ClockRate)
case strings.EqualFold(payloadCodec.Name, PCMU):
codec = NewRTPPCMUCodec(payloadType, payloadCodec.ClockRate)
case strings.EqualFold(payloadCodec.Name, G722):
codec = NewRTPG722Codec(payloadType, payloadCodec.ClockRate)
case strings.EqualFold(payloadCodec.Name, Opus):
codec = NewRTPOpusCodec(payloadType, payloadCodec.ClockRate)
case strings.EqualFold(payloadCodec.Name, VP8):
codec = NewRTPVP8Codec(payloadType, payloadCodec.ClockRate)
case strings.EqualFold(payloadCodec.Name, VP9):
codec = NewRTPVP9Codec(payloadType, payloadCodec.ClockRate)
case strings.EqualFold(payloadCodec.Name, H264):
codec = NewRTPH264Codec(payloadType, payloadCodec.ClockRate)
default:
// ignoring other codecs
continue
case localExtension.isAudio && typ == RTPCodecTypeAudio:
h.isAudio = true
case localExtension.isVideo && typ == RTPCodecTypeVideo:
h.isVideo = true
}
codec.SDPFmtpLine = payloadCodec.Fmtp
m.RegisterCodec(codec)
m.negotiatedHeaderExtensions[id] = h
}
}
return nil
}
// GetCodecsByName returns all codecs by name that are supported by m.
// The returned codecs should not be modified.
func (m *MediaEngine) GetCodecsByName(codecName string) []*RTPCodec {
var codecs []*RTPCodec
for _, codec := range m.codecs {
if strings.EqualFold(codec.Name, codecName) {
codecs = append(codecs, codec)
// Update the MediaEngine from a remote description
func (m *MediaEngine) updateFromRemoteDescription(desc sdp.SessionDescription) error {
for _, media := range desc.MediaDescriptions {
var typ RTPCodecType
switch {
case !m.negotiatedAudio && strings.EqualFold(media.MediaName.Media, "audio"):
m.negotiatedAudio = true
typ = RTPCodecTypeAudio
case !m.negotiatedVideo && strings.EqualFold(media.MediaName.Media, "video"):
m.negotiatedVideo = true
typ = RTPCodecTypeVideo
default:
continue
}
codecs, err := codecsFromMediaDescription(media)
if err != nil {
return err
}
for _, codec := range codecs {
if err = m.updateCodecParameters(codec, typ); err != nil {
return err
}
}
extensions, err := rtpExtensionsFromMediaDescription(media)
if err != nil {
return err
}
for id, extension := range extensions {
if err = m.updateHeaderExtension(extension, id, typ); err != nil {
return err
}
}
}
return codecs
return nil
}
func (m *MediaEngine) getCodec(payloadType uint8) (*RTPCodec, error) {
for _, codec := range m.codecs {
if codec.PayloadType == payloadType {
return codec, nil
func (m *MediaEngine) getCodecsByKind(typ RTPCodecType) []RTPCodecParameters {
if typ == RTPCodecTypeVideo {
if m.negotiatedVideo {
return m.negotiatedVideoCodecs
}
return m.videoCodecs
} else if typ == RTPCodecTypeAudio {
if m.negotiatedAudio {
return m.negotiatedAudioCodecs
}
return m.audioCodecs
}
return nil
}
func (m *MediaEngine) negotiatedHeaderExtensionsForType(typ RTPCodecType) map[int]mediaEngineHeaderExtension {
headerExtensions := map[int]mediaEngineHeaderExtension{}
for id, e := range m.negotiatedHeaderExtensions {
if e.isAudio && typ == RTPCodecTypeAudio || e.isVideo && typ == RTPCodecTypeVideo {
headerExtensions[id] = e
}
}
return nil, ErrCodecNotFound
return headerExtensions
}
func (m *MediaEngine) getCodecSDP(sdpCodec sdp.Codec) (*RTPCodec, error) {
for _, codec := range m.codecs {
if strings.EqualFold(codec.Name, sdpCodec.Name) &&
codec.ClockRate == sdpCodec.ClockRate &&
(sdpCodec.EncodingParameters == "" ||
strconv.Itoa(int(codec.Channels)) == sdpCodec.EncodingParameters) &&
codec.SDPFmtpLine == sdpCodec.Fmtp { // pion/webrtc#43
return codec, nil
}
}
return nil, ErrCodecNotFound
}
// GetCodecsByKind returns all codecs of kind kind that are supported by m.
// The returned codecs should not be modified.
func (m *MediaEngine) GetCodecsByKind(kind RTPCodecType) []*RTPCodec {
var codecs []*RTPCodec
for _, codec := range m.codecs {
if codec.Type == kind {
codecs = append(codecs, codec)
}
}
return codecs
}
// Names for the default codecs supported by Pion WebRTC
const (
PCMU = "PCMU"
PCMA = "PCMA"
G722 = "G722"
Opus = "opus"
VP8 = "VP8"
VP9 = "VP9"
H264 = "H264"
)
// NewRTPPCMUCodec is a helper to create a PCMU codec
func NewRTPPCMUCodec(payloadType uint8, clockrate uint32) *RTPCodec {
c := NewRTPCodec(RTPCodecTypeAudio,
PCMU,
clockrate,
0,
"",
payloadType,
&codecs.G711Payloader{})
return c
}
// NewRTPPCMACodec is a helper to create a PCMA codec
func NewRTPPCMACodec(payloadType uint8, clockrate uint32) *RTPCodec {
c := NewRTPCodec(RTPCodecTypeAudio,
PCMA,
clockrate,
0,
"",
payloadType,
&codecs.G711Payloader{})
return c
}
// NewRTPG722Codec is a helper to create a G722 codec
func NewRTPG722Codec(payloadType uint8, clockrate uint32) *RTPCodec {
c := NewRTPCodec(RTPCodecTypeAudio,
G722,
clockrate,
0,
"",
payloadType,
&codecs.G722Payloader{})
return c
}
// NewRTPOpusCodec is a helper to create an Opus codec
func NewRTPOpusCodec(payloadType uint8, clockrate uint32) *RTPCodec {
c := NewRTPCodec(RTPCodecTypeAudio,
Opus,
clockrate,
2, // According to RFC7587, Opus RTP streams must have exactly 2 channels.
"minptime=10;useinbandfec=1",
payloadType,
&codecs.OpusPayloader{})
return c
}
// NewRTPVP8Codec is a helper to create an VP8 codec
func NewRTPVP8Codec(payloadType uint8, clockrate uint32) *RTPCodec {
c := NewRTPCodec(RTPCodecTypeVideo,
VP8,
clockrate,
0,
"",
payloadType,
&codecs.VP8Payloader{})
return c
}
// NewRTPVP8CodecExt is a helper to create an VP8 codec
func NewRTPVP8CodecExt(payloadType uint8, clockrate uint32, rtcpfb []RTCPFeedback, fmtp string) *RTPCodec {
c := NewRTPCodecExt(RTPCodecTypeVideo,
VP8,
clockrate,
0,
fmtp,
payloadType,
rtcpfb,
&codecs.VP8Payloader{})
return c
}
// NewRTPVP9Codec is a helper to create an VP9 codec
func NewRTPVP9Codec(payloadType uint8, clockrate uint32) *RTPCodec {
c := NewRTPCodec(RTPCodecTypeVideo,
VP9,
clockrate,
0,
"",
payloadType,
&codecs.VP9Payloader{})
return c
}
// NewRTPVP9CodecExt is a helper to create an VP8 codec
func NewRTPVP9CodecExt(payloadType uint8, clockrate uint32, rtcpfb []RTCPFeedback, fmtp string) *RTPCodec {
c := NewRTPCodecExt(RTPCodecTypeVideo,
VP9,
clockrate,
0,
fmtp,
payloadType,
rtcpfb,
&codecs.VP9Payloader{})
return c
}
// NewRTPH264Codec is a helper to create an H264 codec
func NewRTPH264Codec(payloadType uint8, clockrate uint32) *RTPCodec {
c := NewRTPCodec(RTPCodecTypeVideo,
H264,
clockrate,
0,
"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f",
payloadType,
&codecs.H264Payloader{})
return c
}
// NewRTPH264CodecExt is a helper to create an H264 codec
func NewRTPH264CodecExt(payloadType uint8, clockrate uint32, rtcpfb []RTCPFeedback, fmtp string) *RTPCodec {
c := NewRTPCodecExt(RTPCodecTypeVideo,
H264,
clockrate,
0,
fmtp,
payloadType,
rtcpfb,
&codecs.H264Payloader{})
return c
}
// RTPCodecType determines the type of a codec
type RTPCodecType int
const (
// RTPCodecTypeAudio indicates this is an audio codec
RTPCodecTypeAudio RTPCodecType = iota + 1
// RTPCodecTypeVideo indicates this is a video codec
RTPCodecTypeVideo
)
func (t RTPCodecType) String() string {
switch t {
case RTPCodecTypeAudio:
return "audio"
case RTPCodecTypeVideo:
return "video"
func payloaderForCodec(codec RTPCodecCapability) (rtp.Payloader, error) {
switch strings.ToLower(codec.MimeType) {
case mimeTypeH264:
return &codecs.H264Payloader{}, nil
case mimeTypeOpus:
return &codecs.OpusPayloader{}, nil
case mimeTypeVP8:
return &codecs.VP8Payloader{}, nil
case mimeTypeVP9:
return &codecs.VP9Payloader{}, nil
case mimeTypeG722:
return &codecs.G722Payloader{}, nil
case mimeTypePCMU, mimeTypePCMA:
return &codecs.G711Payloader{}, nil
default:
return ErrUnknownType.Error()
}
}
// NewRTPCodecType creates a RTPCodecType from a string
func NewRTPCodecType(r string) RTPCodecType {
switch {
case strings.EqualFold(r, "audio"):
return RTPCodecTypeAudio
case strings.EqualFold(r, "video"):
return RTPCodecTypeVideo
default:
return RTPCodecType(0)
}
}
// RTPCodec represents a codec supported by the PeerConnection
type RTPCodec struct {
RTPCodecCapability
Type RTPCodecType
Name string
PayloadType uint8
Payloader rtp.Payloader
statsID string
}
// NewRTPCodec is used to define a new codec
func NewRTPCodec(
codecType RTPCodecType,
name string,
clockrate uint32,
channels uint16,
fmtp string,
payloadType uint8,
payloader rtp.Payloader,
) *RTPCodec {
return &RTPCodec{
RTPCodecCapability: RTPCodecCapability{
MimeType: codecType.String() + "/" + name,
ClockRate: clockrate,
Channels: channels,
SDPFmtpLine: fmtp,
},
PayloadType: payloadType,
Payloader: payloader,
Type: codecType,
Name: name,
statsID: fmt.Sprintf("RTPCodec-%d", time.Now().UnixNano()),
}
}
// NewRTPCodecExt is used to define a new codec
func NewRTPCodecExt(
codecType RTPCodecType,
name string,
clockrate uint32,
channels uint16,
fmtp string,
payloadType uint8,
rtcpfb []RTCPFeedback,
payloader rtp.Payloader,
) *RTPCodec {
return &RTPCodec{
RTPCodecCapability: RTPCodecCapability{
MimeType: codecType.String() + "/" + name,
ClockRate: clockrate,
Channels: channels,
SDPFmtpLine: fmtp,
RTCPFeedback: rtcpfb,
},
PayloadType: payloadType,
Payloader: payloader,
Type: codecType,
Name: name,
}
}
// RTPCodecCapability provides information about codec capabilities.
type RTPCodecCapability struct {
MimeType string
ClockRate uint32
Channels uint16
SDPFmtpLine string
RTCPFeedback []RTCPFeedback
}
// RTPHeaderExtensionCapability is used to define a RFC5285 RTP header extension supported by the codec.
type RTPHeaderExtensionCapability struct {
URI string
}
// RTPCapabilities represents the capabilities of a transceiver
type RTPCapabilities struct {
Codecs []RTPCodecCapability
HeaderExtensions []RTPHeaderExtensionCapability
}
func (m *MediaEngine) collectStats(collector *statsReportCollector) {
for _, codec := range m.codecs {
collector.Collecting()
stats := CodecStats{
Timestamp: statsTimestampFrom(time.Now()),
Type: StatsTypeCodec,
ID: codec.statsID,
PayloadType: codec.PayloadType,
MimeType: codec.MimeType,
ClockRate: codec.ClockRate,
Channels: uint8(codec.Channels),
SDPFmtpLine: codec.SDPFmtpLine,
}
collector.Collect(stats.ID, stats)
return nil, ErrNoPayloaderForCodec
}
}

View File

@@ -4,98 +4,12 @@ package webrtc
import (
"regexp"
"strings"
"testing"
"github.com/pion/sdp/v3"
"github.com/stretchr/testify/assert"
)
const sdpValue = `v=0
o=- 884433216 1576829404 IN IP4 0.0.0.0
s=-
t=0 0
a=fingerprint:sha-256 1D:6B:6D:18:95:41:F9:BC:E4:AC:25:6A:26:A3:C8:09:D2:8C:EE:1B:7D:54:53:33:F7:E3:2C:0D:FE:7A:9D:6B
a=group:BUNDLE 0 1 2
m=audio 9 UDP/TLS/RTP/SAVPF 0 8 111 9
c=IN IP4 0.0.0.0
a=mid:0
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10;useinbandfec=1
a=rtpmap:9 G722/8000
a=ssrc:1823804162 cname:pion1
a=ssrc:1823804162 msid:pion1 audio
a=ssrc:1823804162 mslabel:pion1
a=ssrc:1823804162 label:audio
a=msid:pion1 audio
m=video 9 UDP/TLS/RTP/SAVPF 105 115 135
c=IN IP4 0.0.0.0
a=mid:1
a=rtpmap:105 VP8/90000
a=rtpmap:115 H264/90000
a=fmtp:115 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
a=rtpmap:135 VP9/90000
a=ssrc:2949882636 cname:pion2
a=ssrc:2949882636 msid:pion2 video
a=ssrc:2949882636 mslabel:pion2
a=ssrc:2949882636 label:video
a=msid:pion2 video
m=application 9 DTLS/SCTP 5000
c=IN IP4 0.0.0.0
a=mid:2
a=sctpmap:5000 webrtc-datachannel 1024
`
func TestCodecRegistration(t *testing.T) {
api := NewAPI()
const invalidPT = 255
api.mediaEngine.RegisterDefaultCodecs()
testCases := []struct {
c uint8
e error
}{
{DefaultPayloadTypePCMU, nil},
{DefaultPayloadTypePCMA, nil},
{DefaultPayloadTypeG722, nil},
{DefaultPayloadTypeOpus, nil},
{DefaultPayloadTypeVP8, nil},
{DefaultPayloadTypeVP9, nil},
{DefaultPayloadTypeH264, nil},
{invalidPT, ErrCodecNotFound},
}
for _, f := range testCases {
_, err := api.mediaEngine.getCodec(f.c)
assert.Equal(t, f.e, err)
}
_, err := api.mediaEngine.getCodecSDP(sdp.Codec{PayloadType: invalidPT})
assert.Equal(t, err, ErrCodecNotFound)
}
func TestPopulateFromSDP(t *testing.T) {
m := MediaEngine{}
assertCodecWithPayloadType := func(name string, payloadType uint8) {
for _, c := range m.codecs {
if c.PayloadType == payloadType && c.Name == name {
return
}
}
t.Fatalf("Failed to find codec(%s) with PayloadType(%d)", name, payloadType)
}
m.RegisterDefaultCodecs()
assert.NoError(t, m.PopulateFromSDP(SessionDescription{SDP: sdpValue}))
assertCodecWithPayloadType(Opus, 111)
assertCodecWithPayloadType(VP8, 105)
assertCodecWithPayloadType(H264, 115)
assertCodecWithPayloadType(VP9, 135)
}
// pion/webrtc#1078
func TestOpusCase(t *testing.T) {
pc, err := NewPeerConnection(Configuration{})
@@ -111,61 +25,143 @@ func TestOpusCase(t *testing.T) {
assert.NoError(t, pc.Close())
}
// pion/webrtc#1442
func TestCaseInsensitive(t *testing.T) {
m := MediaEngine{}
m.RegisterDefaultCodecs()
testCases := []struct {
nameUpperCase string
nameLowerCase string
clockrate uint32
fmtp string
}{
{strings.ToUpper(Opus), strings.ToLower(Opus), 48000, "minptime=10;useinbandfec=1"},
{strings.ToUpper(PCMU), strings.ToLower(PCMU), 8000, ""},
{strings.ToUpper(PCMA), strings.ToLower(PCMA), 8000, ""},
{strings.ToUpper(G722), strings.ToLower(G722), 8000, ""},
{strings.ToUpper(VP8), strings.ToLower(VP8), 90000, ""},
{strings.ToUpper(VP9), strings.ToLower(VP9), 90000, ""},
{strings.ToUpper(H264), strings.ToLower(H264), 90000, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"},
func TestMediaEngineRemoteDescription(t *testing.T) {
mustParse := func(raw string) sdp.SessionDescription {
s := sdp.SessionDescription{}
assert.NoError(t, s.Unmarshal([]byte(raw)))
return s
}
for _, f := range testCases {
upperCase, err := m.getCodecSDP(sdp.Codec{
Name: f.nameUpperCase,
ClockRate: f.clockrate,
Fmtp: f.fmtp,
})
t.Run("No Media", func(t *testing.T) {
const noMedia = `v=0
o=- 4596489990601351948 2 IN IP4 127.0.0.1
s=-
t=0 0
`
m := MediaEngine{}
assert.NoError(t, m.RegisterDefaultCodecs())
assert.NoError(t, m.updateFromRemoteDescription(mustParse(noMedia)))
assert.False(t, m.negotiatedVideo)
assert.False(t, m.negotiatedAudio)
})
t.Run("Enable Opus", func(t *testing.T) {
const opusSamePayload = `v=0
o=- 4596489990601351948 2 IN IP4 127.0.0.1
s=-
t=0 0
m=audio 9 UDP/TLS/RTP/SAVPF 111
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10; useinbandfec=1
`
m := MediaEngine{}
assert.NoError(t, m.RegisterDefaultCodecs())
assert.NoError(t, m.updateFromRemoteDescription(mustParse(opusSamePayload)))
assert.False(t, m.negotiatedVideo)
assert.True(t, m.negotiatedAudio)
opusCodec, err := m.getCodecByPayload(111)
assert.NoError(t, err)
assert.Equal(t, opusCodec.MimeType, mimeTypeOpus)
})
lowerCase, err := m.getCodecSDP(sdp.Codec{
Name: f.nameLowerCase,
ClockRate: f.clockrate,
Fmtp: f.fmtp,
})
t.Run("Change Payload Type", func(t *testing.T) {
const opusSamePayload = `v=0
o=- 4596489990601351948 2 IN IP4 127.0.0.1
s=-
t=0 0
m=audio 9 UDP/TLS/RTP/SAVPF 112
a=rtpmap:112 opus/48000/2
a=fmtp:112 minptime=10; useinbandfec=1
`
m := MediaEngine{}
assert.NoError(t, m.RegisterDefaultCodecs())
assert.NoError(t, m.updateFromRemoteDescription(mustParse(opusSamePayload)))
assert.False(t, m.negotiatedVideo)
assert.True(t, m.negotiatedAudio)
_, err := m.getCodecByPayload(111)
assert.Error(t, err)
opusCodec, err := m.getCodecByPayload(112)
assert.NoError(t, err)
assert.Equal(t, opusCodec.MimeType, mimeTypeOpus)
})
assert.Equal(t, upperCase, lowerCase)
}
}
func TestGetCodecsByName(t *testing.T) {
var cdc *RTPCodec
m := MediaEngine{}
assert.NoError(t, m.PopulateFromSDP(SessionDescription{SDP: sdpValue}))
assertGetCodecsByName := func(name string) {
for _, cdc = range m.GetCodecsByName(name) {
if strings.EqualFold(cdc.Name, name) {
return
}
}
t.Fatalf("Failed to getting codec(%s) by name (%s)", cdc.Name, name)
}
assertGetCodecsByName(VP8)
assertGetCodecsByName(H264)
assertGetCodecsByName(VP9)
assertGetCodecsByName(Opus)
t.Run("Case Insensitive", func(t *testing.T) {
const opusUpcase = `v=0
o=- 4596489990601351948 2 IN IP4 127.0.0.1
s=-
t=0 0
m=audio 9 UDP/TLS/RTP/SAVPF 111
a=rtpmap:111 OPUS/48000/2
a=fmtp:111 minptime=10; useinbandfec=1
`
m := MediaEngine{}
assert.NoError(t, m.RegisterDefaultCodecs())
assert.NoError(t, m.updateFromRemoteDescription(mustParse(opusUpcase)))
assert.False(t, m.negotiatedVideo)
assert.True(t, m.negotiatedAudio)
opusCodec, err := m.getCodecByPayload(111)
assert.NoError(t, err)
assert.Equal(t, opusCodec.MimeType, "audio/OPUS")
})
t.Run("Handle different fmtp", func(t *testing.T) {
const opusNoFmtp = `v=0
o=- 4596489990601351948 2 IN IP4 127.0.0.1
s=-
t=0 0
m=audio 9 UDP/TLS/RTP/SAVPF 111
a=rtpmap:111 opus/48000/2
`
m := MediaEngine{}
assert.NoError(t, m.RegisterDefaultCodecs())
assert.NoError(t, m.updateFromRemoteDescription(mustParse(opusNoFmtp)))
assert.False(t, m.negotiatedVideo)
assert.True(t, m.negotiatedAudio)
opusCodec, err := m.getCodecByPayload(111)
assert.NoError(t, err)
assert.Equal(t, opusCodec.MimeType, mimeTypeOpus)
})
t.Run("Header Extensions", func(t *testing.T) {
const headerExtensions = `v=0
o=- 4596489990601351948 2 IN IP4 127.0.0.1
s=-
t=0 0
m=audio 9 UDP/TLS/RTP/SAVPF 111
a=extmap:7 urn:ietf:params:rtp-hdrext:sdes:mid
a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
a=rtpmap:111 opus/48000/2
`
m := MediaEngine{}
assert.NoError(t, m.RegisterDefaultCodecs())
assert.NoError(t, m.updateFromRemoteDescription(mustParse(headerExtensions)))
assert.False(t, m.negotiatedVideo)
assert.True(t, m.negotiatedAudio)
absID, absAudioEnabled, absVideoEnabled := m.GetHeaderExtensionID(RTPHeaderExtensionCapability{sdp.ABSSendTimeURI})
assert.Equal(t, absID, 0)
assert.False(t, absAudioEnabled)
assert.False(t, absVideoEnabled)
midID, midAudioEnabled, midVideoEnabled := m.GetHeaderExtensionID(RTPHeaderExtensionCapability{sdp.SDESMidURI})
assert.Equal(t, midID, 7)
assert.True(t, midAudioEnabled)
assert.False(t, midVideoEnabled)
})
}

View File

@@ -1,6 +1,5 @@
// +build !js
// Package webrtc implements the WebRTC 1.0 as defined in W3C WebRTC specification document.
package webrtc
import (
@@ -65,7 +64,7 @@ type PeerConnection struct {
onSignalingStateChangeHandler func(SignalingState)
onICEConnectionStateChangeHandler func(ICEConnectionState)
onConnectionStateChangeHandler func(PeerConnectionState)
onTrackHandler func(*Track, *RTPReceiver)
onTrackHandler func(*TrackRemote, *RTPReceiver)
onDataChannelHandler func(*DataChannel)
onNegotiationNeededHandler atomic.Value // func()
@@ -83,7 +82,9 @@ type PeerConnection struct {
// codecs. See API.NewPeerConnection for details.
func NewPeerConnection(configuration Configuration) (*PeerConnection, error) {
m := MediaEngine{}
m.RegisterDefaultCodecs()
if err := m.RegisterDefaultCodecs(); err != nil {
return nil, err
}
api := NewAPI(WithMediaEngine(m))
return api.NewPeerConnection(configuration)
}
@@ -353,7 +354,7 @@ func (pc *PeerConnection) checkNegotiationNeeded() bool { //nolint:gocognit
// Step 5.3.1
if t.Direction() == RTPTransceiverDirectionSendrecv || t.Direction() == RTPTransceiverDirectionSendonly {
descMsid, okMsid := m.Attribute(sdp.AttrKeyMsid)
if !okMsid || descMsid != t.Sender().Track().Msid() {
if !okMsid || descMsid != t.Sender().Track().StreamID() {
return true
}
}
@@ -403,13 +404,13 @@ func (pc *PeerConnection) OnICEGatheringStateChange(f func(ICEGathererState)) {
// OnTrack sets an event handler which is called when remote track
// arrives from a remote peer.
func (pc *PeerConnection) OnTrack(f func(*Track, *RTPReceiver)) {
func (pc *PeerConnection) OnTrack(f func(*TrackRemote, *RTPReceiver)) {
pc.mu.Lock()
defer pc.mu.Unlock()
pc.onTrackHandler = f
}
func (pc *PeerConnection) onTrack(t *Track, r *RTPReceiver) {
func (pc *PeerConnection) onTrack(t *TrackRemote, r *RTPReceiver) {
pc.mu.RLock()
handler := pc.onTrackHandler
pc.mu.RUnlock()
@@ -968,6 +969,10 @@ func (pc *PeerConnection) SetRemoteDescription(desc SessionDescription) error {
return err
}
if err := pc.api.mediaEngine.updateFromRemoteDescription(*desc.parsed); err != nil {
return err
}
var t *RTPTransceiver
localTransceivers := append([]*RTPTransceiver{}, pc.GetTransceivers()...)
detectedPlanB := descriptionIsPlanB(pc.RemoteDescription())
@@ -1099,7 +1104,7 @@ func (pc *PeerConnection) startReceiver(incoming trackDetails, receiver *RTPRece
for i := range receiver.tracks {
receiver.tracks[i].track.mu.Lock()
receiver.tracks[i].track.id = incoming.id
receiver.tracks[i].track.label = incoming.label
receiver.tracks[i].track.streamID = incoming.streamID
receiver.tracks[i].track.mu.Unlock()
}
@@ -1114,14 +1119,14 @@ func (pc *PeerConnection) startReceiver(incoming trackDetails, receiver *RTPRece
return
}
codec, err := pc.api.mediaEngine.getCodec(receiver.Track().PayloadType())
codec, err := pc.api.mediaEngine.getCodecByPayload(receiver.Track().PayloadType())
if err != nil {
pc.log.Warnf("no codec could be found for payloadType %d", receiver.Track().PayloadType())
return
}
receiver.Track().mu.Lock()
receiver.Track().kind = codec.Type
receiver.Track().kind = receiver.kind
receiver.Track().codec = codec
receiver.Track().mu.Unlock()
@@ -1208,8 +1213,8 @@ func (pc *PeerConnection) startRTPSenders(currentTransceivers []*RTPTransceiver)
err := transceiver.Sender().Send(RTPSendParameters{
Encodings: RTPEncodingParameters{
RTPCodingParameters{
SSRC: transceiver.Sender().Track().SSRC(),
PayloadType: transceiver.Sender().Track().PayloadType(),
SSRC: transceiver.Sender().ssrc,
PayloadType: transceiver.Sender().payloadType,
},
},
})
@@ -1257,7 +1262,7 @@ func (pc *PeerConnection) startSCTP() {
pc.sctpTransport.lock.Unlock()
}
func (pc *PeerConnection) handleUndeclaredSSRC(rtpStream io.Reader, ssrc uint32) error { //nolint:gocognit
func (pc *PeerConnection) handleUndeclaredSSRC(rtpStream io.Reader, ssrc SSRC) error { //nolint:gocognit
remoteDescription := pc.RemoteDescription()
if remoteDescription == nil {
return errPeerConnRemoteDescriptionNil
@@ -1290,16 +1295,14 @@ func (pc *PeerConnection) handleUndeclaredSSRC(rtpStream io.Reader, ssrc uint32)
return nil
}
// Simulcast no longer uses SSRCes, but RID instead. We then use that value to populate rest of Track Data
matchedSDPMap, err := matchedAnswerExt(pc.RemoteDescription().parsed, pc.api.settingEngine.getSDPExtensions())
if err != nil {
return err
midExtensionID, audioSupported, videoSupported := pc.api.mediaEngine.GetHeaderExtensionID(RTPHeaderExtensionCapability{sdp.SDESMidURI})
if !audioSupported && !videoSupported {
return errPeerConnSimulcastMidRTPExtensionRequired
}
sdesMidExtMap := getExtMapByURI(matchedSDPMap, sdp.SDESMidURI)
sdesStreamIDExtMap := getExtMapByURI(matchedSDPMap, sdp.SDESRTPStreamIDURI)
if sdesMidExtMap == nil || sdesStreamIDExtMap == nil {
return errPeerConnSimulcastMidAndRidRTPExtensionRequired
streamIDExtensionID, audioSupported, videoSupported := pc.api.mediaEngine.GetHeaderExtensionID(RTPHeaderExtensionCapability{sdp.SDESRTPStreamIDURI})
if !audioSupported && !videoSupported {
return errPeerConnSimulcastStreamIDRTPExtensionRequired
}
b := make([]byte, receiveMTU)
@@ -1310,7 +1313,7 @@ func (pc *PeerConnection) handleUndeclaredSSRC(rtpStream io.Reader, ssrc uint32)
return err
}
maybeMid, maybeRid, payloadType, err := handleUnknownRTPPacket(b[:i], sdesMidExtMap, sdesStreamIDExtMap)
maybeMid, maybeRid, payloadType, err := handleUnknownRTPPacket(b[:i], uint8(midExtensionID), uint8(streamIDExtensionID))
if err != nil {
return err
}
@@ -1326,7 +1329,7 @@ func (pc *PeerConnection) handleUndeclaredSSRC(rtpStream io.Reader, ssrc uint32)
continue
}
codec, err := pc.api.mediaEngine.getCodec(payloadType)
codec, err := pc.api.mediaEngine.getCodecByPayload(payloadType)
if err != nil {
return err
}
@@ -1364,7 +1367,7 @@ func (pc *PeerConnection) undeclaredMediaProcessor() {
return
}
if err := pc.handleUndeclaredSSRC(stream, ssrc); err != nil {
if err := pc.handleUndeclaredSSRC(stream, SSRC(ssrc)); err != nil {
pc.log.Errorf("Incoming unhandled RTP ssrc(%d), OnTrack will not be fired. %v", ssrc, err)
}
}
@@ -1468,7 +1471,7 @@ func (pc *PeerConnection) GetTransceivers() []*RTPTransceiver {
}
// AddTrack adds a Track to the PeerConnection
func (pc *PeerConnection) AddTrack(track *Track) (*RTPSender, error) {
func (pc *PeerConnection) AddTrack(track TrackLocal) (*RTPSender, error) {
if pc.isClosed.get() {
return nil, &rtcerr.InvalidStateError{Err: ErrConnectionClosed}
}
@@ -1503,12 +1506,6 @@ func (pc *PeerConnection) AddTrack(track *Track) (*RTPSender, error) {
return transceiver.Sender(), nil
}
// AddTransceiver Create a new RtpTransceiver and add it to the set of transceivers.
// Deprecated: Use AddTrack, AddTransceiverFromKind or AddTransceiverFromTrack
func (pc *PeerConnection) AddTransceiver(trackOrKind RTPCodecType, init ...RtpTransceiverInit) (*RTPTransceiver, error) {
return pc.AddTransceiverFromKind(trackOrKind, init...)
}
// RemoveTrack removes a Track from the PeerConnection
func (pc *PeerConnection) RemoveTrack(sender *RTPSender) error {
if pc.isClosed.get() {
@@ -1553,18 +1550,17 @@ func (pc *PeerConnection) AddTransceiverFromKind(kind RTPCodecType, init ...RtpT
switch direction {
case RTPTransceiverDirectionSendrecv:
codecs := pc.api.mediaEngine.GetCodecsByKind(kind)
codecs := pc.api.mediaEngine.getCodecsByKind(kind)
if len(codecs) == 0 {
return nil, fmt.Errorf("%w: %s", errPeerConnCodecsNotFound, kind.String())
return nil, ErrNoCodecsAvailable
}
track, err := pc.NewTrack(codecs[0].PayloadType, util.RandUint32(), util.MathRandAlpha(trackDefaultIDLength), util.MathRandAlpha(trackDefaultLabelLength))
track, err := NewTrackLocalStaticSample(codecs[0].RTPCodecCapability, util.MathRandAlpha(16), util.MathRandAlpha(16))
if err != nil {
return nil, err
}
return pc.AddTransceiverFromTrack(track, init...)
case RTPTransceiverDirectionRecvonly:
receiver, err := pc.api.NewRTPReceiver(kind, pc.dtlsTransport)
if err != nil {
@@ -1587,7 +1583,7 @@ func (pc *PeerConnection) AddTransceiverFromKind(kind RTPCodecType, init ...RtpT
}
// AddTransceiverFromTrack Create a new RtpTransceiver(SendRecv or SendOnly) and add it to the set of transceivers.
func (pc *PeerConnection) AddTransceiverFromTrack(track *Track, init ...RtpTransceiverInit) (*RTPTransceiver, error) {
func (pc *PeerConnection) AddTransceiverFromTrack(track TrackLocal, init ...RtpTransceiverInit) (*RTPTransceiver, error) {
if pc.isClosed.get() {
return nil, &rtcerr.InvalidStateError{Err: ErrConnectionClosed}
}
@@ -1639,7 +1635,7 @@ func (pc *PeerConnection) AddTransceiverFromTrack(track *Track, init ...RtpTrans
return t, nil
default:
return nil, errPeerConnAddTransceiverFromTrackOneTransceiver
return nil, errPeerConnAddTransceiverFromTrackSupport
}
}
@@ -1806,18 +1802,6 @@ func (pc *PeerConnection) Close() error {
return util.FlattenErrs(closeErrs)
}
// NewTrack Creates a new Track
func (pc *PeerConnection) NewTrack(payloadType uint8, ssrc uint32, id, label string) (*Track, error) {
codec, err := pc.api.mediaEngine.getCodec(payloadType)
if err != nil {
return nil, err
} else if codec.Payloader == nil {
return nil, errPeerConnCodecPayloaderNotSet
}
return NewTrack(payloadType, ssrc, id, label, codec)
}
func (pc *PeerConnection) newRTPTransceiver(
receiver *RTPReceiver,
sender *RTPSender,
@@ -2008,7 +1992,7 @@ func (pc *PeerConnection) startRTP(isRenegotiation bool, remoteDesc *SessionDesc
if details := trackDetailsForSSRC(trackDetails, ssrc); details != nil {
t.Receiver().Track().id = details.id
t.Receiver().Track().label = details.label
t.Receiver().Track().streamID = details.streamID
t.Receiver().Track().mu.Unlock()
continue
}
@@ -2040,11 +2024,6 @@ func (pc *PeerConnection) startRTP(isRenegotiation bool, remoteDesc *SessionDesc
}
}
// GetRegisteredRTPCodecs gets a list of registered RTPCodec from the underlying constructed MediaEngine
func (pc *PeerConnection) GetRegisteredRTPCodecs(kind RTPCodecType) []*RTPCodec {
return pc.api.mediaEngine.GetCodecsByKind(kind)
}
// generateUnmatchedSDP generates an SDP that doesn't take remote state into account
// This is used for the initial call for CreateOffer
func (pc *PeerConnection) generateUnmatchedSDP(transceivers []*RTPTransceiver, useIdentity bool) (*sdp.SessionDescription, error) {
@@ -2109,7 +2088,7 @@ func (pc *PeerConnection) generateUnmatchedSDP(transceivers []*RTPTransceiver, u
return nil, err
}
return populateSDP(d, isPlanB, dtlsFingerprints, pc.api.settingEngine.sdpMediaLevelFingerprints, pc.api.settingEngine.candidates.ICELite, pc.api.mediaEngine, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), candidates, iceParams, mediaSections, pc.ICEGatheringState(), pc.api.settingEngine.getSDPExtensions())
return populateSDP(d, isPlanB, dtlsFingerprints, pc.api.settingEngine.sdpMediaLevelFingerprints, pc.api.settingEngine.candidates.ICELite, pc.api.mediaEngine, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), candidates, iceParams, mediaSections, pc.ICEGatheringState())
}
// generateMatchedSDP generates a SDP and takes the remote state into account
@@ -2227,12 +2206,7 @@ func (pc *PeerConnection) generateMatchedSDP(transceivers []*RTPTransceiver, use
return nil, err
}
matchedSDPMap, err := matchedAnswerExt(pc.RemoteDescription().parsed, pc.api.settingEngine.getSDPExtensions())
if err != nil {
return nil, err
}
return populateSDP(d, detectedPlanB, dtlsFingerprints, pc.api.settingEngine.sdpMediaLevelFingerprints, pc.api.settingEngine.candidates.ICELite, pc.api.mediaEngine, connectionRole, candidates, iceParams, mediaSections, pc.ICEGatheringState(), matchedSDPMap)
return populateSDP(d, detectedPlanB, dtlsFingerprints, pc.api.settingEngine.sdpMediaLevelFingerprints, pc.api.settingEngine.candidates.ICELite, pc.api.mediaEngine, connectionRole, candidates, iceParams, mediaSections, pc.ICEGatheringState())
}
func (pc *PeerConnection) setGatherCompleteHandler(handler func()) {

View File

@@ -302,7 +302,7 @@ func TestPeerConnection_EventHandlers_Go(t *testing.T) {
assert.NotPanics(t, func() { pc.onTrack(nil, nil) })
assert.NotPanics(t, func() { pc.onICEConnectionStateChange(ice.ConnectionStateNew) })
pc.OnTrack(func(t *Track, r *RTPReceiver) {
pc.OnTrack(func(t *TrackRemote, r *RTPReceiver) {
close(onTrackCalled)
})
@@ -324,7 +324,7 @@ func TestPeerConnection_EventHandlers_Go(t *testing.T) {
assert.NotPanics(t, func() { go pc.onDataChannelHandler(nil) })
// Verify that the set handlers are called
assert.NotPanics(t, func() { pc.onTrack(&Track{}, &RTPReceiver{}) })
assert.NotPanics(t, func() { pc.onTrack(&TrackRemote{}, &RTPReceiver{}) })
assert.NotPanics(t, func() { pc.onICEConnectionStateChange(ice.ConnectionStateNew) })
assert.NotPanics(t, func() { go pc.onDataChannelHandler(&DataChannel{api: api}) })
@@ -1036,13 +1036,14 @@ type trackRecords struct {
receivedTrackIDs map[string]struct{}
}
func (r *trackRecords) newTrackParameter() (uint8, uint32, string, string) {
func (r *trackRecords) newTrack() (*TrackLocalStaticRTP, error) {
trackID := fmt.Sprintf("pion-track-%d", len(r.trackIDs))
track, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: "video/vp8"}, trackID, "pion")
r.trackIDs[trackID] = struct{}{}
return DefaultPayloadTypeVP8, uint32(len(r.trackIDs)), trackID, "pion"
return track, err
}
func (r *trackRecords) handleTrack(t *Track, _ *RTPReceiver) {
func (r *trackRecords) handleTrack(t *TrackRemote, _ *RTPReceiver) {
r.mu.Lock()
defer r.mu.Unlock()
tID := t.ID()
@@ -1065,7 +1066,7 @@ func TestPeerConnection_MassiveTracks(t *testing.T) {
trackIDs: make(map[string]struct{}),
receivedTrackIDs: make(map[string]struct{}),
}
tracks = []*Track{}
tracks = []*TrackLocalStaticRTP{}
trackCount = 256
pingInterval = 1 * time.Second
noiseInterval = 100 * time.Microsecond
@@ -1081,7 +1082,6 @@ func TestPeerConnection_MassiveTracks(t *testing.T) {
ExtensionProfile: 1,
Version: 2,
PayloadOffset: 20,
PayloadType: DefaultPayloadTypeVP8,
SequenceNumber: 27023,
Timestamp: 3653407706,
CSRC: []uint32{},
@@ -1091,12 +1091,12 @@ func TestPeerConnection_MassiveTracks(t *testing.T) {
connected = make(chan struct{})
stopped = make(chan struct{})
)
api.mediaEngine.RegisterDefaultCodecs()
assert.NoError(t, api.mediaEngine.RegisterDefaultCodecs())
offerPC, answerPC, err := api.newPair(Configuration{})
assert.NoError(t, err)
// Create massive tracks.
for range make([]struct{}, trackCount) {
track, err := offerPC.NewTrack(tRecs.newTrackParameter())
track, err := tRecs.newTrack()
assert.NoError(t, err)
_, err = offerPC.AddTrack(track)
assert.NoError(t, err)
@@ -1126,7 +1126,6 @@ func TestPeerConnection_MassiveTracks(t *testing.T) {
<-connected
time.Sleep(1 * time.Second)
for _, track := range tracks {
samplePkt.SSRC = track.SSRC()
assert.NoError(t, track.WriteRTP(samplePkt))
}
// Ping trackRecords to see if any track event not received yet.

View File

@@ -48,19 +48,17 @@ each side gets something (and asserts payload contents)
// nolint: gocyclo
func TestPeerConnection_Media_Sample(t *testing.T) {
const (
expectedTrackID = "video"
expectedTrackLabel = "pion"
expectedTrackID = "video"
expectedStreamID = "pion"
)
api := NewAPI()
lim := test.TimeOut(time.Second * 30)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
api.mediaEngine.RegisterDefaultCodecs()
pcOffer, pcAnswer, err := api.newPair(Configuration{})
pcOffer, pcAnswer, err := newPair()
if err != nil {
t.Fatal(err)
}
@@ -77,14 +75,14 @@ func TestPeerConnection_Media_Sample(t *testing.T) {
trackMetadataValid := make(chan error)
pcAnswer.OnTrack(func(track *Track, receiver *RTPReceiver) {
pcAnswer.OnTrack(func(track *TrackRemote, receiver *RTPReceiver) {
if track.ID() != expectedTrackID {
trackMetadataValid <- fmt.Errorf("%w: expected(%s) actual(%s)", errIncomingTrackIDInvalid, expectedTrackID, track.ID())
return
}
if track.Label() != expectedTrackLabel {
trackMetadataValid <- fmt.Errorf("%w: expected(%s) actual(%s)", errIncomingTrackLabelInvalid, expectedTrackLabel, track.Label())
if track.StreamID() != expectedStreamID {
trackMetadataValid <- fmt.Errorf("%w: expected(%s) actual(%s)", errIncomingTrackLabelInvalid, expectedStreamID, track.StreamID())
return
}
close(trackMetadataValid)
@@ -92,7 +90,7 @@ func TestPeerConnection_Media_Sample(t *testing.T) {
go func() {
for {
time.Sleep(time.Millisecond * 100)
if routineErr := pcAnswer.WriteRTCP([]rtcp.Packet{&rtcp.RapidResynchronizationRequest{SenderSSRC: track.SSRC(), MediaSSRC: track.SSRC()}}); routineErr != nil {
if routineErr := pcAnswer.WriteRTCP([]rtcp.Packet{&rtcp.RapidResynchronizationRequest{SenderSSRC: uint32(track.SSRC()), MediaSSRC: uint32(track.SSRC())}}); routineErr != nil {
awaitRTCPReceiverSend <- routineErr
return
}
@@ -128,11 +126,11 @@ func TestPeerConnection_Media_Sample(t *testing.T) {
}
})
vp8Track, err := pcOffer.NewTrack(DefaultPayloadTypeVP8, randutil.NewMathRandomGenerator().Uint32(), expectedTrackID, expectedTrackLabel)
vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, expectedTrackID, expectedStreamID)
if err != nil {
t.Fatal(err)
}
rtpSender, err := pcOffer.AddTrack(vp8Track)
sender, err := pcOffer.AddTrack(vp8Track)
if err != nil {
t.Fatal(err)
}
@@ -140,7 +138,7 @@ func TestPeerConnection_Media_Sample(t *testing.T) {
go func() {
for {
time.Sleep(time.Millisecond * 100)
if routineErr := vp8Track.WriteSample(media.Sample{Data: []byte{0x00}, Samples: 1}); routineErr != nil {
if routineErr := vp8Track.WriteSample(media.Sample{Data: []byte{0x00}, Duration: time.Second}); routineErr != nil {
fmt.Println(routineErr)
}
@@ -156,7 +154,7 @@ func TestPeerConnection_Media_Sample(t *testing.T) {
go func() {
for {
time.Sleep(time.Millisecond * 100)
if routineErr := pcOffer.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{SenderSSRC: vp8Track.SSRC(), MediaSSRC: vp8Track.SSRC()}}); routineErr != nil {
if routineErr := pcOffer.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{SenderSSRC: uint32(sender.ssrc), MediaSSRC: uint32(sender.ssrc)}}); routineErr != nil {
awaitRTCPSenderSend <- routineErr
}
@@ -170,15 +168,12 @@ func TestPeerConnection_Media_Sample(t *testing.T) {
}()
go func() {
if _, routineErr := rtpSender.Read(make([]byte, 1400)); routineErr == nil {
if _, routineErr := sender.Read(make([]byte, 1400)); routineErr == nil {
close(awaitRTCPSenderRecv)
}
}()
err = signalPair(pcOffer, pcAnswer)
if err != nil {
t.Fatal(err)
}
assert.NoError(t, signalPair(pcOffer, pcAnswer))
err, ok := <-trackMetadataValid
if ok {
@@ -189,14 +184,12 @@ func TestPeerConnection_Media_Sample(t *testing.T) {
<-awaitRTPSend
<-awaitRTCPSenderRecv
err, ok = <-awaitRTCPSenderSend
if ok {
if err, ok = <-awaitRTCPSenderSend; ok {
t.Fatal(err)
}
<-awaitRTCPReceiverRecv
err, ok = <-awaitRTCPReceiverSend
if ok {
if err, ok = <-awaitRTCPReceiverSend; ok {
t.Fatal(err)
}
@@ -223,9 +216,7 @@ func TestPeerConnection_Media_Shutdown(t *testing.T) {
report := test.CheckRoutines(t)
defer report()
api := NewAPI()
api.mediaEngine.RegisterDefaultCodecs()
pcOffer, pcAnswer, err := api.newPair(Configuration{})
pcOffer, pcAnswer, err := newPair()
if err != nil {
t.Fatal(err)
}
@@ -240,11 +231,12 @@ func TestPeerConnection_Media_Shutdown(t *testing.T) {
t.Fatal(err)
}
opusTrack, err := pcOffer.NewTrack(DefaultPayloadTypeOpus, randutil.NewMathRandomGenerator().Uint32(), "audio", "pion1")
opusTrack, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "audio/opus"}, "audio", "pion1")
if err != nil {
t.Fatal(err)
}
vp8Track, err := pcOffer.NewTrack(DefaultPayloadTypeVP8, randutil.NewMathRandomGenerator().Uint32(), "video", "pion2")
vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion2")
if err != nil {
t.Fatal(err)
}
@@ -258,7 +250,7 @@ func TestPeerConnection_Media_Shutdown(t *testing.T) {
var onTrackFiredLock sync.Mutex
onTrackFired := false
pcAnswer.OnTrack(func(track *Track, receiver *RTPReceiver) {
pcAnswer.OnTrack(func(track *TrackRemote, receiver *RTPReceiver) {
onTrackFiredLock.Lock()
defer onTrackFiredLock.Unlock()
onTrackFired = true
@@ -325,15 +317,12 @@ func TestPeerConnection_Media_Disconnected(t *testing.T) {
s := SettingEngine{}
s.SetICETimeouts(1*time.Second, 5*time.Second, 250*time.Millisecond)
api := NewAPI(WithSettingEngine(s))
api.mediaEngine.RegisterDefaultCodecs()
pcOffer, pcAnswer, err := api.newPair(Configuration{})
pcOffer, pcAnswer, err := newPair()
if err != nil {
t.Fatal(err)
}
vp8Track, err := pcOffer.NewTrack(DefaultPayloadTypeVP8, randutil.NewMathRandomGenerator().Uint32(), "video", "pion2")
vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion2")
if err != nil {
t.Fatal(err)
}
@@ -379,7 +368,7 @@ func TestPeerConnection_Media_Disconnected(t *testing.T) {
t.Fatal(err)
}
for i := 0; i <= 5; i++ {
if rtpErr := vp8Track.WriteSample(media.Sample{Data: []byte{0x00}, Samples: 1}); rtpErr != nil {
if rtpErr := vp8Track.WriteSample(media.Sample{Data: []byte{0x00}, Duration: time.Second}); rtpErr != nil {
t.Fatal(rtpErr)
} else if rtcpErr := pcOffer.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: 0}}); rtcpErr != nil {
t.Fatal(rtcpErr)
@@ -395,15 +384,14 @@ Integration test for behavior around media and closing
* Writing and Reading from tracks should return io.EOF when the PeerConnection is closed
*/
func TestPeerConnection_Media_Closed(t *testing.T) {
lim := test.TimeOut(time.Second * 30)
return // Sean-Der, move bindings earlier
lim := test.TimeOut(time.Second * 30) //nolint: govet
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
api := NewAPI()
api.mediaEngine.RegisterDefaultCodecs()
pcOffer, pcAnswer, err := api.newPair(Configuration{})
pcOffer, pcAnswer, err := newPair()
if err != nil {
t.Fatal(err)
}
@@ -413,7 +401,7 @@ func TestPeerConnection_Media_Closed(t *testing.T) {
t.Fatal(err)
}
vp8Writer, err := pcOffer.NewTrack(DefaultPayloadTypeVP8, randutil.NewMathRandomGenerator().Uint32(), "video", "pion2")
vp8Writer, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion2")
if err != nil {
t.Fatal(err)
}
@@ -422,16 +410,16 @@ func TestPeerConnection_Media_Closed(t *testing.T) {
t.Fatal(err)
}
answerChan := make(chan *Track)
pcAnswer.OnTrack(func(t *Track, r *RTPReceiver) {
answerChan := make(chan *TrackRemote)
pcAnswer.OnTrack(func(t *TrackRemote, r *RTPReceiver) {
answerChan <- t
})
assert.NoError(t, signalPair(pcOffer, pcAnswer))
vp8Reader := func() *Track {
vp8Reader := func() *TrackRemote {
for {
if err = vp8Writer.WriteSample(media.Sample{Data: []byte{0x00}, Samples: 1}); err != nil {
if err = vp8Writer.WriteSample(media.Sample{Data: []byte{0x00}, Duration: time.Second}); err != nil {
t.Fatal(err)
}
time.Sleep(time.Millisecond * 25)
@@ -464,8 +452,8 @@ func TestPeerConnection_Media_Closed(t *testing.T) {
assert.NoError(t, pcOffer.Close())
assert.NoError(t, pcAnswer.Close())
if err = vp8Writer.WriteSample(media.Sample{Data: []byte{0x00}, Samples: 1}); !errors.Is(err, io.ErrClosedPipe) {
t.Fatal("Write to Track with no RTPSenders did not return io.ErrClosedPipe")
if err = vp8Writer.WriteSample(media.Sample{Data: []byte{0x00}, Duration: time.Second}); !errors.Is(err, io.ErrClosedPipe) {
t.Fatal("Write to TrackLocal with no RTPSenders did not return io.ErrClosedPipe")
} else if err = pcAnswer.WriteRTCP([]rtcp.Packet{&rtcp.RapidResynchronizationRequest{SenderSSRC: 0, MediaSSRC: 0}}); !errors.Is(err, io.ErrClosedPipe) {
t.Fatal("WriteRTCP to closed PeerConnection did not return io.ErrClosedPipe")
}
@@ -480,25 +468,23 @@ func TestUndeclaredSSRC(t *testing.T) {
report := test.CheckRoutines(t)
defer report()
api := NewAPI()
api.mediaEngine.RegisterDefaultCodecs()
pcOffer, pcAnswer, err := api.newPair(Configuration{})
assert.NoError(t, err)
_, err = pcOffer.CreateDataChannel("test-channel", nil)
pcOffer, pcAnswer, err := newPair()
assert.NoError(t, err)
_, err = pcAnswer.AddTransceiverFromKind(RTPCodecTypeVideo)
assert.NoError(t, err)
vp8Writer, err := pcOffer.NewTrack(DefaultPayloadTypeVP8, randutil.NewMathRandomGenerator().Uint32(), "video", "pion2")
_, err = pcOffer.CreateDataChannel("test-channel", nil)
assert.NoError(t, err)
vp8Writer, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion2")
assert.NoError(t, err)
_, err = pcOffer.AddTrack(vp8Writer)
assert.NoError(t, err)
onTrackFired := make(chan *Track)
pcAnswer.OnTrack(func(t *Track, r *RTPReceiver) {
onTrackFired := make(chan *TrackRemote)
pcAnswer.OnTrack(func(t *TrackRemote, r *RTPReceiver) {
close(onTrackFired)
})
@@ -517,7 +503,7 @@ func TestUndeclaredSSRC(t *testing.T) {
inApplicationMedia := false
for scanner.Scan() {
l := scanner.Text()
if strings.HasPrefix(l, "m=") {
if strings.HasPrefix(l, "m=application") {
inApplicationMedia = !inApplicationMedia
} else if strings.HasPrefix(l, "a=ssrc") {
continue
@@ -545,7 +531,7 @@ func TestUndeclaredSSRC(t *testing.T) {
go func() {
for {
assert.NoError(t, vp8Writer.WriteSample(media.Sample{Data: []byte{0x00}, Samples: 1}))
assert.NoError(t, vp8Writer.WriteSample(media.Sample{Data: []byte{0x00}, Duration: time.Second}))
time.Sleep(time.Millisecond * 25)
select {
@@ -569,9 +555,7 @@ func TestOfferRejectionMissingCodec(t *testing.T) {
report := test.CheckRoutines(t)
defer report()
api := NewAPI()
api.mediaEngine.RegisterDefaultCodecs()
pc, err := api.NewPeerConnection(Configuration{})
pc, err := NewPeerConnection(Configuration{})
if err != nil {
t.Fatal(err)
}
@@ -582,7 +566,7 @@ func TestOfferRejectionMissingCodec(t *testing.T) {
t.Fatal(err)
}
track, err := pc.NewTrack(DefaultPayloadTypeVP8, randutil.NewMathRandomGenerator().Uint32(), "video", "pion2")
track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion2")
if err != nil {
t.Fatal(err)
}
@@ -625,11 +609,10 @@ func TestAddTransceiverFromTrackSendOnly(t *testing.T) {
t.Error(err.Error())
}
track, err := pc.NewTrack(
DefaultPayloadTypeOpus,
0xDEADBEEF,
track, err := NewTrackLocalStaticSample(
RTPCodecCapability{MimeType: "audio/Opus"},
"track-id",
"track-label",
"stream-id",
)
if err != nil {
t.Error(err.Error())
@@ -682,11 +665,10 @@ func TestAddTransceiverFromTrackSendRecv(t *testing.T) {
t.Error(err.Error())
}
track, err := pc.NewTrack(
DefaultPayloadTypeOpus,
0xDEADBEEF,
track, err := NewTrackLocalStaticSample(
RTPCodecCapability{MimeType: "audio/Opus"},
"track-id",
"track-label",
"stream-id",
)
if err != nil {
t.Error(err.Error())
@@ -735,7 +717,7 @@ func TestAddTransceiver(t *testing.T) {
t.Error(err.Error())
}
transceiver, err := pc.AddTransceiver(RTPCodecTypeVideo, RtpTransceiverInit{
transceiver, err := pc.AddTransceiverFromKind(RTPCodecTypeVideo, RtpTransceiverInit{
Direction: RTPTransceiverDirectionSendrecv,
})
if err != nil {
@@ -762,10 +744,7 @@ func TestAddTransceiver(t *testing.T) {
}
func TestAddTransceiverAddTrack_Reuse(t *testing.T) {
mediaEngine := MediaEngine{}
mediaEngine.RegisterDefaultCodecs()
api := NewAPI(WithMediaEngine(mediaEngine))
pc, err := api.NewPeerConnection(Configuration{})
pc, err := NewPeerConnection(Configuration{})
assert.NoError(t, err)
tr, err := pc.AddTransceiverFromKind(
@@ -776,8 +755,8 @@ func TestAddTransceiverAddTrack_Reuse(t *testing.T) {
assert.Equal(t, []*RTPTransceiver{tr}, pc.GetTransceivers())
addTrack := func() (*Track, *RTPSender) {
track, err := pc.NewTrack(DefaultPayloadTypeVP8, randutil.NewMathRandomGenerator().Uint32(), "foo", "bar")
addTrack := func() (TrackLocal, *RTPSender) {
track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "foo", "bar")
assert.NoError(t, err)
sender, err := pc.AddTrack(track)
@@ -803,10 +782,7 @@ func TestAddTransceiverAddTrack_Reuse(t *testing.T) {
}
func TestAddTransceiverAddTrack_NewRTPSender_Error(t *testing.T) {
mediaEngine := MediaEngine{}
mediaEngine.RegisterDefaultCodecs()
api := NewAPI(WithMediaEngine(mediaEngine))
pc, err := api.NewPeerConnection(Configuration{})
pc, err := NewPeerConnection(Configuration{})
assert.NoError(t, err)
_, err = pc.AddTransceiverFromKind(
@@ -818,7 +794,7 @@ func TestAddTransceiverAddTrack_NewRTPSender_Error(t *testing.T) {
dtlsTransport := pc.dtlsTransport
pc.dtlsTransport = nil
track, err := pc.NewTrack(DefaultPayloadTypeVP8, randutil.NewMathRandomGenerator().Uint32(), "foo", "bar")
track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "foo", "bar")
assert.NoError(t, err)
_, err = pc.AddTrack(track)
@@ -831,10 +807,7 @@ func TestAddTransceiverAddTrack_NewRTPSender_Error(t *testing.T) {
}
func TestRtpSenderReceiver_ReadClose_Error(t *testing.T) {
mediaEngine := MediaEngine{}
mediaEngine.RegisterDefaultCodecs()
api := NewAPI(WithMediaEngine(mediaEngine))
pc, err := api.NewPeerConnection(Configuration{})
pc, err := NewPeerConnection(Configuration{})
assert.NoError(t, err)
tr, err := pc.AddTransceiverFromKind(
@@ -930,9 +903,8 @@ func TestAddTransceiverFromTrackFailsRecvOnly(t *testing.T) {
t.Error(err.Error())
}
track, err := pc.NewTrack(
DefaultPayloadTypeH264,
0xDEADBEEF,
track, err := NewTrackLocalStaticSample(
RTPCodecCapability{MimeType: "video/h264", SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"},
"track-id",
"track-label",
)
@@ -952,112 +924,10 @@ func TestAddTransceiverFromTrackFailsRecvOnly(t *testing.T) {
assert.NoError(t, pc.Close())
}
func TestOmitMediaFromBundleIfUnsupported(t *testing.T) {
const sdpOfferWithAudioAndVideo = `v=0
o=- 6476616870435111971 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0 1
m=audio 9 UDP/TLS/RTP/SAVPF 111
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:sRIG
a=ice-pwd:yZb5ZMsBlPoK577sGhjvEUtT
a=ice-options:trickle
a=fingerprint:sha-256 27:EF:25:BF:57:45:BC:1C:0D:36:42:FF:5E:93:71:D2:41:58:EA:46:FD:A8:2A:F3:13:94:6E:E6:43:23:CB:D7
a=setup:actpass
a=mid:0
a=sendrecv
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10;useinbandfec=1
m=video 9 UDP/TLS/RTP/SAVPF 96
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:sRIG
a=ice-pwd:yZb5ZMsBlPoK577sGhjvEUtT
a=ice-options:trickle
a=fingerprint:sha-256 27:EF:25:BF:57:45:BC:1C:0D:36:42:FF:5E:93:71:D2:41:58:EA:46:FD:A8:2A:F3:13:94:6E:E6:43:23:CB:D7
a=setup:actpass
a=mid:1
a=sendrecv
a=rtpmap:96 H264/90000
a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c1f
`
lim := test.TimeOut(time.Second * 30)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
mediaEngine := MediaEngine{}
mediaEngine.RegisterCodec(
NewRTPH264Codec(DefaultPayloadTypeH264, 90000),
)
api := NewAPI(
WithMediaEngine(mediaEngine),
)
pc, err := api.NewPeerConnection(Configuration{})
if err != nil {
t.Error(err.Error())
}
if err = pc.SetRemoteDescription(SessionDescription{
Type: SDPTypeOffer,
SDP: sdpOfferWithAudioAndVideo,
}); nil != err {
t.Error(err.Error())
}
answer, err := pc.CreateAnswer(nil)
if nil != err {
t.Error(err.Error())
}
success := false
for _, attr := range answer.parsed.Attributes {
if attr.Key == "group" {
if attr.Value == "BUNDLE 1" {
success = true
}
}
}
if !success {
t.Fail()
}
assert.NoError(t, pc.Close())
}
func TestGetRegisteredRTPCodecs(t *testing.T) {
mediaEngine := MediaEngine{}
expectedCodec := NewRTPH264Codec(DefaultPayloadTypeH264, 90000)
mediaEngine.RegisterCodec(expectedCodec)
api := NewAPI(WithMediaEngine(mediaEngine))
pc, err := api.NewPeerConnection(Configuration{})
if err != nil {
t.Error(err.Error())
}
codecs := pc.GetRegisteredRTPCodecs(RTPCodecTypeVideo)
if len(codecs) != 1 {
t.Errorf("expected to get only 1 codec but got %d codecs", len(codecs))
}
actualCodec := codecs[0]
if actualCodec != expectedCodec {
t.Errorf("expected to get %v but got %v", expectedCodec, actualCodec)
}
assert.NoError(t, pc.Close())
}
func TestPlanBMediaExchange(t *testing.T) {
runTest := func(trackCount int, t *testing.T) {
addSingleTrack := func(p *PeerConnection) *Track {
track, err := p.NewTrack(DefaultPayloadTypeVP8, randutil.NewMathRandomGenerator().Uint32(), fmt.Sprintf("video-%d", randutil.NewMathRandomGenerator().Uint32()), fmt.Sprintf("video-%d", randutil.NewMathRandomGenerator().Uint32()))
addSingleTrack := func(p *PeerConnection) *TrackLocalStaticSample {
track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, fmt.Sprintf("video-%d", randutil.NewMathRandomGenerator().Uint32()), fmt.Sprintf("video-%d", randutil.NewMathRandomGenerator().Uint32()))
assert.NoError(t, err)
_, err = p.AddTrack(track)
@@ -1074,7 +944,7 @@ func TestPlanBMediaExchange(t *testing.T) {
var onTrackWaitGroup sync.WaitGroup
onTrackWaitGroup.Add(trackCount)
pcAnswer.OnTrack(func(track *Track, r *RTPReceiver) {
pcAnswer.OnTrack(func(track *TrackRemote, r *RTPReceiver) {
onTrackWaitGroup.Done()
})
@@ -1087,7 +957,7 @@ func TestPlanBMediaExchange(t *testing.T) {
_, err = pcAnswer.AddTransceiverFromKind(RTPCodecTypeVideo)
assert.NoError(t, err)
outboundTracks := []*Track{}
outboundTracks := []*TrackLocalStaticSample{}
for i := 0; i < trackCount; i++ {
outboundTracks = append(outboundTracks, addSingleTrack(pcOffer))
}
@@ -1099,7 +969,7 @@ func TestPlanBMediaExchange(t *testing.T) {
select {
case <-time.After(20 * time.Millisecond):
for _, track := range outboundTracks {
assert.NoError(t, track.WriteSample(media.Sample{Data: []byte{0x00}, Samples: 1}))
assert.NoError(t, track.WriteSample(media.Sample{Data: []byte{0x00}, Duration: time.Second}))
}
case <-done:
return
@@ -1143,7 +1013,7 @@ func TestPeerConnection_Start_Only_Negotiated_Senders(t *testing.T) {
assert.NoError(t, err)
defer func() { assert.NoError(t, pcAnswer.Close()) }()
track1, err := pcOffer.NewTrack(DefaultPayloadTypeVP8, randutil.NewMathRandomGenerator().Uint32(), "video", "pion1")
track1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion1")
require.NoError(t, err)
sender1, err := pcOffer.AddTrack(track1)
@@ -1164,7 +1034,7 @@ func TestPeerConnection_Start_Only_Negotiated_Senders(t *testing.T) {
// Add a new track between providing the offer and applying the answer
track2, err := pcOffer.NewTrack(DefaultPayloadTypeVP8, randutil.NewMathRandomGenerator().Uint32(), "video", "pion2")
track2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion2")
require.NoError(t, err)
sender2, err := pcOffer.AddTrack(track2)
@@ -1195,21 +1065,19 @@ func TestPeerConnection_Start_Right_Receiver(t *testing.T) {
return false, fmt.Errorf("%w: %q", errNoTransceiverwithMid, mid)
}
api := NewAPI()
lim := test.TimeOut(time.Second * 30)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
api.mediaEngine.RegisterDefaultCodecs()
pcOffer, pcAnswer, err := api.newPair(Configuration{})
pcOffer, pcAnswer, err := newPair()
require.NoError(t, err)
_, err = pcAnswer.AddTransceiverFromKind(RTPCodecTypeVideo, RtpTransceiverInit{Direction: RTPTransceiverDirectionRecvonly})
assert.NoError(t, err)
track1, err := pcOffer.NewTrack(DefaultPayloadTypeVP8, randutil.NewMathRandomGenerator().Uint32(), "video", "pion1")
track1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion1")
require.NoError(t, err)
sender1, err := pcOffer.AddTrack(track1)

View File

@@ -12,7 +12,6 @@ import (
"testing"
"time"
"github.com/pion/randutil"
"github.com/pion/transport/test"
"github.com/pion/webrtc/v3/internal/util"
"github.com/pion/webrtc/v3/pkg/media"
@@ -21,12 +20,12 @@ import (
"github.com/stretchr/testify/require"
)
func sendVideoUntilDone(done <-chan struct{}, t *testing.T, tracks []*Track) {
func sendVideoUntilDone(done <-chan struct{}, t *testing.T, tracks []*TrackLocalStaticSample) {
for {
select {
case <-time.After(20 * time.Millisecond):
for _, track := range tracks {
assert.NoError(t, track.WriteSample(media.Sample{Data: []byte{0x00}, Samples: 1}))
assert.NoError(t, track.WriteSample(media.Sample{Data: []byte{0x00}, Duration: time.Second}))
}
case <-done:
return
@@ -34,7 +33,7 @@ func sendVideoUntilDone(done <-chan struct{}, t *testing.T, tracks []*Track) {
}
}
func sdpMidHasSsrc(offer SessionDescription, mid string, ssrc uint32) bool {
func sdpMidHasSsrc(offer SessionDescription, mid string, ssrc SSRC) bool {
for _, media := range offer.parsed.MediaDescriptions {
cmid, ok := media.Attribute("mid")
if !ok {
@@ -54,7 +53,7 @@ func sdpMidHasSsrc(offer SessionDescription, mid string, ssrc uint32) bool {
continue
}
if uint32(ssrcInt64) == ssrc {
if uint32(ssrcInt64) == uint32(ssrc) {
return true
}
}
@@ -68,22 +67,20 @@ func sdpMidHasSsrc(offer SessionDescription, mid string, ssrc uint32) bool {
* - We are able to re-negotiate and AddTrack is properly called
*/
func TestPeerConnection_Renegotiation_AddTrack(t *testing.T) {
api := NewAPI()
lim := test.TimeOut(time.Second * 30)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
api.mediaEngine.RegisterDefaultCodecs()
pcOffer, pcAnswer, err := api.newPair(Configuration{})
pcOffer, pcAnswer, err := newPair()
if err != nil {
t.Fatal(err)
}
haveRenegotiated := &atomicBool{}
onTrackFired, onTrackFiredFunc := context.WithCancel(context.Background())
pcAnswer.OnTrack(func(track *Track, r *RTPReceiver) {
pcAnswer.OnTrack(func(track *TrackRemote, r *RTPReceiver) {
if !haveRenegotiated.get() {
t.Fatal("OnTrack was called before renegotiation")
}
@@ -95,7 +92,7 @@ func TestPeerConnection_Renegotiation_AddTrack(t *testing.T) {
_, err = pcAnswer.AddTransceiverFromKind(RTPCodecTypeVideo, RtpTransceiverInit{Direction: RTPTransceiverDirectionRecvonly})
assert.NoError(t, err)
vp8Track, err := pcOffer.NewTrack(DefaultPayloadTypeVP8, randutil.NewMathRandomGenerator().Uint32(), "foo", "bar")
vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "foo", "bar")
assert.NoError(t, err)
sender, err := pcOffer.AddTrack(vp8Track)
@@ -103,7 +100,7 @@ func TestPeerConnection_Renegotiation_AddTrack(t *testing.T) {
// Send 10 packets, OnTrack MUST not be fired
for i := 0; i <= 10; i++ {
assert.NoError(t, vp8Track.WriteSample(media.Sample{Data: []byte{0x00}, Samples: 1}))
assert.NoError(t, vp8Track.WriteSample(media.Sample{Data: []byte{0x00}, Duration: time.Second}))
time.Sleep(20 * time.Millisecond)
}
@@ -120,14 +117,14 @@ func TestPeerConnection_Renegotiation_AddTrack(t *testing.T) {
assert.NoError(t, pcAnswer.SetLocalDescription(answer))
pcOffer.ops.Done()
assert.Equal(t, 0, len(vp8Track.activeSenders))
assert.Equal(t, 0, len(vp8Track.rtpTrack.bindings))
assert.NoError(t, pcOffer.SetRemoteDescription(answer))
pcOffer.ops.Done()
assert.Equal(t, 1, len(vp8Track.activeSenders))
assert.Equal(t, 1, len(vp8Track.rtpTrack.bindings))
sendVideoUntilDone(onTrackFired.Done(), t, []*Track{vp8Track})
sendVideoUntilDone(onTrackFired.Done(), t, []*TrackLocalStaticSample{vp8Track})
assert.NoError(t, pcOffer.Close())
assert.NoError(t, pcAnswer.Close())
@@ -135,11 +132,11 @@ func TestPeerConnection_Renegotiation_AddTrack(t *testing.T) {
// Assert that adding tracks across multiple renegotiations performs as expected
func TestPeerConnection_Renegotiation_AddTrack_Multiple(t *testing.T) {
addTrackWithLabel := func(trackName string, pcOffer, pcAnswer *PeerConnection) *Track {
addTrackWithLabel := func(trackID string, pcOffer, pcAnswer *PeerConnection) *TrackLocalStaticSample {
_, err := pcAnswer.AddTransceiverFromKind(RTPCodecTypeVideo, RtpTransceiverInit{Direction: RTPTransceiverDirectionRecvonly})
assert.NoError(t, err)
track, err := pcOffer.NewTrack(DefaultPayloadTypeVP8, randutil.NewMathRandomGenerator().Uint32(), trackName, trackName)
track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, trackID, trackID)
assert.NoError(t, err)
_, err = pcOffer.AddTrack(track)
@@ -148,33 +145,31 @@ func TestPeerConnection_Renegotiation_AddTrack_Multiple(t *testing.T) {
return track
}
trackNames := []string{util.MathRandAlpha(trackDefaultIDLength), util.MathRandAlpha(trackDefaultIDLength), util.MathRandAlpha(trackDefaultIDLength)}
outboundTracks := []*Track{}
trackIDs := []string{util.MathRandAlpha(16), util.MathRandAlpha(16), util.MathRandAlpha(16)}
outboundTracks := []*TrackLocalStaticSample{}
onTrackCount := map[string]int{}
onTrackChan := make(chan struct{}, 1)
api := NewAPI()
lim := test.TimeOut(time.Second * 30)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
api.mediaEngine.RegisterDefaultCodecs()
pcOffer, pcAnswer, err := api.newPair(Configuration{})
pcOffer, pcAnswer, err := newPair()
if err != nil {
t.Fatal(err)
}
pcAnswer.OnTrack(func(track *Track, r *RTPReceiver) {
onTrackCount[track.Label()]++
pcAnswer.OnTrack(func(track *TrackRemote, r *RTPReceiver) {
onTrackCount[track.ID()]++
onTrackChan <- struct{}{}
})
assert.NoError(t, signalPair(pcOffer, pcAnswer))
for i := range trackNames {
outboundTracks = append(outboundTracks, addTrackWithLabel(trackNames[i], pcOffer, pcAnswer))
for i := range trackIDs {
outboundTracks = append(outboundTracks, addTrackWithLabel(trackIDs[i], pcOffer, pcAnswer))
assert.NoError(t, signalPair(pcOffer, pcAnswer))
sendVideoUntilDone(onTrackChan, t, outboundTracks)
}
@@ -182,9 +177,9 @@ func TestPeerConnection_Renegotiation_AddTrack_Multiple(t *testing.T) {
assert.NoError(t, pcOffer.Close())
assert.NoError(t, pcAnswer.Close())
assert.Equal(t, onTrackCount[trackNames[0]], 1)
assert.Equal(t, onTrackCount[trackNames[1]], 1)
assert.Equal(t, onTrackCount[trackNames[2]], 1)
assert.Equal(t, onTrackCount[trackIDs[0]], 1)
assert.Equal(t, onTrackCount[trackIDs[1]], 1)
assert.Equal(t, onTrackCount[trackIDs[2]], 1)
}
// Assert that renegotiation triggers OnTrack() with correct ID and label from
@@ -195,15 +190,13 @@ func TestPeerConnection_Renegotiation_AddTrack_Multiple(t *testing.T) {
// track later and renegotiate, and track ID and label will be set by the time
// first packets are received.
func TestPeerConnection_Renegotiation_AddTrack_Rename(t *testing.T) {
api := NewAPI()
lim := test.TimeOut(time.Second * 30)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
api.mediaEngine.RegisterDefaultCodecs()
pcOffer, pcAnswer, err := api.newPair(Configuration{})
pcOffer, pcAnswer, err := newPair()
if err != nil {
t.Fatal(err)
}
@@ -211,7 +204,7 @@ func TestPeerConnection_Renegotiation_AddTrack_Rename(t *testing.T) {
haveRenegotiated := &atomicBool{}
onTrackFired, onTrackFiredFunc := context.WithCancel(context.Background())
var atomicRemoteTrack atomic.Value
pcOffer.OnTrack(func(track *Track, r *RTPReceiver) {
pcOffer.OnTrack(func(track *TrackRemote, r *RTPReceiver) {
if !haveRenegotiated.get() {
t.Fatal("OnTrack was called before renegotiation")
}
@@ -221,30 +214,29 @@ func TestPeerConnection_Renegotiation_AddTrack_Rename(t *testing.T) {
_, err = pcOffer.AddTransceiverFromKind(RTPCodecTypeVideo, RtpTransceiverInit{Direction: RTPTransceiverDirectionRecvonly})
assert.NoError(t, err)
vp8Track, err := pcAnswer.NewTrack(DefaultPayloadTypeVP8, randutil.NewMathRandomGenerator().Uint32(), "foo1", "bar1")
vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "foo1", "bar1")
assert.NoError(t, err)
_, err = pcAnswer.AddTrack(vp8Track)
assert.NoError(t, err)
assert.NoError(t, signalPair(pcOffer, pcAnswer))
vp8Track.id = "foo2"
vp8Track.label = "bar2"
vp8Track.rtpTrack.id = "foo2"
vp8Track.rtpTrack.streamID = "bar2"
haveRenegotiated.set(true)
assert.NoError(t, signalPair(pcOffer, pcAnswer))
sendVideoUntilDone(onTrackFired.Done(), t, []*Track{vp8Track})
sendVideoUntilDone(onTrackFired.Done(), t, []*TrackLocalStaticSample{vp8Track})
assert.NoError(t, pcOffer.Close())
assert.NoError(t, pcAnswer.Close())
remoteTrack, ok := atomicRemoteTrack.Load().(*Track)
remoteTrack, ok := atomicRemoteTrack.Load().(*TrackRemote)
require.True(t, ok)
require.NotNil(t, remoteTrack)
assert.Equal(t, vp8Track.SSRC(), remoteTrack.SSRC())
assert.Equal(t, "foo2", remoteTrack.ID())
assert.Equal(t, "bar2", remoteTrack.Label())
assert.Equal(t, "bar2", remoteTrack.StreamID())
}
// TestPeerConnection_Transceiver_Mid tests that we'll provide the same
@@ -262,16 +254,16 @@ func TestPeerConnection_Transceiver_Mid(t *testing.T) {
pcAnswer, err := NewPeerConnection(Configuration{})
assert.NoError(t, err)
track1, err := pcOffer.NewTrack(DefaultPayloadTypeVP8, randutil.NewMathRandomGenerator().Uint32(), "video", "pion1")
track1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion1")
require.NoError(t, err)
sender1, err := pcOffer.AddTrack(track1)
require.NoError(t, err)
track2, err := pcOffer.NewTrack(DefaultPayloadTypeVP8, randutil.NewMathRandomGenerator().Uint32(), "video", "pion2")
track2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion2")
require.NoError(t, err)
_, err = pcOffer.AddTrack(track2)
sender2, err := pcOffer.AddTrack(track2)
require.NoError(t, err)
// this will create the initial offer using generateUnmatchedSDP
@@ -300,19 +292,18 @@ func TestPeerConnection_Transceiver_Mid(t *testing.T) {
// Must have 3 media descriptions (2 video channels)
assert.Equal(t, len(offer.parsed.MediaDescriptions), 2)
assert.True(t, sdpMidHasSsrc(offer, "0", track1.SSRC()), "Expected mid %q with ssrc %d, offer.SDP: %s", "0", track1.SSRC(), offer.SDP)
assert.True(t, sdpMidHasSsrc(offer, "0", sender1.ssrc), "Expected mid %q with ssrc %d, offer.SDP: %s", "0", sender1.ssrc, offer.SDP)
// Remove first track, must keep same number of media
// descriptions and same track ssrc for mid 1 as previous
err = pcOffer.RemoveTrack(sender1)
assert.NoError(t, err)
assert.NoError(t, pcOffer.RemoveTrack(sender1))
offer, err = pcOffer.CreateOffer(nil)
assert.NoError(t, err)
assert.Equal(t, len(offer.parsed.MediaDescriptions), 2)
assert.True(t, sdpMidHasSsrc(offer, "1", track2.SSRC()), "Expected mid %q with ssrc %d, offer.SDP: %s", "1", track2.SSRC(), offer.SDP)
assert.True(t, sdpMidHasSsrc(offer, "1", sender2.ssrc), "Expected mid %q with ssrc %d, offer.SDP: %s", "1", sender2.ssrc, offer.SDP)
_, err = pcAnswer.CreateAnswer(nil)
assert.Error(t, err, &rtcerr.InvalidStateError{Err: ErrIncorrectSignalingState})
@@ -320,10 +311,10 @@ func TestPeerConnection_Transceiver_Mid(t *testing.T) {
pcOffer.ops.Done()
pcAnswer.ops.Done()
track3, err := pcOffer.NewTrack(DefaultPayloadTypeVP8, randutil.NewMathRandomGenerator().Uint32(), "video", "pion3")
track3, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion3")
require.NoError(t, err)
_, err = pcOffer.AddTrack(track3)
sender3, err := pcOffer.AddTrack(track3)
require.NoError(t, err)
offer, err = pcOffer.CreateOffer(nil)
@@ -332,8 +323,8 @@ func TestPeerConnection_Transceiver_Mid(t *testing.T) {
// We reuse the existing non-sending transceiver
assert.Equal(t, len(offer.parsed.MediaDescriptions), 2)
assert.True(t, sdpMidHasSsrc(offer, "0", track3.SSRC()), "Expected mid %q with ssrc %d, offer.sdp: %s", "0", track3.SSRC(), offer.SDP)
assert.True(t, sdpMidHasSsrc(offer, "1", track2.SSRC()), "Expected mid %q with ssrc %d, offer.sdp: %s", "1", track2.SSRC(), offer.SDP)
assert.True(t, sdpMidHasSsrc(offer, "0", sender3.ssrc), "Expected mid %q with ssrc %d, offer.sdp: %s", "0", sender3.ssrc, offer.SDP)
assert.True(t, sdpMidHasSsrc(offer, "1", sender2.ssrc), "Expected mid %q with ssrc %d, offer.sdp: %s", "1", sender2.ssrc, offer.SDP)
assert.NoError(t, pcOffer.Close())
assert.NoError(t, pcAnswer.Close())
@@ -352,10 +343,10 @@ func TestPeerConnection_Renegotiation_CodecChange(t *testing.T) {
pcAnswer, err := NewPeerConnection(Configuration{})
assert.NoError(t, err)
track1, err := pcOffer.NewTrack(DefaultPayloadTypeVP8, 123, "video1", "pion1")
track1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video1", "pion1")
require.NoError(t, err)
track2, err := pcOffer.NewTrack(DefaultPayloadTypeVP9, 456, "video2", "pion2")
track2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video2", "pion2")
require.NoError(t, err)
sender1, err := pcOffer.AddTrack(track1)
@@ -364,9 +355,9 @@ func TestPeerConnection_Renegotiation_CodecChange(t *testing.T) {
_, err = pcAnswer.AddTransceiverFromKind(RTPCodecTypeVideo, RtpTransceiverInit{Direction: RTPTransceiverDirectionRecvonly})
require.NoError(t, err)
tracksCh := make(chan *Track)
tracksCh := make(chan *TrackRemote)
tracksClosed := make(chan struct{})
pcAnswer.OnTrack(func(track *Track, r *RTPReceiver) {
pcAnswer.OnTrack(func(track *TrackRemote, r *RTPReceiver) {
tracksCh <- track
for {
if _, readErr := track.ReadRTP(); readErr == io.EOF {
@@ -388,23 +379,20 @@ func TestPeerConnection_Renegotiation_CodecChange(t *testing.T) {
require.Equal(t, "0", transceivers[0].Mid())
ctx, cancel := context.WithCancel(context.Background())
go sendVideoUntilDone(ctx.Done(), t, []*Track{track1})
go sendVideoUntilDone(ctx.Done(), t, []*TrackLocalStaticSample{track1})
remoteTrack1 := <-tracksCh
cancel()
assert.Equal(t, uint32(123), remoteTrack1.SSRC())
assert.Equal(t, "video1", remoteTrack1.ID())
assert.Equal(t, "pion1", remoteTrack1.Label())
assert.Equal(t, "pion1", remoteTrack1.StreamID())
err = pcOffer.RemoveTrack(sender1)
require.NoError(t, err)
require.NoError(t, pcOffer.RemoveTrack(sender1))
sender2, err := pcOffer.AddTrack(track2)
require.NoError(t, err)
err = signalPair(pcOffer, pcAnswer)
require.NoError(t, err)
require.NoError(t, signalPair(pcOffer, pcAnswer))
<-tracksClosed
transceivers = pcOffer.GetTransceivers()
@@ -416,36 +404,32 @@ func TestPeerConnection_Renegotiation_CodecChange(t *testing.T) {
require.Equal(t, "0", transceivers[0].Mid())
ctx, cancel = context.WithCancel(context.Background())
go sendVideoUntilDone(ctx.Done(), t, []*Track{track2})
go sendVideoUntilDone(ctx.Done(), t, []*TrackLocalStaticSample{track2})
remoteTrack2 := <-tracksCh
cancel()
err = pcOffer.RemoveTrack(sender2)
require.NoError(t, err)
require.NoError(t, pcOffer.RemoveTrack(sender2))
err = signalPair(pcOffer, pcAnswer)
require.NoError(t, err)
<-tracksClosed
assert.Equal(t, uint32(456), remoteTrack2.SSRC())
assert.Equal(t, "video2", remoteTrack2.ID())
assert.Equal(t, "pion2", remoteTrack2.Label())
assert.Equal(t, "pion2", remoteTrack2.StreamID())
require.NoError(t, pcOffer.Close())
require.NoError(t, pcAnswer.Close())
}
func TestPeerConnection_Renegotiation_RemoveTrack(t *testing.T) {
api := NewAPI()
lim := test.TimeOut(time.Second * 30)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
api.mediaEngine.RegisterDefaultCodecs()
pcOffer, pcAnswer, err := api.newPair(Configuration{})
pcOffer, pcAnswer, err := newPair()
if err != nil {
t.Fatal(err)
}
@@ -453,16 +437,16 @@ func TestPeerConnection_Renegotiation_RemoveTrack(t *testing.T) {
_, err = pcAnswer.AddTransceiverFromKind(RTPCodecTypeVideo, RtpTransceiverInit{Direction: RTPTransceiverDirectionRecvonly})
assert.NoError(t, err)
vp8Track, err := pcOffer.NewTrack(DefaultPayloadTypeVP8, randutil.NewMathRandomGenerator().Uint32(), "foo", "bar")
vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "foo", "bar")
assert.NoError(t, err)
rtpSender, err := pcOffer.AddTrack(vp8Track)
sender, err := pcOffer.AddTrack(vp8Track)
assert.NoError(t, err)
onTrackFired, onTrackFiredFunc := context.WithCancel(context.Background())
trackClosed, trackClosedFunc := context.WithCancel(context.Background())
pcAnswer.OnTrack(func(track *Track, r *RTPReceiver) {
pcAnswer.OnTrack(func(track *TrackRemote, r *RTPReceiver) {
onTrackFiredFunc()
for {
@@ -474,9 +458,9 @@ func TestPeerConnection_Renegotiation_RemoveTrack(t *testing.T) {
})
assert.NoError(t, signalPair(pcOffer, pcAnswer))
sendVideoUntilDone(onTrackFired.Done(), t, []*Track{vp8Track})
sendVideoUntilDone(onTrackFired.Done(), t, []*TrackLocalStaticSample{vp8Track})
assert.NoError(t, pcOffer.RemoveTrack(rtpSender))
assert.NoError(t, pcOffer.RemoveTrack(sender))
assert.NoError(t, signalPair(pcOffer, pcAnswer))
<-trackClosed.Done()
@@ -485,21 +469,19 @@ func TestPeerConnection_Renegotiation_RemoveTrack(t *testing.T) {
}
func TestPeerConnection_RoleSwitch(t *testing.T) {
api := NewAPI()
lim := test.TimeOut(time.Second * 30)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
api.mediaEngine.RegisterDefaultCodecs()
pcFirstOfferer, pcSecondOfferer, err := api.newPair(Configuration{})
pcFirstOfferer, pcSecondOfferer, err := newPair()
if err != nil {
t.Fatal(err)
}
onTrackFired, onTrackFiredFunc := context.WithCancel(context.Background())
pcFirstOfferer.OnTrack(func(track *Track, r *RTPReceiver) {
pcFirstOfferer.OnTrack(func(track *TrackRemote, r *RTPReceiver) {
onTrackFiredFunc()
})
@@ -510,14 +492,14 @@ func TestPeerConnection_RoleSwitch(t *testing.T) {
_, err = pcFirstOfferer.AddTransceiverFromKind(RTPCodecTypeVideo, RtpTransceiverInit{Direction: RTPTransceiverDirectionRecvonly})
assert.NoError(t, err)
vp8Track, err := pcSecondOfferer.NewTrack(DefaultPayloadTypeVP8, randutil.NewMathRandomGenerator().Uint32(), "foo", "bar")
vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "foo", "bar")
assert.NoError(t, err)
_, err = pcSecondOfferer.AddTrack(vp8Track)
assert.NoError(t, err)
assert.NoError(t, signalPair(pcSecondOfferer, pcFirstOfferer))
sendVideoUntilDone(onTrackFired.Done(), t, []*Track{vp8Track})
sendVideoUntilDone(onTrackFired.Done(), t, []*TrackLocalStaticSample{vp8Track})
assert.NoError(t, pcFirstOfferer.Close())
assert.NoError(t, pcSecondOfferer.Close())
@@ -536,7 +518,7 @@ func TestPeerConnection_Renegotiation_Trickle(t *testing.T) {
settingEngine := SettingEngine{}
api := NewAPI(WithSettingEngine(settingEngine))
api.mediaEngine.RegisterDefaultCodecs()
assert.NoError(t, api.mediaEngine.RegisterDefaultCodecs())
// Invalid STUN server on purpose, will stop ICE Gathering from completing in time
pcOffer, pcAnswer, err := api.newPair(Configuration{
@@ -595,21 +577,19 @@ func TestPeerConnection_Renegotiation_Trickle(t *testing.T) {
}
func TestPeerConnection_Renegotiation_SetLocalDescription(t *testing.T) {
api := NewAPI()
lim := test.TimeOut(time.Second * 30)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
api.mediaEngine.RegisterDefaultCodecs()
pcOffer, pcAnswer, err := api.newPair(Configuration{})
pcOffer, pcAnswer, err := newPair()
if err != nil {
t.Fatal(err)
}
onTrackFired, onTrackFiredFunc := context.WithCancel(context.Background())
pcOffer.OnTrack(func(track *Track, r *RTPReceiver) {
pcOffer.OnTrack(func(track *TrackRemote, r *RTPReceiver) {
onTrackFiredFunc()
})
@@ -621,7 +601,7 @@ func TestPeerConnection_Renegotiation_SetLocalDescription(t *testing.T) {
_, err = pcOffer.AddTransceiverFromKind(RTPCodecTypeVideo, RtpTransceiverInit{Direction: RTPTransceiverDirectionRecvonly})
assert.NoError(t, err)
localTrack, err := pcAnswer.NewTrack(DefaultPayloadTypeVP8, randutil.NewMathRandomGenerator().Uint32(), "foo", "bar")
localTrack, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "foo", "bar")
assert.NoError(t, err)
sender, err := pcAnswer.AddTrack(localTrack)
@@ -637,16 +617,16 @@ func TestPeerConnection_Renegotiation_SetLocalDescription(t *testing.T) {
assert.True(t, sender.isNegotiated())
pcAnswer.ops.Done()
assert.Equal(t, 0, len(localTrack.activeSenders))
assert.Equal(t, 0, len(localTrack.rtpTrack.bindings))
assert.NoError(t, pcAnswer.SetLocalDescription(answer))
pcAnswer.ops.Done()
assert.Equal(t, 1, len(localTrack.activeSenders))
assert.Equal(t, 1, len(localTrack.rtpTrack.bindings))
assert.NoError(t, pcOffer.SetRemoteDescription(answer))
sendVideoUntilDone(onTrackFired.Done(), t, []*Track{localTrack})
sendVideoUntilDone(onTrackFired.Done(), t, []*TrackLocalStaticSample{localTrack})
assert.NoError(t, pcOffer.Close())
assert.NoError(t, pcAnswer.Close())
@@ -674,15 +654,13 @@ func TestPeerConnection_Renegotiation_NoApplication(t *testing.T) {
assert.NoError(t, pcOffer.SetRemoteDescription(*pcAnswer.LocalDescription()))
}
api := NewAPI()
lim := test.TimeOut(time.Second * 30)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
api.mediaEngine.RegisterDefaultCodecs()
pcOffer, pcAnswer, err := api.newPair(Configuration{})
pcOffer, pcAnswer, err := newPair()
if err != nil {
t.Fatal(err)
}
@@ -738,7 +716,7 @@ func TestAddDataChannelDuringRenegotation(t *testing.T) {
pcAnswer, err := NewPeerConnection(Configuration{})
assert.NoError(t, err)
track, err := pcOffer.NewTrack(DefaultPayloadTypeVP8, randutil.NewMathRandomGenerator().Uint32(), "video", "pion")
track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
assert.NoError(t, err)
_, err = pcOffer.AddTrack(track)
@@ -831,7 +809,7 @@ func TestNegotiationNeededRemoveTrack(t *testing.T) {
pcAnswer, err := NewPeerConnection(Configuration{})
assert.NoError(t, err)
track, err := pcOffer.NewTrack(DefaultPayloadTypeVP8, randutil.NewMathRandomGenerator().Uint32(), "video", "pion")
track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
assert.NoError(t, err)
pcOffer.OnNegotiationNeeded(func() {
@@ -860,14 +838,12 @@ func TestNegotiationNeededRemoveTrack(t *testing.T) {
sender, err := pcOffer.AddTrack(track)
assert.NoError(t, err)
err = track.WriteSample(media.Sample{Data: []byte{0x00}, Samples: 1})
assert.NoError(t, err)
assert.NoError(t, track.WriteSample(media.Sample{Data: []byte{0x00}, Duration: time.Second}))
wg.Wait()
wg.Add(1)
err = pcOffer.RemoveTrack(sender)
assert.NoError(t, err)
assert.NoError(t, pcOffer.RemoveTrack(sender))
wg.Wait()
@@ -876,15 +852,13 @@ func TestNegotiationNeededRemoveTrack(t *testing.T) {
}
func TestNegotiationNeededStressOneSided(t *testing.T) {
api := NewAPI()
lim := test.TimeOut(time.Second * 30)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
api.mediaEngine.RegisterDefaultCodecs()
pcA, pcB, err := api.newPair(Configuration{})
pcA, pcB, err := newPair()
assert.NoError(t, err)
var wg sync.WaitGroup
@@ -895,13 +869,13 @@ func TestNegotiationNeededStressOneSided(t *testing.T) {
})
for i := 0; i < 500; i++ {
track, err := pcA.NewTrack(DefaultPayloadTypeVP8, randutil.NewMathRandomGenerator().Uint32(), "video", "pion")
track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
assert.NoError(t, err)
_, err = pcA.AddTrack(track)
assert.NoError(t, err)
err = track.WriteSample(media.Sample{Data: []byte{0x00}, Samples: 1})
err = track.WriteSample(media.Sample{Data: []byte{0x00}, Duration: time.Second})
assert.NoError(t, err)
time.Sleep(10 * time.Millisecond)
@@ -919,15 +893,13 @@ func TestNegotiationNeededStressOneSided(t *testing.T) {
// TestPeerConnection_Renegotiation_DisableTrack asserts that if a remote track is set inactive
// that locally it goes inactive as well
func TestPeerConnection_Renegotiation_DisableTrack(t *testing.T) {
api := NewAPI()
lim := test.TimeOut(time.Second * 30)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
api.mediaEngine.RegisterDefaultCodecs()
pcOffer, pcAnswer, err := api.newPair(Configuration{})
pcOffer, pcAnswer, err := newPair()
assert.NoError(t, err)
// Create two transceivers

View File

@@ -7,17 +7,11 @@ import (
"github.com/pion/rtp"
)
// A Sample contains encoded media and the number of samples in that media (see NSamples).
// A Sample contains encoded media and timing information
type Sample struct {
Data []byte
Samples uint32
}
// NSamples calculates the number of samples in media of length d with sampling frequency f.
// For example, NSamples(20 * time.Millisecond, 48000) will return the number of samples
// in a 20 millisecond segment of Opus audio recorded at 48000 samples per second.
func NSamples(d time.Duration, freq int) uint32 {
return uint32(time.Duration(freq) * d / time.Second)
Data []byte
Timestamp time.Time
Duration time.Duration
}
// Writer defines an interface to handle

View File

@@ -1,13 +1 @@
package media_test
import (
"testing"
"time"
"github.com/pion/webrtc/v3/pkg/media"
"github.com/stretchr/testify/assert"
)
func TestNSamples(t *testing.T) {
assert.Equal(t, media.NSamples(20*time.Millisecond, 48000), uint32(48000*0.02))
}

View File

@@ -2,6 +2,8 @@
package samplebuilder
import (
"time"
"github.com/pion/rtp"
"github.com/pion/webrtc/v3/pkg/media"
)
@@ -14,6 +16,9 @@ type SampleBuilder struct {
// Interface that allows us to take RTP packets to samples
depacketizer rtp.Depacketizer
// sampleRate allows us to compute duration of media.SamplecA
sampleRate uint32
// Last seqnum that has been added to buffer
lastPush uint16
@@ -34,8 +39,8 @@ type SampleBuilder struct {
// A large maxLate will result in less packet loss but higher latency.
// The depacketizer extracts media samples from RTP packets.
// Several depacketizers are available in package github.com/pion/rtp/codecs.
func New(maxLate uint16, depacketizer rtp.Depacketizer, opts ...Option) *SampleBuilder {
s := &SampleBuilder{maxLate: maxLate, depacketizer: depacketizer}
func New(maxLate uint16, depacketizer rtp.Depacketizer, sampleRate uint32, opts ...Option) *SampleBuilder {
s := &SampleBuilder{maxLate: maxLate, depacketizer: depacketizer, sampleRate: sampleRate}
for _, o := range opts {
o(s)
}
@@ -84,7 +89,8 @@ func (s *SampleBuilder) buildSample(firstBuffer uint16) (*media.Sample, uint32)
for j := firstBuffer; j < i; j++ {
s.buffer[j] = nil
}
return &media.Sample{Data: data, Samples: samples}, s.lastPopTimestamp
return &media.Sample{Data: data, Duration: time.Duration((samples/s.sampleRate)*1000) * time.Millisecond}, s.lastPopTimestamp
}
p, err := s.depacketizer.Unmarshal(s.buffer[i].Payload)

View File

@@ -3,6 +3,7 @@ package samplebuilder
import (
"fmt"
"testing"
"time"
"github.com/pion/rtp"
"github.com/pion/webrtc/v3/pkg/media"
@@ -58,7 +59,7 @@ func TestSampleBuilder(t *testing.T) {
{Header: rtp.Header{SequenceNumber: 5002, Timestamp: 7}, Payload: []byte{0x03}},
},
samples: []*media.Sample{
{Data: []byte{0x02}, Samples: 1},
{Data: []byte{0x02}, Duration: time.Second},
},
timestamps: []uint32{
6,
@@ -74,7 +75,7 @@ func TestSampleBuilder(t *testing.T) {
{Header: rtp.Header{SequenceNumber: 5003, Timestamp: 7}, Payload: []byte{0x04}},
},
samples: []*media.Sample{
{Data: []byte{0x02, 0x03}, Samples: 1},
{Data: []byte{0x02, 0x03}, Duration: time.Second},
},
timestamps: []uint32{
6,
@@ -102,7 +103,7 @@ func TestSampleBuilder(t *testing.T) {
withHeadChecker: true,
headBytes: []byte{0x02},
samples: []*media.Sample{
{Data: []byte{0x02}, Samples: 0},
{Data: []byte{0x02}, Duration: 0},
},
timestamps: []uint32{
6,
@@ -133,10 +134,10 @@ func TestSampleBuilder(t *testing.T) {
{Header: rtp.Header{SequenceNumber: 5005, Timestamp: 6}, Payload: []byte{0x06}},
},
samples: []*media.Sample{
{Data: []byte{0x02}, Samples: 1},
{Data: []byte{0x03}, Samples: 1},
{Data: []byte{0x04}, Samples: 1},
{Data: []byte{0x05}, Samples: 1},
{Data: []byte{0x02}, Duration: time.Second},
{Data: []byte{0x03}, Duration: time.Second},
{Data: []byte{0x04}, Duration: time.Second},
{Data: []byte{0x05}, Duration: time.Second},
},
timestamps: []uint32{
2,
@@ -159,7 +160,7 @@ func TestSampleBuilder(t *testing.T) {
))
}
s := New(t.maxLate, &fakeDepacketizer{}, opts...)
s := New(t.maxLate, &fakeDepacketizer{}, 1, opts...)
samples := []*media.Sample{}
for _, p := range t.packets {
@@ -183,7 +184,7 @@ func TestSampleBuilder(t *testing.T) {
))
}
s := New(t.maxLate, &fakeDepacketizer{}, opts...)
s := New(t.maxLate, &fakeDepacketizer{}, 1, opts...)
samples := []*media.Sample{}
timestamps := []uint32{}
@@ -204,17 +205,17 @@ func TestSampleBuilder(t *testing.T) {
// SampleBuilder should respect maxLate if we popped successfully but then have a gap larger then maxLate
func TestSampleBuilderMaxLate(t *testing.T) {
assert := assert.New(t)
s := New(50, &fakeDepacketizer{})
s := New(50, &fakeDepacketizer{}, 1)
s.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 0, Timestamp: 1}, Payload: []byte{0x01}})
s.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 1, Timestamp: 2}, Payload: []byte{0x01}})
s.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 2, Timestamp: 3}, Payload: []byte{0x01}})
assert.Equal(s.Pop(), &media.Sample{Data: []byte{0x01}, Samples: 1}, "Failed to build samples before gap")
assert.Equal(s.Pop(), &media.Sample{Data: []byte{0x01}, Duration: time.Second}, "Failed to build samples before gap")
s.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 5000, Timestamp: 500}, Payload: []byte{0x02}})
s.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 5001, Timestamp: 501}, Payload: []byte{0x02}})
s.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 5002, Timestamp: 502}, Payload: []byte{0x02}})
assert.Equal(s.Pop(), &media.Sample{Data: []byte{0x02}, Samples: 1}, "Failed to build samples after large gap")
assert.Equal(s.Pop(), &media.Sample{Data: []byte{0x02}, Duration: time.Second}, "Failed to build samples after large gap")
}
func TestSeqnumDistance(t *testing.T) {
@@ -247,7 +248,7 @@ func TestSampleBuilderCleanReference(t *testing.T) {
} {
seqStart := seqStart
t.Run(fmt.Sprintf("From%d", seqStart), func(t *testing.T) {
s := New(10, &fakeDepacketizer{})
s := New(10, &fakeDepacketizer{}, 1)
s.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 0 + seqStart, Timestamp: 0}, Payload: []byte{0x01}})
s.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 1 + seqStart, Timestamp: 0}, Payload: []byte{0x02}})

7
rtpcapabilities.go Normal file
View File

@@ -0,0 +1,7 @@
package webrtc
// RTPCapabilities represents the capabilities of a transceiver
type RTPCapabilities struct {
Codecs []RTPCodecCapability
HeaderExtensions []RTPHeaderExtensionCapability
}

99
rtpcodec.go Normal file
View File

@@ -0,0 +1,99 @@
package webrtc
import (
"strings"
)
// RTPCodecType determines the type of a codec
type RTPCodecType int
const (
// RTPCodecTypeAudio indicates this is an audio codec
RTPCodecTypeAudio RTPCodecType = iota + 1
// RTPCodecTypeVideo indicates this is a video codec
RTPCodecTypeVideo
)
func (t RTPCodecType) String() string {
switch t {
case RTPCodecTypeAudio:
return "audio"
case RTPCodecTypeVideo:
return "video" //nolint: goconst
default:
return ErrUnknownType.Error()
}
}
// NewRTPCodecType creates a RTPCodecType from a string
func NewRTPCodecType(r string) RTPCodecType {
switch {
case strings.EqualFold(r, RTPCodecTypeAudio.String()):
return RTPCodecTypeAudio
case strings.EqualFold(r, RTPCodecTypeVideo.String()):
return RTPCodecTypeVideo
default:
return RTPCodecType(0)
}
}
// RTPCodecCapability provides information about codec capabilities.
//
// https://w3c.github.io/webrtc-pc/#dictionary-rtcrtpcodeccapability-members
type RTPCodecCapability struct {
MimeType string
ClockRate uint32
Channels uint16
SDPFmtpLine string
RTCPFeedback []RTCPFeedback
}
// RTPHeaderExtensionCapability is used to define a RFC5285 RTP header extension supported by the codec.
//
// https://w3c.github.io/webrtc-pc/#dom-rtcrtpcapabilities-headerextensions
type RTPHeaderExtensionCapability struct {
URI string
}
// RTPCodecParameters is a sequence containing the media codecs that an RtpSender
// will choose from, as well as entries for RTX, RED and FEC mechanisms. This also
// includes the PayloadType that has been negotiated
//
// https://w3c.github.io/webrtc-pc/#rtcrtpcodecparameters
type RTPCodecParameters struct {
RTPCodecCapability
PayloadType PayloadType
statsID string
}
// RTCRtpCapabilities is a list of supported codecs and header extensions
//
// https://w3c.github.io/webrtc-pc/#rtcrtpcapabilities
type RTCRtpCapabilities struct {
HeaderExtensions []RTPHeaderExtensionCapability
Codecs []RTPCodecCapability
}
// Do a fuzzy find for a codec in the list of codecs
// Used for lookup up a codec in an existing list to find a match
func codecParametersFuzzySearch(needle RTPCodecParameters, haystack []RTPCodecParameters) (RTPCodecParameters, error) {
// First attempt to match on MimeType + SDPFmtpLine
for _, c := range haystack {
if strings.EqualFold(c.RTPCodecCapability.MimeType, needle.RTPCodecCapability.MimeType) &&
c.RTPCodecCapability.SDPFmtpLine == needle.RTPCodecCapability.SDPFmtpLine {
return c, nil
}
}
// Fallback to just MimeType
for _, c := range haystack {
if strings.EqualFold(c.RTPCodecCapability.MimeType, needle.RTPCodecCapability.MimeType) {
return c, nil
}
}
return RTPCodecParameters{}, ErrCodecNotFound
}

View File

@@ -4,7 +4,7 @@ package webrtc
// This is a subset of the RFC since Pion WebRTC doesn't implement encoding/decoding itself
// http://draft.ortc.org/#dom-rtcrtpcodingparameters
type RTPCodingParameters struct {
RID string `json:"rid"`
SSRC uint32 `json:"ssrc"`
PayloadType uint8 `json:"payloadType"`
RID string `json:"rid"`
SSRC SSRC `json:"ssrc"`
PayloadType PayloadType `json:"payloadType"`
}

View File

@@ -14,12 +14,12 @@ import (
// trackStreams maintains a mapping of RTP/RTCP streams to a specific track
// a RTPReceiver may contain multiple streams if we are dealing with Multicast
type trackStreams struct {
track *Track
track *TrackRemote
rtpReadStream *srtp.ReadStreamSRTP
rtcpReadStream *srtp.ReadStreamSRTCP
}
// RTPReceiver allows an application to inspect the receipt of a Track
// RTPReceiver allows an application to inspect the receipt of a TrackRemote
type RTPReceiver struct {
kind RTPCodecType
transport *DTLSTransport
@@ -57,8 +57,8 @@ func (r *RTPReceiver) Transport() *DTLSTransport {
return r.transport
}
// Track returns the RtpTransceiver track
func (r *RTPReceiver) Track() *Track {
// Track returns the RtpTransceiver TrackRemote
func (r *RTPReceiver) Track() *TrackRemote {
r.mu.RLock()
defer r.mu.RUnlock()
@@ -70,11 +70,11 @@ func (r *RTPReceiver) Track() *Track {
// Tracks returns the RtpTransceiver tracks
// A RTPReceiver to support Simulcast may now have multiple tracks
func (r *RTPReceiver) Tracks() []*Track {
func (r *RTPReceiver) Tracks() []*TrackRemote {
r.mu.RLock()
defer r.mu.RUnlock()
var tracks []*Track
var tracks []*TrackRemote
for i := range r.tracks {
tracks = append(tracks, r.tracks[i].track)
}
@@ -94,7 +94,7 @@ func (r *RTPReceiver) Receive(parameters RTPReceiveParameters) error {
if len(parameters.Encodings) == 1 && parameters.Encodings[0].SSRC != 0 {
t := trackStreams{
track: &Track{
track: &TrackRemote{
kind: r.kind,
ssrc: parameters.Encodings[0].SSRC,
receiver: r,
@@ -111,7 +111,7 @@ func (r *RTPReceiver) Receive(parameters RTPReceiveParameters) error {
} else {
for _, encoding := range parameters.Encodings {
r.tracks = append(r.tracks, trackStreams{
track: &Track{
track: &TrackRemote{
kind: r.kind,
rid: encoding.RID,
receiver: r,
@@ -211,7 +211,7 @@ func (r *RTPReceiver) Stop() error {
return nil
}
func (r *RTPReceiver) streamsForTrack(t *Track) *trackStreams {
func (r *RTPReceiver) streamsForTrack(t *TrackRemote) *trackStreams {
for i := range r.tracks {
if r.tracks[i].track == t {
return &r.tracks[i]
@@ -221,7 +221,7 @@ func (r *RTPReceiver) streamsForTrack(t *Track) *trackStreams {
}
// readRTP should only be called by a track, this only exists so we can keep state in one place
func (r *RTPReceiver) readRTP(b []byte, reader *Track) (n int, err error) {
func (r *RTPReceiver) readRTP(b []byte, reader *TrackRemote) (n int, err error) {
<-r.received
if t := r.streamsForTrack(reader); t != nil {
return t.rtpReadStream.Read(b)
@@ -232,14 +232,14 @@ func (r *RTPReceiver) readRTP(b []byte, reader *Track) (n int, err error) {
// receiveForRid is the sibling of Receive expect for RIDs instead of SSRCs
// It populates all the internal state for the given RID
func (r *RTPReceiver) receiveForRid(rid string, codec *RTPCodec, ssrc uint32) (*Track, error) {
func (r *RTPReceiver) receiveForRid(rid string, codec RTPCodecParameters, ssrc SSRC) (*TrackRemote, error) {
r.mu.Lock()
defer r.mu.Unlock()
for i := range r.tracks {
if r.tracks[i].track.RID() == rid {
r.tracks[i].track.mu.Lock()
r.tracks[i].track.kind = codec.Type
r.tracks[i].track.kind = r.kind
r.tracks[i].track.codec = codec
r.tracks[i].track.ssrc = ssrc
r.tracks[i].track.mu.Unlock()
@@ -257,13 +257,13 @@ func (r *RTPReceiver) receiveForRid(rid string, codec *RTPCodec, ssrc uint32) (*
return nil, fmt.Errorf("%w: %d", errRTPReceiverForSSRCTrackStreamNotFound, ssrc)
}
func (r *RTPReceiver) streamsForSSRC(ssrc uint32) (*srtp.ReadStreamSRTP, *srtp.ReadStreamSRTCP, error) {
func (r *RTPReceiver) streamsForSSRC(ssrc SSRC) (*srtp.ReadStreamSRTP, *srtp.ReadStreamSRTCP, error) {
srtpSession, err := r.transport.getSRTPSession()
if err != nil {
return nil, nil, err
}
rtpReadStream, err := srtpSession.OpenReadStream(ssrc)
rtpReadStream, err := srtpSession.OpenReadStream(uint32(ssrc))
if err != nil {
return nil, nil, err
}
@@ -273,7 +273,7 @@ func (r *RTPReceiver) streamsForSSRC(ssrc uint32) (*srtp.ReadStreamSRTP, *srtp.R
return nil, nil, err
}
rtcpReadStream, err := srtcpSession.OpenReadStream(ssrc)
rtcpReadStream, err := srtcpSession.OpenReadStream(uint32(ssrc))
if err != nil {
return nil, nil, err
}

View File

@@ -6,18 +6,21 @@ import (
"io"
"sync"
"github.com/pion/randutil"
"github.com/pion/rtcp"
"github.com/pion/rtp"
"github.com/pion/srtp"
)
// RTPSender allows an application to control how a given Track is encoded and transmitted to a remote peer
type RTPSender struct {
track *Track
track TrackLocal
rtcpReadStream *srtp.ReadStreamSRTCP
transport *DTLSTransport
payloadType PayloadType
ssrc SSRC
// nolint:godox
// TODO(sgotti) remove this when in future we'll avoid replacing
// a transceiver sender since we can just check the
@@ -26,25 +29,24 @@ type RTPSender struct {
// A reference to the associated api object
api *API
id string
mu sync.RWMutex
sendCalled, stopCalled chan interface{}
}
// NewRTPSender constructs a new RTPSender
func (api *API) NewRTPSender(track *Track, transport *DTLSTransport) (*RTPSender, error) {
func (api *API) NewRTPSender(track TrackLocal, transport *DTLSTransport) (*RTPSender, error) {
if track == nil {
return nil, errRTPSenderTrackNil
} else if transport == nil {
return nil, errRTPSenderDTLSTransportNil
}
track.mu.Lock()
defer track.mu.Unlock()
if track.receiver != nil {
return nil, errRTPSenderCannotConstructRemoteTrack
id, err := randutil.GenerateCryptoRandomString(32, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
if err != nil {
return nil, err
}
track.totalSenderCount++
return &RTPSender{
track: track,
@@ -52,6 +54,8 @@ func (api *API) NewRTPSender(track *Track, transport *DTLSTransport) (*RTPSender
api: api,
sendCalled: make(chan interface{}),
stopCalled: make(chan interface{}),
ssrc: SSRC(randutil.NewMathRandomGenerator().Uint32()),
id: id,
}, nil
}
@@ -76,13 +80,13 @@ func (r *RTPSender) Transport() *DTLSTransport {
}
// Track returns the RTCRtpTransceiver track, or nil
func (r *RTPSender) Track() *Track {
func (r *RTPSender) Track() TrackLocal {
r.mu.RLock()
defer r.mu.RUnlock()
return r.track
}
func (r *RTPSender) setTrack(track *Track) {
func (r *RTPSender) setTrack(track TrackLocal) {
r.mu.Lock()
defer r.mu.Unlock()
r.track = track
@@ -102,14 +106,29 @@ func (r *RTPSender) Send(parameters RTPSendParameters) error {
return err
}
r.rtcpReadStream, err = srtcpSession.OpenReadStream(parameters.Encodings.SSRC)
r.rtcpReadStream, err = srtcpSession.OpenReadStream(uint32(parameters.Encodings.SSRC))
if err != nil {
return err
}
r.track.mu.Lock()
r.track.activeSenders = append(r.track.activeSenders, r)
r.track.mu.Unlock()
srtpSession, err := r.transport.getSRTPSession()
if err != nil {
return err
}
rtpWriteStream, err := srtpSession.OpenWriteStream()
if err != nil {
return err
}
if err = r.track.Bind(TrackLocalContext{
id: r.id,
codecs: r.api.mediaEngine.getCodecsByKind(r.track.Kind()),
ssrc: parameters.Encodings.SSRC,
writeStream: rtpWriteStream,
}); err != nil {
return err
}
close(r.sendCalled)
return nil
@@ -125,25 +144,21 @@ func (r *RTPSender) Stop() error {
return nil
default:
}
r.track.mu.Lock()
defer r.track.mu.Unlock()
filtered := []*RTPSender{}
for _, s := range r.track.activeSenders {
if s != r {
filtered = append(filtered, s)
} else {
r.track.totalSenderCount--
}
}
r.track.activeSenders = filtered
close(r.stopCalled)
if r.hasSent() {
return r.rtcpReadStream.Close()
if !r.hasSent() {
return nil
}
return nil
if err := r.track.Unbind(TrackLocalContext{
id: r.id,
codecs: r.api.mediaEngine.getCodecsByKind(r.track.Kind()),
ssrc: r.ssrc,
}); err != nil {
return err
}
return r.rtcpReadStream.Close()
}
// Read reads incoming RTCP for this RTPReceiver
@@ -167,31 +182,6 @@ func (r *RTPSender) ReadRTCP() ([]rtcp.Packet, error) {
return rtcp.Unmarshal(b[:i])
}
// SendRTP sends a RTP packet on this RTPSender
//
// You should use Track instead to send packets. This is exposed because pion/webrtc currently
// provides no way for users to send RTP packets directly. This is makes users unable to send
// retransmissions to a single RTPSender. in /v3 this will go away, only use this API if you really
// need it.
func (r *RTPSender) SendRTP(header *rtp.Header, payload []byte) (int, error) {
select {
case <-r.stopCalled:
return 0, errRTPSenderStopped
case <-r.sendCalled:
srtpSession, err := r.transport.getSRTPSession()
if err != nil {
return 0, err
}
writeStream, err := srtpSession.OpenWriteStream()
if err != nil {
return 0, err
}
return writeStream.WriteRTP(header, payload)
}
}
// hasSent tells if data has been ever sent for this instance
func (r *RTPSender) hasSent() bool {
select {

View File

@@ -7,7 +7,6 @@ import (
"sync/atomic"
"github.com/pion/rtp"
"github.com/pion/sdp/v3"
)
// RTPTransceiver represents a combination of an RTPSender and an RTPReceiver that share a common mid.
@@ -31,7 +30,7 @@ func (t *RTPTransceiver) Sender() *RTPSender {
}
// SetSender sets the RTPSender and Track to current transceiver
func (t *RTPTransceiver) SetSender(s *RTPSender, track *Track) error {
func (t *RTPTransceiver) SetSender(s *RTPSender, track TrackLocal) error {
t.setSender(s)
return t.setSendingTrack(track)
}
@@ -101,7 +100,7 @@ func (t *RTPTransceiver) setDirection(d RTPTransceiverDirection) {
t.direction.Store(d)
}
func (t *RTPTransceiver) setSendingTrack(track *Track) error {
func (t *RTPTransceiver) setSendingTrack(track TrackLocal) error {
t.Sender().setTrack(track)
if track == nil {
t.setSender(nil)
@@ -163,7 +162,7 @@ func satisfyTypeAndDirection(remoteKind RTPCodecType, remoteDirection RTPTransce
// handleUnknownRTPPacket consumes a single RTP Packet and returns information that is helpful
// for demuxing and handling an unknown SSRC (usually for Simulcast)
func handleUnknownRTPPacket(buf []byte, sdesMidExtMap, sdesStreamIDExtMap *sdp.ExtMap) (mid, rid string, payloadType uint8, err error) {
func handleUnknownRTPPacket(buf []byte, midExtensionID, streamIDExtensionID uint8) (mid, rid string, payloadType PayloadType, err error) {
rp := &rtp.Packet{}
if err = rp.Unmarshal(buf); err != nil {
return
@@ -173,12 +172,12 @@ func handleUnknownRTPPacket(buf []byte, sdesMidExtMap, sdesStreamIDExtMap *sdp.E
return
}
payloadType = rp.PayloadType
if payload := rp.GetExtension(uint8(sdesMidExtMap.Value)); payload != nil {
payloadType = PayloadType(rp.PayloadType)
if payload := rp.GetExtension(midExtensionID); payload != nil {
mid = string(payload)
}
if payload := rp.GetExtension(uint8(sdesStreamIDExtMap.Value)); payload != nil {
if payload := rp.GetExtension(streamIDExtensionID); payload != nil {
rid = string(payload)
}

View File

@@ -7,4 +7,6 @@ type RTPTransceiverInit struct {
// Streams []*Track
}
// RtpTransceiverInit is a temporary mapping while we fix case sensitivity
// Deprecated: Use RTPTransceiverInit instead
type RtpTransceiverInit = RTPTransceiverInit //nolint: stylecheck,golint

235
sdp.go
View File

@@ -4,6 +4,7 @@ package webrtc
import (
"fmt"
"net/url"
"regexp"
"strconv"
"strings"
@@ -16,15 +17,15 @@ import (
// trackDetails represents any media source that can be represented in a SDP
// This isn't keyed by SSRC because it also needs to support rid based sources
type trackDetails struct {
mid string
kind RTPCodecType
label string
id string
ssrc uint32
rids []string
mid string
kind RTPCodecType
streamID string
id string
ssrc SSRC
rids []string
}
func trackDetailsForSSRC(trackDetails []trackDetails, ssrc uint32) *trackDetails {
func trackDetailsForSSRC(trackDetails []trackDetails, ssrc SSRC) *trackDetails {
for i := range trackDetails {
if trackDetails[i].ssrc == ssrc {
return &trackDetails[i]
@@ -33,7 +34,7 @@ func trackDetailsForSSRC(trackDetails []trackDetails, ssrc uint32) *trackDetails
return nil
}
func filterTrackWithSSRC(incomingTracks []trackDetails, ssrc uint32) []trackDetails {
func filterTrackWithSSRC(incomingTracks []trackDetails, ssrc SSRC) []trackDetails {
filtered := []trackDetails{}
for i := range incomingTracks {
if incomingTracks[i].ssrc != ssrc {
@@ -43,16 +44,6 @@ func filterTrackWithSSRC(incomingTracks []trackDetails, ssrc uint32) []trackDeta
return filtered
}
// SDPSectionType specifies media type sections
type SDPSectionType string
// Common SDP sections
const (
SDPSectionGlobal = SDPSectionType("global")
SDPSectionVideo = SDPSectionType("video")
SDPSectionAudio = SDPSectionType("audio")
)
// extract all trackDetails from an SDP.
func trackDetailsFromSDP(log logging.LeveledLogger, s *sdp.SessionDescription) []trackDetails { // nolint:gocognit
incomingTracks := []trackDetails{}
@@ -60,7 +51,7 @@ func trackDetailsFromSDP(log logging.LeveledLogger, s *sdp.SessionDescription) [
for _, media := range s.MediaDescriptions {
// Plan B can have multiple tracks in a signle media section
trackLabel := ""
streamID := ""
trackID := ""
// If media section is recvonly or inactive skip
@@ -101,7 +92,7 @@ func trackDetailsFromSDP(log logging.LeveledLogger, s *sdp.SessionDescription) [
continue
}
rtxRepairFlows[uint32(rtxRepairFlow)] = true
incomingTracks = filterTrackWithSSRC(incomingTracks, uint32(rtxRepairFlow)) // Remove if rtx was added as track before
incomingTracks = filterTrackWithSSRC(incomingTracks, SSRC(rtxRepairFlow)) // Remove if rtx was added as track before
}
}
@@ -111,7 +102,7 @@ func trackDetailsFromSDP(log logging.LeveledLogger, s *sdp.SessionDescription) [
case sdp.AttrKeyMsid:
split := strings.Split(attr.Value, " ")
if len(split) == 2 {
trackLabel = split[0]
streamID = split[0]
trackID = split[1]
}
@@ -128,14 +119,14 @@ func trackDetailsFromSDP(log logging.LeveledLogger, s *sdp.SessionDescription) [
}
if len(split) == 3 && strings.HasPrefix(split[1], "msid:") {
trackLabel = split[1][len("msid:"):]
streamID = split[1][len("msid:"):]
trackID = split[2]
}
isNewTrack := true
trackDetails := &trackDetails{}
for i := range incomingTracks {
if incomingTracks[i].ssrc == uint32(ssrc) {
if incomingTracks[i].ssrc == SSRC(ssrc) {
trackDetails = &incomingTracks[i]
isNewTrack = false
}
@@ -143,9 +134,9 @@ func trackDetailsFromSDP(log logging.LeveledLogger, s *sdp.SessionDescription) [
trackDetails.mid = midValue
trackDetails.kind = codecType
trackDetails.label = trackLabel
trackDetails.streamID = streamID
trackDetails.id = trackID
trackDetails.ssrc = uint32(ssrc)
trackDetails.ssrc = SSRC(ssrc)
if isNewTrack {
incomingTracks = append(incomingTracks, *trackDetails)
@@ -153,13 +144,13 @@ func trackDetailsFromSDP(log logging.LeveledLogger, s *sdp.SessionDescription) [
}
}
if rids := getRids(media); len(rids) != 0 && trackID != "" && trackLabel != "" {
if rids := getRids(media); len(rids) != 0 && trackID != "" && streamID != "" {
newTrack := trackDetails{
mid: midValue,
kind: codecType,
label: trackLabel,
id: trackID,
rids: []string{},
mid: midValue,
kind: codecType,
streamID: streamID,
id: trackID,
rids: []string{},
}
for rid := range rids {
newTrack.rids = append(newTrack.rids, rid)
@@ -285,7 +276,7 @@ func populateLocalCandidates(sessionDescription *SessionDescription, i *ICEGathe
}
}
func addTransceiverSDP(d *sdp.SessionDescription, isPlanB, shouldAddCandidates bool, dtlsFingerprints []DTLSFingerprint, mediaEngine *MediaEngine, midValue string, iceParams ICEParameters, candidates []ICECandidate, dtlsRole sdp.ConnectionRole, iceGatheringState ICEGatheringState, extMaps map[SDPSectionType][]sdp.ExtMap, mediaSection mediaSection) (bool, error) {
func addTransceiverSDP(d *sdp.SessionDescription, isPlanB, shouldAddCandidates bool, dtlsFingerprints []DTLSFingerprint, mediaEngine *MediaEngine, midValue string, iceParams ICEParameters, candidates []ICECandidate, dtlsRole sdp.ConnectionRole, iceGatheringState ICEGatheringState, mediaSection mediaSection) (bool, error) {
transceivers := mediaSection.transceivers
if len(transceivers) < 1 {
return false, errSDPZeroTransceivers
@@ -299,9 +290,11 @@ func addTransceiverSDP(d *sdp.SessionDescription, isPlanB, shouldAddCandidates b
WithPropertyAttribute(sdp.AttrKeyRTCPMux).
WithPropertyAttribute(sdp.AttrKeyRTCPRsize)
codecs := mediaEngine.GetCodecsByKind(t.kind)
codecs := mediaEngine.getCodecsByKind(t.kind)
for _, codec := range codecs {
media.WithCodec(codec.PayloadType, codec.Name, codec.ClockRate, codec.Channels, codec.SDPFmtpLine)
name := strings.TrimPrefix(codec.MimeType, "audio/")
name = strings.TrimPrefix(name, "video/")
media.WithCodec(uint8(codec.PayloadType), name, codec.ClockRate, codec.Channels, codec.SDPFmtpLine)
for _, feedback := range codec.RTPCodecCapability.RTCPFeedback {
media.WithValueAttribute("rtcp-fb", fmt.Sprintf("%d %s %s", codec.PayloadType, feedback.Type, feedback.Parameter))
@@ -320,11 +313,12 @@ func addTransceiverSDP(d *sdp.SessionDescription, isPlanB, shouldAddCandidates b
return false, nil
}
// Add extmaps
if maps, ok := extMaps[SDPSectionType(t.kind.String())]; ok {
for _, m := range maps {
media.WithExtMap(m)
for id, rtpExtension := range mediaEngine.negotiatedHeaderExtensionsForType(t.kind) {
extURL, err := url.Parse(rtpExtension.uri)
if err != nil {
return false, err
}
media.WithExtMap(sdp.ExtMap{Value: id, URI: extURL})
}
if len(mediaSection.ridMap) > 0 {
@@ -341,9 +335,9 @@ func addTransceiverSDP(d *sdp.SessionDescription, isPlanB, shouldAddCandidates b
for _, mt := range transceivers {
if mt.Sender() != nil && mt.Sender().Track() != nil {
track := mt.Sender().Track()
media = media.WithMediaSource(track.SSRC(), track.Label() /* cname */, track.Label() /* streamLabel */, track.ID())
media = media.WithMediaSource(uint32(mt.Sender().ssrc), track.StreamID() /* cname */, track.StreamID() /* streamLabel */, track.ID())
if !isPlanB {
media = media.WithPropertyAttribute("msid:" + track.Label() + " " + track.ID())
media = media.WithPropertyAttribute("msid:" + track.StreamID() + " " + track.ID())
break
}
}
@@ -374,7 +368,7 @@ type mediaSection struct {
}
// populateSDP serializes a PeerConnections state into an SDP
func populateSDP(d *sdp.SessionDescription, isPlanB bool, dtlsFingerprints []DTLSFingerprint, mediaDescriptionFingerprint bool, isICELite bool, mediaEngine *MediaEngine, connectionRole sdp.ConnectionRole, candidates []ICECandidate, iceParams ICEParameters, mediaSections []mediaSection, iceGatheringState ICEGatheringState, extMaps map[SDPSectionType][]sdp.ExtMap) (*sdp.SessionDescription, error) {
func populateSDP(d *sdp.SessionDescription, isPlanB bool, dtlsFingerprints []DTLSFingerprint, mediaDescriptionFingerprint bool, isICELite bool, mediaEngine *MediaEngine, connectionRole sdp.ConnectionRole, candidates []ICECandidate, iceParams ICEParameters, mediaSections []mediaSection, iceGatheringState ICEGatheringState) (*sdp.SessionDescription, error) {
var err error
mediaDtlsFingerprints := []DTLSFingerprint{}
@@ -403,7 +397,7 @@ func populateSDP(d *sdp.SessionDescription, isPlanB bool, dtlsFingerprints []DTL
return nil, err
}
} else {
shouldAddID, err = addTransceiverSDP(d, isPlanB, shouldAddCanidates, mediaDtlsFingerprints, mediaEngine, m.id, iceParams, candidates, connectionRole, iceGatheringState, extMaps, m)
shouldAddID, err = addTransceiverSDP(d, isPlanB, shouldAddCanidates, mediaDtlsFingerprints, mediaEngine, m.id, iceParams, candidates, connectionRole, iceGatheringState, m)
if err != nil {
return nil, err
}
@@ -425,13 +419,6 @@ func populateSDP(d *sdp.SessionDescription, isPlanB bool, dtlsFingerprints []DTL
d = d.WithValueAttribute(sdp.AttrKeyICELite, sdp.AttrKeyICELite)
}
// Add global exts
if maps, ok := extMaps[SDPSectionGlobal]; ok {
for _, m := range maps {
d.WithPropertyAttribute(m.Marshal())
}
}
return d.WithValueAttribute(sdp.AttrKeyGroup, bundleValue), nil
}
@@ -565,94 +552,6 @@ func haveApplicationMediaSection(desc *sdp.SessionDescription) bool {
return false
}
func matchedAnswerExt(descriptions *sdp.SessionDescription, localMaps map[SDPSectionType][]sdp.ExtMap) (map[SDPSectionType][]sdp.ExtMap, error) {
remoteExtMaps, err := remoteExts(descriptions)
if err != nil {
return nil, err
}
return answerExtMaps(remoteExtMaps, localMaps), nil
}
func answerExtMaps(remoteExtMaps map[SDPSectionType]map[int]sdp.ExtMap, localMaps map[SDPSectionType][]sdp.ExtMap) map[SDPSectionType][]sdp.ExtMap {
ret := map[SDPSectionType][]sdp.ExtMap{}
for mediaType, remoteExtMap := range remoteExtMaps {
if _, ok := ret[mediaType]; !ok {
ret[mediaType] = []sdp.ExtMap{}
}
for _, extItem := range remoteExtMap {
// add remote ext that match locally available ones
for _, extMap := range localMaps[mediaType] {
if extMap.URI.String() == extItem.URI.String() {
ret[mediaType] = append(ret[mediaType], extItem)
}
}
}
}
return ret
}
func remoteExts(session *sdp.SessionDescription) (map[SDPSectionType]map[int]sdp.ExtMap, error) {
remoteExtMaps := map[SDPSectionType]map[int]sdp.ExtMap{}
maybeAddExt := func(attr sdp.Attribute, mediaType SDPSectionType) error {
if attr.Key != "extmap" {
return nil
}
em := &sdp.ExtMap{}
if err := em.Unmarshal("extmap:" + attr.Value); err != nil {
return fmt.Errorf("%w: %v", errSDPParseExtMap, err)
}
if remoteExtMap, ok := remoteExtMaps[mediaType][em.Value]; ok {
if remoteExtMap.Value != em.Value {
return errSDPRemoteDescriptionChangedExtMap
}
} else {
remoteExtMaps[mediaType][em.Value] = *em
}
return nil
}
// populate the extmaps from the current remote description
for _, media := range session.MediaDescriptions {
mediaType := SDPSectionType(media.MediaName.Media)
// populate known remote extmap and handle conflicts.
if _, ok := remoteExtMaps[mediaType]; !ok {
remoteExtMaps[mediaType] = map[int]sdp.ExtMap{}
}
for _, attr := range media.Attributes {
if err := maybeAddExt(attr, mediaType); err != nil {
return nil, err
}
}
}
// Add global exts
for _, attr := range session.Attributes {
if err := maybeAddExt(attr, SDPSectionGlobal); err != nil {
return nil, err
}
}
return remoteExtMaps, nil
}
// GetExtMapByURI return a copy of the extmap matching the provided
// URI. Note that the extmap value will change if not yet negotiated
func getExtMapByURI(exts map[SDPSectionType][]sdp.ExtMap, uri string) *sdp.ExtMap {
for _, extList := range exts {
for _, extMap := range extList {
if extMap.URI.String() == uri {
return &sdp.ExtMap{
Value: extMap.Value,
Direction: extMap.Direction,
URI: extMap.URI,
ExtAttr: extMap.ExtAttr,
}
}
}
}
return nil
}
func getByMid(searchMid string, desc *SessionDescription) *sdp.MediaDescription {
for _, m := range desc.parsed.MediaDescriptions {
if mid, ok := m.Attribute(sdp.AttrKeyMID); ok && mid == searchMid {
@@ -671,3 +570,65 @@ func haveDataChannel(desc *SessionDescription) *sdp.MediaDescription {
}
return nil
}
func codecsFromMediaDescription(m *sdp.MediaDescription) (out []RTPCodecParameters, err error) {
s := &sdp.SessionDescription{
MediaDescriptions: []*sdp.MediaDescription{m},
}
for _, payloadStr := range m.MediaName.Formats {
payloadType, err := strconv.Atoi(payloadStr)
if err != nil {
return nil, err
}
codec, err := s.GetCodecForPayloadType(uint8(payloadType))
if err != nil {
if payloadType == 0 {
continue
}
return nil, err
}
channels := uint16(0)
val, err := strconv.Atoi(codec.EncodingParameters)
if err == nil {
channels = uint16(val)
}
feedback := []RTCPFeedback{}
for _, raw := range codec.RTCPFeedback {
split := strings.Split(raw, " ")
entry := RTCPFeedback{Type: split[0]}
if len(split) == 2 {
entry.Parameter = split[1]
}
feedback = append(feedback, entry)
}
out = append(out, RTPCodecParameters{
RTPCodecCapability: RTPCodecCapability{m.MediaName.Media + "/" + codec.Name, codec.ClockRate, channels, codec.Fmtp, feedback},
PayloadType: PayloadType(payloadType),
})
}
return out, nil
}
func rtpExtensionsFromMediaDescription(m *sdp.MediaDescription) (map[string]int, error) {
out := map[string]int{}
for _, a := range m.Attributes {
if a.Key == sdp.AttrKeyExtMap {
e := sdp.ExtMap{}
if err := e.Unmarshal(*a.String()); err != nil {
return nil, err
}
out[e.URI.String()] = e.Value
}
}
return out, nil
}

View File

@@ -6,7 +6,6 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"net/url"
"strings"
"testing"
@@ -220,15 +219,15 @@ func TestTrackDetailsFromSDP(t *testing.T) {
assert.Fail(t, "missing audio track with ssrc:2000")
} else {
assert.Equal(t, RTPCodecTypeAudio, track.kind)
assert.Equal(t, uint32(2000), track.ssrc)
assert.Equal(t, "audio_trk_label", track.label)
assert.Equal(t, SSRC(2000), track.ssrc)
assert.Equal(t, "audio_trk_label", track.streamID)
}
if track := trackDetailsForSSRC(tracks, 3000); track == nil {
assert.Fail(t, "missing video track with ssrc:3000")
} else {
assert.Equal(t, RTPCodecTypeVideo, track.kind)
assert.Equal(t, uint32(3000), track.ssrc)
assert.Equal(t, "video_trk_label", track.label)
assert.Equal(t, SSRC(3000), track.ssrc)
assert.Equal(t, "video_trk_label", track.streamID)
}
if track := trackDetailsForSSRC(tracks, 4000); track != nil {
assert.Fail(t, "got the rtx track ssrc:3000 which should have been skipped")
@@ -237,9 +236,9 @@ func TestTrackDetailsFromSDP(t *testing.T) {
assert.Fail(t, "missing video track with ssrc:5000")
} else {
assert.Equal(t, RTPCodecTypeVideo, track.kind)
assert.Equal(t, uint32(5000), track.ssrc)
assert.Equal(t, SSRC(5000), track.ssrc)
assert.Equal(t, "video_trk_id", track.id)
assert.Equal(t, "video_stream_id", track.label)
assert.Equal(t, "video_stream_id", track.streamID)
}
})
@@ -306,8 +305,7 @@ func TestHaveApplicationMediaSection(t *testing.T) {
func TestMediaDescriptionFingerprints(t *testing.T) {
engine := &MediaEngine{}
engine.RegisterCodec(NewRTPH264Codec(DefaultPayloadTypeH264, 90000))
engine.RegisterCodec(NewRTPOpusCodec(DefaultPayloadTypeOpus, 48000))
assert.NoError(t, engine.RegisterDefaultCodecs())
sk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
assert.NoError(t, err)
@@ -349,7 +347,7 @@ func TestMediaDescriptionFingerprints(t *testing.T) {
s, err = populateSDP(s, false,
dtlsFingerprints,
SDPMediaDescriptionFingerprints,
false, engine, sdp.ConnectionRoleActive, []ICECandidate{}, ICEParameters{}, media, ICEGatheringStateNew, nil)
false, engine, sdp.ConnectionRoleActive, []ICECandidate{}, ICEParameters{}, media, ICEGatheringStateNew)
assert.NoError(t, err)
sdparray, err := s.Marshal()
@@ -364,67 +362,6 @@ func TestMediaDescriptionFingerprints(t *testing.T) {
}
func TestPopulateSDP(t *testing.T) {
t.Run("Extensions", func(t *testing.T) {
transportCCURL, _ := url.Parse(sdp.TransportCCURI)
absSendURL, _ := url.Parse(sdp.ABSSendTimeURI)
globalExts := []sdp.ExtMap{
{
URI: transportCCURL,
},
}
videoExts := []sdp.ExtMap{
{
URI: absSendURL,
},
}
tr := &RTPTransceiver{kind: RTPCodecTypeVideo}
tr.setDirection(RTPTransceiverDirectionSendrecv)
mediaSections := []mediaSection{{id: "video", transceivers: []*RTPTransceiver{tr}}}
se := SettingEngine{}
se.AddSDPExtensions(SDPSectionGlobal, globalExts)
se.AddSDPExtensions(SDPSectionVideo, videoExts)
m := MediaEngine{}
m.RegisterDefaultCodecs()
d := &sdp.SessionDescription{}
offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, &m, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete, se.getSDPExtensions())
assert.Nil(t, err)
// Check global extensions
var found bool
for _, a := range offerSdp.Attributes {
if strings.Contains(a.Key, transportCCURL.String()) {
found = true
break
}
}
assert.Equal(t, true, found, "Global extension should be present")
// Check video extension
found = false
var foundGlobal bool
for _, desc := range offerSdp.MediaDescriptions {
if desc.MediaName.Media != mediaNameVideo {
continue
}
for _, a := range desc.Attributes {
if strings.Contains(a.Key, absSendURL.String()) {
found = true
}
if strings.Contains(a.Key, transportCCURL.String()) {
foundGlobal = true
}
}
}
assert.Equal(t, true, found, "Video extension should be present")
// Test video does not contain global
assert.Equal(t, false, foundGlobal, "Global extension should not be present in video section")
})
t.Run("Rid", func(t *testing.T) {
tr := &RTPTransceiver{kind: RTPCodecTypeVideo}
tr.setDirection(RTPTransceiverDirectionRecvonly)
@@ -436,17 +373,17 @@ func TestPopulateSDP(t *testing.T) {
se := SettingEngine{}
m := MediaEngine{}
m.RegisterDefaultCodecs()
assert.NoError(t, m.RegisterDefaultCodecs())
d := &sdp.SessionDescription{}
offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, &m, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete, se.getSDPExtensions())
offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, &m, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete)
assert.Nil(t, err)
// Test contains rid map keys
var found bool
for _, desc := range offerSdp.MediaDescriptions {
if desc.MediaName.Media != mediaNameVideo {
if desc.MediaName.Media != "video" {
continue
}
for _, a := range desc.Attributes {
@@ -462,55 +399,6 @@ func TestPopulateSDP(t *testing.T) {
})
}
func TestMatchedAnswerExt(t *testing.T) {
s := &sdp.SessionDescription{
MediaDescriptions: []*sdp.MediaDescription{
{
MediaName: sdp.MediaName{
Media: "video",
},
Attributes: []sdp.Attribute{
{Key: "sendrecv"},
{Key: "ssrc", Value: "2000"},
{Key: "extmap", Value: "5 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time"},
},
},
},
}
transportCCURL, _ := url.Parse(sdp.TransportCCURI)
absSendURL, _ := url.Parse(sdp.ABSSendTimeURI)
exts := []sdp.ExtMap{
{
URI: transportCCURL,
},
{
URI: absSendURL,
},
}
se := SettingEngine{}
se.AddSDPExtensions(SDPSectionVideo, exts)
ansMaps, err := matchedAnswerExt(s, se.getSDPExtensions())
if err != nil {
t.Fatalf("Ext parse error %v", err)
}
if maps := ansMaps[SDPSectionVideo]; maps != nil {
// Check answer contains intersect of remote and local
// Abs send time should be only extenstion
assert.Equal(t, 1, len(maps), "Only one extension should be active")
assert.Equal(t, absSendURL, maps[0].URI, "Only abs-send-time should be active")
// Check answer uses remote IDs
assert.Equal(t, 5, maps[0].Value, "Should use remote ext ID")
} else {
t.Fatal("No video ext maps found")
}
}
func TestGetRIDs(t *testing.T) {
m := []*sdp.MediaDescription{
{
@@ -531,3 +419,65 @@ func TestGetRIDs(t *testing.T) {
assert.Fail(t, "rid values should contain 'f'")
}
}
func TestCodecsFromMediaDescription(t *testing.T) {
t.Run("Codec Only", func(t *testing.T) {
codecs, err := codecsFromMediaDescription(&sdp.MediaDescription{
MediaName: sdp.MediaName{
Media: "audio",
Formats: []string{"111"},
},
Attributes: []sdp.Attribute{
{Key: "rtpmap", Value: "111 opus/48000/2"},
},
})
assert.Equal(t, codecs, []RTPCodecParameters{
{
RTPCodecCapability: RTPCodecCapability{mimeTypeOpus, 48000, 2, "", []RTCPFeedback{}},
PayloadType: 111,
},
})
assert.NoError(t, err)
})
t.Run("Codec with fmtp/rtcp-fb", func(t *testing.T) {
codecs, err := codecsFromMediaDescription(&sdp.MediaDescription{
MediaName: sdp.MediaName{
Media: "audio",
Formats: []string{"111"},
},
Attributes: []sdp.Attribute{
{Key: "rtpmap", Value: "111 opus/48000/2"},
{Key: "fmtp", Value: "111 minptime=10;useinbandfec=1"},
{Key: "rtcp-fb", Value: "111 goog-remb"},
{Key: "rtcp-fb", Value: "111 ccm fir"},
},
})
assert.Equal(t, codecs, []RTPCodecParameters{
{
RTPCodecCapability: RTPCodecCapability{mimeTypeOpus, 48000, 2, "minptime=10;useinbandfec=1", []RTCPFeedback{{"goog-remb", ""}, {"ccm", "fir"}}},
PayloadType: 111,
},
})
assert.NoError(t, err)
})
}
func TestRtpExtensionsFromMediaDescription(t *testing.T) {
extensions, err := rtpExtensionsFromMediaDescription(&sdp.MediaDescription{
MediaName: sdp.MediaName{
Media: "audio",
Formats: []string{"111"},
},
Attributes: []sdp.Attribute{
{Key: "extmap", Value: "1 " + sdp.ABSSendTimeURI},
{Key: "extmap", Value: "3 " + sdp.SDESMidURI},
},
})
assert.NoError(t, err)
assert.Equal(t, extensions[sdp.ABSSendTimeURI], 1)
assert.Equal(t, extensions[sdp.SDESMidURI], 3)
}

View File

@@ -173,28 +173,28 @@ func TestSDPSemantics_PlanBAnswerSenders(t *testing.T) {
t.Errorf("NewPeerConnection failed: %v", err)
}
video1, err := NewTrack(DefaultPayloadTypeH264, 1, "1", "1", NewRTPH264Codec(DefaultPayloadTypeH264, 90000))
video1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/h264", SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"}, "1", "1")
if err != nil {
t.Errorf("Failed to create video track")
}
if _, err = apc.AddTrack(video1); err != nil {
t.Errorf("Failed to add video track")
}
video2, err := NewTrack(DefaultPayloadTypeH264, 2, "2", "2", NewRTPH264Codec(DefaultPayloadTypeH264, 90000))
video2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/h264", SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"}, "2", "2")
if err != nil {
t.Errorf("Failed to create video track")
}
if _, err = apc.AddTrack(video2); err != nil {
t.Errorf("Failed to add video track")
}
audio1, err := NewTrack(DefaultPayloadTypeOpus, 3, "3", "3", NewRTPOpusCodec(DefaultPayloadTypeOpus, 48000))
audio1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "audio/opus"}, "3", "3")
if err != nil {
t.Errorf("Failed to create audio track")
}
if _, err = apc.AddTrack(audio1); err != nil {
t.Errorf("Failed to add audio track")
}
audio2, err := NewTrack(DefaultPayloadTypeOpus, 4, "4", "4", NewRTPOpusCodec(DefaultPayloadTypeOpus, 48000))
audio2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "audio/opus"}, "4", "4")
if err != nil {
t.Errorf("Failed to create audio track")
}
@@ -267,28 +267,28 @@ func TestSDPSemantics_UnifiedPlanWithFallback(t *testing.T) {
t.Errorf("NewPeerConnection failed: %v", err)
}
video1, err := NewTrack(DefaultPayloadTypeH264, 1, "1", "1", NewRTPH264Codec(DefaultPayloadTypeH264, 90000))
video1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/h264", SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"}, "1", "1")
if err != nil {
t.Errorf("Failed to create video track")
}
if _, err = apc.AddTrack(video1); err != nil {
t.Errorf("Failed to add video track")
}
video2, err := NewTrack(DefaultPayloadTypeH264, 2, "2", "2", NewRTPH264Codec(DefaultPayloadTypeH264, 90000))
video2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/h264", SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"}, "2", "2")
if err != nil {
t.Errorf("Failed to create video track")
}
if _, err = apc.AddTrack(video2); err != nil {
t.Errorf("Failed to add video track")
}
audio1, err := NewTrack(DefaultPayloadTypeOpus, 3, "3", "3", NewRTPOpusCodec(DefaultPayloadTypeOpus, 48000))
audio1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "audio/opus"}, "3", "3")
if err != nil {
t.Errorf("Failed to create audio track")
}
if _, err = apc.AddTrack(audio1); err != nil {
t.Errorf("Failed to add audio track")
}
audio2, err := NewTrack(DefaultPayloadTypeOpus, 4, "4", "4", NewRTPOpusCodec(DefaultPayloadTypeOpus, 48000))
audio2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "audio/opus"}, "4", "4")
if err != nil {
t.Errorf("Failed to create audio track")
}

View File

@@ -7,7 +7,6 @@ import (
"github.com/pion/ice/v2"
"github.com/pion/logging"
"github.com/pion/sdp/v3"
"github.com/pion/transport/vnet"
"golang.org/x/net/proxy"
)
@@ -49,7 +48,6 @@ type SettingEngine struct {
SRTCP *uint
}
sdpMediaLevelFingerprints bool
sdpExtensions map[SDPSectionType][]sdp.ExtMap
answeringDTLSRole DTLSRole
disableCertificateFingerprintVerification bool
disableSRTPReplayProtection bool
@@ -250,68 +248,6 @@ func (e *SettingEngine) SetICETCPMux(tcpMux ice.TCPMux) {
e.iceTCPMux = tcpMux
}
// AddSDPExtensions adds available and offered extensions for media type.
//
// Ext IDs are optional and generated if you do not provide them
// SDP answers will only include extensions supported by both sides
func (e *SettingEngine) AddSDPExtensions(mediaType SDPSectionType, exts []sdp.ExtMap) {
if e.sdpExtensions == nil {
e.sdpExtensions = make(map[SDPSectionType][]sdp.ExtMap)
}
if _, ok := e.sdpExtensions[mediaType]; !ok {
e.sdpExtensions[mediaType] = []sdp.ExtMap{}
}
e.sdpExtensions[mediaType] = append(e.sdpExtensions[mediaType], exts...)
}
func (e *SettingEngine) getSDPExtensions() map[SDPSectionType][]sdp.ExtMap {
var lastID int
idMap := map[string]int{}
// Build provided ext id map
for _, extList := range e.sdpExtensions {
for _, ext := range extList {
if ext.Value != 0 {
idMap[ext.URI.String()] = ext.Value
}
}
}
// Find next available ID
nextID := func() {
var done bool
for !done {
lastID++
var found bool
for _, v := range idMap {
if lastID == v {
found = true
break
}
}
if !found {
done = true
}
}
}
// Assign missing IDs across all media types based on URI
for mType, extList := range e.sdpExtensions {
for i, ext := range extList {
if ext.Value == 0 {
if id, ok := idMap[ext.URI.String()]; ok {
e.sdpExtensions[mType][i].Value = id
} else {
nextID()
e.sdpExtensions[mType][i].Value = lastID
idMap[ext.URI.String()] = lastID
}
}
}
}
return e.sdpExtensions
}
// SetICEProxyDialer sets the proxy dialer interface based on golang.org/x/net/proxy.
func (e *SettingEngine) SetICEProxyDialer(d proxy.Dialer) {
e.iceProxyDialer = d

View File

@@ -153,7 +153,7 @@ type CodecStats struct {
ID string `json:"id"`
// PayloadType as used in RTP encoding or decoding
PayloadType uint8 `json:"payloadType"`
PayloadType PayloadType `json:"payloadType"`
// CodecType of this CodecStats
CodecType CodecType `json:"codecType"`
@@ -196,7 +196,7 @@ type InboundRTPStreamStats struct {
// SSRC is the 32-bit unsigned integer value used to identify the source of the
// stream of RTP packets that this stats object concerns.
SSRC uint32 `json:"ssrc"`
SSRC SSRC `json:"ssrc"`
// Kind is either "audio" or "video"
Kind string `json:"kind"`
@@ -363,7 +363,7 @@ type OutboundRTPStreamStats struct {
// SSRC is the 32-bit unsigned integer value used to identify the source of the
// stream of RTP packets that this stats object concerns.
SSRC uint32 `json:"ssrc"`
SSRC SSRC `json:"ssrc"`
// Kind is either "audio" or "video"
Kind string `json:"kind"`
@@ -492,7 +492,7 @@ type RemoteInboundRTPStreamStats struct {
// SSRC is the 32-bit unsigned integer value used to identify the source of the
// stream of RTP packets that this stats object concerns.
SSRC uint32 `json:"ssrc"`
SSRC SSRC `json:"ssrc"`
// Kind is either "audio" or "video"
Kind string `json:"kind"`
@@ -600,7 +600,7 @@ type RemoteOutboundRTPStreamStats struct {
// SSRC is the 32-bit unsigned integer value used to identify the source of the
// stream of RTP packets that this stats object concerns.
SSRC uint32 `json:"ssrc"`
SSRC SSRC `json:"ssrc"`
// Kind is either "audio" or "video"
Kind string `json:"kind"`
@@ -685,7 +685,7 @@ type RTPContributingSourceStats struct {
// ContributorSSRC is the SSRC identifier of the contributing source represented
// by this stats object. It is a 32-bit unsigned integer that appears in the CSRC
// list of any packets the relevant source contributed to.
ContributorSSRC uint32 `json:"contributorSsrc"`
ContributorSSRC SSRC `json:"contributorSsrc"`
// InboundRTPStreamID is the ID of the InboundRTPStreamStats object representing
// the inbound RTP stream that this contributing source is contributing to.

View File

@@ -78,7 +78,7 @@ func (r StatsReport) GetCertificateStats(c *Certificate) (CertificateStats, bool
}
// GetCodecStats is a helper method to return the associated stats for a given Codec
func (r StatsReport) GetCodecStats(c *RTPCodec) (CodecStats, bool) {
func (r StatsReport) GetCodecStats(c *RTPCodecParameters) (CodecStats, bool) {
statsID := c.statsID
stats, ok := r[statsID]
if !ok {

View File

@@ -9,7 +9,6 @@ import (
"testing"
"time"
"github.com/pion/randutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -101,7 +100,7 @@ func getDataChannelStats(t *testing.T, report StatsReport, dc *DataChannel) Data
return stats
}
func getCodecStats(t *testing.T, report StatsReport, c *RTPCodec) CodecStats {
func getCodecStats(t *testing.T, report StatsReport, c *RTPCodecParameters) CodecStats {
stats, ok := report.GetCodecStats(c)
assert.True(t, ok)
assert.Equal(t, stats.Type, StatsTypeCodec)
@@ -204,7 +203,7 @@ func TestPeerConnection_GetStats(t *testing.T) {
offerPC, answerPC, err := newPair()
assert.NoError(t, err)
track1, err := offerPC.NewTrack(DefaultPayloadTypeVP8, randutil.NewMathRandomGenerator().Uint32(), "video", "pion1")
track1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion1")
require.NoError(t, err)
_, err = offerPC.AddTrack(track1)
@@ -279,8 +278,12 @@ func TestPeerConnection_GetStats(t *testing.T) {
assert.NotEmpty(t, findRemoteCandidateStats(reportPCAnswer))
assert.NotEmpty(t, findCandidatePairStats(t, reportPCAnswer))
assert.NoError(t, err)
for _, codec := range offerPC.api.mediaEngine.codecs {
codecStat := getCodecStats(t, reportPCOffer, codec)
for i := range offerPC.api.mediaEngine.videoCodecs {
codecStat := getCodecStats(t, reportPCOffer, &(offerPC.api.mediaEngine.videoCodecs[i]))
assert.NotEmpty(t, codecStat)
}
for i := range offerPC.api.mediaEngine.audioCodecs {
codecStat := getCodecStats(t, reportPCOffer, &(offerPC.api.mediaEngine.audioCodecs[i]))
assert.NotEmpty(t, codecStat)
}

263
track.go
View File

@@ -1,263 +0,0 @@
// +build !js
package webrtc
import (
"io"
"sync"
"github.com/pion/rtp"
"github.com/pion/webrtc/v3/internal/util"
"github.com/pion/webrtc/v3/pkg/media"
)
const (
rtpOutboundMTU = 1200
trackDefaultIDLength = 16
trackDefaultLabelLength = 16
)
// Track represents a single media track
type Track struct {
mu sync.RWMutex
id string
payloadType uint8
kind RTPCodecType
label string
ssrc uint32
codec *RTPCodec
rid string
packetizer rtp.Packetizer
receiver *RTPReceiver
activeSenders []*RTPSender
totalSenderCount int // count of all senders (accounts for senders that have not been started yet)
peeked []byte
}
// ID gets the ID of the track
func (t *Track) ID() string {
t.mu.RLock()
defer t.mu.RUnlock()
return t.id
}
// RID gets the RTP Stream ID of this Track
// With Simulcast you will have multiple tracks with the same ID, but different RID values.
// In many cases a Track will not have an RID, so it is important to assert it is non-zero
func (t *Track) RID() string {
t.mu.RLock()
defer t.mu.RUnlock()
return t.rid
}
// PayloadType gets the PayloadType of the track
func (t *Track) PayloadType() uint8 {
t.mu.RLock()
defer t.mu.RUnlock()
return t.payloadType
}
// Kind gets the Kind of the track
func (t *Track) Kind() RTPCodecType {
t.mu.RLock()
defer t.mu.RUnlock()
return t.kind
}
// Label gets the Label of the track
func (t *Track) Label() string {
t.mu.RLock()
defer t.mu.RUnlock()
return t.label
}
// SSRC gets the SSRC of the track
func (t *Track) SSRC() uint32 {
t.mu.RLock()
defer t.mu.RUnlock()
return t.ssrc
}
// Msid gets the Msid of the track
func (t *Track) Msid() string {
return t.Label() + " " + t.ID()
}
// Codec gets the Codec of the track
func (t *Track) Codec() *RTPCodec {
t.mu.RLock()
defer t.mu.RUnlock()
return t.codec
}
// Packetizer gets the Packetizer of the track
func (t *Track) Packetizer() rtp.Packetizer {
t.mu.RLock()
defer t.mu.RUnlock()
return t.packetizer
}
// Read reads data from the track. If this is a local track this will error
func (t *Track) Read(b []byte) (n int, err error) {
t.mu.RLock()
r := t.receiver
if t.totalSenderCount != 0 || r == nil {
t.mu.RUnlock()
return 0, errTrackLocalTrackRead
}
peeked := t.peeked != nil
t.mu.RUnlock()
if peeked {
t.mu.Lock()
data := t.peeked
t.peeked = nil
t.mu.Unlock()
// someone else may have stolen our packet when we
// released the lock. Deal with it.
if data != nil {
n = copy(b, data)
return
}
}
return r.readRTP(b, t)
}
// peek is like Read, but it doesn't discard the packet read
func (t *Track) peek(b []byte) (n int, err error) {
n, err = t.Read(b)
if err != nil {
return
}
t.mu.Lock()
// this might overwrite data if somebody peeked between the Read
// and us getting the lock. Oh well, we'll just drop a packet in
// that case.
data := make([]byte, n)
n = copy(data, b[:n])
t.peeked = data
t.mu.Unlock()
return
}
// ReadRTP is a convenience method that wraps Read and unmarshals for you
func (t *Track) ReadRTP() (*rtp.Packet, error) {
b := make([]byte, receiveMTU)
i, err := t.Read(b)
if err != nil {
return nil, err
}
r := &rtp.Packet{}
if err := r.Unmarshal(b[:i]); err != nil {
return nil, err
}
return r, nil
}
// Write writes data to the track. If this is a remote track this will error
func (t *Track) Write(b []byte) (n int, err error) {
packet := &rtp.Packet{}
err = packet.Unmarshal(b)
if err != nil {
return 0, err
}
err = t.WriteRTP(packet)
if err != nil {
return 0, err
}
return len(b), nil
}
// WriteSample packetizes and writes to the track
func (t *Track) WriteSample(s media.Sample) error {
packets := t.packetizer.Packetize(s.Data, s.Samples)
for _, p := range packets {
err := t.WriteRTP(p)
if err != nil {
return err
}
}
return nil
}
// WriteRTP writes RTP packets to the track
func (t *Track) WriteRTP(p *rtp.Packet) error {
t.mu.RLock()
if t.receiver != nil {
t.mu.RUnlock()
return errTrackLocalTrackWrite
}
senders := t.activeSenders
totalSenderCount := t.totalSenderCount
t.mu.RUnlock()
if totalSenderCount == 0 {
return io.ErrClosedPipe
}
writeErrs := []error{}
for _, s := range senders {
if _, err := s.SendRTP(&p.Header, p.Payload); err != nil {
writeErrs = append(writeErrs, err)
}
}
return util.FlattenErrs(writeErrs)
}
// NewTrack initializes a new *Track
func NewTrack(payloadType uint8, ssrc uint32, id, label string, codec *RTPCodec) (*Track, error) {
if ssrc == 0 {
return nil, errTrackSSRCNewTrackZero
}
packetizer := rtp.NewPacketizer(
rtpOutboundMTU,
payloadType,
ssrc,
codec.Payloader,
rtp.NewRandomSequencer(),
codec.ClockRate,
)
return &Track{
id: id,
payloadType: payloadType,
kind: codec.Type,
label: label,
ssrc: ssrc,
codec: codec,
packetizer: packetizer,
}, nil
}
// determinePayloadType blocks and reads a single packet to determine the PayloadType for this Track
// this is useful if we are dealing with a remote track and we can't announce it to the user until we know the payloadType
func (t *Track) determinePayloadType() error {
b := make([]byte, receiveMTU)
n, err := t.peek(b)
if err != nil {
return err
}
r := rtp.Packet{}
if err := r.Unmarshal(b[:n]); err != nil {
return err
}
t.mu.Lock()
t.payloadType = r.PayloadType
defer t.mu.Unlock()
return nil
}

68
track_local.go Normal file
View File

@@ -0,0 +1,68 @@
package webrtc
import "github.com/pion/rtp"
// TrackLocalWriter is the Writer for outbound RTP Packets
type TrackLocalWriter interface {
// WriteRTP encrypts a RTP packet and writes to the connection
WriteRTP(header *rtp.Header, payload []byte) (int, error)
// Write encrypts and writes a full RTP packet
Write(b []byte) (int, error)
}
// TrackLocalContext is the Context passed when a TrackLocal has been Binded/Unbinded from a PeerConnection
type TrackLocalContext struct {
id string
codecs []RTPCodecParameters
ssrc SSRC
writeStream TrackLocalWriter
}
// CodecParameters returns the negotiated RTPCodecParameters. These are the codecs supported by both
// PeerConnections and the SSRC/PayloadTypes
func (t *TrackLocalContext) CodecParameters() []RTPCodecParameters {
return t.codecs
}
// SSRC requires the negotiated SSRC of this track
// This track may have multiple if RTX is enabled
func (t *TrackLocalContext) SSRC() SSRC {
return t.ssrc
}
// WriteStream returns the WriteStream for this TrackLocal. The implementer writes the outbound
// media packets to it
func (t *TrackLocalContext) WriteStream() TrackLocalWriter {
return t.writeStream
}
// ID is a unique identifier that is used for both Bind/Unbind
func (t *TrackLocalContext) ID() string {
return t.id
}
// TrackLocal is an interface that controls how the user can send media
// The user can provide their own TrackLocal implementatiosn, or use
// the implementations in pkg/media
type TrackLocal interface {
// Bind should implement the way how the media data flows from the Track to the PeerConnection
// This will be called internally after signaling is complete and the list of available
// codecs has been determined
Bind(TrackLocalContext) error
// Unbind should implement the teardown logic when the track is no longer needed. This happens
// because a track has been stopped.
Unbind(TrackLocalContext) error
// ID is the unique identifier for this Track. This should be unique for the
// stream, but doesn't have to globally unique. A common example would be 'audio' or 'video'
// and StreamID would be 'desktop' or 'webcam'
ID() string
// StreamID is the group this track belongs too. This must be unique
StreamID() string
// Kind controls if this TrackLocal is audio or video
Kind() RTPCodecType
}

235
track_local_static.go Normal file
View File

@@ -0,0 +1,235 @@
// +build !js
package webrtc
import (
"strings"
"sync"
"github.com/pion/rtp"
"github.com/pion/webrtc/v3/internal/util"
"github.com/pion/webrtc/v3/pkg/media"
)
// trackBinding is a single bind for a Track
// Bind can be called multiple times, this stores the
// result for a single bind call so that it can be used when writing
type trackBinding struct {
id string
ssrc SSRC
payloadType PayloadType
writeStream TrackLocalWriter
}
// TrackLocalStaticRTP is a TrackLocal that has a pre-set codec and accepts RTP Packets.
// If you wish to send a media.Sample use TrackLocalStaticSample
type TrackLocalStaticRTP struct {
mu sync.RWMutex
bindings []trackBinding
codec RTPCodecCapability
id, streamID string
}
// NewTrackLocalStaticRTP returns a TrackLocalStaticRTP.
func NewTrackLocalStaticRTP(c RTPCodecCapability, id, streamID string) (*TrackLocalStaticRTP, error) {
return &TrackLocalStaticRTP{
codec: c,
bindings: []trackBinding{},
id: id,
streamID: streamID,
}, nil
}
// Bind is called by the PeerConnection after negotiation is complete
// This asserts that the code requested is supported by the remote peer.
// If so it setups all the state (SSRC and PayloadType) to have a call
func (s *TrackLocalStaticRTP) Bind(t TrackLocalContext) error {
s.mu.Lock()
defer s.mu.Unlock()
parameters := RTPCodecParameters{RTPCodecCapability: s.codec}
if codec, err := codecParametersFuzzySearch(parameters, t.CodecParameters()); err == nil {
s.bindings = append(s.bindings, trackBinding{
ssrc: t.SSRC(),
payloadType: codec.PayloadType,
writeStream: t.WriteStream(),
id: t.ID(),
})
return nil
}
return ErrUnsupportedCodec
}
// Unbind implements the teardown logic when the track is no longer needed. This happens
// because a track has been stopped.
func (s *TrackLocalStaticRTP) Unbind(t TrackLocalContext) error {
s.mu.Lock()
defer s.mu.Unlock()
for i := range s.bindings {
if s.bindings[i].id == t.ID() {
s.bindings[i] = s.bindings[len(s.bindings)-1]
s.bindings = s.bindings[:len(s.bindings)-1]
return nil
}
}
return ErrUnbindFailed
}
// ID is the unique identifier for this Track. This should be unique for the
// stream, but doesn't have to globally unique. A common example would be 'audio' or 'video'
// and StreamID would be 'desktop' or 'webcam'
func (s *TrackLocalStaticRTP) ID() string { return s.id }
// StreamID is the group this track belongs too. This must be unique
func (s *TrackLocalStaticRTP) StreamID() string { return s.streamID }
// Kind controls if this TrackLocal is audio or video
func (s *TrackLocalStaticRTP) Kind() RTPCodecType {
switch {
case strings.HasPrefix(s.codec.MimeType, "audio/"):
return RTPCodecTypeAudio
case strings.HasPrefix(s.codec.MimeType, "video/"):
return RTPCodecTypeVideo
default:
return RTPCodecType(0)
}
}
// WriteRTP writes a RTP Packet to the TrackLocalStaticRTP
// If one PeerConnection fails the packets will still be sent to
// all PeerConnections. The error message will contain the ID of the failed
// PeerConnections so you can remove them
func (s *TrackLocalStaticRTP) WriteRTP(p *rtp.Packet) error {
s.mu.RLock()
defer s.mu.RUnlock()
writeErrs := []error{}
for _, b := range s.bindings {
p.Header.SSRC = uint32(b.ssrc)
p.Header.PayloadType = uint8(b.payloadType)
if _, err := b.writeStream.WriteRTP(&p.Header, p.Payload); err != nil {
writeErrs = append(writeErrs, err)
}
}
return util.FlattenErrs(writeErrs)
}
// Write writes a RTP Packet as a buffer to the TrackLocalStaticRTP
// If one PeerConnection fails the packets will still be sent to
// all PeerConnections. The error message will contain the ID of the failed
// PeerConnections so you can remove them
func (s *TrackLocalStaticRTP) Write(b []byte) (n int, err error) {
packet := &rtp.Packet{}
if err = packet.Unmarshal(b); err != nil {
return 0, err
}
return len(b), s.WriteRTP(packet)
}
// TrackLocalStaticSample is a TrackLocal that has a pre-set codec and accepts Samples.
// If you wish to send a RTP Packet use TrackLocalStaticRTP
type TrackLocalStaticSample struct {
packetizer rtp.Packetizer
rtpTrack *TrackLocalStaticRTP
clockRate float64
}
// NewTrackLocalStaticSample returns a TrackLocalStaticSample
func NewTrackLocalStaticSample(c RTPCodecCapability, id, streamID string) (*TrackLocalStaticSample, error) {
rtpTrack, err := NewTrackLocalStaticRTP(c, id, streamID)
if err != nil {
return nil, err
}
return &TrackLocalStaticSample{
rtpTrack: rtpTrack,
}, nil
}
// ID is the unique identifier for this Track. This should be unique for the
// stream, but doesn't have to globally unique. A common example would be 'audio' or 'video'
// and StreamID would be 'desktop' or 'webcam'
func (s *TrackLocalStaticSample) ID() string { return s.rtpTrack.ID() }
// StreamID is the group this track belongs too. This must be unique
func (s *TrackLocalStaticSample) StreamID() string { return s.rtpTrack.StreamID() }
// Kind controls if this TrackLocal is audio or video
func (s *TrackLocalStaticSample) Kind() RTPCodecType { return s.rtpTrack.Kind() }
// Bind is called by the PeerConnection after negotiation is complete
// This asserts that the code requested is supported by the remote peer.
// If so it setups all the state (SSRC and PayloadType) to have a call
func (s *TrackLocalStaticSample) Bind(t TrackLocalContext) error {
if err := s.rtpTrack.Bind(t); err != nil {
return err
}
s.rtpTrack.mu.Lock()
defer s.rtpTrack.mu.Unlock()
// We only need one packetizer
if s.packetizer != nil {
return nil
}
parameters := RTPCodecParameters{RTPCodecCapability: s.rtpTrack.codec}
codec, err := codecParametersFuzzySearch(parameters, t.CodecParameters())
if err != nil {
return err
}
payloader, err := payloaderForCodec(s.rtpTrack.codec)
if err != nil {
return err
}
s.packetizer = rtp.NewPacketizer(
rtpOutboundMTU,
0, // Value is handled when writing
0, // Value is handled when writing
payloader,
rtp.NewRandomSequencer(),
codec.ClockRate,
)
s.clockRate = float64(codec.RTPCodecCapability.ClockRate)
return nil
}
// Unbind implements the teardown logic when the track is no longer needed. This happens
// because a track has been stopped.
func (s *TrackLocalStaticSample) Unbind(t TrackLocalContext) error {
return s.rtpTrack.Unbind(t)
}
// WriteSample writes a Sample to the TrackLocalStaticSample
// If one PeerConnection fails the packets will still be sent to
// all PeerConnections. The error message will contain the ID of the failed
// PeerConnections so you can remove them
func (s *TrackLocalStaticSample) WriteSample(sample media.Sample) error {
s.rtpTrack.mu.RLock()
p := s.packetizer
clockRate := s.clockRate
s.rtpTrack.mu.RUnlock()
if p == nil {
return nil
}
samples := sample.Duration.Seconds() * clockRate
packets := p.(rtp.Packetizer).Packetize(sample.Data, uint32(samples))
writeErrs := []error{}
for _, p := range packets {
if err := s.rtpTrack.WriteRTP(p); err != nil {
writeErrs = append(writeErrs, err)
}
}
return util.FlattenErrs(writeErrs)
}

161
track_remote.go Normal file
View File

@@ -0,0 +1,161 @@
// +build !js
package webrtc
import (
"sync"
"github.com/pion/rtp"
)
// TrackRemote represents a single inbound source of media
type TrackRemote struct {
mu sync.RWMutex
id string
streamID string
payloadType PayloadType
kind RTPCodecType
ssrc SSRC
codec RTPCodecParameters
rid string
receiver *RTPReceiver
peeked []byte
}
// ID is the unique identifier for this Track. This should be unique for the
// stream, but doesn't have to globally unique. A common example would be 'audio' or 'video'
// and StreamID would be 'desktop' or 'webcam'
func (t *TrackRemote) ID() string {
t.mu.RLock()
defer t.mu.RUnlock()
return t.id
}
// RID gets the RTP Stream ID of this Track
// With Simulcast you will have multiple tracks with the same ID, but different RID values.
// In many cases a TrackRemote will not have an RID, so it is important to assert it is non-zero
func (t *TrackRemote) RID() string {
t.mu.RLock()
defer t.mu.RUnlock()
return t.rid
}
// PayloadType gets the PayloadType of the track
func (t *TrackRemote) PayloadType() PayloadType {
t.mu.RLock()
defer t.mu.RUnlock()
return t.payloadType
}
// Kind gets the Kind of the track
func (t *TrackRemote) Kind() RTPCodecType {
t.mu.RLock()
defer t.mu.RUnlock()
return t.kind
}
// StreamID is the group this track belongs too. This must be unique
func (t *TrackRemote) StreamID() string {
t.mu.RLock()
defer t.mu.RUnlock()
return t.streamID
}
// SSRC gets the SSRC of the track
func (t *TrackRemote) SSRC() SSRC {
t.mu.RLock()
defer t.mu.RUnlock()
return t.ssrc
}
// Msid gets the Msid of the track
func (t *TrackRemote) Msid() string {
return t.StreamID() + " " + t.ID()
}
// Codec gets the Codec of the track
func (t *TrackRemote) Codec() RTPCodecParameters {
t.mu.RLock()
defer t.mu.RUnlock()
return t.codec
}
// Read reads data from the track.
func (t *TrackRemote) Read(b []byte) (n int, err error) {
t.mu.RLock()
r := t.receiver
peeked := t.peeked != nil
t.mu.RUnlock()
if peeked {
t.mu.Lock()
data := t.peeked
t.peeked = nil
t.mu.Unlock()
// someone else may have stolen our packet when we
// released the lock. Deal with it.
if data != nil {
n = copy(b, data)
return
}
}
return r.readRTP(b, t)
}
// peek is like Read, but it doesn't discard the packet read
func (t *TrackRemote) peek(b []byte) (n int, err error) {
n, err = t.Read(b)
if err != nil {
return
}
t.mu.Lock()
// this might overwrite data if somebody peeked between the Read
// and us getting the lock. Oh well, we'll just drop a packet in
// that case.
data := make([]byte, n)
n = copy(data, b[:n])
t.peeked = data
t.mu.Unlock()
return
}
// ReadRTP is a convenience method that wraps Read and unmarshals for you
func (t *TrackRemote) ReadRTP() (*rtp.Packet, error) {
b := make([]byte, receiveMTU)
i, err := t.Read(b)
if err != nil {
return nil, err
}
r := &rtp.Packet{}
if err := r.Unmarshal(b[:i]); err != nil {
return nil, err
}
return r, nil
}
// determinePayloadType blocks and reads a single packet to determine the PayloadType for this Track
// this is useful because we can't announce it to the user until we know the payloadType
func (t *TrackRemote) determinePayloadType() error {
b := make([]byte, receiveMTU)
n, err := t.peek(b)
if err != nil {
return err
}
r := rtp.Packet{}
if err := r.Unmarshal(b[:n]); err != nil {
return err
}
t.mu.Lock()
t.payloadType = PayloadType(r.PayloadType)
defer t.mu.Unlock()
return nil
}

View File

@@ -1,130 +1,3 @@
// +build !js
package webrtc
import (
"testing"
"github.com/pion/randutil"
"github.com/stretchr/testify/assert"
)
func TestNewVideoTrack(t *testing.T) {
m := MediaEngine{}
m.RegisterCodec(NewRTPVP8Codec(DefaultPayloadTypeVP8, 90000))
api := NewAPI(WithMediaEngine(m))
peerConfig := Configuration{
ICEServers: []ICEServer{
{
URLs: []string{"stun:stun.l.google.com:19302"},
},
},
}
peer, _ := api.NewPeerConnection(peerConfig)
_, err := peer.NewTrack(DefaultPayloadTypeVP8, randutil.NewMathRandomGenerator().Uint32(), "video", "pion")
if err != nil {
t.Error("Failed to new video track")
}
}
func TestNewAudioTrack(t *testing.T) {
m := MediaEngine{}
m.RegisterCodec(NewRTPOpusCodec(DefaultPayloadTypeOpus, 48000))
api := NewAPI(WithMediaEngine(m))
peerConfig := Configuration{
ICEServers: []ICEServer{
{
URLs: []string{"stun:stun.l.google.com:19302"},
},
},
}
peer, _ := api.NewPeerConnection(peerConfig)
_, err := peer.NewTrack(DefaultPayloadTypeOpus, randutil.NewMathRandomGenerator().Uint32(), "audio", "pion")
if err != nil {
t.Error("Failed to new audio track")
}
}
func TestNewTracks(t *testing.T) {
m := MediaEngine{}
m.RegisterCodec(NewRTPOpusCodec(DefaultPayloadTypeOpus, 48000))
m.RegisterCodec(NewRTPVP8Codec(DefaultPayloadTypeVP8, 90000))
api := NewAPI(WithMediaEngine(m))
peerConfig := Configuration{
ICEServers: []ICEServer{
{
URLs: []string{"stun:stun.l.google.com:19302"},
},
},
}
peer, _ := api.NewPeerConnection(peerConfig)
_, err := peer.NewTrack(DefaultPayloadTypeOpus, randutil.NewMathRandomGenerator().Uint32(), "audio", "pion")
if err != nil {
t.Error("Failed to new audio track")
}
_, err = peer.NewTrack(DefaultPayloadTypeVP8, randutil.NewMathRandomGenerator().Uint32(), "video", "pion")
if err != nil {
t.Error("Failed to new video track")
}
}
func TestNewTracksWrite(t *testing.T) {
m := MediaEngine{}
m.RegisterCodec(NewRTPOpusCodec(DefaultPayloadTypeOpus, 48000))
m.RegisterCodec(NewRTPVP8Codec(DefaultPayloadTypeVP8, 90000))
api := NewAPI(WithMediaEngine(m))
peerConfig := Configuration{
ICEServers: []ICEServer{
{
URLs: []string{"stun:stun.l.google.com:19302"},
},
},
}
peer, _ := api.NewPeerConnection(peerConfig)
videoTrack, err := peer.NewTrack(DefaultPayloadTypeOpus, randutil.NewMathRandomGenerator().Uint32(), "audio", "pion")
if err != nil {
t.Error("Failed to new video track")
}
audioTrack, err := peer.NewTrack(DefaultPayloadTypeVP8, randutil.NewMathRandomGenerator().Uint32(), "video", "pion")
if err != nil {
t.Error("Failed to new audio track")
}
if _, err = peer.AddTrack(audioTrack); err != nil {
t.Fatal(err)
} else if _, err = peer.AddTrack(videoTrack); err != nil {
t.Fatal(err)
}
rtpBuf := make([]byte, 1400)
_, err = videoTrack.Write(rtpBuf)
if err != nil {
t.Error("Failed to write to video track")
}
_, err = audioTrack.Write(rtpBuf)
if err != nil {
t.Error("Failed to write to audio track")
}
}
func TestTrackReadWhenNotAdded(t *testing.T) {
peerConnection, err := NewPeerConnection(Configuration{})
assert.NoError(t, err)
track, err := peerConnection.NewTrack(DefaultPayloadTypeOpus, randutil.NewMathRandomGenerator().Uint32(), "audio", "pion")
assert.NoError(t, err)
_, err = track.Read([]byte{})
assert.Error(t, err)
}

21
webrtc.go Normal file
View File

@@ -0,0 +1,21 @@
// Package webrtc implements the WebRTC 1.0 as defined in W3C WebRTC specification document.
package webrtc
// SSRC represents a synchronization source
// A synchronization source is a randomly chosen
// value meant to be globally unique within a particular
// RTP session. Used to identify a single stream of media.
//
// https://tools.ietf.org/html/rfc3550#section-3
type SSRC uint32
// PayloadType identifies the format of the RTP payload and determines
// its interpretation by the application. Each codec in a RTP Session
// will have a different PayloadType
//
// https://tools.ietf.org/html/rfc3550#section-3
type PayloadType uint8
const (
rtpOutboundMTU = 1200
)