Files
webrtc/mediaengine.go
Jingyang Kang 334692b122 Add findFECPayloadType (#3084)
#### Description

Before this, any interceptor that tries to read FEC payload type from
`info *interceptor.StreamInfo`, always gets 0. This PR implements the
logic to find FEC payload type.

Also, MimeType consts are moved from mediaengine.go to mimetype.go,
since mediaengine.go is not included in the WASM build.
2025-04-07 20:29:51 +08:00

788 lines
21 KiB
Go

// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
//go:build !js
// +build !js
package webrtc
import (
"errors"
"fmt"
"strconv"
"strings"
"sync"
"time"
"github.com/pion/rtp"
"github.com/pion/rtp/codecs"
"github.com/pion/sdp/v3"
"github.com/pion/webrtc/v4/internal/fmtp"
)
type mediaEngineHeaderExtension struct {
uri string
isAudio, isVideo bool
// If set only Transceivers of this direction are allowed
allowedDirections []RTPTransceiverDirection
}
// A MediaEngine defines the codecs supported by a PeerConnection, and the
// configuration of those codecs.
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
mu sync.RWMutex
}
// RegisterDefaultCodecs registers the default codecs supported by Pion WebRTC.
// RegisterDefaultCodecs is not safe for concurrent use.
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: rtp.PayloadTypeG722,
},
{
RTPCodecCapability: RTPCodecCapability{MimeTypePCMU, 8000, 0, "", nil},
PayloadType: rtp.PayloadTypePCMU,
},
{
RTPCodecCapability: RTPCodecCapability{MimeTypePCMA, 8000, 0, "", nil},
PayloadType: rtp.PayloadTypePCMA,
},
} {
if err := m.RegisterCodec(codec, 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{MimeTypeRTX, 90000, 0, "apt=96", nil},
PayloadType: 97,
},
{
RTPCodecCapability: RTPCodecCapability{
MimeTypeH264, 90000, 0,
"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f",
videoRTCPFeedback,
},
PayloadType: 102,
},
{
RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=102", nil},
PayloadType: 103,
},
{
RTPCodecCapability: RTPCodecCapability{
MimeTypeH264, 90000, 0,
"level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f",
videoRTCPFeedback,
},
PayloadType: 104,
},
{
RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=104", nil},
PayloadType: 105,
},
{
RTPCodecCapability: RTPCodecCapability{
MimeTypeH264, 90000, 0,
"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
videoRTCPFeedback,
},
PayloadType: 106,
},
{
RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=106", nil},
PayloadType: 107,
},
{
RTPCodecCapability: RTPCodecCapability{
MimeTypeH264, 90000, 0,
"level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f",
videoRTCPFeedback,
},
PayloadType: 108,
},
{
RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=108", nil},
PayloadType: 109,
},
{
RTPCodecCapability: RTPCodecCapability{
MimeTypeH264, 90000, 0,
"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f",
videoRTCPFeedback,
},
PayloadType: 127,
},
{
RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=127", nil},
PayloadType: 125,
},
{
RTPCodecCapability: RTPCodecCapability{
MimeTypeH264,
90000, 0,
"level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=4d001f",
videoRTCPFeedback,
},
PayloadType: 39,
},
{
RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=39", nil},
PayloadType: 40,
},
{
RTPCodecCapability: RTPCodecCapability{MimeTypeAV1, 90000, 0, "", videoRTCPFeedback},
PayloadType: 45,
},
{
RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=45", nil},
PayloadType: 46,
},
{
RTPCodecCapability: RTPCodecCapability{MimeTypeVP9, 90000, 0, "profile-id=0", videoRTCPFeedback},
PayloadType: 98,
},
{
RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=98", nil},
PayloadType: 99,
},
{
RTPCodecCapability: RTPCodecCapability{MimeTypeVP9, 90000, 0, "profile-id=2", videoRTCPFeedback},
PayloadType: 100,
},
{
RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=100", nil},
PayloadType: 101,
},
{
RTPCodecCapability: RTPCodecCapability{
MimeTypeH264, 90000, 0,
"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f",
videoRTCPFeedback,
},
PayloadType: 112,
},
{
RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=112", nil},
PayloadType: 113,
},
} {
if err := m.RegisterCodec(codec, RTPCodecTypeVideo); err != nil {
return err
}
}
return nil
}
// addCodec will append codec if it not exists.
func (m *MediaEngine) addCodec(codecs []RTPCodecParameters, codec RTPCodecParameters) ([]RTPCodecParameters, error) {
for _, c := range codecs {
if c.PayloadType == codec.PayloadType {
if c.MimeType == codec.MimeType &&
fmtp.ClockRateEqual(c.MimeType, c.ClockRate, codec.ClockRate) &&
fmtp.ChannelsEqual(c.MimeType, c.Channels, codec.Channels) {
return codecs, nil
}
return codecs, ErrCodecAlreadyRegistered
}
}
return append(codecs, codec), nil
}
// RegisterCodec adds codec to the MediaEngine
// These are the list of codecs supported by this PeerConnection.
func (m *MediaEngine) RegisterCodec(codec RTPCodecParameters, typ RTPCodecType) error {
m.mu.Lock()
defer m.mu.Unlock()
var err error
codec.statsID = fmt.Sprintf("RTPCodec-%d", time.Now().UnixNano())
switch typ {
case RTPCodecTypeAudio:
m.audioCodecs, err = m.addCodec(m.audioCodecs, codec)
case RTPCodecTypeVideo:
m.videoCodecs, err = m.addCodec(m.videoCodecs, codec)
default:
return ErrUnknownType
}
return err
}
// RegisterHeaderExtension adds a header extension to the MediaEngine
// To determine the negotiated value use `GetHeaderExtensionID` after signaling is complete.
//
//nolint:cyclop
func (m *MediaEngine) RegisterHeaderExtension(
extension RTPHeaderExtensionCapability,
typ RTPCodecType,
allowedDirections ...RTPTransceiverDirection,
) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.negotiatedHeaderExtensions == nil {
m.negotiatedHeaderExtensions = map[int]mediaEngineHeaderExtension{}
}
if len(allowedDirections) == 0 {
allowedDirections = []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly, RTPTransceiverDirectionSendonly}
}
for _, direction := range allowedDirections {
if direction != RTPTransceiverDirectionRecvonly && direction != RTPTransceiverDirectionSendonly {
return ErrRegisterHeaderExtensionInvalidDirection
}
}
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
m.headerExtensions[extensionIndex].allowedDirections = allowedDirections
return nil
}
// RegisterFeedback adds feedback mechanism to already registered codecs.
func (m *MediaEngine) RegisterFeedback(feedback RTCPFeedback, typ RTPCodecType) {
m.mu.Lock()
defer m.mu.Unlock()
if typ == RTPCodecTypeVideo {
for i, v := range m.videoCodecs {
v.RTCPFeedback = append(v.RTCPFeedback, feedback)
m.videoCodecs[i] = v
}
} else if typ == RTPCodecTypeAudio {
for i, v := range m.audioCodecs {
v.RTCPFeedback = append(v.RTCPFeedback, feedback)
m.audioCodecs[i] = v
}
}
}
// 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,
) {
m.mu.RLock()
defer m.mu.RUnlock()
if m.negotiatedHeaderExtensions == nil {
return 0, false, false
}
for id, h := range m.negotiatedHeaderExtensions {
if extension.URI == h.uri {
return id, h.isAudio, h.isVideo
}
}
return
}
// copy copies any user modifiable state of the MediaEngine
// all internal state is reset.
func (m *MediaEngine) copy() *MediaEngine {
m.mu.Lock()
defer m.mu.Unlock()
cloned := &MediaEngine{
videoCodecs: append([]RTPCodecParameters{}, m.videoCodecs...),
audioCodecs: append([]RTPCodecParameters{}, m.audioCodecs...),
headerExtensions: append([]mediaEngineHeaderExtension{}, m.headerExtensions...),
}
if len(m.headerExtensions) > 0 {
cloned.negotiatedHeaderExtensions = map[int]mediaEngineHeaderExtension{}
}
return cloned
}
func findCodecByPayload(codecs []RTPCodecParameters, payloadType PayloadType) *RTPCodecParameters {
for _, codec := range codecs {
if codec.PayloadType == payloadType {
return &codec
}
}
return nil
}
func (m *MediaEngine) getCodecByPayload(payloadType PayloadType) (RTPCodecParameters, RTPCodecType, error) {
m.mu.RLock()
defer m.mu.RUnlock()
// if we've negotiated audio or video, check the negotiated types before our
// built-in payload types, to ensure we pick the codec the other side wants.
if m.negotiatedVideo {
if codec := findCodecByPayload(m.negotiatedVideoCodecs, payloadType); codec != nil {
return *codec, RTPCodecTypeVideo, nil
}
}
if m.negotiatedAudio {
if codec := findCodecByPayload(m.negotiatedAudioCodecs, payloadType); codec != nil {
return *codec, RTPCodecTypeAudio, nil
}
}
if !m.negotiatedVideo {
if codec := findCodecByPayload(m.videoCodecs, payloadType); codec != nil {
return *codec, RTPCodecTypeVideo, nil
}
}
if !m.negotiatedAudio {
if codec := findCodecByPayload(m.audioCodecs, payloadType); codec != nil {
return *codec, RTPCodecTypeAudio, nil
}
}
return RTPCodecParameters{}, 0, ErrCodecNotFound
}
func (m *MediaEngine) collectStats(collector *statsReportCollector) {
m.mu.RLock()
defer m.mu.RUnlock()
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), //nolint:gosec // G115
SDPFmtpLine: codec.SDPFmtpLine,
}
collector.Collect(stats.ID, stats)
}
}
statsLoop(m.videoCodecs)
statsLoop(m.audioCodecs)
}
// Look up a codec and enable if it exists.
//
//nolint:cyclop
func (m *MediaEngine) matchRemoteCodec(
remoteCodec RTPCodecParameters,
typ RTPCodecType,
exactMatches, partialMatches []RTPCodecParameters,
) (RTPCodecParameters, codecMatchType, error) {
codecs := m.videoCodecs
if typ == RTPCodecTypeAudio {
codecs = m.audioCodecs
}
remoteFmtp := fmtp.Parse(
remoteCodec.RTPCodecCapability.MimeType,
remoteCodec.RTPCodecCapability.ClockRate,
remoteCodec.RTPCodecCapability.Channels,
remoteCodec.RTPCodecCapability.SDPFmtpLine)
if apt, hasApt := remoteFmtp.Parameter("apt"); hasApt { //nolint:nestif
payloadType, err := strconv.ParseUint(apt, 10, 8)
if err != nil {
return RTPCodecParameters{}, codecMatchNone, err
}
aptMatch := codecMatchNone
var aptCodec RTPCodecParameters
for _, codec := range exactMatches {
if codec.PayloadType == PayloadType(payloadType) {
aptMatch = codecMatchExact
aptCodec = codec
break
}
}
if aptMatch == codecMatchNone {
for _, codec := range partialMatches {
if codec.PayloadType == PayloadType(payloadType) {
aptMatch = codecMatchPartial
aptCodec = codec
break
}
}
}
if aptMatch == codecMatchNone {
return RTPCodecParameters{}, codecMatchNone, nil // not an error, we just ignore this codec we don't support
}
// replace the apt value with the original codec's payload type
toMatchCodec := remoteCodec
if aptMatched, mt := codecParametersFuzzySearch(aptCodec, codecs); mt == aptMatch {
toMatchCodec.SDPFmtpLine = strings.Replace(
toMatchCodec.SDPFmtpLine,
fmt.Sprintf("apt=%d", payloadType),
fmt.Sprintf("apt=%d", aptMatched.PayloadType),
1,
)
}
// if apt's media codec is partial match, then apt codec must be partial match too.
localCodec, matchType := codecParametersFuzzySearch(toMatchCodec, codecs)
if matchType == codecMatchExact && aptMatch == codecMatchPartial {
matchType = codecMatchPartial
}
return localCodec, matchType, nil
}
localCodec, matchType := codecParametersFuzzySearch(remoteCodec, codecs)
return localCodec, matchType, nil
}
// Update header extensions from a remote media section.
func (m *MediaEngine) updateHeaderExtensionFromMediaSection(media *sdp.MediaDescription) error {
var typ RTPCodecType
switch {
case strings.EqualFold(media.MediaName.Media, "audio"):
typ = RTPCodecTypeAudio
case strings.EqualFold(media.MediaName.Media, "video"):
typ = RTPCodecTypeVideo
default:
return nil
}
extensions, err := rtpExtensionsFromMediaDescription(media)
if err != nil {
return err
}
for extension, id := range extensions {
if err = m.updateHeaderExtension(id, extension, typ); err != nil {
return err
}
}
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 {
return nil
}
for _, localExtension := range m.headerExtensions {
if localExtension.uri == extension {
h := mediaEngineHeaderExtension{uri: extension, allowedDirections: localExtension.allowedDirections}
if existingValue, ok := m.negotiatedHeaderExtensions[id]; ok {
h = existingValue
}
switch {
case localExtension.isAudio && typ == RTPCodecTypeAudio:
h.isAudio = true
case localExtension.isVideo && typ == RTPCodecTypeVideo:
h.isVideo = true
}
m.negotiatedHeaderExtensions[id] = h
}
}
return nil
}
func (m *MediaEngine) pushCodecs(codecs []RTPCodecParameters, typ RTPCodecType) error {
var joinedErr error
for _, codec := range codecs {
var err error
if typ == RTPCodecTypeAudio {
m.negotiatedAudioCodecs, err = m.addCodec(m.negotiatedAudioCodecs, codec)
} else if typ == RTPCodecTypeVideo {
m.negotiatedVideoCodecs, err = m.addCodec(m.negotiatedVideoCodecs, codec)
}
if err != nil {
joinedErr = errors.Join(joinedErr, err)
}
}
return joinedErr
}
// Update the MediaEngine from a remote description.
func (m *MediaEngine) updateFromRemoteDescription(desc sdp.SessionDescription) error { //nolint:cyclop
m.mu.Lock()
defer m.mu.Unlock()
for _, media := range desc.MediaDescriptions {
var typ RTPCodecType
switch {
case strings.EqualFold(media.MediaName.Media, "audio"):
typ = RTPCodecTypeAudio
case strings.EqualFold(media.MediaName.Media, "video"):
typ = RTPCodecTypeVideo
}
switch {
case !m.negotiatedAudio && typ == RTPCodecTypeAudio:
m.negotiatedAudio = true
case !m.negotiatedVideo && typ == RTPCodecTypeVideo:
m.negotiatedVideo = true
default:
// update header extesions from remote sdp if codec is negotiated, Firefox
// would send updated header extension in renegotiation.
// e.g. publish first track without simucalst ->negotiated-> publish second track with simucalst
// then the two media secontions have different rtp header extensions in offer
if err := m.updateHeaderExtensionFromMediaSection(media); err != nil {
return err
}
continue
}
codecs, err := codecsFromMediaDescription(media)
if err != nil {
return err
}
exactMatches := make([]RTPCodecParameters, 0, len(codecs))
partialMatches := make([]RTPCodecParameters, 0, len(codecs))
for _, remoteCodec := range codecs {
localCodec, matchType, mErr := m.matchRemoteCodec(remoteCodec, typ, exactMatches, partialMatches)
if mErr != nil {
return mErr
}
remoteCodec.RTCPFeedback = rtcpFeedbackIntersection(localCodec.RTCPFeedback, remoteCodec.RTCPFeedback)
if matchType == codecMatchExact {
exactMatches = append(exactMatches, remoteCodec)
} else if matchType == codecMatchPartial {
partialMatches = append(partialMatches, remoteCodec)
}
}
// use exact matches when they exist, otherwise fall back to partial
switch {
case len(exactMatches) > 0:
err = m.pushCodecs(exactMatches, typ)
case len(partialMatches) > 0:
err = m.pushCodecs(partialMatches, typ)
default:
// no match, not negotiated
continue
}
if err != nil {
return err
}
if err := m.updateHeaderExtensionFromMediaSection(media); err != nil {
return err
}
}
return nil
}
func (m *MediaEngine) getCodecsByKind(typ RTPCodecType) []RTPCodecParameters {
m.mu.RLock()
defer m.mu.RUnlock()
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
}
//nolint:gocognit,cyclop
func (m *MediaEngine) getRTPParametersByKind(typ RTPCodecType, directions []RTPTransceiverDirection) RTPParameters {
headerExtensions := make([]RTPHeaderExtensionParameter, 0)
// perform before locking to prevent recursive RLocks
foundCodecs := m.getCodecsByKind(typ)
m.mu.RLock()
defer m.mu.RUnlock()
//nolint:nestif
if (m.negotiatedVideo && typ == RTPCodecTypeVideo) || (m.negotiatedAudio && typ == RTPCodecTypeAudio) {
for id, e := range m.negotiatedHeaderExtensions {
if haveRTPTransceiverDirectionIntersection(e.allowedDirections, directions) &&
(e.isAudio && typ == RTPCodecTypeAudio || e.isVideo && typ == RTPCodecTypeVideo) {
headerExtensions = append(headerExtensions, RTPHeaderExtensionParameter{ID: id, URI: e.uri})
}
}
} else {
mediaHeaderExtensions := make(map[int]mediaEngineHeaderExtension)
for _, ext := range m.headerExtensions {
usingNegotiatedID := false
for id := range m.negotiatedHeaderExtensions {
if m.negotiatedHeaderExtensions[id].uri == ext.uri {
usingNegotiatedID = true
mediaHeaderExtensions[id] = ext
break
}
}
if !usingNegotiatedID {
for id := 1; id < 15; id++ {
idAvailable := true
if _, ok := mediaHeaderExtensions[id]; ok {
idAvailable = false
}
if _, taken := m.negotiatedHeaderExtensions[id]; idAvailable && !taken {
mediaHeaderExtensions[id] = ext
break
}
}
}
}
for id, e := range mediaHeaderExtensions {
if haveRTPTransceiverDirectionIntersection(e.allowedDirections, directions) &&
(e.isAudio && typ == RTPCodecTypeAudio || e.isVideo && typ == RTPCodecTypeVideo) {
headerExtensions = append(headerExtensions, RTPHeaderExtensionParameter{ID: id, URI: e.uri})
}
}
}
return RTPParameters{
HeaderExtensions: headerExtensions,
Codecs: foundCodecs,
}
}
func (m *MediaEngine) getRTPParametersByPayloadType(payloadType PayloadType) (RTPParameters, error) {
codec, typ, err := m.getCodecByPayload(payloadType)
if err != nil {
return RTPParameters{}, err
}
m.mu.RLock()
defer m.mu.RUnlock()
headerExtensions := make([]RTPHeaderExtensionParameter, 0)
for id, e := range m.negotiatedHeaderExtensions {
if e.isAudio && typ == RTPCodecTypeAudio || e.isVideo && typ == RTPCodecTypeVideo {
headerExtensions = append(headerExtensions, RTPHeaderExtensionParameter{ID: id, URI: e.uri})
}
}
return RTPParameters{
HeaderExtensions: headerExtensions,
Codecs: []RTPCodecParameters{codec},
}, nil
}
func payloaderForCodec(codec RTPCodecCapability) (rtp.Payloader, error) {
switch strings.ToLower(codec.MimeType) {
case strings.ToLower(MimeTypeH264):
return &codecs.H264Payloader{}, nil
case strings.ToLower(MimeTypeH265):
return &codecs.H265Payloader{}, nil
case strings.ToLower(MimeTypeOpus):
return &codecs.OpusPayloader{}, nil
case strings.ToLower(MimeTypeVP8):
return &codecs.VP8Payloader{
EnablePictureID: true,
}, nil
case strings.ToLower(MimeTypeVP9):
return &codecs.VP9Payloader{}, nil
case strings.ToLower(MimeTypeAV1):
return &codecs.AV1Payloader{}, nil
case strings.ToLower(MimeTypeG722):
return &codecs.G722Payloader{}, nil
case strings.ToLower(MimeTypePCMU), strings.ToLower(MimeTypePCMA):
return &codecs.G711Payloader{}, nil
default:
return nil, ErrNoPayloaderForCodec
}
}
func (m *MediaEngine) isRTXEnabled(typ RTPCodecType, directions []RTPTransceiverDirection) bool {
for _, p := range m.getRTPParametersByKind(typ, directions).Codecs {
if p.MimeType == MimeTypeRTX {
return true
}
}
return false
}
func (m *MediaEngine) isFECEnabled(typ RTPCodecType, directions []RTPTransceiverDirection) bool {
for _, p := range m.getRTPParametersByKind(typ, directions).Codecs {
if strings.Contains(p.MimeType, MimeTypeFlexFEC) {
return true
}
}
return false
}