// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT

//go:build !js
// +build !js

package webrtc

import (
	
	
	
	
	
	

	
	
	
	
)

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
	negotiateMultiCodecs             bool

	videoCodecs, audioCodecs                     []RTPCodecParameters
	negotiatedVideoCodecs, negotiatedAudioCodecs []RTPCodecParameters

	headerExtensions           []mediaEngineHeaderExtension
	negotiatedHeaderExtensions map[int]mediaEngineHeaderExtension

	mu sync.RWMutex
}

// setMultiCodecNegotiation enables or disables the negotiation of multiple codecs.
func ( *MediaEngine) ( bool) {
	.mu.Lock()
	defer .mu.Unlock()

	.negotiateMultiCodecs = 
}

// multiCodecNegotiation returns the current state of the negotiation of multiple codecs.
func ( *MediaEngine) () bool {
	.mu.RLock()
	defer .mu.RUnlock()

	return .negotiateMultiCodecs
}

// RegisterDefaultCodecs registers the default codecs supported by Pion WebRTC.
// RegisterDefaultCodecs is not safe for concurrent use.
func ( *MediaEngine) () error {
	// Default Pion Audio Codecs
	for ,  := 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  := .RegisterCodec(, RTPCodecTypeAudio);  != nil {
			return 
		}
	}

	 := []RTCPFeedback{{"goog-remb", ""}, {"ccm", "fir"}, {"nack", ""}, {"nack", "pli"}}
	for ,  := range []RTPCodecParameters{
		{
			RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", },
			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",
				,
			},
			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",
				,
			},
			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",
				,
			},
			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",
				,
			},
			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",
				,
			},
			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",
				,
			},
			PayloadType: 39,
		},
		{
			RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=39", nil},
			PayloadType:        40,
		},
		{
			RTPCodecCapability: RTPCodecCapability{
				MimeType:     MimeTypeH265,
				ClockRate:    90000,
				RTCPFeedback: ,
			},
			PayloadType: 116,
		},
		{
			RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=116", nil},
			PayloadType:        117,
		},
		{
			RTPCodecCapability: RTPCodecCapability{MimeTypeAV1, 90000, 0, "", },
			PayloadType:        45,
		},
		{
			RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=45", nil},
			PayloadType:        46,
		},

		{
			RTPCodecCapability: RTPCodecCapability{MimeTypeVP9, 90000, 0, "profile-id=0", },
			PayloadType:        98,
		},
		{
			RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=98", nil},
			PayloadType:        99,
		},

		{
			RTPCodecCapability: RTPCodecCapability{MimeTypeVP9, 90000, 0, "profile-id=2", },
			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",
				,
			},
			PayloadType: 112,
		},
		{
			RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=112", nil},
			PayloadType:        113,
		},
	} {
		if  := .RegisterCodec(, RTPCodecTypeVideo);  != nil {
			return 
		}
	}

	return nil
}

// addCodec will append codec if it not exists.
func ( *MediaEngine) ( []RTPCodecParameters,  RTPCodecParameters) ([]RTPCodecParameters, error) {
	for ,  := range  {
		if .PayloadType == .PayloadType {
			if strings.EqualFold(.MimeType, .MimeType) &&
				fmtp.ClockRateEqual(.MimeType, .ClockRate, .ClockRate) &&
				fmtp.ChannelsEqual(.MimeType, .Channels, .Channels) {
				return , nil
			}

			return , ErrCodecAlreadyRegistered
		}
	}

	return append(, ), nil
}

// RegisterCodec adds codec to the MediaEngine
// These are the list of codecs supported by this PeerConnection.
func ( *MediaEngine) ( RTPCodecParameters,  RTPCodecType) error {
	.mu.Lock()
	defer .mu.Unlock()

	var  error
	.statsID = fmt.Sprintf("RTPCodec-%d", time.Now().UnixNano())
	switch  {
	case RTPCodecTypeAudio:
		.audioCodecs,  = .addCodec(.audioCodecs, )
	case RTPCodecTypeVideo:
		.videoCodecs,  = .addCodec(.videoCodecs, )
	default:
		return ErrUnknownType
	}

	return 
}

