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

fix: force baseline profiles for h264 and vp9 #1555

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
28 changes: 24 additions & 4 deletions packages/client/src/rtc/__tests__/codecs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ describe('codecs', () => {
// prettier-ignore
expect(codecs?.map((c) => [c.mimeType, c.sdpFmtpLine])).toEqual([
['video/VP8', undefined],
['video/H264', 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c1f'],
['video/rtx', undefined],
['video/H264', 'level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f'],
['video/H264', 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f'],
['video/H264', 'level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=640c1f'],
['video/H264', 'level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f'],
['video/H264', 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c1f'],
['video/rtx', undefined],
['video/VP9', 'profile-id=0'],
['video/VP9', 'profile-id=2'],
['video/red', undefined],
Expand All @@ -46,8 +46,8 @@ describe('codecs', () => {
expect(codecs?.map((c) => [c.mimeType, c.sdpFmtpLine])).toEqual([
['video/H264', 'level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f'],
['video/H264', 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f'],
['video/H264', 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c1f'],
['video/H264', 'level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=640c1f'],
['video/H264', 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c1f'],
['video/rtx', undefined],
['video/VP8', undefined],
['video/VP9', 'profile-id=0'],
Expand Down Expand Up @@ -75,6 +75,26 @@ describe('codecs', () => {
['video/red', undefined],
]);
});

it('should pick the profile-0 VP9 codec', () => {
RTCRtpReceiver.getCapabilities = vi.fn().mockReturnValue(videoCodecs);
const codecs = getPreferredCodecs('video', 'vp9');
expect(codecs).toBeDefined();
// prettier-ignore
expect(codecs?.map((c) => [c.mimeType, c.sdpFmtpLine])).toEqual([
['video/VP9', 'profile-id=0'],
['video/VP9', 'profile-id=2'],
['video/H264', 'level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f'],
['video/H264', 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f'],
['video/H264', 'level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=640c1f'],
['video/H264', 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c1f'],
['video/rtx', undefined],
['video/VP8', undefined],
['video/red', undefined],
['video/ulpfec', undefined],
['video/flexfec-03', 'repair-window=10000000'],
]);
});
});

// prettier-ignore
Expand Down
127 changes: 86 additions & 41 deletions packages/client/src/rtc/codecs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getOSInfo } from '../client-details';
import { isReactNative } from '../helpers/platforms';
import { isFirefox, isSafari } from '../helpers/browsers';
import { combineComparators, Comparator } from '../sorting';
import type { PreferredCodec } from '../types';

/**
Expand All @@ -21,55 +22,85 @@ export const getPreferredCodecs = (
if (!capabilities) return;

const preferred: RTCRtpCodecCapability[] = [];
const partiallyPreferred: RTCRtpCodecCapability[] = [];
const unpreferred: RTCRtpCodecCapability[] = [];
const unpreferred: Record<string, RTCRtpCodecCapability[]> = {};

const preferredCodecMimeType = `${kind}/${preferredCodec.toLowerCase()}`;
const codecToRemoveMimeType =
codecToRemove && `${kind}/${codecToRemove.toLowerCase()}`;

for (const codec of capabilities.codecs) {
const codecMimeType = codec.mimeType.toLowerCase();

const shouldRemoveCodec = codecMimeType === codecToRemoveMimeType;
if (shouldRemoveCodec) continue; // skip this codec

const isPreferredCodec = codecMimeType === preferredCodecMimeType;
if (!isPreferredCodec) {
unpreferred.push(codec);
continue;
}

// h264 is a special case, we want to prioritize the baseline codec with
// profile-level-id is 42e01f and packetization-mode=0 for maximum
// cross-browser compatibility.
// this branch covers the other cases, such as vp8.
if (codecMimeType !== 'video/h264') {
if (codecMimeType === codecToRemoveMimeType) continue; // skip this codec
if (codecMimeType === preferredCodecMimeType) {
preferred.push(codec);
continue;
}

const sdpFmtpLine = codec.sdpFmtpLine;
if (!sdpFmtpLine || !sdpFmtpLine.includes('profile-level-id=42e01f')) {
// this is not the baseline h264 codec, prioritize it lower
partiallyPreferred.push(codec);
continue;
}

// packetization-mode mode is optional; when not present it defaults to 0:
// https://datatracker.ietf.org/doc/html/rfc6184#section-6.2
if (
sdpFmtpLine.includes('packetization-mode=0') ||
!sdpFmtpLine.includes('packetization-mode')
) {
preferred.unshift(codec);
} else {
preferred.push(codec);
(unpreferred[codecMimeType] ??= []).push(codec);
}
}

// return a sorted list of codecs, with the preferred codecs first
return [...preferred, ...partiallyPreferred, ...unpreferred];
return preferred
.concat(Object.values(unpreferred).flatMap((v) => v))
.sort(combineComparators(h264Comparator, vp9Comparator));
};

/**
* A comparator for sorting H264 codecs.
* We want to prioritize the baseline codec with profile-level-id is 42e01f
* and packetization-mode=0 for maximum cross-browser compatibility.
*/
const h264Comparator: Comparator<RTCRtpCodecCapability> = (a, b) => {
const aMimeType = a.mimeType.toLowerCase();
const bMimeType = b.mimeType.toLowerCase();
if (aMimeType !== 'video/h264' || bMimeType !== 'video/h264') return 0;

const aFmtpLine = a.sdpFmtpLine;
const bFmtpLine = b.sdpFmtpLine;
if (!aFmtpLine || !bFmtpLine) return 0;

// h264 is a special case, we want to prioritize the baseline codec with
// profile-level-id is 42e01f and packetization-mode=0 for maximum
// cross-browser compatibility.
const aIsBaseline = aFmtpLine.includes('profile-level-id=42e01f');
const bIsBaseline = bFmtpLine.includes('profile-level-id=42e01f');
if (aIsBaseline && !bIsBaseline) return -1;
if (!aIsBaseline && bIsBaseline) return 1;

const aPacketizationMode0 =
aFmtpLine.includes('packetization-mode=0') ||
!aFmtpLine.includes('packetization-mode');
const bPacketizationMode0 =
bFmtpLine.includes('packetization-mode=0') ||
!bFmtpLine.includes('packetization-mode');
if (aPacketizationMode0 && !bPacketizationMode0) return -1;
if (!aPacketizationMode0 && bPacketizationMode0) return 1;

return 0;
};

/**
* A comparator for sorting VP9 codecs.
* We want to prioritize the profile-id=0 codec for maximum compatibility.
*/
const vp9Comparator: Comparator<RTCRtpCodecCapability> = (a, b) => {
const aMimeType = a.mimeType.toLowerCase();
const bMimeType = b.mimeType.toLowerCase();
if (aMimeType !== 'video/vp9' || bMimeType !== 'video/vp9') return 0;

const aFmtpLine = a.sdpFmtpLine;
const bFmtpLine = b.sdpFmtpLine;
if (!aFmtpLine || !bFmtpLine) return 0;

// for vp9, we want to prioritize the profile-id=0 codec
// for maximum cross-browser compatibility.
const aIsProfile0 =
aFmtpLine.includes('profile-id=0') || !aFmtpLine.includes('profile-id');
const bIsProfile0 =
bFmtpLine.includes('profile-id=0') || !bFmtpLine.includes('profile-id');
if (aIsProfile0 && !bIsProfile0) return -1;
if (!aIsProfile0 && bIsProfile0) return 1;

return 0;
};

/**
Expand Down Expand Up @@ -111,6 +142,18 @@ export const getOptimalVideoCodec = (
return preferredOr(preferredCodec, 'vp8');
};

/**
* Returns whether the H264 codec supports the baseline profile.
*/
const h264SupportsBaseline = (codec: RTCRtpCodecCapability) => {
const fmtpLine = codec.sdpFmtpLine;
if (!fmtpLine) return false;
const packetization0 =
fmtpLine.includes('packetization-mode=0') ||
!fmtpLine.includes('packetization-mode');
return fmtpLine.includes('profile-level-id=42e01f') && packetization0;
};

/**
* Determines if the platform supports the preferred codec.
* If not, it returns the fallback codec.
Expand All @@ -128,11 +171,13 @@ const preferredOr = (
// so we disable it for them.
if (isSvcCodec(codec) && (isSafari() || isFirefox())) return fallback;

const { codecs } = capabilities;
const codecMimeType = `video/${codec}`.toLowerCase();
return codecs.some((c) => c.mimeType.toLowerCase() === codecMimeType)
? codec
: fallback;
const isSupported = capabilities.codecs.some(
(c) =>
c.mimeType.toLowerCase() === codecMimeType &&
(codec === 'h264' ? h264SupportsBaseline(c) : true),
);
return isSupported ? codec : fallback;
};

/**
Expand All @@ -145,8 +190,8 @@ export const isSvcCodec = (codecOrMimeType: string | undefined) => {
codecOrMimeType = codecOrMimeType.toLowerCase();
return (
codecOrMimeType === 'vp9' ||
codecOrMimeType === 'av1' ||
codecOrMimeType === 'video/vp9' ||
codecOrMimeType === 'av1' ||
codecOrMimeType === 'video/av1'
);
};
Loading