最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

基于Vue3中實(shí)現(xiàn)耳機(jī)和揚(yáng)聲器切換的完整方案

 更新時(shí)間:2026年02月03日 08:56:12   作者:經(jīng)年未遠(yuǎn)  
在 Web 應(yīng)用中實(shí)現(xiàn)音頻輸出設(shè)備的動(dòng)態(tài)切換,允許用戶在揚(yáng)聲器、耳機(jī)、藍(lán)牙設(shè)備等不同音頻輸出設(shè)備之間自由切換,本文給大家介紹了基于Vue3中實(shí)現(xiàn)耳機(jī)和揚(yáng)聲器切換的完整方案,需要的朋友可以參考下

功能概述

在 Web 應(yīng)用中實(shí)現(xiàn)音頻輸出設(shè)備的動(dòng)態(tài)切換,允許用戶在揚(yáng)聲器、耳機(jī)、藍(lán)牙設(shè)備等不同音頻輸出設(shè)備之間自由切換。

應(yīng)用場(chǎng)景

  • 在線教育平臺(tái)的語(yǔ)音通話
  • 視頻會(huì)議系統(tǒng)
  • 在線音樂播放器
  • 語(yǔ)音聊天應(yīng)用

核心功能

  • 枚舉所有可用的音頻輸出設(shè)備
  • 動(dòng)態(tài)切換音頻輸出設(shè)備
  • 智能識(shí)別設(shè)備類型(揚(yáng)聲器/耳機(jī)/藍(lán)牙)
  • 跨平臺(tái)支持(H5 + 微信小程序)

技術(shù)原理

1. Web Audio API 架構(gòu)

音頻源 → AudioBufferSourceNode → MediaStreamDestination → <audio> 元素 → 音頻輸出設(shè)備
                                                                ↓
                                                          setSinkId(deviceId)

2. 關(guān)鍵 API

2.1 枚舉音頻設(shè)備

navigator.mediaDevices.enumerateDevices();

返回所有媒體設(shè)備,包括:

  • audioinput:音頻輸入設(shè)備(麥克風(fēng))
  • audiooutput:音頻輸出設(shè)備(揚(yáng)聲器/耳機(jī))
  • videoinput:視頻輸入設(shè)備(攝像頭)

2.2 切換音頻輸出

audioElement.setSinkId(deviceId);

<audio> 元素的輸出切換到指定設(shè)備。

2.3 創(chuàng)建音頻流

const destination = audioContext.createMediaStreamDestination();
audioElement.srcObject = destination.stream;

3. 為什么需要 MediaStreamDestination?

直接使用 AudioContext.destination 無(wú)法切換設(shè)備,因?yàn)樗苯虞敵龅侥J(rèn)設(shè)備。通過(guò) MediaStreamDestination 創(chuàng)建一個(gè)中間流,再連接到 <audio> 元素,就可以使用 setSinkId 切換設(shè)備。

實(shí)現(xiàn)步驟

步驟 1:創(chuàng)建音頻播放器類

class AudioStreamPlayer {
  private audioContext: AudioContext;
  private mediaStreamDestination: MediaStreamAudioDestinationNode;
  private audioElement: HTMLAudioElement;
  private currentSinkId: string = "";

  constructor() {
    this.initWebAudio();
  }

  private initWebAudio() {
    // 創(chuàng)建 AudioContext
    this.audioContext = new AudioContext({
      sampleRate: 16000,
      latencyHint: "interactive",
    });

    // 創(chuàng)建 MediaStreamDestination
    this.mediaStreamDestination = this.audioContext.createMediaStreamDestination();

    // 創(chuàng)建 audio 元素并連接到流
    this.audioElement = document.createElement("audio");
    this.audioElement.autoplay = true;
    this.audioElement.srcObject = this.mediaStreamDestination.stream;

    // 啟動(dòng)播放
    this.audioElement.play().catch((err) => {
      console.warn("自動(dòng)播放被阻止,需要用戶交互:", err);
    });
  }

