diff --git a/.github/labeler.yml b/.github/labeler.yml
index a57a165c3..a640c57cb 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -55,6 +55,8 @@
- packages/tizen_package_manager/**/*
"p: tizen_rpc_port":
- packages/tizen_rpc_port/**/*
+"p: tizen_stt":
+ - packages/tizen_stt/**/*
"p: tizen_window_manager":
- packages/tizen_window_manager/**/*
"p: url_launcher":
diff --git a/.github/recipe.yaml b/.github/recipe.yaml
index 20dc26aea..906fa34b3 100644
--- a/.github/recipe.yaml
+++ b/.github/recipe.yaml
@@ -34,6 +34,7 @@ plugins:
geolocator: []
in_app_purchase: []
network_info_plus: []
+ tizen_stt: []
video_player_avplay: []
video_player_videohole: []
diff --git a/README.md b/README.md
index dcb4a16ba..2579b04a2 100644
--- a/README.md
+++ b/README.md
@@ -43,6 +43,7 @@ The _"non-endorsed"_ status means that the plugin is not endorsed by the origina
| [**tizen_notification**](packages/tizen_notification) | (Tizen-only) | [](https://pub.dev/packages/tizen_notification) | N/A |
| [**tizen_package_manager**](packages/tizen_package_manager) | (Tizen-only) | [](https://pub.dev/packages/tizen_package_manager) | N/A |
| [**tizen_rpc_port**](packages/tizen_rpc_port) | (Tizen-only) | [](https://pub.dev/packages/tizen_rpc_port) | N/A |
+| [**tizen_stt**](packages/tizen_stt) | (Tizen-only) | [](https://pub.dev/packages/tizen_stt) | N/A |
| [**tizen_window_manager**](packages/tizen_window_manager) | (Tizen-only) | [](https://pub.dev/packages/tizen_window_manager) | N/A |
| [**url_launcher_tizen**](packages/url_launcher) | [url_launcher](https://pub.dev/packages/url_launcher) (1st-party) | [](https://pub.dev/packages/url_launcher_tizen) | No |
| [**video_player_avplay**](packages/video_player_avplay) | (Tizen-only) | [](https://pub.dev/packages/video_player_avplay) | N/A |
@@ -85,6 +86,7 @@ The _"non-endorsed"_ status means that the plugin is not endorsed by the origina
| [**tizen_notification**](packages/tizen_notification) | ✔️ | ✔️ | ❌ | API not supported |
| [**tizen_package_manager**](packages/tizen_package_manager) | ✔️ | ✔️ | ✔️ |
| [**tizen_rpc_port**](packages/tizen_rpc_port) | ✔️ | ✔️ | ✔️ |
+| [**tizen_stt**](packages/tizen_stt) | ✔️ | ❌ | ❌ | Requires a supported microphone. |
| [**tizen_window_manager**](packages/tizen_window_manager) | ✔️ | ✔️ | ✔️ |
| [**url_launcher_tizen**](packages/url_launcher) | ✔️ | ❌ | ❌ | No browser app |
| [**video_player_avplay**](packages/video_player_avplay) | ✔️ | ❌ | ❌ | See README for details |
diff --git a/packages/tizen_stt/CHANGELOG.md b/packages/tizen_stt/CHANGELOG.md
new file mode 100644
index 000000000..75674c185
--- /dev/null
+++ b/packages/tizen_stt/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 0.1.0
+
+* Initial Tizen STT engine-recorder implementation.
diff --git a/packages/tizen_stt/LICENSE b/packages/tizen_stt/LICENSE
new file mode 100644
index 000000000..4b455a8ce
--- /dev/null
+++ b/packages/tizen_stt/LICENSE
@@ -0,0 +1,25 @@
+Copyright (c) 2026 Samsung Electronics Co., Ltd. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of the copyright holder nor the names of the
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/packages/tizen_stt/README.md b/packages/tizen_stt/README.md
new file mode 100644
index 000000000..7ddf9c352
--- /dev/null
+++ b/packages/tizen_stt/README.md
@@ -0,0 +1,42 @@
+# tizen_stt
+
+Speech-to-text for Samsung TVs using the Tizen STT engine.
+
+## Requirements
+
+Requires a supported microphone.
+
+Add the following to `tizen/tizen-manifest.xml`:
+
+```xml
+
+ http://tizen.org/privilege/recorder
+ http://tizen.org/privilege/internet
+
+
+
+```
+
+## Usage
+
+```dart
+import 'package:tizen_stt/tizen_stt.dart';
+
+final stt = TizenStt();
+final subscription = stt.events.listen((event) {
+ if (event.isPartialResult || event.isFinalResult) print(event.text);
+ if (event.isError) print(event.errorName ?? event.message);
+});
+
+await stt.initialize();
+await stt.startListening();
+// After the user speaks:
+await stt.stopListening();
+// After receiving the result, when finished:
+await stt.dispose();
+await subscription.cancel();
+```
+
+Use `getLanguages()` to select a language and `cancel()` to discard recognition.
+All instances share one session. Handle `PlatformException` from control calls.
+See the [example](example) for a complete app.
diff --git a/packages/tizen_stt/example/README.md b/packages/tizen_stt/example/README.md
new file mode 100644
index 000000000..0e69a4ca7
--- /dev/null
+++ b/packages/tizen_stt/example/README.md
@@ -0,0 +1,9 @@
+# tizen_stt_example
+
+[](https://pub.dev/packages/tizen_stt)
+
+Demonstrates how to use the tizen_stt plugin.
+
+## Getting Started
+
+To run this app on your Tizen device, use [flutter-tizen](https://github.com/flutter-tizen/flutter-tizen).
diff --git a/packages/tizen_stt/example/integration_test/plugin_integration_test.dart b/packages/tizen_stt/example/integration_test/plugin_integration_test.dart
new file mode 100644
index 000000000..5ebd9f0c2
--- /dev/null
+++ b/packages/tizen_stt/example/integration_test/plugin_integration_test.dart
@@ -0,0 +1,51 @@
+// Copyright 2026 Samsung Electronics Co., Ltd. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+import 'package:tizen_stt/tizen_stt.dart';
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+ final TizenStt stt = TizenStt();
+
+ setUp(() async {
+ expect(await stt.initialize(), TizenSttState.ready);
+ });
+
+ tearDown(() async {
+ await stt.dispose();
+ });
+
+ Future expectTransition(
+ Future Function() action,
+ TizenSttState state,
+ ) async {
+ final Future event = stt.events
+ .timeout(const Duration(seconds: 10))
+ .firstWhere(
+ (TizenSttEvent event) => event.isError || event.state == state)
+ .then((TizenSttEvent event) =>
+ expect(event.state, state, reason: event.raw.toString()));
+ await Future.wait(>[event, action()]);
+ }
+
+ test('languages and session disposal', () async {
+ expect(await stt.getLanguages(), isNotEmpty);
+ expect(await stt.getState(), TizenSttState.ready);
+ await stt.dispose();
+ expect(await stt.getState(), TizenSttState.notCreated);
+ });
+
+ test('recording can be cancelled', () async {
+ await expectTransition(stt.startListening, TizenSttState.recording);
+ await expectTransition(stt.cancel, TizenSttState.ready);
+ expect(await stt.getState(), TizenSttState.ready);
+ });
+
+ test('stopping recording starts processing', () async {
+ await expectTransition(stt.startListening, TizenSttState.recording);
+ await expectTransition(stt.stopListening, TizenSttState.processing);
+ });
+}
diff --git a/packages/tizen_stt/example/lib/main.dart b/packages/tizen_stt/example/lib/main.dart
new file mode 100644
index 000000000..1160c7663
--- /dev/null
+++ b/packages/tizen_stt/example/lib/main.dart
@@ -0,0 +1,209 @@
+// Copyright 2026 Samsung Electronics Co., Ltd. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:tizen_stt/tizen_stt.dart';
+
+void main() => runApp(const MyApp());
+
+/// Minimal STT test application.
+class MyApp extends StatelessWidget {
+ /// Creates the example application.
+ const MyApp({super.key});
+
+ @override
+ Widget build(BuildContext context) => const MaterialApp(home: SttPage());
+}
+
+/// Exercises the native session lifecycle.
+class SttPage extends StatefulWidget {
+ /// Creates the test page.
+ const SttPage({super.key});
+
+ @override
+ State createState() => _SttPageState();
+}
+
+class _SttPageState extends State with WidgetsBindingObserver {
+ final TizenStt _stt = TizenStt();
+ late final StreamSubscription _subscription;
+ TizenSttState _state = TizenSttState.notCreated;
+ bool _busy = false;
+ bool _partial = false;
+ List _languages = [];
+ String? _language;
+ String _text = '';
+ String _error = '';
+
+ @override
+ void initState() {
+ super.initState();
+ WidgetsBinding.instance.addObserver(this);
+ _subscription = _stt.events.listen((TizenSttEvent event) {
+ if (!mounted) {
+ return;
+ }
+ setState(() {
+ if (event.isStateChange) {
+ _state = event.state!;
+ }
+ if (event.isPartialResult || event.isFinalResult) {
+ _text = event.text ?? '';
+ }
+ if (event.isError) {
+ _error = event.errorName ?? event.message ?? 'Recognition failed';
+ }
+ });
+ }, onError: (Object error) {
+ if (mounted) {
+ setState(() => _error = error.toString());
+ }
+ });
+ }
+
+ Future _run(Future Function() action) async {
+ if (_busy) {
+ return;
+ }
+ setState(() {
+ _busy = true;
+ _error = '';
+ });
+ try {
+ await action();
+ } on PlatformException catch (error) {
+ if (mounted) {
+ setState(() => _error = '${error.code}: ${error.message ?? ''}');
+ }
+ } finally {
+ if (mounted) {
+ setState(() => _busy = false);
+ }
+ }
+ }
+
+ Future _refresh() async {
+ final TizenSttState state = await _stt.getState();
+ if (mounted) {
+ setState(() => _state = state);
+ }
+ }
+
+ Future _initialize() async {
+ await _stt.initialize();
+ final List languages = await _stt.getLanguages();
+ if (mounted) {
+ setState(() {
+ _languages = languages;
+ _language = null;
+ });
+ }
+ await _refresh();
+ }
+
+ Future _record() async {
+ if (_state == TizenSttState.recording) {
+ await _stt.stopListening();
+ } else {
+ setState(() => _text = '');
+ await _stt.startListening(
+ language: _language,
+ recognitionType: _partial
+ ? TizenSttRecognitionType.freePartial
+ : TizenSttRecognitionType.free);
+ }
+ await _refresh();
+ }
+
+ @override
+ void didChangeAppLifecycleState(AppLifecycleState state) {
+ if (state == AppLifecycleState.paused) {
+ // Release the microphone even if a command is in flight.
+ unawaited(
+ _stt.dispose().then((_) => _refresh()).catchError((Object error) {
+ if (mounted) {
+ setState(() => _error = error.toString());
+ }
+ }));
+ }
+ }
+
+ @override
+ void dispose() {
+ WidgetsBinding.instance.removeObserver(this);
+ unawaited(_subscription.cancel());
+ unawaited(
+ _stt.dispose().catchError((Object error) => debugPrint('$error')));
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) => Scaffold(
+ appBar: AppBar(title: const Text('Tizen STT')),
+ body: ListView(padding: const EdgeInsets.all(24), children: [
+ Text('State: ${_state.name}', key: const Key('state')),
+ if (_busy) const Text('Working…'),
+ if (_error.isNotEmpty) Text(_error, key: const Key('error')),
+ Wrap(spacing: 12, children: [
+ ElevatedButton(
+ autofocus: true,
+ onPressed: !_busy &&
+ (_state == TizenSttState.notCreated ||
+ _state == TizenSttState.created)
+ ? () => _run(_initialize)
+ : null,
+ child: const Text('Initialize')),
+ ElevatedButton(
+ key: const Key('record_button'),
+ onPressed: !_busy &&
+ (_state == TizenSttState.ready ||
+ _state == TizenSttState.recording)
+ ? () => _run(_record)
+ : null,
+ child: Text(
+ _state == TizenSttState.recording ? 'Stop' : 'Record')),
+ ElevatedButton(
+ onPressed: !_busy &&
+ (_state == TizenSttState.recording ||
+ _state == TizenSttState.processing)
+ ? () => _run(() async {
+ await _stt.cancel();
+ await _refresh();
+ })
+ : null,
+ child: const Text('Cancel')),
+ ElevatedButton(
+ onPressed: !_busy && _state != TizenSttState.notCreated
+ ? () => _run(() async {
+ await _stt.dispose();
+ await _refresh();
+ })
+ : null,
+ child: const Text('Dispose')),
+ ]),
+ DropdownButton(
+ value: _language,
+ hint: const Text('Automatic language'),
+ items: _languages
+ .map((String language) => DropdownMenuItem(
+ value: language, child: Text(language)))
+ .toList(),
+ onChanged: !_busy && _state == TizenSttState.ready
+ ? (String? value) => setState(() => _language = value)
+ : null),
+ CheckboxListTile(
+ title: const Text('Partial results (if supported)'),
+ value: _partial,
+ onChanged: !_busy && _state == TizenSttState.ready
+ ? (bool? value) => setState(() => _partial = value!)
+ : null),
+ const SizedBox(height: 24),
+ SelectableText(
+ _text.isEmpty ? 'Recognized text appears here.' : _text),
+ ]),
+ );
+}
diff --git a/packages/tizen_stt/example/pubspec.yaml b/packages/tizen_stt/example/pubspec.yaml
new file mode 100644
index 000000000..de7349079
--- /dev/null
+++ b/packages/tizen_stt/example/pubspec.yaml
@@ -0,0 +1,24 @@
+name: tizen_stt_example
+description: Demonstrates how to use the tizen_stt plugin.
+publish_to: none
+
+environment:
+ sdk: ">=3.1.0 <4.0.0"
+
+dependencies:
+ flutter:
+ sdk: flutter
+ tizen_stt:
+ path: ../
+
+dev_dependencies:
+ flutter_driver:
+ sdk: flutter
+ flutter_test:
+ sdk: flutter
+ integration_test:
+ sdk: flutter
+ integration_test_tizen: ^2.0.1
+
+flutter:
+ uses-material-design: true
diff --git a/packages/tizen_stt/example/test_driver/integration_test.dart b/packages/tizen_stt/example/test_driver/integration_test.dart
new file mode 100644
index 000000000..63206eef5
--- /dev/null
+++ b/packages/tizen_stt/example/test_driver/integration_test.dart
@@ -0,0 +1,7 @@
+// Copyright 2026 Samsung Electronics Co., Ltd. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:integration_test/integration_test_driver.dart';
+
+Future main() => integrationDriver();
diff --git a/packages/tizen_stt/example/tizen/.gitignore b/packages/tizen_stt/example/tizen/.gitignore
new file mode 100644
index 000000000..ac0738be2
--- /dev/null
+++ b/packages/tizen_stt/example/tizen/.gitignore
@@ -0,0 +1,12 @@
+flutter/
+.vs/
+*.user
+bin/
+obj/
+
+# Tizen Core CLI (tz) related files
+tizen_dotnet_project.yaml
+*.csproj.backup
+
+# Flutter-tizen dependency information file
+.app.deps.json
diff --git a/packages/tizen_stt/example/tizen/App.cs b/packages/tizen_stt/example/tizen/App.cs
new file mode 100644
index 000000000..6dd4a6356
--- /dev/null
+++ b/packages/tizen_stt/example/tizen/App.cs
@@ -0,0 +1,20 @@
+using Tizen.Flutter.Embedding;
+
+namespace Runner
+{
+ public class App : FlutterApplication
+ {
+ protected override void OnCreate()
+ {
+ base.OnCreate();
+
+ GeneratedPluginRegistrant.RegisterPlugins(this);
+ }
+
+ static void Main(string[] args)
+ {
+ var app = new App();
+ app.Run(args);
+ }
+ }
+}
diff --git a/packages/tizen_stt/example/tizen/Runner.csproj b/packages/tizen_stt/example/tizen/Runner.csproj
new file mode 100644
index 000000000..e6b2d1c31
--- /dev/null
+++ b/packages/tizen_stt/example/tizen/Runner.csproj
@@ -0,0 +1,19 @@
+
+
+
+ Exe
+ tizen60
+
+
+
+
+
+
+
+
+
+ %(RecursiveDir)
+
+
+
+
diff --git a/packages/tizen_stt/example/tizen/shared/res/ic_launcher.png b/packages/tizen_stt/example/tizen/shared/res/ic_launcher.png
new file mode 100644
index 000000000..4d6372eeb
Binary files /dev/null and b/packages/tizen_stt/example/tizen/shared/res/ic_launcher.png differ
diff --git a/packages/tizen_stt/example/tizen/tizen-manifest.xml b/packages/tizen_stt/example/tizen/tizen-manifest.xml
new file mode 100644
index 000000000..f4f78526d
--- /dev/null
+++ b/packages/tizen_stt/example/tizen/tizen-manifest.xml
@@ -0,0 +1,17 @@
+
+
+
+
+ tizen_stt_example
+ ic_launcher.png
+
+
+
+
+ http://tizen.org/privilege/recorder
+ http://tizen.org/privilege/internet
+
+
+
+
+
diff --git a/packages/tizen_stt/lib/tizen_stt.dart b/packages/tizen_stt/lib/tizen_stt.dart
new file mode 100644
index 000000000..fb787e8f1
--- /dev/null
+++ b/packages/tizen_stt/lib/tizen_stt.dart
@@ -0,0 +1,78 @@
+// Copyright 2026 Samsung Electronics Co., Ltd. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:flutter/services.dart';
+
+import 'tizen_stt_event.dart';
+import 'tizen_stt_state.dart';
+
+export 'tizen_stt_event.dart';
+export 'tizen_stt_state.dart';
+
+/// Speech recognition through the Tizen STT service.
+///
+/// All instances share one native session. Subscribe to [events] before use,
+/// and call [dispose] when finished. This plugin uses the engine's recorder;
+/// microphone routing and recognition support depend on the installed engine.
+class TizenStt {
+ static const MethodChannel _channel = MethodChannel('tizen_stt');
+ static const EventChannel _eventChannel = EventChannel('tizen_stt/events');
+ static Stream? _events;
+
+ /// Broadcast stream of state changes, partial/final results and errors.
+ Stream get events =>
+ _events ??= _eventChannel.receiveBroadcastStream().map((dynamic event) =>
+ TizenSttEvent.fromMap(event as Map));
+
+ /// Creates the session and waits for READY, or throws [PlatformException].
+ ///
+ /// Preparation times out after 10 seconds. Call once before starting;
+ /// concurrent initialization requests are rejected. If already prepared,
+ /// returns the current state without interrupting recognition.
+ Future initialize() => _invokeState('initialize');
+
+ /// Starts recording after [initialize] has completed.
+ ///
+ /// A null [language] uses the engine's automatic language selection.
+ /// Otherwise use a value returned by [getLanguages], such as `en_US`.
+ /// Recognition types and partial results depend on the installed engine.
+ /// The returned state is a snapshot; use [events] for later transitions.
+ Future startListening({
+ String? language,
+ String recognitionType = TizenSttRecognitionType.free,
+ }) =>
+ _invokeState('start', {
+ 'language': language,
+ 'type': recognitionType,
+ });
+
+ /// Stops recording and requests a final result (or error) through [events].
+ Future stopListening() => _invokeState('stop');
+
+ /// Discards recognition. Cancelling preparation releases the session.
+ /// Does nothing when idle or already disposed.
+ Future cancel() => _invokeState('cancel');
+
+ /// Returns the current session state without creating a session.
+ Future getState() => _invokeState('getState');
+
+ /// Lists languages supported by the engine after [initialize] completes.
+ Future> getLanguages() async =>
+ (await _channel.invokeListMethod('getLanguages'))!;
+
+ /// Cancels pending work and releases the shared native session.
+ /// Safe to call repeatedly. Call [initialize] to use the session again.
+ Future dispose() => _channel.invokeMethod('dispose');
+
+ Future _invokeState(
+ String method, [
+ Map? arguments,
+ ]) async {
+ final Map? result =
+ await _channel.invokeMapMethod(method, arguments);
+ return TizenSttState.fromStatus(result!);
+ }
+}
diff --git a/packages/tizen_stt/lib/tizen_stt_event.dart b/packages/tizen_stt/lib/tizen_stt_event.dart
new file mode 100644
index 000000000..591101f7b
--- /dev/null
+++ b/packages/tizen_stt/lib/tizen_stt_event.dart
@@ -0,0 +1,107 @@
+// Copyright 2026 Samsung Electronics Co., Ltd. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'tizen_stt_state.dart';
+
+/// Recognition type passed to `TizenStt.startListening`, matching Tizen's
+/// `STT_RECOGNITION_TYPE_*` string constants.
+///
+/// Which types an engine supports varies; [TizenSttRecognitionType.free]
+/// (free-form dictation) is the common default.
+class TizenSttRecognitionType {
+ const TizenSttRecognitionType._();
+
+ /// Free-form dictation (`STT_RECOGNITION_TYPE_FREE`).
+ static const String free = 'stt.recognition.type.FREE';
+
+ /// Free-form dictation with partial results (`...FREE.PARTIAL`).
+ static const String freePartial = 'stt.recognition.type.FREE.PARTIAL';
+
+ /// Short search phrases (`STT_RECOGNITION_TYPE_SEARCH`).
+ static const String search = 'stt.recognition.type.SEARCH';
+
+ /// Web search phrases (`STT_RECOGNITION_TYPE_WEB_SEARCH`).
+ static const String webSearch = 'stt.recognition.type.WEB_SEARCH';
+
+ /// Map/navigation phrases (`STT_RECOGNITION_TYPE_MAP`).
+ static const String map = 'stt.recognition.type.MAP';
+}
+
+/// An event emitted on `TizenStt.events`.
+///
+/// One of three [type]s: `state` (engine state changed), `result` (recognition
+/// output — partial, final or error), or `error`. The original platform payload is always
+/// available via [raw]; the typed getters below cover the common cases.
+class TizenSttEvent {
+ /// Creates an event with its original native payload.
+ const TizenSttEvent({
+ required this.type,
+ required this.raw,
+ this.text,
+ this.message,
+ });
+
+ /// Builds an event from the raw platform map sent over the event channel.
+ factory TizenSttEvent.fromMap(Map map) {
+ return TizenSttEvent(
+ type: map['type'] as String? ?? 'unknown',
+ raw: Map.from(map),
+ text: map['text'] as String?,
+ message: map['message'] as String?,
+ );
+ }
+
+ /// Event category: `state`, `result`, or `error`.
+ final String type;
+
+ /// Recognized text, for `result` events.
+ final String? text;
+
+ /// Human-readable recognition message, if provided by the engine.
+ final String? message;
+
+ /// The unmodified platform payload, for fields not exposed as getters.
+ final Map raw;
+
+ /// Whether this is a `state` transition event.
+ bool get isStateChange => type == 'state';
+
+ /// The new engine state, for `state` events; otherwise `null`.
+ TizenSttState? get state {
+ if (!isStateChange) {
+ return null;
+ }
+ final Object? current = raw['current'];
+ if (current is Map) {
+ return TizenSttState.fromName(current['name'] as String?);
+ }
+ return null;
+ }
+
+ /// Whether this is a final recognition result.
+ bool get isFinalResult => type == 'result' && raw['event'] == 'final';
+
+ /// Whether this is a partial (in-progress) recognition result.
+ bool get isPartialResult => type == 'result' && raw['event'] == 'partial';
+
+ /// Whether this is an error event.
+ bool get isError =>
+ type == 'error' || (type == 'result' && raw['event'] == 'error');
+
+ /// The Tizen error name (e.g. `STT_ERROR_OPERATION_FAILED`), for `error`
+ /// events; otherwise `null`.
+ String? get errorName => isError ? raw['name'] as String? : null;
+
+ /// Best text to show for this event: recognized [text], else [message],
+ /// else the raw payload.
+ String get displayText {
+ if (text != null && text!.isNotEmpty) {
+ return text!;
+ }
+ if (message != null && message!.isNotEmpty) {
+ return message!;
+ }
+ return raw.toString();
+ }
+}
diff --git a/packages/tizen_stt/lib/tizen_stt_state.dart b/packages/tizen_stt/lib/tizen_stt_state.dart
new file mode 100644
index 000000000..fe2376b24
--- /dev/null
+++ b/packages/tizen_stt/lib/tizen_stt_state.dart
@@ -0,0 +1,57 @@
+// Copyright 2026 Samsung Electronics Co., Ltd. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// Lifecycle state of the STT engine, mirroring Tizen's `stt_state_e`.
+///
+/// Transitions are driven by the engine and surfaced both as the return value
+/// of the control methods on `TizenStt` and as `state` events on
+/// `TizenStt.events`.
+enum TizenSttState {
+ /// No engine handle exists yet, or it was released by `dispose()`.
+ notCreated,
+
+ /// Handle created but not prepared yet (`STT_STATE_CREATED`).
+ created,
+
+ /// Prepared and idle — ready to start listening (`STT_STATE_READY`).
+ ready,
+
+ /// Capturing audio (`STT_STATE_RECORDING`).
+ recording,
+
+ /// Recognizing the captured audio (`STT_STATE_PROCESSING`).
+ processing,
+
+ /// The state could not be determined.
+ unknown;
+
+ /// Maps a state name (as emitted by the platform) to a [TizenSttState].
+ static TizenSttState fromName(String? name) {
+ switch (name) {
+ case 'notCreated':
+ return TizenSttState.notCreated;
+ case 'created':
+ return TizenSttState.created;
+ case 'ready':
+ return TizenSttState.ready;
+ case 'recording':
+ return TizenSttState.recording;
+ case 'processing':
+ return TizenSttState.processing;
+ default:
+ return TizenSttState.unknown;
+ }
+ }
+
+ /// Extracts the state from a platform status map shaped like
+ /// `{ "state": { "code": int, "name": String } }`. Also accepts a bare
+ /// `{ "name": String }` map (as carried by `state` events).
+ static TizenSttState fromStatus(Map status) {
+ final Object? state = status['state'];
+ if (state is Map) {
+ return fromName(state['name'] as String?);
+ }
+ return fromName(status['name'] as String?);
+ }
+}
diff --git a/packages/tizen_stt/pubspec.yaml b/packages/tizen_stt/pubspec.yaml
new file mode 100644
index 000000000..45bbde8a0
--- /dev/null
+++ b/packages/tizen_stt/pubspec.yaml
@@ -0,0 +1,24 @@
+name: tizen_stt
+description: A Flutter plugin for speech recognition using the Tizen STT service.
+version: 0.1.0
+homepage: https://github.com/flutter-tizen/plugins
+repository: https://github.com/flutter-tizen/plugins/tree/main/packages/tizen_stt
+
+environment:
+ sdk: ">=3.1.0 <4.0.0"
+ flutter: ">=3.13.0"
+
+dependencies:
+ flutter:
+ sdk: flutter
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+flutter:
+ plugin:
+ platforms:
+ tizen:
+ pluginClass: TizenSttPlugin
+ fileName: tizen_stt_plugin.h
diff --git a/packages/tizen_stt/test/tizen_stt_method_channel_test.dart b/packages/tizen_stt/test/tizen_stt_method_channel_test.dart
new file mode 100644
index 000000000..cdb4d8044
--- /dev/null
+++ b/packages/tizen_stt/test/tizen_stt_method_channel_test.dart
@@ -0,0 +1,137 @@
+// Copyright 2026 Samsung Electronics Co., Ltd. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:tizen_stt/tizen_stt.dart';
+
+void main() {
+ TestWidgetsFlutterBinding.ensureInitialized();
+ final TestDefaultBinaryMessenger messenger =
+ TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
+ const MethodChannel channel = MethodChannel('tizen_stt');
+ const MethodChannel eventChannel = MethodChannel('tizen_stt/events');
+ final TizenStt stt = TizenStt();
+ final List calls = [];
+
+ setUp(() {
+ calls.clear();
+ messenger.setMockMethodCallHandler(channel, (MethodCall call) async {
+ calls.add(call);
+ if (call.method == 'getLanguages') {
+ return ['en_US', 'ko_KR'];
+ }
+ if (call.method == 'dispose') {
+ return null;
+ }
+ return {
+ 'state': {'name': 'ready'}
+ };
+ });
+ messenger.setMockMethodCallHandler(
+ eventChannel, (MethodCall call) async => null);
+ });
+ tearDown(() {
+ messenger.setMockMethodCallHandler(channel, null);
+ messenger.setMockMethodCallHandler(eventChannel, null);
+ });
+
+ test('control calls forward arguments and decode state and languages',
+ () async {
+ expect(await stt.initialize(), TizenSttState.ready);
+ expect(
+ await stt.startListening(
+ language: 'ko_KR',
+ recognitionType: TizenSttRecognitionType.freePartial),
+ TizenSttState.ready);
+ expect(calls.last.arguments, {
+ 'language': 'ko_KR',
+ 'type': TizenSttRecognitionType.freePartial
+ });
+ await stt.startListening();
+ expect(calls.last.arguments, {
+ 'language': null,
+ 'type': TizenSttRecognitionType.free
+ });
+ await stt.stopListening();
+ await stt.cancel();
+ await stt.getState();
+ expect(await stt.getLanguages(), ['en_US', 'ko_KR']);
+ await stt.dispose();
+ expect(calls.map((MethodCall call) => call.method), [
+ 'initialize',
+ 'start',
+ 'start',
+ 'stop',
+ 'cancel',
+ 'getState',
+ 'getLanguages',
+ 'dispose'
+ ]);
+ });
+
+ test('initialization stays pending and propagates native failure', () async {
+ final Completer ready = Completer();
+ messenger.setMockMethodCallHandler(
+ channel, (MethodCall call) => ready.future);
+ bool completed = false;
+ final Future initializing =
+ stt.initialize().then((TizenSttState state) {
+ completed = true;
+ return state;
+ });
+ await Future.delayed(Duration.zero);
+ expect(completed, isFalse);
+ final Future expectation = expectLater(
+ initializing,
+ throwsA(isA().having((PlatformException e) => e.code,
+ 'code', 'STT_ERROR_ENGINE_NOT_FOUND')));
+ ready.completeError(PlatformException(code: 'STT_ERROR_ENGINE_NOT_FOUND'));
+ await expectation;
+ });
+
+ test('shared event stream decodes state, results and both error forms',
+ () async {
+ final List events = [];
+ final StreamSubscription first =
+ stt.events.listen(events.add);
+ final List otherEvents = [];
+ final StreamSubscription second =
+ TizenStt().events.listen(otherEvents.add);
+ await Future.delayed(Duration.zero);
+ for (final Map event in >[
+ {
+ 'type': 'state',
+ 'current': {'name': 'ready'}
+ },
+ {'type': 'result', 'event': 'partial', 'text': 'hello'},
+ {
+ 'type': 'result',
+ 'event': 'final',
+ 'text': 'hello world'
+ },
+ {
+ 'type': 'result',
+ 'event': 'error',
+ 'message': 'no match'
+ },
+ {'type': 'error', 'name': 'STT_ERROR_NO_SPEECH'},
+ ]) {
+ final ByteData data =
+ const StandardMethodCodec().encodeSuccessEnvelope(event);
+ await messenger.handlePlatformMessage('tizen_stt/events', data, (_) {});
+ }
+ expect(events.first.state, TizenSttState.ready);
+ expect(events[1].isPartialResult, isTrue);
+ expect(events[2].isFinalResult, isTrue);
+ expect(events[2].text, 'hello world');
+ expect(events[3].isError, isTrue);
+ expect(events[4].errorName, 'STT_ERROR_NO_SPEECH');
+ expect(otherEvents.length, events.length);
+ await first.cancel();
+ await second.cancel();
+ });
+}
diff --git a/packages/tizen_stt/tizen/.gitignore b/packages/tizen_stt/tizen/.gitignore
new file mode 100644
index 000000000..a2a7d62b1
--- /dev/null
+++ b/packages/tizen_stt/tizen/.gitignore
@@ -0,0 +1,5 @@
+.cproject
+.sign
+crash-info/
+Debug/
+Release/
diff --git a/packages/tizen_stt/tizen/inc/tizen_stt_plugin.h b/packages/tizen_stt/tizen/inc/tizen_stt_plugin.h
new file mode 100644
index 000000000..52cea5bef
--- /dev/null
+++ b/packages/tizen_stt/tizen/inc/tizen_stt_plugin.h
@@ -0,0 +1,27 @@
+// Copyright 2026 Samsung Electronics Co., Ltd. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef FLUTTER_PLUGIN_TIZEN_STT_PLUGIN_H_
+#define FLUTTER_PLUGIN_TIZEN_STT_PLUGIN_H_
+
+#include
+
+#ifdef FLUTTER_PLUGIN_IMPL
+#define FLUTTER_PLUGIN_EXPORT __attribute__((visibility("default")))
+#else
+#define FLUTTER_PLUGIN_EXPORT
+#endif
+
+#if defined(__cplusplus)
+extern "C" {
+#endif
+
+FLUTTER_PLUGIN_EXPORT void TizenSttPluginRegisterWithRegistrar(
+ FlutterDesktopPluginRegistrarRef registrar);
+
+#if defined(__cplusplus)
+} // extern "C"
+#endif
+
+#endif // FLUTTER_PLUGIN_TIZEN_STT_PLUGIN_H_
diff --git a/packages/tizen_stt/tizen/project_def.prop b/packages/tizen_stt/tizen/project_def.prop
new file mode 100644
index 000000000..45337128d
--- /dev/null
+++ b/packages/tizen_stt/tizen/project_def.prop
@@ -0,0 +1,23 @@
+# See https://docs.tizen.org/application/tizen-studio/native-tools/project-conversion
+# for details.
+
+APPNAME = tizen_stt_plugin
+type = staticLib
+profile = common-6.0
+
+# Source files
+USER_SRCS += src/*.cc
+
+# User defines
+USER_DEFS =
+USER_UNDEFS =
+USER_CPP_DEFS = FLUTTER_PLUGIN_IMPL
+USER_CPP_UNDEFS =
+
+# User includes
+USER_INC_DIRS = inc src
+USER_INC_FILES =
+USER_CPP_INC_FILES =
+
+# Tizen Native API dependencies
+USER_PKGS = stt glib-2.0
diff --git a/packages/tizen_stt/tizen/src/tizen_stt_plugin.cc b/packages/tizen_stt/tizen/src/tizen_stt_plugin.cc
new file mode 100644
index 000000000..a5cbb4afe
--- /dev/null
+++ b/packages/tizen_stt/tizen/src/tizen_stt_plugin.cc
@@ -0,0 +1,492 @@
+// Copyright 2026 Samsung Electronics Co., Ltd. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "tizen_stt_plugin.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+namespace {
+
+using EncodableValue = flutter::EncodableValue;
+using EncodableMap = flutter::EncodableMap;
+using EncodableList = flutter::EncodableList;
+using EventSink = flutter::EventSink;
+using StreamHandlerError = flutter::StreamHandlerError;
+
+constexpr char kMethodChannelName[] = "tizen_stt";
+constexpr char kEventChannelName[] = "tizen_stt/events";
+
+std::string ErrorName(int error) {
+ switch (error) {
+ case STT_ERROR_NONE:
+ return "STT_ERROR_NONE";
+ case STT_ERROR_OUT_OF_MEMORY:
+ return "STT_ERROR_OUT_OF_MEMORY";
+ case STT_ERROR_IO_ERROR:
+ return "STT_ERROR_IO_ERROR";
+ case STT_ERROR_INVALID_PARAMETER:
+ return "STT_ERROR_INVALID_PARAMETER";
+ case STT_ERROR_TIMED_OUT:
+ return "STT_ERROR_TIMED_OUT";
+ case STT_ERROR_RECORDER_BUSY:
+ return "STT_ERROR_RECORDER_BUSY";
+ case STT_ERROR_OUT_OF_NETWORK:
+ return "STT_ERROR_OUT_OF_NETWORK";
+ case STT_ERROR_PERMISSION_DENIED:
+ return "STT_ERROR_PERMISSION_DENIED";
+ case STT_ERROR_NOT_SUPPORTED:
+ return "STT_ERROR_NOT_SUPPORTED";
+ case STT_ERROR_INVALID_STATE:
+ return "STT_ERROR_INVALID_STATE";
+ case STT_ERROR_INVALID_LANGUAGE:
+ return "STT_ERROR_INVALID_LANGUAGE";
+ case STT_ERROR_ENGINE_NOT_FOUND:
+ return "STT_ERROR_ENGINE_NOT_FOUND";
+ case STT_ERROR_OPERATION_FAILED:
+ return "STT_ERROR_OPERATION_FAILED";
+ case STT_ERROR_NOT_SUPPORTED_FEATURE:
+ return "STT_ERROR_NOT_SUPPORTED_FEATURE";
+ case STT_ERROR_RECORDING_TIMED_OUT:
+ return "STT_ERROR_RECORDING_TIMED_OUT";
+ case STT_ERROR_NO_SPEECH:
+ return "STT_ERROR_NO_SPEECH";
+ case STT_ERROR_IN_PROGRESS_TO_READY:
+ return "STT_ERROR_IN_PROGRESS_TO_READY";
+ case STT_ERROR_IN_PROGRESS_TO_RECORDING:
+ return "STT_ERROR_IN_PROGRESS_TO_RECORDING";
+ case STT_ERROR_IN_PROGRESS_TO_PROCESSING:
+ return "STT_ERROR_IN_PROGRESS_TO_PROCESSING";
+ case STT_ERROR_SERVICE_RESET:
+ return "STT_ERROR_SERVICE_RESET";
+ default:
+ return "STT_ERROR_" + std::to_string(error);
+ }
+}
+
+std::string StateName(stt_state_e state) {
+ switch (state) {
+ case STT_STATE_CREATED:
+ return "created";
+ case STT_STATE_READY:
+ return "ready";
+ case STT_STATE_RECORDING:
+ return "recording";
+ case STT_STATE_PROCESSING:
+ return "processing";
+ default:
+ return "unknown";
+ }
+}
+
+std::string ResultEventName(stt_result_event_e event) {
+ switch (event) {
+ case STT_RESULT_EVENT_FINAL_RESULT:
+ return "final";
+ case STT_RESULT_EVENT_PARTIAL_RESULT:
+ return "partial";
+ case STT_RESULT_EVENT_ERROR:
+ return "error";
+ default:
+ return "unknown";
+ }
+}
+
+EncodableMap ErrorMap(int error, const std::string& operation) {
+ return EncodableMap{
+ {EncodableValue("operation"), EncodableValue(operation)},
+ {EncodableValue("code"), EncodableValue(error)},
+ {EncodableValue("name"), EncodableValue(ErrorName(error))},
+ };
+}
+
+EncodableMap StateMap(stt_state_e state) {
+ return EncodableMap{
+ {EncodableValue("code"), EncodableValue(static_cast(state))},
+ {EncodableValue("name"), EncodableValue(StateName(state))},
+ };
+}
+
+bool CollectLanguage(stt_h stt, const char* language, void* user_data) {
+ auto* languages = static_cast(user_data);
+ languages->push_back(EncodableValue(language ? language : ""));
+ return true;
+}
+
+class TizenSttPlugin : public flutter::Plugin {
+ public:
+ static void RegisterWithRegistrar(flutter::PluginRegistrar* registrar) {
+ auto plugin = std::make_unique();
+ auto* plugin_pointer = plugin.get();
+
+ auto method_channel =
+ std::make_unique>(
+ registrar->messenger(), kMethodChannelName,
+ &flutter::StandardMethodCodec::GetInstance());
+ method_channel->SetMethodCallHandler(
+ [plugin_pointer](const auto& call, auto result) {
+ plugin_pointer->HandleMethodCall(call, std::move(result));
+ });
+ plugin->method_channel_ = std::move(method_channel);
+
+ auto event_channel =
+ std::make_unique>(
+ registrar->messenger(), kEventChannelName,
+ &flutter::StandardMethodCodec::GetInstance());
+ auto handler =
+ std::make_unique>(
+ [plugin_pointer](const EncodableValue* arguments,
+ std::unique_ptr&& events)
+ -> std::unique_ptr {
+ plugin_pointer->event_sink_ = std::move(events);
+ return nullptr;
+ },
+ [plugin_pointer](const EncodableValue* arguments)
+ -> std::unique_ptr {
+ plugin_pointer->event_sink_.reset();
+ return nullptr;
+ });
+ event_channel->SetStreamHandler(std::move(handler));
+ plugin->event_channel_ = std::move(event_channel);
+
+ registrar->AddPlugin(std::move(plugin));
+ }
+
+ ~TizenSttPlugin() override {
+ DisposeStt();
+ ClearEvents();
+ }
+
+ private:
+ void HandleMethodCall(
+ const flutter::MethodCall& method_call,
+ std::unique_ptr> result) {
+ const auto& method_name = method_call.method_name();
+ if (method_name == "initialize") {
+ if (initialize_result_) {
+ ReplyError(std::move(result), STT_ERROR_IN_PROGRESS_TO_READY,
+ method_name);
+ return;
+ }
+ int ret = Initialize();
+ stt_state_e state = STT_STATE_CREATED;
+ if (ret == STT_ERROR_NONE) ret = stt_get_state(stt_, &state);
+ if (ret != STT_ERROR_NONE) {
+ DisposeStt();
+ ReplyError(std::move(result), ret, method_name);
+ } else if (state == STT_STATE_CREATED) {
+ initialize_result_ = std::move(result);
+ initialize_timer_id_ = g_timeout_add(
+ 10000,
+ [](gpointer data) -> gboolean {
+ auto* self = static_cast(data);
+ self->initialize_timer_id_ = 0;
+ self->CompleteInitialize(STT_ERROR_TIMED_OUT);
+ self->DisposeStt();
+ return G_SOURCE_REMOVE;
+ },
+ this);
+ } else {
+ ReplyStatus(std::move(result));
+ }
+ return;
+ }
+ if (method_name == "getLanguages") {
+ EncodableList languages;
+ int ret = stt_ ? stt_foreach_supported_languages(stt_, CollectLanguage,
+ &languages)
+ : STT_ERROR_INVALID_STATE;
+ if (ret != STT_ERROR_NONE) {
+ ReplyError(std::move(result), ret, method_name);
+ } else {
+ result->Success(EncodableValue(languages));
+ }
+ return;
+ }
+ if (method_name == "start" || method_name == "stop" ||
+ method_name == "cancel") {
+ int ret = method_name == "start" ? Start(method_call.arguments())
+ : method_name == "stop" ? Stop()
+ : Cancel();
+ if (ret != STT_ERROR_NONE) {
+ ReplyError(std::move(result), ret, method_name);
+ } else {
+ ReplyStatus(std::move(result));
+ }
+ return;
+ }
+ if (method_name == "dispose") {
+ int ret = DisposeStt();
+ if (ret != STT_ERROR_NONE) {
+ ReplyError(std::move(result), ret, "stt_destroy");
+ return;
+ }
+ result->Success();
+ return;
+ }
+ if (method_name == "getState") {
+ ReplyStatus(std::move(result));
+ return;
+ }
+ result->NotImplemented();
+ }
+
+ int EnsureHandle() {
+ if (stt_) {
+ return STT_ERROR_NONE;
+ }
+
+ int ret = stt_create(&stt_);
+ if (ret != STT_ERROR_NONE) {
+ stt_ = nullptr;
+ return ret;
+ }
+
+ ret = stt_set_state_changed_cb(stt_, OnStateChanged, this);
+ if (ret == STT_ERROR_NONE)
+ ret = stt_set_recognition_result_cb(stt_, OnRecognitionResult, this);
+ if (ret == STT_ERROR_NONE) ret = stt_set_error_cb(stt_, OnError, this);
+ if (ret != STT_ERROR_NONE) return ret;
+ return STT_ERROR_NONE;
+ }
+
+ int Initialize() {
+ int ret = EnsureHandle();
+ if (ret != STT_ERROR_NONE) {
+ return ret;
+ }
+
+ stt_state_e state = STT_STATE_CREATED;
+ ret = stt_get_state(stt_, &state);
+ if (ret != STT_ERROR_NONE) {
+ return ret;
+ }
+ if (state == STT_STATE_CREATED) {
+ ret = stt_prepare(stt_);
+ if (ret != STT_ERROR_NONE) {
+ return ret;
+ }
+ }
+ return STT_ERROR_NONE;
+ }
+
+ void CompleteInitialize(int error) {
+ if (initialize_timer_id_) {
+ g_source_remove(initialize_timer_id_);
+ initialize_timer_id_ = 0;
+ }
+ if (!initialize_result_) return;
+ auto result = std::move(initialize_result_);
+ if (error == STT_ERROR_NONE) {
+ ReplyStatus(std::move(result));
+ } else {
+ ReplyError(std::move(result), error, "initialize");
+ }
+ }
+
+ int Start(const EncodableValue* arguments) {
+ if (!stt_) return STT_ERROR_INVALID_STATE;
+ if (!arguments || !std::holds_alternative(*arguments)) {
+ return STT_ERROR_INVALID_PARAMETER;
+ }
+ const auto& args = std::get(*arguments);
+ auto language = args.find(EncodableValue("language"));
+ auto type = args.find(EncodableValue("type"));
+ if (language == args.end() || type == args.end() ||
+ (!std::holds_alternative(language->second) &&
+ !std::holds_alternative(language->second)) ||
+ !std::holds_alternative(type->second)) {
+ return STT_ERROR_INVALID_PARAMETER;
+ }
+ const auto* language_value = std::get_if(&language->second);
+ const auto& type_value = std::get(type->second);
+ if ((language_value && (language_value->empty() ||
+ language_value->find('\0') != std::string::npos)) ||
+ type_value.empty() || type_value.find('\0') != std::string::npos) {
+ return STT_ERROR_INVALID_PARAMETER;
+ }
+ return stt_start(stt_, language_value ? language_value->c_str() : nullptr,
+ type_value.c_str());
+ }
+
+ int Stop() {
+ if (!stt_) {
+ return STT_ERROR_INVALID_STATE;
+ }
+ return stt_stop(stt_);
+ }
+
+ int Cancel() {
+ if (!stt_) return STT_ERROR_NONE;
+ if (initialize_result_) {
+ return DisposeStt();
+ }
+ stt_state_e state;
+ int ret = stt_get_state(stt_, &state);
+ if (ret != STT_ERROR_NONE) return ret;
+ if (state == STT_STATE_CREATED || state == STT_STATE_READY)
+ return STT_ERROR_NONE;
+ return stt_cancel(stt_);
+ }
+
+ int DisposeStt() {
+ CompleteInitialize(STT_ERROR_INVALID_STATE);
+ if (stt_) {
+ int ret = stt_destroy(stt_);
+ if (ret != STT_ERROR_NONE) return ret;
+ stt_ = nullptr;
+ }
+ ClearEvents();
+ return STT_ERROR_NONE;
+ }
+
+ void ReplyStatus(
+ std::unique_ptr> result) {
+ EncodableMap state_map{
+ {EncodableValue("code"), EncodableValue(-1)},
+ {EncodableValue("name"), EncodableValue("notCreated")},
+ };
+ if (stt_) {
+ stt_state_e state;
+ int ret = stt_get_state(stt_, &state);
+ if (ret != STT_ERROR_NONE) {
+ ReplyError(std::move(result), ret, "stt_get_state");
+ return;
+ }
+ state_map = StateMap(state);
+ }
+ result->Success(EncodableValue(EncodableMap{
+ {EncodableValue("state"), EncodableValue(state_map)},
+ }));
+ }
+
+ void ReplyError(
+ std::unique_ptr> result,
+ int error, const std::string& operation) {
+ result->Error(ErrorName(error), operation + " failed: " + ErrorName(error),
+ EncodableValue(ErrorMap(error, operation)));
+ }
+
+ void EmitError(int error, const std::string& operation) {
+ EncodableMap map = ErrorMap(error, operation);
+ map[EncodableValue("type")] = EncodableValue("error");
+ EmitEvent(map);
+ }
+
+ void EmitEvent(EncodableMap map) {
+ std::lock_guard lock(queue_mutex_);
+ event_queue_.push(EncodableValue(std::move(map)));
+ if (!event_source_id_) {
+ event_source_id_ = g_idle_add_full(
+ G_PRIORITY_DEFAULT,
+ [](gpointer data) -> gboolean {
+ static_cast(data)->DrainEvents();
+ return G_SOURCE_REMOVE;
+ },
+ this, nullptr);
+ }
+ }
+
+ void ClearEvents() {
+ std::lock_guard lock(queue_mutex_);
+ if (event_source_id_) {
+ g_source_remove(event_source_id_);
+ event_source_id_ = 0;
+ }
+ std::queue().swap(event_queue_);
+ }
+
+ void DrainEvents() {
+ std::queue pending;
+ {
+ std::lock_guard lock(queue_mutex_);
+ event_source_id_ = 0;
+ std::swap(pending, event_queue_);
+ }
+ while (!pending.empty()) {
+ const auto& map = std::get(pending.front());
+ const auto& type = std::get(map.at(EncodableValue("type")));
+ if (initialize_result_ && type == "state" &&
+ std::get(map.at(EncodableValue("current")))
+ .at(EncodableValue("name")) == EncodableValue("ready")) {
+ CompleteInitialize(STT_ERROR_NONE);
+ } else if (initialize_result_ && type == "error") {
+ CompleteInitialize(std::get(map.at(EncodableValue("code"))));
+ DisposeStt();
+ }
+ if (event_sink_) {
+ event_sink_->Success(pending.front());
+ }
+ pending.pop();
+ }
+ }
+
+ static void OnStateChanged(stt_h stt, stt_state_e previous,
+ stt_state_e current, void* user_data) {
+ auto* self = static_cast(user_data);
+ self->EmitEvent(EncodableMap{
+ {EncodableValue("type"), EncodableValue("state")},
+ {EncodableValue("previous"), EncodableValue(StateMap(previous))},
+ {EncodableValue("current"), EncodableValue(StateMap(current))},
+ });
+ }
+
+ static void OnRecognitionResult(stt_h stt, stt_result_event_e event,
+ const char** data, int data_count,
+ const char* msg, void* user_data) {
+ auto* self = static_cast(user_data);
+ EncodableList texts;
+ for (int i = 0; i < data_count; ++i) {
+ texts.push_back(EncodableValue(data && data[i] ? data[i] : ""));
+ }
+ std::string text;
+ if (!texts.empty() && std::holds_alternative(texts.front())) {
+ text = std::get(texts.front());
+ }
+ self->EmitEvent(EncodableMap{
+ {EncodableValue("type"), EncodableValue("result")},
+ {EncodableValue("event"), EncodableValue(ResultEventName(event))},
+ {EncodableValue("text"), EncodableValue(text)},
+ {EncodableValue("texts"), EncodableValue(texts)},
+ {EncodableValue("message"), EncodableValue(msg ? msg : "")},
+ });
+ }
+
+ static void OnError(stt_h stt, stt_error_e reason, void* user_data) {
+ auto* self = static_cast(user_data);
+ self->EmitError(reason, "stt_error_cb");
+ }
+
+ stt_h stt_ = nullptr;
+ guint initialize_timer_id_ = 0;
+ std::unique_ptr> initialize_result_;
+ guint event_source_id_ = 0;
+ std::mutex queue_mutex_;
+ std::queue event_queue_;
+ std::unique_ptr event_sink_;
+ std::unique_ptr> method_channel_;
+ std::unique_ptr> event_channel_;
+};
+
+} // namespace
+
+void TizenSttPluginRegisterWithRegistrar(
+ FlutterDesktopPluginRegistrarRef registrar) {
+ TizenSttPlugin::RegisterWithRegistrar(
+ flutter::PluginRegistrarManager::GetInstance()
+ ->GetRegistrar(registrar));
+}