init
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2025-09-02 14:49:16 +08:00
commit 38ba663466
2885 changed files with 391107 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
/**
* Action type to INIT_DEVICE.
*/
export const INIT_DEVICE = 'INIT_DEVICE';
/**
* Action type to CLOSE_HID_DEVICE.
*/
export const CLOSE_HID_DEVICE = 'CLOSE_HID_DEVICE';
/**
* Action type to REQUEST_HID_DEVICE.
*/
export const REQUEST_HID_DEVICE = 'REQUEST_HID_DEVICE';
/**
* Action type to UPDATE_DEVICE.
*/
export const UPDATE_DEVICE = 'UPDATE_DEVICE';

View File

@@ -0,0 +1,51 @@
import { CLOSE_HID_DEVICE, INIT_DEVICE, REQUEST_HID_DEVICE, UPDATE_DEVICE } from './actionTypes';
import { IDeviceInfo } from './types';
/**
* Action used to init device.
*
* @param {IDeviceInfo} deviceInfo - Telephony device information.
* @returns {Object}
*/
export function initDeviceInfo(deviceInfo: IDeviceInfo) {
return {
type: INIT_DEVICE,
deviceInfo
};
}
/**
* Request hid device.
*
* @returns {Object}
*/
export function closeHidDevice() {
return {
type: CLOSE_HID_DEVICE
};
}
/**
* Request hid device.
*
* @param {IDeviceInfo} deviceInfo - Telephony device information.
* @returns {Object}
*/
export function requestHidDevice() {
return {
type: REQUEST_HID_DEVICE
};
}
/**
* Action used to init device.
*
* @param {IDeviceInfo} deviceInfo - Telephony device information.
* @returns {Object}
*/
export function updateDeviceInfo(deviceInfo: IDeviceInfo) {
return {
type: UPDATE_DEVICE,
updates: deviceInfo
};
}

View File

@@ -0,0 +1,121 @@
import { IReduxState, IStore } from '../app/types';
import { MEDIA_TYPE } from '../base/media/constants';
import { muteLocal } from '../video-menu/actions.any';
import { updateDeviceInfo } from './actions';
import { ACTION_HOOK_TYPE_NAME, EVENT_TYPE, IDeviceInfo } from './types';
import WebHidManager from './webhid-manager';
/**
* Attach web hid event listeners.
*
* @param {Function} initDeviceListener - Init hid device listener.
* @param {Function} updateDeviceListener - Update hid device listener.
* @returns {void}
*/
export function attachHidEventListeners(
initDeviceListener: EventListenerOrEventListenerObject,
updateDeviceListener: EventListenerOrEventListenerObject
) {
const hidManager = getWebHidInstance();
if (typeof initDeviceListener === 'function') {
hidManager.addEventListener(EVENT_TYPE.INIT_DEVICE, initDeviceListener);
}
if (typeof updateDeviceListener === 'function') {
hidManager.addEventListener(EVENT_TYPE.UPDATE_DEVICE, updateDeviceListener);
}
}
/**
* Returns instance of web hid manager.
*
* @returns {WebHidManager} - WebHidManager instance.
*/
export function getWebHidInstance(): WebHidManager {
const hidManager = WebHidManager.getInstance();
return hidManager;
}
/**
* Returns root conference state.
*
* @param {IReduxState} state - Global state.
* @returns {Object} Conference state.
*/
export const getWebHidState = (state: IReduxState) => state['features/web-hid'];
/**
* Returns true if hid is supported.
*
* @returns {boolean}
*/
export function isDeviceHidSupported(): boolean {
const hidManager = getWebHidInstance();
return hidManager.isSupported();
}
/**
* Returns device info from state.
*
* @param {IReduxState} state - Global state.
* @returns {boolean}
*/
export function getDeviceInfo(state: IReduxState): IDeviceInfo {
const hidState = getWebHidState(state);
return hidState.deviceInfo;
}
/**
* Handles updating hid device.
*
* @param {Function} dispatch - Redux dispatch.
* @param {Function} customEventData - Custom event data.
* @returns {void}
*/
export function handleUpdateHidDevice(
dispatch: IStore['dispatch'],
customEventData: CustomEvent<{ actionResult?: { eventName: string; }; deviceInfo: IDeviceInfo; }>
) {
dispatch(updateDeviceInfo(customEventData.detail.deviceInfo));
if (customEventData.detail?.actionResult?.eventName === ACTION_HOOK_TYPE_NAME.MUTE_SWITCH_ON) {
dispatch(muteLocal(true, MEDIA_TYPE.AUDIO));
} else if (customEventData.detail?.actionResult?.eventName === ACTION_HOOK_TYPE_NAME.MUTE_SWITCH_OFF) {
dispatch(muteLocal(false, MEDIA_TYPE.AUDIO));
}
}
/**
* Remove web hid event listeners.
*
* @param {Function} initDeviceListener - Init hid device listener.
* @param {Function} updateDeviceListener - Update hid device listener.
* @returns {void}
*/
export function removeHidEventListeners(
initDeviceListener: EventListenerOrEventListenerObject,
updateDeviceListener: EventListenerOrEventListenerObject
) {
const hidManager = getWebHidInstance();
if (typeof initDeviceListener === 'function') {
hidManager.removeEventListener(EVENT_TYPE.INIT_DEVICE, initDeviceListener);
}
if (typeof updateDeviceListener === 'function') {
hidManager.removeEventListener(EVENT_TYPE.UPDATE_DEVICE, updateDeviceListener);
}
}
/**
* Returns true if there is no device info provided.
*
* @param {IDeviceInfo} deviceInfo - Device info state.
* @returns {boolean}
*/
export function shouldRequestHIDDevice(deviceInfo: IDeviceInfo): boolean {
return !deviceInfo?.device || Object.keys(deviceInfo).length === 0;
}

