2020-11-07 13:16:07 +01:00
|
|
|
import * as sdpTransform from "sdp-transform";
|
|
|
|
import {MediaDescription} from "sdp-transform";
|
|
|
|
|
|
|
|
interface SdpCodec {
|
|
|
|
payload: number;
|
|
|
|
codec: string;
|
|
|
|
rate?: number;
|
|
|
|
encoding?: number;
|
|
|
|
fmtp?: { [key: string]: string | number },
|
|
|
|
rtcpFb?: string[]
|
|
|
|
}
|
|
|
|
|
|
|
|
/* These MUST be the payloads used by the remote as well */
|
|
|
|
const OPUS_VOICE_PAYLOAD_TYPE = 111;
|
|
|
|
const OPUS_MUSIC_PAYLOAD_TYPE = 112;
|
2020-11-22 13:48:15 +01:00
|
|
|
const H264_PAYLOAD_TYPE = 126;
|
2020-11-07 13:16:07 +01:00
|
|
|
|
|
|
|
type SdpMedia = {
|
|
|
|
type: string;
|
|
|
|
port: number;
|
|
|
|
protocol: string;
|
|
|
|
payloads?: string;
|
|
|
|
} & MediaDescription;
|
|
|
|
|
|
|
|
export class SdpProcessor {
|
|
|
|
private static readonly kAudioCodecs: SdpCodec[] = [
|
|
|
|
{
|
|
|
|
/* Opus Mono/Opus Voice */
|
|
|
|
payload: OPUS_VOICE_PAYLOAD_TYPE,
|
|
|
|
codec: "opus",
|
|
|
|
rate: 48000,
|
|
|
|
encoding: 2,
|
2020-11-15 23:28:00 +01:00
|
|
|
fmtp: { minptime: 1, maxptime: 20, useinbandfec: 1, stereo: 0 },
|
2020-11-07 13:16:07 +01:00
|
|
|
rtcpFb: [ "transport-cc" ]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
/* Opus Stereo/Opus Music */
|
|
|
|
payload: OPUS_MUSIC_PAYLOAD_TYPE,
|
|
|
|
codec: "opus",
|
|
|
|
rate: 48000,
|
|
|
|
encoding: 2,
|
|
|
|
fmtp: { minptime: 1, maxptime: 20, useinbandfec: 1, stereo: 1 },
|
|
|
|
rtcpFb: [ "transport-cc" ]
|
|
|
|
}
|
|
|
|
];
|
|
|
|
|
|
|
|
private static readonly kVideoCodecs: SdpCodec[] = [
|
2020-11-22 13:48:15 +01:00
|
|
|
/* TODO: Set AS as well! */
|
2020-11-07 13:16:07 +01:00
|
|
|
{
|
2020-11-22 13:48:15 +01:00
|
|
|
payload: H264_PAYLOAD_TYPE,
|
|
|
|
codec: "H264",
|
2020-11-07 13:16:07 +01:00
|
|
|
rate: 90000,
|
2020-11-22 13:48:15 +01:00
|
|
|
rtcpFb: [ "nack", "nack pli", "ccm fir", "transport-cc" ],
|
|
|
|
//42001f | Original: 42e01f
|
|
|
|
fmtp: {
|
|
|
|
"level-asymmetry-allowed": 1, "packetization-mode": 1, "profile-level-id": "42e01f", "max-br": 25000, "max-fr": 60,
|
|
|
|
"x-google-max-bitrate": 22 * 1000,
|
|
|
|
"x-google-start-bitrate": 22 * 1000, /* Fun fact: This actually controls the max bitrate for google chrome */
|
|
|
|
}
|
2020-11-07 13:16:07 +01:00
|
|
|
}
|
|
|
|
];
|
|
|
|
|
|
|
|
private rtpRemoteChannelMapping: {[key: string]: number};
|
|
|
|
private rtpLocalChannelMapping: {[key: string]: number};
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
this.reset();
|
|
|
|
}
|
|
|
|
|
|
|
|
reset() {
|
|
|
|
this.rtpRemoteChannelMapping = {};
|
|
|
|
this.rtpLocalChannelMapping = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
getRemoteSsrcFromFromMediaId(mediaId: string) : number | undefined {
|
|
|
|
return this.rtpRemoteChannelMapping[mediaId];
|
|
|
|
}
|
|
|
|
|
|
|
|
getLocalSsrcFromFromMediaId(mediaId: string) : number | undefined {
|
|
|
|
return this.rtpLocalChannelMapping[mediaId];
|
|
|
|
}
|
|
|
|
|
|
|
|
processIncomingSdp(sdpString: string, _mode: "offer" | "answer") : string {
|
2020-11-22 13:48:15 +01:00
|
|
|
/* The server somehow does not encode the level id in hex */
|
|
|
|
sdpString = sdpString.replace(/profile-level-id=4325407/g, "profile-level-id=42e01f");
|
|
|
|
|
2020-11-07 13:16:07 +01:00
|
|
|
const sdp = sdpTransform.parse(sdpString);
|
|
|
|
this.rtpRemoteChannelMapping = SdpProcessor.generateRtpSSrcMapping(sdp);
|
2020-11-22 13:48:15 +01:00
|
|
|
|
|
|
|
/* FIXME! */
|
|
|
|
SdpProcessor.patchLocalCodecs(sdp);
|
|
|
|
|
2020-11-07 13:16:07 +01:00
|
|
|
return sdpTransform.write(sdp);
|
|
|
|
}
|
|
|
|
|
|
|
|
processOutgoingSdp(sdpString: string, mode: "offer" | "answer") : string {
|
|
|
|
const sdp = sdpTransform.parse(sdpString);
|
|
|
|
|
|
|
|
/* apply the "root" fingerprint to each media, FF fix */
|
|
|
|
if(sdp.fingerprint) {
|
|
|
|
sdp.media.forEach(media => media.fingerprint = sdp.fingerprint);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* remove the FID groups for video (We don't support that) */
|
|
|
|
for(const media of sdp.media) {
|
|
|
|
if(!media.ssrcGroups) { continue; }
|
|
|
|
for(const group of media.ssrcGroups.slice()) {
|
|
|
|
if(group.semantics === "FID") {
|
|
|
|
/* Keep the first ssrc which is the primary source. The other is for FID */
|
|
|
|
const ids = group.ssrcs.split(" ").map(ssrc => parseInt(ssrc) >>> 0).slice(1);
|
|
|
|
media.ssrcs = media.ssrcs.filter(ssrc => ids.indexOf(parseInt(ssrc.id as string) >>> 0) === -1);
|
|
|
|
media.ssrcGroups.remove(group);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.rtpLocalChannelMapping = SdpProcessor.generateRtpSSrcMapping(sdp);
|
|
|
|
|
|
|
|
SdpProcessor.patchLocalCodecs(sdp);
|
|
|
|
|
|
|
|
return sdpTransform.write(sdp);
|
|
|
|
}
|
|
|
|
|
|
|
|
private static generateRtpSSrcMapping(sdp: sdpTransform.SessionDescription) {
|
|
|
|
const mapping = {};
|
|
|
|
for(let media of sdp.media) {
|
|
|
|
if(typeof media.mid === "undefined") {
|
|
|
|
throw tra("missing media id for line {}", sdp.media.indexOf(media));
|
|
|
|
}
|
|
|
|
|
|
|
|
/* every ssrc MUST have a cname */
|
|
|
|
const ssrcs = (media.ssrcs || []).filter(e => e.attribute === "cname");
|
|
|
|
ssrcs.forEach(ssrc => {
|
|
|
|
if(typeof mapping[media.mid] === "undefined") {
|
|
|
|
mapping[media.mid] = ssrc.id as number >>> 0;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return mapping;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static patchLocalCodecs(sdp: sdpTransform.SessionDescription) {
|
|
|
|
for(let media of sdp.media) {
|
|
|
|
if(media.type !== "video" && media.type !== "audio") {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
media.fmtp = [];
|
|
|
|
media.rtp = [];
|
|
|
|
media.rtcpFb = [];
|
|
|
|
media.rtcpFbTrrInt = [];
|
|
|
|
|
|
|
|
for(let codec of (media.type === "audio" ? this.kAudioCodecs : this.kVideoCodecs)) {
|
|
|
|
media.rtp.push({
|
|
|
|
payload: codec.payload,
|
|
|
|
codec: codec.codec,
|
|
|
|
encoding: codec.encoding,
|
|
|
|
rate: codec.rate
|
|
|
|
});
|
|
|
|
|
|
|
|
codec.rtcpFb?.forEach(fb => media.rtcpFb.push({
|
|
|
|
payload: codec.payload,
|
|
|
|
type: fb
|
|
|
|
}));
|
|
|
|
|
|
|
|
if(codec.fmtp && Object.keys(codec.fmtp).length > 0) {
|
|
|
|
media.fmtp.push({
|
|
|
|
payload: codec.payload,
|
|
|
|
config: Object.keys(codec.fmtp).map(e => e + "=" + codec.fmtp[e]).join(";")
|
|
|
|
});
|
2020-11-22 13:48:15 +01:00
|
|
|
if(media.type === "audio") {
|
|
|
|
media.maxptime = media.fmtp["maxptime"];
|
|
|
|
}
|
2020-11-07 13:16:07 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
media.payloads = media.rtp.map(e => e.payload).join(" ");
|
2020-11-22 13:48:15 +01:00
|
|
|
media.bandwidth = [{
|
|
|
|
type: "AS",
|
|
|
|
limit: 12000
|
|
|
|
}]
|
2020-11-07 13:16:07 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export namespace SdpCompressor {
|
|
|
|
export function decompressSdp(sdp: string, mode: number) : string {
|
|
|
|
if(mode === 0) {
|
|
|
|
return sdp;
|
|
|
|
} else if(mode === 1) {
|
|
|
|
/* TODO! */
|
|
|
|
return sdp;
|
|
|
|
} else {
|
|
|
|
throw tr("unsupported compression mode");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function compressSdp(sdp: string, mode: number) : string {
|
|
|
|
if(mode === 0) {
|
|
|
|
return sdp;
|
|
|
|
} else if(mode === 1) {
|
|
|
|
/* TODO! */
|
|
|
|
return sdp;
|
|
|
|
} else {
|
|
|
|
throw tr("unsupported compression mode");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|