当前位置: 首页 > news >正文

WebRtc语音通话前置铃声处理

目标

在H5页面拨打webrtc 通话之前播放前置铃声。

问题

在移动端静音模式、音量调到最低播放音频是没有声音。

解决方案

通过 navigator.mediaDevices.getUserMedia + AudioContext 解决。

原因:

  1. 使用audio 播放的时候,h5音频播放使用的是媒体通道,这个时候音量可能会被用户调到最低,但是由于 H5 页面无法控制系统的音量修改的。当然也可以通过与移动端原生方法进行通信,这样工作量就增加了。
  2. 在使用 webrtc 播放的时候,使用的是语音通话的通道,这样可以忽略掉移动端的静音模式及音量控制的问题,这里可以设置【audioCtx.createGain();】 initialGain 扩大音量。
  3. 如果没有经过 navigator.mediaDevices.getUserMedia 初始化,直接使用 AudioContext 去播放音频的时候,静音模式下是没有声音的。
import { useState, useEffect, useRef, useCallback } from "react";/*** @typedef {Object} EnhancedAudioControls* @property {() => void} play - 播放音频。* @property {() => void} stop - 停止当前播放的音频。* @property {boolean} isPlaying - 音频是否正在播放。* @property {boolean} isLoading - 音频数据是否正在加载。* @property {boolean} isMicrophoneActive - 麦克风流是否已激活(音频优先级增强)。* @property {number} gainValue - 当前的音频增益值。* @property {(value: number) => void} setGainValue - 设置音频增益值(建议 1.0 - 5.0)。* @property {(isLooping: boolean) => void} setLooping - 设置是否循环播放。* @property {boolean} isLooping - 当前是否为循环模式。* @property {string | null} error - 错误信息。*/const MAX_SAFE_GAIN = 50000000.0; // Web Audio 增益上限,防止严重削波/*** 封装 WebRTC 音频激活和 Web Audio 播放,以最大化音频优先级和音量。* * @param {string} mp3Url - 待播放的 MP3 文件的 URL 路径。* @param {number} initialGain - 初始增益值 (默认为 4.5,已调大以满足用户要求)。* @param {boolean} initialLooping - 初始是否循环播放 (默认为 false)。* @returns {EnhancedAudioControls}*/
const useWebRTCEnhancedAudio = (mp3Url = "", initialGain = 10000, initialLooping = true) => {const [isLoading, setIsLoading] = useState(true);const [isMicrophoneActive, setIsMicrophoneActive] = useState(false);const [isPlaying, setIsPlaying] = useState(false);const [gainValue, setGainValue] = useState(Math.min(initialGain, MAX_SAFE_GAIN));const [isLooping, setLooping] = useState(initialLooping);const [error, setError] = useState<any>(null);const audioContextRef = useRef<any>(null);const streamRef = useRef<any>(null);const gainNodeRef = useRef<any>(null);const audioBufferRef = useRef<any>(null);const currentSourceNodeRef = useRef<any>(null);// --- 1. 停止播放功能 (Callback) ---const stop = useCallback(() => {if (currentSourceNodeRef.current) {try {// 确保在停止前断开连接,避免内存泄漏currentSourceNodeRef.current.stop();currentSourceNodeRef.current.disconnect();currentSourceNodeRef.current = null;setIsPlaying(false);} catch (e) {// 忽略 AudioBufferSourceNode 已停止的错误// console.warn("停止音频失败或音频已停止:", e);}}}, []);// --- 2. 播放功能 (Callback) ---const play = useCallback(async () => {if (!audioContextRef.current || !audioBufferRef.current) {console.warn("音频未加载或上下文未就绪");return;}// 播放新音频前,先停止正在播放的音频(关键:用于应用新的 loop 属性)stop();const audioCtx = audioContextRef.current;const gainNode = gainNodeRef.current;// 处理移动端自动播放限制if (audioCtx.state === "suspended") {await audioCtx.resume();}try {// 每次播放都必须创建一个新的 source nodeconst source = audioCtx.createBufferSource();source.buffer = audioBufferRef.current;source.loop = isLooping; // **【循环设置】** 应用当前的循环状态source.connect(gainNode);currentSourceNodeRef.current = source;setIsPlaying(true);source.start(0);source.onended = () => {// 非循环模式下播放结束后清理引用和状态if (currentSourceNodeRef.current === source && !isLooping) {currentSourceNodeRef.current = null;setIsPlaying(false);}};} catch (e) {console.error("播放音频失败:", e);setIsPlaying(false);}}, [stop, isLooping]); // play 依赖 stop 和 isLooping// --- 3. 辅助函数:加载和解码音频 ---const loadAudioFile = useCallback(async (url: string, audioCtx: any) => {setIsLoading(true);try {const response = await fetch(url);if (!response.ok) {throw new Error(`Failed to load MP3: ${response.statusText}`);}const arrayBuffer = await response.arrayBuffer();const buffer = await audioCtx.decodeAudioData(arrayBuffer);audioBufferRef.current = buffer;setIsLoading(false);} catch (e: any) {console.error("加载或解码音频失败:", e);setError(`加载音频失败: ${e?.message}`);setIsLoading(false);}}, []);// --- 4. 主初始化逻辑 (Effect) ---useEffect(() => {if (!mp3Url) return;const initAudioEngine = async () => {try {// 1. 请求麦克风权限 (WebRTC 优先级增强)const stream = await navigator.mediaDevices.getUserMedia({audio: true,video: false,});streamRef.current = stream;setIsMicrophoneActive(true);// 2. 初始化 Web Audio Contextconst AudioContext = window.AudioContext || (window as any).webkitAudioContext;const audioCtx = new AudioContext();audioContextRef.current = audioCtx;// 3. 创建音频处理节点 (增益和压缩)const gainNode = audioCtx.createGain();gainNode.gain.value = Math.min(gainValue, MAX_SAFE_GAIN);gainNodeRef.current = gainNode;const compressor = audioCtx.createDynamicsCompressor(); // 提升感知响度// 串联: 增益 -> 压缩 -> 扬声器gainNode.connect(compressor);compressor.connect(audioCtx.destination);// 4. 加载 MP3 文件await loadAudioFile(mp3Url, audioCtx);} catch (err) {console.error("初始化音频引擎失败:", err);setError("无法访问麦克风或音频功能。请检查权限。");setIsMicrophoneActive(false);setIsLoading(false);}};initAudioEngine();// 清理函数:关闭所有资源return () => {stop(); // 确保组件卸载时停止播放if (streamRef.current) {streamRef.current.getTracks().forEach((track: any) => track.stop());}if (audioContextRef.current) {audioContextRef.current.close();}};}, [mp3Url, loadAudioFile]);// --- 5. 动态更新增益值 (Effect) ---useEffect(() => {if (gainNodeRef.current && audioContextRef.current) {const safeGain = Math.min(Math.max(gainValue, 0.01), MAX_SAFE_GAIN);// 平滑设置增益,避免播放中断gainNodeRef.current.gain.setValueAtTime(safeGain, audioContextRef.current.currentTime);}}, [gainValue]);// --- 6. 循环状态变更处理 (Effect) ---useEffect(() => {// 当循环状态改变时,如果正在播放,必须重新创建 source node 来应用新的 loop 值。if (isPlaying) {// 重新调用 play,它会先 stop 再以新的 isLooping 值开始播放play();}}, [isLooping]);return {play,stop,isPlaying,isLoading,isMicrophoneActive,gainValue,setGainValue,isLooping, // 返回当前循环状态setLooping,error,};
};export default useWebRTCEnhancedAudio;

