跳到主要内容

使用通话设备切换组件

切换通话输出设备

本文主要介绍AVCastPicker组件接入,实现通话设备切换功能。相关参数可参考@ohos.multimedia.avCastPicker(投播组件)@ohos.multimedia.avCastPickerParam(投播组件参数)。如果希望实现音频输出设备路由切换的效果,请参考实现音频输出设备路由切换

当前系统支持两种组件样式的显示方式:默认样式显示和自定义样式显示。

  • 如果应用选择显示默认样式,当设备切换时,系统将根据当前选择的设备显示系统默认的组件样式。
  • 如果应用选择显示自定义样式,那么需要应用根据设备的变化刷新自己定义的样式。

默认样式实现

  1. 创建voice_call类型的AVSession,AVSession在构造方法中支持不同的类型参数,由AVSessionType定义,voice_call表示通话类型,如果不创建,将显示空列表。

    import { AVCastPicker, AVCastPickerState, AVInputCastPicker, avSession } from '@kit.AVSessionKit';

    @Entry
    @Component
    struct Index {
    @State message: string = '模拟通话';
    @State session: avSession.AVSession | undefined = undefined;
    @State context: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
    // ...

    async init() {
    try {
    let context = this.getUIContext().getHostContext() as Context;
    // 通话开始时创建voice_call类型的avsession。
    this.session = await avSession.createAVSession(context, 'voiptest', 'voice_call');
    } catch (err) {
    console.error(`AVSession create : Error: Code: ${err.code}, message: ${err.message}`);
    }
    // ...
    }
    // ...
    }
  2. 在需要切换设备的通话界面创建AVCastPicker组件。

    import { AVCastPicker } from '@kit.AVSessionKit';

    @Entry
    @Component
    struct OutputCastPicker {
    @State normalColor:Color = Color.White;
    @State activeColor:Color = Color.Blue;
    @State pickerImage: ResourceStr = $r('app.media.sound'); // 自定义资源。
    // ...
    // 创建组件,并设置大小。
    build() {
    Row() {
    Column() {
    AVCastPicker({
    normalColor: this.normalColor,
    activeColor: this.activeColor,
    customPicker: this.ImageBuilder.bind(this), // 新增自定义参数。
    })
    .size({ width: '50%', height: '20%' })
    .id('AVCastPicker')
    // ...
    }
    .width('100%')
    .alignItems(HorizontalAlign.Center)
    }
    .alignItems(VerticalAlign.Center)
    .width('100%')
    .height('100%')
    }

    // 自定义内容。
    @Builder
    ImageBuilder() {
    Text($r('app.string.switch_OutputDevice'))
    Image(this.pickerImage)
    .size({ width: '100%', height: '100%' })
    .backgroundColor('#00000000')
    .fillColor(Color.Black)
    }
    }

    或者创建AVCastPickerHelper组件。

    import { common } from '@kit.AbilityKit';
    import { BusinessError } from '@kit.BasicServicesKit';
    import { avSession } from '@kit.AVSessionKit';

    class MyPage {
    private avCastPicker: avSession.AVCastPickerHelper;

    constructor(context: common.UIAbilityContext) {
    this.avCastPicker = new avSession.AVCastPickerHelper(context);
    }

    async selectCastDevice() {
    const avCastPickerOptions: avSession.AVCastPickerOptions = {
    sessionType: 'video',
    };

    this.avCastPicker.select(avCastPickerOptions).then(() => {
    console.info('select successfully');
    }).catch((err: BusinessError) => {
    console.error('AVCastPicker.select failed with err: ${err.code}, ${err.message}');
    });
    }
    }
  3. 创建VOICE_COMMUNICATION类型的AudioRenderer,并开始播放。具体通话音频播放等实现,请参考开发音频通话功能

    import { audio } from '@kit.AudioKit';
    import { BusinessError } from '@kit.BasicServicesKit';
    import { common } from '@kit.AbilityKit';
    import { resourceManager } from '@kit.LocalizationKit';
    import { fileIo } from '@kit.CoreFileKit';

    class Options {
    public offset: number = 0;
    public length: number = 0;
    }
    export default class AudioRenderer {
    private audioRenderer: audio.AudioRenderer | undefined = undefined;
    private audioStreamInfo: audio.AudioStreamInfo = {
    // 请按照实际场景设置,当前参数仅参考。
    samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000, // 采样率。
    channels: audio.AudioChannel.CHANNEL_2, // 通道。
    sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 采样格式。
    encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW // 编码格式。
    }
    public appContext?: common.UIAbilityContext | undefined = undefined;
    private audioSource = 'test1.wav';
    private fileDescriptor?: resourceManager.RawFileDescriptor | undefined = undefined;
    // ...
    async getStageFileDescriptor(fileName: string): Promise<resourceManager.RawFileDescriptor | undefined> {
    let fileDescriptor: resourceManager.RawFileDescriptor | undefined = undefined;
    if (this.appContext) {
    let mgr = this.appContext.resourceManager;
    this.fileDescriptor = mgr.getRawFdSync(fileName);
    await mgr.getRawFd(fileName).then(value => {
    fileDescriptor = value;
    console.info('case getRawFileDescriptor success fileName: ' + fileName);
    }).catch((error: BusinessError) => {
    console.error('case getRawFileDescriptor err: ' + error);
    });
    }
    return fileDescriptor;
    }

    async startRenderer(): Promise<void> {
    if (this.audioRenderer !== undefined) {
    return;
    }
    this.getStageFileDescriptor(this.audioSource).then((res) => {
    this.fileDescriptor = res;
    });
    if (!this.fileDescriptor) {
    return;
    }
    let file: resourceManager.RawFileDescriptor = this.fileDescriptor;
    try {
    this.audioRenderer = await audio.createAudioRenderer(this.audioRendererOption);
    } catch (error) {
    console.error(`audioRenderer create : Error: ${JSON.stringify(error)}`);
    return;
    }
    let bufferSize: number = this.fileDescriptor.offset;
    let writeDataCallback = (buffer: ArrayBuffer) => {
    let options: Options = {
    offset: bufferSize,
    length: buffer.byteLength
    }
    fileIo.readSync(file.fd, buffer, options);
    bufferSize += buffer.byteLength;
    };
    this.audioRenderer.on('writeData', writeDataCallback);
    await this.audioRenderer.start();
    }

    async stopRenderer(): Promise<void> {
    if (this.audioRenderer) {
    await this.audioRenderer.release();
    this.audioRenderer = undefined;
    }
    if (this.fileDescriptor) {
    this.closeResource(this.audioSource);
    this.fileDescriptor = undefined;
    }
    }

    async closeResource(fileName: string): Promise<void> {
    if (this.appContext) {
    let mgr = this.appContext.resourceManager;
    await mgr.closeRawFd(fileName).then(() => {
    console.info('case closeRawFd success fileName: ' + fileName);
    }).catch((error: BusinessError) => {
    console.error('case closeRawFd err: ' + error);
    });
    }
    }
    }
  4. (可选)如果应用想知道设备切换情况,可以监听当前发声设备切换回调。

    import { audio } from '@kit.AudioKit';
    // ...
    export default class AudioRenderer {
    // ...
    private audioManager: audio.AudioManager | undefined = undefined;
    private audioRoutingManager: audio.AudioRoutingManager | undefined = undefined;
    private audioRendererInfo: audio.AudioRendererInfo = {
    // 需使用通话场景相应的参数。
    usage: audio.StreamUsage.STREAM_USAGE_VIDEO_COMMUNICATION, // 音频流使用类型:VOIP视频通话,默认为扬声器。
    rendererFlags: 0 // 音频渲染器标志:默认为0即可。
    }
    private audioRendererOption: audio.AudioRendererOptions = {
    streamInfo: this.audioStreamInfo,
    rendererInfo: this.audioRendererInfo
    };

    async observerDevices() {
    this.audioManager = audio.getAudioManager(); // 先创建audiomanager。
    if (!this.audioManager) {
    console.error('get audioManager failed');
    return;
    }
    // 再调用AudioManager的方法创建AudioRoutingManager实例。
    this.audioRoutingManager = this.audioManager.getRoutingManager();
    if(!this.audioRoutingManager) {
    return;
    }
    // 可选监听当前发声设备切换回调。
    this.audioRoutingManager.on('preferOutputDeviceChangeForRendererInfo', this.audioRendererInfo, (desc: audio.AudioDeviceDescriptors) => {
    console.info(`device change to: ${desc[0].deviceType}`); // 设备类型。
    });
    }
    // ...
    }
  5. 通话结束后,销毁会话。

    // 通话结束销毁第一步创建的session。
    this.session?.destroy((err) => {
    if (err) {
    console.error(`Failed to destroy session. Code: ${err.code}, message: ${err.message}`);
    } else {
    console.info(`Destroy : SUCCESS `);
    }
    });

