From 534ea6826e13becc0c342b765d2e1590cb9607f6 Mon Sep 17 00:00:00 2001 From: todo2088 Date: Fri, 24 Jul 2026 22:56:05 -0700 Subject: [PATCH 1/4] =?UTF-8?q?fix(windows):=20=E4=BF=AE=E5=A4=8DTTS?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E5=9B=9E=E8=B0=83=E8=A7=A6=E5=8F=91=E7=A9=BA?= =?UTF-8?q?=E6=8C=87=E9=92=88=E5=92=8C=E8=B5=84=E6=BA=90=E6=B3=84=E6=BC=8F?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构了Windows平台TTS插件的完成回调逻辑,使用临界区保护speakResult指针,统一管理等待句柄的注册与反注册,避免重复注册导致的资源泄漏以及回调访问已销毁对象引发的崩溃。同时在stop方法中主动完成未完成的语音合成任务,防止Dart侧永久等待。 --- windows/flutter_tts_plugin.cpp | 81 ++++++++++++++++++++++++++++------ 1 file changed, 68 insertions(+), 13 deletions(-) diff --git a/windows/flutter_tts_plugin.cpp b/windows/flutter_tts_plugin.cpp index 6d59088a..8f55a149 100644 --- a/windows/flutter_tts_plugin.cpp +++ b/windows/flutter_tts_plugin.cpp @@ -250,6 +250,7 @@ namespace { static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar); FlutterTtsPlugin(); virtual ~FlutterTtsPlugin(); + void OnSpeakComplete(); private: // Called when a method is called on this plugin's channel from Dart. void HandleMethodCall( @@ -276,6 +277,7 @@ namespace { bool paused(); FlutterResult speakResult; HANDLE addWaitHandle; + CRITICAL_SECTION cs; }; void FlutterTtsPlugin::RegisterWithRegistrar( @@ -294,6 +296,7 @@ namespace { } FlutterTtsPlugin::FlutterTtsPlugin() { + InitializeCriticalSection(&cs); addWaitHandle = NULL; isPaused = false; speakResult = NULL; @@ -314,18 +317,25 @@ namespace { } FlutterTtsPlugin::~FlutterTtsPlugin() { + if (addWaitHandle != NULL) { + UnregisterWaitEx(addWaitHandle, INVALID_HANDLE_VALUE); + addWaitHandle = NULL; + } + EnterCriticalSection(&cs); + if (speakResult) speakResult.reset(); + LeaveCriticalSection(&cs); + if (pVoice != NULL) { + pVoice->Release(); + pVoice = NULL; + } + DeleteCriticalSection(&cs); ::CoUninitialize(); } - void CALLBACK setResult(PVOID lpParam, BOOLEAN TimerOrWaitFired) + void CALLBACK onSpeakComplete(PVOID lpParam, BOOLEAN TimerOrWaitFired) { - flutter::MethodResult* p = (flutter::MethodResult*) lpParam; - p->Success(1); - } - - void CALLBACK onCompletion(PVOID lpParam, BOOLEAN TimerOrWaitFired) - { - methodChannel->InvokeMethod("speak.onComplete", NULL); + FlutterTtsPlugin* plugin = static_cast(lpParam); + plugin->OnSpeakComplete(); } bool FlutterTtsPlugin::speaking() @@ -339,6 +349,15 @@ namespace { void FlutterTtsPlugin::speak(const std::string text, FlutterResult result) { + // Unregister any previous completion wait first. Without this, a stale + // wait callback could fire against a destroyed MethodResult (the + // previous speak's result), causing an access violation (0xc0000005). + // INVALID_HANDLE_VALUE makes UnregisterWaitEx block until any in-flight + // callback finishes before returning. + if (addWaitHandle != NULL) { + UnregisterWaitEx(addWaitHandle, INVALID_HANDLE_VALUE); + addWaitHandle = NULL; + } HRESULT hr; const std::string arg = "" + text; @@ -349,12 +368,34 @@ namespace { delete[] wstr; HANDLE speakCompletionHandle = pVoice->SpeakCompleteEvent(); methodChannel->InvokeMethod("speak.onStart", NULL); - RegisterWaitForSingleObject(&addWaitHandle, speakCompletionHandle, (WAITORTIMERCALLBACK)&onCompletion, speakResult.get(), INFINITE, WT_EXECUTEONLYONCE); - if (awaitSpeakCompletion){ - speakResult = std::move(result); - RegisterWaitForSingleObject(&addWaitHandle, speakCompletionHandle, (WAITORTIMERCALLBACK)&setResult, speakResult.get(), INFINITE, WT_EXECUTEONLYONCE); + if (awaitSpeakCompletion) { + // Move the result into speakResult BEFORE registering the wait, so + // the callback always observes a valid pointer. Guarded by cs to + // avoid racing with a concurrently firing callback. + EnterCriticalSection(&cs); + speakResult = std::move(result); + LeaveCriticalSection(&cs); + } + else { + result->Success(1); } - else result->Success(1); + // A single completion wait drives both speak.onComplete and resolving + // the awaited speak future. Using one handle avoids the previous bug of + // overwriting addWaitHandle with a second RegisterWaitForSingleObject + // call (which leaked the first registration). + RegisterWaitForSingleObject(&addWaitHandle, speakCompletionHandle, + (WAITORTIMERCALLBACK)&onSpeakComplete, this, INFINITE, + WT_EXECUTEONLYONCE); + } + + void FlutterTtsPlugin::OnSpeakComplete() { + methodChannel->InvokeMethod("speak.onComplete", NULL); + EnterCriticalSection(&cs); + if (speakResult) { + speakResult->Success(1); + speakResult.reset(); + } + LeaveCriticalSection(&cs); } void FlutterTtsPlugin::pause() { @@ -373,10 +414,24 @@ namespace { } void FlutterTtsPlugin::stop() { + // Cancel the pending completion wait first so its callback can't fire + // after we invalidate speakResult below (would be a use-after-free). + if (addWaitHandle != NULL) { + UnregisterWaitEx(addWaitHandle, INVALID_HANDLE_VALUE); + addWaitHandle = NULL; + } pVoice->Speak(L"", 2, NULL); pVoice->Resume(); isPaused = false; methodChannel->InvokeMethod("speak.onCancel", NULL); + // Resolve the awaited speak future (if any) so the Dart side isn't left + // waiting forever after a cancel. + EnterCriticalSection(&cs); + if (speakResult) { + speakResult->Success(1); + speakResult.reset(); + } + LeaveCriticalSection(&cs); } void FlutterTtsPlugin::setVolume(const double newVolume) { From 3670d54c65f6798684320cc7909cc14cfbcf2151 Mon Sep 17 00:00:00 2001 From: todo2088 Date: Fri, 24 Jul 2026 23:02:00 -0700 Subject: [PATCH 2/4] build: bump flutter_tts version to 4.2.5+1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 仅更新应用版本号,进行版本迭代 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index f9da5647..8d3c690e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_tts description: A flutter plugin for Text to Speech. This plugin is supported on iOS, macOS, Android, Web, & Windows. -version: 4.2.5 +version: 4.2.5+1 homepage: https://github.com/dlutton/flutter_tts dependencies: From adaf03b7df459a11b8cd443a7807e8c69f1bf2a5 Mon Sep 17 00:00:00 2001 From: todo2088 Date: Sat, 25 Jul 2026 00:39:22 -0700 Subject: [PATCH 3/4] =?UTF-8?q?fix(windows):=20=E4=BF=AE=E5=A4=8Dflutter?= =?UTF-8?q?=5Ftts=E6=8F=92=E4=BB=B6=E7=9A=84=E5=A4=9A=E9=A1=B9=E7=A8=B3?= =?UTF-8?q?=E5=AE=9A=E6=80=A7=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 修复MediaEnded回调中未重置speakResult导致的内存访问问题 2. 增加pVoice空指针检查,避免调用无效语音实例崩溃 3. 优化COM初始化逻辑,兼容已初始化COM的线程环境 4. 修复语音枚举流程中的资源泄漏和错误处理逻辑 5. 完善停止、暂停、继续播放等操作的空指针安全检查 --- windows/flutter_tts_plugin.cpp | 73 ++++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 26 deletions(-) diff --git a/windows/flutter_tts_plugin.cpp b/windows/flutter_tts_plugin.cpp index 8f55a149..cedf42f0 100644 --- a/windows/flutter_tts_plugin.cpp +++ b/windows/flutter_tts_plugin.cpp @@ -78,14 +78,15 @@ namespace { mPlayer = winrt::Windows::Media::Playback::MediaPlayer::MediaPlayer(); auto mEndedToken = mPlayer.MediaEnded([=](Windows::Media::Playback::MediaPlayer const& sender, - Windows::Foundation::IInspectable const& args) - { - methodChannel->InvokeMethod("speak.onComplete", NULL); - if (awaitSpeakCompletion) { + Windows::Foundation::IInspectable const& args) + { + methodChannel->InvokeMethod("speak.onComplete", NULL); + if (awaitSpeakCompletion && speakResult) { speakResult->Success(1); + speakResult.reset(); } - isSpeaking = false; - }); + isSpeaking = false; + }); } bool FlutterTtsPlugin::speaking() { @@ -129,8 +130,9 @@ namespace { void FlutterTtsPlugin::stop() { methodChannel->InvokeMethod("speak.onCancel", NULL); - if (awaitSpeakCompletion) { + if (awaitSpeakCompletion && speakResult) { speakResult->Success(1); + speakResult.reset(); } mPlayer.Close(); @@ -302,8 +304,12 @@ namespace { speakResult = NULL; pVoice = NULL; HRESULT hr; + // Tolerate COM already being initialized on this thread (e.g. by the + // engine or another plugin). RPC_E_CHANGED_MODE means the thread is + // already in a different apartment; in that case reuse it rather than + // throwing and crashing the host app. hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); - if (FAILED(hr)) + if (FAILED(hr) && hr != RPC_E_CHANGED_MODE) { throw std::exception("TTS init failed"); } @@ -340,6 +346,7 @@ namespace { bool FlutterTtsPlugin::speaking() { + if (pVoice == NULL) return false; SPVOICESTATUS status; pVoice->GetStatus(&status, NULL); if (status.dwRunningState == SPRS_IS_SPEAKING) return true; @@ -349,6 +356,7 @@ namespace { void FlutterTtsPlugin::speak(const std::string text, FlutterResult result) { + if (pVoice == NULL) { result->Success(0); return; } // Unregister any previous completion wait first. Without this, a stale // wait callback could fire against a destroyed MethodResult (the // previous speak's result), causing an access violation (0xc0000005). @@ -399,7 +407,7 @@ namespace { } void FlutterTtsPlugin::pause() { - if (isPaused == false) + if (pVoice != NULL && isPaused == false) { pVoice->Pause(); isPaused = true; @@ -409,7 +417,7 @@ namespace { void FlutterTtsPlugin::continuePlay() { isPaused = false; - pVoice->Resume(); + if (pVoice != NULL) pVoice->Resume(); methodChannel->InvokeMethod("speak.onContinue", NULL); } void FlutterTtsPlugin::stop() @@ -420,8 +428,10 @@ namespace { UnregisterWaitEx(addWaitHandle, INVALID_HANDLE_VALUE); addWaitHandle = NULL; } - pVoice->Speak(L"", 2, NULL); - pVoice->Resume(); + if (pVoice != NULL) { + pVoice->Speak(L"", 2, NULL); + pVoice->Resume(); + } isPaused = false; methodChannel->InvokeMethod("speak.onCancel", NULL); // Resolve the awaited speak future (if any) so the Dart side isn't left @@ -435,12 +445,14 @@ namespace { } void FlutterTtsPlugin::setVolume(const double newVolume) { + if (pVoice == NULL) return; const USHORT volume = (short)(100 * newVolume); pVoice->SetVolume(volume); } void FlutterTtsPlugin::setPitch(const double newPitch) {pitch = newPitch;} void FlutterTtsPlugin::setRate(const double newRate) { + if (pVoice == NULL) return; const long speechRate = (long)((newRate - 0.5) * 15); pVoice->SetRate(speechRate); } @@ -453,24 +465,26 @@ namespace { ULONG ulCount = 0; // Get the number of voices. hr = cpEnum->GetCount(&ulCount); - if (FAILED(hr)) return; + if (FAILED(hr)) { cpEnum->Release(); return; } ISpObjectToken* cpVoiceToken = NULL; while (ulCount--) { cpVoiceToken = NULL; hr = cpEnum->Next(1, &cpVoiceToken, NULL); - if (FAILED(hr)) return; + if (FAILED(hr)) break; CComPtr cpAttribKey; hr = cpVoiceToken->OpenKey(L"Attributes", &cpAttribKey); - if (FAILED(hr)) return; + if (FAILED(hr)) { cpVoiceToken->Release(); continue; } WCHAR* psz = NULL; hr = cpAttribKey->GetStringValue(L"Language", &psz); + if (FAILED(hr) || psz == NULL) { cpVoiceToken->Release(); continue; } wchar_t locale[25]; LCIDToLocaleName((LCID)std::strtol(CW2A(psz), NULL, 16), locale, 25, 0); ::CoTaskMemFree(psz); std::string language = CW2A(locale); psz = NULL; - cpAttribKey->GetStringValue(L"Name", &psz); + hr = cpAttribKey->GetStringValue(L"Name", &psz); + if (FAILED(hr) || psz == NULL) { cpVoiceToken->Release(); continue; } std::string name = CW2A(psz); ::CoTaskMemFree(psz); flutter::EncodableMap voiceInfo; @@ -479,6 +493,7 @@ namespace { voices.push_back(flutter::EncodableMap(voiceInfo)); cpVoiceToken->Release(); } + cpEnum->Release(); } void FlutterTtsPlugin::setVoice(const std::string voiceLanguage, const std::string voiceName, FlutterResult& result) { HRESULT hr; @@ -487,24 +502,25 @@ namespace { if (FAILED(hr)) { result->Success(0); return; } ULONG ulCount = 0; hr = cpEnum->GetCount(&ulCount); - if (FAILED(hr)) { result->Success(0); return; } + if (FAILED(hr)) { cpEnum->Release(); result->Success(0); return; } ISpObjectToken* cpVoiceToken = NULL; bool success = false; while (ulCount--) { cpVoiceToken = NULL; hr = cpEnum->Next(1, &cpVoiceToken, NULL); - if (FAILED(hr)) { result->Success(0); return; } + if (FAILED(hr)) break; CComPtr cpAttribKey; hr = cpVoiceToken->OpenKey(L"Attributes", &cpAttribKey); - if (FAILED(hr)) { result->Success(0); return; } + if (FAILED(hr)) { cpVoiceToken->Release(); continue; } WCHAR* psz = NULL; hr = cpAttribKey->GetStringValue(L"Name", &psz); - if (FAILED(hr)) { result->Success(0); return; } + if (FAILED(hr) || psz == NULL) { cpVoiceToken->Release(); continue; } std::string name = CW2A(psz); ::CoTaskMemFree(psz); psz = NULL; hr = cpAttribKey->GetStringValue(L"Language", &psz); + if (FAILED(hr) || psz == NULL) { cpVoiceToken->Release(); continue; } wchar_t locale[25]; LCIDToLocaleName((LCID)std::strtol(CW2A(psz), NULL, 16), locale, 25, 0); ::CoTaskMemFree(psz); @@ -516,6 +532,7 @@ namespace { } cpVoiceToken->Release(); } + cpEnum->Release(); result->Success(success ? 1 : 0); } void FlutterTtsPlugin::getLanguages(flutter::EncodableList& languages) @@ -528,20 +545,21 @@ namespace { ULONG ulCount = 0; // Get the number of voices. hr = cpEnum->GetCount(&ulCount); - if (FAILED(hr)) return; + if (FAILED(hr)) { cpEnum->Release(); return; } ISpObjectToken* cpVoiceToken = NULL; std::set languagesSet = {}; while (ulCount--) { cpVoiceToken = NULL; hr = cpEnum->Next(1, &cpVoiceToken, NULL); - if (FAILED(hr)) return; + if (FAILED(hr)) break; CComPtr cpAttribKey; hr = cpVoiceToken->OpenKey(L"Attributes", &cpAttribKey); - if (FAILED(hr)) return; + if (FAILED(hr)) { cpVoiceToken->Release(); continue; } WCHAR* psz = NULL; hr = cpAttribKey->GetStringValue(L"Language", &psz); + if (FAILED(hr) || psz == NULL) { cpVoiceToken->Release(); continue; } wchar_t locale[25]; LCIDToLocaleName((LCID)std::strtol(CW2A(psz), NULL, 16), locale, 25, 0); std::string language = CW2A(locale); @@ -549,6 +567,7 @@ namespace { ::CoTaskMemFree(psz); cpVoiceToken->Release(); } + cpEnum->Release(); std::for_each(begin(languagesSet), end(languagesSet), [&languages](const flutter::EncodableValue value) { languages.push_back(value); @@ -562,20 +581,21 @@ namespace { if (FAILED(hr)) { result->Success(0); return; } ULONG ulCount = 0; hr = cpEnum->GetCount(&ulCount); - if (FAILED(hr)) { result->Success(0); return; } + if (FAILED(hr)) { cpEnum->Release(); result->Success(0); return; } ISpObjectToken* cpVoiceToken = NULL; bool found = false; while (ulCount--) { cpVoiceToken = NULL; hr = cpEnum->Next(1, &cpVoiceToken, NULL); - if (FAILED(hr)) { result->Success(0); return; } + if (FAILED(hr)) break; CComPtr cpAttribKey; hr = cpVoiceToken->OpenKey(L"Attributes", &cpAttribKey); - if (FAILED(hr)) { result->Success(0); return; } + if (FAILED(hr)) { cpVoiceToken->Release(); continue; } WCHAR* psz = NULL; hr = cpAttribKey->GetStringValue(L"Language", &psz); + if (FAILED(hr) || psz == NULL) { cpVoiceToken->Release(); continue; } wchar_t locale[25]; LCIDToLocaleName((LCID)std::strtol(CW2A(psz), NULL, 16), locale, 25, 0); std::string language = CW2A(locale); @@ -587,6 +607,7 @@ namespace { ::CoTaskMemFree(psz); cpVoiceToken->Release(); } + cpEnum->Release(); if (found) result->Success(1); else result->Success(0); } From 62b2235a24711022b2d799baf62a7eb850cb9d6a Mon Sep 17 00:00:00 2001 From: todo2088 Date: Sat, 25 Jul 2026 00:43:03 -0700 Subject: [PATCH 4/4] =?UTF-8?q?build:=20=E5=8D=87=E7=BA=A7flutter=5Ftts?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E5=88=B04.2.5+2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 仅更新了pubspec.yaml中的版本号,发布小版本更新 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 8d3c690e..72d52af1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_tts description: A flutter plugin for Text to Speech. This plugin is supported on iOS, macOS, Android, Web, & Windows. -version: 4.2.5+1 +version: 4.2.5+2 homepage: https://github.com/dlutton/flutter_tts dependencies: