github.com/pion/webrtc/v3@v3.2.24/mediaengine.go (about) 1 // SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly> 2 // SPDX-License-Identifier: MIT 3 4 //go:build !js 5 // +build !js 6 7 package webrtc 8 9 import ( 10 "fmt" 11 "strconv" 12 "strings" 13 "sync" 14 "time" 15 16 "github.com/pion/rtp" 17 "github.com/pion/rtp/codecs" 18 "github.com/pion/sdp/v3" 19 "github.com/pion/webrtc/v3/internal/fmtp" 20 ) 21 22 const ( 23 // MimeTypeH264 H264 MIME type. 24 // Note: Matching should be case insensitive. 25 MimeTypeH264 = "video/H264" 26 // MimeTypeH265 H265 MIME type 27 // Note: Matching should be case insensitive. 28 MimeTypeH265 = "video/H265" 29 // MimeTypeOpus Opus MIME type 30 // Note: Matching should be case insensitive. 31 MimeTypeOpus = "audio/opus" 32 // MimeTypeVP8 VP8 MIME type 33 // Note: Matching should be case insensitive. 34 MimeTypeVP8 = "video/VP8" 35 // MimeTypeVP9 VP9 MIME type 36 // Note: Matching should be case insensitive. 37 MimeTypeVP9 = "video/VP9" 38 // MimeTypeAV1 AV1 MIME type 39 // Note: Matching should be case insensitive. 40 MimeTypeAV1 = "video/AV1" 41 // MimeTypeG722 G722 MIME type 42 // Note: Matching should be case insensitive. 43 MimeTypeG722 = "audio/G722" 44 // MimeTypePCMU PCMU MIME type 45 // Note: Matching should be case insensitive. 46 MimeTypePCMU = "audio/PCMU" 47 // MimeTypePCMA PCMA MIME type 48 // Note: Matching should be case insensitive. 49 MimeTypePCMA = "audio/PCMA" 50 ) 51 52 type mediaEngineHeaderExtension struct { 53 uri string 54 isAudio, isVideo bool 55 56 // If set only Transceivers of this direction are allowed 57 allowedDirections []RTPTransceiverDirection 58 } 59 60 // A MediaEngine defines the codecs supported by a PeerConnection, and the 61 // configuration of those codecs. 62 type MediaEngine struct { 63 // If we have attempted to negotiate a codec type yet. 64 negotiatedVideo, negotiatedAudio bool 65 66 videoCodecs, audioCodecs []RTPCodecParameters 67 negotiatedVideoCodecs, negotiatedAudioCodecs []RTPCodecParameters 68 69 headerExtensions []mediaEngineHeaderExtension 70 negotiatedHeaderExtensions map[int]mediaEngineHeaderExtension 71 72 mu sync.RWMutex 73 } 74 75 // RegisterDefaultCodecs registers the default codecs supported by Pion WebRTC. 76 // RegisterDefaultCodecs is not safe for concurrent use. 77 func (m *MediaEngine) RegisterDefaultCodecs() error { 78 // Default Pion Audio Codecs 79 for _, codec := range []RTPCodecParameters{ 80 { 81 RTPCodecCapability: RTPCodecCapability{MimeTypeOpus, 48000, 2, "minptime=10;useinbandfec=1", nil}, 82 PayloadType: 111, 83 }, 84 { 85 RTPCodecCapability: RTPCodecCapability{MimeTypeG722, 8000, 0, "", nil}, 86 PayloadType: 9, 87 }, 88 { 89 RTPCodecCapability: RTPCodecCapability{MimeTypePCMU, 8000, 0, "", nil}, 90 PayloadType: 0, 91 }, 92 { 93 RTPCodecCapability: RTPCodecCapability{MimeTypePCMA, 8000, 0, "", nil}, 94 PayloadType: 8, 95 }, 96 } { 97 if err := m.RegisterCodec(codec, RTPCodecTypeAudio); err != nil { 98 return err 99 } 100 } 101 102 videoRTCPFeedback := []RTCPFeedback{{"goog-remb", ""}, {"ccm", "fir"}, {"nack", ""}, {"nack", "pli"}} 103 for _, codec := range []RTPCodecParameters{ 104 { 105 RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", videoRTCPFeedback}, 106 PayloadType: 96, 107 }, 108 { 109 RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=96", nil}, 110 PayloadType: 97, 111 }, 112 113 { 114 RTPCodecCapability: RTPCodecCapability{MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f", videoRTCPFeedback}, 115 PayloadType: 102, 116 }, 117 { 118 RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=102", nil}, 119 PayloadType: 103, 120 }, 121 122 { 123 RTPCodecCapability: RTPCodecCapability{MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f", videoRTCPFeedback}, 124 PayloadType: 104, 125 }, 126 { 127 RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=104", nil}, 128 PayloadType: 105, 129 }, 130 131 { 132 RTPCodecCapability: RTPCodecCapability{MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", videoRTCPFeedback}, 133 PayloadType: 106, 134 }, 135 { 136 RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=106", nil}, 137 PayloadType: 107, 138 }, 139 140 { 141 RTPCodecCapability: RTPCodecCapability{MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f", videoRTCPFeedback}, 142 PayloadType: 108, 143 }, 144 { 145 RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=108", nil}, 146 PayloadType: 109, 147 }, 148 149 { 150 RTPCodecCapability: RTPCodecCapability{MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f", videoRTCPFeedback}, 151 PayloadType: 127, 152 }, 153 { 154 RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=127", nil}, 155 PayloadType: 125, 156 }, 157 158 { 159 RTPCodecCapability: RTPCodecCapability{MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=4d001f", videoRTCPFeedback}, 160 PayloadType: 39, 161 }, 162 { 163 RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=39", nil}, 164 PayloadType: 40, 165 }, 166 167 { 168 RTPCodecCapability: RTPCodecCapability{MimeTypeAV1, 90000, 0, "", videoRTCPFeedback}, 169 PayloadType: 45, 170 }, 171 { 172 RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=45", nil}, 173 PayloadType: 46, 174 }, 175 176 { 177 RTPCodecCapability: RTPCodecCapability{MimeTypeVP9, 90000, 0, "profile-id=0", videoRTCPFeedback}, 178 PayloadType: 98, 179 }, 180 { 181 RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=98", nil}, 182 PayloadType: 99, 183 }, 184 185 { 186 RTPCodecCapability: RTPCodecCapability{MimeTypeVP9, 90000, 0, "profile-id=2", videoRTCPFeedback}, 187 PayloadType: 100, 188 }, 189 { 190 RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=100", nil}, 191 PayloadType: 101, 192 }, 193 194 { 195 RTPCodecCapability: RTPCodecCapability{MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f", videoRTCPFeedback}, 196 PayloadType: 112, 197 }, 198 { 199 RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=112", nil}, 200 PayloadType: 113, 201 }, 202 } { 203 if err := m.RegisterCodec(codec, RTPCodecTypeVideo); err != nil { 204 return err 205 } 206 } 207 208 return nil 209 } 210 211 // addCodec will append codec if it not exists 212 func (m *MediaEngine) addCodec(codecs []RTPCodecParameters, codec RTPCodecParameters) []RTPCodecParameters { 213 for _, c := range codecs { 214 if c.MimeType == codec.MimeType && c.PayloadType == codec.PayloadType { 215 return codecs 216 } 217 } 218 return append(codecs, codec) 219 } 220 221 // RegisterCodec adds codec to the MediaEngine 222 // These are the list of codecs supported by this PeerConnection. 223 // RegisterCodec is not safe for concurrent use. 224 func (m *MediaEngine) RegisterCodec(codec RTPCodecParameters, typ RTPCodecType) error { 225 m.mu.Lock() 226 defer m.mu.Unlock() 227 228 codec.statsID = fmt.Sprintf("RTPCodec-%d", time.Now().UnixNano()) 229 switch typ { 230 case RTPCodecTypeAudio: 231 m.audioCodecs = m.addCodec(m.audioCodecs, codec) 232 case RTPCodecTypeVideo: 233 m.videoCodecs = m.addCodec(m.videoCodecs, codec) 234 default: 235 return ErrUnknownType 236 } 237 return nil 238 } 239 240 // RegisterHeaderExtension adds a header extension to the MediaEngine 241 // To determine the negotiated value use `GetHeaderExtensionID` after signaling is complete 242 func (m *MediaEngine) RegisterHeaderExtension(extension RTPHeaderExtensionCapability, typ RTPCodecType, allowedDirections ...RTPTransceiverDirection) error { 243 m.mu.Lock() 244 defer m.mu.Unlock() 245 246 if m.negotiatedHeaderExtensions == nil { 247 m.negotiatedHeaderExtensions = map[int]mediaEngineHeaderExtension{} 248 } 249 250 if len(allowedDirections) == 0 { 251 allowedDirections = []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly, RTPTransceiverDirectionSendonly} 252 } 253 254 for _, direction := range allowedDirections { 255 if direction != RTPTransceiverDirectionRecvonly && direction != RTPTransceiverDirectionSendonly { 256 return ErrRegisterHeaderExtensionInvalidDirection 257 } 258 } 259 260 extensionIndex := -1 261 for i := range m.headerExtensions { 262 if extension.URI == m.headerExtensions[i].uri { 263 extensionIndex = i 264 } 265 } 266 267 if extensionIndex == -1 { 268 m.headerExtensions = append(m.headerExtensions, mediaEngineHeaderExtension{}) 269 extensionIndex = len(m.headerExtensions) - 1 270 } 271 272 if typ == RTPCodecTypeAudio { 273 m.headerExtensions[extensionIndex].isAudio = true 274 } else if typ == RTPCodecTypeVideo { 275 m.headerExtensions[extensionIndex].isVideo = true 276 } 277 278 m.headerExtensions[extensionIndex].uri = extension.URI 279 m.headerExtensions[extensionIndex].allowedDirections = allowedDirections 280 281 return nil 282 } 283 284 // RegisterFeedback adds feedback mechanism to already registered codecs. 285 func (m *MediaEngine) RegisterFeedback(feedback RTCPFeedback, typ RTPCodecType) { 286 m.mu.Lock() 287 defer m.mu.Unlock() 288 289 switch typ { 290 case RTPCodecTypeVideo: 291 for i, v := range m.videoCodecs { 292 v.RTCPFeedback = append(v.RTCPFeedback, feedback) 293 m.videoCodecs[i] = v 294 } 295 case RTPCodecTypeAudio: 296 for i, v := range m.audioCodecs { 297 v.RTCPFeedback = append(v.RTCPFeedback, feedback) 298 m.audioCodecs[i] = v 299 } 300 } 301 } 302 303 // getHeaderExtensionID returns the negotiated ID for a header extension. 304 // If the Header Extension isn't enabled ok will be false 305 func (m *MediaEngine) getHeaderExtensionID(extension RTPHeaderExtensionCapability) (val int, audioNegotiated, videoNegotiated bool) { 306 m.mu.RLock() 307 defer m.mu.RUnlock() 308 309 if m.negotiatedHeaderExtensions == nil { 310 return 0, false, false 311 } 312 313 for id, h := range m.negotiatedHeaderExtensions { 314 if extension.URI == h.uri { 315 return id, h.isAudio, h.isVideo 316 } 317 } 318 319 return 320 } 321 322 // copy copies any user modifiable state of the MediaEngine 323 // all internal state is reset 324 func (m *MediaEngine) copy() *MediaEngine { 325 m.mu.Lock() 326 defer m.mu.Unlock() 327 cloned := &MediaEngine{ 328 videoCodecs: append([]RTPCodecParameters{}, m.videoCodecs...), 329 audioCodecs: append([]RTPCodecParameters{}, m.audioCodecs...), 330 headerExtensions: append([]mediaEngineHeaderExtension{}, m.headerExtensions...), 331 } 332 if len(m.headerExtensions) > 0 { 333 cloned.negotiatedHeaderExtensions = map[int]mediaEngineHeaderExtension{} 334 } 335 return cloned 336 } 337 338 func findCodecByPayload(codecs []RTPCodecParameters, payloadType PayloadType) *RTPCodecParameters { 339 for _, codec := range codecs { 340 if codec.PayloadType == payloadType { 341 return &codec 342 } 343 } 344 return nil 345 } 346 347 func (m *MediaEngine) getCodecByPayload(payloadType PayloadType) (RTPCodecParameters, RTPCodecType, error) { 348 m.mu.RLock() 349 defer m.mu.RUnlock() 350 351 // if we've negotiated audio or video, check the negotiated types before our 352 // built-in payload types, to ensure we pick the codec the other side wants. 353 if m.negotiatedVideo { 354 if codec := findCodecByPayload(m.negotiatedVideoCodecs, payloadType); codec != nil { 355 return *codec, RTPCodecTypeVideo, nil 356 } 357 } 358 if m.negotiatedAudio { 359 if codec := findCodecByPayload(m.negotiatedAudioCodecs, payloadType); codec != nil { 360 return *codec, RTPCodecTypeAudio, nil 361 } 362 } 363 if !m.negotiatedVideo { 364 if codec := findCodecByPayload(m.videoCodecs, payloadType); codec != nil { 365 return *codec, RTPCodecTypeVideo, nil 366 } 367 } 368 if !m.negotiatedAudio { 369 if codec := findCodecByPayload(m.audioCodecs, payloadType); codec != nil { 370 return *codec, RTPCodecTypeAudio, nil 371 } 372 } 373 374 return RTPCodecParameters{}, 0, ErrCodecNotFound 375 } 376 377 func (m *MediaEngine) collectStats(collector *statsReportCollector) { 378 m.mu.RLock() 379 defer m.mu.RUnlock() 380 381 statsLoop := func(codecs []RTPCodecParameters) { 382 for _, codec := range codecs { 383 collector.Collecting() 384 stats := CodecStats{ 385 Timestamp: statsTimestampFrom(time.Now()), 386 Type: StatsTypeCodec, 387 ID: codec.statsID, 388 PayloadType: codec.PayloadType, 389 MimeType: codec.MimeType, 390 ClockRate: codec.ClockRate, 391 Channels: uint8(codec.Channels), 392 SDPFmtpLine: codec.SDPFmtpLine, 393 } 394 395 collector.Collect(stats.ID, stats) 396 } 397 } 398 399 statsLoop(m.videoCodecs) 400 statsLoop(m.audioCodecs) 401 } 402 403 // Look up a codec and enable if it exists 404 func (m *MediaEngine) matchRemoteCodec(remoteCodec RTPCodecParameters, typ RTPCodecType, exactMatches, partialMatches []RTPCodecParameters) (codecMatchType, error) { 405 codecs := m.videoCodecs 406 if typ == RTPCodecTypeAudio { 407 codecs = m.audioCodecs 408 } 409 410 remoteFmtp := fmtp.Parse(remoteCodec.RTPCodecCapability.MimeType, remoteCodec.RTPCodecCapability.SDPFmtpLine) 411 if apt, hasApt := remoteFmtp.Parameter("apt"); hasApt { 412 payloadType, err := strconv.ParseUint(apt, 10, 8) 413 if err != nil { 414 return codecMatchNone, err 415 } 416 417 aptMatch := codecMatchNone 418 for _, codec := range exactMatches { 419 if codec.PayloadType == PayloadType(payloadType) { 420 aptMatch = codecMatchExact 421 break 422 } 423 } 424 425 if aptMatch == codecMatchNone { 426 for _, codec := range partialMatches { 427 if codec.PayloadType == PayloadType(payloadType) { 428 aptMatch = codecMatchPartial 429 break 430 } 431 } 432 } 433 434 if aptMatch == codecMatchNone { 435 return codecMatchNone, nil // not an error, we just ignore this codec we don't support 436 } 437 438 // if apt's media codec is partial match, then apt codec must be partial match too 439 _, matchType := codecParametersFuzzySearch(remoteCodec, codecs) 440 if matchType == codecMatchExact && aptMatch == codecMatchPartial { 441 matchType = codecMatchPartial 442 } 443 return matchType, nil 444 } 445 446 _, matchType := codecParametersFuzzySearch(remoteCodec, codecs) 447 return matchType, nil 448 } 449 450 // Look up a header extension and enable if it exists 451 func (m *MediaEngine) updateHeaderExtension(id int, extension string, typ RTPCodecType) error { 452 if m.negotiatedHeaderExtensions == nil { 453 return nil 454 } 455 456 for _, localExtension := range m.headerExtensions { 457 if localExtension.uri == extension { 458 h := mediaEngineHeaderExtension{uri: extension, allowedDirections: localExtension.allowedDirections} 459 if existingValue, ok := m.negotiatedHeaderExtensions[id]; ok { 460 h = existingValue 461 } 462 463 switch { 464 case localExtension.isAudio && typ == RTPCodecTypeAudio: 465 h.isAudio = true 466 case localExtension.isVideo && typ == RTPCodecTypeVideo: 467 h.isVideo = true 468 } 469 470 m.negotiatedHeaderExtensions[id] = h 471 } 472 } 473 return nil 474 } 475 476 func (m *MediaEngine) pushCodecs(codecs []RTPCodecParameters, typ RTPCodecType) { 477 for _, codec := range codecs { 478 if typ == RTPCodecTypeAudio { 479 m.negotiatedAudioCodecs = m.addCodec(m.negotiatedAudioCodecs, codec) 480 } else if typ == RTPCodecTypeVideo { 481 m.negotiatedVideoCodecs = m.addCodec(m.negotiatedVideoCodecs, codec) 482 } 483 } 484 } 485 486 // Update the MediaEngine from a remote description 487 func (m *MediaEngine) updateFromRemoteDescription(desc sdp.SessionDescription) error { 488 m.mu.Lock() 489 defer m.mu.Unlock() 490 491 for _, media := range desc.MediaDescriptions { 492 var typ RTPCodecType 493 switch { 494 case !m.negotiatedAudio && strings.EqualFold(media.MediaName.Media, "audio"): 495 m.negotiatedAudio = true 496 typ = RTPCodecTypeAudio 497 case !m.negotiatedVideo && strings.EqualFold(media.MediaName.Media, "video"): 498 m.negotiatedVideo = true 499 typ = RTPCodecTypeVideo 500 default: 501 continue 502 } 503 504 codecs, err := codecsFromMediaDescription(media) 505 if err != nil { 506 return err 507 } 508 509 exactMatches := make([]RTPCodecParameters, 0, len(codecs)) 510 partialMatches := make([]RTPCodecParameters, 0, len(codecs)) 511 512 for _, codec := range codecs { 513 matchType, mErr := m.matchRemoteCodec(codec, typ, exactMatches, partialMatches) 514 if mErr != nil { 515 return mErr 516 } 517 518 if matchType == codecMatchExact { 519 exactMatches = append(exactMatches, codec) 520 } else if matchType == codecMatchPartial { 521 partialMatches = append(partialMatches, codec) 522 } 523 } 524 525 // use exact matches when they exist, otherwise fall back to partial 526 switch { 527 case len(exactMatches) > 0: 528 m.pushCodecs(exactMatches, typ) 529 case len(partialMatches) > 0: 530 m.pushCodecs(partialMatches, typ) 531 default: 532 // no match, not negotiated 533 continue 534 } 535 536 extensions, err := rtpExtensionsFromMediaDescription(media) 537 if err != nil { 538 return err 539 } 540 541 for extension, id := range extensions { 542 if err = m.updateHeaderExtension(id, extension, typ); err != nil { 543 return err 544 } 545 } 546 } 547 return nil 548 } 549 550 func (m *MediaEngine) getCodecsByKind(typ RTPCodecType) []RTPCodecParameters { 551 m.mu.RLock() 552 defer m.mu.RUnlock() 553 554 if typ == RTPCodecTypeVideo { 555 if m.negotiatedVideo { 556 return m.negotiatedVideoCodecs 557 } 558 559 return m.videoCodecs 560 } else if typ == RTPCodecTypeAudio { 561 if m.negotiatedAudio { 562 return m.negotiatedAudioCodecs 563 } 564 565 return m.audioCodecs 566 } 567 568 return nil 569 } 570 571 func (m *MediaEngine) getRTPParametersByKind(typ RTPCodecType, directions []RTPTransceiverDirection) RTPParameters { //nolint:gocognit 572 headerExtensions := make([]RTPHeaderExtensionParameter, 0) 573 574 // perform before locking to prevent recursive RLocks 575 foundCodecs := m.getCodecsByKind(typ) 576 577 m.mu.RLock() 578 defer m.mu.RUnlock() 579 if m.negotiatedVideo && typ == RTPCodecTypeVideo || 580 m.negotiatedAudio && typ == RTPCodecTypeAudio { 581 for id, e := range m.negotiatedHeaderExtensions { 582 if haveRTPTransceiverDirectionIntersection(e.allowedDirections, directions) && (e.isAudio && typ == RTPCodecTypeAudio || e.isVideo && typ == RTPCodecTypeVideo) { 583 headerExtensions = append(headerExtensions, RTPHeaderExtensionParameter{ID: id, URI: e.uri}) 584 } 585 } 586 } else { 587 mediaHeaderExtensions := make(map[int]mediaEngineHeaderExtension) 588 for _, e := range m.headerExtensions { 589 usingNegotiatedID := false 590 for id := range m.negotiatedHeaderExtensions { 591 if m.negotiatedHeaderExtensions[id].uri == e.uri { 592 usingNegotiatedID = true 593 mediaHeaderExtensions[id] = e 594 break 595 } 596 } 597 if !usingNegotiatedID { 598 for id := 1; id < 15; id++ { 599 idAvailable := true 600 if _, ok := mediaHeaderExtensions[id]; ok { 601 idAvailable = false 602 } 603 if _, taken := m.negotiatedHeaderExtensions[id]; idAvailable && !taken { 604 mediaHeaderExtensions[id] = e 605 break 606 } 607 } 608 } 609 } 610 611 for id, e := range mediaHeaderExtensions { 612 if haveRTPTransceiverDirectionIntersection(e.allowedDirections, directions) && (e.isAudio && typ == RTPCodecTypeAudio || e.isVideo && typ == RTPCodecTypeVideo) { 613 headerExtensions = append(headerExtensions, RTPHeaderExtensionParameter{ID: id, URI: e.uri}) 614 } 615 } 616 } 617 618 return RTPParameters{ 619 HeaderExtensions: headerExtensions, 620 Codecs: foundCodecs, 621 } 622 } 623 624 func (m *MediaEngine) getRTPParametersByPayloadType(payloadType PayloadType) (RTPParameters, error) { 625 codec, typ, err := m.getCodecByPayload(payloadType) 626 if err != nil { 627 return RTPParameters{}, err 628 } 629 630 m.mu.RLock() 631 defer m.mu.RUnlock() 632 headerExtensions := make([]RTPHeaderExtensionParameter, 0) 633 for id, e := range m.negotiatedHeaderExtensions { 634 if e.isAudio && typ == RTPCodecTypeAudio || e.isVideo && typ == RTPCodecTypeVideo { 635 headerExtensions = append(headerExtensions, RTPHeaderExtensionParameter{ID: id, URI: e.uri}) 636 } 637 } 638 639 return RTPParameters{ 640 HeaderExtensions: headerExtensions, 641 Codecs: []RTPCodecParameters{codec}, 642 }, nil 643 } 644 645 func payloaderForCodec(codec RTPCodecCapability) (rtp.Payloader, error) { 646 switch strings.ToLower(codec.MimeType) { 647 case strings.ToLower(MimeTypeH264): 648 return &codecs.H264Payloader{}, nil 649 case strings.ToLower(MimeTypeOpus): 650 return &codecs.OpusPayloader{}, nil 651 case strings.ToLower(MimeTypeVP8): 652 return &codecs.VP8Payloader{ 653 EnablePictureID: true, 654 }, nil 655 case strings.ToLower(MimeTypeVP9): 656 return &codecs.VP9Payloader{}, nil 657 case strings.ToLower(MimeTypeAV1): 658 return &codecs.AV1Payloader{}, nil 659 case strings.ToLower(MimeTypeG722): 660 return &codecs.G722Payloader{}, nil 661 case strings.ToLower(MimeTypePCMU), strings.ToLower(MimeTypePCMA): 662 return &codecs.G711Payloader{}, nil 663 default: 664 return nil, ErrNoPayloaderForCodec 665 } 666 }