diff --git a/src/js/utils.js b/src/js/utils.js index c9503b0d..e6c5f1a9 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -31,13 +31,20 @@ export function wrapPeerConnectionEvent(window, eventNameToWrap, wrapper) { if (!window.RTCPeerConnection) { return; } - const addEventListener = Object.getOwnPropertyDescriptor( - EventTarget.prototype, 'addEventListener'); - if (!addEventListener.writable) { + + const proto = window.RTCPeerConnection.prototype; + + const isAddEventListenerWritable = + tryMakePropertyWritable(proto, 'addEventListener'); + const isRemoveEventListenerWritable = + tryMakePropertyWritable(proto, 'removeEventListener'); + const canPolyfill = + isAddEventListenerWritable && isRemoveEventListenerWritable; + + if (!canPolyfill) { log('Unable to polyfill events'); return; } - const proto = window.RTCPeerConnection.prototype; const nativeAddEventListener = proto.addEventListener; proto.addEventListener = function(nativeEventName, cb) { @@ -104,6 +111,28 @@ export function wrapPeerConnectionEvent(window, eventNameToWrap, wrapper) { }); } +/** + * Tries to ensure the property is writable. + * If the property is read-only but configurable, + * it will be redefined to be writable. + * @param {object} target the object to check the property on. + * @param {string} property the property to check. + * @return {boolean} true if the property is writable, false otherwise. + */ +export function tryMakePropertyWritable(target, property) { + try { + Object.defineProperty(target, property, { + value: target[property], + writable: true, + configurable: true, + }); + + return true; + } catch (e) { + return false; + } +} + export function disableLog(bool) { if (typeof bool !== 'boolean') { return new Error('Argument type: ' + typeof bool + diff --git a/test/unit/wrapPeerConnectionEvent.test.js b/test/unit/wrapPeerConnectionEvent.test.js new file mode 100644 index 00000000..6b4298bd --- /dev/null +++ b/test/unit/wrapPeerConnectionEvent.test.js @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2026 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + +describe('wrapPeerConnectionEvent', () => { + const wrapPeerConnectionEvent = + require('../../dist/utils.js').wrapPeerConnectionEvent; + let window; + let RTCPeerConnection; + let addEventListener; + let removeEventListener; + + beforeEach(() => { + addEventListener = jest.fn(); + removeEventListener = jest.fn(); + RTCPeerConnection = jest.fn(); + + window = { + RTCPeerConnection, + }; + }); + + it('should patch RTCPeerConnection if the props are writable', () => { + changeAccessorDescriptor({writable: false, configurable: true}); + + wrapPeerConnectionEvent(window, 'track', (e) => { + return {type: e.type, wrapped: true}; + }); + + expect(RTCPeerConnection.prototype.addEventListener) + .not.toBe(addEventListener); + expect(RTCPeerConnection.prototype.removeEventListener) + .not.toBe(removeEventListener); + }); + + it('shouldn\'t patch RTCPeerConnection if the props are not configurable', + () => { + changeAccessorDescriptor({writable: false, configurable: false}); + + wrapPeerConnectionEvent(window, 'track', () => { + return null; + }); + + expect(RTCPeerConnection.prototype.addEventListener) + .toBe(addEventListener); + expect(RTCPeerConnection.prototype.removeEventListener) + .toBe(removeEventListener); + }); + + function changeAccessorDescriptor( + {writable, configurable}, + ) { + Object.defineProperty(RTCPeerConnection.prototype, 'addEventListener', { + value: addEventListener, + writable, + configurable, + }); + Object.defineProperty(RTCPeerConnection.prototype, 'removeEventListener', { + value: removeEventListener, + writable, + configurable, + }); + } +}); +