Azure Avatar启动失败:atob解码InvalidCharacterError问题求助
问题诊断:Azure Avatar启动时的base64解码错误
访问网页时持续出现InvalidCharacterError: Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded错误,刷新页面无法解决;偶尔等待30-40秒后刷新可正常加载Avatar,但该方法极不稳定,错误仍频发。
相关代码
utility.js
// utilty.js export const fetchIceCredentials = async () => { try { const response = await axios.get('https://westus2.tts.speech.microsoft.com/cognitiveservices/avatar/relay/token/v1', { headers: { 'Ocp-Apim-Subscription-Key': cogSvcSubKey } }); const { Username: iceUsername, Password: iceCredential } = response.data; if (!iceUsername || !iceCredential) { throw new Error('ICE credentials are missing'); } if (iceUsername && iceCredential) { // Check base64 encoding if (!isValidBase64(iceCredential)) { console.error('ICE credential is not a valid base64 string:', iceCredential); console.error('ICE UserName is not a valid base64 string:', iceUsername); throw new Error('Invalid base64 encoding'); } else{ console.log('iceCredential is valid') return { iceUsername, iceCredential }; } } else { throw new Error('Invalid credentials format'); } } catch (error) { console.error('Error fetching ICE credentials:', error); throw error; } }; export function isValidBase64(str) { const base64Regex = /^([A-Za-z0-9+/=]+)$/; if (!base64Regex.test(str)) { return false; } try { const decodedStr = atob(str); return btoa(decodedStr) === str; } catch (e) { return false; } } export const createWebRTCConnection = (iceServerUrl, iceServerUsername, iceServerCredential) => { try { const peerConnection = new RTCPeerConnection({ iceServers: [{ urls: [iceServerUrl], username: iceServerUsername, credential: iceServerCredential }] }); return peerConnection; } catch (error) { console.error("Error creating WebRTC connection: ", error); throw error; } }; export const createAvatarSynthesizer = () => { try { console.log("Initializing Speech SDK with:", cogSvcSubKey, cogSvcRegion, voiceName); const speechSynthesisConfig = SpeechSDK.SpeechConfig.fromSubscription(cogSvcSubKey, cogSvcRegion); speechSynthesisConfig.speechSynthesisVoiceName = voiceName; const videoFormat = new SpeechSDK.AvatarVideoFormat(); const videoCropTopLeftX = 600; const videoCropBottomRightX = 1320; videoFormat.setCropRange(new SpeechSDK.Coordinate(videoCropTopLeftX, 50), new SpeechSDK.Coordinate(videoCropBottomRightX, 1080)); console.log("Avatar configuration:", avatarCharacter, avatarStyle, avatarBackgroundColor); const avatarConfig = new SpeechSDK.AvatarConfig(avatarCharacter, avatarStyle, videoFormat); avatarConfig.backgroundColor = avatarBackgroundColor; const avatarSynthesizer = new SpeechSDK.AvatarSynthesizer(speechSynthesisConfig, avatarConfig); avatarSynthesizer.avatarEventReceived = (s, e) => { const offsetMessage = e.offset === 0 ? "" : `, offset from session start: ${e.offset / 10000}ms.`; console.log(`[${new Date().toISOString()}] Event received: ${e.description}${offsetMessage}`); }; return avatarSynthesizer; } catch (error) { console.error("Error creating Avatar Synthesizer: ", error); throw error; } };
App.jsx(React代码片段)
// 获取ICE凭证 const { iceUsername, iceCredential } = await fetchIceCredentials(); const iceUrl = avatarAppConfig.iceUrl; let peerConnection = createWebRTCConnection(iceUrl, iceUsername, iceCredential); peerConnection.ontrack = handleOnTrack; peerConnection.addTransceiver('video', { direction: 'sendrecv' }); peerConnection.addTransceiver('audio', { direction: 'sendrecv' }); console.log("WebRTC连接已创建。"); let synthesizer = createAvatarSynthesizer(); setAvatarSynthesizer(synthesizer); peerConnection.oniceconnectionstatechange = e => { console.log("WebRTC状态: " + peerConnection.iceConnectionState); if (peerConnection.iceConnectionState === 'connected') { console.log("已连接到Azure Avatar服务"); } else if (peerConnection.iceConnectionState === 'disconnected' || peerConnection.iceConnectionState === 'failed') { console.log("Azure Avatar服务已断开连接"); } }; synthesizer.startAvatarAsync(peerConnection).then((r) => { console.log("[" + (new Date()).toISOString() + "] Avatar已启动。"); }).catch(async (error) => { console.error("[" + (new Date()).toISOString() + "] Avatar启动失败。错误: " + error); } );
错误信息
Avatar failed to start. Error: InvalidCharacterError: Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.
诊断与解决方案
1. 问题根源
错误出现在atob解码环节,核心原因有两点:
- Azure返回的ICE凭证可能是URL安全base64格式(用
-代替+、_代替/),这种格式无法被原生atob直接解码 - 当前的
isValidBase64校验逻辑不严谨:未验证base64的长度必须是4的倍数,也未处理URL安全格式转换,导致无效凭证绕过校验
2. 修复步骤
(1)更新base64校验函数
修改isValidBase64,支持URL安全格式转换与严格格式校验:
export function isValidBase64(str) { // 转换URL安全base64为标准格式 const standardBase64 = str.replace(/-/g, '+').replace(/_/g, '/'); // 补充必要的填充符,确保长度为4的倍数 const paddedStr = standardBase64.padEnd(standardBase64.length + (4 - standardBase64.length % 4) % 4, '='); // 严格校验:仅允许合法字符,填充符最多2个且只能在末尾 const base64Regex = /^[A-Za-z0-9+/]+={0,2}$/; if (!base64Regex.test(paddedStr)) { return false; } try { const decodedStr = atob(paddedStr); return btoa(decodedStr) === paddedStr; } catch (e) { return false; } }
(2)转换凭证为标准base64格式
在fetchIceCredentials返回凭证前,将URL安全格式转换为标准格式:
// 在return语句前添加转换逻辑 const standardUsername = iceUsername.replace(/-/g, '+').replace(/_/g, '/'); const standardCredential = iceCredential.replace(/-/g, '+').replace(/_/g, '/'); // 补充填充符 const paddedUsername = standardUsername.padEnd(standardUsername.length + (4 - standardUsername.length %4)%4, '='); const paddedCredential = standardCredential.padEnd(standardCredential.length + (4 - standardCredential.length%4)%4, '='); return { iceUsername: paddedUsername, iceCredential: paddedCredential };
(3)添加凭证获取重试机制
针对偶尔等待后可成功的情况,添加指数退避重试逻辑:
export const fetchIceCredentials = async (retries = 3, delay = 1000) => { try { const response = await axios.get('https://westus2.tts.speech.microsoft.com/cognitiveservices/avatar/relay/token/v1', { headers: { 'Ocp-Apim-Subscription-Key': cogSvcSubKey } }); const { Username: iceUsername, Password: iceCredential } = response.data; if (!iceUsername || !iceCredential) { throw new Error('ICE credentials are missing'); } // 转换为标准base64 const standardUsername = iceUsername.replace(/-/g, '+').replace(/_/g, '/'); const standardCredential = iceCredential.replace(/-/g, '+').replace(/_/g, '/'); const paddedUsername = standardUsername.padEnd(standardUsername.length + (4 - standardUsername.length %4)%4, '='); const paddedCredential = standardCredential.padEnd(standardCredential.length + (4 - standardCredential.length%4)%4, '='); if (!isValidBase64(paddedUsername) || !isValidBase64(paddedCredential)) { throw new Error('Invalid base64 encoding'); } return { iceUsername: paddedUsername, iceCredential: paddedCredential }; } catch (error) { if (retries > 0) { console.warn(`重试获取ICE凭证,剩余次数:${retries-1}`); await new Promise(resolve => setTimeout(resolve, delay)); return fetchIceCredentials(retries-1, delay*2); } console.error('Error fetching ICE credentials:', error); throw error; } };
3. 验证逻辑
修改后,确保传递给RTCPeerConnection的凭证是标准base64格式,且通过严格校验,彻底避免atob解码失败的问题。
内容的提问来源于stack exchange,提问作者Hamza Khan
相关产品推荐
相关产品推荐

