-
Notifications
You must be signed in to change notification settings - Fork 3
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
VIDCS-3273: Fix audio output selection crash in android Waiting Room #23
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧑🍳
"@types/react": "^18.0.28", | ||
"@types/react": "^18.3.3", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was me trying to get non-empty react types DefinitelyTyped/DefinitelyTyped#13251 (comment)
import { | ||
getActiveAudioOutputDevice as getVonageActiveAudioOutputDevice, | ||
setAudioOutputDevice as setVonageAudioOutputDevice, | ||
} from '@vonage/client-sdk-video'; | ||
import { useCallback, useEffect, useState } from 'react'; | ||
import { getActiveAudioOutputDevice, setAudioOutputDevice } from '@vonage/client-sdk-video'; | ||
|
||
export type AudioDeviceId = string | null | undefined; | ||
|
||
export type AudioOutputContextType = { | ||
audioOutput: string | undefined | null; | ||
setAudioOutput: (deviceId: AudioDeviceId) => Promise<void>; | ||
currentAudioOutputDevice: string | undefined | null; | ||
setAudioOutputDevice: (deviceId: AudioDeviceId) => Promise<void>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some light renaming/refactoring here. It was super confusing to know what updateAudioOutput
setAudioOutput
, setAudioOutputDevice
etc all did and how they differed
const InputDevices = ({ handleToggle, customLightBlueColor }: InputDevicesProps): ReactElement => { | ||
const { publisher, isPublishing } = usePublisherContext(); | ||
const [devicesAvailable, setDevicesAvailable] = useState<Device[] | null>(null); | ||
const { allMediaDevices } = useDevices(); | ||
const [options, setOptions] = useState<string[]>([]); | ||
const { publisher } = usePublisherContext(); | ||
const { | ||
allMediaDevices: { audioInputDevices }, | ||
} = useDevices(); | ||
|
||
useEffect(() => { | ||
setDevicesAvailable(allMediaDevices.audioInputDevices); | ||
}, [publisher, allMediaDevices, devicesAvailable, isPublishing]); | ||
const changeAudioSource = (deviceId: string) => { | ||
publisher?.setAudioSource(deviceId); | ||
}; | ||
|
||
useEffect(() => { | ||
if (devicesAvailable) { | ||
const audioDevicesAvailable: string[] = devicesAvailable.map((availableDevice: Device) => { | ||
return availableDevice.label; | ||
}); | ||
setOptions(audioDevicesAvailable); | ||
} | ||
}, [devicesAvailable]); | ||
const options = audioInputDevices.map((availableDevice: Device) => { | ||
return availableDevice.label; | ||
}); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These affects and state were not needed and caused multiple renders and chained state effects.
The state comes directly from useDevices, and the state we need can be derived from that state directly, without setting state again. This is how the react docs tells us to do things.
const { audioOutput, setAudioOutput } = useAudioOutputContext(); | ||
const [devicesAvailable, setDevicesAvailable] = useState<AudioOutputDevice[] | null>(null); | ||
const { allMediaDevices } = useDevices(); | ||
|
||
useEffect(() => { | ||
setDevicesAvailable(allMediaDevices.audioOutputDevices); | ||
}, [allMediaDevices]); | ||
const { currentAudioOutputDevice, setAudioOutputDevice } = useAudioOutputContext(); | ||
const { | ||
allMediaDevices: { audioOutputDevices }, | ||
} = useDevices(); | ||
|
||
const changeAudioOutput = async (deviceId: string) => { | ||
await setAudioOutputDevice(deviceId); | ||
setAudioOutput(deviceId); | ||
}; | ||
const isAudioOutputSupported = isGetActiveAudioOutputDeviceSupported(); | ||
|
||
useEffect(() => { | ||
const getActiveAudioOutputDeviceLabel = async () => { | ||
const activeOutputDevice = await getActiveAudioOutputDevice(); | ||
setAudioOutput(activeOutputDevice.deviceId); | ||
}; | ||
getActiveAudioOutputDeviceLabel(); | ||
}, [setAudioOutput]); | ||
const availableDevices = isAudioOutputSupported ? audioOutputDevices : defaultOutputDevices; | ||
|
||
const handleChangeAudioOutput = (event: MouseEvent<HTMLLIElement>) => { | ||
const handleChangeAudioOutput = async (event: MouseEvent<HTMLLIElement>) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as the above file, a lot of this was not needed, made testing hard, and caused multiple render chains before the state was right, when the state could be derived from props/hooks directly.
useEffect(() => { | ||
// Add an event listener to update device list when the list changes (such as plugging or unplugging a device) | ||
mediaDevices.addEventListener('devicechange', updateCurrentAudioOutputDevice); | ||
|
||
return () => { | ||
// Remove the event listener when component unmounts | ||
mediaDevices.removeEventListener('devicechange', updateCurrentAudioOutputDevice); | ||
}; | ||
}, [mediaDevices, updateCurrentAudioOutputDevice]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added this event listener to update the current device when the device list changes. This keeps the UI in sync if the current device was removed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good so far! Just have a few comments/questions. Let me know what you think!
play: playMock, | ||
pause: pauseMock, | ||
currentTime: 9001, | ||
setSinkId: vi.fn(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be undefined? The test name doesn't match the mock.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great catch! Luckily when I updated the value the test still passes 😅
const isSelected = device.deviceId === audioOutput; | ||
<MenuList data-testid="output-devices"> | ||
{availableDevices?.map((device: AudioOutputDevice) => { | ||
// If audio output device selection is not support we show the default device as selected |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit, typo
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
impressive job on this one! left a few questions, mostly just curious
mockGetAudioOutputDevices, | ||
mockSetAudioOutputDevice, | ||
mockGetActiveAudioOutputDevice, | ||
} = vi.hoisted(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorry just curious what's the benefit of using hoisted()
here? why don't just mock()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want access to the mock functions (vi.fn()) that I use for the mocked object properties of the SDK, however if I define them inside vi.mock(() => { //here
then they won't be accesible outside that scope.
If I just define them in the file then they won't have been declared yet when vi.mock(
is called because mock is hoisted to the top of the file above the imports and all declarations.
That's why vitest exposes the vi.hoisted
util to allow you to declare things before the vimock()
s are called.
See the docs:
The call to vi.mock is hoisted, so it doesn't matter where you call it. It will always be executed before all imports. If you need to reference some variables outside of its scope, you can define them inside vi.hoisted and reference them inside vi.mock.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for the explanation 🙏
@@ -61,12 +100,22 @@ describe('AudioInputOutputDevice Component', () => { | |||
); | |||
|
|||
const outputDevicesElement = screen.getByTestId('output-devices'); | |||
expect(outputDevicesElement).toBeInTheDocument(); | |||
await waitFor(() => expect(outputDevicesElement.children).to.have.length(3)); | |||
const firstChild = outputDevicesElement.firstChild as HTMLOptionElement; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: can firstChild
be renamed to something more self-explanatory? something like firstDeviceElement
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
firstChild is an inherited property in the DOM. It's unlikely we'll be able to rename that one 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, thanks for the info! we can make const
s be whatever we'd like them to be, though 🙂 it's just a nit though so if Mike prefers to keep it as a firstChild
I'm okay with that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've removed this variable and inlined the query.
It's a tricky one because firstDeviceElement
or something like that might be more understandable, but this is a test and we are testing the firstChild. Whether that firstChild is a deviceElement depends on if the code is working 🤷
|
||
// Check initial list is correct | ||
await waitFor(() => expect(outputDevicesElement.children).to.have.length(3)); | ||
const firstChild = outputDevicesElement.firstChild as HTMLOptionElement; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same nit here with the Child
if you end up changing it 🙏
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed this variable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Responded to comments
@@ -61,12 +100,22 @@ describe('AudioInputOutputDevice Component', () => { | |||
); | |||
|
|||
const outputDevicesElement = screen.getByTestId('output-devices'); | |||
expect(outputDevicesElement).toBeInTheDocument(); | |||
await waitFor(() => expect(outputDevicesElement.children).to.have.length(3)); | |||
const firstChild = outputDevicesElement.firstChild as HTMLOptionElement; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've removed this variable and inlined the query.
It's a tricky one because firstDeviceElement
or something like that might be more understandable, but this is a test and we are testing the firstChild. Whether that firstChild is a deviceElement depends on if the code is working 🤷
|
||
// Check initial list is correct | ||
await waitFor(() => expect(outputDevicesElement.children).to.have.length(3)); | ||
const firstChild = outputDevicesElement.firstChild as HTMLOptionElement; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed this variable
mockGetAudioOutputDevices, | ||
mockSetAudioOutputDevice, | ||
mockGetActiveAudioOutputDevice, | ||
} = vi.hoisted(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want access to the mock functions (vi.fn()) that I use for the mocked object properties of the SDK, however if I define them inside vi.mock(() => { //here
then they won't be accesible outside that scope.
If I just define them in the file then they won't have been declared yet when vi.mock(
is called because mock is hoisted to the top of the file above the imports and all declarations.
That's why vitest exposes the vi.hoisted
util to allow you to declare things before the vimock()
s are called.
See the docs:
The call to vi.mock is hoisted, so it doesn't matter where you call it. It will always be executed before all imports. If you need to reference some variables outside of its scope, you can define them inside vi.hoisted and reference them inside vi.mock.
play: playMock, | ||
pause: pauseMock, | ||
currentTime: 9001, | ||
setSinkId: vi.fn(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great catch! Luckily when I updated the value the test still passes 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM Great job!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
Tested on Android device (Samsung S21 FE) LGTM!! 🚀 |
What is this PR doing?
How should this be manually tested?
On supported devices:
What are the relevant tickets?
Resolves VIDCS-3273
[ ] Resolves a
Known Issue
.[ ] If yes, did you remove the item from the
docs/KNOWN_ISSUES.md
?[ ] Resolves an item reported in
Issues
.If yes, which issue? Issue Number?
[ ] If yes, did you close the item in
Issues
?