优化

移动端设备可能存在不支持 navigator.mediaDevices.getUserMedia ,因此可以使用 audio 播放进行兜底。

import { useRef, useCallback } from "react";// 自定义 Hook:管理拨号铃声
export const usePlayAudio = () => {const audioRef = useRef<HTMLAudioElement | null>(null);// 初始化音频对象const initAudio = useCallback((mp3Url: string) => {if (!audioRef.current) {audioRef.current = new Audio(mp3Url);audioRef.current.loop = true; // 循环播放}}, []);// 开始播放铃声const playRingtone = useCallback(async (mp3Url: string) => {try {initAudio(mp3Url);if (audioRef.current) {audioRef.current.pause();audioRef.current.currentTime = 0;audioRef.current.volume = 1.0;await audioRef.current.play();}} catch (error) {console.error("播放铃声失败:", error);}},[initAudio],);// 停止播放铃声const stopRingtone = useCallback(() => {if (audioRef.current) {audioRef.current.volume = 0;audioRef.current.pause();audioRef.current.currentTime = 0;// 彻底清空缓冲区audioRef.current.src = "";audioRef.current.load();}}, []);// 清理资源const cleanup = useCallback(() => {if (audioRef.current) {audioRef.current.pause();audioRef.current.src = "";audioRef.current.load(); // 重新加载(清空缓冲)audioRef.current.currentTime = 0;audioRef.current = null;}}, []);return {initAudio,playRingtone,stopRingtone,cleanup,};
};