  // 播放音頻數(shù)據(jù)
  playAudio(audioBuffer: AudioBuffer) {
    const sourceNode = this.audioContext.createBufferSource();
    sourceNode.buffer = audioBuffer;

    // 連接到 MediaStreamDestination(關(guān)鍵!)
    sourceNode.connect(this.mediaStreamDestination);

    sourceNode.start();
  }

  // 切換音頻輸出設(shè)備
  async setSinkId(deviceId: string): Promise<boolean> {
    if (!this.audioElement) {
      console.warn("audio 元素未初始化");
      return false;
    }

    if (typeof this.audioElement.setSinkId !== "function") {
      console.warn("瀏覽器不支持 setSinkId");
      return false;
    }

    try {
      await this.audioElement.setSinkId(deviceId);
      this.currentSinkId = deviceId;

      // 確保 audio 元素仍在播放
      if (this.audioElement.paused) {
        await this.audioElement.play();
      }

      console.log("? 音頻輸出設(shè)備已切換:", deviceId);
      return true;
    } catch (error) {
      console.error("? 切換音頻輸出設(shè)備失敗:", error);
      return false;
    }
  }
}

步驟 2:創(chuàng)建設(shè)備管理 Hook

// useSpeaker.ts
import { ref, onMounted } from "vue";

export enum SpeakerMode {
  Speaker = "speaker", // 揚(yáng)聲器
  Earphone = "earphone", // 耳機(jī)
  Receiver = "receiver", // 聽筒(僅移動(dòng)端)
}

export function useSpeaker(setSinkIdCallback?: (sinkId: string) => Promise<boolean>) {
  const audioDevices = ref<MediaDeviceInfo[]>([]);
  const currentSpeaker = ref<SpeakerMode>(SpeakerMode.Speaker);

  // 獲取所有音頻輸出設(shè)備
  const getAudioDevices = async () => {
    try {
      // 先請(qǐng)求麥克風(fēng)權(quán)限,以獲取完整的設(shè)備標(biāo)簽
      try {
        const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
        stream.getTracks().forEach((track) => track.stop());
      } catch (err) {
        console.warn("無(wú)法獲取麥克風(fēng)權(quán)限:", err);
      }

      // 枚舉所有設(shè)備
      const devices = await navigator.mediaDevices.enumerateDevices();
      audioDevices.value = devices.filter((device) => device.kind === "audiooutput");

      console.log("可用的音頻輸出設(shè)備:", audioDevices.value);
    } catch (error) {
      console.error("獲取音頻設(shè)備失敗:", error);
    }
  };

  // 選擇揚(yáng)聲器模式
  const selectSpeaker = async (mode: SpeakerMode) => {
    currentSpeaker.value = mode;

    // 刷新設(shè)備列表
    await getAudioDevices();

    if (audioDevices.value.length === 0) {
      console.warn("未找到可用的音頻輸出設(shè)備");
      return;
    }

    let targetDevice: MediaDeviceInfo | undefined;

    if (mode === SpeakerMode.Speaker) {
      // 揚(yáng)聲器:優(yōu)先選擇 default 設(shè)備
      targetDevice = audioDevices.value.find((device) => device.deviceId === "default");

      if (!targetDevice) {
        // 如果沒有 default,找第一個(gè)非耳機(jī)設(shè)備
        targetDevice = audioDevices.value.find((device) => !device.label.toLowerCase().includes("headphone") && !device.label.toLowerCase().includes("headset") && !device.label.toLowerCase().includes("耳機(jī)") && !device.label.toLowerCase().includes("bluetooth"));
      }

      if (!targetDevice && audioDevices.value.length > 0) {
        targetDevice = audioDevices.value[0];
      }
    } else if (mode === SpeakerMode.Earphone) {
      // 耳機(jī):選擇包含 headphone/headset/耳機(jī) 的設(shè)備
      targetDevice = audioDevices.value.find((device) => device.label.toLowerCase().includes("headphone") || device.label.toLowerCase().includes("headset") || device.label.toLowerCase().includes("耳機(jī)"));

      if (!targetDevice) {
        console.warn("未檢測(cè)到耳機(jī)設(shè)備");
        return;
      }
    }

    if (!targetDevice) {
      console.warn("未找到目標(biāo)設(shè)備");
      return;
    }

    console.log("目標(biāo)設(shè)備:", targetDevice.label, targetDevice.deviceId);

    // 調(diào)用切換回調(diào)
    if (setSinkIdCallback) {
      const success = await setSinkIdCallback(targetDevice.deviceId);
      if (success) {
        console.log(`? 已切換到: ${targetDevice.label}`);
      }
    }
  };

  // 初始化時(shí)獲取設(shè)備列表
  onMounted(() => {
    getAudioDevices();
  });

  return {
    audioDevices,
    currentSpeaker,
    getAudioDevices,
    selectSpeaker,
  };
}

