Skip to content

Commit 6c0aabd

Browse files
authored
Merge pull request #9028 from daily-co/pre-939-bug-mediastreamtracks-fail-deep-equality-comparison
PRE-939 Implement custom deepEqual function to correctly handle MediaStreamTracks
2 parents b697e25 + 2268779 commit 6c0aabd

File tree

5 files changed

+254
-3
lines changed

5 files changed

+254
-3
lines changed

jest-setup.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
11
import '@testing-library/jest-dom';
2+
import { FakeMediaStreamTrack } from 'fake-mediastreamtrack';
3+
import faker from 'faker';
24

35
class MediaStream {
6+
active: boolean;
7+
id: string;
48
tracks: MediaStreamTrack[] = [];
59

6-
constructor(tracks: MediaStreamTrack[]) {
10+
constructor(tracks: MediaStreamTrack[] = [], id?: string) {
711
this.tracks = tracks;
12+
this.id = id ?? faker.datatype.uuid();
13+
this.active = true;
14+
}
15+
16+
addTrack(track: MediaStreamTrack) {
17+
this.tracks.push(track);
18+
}
19+
20+
removeTrack(track: MediaStreamTrack) {
21+
this.tracks = this.tracks.filter(t => t.id !== track.id);
822
}
923

1024
getAudioTracks() {
@@ -18,12 +32,21 @@ class MediaStream {
1832
getTracks() {
1933
return this.tracks;
2034
}
35+
36+
stop() {
37+
this.active = false;
38+
}
2139
}
2240

2341
Object.defineProperty(window, 'MediaStream', {
2442
value: MediaStream,
2543
});
2644

45+
Object.defineProperty(window, 'MediaStreamTrack', {
46+
writable: true,
47+
value: FakeMediaStreamTrack,
48+
})
49+
2750
Object.defineProperty(HTMLVideoElement.prototype, 'load', {
2851
value: () => {},
2952
});

src/DailyProvider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import DailyIframe, {
44
DailyEventObject,
55
DailyFactoryOptions,
66
} from '@daily-co/daily-js';
7-
import deepEqual from 'fast-deep-equal';
87
import React, { useCallback, useEffect, useRef, useState } from 'react';
98
import { RecoilRoot, RecoilRootProps } from 'recoil';
109

@@ -16,6 +15,7 @@ import { DailyMeeting } from './DailyMeeting';
1615
import { DailyParticipants } from './DailyParticipants';
1716
import { DailyRecordings } from './DailyRecordings';
1817
import { DailyRoom } from './DailyRoom';
18+
import { deepEqual } from './lib/deepEqual';
1919

2020
type BaseProps =
2121
| DailyFactoryOptions

src/hooks/useParticipantProperty.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import deepEqual from 'fast-deep-equal';
21
import { useCallback, useEffect, useState } from 'react';
32
import {
43
selectorFamily,
@@ -11,6 +10,7 @@ import {
1110
participantState,
1211
} from '../DailyParticipants';
1312
import { RECOIL_PREFIX } from '../lib/constants';
13+
import { deepEqual } from '../lib/deepEqual';
1414
import type { NumericKeys } from '../types/NumericKeys';
1515
import type { Paths } from '../types/paths';
1616
import type { PathValue } from '../types/pathValue';

src/lib/deepEqual.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/**
2+
* Compares two variables for deep equality.
3+
* Gracefully handles equality checks on MediaStreamTracks by comparing their ids.
4+
*/
5+
export function deepEqual(a: any, b: any): boolean {
6+
if (a === b) return true;
7+
8+
// Handle special case for MediaStream
9+
if (a instanceof MediaStream && b instanceof MediaStream) {
10+
return (
11+
a.id === b.id &&
12+
a.active === b.active &&
13+
a.getTracks().length === b.getTracks().length &&
14+
a.getTracks().every((track, idx) => deepEqual(track, b.getTracks()[idx]))
15+
);
16+
}
17+
18+
// Handle special case for MediaStreamTrack
19+
if (a instanceof MediaStreamTrack && b instanceof MediaStreamTrack) {
20+
return a.id === b.id && a.kind === b.kind && a.readyState === b.readyState;
21+
}
22+
23+
// Handle special case for Date
24+
if (a instanceof Date && b instanceof Date) {
25+
return a.getTime() === b.getTime();
26+
}
27+
28+
// Handle special case for RegExp
29+
if (a instanceof RegExp && b instanceof RegExp) {
30+
return a.source === b.source && a.flags === b.flags;
31+
}
32+
33+
// If a or b are not objects or null, they can't be deeply equal
34+
if (
35+
typeof a !== 'object' ||
36+
a === null ||
37+
typeof b !== 'object' ||
38+
b === null
39+
) {
40+
return false;
41+
}
42+
43+
// Get the keys of a and b. This handles both arrays and objects, since arrays are technically objects.
44+
let keysA = Object.keys(a);
45+
let keysB = Object.keys(b);
46+
47+
// If the number of keys are different, the objects are not equal
48+
if (keysA.length !== keysB.length) return false;
49+
50+
// Construct unique set of all keys in both a and b
51+
const keysSet = new Set([...keysA, ...keysB]);
52+
53+
for (let key of keysSet) {
54+
if (
55+
// If key exists in one, but not the other -> not equal
56+
(key in a && !(key in b)) ||
57+
(key in b && !(key in a)) ||
58+
// Both keys exist in both object -> run nested equality check
59+
!deepEqual(a[key], b[key])
60+
)
61+
return false;
62+
}
63+
64+
// All keys and values match -> the objects are deeply equal
65+
return true;
66+
}

test/lib/deepEqual.test.ts

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/// <reference types="@types/jest" />
2+
3+
import { FakeMediaStreamTrack } from 'fake-mediastreamtrack';
4+
5+
import { deepEqual } from '../../src/lib/deepEqual';
6+
7+
describe('deepEqual', () => {
8+
describe('Primitives and simple types', () => {
9+
it.each`
10+
a | b | expected
11+
${1} | ${1} | ${true}
12+
${'string'} | ${'string'} | ${true}
13+
${'a'} | ${'b'} | ${false}
14+
${null} | ${null} | ${true}
15+
${undefined} | ${undefined} | ${true}
16+
${42} | ${'42'} | ${false}
17+
`('returns $expected for a: $a and b: $b', ({ a, b, expected }) => {
18+
expect(deepEqual(a, b)).toBe(expected);
19+
});
20+
});
21+
22+
describe('Arrays', () => {
23+
it.each`
24+
a | b | expected
25+
${[1, 2, 3]} | ${[1, 2, 3]} | ${true}
26+
${[1, 2, 3]} | ${[3, 2, 1]} | ${false}
27+
${[1, [2, 3]]} | ${[1, [2, 3]]} | ${true}
28+
${[1, [2, 3]]} | ${[1, 2, 3]} | ${false}
29+
${[]} | ${[]} | ${true}
30+
`('returns $expected for a: $a and b: $b', ({ a, b, expected }) => {
31+
expect(deepEqual(a, b)).toBe(expected);
32+
});
33+
});
34+
35+
describe('Objects', () => {
36+
it.each`
37+
a | b | expected
38+
${{ key: 'value' }} | ${{ key: 'value' }} | ${true}
39+
${{ key: 'value' }} | ${{ key: 'different' }} | ${false}
40+
${{ key1: 'value', key2: { nestedKey: 'nestedValue' } }} | ${{ key1: 'value', key2: { nestedKey: 'nestedValue' } }} | ${true}
41+
${{ key1: 'value' }} | ${{ key2: 'value' }} | ${false}
42+
${{}} | ${{}} | ${true}
43+
`('returns $expected for a: $a and b: $b', ({ a, b, expected }) => {
44+
expect(deepEqual(a, b)).toBe(expected);
45+
});
46+
});
47+
48+
describe('MediaStream', () => {
49+
const streamA = new MediaStream();
50+
afterEach(() => {
51+
streamA.getTracks().forEach((track) => {
52+
streamA.removeTrack(track);
53+
track.stop();
54+
});
55+
});
56+
it('returns true when streams are equal', () => {
57+
expect(deepEqual(streamA, streamA)).toBe(true);
58+
});
59+
it('returns true when equal stream has the same tracks', () => {
60+
const track = new FakeMediaStreamTrack({ kind: 'audio' });
61+
streamA.addTrack(track);
62+
expect(deepEqual(streamA, streamA)).toBe(true);
63+
});
64+
it('returns false when streams are not equal', () => {
65+
const streamB = new MediaStream();
66+
expect(deepEqual(streamA, streamB)).toBe(false);
67+
});
68+
it('returns false when amount of tracks differs', () => {
69+
const track = new FakeMediaStreamTrack({ kind: 'audio' });
70+
// @ts-ignore
71+
const streamC = new MediaStream([], streamA.id);
72+
streamA.addTrack(track);
73+
expect(deepEqual(streamA, streamC)).toBe(false);
74+
});
75+
it('returns false when track id differs', () => {
76+
const track1 = new FakeMediaStreamTrack({ kind: 'audio' });
77+
const track2 = new FakeMediaStreamTrack({ kind: 'audio' });
78+
// Uses mock from jest-setup.ts, but TypeScript still thinks this is a valid MediaStream
79+
// @ts-ignore
80+
const streamC = new MediaStream([], streamA.id);
81+
streamA.addTrack(track1);
82+
streamC.addTrack(track2);
83+
expect(deepEqual(streamA, streamC)).toBe(false);
84+
});
85+
});
86+
87+
describe('MediaStreamTracks', () => {
88+
it.each`
89+
a | b | expected
90+
${new FakeMediaStreamTrack({ id: '1', kind: 'audio', readyState: 'live' })} | ${new FakeMediaStreamTrack({ id: '1', kind: 'audio', readyState: 'live' })} | ${true}
91+
${new FakeMediaStreamTrack({ id: '1', kind: 'audio', readyState: 'live' })} | ${new FakeMediaStreamTrack({ id: '2', kind: 'audio', readyState: 'live' })} | ${false}
92+
${new FakeMediaStreamTrack({ id: '1', kind: 'audio', readyState: 'live' })} | ${new FakeMediaStreamTrack({ id: '1', kind: 'video', readyState: 'live' })} | ${false}
93+
${new FakeMediaStreamTrack({ id: '1', kind: 'audio', readyState: 'live' })} | ${new FakeMediaStreamTrack({ id: '1', kind: 'audio', readyState: 'ended' })} | ${false}
94+
`('returns $expected for a: $a and b: $b', ({ a, b, expected }) => {
95+
expect(deepEqual(a, b)).toBe(expected);
96+
});
97+
});
98+
99+
describe('Date objects', () => {
100+
it.each`
101+
a | b | expected
102+
${new Date('2023-01-01')} | ${new Date('2023-01-01')} | ${true}
103+
${new Date('2023-01-01')} | ${new Date('2022-01-01')} | ${false}
104+
${new Date('2023-01-01')} | ${'2023-01-01'} | ${false}
105+
`('returns $expected for a: $a and b: $b', ({ a, b, expected }) => {
106+
expect(deepEqual(a, b)).toBe(expected);
107+
});
108+
});
109+
110+
describe('RegExp objects', () => {
111+
it.each`
112+
a | b | expected
113+
${/test/gi} | ${/test/gi} | ${true}
114+
${/test/g} | ${/test/gi} | ${false}
115+
${/test/g} | ${'/test/g'} | ${false}
116+
`('returns $expected for a: $a and b: $b', ({ a, b, expected }) => {
117+
expect(deepEqual(a, b)).toBe(expected);
118+
});
119+
});
120+
121+
describe('Functions', () => {
122+
const funcA = () => 'test';
123+
const funcB = () => 'test';
124+
it.each`
125+
a | b | expected
126+
${funcA} | ${funcA} | ${true}
127+
${funcA} | ${funcB} | ${false}
128+
`('returns $expected for a: $a and b: $b', ({ a, b, expected }) => {
129+
expect(deepEqual(a, b)).toBe(expected);
130+
});
131+
});
132+
133+
describe('Complex nested objects', () => {
134+
const complexObjA = {
135+
string: 'value',
136+
number: 42,
137+
bool: false,
138+
date: new Date('2023-01-01'),
139+
regex: /test/g,
140+
array: [1, { key: 'nested' }, [2, 3]],
141+
func: () => 'test',
142+
};
143+
144+
const complexObjB = {
145+
...complexObjA,
146+
array: [1, { key: 'nested' }, [2, 3]], // Identical but different reference
147+
};
148+
149+
const complexObjC = {
150+
...complexObjA,
151+
string: 'different',
152+
};
153+
154+
it.each`
155+
a | b | expected
156+
${complexObjA} | ${complexObjB} | ${true}
157+
${complexObjA} | ${complexObjC} | ${false}
158+
`('returns $expected for a: $a and b: $b', ({ a, b, expected }) => {
159+
expect(deepEqual(a, b)).toBe(expected);
160+
});
161+
});
162+
});

0 commit comments

Comments
 (0)