H5页面中实现扬声器听筒切换
==============

#### 问题现象

如何在H5页面中播放音频，并且实现扬声器听筒切换？

#### 背景知识

*   [setDefaultOutputDevice](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-audio-audiorenderer#setdefaultoutputdevice12)：设置默认发声设备。本接口仅适用于[StreamUsage](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-audio-e#streamusage)为语音消息、VoIP语音通话或者VoIP视频通话的场景，支持听筒、扬声器和系统默认设备。
*   [AudioRenderer](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-audio-audiorenderer)：用于音频输出的API，仅支持PCM格式，需要应用持续写入音频数据进行工作。应用可以在输入前添加数据预处理，如设定音频文件的采样率、位宽、音频流信息等。
*   [javaScriptProxy](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-basic-components-web-attributes#javascriptproxy)：将javaScriptProxy中的ArkTS对象注册到Web组件中，该对象将使用JavaScriptProxy中指定的名称注册到网页的所有框架中。

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/3b/v3/bQYFRNRdR_ekcUCMmGY8nw/note_3.0-zh-cn.png?HW-CC-KV=V1&HW-CC-Date=20260709T134247Z&HW-CC-Expire=86400&HW-CC-Sign=6E87CD184134CA9717643EB180301BC73B680E7B1943E92897471194BB2F39F8)

javaScriptProxy接口需要和deleteJavaScriptRegister接口配合使用，防止内存泄漏，并且javaScriptProxy对象的所有参数不支持更新。

#### 解决方案

*   使用AudioRenderer播放音频，详情可以参考AudioRenderer[开发步骤和注意事项](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/using-audiorenderer-for-playback#开发步骤及注意事项)以及[完整示例代码](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/using-audiorenderer-for-playback#完整示例)。
*   调用setDefaultOutputDevice()方法进行听筒和扬声器的切换。
    
    参考示例代码如下
    
    class OutputDeviceChange {
     _ // 扬声器播放_
      async changeToSpeaker() {
        audioRenderer.setDefaultOutputDevice(audio.DeviceType.SPEAKER).then(() => {
          console.info('setDefaultOutputDevice to SPEAKER Success!');
        }).catch((err: BusinessError) => {
          console.error(\`setDefaultOutputDevice Fail: ${err}\`);
        });
      }
     _ // 听筒播放_
      async changeToEarpiece() {
        audioRenderer.setDefaultOutputDevice(audio.DeviceType.EARPIECE).then(() => {
          console.info('setDefaultOutputDevice to EARPIECE Success!');
        }).catch((err: BusinessError) => {
          console.error(\`setDefaultOutputDevice Fail: ${err}\`);
        });
      }
     _ // AudioRenderer播放音频_
      async startRenderer() {
        await audioRenderer.start();
      }
    }
    
*   使用javaScriptProxy()接口在前端页面调用应用侧函数，详情可以参考[如何建立应用侧与h5侧的交互通道](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/web-in-page-app-function-invoking#如何建立应用侧与h5侧的交互通道)。

完整示例代码参考如下：

import { audio } from '@kit.AudioKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { webview } from '@kit.ArkWeb';

_// 设置AudioRenderer参数_
let audioRenderer: audio.AudioRenderer;
let audioStreamInfo: audio.AudioStreamInfo = {
  samplingRate: audio.AudioSamplingRate.SAMPLE\_RATE\_8000, _// 采样率。_
  channels: audio.AudioChannel.CHANNEL\_1, _// 通道。_
  sampleFormat: audio.AudioSampleFormat.SAMPLE\_FORMAT\_S16LE, _// 采样格式。_
  encodingType: audio.AudioEncodingType.ENCODING\_TYPE\_RAW _// 编码格式。_
};
let audioRendererInfo: audio.AudioRendererInfo = {
  usage: audio.StreamUsage.STREAM\_USAGE\_VOICE\_COMMUNICATION, _// 音频流使用类型：音乐。根据业务场景配置，参考StreamUsage。_
  rendererFlags: 0 _// 音频渲染器标志。_
};
let audioRendererOptions: audio.AudioRendererOptions = {
  streamInfo: audioStreamInfo,
  rendererInfo: audioRendererInfo
};

class OutputDeviceChange {
 _ // 扬声器播放_
  async changeToSpeaker() {
    audioRenderer.setDefaultOutputDevice(audio.DeviceType.SPEAKER).then(() => {
      console.info('setDefaultOutputDevice to SPEAKER Success!');
    }).catch((err: BusinessError) => {
      console.error(\`setDefaultOutputDevice Fail: ${err}\`);
    });
  }
 _ // 听筒播放_
  async changeToEarpiece() {
    audioRenderer.setDefaultOutputDevice(audio.DeviceType.EARPIECE).then(() => {
      console.info('setDefaultOutputDevice to EARPIECE Success!');
    }).catch((err: BusinessError) => {
      console.error(\`setDefaultOutputDevice Fail: ${err}\`);
    });
  }
 _ // AudioRenderer播放音频_
  async startRenderer() {
    await audioRenderer.start();
  }
}

@Entry
@Component
struct PlayPcmDataDemo {
 _ //声明需要注册的对象_
  @State testObj: OutputDeviceChange = new OutputDeviceChange();
  webviewController: webview.WebviewController = new webview.WebviewController();
  context = this.getUIContext().getHostContext() as common.UIAbilityContext;
  audioData: Uint8Array = generateTestPCM(); _// 测试PCM数据，按需替换为其他音频数据源_
  writeOffset = 0;

  async aboutToAppear(): Promise<void> {
    audioRenderer = await audio.createAudioRenderer(audioRendererOptions);
    await this.init(); _//初始化_
  }

  async aboutToDisappear(): Promise<void> {
    await audioRenderer.release();
    this.stopAndFlush();
    try {
      this.webviewController.deleteJavaScriptRegister('testObjName');
    } catch (error) {
      console.error(\`ErrorCode:${(error as BusinessError).code},Message:${(error as BusinessError).message}\`);
    }
  }

  build() {
    Column() {
     _ // web组件加载本地html页面_
      Web({ src: $rawfile('SetOutputDevice.html'), controller: this.webviewController })
        .geolocationAccess(false)
        .fileAccess(false)
      _  // 将对象注入web_
        .javaScriptProxy({
          object: this.testObj,
          name: 'testObjName',
          methodList: \["startRenderer", "changeToEarpiece", "changeToSpeaker"\],
          controller: this.webviewController,
        })
        .javaScriptAccess(true);
    };
  }

  async init() {
    audioRenderer.on('writeData', (buffer: ArrayBuffer) => {
      if (!this.audioData) {
        return audio.AudioDataCallbackResult.INVALID;
      }
      let bufferView = new Uint8Array(buffer);
      let writeLen = Math.min(buffer.byteLength, this.audioData.byteLength - this.writeOffset);
      if (writeLen <= 0) {
        this.writeOffset = 0;
        console.info('Play Done');
        return audio.AudioDataCallbackResult.INVALID;
      }
      bufferView.set(this.audioData.slice(this.writeOffset, this.writeOffset + writeLen));
      this.writeOffset += writeLen;
      return audio.AudioDataCallbackResult.VALID;
    });
  }

  async stopAndFlush() {
    console.info('renderer status' + audioRenderer.state);
    audioRenderer.stop().then(() => {
      console.error('Renderer stop ok.');
    }).catch((err: BusinessError) => {
      console.error('Renderer stop failed. ', err);
    })
    ;
    audioRenderer.flush().then(() => {
      console.error('Renderer flush ok.');
    }).catch((err: BusinessError) => {
      console.error('renderer flush err. ' + err);
    });
    this.writeOffset = 0;
  }
}

function generateTestPCM(): Uint8Array {
  const sampleRate = 8000;
  const noteDuration = 0.5;
  const amplitude = 0.35;

  const freqMap: Record<number, number> = {
    1: 523.25, _// C5_
    2: 587.33, _// D5_
    3: 659.25, _// E5_
    4: 698.46, _// F5_
    5: 783.99, _// G5_
    6: 880.00  _// A5_
  };

  const melody = \[
    1, 1, 5, 5, 6, 6, 5, 0,
    4, 4, 3, 3, 2, 2, 1, 0,
    5, 5, 4, 4, 3, 3, 2, 0
  \];

  const samplesPerNote = Math.floor(sampleRate \* noteDuration); _// 4000_
  const totalSamples = samplesPerNote \* melody.length; _// 96,000_
  const buffer = new ArrayBuffer(totalSamples \* 2); _// 192,000 bytes_
  const view = new DataView(buffer);

  let idx = 0;
  for (const note of melody) {
    const freq = note ? freqMap\[note\] : 0;
    for (let i = 0; i < samplesPerNote; i++) {
      const t = i / sampleRate;
      const wave = freq ? amplitude \* Math.sin(2 \* Math.PI \* freq \* t) : 0;
      const sample = Math.round(wave \* 32767);
      const clamped = Math.max(-32768, Math.min(32767, sample));
      view.setInt16(idx \* 2, clamped, true);
      idx++;
    }
  }
  return new Uint8Array(buffer);
}

html页面：

_<!-- index.html -->_
<!DOCTYPE html>
<html>
<body>
<div align="center">
<button type="button" onclick="audioPlay()" style="width: 450px; height: 120px;font-size: 50px;">Play</button>
<p id="demo"></p>
<script>
    function audioPlay() {
        let str = testObjName.startRenderer();
        document.getElementById("demo").innerHTML = str;
    }
</script>

<button type="button" onclick="changeToEarpiece()" style="width: 450px; height: 120px;font-size: 50px;">ChangeEarpiece</button>
<p id="demo1"></p>
<script>
    function changeToEarpiece() {
        let str = testObjName.changeToEarpiece();
        document.getElementById("demo1").innerHTML = str;
    }
</script>

<button type="button" onclick="changeToSpeaker()" style="width: 450px; height: 120px;font-size: 50px;">ChangeSpeaker</button>
<p id="demo2"></p>
<script>
    function changeToSpeaker() {
        let str = testObjName.changeToSpeaker();
        document.getElementById("demo2").innerHTML = str;
    }
</script>
</div>
</body>
</html>