实现: useHighPriorityAudio

import { useState, useEffect, useMemo, useCallback } from "react";
import { usePlayAudio } from "../usePlayAudio";
import useWebRTCEnhancedAudio from "../useWebRTCEnhancedAudio";/*** @typedef {Object} UnifiedAudioControls* @property {(mp3Url: string) => void} playRingtone - 开始播放铃声 (统一 API)。* @property {() => void} stopRingtone - 停止播放铃声 (统一 API)。* @property {() => void} cleanup - 清理资源。* @property {string} mode - 当前使用的音频模式: 'WebRTC' 或 'HTML_AUDIO'。* @property {string | null} error - 错误信息。* @property {boolean} isWebRTCAvailable - 浏览器是否支持 WebRTC。*/// 能力检测函数
const checkWebRTCAvailability = () => {// @ts-ignorereturn !!(navigator.mediaDevices && navigator?.mediaDevices?.getUserMedia && (window.AudioContext || window?.webkitAudioContext));
};/*** 兼容性 Wrapper Hook:优先使用 WebRTC 增强音频,回退到 HTML Audio。* @returns {UnifiedAudioControls}*/
export const useHighPriorityAudio = (audioUrl: string) => {// 检查浏览器能力,只需要运行一次const [isWebRTCAvailable, setIsWebRTCAvailable] = useState(checkWebRTCAvailability());// const [audioUrl, setAudioUrl] = useState(fileUrl || "");const [hasError, setHasError] = useState(false);// 实例化 WebRTC Hook (即使不用,也需要实例化以保证 Hook 规则)// 由于 WebRTC Hook 复杂,我们需要将它的初始化逻辑移到 Wrapper Hook 内部// 我们不能在 Hook 内部有条件地调用 Hook,所以我们总是调用两个 Hook,并根据条件使用它们的返回值。// --- 1. WebRTC Enhanced Hook ---const {play: webRtcPlay, // 重命名,避免冲突stop: webRtcStop,isMicrophoneActive,error: webRtcError,// WebRTC Hook 通常需要在初始化时加载音频,所以我们在这里保持 Hook 简洁} = useWebRTCEnhancedAudio(audioUrl, 5000, true);// --- 2. HTML Audio Hook ---const { playRingtone: htmlPlay, stopRingtone: htmlStop, cleanup: htmlCleanup } = usePlayAudio();// 确定当前模式const mode = useMemo(() => {// 如果 WebRTC 可用,且没有发生权限错误(即麦克风已激活),则使用 WebRTC// 注意:WebRTC Hook 在内部会尝试激活麦克风。如果失败,它会返回一个错误。if (isWebRTCAvailable && isMicrophoneActive) {console.log("WebRTC 可支持");return "WebRTC";}return "HTML_AUDIO";}, [isWebRTCAvailable, isMicrophoneActive]);// 监听 WebRTC 错误,如果 WebRTC 失败,我们退化到 HTML_AUDIOuseEffect(() => {if (webRtcError) {console.warn("WebRTC 初始化失败或权限被拒,降级到 HTML Audio。", webRtcError);setIsWebRTCAvailable(false); // 视为不可用,强制切换到 HTML Audio 模式setHasError(true);}}, [webRtcError]);// --- 统一 API 实现 ---const playRingtone = useCallback(async (mp3Url: string) => {// setAudioUrl(mp3Url); // 更新 URL,触发 WebRTC 内部加载if (mode === "WebRTC") {console.log("使用 WebRTC/Web Audio (高优先级) 播放...");// WebRTC 模式需要先加载音频,在 useEffect 中已经加载,直接调用 playwebRtcPlay();} else {console.log("使用 HTML Audio (标准) 播放...");// HTML Audio 模式,调用其播放逻辑htmlPlay(mp3Url);}},[mode, webRtcPlay, htmlPlay],);const stopRingtone = useCallback(() => {if (mode === "WebRTC") {webRtcStop();} else {htmlStop();}}, [mode, webRtcStop, htmlStop]);const cleanup = useCallback(() => {webRtcStop(); // 停止 WebRTC 播放htmlCleanup(); // 清理 HTML Audio 资源// WebRTC Hook 的清理逻辑在它自己的 useEffect return 中}, [webRtcStop, htmlCleanup]);return {playRingtone,stopRingtone,cleanup,mode,isWebRTCAvailable,error: hasError ? "WebRTC 权限被拒,已切换到标准模式。" : null,};
};

