You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET 9 MAUI HybridWebView中Azure语音化身无音频问题求助

.NET 9 MAUI Android应用Azure Speech to Avatar无音频输出问题

我正在开发面向Android平台的.NET 9 MAUI移动应用,通过HybridWebView加载Azure Speech to Avatar视频,当前能正常显示化身画面及唇部动作,但无音频输出。


1. XAML中HybridWebView配置

<StackLayout Grid.Row="0" Orientation="Horizontal" VerticalOptions="Center" Margin="0,10,0,10">    
    <Grid RowDefinitions="Auto,*"  
          ColumnDefinitions="*"
          MaximumWidthRequest="400"
          MaximumHeightRequest="1000">         
        <HybridWebView x:Name="hybridWebView" 
                       RawMessageReceived="OnHybridWebViewRawMessageReceived"            
                       Grid.Row="1" />     
    </Grid> 
</StackLayout>

2. .NET调用JS生成化身的代码

var result = await hybridWebView.InvokeJavaScriptAsync("speak",     
    HybridJsContext.Default.DictionaryStringString,     
    [com.Message, Controllers.CurrentLanguage], 
    [HybridJsContext.Default.String, HybridJsContext.Default.String]);

3. HTML页面结构

<div class="avatar-container">
    <div id="videoContainer">
        <div id="overlayArea" style="position: absolute; top: 0; left: 0;" hidden="hidden">
            <p id="overlayText" style="font-size: large;"></p>
            <img id="overlayImage" src="your-image-source.png" alt="Overlay Image">
        </div>
        <div id="remoteVideo"></div>
        <canvas id="canvas" width="1920" height="1080" style="background-color: transparent; background-image: url('AvatarBackground.jpg'); background-size: cover; background-position: center;" hidden="hidden"></canvas>
        <canvas id="tmpCanvas" width="1920" height="1080" hidden="hidden"></canvas>
    </div>
</div>

4. 初始化Avatar的JS脚本

if (typeof SpeechSDK === "undefined") {
    console.log("INFO FROM INDEX.HTML: ERROR-IN-SCRIPT: Speech SDK is not loaded.", "color: red; font-size: 16px;");
} else {
    window.speechConfig = SpeechSDK.SpeechConfig.fromSubscription("[SUBSCRIPTIONKEY]", "[REGION]");
    window.avatarConfig = new SpeechSDK.AvatarConfig("lisa", "casual-sitting");

    window.peerConnection = new RTCPeerConnection({
        iceServers: [{
            urls: ["[TURN-URL-AND-PORT]"],
            username: "[USERNAME]",
            credential: "[CREDENTIALS]"
        }]
    });

    // Log connection state changes
    window.peerConnection.oniceconnectionstatechange = function () {
        console.log("INFO FROM INDEX.HTML: ICE Connection State: " + window.peerConnection.iceConnectionState, "color: orange; font-size: 14px;");
        if (window.peerConnection.iceConnectionState === 'connected') {
            console.log("INFO FROM INDEX.HTML: TURN server connection established.", "color: green; font-size: 14px;");
        } else if (window.peerConnection.iceConnectionState === 'failed') {
            console.error("ERROR FROM INDEX.HTML: TURN server connection failed.", "color: red; font-size: 14px;");
        }
    };

    window.peerConnection.ontrack = function (event) {
        console.log("INFO FROM INDEX.HTML: Track received: " + event.track.kind, "color: blue; font-size: 14px;");
        if (event.track.kind === 'video') {
            const videoElement = document.createElement('video');
            videoElement.srcObject = event.streams[0];
            videoElement.autoplay = true;
            videoElement.muted = true; // Add this line to enable autoplay
            videoElement.playsInline = true; // For iOS
            videoElement.style.width = '100%';
            window.remoteVideoDiv = document.getElementById('remoteVideo');
            remoteVideoDiv.innerHTML = '';
            remoteVideoDiv.appendChild(videoElement);
            console.info("INFO FROM INDEX.HTML: Video element appended.", "color: green; font-size: 12px;");
        }

        if (event.track.kind === 'audio') {
            window.audioElement = document.createElement('audio');
            audioElement.srcObject = event.streams[0];
            audioElement.autoplay = true;
            audioElement.muted = true;
            document.body.appendChild(audioElement);
            console.info("INFO FROM INDEX.HTML: Audio element appended.", "color: green; font-size: 12px;");
        }
    };

    window.peerConnection.addTransceiver('video', { direction: 'sendrecv' });
    window.peerConnection.addTransceiver('audio', { direction: 'sendrecv' });

    window.avatarSynthesizer = new SpeechSDK.AvatarSynthesizer(window.speechConfig, window.avatarConfig);

    window.avatarSynthesizer.startAvatarAsync(window.peerConnection).then(
        () => {
            console.info("INFO FROM INDEX.HTML: Avatar started successfully.", "color: green; font-size: 14px;");
            window.HybridWebView.InvokeDotNet('FinishInitializeAvatar');
            // window.speak("Hello, this is a test message from your avatar.", "en");
        }
    ).catch(
        (error) => {
            console.error("ERROR FROM INDEX.HTML: ERROR-IN-SCRIPT: Avatar failed to start. Error: " + error, "color: red; font-size: 16px;");
        }
    );
}

5. 执行语音的JS函数

window.speak = (textToSpeech, detectedLanguage) => {
    const audioElement = document.getElementById('audio');

    if (audioElement) {
        audioElement.muted = false;
    }
    const videoElement = document.getElementById('video');

    if (videoElement) {
        videoElement.muted = false;
    }
    window.audioElement.muted = false;
    window.remoteVideoDiv.muted = false;
    let myMap = new Map([["de", "de-DE"], ["en", "en-GB"], ["fr", "fr-FR"], ["es", "es-ES"], ["pl", "pl-PL"], ["pt", "pt-PT"], ["tr", "tr-TR"], ["nl", "nl-NL"]]);
    let language = myMap.get(detectedLanguage);

    let spokenText = textToSpeech;
    let spokenSsml = `<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xmlns:mstts='http://www.w3.org/2001/mstts' xml:lang="en-US">
        <voice name='en-US-JennyMultilingualNeural'>
            <lang xml:lang='${language}'>
                ${htmlEncode(spokenText)}
            </lang>
        </voice>
        </speak>`;

    return window.avatarSynthesizer.speakSsmlAsync(spokenSsml).then(
        (result) => {
            if (result.reason === SpeechSDK.ResultReason.SynthesizingAudioCompleted) {
                console.log("INFO FROM INDEX.HTML: Speech synthesized successfully with text " + spokenText, "color: green; font-size: 12px;");
                return "success";
            }
            return "failed";
        })
        .catch((error) => {
            console.log("ERROR FROM INDEX.HTML: ERROR-IN-SCRIPT: Failed to synthesize speech. Error: " + error, "color: red; font-size: 16px;");
            return "failed";
        });
}

6. Android权限配置

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

已排查的措施

  • 确认Android权限已配置齐全,包含RECORD_AUDIO、MODIFY_AUDIO_SETTINGS等音频相关权限
  • 在speak函数中尝试多种方式解除元素静音,包括window.audioElement、remoteVideoDiv等
  • 更换耳机测试,同时在Android模拟器及Pixel 8真机上验证问题
  • 日志显示Avatar初始化成功,语音合成返回"success",ICE连接状态为connected

请求提供进一步的排查思路或解决方案。

内容的提问来源于stack exchange,提问作者Ricardo Enrico Jahn

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 05:35:56