@xipkg/calls-utils 0.1.15 → 0.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +24 -1
- package/dist/index.mjs +57 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -35,6 +35,29 @@ type SoundType = keyof typeof SOUND_PATHS;
|
|
|
35
35
|
* @param volume - громкость от 0 до 1 (0 = беззвучно, 1 = максимальная громкость)
|
|
36
36
|
*/
|
|
37
37
|
declare const playSound: (soundType: SoundType, volume?: number) => void;
|
|
38
|
+
type PlaySoundOnDeviceOptions = {
|
|
39
|
+
volume?: number;
|
|
40
|
+
/** deviceId устройства вывода (HTMLMediaElement.setSinkId), если поддерживается */
|
|
41
|
+
sinkId?: string;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Воспроизводит тестовый звук на выбранном устройстве вывода.
|
|
45
|
+
* Возвращает Promise, который резолвится после старта playback (или reject при ошибке).
|
|
46
|
+
* Требует, чтобы хост отдавал файлы из `/sounds/...`.
|
|
47
|
+
*/
|
|
48
|
+
declare const playSoundOnDevice: (soundType: SoundType, options?: PlaySoundOnDeviceOptions) => Promise<void>;
|
|
49
|
+
type PlaySpeakerTestToneOptions = {
|
|
50
|
+
volume?: number;
|
|
51
|
+
/** deviceId устройства вывода (AudioContext.setSinkId), если поддерживается */
|
|
52
|
+
sinkId?: string;
|
|
53
|
+
durationMs?: number;
|
|
54
|
+
frequencyHz?: number;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Тестовый тон через Web Audio API — не зависит от файлов `/sounds/*` на хосте.
|
|
58
|
+
* Использует AudioContext.setSinkId, когда браузер и deviceId это позволяют.
|
|
59
|
+
*/
|
|
60
|
+
declare const playSpeakerTestTone: (options?: PlaySpeakerTestToneOptions) => Promise<void>;
|
|
38
61
|
|
|
39
62
|
/** События телеметрии шумоподавления для umami. */
|
|
40
63
|
declare const NOISE_CANCELLATION_EVENTS: {
|
|
@@ -46,4 +69,4 @@ declare const NOISE_CANCELLATION_EVENTS: {
|
|
|
46
69
|
type NoiseCancellationEvent = (typeof NOISE_CANCELLATION_EVENTS)[keyof typeof NOISE_CANCELLATION_EVENTS];
|
|
47
70
|
declare function trackNoiseCancellationEvent(event: NoiseCancellationEvent, data?: Record<string, string>): void;
|
|
48
71
|
|
|
49
|
-
export { NOISE_CANCELLATION_EVENTS, isChromiumBased, isFireFox, isLocal, isMacintosh, isSafari, playSound, trackNoiseCancellationEvent, useFullScreen, useKeyPress, useMedia, usePhoneLayout };
|
|
72
|
+
export { NOISE_CANCELLATION_EVENTS, isChromiumBased, isFireFox, isLocal, isMacintosh, isSafari, playSound, playSoundOnDevice, playSpeakerTestTone, trackNoiseCancellationEvent, useFullScreen, useKeyPress, useMedia, usePhoneLayout };
|
package/dist/index.mjs
CHANGED
|
@@ -151,6 +151,62 @@ var playSound = (soundType, volume = 1) => {
|
|
|
151
151
|
console.error(`\u274C Error playing sound ${soundType}:`, error);
|
|
152
152
|
}
|
|
153
153
|
};
|
|
154
|
+
var playSoundOnDevice = async (soundType, options = {}) => {
|
|
155
|
+
const { volume = 1, sinkId } = options;
|
|
156
|
+
const audio = new Audio(SOUND_PATHS[soundType]);
|
|
157
|
+
audio.volume = Math.max(0, Math.min(1, volume));
|
|
158
|
+
const audioWithSink = audio;
|
|
159
|
+
if (sinkId && typeof audioWithSink.setSinkId === "function") {
|
|
160
|
+
try {
|
|
161
|
+
await audioWithSink.setSinkId(sinkId);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.warn("\u26A0\uFE0F Failed to set audio output device:", error);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
audio.currentTime = 0;
|
|
167
|
+
await audio.play();
|
|
168
|
+
};
|
|
169
|
+
var playSpeakerTestTone = async (options = {}) => {
|
|
170
|
+
const { volume = 1, sinkId, durationMs = 900, frequencyHz = 880 } = options;
|
|
171
|
+
const AudioContextCtor = window.AudioContext || window.webkitAudioContext;
|
|
172
|
+
if (!AudioContextCtor) {
|
|
173
|
+
throw new Error("AudioContext is not supported");
|
|
174
|
+
}
|
|
175
|
+
const ctx = new AudioContextCtor();
|
|
176
|
+
try {
|
|
177
|
+
if (sinkId && typeof ctx.setSinkId === "function") {
|
|
178
|
+
try {
|
|
179
|
+
await ctx.setSinkId(sinkId);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.warn("\u26A0\uFE0F Failed to set audio output device for test tone:", error);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (ctx.state === "suspended") {
|
|
185
|
+
await ctx.resume();
|
|
186
|
+
}
|
|
187
|
+
const oscillator = ctx.createOscillator();
|
|
188
|
+
const gain = ctx.createGain();
|
|
189
|
+
oscillator.type = "sine";
|
|
190
|
+
oscillator.frequency.value = frequencyHz;
|
|
191
|
+
const peak = Math.max(0, Math.min(1, volume)) * 0.22;
|
|
192
|
+
const now = ctx.currentTime;
|
|
193
|
+
const end = now + durationMs / 1e3;
|
|
194
|
+
gain.gain.setValueAtTime(0, now);
|
|
195
|
+
gain.gain.linearRampToValueAtTime(peak, now + 0.04);
|
|
196
|
+
gain.gain.linearRampToValueAtTime(peak * 0.85, end - 0.12);
|
|
197
|
+
gain.gain.linearRampToValueAtTime(0, end);
|
|
198
|
+
oscillator.connect(gain);
|
|
199
|
+
gain.connect(ctx.destination);
|
|
200
|
+
oscillator.start(now);
|
|
201
|
+
oscillator.stop(end);
|
|
202
|
+
await new Promise((resolve) => {
|
|
203
|
+
window.setTimeout(resolve, durationMs + 40);
|
|
204
|
+
});
|
|
205
|
+
} finally {
|
|
206
|
+
await ctx.close().catch(() => {
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
};
|
|
154
210
|
|
|
155
211
|
// src/noiseCancellationTelemetry.ts
|
|
156
212
|
var NOISE_CANCELLATION_EVENTS = {
|
|
@@ -171,6 +227,6 @@ function trackNoiseCancellationEvent(event, data) {
|
|
|
171
227
|
}
|
|
172
228
|
}
|
|
173
229
|
|
|
174
|
-
export { NOISE_CANCELLATION_EVENTS, isChromiumBased, isFireFox, isLocal, isMacintosh, isSafari, playSound, trackNoiseCancellationEvent, useFullScreen, useKeyPress, useMedia, usePhoneLayout };
|
|
230
|
+
export { NOISE_CANCELLATION_EVENTS, isChromiumBased, isFireFox, isLocal, isMacintosh, isSafari, playSound, playSoundOnDevice, playSpeakerTestTone, trackNoiseCancellationEvent, useFullScreen, useKeyPress, useMedia, usePhoneLayout };
|
|
175
231
|
//# sourceMappingURL=index.mjs.map
|
|
176
232
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/useFullScreen.ts","../src/useKeyPress.ts","../src/useMedia.ts","../src/usePhoneLayout.ts","../src/livekit.ts","../src/sounds.ts","../src/noiseCancellationTelemetry.ts"],"names":["useEffect","useState"],"mappings":"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","file":"index.mjs","sourcesContent":["import { useState, useEffect } from 'react';\n\nexport const useFullScreen = (containerId: string) => {\n const [isFullScreen, setIsFullScreen] = useState(false);\n\n const hasStandardFullscreen =\n typeof document !== 'undefined' &&\n document.fullscreenEnabled &&\n typeof HTMLElement.prototype.requestFullscreen === 'function';\n\n const isFullScreenSupported = hasStandardFullscreen;\n\n const toggleFullScreen = () => {\n const element = document.getElementById(containerId);\n const fullScreen = document.fullscreenElement;\n\n if (fullScreen) {\n document.exitFullscreen();\n setIsFullScreen(false);\n } else {\n element?.requestFullscreen();\n setIsFullScreen(true);\n }\n };\n\n useEffect(() => {\n if (!isFullScreenSupported) return;\n\n const handleFullScreenChange = () => {\n const fullScreen = !!document.fullscreenElement;\n setIsFullScreen(fullScreen);\n };\n\n document.addEventListener('fullscreenchange', handleFullScreenChange);\n\n return () => {\n document.removeEventListener('fullscreenchange', handleFullScreenChange);\n };\n }, [isFullScreenSupported]);\n\n return { toggleFullScreen, isFullScreen, isFullScreenSupported };\n};\n","import { useEffect } from 'react';\n\nexport const useKeyPress = (targetKey: string, callback: (event: KeyboardEvent) => void) => {\n useEffect(() => {\n // console.log(`Added listener for ${targetKey}`);\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === targetKey) {\n callback(event);\n }\n };\n\n window.addEventListener('keydown', handleKeyDown);\n return () => {\n // console.log(`Removed listener for ${targetKey}`);\n window.removeEventListener('keydown', handleKeyDown);\n };\n }, [targetKey, callback]);\n};\n","import { useEffect, useState } from 'react';\n\nconst isBrowser = typeof window !== 'undefined';\n\nconst getInitialState = (query: string, defaultState?: boolean) => {\n // Prevent a React hydration mismatch when a default value\n // is provided by not defaulting to window.matchMedia(query).matches.\n if (defaultState !== undefined) {\n return defaultState;\n }\n\n if (isBrowser) {\n return window.matchMedia(query).matches;\n }\n\n return false;\n};\n\nexport const useMedia = (query: string, defaultState?: boolean) => {\n const [state, setState] = useState(getInitialState(query, defaultState));\n\n useEffect(() => {\n let mounted = true;\n const mql = window.matchMedia(query);\n const onChange = () => {\n if (!mounted) {\n return;\n }\n setState(!!mql.matches);\n };\n\n mql.addEventListener('change', onChange);\n setState(mql.matches);\n\n return () => {\n mounted = false;\n mql.removeEventListener('change', onChange);\n };\n }, [query]);\n\n return state;\n};\n","import { useMedia } from './useMedia';\n\n/** Только телефон: планшеты и iPad используют десктопный compact-вид. */\nexport function usePhoneLayout(): boolean {\n return useMedia('(max-width: 720px)');\n}\n","import { getBrowser, LocalParticipant, LogLevel, Participant, setLogLevel } from 'livekit-client';\n\nexport const silenceLiveKitLogs = (shouldSilenceLogs: boolean) => {\n setLogLevel(shouldSilenceLogs ? LogLevel.silent : LogLevel.debug);\n};\n\nexport function isFireFox(): boolean {\n return getBrowser()?.name === 'Firefox';\n}\n\nexport function isChromiumBased(): boolean {\n return getBrowser()?.name === 'Chrome';\n}\n\nexport function isSafari(): boolean {\n return getBrowser()?.name === 'Safari';\n}\n\nexport function isLocal(p: Participant) {\n return p instanceof LocalParticipant;\n}\n\nexport function isMacintosh() {\n return navigator.platform.indexOf('Mac') > -1;\n}\n","/**\n * Утилита для воспроизведения звуков уведомлений в ВКС\n */\n\nconst SOUND_PATHS = {\n chatMessage: '/sounds/chat-message.wav',\n handRaise: '/sounds/hand-raise.wav',\n userJoin: '/sounds/user-join-to-call.mp3',\n userLeft: '/sounds/user-left-from-call.mp3',\n} as const;\n\ntype SoundType = keyof typeof SOUND_PATHS;\n\n/** Для join/left — воспроизводим только начало файла (мс). */\nconst JOIN_LEAVE_PLAY_MS = 500;\n\ntype TrimmedSoundType = Extract<SoundType, 'userJoin' | 'userLeft'>;\nconst trimTimeouts = new Map<TrimmedSoundType, ReturnType<typeof setTimeout>>();\n\n// Кэш для Audio объектов, чтобы не создавать их каждый раз\nconst soundCache = new Map<SoundType, HTMLAudioElement>();\n\n/**\n * Получает или создает Audio объект для звука\n */\nconst getAudio = (soundType: SoundType): HTMLAudioElement => {\n if (!soundCache.has(soundType)) {\n const audio = new Audio(SOUND_PATHS[soundType]);\n audio.preload = 'auto';\n soundCache.set(soundType, audio);\n }\n return soundCache.get(soundType)!;\n};\n\nconst clearJoinLeaveTrim = (soundType: TrimmedSoundType) => {\n const id = trimTimeouts.get(soundType);\n if (id !== undefined) {\n clearTimeout(id);\n trimTimeouts.delete(soundType);\n }\n};\n\nconst scheduleJoinLeaveTrim = (audio: HTMLAudioElement, soundType: TrimmedSoundType) => {\n clearJoinLeaveTrim(soundType);\n const id = setTimeout(() => {\n try {\n audio.pause();\n audio.currentTime = 0;\n } catch {\n /* ignore */\n }\n trimTimeouts.delete(soundType);\n }, JOIN_LEAVE_PLAY_MS);\n trimTimeouts.set(soundType, id);\n};\n\n/**\n * Воспроизводит звук с указанной громкостью\n * @param soundType - тип звука (chatMessage или handRaise)\n * @param volume - громкость от 0 до 1 (0 = беззвучно, 1 = максимальная громкость)\n */\nexport const playSound = (soundType: SoundType, volume: number = 1): void => {\n try {\n const audio = getAudio(soundType);\n\n if (soundType === 'userJoin' || soundType === 'userLeft') {\n clearJoinLeaveTrim(soundType);\n }\n\n // Устанавливаем громкость (от 0 до 1)\n const clampedVolume = Math.max(0, Math.min(1, volume));\n audio.volume = clampedVolume;\n\n // Сбрасываем позицию на начало и воспроизводим\n audio.currentTime = 0;\n void audio\n .play()\n .then(() => {\n if (soundType === 'userJoin' || soundType === 'userLeft') {\n scheduleJoinLeaveTrim(audio, soundType);\n }\n })\n .catch((error) => {\n // Игнорируем ошибки воспроизведения (например, если пользователь не взаимодействовал со страницей)\n console.warn(`⚠️ Failed to play sound ${soundType}:`, error);\n });\n } catch (error) {\n console.error(`❌ Error playing sound ${soundType}:`, error);\n }\n};\n","/** События телеметрии шумоподавления для umami. */\nexport const NOISE_CANCELLATION_EVENTS = {\n MODE_CHANGED: 'noise_cancellation_mode_changed',\n APPLY_FAILED: 'noise_cancellation_apply_failed',\n FALLBACK_TO_WEBRTC: 'noise_cancellation_fallback_to_webrtc',\n KRISP_UNSUPPORTED_BROWSER: 'noise_cancellation_krisp_unsupported_browser',\n} as const;\n\ntype NoiseCancellationEvent =\n (typeof NOISE_CANCELLATION_EVENTS)[keyof typeof NOISE_CANCELLATION_EVENTS];\n\nfunction getUmami(): { track: (name: string, data?: Record<string, string>) => void } | undefined {\n if (typeof window === 'undefined') return undefined;\n const win = window as Window & {\n umami?: { track: (name: string, data?: Record<string, string>) => void };\n };\n return win.umami;\n}\n\nexport function trackNoiseCancellationEvent(\n event: NoiseCancellationEvent,\n data?: Record<string, string>,\n): void {\n const umami = getUmami();\n if (umami) {\n umami.track(event, data);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/useFullScreen.ts","../src/useKeyPress.ts","../src/useMedia.ts","../src/usePhoneLayout.ts","../src/livekit.ts","../src/sounds.ts","../src/noiseCancellationTelemetry.ts"],"names":["useEffect","useState"],"mappings":";;;;AAEO,IAAM,aAAA,GAAgB,CAAC,WAAA,KAAwB;AACpD,EAAA,MAAM,CAAC,YAAA,EAAc,eAAe,CAAA,GAAI,SAAS,KAAK,CAAA;AAEtD,EAAA,MAAM,qBAAA,GACJ,OAAO,QAAA,KAAa,WAAA,IACpB,SAAS,iBAAA,IACT,OAAO,WAAA,CAAY,SAAA,CAAU,iBAAA,KAAsB,UAAA;AAErD,EAAA,MAAM,qBAAA,GAAwB,qBAAA;AAE9B,EAAA,MAAM,mBAAmB,MAAM;AAC7B,IAAA,MAAM,OAAA,GAAU,QAAA,CAAS,cAAA,CAAe,WAAW,CAAA;AACnD,IAAA,MAAM,aAAa,QAAA,CAAS,iBAAA;AAE5B,IAAA,IAAI,UAAA,EAAY;AACd,MAAA,QAAA,CAAS,cAAA,EAAe;AACxB,MAAA,eAAA,CAAgB,KAAK,CAAA;AAAA,IACvB,CAAA,MAAO;AACL,MAAA,OAAA,EAAS,iBAAA,EAAkB;AAC3B,MAAA,eAAA,CAAgB,IAAI,CAAA;AAAA,IACtB;AAAA,EACF,CAAA;AAEA,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,qBAAA,EAAuB;AAE5B,IAAA,MAAM,yBAAyB,MAAM;AACnC,MAAA,MAAM,UAAA,GAAa,CAAC,CAAC,QAAA,CAAS,iBAAA;AAC9B,MAAA,eAAA,CAAgB,UAAU,CAAA;AAAA,IAC5B,CAAA;AAEA,IAAA,QAAA,CAAS,gBAAA,CAAiB,oBAAoB,sBAAsB,CAAA;AAEpE,IAAA,OAAO,MAAM;AACX,MAAA,QAAA,CAAS,mBAAA,CAAoB,oBAAoB,sBAAsB,CAAA;AAAA,IACzE,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,qBAAqB,CAAC,CAAA;AAE1B,EAAA,OAAO,EAAE,gBAAA,EAAkB,YAAA,EAAc,qBAAA,EAAsB;AACjE;ACvCO,IAAM,WAAA,GAAc,CAAC,SAAA,EAAmB,QAAA,KAA6C;AAC1F,EAAAA,UAAU,MAAM;AAEd,IAAA,MAAM,aAAA,GAAgB,CAAC,KAAA,KAAyB;AAC9C,MAAA,IAAI,KAAA,CAAM,QAAQ,SAAA,EAAW;AAC3B,QAAA,QAAA,CAAS,KAAK,CAAA;AAAA,MAChB;AAAA,IACF,CAAA;AAEA,IAAA,MAAA,CAAO,gBAAA,CAAiB,WAAW,aAAa,CAAA;AAChD,IAAA,OAAO,MAAM;AAEX,MAAA,MAAA,CAAO,mBAAA,CAAoB,WAAW,aAAa,CAAA;AAAA,IACrD,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,SAAA,EAAW,QAAQ,CAAC,CAAA;AAC1B;ACfA,IAAM,SAAA,GAAY,OAAO,MAAA,KAAW,WAAA;AAEpC,IAAM,eAAA,GAAkB,CAAC,KAAA,EAAe,YAAA,KAA2B;AAGjE,EAAA,IAAI,iBAAiB,MAAA,EAAW;AAC9B,IAAA,OAAO,YAAA;AAAA,EACT;AAEA,EAAA,IAAI,SAAA,EAAW;AACb,IAAA,OAAO,MAAA,CAAO,UAAA,CAAW,KAAK,CAAA,CAAE,OAAA;AAAA,EAClC;AAEA,EAAA,OAAO,KAAA;AACT,CAAA;AAEO,IAAM,QAAA,GAAW,CAAC,KAAA,EAAe,YAAA,KAA2B;AACjE,EAAA,MAAM,CAAC,OAAO,QAAQ,CAAA,GAAIC,SAAS,eAAA,CAAgB,KAAA,EAAO,YAAY,CAAC,CAAA;AAEvE,EAAAD,UAAU,MAAM;AACd,IAAA,IAAI,OAAA,GAAU,IAAA;AACd,IAAA,MAAM,GAAA,GAAM,MAAA,CAAO,UAAA,CAAW,KAAK,CAAA;AACnC,IAAA,MAAM,WAAW,MAAM;AACrB,MAAA,IAAI,CAAC,OAAA,EAAS;AACZ,QAAA;AAAA,MACF;AACA,MAAA,QAAA,CAAS,CAAC,CAAC,GAAA,CAAI,OAAO,CAAA;AAAA,IACxB,CAAA;AAEA,IAAA,GAAA,CAAI,gBAAA,CAAiB,UAAU,QAAQ,CAAA;AACvC,IAAA,QAAA,CAAS,IAAI,OAAO,CAAA;AAEpB,IAAA,OAAO,MAAM;AACX,MAAA,OAAA,GAAU,KAAA;AACV,MAAA,GAAA,CAAI,mBAAA,CAAoB,UAAU,QAAQ,CAAA;AAAA,IAC5C,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,KAAK,CAAC,CAAA;AAEV,EAAA,OAAO,KAAA;AACT;;;ACtCO,SAAS,cAAA,GAA0B;AACxC,EAAA,OAAO,SAAS,oBAAoB,CAAA;AACtC;ACCO,SAAS,SAAA,GAAqB;AACnC,EAAA,OAAO,UAAA,IAAc,IAAA,KAAS,SAAA;AAChC;AAEO,SAAS,eAAA,GAA2B;AACzC,EAAA,OAAO,UAAA,IAAc,IAAA,KAAS,QAAA;AAChC;AAEO,SAAS,QAAA,GAAoB;AAClC,EAAA,OAAO,UAAA,IAAc,IAAA,KAAS,QAAA;AAChC;AAEO,SAAS,QAAQ,CAAA,EAAgB;AACtC,EAAA,OAAO,CAAA,YAAa,gBAAA;AACtB;AAEO,SAAS,WAAA,GAAc;AAC5B,EAAA,OAAO,SAAA,CAAU,QAAA,CAAS,OAAA,CAAQ,KAAK,CAAA,GAAI,EAAA;AAC7C;;;ACpBA,IAAM,WAAA,GAAc;AAAA,EAClB,WAAA,EAAa,0BAAA;AAAA,EACb,SAAA,EAAW,wBAAA;AAAA,EACX,QAAA,EAAU,+BAAA;AAAA,EACV,QAAA,EAAU;AACZ,CAAA;AAKA,IAAM,kBAAA,GAAqB,GAAA;AAG3B,IAAM,YAAA,uBAAmB,GAAA,EAAqD;AAG9E,IAAM,UAAA,uBAAiB,GAAA,EAAiC;AAKxD,IAAM,QAAA,GAAW,CAAC,SAAA,KAA2C;AAC3D,EAAA,IAAI,CAAC,UAAA,CAAW,GAAA,CAAI,SAAS,CAAA,EAAG;AAC9B,IAAA,MAAM,KAAA,GAAQ,IAAI,KAAA,CAAM,WAAA,CAAY,SAAS,CAAC,CAAA;AAC9C,IAAA,KAAA,CAAM,OAAA,GAAU,MAAA;AAChB,IAAA,UAAA,CAAW,GAAA,CAAI,WAAW,KAAK,CAAA;AAAA,EACjC;AACA,EAAA,OAAO,UAAA,CAAW,IAAI,SAAS,CAAA;AACjC,CAAA;AAEA,IAAM,kBAAA,GAAqB,CAAC,SAAA,KAAgC;AAC1D,EAAA,MAAM,EAAA,GAAK,YAAA,CAAa,GAAA,CAAI,SAAS,CAAA;AACrC,EAAA,IAAI,OAAO,MAAA,EAAW;AACpB,IAAA,YAAA,CAAa,EAAE,CAAA;AACf,IAAA,YAAA,CAAa,OAAO,SAAS,CAAA;AAAA,EAC/B;AACF,CAAA;AAEA,IAAM,qBAAA,GAAwB,CAAC,KAAA,EAAyB,SAAA,KAAgC;AACtF,EAAA,kBAAA,CAAmB,SAAS,CAAA;AAC5B,EAAA,MAAM,EAAA,GAAK,WAAW,MAAM;AAC1B,IAAA,IAAI;AACF,MAAA,KAAA,CAAM,KAAA,EAAM;AACZ,MAAA,KAAA,CAAM,WAAA,GAAc,CAAA;AAAA,IACtB,CAAA,CAAA,MAAQ;AAAA,IAER;AACA,IAAA,YAAA,CAAa,OAAO,SAAS,CAAA;AAAA,EAC/B,GAAG,kBAAkB,CAAA;AACrB,EAAA,YAAA,CAAa,GAAA,CAAI,WAAW,EAAE,CAAA;AAChC,CAAA;AAOO,IAAM,SAAA,GAAY,CAAC,SAAA,EAAsB,MAAA,GAAiB,CAAA,KAAY;AAC3E,EAAA,IAAI;AACF,IAAA,MAAM,KAAA,GAAQ,SAAS,SAAS,CAAA;AAEhC,IAAA,IAAI,SAAA,KAAc,UAAA,IAAc,SAAA,KAAc,UAAA,EAAY;AACxD,MAAA,kBAAA,CAAmB,SAAS,CAAA;AAAA,IAC9B;AAGA,IAAA,MAAM,aAAA,GAAgB,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,GAAA,CAAI,CAAA,EAAG,MAAM,CAAC,CAAA;AACrD,IAAA,KAAA,CAAM,MAAA,GAAS,aAAA;AAGf,IAAA,KAAA,CAAM,WAAA,GAAc,CAAA;AACpB,IAAA,KAAK,KAAA,CACF,IAAA,EAAK,CACL,IAAA,CAAK,MAAM;AACV,MAAA,IAAI,SAAA,KAAc,UAAA,IAAc,SAAA,KAAc,UAAA,EAAY;AACxD,QAAA,qBAAA,CAAsB,OAAO,SAAS,CAAA;AAAA,MACxC;AAAA,IACF,CAAC,CAAA,CACA,KAAA,CAAM,CAAC,KAAA,KAAU;AAEhB,MAAA,OAAA,CAAQ,IAAA,CAAK,CAAA,kCAAA,EAA2B,SAAS,CAAA,CAAA,CAAA,EAAK,KAAK,CAAA;AAAA,IAC7D,CAAC,CAAA;AAAA,EACL,SAAS,KAAA,EAAO;AACd,IAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,2BAAA,EAAyB,SAAS,CAAA,CAAA,CAAA,EAAK,KAAK,CAAA;AAAA,EAC5D;AACF;AAiBO,IAAM,iBAAA,GAAoB,OAC/B,SAAA,EACA,OAAA,GAAoC,EAAC,KACnB;AAClB,EAAA,MAAM,EAAE,MAAA,GAAS,CAAA,EAAG,MAAA,EAAO,GAAI,OAAA;AAC/B,EAAA,MAAM,KAAA,GAAQ,IAAI,KAAA,CAAM,WAAA,CAAY,SAAS,CAAC,CAAA;AAC9C,EAAA,KAAA,CAAM,MAAA,GAAS,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,GAAA,CAAI,CAAA,EAAG,MAAM,CAAC,CAAA;AAE9C,EAAA,MAAM,aAAA,GAAgB,KAAA;AAItB,EAAA,IAAI,MAAA,IAAU,OAAO,aAAA,CAAc,SAAA,KAAc,UAAA,EAAY;AAC3D,IAAA,IAAI;AACF,MAAA,MAAM,aAAA,CAAc,UAAU,MAAM,CAAA;AAAA,IACtC,SAAS,KAAA,EAAO;AACd,MAAA,OAAA,CAAQ,IAAA,CAAK,mDAAyC,KAAK,CAAA;AAAA,IAC7D;AAAA,EACF;AAEA,EAAA,KAAA,CAAM,WAAA,GAAc,CAAA;AACpB,EAAA,MAAM,MAAM,IAAA,EAAK;AACnB;AAcO,IAAM,mBAAA,GAAsB,OACjC,OAAA,GAAsC,EAAC,KACrB;AAClB,EAAA,MAAM,EAAE,SAAS,CAAA,EAAG,MAAA,EAAQ,aAAa,GAAA,EAAK,WAAA,GAAc,KAAI,GAAI,OAAA;AAEpE,EAAA,MAAM,gBAAA,GACJ,MAAA,CAAO,YAAA,IACN,MAAA,CAAmE,kBAAA;AAEtE,EAAA,IAAI,CAAC,gBAAA,EAAkB;AACrB,IAAA,MAAM,IAAI,MAAM,+BAA+B,CAAA;AAAA,EACjD;AAEA,EAAA,MAAM,GAAA,GAAM,IAAI,gBAAA,EAAiB;AAEjC,EAAA,IAAI;AACF,IAAA,IAAI,MAAA,IAAU,OAAO,GAAA,CAAI,SAAA,KAAc,UAAA,EAAY;AACjD,MAAA,IAAI;AACF,QAAA,MAAM,GAAA,CAAI,UAAU,MAAM,CAAA;AAAA,MAC5B,SAAS,KAAA,EAAO;AACd,QAAA,OAAA,CAAQ,IAAA,CAAK,iEAAuD,KAAK,CAAA;AAAA,MAC3E;AAAA,IACF;AAEA,IAAA,IAAI,GAAA,CAAI,UAAU,WAAA,EAAa;AAC7B,MAAA,MAAM,IAAI,MAAA,EAAO;AAAA,IACnB;AAEA,IAAA,MAAM,UAAA,GAAa,IAAI,gBAAA,EAAiB;AACxC,IAAA,MAAM,IAAA,GAAO,IAAI,UAAA,EAAW;AAC5B,IAAA,UAAA,CAAW,IAAA,GAAO,MAAA;AAClB,IAAA,UAAA,CAAW,UAAU,KAAA,GAAQ,WAAA;AAE7B,IAAA,MAAM,IAAA,GAAO,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,GAAA,CAAI,CAAA,EAAG,MAAM,CAAC,CAAA,GAAI,IAAA;AAChD,IAAA,MAAM,MAAM,GAAA,CAAI,WAAA;AAChB,IAAA,MAAM,GAAA,GAAM,MAAM,UAAA,GAAa,GAAA;AAE/B,IAAA,IAAA,CAAK,IAAA,CAAK,cAAA,CAAe,CAAA,EAAG,GAAG,CAAA;AAC/B,IAAA,IAAA,CAAK,IAAA,CAAK,uBAAA,CAAwB,IAAA,EAAM,GAAA,GAAM,IAAI,CAAA;AAClD,IAAA,IAAA,CAAK,IAAA,CAAK,uBAAA,CAAwB,IAAA,GAAO,IAAA,EAAM,MAAM,IAAI,CAAA;AACzD,IAAA,IAAA,CAAK,IAAA,CAAK,uBAAA,CAAwB,CAAA,EAAG,GAAG,CAAA;AAExC,IAAA,UAAA,CAAW,QAAQ,IAAI,CAAA;AACvB,IAAA,IAAA,CAAK,OAAA,CAAQ,IAAI,WAAW,CAAA;AAC5B,IAAA,UAAA,CAAW,MAAM,GAAG,CAAA;AACpB,IAAA,UAAA,CAAW,KAAK,GAAG,CAAA;AAEnB,IAAA,MAAM,IAAI,OAAA,CAAc,CAAC,OAAA,KAAY;AACnC,MAAA,MAAA,CAAO,UAAA,CAAW,OAAA,EAAS,UAAA,GAAa,EAAE,CAAA;AAAA,IAC5C,CAAC,CAAA;AAAA,EACH,CAAA,SAAE;AACA,IAAA,MAAM,GAAA,CAAI,KAAA,EAAM,CAAE,KAAA,CAAM,MAAM;AAAA,IAE9B,CAAC,CAAA;AAAA,EACH;AACF;;;ACpMO,IAAM,yBAAA,GAA4B;AAAA,EACvC,YAAA,EAAc,iCAAA;AAAA,EACd,YAAA,EAAc,iCAAA;AAAA,EACd,kBAAA,EAAoB,uCAAA;AAAA,EACpB,yBAAA,EAA2B;AAC7B;AAKA,SAAS,QAAA,GAAyF;AAChG,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,EAAa,OAAO,MAAA;AAC1C,EAAA,MAAM,GAAA,GAAM,MAAA;AAGZ,EAAA,OAAO,GAAA,CAAI,KAAA;AACb;AAEO,SAAS,2BAAA,CACd,OACA,IAAA,EACM;AACN,EAAA,MAAM,QAAQ,QAAA,EAAS;AACvB,EAAA,IAAI,KAAA,EAAO;AACT,IAAA,KAAA,CAAM,KAAA,CAAM,OAAO,IAAI,CAAA;AAAA,EACzB;AACF","file":"index.mjs","sourcesContent":["import { useState, useEffect } from 'react';\n\nexport const useFullScreen = (containerId: string) => {\n const [isFullScreen, setIsFullScreen] = useState(false);\n\n const hasStandardFullscreen =\n typeof document !== 'undefined' &&\n document.fullscreenEnabled &&\n typeof HTMLElement.prototype.requestFullscreen === 'function';\n\n const isFullScreenSupported = hasStandardFullscreen;\n\n const toggleFullScreen = () => {\n const element = document.getElementById(containerId);\n const fullScreen = document.fullscreenElement;\n\n if (fullScreen) {\n document.exitFullscreen();\n setIsFullScreen(false);\n } else {\n element?.requestFullscreen();\n setIsFullScreen(true);\n }\n };\n\n useEffect(() => {\n if (!isFullScreenSupported) return;\n\n const handleFullScreenChange = () => {\n const fullScreen = !!document.fullscreenElement;\n setIsFullScreen(fullScreen);\n };\n\n document.addEventListener('fullscreenchange', handleFullScreenChange);\n\n return () => {\n document.removeEventListener('fullscreenchange', handleFullScreenChange);\n };\n }, [isFullScreenSupported]);\n\n return { toggleFullScreen, isFullScreen, isFullScreenSupported };\n};\n","import { useEffect } from 'react';\n\nexport const useKeyPress = (targetKey: string, callback: (event: KeyboardEvent) => void) => {\n useEffect(() => {\n // console.log(`Added listener for ${targetKey}`);\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === targetKey) {\n callback(event);\n }\n };\n\n window.addEventListener('keydown', handleKeyDown);\n return () => {\n // console.log(`Removed listener for ${targetKey}`);\n window.removeEventListener('keydown', handleKeyDown);\n };\n }, [targetKey, callback]);\n};\n","import { useEffect, useState } from 'react';\n\nconst isBrowser = typeof window !== 'undefined';\n\nconst getInitialState = (query: string, defaultState?: boolean) => {\n // Prevent a React hydration mismatch when a default value\n // is provided by not defaulting to window.matchMedia(query).matches.\n if (defaultState !== undefined) {\n return defaultState;\n }\n\n if (isBrowser) {\n return window.matchMedia(query).matches;\n }\n\n return false;\n};\n\nexport const useMedia = (query: string, defaultState?: boolean) => {\n const [state, setState] = useState(getInitialState(query, defaultState));\n\n useEffect(() => {\n let mounted = true;\n const mql = window.matchMedia(query);\n const onChange = () => {\n if (!mounted) {\n return;\n }\n setState(!!mql.matches);\n };\n\n mql.addEventListener('change', onChange);\n setState(mql.matches);\n\n return () => {\n mounted = false;\n mql.removeEventListener('change', onChange);\n };\n }, [query]);\n\n return state;\n};\n","import { useMedia } from './useMedia';\n\n/** Только телефон: планшеты и iPad используют десктопный compact-вид. */\nexport function usePhoneLayout(): boolean {\n return useMedia('(max-width: 720px)');\n}\n","import { getBrowser, LocalParticipant, LogLevel, Participant, setLogLevel } from 'livekit-client';\n\nexport const silenceLiveKitLogs = (shouldSilenceLogs: boolean) => {\n setLogLevel(shouldSilenceLogs ? LogLevel.silent : LogLevel.debug);\n};\n\nexport function isFireFox(): boolean {\n return getBrowser()?.name === 'Firefox';\n}\n\nexport function isChromiumBased(): boolean {\n return getBrowser()?.name === 'Chrome';\n}\n\nexport function isSafari(): boolean {\n return getBrowser()?.name === 'Safari';\n}\n\nexport function isLocal(p: Participant) {\n return p instanceof LocalParticipant;\n}\n\nexport function isMacintosh() {\n return navigator.platform.indexOf('Mac') > -1;\n}\n","/**\n * Утилита для воспроизведения звуков уведомлений в ВКС\n */\n\nconst SOUND_PATHS = {\n chatMessage: '/sounds/chat-message.wav',\n handRaise: '/sounds/hand-raise.wav',\n userJoin: '/sounds/user-join-to-call.mp3',\n userLeft: '/sounds/user-left-from-call.mp3',\n} as const;\n\ntype SoundType = keyof typeof SOUND_PATHS;\n\n/** Для join/left — воспроизводим только начало файла (мс). */\nconst JOIN_LEAVE_PLAY_MS = 500;\n\ntype TrimmedSoundType = Extract<SoundType, 'userJoin' | 'userLeft'>;\nconst trimTimeouts = new Map<TrimmedSoundType, ReturnType<typeof setTimeout>>();\n\n// Кэш для Audio объектов, чтобы не создавать их каждый раз\nconst soundCache = new Map<SoundType, HTMLAudioElement>();\n\n/**\n * Получает или создает Audio объект для звука\n */\nconst getAudio = (soundType: SoundType): HTMLAudioElement => {\n if (!soundCache.has(soundType)) {\n const audio = new Audio(SOUND_PATHS[soundType]);\n audio.preload = 'auto';\n soundCache.set(soundType, audio);\n }\n return soundCache.get(soundType)!;\n};\n\nconst clearJoinLeaveTrim = (soundType: TrimmedSoundType) => {\n const id = trimTimeouts.get(soundType);\n if (id !== undefined) {\n clearTimeout(id);\n trimTimeouts.delete(soundType);\n }\n};\n\nconst scheduleJoinLeaveTrim = (audio: HTMLAudioElement, soundType: TrimmedSoundType) => {\n clearJoinLeaveTrim(soundType);\n const id = setTimeout(() => {\n try {\n audio.pause();\n audio.currentTime = 0;\n } catch {\n /* ignore */\n }\n trimTimeouts.delete(soundType);\n }, JOIN_LEAVE_PLAY_MS);\n trimTimeouts.set(soundType, id);\n};\n\n/**\n * Воспроизводит звук с указанной громкостью\n * @param soundType - тип звука (chatMessage или handRaise)\n * @param volume - громкость от 0 до 1 (0 = беззвучно, 1 = максимальная громкость)\n */\nexport const playSound = (soundType: SoundType, volume: number = 1): void => {\n try {\n const audio = getAudio(soundType);\n\n if (soundType === 'userJoin' || soundType === 'userLeft') {\n clearJoinLeaveTrim(soundType);\n }\n\n // Устанавливаем громкость (от 0 до 1)\n const clampedVolume = Math.max(0, Math.min(1, volume));\n audio.volume = clampedVolume;\n\n // Сбрасываем позицию на начало и воспроизводим\n audio.currentTime = 0;\n void audio\n .play()\n .then(() => {\n if (soundType === 'userJoin' || soundType === 'userLeft') {\n scheduleJoinLeaveTrim(audio, soundType);\n }\n })\n .catch((error) => {\n // Игнорируем ошибки воспроизведения (например, если пользователь не взаимодействовал со страницей)\n console.warn(`⚠️ Failed to play sound ${soundType}:`, error);\n });\n } catch (error) {\n console.error(`❌ Error playing sound ${soundType}:`, error);\n }\n};\n\ntype PlaySoundOnDeviceOptions = {\n volume?: number;\n /** deviceId устройства вывода (HTMLMediaElement.setSinkId), если поддерживается */\n sinkId?: string;\n};\n\ntype AudioContextWithSink = AudioContext & {\n setSinkId?: (id: string) => Promise<void>;\n};\n\n/**\n * Воспроизводит тестовый звук на выбранном устройстве вывода.\n * Возвращает Promise, который резолвится после старта playback (или reject при ошибке).\n * Требует, чтобы хост отдавал файлы из `/sounds/...`.\n */\nexport const playSoundOnDevice = async (\n soundType: SoundType,\n options: PlaySoundOnDeviceOptions = {},\n): Promise<void> => {\n const { volume = 1, sinkId } = options;\n const audio = new Audio(SOUND_PATHS[soundType]);\n audio.volume = Math.max(0, Math.min(1, volume));\n\n const audioWithSink = audio as HTMLAudioElement & {\n setSinkId?: (id: string) => Promise<void>;\n };\n\n if (sinkId && typeof audioWithSink.setSinkId === 'function') {\n try {\n await audioWithSink.setSinkId(sinkId);\n } catch (error) {\n console.warn('⚠️ Failed to set audio output device:', error);\n }\n }\n\n audio.currentTime = 0;\n await audio.play();\n};\n\ntype PlaySpeakerTestToneOptions = {\n volume?: number;\n /** deviceId устройства вывода (AudioContext.setSinkId), если поддерживается */\n sinkId?: string;\n durationMs?: number;\n frequencyHz?: number;\n};\n\n/**\n * Тестовый тон через Web Audio API — не зависит от файлов `/sounds/*` на хосте.\n * Использует AudioContext.setSinkId, когда браузер и deviceId это позволяют.\n */\nexport const playSpeakerTestTone = async (\n options: PlaySpeakerTestToneOptions = {},\n): Promise<void> => {\n const { volume = 1, sinkId, durationMs = 900, frequencyHz = 880 } = options;\n\n const AudioContextCtor =\n window.AudioContext ||\n (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n\n if (!AudioContextCtor) {\n throw new Error('AudioContext is not supported');\n }\n\n const ctx = new AudioContextCtor() as AudioContextWithSink;\n\n try {\n if (sinkId && typeof ctx.setSinkId === 'function') {\n try {\n await ctx.setSinkId(sinkId);\n } catch (error) {\n console.warn('⚠️ Failed to set audio output device for test tone:', error);\n }\n }\n\n if (ctx.state === 'suspended') {\n await ctx.resume();\n }\n\n const oscillator = ctx.createOscillator();\n const gain = ctx.createGain();\n oscillator.type = 'sine';\n oscillator.frequency.value = frequencyHz;\n\n const peak = Math.max(0, Math.min(1, volume)) * 0.22;\n const now = ctx.currentTime;\n const end = now + durationMs / 1000;\n\n gain.gain.setValueAtTime(0, now);\n gain.gain.linearRampToValueAtTime(peak, now + 0.04);\n gain.gain.linearRampToValueAtTime(peak * 0.85, end - 0.12);\n gain.gain.linearRampToValueAtTime(0, end);\n\n oscillator.connect(gain);\n gain.connect(ctx.destination);\n oscillator.start(now);\n oscillator.stop(end);\n\n await new Promise<void>((resolve) => {\n window.setTimeout(resolve, durationMs + 40);\n });\n } finally {\n await ctx.close().catch(() => {\n /* ignore */\n });\n }\n};\n","/** События телеметрии шумоподавления для umami. */\nexport const NOISE_CANCELLATION_EVENTS = {\n MODE_CHANGED: 'noise_cancellation_mode_changed',\n APPLY_FAILED: 'noise_cancellation_apply_failed',\n FALLBACK_TO_WEBRTC: 'noise_cancellation_fallback_to_webrtc',\n KRISP_UNSUPPORTED_BROWSER: 'noise_cancellation_krisp_unsupported_browser',\n} as const;\n\ntype NoiseCancellationEvent =\n (typeof NOISE_CANCELLATION_EVENTS)[keyof typeof NOISE_CANCELLATION_EVENTS];\n\nfunction getUmami(): { track: (name: string, data?: Record<string, string>) => void } | undefined {\n if (typeof window === 'undefined') return undefined;\n const win = window as Window & {\n umami?: { track: (name: string, data?: Record<string, string>) => void };\n };\n return win.umami;\n}\n\nexport function trackNoiseCancellationEvent(\n event: NoiseCancellationEvent,\n data?: Record<string, string>,\n): void {\n const umami = getUmami();\n if (umami) {\n umami.track(event, data);\n }\n}\n"]}
|