// RegisterHeaderExtension adds a header extension to the MediaEngine
// To determine the negotiated value use `GetHeaderExtensionID` after signaling is complete.
//
//nolint:cyclop
func ( *MediaEngine) (
	 RTPHeaderExtensionCapability,
	 RTPCodecType,
	 ...RTPTransceiverDirection,
) error {
	.mu.Lock()
	defer .mu.Unlock()

	if .negotiatedHeaderExtensions == nil {
		.negotiatedHeaderExtensions = map[int]mediaEngineHeaderExtension{}
	}

	if len() == 0 {
		 = []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly, RTPTransceiverDirectionSendonly}
	}

	for ,  := range  {
		if  != RTPTransceiverDirectionRecvonly &&  != RTPTransceiverDirectionSendonly {
			return ErrRegisterHeaderExtensionInvalidDirection
		}
	}

	 := -1
	for  := range .headerExtensions {
		if .URI == .headerExtensions[].uri {
			 = 
		}
	}

	if  == -1 {
		.headerExtensions = append(.headerExtensions, mediaEngineHeaderExtension{})
		 = len(.headerExtensions) - 1
	}

	if  == RTPCodecTypeAudio {
		.headerExtensions[].isAudio = true
	} else if  == RTPCodecTypeVideo {
		.headerExtensions[].isVideo = true
	}

	.headerExtensions[].uri = .URI
	.headerExtensions[].allowedDirections = 

	return nil
}

// RegisterFeedback adds feedback mechanism to already registered codecs.
func ( *MediaEngine) ( RTCPFeedback,  RTPCodecType) {
	.mu.Lock()
	defer .mu.Unlock()

	if  == RTPCodecTypeVideo {
		for ,  := range .videoCodecs {
			.RTCPFeedback = append(.RTCPFeedback, )
			.videoCodecs[] = 
		}
	} else if  == RTPCodecTypeAudio {
		for ,  := range .audioCodecs {
			.RTCPFeedback = append(.RTCPFeedback, )
			.audioCodecs[] = 
		}
	}
}

// getHeaderExtensionID returns the negotiated ID for a header extension.
// If the Header Extension isn't enabled ok will be false.
func ( *MediaEngine) ( RTPHeaderExtensionCapability) (
	 int,
	,  bool,
) {
	.mu.RLock()
	defer .mu.RUnlock()

	if .negotiatedHeaderExtensions == nil {
		return 0, false, false
	}

	for ,  := range .negotiatedHeaderExtensions {
		if .URI == .uri {
			return , .isAudio, .isVideo
		}
	}

	return
}

// copy copies any user modifiable state of the MediaEngine
// all internal state is reset.
func ( *MediaEngine) () *MediaEngine {
	.mu.Lock()
	defer .mu.Unlock()
	 := &MediaEngine{
		videoCodecs:      append([]RTPCodecParameters{}, .videoCodecs...),
		audioCodecs:      append([]RTPCodecParameters{}, .audioCodecs...),
		headerExtensions: append([]mediaEngineHeaderExtension{}, .headerExtensions...),
	}
	if len(.headerExtensions) > 0 {
		.negotiatedHeaderExtensions = map[int]mediaEngineHeaderExtension{}
	}

	return 
}

func findCodecByPayload( []RTPCodecParameters,  PayloadType) *RTPCodecParameters {
	for ,  := range  {
		if .PayloadType ==  {
			return &
		}
	}

	return nil
}

func ( *MediaEngine) ( PayloadType) (RTPCodecParameters, RTPCodecType, error) {
	.mu.RLock()
	defer .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 .negotiatedVideo {
		if  := findCodecByPayload(.negotiatedVideoCodecs, );  != nil {
			return *, RTPCodecTypeVideo, nil
		}
	}
	if .negotiatedAudio {
		if  := findCodecByPayload(.negotiatedAudioCodecs, );  != nil {
			return *, RTPCodecTypeAudio, nil
		}
	}
	if !.negotiatedVideo {
		if  := findCodecByPayload(.videoCodecs, );  != nil {
			return *, RTPCodecTypeVideo, nil
		}
	}
	if !.negotiatedAudio {
		if  := findCodecByPayload(.audioCodecs, );  != nil {
			return *, RTPCodecTypeAudio, nil
		}
	}

	return RTPCodecParameters{}, 0, ErrCodecNotFound
}

func ( *MediaEngine) ( *statsReportCollector) {
	.mu.RLock()
	defer .mu.RUnlock()

	 := func( []RTPCodecParameters) {
		for ,  := range  {
			.Collecting()
			 := CodecStats{
				Timestamp:   statsTimestampFrom(time.Now()),
				Type:        StatsTypeCodec,
				ID:          .statsID,
				PayloadType: .PayloadType,
				MimeType:    .MimeType,
				ClockRate:   .ClockRate,
				Channels:    uint8(.Channels), //nolint:gosec // G115
				SDPFmtpLine: .SDPFmtpLine,
			}

			.Collect(.ID, )
		}
	}

	(.videoCodecs)
	(.audioCodecs)
}

