Skip to content

fix: useDevicesList in firefox #4674

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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

markusgeert
Copy link

@markusgeert markusgeert commented Mar 21, 2025

Before submitting the PR, please make sure you do the following

  • Read the Contributing Guidelines.
  • Read the Pull Request Guidelines.
  • Check that there isn't already a PR that solves the problem the same way to avoid creating a duplicate.
  • Provide a description in this PR that addresses what the PR is solving, or reference the issue that it solves (e.g. fixes #123).
  • Ideally, include relevant tests that fail without this PR but pass with it.
⚠️ Slowing down new functions

Warning: Slowing down new functions

As the VueUse audience continues to grow, we have been inundated with an overwhelming number of feature requests and pull requests. As a result, maintaining the project has become increasingly challenging and has stretched our capacity to its limits. As such, in the near future, we may need to slow down our acceptance of new features and prioritize the stability and quality of existing functions. Please note that new features for VueUse may not be accepted at this time. If you have any new ideas, we suggest that you first incorporate them into your own codebase, iterate on them to suit your needs, and assess their generalizability. If you strongly believe that your ideas are beneficial to the community, you may submit a pull request along with your use cases, and we would be happy to review and discuss them. Thank you for your understanding.


Description

Fixes #4672. In Firefox it's not enough to only check if permission has already been granted before devices can be listed. Listing the devices always requires an active stream. This PR makes sure to always create that new stream in Firefox, even when permissions have already been granted. By doing this, listing devices continues to work in FF after a reload.

Additional context

I have refactored some logic into the getCurrentPermission function, so that it can be ran only when the browser is not Firefox. This does require an await call inside an if condition, which does feel a bit dirty.

@dosubot dosubot bot added the size:S This PR changes 10-29 lines, ignoring generated files. label Mar 21, 2025
@ilyaliao
Copy link
Member

Do we need to refactor? It seems like just alwaysRequireStreamToEnumerate is enough to work properly.

@markusgeert
Copy link
Author

Do we need to refactor? It seems like just alwaysRequireStreamToEnumerate is enough to work properly.

Sure, but by doing this we prevent usePermission and the subsequent query method from being called in Firefox. Just adding alwaysRequireStreamToEnumerate without refactor would still query the permissions in Firefox. If that's not a concern I can remove the refactor.

@OrbisK
Copy link
Collaborator

OrbisK commented Mar 24, 2025

If we have browser-specific stuff here, we should cover that with browser tests.

@markusgeert
Copy link
Author

If we have browser-specific stuff here, we should cover that with browser tests.

I agree that that would be nice. However, you can't grant permissions to camera and microphone in Playwright (microsoft/playwright#5445), which also ties into their lack of support for testing WebRTC in browsers other than Chromium (microsoft/playwright#2973).

It's possible to mock the involved API's. However, this kind of circumvents the point of a test, since that test would just be testing our mocks.

In my opinion, adding tests here would be too much work for what it's worth (if even possible).

@OrbisK let me know what direction you want to take this (closing vs. merging). If you still want to merge, let me know what needs to be done before this can be considered ready to merge.

@OrbisK
Copy link
Collaborator

OrbisK commented Mar 24, 2025

It's possible to mock the involved API's. However, this kind of circumvents the point of a test, since that test would just be testing our mocks.

Agreed, let's test it manually as best we can 😄

@ilyaliao
Copy link
Member

Sure, but by doing this we prevent usePermission and the subsequent query method from being called in Firefox. Just adding alwaysRequireStreamToEnumerate without refactor would still query the permissions in Firefox. If that's not a concern I can remove the refactor.

@OrbisK What do you think? Do we need to refactor?

@OrbisK
Copy link
Collaborator

OrbisK commented Mar 25, 2025

Sure, but by doing this we prevent usePermission and the subsequent query method from being called in Firefox. Just adding alwaysRequireStreamToEnumerate without refactor would still query the permissions in Firefox. If that's not a concern I can remove the refactor.

@OrbisK What do you think? Do we need to refactor?

We should definitely not call usePermission inside the function. This will cause a memory leak.

https://github.com/vueuse/vueuse/pull/4674/files#diff-1c6f5be67d3284eb4ebf86f4553255b7271ea803ca1fe5134f397d5c8759933fR77

I wonder if we can detect if we need the stream from something other than the useragent. Like we did with the fallback part of useClipboard.

@markusgeert
Copy link
Author

markusgeert commented Mar 25, 2025

I wonder if we can detect if we need the stream from something other than the useragent. Like we did with the fallback part of useClipboard.

What I can do is add a check in update that'll check if the value of permission is granted, while the value of the deviceId's are all empty strings. When that holds, we can initiate a stream and call the enumerateDevices function again, like so:

async function update() {
  if (!isSupported.value)
    return

  devices.value = await navigator!.mediaDevices.enumerateDevices()

  const { state, query } = usePermission(deviceName, { controls: true })
  await query()

  if (!stream && state.value === "granted" && devices.value.every(device => device.deviceId === "")) {
    await startStream();
    devices.value = await navigator!.mediaDevices.enumerateDevices()
  }

  onUpdated?.(devices.value)
  stopStream()
}

where the startStream and stopStream functions have to be created. The only downside of this is that 'enumerateDevices' will be called twice in FF, but I don't see a way of avoiding that without checking the useragent explicitly.

EDIT: this way the ensurePermissions function can also remain unchanged

@OrbisK
Copy link
Collaborator

OrbisK commented Mar 26, 2025

I am not against relying on the useragent if it solves the problem. Is FF the only browser with this behaviour? And why?


A general thing with your PR and proposed changes:

You should not call composables within functions other than composables. Composables should always be called at setup level.

Example

// useFoo
function useFoo(s: MaybeRef<string>){
  const lowerCase = computed(()=>{
    return toValue(s).toLowerCase()
  })
  
  watch(()=>s,()=>{
    console.log("s changed")
  }

  return {
    lowerCase
  }
} 
// bad usage
const test = ref("abc")

function doSomeThing(){
  const { lowerCase } = useFoo(test)
  /* ... */ 
  return /* ... */ 
}
doSomeThing() // this will call useFoo the first time -> creating a computed and a watcher
test.value = "123" // this will call console.log once -> expected
doSomeThing() // this will call useFoo the first time -> creating a computed and a watcher
doSomeThing() // this will call useFoo the second time -> creating a computed and a watcher again, but does not  dispose the first

test.value = "123" // this will call console.log twice 

Alex has a great video on this: https://www.youtube.com/watch?v=njsGVmcWviY (its about useFetch, but can be applied to any composable)

@markusgeert
Copy link
Author

I am not against relying on the useragent if it solves the problem. Is FF the only browser with this behaviour? And why?

From what I can gather, this only happens in FF. And while the issue is described on SO, the only other documentation I could find is the spec which states that you need a live MediaStreamTrack in order to be able to enumerate devices. If I'm reading that correctly, FF is the only browser properly adhering to the spec. Meanwhile Chrome and Safari allow enumeration of devices without an active mediastream. In that light, checking useragent might seem like a good workaround.

Another solution is to always create a MediaStream before trying to enumerate the devices. This does adhere to the spec, and will work in each browser, but has two important downsides:

  • It takes significant time to initiate a MediaStream
  • Visual camera-active indicators become active on creation of a MediaStream (eg. my MacBook will show a green indicator next to the camera).

My preference goes out the solution I proposed earlier, first trying to enumerate the devices, and if that fails retry with an active MediaStream. This has the benefit of not impacting the majority of users (on Chrome and Safari) with the downsides mentioned above, no explicit useragent checking, still functioning if behavior of any of the browsers change, with the only downside being having to call enumerateDevices twice in FF.

You should not call composables within functions other than composables. Composables should always be called at setup level.

I'll make sure to update the PR accordingly.

@OrbisK
Copy link
Collaborator

OrbisK commented Mar 26, 2025

@markusgeert
Copy link
Author

@OrbisK @ilyaliao let's get this PR finished up. If I change the update method in the way I describe here, would you merge? If so, I'll get right on it.

@OrbisK
Copy link
Collaborator

OrbisK commented Apr 9, 2025

@OrbisK @ilyaliao let's get this PR finished up. If I change the update method in the way I describe here, would you merge? If so, I'll get right on it.

Thank you for your commitment. But I think we need to investigate the intended solution for this. From the w3c thread it looks like there should be a proper way to solve this (it also looks like FF is doing it the correct way). I have not had time to look into it yet. Maybe I can do it tomorrow.

TLDR: Let me check the Threads. I will get back to you!

@Nikitatopodin
Copy link
Contributor

Nikitatopodin commented Apr 28, 2025

Hey everyone! Just want to propose a solution with adding some flag that we pass to useDevicesList options, for exmaple, forceGUMToEnumerate or smth similiar with default false. And we just modify ensurePermissions this way:

async function ensurePermissions() {
    const deviceName = constraints.video ? 'camera' : 'microphone'
    if (!isSupported.value)
      return false
    if (permissionGranted.value && !forceGUMToEnumerate)
      return true
    const { state, query } = usePermission(deviceName, { controls: true })
    await query()
    if (state.value !== 'granted' || forceGUMToEnumerate) {
      let granted = true
      try {
        stream = await navigator!.mediaDevices.getUserMedia(constraints)
      }
      catch {
        stream = null
        granted = false
      }
      update()
      permissionGranted.value = granted
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
size:S This PR changes 10-29 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

BUG | useDevicesList | Doesn't list devices after reload in Firefox
4 participants