自定义样式实现

自定义样式通过设置CustomBuilder类型的参数customPicker实现。

实现自定义样式的步骤与实现默认样式基本相同,开发者可参考默认样式实现,完成创建AVSession、实现音频播放等步骤。

存在差异的步骤如下所示。

  1. 创建自定义AVCastPicker,需要新增自定义参数(对应默认样式实现步骤2)。

    import { AVCastPicker } from '@kit.AVSessionKit';
    // ...

    @Entry
    @Component
    struct SelfCastPicker {
    @State pickerImage: ResourceStr = $r('app.media.earpiece'); // 自定义资源。
    // ...
    build() {
    Row() {
    Column() {
    AVCastPicker(
    {
    customPicker: (): void => this.ImageBuilder() // 新增自定义参数。
    }
    ).size({ height: 45, width: 45 })
    }
    }
    }

    // 自定义内容。
    @Builder
    ImageBuilder() {
    Image(this.pickerImage)
    .size({ width: '100%', height: '100%' })
    .backgroundColor('#00000000')
    .fillColor(Color.Black)
    }
    }
  2. 如果应用要根据出声设备变化而改变自定义样式,必须监听设备切换,然后实时刷新自定义样式(对应默认样式实现步骤4)。

    import { audio } from '@kit.AudioKit';

    @Entry
    @Component
    struct SelfCastPicker {
    // ...
    async selfObserverDevices() {
    let audioManager = audio.getAudioManager();
    let audioRoutingManager = audioManager.getRoutingManager();

    // 初次拉起AVCastPicker时需获取当前设备,刷新显示。
    this.changePickerShow(audioRoutingManager.getPreferredOutputDeviceForRendererInfoSync(this.audioRendererInfo));

    // 监听当前发声设备切换,及时根据不同设备类型显示不同的样式。
    audioRoutingManager.on('preferOutputDeviceChangeForRendererInfo', this.audioRendererInfo, (desc: audio.AudioDeviceDescriptors) => {
    this.changePickerShow(audioRoutingManager.getPreferredOutputDeviceForRendererInfoSync(this.audioRendererInfo));
    });
    }

    // 设备更新后刷新自定义资源pickerImage。
    private changePickerShow(desc: audio.AudioDeviceDescriptors) {
    if(!desc || !desc.length || !desc[0]) {
    return;
    }
    if (desc[0].deviceType === 2) {
    this.pickerImage = $r('app.media.sound');
    } else if (desc[0].deviceType === 7) {
    this.pickerImage = $r('app.media.bluetooth');
    } else {
    this.pickerImage = $r('app.media.earpiece');
    }
    }
    // ...
    }