// Look up a codec and enable if it exists.
//
//nolint:cyclop
func ( *MediaEngine) (
	 RTPCodecParameters,
	 RTPCodecType,
	,  []RTPCodecParameters,
) (RTPCodecParameters, codecMatchType, error) {
	 := .videoCodecs
	if  == RTPCodecTypeAudio {
		 = .audioCodecs
	}

	 := fmtp.Parse(
		.RTPCodecCapability.MimeType,
		.RTPCodecCapability.ClockRate,
		.RTPCodecCapability.Channels,
		.RTPCodecCapability.SDPFmtpLine)

	if ,  := .Parameter("apt");  { //nolint:nestif
		,  := strconv.ParseUint(, 10, 8)
		if  != nil {
			return RTPCodecParameters{}, codecMatchNone, 
		}

		 := codecMatchNone
		var  RTPCodecParameters
		for ,  := range  {
			if .PayloadType == PayloadType() {
				 = codecMatchExact
				 = 

				break
			}
		}

		if  == codecMatchNone {
			for ,  := range  {
				if .PayloadType == PayloadType() {
					 = codecMatchPartial
					 = 

					break
				}
			}
		}

		if  == 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
		 := 
		if ,  := codecParametersFuzzySearch(, );  ==  {
			.SDPFmtpLine = strings.Replace(
				.SDPFmtpLine,
				fmt.Sprintf("apt=%d", ),
				fmt.Sprintf("apt=%d", .PayloadType),
				1,
			)
		}

		// if apt's media codec is partial match, then apt codec must be partial match too.
		,  := codecParametersFuzzySearch(, )
		if  == codecMatchExact &&  == codecMatchPartial {
			 = codecMatchPartial
		}

		return , , nil
	}

	,  := codecParametersFuzzySearch(, )

	return , , nil
}

// Update header extensions from a remote media section.
func ( *MediaEngine) ( *sdp.MediaDescription) error {
	var  RTPCodecType
	switch {
	case strings.EqualFold(.MediaName.Media, "audio"):
		 = RTPCodecTypeAudio
	case strings.EqualFold(.MediaName.Media, "video"):
		 = RTPCodecTypeVideo
	default:
		return nil
	}
	,  := rtpExtensionsFromMediaDescription()
	if  != nil {
		return 
	}

	for ,  := range  {
		if  = .updateHeaderExtension(, , );  != nil {
			return 
		}
	}

	return nil
}

// Look up a header extension and enable if it exists.
func ( *MediaEngine) ( int,  string,  RTPCodecType) error {
	if .negotiatedHeaderExtensions == nil {
		return nil
	}

	for ,  := range .headerExtensions {
		if .uri ==  {
			 := mediaEngineHeaderExtension{uri: , allowedDirections: .allowedDirections}
			if ,  := .negotiatedHeaderExtensions[];  {
				 = 
			}

			switch {
			case .isAudio &&  == RTPCodecTypeAudio:
				.isAudio = true
			case .isVideo &&  == RTPCodecTypeVideo:
				.isVideo = true
			}

			.negotiatedHeaderExtensions[] = 
		}
	}

	return nil
}

func ( *MediaEngine) ( []RTPCodecParameters,  RTPCodecType) error {
	var  error
	for ,  := range  {
		var  error
		if  == RTPCodecTypeAudio {
			.negotiatedAudioCodecs,  = .addCodec(.negotiatedAudioCodecs, )
		} else if  == RTPCodecTypeVideo {
			.negotiatedVideoCodecs,  = .addCodec(.negotiatedVideoCodecs, )
		}
		if  != nil {
			 = errors.Join(, )
		}
	}

	return 
}

// Update the MediaEngine from a remote description.
func ( *MediaEngine) ( sdp.SessionDescription) error { //nolint:cyclop,gocognit
	.mu.Lock()
	defer .mu.Unlock()

	for ,  := range .MediaDescriptions {
		var  RTPCodecType

		switch {
		case strings.EqualFold(.MediaName.Media, "audio"):
			 = RTPCodecTypeAudio
		case strings.EqualFold(.MediaName.Media, "video"):
			 = RTPCodecTypeVideo
		}

		switch {
		case !.negotiatedAudio &&  == RTPCodecTypeAudio:
			.negotiatedAudio = true
		case !.negotiatedVideo &&  == RTPCodecTypeVideo:
			.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  := .updateHeaderExtensionFromMediaSection();  != nil {
				return 
			}

			if !.negotiateMultiCodecs || ( != RTPCodecTypeAudio &&  != RTPCodecTypeVideo) {
				continue
			}
		}

		,  := codecsFromMediaDescription()
		if  != nil {
			return 
		}

		 := make([]RTPCodecParameters, 0, len())
		 := make([]RTPCodecParameters, 0, len())

		for ,  := range  {
			, ,  := .matchRemoteCodec(, , , )
			if  != nil {
				return 
			}

			.RTCPFeedback = rtcpFeedbackIntersection(.RTCPFeedback, .RTCPFeedback)

			if  == codecMatchExact {
				 = append(, )
			} else if  == codecMatchPartial {
				 = append(, )
			}
		}

		// use exact matches when they exist, otherwise fall back to partial
		switch {
		case len() > 0:
			 = .pushCodecs(, )
		case len() > 0:
			 = .pushCodecs(, )
		default:
			// no match, not negotiated
			continue
		}
		if  != nil {
			return 
		}

		if  := .updateHeaderExtensionFromMediaSection();  != nil {
			return 
		}
	}

	return nil
}

