mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-21 17:25:36 +00:00
Bug 1213453 - Correlate video device group id based on device name. r=pehrsons
Differential Revision: https://phabricator.services.mozilla.com/D20372 --HG-- extra : moz-landing-system : lando
This commit is contained in:
parent
6efd82fb6f
commit
abfb29171d
@ -1779,6 +1779,70 @@ class GetUserMediaRunnableWrapper : public Runnable {
|
||||
};
|
||||
#endif
|
||||
|
||||
// This function tries to guess the group id for a video device
|
||||
// based on the device name. If only one audio device's name contains
|
||||
// the name of the video device, then, this video device will take
|
||||
// the group id of the audio device. Since this is a guess we try
|
||||
// to minimize the probability of false positive. If we fail to find
|
||||
// a correlation we leave the video group id untouched. In that case the
|
||||
// group id will be the video device name.
|
||||
/* static */
|
||||
void MediaManager::GuessVideoDeviceGroupIDs(MediaDeviceSet& aDevices) {
|
||||
// Run the logic in a lambda to avoid duplication.
|
||||
auto updateGroupIdIfNeeded = [&](RefPtr<MediaDevice>& aVideo,
|
||||
const dom::MediaDeviceKind aKind) -> bool {
|
||||
MOZ_ASSERT(aVideo->mKind == dom::MediaDeviceKind::Videoinput);
|
||||
MOZ_ASSERT(aKind == dom::MediaDeviceKind::Audioinput ||
|
||||
aKind == dom::MediaDeviceKind::Audiooutput);
|
||||
// This will store the new group id if a match is found.
|
||||
nsString newVideoGroupID;
|
||||
// If the group id needs to be updated this will become true. It is
|
||||
// necessary when the new group id is an empty string. Without this extra
|
||||
// variable to signal the update, we would resort to test if
|
||||
// `newVideoGroupId` is empty. However,
|
||||
// that check does not work when the new group id is an empty string.
|
||||
bool updateGroupId = false;
|
||||
for (const RefPtr<MediaDevice>& dev : aDevices) {
|
||||
if (dev->mKind != aKind) {
|
||||
continue;
|
||||
}
|
||||
if (!FindInReadable(aVideo->mName, dev->mName)) {
|
||||
continue;
|
||||
}
|
||||
if (newVideoGroupID.IsEmpty()) {
|
||||
// This is only expected on first match. If that's the only match group
|
||||
// id will be updated to this one at the end of the loop.
|
||||
updateGroupId = true;
|
||||
newVideoGroupID = dev->mGroupID;
|
||||
} else {
|
||||
// More than one device found, it is impossible to know which group id
|
||||
// is the correct one.
|
||||
updateGroupId = false;
|
||||
newVideoGroupID = NS_LITERAL_STRING("");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (updateGroupId) {
|
||||
aVideo =
|
||||
new MediaDevice(aVideo, aVideo->mID, newVideoGroupID, aVideo->mRawID);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
for (RefPtr<MediaDevice>& video : aDevices) {
|
||||
if (video->mKind != dom::MediaDeviceKind::Videoinput) {
|
||||
continue;
|
||||
}
|
||||
if (updateGroupIdIfNeeded(video, dom::MediaDeviceKind::Audioinput)) {
|
||||
// GroupId has been updated, continue to the next video device
|
||||
continue;
|
||||
}
|
||||
// GroupId has not been updated, check among the outputs
|
||||
updateGroupIdIfNeeded(video, dom::MediaDeviceKind::Audiooutput);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EnumerateRawDevices - Enumerate a list of audio & video devices that
|
||||
* satisfy passed-in constraints. List contains raw id's.
|
||||
@ -1891,6 +1955,9 @@ RefPtr<MediaManager::MgrPromise> MediaManager::EnumerateRawDevices(
|
||||
MediaSinkEnum::Speaker, &outputs);
|
||||
aOutDevices->AppendElements(outputs);
|
||||
}
|
||||
if (hasVideo) {
|
||||
GuessVideoDeviceGroupIDs(*aOutDevices);
|
||||
}
|
||||
|
||||
holder->Resolve(false, __func__);
|
||||
});
|
||||
|
@ -280,6 +280,7 @@ class MediaManager final : public nsIMediaManagerService,
|
||||
const uint64_t aWindowId);
|
||||
static already_AddRefed<nsIWritableVariant> ToJSArray(
|
||||
MediaDeviceSet& aDevices);
|
||||
static void GuessVideoDeviceGroupIDs(MediaManager::MediaDeviceSet& aDevices);
|
||||
|
||||
private:
|
||||
enum class DeviceEnumerationType : uint8_t {
|
||||
|
336
dom/media/gtest/TestGroupId.cpp
Normal file
336
dom/media/gtest/TestGroupId.cpp
Normal file
@ -0,0 +1,336 @@
|
||||
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#include "AudioDeviceInfo.h"
|
||||
#include "MediaManager.h"
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest-printers.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "mozilla/Attributes.h"
|
||||
#include "mozilla/UniquePtr.h"
|
||||
#include "nsTArray.h"
|
||||
#include "webrtc/MediaEngineSource.h"
|
||||
|
||||
using ::testing::Return;
|
||||
|
||||
void PrintTo(const nsString& aValue, ::std::ostream* aStream) {
|
||||
NS_ConvertUTF16toUTF8 str(aValue);
|
||||
(*aStream) << str.get();
|
||||
}
|
||||
void PrintTo(const nsCString& aValue, ::std::ostream* aStream) {
|
||||
(*aStream) << aValue.get();
|
||||
}
|
||||
|
||||
class MockMediaEngineSource : public MediaEngineSource {
|
||||
public:
|
||||
MOCK_CONST_METHOD0(GetMediaSource, dom::MediaSourceEnum());
|
||||
|
||||
/* Unused overrides */
|
||||
MOCK_CONST_METHOD0(GetName, nsString());
|
||||
MOCK_CONST_METHOD0(GetUUID, nsCString());
|
||||
MOCK_CONST_METHOD0(GetGroupId, nsString());
|
||||
MOCK_METHOD6(Allocate, nsresult(const dom::MediaTrackConstraints&,
|
||||
const MediaEnginePrefs&, const nsString&,
|
||||
const ipc::PrincipalInfo&, AllocationHandle**,
|
||||
const char**));
|
||||
MOCK_METHOD4(SetTrack, void(const RefPtr<const AllocationHandle>&,
|
||||
const RefPtr<SourceMediaStream>&, TrackID,
|
||||
const PrincipalHandle&));
|
||||
MOCK_METHOD1(Start, nsresult(const RefPtr<const AllocationHandle>&));
|
||||
MOCK_METHOD5(Reconfigure, nsresult(const RefPtr<AllocationHandle>&,
|
||||
const dom::MediaTrackConstraints&,
|
||||
const MediaEnginePrefs&, const nsString&,
|
||||
const char**));
|
||||
MOCK_METHOD1(Stop, nsresult(const RefPtr<const AllocationHandle>&));
|
||||
MOCK_METHOD1(Deallocate, nsresult(const RefPtr<const AllocationHandle>&));
|
||||
MOCK_CONST_METHOD2(GetBestFitnessDistance,
|
||||
uint32_t(const nsTArray<const NormalizedConstraintSet*>&,
|
||||
const nsString&));
|
||||
MOCK_METHOD6(Pull,
|
||||
void(const RefPtr<const AllocationHandle>& aHandle,
|
||||
const RefPtr<SourceMediaStream>& aStream, TrackID aTrackID,
|
||||
StreamTime aEndOfAppendedData, StreamTime aDesiredTime,
|
||||
const PrincipalHandle& aPrincipalHandle));
|
||||
};
|
||||
|
||||
RefPtr<AudioDeviceInfo> MakeAudioDeviceInfo(const nsString aName) {
|
||||
return MakeRefPtr<AudioDeviceInfo>(
|
||||
nullptr, aName, NS_LITERAL_STRING("GroupId"), NS_LITERAL_STRING("Vendor"),
|
||||
AudioDeviceInfo::TYPE_OUTPUT, AudioDeviceInfo::STATE_ENABLED,
|
||||
AudioDeviceInfo::PREF_NONE, AudioDeviceInfo::FMT_F32LE,
|
||||
AudioDeviceInfo::FMT_F32LE, 2u, 44100u, 44100u, 44100u, 0, 0);
|
||||
}
|
||||
|
||||
RefPtr<MediaDevice> MakeCameraDevice(const nsString& aName,
|
||||
const nsString& aGroupId) {
|
||||
auto v = MakeRefPtr<MockMediaEngineSource>();
|
||||
EXPECT_CALL(*v, GetMediaSource())
|
||||
.WillRepeatedly(Return(dom::MediaSourceEnum::Camera));
|
||||
|
||||
return MakeRefPtr<MediaDevice>(v, aName, NS_LITERAL_STRING(""), aGroupId,
|
||||
NS_LITERAL_STRING(""));
|
||||
}
|
||||
|
||||
RefPtr<MediaDevice> MakeMicDevice(const nsString& aName,
|
||||
const nsString& aGroupId) {
|
||||
auto a = MakeRefPtr<MockMediaEngineSource>();
|
||||
EXPECT_CALL(*a, GetMediaSource())
|
||||
.WillRepeatedly(Return(dom::MediaSourceEnum::Microphone));
|
||||
|
||||
return MakeRefPtr<MediaDevice>(a, aName, NS_LITERAL_STRING(""), aGroupId,
|
||||
NS_LITERAL_STRING(""));
|
||||
}
|
||||
|
||||
RefPtr<MediaDevice> MakeSpeakerDevice(const nsString& aName,
|
||||
const nsString& aGroupId) {
|
||||
return MakeRefPtr<MediaDevice>(MakeAudioDeviceInfo(aName),
|
||||
NS_LITERAL_STRING("ID"), aGroupId,
|
||||
NS_LITERAL_STRING("RawID"));
|
||||
}
|
||||
|
||||
/* Verify that when an audio input device name contains the video input device
|
||||
* name the video device group id is updated to become equal to the audio
|
||||
* device group id. */
|
||||
TEST(TestGroupId, MatchInput_PartOfName) {
|
||||
MediaManager::MediaDeviceSet devices;
|
||||
|
||||
devices.AppendElement(
|
||||
MakeCameraDevice(NS_LITERAL_STRING("Vendor Model"),
|
||||
NS_LITERAL_STRING("Cam-Model-GroupId")));
|
||||
|
||||
devices.AppendElement(
|
||||
MakeMicDevice(NS_LITERAL_STRING("Vendor Model Analog Stereo"),
|
||||
NS_LITERAL_STRING("Mic-Model-GroupId")));
|
||||
|
||||
MediaManager::GuessVideoDeviceGroupIDs(devices);
|
||||
|
||||
EXPECT_EQ(devices[0]->mGroupID, devices[1]->mGroupID)
|
||||
<< "Video group id is the same as audio input group id.";
|
||||
}
|
||||
|
||||
/* Verify that when an audio input device name is the same as the video input
|
||||
* device name the video device group id is updated to become equal to the audio
|
||||
* device group id. */
|
||||
TEST(TestGroupId, MatchInput_FullName) {
|
||||
MediaManager::MediaDeviceSet devices;
|
||||
|
||||
devices.AppendElement(
|
||||
MakeCameraDevice(NS_LITERAL_STRING("Vendor Model"),
|
||||
NS_LITERAL_STRING("Cam-Model-GroupId")));
|
||||
|
||||
devices.AppendElement(MakeMicDevice(NS_LITERAL_STRING("Vendor Model"),
|
||||
NS_LITERAL_STRING("Mic-Model-GroupId")));
|
||||
|
||||
MediaManager::GuessVideoDeviceGroupIDs(devices);
|
||||
|
||||
EXPECT_EQ(devices[0]->mGroupID, devices[1]->mGroupID)
|
||||
<< "Video group id is the same as audio input group id.";
|
||||
}
|
||||
|
||||
/* Verify that when an audio input device name does not contain the video input
|
||||
* device name the video device group id does not change. */
|
||||
TEST(TestGroupId, NoMatchInput) {
|
||||
MediaManager::MediaDeviceSet devices;
|
||||
|
||||
nsString Cam_Model_GroupId = NS_LITERAL_STRING("Cam-Model-GroupId");
|
||||
devices.AppendElement(
|
||||
MakeCameraDevice(NS_LITERAL_STRING("Vendor Model"), Cam_Model_GroupId));
|
||||
|
||||
devices.AppendElement(MakeMicDevice(NS_LITERAL_STRING("Model Analog Stereo"),
|
||||
NS_LITERAL_STRING("Mic-Model-GroupId")));
|
||||
|
||||
MediaManager::GuessVideoDeviceGroupIDs(devices);
|
||||
|
||||
EXPECT_EQ(devices[0]->mGroupID, Cam_Model_GroupId)
|
||||
<< "Video group id has not been updated.";
|
||||
EXPECT_NE(devices[0]->mGroupID, devices[1]->mGroupID)
|
||||
<< "Video group id is different than audio input group id.";
|
||||
}
|
||||
|
||||
/* Verify that when more that one audio input and more than one audio output
|
||||
* device name contain the video input device name the video device group id
|
||||
* does not change. */
|
||||
TEST(TestGroupId, NoMatch_TwoIdenticalDevices) {
|
||||
MediaManager::MediaDeviceSet devices;
|
||||
|
||||
nsString Cam_Model_GroupId = NS_LITERAL_STRING("Cam-Model-GroupId");
|
||||
devices.AppendElement(
|
||||
MakeCameraDevice(NS_LITERAL_STRING("Vendor Model"), Cam_Model_GroupId));
|
||||
|
||||
devices.AppendElement(
|
||||
MakeMicDevice(NS_LITERAL_STRING("Vendor Model Analog Stereo"),
|
||||
NS_LITERAL_STRING("Mic-Model-GroupId")));
|
||||
devices.AppendElement(
|
||||
MakeMicDevice(NS_LITERAL_STRING("Vendor Model Analog Stereo"),
|
||||
NS_LITERAL_STRING("Mic-Model-GroupId")));
|
||||
|
||||
devices.AppendElement(
|
||||
MakeSpeakerDevice(NS_LITERAL_STRING("Vendor Model Analog Stereo"),
|
||||
NS_LITERAL_STRING("Speaker-Model-GroupId")));
|
||||
devices.AppendElement(
|
||||
MakeSpeakerDevice(NS_LITERAL_STRING("Vendor Model Analog Stereo"),
|
||||
NS_LITERAL_STRING("Speaker-Model-GroupId")));
|
||||
|
||||
MediaManager::GuessVideoDeviceGroupIDs(devices);
|
||||
|
||||
EXPECT_EQ(devices[0]->mGroupID, Cam_Model_GroupId)
|
||||
<< "Video group id has not been updated.";
|
||||
EXPECT_NE(devices[0]->mGroupID, devices[1]->mGroupID)
|
||||
<< "Video group id is different than audio input group id.";
|
||||
EXPECT_NE(devices[0]->mGroupID, devices[3]->mGroupID)
|
||||
<< "Video group id is different than audio output group id.";
|
||||
}
|
||||
|
||||
/* Verify that when more that one audio input device name contain the video
|
||||
* input device name the video device group id is not updated by audio input
|
||||
* device group id but it continues looking at audio output devices where it
|
||||
* finds a match so video input group id is updated by audio output group id. */
|
||||
TEST(TestGroupId, Match_TwoIdenticalInputsMatchOutput) {
|
||||
MediaManager::MediaDeviceSet devices;
|
||||
|
||||
nsString Cam_Model_GroupId = NS_LITERAL_STRING("Cam-Model-GroupId");
|
||||
devices.AppendElement(
|
||||
MakeCameraDevice(NS_LITERAL_STRING("Vendor Model"), Cam_Model_GroupId));
|
||||
|
||||
devices.AppendElement(
|
||||
MakeMicDevice(NS_LITERAL_STRING("Vendor Model Analog Stereo"),
|
||||
NS_LITERAL_STRING("Mic-Model-GroupId")));
|
||||
devices.AppendElement(
|
||||
MakeMicDevice(NS_LITERAL_STRING("Vendor Model Analog Stereo"),
|
||||
NS_LITERAL_STRING("Mic-Model-GroupId")));
|
||||
|
||||
devices.AppendElement(
|
||||
MakeSpeakerDevice(NS_LITERAL_STRING("Vendor Model Analog Stereo"),
|
||||
NS_LITERAL_STRING("Speaker-Model-GroupId")));
|
||||
|
||||
MediaManager::GuessVideoDeviceGroupIDs(devices);
|
||||
|
||||
EXPECT_EQ(devices[0]->mGroupID, devices[3]->mGroupID)
|
||||
<< "Video group id is the same as audio output group id.";
|
||||
}
|
||||
|
||||
/* Verify that when more that one audio input and more than one audio output
|
||||
* device names contain the video input device name the video device group id
|
||||
* does not change. */
|
||||
TEST(TestGroupId, NoMatch_ThreeIdenticalDevices) {
|
||||
MediaManager::MediaDeviceSet devices;
|
||||
|
||||
nsString Cam_Model_GroupId = NS_LITERAL_STRING("Cam-Model-GroupId");
|
||||
devices.AppendElement(
|
||||
MakeCameraDevice(NS_LITERAL_STRING("Vendor Model"), Cam_Model_GroupId));
|
||||
|
||||
devices.AppendElement(
|
||||
MakeMicDevice(NS_LITERAL_STRING("Vendor Model Analog Stereo"),
|
||||
NS_LITERAL_STRING("Mic-Model-GroupId")));
|
||||
devices.AppendElement(
|
||||
MakeMicDevice(NS_LITERAL_STRING("Vendor Model Analog Stereo"),
|
||||
NS_LITERAL_STRING("Mic-Model-GroupId")));
|
||||
devices.AppendElement(
|
||||
MakeMicDevice(NS_LITERAL_STRING("Vendor Model Analog Stereo"),
|
||||
NS_LITERAL_STRING("Mic-Model-GroupId")));
|
||||
|
||||
devices.AppendElement(
|
||||
MakeSpeakerDevice(NS_LITERAL_STRING("Vendor Model Analog Stereo"),
|
||||
NS_LITERAL_STRING("Speaker-Model-GroupId")));
|
||||
devices.AppendElement(
|
||||
MakeSpeakerDevice(NS_LITERAL_STRING("Vendor Model Analog Stereo"),
|
||||
NS_LITERAL_STRING("Speaker-Model-GroupId")));
|
||||
devices.AppendElement(
|
||||
MakeSpeakerDevice(NS_LITERAL_STRING("Vendor Model Analog Stereo"),
|
||||
NS_LITERAL_STRING("Speaker-Model-GroupId")));
|
||||
|
||||
MediaManager::GuessVideoDeviceGroupIDs(devices);
|
||||
|
||||
EXPECT_EQ(devices[0]->mGroupID, Cam_Model_GroupId)
|
||||
<< "Video group id has not been updated.";
|
||||
EXPECT_NE(devices[0]->mGroupID, devices[1]->mGroupID)
|
||||
<< "Video group id is different than audio input group id.";
|
||||
EXPECT_NE(devices[0]->mGroupID, devices[4]->mGroupID)
|
||||
<< "Video group id is different than audio output group id.";
|
||||
}
|
||||
|
||||
/* Verify that when an audio output device name contains the video input device
|
||||
* name the video device group id is updated to become equal to the audio
|
||||
* device group id. */
|
||||
TEST(TestGroupId, MatchOutput) {
|
||||
MediaManager::MediaDeviceSet devices;
|
||||
|
||||
devices.AppendElement(
|
||||
MakeCameraDevice(NS_LITERAL_STRING("Vendor Model"),
|
||||
NS_LITERAL_STRING("Cam-Model-GroupId")));
|
||||
|
||||
devices.AppendElement(MakeMicDevice(NS_LITERAL_STRING("Mic Analog Stereo"),
|
||||
NS_LITERAL_STRING("Mic-Model-GroupId")));
|
||||
|
||||
devices.AppendElement(
|
||||
MakeSpeakerDevice(NS_LITERAL_STRING("Vendor Model Analog Stereo"),
|
||||
NS_LITERAL_STRING("Speaker-Model-GroupId")));
|
||||
|
||||
MediaManager::GuessVideoDeviceGroupIDs(devices);
|
||||
|
||||
EXPECT_EQ(devices[0]->mGroupID, devices[2]->mGroupID)
|
||||
<< "Video group id is the same as audio output group id.";
|
||||
}
|
||||
|
||||
/* Verify that when an audio input device name is the same as audio output
|
||||
* device and video input device name the video device group id is updated to
|
||||
* become equal to the audio input device group id. */
|
||||
TEST(TestGroupId, InputOutputSameName) {
|
||||
MediaManager::MediaDeviceSet devices;
|
||||
|
||||
devices.AppendElement(
|
||||
MakeCameraDevice(NS_LITERAL_STRING("Vendor Model"),
|
||||
NS_LITERAL_STRING("Cam-Model-GroupId")));
|
||||
|
||||
devices.AppendElement(MakeMicDevice(NS_LITERAL_STRING("Vendor Model"),
|
||||
NS_LITERAL_STRING("Mic-Model-GroupId")));
|
||||
|
||||
devices.AppendElement(
|
||||
MakeSpeakerDevice(NS_LITERAL_STRING("Vendor Model"),
|
||||
NS_LITERAL_STRING("Speaker-Model-GroupId")));
|
||||
|
||||
MediaManager::GuessVideoDeviceGroupIDs(devices);
|
||||
|
||||
EXPECT_EQ(devices[0]->mGroupID, devices[1]->mGroupID)
|
||||
<< "Video input group id is the same as audio input group id.";
|
||||
}
|
||||
|
||||
/* Verify that when an audio input device name contains the video input device
|
||||
* and the audio input group id is an empty string, the video device group id
|
||||
* is updated to become equal to the audio device group id. */
|
||||
TEST(TestGroupId, InputEmptyGroupId) {
|
||||
MediaManager::MediaDeviceSet devices;
|
||||
|
||||
devices.AppendElement(
|
||||
MakeCameraDevice(NS_LITERAL_STRING("Vendor Model"),
|
||||
NS_LITERAL_STRING("Cam-Model-GroupId")));
|
||||
|
||||
devices.AppendElement(
|
||||
MakeMicDevice(NS_LITERAL_STRING("Vendor Model"), NS_LITERAL_STRING("")));
|
||||
|
||||
MediaManager::GuessVideoDeviceGroupIDs(devices);
|
||||
|
||||
EXPECT_EQ(devices[0]->mGroupID, devices[1]->mGroupID)
|
||||
<< "Video input group id is the same as audio input group id.";
|
||||
}
|
||||
|
||||
/* Verify that when an audio output device name contains the video input device
|
||||
* and the audio output group id is an empty string, the video device group id
|
||||
* is updated to become equal to the audio output device group id. */
|
||||
TEST(TestGroupId, OutputEmptyGroupId) {
|
||||
MediaManager::MediaDeviceSet devices;
|
||||
|
||||
devices.AppendElement(
|
||||
MakeCameraDevice(NS_LITERAL_STRING("Vendor Model"),
|
||||
NS_LITERAL_STRING("Cam-Model-GroupId")));
|
||||
|
||||
devices.AppendElement(MakeSpeakerDevice(NS_LITERAL_STRING("Vendor Model"),
|
||||
NS_LITERAL_STRING("")));
|
||||
|
||||
MediaManager::GuessVideoDeviceGroupIDs(devices);
|
||||
|
||||
EXPECT_EQ(devices[0]->mGroupID, devices[1]->mGroupID)
|
||||
<< "Video input group id is the same as audio output group id.";
|
||||
}
|
@ -28,6 +28,7 @@ UNIFIED_SOURCES += [
|
||||
'TestGMPCrossOrigin.cpp',
|
||||
'TestGMPRemoveAndDelete.cpp',
|
||||
'TestGMPUtils.cpp',
|
||||
'TestGroupId.cpp',
|
||||
'TestIntervalSet.cpp',
|
||||
'TestMediaDataDecoder.cpp',
|
||||
'TestMediaDataEncoder.cpp',
|
||||
|
Loading…
Reference in New Issue
Block a user