1 2/* 3* Copyright (C) 2024 Huawei Device Co., Ltd. 4* Licensed under the Apache License, Version 2.0 (the "License"); 5* you may not use this file except in compliance with the License. 6* You may obtain a copy of the License at 7* 8* http://www.apache.org/licenses/LICENSE-2.0 9* 10* Unless required by applicable law or agreed to in writing, software 11* distributed under the License is distributed on an "AS IS" BASIS, 12* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13* See the License for the specific language governing permissions and 14* limitations under the License. 15*/ 16 17import { audio } from '@kit.AudioKit'; 18import { fileIo } from '@kit.CoreFileKit'; 19import { BusinessError } from '@kit.BasicServicesKit'; 20import { resourceManager } from '@kit.LocalizationKit'; 21import { common } from '@kit.AbilityKit'; 22import { avSession, AVCastPicker, AVCastPickerState } from '@kit.AVSessionKit'; 23 24class Options { 25 public offset: number = 0; 26 public length: number = 0; 27} 28 29@Entry 30@Component 31struct customPicker { 32 @State session: avSession.AVSession | undefined = undefined; 33 @State avCastPickerColor:Color = Color.White; 34 @State pickerImage: ResourceStr = $r('app.media.ic_earpiece'); 35 private appContext?: common.Context | undefined = undefined; 36 private audioManager: audio.AudioManager | undefined = undefined; 37 private audioRoutingManager: audio.AudioRoutingManager | undefined = undefined; 38 private audioRenderer: audio.AudioRenderer | undefined = undefined; 39 private audioSource = 'test1.wav'; 40 private fileDescriptor?: resourceManager.RawFileDescriptor | undefined = undefined; 41 private audioRendererInfo: audio.AudioRendererInfo = { 42 usage: audio.StreamUsage.STREAM_USAGE_VOICE_COMMUNICATION, 43 rendererFlags: 0, 44 }; 45 private audioStreamInfo: audio.AudioStreamInfo = { 46 samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000, // 采样率 47 channels: audio.AudioChannel.CHANNEL_2, // 通道 48 sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 采样格式 49 encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW // 编码格式 50 }; 51 private audioRendererOption: audio.AudioRendererOptions = { 52 streamInfo: this.audioStreamInfo, 53 rendererInfo: this.audioRendererInfo 54 }; 55 56 async aboutToAppear() { 57 console.log('about to appear'); 58 await this.init(); 59 } 60 61 async init() { 62 if (!this.appContext) { 63 this.appContext = getContext(); 64 } 65 this.session = await avSession.createAVSession(this.appContext, 'voiptest', 'voice_call'); 66 this.observerDevices(); 67 } 68 69 async observerDevices() { 70 this.audioManager = audio.getAudioManager(); 71 if (!this.audioManager) { 72 console.error('get audioManager failed'); 73 return; 74 } 75 this.audioRoutingManager = this.audioManager.getRoutingManager(); 76 if(!this.audioRoutingManager) { 77 return; 78 } 79 let desc: audio.AudioDeviceDescriptors = 80 this.audioRoutingManager.getPreferredOutputDeviceForRendererInfoSync(this.audioRendererInfo); 81 this.changePickerShow(desc); //第一次拉起picker时获取当前输出设备 82 this.audioRoutingManager.on('preferOutputDeviceChangeForRendererInfo', this.audioRendererInfo, (desc: audio.AudioDeviceDescriptors) => { 83 if(!this.audioRoutingManager) { 84 return; 85 } 86 console.log(`device change to: ${desc[0].deviceType}`); 87 let devices: audio.AudioDeviceDescriptors = 88 this.audioRoutingManager.getPreferredOutputDeviceForRendererInfoSync(this.audioRendererInfo); 89 this.changePickerShow(devices); 90 }); 91 } 92 93 private changePickerShow(desc: audio.AudioDeviceDescriptors): void { 94 if (desc[0].deviceType === 2) { 95 this.pickerImage = $r('app.media.ic_public_sound'); 96 } else if (desc[0].deviceType === 7) { 97 this.pickerImage = $r('app.media.ic_bluetooth'); 98 } else { 99 this.pickerImage = $r('app.media.ic_earpiece'); 100 } 101 } 102 103 async getStageFileDescriptor(fileName: string): Promise<resourceManager.RawFileDescriptor | undefined> { 104 let fileDescriptor: resourceManager.RawFileDescriptor | undefined = undefined; 105 if (this.appContext) { 106 let mgr = this.appContext.resourceManager; 107 this.fileDescriptor = mgr.getRawFdSync(fileName); 108 await mgr.getRawFd(fileName).then(value => { 109 fileDescriptor = value; 110 console.log(`case getRawFileDescriptor success fileName: ${fileName}`); 111 }).catch((err: BusinessError) => { 112 console.error(`case getRawFileDescriptor err: code: ${err.code}, message: ${err.message}`); 113 }); 114 } 115 return fileDescriptor; 116 } 117 118 async startRenderer(): Promise<void> { 119 if (this.audioRenderer !== undefined) { 120 return; 121 } 122 this.getStageFileDescriptor(this.audioSource).then((res) => { 123 this.fileDescriptor = res; 124 }); 125 if (!this.fileDescriptor) { 126 return; 127 } 128 let file: resourceManager.RawFileDescriptor = this.fileDescriptor; 129 try { 130 this.audioRenderer = await audio.createAudioRenderer(this.audioRendererOption); 131 } catch (error) { 132 console.error(`audioRenderer create : Error: ${JSON.stringify(error)}`); 133 return; 134 } 135 let bufferSize: number = this.fileDescriptor.offset; 136 let writeDataCallback = (buffer: ArrayBuffer) => { 137 let options: Options = { 138 offset: bufferSize, 139 length: buffer.byteLength 140 } 141 fileIo.readSync(file.fd, buffer, options); 142 bufferSize += buffer.byteLength; 143 }; 144 this.audioRenderer.on('writeData', writeDataCallback); 145 await this.audioRenderer.start(); 146 } 147 148 async stopRenderer(): Promise<void> { 149 if (this.audioRenderer) { 150 await this.audioRenderer.release(); 151 this.audioRenderer = undefined; 152 } 153 if (this.fileDescriptor) { 154 this.closeResource(this.audioSource); 155 this.fileDescriptor = undefined; 156 } 157 } 158 159 async closeResource(fileName: string): Promise<void> { 160 if (this.appContext) { 161 let mgr = this.appContext.resourceManager; 162 await mgr.closeRawFd(fileName).then(() => { 163 console.log(`case closeRawFd success fileName: ${fileName}`); 164 }).catch((err: BusinessError) => { 165 console.error(`case closeRawFd err: code: ${err.code}, message: ${err.message}`); 166 }); 167 } 168 } 169 170 onBackPress(): void { 171 this.stopRenderer(); 172 this.destroy(); 173 } 174 175 async onPageHide(): Promise<void> { 176 this.stopRenderer(); 177 this.destroy(); 178 } 179 180 onPageShow(): void { 181 this.init(); 182 } 183 184 destroy(): void { 185 this.appContext = undefined; 186 if (this.audioRoutingManager !== undefined) { 187 this.audioRoutingManager.off('preferOutputDeviceChangeForRendererInfo'); 188 } 189 try { 190 if (this.session) { 191 this.session?.destroy(); 192 } 193 } catch (err) { 194 console.error('session destroy failed'); 195 } 196 } 197 198 aboutToDisappear() { 199 console.log('about to disappear'); 200 this.destroy(); 201 } 202 203 @Builder 204 ImageBuilder(): void { 205 Image(this.pickerImage) 206 .size({ width: '100%', height: '100%' }) 207 .backgroundColor('#00000000') 208 .fillColor(Color.Black) 209 } 210 211 build() { 212 Row() { 213 Column() { 214 Button() { 215 Text('start').fontSize(22).fontColor(Color.White) 216 } 217 .size({ width: 64, height: 64 }) 218 .onClick(() => { 219 this.startRenderer(); 220 }) 221 .type(ButtonType.Circle) 222 } 223 .size({ width: '33%', height: 64 }) 224 225 Column() { 226 Button() { 227 Text('stop').fontSize(22).fontColor(Color.White) 228 } 229 .size({ width: 64, height: 64 }) 230 .onClick(() => { 231 this.stopRenderer(); 232 }) 233 .type(ButtonType.Circle) 234 } 235 .size({ width: '33%', height: 64 }) 236 237 Column() { 238 Button() { 239 AVCastPicker({ 240 normalColor: this.avCastPickerColor, 241 activeColor: this.avCastPickerColor, 242 customPicker: (): void => this.ImageBuilder(), 243 onStateChange: (state: AVCastPickerState) => { 244 console.info(`change avcastpicker: ${state}`); 245 } 246 }) 247 .size({ width: 45, height: 45 }) 248 } 249 .size({ width: 64, height: 64 }) 250 .type(ButtonType.Circle) 251 .backgroundColor(Color.Orange) 252 } 253 .size({ width: '33%', height: 64 }) 254 } 255 .margin({ top: 300}) 256 .justifyContent(FlexAlign.SpaceBetween) 257 } 258}