func ( *MediaEngine) ( RTPCodecType) []RTPCodecParameters {
	.mu.RLock()
	defer .mu.RUnlock()

	if  == RTPCodecTypeVideo {
		if .negotiatedVideo {
			return .negotiatedVideoCodecs
		}

		return .videoCodecs
	} else if  == RTPCodecTypeAudio {
		if .negotiatedAudio {
			return .negotiatedAudioCodecs
		}

		return .audioCodecs
	}

	return nil
}

//nolint:gocognit,cyclop
func ( *MediaEngine) ( RTPCodecType,  []RTPTransceiverDirection) RTPParameters {
	 := make([]RTPHeaderExtensionParameter, 0)

	// perform before locking to prevent recursive RLocks
	 := .getCodecsByKind()

	.mu.RLock()
	defer .mu.RUnlock()

	//nolint:nestif
	if (.negotiatedVideo &&  == RTPCodecTypeVideo) || (.negotiatedAudio &&  == RTPCodecTypeAudio) {
		for ,  := range .negotiatedHeaderExtensions {
			if haveRTPTransceiverDirectionIntersection(.allowedDirections, ) &&
				(.isAudio &&  == RTPCodecTypeAudio || .isVideo &&  == RTPCodecTypeVideo) {
				 = append(, RTPHeaderExtensionParameter{ID: , URI: .uri})
			}
		}
	} else {
		 := make(map[int]mediaEngineHeaderExtension)
		for ,  := range .headerExtensions {
			 := false
			for  := range .negotiatedHeaderExtensions {
				if .negotiatedHeaderExtensions[].uri == .uri {
					 = true
					[] = 

					break
				}
			}
			if ! {
				for  := 1;  < 15; ++ {
					 := true
					if ,  := [];  {
						 = false
					}
					if ,  := .negotiatedHeaderExtensions[];  && ! {
						[] = 

						break
					}
				}
			}
		}

		for ,  := range  {
			if haveRTPTransceiverDirectionIntersection(.allowedDirections, ) &&
				(.isAudio &&  == RTPCodecTypeAudio || .isVideo &&  == RTPCodecTypeVideo) {
				 = append(, RTPHeaderExtensionParameter{ID: , URI: .uri})
			}
		}
	}

	return RTPParameters{
		HeaderExtensions: ,
		Codecs:           ,
	}
}

func ( *MediaEngine) ( PayloadType) (RTPParameters, error) {
	, ,  := .getCodecByPayload()
	if  != nil {
		return RTPParameters{}, 
	}

	.mu.RLock()
	defer .mu.RUnlock()
	 := make([]RTPHeaderExtensionParameter, 0)
	for ,  := range .negotiatedHeaderExtensions {
		if .isAudio &&  == RTPCodecTypeAudio || .isVideo &&  == RTPCodecTypeVideo {
			 = append(, RTPHeaderExtensionParameter{ID: , URI: .uri})
		}
	}

	return RTPParameters{
		HeaderExtensions: ,
		Codecs:           []RTPCodecParameters{},
	}, nil
}

func payloaderForCodec( RTPCodecCapability) (rtp.Payloader, error) {
	switch strings.ToLower(.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 ( *MediaEngine) ( RTPCodecType,  []RTPTransceiverDirection) bool {
	for ,  := range .getRTPParametersByKind(, ).Codecs {
		if strings.EqualFold(.MimeType, MimeTypeRTX) {
			return true
		}
	}

	return false
}

func ( *MediaEngine) ( RTPCodecType,  []RTPTransceiverDirection) bool {
	for ,  := range .getRTPParametersByKind(, ).Codecs {
		if strings.Contains(strings.ToLower(.MimeType), MimeTypeFlexFEC) {
			return true
		}
	}

	return false
}