步驟 3:在 Vue 組件中使用

<template>
  <div class="audio-player">
    <!-- 揚(yáng)聲器切換按鈕 -->
    <button @click="showDeviceSelector = true">切換音頻設(shè)備</button>

    <!-- 設(shè)備選擇彈窗 -->
    <div v-if="showDeviceSelector" class="device-selector">
      <div v-for="device in audioDevices" :key="device.deviceId" @click="switchDevice(device.deviceId)" class="device-item">
        {{ device.label }}
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { useSpeaker, SpeakerMode } from "./useSpeaker";

const showDeviceSelector = ref(false);
const audioPlayer = new AudioStreamPlayer();

// 使用設(shè)備管理 Hook
const { audioDevices, selectSpeaker } = useSpeaker((sinkId) => audioPlayer.setSinkId(sinkId));

// 切換設(shè)備
const switchDevice = async (deviceId: string) => {
  const success = await audioPlayer.setSinkId(deviceId);
  if (success) {
    showDeviceSelector.value = false;
  }
};
</script>

完整代碼

1. AudioStreamPlayer 完整實(shí)現(xiàn)

/**
 * 音頻流播放器
 * 支持動(dòng)態(tài)切換音頻輸出設(shè)備
 */
export class AudioStreamPlayer {
  private audioContext: AudioContext | null = null;
  private audioQueue: ArrayBuffer[] = [];
  private isPlaying = false;
  private currentSinkId = "";
  private audioElement: HTMLAudioElement | null = null;
  private mediaStreamDestination: MediaStreamAudioDestinationNode | null = null;
  private sourceNodes: AudioBufferSourceNode[] = [];
  private nextPlayTime = 0;

  constructor() {
    this.initWebAudio();
  }

  /**
   * 初始化 Web Audio API
   */
  private initWebAudio() {
    this.audioContext = new AudioContext({
      sampleRate: 16000,
      latencyHint: "interactive",
    });

    // 創(chuàng)建 MediaStreamDestination 和 audio 元素以支持設(shè)備切換
    this.mediaStreamDestination = this.audioContext.createMediaStreamDestination();

    this.audioElement = document.createElement("audio");
    this.audioElement.autoplay = true;
    this.audioElement.srcObject = this.mediaStreamDestination.stream;

    // 嘗試播放
    const tryPlay = () => {
      if (this.audioElement) {
        this.audioElement
          .play()
          .then(() => {
            console.log("? audio 元素已開始播放");
          })
          .catch((err) => {
            console.warn("?? 自動(dòng)播放被阻止,需要用戶交互:", err);
            // 監(jiān)聽用戶交互后重試
            const retryPlay = () => {
              if (this.audioElement) {
                this.audioElement.play();
              }
            };
            document.addEventListener("click", retryPlay, { once: true });
          });
      }
    };

    tryPlay();
    console.log("? 已創(chuàng)建 audio 元素用于設(shè)備切換");
  }

