Skip to content

Instantly share code, notes, and snippets.

@nazar-pc
Created June 19, 2020 19:22
Show Gist options
  • Save nazar-pc/58ffd40db680ecae07198b15b919057e to your computer and use it in GitHub Desktop.
Save nazar-pc/58ffd40db680ecae07198b15b919057e to your computer and use it in GitHub Desktop.
import {MediaKind, RtpCodecParameters, RtpParameters} from "mediasoup/lib/RtpParameters";
import {DtlsFingerprint, DtlsParameters, IceCandidate, IceParameters} from "mediasoup/lib/WebRtcTransport";
export interface ISdpConsumer {
id: string;
kind: MediaKind;
rtpParameters: RtpParameters;
removed: boolean;
}
export interface ITransportParameters {
dtlsParameters: DtlsParameters;
iceCandidates: IceCandidate[];
iceParameters: IceParameters;
}
const RTP_PROBATOR_MID = 'probator';
const RTP_PROBATOR_SSRC = 1234;
const RTP_PROBATOR_CODEC_PAYLOAD_TYPE = 127;
/**
* Hacky, but straightforward and supposedly correct SDP generator
*
* Tailored to mimic necessary mediasoup-client behavior
* Doesn't support data channels, encrypted header extensions and header extensions parameters and many other things
*
* @param transportParameters
* @param consumers
* @param counter Must start with 1 and increase by 1 with each new call for the same connection
*/
export function generateSdp(transportParameters: ITransportParameters, consumers: ISdpConsumer[], counter: number): string {
consumers = withProbator(consumers);
const dtlsFingerprint = transportParameters.dtlsParameters.fingerprints[transportParameters.dtlsParameters.fingerprints.length - 1] as DtlsFingerprint;
const bundleElements = consumers.map(({removed, rtpParameters}, index) => removed && index !== 0 ? null : rtpParameters.mid).filter(Boolean).join(' ');
let sdp = `
v=0
o=- 10000 ${counter} IN IP4 0.0.0.0
s=-
t=0 0
a=ice-lite
a=fingerprint:${dtlsFingerprint.algorithm} ${dtlsFingerprint.value}
a=msid-semantic: WMS *
a=group:BUNDLE ${bundleElements}
`;
let firstSection = true;
for (const {id, kind, removed, rtpParameters} of consumers) {
const port = removed && !firstSection ? 0 : 7;
const payloadTypes = rtpParameters.codecs.map((codec) => codec.payloadType).join(' ');
sdp += `
m=${kind} ${port} UDP/TLS/RTP/SAVPF ${payloadTypes}
c=IN IP4 127.0.0.1
`;
for (const codec of rtpParameters.codecs) {
const encodingName = codec.mimeType.split('/').pop();
const audioChannels = codec.channels ? '/' + codec.channels : '';
sdp += `
a=rtpmap:${codec.payloadType} ${encodingName}/${codec.clockRate}${audioChannels}
`;
}
for (const codec of rtpParameters.codecs) {
if (Object.keys(codec.parameters).length) {
const parameters: string[] = [];
for (const [key, value] of Object.entries(codec.parameters)) {
parameters.push(`${key}=${value}`);
}
sdp += `
a=fmtp:${codec.payloadType} ${parameters.join(';')}
`;
}
}
for (const codec of rtpParameters.codecs) {
if (codec.rtcpFeedback) {
for (const rtcpFeedback of codec.rtcpFeedback) {
const feedback = rtcpFeedback.type + (rtcpFeedback.parameter ? ' ' + rtcpFeedback.parameter : '');
sdp += `
a=rtcp-fb:${codec.payloadType} ${feedback}
`;
}
}
}
if (!removed) {
for (const headerExtension of rtpParameters.headerExtensions ?? []) {
if (headerExtension.encrypt) {
// Not supported
continue;
}
// Parameters are ignored currently
sdp += `
a=extmap:${headerExtension.id} ${headerExtension.uri}
`;
}
}
sdp += `
a=setup:actpass
a=mid:${rtpParameters.mid}
a=msid:${rtpParameters.rtcp?.cname} ${id}
a=${removed ? 'inactive' : 'sendonly'}
a=ice-ufrag:${transportParameters.iceParameters.usernameFragment}
a=ice-pwd:${transportParameters.iceParameters.password}
`;
for (const candidate of transportParameters.iceCandidates) {
sdp += `
a=candidate:${candidate.foundation} 1 ${candidate.protocol} ${candidate.priority} ${candidate.ip} ${candidate.port} typ ${candidate.type}
`;
}
sdp += `
a=end-of-candidates
a=ice-options:renomination
`;
if (!removed) {
for (const encoding of rtpParameters.encodings ?? []) {
sdp += `
a=ssrc:${encoding.ssrc} cname:${rtpParameters.rtcp?.cname}
`;
if (encoding.rtx) {
sdp += `
a=ssrc:${encoding.rtx.ssrc} cname:${rtpParameters.rtcp?.cname}
a=ssrc-group:FID ${encoding.ssrc} ${encoding.rtx.ssrc}
`;
}
}
}
if (rtpParameters.rtcp?.mux) {
sdp += `
a=rtcp-mux
`;
}
if (rtpParameters.rtcp?.reducedSize) {
sdp += `
a=rtcp-rsize
`;
}
firstSection = false;
}
return sdp.trimLeft().replace(/\n+/g, '\n');
}
export function extractDtlsFromSdp(sdp: string): DtlsParameters {
const match = sdp.match(/a=fingerprint:([^\s]+) ([^\s]+)/);
if (!match) {
throw new Error('Unexpected SDP contents, cannot find DTLS parameters');
}
return {
// tslint:disable:object-literal-sort-keys
role: 'client',
fingerprints: [{
algorithm: match[1],
value: match[2],
}],
// tslint:enablle:object-literal-sort-keys
};
}
function withProbator(consumers: ISdpConsumer[]): ISdpConsumer[] {
consumers = clone(consumers);
const index = consumers.findIndex(({kind}) => {
return kind === 'video';
});
// This is a modified version of what is inside of mediasoup-client
if (index !== -1) {
const firstVideoConsumer = consumers[index];
const codec: RtpCodecParameters = clone(
firstVideoConsumer.rtpParameters.codecs[0],
);
codec.payloadType = RTP_PROBATOR_CODEC_PAYLOAD_TYPE;
consumers.splice(index + 1, 0, {
id: 'probator',
kind: 'video',
removed: false,
rtpParameters: {
// tslint:disable:object-literal-sort-keys
codecs: [codec],
headerExtensions: firstVideoConsumer.rtpParameters.headerExtensions,
encodings: [{
ssrc: RTP_PROBATOR_SSRC,
}],
rtcp: {
cname: "probator",
reducedSize: true,
mux: true,
},
mid: RTP_PROBATOR_MID,
// tslint:enablle:object-literal-sort-keys
},
});
}
return consumers;
}
function clone<T>(o: T): T {
return JSON.parse(JSON.stringify(o));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment