From 5452bbe6eb5d28aa543aeedb424f706b60b0cbbb Mon Sep 17 00:00:00 2001 From: Filip Nowakowski Date: Mon, 3 Nov 2025 10:10:37 +0100 Subject: [PATCH 1/5] feat: add CodecInfo class with methods to check codec availability --- src/codec-info.spec.ts | 71 ++++++++++++++++++++++++++++++++++++++++++ src/codec-info.ts | 54 ++++++++++++++++++++++++++++++++ src/index.ts | 1 + 3 files changed, 126 insertions(+) create mode 100644 src/codec-info.spec.ts create mode 100644 src/codec-info.ts diff --git a/src/codec-info.spec.ts b/src/codec-info.spec.ts new file mode 100644 index 0000000..270fdb4 --- /dev/null +++ b/src/codec-info.spec.ts @@ -0,0 +1,71 @@ +import { CodecInfo } from './codec-info'; + +describe('CodecInfo', () => { + describe('isH264Available', () => { + it('should return true when the H.264 codec is available', async () => { + expect.assertions(1); + + Object.defineProperty(window, 'RTCPeerConnection', { + writable: true, + value: jest.fn().mockReturnValue({ + createOffer: jest.fn().mockResolvedValue({ + sdp: 'a=rtpmap:124 H264/90000', + }), + close: jest.fn(), + } as unknown as RTCPeerConnection), + }); + + await expect(CodecInfo.isH264Available()).resolves.toBe(true); + }); + + it('should return false when the H.264 codec is not available', async () => { + expect.assertions(1); + + Object.defineProperty(window, 'RTCPeerConnection', { + writable: true, + value: jest.fn().mockReturnValue({ + createOffer: jest.fn().mockResolvedValue({ + sdp: 'a=rtpmap:36 rtx/90000', + }), + close: jest.fn(), + } as unknown as RTCPeerConnection), + }); + + await expect(CodecInfo.isH264Available()).resolves.toBe(false); + }); + }); + + describe('isAV1Available', () => { + it('should return true when the AV1 codec is available', async () => { + expect.assertions(1); + + Object.defineProperty(window, 'RTCPeerConnection', { + writable: true, + value: jest.fn().mockReturnValue({ + createOffer: jest.fn().mockResolvedValue({ + sdp: 'a=rtpmap:124 AV1/90000', + }), + close: jest.fn(), + } as unknown as RTCPeerConnection), + }); + + await expect(CodecInfo.isAV1Available()).resolves.toBe(true); + }); + + it('should return false when the AV1 codec is not available', async () => { + expect.assertions(1); + + Object.defineProperty(window, 'RTCPeerConnection', { + writable: true, + value: jest.fn().mockReturnValue({ + createOffer: jest.fn().mockResolvedValue({ + sdp: 'a=rtpmap:36 rtx/90000', + }), + close: jest.fn(), + } as unknown as RTCPeerConnection), + }); + + await expect(CodecInfo.isAV1Available()).resolves.toBe(false); + }); + }); +}); diff --git a/src/codec-info.ts b/src/codec-info.ts new file mode 100644 index 0000000..4640342 --- /dev/null +++ b/src/codec-info.ts @@ -0,0 +1,54 @@ +/** + * CodecInfo class to provide static methods for codec information. + */ +export class CodecInfo { + /** + * Notifies the user whether or not the H.264 + * codec is present. + * @returns -boolean. + */ + static async isH264Available(): Promise { + let hasCodec = false; + + try { + const peerConnection = new window.RTCPeerConnection(); + const offer = await peerConnection.createOffer({ + offerToReceiveVideo: true, + }); + + if (offer?.sdp?.match(/^a=rtpmap:\d+\s+H264\/\d+/m)) { + hasCodec = true; + } + peerConnection.close(); + } catch (error) { + return false; + } + + return hasCodec; + } + + /** + * Notifies the user whether or not the H.264 + * codec is present. + * @returns -boolean. + */ + static async isAV1Available(): Promise { + let hasCodec = false; + + try { + const peerConnection = new window.RTCPeerConnection(); + const offer = await peerConnection.createOffer({ + offerToReceiveVideo: true, + }); + + if (offer?.sdp?.match(/^a=rtpmap:\d+\s+AV1\/\d+/m)) { + hasCodec = true; + } + peerConnection.close(); + } catch (error) { + return false; + } + + return hasCodec; + } +} diff --git a/src/index.ts b/src/index.ts index ac1bd5d..63ac913 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ export * from './browser-info'; +export * from './codec-info'; export * from './cpu-info'; export * from './system-info'; export * from './web-capabilities'; From dd6253990537379126fc0052ddaf5c595760075a Mon Sep 17 00:00:00 2001 From: Filip Nowakowski Date: Mon, 3 Nov 2025 15:19:45 +0100 Subject: [PATCH 2/5] refactor: remove CodecInfo class and related tests --- src/codec-info.spec.ts | 71 ------------------------------------ src/codec-info.ts | 54 --------------------------- src/index.ts | 1 - src/web-capabilities.spec.ts | 35 ++++++++++++++++++ src/web-capabilities.ts | 22 +++++++++++ 5 files changed, 57 insertions(+), 126 deletions(-) delete mode 100644 src/codec-info.spec.ts delete mode 100644 src/codec-info.ts diff --git a/src/codec-info.spec.ts b/src/codec-info.spec.ts deleted file mode 100644 index 270fdb4..0000000 --- a/src/codec-info.spec.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { CodecInfo } from './codec-info'; - -describe('CodecInfo', () => { - describe('isH264Available', () => { - it('should return true when the H.264 codec is available', async () => { - expect.assertions(1); - - Object.defineProperty(window, 'RTCPeerConnection', { - writable: true, - value: jest.fn().mockReturnValue({ - createOffer: jest.fn().mockResolvedValue({ - sdp: 'a=rtpmap:124 H264/90000', - }), - close: jest.fn(), - } as unknown as RTCPeerConnection), - }); - - await expect(CodecInfo.isH264Available()).resolves.toBe(true); - }); - - it('should return false when the H.264 codec is not available', async () => { - expect.assertions(1); - - Object.defineProperty(window, 'RTCPeerConnection', { - writable: true, - value: jest.fn().mockReturnValue({ - createOffer: jest.fn().mockResolvedValue({ - sdp: 'a=rtpmap:36 rtx/90000', - }), - close: jest.fn(), - } as unknown as RTCPeerConnection), - }); - - await expect(CodecInfo.isH264Available()).resolves.toBe(false); - }); - }); - - describe('isAV1Available', () => { - it('should return true when the AV1 codec is available', async () => { - expect.assertions(1); - - Object.defineProperty(window, 'RTCPeerConnection', { - writable: true, - value: jest.fn().mockReturnValue({ - createOffer: jest.fn().mockResolvedValue({ - sdp: 'a=rtpmap:124 AV1/90000', - }), - close: jest.fn(), - } as unknown as RTCPeerConnection), - }); - - await expect(CodecInfo.isAV1Available()).resolves.toBe(true); - }); - - it('should return false when the AV1 codec is not available', async () => { - expect.assertions(1); - - Object.defineProperty(window, 'RTCPeerConnection', { - writable: true, - value: jest.fn().mockReturnValue({ - createOffer: jest.fn().mockResolvedValue({ - sdp: 'a=rtpmap:36 rtx/90000', - }), - close: jest.fn(), - } as unknown as RTCPeerConnection), - }); - - await expect(CodecInfo.isAV1Available()).resolves.toBe(false); - }); - }); -}); diff --git a/src/codec-info.ts b/src/codec-info.ts deleted file mode 100644 index 4640342..0000000 --- a/src/codec-info.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * CodecInfo class to provide static methods for codec information. - */ -export class CodecInfo { - /** - * Notifies the user whether or not the H.264 - * codec is present. - * @returns -boolean. - */ - static async isH264Available(): Promise { - let hasCodec = false; - - try { - const peerConnection = new window.RTCPeerConnection(); - const offer = await peerConnection.createOffer({ - offerToReceiveVideo: true, - }); - - if (offer?.sdp?.match(/^a=rtpmap:\d+\s+H264\/\d+/m)) { - hasCodec = true; - } - peerConnection.close(); - } catch (error) { - return false; - } - - return hasCodec; - } - - /** - * Notifies the user whether or not the H.264 - * codec is present. - * @returns -boolean. - */ - static async isAV1Available(): Promise { - let hasCodec = false; - - try { - const peerConnection = new window.RTCPeerConnection(); - const offer = await peerConnection.createOffer({ - offerToReceiveVideo: true, - }); - - if (offer?.sdp?.match(/^a=rtpmap:\d+\s+AV1\/\d+/m)) { - hasCodec = true; - } - peerConnection.close(); - } catch (error) { - return false; - } - - return hasCodec; - } -} diff --git a/src/index.ts b/src/index.ts index 63ac913..ac1bd5d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,4 @@ export * from './browser-info'; -export * from './codec-info'; export * from './cpu-info'; export * from './system-info'; export * from './web-capabilities'; diff --git a/src/web-capabilities.spec.ts b/src/web-capabilities.spec.ts index f55d1f9..2f958a5 100644 --- a/src/web-capabilities.spec.ts +++ b/src/web-capabilities.spec.ts @@ -95,4 +95,39 @@ describe('WebCapabilities', () => { expect(WebCapabilities.supportsEncodedStreamTransforms()).toBe(CapabilityState.NOT_CAPABLE); }); }); + describe('isCapableOfReceivingVideoCodec', () => { + afterEach(() => { + // Clean up window modifications + delete (window as Window & { RTCRtpReceiver?: unknown }).RTCRtpReceiver; + }); + + it('should return CAPABLE when the codec is supported', () => { + expect.assertions(1); + Object.defineProperty(window, 'RTCRtpReceiver', { + writable: true, + value: { + getCapabilities: jest.fn().mockReturnValue({ + codecs: [{ mimeType: 'video/AV1' }], + } as unknown as RTCRtpCapabilities), + }, + }); + expect(WebCapabilities.isCapableOfReceivingVideoCodec('video/AV1')).toBe( + CapabilityState.CAPABLE + ); + }); + it('should return NOT_CAPABLE when the codec is not supported', () => { + expect.assertions(1); + Object.defineProperty(window, 'RTCRtpReceiver', { + writable: true, + value: { + getCapabilities: jest.fn().mockReturnValue({ + codecs: [{ mimeType: 'video/H264' }], + } as unknown as RTCRtpCapabilities), + }, + }); + expect(WebCapabilities.isCapableOfReceivingVideoCodec('video/AV1')).toBe( + CapabilityState.NOT_CAPABLE + ); + }); + }); }); diff --git a/src/web-capabilities.ts b/src/web-capabilities.ts index 6ee10b7..41713ca 100644 --- a/src/web-capabilities.ts +++ b/src/web-capabilities.ts @@ -90,4 +90,26 @@ export class WebCapabilities { ? CapabilityState.CAPABLE : CapabilityState.NOT_CAPABLE; } + + /** + * Checks whether the browser is capable of receiving a specific video codec. + * + * @param mimeType - The MIME type of the codec to check. + * @returns A {@link CapabilityState}. + * @example + * ```javascript + * const state = WebCapabilities.isCapableOfReceivingVideoCodec('video/AV1'); + * if (state === CapabilityState.CAPABLE) { + * console.log('The browser is capable of receiving AV1 video.'); + * } else { + * console.log('The browser is not capable of receiving AV1 video.'); + * } + * ``` + */ + static isCapableOfReceivingVideoCodec(mimeType: string): CapabilityState { + const codecs = RTCRtpReceiver.getCapabilities('video')?.codecs || []; + return codecs.some((codec) => codec.mimeType === mimeType) + ? CapabilityState.CAPABLE + : CapabilityState.NOT_CAPABLE; + } } From 4a5b1bb8c18bd3fa53d145e50e95f1b4f8741cdd Mon Sep 17 00:00:00 2001 From: Filip Nowakowski Date: Tue, 4 Nov 2025 12:37:13 +0100 Subject: [PATCH 3/5] refactor: update WebCapabilities methods for codec support checks --- src/web-capabilities.spec.ts | 5 +++++ src/web-capabilities.ts | 25 ++++++++----------------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/web-capabilities.spec.ts b/src/web-capabilities.spec.ts index 2f958a5..b911938 100644 --- a/src/web-capabilities.spec.ts +++ b/src/web-capabilities.spec.ts @@ -103,6 +103,7 @@ describe('WebCapabilities', () => { it('should return CAPABLE when the codec is supported', () => { expect.assertions(1); + Object.defineProperty(window, 'RTCRtpReceiver', { writable: true, value: { @@ -111,12 +112,15 @@ describe('WebCapabilities', () => { } as unknown as RTCRtpCapabilities), }, }); + expect(WebCapabilities.isCapableOfReceivingVideoCodec('video/AV1')).toBe( CapabilityState.CAPABLE ); }); + it('should return NOT_CAPABLE when the codec is not supported', () => { expect.assertions(1); + Object.defineProperty(window, 'RTCRtpReceiver', { writable: true, value: { @@ -125,6 +129,7 @@ describe('WebCapabilities', () => { } as unknown as RTCRtpCapabilities), }, }); + expect(WebCapabilities.isCapableOfReceivingVideoCodec('video/AV1')).toBe( CapabilityState.NOT_CAPABLE ); diff --git a/src/web-capabilities.ts b/src/web-capabilities.ts index 41713ca..0487cdf 100644 --- a/src/web-capabilities.ts +++ b/src/web-capabilities.ts @@ -81,34 +81,25 @@ export class WebCapabilities { } /** - * Checks whether the browser supports encoded stream transforms. + * Checks whether the browser is capable of receiving a specific video codec. * + * @param mimeType - The MIME type of the codec to check. * @returns A {@link CapabilityState}. */ - static supportsEncodedStreamTransforms(): CapabilityState { - return window.RTCRtpSender && 'transform' in RTCRtpSender.prototype + static isCapableOfReceivingVideoCodec(mimeType: string): CapabilityState { + const codecs = RTCRtpReceiver.getCapabilities('video')?.codecs || []; + return codecs.some((codec) => codec.mimeType === mimeType) ? CapabilityState.CAPABLE : CapabilityState.NOT_CAPABLE; } /** - * Checks whether the browser is capable of receiving a specific video codec. + * Checks whether the browser supports encoded stream transforms. * - * @param mimeType - The MIME type of the codec to check. * @returns A {@link CapabilityState}. - * @example - * ```javascript - * const state = WebCapabilities.isCapableOfReceivingVideoCodec('video/AV1'); - * if (state === CapabilityState.CAPABLE) { - * console.log('The browser is capable of receiving AV1 video.'); - * } else { - * console.log('The browser is not capable of receiving AV1 video.'); - * } - * ``` */ - static isCapableOfReceivingVideoCodec(mimeType: string): CapabilityState { - const codecs = RTCRtpReceiver.getCapabilities('video')?.codecs || []; - return codecs.some((codec) => codec.mimeType === mimeType) + static supportsEncodedStreamTransforms(): CapabilityState { + return window.RTCRtpSender && 'transform' in RTCRtpSender.prototype ? CapabilityState.CAPABLE : CapabilityState.NOT_CAPABLE; } From 01dd296c9609a6676a998ba54f150446a77cb744 Mon Sep 17 00:00:00 2001 From: evujici Date: Thu, 6 Nov 2025 11:07:11 +0100 Subject: [PATCH 4/5] chore(web-capabilities): add the correct mimeType type --- src/web-capabilities.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/web-capabilities.ts b/src/web-capabilities.ts index 0487cdf..168b7db 100644 --- a/src/web-capabilities.ts +++ b/src/web-capabilities.ts @@ -86,7 +86,9 @@ export class WebCapabilities { * @param mimeType - The MIME type of the codec to check. * @returns A {@link CapabilityState}. */ - static isCapableOfReceivingVideoCodec(mimeType: string): CapabilityState { + static isCapableOfReceivingVideoCodec( + mimeType: RTCRtpCodecCapability['mimeType'] + ): CapabilityState { const codecs = RTCRtpReceiver.getCapabilities('video')?.codecs || []; return codecs.some((codec) => codec.mimeType === mimeType) ? CapabilityState.CAPABLE From ae2d3c5c2c5af5632cad2e377a744f8b254429c7 Mon Sep 17 00:00:00 2001 From: evujici Date: Thu, 6 Nov 2025 11:18:40 +0100 Subject: [PATCH 5/5] test(web-capabilities): add test for undefined return --- src/web-capabilities.spec.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/web-capabilities.spec.ts b/src/web-capabilities.spec.ts index b911938..57fde12 100644 --- a/src/web-capabilities.spec.ts +++ b/src/web-capabilities.spec.ts @@ -134,5 +134,20 @@ describe('WebCapabilities', () => { CapabilityState.NOT_CAPABLE ); }); + + it('should return NOT_CAPABLE when getCapabilities returns undefined', () => { + expect.assertions(1); + + Object.defineProperty(window, 'RTCRtpReceiver', { + writable: true, + value: { + getCapabilities: jest.fn().mockReturnValue(undefined), + }, + }); + + expect(WebCapabilities.isCapableOfReceivingVideoCodec('video/AV1')).toBe( + CapabilityState.NOT_CAPABLE + ); + }); }); });