Skip to content

Commit

Permalink
feat: add retainCodecs and retainCandidates functions (#18)
Browse files Browse the repository at this point in the history
* feat: add filterCodecs and filterCandidates functions

* chore: rename filter functions

* chore: update unit test descriptions

---------

Co-authored-by: Bryce Tham <[email protected]>
  • Loading branch information
brycetham and Bryce Tham authored Aug 10, 2023
1 parent ad0cec2 commit 8bbb081
Show file tree
Hide file tree
Showing 5 changed files with 428 additions and 7 deletions.
4 changes: 4 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"gohri",
"hdrext",
"inferencing",
"ISAC",
"libauth",
"mindmeld",
"mkdir",
Expand All @@ -45,6 +46,8 @@
"Onnx",
"onnxruntime",
"packetization",
"PCMA",
"PCMU",
"prettierignore",
"raddr",
"remb",
Expand All @@ -66,6 +69,7 @@
"transpiled",
"typedoc",
"ufrag",
"ulpfec",
"untracked",
"videostateupdate",
"VITE",
Expand Down
122 changes: 122 additions & 0 deletions src/munge.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import * as fs from 'fs';
import { AvMediaDescription, CodecInfo, Sdp } from './model';
import { removeCodec, retainCandidates, retainCodecs } from './munge';
import { parse } from './parser';

/**
* Validate that the sdp offer does not use any video codecs besides h264 or opus.
*
* @param offer - The sdp offer string to validate.
* @returns True if the offer is valid.
*/
const validateOfferCodecs = (offer: Sdp): boolean => {
offer.avMedia
.filter((av: AvMediaDescription) => av.type === 'video')
.forEach((av: AvMediaDescription) => {
[...av.codecs.values()].forEach((c: CodecInfo) => {
if (c.name?.toLowerCase() !== 'h264') {
throw new Error('SDP contains non-h264 codec in video media description');
}
});
});
offer.avMedia
.filter((av: AvMediaDescription) => av.type === 'audio')
.forEach((av: AvMediaDescription) => {
[...av.codecs.values()].forEach((c: CodecInfo) => {
if (c.name?.toLowerCase() !== 'opus') {
throw new Error('SDP contains non-opus codec in audio media description');
}
});
});
return true;
};

describe('munging', () => {
describe('removeCodec', () => {
it('should remove codecs correctly when passing in an SDP', () => {
expect.hasAssertions();
const offer = fs.readFileSync('./src/sdp-corpus/offer_with_extra_codecs.sdp', 'utf-8');
const parsed = parse(offer);

const unwantedVideoCodecs = ['VP8', 'VP9', 'AV1', 'rtx', 'red', 'ulpfec'];
const unwantedAudioCodecs = ['red', 'ISAC', 'G722', 'PCMU', 'PCMA', 'CN', 'telephone-event'];

unwantedVideoCodecs.forEach((codec) => removeCodec(parsed, codec));
unwantedAudioCodecs.forEach((codec) => removeCodec(parsed, codec));
expect(validateOfferCodecs(parsed)).toBe(true);
});
it('should remove codecs correctly when passing in an AvMediaDescription', () => {
expect.hasAssertions();
const offer = fs.readFileSync('./src/sdp-corpus/offer_with_extra_codecs.sdp', 'utf-8');
const parsed = parse(offer);

const unwantedVideoCodecs = ['VP8', 'VP9', 'AV1', 'rtx', 'red', 'ulpfec'];
const unwantedAudioCodecs = ['red', 'ISAC', 'G722', 'PCMU', 'PCMA', 'CN', 'telephone-event'];

parsed.avMedia.forEach((av) => {
unwantedVideoCodecs.forEach((codec) => removeCodec(av, codec));
unwantedAudioCodecs.forEach((codec) => removeCodec(av, codec));
});
expect(validateOfferCodecs(parsed)).toBe(true);
});
});
describe('retainCodecs', () => {
it('should retain codecs correctly when passing in an SDP', () => {
expect.hasAssertions();
const offer = fs.readFileSync('./src/sdp-corpus/offer_with_extra_codecs.sdp', 'utf-8');
const parsed = parse(offer);

retainCodecs(parsed, ['h264', 'opus']);
expect(validateOfferCodecs(parsed)).toBe(true);
});
it('should retain codecs correctly when passing in an AvMediaDescription', () => {
expect.hasAssertions();
const offer = fs.readFileSync('./src/sdp-corpus/offer_with_extra_codecs.sdp', 'utf-8');
const parsed = parse(offer);

parsed.avMedia.forEach((av) => {
retainCodecs(av, ['h264', 'opus']);
});
expect(validateOfferCodecs(parsed)).toBe(true);
});
});

describe('retainCandidates', () => {
it('should retain candidates correctly when passing in an SDP', () => {
expect.hasAssertions();
const offer = fs.readFileSync('./src/sdp-corpus/answer_with_extra_candidates.sdp', 'utf-8');
const parsed = parse(offer);

// should return true when some candidates have been filtered out
expect(retainCandidates(parsed, ['udp', 'tcp'])).toBeTruthy();
parsed.media.forEach((mline) => {
expect(mline.iceInfo.candidates).toHaveLength(4);
expect(
mline.iceInfo.candidates.every((candidate) =>
['udp', 'tcp'].includes(candidate.transport.toLowerCase())
)
).toBeTruthy();
});
// should return false when no candidates have been filtered out
expect(retainCandidates(parsed, ['udp', 'tcp'])).toBeFalsy();
});
it('should retain candidates correctly when passing in an AvMediaDescription', () => {
expect.hasAssertions();
const offer = fs.readFileSync('./src/sdp-corpus/answer_with_extra_candidates.sdp', 'utf-8');
const parsed = parse(offer);

parsed.media.forEach((media) => {
// should return true when some candidates have been filtered out
expect(retainCandidates(media, ['udp', 'tcp'])).toBeTruthy();
expect(media.iceInfo.candidates).toHaveLength(4);
expect(
media.iceInfo.candidates.every((candidate) =>
['udp', 'tcp'].includes(candidate.transport.toLowerCase())
)
).toBeTruthy();
// should return false when no candidates have been filtered out
expect(retainCandidates(media, ['udp', 'tcp'])).toBeFalsy();
});
});
});
});
70 changes: 63 additions & 7 deletions src/munge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { AvMediaDescription, CodecInfo, Sdp } from './model';
import { AvMediaDescription, CodecInfo, MediaDescription, Sdp } from './model';

/**
* Disable an rtcp-fb value from all media blocks in the given SDP.
Expand Down Expand Up @@ -42,17 +42,73 @@ export function disableRemb(sdp: Sdp) {

/**
* Remove the codec with the given name (as well as any secondary codecs associated with
* it) from the media blocks in the given SDP.
* it) from the media blocks in the given SDP or audio/video media description.
*
* @param sdp - The SDP from which to remove the given codec.
* @param codecName - The name of the codec to filter.
* @param sdpOrAv - The {@link Sdp} or {@link AvMediaDescription} from which to remove the given codec.
* @param codecName - The name of the codec to remove.
*/
export function removeCodec(sdp: Sdp, codecName: string) {
sdp.avMedia.forEach((media: AvMediaDescription) => {
export function removeCodec(sdpOrAv: Sdp | AvMediaDescription, codecName: string) {
const mediaDescriptions = sdpOrAv instanceof Sdp ? sdpOrAv.avMedia : [sdpOrAv];
mediaDescriptions.forEach((media: AvMediaDescription) => {
const codecInfos = [...media.codecs.entries()].filter(
([, ci]) => ci.name?.toLowerCase() === codecName.toLowerCase()
);

codecInfos.forEach(([pt]) => media.removePt(pt));
});
}

/**
* Retain specific codecs, filtering out unwanted ones from the given SDP or audio/video media
* description.
*
* Note: Done this way because of a feature not implemented in all browsers, currently missing in
* Firefox. Once that is added we can use `RTPSender.getCapabilities` and filter those to call
* with `RTCRtpTransceiver.setCodecPreferences` instead of doing this manually.
*
* @param sdpOrAv - The {@link Sdp} or {@link AvMediaDescription} from which to filter codecs.
* @param allowedCodecNames - The names of the codecs that should remain in the SDP.
*/
export function retainCodecs(
sdpOrAv: Sdp | AvMediaDescription,
allowedCodecNames: Array<string>
): void {
const avMediaDescriptions = sdpOrAv instanceof Sdp ? sdpOrAv.avMedia : [sdpOrAv];
const allowedLowerCase = allowedCodecNames.map((s) => s.toLowerCase());

avMediaDescriptions
.map((av) => {
return [...av.codecs.values()].map((c) => c.name as string);
})
.flat()
.filter((codecName) => !allowedLowerCase.includes(codecName.toLowerCase()))
.forEach((unwantedCodec) => removeCodec(sdpOrAv, unwantedCodec));
}

/**
* Retain specific candidates, filtering out unwanted ones from the given SDP or media description
* by transport type.
*
* @param sdpOrMedia - The {@link Sdp} or {@link MediaDescription} from which to filter candidates.
* @param allowedTransportTypes - The names of the transport types of the candidates that should remain in the SDP.
* @returns A boolean that indicates if some candidates have been filtered out.
*/
export function retainCandidates(
sdpOrMedia: Sdp | MediaDescription,
allowedTransportTypes: Array<string>
) {
const mediaDescriptions = sdpOrMedia instanceof Sdp ? sdpOrMedia.media : [sdpOrMedia];
let filtered = false;

mediaDescriptions.forEach((media) => {
// eslint-disable-next-line no-param-reassign
media.iceInfo.candidates = media.iceInfo.candidates.filter((candidate) => {
if (allowedTransportTypes.includes(candidate.transport.toLowerCase())) {
return true;
}
filtered = true;
return false;
});
});

return filtered;
}
64 changes: 64 additions & 0 deletions src/sdp-corpus/answer_with_extra_candidates.sdp
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
v=0
o=homer 0 0 IN IP4 10.224.203.38
s=-
i=linus;homer:focal-3143;mf:3653
c=IN IP4 10.224.203.38
b=TIAS:1021256000
t=0 0
a=group:BUNDLE 0 1
a=ice-lite
m=video 5004 RTP/AVP 102 103
c=IN IP4 10.224.203.38
b=TIAS:1000000000
a=content:main
a=sendrecv
a=rtpmap:102 H264/90000
a=fmtp:102 profile-level-id=420034;packetization-mode=1;max-mbps=2073600;max-fs=36864;max-fps=3000;max-br=1000000;max-dpb=69120;level-asymmetry-allowed=1
a=rtpmap:103 rtx/90000
a=fmtp:103 apt=102
a=rtcp-fb:* goog-remb
a=rtcp-fb:* ccm fir
a=rtcp-fb:* nack
a=rtcp-fb:* nack pli
a=extmap:2/sendrecv http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=rtcp-mux
a=mid:0
a=jmp:v1
a=jmp-source:0
a=jmp-stream-id-mode:SSRC
a=setup:passive
a=fingerprint:sha-256 2B:C0:C2:C8:9D:B0:D3:B1:B6:FC:D2:98:23:40:A4:14:A3:79:99:E1:AA:AD:4C:75:F3:07:F8:13:AB:A5:3F:1F
a=label:0
a=ice-ufrag:CQkMoaMl
a=ice-pwd:XuRyavxtbUskqv61aunHNusXpaHjll88
a=candidate:0 1 UDP 2130706431 10.224.203.38 5004 typ host
a=candidate:1 1 UDP 2130706175 10.224.203.38 33434 typ host
a=candidate:2 1 TCP 1962934271 10.224.203.38 5004 typ host tcptype passive
a=candidate:3 1 TCP 1962934015 10.224.203.38 33434 typ host tcptype passive
a=candidate:4 1 xTLS 1795162111 10.224.203.38 443 typ host tcptype passive fingerprint sha-1;C3:C7:01:E9:C0:5D:74:BA:E8:3A:A4:D4:95:F2:75:3A:84:B0:F3:4B
a=rtcp:5005 IN IP4 10.224.203.38
m=audio 5004 RTP/AVP 111
c=IN IP4 10.224.203.38
b=TIAS:1000000
a=content:main
a=sendrecv
a=rtpmap:111 opus/48000/2
a=fmtp:111 maxplaybackrate=48000;maxaveragebitrate=64000;stereo=1;sprop-stereo=1;useinbandfec=1
a=extmap:14/sendrecv urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:2/sendrecv http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=rtcp-mux
a=mid:1
a=jmp:v1
a=jmp-source:0
a=jmp-stream-id-mode:SSRC
a=setup:passive
a=fingerprint:sha-256 2B:C0:C2:C8:9D:B0:D3:B1:B6:FC:D2:98:23:40:A4:14:A3:79:99:E1:AA:AD:4C:75:F3:07:F8:13:AB:A5:3F:1F
a=label:1
a=ice-ufrag:TSd3jAfz
a=ice-pwd:IgtEWo1oeuGbnYsnp85PeOETLfXZFrj8
a=candidate:0 1 UDP 2130706431 10.224.203.38 5004 typ host
a=candidate:1 1 UDP 2130706175 10.224.203.38 33434 typ host
a=candidate:2 1 TCP 1962934271 10.224.203.38 5004 typ host tcptype passive
a=candidate:3 1 TCP 1962934015 10.224.203.38 33434 typ host tcptype passive
a=candidate:4 1 xTLS 1795162111 10.224.203.38 443 typ host tcptype passive fingerprint sha-1;C3:C7:01:E9:C0:5D:74:BA:E8:3A:A4:D4:95:F2:75:3A:84:B0:F3:4B
a=rtcp:5005 IN IP4 10.224.203.38
Loading

0 comments on commit 8bbb081

Please sign in to comment.