切换通话输入设备(仅在PC/2in1设备可用)

系统不再提供音频输入设备切换的API,如果需要在应用内切换音频输入设备,并实现AVInputCastPicker组件,相关参数可参考@ohos.multimedia.avInputCastPicker@ohos.multimedia.avCastPickerParam

本文将主要介绍AVInputCastPicker组件接入,实现通话输入设备切换功能。

当前系统支持两种组件样式的显示方式:默认样式显示和自定义样式显示。

  • 如果应用选择显示默认样式,当设备切换时,系统将根据当前选择的设备显示系统默认的组件样式。
  • 如果应用选择显示自定义样式,那么需要应用根据设备的变化刷新自己定义的样式。

默认实现方式

  1. 在需要切换设备的通话界面创建AVInputCastPicker组件。

    import { AVCastPickerState, AVInputCastPicker } from '@kit.AVSessionKit';

    // ...
    // 设备列表显示状态变化回调(可选)。
    private onStateChange(state: AVCastPickerState) {
    if (state === AVCastPickerState.STATE_APPEARING) {
    console.info('The picker starts showing.');
    } else if (state === AVCastPickerState.STATE_DISAPPEARING) {
    console.info('The picker finishes presenting.');
    }
    }

    // 创建组件,并设置大小。
    build() {
    Row() {
    Column() {
    AVInputCastPicker(
    {
    onStateChange: this.onStateChange
    }
    ).size({ height: 45, width: 45 })
    }
    }
    }
  2. 实现通话功能,请参考开发音频通话功能

自定义实现方式

自定义样式通过设置AVInputCastPicker中的参数customPicker实现。

  1. 创建自定义AVInputCastPicker,需要新增自定义参数。

    import { AVCastPickerState, AVInputCastPicker } from '@kit.AVSessionKit';

    @Entry
    @Component
    struct InputCastPicker {
    @State pickerImage: ResourceStr = $r('app.media.sound'); // 自定义资源。
    // ...

    // 设备列表显示状态变化回调(可选)。
    private onStateChange(state: AVCastPickerState) {
    if (state === AVCastPickerState.STATE_APPEARING) {
    console.info('The picker starts showing.');
    } else if (state === AVCastPickerState.STATE_DISAPPEARING) {
    console.info('The picker finishes presenting.');
    }
    }

    build() {
    Row() {
    Column() {
    AVInputCastPicker(
    {
    customPicker: this.ImageBuilder.bind(this), // 新增自定义参数。
    onStateChange: this.onStateChange
    }
    )
    .size({ width: '50%', height: '20%' })
    .id('AVInputCastPicker')
    // ...
    }
    .width('100%')
    .alignItems(HorizontalAlign.Center)
    }
    .alignItems(VerticalAlign.Center)
    .width('100%')
    .height('100%')
    }

    // 自定义内容。
    @Builder
    ImageBuilder() {
    Text($r('app.string.switch_InputDevice'))
    Image(this.pickerImage)
    .size({ width: '100%', height: '100%' })
    .backgroundColor('#00000000')
    .fillColor(Color.Black)
    }
    }
  2. 实现通话功能,请参考开发音频通话功能