View File

@@ -0,0 +1,3 @@
import { getLogger } from '../base/logging/functions';
export default getLogger('features/hid');

View File

@@ -0,0 +1,145 @@
import { IStore } from '../app/types';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
import { getWebHIDFeatureConfig } from '../base/config/functions.web';
import { SET_AUDIO_MUTED } from '../base/media/actionTypes';
import { isAudioMuted } from '../base/media/functions';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { CLOSE_HID_DEVICE, REQUEST_HID_DEVICE } from './actionTypes';
import { initDeviceInfo } from './actions';
import {
attachHidEventListeners,
getWebHidInstance,
handleUpdateHidDevice,
isDeviceHidSupported,
removeHidEventListeners
} from './functions';
import logger from './logger';
import { COMMANDS, IDeviceInfo } from './types';
/**
* A listener for initialising the webhid device.
*/
let initDeviceListener: (e: any) => void;
/**
* A listener for updating the webhid device.
*/
let updateDeviceListener: (e: any) => void;
/**
* The redux middleware for {@link WebHid}.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register((store: IStore) => next => action => {
const { dispatch, getState } = store;
if (!getWebHIDFeatureConfig(getState())) {
return next(action);
}
switch (action.type) {
case APP_WILL_MOUNT: {
const hidManager = getWebHidInstance();
if (!hidManager.isSupported()) {
logger.warn('HID is not supported');
break;
}
const _initDeviceListener = (e: CustomEvent<{ deviceInfo: IDeviceInfo; }>) =>
dispatch(initDeviceInfo(e.detail.deviceInfo));
const _updateDeviceListener
= (e: CustomEvent<{ actionResult: { eventName: string; }; deviceInfo: IDeviceInfo; }>) =>
handleUpdateHidDevice(dispatch, e);
initDeviceListener = _initDeviceListener;
updateDeviceListener = _updateDeviceListener;
hidManager.listenToConnectedHid();
attachHidEventListeners(initDeviceListener, updateDeviceListener);
break;
}
case APP_WILL_UNMOUNT: {
const hidManager = getWebHidInstance();
if (!isDeviceHidSupported()) {
break;
}
removeHidEventListeners(initDeviceListener, updateDeviceListener);
hidManager.close();
break;
}
case CLOSE_HID_DEVICE: {
const hidManager = getWebHidInstance();
// cleanup event handlers when hid device is removed from Settings.
removeHidEventListeners(initDeviceListener, updateDeviceListener);
hidManager.close();
break;
}
case REQUEST_HID_DEVICE: {
_onRequestHIDDevice(store);
break;
}
case SET_AUDIO_MUTED: {
const hidManager = getWebHidInstance();
if (!isDeviceHidSupported()) {
break;
}
hidManager.sendDeviceReport({ command: action.muted ? COMMANDS.MUTE_ON : COMMANDS.MUTE_OFF });
break;
}
}
return next(action);
});
/**
* Handles HID device requests.
*
* @param {IStore} store - The redux store.
* @returns {Promise}
*/
async function _onRequestHIDDevice(store: IStore) {
const { dispatch } = store;
const hidManager = getWebHidInstance();
const availableDevices = await hidManager.requestHidDevices();
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
if (!availableDevices || !availableDevices.length) {
logger.info('HID device not available');
return;
}
const _initDeviceListener = (e: CustomEvent<{ deviceInfo: IDeviceInfo; }>) =>
dispatch(initDeviceInfo(e.detail.deviceInfo));
const _updateDeviceListener
= (e: CustomEvent<{ actionResult: { eventName: string; }; deviceInfo: IDeviceInfo; }>) => {
handleUpdateHidDevice(dispatch, e);
};
initDeviceListener = _initDeviceListener;
updateDeviceListener = _updateDeviceListener;
attachHidEventListeners(initDeviceListener, updateDeviceListener);
await hidManager.listenToConnectedHid();
// sync headset to mute if participant is already muted.
if (isAudioMuted(store.getState())) {
hidManager.sendDeviceReport({ command: COMMANDS.MUTE_ON });
}
}