使用:

const { playRingtone, stopRingtone, cleanup, mode, isWebRTCAvailable, error } = useHighPriorityAudio();
http://www.dtcms.com/a/585893.html

相关文章:

  • 使用XSHELL远程操作数据库
  • 淘宝客网站域名宜昌做网站哪家最便宜
  • 微信小程序中使用 MQTT 实现实时通信:技术难点与实践指南
  • Java computeIfAbsent() 方法详解
  • 做网站市场报价免费企业网站开源系统
  • 天元建设集团有限公司企业代码东莞做网站seo
  • Web前端摄像头调用安全性分析
  • 绵阳网站建设怎么做免费查公司
  • std之list
  • 前端:前端/浏览器 可以录屏吗 / 实践 / 录制 Microsoft Edge 标签页、应用窗口、整个屏幕
  • 做网站像美团一样多少钱中国最新军事消息
  • 软件项目管理实验报告(黑龙江大学)
  • 网络建设需求台州做网站优化
  • PostgreSQL一些概念特性
  • 宁夏建设厅网站6青岛网站建设公司好找吗
  • 社交营销可以用于网站制作行业吗怎样做建网站做淘客
  • 玩转Rust高级应用 如何让让运算符支持自定义类型,通过运算符重载的方式是针对自定义类型吗?
  • 基于Keras的MNIST手写数字识别卷积神经网络设计与实现
  • 百度资料怎么做网站型云网站建设
  • IP配置的基本要求
  • 单母线接线典型操作顺序
  • LightGBM三部曲:LightGBM原理
  • 【C++】C++中的文件IO
  • wordpress手机站如何做负面口碑营销案例
  • 谷歌黑客语法挖掘 SQL 注入漏洞
  • ps做网站logo青海做网站多少钱
  • Qt开发——环境搭建
  • 32HAL——RTC时钟
  • C#知识补充(一)——ref和out、成员属性、万物之父和装箱拆箱、抽象类和抽象方法、接口
  • 专业的设计网站建设网站做地区定位跳转