[Pion/WebRTC][TypeScript] Use specific video codec
Tags: go, typescript, pion, webrtc
Published: 2023-05-17 16:46:57

## Intro
At least in my environment, "VP8" is used as the default video codec for WebRTC.
But according to the SDP what I read last time, other codecs should be available as well.

* [[Pion/WebRTC] Try reading Offer messages](https://dev.to/masanori_msl/pionwebrtc-try-reading-offer-messages-4nho)

So in this time, I will try using specific video codec.

Because the video codec used by WebRTC is determined by the SDP of both the Offer and Answer, I will try server-side(Offer) and client-side(Answer)/

## Sample
* [webappsample - GitHub](https://github.com/masanori840816/webappsample)

## [Client-side] Remove other video codecs from Offer SDP
To force using specific video codec, I can remove other codecs from Offer SDP.

#### videoCodecRemover.ts
```ts
import { hasAnyTexts } from "../hasAnyTexts";

/** Remove video codecs from SDP except specified ones */
export function removeVideoCodec(orgsdp: string, leaveMimeType: string): string {
    if(!hasAnyTexts(leaveMimeType)) {
        return orgsdp;
    }
    const codecs = RTCRtpSender.getCapabilities("video")?.codecs;
    if(codecs == null) {
        return orgsdp;
    }
    let modsdp = orgsdp;
    for(const c of codecs) {
        if(c.mimeType === leaveMimeType) {
            continue;
        }
        modsdp = internalRemoveVideoCodec(modsdp, c.mimeType);
    }    
    return modsdp;
}
/** Remove target MIME Type from SDP */
function internalRemoveVideoCodec(sdp: string, removeMimeType: string): string {
    const videoCodec = removeMimeType.replace("video/", "");    
    const codecre = new RegExp('(a=rtpmap:(\\d*) ' + videoCodec + '/90000\\r\\n)');
    const rtpmaps = sdp.match(codecre);        
    if (rtpmaps == null || rtpmaps.length <= 2) {
        return sdp;
    }
    const rtpmap = rtpmaps[2];
    let modsdp = sdp.replace(codecre, "");
    modsdp = modsdp.replace(new RegExp('(a=rtcp-fb:' + rtpmap + '.*\r\n)', 'g'), "");
    modsdp = modsdp.replace(new RegExp('(a=fmtp:' + rtpmap + '.*\r\n)', 'g'), "");
    const aptpre = new RegExp('(a=fmtp:(\\d*) apt=' + rtpmap + '\\r\\n)');
    const aptmaps = modsdp.match(aptpre);
    let fmtpmap = "";
    if (aptmaps != null && aptmaps.length >= 3) {
        fmtpmap = aptmaps[2] ?? "";
        modsdp = modsdp.replace(aptpre, "");
        // remove video codecs by the "fmtpmap" value.
        modsdp = modsdp.replace(new RegExp('(a=rtpmap:' + fmtpmap + '.*\r\n)', 'g'), "");
        modsdp = modsdp.replace(new RegExp('(a=rtcp-fb:' + fmtpmap + '.*\r\n)', 'g'), "");
    }
    const videore = /(m=video.*\r\n)/;
    const videolines = modsdp.match(videore);
    if (videolines != null) {
        //If many m=video are found in SDP, this program doesn't work.
        const videoline = videolines[0].substring(0, videolines[0].length - 2);
        const videoelem = videoline.split(" ");
        let modvideoline = videoelem[0] ?? "";
        for (let i = 1; i < videoelem.length; i++) {
            if (videoelem[i] == rtpmap || videoelem[i] == fmtpmap) {
                continue;
            }
            modvideoline += " " + videoelem[i];
        }
        modvideoline += "\r\n";
        modsdp = modsdp.replace(videore, modvideoline);
    }
    return internalRemoveVideoCodec(modsdp, removeMimeType);
}
```
#### main.page.ts
```ts
...
function handleReceivedMessage(value: string) {
    const message = JSON.parse(value);
    if(!checkIsClientMessage(message)) {
        console.error(`Invalid message type ${value}`);
        return;
    }
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    let offerData: any;
    let preferredMimeType: string|null = null;
    switch(message.event) {
...
        case "offer":
            offerData = JSON.parse(message.data);
            if(view.getForceVideoCodec()) {
                // 1. SDP, 2. MIME Type
                offerData.sdp = removeVideoCodec(offerData.sdp, view.getPreferredVideoCodec());
            } else {
...
            }       
            if(webrtc.handleOffer(offerData, preferredMimeType) !== true) {
                close();
            }
            break;
...
    }
}
...
```
#### Before
```log
...
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 123 118 116
c=IN IP4 0.0.0.0
a=setup:actpass
a=mid:0
a=ice-ufrag:qTksQATCTrhvEAcY
a=ice-pwd:dGLtcbyrUNKsdYxHDEjVuYgwUWdMqqkN
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:96 VP8/90000
a=rtcp-fb:96 goog-remb 
a=rtcp-fb:96 ccm fir
a=rtcp-fb:96 nack 
a=rtcp-fb:96 nack pli
a=rtcp-fb:96 nack 
a=rtcp-fb:96 nack pli
a=rtcp-fb:96 transport-cc 
a=rtpmap:97 rtx/90000
a=fmtp:97 apt=96
a=rtcp-fb:97 nack 
a=rtcp-fb:97 nack pli
a=rtcp-fb:97 transport-cc 
a=rtpmap:98 VP9/90000
a=fmtp:98 profile-id=0
a=rtcp-fb:98 goog-remb 
a=rtcp-fb:98 ccm fir
a=rtcp-fb:98 nack 
a=rtcp-fb:98 nack pli
a=rtcp-fb:98 nack 
a=rtcp-fb:98 nack pli
a=rtcp-fb:98 transport-cc 
a=rtpmap:99 rtx/90000
a=fmtp:99 apt=98
a=rtcp-fb:99 nack 
a=rtcp-fb:99 nack pli
a=rtcp-fb:99 transport-cc 
a=rtpmap:100 VP9/90000
a=fmtp:100 profile-id=1
a=rtcp-fb:100 goog-remb 
a=rtcp-fb:100 ccm fir
a=rtcp-fb:100 nack 
a=rtcp-fb:100 nack pli
a=rtcp-fb:100 nack 
a=rtcp-fb:100 nack pli
a=rtcp-fb:100 transport-cc 
a=rtpmap:101 rtx/90000
a=fmtp:101 apt=100
a=rtcp-fb:101 nack 
a=rtcp-fb:101 nack pli
a=rtcp-fb:101 transport-cc 
a=rtpmap:102 H264/90000
a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
a=rtcp-fb:102 goog-remb 
a=rtcp-fb:102 ccm fir
a=rtcp-fb:102 nack 
a=rtcp-fb:102 nack pli
a=rtcp-fb:102 nack 
a=rtcp-fb:102 nack pli
a=rtcp-fb:102 transport-cc 
a=rtpmap:121 rtx/90000
a=fmtp:121 apt=102
a=rtcp-fb:121 nack 
a=rtcp-fb:121 nack pli
a=rtcp-fb:121 transport-cc 
a=rtpmap:127 H264/90000
a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f
a=rtcp-fb:127 goog-remb 
a=rtcp-fb:127 ccm fir
a=rtcp-fb:127 nack 
a=rtcp-fb:127 nack pli
a=rtcp-fb:127 nack 
a=rtcp-fb:127 nack pli
a=rtcp-fb:127 transport-cc 
a=rtpmap:120 rtx/90000
a=fmtp:120 apt=127
a=rtcp-fb:120 nack 
a=rtcp-fb:120 nack pli
a=rtcp-fb:120 transport-cc 
a=rtpmap:125 H264/90000
a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=rtcp-fb:125 goog-remb 
a=rtcp-fb:125 ccm fir
a=rtcp-fb:125 nack 
a=rtcp-fb:125 nack pli
a=rtcp-fb:125 nack 
a=rtcp-fb:125 nack pli
a=rtcp-fb:125 transport-cc 
a=rtpmap:107 rtx/90000
a=fmtp:107 apt=125
a=rtcp-fb:107 nack 
a=rtcp-fb:107 nack pli
a=rtcp-fb:107 transport-cc 
a=rtpmap:108 H264/90000
a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f
a=rtcp-fb:108 goog-remb 
a=rtcp-fb:108 ccm fir
a=rtcp-fb:108 nack 
a=rtcp-fb:108 nack pli
a=rtcp-fb:108 nack 
a=rtcp-fb:108 nack pli
a=rtcp-fb:108 transport-cc 
a=rtpmap:109 rtx/90000
a=fmtp:109 apt=108
a=rtcp-fb:109 nack 
a=rtcp-fb:109 nack pli
a=rtcp-fb:109 transport-cc 
a=rtpmap:123 H264/90000
a=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032
a=rtcp-fb:123 goog-remb 
a=rtcp-fb:123 ccm fir
a=rtcp-fb:123 nack 
a=rtcp-fb:123 nack pli
a=rtcp-fb:123 nack 
a=rtcp-fb:123 nack pli
a=rtcp-fb:123 transport-cc 
a=rtpmap:118 rtx/90000
a=fmtp:118 apt=123
a=rtcp-fb:118 nack 
a=rtcp-fb:118 nack pli
a=rtcp-fb:118 transport-cc 
a=rtpmap:116 ulpfec/90000
a=rtcp-fb:116 nack 
a=rtcp-fb:116 nack pli
a=rtcp-fb:116 transport-cc 
a=extmap:1 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=recvonly
...
```

#### After
```log
...
m=video 9 UDP/TLS/RTP/SAVPF 102 127 125 108 123
c=IN IP4 0.0.0.0
a=setup:actpass
a=mid:0
a=ice-ufrag:qTksQATCTrhvEAcY
a=ice-pwd:dGLtcbyrUNKsdYxHDEjVuYgwUWdMqqkN
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:102 H264/90000
a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
a=rtcp-fb:102 goog-remb 
a=rtcp-fb:102 ccm fir
a=rtcp-fb:102 nack 
a=rtcp-fb:102 nack pli
a=rtcp-fb:102 nack 
a=rtcp-fb:102 nack pli
a=rtcp-fb:102 transport-cc 
a=rtpmap:127 H264/90000
a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f
a=rtcp-fb:127 goog-remb 
a=rtcp-fb:127 ccm fir
a=rtcp-fb:127 nack 
a=rtcp-fb:127 nack pli
a=rtcp-fb:127 nack 
a=rtcp-fb:127 nack pli
a=rtcp-fb:127 transport-cc 
a=rtpmap:125 H264/90000
a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=rtcp-fb:125 goog-remb 
a=rtcp-fb:125 ccm fir
a=rtcp-fb:125 nack 
a=rtcp-fb:125 nack pli
a=rtcp-fb:125 nack 
a=rtcp-fb:125 nack pli
a=rtcp-fb:125 transport-cc 
a=rtpmap:108 H264/90000
a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f
a=rtcp-fb:108 goog-remb 
a=rtcp-fb:108 ccm fir
a=rtcp-fb:108 nack 
a=rtcp-fb:108 nack pli
a=rtcp-fb:108 nack 
a=rtcp-fb:108 nack pli
a=rtcp-fb:108 transport-cc 
a=rtpmap:123 H264/90000
a=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032
a=rtcp-fb:123 goog-remb 
a=rtcp-fb:123 ccm fir
a=rtcp-fb:123 nack 
a=rtcp-fb:123 nack pli
a=rtcp-fb:123 nack 
a=rtcp-fb:123 nack pli
a=rtcp-fb:123 transport-cc 
a=extmap:1 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=recvonly
...
```

* [How to remove video codecs in webrtc sdp?](https://stackoverflow.com/questions/52738290/how-to-remove-video-codecs-in-webrtc-sdp)

This method works, but has problems.
If another client is already connected with a different video codec, the connection will fail.


## [Client-side] setCodecPreferences
Because ghe priority of the video codecs used matches the order of description in SDP, I sort them by "setCodePreferences"

* [RTCRtpTransceiver: setCodecPreferences() method - MDN](https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpTransceiver/setCodecPreferences)
* [Codecs used by WebRTC - MDN](https://developer.mozilla.org/en-US/docs/Web/Media/Formats/WebRTC_codecs)

#### preferVideoCodec.ts
```ts
import { hasAnyTexts } from "ts/hasAnyTexts";

/** Prefer Specified Video Codec */
export function preferVideoCodec(codecs: RTCRtpCodecCapability[], mimeType: string): RTCRtpCodecCapability[] {
    // skip sorting video codecs if the mimeType is empty.
    if(!hasAnyTexts(mimeType)) {
        return codecs;
    }
    const otherCodecs: RTCRtpCodecCapability[] = [];
    const sortedCodecs: RTCRtpCodecCapability[] = [];
    codecs.forEach((codec) => {
      if (codec.mimeType === mimeType) {
        sortedCodecs.push(codec);
      } else {
        otherCodecs.push(codec);
      }
    });  
    return sortedCodecs.concat(otherCodecs);
}
```

#### webrtc.controller.ts
```ts
...
public handleOffer(data: RTCSessionDescription | null | undefined, preferredMimeType: string|null): boolean {
...
        this.peerConnection.setRemoteDescription(data);
        // prefer specific video codec before calling createAnswer
        if(hasAnyTexts(preferredMimeType)) {
            this.peerConnection.getTransceivers()
            .forEach((transceiver) => {
                const kind = transceiver.sender.track?.kind;
                if(kind == null) {
                    return;
                }
                if (kind === "video") {
                    let codecs = RTCRtpSender.getCapabilities(kind)?.codecs;
                    if(codecs == null) {
                        return;
                    }
                    codecs = preferVideoCodec(codecs, preferredMimeType);                    
                    transceiver.setCodecPreferences([...codecs, ...codecs]);
                }
            });
        }
        this.peerConnection.createAnswer()
            .then(answer => {
                if (this.peerConnection != null) {
                    this.peerConnection.setLocalDescription(answer);
                }
                if (this.answerSentEvent != null) {
                    this.answerSentEvent(answer);
                }
            });
        return true;
    }
...
```

#### After(Answer SDP, I select H264)
```log
...
m=video 60274 UDP/TLS/RTP/SAVPF 102 121 127 120 125 107 108 109 123 118 96 97 98 99 116
c=IN IP4 60.46.180.81
a=rtcp:9 IN IP4 0.0.0.0
a=candidate:4106528496 1 udp 2122260223 192.168.86.22 60274 typ host generation 0 network-id 1 network-cost 10
a=candidate:801605297 1 udp 1686052607 60.46.180.81 60274 typ srflx raddr 192.168.86.22 rport 60274 generation 0 network-id 1 network-cost 10
a=candidate:2321187837 1 tcp 1518280447 192.168.86.22 9 typ host tcptype active generation 0 network-id 1 network-cost 10
a=ice-ufrag:1jxi
a=ice-pwd:6y/munHjivGnsDyd+lTRNimE
a=ice-options:trickle
a=fingerprint:sha-256 BB:4B:E6:D0:EE:59:B3:1D:1C:F1:50:59:C6:4E:C7:92:57:94:AF:46:D9:42:1E:6D:5C:EE:CB:69:3D:72:AE:02
a=setup:active
a=mid:0
a=extmap:1 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=sendonly
a=msid:67076c8b-0491-43fc-b940-66433c9e00bd c98ca17c-7198-495a-90e3-d122c57be656
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:102 H264/90000
a=rtcp-fb:102 goog-remb
a=rtcp-fb:102 ccm fir
a=rtcp-fb:102 nack
a=rtcp-fb:102 nack pli
a=rtcp-fb:102 transport-cc
a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
a=rtpmap:121 rtx/90000
a=rtcp-fb:121 nack
a=rtcp-fb:121 nack pli
a=rtcp-fb:121 transport-cc
a=fmtp:121 apt=102
a=rtpmap:127 H264/90000
a=rtcp-fb:127 goog-remb
a=rtcp-fb:127 ccm fir
a=rtcp-fb:127 nack
a=rtcp-fb:127 nack pli
a=rtcp-fb:127 transport-cc
a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f
a=rtpmap:120 rtx/90000
a=rtcp-fb:120 nack
a=rtcp-fb:120 nack pli
a=rtcp-fb:120 transport-cc
a=fmtp:120 apt=127
a=rtpmap:125 H264/90000
a=rtcp-fb:125 goog-remb
a=rtcp-fb:125 ccm fir
a=rtcp-fb:125 nack
a=rtcp-fb:125 nack pli
a=rtcp-fb:125 transport-cc
a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=rtpmap:107 rtx/90000
a=rtcp-fb:107 nack
a=rtcp-fb:107 nack pli
a=rtcp-fb:107 transport-cc
a=fmtp:107 apt=125
a=rtpmap:108 H264/90000
a=rtcp-fb:108 goog-remb
a=rtcp-fb:108 ccm fir
a=rtcp-fb:108 nack
a=rtcp-fb:108 nack pli
a=rtcp-fb:108 transport-cc
a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f
a=rtpmap:109 rtx/90000
a=rtcp-fb:109 nack
a=rtcp-fb:109 nack pli
a=rtcp-fb:109 transport-cc
a=fmtp:109 apt=108
a=rtpmap:123 H264/90000
a=rtcp-fb:123 goog-remb
a=rtcp-fb:123 ccm fir
a=rtcp-fb:123 nack
a=rtcp-fb:123 nack pli
a=rtcp-fb:123 transport-cc
a=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032
a=rtpmap:118 rtx/90000
a=rtcp-fb:118 nack
a=rtcp-fb:118 nack pli
a=rtcp-fb:118 transport-cc
a=fmtp:118 apt=123
a=rtpmap:96 VP8/90000
a=rtcp-fb:96 goog-remb
a=rtcp-fb:96 ccm fir
a=rtcp-fb:96 nack
a=rtcp-fb:96 nack pli
a=rtcp-fb:96 transport-cc
a=rtpmap:97 rtx/90000
a=rtcp-fb:97 nack
a=rtcp-fb:97 nack pli
a=rtcp-fb:97 transport-cc
a=fmtp:97 apt=96
a=rtpmap:98 VP9/90000
a=rtcp-fb:98 goog-remb
a=rtcp-fb:98 ccm fir
a=rtcp-fb:98 nack
a=rtcp-fb:98 nack pli
a=rtcp-fb:98 transport-cc
a=fmtp:98 profile-id=0
a=rtpmap:99 rtx/90000
a=rtcp-fb:99 nack
a=rtcp-fb:99 nack pli
a=rtcp-fb:99 transport-cc
a=fmtp:99 apt=98
a=rtpmap:116 ulpfec/90000
a=rtcp-fb:116 nack
a=rtcp-fb:116 nack pli
a=rtcp-fb:116 transport-cc
a=ssrc-group:FID 1751253213 3231317468
a=ssrc:1751253213 cname:TI63w5WXK1xmxz8A
a=ssrc:1751253213 msid:67076c8b-0491-43fc-b940-66433c9e00bd c98ca17c-7198-495a-90e3-d122c57be656
a=ssrc:3231317468 cname:TI63w5WXK1xmxz8A
a=ssrc:3231317468 msid:67076c8b-0491-43fc-b940-66433c9e00bd c98ca17c-7198-495a-90e3-d122c57be656
...
```
## [Server-side] setCodecPreferences
Pion/WebRTC also has "setCodecPreferences".

So I can sort video codecs for Offer SDP as same as the client-side.

#### peerConnectionState.go
```go
...
func NewPeerConnection() (*webrtc.PeerConnection, error) {
	peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{
		ICEServers: []webrtc.ICEServer{
			{
				URLs: []string{
					"stun:stun.l.google.com:19302",
				},
			},
		},
	})
	if err != nil {
		return nil, err
	}
	for _, typ := range []webrtc.RTPCodecType{webrtc.RTPCodecTypeVideo, webrtc.RTPCodecTypeAudio} {
		if _, err := peerConnection.AddTransceiverFromKind(typ, webrtc.RTPTransceiverInit{
			Direction: webrtc.RTPTransceiverDirectionRecvonly,
		}); err != nil {
			return nil, err
		}
	}
	for _, trn := range peerConnection.GetTransceivers() {
		if trn.Kind() == webrtc.RTPCodecTypeVideo {
			// prefer "H264" for example
			preferVideoCodec(trn, "video/H264")
		}
	}
	return peerConnection, nil
}
...
func preferVideoCodec(trn *webrtc.RTPTransceiver, mimeType string) {
	originalCodecs := trn.Receiver().GetParameters().Codecs
	sortedCodecs := make([]webrtc.RTPCodecParameters, len(originalCodecs))
	index := 0
	for _, codec := range originalCodecs {
		if codec.MimeType == mimeType {
			sortedCodecs[index] = codec
			index += 1
		}
	}
	for _, codec := range originalCodecs {
		if codec.MimeType != mimeType {
			sortedCodecs[index] = codec
			index += 1
		}
	}
	trn.SetCodecPreferences(sortedCodecs)
}
```

#### After(Offer SDP)
```log
...
m=video 9 UDP/TLS/RTP/SAVPF 102 127 125 108 123 96 97 98 99 100 101 121 120 107 109 118 116
c=IN IP4 0.0.0.0
a=setup:actpass
a=mid:0
a=ice-ufrag:psvRzwNhTPFgjkEt
a=ice-pwd:VdywVCOKklKxZknlYRpLusBpjaTxOneN
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:102 H264/90000
a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
a=rtcp-fb:102 goog-remb 
a=rtcp-fb:102 ccm fir
a=rtcp-fb:102 nack 
a=rtcp-fb:102 nack pli
a=rtcp-fb:102 nack 
a=rtcp-fb:102 nack pli
a=rtcp-fb:102 transport-cc 
a=rtpmap:127 H264/90000
a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f
a=rtcp-fb:127 goog-remb 
a=rtcp-fb:127 ccm fir
a=rtcp-fb:127 nack 
a=rtcp-fb:127 nack pli
a=rtcp-fb:127 nack 
a=rtcp-fb:127 nack pli
a=rtcp-fb:127 transport-cc 
a=rtpmap:125 H264/90000
a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=rtcp-fb:125 goog-remb 
a=rtcp-fb:125 ccm fir
a=rtcp-fb:125 nack 
a=rtcp-fb:125 nack pli
a=rtcp-fb:125 nack 
a=rtcp-fb:125 nack pli
a=rtcp-fb:125 transport-cc 
a=rtpmap:108 H264/90000
a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f
a=rtcp-fb:108 goog-remb 
a=rtcp-fb:108 ccm fir
a=rtcp-fb:108 nack 
a=rtcp-fb:108 nack pli
a=rtcp-fb:108 nack 
a=rtcp-fb:108 nack pli
a=rtcp-fb:108 transport-cc 
a=rtpmap:123 H264/90000
a=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032
a=rtcp-fb:123 goog-remb 
a=rtcp-fb:123 ccm fir
a=rtcp-fb:123 nack 
a=rtcp-fb:123 nack pli
a=rtcp-fb:123 nack 
a=rtcp-fb:123 nack pli
a=rtcp-fb:123 transport-cc 
a=rtpmap:96 VP8/90000
a=rtcp-fb:96 goog-remb 
a=rtcp-fb:96 ccm fir
a=rtcp-fb:96 nack 
a=rtcp-fb:96 nack pli
a=rtcp-fb:96 nack 
a=rtcp-fb:96 nack pli
a=rtcp-fb:96 transport-cc 
a=rtpmap:97 rtx/90000
a=fmtp:97 apt=96
a=rtcp-fb:97 nack 
a=rtcp-fb:97 nack pli
a=rtcp-fb:97 transport-cc 
a=rtpmap:98 VP9/90000
a=fmtp:98 profile-id=0
a=rtcp-fb:98 goog-remb 
a=rtcp-fb:98 ccm fir
a=rtcp-fb:98 nack 
a=rtcp-fb:98 nack pli
a=rtcp-fb:98 nack 
a=rtcp-fb:98 nack pli
a=rtcp-fb:98 transport-cc 
a=rtpmap:99 rtx/90000
a=fmtp:99 apt=98
a=rtcp-fb:99 nack 
a=rtcp-fb:99 nack pli
a=rtcp-fb:99 transport-cc 
a=rtpmap:100 VP9/90000
a=fmtp:100 profile-id=1
a=rtcp-fb:100 goog-remb 
a=rtcp-fb:100 ccm fir
a=rtcp-fb:100 nack 
a=rtcp-fb:100 nack pli
a=rtcp-fb:100 nack 
a=rtcp-fb:100 nack pli
a=rtcp-fb:100 transport-cc 
a=rtpmap:101 rtx/90000
a=fmtp:101 apt=100
a=rtcp-fb:101 nack 
a=rtcp-fb:101 nack pli
a=rtcp-fb:101 transport-cc 
a=rtpmap:121 rtx/90000
a=fmtp:121 apt=102
a=rtcp-fb:121 nack 
a=rtcp-fb:121 nack pli
a=rtcp-fb:121 transport-cc 
a=rtpmap:120 rtx/90000
a=fmtp:120 apt=127
a=rtcp-fb:120 nack 
a=rtcp-fb:120 nack pli
a=rtcp-fb:120 transport-cc 
a=rtpmap:107 rtx/90000
a=fmtp:107 apt=125
a=rtcp-fb:107 nack 
a=rtcp-fb:107 nack pli
a=rtcp-fb:107 transport-cc 
a=rtpmap:109 rtx/90000
a=fmtp:109 apt=108
a=rtcp-fb:109 nack 
a=rtcp-fb:109 nack pli
a=rtcp-fb:109 transport-cc 
a=rtpmap:118 rtx/90000
a=fmtp:118 apt=123
a=rtcp-fb:118 nack 
a=rtcp-fb:118 nack pli
a=rtcp-fb:118 transport-cc 
a=rtpmap:116 ulpfec/90000
a=rtcp-fb:116 nack 
a=rtcp-fb:116 nack pli
a=rtcp-fb:116 transport-cc 
a=extmap:1 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=recvonly
...
```