  /**
   * 設(shè)置音頻輸出設(shè)備
   */
  async setSinkId(sinkId: string): Promise<boolean> {
    if (!this.audioElement) {
      console.warn("?? audio 元素未初始化");
      return false;
    }

    if (typeof (this.audioElement as any).setSinkId !== "function") {
      console.warn("?? 瀏覽器不支持 setSinkId");
      return false;
    }

    try {
      console.log("?? 當(dāng)前 sinkId:", (this.audioElement as any).sinkId);
      console.log("?? 準(zhǔn)備切換到設(shè)備:", sinkId);

      await (this.audioElement as any).setSinkId(sinkId);
      this.currentSinkId = sinkId;

      console.log("? 音頻輸出設(shè)備已切換:", sinkId);

      // 確保 audio 元素仍在播放
      if (this.audioElement.paused) {
        await this.audioElement.play();
      }

      return true;
    } catch (error) {
      console.error("? 切換音頻輸出設(shè)備失敗:", error);
      return false;
    }
  }

  /**
   * 添加音頻數(shù)據(jù)塊
   */
  addAudioChunk(arrayBuffer: ArrayBuffer): void {
    if (!arrayBuffer || arrayBuffer.byteLength === 0) {
      return;
    }

    this.audioQueue.push(arrayBuffer);
    this.processQueue();
  }

  /**
   * 處理音頻隊(duì)列
   */
  private processQueue() {
    if (this.audioQueue.length === 0 || !this.audioContext) {
      return;
    }

    const chunk = this.audioQueue.shift();
    if (!chunk) return;

    // 解碼音頻數(shù)據(jù)
    this.audioContext
      .decodeAudioData(chunk.slice(0))
      .then((decodedData) => {
        this.playAudio(decodedData);
      })
      .catch((error) => {
        console.error("音頻解碼失敗:", error);
      })
      .finally(() => {
        this.processQueue();
      });
  }

  /**
   * 播放音頻
   */
  private playAudio(audioBuffer: AudioBuffer) {
    if (!this.audioContext || !this.mediaStreamDestination) return;

    const sourceNode = this.audioContext.createBufferSource();
    sourceNode.buffer = audioBuffer;

    // ?? 關(guān)鍵:連接到 mediaStreamDestination
    sourceNode.connect(this.mediaStreamDestination);

    // 計(jì)算播放時(shí)間
    if (this.nextPlayTime === 0 || this.nextPlayTime < this.audioContext.currentTime) {
      this.nextPlayTime = this.audioContext.currentTime + 0.05;
    }

    sourceNode.start(this.nextPlayTime);
    this.nextPlayTime += audioBuffer.duration;

    this.sourceNodes.push(sourceNode);

    sourceNode.onended = () => {
      sourceNode.disconnect();
      this.sourceNodes = this.sourceNodes.filter((n) => n !== sourceNode);

      if (this.sourceNodes.length === 0 && this.audioQueue.length === 0) {
        this.nextPlayTime = 0;
        this.isPlaying = false;
      }
    };

    if (!this.isPlaying) {
      this.isPlaying = true;
    }
  }

  /**
   * 清空播放隊(duì)列
   */
  clear(): void {
    this.audioQueue = [];
    this.sourceNodes.forEach((node) => {
      try {
        node.stop();
        node.disconnect();
      } catch (e) {
        // ignore
      }
    });
    this.sourceNodes = [];
    this.nextPlayTime = 0;
    this.isPlaying = false;
  }

  /**
   * 關(guān)閉音頻上下文
   */
  close(): void {
    this.clear();
    if (this.audioContext && this.audioContext.state !== "closed") {
      this.audioContext.close();
      this.audioContext = null;
    }
  }
}

2. useSpeaker Hook 完整實(shí)現(xiàn)

import { ref, onMounted } from "vue";

export enum SpeakerMode {
  Speaker = "speaker",
  Earphone = "earphone",
  Receiver = "receiver",
}

export interface SpeakerOption {
  label: string;
  value: SpeakerMode;
  icon: string;
}

