Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add retainCodecs and retainCandidates functions #18

Merged
merged 3 commits into from
Aug 10, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 { filterCandidates, filterCodecs, removeCodec } 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('filterCodecs', () => {
it('should filter 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);

filterCodecs(parsed, ['h264', 'opus']);
expect(validateOfferCodecs(parsed)).toBe(true);
});
it('should filter 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) => {
filterCodecs(av, ['h264', 'opus']);
});
expect(validateOfferCodecs(parsed)).toBe(true);
});
});

describe('filterCandidates', () => {
it('should filter 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(filterCandidates(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(filterCandidates(parsed, ['udp', 'tcp'])).toBeFalsy();
});
it('should filter 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(filterCandidates(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(filterCandidates(media, ['udp', 'tcp'])).toBeFalsy();
});
});
});
});
68 changes: 61 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,71 @@ 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));
});
}

/**
* Filter out unwanted codecs 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 filterCodecs(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering if we can name this something better...I feel like there's ambiguity around if the passed list are the ones being kept or the ones being removed.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

retainCodecs maybe

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I can also rename filterCandidates to retainCandidates.

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));
}

/**
* Filter out unwanted candidates 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 filterCandidates(
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