diff --git a/src/polyfill/RTCDataChannel.ts b/src/polyfill/RTCDataChannel.ts index 4300361..38b9940 100644 --- a/src/polyfill/RTCDataChannel.ts +++ b/src/polyfill/RTCDataChannel.ts @@ -30,7 +30,7 @@ export default class RTCDataChannel extends EventTarget implements globalThis.RT super(); this.#dataChannel = dataChannel; - this.#binaryType = 'blob'; + this.#binaryType = 'arraybuffer'; this.#readyState = this.#dataChannel.isOpen() ? 'open' : 'connecting'; this.#bufferedAmountLowThreshold = 0; this.#maxPacketLifeTime = opts.maxPacketLifeTime ?? null; @@ -77,10 +77,29 @@ export default class RTCDataChannel extends EventTarget implements globalThis.RT this.dispatchEvent(new Event('bufferedamountlow')); }); - this.#dataChannel.onMessage((data) => { - if (ArrayBuffer.isView(data)) { - if (this.binaryType == 'arraybuffer') data = data.buffer; - else data = Buffer.from(data.buffer); + this.#dataChannel.onMessage((message) => { + if (typeof message === 'string') { + this.dispatchEvent(new MessageEvent('message', { data: message })); + return + } + + let data: Blob | ArrayBuffer; + + if (message instanceof ArrayBuffer) { + data = message; + } else { + data = message.buffer; + + if (message.byteOffset !== 0 || message.byteLength !== message.buffer.byteLength) { + // message is view on underlying buffer, must create new + // ArrayBuffer that only contains message data + data = new ArrayBuffer(message.byteLength); + new Uint8Array(data, 0, message.byteLength).set(message); + } + } + + if (this.#binaryType === 'blob') { + data = new Blob([data]); } this.dispatchEvent(new MessageEvent('message', { data })); diff --git a/test/jest-tests/polyfill.test.ts b/test/jest-tests/polyfill.test.ts index 96ebf70..ee72770 100644 --- a/test/jest-tests/polyfill.test.ts +++ b/test/jest-tests/polyfill.test.ts @@ -4,6 +4,19 @@ import { RTCPeerConnection } from '../../src/polyfill/index'; import { PeerConnection } from '../../src/lib/index'; import { eventPromise } from '../fixtures/event-promise'; +// Polyfill for Promise.withResolvers for Node < 20 +if (!Promise.withResolvers) { + (Promise as any).withResolvers = function (): any { + let resolve: (value: T | PromiseLike) => void; + let reject: (reason?: any) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve: resolve!, reject: reject! }; + }; +} + describe('polyfill', () => { // Default is 5000 ms but we need more jest.setTimeout(30000); @@ -20,213 +33,234 @@ describe('polyfill', () => { expect(pc).toBeTruthy(); }); - test('P2P Test', () => { - return new Promise((done) => { - // Mocks - const p1ConnectionStateMock = jest.fn(); - const p1IceConnectionStateMock = jest.fn(); - const p1IceGatheringStateMock = jest.fn(); - const p1IceCandidateMock = jest.fn(); - const p1SDPMock = jest.fn(); - const p1DCMock = jest.fn(); - const p1MessageMock = jest.fn(); - const p2ConnectionStateMock = jest.fn(); - const p2IceConnectionStateMock = jest.fn(); - const p2IceGatheringStateMock = jest.fn(); - const p2IceCandidateMock = jest.fn(); - const p2SDPMock = jest.fn(); - const p2DCMock = jest.fn(); - const p2MessageMock = jest.fn(); - - const peer1 = new RTCPeerConnection({ - peerIdentity: 'peer1', - iceServers: [{ urls: ['stun:stun.l.google.com:19302'] }], - }); - - const peer2 = new RTCPeerConnection({ - peerIdentity: 'peer2', - iceServers: [{ urls: ['stun:stun.l.google.com:19302'] }], - }); - - let dc1: RTCDataChannel = null; - let dc2: RTCDataChannel = null; - - // Creates a fixed binary data for testing - function createBinaryTestData(): ArrayBufferView { - const binaryData = new Uint8Array(17); - const dv = new DataView(binaryData.buffer); - dv.setInt8(0, 123); - dv.setFloat32(1, 123.456); - dv.setUint32(5, 987654321); - dv.setFloat64(9, 789.012); - return binaryData; - } - - // Compares the received binary data to the expected value of the fixed binary data - function analyzeBinaryTestData(binaryData: ArrayBufferLike): boolean { - const dv = new DataView(binaryData); - return ( - dv.getInt8(0) == 123 && - dv.getFloat32(1) == Math.fround(123.456) && - dv.getUint32(5) == 987654321 && - dv.getFloat64(9) == 789.012 - ); - } + test('P2P Test', async () => { + // Mocks + const p1ConnectionStateMock = jest.fn(); + const p1IceConnectionStateMock = jest.fn(); + const p1IceGatheringStateMock = jest.fn(); + const p1IceCandidateMock = jest.fn(); + const p1SDPMock = jest.fn(); + const p1DCMock = jest.fn(); + const p1MessageMock = jest.fn(); + const p2ConnectionStateMock = jest.fn(); + const p2IceConnectionStateMock = jest.fn(); + const p2IceGatheringStateMock = jest.fn(); + const p2IceCandidateMock = jest.fn(); + const p2SDPMock = jest.fn(); + const p2DCMock = jest.fn(); + const p2MessageMock = jest.fn(); + + const peer1 = new RTCPeerConnection({ + peerIdentity: 'peer1', + iceServers: [{ urls: ['stun:stun.l.google.com:19302'] }], + }); - // We will set the "binaryType" and then send/receive the "data" from the datachannel in each test, and then compare them. - // For example, the first line will send a "Hello" string after setting binaryType to "arraybuffer". - const testMessages = [ - { binaryType: 'arraybuffer', data: 'Hello' }, - { binaryType: 'arraybuffer', data: createBinaryTestData() }, - { binaryType: 'blob', data: createBinaryTestData() }, - ]; - - // Index of the message in testMessages that we are currently testing. - let currentIndex: number = -1; - - // We run this function to analyze the data just after receiving it from the datachannel. - function analyzeData(idx: number, data: string | Buffer | ArrayBuffer): boolean { - switch (idx) { - case 0: // binaryType is not used here because data is a string ("Hello"). - return (data as string) == testMessages[idx].data; - case 1: // binaryType is "arraybuffer" and data is expected to be an ArrayBuffer. - return analyzeBinaryTestData(data as ArrayBufferLike); - case 2: // binaryType is "blob" and data is expected to be a Buffer. - return analyzeBinaryTestData((data as Buffer).buffer); - } - return false; - } + const peer2 = new RTCPeerConnection({ + peerIdentity: 'peer2', + iceServers: [{ urls: ['stun:stun.l.google.com:19302'] }], + }); - function finalizeTest(): void { - peer1.close(); - peer2.close(); + let dc1: RTCDataChannel = null; + let dc2: RTCDataChannel = null; + + // Creates a fixed binary data for testing + function createBinaryTestData(binaryData = new ArrayBuffer(17), offset = 0): ArrayBufferView { + const dv = new DataView(binaryData, offset); + dv.setInt8(0, 123); + dv.setFloat32(1, 123.456); + dv.setUint32(5, 987654321); + dv.setFloat64(9, 789.012); + return new Uint8Array(binaryData, offset, 17); + } - // State Callbacks - expect(p1ConnectionStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); - expect(p2ConnectionStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); - expect(p1IceConnectionStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); - expect(p2IceConnectionStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); - expect(p1IceGatheringStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); - expect(p2IceGatheringStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); + // Compares the received binary data to the expected value of the fixed binary data + function analyzeBinaryTestData(binaryData: ArrayBufferLike): boolean { + const dv = new DataView(binaryData); + return ( + dv.getInt8(0) == 123 && + dv.getFloat32(1) == Math.fround(123.456) && + dv.getUint32(5) == 987654321 && + dv.getFloat64(9) == 789.012 + ); + } - // SDP - expect(p1SDPMock.mock.calls.length).toBe(1); - expect(p2SDPMock.mock.calls.length).toBe(1); + // We will set the "binaryType" and then send/receive the "data" from the datachannel in each test, and then compare them. + // For example, the first line will send a "Hello" string after setting binaryType to "arraybuffer". + const testMessages = [ + { binaryType: 'arraybuffer', data: 'Hello' }, + { binaryType: 'arraybuffer', data: createBinaryTestData() }, + { binaryType: 'arraybuffer', data: createBinaryTestData(new ArrayBuffer(100)) }, + { binaryType: 'arraybuffer', data: createBinaryTestData(new ArrayBuffer(100), 50) }, + { binaryType: 'arraybuffer', data: createBinaryTestData(new ArrayBuffer(100), 50) }, + { binaryType: 'blob', data: 'Hello' }, + { binaryType: 'blob', data: new Blob([createBinaryTestData()]) }, + ]; + + const testMessageCount = Object.keys(testMessages).length; + + // Index of the message in testMessages that we are currently testing. + let currentIndex: number = -1; + + // We run this function to analyze the data just after receiving it from the datachannel. + async function analyzeData(idx: number, data: Blob | ArrayBuffer | string): Promise { + switch (idx) { + case 0: // binaryType is not used here because data is a string ("Hello"). + return data === 'Hello'; + case 1: // binaryType is "arraybuffer" and data is expected to be an ArrayBuffer. + return analyzeBinaryTestData(data as ArrayBufferLike); + case 2: // binaryType is "arraybuffer" and data is expected from a view on a larger ArrayBuffer + return analyzeBinaryTestData(data as ArrayBufferLike); + case 3: // binaryType is "arraybuffer" and data was created from a view on a larger ArrayBuffer with an offset + return analyzeBinaryTestData(data as ArrayBufferLike); + case 4: // binaryType is "arraybuffer" and data was created from a view on a larger ArrayBuffer with an offset + return analyzeBinaryTestData(data as ArrayBufferLike); + case 5: // binaryType is "blob" and data is expected to be a string ("Hello"). + return data === 'Hello' + case 6: // binaryType is "blob" and data is expected to be a Blob. + return analyzeBinaryTestData(await (data as Blob).arrayBuffer()); - // Candidates - expect(p1IceCandidateMock.mock.calls.length).toBeGreaterThanOrEqual(1); - expect(p2IceCandidateMock.mock.calls.length).toBeGreaterThanOrEqual(1); + } + return false; + } - // DataChannel - expect(p1DCMock.mock.calls.length).toBe(1); - expect(p2DCMock.mock.calls.length).toBe(1); + async function finalizeTest(): Promise { + peer1.close(); + peer2.close(); + + // State Callbacks + expect(p1ConnectionStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); + expect(p2ConnectionStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); + expect(p1IceConnectionStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); + expect(p2IceConnectionStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); + expect(p1IceGatheringStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); + expect(p2IceGatheringStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); + + // SDP + expect(p1SDPMock.mock.calls.length).toBe(1); + expect(p2SDPMock.mock.calls.length).toBe(1); + + // Candidates + expect(p1IceCandidateMock.mock.calls.length).toBeGreaterThanOrEqual(1); + expect(p2IceCandidateMock.mock.calls.length).toBeGreaterThanOrEqual(1); + + // DataChannel + expect(p1DCMock.mock.calls.length).toBe(1); + expect(p2DCMock.mock.calls.length).toBe(1); + + expect(p1MessageMock.mock.calls.length).toBe(testMessageCount); + expect(p2MessageMock.mock.calls.length).toBe(testMessageCount); + + // Analyze and compare received messages + for (let i = 0; i < testMessageCount; i++) { + expect(await analyzeData(i, p1MessageMock.mock.calls[i][0] as any)).toEqual(true); + expect(await analyzeData(i, p2MessageMock.mock.calls[i][0] as any)).toEqual(true); + } + } - expect(p1MessageMock.mock.calls.length).toBe(3); - expect(p2MessageMock.mock.calls.length).toBe(3); + // starts the next message-sending test + async function nextSendTest(): Promise { + // Get the next test data + const current = testMessages[++currentIndex]; - // Analyze and compare received messages - expect(analyzeData(0, p1MessageMock.mock.calls[0][0] as any)).toEqual(true); - expect(analyzeData(1, p1MessageMock.mock.calls[1][0] as any)).toEqual(true); - expect(analyzeData(2, p1MessageMock.mock.calls[2][0] as any)).toEqual(true); + if (!current) { + return; + } - expect(analyzeData(0, p2MessageMock.mock.calls[0][0] as any)).toEqual(true); - expect(analyzeData(1, p2MessageMock.mock.calls[1][0] as any)).toEqual(true); - expect(analyzeData(2, p2MessageMock.mock.calls[2][0] as any)).toEqual(true); + // Assign the binaryType value + dc1.binaryType = current.binaryType as BinaryType; - done(); + // dc2 also is initialized ? + if (dc2) { + dc2.binaryType = current.binaryType as BinaryType; } - // starts the next message-sending test - function nextSendTest(): void { - // Get the next test data - const current = testMessages[++currentIndex]; - - // If finished, quit - if (!current) { - finalizeTest(); - return; - } - - // Assign the binaryType value - dc1.binaryType = current.binaryType as BinaryType; - // dc2 also is initialized ? - if (dc2) { - dc2.binaryType = current.binaryType as BinaryType; - } - - // Send the test message - // workaround for https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1973 - if (typeof current.data === 'string') { - dc1.send(current.data); - } else { - dc1.send(current.data); - } - } + // Send the test message + dc1.send(current.data); + } - // Set Callbacks - peer1.onconnectionstatechange = (): void => { - p1ConnectionStateMock(); - }; - peer1.oniceconnectionstatechange = (): void => { - p1IceConnectionStateMock(); - }; - peer1.onicegatheringstatechange = (): void => { - p1IceGatheringStateMock(); - }; - peer1.onicecandidate = (e): void => { - p1IceCandidateMock(); - peer2.addIceCandidate(e.candidate); + // Set Callbacks + peer1.onconnectionstatechange = (): void => { + p1ConnectionStateMock(); + }; + peer1.oniceconnectionstatechange = (): void => { + p1IceConnectionStateMock(); + }; + peer1.onicegatheringstatechange = (): void => { + p1IceGatheringStateMock(); + }; + peer1.onicecandidate = (e): void => { + p1IceCandidateMock(); + peer2.addIceCandidate(e.candidate); + }; + + // Set Callbacks + peer2.onconnectionstatechange = (): void => { + p2ConnectionStateMock(); + }; + peer2.oniceconnectionstatechange = (): void => { + p2IceConnectionStateMock(); + }; + peer2.onicegatheringstatechange = (): void => { + p2IceGatheringStateMock(); + }; + peer2.onicecandidate = (e): void => { + p2IceCandidateMock(); + peer1.addIceCandidate(e.candidate); + }; + peer2.ondatachannel = (dce): void => { + p2DCMock(); + dc2 = dce.channel; + dc2.onmessage = (msg): void => { + p2MessageMock(msg.data); + + // send the received message from peer2 back to peer1 + dc2.send(msg.data); }; + }; - // Set Callbacks - peer2.onconnectionstatechange = (): void => { - p2ConnectionStateMock(); - }; - peer2.oniceconnectionstatechange = (): void => { - p2IceConnectionStateMock(); - }; - peer2.onicegatheringstatechange = (): void => { - p2IceGatheringStateMock(); - }; - peer2.onicecandidate = (e): void => { - p2IceCandidateMock(); - peer1.addIceCandidate(e.candidate); - }; - peer2.ondatachannel = (dce): void => { - p2DCMock(); - dc2 = dce.channel; - dc2.onmessage = (msg): void => { - p2MessageMock(msg.data); - - // send the received message from peer2 back to peer1 - dc2.send(msg.data); - }; - }; + // Actions + peer1.createOffer().then((desc) => { + p1SDPMock(); + peer2.setRemoteDescription(desc); + }); + //.catch((err) => console.error(err)); - // Actions - peer1.createOffer().then((desc) => { - p1SDPMock(); - peer2.setRemoteDescription(desc); - }); - //.catch((err) => console.error(err)); + peer2.createAnswer().then((answerDesc) => { + p2SDPMock(); + peer1.setRemoteDescription(answerDesc); + }); + //.catch((err) => console.error('Couldn't create answer', err)); + + const sentAll = Promise.withResolvers(); - peer2.createAnswer().then((answerDesc) => { - p2SDPMock(); - peer1.setRemoteDescription(answerDesc); + dc1 = peer1.createDataChannel('test-p2p'); + dc1.onopen = (): void => { + p1DCMock(); + + nextSendTest().catch((err) => { + sentAll.reject(err); }); - //.catch((err) => console.error('Couldn't create answer', err)); + }; + dc1.onmessage = (msg): void => { + // peer2 sends all messages back to peer1 + p1MessageMock(msg.data); + + if (p1MessageMock.mock.calls.length === testMessageCount) { + finalizeTest() + .then(() => { + sentAll.resolve(); + }) + .catch((err) => { + sentAll.reject(err); + }); + } else { + nextSendTest().catch((err) => { + sentAll.reject(err); + }); + } + }; - dc1 = peer1.createDataChannel('test-p2p'); - dc1.onopen = (): void => { - p1DCMock(); - nextSendTest(); - }; - dc1.onmessage = (msg): void => { - // peer2 sends all messages back to peer1 - p1MessageMock(msg.data); - nextSendTest(); - }; - }); + await sentAll.promise; }); test('it can access datachannel informational fields after closing', async () => {