export function useSpeaker(setSinkIdCallback?: (sinkId: string) => Promise<boolean>) {
  const showSpeakerPopup = ref(false);
  const currentSpeaker = ref<SpeakerMode>(SpeakerMode.Speaker);
  const audioDevices = ref<MediaDeviceInfo[]>([]);

  const speakerOptions: SpeakerOption[] = [
    { label: "揚(yáng)聲器", value: SpeakerMode.Speaker, icon: "speaker" },
    { label: "耳機(jī)", value: SpeakerMode.Earphone, icon: "headset" },
    { label: "聽筒", value: SpeakerMode.Receiver, icon: "phone" },
  ];

  /**
   * 獲取可用的音頻輸出設(shè)備
   */
  const getAudioDevices = async () => {
    try {
      if (!navigator.mediaDevices?.enumerateDevices) {
        console.warn("瀏覽器不支持枚舉設(shè)備");
        return;
      }

      // 先請(qǐng)求麥克風(fēng)權(quán)限,以獲取完整的設(shè)備標(biāo)簽
      try {
        const stream = await navigator.mediaDevices.getUserMedia({
          audio: true,
        });
        stream.getTracks().forEach((track) => track.stop());
        console.log("? 已獲取麥克風(fēng)權(quán)限");
      } catch (err) {
        console.warn("?? 無(wú)法獲取麥克風(fēng)權(quán)限:", err);
      }

      const devices = await navigator.mediaDevices.enumerateDevices();
      audioDevices.value = devices.filter((device) => device.kind === "audiooutput");

      console.log("?? 可用的音頻輸出設(shè)備:", audioDevices.value);
      audioDevices.value.forEach((device, index) => {
        console.log(`  ${index + 1}. ${device.label} (${device.deviceId})`);
      });
    } catch (error) {
      console.error("? 獲取音頻設(shè)備失敗:", error);
    }
  };

  /**
   * 切換彈窗顯示
   */
  const toggleSpeakerPopup = () => {
    showSpeakerPopup.value = !showSpeakerPopup.value;
  };

  /**
   * 選擇揚(yáng)聲器模式
   */
  const selectSpeaker = async (mode: SpeakerMode) => {
    currentSpeaker.value = mode;
    showSpeakerPopup.value = false;

    // 檢查瀏覽器是否支持 setSinkId
    const testAudio = document.createElement("audio");
    if (typeof (testAudio as any).setSinkId !== "function") {
      console.warn("瀏覽器不支持 setSinkId API");
      return;
    }

    // 刷新設(shè)備列表
    await getAudioDevices();

    if (audioDevices.value.length === 0) {
      console.warn("未找到可用的音頻輸出設(shè)備");
      return;
    }

    let targetDevice: MediaDeviceInfo | undefined;

    if (mode === SpeakerMode.Speaker) {
      // 揚(yáng)聲器:優(yōu)先選擇 default 設(shè)備
      targetDevice = audioDevices.value.find((device) => device.deviceId === "default");

      if (!targetDevice) {
        // 找第一個(gè)非耳機(jī)設(shè)備
        targetDevice = audioDevices.value.find((device) => !device.label.toLowerCase().includes("headphone") && !device.label.toLowerCase().includes("headset") && !device.label.toLowerCase().includes("耳機(jī)") && !device.label.toLowerCase().includes("bluetooth"));
      }

      if (!targetDevice && audioDevices.value.length > 0) {
        targetDevice = audioDevices.value[0];
      }
    } else if (mode === SpeakerMode.Earphone) {
      // 耳機(jī):選擇包含 headphone/headset/耳機(jī) 的設(shè)備
      targetDevice = audioDevices.value.find((device) => device.label.toLowerCase().includes("headphone") || device.label.toLowerCase().includes("headset") || device.label.toLowerCase().includes("耳機(jī)"));

      if (!targetDevice) {
        console.warn("未檢測(cè)到耳機(jī)設(shè)備");
        return;
      }
    } else if (mode === SpeakerMode.Receiver) {
      // 聽筒:PC 不支持
      console.warn("PC 不支持聽筒模式");
      return;
    }

    if (!targetDevice) {
      console.warn("未找到目標(biāo)設(shè)備");
      return;
    }

    console.log("?? 目標(biāo)設(shè)備:", targetDevice.label, targetDevice.deviceId);

    // 切換音頻輸出
    if (setSinkIdCallback) {
      const success = await setSinkIdCallback(targetDevice.deviceId);
      if (success) {
        console.log(`? 已切換到: ${targetDevice.label}`);
      }
    }
  };

  // 初始化時(shí)獲取設(shè)備列表
  onMounted(() => {
    getAudioDevices();
  });

  return {
    showSpeakerPopup,
    currentSpeaker,
    audioDevices,
    speakerOptions,
    toggleSpeakerPopup,
    selectSpeaker,
    getAudioDevices,
  };
}

