mirror of
https://github.com/pion/webrtc.git
synced 2025-11-02 11:34:32 +08:00
api: support a custom media engine
This commit is contained in:
@@ -67,7 +67,7 @@ var (
|
|||||||
ErrModPeerIdentity = errors.New("peer identity cannot be modified")
|
ErrModPeerIdentity = errors.New("peer identity cannot be modified")
|
||||||
ErrModCertificates = errors.New("certificates cannot be modified")
|
ErrModCertificates = errors.New("certificates cannot be modified")
|
||||||
ErrModRtcpMuxPolicy = errors.New("rtcp mux policy cannot be modified")
|
ErrModRtcpMuxPolicy = errors.New("rtcp mux policy cannot be modified")
|
||||||
ErrModIceCandidatePoolSize = errors.New("ice candidate pool size cannot be modified")
|
ErrModICECandidatePoolSize = errors.New("ice candidate pool size cannot be modified")
|
||||||
)
|
)
|
||||||
|
|
||||||
// InvalidModificationError indicates the object can not be modified in this way.
|
// InvalidModificationError indicates the object can not be modified in this way.
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a audio track
|
// Create a audio track
|
||||||
opusTrack, err := peerConnection.NewRTCTrack(webrtc.PayloadTypeOpus, "audio", "pion1")
|
opusTrack, err := peerConnection.NewRTCTrack(webrtc.DefaultPayloadTypeOpus, "audio", "pion1")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a video track
|
// Create a video track
|
||||||
vp8Track, err := peerConnection.NewRTCTrack(webrtc.PayloadTypeVP8, "video", "pion2")
|
vp8Track, err := peerConnection.NewRTCTrack(webrtc.DefaultPayloadTypeVP8, "video", "pion2")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ func main() {
|
|||||||
|
|
||||||
// Setup the codecs you want to use.
|
// Setup the codecs you want to use.
|
||||||
// We'll use a VP8 codec but you can also define your own
|
// We'll use a VP8 codec but you can also define your own
|
||||||
webrtc.RegisterCodec(webrtc.NewRTCRtpOpusCodec(webrtc.PayloadTypeOpus, 48000, 2))
|
webrtc.RegisterCodec(webrtc.NewRTCRtpOpusCodec(webrtc.DefaultPayloadTypeOpus, 48000, 2))
|
||||||
webrtc.RegisterCodec(webrtc.NewRTCRtpVP8Codec(webrtc.PayloadTypeVP8, 90000))
|
webrtc.RegisterCodec(webrtc.NewRTCRtpVP8Codec(webrtc.DefaultPayloadTypeVP8, 90000))
|
||||||
|
|
||||||
// Create a new RTCPeerConnection
|
// Create a new RTCPeerConnection
|
||||||
peerConnection, err := webrtc.New(webrtc.RTCConfiguration{})
|
peerConnection, err := webrtc.New(webrtc.RTCConfiguration{})
|
||||||
|
|||||||
@@ -121,10 +121,10 @@ func (d *MediaDescription) WithCodec(payloadType uint8, name string, clockrate u
|
|||||||
// WithMediaSource adds media source information to the media description
|
// WithMediaSource adds media source information to the media description
|
||||||
func (d *MediaDescription) WithMediaSource(ssrc uint32, cname, streamLabel, label string) *MediaDescription {
|
func (d *MediaDescription) WithMediaSource(ssrc uint32, cname, streamLabel, label string) *MediaDescription {
|
||||||
return d.
|
return d.
|
||||||
WithValueAttribute("ssrc", fmt.Sprintf("%d cname:%s", ssrc, cname)). // Deprecated but not pased out?
|
WithValueAttribute("ssrc", fmt.Sprintf("%d cname:%s", ssrc, cname)). // Deprecated but not phased out?
|
||||||
WithValueAttribute("ssrc", fmt.Sprintf("%d msid:%s %s", ssrc, streamLabel, label)).
|
WithValueAttribute("ssrc", fmt.Sprintf("%d msid:%s %s", ssrc, streamLabel, label)).
|
||||||
WithValueAttribute("ssrc", fmt.Sprintf("%d mslabel:%s", ssrc, streamLabel)). // Deprecated but not pased out?
|
WithValueAttribute("ssrc", fmt.Sprintf("%d mslabel:%s", ssrc, streamLabel)). // Deprecated but not phased out?
|
||||||
WithValueAttribute("ssrc", fmt.Sprintf("%d label:%s", ssrc, label)) // Deprecated but not pased out?
|
WithValueAttribute("ssrc", fmt.Sprintf("%d label:%s", ssrc, label)) // Deprecated but not phased out?
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithCandidate adds an ICE candidate to the media description
|
// WithCandidate adds an ICE candidate to the media description
|
||||||
|
|||||||
@@ -8,21 +8,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SessionBuilderTrack represents a single track in a SessionBuilder
|
|
||||||
type SessionBuilderTrack struct {
|
|
||||||
SSRC uint32
|
|
||||||
IsAudio bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// SessionBuilder provides an easy way to build an SDP for an RTCPeerConnection
|
|
||||||
type SessionBuilder struct {
|
|
||||||
IceUsername, IcePassword, Fingerprint string
|
|
||||||
|
|
||||||
Candidates []string
|
|
||||||
|
|
||||||
Tracks []*SessionBuilderTrack
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConnectionRole indicates which of the end points should initiate the connection establishment
|
// ConnectionRole indicates which of the end points should initiate the connection establishment
|
||||||
type ConnectionRole int
|
type ConnectionRole int
|
||||||
|
|
||||||
|
|||||||
2
media.go
2
media.go
@@ -133,7 +133,7 @@ type RTCTrack struct {
|
|||||||
|
|
||||||
// NewRTCTrack is used to create a new RTCTrack
|
// NewRTCTrack is used to create a new RTCTrack
|
||||||
func (r *RTCPeerConnection) NewRTCTrack(payloadType uint8, id, label string) (*RTCTrack, error) {
|
func (r *RTCPeerConnection) NewRTCTrack(payloadType uint8, id, label string) (*RTCTrack, error) {
|
||||||
codec, err := rtcMediaEngine.getCodec(payloadType)
|
codec, err := r.mediaEngine.getCodec(payloadType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,48 +9,50 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RegisterCodec is used to register a codec with the DefaultMediaEngine
|
||||||
|
func RegisterCodec(codec *RTCRtpCodec) {
|
||||||
|
DefaultMediaEngine.RegisterCodec(codec)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Phase out DefaultPayloadTypes in favor or dynamic assignment in 96-127 range
|
||||||
|
|
||||||
// PayloadTypes for the default codecs
|
// PayloadTypes for the default codecs
|
||||||
const (
|
const (
|
||||||
PayloadTypeOpus = 111
|
DefaultPayloadTypeOpus = 111
|
||||||
PayloadTypeVP8 = 96
|
DefaultPayloadTypeVP8 = 96
|
||||||
PayloadTypeVP9 = 98
|
DefaultPayloadTypeVP9 = 98
|
||||||
PayloadTypeH264 = 100
|
DefaultPayloadTypeH264 = 100
|
||||||
)
|
)
|
||||||
|
|
||||||
// Names for the default codecs
|
|
||||||
const (
|
|
||||||
Opus = "opus"
|
|
||||||
VP8 = "VP8"
|
|
||||||
VP9 = "VP9"
|
|
||||||
H264 = "H264"
|
|
||||||
)
|
|
||||||
|
|
||||||
var rtcMediaEngine = &mediaEngine{}
|
|
||||||
|
|
||||||
// RegisterDefaultCodecs is a helper that registers the default codecs supported by pions-webrtc
|
// RegisterDefaultCodecs is a helper that registers the default codecs supported by pions-webrtc
|
||||||
func RegisterDefaultCodecs() {
|
func RegisterDefaultCodecs() {
|
||||||
RegisterCodec(NewRTCRtpOpusCodec(PayloadTypeOpus, 48000, 2))
|
RegisterCodec(NewRTCRtpOpusCodec(DefaultPayloadTypeOpus, 48000, 2))
|
||||||
RegisterCodec(NewRTCRtpVP8Codec(PayloadTypeVP8, 90000))
|
RegisterCodec(NewRTCRtpVP8Codec(DefaultPayloadTypeVP8, 90000))
|
||||||
RegisterCodec(NewRTCRtpH264Codec(PayloadTypeH264, 90000))
|
RegisterCodec(NewRTCRtpH264Codec(DefaultPayloadTypeH264, 90000))
|
||||||
RegisterCodec(NewRTCRtpVP9Codec(PayloadTypeVP9, 90000))
|
RegisterCodec(NewRTCRtpVP9Codec(DefaultPayloadTypeVP9, 90000))
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterCodec is used to register a codec
|
// DefaultMediaEngine is the default MediaEngine used by RTCPeerConnections
|
||||||
func RegisterCodec(codec *RTCRtpCodec) {
|
var DefaultMediaEngine = NewMediaEngine()
|
||||||
rtcMediaEngine.RegisterCodec(codec)
|
|
||||||
|
// NewMediaEngine creates a new MediaEngine
|
||||||
|
func NewMediaEngine() *MediaEngine {
|
||||||
|
return &MediaEngine{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type mediaEngine struct {
|
// MediaEngine defines the codecs supported by a RTCPeerConnection
|
||||||
|
type MediaEngine struct {
|
||||||
codecs []*RTCRtpCodec
|
codecs []*RTCRtpCodec
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mediaEngine) RegisterCodec(codec *RTCRtpCodec) uint8 {
|
// RegisterCodec registers a codec to a media engine
|
||||||
|
func (m *MediaEngine) RegisterCodec(codec *RTCRtpCodec) uint8 {
|
||||||
// TODO: generate PayloadType if not set
|
// TODO: generate PayloadType if not set
|
||||||
m.codecs = append(m.codecs, codec)
|
m.codecs = append(m.codecs, codec)
|
||||||
return codec.PayloadType
|
return codec.PayloadType
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mediaEngine) getCodec(payloadType uint8) (*RTCRtpCodec, error) {
|
func (m *MediaEngine) getCodec(payloadType uint8) (*RTCRtpCodec, error) {
|
||||||
for _, codec := range m.codecs {
|
for _, codec := range m.codecs {
|
||||||
if codec.PayloadType == payloadType {
|
if codec.PayloadType == payloadType {
|
||||||
return codec, nil
|
return codec, nil
|
||||||
@@ -59,7 +61,7 @@ func (m *mediaEngine) getCodec(payloadType uint8) (*RTCRtpCodec, error) {
|
|||||||
return nil, errors.New("Codec not found")
|
return nil, errors.New("Codec not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mediaEngine) getCodecSDP(sdpCodec sdp.Codec) (*RTCRtpCodec, error) {
|
func (m *MediaEngine) getCodecSDP(sdpCodec sdp.Codec) (*RTCRtpCodec, error) {
|
||||||
for _, codec := range m.codecs {
|
for _, codec := range m.codecs {
|
||||||
if codec.Name == sdpCodec.Name &&
|
if codec.Name == sdpCodec.Name &&
|
||||||
codec.ClockRate == sdpCodec.ClockRate &&
|
codec.ClockRate == sdpCodec.ClockRate &&
|
||||||
@@ -72,9 +74,9 @@ func (m *mediaEngine) getCodecSDP(sdpCodec sdp.Codec) (*RTCRtpCodec, error) {
|
|||||||
return nil, errors.New("Codec not found")
|
return nil, errors.New("Codec not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mediaEngine) getCodecsByKind(kind RTCRtpCodecType) []*RTCRtpCodec {
|
func (m *MediaEngine) getCodecsByKind(kind RTCRtpCodecType) []*RTCRtpCodec {
|
||||||
var codecs []*RTCRtpCodec
|
var codecs []*RTCRtpCodec
|
||||||
for _, codec := range rtcMediaEngine.codecs {
|
for _, codec := range m.codecs {
|
||||||
if codec.Type == kind {
|
if codec.Type == kind {
|
||||||
codecs = append(codecs, codec)
|
codecs = append(codecs, codec)
|
||||||
}
|
}
|
||||||
@@ -82,6 +84,14 @@ func (m *mediaEngine) getCodecsByKind(kind RTCRtpCodecType) []*RTCRtpCodec {
|
|||||||
return codecs
|
return codecs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Names for the default codecs supported by pions-webrtc
|
||||||
|
const (
|
||||||
|
Opus = "opus"
|
||||||
|
VP8 = "VP8"
|
||||||
|
VP9 = "VP9"
|
||||||
|
H264 = "H264"
|
||||||
|
)
|
||||||
|
|
||||||
// NewRTCRtpOpusCodec is a helper to create an Opus codec
|
// NewRTCRtpOpusCodec is a helper to create an Opus codec
|
||||||
func NewRTCRtpOpusCodec(payloadType uint8, clockrate uint32, channels uint16) *RTCRtpCodec {
|
func NewRTCRtpOpusCodec(payloadType uint8, clockrate uint32, channels uint16) *RTCRtpCodec {
|
||||||
c := NewRTCRtpCodec(RTCRtpCodecTypeAudio,
|
c := NewRTCRtpCodec(RTCRtpCodecTypeAudio,
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ func (r *RTCPeerConnection) validateICECandidatePoolSize(config RTCConfiguration
|
|||||||
current := r.config
|
current := r.config
|
||||||
if r.LocalDescription != nil &&
|
if r.LocalDescription != nil &&
|
||||||
config.ICECandidatePoolSize != current.ICECandidatePoolSize {
|
config.ICECandidatePoolSize != current.ICECandidatePoolSize {
|
||||||
return &InvalidModificationError{Err: ErrModIceCandidatePoolSize}
|
return &InvalidModificationError{Err: ErrModICECandidatePoolSize}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -265,22 +265,27 @@ func parseICEServer(server RTCICEServer, rawURL string) (ice.URL, error) {
|
|||||||
return iceurl, &SyntaxError{Err: err}
|
return iceurl, &SyntaxError{Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, isPass := server.Credential.(string)
|
|
||||||
_, isOauth := server.Credential.(RTCOAuthCredential)
|
|
||||||
noPass := !isPass && !isOauth
|
|
||||||
|
|
||||||
if iceurl.Type == ice.ServerTypeTURN {
|
if iceurl.Type == ice.ServerTypeTURN {
|
||||||
if server.Username == "" ||
|
if server.Username == "" {
|
||||||
noPass {
|
|
||||||
return iceurl, &InvalidAccessError{Err: ErrNoTurnCred}
|
return iceurl, &InvalidAccessError{Err: ErrNoTurnCred}
|
||||||
}
|
}
|
||||||
if server.CredentialType == RTCICECredentialTypePassword &&
|
|
||||||
!isPass {
|
switch t := server.Credential.(type) {
|
||||||
return iceurl, &InvalidAccessError{Err: ErrTurnCred}
|
case string:
|
||||||
}
|
if t == "" {
|
||||||
if server.CredentialType == RTCICECredentialTypeOauth &&
|
return iceurl, &InvalidAccessError{Err: ErrNoTurnCred}
|
||||||
!isOauth {
|
} else if server.CredentialType != RTCICECredentialTypePassword {
|
||||||
|
return iceurl, &InvalidAccessError{Err: ErrTurnCred}
|
||||||
|
}
|
||||||
|
|
||||||
|
case RTCOAuthCredential:
|
||||||
|
if server.CredentialType != RTCICECredentialTypeOauth {
|
||||||
|
return iceurl, &InvalidAccessError{Err: ErrTurnCred}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
return iceurl, &InvalidAccessError{Err: ErrTurnCred}
|
return iceurl, &InvalidAccessError{Err: ErrTurnCred}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return iceurl, nil
|
return iceurl, nil
|
||||||
|
|||||||
@@ -100,13 +100,15 @@ type RTCPeerConnection struct {
|
|||||||
connectionState RTCPeerConnectionState
|
connectionState RTCPeerConnectionState
|
||||||
|
|
||||||
// Media
|
// Media
|
||||||
|
mediaEngine *MediaEngine
|
||||||
rtpTransceivers []*RTCRtpTransceiver
|
rtpTransceivers []*RTCRtpTransceiver
|
||||||
Ontrack func(*RTCTrack)
|
Ontrack func(*RTCTrack)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Public
|
||||||
|
|
||||||
// New creates a new RTCPeerConfiguration with the provided configuration
|
// New creates a new RTCPeerConfiguration with the provided configuration
|
||||||
func New(config RTCConfiguration) (*RTCPeerConnection, error) {
|
func New(config RTCConfiguration) (*RTCPeerConnection, error) {
|
||||||
|
|
||||||
r := &RTCPeerConnection{
|
r := &RTCPeerConnection{
|
||||||
config: config,
|
config: config,
|
||||||
signalingState: RTCSignalingStateStable,
|
signalingState: RTCSignalingStateStable,
|
||||||
@@ -114,6 +116,7 @@ func New(config RTCConfiguration) (*RTCPeerConnection, error) {
|
|||||||
iceGatheringState: ice.GatheringStateNew,
|
iceGatheringState: ice.GatheringStateNew,
|
||||||
iceConnectionState: ice.ConnectionStateNew,
|
iceConnectionState: ice.ConnectionStateNew,
|
||||||
connectionState: RTCPeerConnectionStateNew,
|
connectionState: RTCPeerConnectionStateNew,
|
||||||
|
mediaEngine: DefaultMediaEngine,
|
||||||
}
|
}
|
||||||
err := r.SetConfiguration(config)
|
err := r.SetConfiguration(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -122,12 +125,14 @@ func New(config RTCConfiguration) (*RTCPeerConnection, error) {
|
|||||||
|
|
||||||
r.tlscfg = dtls.NewTLSCfg()
|
r.tlscfg = dtls.NewTLSCfg()
|
||||||
|
|
||||||
// TODO: Initialize ICE Agent
|
|
||||||
|
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public
|
// SetMediaEngine allows overwriting the default media engine used by the RTCPeerConnection
|
||||||
|
// This enables RTCPeerConnection with support for different codecs
|
||||||
|
func (r *RTCPeerConnection) SetMediaEngine(m *MediaEngine) {
|
||||||
|
r.mediaEngine = m
|
||||||
|
}
|
||||||
|
|
||||||
// SetIdentityProvider is used to configure an identity provider to generate identity assertions
|
// SetIdentityProvider is used to configure an identity provider to generate identity assertions
|
||||||
func (r *RTCPeerConnection) SetIdentityProvider(provider string) error {
|
func (r *RTCPeerConnection) SetIdentityProvider(provider string) error {
|
||||||
@@ -161,7 +166,7 @@ func (r *RTCPeerConnection) generateChannel(ssrc uint32, payloadType uint8) (buf
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
codec, err := rtcMediaEngine.getCodecSDP(sdpCodec)
|
codec, err := r.mediaEngine.getCodecSDP(sdpCodec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Codec %s in not registered\n", sdpCodec)
|
fmt.Printf("Codec %s in not registered\n", sdpCodec)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ func (r *RTCPeerConnection) SetRemoteDescription(desc RTCSessionDescription) err
|
|||||||
// RTCOfferOptions describes the options used to control the offer creation process
|
// RTCOfferOptions describes the options used to control the offer creation process
|
||||||
type RTCOfferOptions struct {
|
type RTCOfferOptions struct {
|
||||||
VoiceActivityDetection bool
|
VoiceActivityDetection bool
|
||||||
IceRestart bool
|
ICERestart bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateOffer starts the RTCPeerConnection and generates the localDescription
|
// CreateOffer starts the RTCPeerConnection and generates the localDescription
|
||||||
@@ -181,7 +181,7 @@ func (r *RTCPeerConnection) CreateOffer(options *RTCOfferOptions) (RTCSessionDes
|
|||||||
track := tranceiver.Sender.Track
|
track := tranceiver.Sender.Track
|
||||||
cname := "pion" // TODO: Support RTP streams synchronisation
|
cname := "pion" // TODO: Support RTP streams synchronisation
|
||||||
steamlabel := "pion" // TODO: Support steam labels
|
steamlabel := "pion" // TODO: Support steam labels
|
||||||
codec, err := rtcMediaEngine.getCodec(track.PayloadType)
|
codec, err := r.mediaEngine.getCodec(track.PayloadType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return RTCSessionDescription{}, err
|
return RTCSessionDescription{}, err
|
||||||
}
|
}
|
||||||
@@ -273,7 +273,7 @@ func (r *RTCPeerConnection) addAnswerMedia(d *sdp.SessionDescription, codecType
|
|||||||
track := tranceiver.Sender.Track
|
track := tranceiver.Sender.Track
|
||||||
cname := track.Label // TODO: Support RTP streams synchronisation
|
cname := track.Label // TODO: Support RTP streams synchronisation
|
||||||
steamlabel := track.Label // TODO: Support steam labels
|
steamlabel := track.Label // TODO: Support steam labels
|
||||||
codec, err := rtcMediaEngine.getCodec(track.PayloadType)
|
codec, err := r.mediaEngine.getCodec(track.PayloadType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -314,7 +314,7 @@ func (r *RTCPeerConnection) addAnswerMedia(d *sdp.SessionDescription, codecType
|
|||||||
WithPropertyAttribute(sdp.AttrKeyRtcpMux). // TODO: support RTCP fallback
|
WithPropertyAttribute(sdp.AttrKeyRtcpMux). // TODO: support RTCP fallback
|
||||||
WithPropertyAttribute(sdp.AttrKeyRtcpRsize) // TODO: Support Reduced-Size RTCP?
|
WithPropertyAttribute(sdp.AttrKeyRtcpRsize) // TODO: Support Reduced-Size RTCP?
|
||||||
|
|
||||||
for _, codec := range rtcMediaEngine.getCodecsByKind(codecType) {
|
for _, codec := range r.mediaEngine.getCodecsByKind(codecType) {
|
||||||
media.WithCodec(
|
media.WithCodec(
|
||||||
codec.PayloadType,
|
codec.PayloadType,
|
||||||
codec.Name,
|
codec.Name,
|
||||||
|
|||||||
Reference in New Issue
Block a user