View File

@@ -0,0 +1,43 @@
import ReducerRegistry from '../base/redux/ReducerRegistry';
import { CLOSE_HID_DEVICE, INIT_DEVICE, UPDATE_DEVICE } from './actionTypes';
import { IDeviceInfo } from './types';
/**
* The initial state of the web-hid feature.
*/
const DEFAULT_STATE = {
deviceInfo: {} as IDeviceInfo
};
export interface IWebHid {
deviceInfo: IDeviceInfo;
}
ReducerRegistry.register<IWebHid>(
'features/web-hid',
(state: IWebHid = DEFAULT_STATE, action): IWebHid => {
switch (action.type) {
case INIT_DEVICE:
return {
...state,
deviceInfo: action.deviceInfo
};
case UPDATE_DEVICE:
return {
...state,
deviceInfo: {
...state.deviceInfo,
...action.updates
}
};
case CLOSE_HID_DEVICE:
return {
...state,
deviceInfo: DEFAULT_STATE.deviceInfo
};
default:
return state;
}
});

View File

@@ -0,0 +1,44 @@
export const EVENT_TYPE = {
INIT_DEVICE: 'INIT_DEVICE',
UPDATE_DEVICE: 'UPDATE_DEVICE'
};
export const HOOK_STATUS = {
ON: 'on',
OFF: 'off'
};
export const COMMANDS = {
ON_HOOK: 'onHook',
OFF_HOOK: 'offHook',
MUTE_OFF: 'muteOff',
MUTE_ON: 'muteOn',
ON_RING: 'onRing',
OFF_RING: 'offRing',
ON_HOLD: 'onHold',
OFF_HOLD: 'offHold'
};
export const INPUT_REPORT_EVENT_NAME = {
ON_DEVICE_HOOK_SWITCH: 'ondevicehookswitch',
ON_DEVICE_MUTE_SWITCH: 'ondevicemuteswitch'
};
export const ACTION_HOOK_TYPE_NAME = {
HOOK_SWITCH_ON: 'HOOK_SWITCH_ON',
HOOK_SWITCH_OFF: 'HOOK_SWITCH_OFF',
MUTE_SWITCH_ON: 'MUTE_SWITCH_ON',
MUTE_SWITCH_OFF: 'MUTE_SWITCH_OFF',
VOLUME_CHANGE_UP: 'VOLUME_CHANGE_UP',
VOLUME_CHANGE_DOWN: 'VOLUME_CHANGE_DOWN'
};
export interface IDeviceInfo {
// @ts-ignore
device: HIDDevice;
hold: boolean;
hookStatus: string;
muted: boolean;
ring: boolean;
}

View File

@@ -0,0 +1,52 @@
/**
* Telephony usage actions based on HID Usage tables for Universal Serial Bus (page 112.).
*
*/
export const TELEPHONY_DEVICE_USAGE_PAGE = 11;
/** Telephony usages
* - used to parse HIDDevice UsageId collections
** - outputReports has mute and offHook
** - inputReports exists hookSwitch and phoneMute.
**/
export const DEVICE_USAGE = {
/* outputReports. */
mute: {
usageId: 0x080009,
usageName: 'Mute'
},
offHook: {
usageId: 0x080017,
usageName: 'Off Hook'
},
ring: {
usageId: 0x080018,
usageName: 'Ring'
},
hold: {
usageId: 0x080020,
usageName: 'Hold'
},
/* inputReports. */
hookSwitch: {
usageId: 0x0b0020,
usageName: 'Hook Switch'
},
phoneMute: {
usageId: 0x0b002f,
usageName: 'Phone Mute'
}
};
/**
* Filter with telephony devices based on HID Usage tables for Universal Serial Bus (page 17).
*
* @type {{ filters: { usagePage: string }; exclusionFilters: {}; }}
*/
export const requestTelephonyHID = {
filters: [ {
usagePage: TELEPHONY_DEVICE_USAGE_PAGE
} ],
exclusionFilters: []
};

File diff suppressed because it is too large Load Diff