常見問題

Q1: 為什么藍(lán)牙耳機(jī)可以切換,有線耳機(jī)不行?

A: 這是硬件和驅(qū)動(dòng)的設(shè)計(jì)導(dǎo)致的:

  • 藍(lán)牙耳機(jī):通過(guò)藍(lán)牙適配器連接,被識(shí)別為獨(dú)立的音頻輸出設(shè)備,有獨(dú)立的設(shè)備 ID
  • 有線耳機(jī):插入 3.5mm 或 Type-C 接口后,與內(nèi)置揚(yáng)聲器共享同一個(gè)音頻芯片,系統(tǒng)只看到一個(gè)設(shè)備

當(dāng)插入有線耳機(jī)時(shí),音頻芯片會(huì)在硬件層面自動(dòng)切換輸出到耳機(jī),對(duì)操作系統(tǒng)和瀏覽器來(lái)說(shuō)仍然是同一個(gè)設(shè)備,因此無(wú)法通過(guò)軟件切換。

Q2: 為什么需要請(qǐng)求麥克風(fēng)權(quán)限?

A: 出于隱私保護(hù),瀏覽器在未獲得權(quán)限時(shí),enumerateDevices() 返回的設(shè)備標(biāo)簽(label)會(huì)是空的或通用名稱。請(qǐng)求麥克風(fēng)權(quán)限后,才能獲取完整的設(shè)備名稱,便于識(shí)別設(shè)備類型。

Q3: 如何判斷瀏覽器是否支持設(shè)備切換?

A: 檢查 HTMLMediaElement.setSinkId 方法是否存在:

const audio = document.createElement("audio");
if (typeof audio.setSinkId === "function") {
  console.log("? 瀏覽器支持設(shè)備切換");
} else {
  console.log("? 瀏覽器不支持設(shè)備切換");
}

Q4: 為什么自動(dòng)播放會(huì)失?。?/h3>

A: 現(xiàn)代瀏覽器的自動(dòng)播放策略要求:

  • 用戶必須與頁(yè)面有過(guò)交互(點(diǎn)擊、觸摸等)
  • 或者音頻是靜音的

解決方案:

  1. 在用戶交互后再初始化音頻
  2. 監(jiān)聽用戶交互事件,重試播放
  3. 提示用戶點(diǎn)擊頁(yè)面以啟用音頻

Q5: 如何在微信小程序中實(shí)現(xiàn)設(shè)備切換?

A: 微信小程序使用不同的 API:

// 切換到揚(yáng)聲器
uni.setInnerAudioOption({
  obeyMuteSwitch: false,
  speakerOn: true,
});

// 切換到聽筒
uni.setInnerAudioOption({
  obeyMuteSwitch: true,
  speakerOn: false,
});

瀏覽器兼容性

setSinkId API 支持情況

瀏覽器版本支持情況
Chrome49+? 完全支持
Edge79+? 完全支持
Firefox116+? 完全支持
Safari?? 不支持
Opera36+? 完全支持

