mirror of
https://github.com/pion/webrtc.git
synced 2025-09-26 19:21:12 +08:00
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:
@@ -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")
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
39
errors.go
39
errors.go
@@ -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")
|
||||
)
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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{}
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -17,7 +17,7 @@
|
||||
|
||||
<script>
|
||||
let activeVideos = 0
|
||||
let pc = new RTCPeerConnection()({
|
||||
let pc = new RTCPeerConnection({
|
||||
iceServers: [
|
||||
{
|
||||
urls: 'stun:stun.l.google.com:19302'
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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))
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
||||
|
@@ -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) {
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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
|
||||
|
768
mediaengine.go
768
mediaengine.go
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
})
|
||||
}
|
||||
|
@@ -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()) {
|
||||
|
@@ -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.
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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))
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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
7
rtpcapabilities.go
Normal 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
99
rtpcodec.go
Normal 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
|
||||
}
|
@@ -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"`
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
98
rtpsender.go
98
rtpsender.go
@@ -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 {
|
||||
|
@@ -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)
|
||||
}
|
||||
|
||||
|
@@ -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
235
sdp.go
@@ -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
|
||||
}
|
||||
|
196
sdp_test.go
196
sdp_test.go
@@ -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)
|
||||
}
|
||||
|
@@ -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")
|
||||
}
|
||||
|
@@ -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
|
||||
|
12
stats.go
12
stats.go
@@ -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.
|
||||
|
@@ -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 {
|
||||
|
@@ -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
263
track.go
@@ -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
68
track_local.go
Normal 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
235
track_local_static.go
Normal 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
161
track_remote.go
Normal 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
|
||||
}
|
127
track_test.go
127
track_test.go
@@ -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
21
webrtc.go
Normal 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
|
||||
)
|
Reference in New Issue
Block a user