兼容性檢測(cè)

function checkAudioDeviceSwitchSupport() {
  // 檢查 enumerateDevices
  if (!navigator.mediaDevices?.enumerateDevices) {
    return {
      supported: false,
      reason: "瀏覽器不支持枚舉設(shè)備",
    };
  }

  // 檢查 setSinkId
  const audio = document.createElement("audio");
  if (typeof audio.setSinkId !== "function") {
    return {
      supported: false,
      reason: "瀏覽器不支持 setSinkId API",
    };
  }

  return {
    supported: true,
    reason: "瀏覽器完全支持音頻設(shè)備切換",
  };
}

// 使用
const result = checkAudioDeviceSwitchSupport();
console.log(result);

最佳實(shí)踐

1. 錯(cuò)誤處理

async function switchAudioDevice(deviceId: string) {
  try {
    // 檢查瀏覽器支持
    if (typeof audioElement.setSinkId !== 'function') {
      throw new Error('瀏覽器不支持設(shè)備切換');
    }

    // 切換設(shè)備
    await audioElement.setSinkId(deviceId);

    // 確保播放狀態(tài)
    if (audioElement.paused) {
      await audioElement.play();
    }

    // 用戶反饋
    showToast('設(shè)備切換成功');

  } catch (error) {
    console.error('設(shè)備切換失敗:', error);
    showToast('設(shè)備切換失敗,請(qǐng)重試');
  }
}

2. 用戶體驗(yàn)優(yōu)化

// 1. 記住用戶選擇
localStorage.setItem("preferredAudioDevice", deviceId);

// 2. 自動(dòng)恢復(fù)上次選擇
const savedDeviceId = localStorage.getItem("preferredAudioDevice");
if (savedDeviceId) {
  await switchAudioDevice(savedDeviceId);
}

// 3. 監(jiān)聽設(shè)備變化
navigator.mediaDevices.addEventListener("devicechange", () => {
  console.log("設(shè)備列表已變化,刷新設(shè)備列表");
  getAudioDevices();
});

3. 性能優(yōu)化

// 避免頻繁枚舉設(shè)備
let deviceListCache: MediaDeviceInfo[] = [];
let lastEnumerateTime = 0;
const CACHE_DURATION = 5000; // 5秒緩存

async function getAudioDevices() {
  const now = Date.now();
  if (now - lastEnumerateTime < CACHE_DURATION) {
    return deviceListCache;
  }

  const devices = await navigator.mediaDevices.enumerateDevices();
  deviceListCache = devices.filter(d => d.kind === 'audiooutput');
  lastEnumerateTime = now;

  return deviceListCache;
}

總結(jié)

通過(guò) Web Audio API 的 MediaStreamDestinationHTMLMediaElement.setSinkId,我們可以實(shí)現(xiàn)靈活的音頻輸出設(shè)備切換功能。關(guān)鍵點(diǎn):

  1. 使用 MediaStreamDestination 創(chuàng)建音頻流
  2. 將音頻流連接到 <audio> 元素
  3. 使用 setSinkId 切換輸出設(shè)備
  4. 處理瀏覽器兼容性和自動(dòng)播放策略
  5. 提供良好的用戶體驗(yàn)和錯(cuò)誤處理

這個(gè)方案已在生產(chǎn)環(huán)境中驗(yàn)證,適用于在線教育、視頻會(huì)議等場(chǎng)景。

以上就是基于Vue3中實(shí)現(xiàn)耳機(jī)和揚(yáng)聲器切換的完整方案的詳細(xì)內(nèi)容,更多關(guān)于Vue3耳機(jī)和揚(yáng)聲器切換的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 一文搞懂Vue2中的組件通信

    一文搞懂Vue2中的組件通信

    這篇文章主要為大家介紹了Vue2中的組件通信方式,文中通過(guò)示例進(jìn)行了詳細(xì)的介紹,對(duì)我們學(xué)習(xí)Vue有一定的幫助,感興趣的小伙伴可以了解一下
    2022-07-07
  • Vue.js中前端如何處理從后端返回的Excel文件流詳解

    Vue.js中前端如何處理從后端返回的Excel文件流詳解

    這篇文章主要介紹了如何在Vue項(xiàng)目中使用axios處理后端返回的Excel文件流,并提供下載功能,文中通過(guò)代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2025-01-01
  • Vue為何棄用Ajax,選擇Axios?ajax與axios的區(qū)別?

    Vue為何棄用Ajax,選擇Axios?ajax與axios的區(qū)別?

    ajax技術(shù)實(shí)現(xiàn)了局部數(shù)據(jù)的刷新,axios實(shí)現(xiàn)了對(duì)ajax的封裝;axios有的ajax都有,ajax有的axios不一定有??偨Y(jié)一句話就是axios是ajax,ajax不止axios。
    2023-01-01
  • element-ui 中如何修改loading加載樣式

    element-ui 中如何修改loading加載樣式

    element-ui 中的 loading 加載功能,默認(rèn)是全屏加載效果,設(shè)置局部,需要自定義樣式或者修改樣式,下面給大家分享實(shí)例代碼,感興趣的朋友跟隨小編一起看看吧
    2024-05-05
  • vue?axios中的get請(qǐng)求方式

    vue?axios中的get請(qǐng)求方式

    這篇文章主要介紹了vue?axios中的get請(qǐng)求方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-07-07
  • vue中各選項(xiàng)及鉤子函數(shù)執(zhí)行順序詳解

    vue中各選項(xiàng)及鉤子函數(shù)執(zhí)行順序詳解

    今天小編就為大家分享一篇vue中各選項(xiàng)及鉤子函數(shù)執(zhí)行順序詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2018-08-08
  • Vue+element 解決瀏覽器自動(dòng)填充記住的賬號(hào)密碼問題

    Vue+element 解決瀏覽器自動(dòng)填充記住的賬號(hào)密碼問題

    我們?cè)谧鰂orm表單的時(shí)候,會(huì)發(fā)現(xiàn),瀏覽器會(huì)自動(dòng)的將我們之前保存的密碼,自動(dòng)的填充到表單中input 為 type="password" 的框中,如何實(shí)現(xiàn)此功能呢,下面小編給大家介紹下,感興趣的朋友一起看看吧
    2019-06-06
  • vue判斷內(nèi)容是否滑動(dòng)到底部的三種方式

    vue判斷內(nèi)容是否滑動(dòng)到底部的三種方式

    這篇文章主要介紹了vue判斷內(nèi)容是否滑動(dòng)到底部的三種方式,文中通過(guò)代碼示例給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下
    2024-04-04
  • Vue-resource安裝過(guò)程及使用方法解析

    Vue-resource安裝過(guò)程及使用方法解析

    這篇文章主要介紹了Vue-resource安裝過(guò)程及使用方法解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-07-07
  • vue-cli 使用axios的操作方法及整合axios的多種方法

    vue-cli 使用axios的操作方法及整合axios的多種方法

    這篇文章主要介紹了vue-cli 使用axios的操作方法及整合axios的多種方法,vue-cli整合axios的多種方法,小編一一給大家列出來(lái)了,大家根據(jù)自身需要選擇,需要的朋友可以參考下
    2018-09-09

最新評(píng)論

武乡县| 铁岭市| 岳普湖县| 明星| 红桥区| 冷水江市| 揭西县| 白玉县| 瑞安市| 襄城县| 进贤县| 中阳县| 保康县| 兰西县| 汉川市| 武乡县| 延津县| 梅河口市| 丹凤县| 蒙阴县| 庆云县| 徐闻县| 疏勒县| 宣化县| 林口县| 房山区| 卓尼县| 屯留县| 同仁县| 威信县| 修武县| 奉化市| 阜新| 宁陵县| 依兰县| 张北县| 阳朔县| 沁阳市| 汉沽区| 上林县| 斗六市|