From 03f0af21a81b87cb5961b64662499b709ffada04 Mon Sep 17 00:00:00 2001 From: Pedro Paulo de Amorim Date: Tue, 24 Feb 2026 23:39:28 +0000 Subject: [PATCH 01/21] chore: bump all package versions to 3.0.5 and update changelogs --- CHANGELOG.md | 17 + docs/architecture.md | 610 ++++++++-------- docs/code-quality.md | 656 +++++++++--------- docs/issues-and-improvements.md | 396 +++++------ opus_dart/analysis_options.yaml | 12 +- opus_dart/pubspec.yaml | 2 +- opus_flutter/CHANGELOG.md | 119 ++-- opus_flutter/pubspec.yaml | 2 +- opus_flutter_android/CHANGELOG.md | 22 +- opus_flutter_android/pubspec.yaml | 2 +- opus_flutter_ios/CHANGELOG.md | 36 +- opus_flutter_ios/pubspec.yaml | 2 +- opus_flutter_linux/CHANGELOG.md | 16 +- opus_flutter_linux/pubspec.yaml | 2 +- opus_flutter_macos/CHANGELOG.md | 20 +- opus_flutter_macos/pubspec.yaml | 2 +- opus_flutter_platform_interface/CHANGELOG.md | 16 +- .../analysis_options.yaml | 2 +- opus_flutter_platform_interface/pubspec.yaml | 2 +- opus_flutter_web/CHANGELOG.md | 28 +- opus_flutter_web/analysis_options.yaml | 2 +- opus_flutter_web/pubspec.yaml | 2 +- opus_flutter_windows/CHANGELOG.md | 16 +- opus_flutter_windows/pubspec.yaml | 2 +- 24 files changed, 1040 insertions(+), 946 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 123e597..74c6b5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 3.0.5 + +* Bump all package versions + +| Package | Version | +|---------|---------| +| opus_dart | 3.0.5 | +| opus_flutter | 3.0.5 | +| opus_flutter_android | 3.0.5 | +| opus_flutter_ios | 3.0.5 | +| opus_flutter_linux | 3.0.5 | +| opus_flutter_macos | 3.0.5 | +| opus_flutter_platform_interface | 3.0.5 | +| opus_flutter_web | 3.0.5 | +| opus_flutter_windows | 3.0.5 | + + ## 3.0.4 * Add Swift Package Manager support to `opus_codec_ios` and `opus_codec_macos` diff --git a/docs/architecture.md b/docs/architecture.md index dd0888c..8fedac4 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,305 +1,305 @@ -# Architecture - -This document explains how the opus_flutter library works, from high-level design decisions down to platform-specific implementation details. - -## Overview - -opus_flutter is a **federated Flutter plugin** whose sole purpose is to load the [Opus audio codec](https://opus-codec.org/) as a `DynamicLibrary` so it can be consumed by the vendored `opus_dart` package. It does not expose Opus encoding/decoding APIs directly -- that responsibility belongs to opus_dart. - -`opus_dart` was originally an [external package](https://github.com/EPNW/opus_dart) but has been vendored into this repository to reduce external dependency complexity and allow direct maintenance (Dart 3 compatibility fixes, type safety improvements, etc.). - -The plugin follows Flutter's [federated plugin architecture](https://docs.flutter.dev/packages-and-plugins/developing-packages#federated-plugins), which splits a plugin into: - -1. An **app-facing package** that developers add to their `pubspec.yaml`. -2. A **platform interface** that defines the contract all implementations must satisfy. -3. One or more **platform packages**, each containing the native code and Dart glue for a single platform. - -## Package Dependency Graph - -```mermaid -graph TD - A[opus_flutter
app-facing package] --> PI[opus_flutter_platform_interface] - A --> AN[opus_flutter_android] - A --> IO[opus_flutter_ios] - A --> LI[opus_flutter_linux] - A --> MA[opus_flutter_macos] - A --> WE[opus_flutter_web] - A --> WI[opus_flutter_windows] - - AN --> PI - IO --> PI - LI --> PI - MA --> PI - WE --> PI - WI --> PI - - EX[opus_flutter/example] --> A - EX --> OD[opus_dart
vendored] - - style A fill:#4fc3f7,color:#000 - style PI fill:#fff9c4,color:#000 - style AN fill:#a5d6a7,color:#000 - style IO fill:#a5d6a7,color:#000 - style LI fill:#a5d6a7,color:#000 - style MA fill:#a5d6a7,color:#000 - style WE fill:#a5d6a7,color:#000 - style WI fill:#a5d6a7,color:#000 - style OD fill:#ce93d8,color:#000 - style EX fill:#b0bec5,color:#000 -``` - -All platform packages depend on `opus_flutter_platform_interface`. The main `opus_flutter` package depends on all of them and declares the federated plugin mapping in its `pubspec.yaml`. The vendored `opus_dart` package consumes the `DynamicLibrary` returned by `opus_flutter.load()` and provides the actual encoding/decoding APIs. - -## Vendored opus_dart - -`opus_dart` lives at `opus_dart/` in the repository root. It was vendored from [EPNW/opus_dart](https://github.com/EPNW/opus_dart) (v3.0.1) and updated for Dart 3 and `wasm_ffi` 2.x compatibility. - -### Cross-platform FFI via proxy_ffi.dart - -`opus_dart` must work with both `dart:ffi` (native platforms) and `wasm_ffi` (web). This is handled by `proxy_ffi.dart`, which uses Dart conditional exports: - -```dart -export 'dart:ffi' if (dart.library.js_interop) 'package:wasm_ffi/ffi.dart'; -export 'init_ffi.dart' if (dart.library.js_interop) 'init_web.dart'; -``` - -All source files import `proxy_ffi.dart` instead of `dart:ffi` directly, getting the correct platform types resolved at compile time. This allows a single codebase for both native and web. - -### Initialization flow - -`initOpus(Object opusLib)` accepts `Object` so callers don't need to cast from the platform interface's `Future` return type. The platform-specific `createApiObject()` (from `init_ffi.dart` or `init_web.dart`, selected by the conditional export) casts to the concrete `DynamicLibrary` and creates the `ApiObject` with properly typed fields. - -### Type safety: why dynamic was eliminated - -The original `opus_dart` used `dynamic` for several fields and parameters to bridge the gap between `dart:ffi` and `web_ffi` types. This caused **runtime crashes** in modern Dart because many `dart:ffi` methods that appear to be instance methods are actually **extension methods** -- and extension methods cannot be dispatched through `dynamic`. - -Methods that fail through `dynamic` dispatch: -- `Allocator.call()` -- allocates `sizeOf() * count` bytes -- `Pointer.operator []` -- indexed byte access -- `Pointer.asTypedList()` -- creates a typed view over native memory -- `Pointer.value` (getter) -- reads the value at a pointer -- `DynamicLibrary.lookupFunction<>()` -- looks up and binds a native function - -All fields and parameters are now statically typed (using types from `proxy_ffi.dart`), so extension methods resolve at compile time. The only `dynamic` that remains is suppressed via `// ignore_for_file` in `init_web.dart`, which is a web-only file analyzed on native where `wasm_ffi` types don't match `dart:ffi` types -- a known limitation of the conditional import pattern. - -## Platform Interface - -The core abstraction lives in `opus_flutter_platform_interface`: - -```dart -abstract class OpusFlutterPlatform extends PlatformInterface { - static OpusFlutterPlatform _instance = OpusFlutterPlatformUnsupported(); - - static OpusFlutterPlatform get instance => _instance; - static set instance(OpusFlutterPlatform instance) { ... } - - Future load() { - throw UnimplementedError('load() has not been implemented.'); - } -} -``` - -Key design points: - -- Extends `PlatformInterface` from `plugin_platform_interface` to enforce that implementations extend (not implement) the class. -- Holds a static singleton `instance` that each platform package replaces at registration time. -- The default instance is `OpusFlutterPlatformUnsupported`, which throws `UnsupportedError`. -- The single API surface is `Future load()`, which returns a `DynamicLibrary`. - -The return type is `Object` rather than a specific `DynamicLibrary` because the web uses `wasm_ffi`'s `DynamicLibrary` class (a separate type from `dart:ffi`'s `DynamicLibrary`). - -## Plugin Registration - -Each platform package declares a `dartPluginClass` in its `pubspec.yaml`. Flutter automatically calls the static `registerWith()` method during app initialization, which sets `OpusFlutterPlatform.instance` to the platform-specific implementation. - -```mermaid -flowchart LR - entry[opus_flutter.dart] --> load[opus_flutter_load.dart] - load --> pi[OpusFlutterPlatform.instance.load] - - subgraph Auto-registered by Flutter - android[OpusFlutterAndroid.registerWith] - ios[OpusFlutterIOS.registerWith] - linux[OpusFlutterLinux.registerWith] - macos[OpusFlutterMacOS.registerWith] - windows[OpusFlutterWindows.registerWith] - web[OpusFlutterWeb.registerWith] - end -``` - -The main package has a single entry point (`opus_flutter_load.dart`) that delegates to `OpusFlutterPlatform.instance.load()`. No platform-specific imports or conditional exports are needed. - -## Platform Implementations - -### Android - -| Aspect | Detail | -|--------|--------| -| Language | Java (plugin stub), C (opus via CMake) | -| Library loading | `DynamicLibrary.open('libopus.so')` | -| Opus distribution | Built from source at Gradle build time via CMake `FetchContent` (fetches opus v1.5.2 from GitHub) | -| Plugin class | `OpusFlutterAndroidPlugin` -- empty `FlutterPlugin` stub | - -The Android build uses `FetchContent` in `CMakeLists.txt` to download opus source and compile it as a shared library. This means no opus source code is checked into the repository for Android. - -### iOS - -| Aspect | Detail | -|--------|--------| -| Language | Swift + Objective-C (plugin stub) | -| Library loading | `DynamicLibrary.process()` (opus is statically linked via the vendored framework) | -| Opus distribution | Pre-built `opus.xcframework` with slices for `ios-arm64` (device) and `ios-arm64_x86_64-simulator` | -| Build script | `build_xcframework.sh` clones opus, builds with CMake, wraps as dynamic framework, assembles xcframework | -| Plugin class | `OpusFlutterIosPlugin` (ObjC) bridges to `SwiftOpusFlutterIosPlugin` (Swift) -- both are empty stubs | - -Since opus is linked into the process, `DynamicLibrary.process()` finds the symbols without needing a file path. - -### macOS - -| Aspect | Detail | -|--------|--------| -| Language | Swift (plugin stub) | -| Library loading | `DynamicLibrary.process()` | -| Opus distribution | Pre-built `opus.xcframework` with a `macos-arm64_x86_64` universal binary | -| Build script | `build_xcframework.sh` -- same approach as iOS, targeting the `macosx` SDK | -| Plugin class | `OpusFlutterMacosPlugin` -- empty stub importing `FlutterMacOS` | - -### Linux - -| Aspect | Detail | -|--------|--------| -| Language | Dart only (no native plugin class) | -| Library loading | `DynamicLibrary.open(path)` after copying `.so` to a temp directory | -| Opus distribution | Pre-built shared libraries (`libopus_x86_64.so.blob`, `libopus_aarch64.so.blob`) stored as Flutter assets | -| Build method | Native + cross-compiled from Docker (`Dockerfile`, Ubuntu 20.04 base for glibc 2.31 compatibility) | -| Fallback | If bundled binary fails to load, falls back to system `libopus.so.0` | -| Registration | Uses `dartPluginClass: OpusFlutterLinux` with `pluginClass: none` | - -At runtime, the Linux implementation: - -1. Detects architecture via `Abi.current()` (`linuxX64` or `linuxArm64`). -2. Copies the matching `.so.blob` asset from `rootBundle` to a temp directory. -3. Opens the copied library with `DynamicLibrary.open()`. -4. If any step fails, falls back to `DynamicLibrary.open('libopus.so.0')` (system library). - -### Windows - -| Aspect | Detail | -|--------|--------| -| Language | Dart only (no native plugin class) | -| Library loading | `DynamicLibrary.open(path)` after copying DLL to a temp directory | -| Opus distribution | Pre-built DLLs (`libopus_x64.dll.blob`, `libopus_x86.dll.blob`) stored as Flutter assets | -| Build method | Cross-compiled from Linux using MinGW via Docker (`Dockerfile`) | -| Registration | Uses `dartPluginClass: OpusFlutterWindows` with `pluginClass: none` | - -At runtime, the Windows implementation: - -1. Uses `path_provider` to get a temp directory. -2. Copies the correct DLL (x64 or x86 based on `Platform.version`) from assets to disk. -3. Also copies the opus license file. -4. Opens the DLL with `DynamicLibrary.open()`. - -### Web - -| Aspect | Detail | -|--------|--------| -| Language | Dart (uses `wasm_ffi` and `inject_js`) | -| Library loading | Injects `libopus.js`, loads `libopus.wasm`, returns `wasm_ffi` `DynamicLibrary` | -| Opus distribution | Pre-built `libopus.js` + `libopus.wasm` stored as Flutter assets | -| Build method | Compiled with Emscripten via Docker (`Dockerfile`) | -| Registration | Uses `pluginClass: OpusFlutterWeb` with `fileName: opus_flutter_web.dart` | - -The web implementation: - -1. Injects the Emscripten-generated JS glue (`libopus.js`) into the page. -2. Fetches the WASM binary (`libopus.wasm`). -3. Initializes `wasm_ffi`'s `Memory` and compiles the Emscripten module. -4. Returns a `wasm_ffi` `DynamicLibrary.fromModule()`. - -## Opus Build Pipeline - -Each platform has a different strategy for building and distributing the opus binary: - -```mermaid -flowchart TB - subgraph Source - GH[github.com/xiph/opus
tag v1.5.2] - end - - subgraph Android - GH -->|CMake FetchContent
at Gradle build time| AS[libopus.so
built on developer machine] - end - - subgraph iOS - GH -->|build_xcframework.sh
clones + CMake| IF[opus.xcframework
arm64 + simulator] - end - - subgraph macOS - GH -->|build_xcframework.sh
clones + CMake| MF[opus.xcframework
arm64 + x86_64 universal] - end - - subgraph Linux - GH -->|Dockerfile
native + cross-compile| LF[libopus_x86_64.so
libopus_aarch64.so] - end - - subgraph Windows - GH -->|Dockerfile
MinGW cross-compile| WF[libopus_x64.dll
libopus_x86.dll] - end - - subgraph Web - GH -->|Dockerfile
Emscripten| EF[libopus.js + libopus.wasm] - end -``` - -## Opus Version - -All platforms build from or bundle **libopus v1.5.2**, fetched from https://github.com/xiph/opus. On Linux, the system-installed version is used. - -## Data Flow - -```mermaid -sequenceDiagram - participant App as User Code - participant OF as opus_flutter - participant PI as OpusFlutterPlatform - participant Impl as Platform Implementation - participant OD as opus_dart - - App->>OF: load() - OF->>PI: instance.load() - PI->>Impl: load() - - alt Android - Impl-->>PI: DynamicLibrary.open('libopus.so') - else iOS / macOS - Impl-->>PI: DynamicLibrary.process() - else Linux - Impl->>Impl: Copy .so to temp dir - Impl-->>PI: DynamicLibrary.open(path) - else Windows - Impl->>Impl: Copy DLL to temp dir - Impl-->>PI: DynamicLibrary.open(path) - else Web - Impl->>Impl: Inject JS, load WASM - Impl-->>PI: wasm_ffi DynamicLibrary - end - - PI-->>OF: Object (DynamicLibrary) - OF-->>App: Object - App->>OD: initOpus(object) - OD->>OD: Cast to DynamicLibrary, bind FFI functions - App->>OD: Encode / Decode audio -``` - -## Example App - -The example app (`opus_flutter/example`) demonstrates: - -1. Loading opus via `opus_flutter.load()` (returns `Future`). -2. Passing the result directly to `initOpus()` -- no cast needed. -3. Reading a raw PCM audio file from assets. -4. Streaming it through `StreamOpusEncoder` then `StreamOpusDecoder`. -5. Wrapping the result in a WAV header. -6. Sharing the output file via `share_plus`. - -The example depends on the vendored `opus_dart` via a path dependency (`path: ../../opus_dart`). +# Architecture + +This document explains how the opus_flutter library works, from high-level design decisions down to platform-specific implementation details. + +## Overview + +opus_flutter is a **federated Flutter plugin** whose sole purpose is to load the [Opus audio codec](https://opus-codec.org/) as a `DynamicLibrary` so it can be consumed by the vendored `opus_dart` package. It does not expose Opus encoding/decoding APIs directly -- that responsibility belongs to opus_dart. + +`opus_dart` was originally an [external package](https://github.com/EPNW/opus_dart) but has been vendored into this repository to reduce external dependency complexity and allow direct maintenance (Dart 3 compatibility fixes, type safety improvements, etc.). + +The plugin follows Flutter's [federated plugin architecture](https://docs.flutter.dev/packages-and-plugins/developing-packages#federated-plugins), which splits a plugin into: + +1. An **app-facing package** that developers add to their `pubspec.yaml`. +2. A **platform interface** that defines the contract all implementations must satisfy. +3. One or more **platform packages**, each containing the native code and Dart glue for a single platform. + +## Package Dependency Graph + +```mermaid +graph TD + A[opus_flutter
app-facing package] --> PI[opus_flutter_platform_interface] + A --> AN[opus_flutter_android] + A --> IO[opus_flutter_ios] + A --> LI[opus_flutter_linux] + A --> MA[opus_flutter_macos] + A --> WE[opus_flutter_web] + A --> WI[opus_flutter_windows] + + AN --> PI + IO --> PI + LI --> PI + MA --> PI + WE --> PI + WI --> PI + + EX[opus_flutter/example] --> A + EX --> OD[opus_dart
vendored] + + style A fill:#4fc3f7,color:#000 + style PI fill:#fff9c4,color:#000 + style AN fill:#a5d6a7,color:#000 + style IO fill:#a5d6a7,color:#000 + style LI fill:#a5d6a7,color:#000 + style MA fill:#a5d6a7,color:#000 + style WE fill:#a5d6a7,color:#000 + style WI fill:#a5d6a7,color:#000 + style OD fill:#ce93d8,color:#000 + style EX fill:#b0bec5,color:#000 +``` + +All platform packages depend on `opus_flutter_platform_interface`. The main `opus_flutter` package depends on all of them and declares the federated plugin mapping in its `pubspec.yaml`. The vendored `opus_dart` package consumes the `DynamicLibrary` returned by `opus_flutter.load()` and provides the actual encoding/decoding APIs. + +## Vendored opus_dart + +`opus_dart` lives at `opus_dart/` in the repository root. It was vendored from [EPNW/opus_dart](https://github.com/EPNW/opus_dart) (v3.0.1) and updated for Dart 3 and `wasm_ffi` 2.x compatibility. + +### Cross-platform FFI via proxy_ffi.dart + +`opus_dart` must work with both `dart:ffi` (native platforms) and `wasm_ffi` (web). This is handled by `proxy_ffi.dart`, which uses Dart conditional exports: + +```dart +export 'dart:ffi' if (dart.library.js_interop) 'package:wasm_ffi/ffi.dart'; +export 'init_ffi.dart' if (dart.library.js_interop) 'init_web.dart'; +``` + +All source files import `proxy_ffi.dart` instead of `dart:ffi` directly, getting the correct platform types resolved at compile time. This allows a single codebase for both native and web. + +### Initialization flow + +`initOpus(Object opusLib)` accepts `Object` so callers don't need to cast from the platform interface's `Future` return type. The platform-specific `createApiObject()` (from `init_ffi.dart` or `init_web.dart`, selected by the conditional export) casts to the concrete `DynamicLibrary` and creates the `ApiObject` with properly typed fields. + +### Type safety: why dynamic was eliminated + +The original `opus_dart` used `dynamic` for several fields and parameters to bridge the gap between `dart:ffi` and `web_ffi` types. This caused **runtime crashes** in modern Dart because many `dart:ffi` methods that appear to be instance methods are actually **extension methods** -- and extension methods cannot be dispatched through `dynamic`. + +Methods that fail through `dynamic` dispatch: +- `Allocator.call()` -- allocates `sizeOf() * count` bytes +- `Pointer.operator []` -- indexed byte access +- `Pointer.asTypedList()` -- creates a typed view over native memory +- `Pointer.value` (getter) -- reads the value at a pointer +- `DynamicLibrary.lookupFunction<>()` -- looks up and binds a native function + +All fields and parameters are now statically typed (using types from `proxy_ffi.dart`), so extension methods resolve at compile time. The only `dynamic` that remains is suppressed via `// ignore_for_file` in `init_web.dart`, which is a web-only file analyzed on native where `wasm_ffi` types don't match `dart:ffi` types -- a known limitation of the conditional import pattern. + +## Platform Interface + +The core abstraction lives in `opus_flutter_platform_interface`: + +```dart +abstract class OpusFlutterPlatform extends PlatformInterface { + static OpusFlutterPlatform _instance = OpusFlutterPlatformUnsupported(); + + static OpusFlutterPlatform get instance => _instance; + static set instance(OpusFlutterPlatform instance) { ... } + + Future load() { + throw UnimplementedError('load() has not been implemented.'); + } +} +``` + +Key design points: + +- Extends `PlatformInterface` from `plugin_platform_interface` to enforce that implementations extend (not implement) the class. +- Holds a static singleton `instance` that each platform package replaces at registration time. +- The default instance is `OpusFlutterPlatformUnsupported`, which throws `UnsupportedError`. +- The single API surface is `Future load()`, which returns a `DynamicLibrary`. + +The return type is `Object` rather than a specific `DynamicLibrary` because the web uses `wasm_ffi`'s `DynamicLibrary` class (a separate type from `dart:ffi`'s `DynamicLibrary`). + +## Plugin Registration + +Each platform package declares a `dartPluginClass` in its `pubspec.yaml`. Flutter automatically calls the static `registerWith()` method during app initialization, which sets `OpusFlutterPlatform.instance` to the platform-specific implementation. + +```mermaid +flowchart LR + entry[opus_flutter.dart] --> load[opus_flutter_load.dart] + load --> pi[OpusFlutterPlatform.instance.load] + + subgraph Auto-registered by Flutter + android[OpusFlutterAndroid.registerWith] + ios[OpusFlutterIOS.registerWith] + linux[OpusFlutterLinux.registerWith] + macos[OpusFlutterMacOS.registerWith] + windows[OpusFlutterWindows.registerWith] + web[OpusFlutterWeb.registerWith] + end +``` + +The main package has a single entry point (`opus_flutter_load.dart`) that delegates to `OpusFlutterPlatform.instance.load()`. No platform-specific imports or conditional exports are needed. + +## Platform Implementations + +### Android + +| Aspect | Detail | +|--------|--------| +| Language | Java (plugin stub), C (opus via CMake) | +| Library loading | `DynamicLibrary.open('libopus.so')` | +| Opus distribution | Built from source at Gradle build time via CMake `FetchContent` (fetches opus v1.5.2 from GitHub) | +| Plugin class | `OpusFlutterAndroidPlugin` -- empty `FlutterPlugin` stub | + +The Android build uses `FetchContent` in `CMakeLists.txt` to download opus source and compile it as a shared library. This means no opus source code is checked into the repository for Android. + +### iOS + +| Aspect | Detail | +|--------|--------| +| Language | Swift + Objective-C (plugin stub) | +| Library loading | `DynamicLibrary.process()` (opus is statically linked via the vendored framework) | +| Opus distribution | Pre-built `opus.xcframework` with slices for `ios-arm64` (device) and `ios-arm64_x86_64-simulator` | +| Build script | `build_xcframework.sh` clones opus, builds with CMake, wraps as dynamic framework, assembles xcframework | +| Plugin class | `OpusFlutterIosPlugin` (ObjC) bridges to `SwiftOpusFlutterIosPlugin` (Swift) -- both are empty stubs | + +Since opus is linked into the process, `DynamicLibrary.process()` finds the symbols without needing a file path. + +### macOS + +| Aspect | Detail | +|--------|--------| +| Language | Swift (plugin stub) | +| Library loading | `DynamicLibrary.process()` | +| Opus distribution | Pre-built `opus.xcframework` with a `macos-arm64_x86_64` universal binary | +| Build script | `build_xcframework.sh` -- same approach as iOS, targeting the `macosx` SDK | +| Plugin class | `OpusFlutterMacosPlugin` -- empty stub importing `FlutterMacOS` | + +### Linux + +| Aspect | Detail | +|--------|--------| +| Language | Dart only (no native plugin class) | +| Library loading | `DynamicLibrary.open(path)` after copying `.so` to a temp directory | +| Opus distribution | Pre-built shared libraries (`libopus_x86_64.so.blob`, `libopus_aarch64.so.blob`) stored as Flutter assets | +| Build method | Native + cross-compiled from Docker (`Dockerfile`, Ubuntu 20.04 base for glibc 2.31 compatibility) | +| Fallback | If bundled binary fails to load, falls back to system `libopus.so.0` | +| Registration | Uses `dartPluginClass: OpusFlutterLinux` with `pluginClass: none` | + +At runtime, the Linux implementation: + +1. Detects architecture via `Abi.current()` (`linuxX64` or `linuxArm64`). +2. Copies the matching `.so.blob` asset from `rootBundle` to a temp directory. +3. Opens the copied library with `DynamicLibrary.open()`. +4. If any step fails, falls back to `DynamicLibrary.open('libopus.so.0')` (system library). + +### Windows + +| Aspect | Detail | +|--------|--------| +| Language | Dart only (no native plugin class) | +| Library loading | `DynamicLibrary.open(path)` after copying DLL to a temp directory | +| Opus distribution | Pre-built DLLs (`libopus_x64.dll.blob`, `libopus_x86.dll.blob`) stored as Flutter assets | +| Build method | Cross-compiled from Linux using MinGW via Docker (`Dockerfile`) | +| Registration | Uses `dartPluginClass: OpusFlutterWindows` with `pluginClass: none` | + +At runtime, the Windows implementation: + +1. Uses `path_provider` to get a temp directory. +2. Copies the correct DLL (x64 or x86 based on `Platform.version`) from assets to disk. +3. Also copies the opus license file. +4. Opens the DLL with `DynamicLibrary.open()`. + +### Web + +| Aspect | Detail | +|--------|--------| +| Language | Dart (uses `wasm_ffi` and `inject_js`) | +| Library loading | Injects `libopus.js`, loads `libopus.wasm`, returns `wasm_ffi` `DynamicLibrary` | +| Opus distribution | Pre-built `libopus.js` + `libopus.wasm` stored as Flutter assets | +| Build method | Compiled with Emscripten via Docker (`Dockerfile`) | +| Registration | Uses `pluginClass: OpusFlutterWeb` with `fileName: opus_flutter_web.dart` | + +The web implementation: + +1. Injects the Emscripten-generated JS glue (`libopus.js`) into the page. +2. Fetches the WASM binary (`libopus.wasm`). +3. Initializes `wasm_ffi`'s `Memory` and compiles the Emscripten module. +4. Returns a `wasm_ffi` `DynamicLibrary.fromModule()`. + +## Opus Build Pipeline + +Each platform has a different strategy for building and distributing the opus binary: + +```mermaid +flowchart TB + subgraph Source + GH[github.com/xiph/opus
tag v1.5.2] + end + + subgraph Android + GH -->|CMake FetchContent
at Gradle build time| AS[libopus.so
built on developer machine] + end + + subgraph iOS + GH -->|build_xcframework.sh
clones + CMake| IF[opus.xcframework
arm64 + simulator] + end + + subgraph macOS + GH -->|build_xcframework.sh
clones + CMake| MF[opus.xcframework
arm64 + x86_64 universal] + end + + subgraph Linux + GH -->|Dockerfile
native + cross-compile| LF[libopus_x86_64.so
libopus_aarch64.so] + end + + subgraph Windows + GH -->|Dockerfile
MinGW cross-compile| WF[libopus_x64.dll
libopus_x86.dll] + end + + subgraph Web + GH -->|Dockerfile
Emscripten| EF[libopus.js + libopus.wasm] + end +``` + +## Opus Version + +All platforms build from or bundle **libopus v1.5.2**, fetched from https://github.com/xiph/opus. On Linux, the system-installed version is used. + +## Data Flow + +```mermaid +sequenceDiagram + participant App as User Code + participant OF as opus_flutter + participant PI as OpusFlutterPlatform + participant Impl as Platform Implementation + participant OD as opus_dart + + App->>OF: load() + OF->>PI: instance.load() + PI->>Impl: load() + + alt Android + Impl-->>PI: DynamicLibrary.open('libopus.so') + else iOS / macOS + Impl-->>PI: DynamicLibrary.process() + else Linux + Impl->>Impl: Copy .so to temp dir + Impl-->>PI: DynamicLibrary.open(path) + else Windows + Impl->>Impl: Copy DLL to temp dir + Impl-->>PI: DynamicLibrary.open(path) + else Web + Impl->>Impl: Inject JS, load WASM + Impl-->>PI: wasm_ffi DynamicLibrary + end + + PI-->>OF: Object (DynamicLibrary) + OF-->>App: Object + App->>OD: initOpus(object) + OD->>OD: Cast to DynamicLibrary, bind FFI functions + App->>OD: Encode / Decode audio +``` + +## Example App + +The example app (`opus_flutter/example`) demonstrates: + +1. Loading opus via `opus_flutter.load()` (returns `Future`). +2. Passing the result directly to `initOpus()` -- no cast needed. +3. Reading a raw PCM audio file from assets. +4. Streaming it through `StreamOpusEncoder` then `StreamOpusDecoder`. +5. Wrapping the result in a WAV header. +6. Sharing the output file via `share_plus`. + +The example depends on the vendored `opus_dart` via a path dependency (`path: ../../opus_dart`). diff --git a/docs/code-quality.md b/docs/code-quality.md index 7c44f9a..8b04702 100644 --- a/docs/code-quality.md +++ b/docs/code-quality.md @@ -1,328 +1,328 @@ -# Code Quality - -This document provides an assessment of the opus_flutter codebase's quality across multiple dimensions. - -## Summary - -```mermaid -quadrantChart - title Code Quality Assessment - x-axis Low Impact --> High Impact - y-axis Low Quality --> High Quality - quadrant-1 Strengths - quadrant-2 Monitor - quadrant-3 Low Priority - quadrant-4 Address First - Architecture: [0.85, 0.85] - Code clarity: [0.6, 0.8] - Documentation: [0.5, 0.55] - Test coverage: [0.9, 0.4] - Consistency: [0.4, 0.75] - Maintainability: [0.75, 0.75] - Build system: [0.7, 0.7] -``` - -| Dimension | Rating | Notes | -|-----------|--------|-------| -| Architecture | Good | Clean federated plugin structure | -| Code clarity | Good | Small, focused files with clear intent | -| Documentation | Fair | Public APIs documented, some packages lack detail | -| Test coverage | Fair | Unit tests for platform interface and registration logic | -| Consistency | Good | Uniform patterns across all packages | -| Maintainability | Good | Clean architecture, proper plugin registration | -| Build system | Good | Modern AGP, deterministic native builds | - ---- - -## Architecture - -**Rating: Good** - -The project follows Flutter's recommended federated plugin pattern correctly: - -- Clear separation between the app-facing package, platform interface, and platform implementations. -- Each package has a single responsibility. -- The platform interface uses `PlatformInterface` from `plugin_platform_interface` with proper token verification. -- All platform packages self-register via `dartPluginClass` and `registerWith()`. -- A single entry point (`opus_flutter_load.dart`) delegates to the platform interface without platform-specific imports. - ---- - -## File-by-File Analysis - -### Platform Interface (`opus_flutter_platform_interface`) - -| File | Lines | Quality | Notes | -|------|-------|---------|-------| -| `opus_flutter_platform_interface.dart` | 3 | Good | Clean barrel export | -| `opus_flutter_platform_interface.dart` (src) | 46 | Good | Proper PlatformInterface usage, clear docs | -| `opus_flutter_platform_unsupported.dart` | 12 | Good | Appropriate default fallback | - -No issues. Well-structured. - -### Main Package (`opus_flutter`) - -| File | Lines | Quality | Notes | -|------|-------|---------|-------| -| `opus_flutter.dart` | 4 | Good | Single clean export | -| `opus_flutter_load.dart` | 15 | Good | Simple delegation to platform interface | - -The main package is minimal by design -- it exports a single `load()` function that delegates to `OpusFlutterPlatform.instance.load()`. Platform registration is handled automatically by Flutter via `dartPluginClass`. - -### Android (`opus_flutter_android`) - -| File | Lines | Quality | Notes | -|------|-------|---------|-------| -| `opus_flutter_android.dart` | 19 | Good | Clean, self-registering via `dartPluginClass` | -| `OpusFlutterAndroidPlugin.java` | 14 | Good | Empty stub, expected for FFI-only plugins | -| `CMakeLists.txt` | 16 | Good | Modern FetchContent approach | -| `build.gradle` | 59 | Good | AGP 8.7.0, compileSdk 35, Java 17 | - -The CMakeLists.txt is well-written and concise. The build.gradle uses AGP 8.7.0 with Java 17 compatibility. Test dependencies (`junit`, `mockito`) are included but no actual tests exist yet. - -### iOS (`opus_flutter_ios`) - -| File | Lines | Quality | Notes | -|------|-------|---------|-------| -| `opus_flutter_ios.dart` | 19 | Good | Clean, self-registering via `dartPluginClass` | -| `OpusFlutterIosPlugin.swift` | 8 | Good | Minimal Swift-only stub | -| `build_xcframework.sh` | 239 | Good | Well-structured, documented, error handling | - -Uses Swift-only registration (no ObjC bridge). The build script is well-written with clear sections, error checking, and cleanup. - -### macOS (`opus_flutter_macos`) - -| File | Lines | Quality | Notes | -|------|-------|---------|-------| -| `opus_flutter_macos.dart` | 19 | Good | Clean, self-registering via `dartPluginClass` | -| `OpusFlutterMacosPlugin.swift` | 8 | Good | Minimal Swift-only stub | -| `build_xcframework.sh` | 222 | Good | Adapted from iOS script, well-structured | - -Cleanest platform implementation. Uses Swift-only registration (no ObjC bridge). - -### Web (`opus_flutter_web`) - -| File | Lines | Quality | Notes | -|------|-------|---------|-------| -| `opus_flutter_web.dart` | 42 | Good | Most complex platform impl, uses actively maintained wasm_ffi | - -The web implementation is the most involved platform package. It has to inject JavaScript, load WASM, initialize memory, and bridge through `wasm_ffi`. The migration from the unmaintained `web_ffi` to `wasm_ffi` (v2.2.0, actively maintained) has resolved the dependency risk. - -### Windows (`opus_flutter_windows`) - -| File | Lines | Quality | Notes | -|------|-------|---------|-------| -| `opus_flutter_windows.dart` | 57 | Good | Asset copying logic, proper arch detection via `Abi.current()` | - -The Windows implementation has the most runtime logic: copying DLLs from assets to a temp directory, detecting architecture via `Abi.current()`, and loading dynamically. - -### Example App - -| File | Lines | Quality | Notes | -|------|-------|---------|-------| -| `main.dart` | 171 | Good | Clean demo of encoding/decoding with share functionality | - -The example app demonstrates a complete encode/decode pipeline with file sharing. Code style is clean with proper return types and no unnecessary overrides. Uses vendored `opus_dart` via path dependency. - -### Vendored opus_dart (`opus_dart`) - -| File | Lines | Quality | Notes | -|------|-------|---------|-------| -| `opus_dart.dart` | 10 | Good | Clean barrel export | -| `proxy_ffi.dart` | 3 | Good | Conditional export: `dart:ffi` on native, `wasm_ffi/ffi.dart` on web | -| `init_ffi.dart` | 9 | Good | Native init: casts `Object` to `DynamicLibrary`, passes `malloc` as allocator | -| `init_web.dart` | 31 | Good | Web init: registers opaque types, uses `wasm_ffi` allocator. Cross-platform analysis artifacts suppressed via `ignore_for_file` | -| `opus_dart_misc.dart` | 78 | Good | Core types and `initOpus()` entry point. All fields statically typed | -| `opus_dart_encoder.dart` | 344 | Good | Simple and buffered encoder implementations | -| `opus_dart_decoder.dart` | 503 | Good | Simple and buffered decoder implementations | -| `opus_dart_packet.dart` | 95 | Good | Packet inspection utilities | -| `opus_dart_streaming.dart` | 343 | Good | Stream transformers for encode/decode pipelines | -| `wrappers/*.dart` | ~600 | Good | FFI bindings. `final` class modifier added for Dart 3 | - -The vendored package required several fixes for modern Dart compatibility: - -- **`dynamic` elimination:** The original code used `dynamic` for `ApiObject.allocator`, `ApiObject` constructor parameter, and the `_asString` helper. This caused `NoSuchMethodError` at runtime because `dart:ffi` extension methods (`Allocator.call()`, `Pointer.operator []`, `Pointer.asTypedList()`) cannot be dispatched through `dynamic`. All are now statically typed via `proxy_ffi.dart`. -- **`Pointer.elementAt()` removed:** Replaced with `Pointer` extension's `operator []` (deprecated in Dart 3.3, removed in later SDKs). -- **`wasm_ffi` 2.x API:** Corrected import paths (`ffi.dart` not `wasm_ffi.dart`), replaced `boundMemory` with `allocator`. -- **Dart 3 class modifiers:** All `Opaque` subclasses marked `final`. - ---- - -## Dart Style and Conventions - -### Positive Patterns - -- Doc comments on all public APIs using `///` syntax. -- `@override` annotations used (macOS package). -- All platform packages use `dartPluginClass` for self-registration. -- Clear package naming following Flutter conventions. - -### Issues Found - -| Issue | Location | Status | -|-------|----------|--------| -| ~~`new` keyword used in Dart 3 codebase~~ | Various files | Resolved | -| ~~`void` return with `async`~~ | `example/main.dart` | Resolved | -| ~~Empty `initState()` override~~ | `example/main.dart` | Resolved | -| ~~Missing `@override` on `load()`~~ | Platform implementations | Resolved | -| ~~Inconsistent quote style (double vs single)~~ | `example/pubspec.yaml` | Resolved | -| ~~`dynamic` causing runtime extension method failures~~ | `opus_dart` (vendored) | Resolved | -| ~~Removed `dart:ffi` API (`Pointer.elementAt`)~~ | `opus_dart` (vendored) | Resolved | -| ~~Wrong `wasm_ffi` import paths~~ | `opus_dart` (vendored) | Resolved | -| ~~Missing `final` on `Opaque` subclasses~~ | `opus_dart` (vendored) | Resolved | - ---- - -## Dependency Health - -```mermaid -graph LR - subgraph Low Risk - A[plugin_platform_interface
^2.1.8 • Active] - B[flutter_lints
^5.0.0 • Active] - C[path_provider
^2.1.5 • Active] - D[share_plus
^10.0.0 • Active] - E[platform_info
^5.0.0 • Active] - H[wasm_ffi
^2.1.0 • Active] - I[ffi
^2.1.0 • Active] - end - - subgraph Medium Risk - F[inject_js
^2.1.0 • 15 months ago] - end - - subgraph Vendored - G[opus_dart
v3.0.1 • in-repo] - end - - style A fill:#c8e6c9,color:#000 - style B fill:#c8e6c9,color:#000 - style C fill:#c8e6c9,color:#000 - style D fill:#c8e6c9,color:#000 - style E fill:#c8e6c9,color:#000 - style F fill:#fff9c4,color:#000 - style G fill:#ce93d8,color:#000 - style H fill:#c8e6c9,color:#000 - style I fill:#c8e6c9,color:#000 -``` - -| Dependency | Version | Last Updated | Risk | -|------------|---------|-------------|------| -| `plugin_platform_interface` | ^2.1.8 | Active | Low | -| `flutter_lints` | ^5.0.0 | Active | Low | -| `path_provider` | ^2.1.5 | Active | Low | -| `inject_js` | ^2.1.0 | 15 months ago | Medium | -| `wasm_ffi` | ^2.1.0 | Active | Low | -| `ffi` | ^2.1.0 | Active | Low | -| `opus_dart` | v3.0.1 | Vendored | None | -| `share_plus` | ^10.0.0 | Active | Low | -| `platform_info` | ^5.0.0 | Active | Low | - -`opus_dart` has been vendored into the repository, eliminating it as an external dependency risk. The migration from `web_ffi` to `wasm_ffi` previously eliminated the highest-risk dependency. - ---- - -## Build System Quality - -### Android -- **Approach:** CMake FetchContent (downloads opus at build time). -- **Strength:** No vendored sources; always builds from a pinned tag. -- **Risk:** Requires internet during build; network issues or removed GitHub tags will break builds. -- **AGP:** 8.7.0 with Java 17, compileSdk 35. - -### iOS -- **Approach:** Pre-built xcframework via shell script. -- **Strength:** Deterministic; no network needed at app build time. -- **Risk:** Script must be re-run manually to update opus. - -### Linux -- **Approach:** Pre-built shared libraries via Docker, stored as Flutter assets. -- **Strength:** Deterministic; no network or system dependency needed at app build time. Supports x86_64 and aarch64. -- **Risk:** None significant. Uses `ubuntu:20.04` as base image (glibc 2.31 for broad compatibility). Falls back to system `libopus.so.0` if bundled binary fails. - -### macOS -- **Approach:** Same as iOS. -- **Strength/Risk:** Same as iOS. - -### Windows -- **Approach:** Cross-compiled via Docker, DLLs stored as assets. -- **Strength:** Deterministic; no network needed at app build time. -- **Risk:** None significant. Uses `ubuntu:24.04` as base image. - -### Web -- **Approach:** Compiled via Emscripten in Docker. -- **Strength:** Deterministic output. -- **Risk:** Same Docker base image concern as Windows. - ---- - -## Lint Coverage - -| Package | Has `analysis_options.yaml` | Lint package | -|---------|----------------------------|-------------| -| opus_flutter | Yes | flutter_lints | -| opus_flutter_platform_interface | Yes | flutter_lints | -| opus_flutter_android | Yes | flutter_lints | -| opus_flutter_ios | Yes | flutter_lints | -| opus_flutter_linux | Yes | flutter_lints | -| opus_flutter_macos | Yes | flutter_lints | -| opus_flutter_web | Yes | flutter_lints | -| opus_flutter_windows | Yes | flutter_lints | -| opus_dart | Yes | `package:lints/recommended.yaml` (`constant_identifier_names` disabled for C API names) | -| example | Yes | flutter_lints | - -All Flutter packages have lint configuration referencing `package:flutter_lints/flutter.yaml`. The vendored `opus_dart` is a pure Dart package and uses `package:lints/recommended.yaml` (the non-Flutter equivalent) with `constant_identifier_names` disabled since the FFI wrappers mirror upstream C API naming conventions. All files pass `dart analyze` and `dart format`. - ---- - -## Test Coverage - -| Package | Unit Tests | Widget Tests | Integration Tests | -|---------|-----------|-------------|-------------------| -| opus_flutter | 2 tests | None | None | -| opus_flutter_platform_interface | 6 tests | None | None | -| opus_flutter_android | 3 tests | None | None | -| opus_flutter_ios | 2 tests | None | None | -| opus_flutter_linux | 2 tests | None | None | -| opus_flutter_macos | 2 tests | None | None | -| opus_flutter_web | 1 test | None | None | -| opus_flutter_windows | 2 tests | None | None | -| opus_dart | 13 tests | None | None | -| example | None | None | None | - -Unit tests cover the platform interface contract (singleton, token verification, version constant, error handling) and registration logic (`registerWith()`, class hierarchy) for each platform. Native library loading (`DynamicLibrary.open()`, `DynamicLibrary.process()`) cannot be unit tested as it requires the actual opus binary. CI runs all tests on every push. - -The vendored `opus_dart` has 13 unit tests covering pure-logic helpers (`maxSamplesPerPacket()`, error classes, enum completeness). FFI-dependent code (encoding/decoding) requires the actual opus library and would need integration-level tests. - ---- - -## Recommendations by Priority - -### High Priority - -1. ~~**Add tests**~~ -- Resolved: unit tests added for platform interface and all platform implementations. - -### Medium Priority - -2. ~~**Update Docker base images**~~ -- Resolved: Windows Dockerfile updated from `ubuntu:bionic` (18.04, EOL) to `ubuntu:24.04`. -3. ~~**Add `opus_dart` to CI**~~ -- Resolved: dedicated `analyze-opus-dart` and `test-opus-dart` jobs added. -4. ~~**Add `analysis_options.yaml` to `opus_dart`**~~ -- Resolved: uses `package:lints/recommended.yaml`. -5. ~~**Fix `opus_dart` formatting**~~ -- Resolved: all files pass `dart format`. -6. ~~**Add unit tests for `opus_dart` pure logic**~~ -- Resolved: 13 tests added. - -### Resolved - -- ~~**Add CI/CD**~~ -- GitHub Actions workflow added. -- ~~**Add `analysis_options.yaml`**~~ -- All packages have consistent lint rules. -- ~~**Evaluate web_ffi alternatives**~~ -- Migrated to `wasm_ffi` ^2.2.0. -- ~~**Check if Flutter workarounds are still needed**~~ -- Removed; all platforms use `dartPluginClass`. -- ~~**Fix Dockerfile typos**~~ -- `DEBIAN_FRONTEND` corrected. -- ~~**Simplify iOS plugin**~~ -- Swift-only, ObjC bridge removed. -- ~~**Remove `new` keyword**~~ -- Cleaned up across codebase. -- ~~**Align podspec versions**~~ -- Matched to pubspec versions. -- ~~**Add Linux support**~~ -- `opus_flutter_linux` package added. -- ~~**Vendor opus_dart**~~ -- Copied into repository, updated for Dart 3 and `wasm_ffi` 2.x. -- ~~**Fix `dynamic` dispatch crashes**~~ -- Eliminated all `dynamic` usage in `opus_dart` that caused `NoSuchMethodError` at runtime when `dart:ffi` extension methods were called through dynamic dispatch. -- ~~**Fix removed `dart:ffi` APIs**~~ -- Replaced `Pointer.elementAt()` with modern `operator []`. -- ~~**Fix `wasm_ffi` import paths**~~ -- Corrected from `wasm_ffi.dart` / `wasm_ffi_modules.dart` to `ffi.dart`. -- ~~**Dart 3 class modifier compliance**~~ -- Added `final` to all `Opaque` subclasses in wrapper files. +# Code Quality + +This document provides an assessment of the opus_flutter codebase's quality across multiple dimensions. + +## Summary + +```mermaid +quadrantChart + title Code Quality Assessment + x-axis Low Impact --> High Impact + y-axis Low Quality --> High Quality + quadrant-1 Strengths + quadrant-2 Monitor + quadrant-3 Low Priority + quadrant-4 Address First + Architecture: [0.85, 0.85] + Code clarity: [0.6, 0.8] + Documentation: [0.5, 0.55] + Test coverage: [0.9, 0.4] + Consistency: [0.4, 0.75] + Maintainability: [0.75, 0.75] + Build system: [0.7, 0.7] +``` + +| Dimension | Rating | Notes | +|-----------|--------|-------| +| Architecture | Good | Clean federated plugin structure | +| Code clarity | Good | Small, focused files with clear intent | +| Documentation | Fair | Public APIs documented, some packages lack detail | +| Test coverage | Fair | Unit tests for platform interface and registration logic | +| Consistency | Good | Uniform patterns across all packages | +| Maintainability | Good | Clean architecture, proper plugin registration | +| Build system | Good | Modern AGP, deterministic native builds | + +--- + +## Architecture + +**Rating: Good** + +The project follows Flutter's recommended federated plugin pattern correctly: + +- Clear separation between the app-facing package, platform interface, and platform implementations. +- Each package has a single responsibility. +- The platform interface uses `PlatformInterface` from `plugin_platform_interface` with proper token verification. +- All platform packages self-register via `dartPluginClass` and `registerWith()`. +- A single entry point (`opus_flutter_load.dart`) delegates to the platform interface without platform-specific imports. + +--- + +## File-by-File Analysis + +### Platform Interface (`opus_flutter_platform_interface`) + +| File | Lines | Quality | Notes | +|------|-------|---------|-------| +| `opus_flutter_platform_interface.dart` | 3 | Good | Clean barrel export | +| `opus_flutter_platform_interface.dart` (src) | 46 | Good | Proper PlatformInterface usage, clear docs | +| `opus_flutter_platform_unsupported.dart` | 12 | Good | Appropriate default fallback | + +No issues. Well-structured. + +### Main Package (`opus_flutter`) + +| File | Lines | Quality | Notes | +|------|-------|---------|-------| +| `opus_flutter.dart` | 4 | Good | Single clean export | +| `opus_flutter_load.dart` | 15 | Good | Simple delegation to platform interface | + +The main package is minimal by design -- it exports a single `load()` function that delegates to `OpusFlutterPlatform.instance.load()`. Platform registration is handled automatically by Flutter via `dartPluginClass`. + +### Android (`opus_flutter_android`) + +| File | Lines | Quality | Notes | +|------|-------|---------|-------| +| `opus_flutter_android.dart` | 19 | Good | Clean, self-registering via `dartPluginClass` | +| `OpusFlutterAndroidPlugin.java` | 14 | Good | Empty stub, expected for FFI-only plugins | +| `CMakeLists.txt` | 16 | Good | Modern FetchContent approach | +| `build.gradle` | 59 | Good | AGP 8.7.0, compileSdk 35, Java 17 | + +The CMakeLists.txt is well-written and concise. The build.gradle uses AGP 8.7.0 with Java 17 compatibility. Test dependencies (`junit`, `mockito`) are included but no actual tests exist yet. + +### iOS (`opus_flutter_ios`) + +| File | Lines | Quality | Notes | +|------|-------|---------|-------| +| `opus_flutter_ios.dart` | 19 | Good | Clean, self-registering via `dartPluginClass` | +| `OpusFlutterIosPlugin.swift` | 8 | Good | Minimal Swift-only stub | +| `build_xcframework.sh` | 239 | Good | Well-structured, documented, error handling | + +Uses Swift-only registration (no ObjC bridge). The build script is well-written with clear sections, error checking, and cleanup. + +### macOS (`opus_flutter_macos`) + +| File | Lines | Quality | Notes | +|------|-------|---------|-------| +| `opus_flutter_macos.dart` | 19 | Good | Clean, self-registering via `dartPluginClass` | +| `OpusFlutterMacosPlugin.swift` | 8 | Good | Minimal Swift-only stub | +| `build_xcframework.sh` | 222 | Good | Adapted from iOS script, well-structured | + +Cleanest platform implementation. Uses Swift-only registration (no ObjC bridge). + +### Web (`opus_flutter_web`) + +| File | Lines | Quality | Notes | +|------|-------|---------|-------| +| `opus_flutter_web.dart` | 42 | Good | Most complex platform impl, uses actively maintained wasm_ffi | + +The web implementation is the most involved platform package. It has to inject JavaScript, load WASM, initialize memory, and bridge through `wasm_ffi`. The migration from the unmaintained `web_ffi` to `wasm_ffi` (v2.2.0, actively maintained) has resolved the dependency risk. + +### Windows (`opus_flutter_windows`) + +| File | Lines | Quality | Notes | +|------|-------|---------|-------| +| `opus_flutter_windows.dart` | 57 | Good | Asset copying logic, proper arch detection via `Abi.current()` | + +The Windows implementation has the most runtime logic: copying DLLs from assets to a temp directory, detecting architecture via `Abi.current()`, and loading dynamically. + +### Example App + +| File | Lines | Quality | Notes | +|------|-------|---------|-------| +| `main.dart` | 171 | Good | Clean demo of encoding/decoding with share functionality | + +The example app demonstrates a complete encode/decode pipeline with file sharing. Code style is clean with proper return types and no unnecessary overrides. Uses vendored `opus_dart` via path dependency. + +### Vendored opus_dart (`opus_dart`) + +| File | Lines | Quality | Notes | +|------|-------|---------|-------| +| `opus_dart.dart` | 10 | Good | Clean barrel export | +| `proxy_ffi.dart` | 3 | Good | Conditional export: `dart:ffi` on native, `wasm_ffi/ffi.dart` on web | +| `init_ffi.dart` | 9 | Good | Native init: casts `Object` to `DynamicLibrary`, passes `malloc` as allocator | +| `init_web.dart` | 31 | Good | Web init: registers opaque types, uses `wasm_ffi` allocator. Cross-platform analysis artifacts suppressed via `ignore_for_file` | +| `opus_dart_misc.dart` | 78 | Good | Core types and `initOpus()` entry point. All fields statically typed | +| `opus_dart_encoder.dart` | 344 | Good | Simple and buffered encoder implementations | +| `opus_dart_decoder.dart` | 503 | Good | Simple and buffered decoder implementations | +| `opus_dart_packet.dart` | 95 | Good | Packet inspection utilities | +| `opus_dart_streaming.dart` | 343 | Good | Stream transformers for encode/decode pipelines | +| `wrappers/*.dart` | ~600 | Good | FFI bindings. `final` class modifier added for Dart 3 | + +The vendored package required several fixes for modern Dart compatibility: + +- **`dynamic` elimination:** The original code used `dynamic` for `ApiObject.allocator`, `ApiObject` constructor parameter, and the `_asString` helper. This caused `NoSuchMethodError` at runtime because `dart:ffi` extension methods (`Allocator.call()`, `Pointer.operator []`, `Pointer.asTypedList()`) cannot be dispatched through `dynamic`. All are now statically typed via `proxy_ffi.dart`. +- **`Pointer.elementAt()` removed:** Replaced with `Pointer` extension's `operator []` (deprecated in Dart 3.3, removed in later SDKs). +- **`wasm_ffi` 2.x API:** Corrected import paths (`ffi.dart` not `wasm_ffi.dart`), replaced `boundMemory` with `allocator`. +- **Dart 3 class modifiers:** All `Opaque` subclasses marked `final`. + +--- + +## Dart Style and Conventions + +### Positive Patterns + +- Doc comments on all public APIs using `///` syntax. +- `@override` annotations used (macOS package). +- All platform packages use `dartPluginClass` for self-registration. +- Clear package naming following Flutter conventions. + +### Issues Found + +| Issue | Location | Status | +|-------|----------|--------| +| ~~`new` keyword used in Dart 3 codebase~~ | Various files | Resolved | +| ~~`void` return with `async`~~ | `example/main.dart` | Resolved | +| ~~Empty `initState()` override~~ | `example/main.dart` | Resolved | +| ~~Missing `@override` on `load()`~~ | Platform implementations | Resolved | +| ~~Inconsistent quote style (double vs single)~~ | `example/pubspec.yaml` | Resolved | +| ~~`dynamic` causing runtime extension method failures~~ | `opus_dart` (vendored) | Resolved | +| ~~Removed `dart:ffi` API (`Pointer.elementAt`)~~ | `opus_dart` (vendored) | Resolved | +| ~~Wrong `wasm_ffi` import paths~~ | `opus_dart` (vendored) | Resolved | +| ~~Missing `final` on `Opaque` subclasses~~ | `opus_dart` (vendored) | Resolved | + +--- + +## Dependency Health + +```mermaid +graph LR + subgraph Low Risk + A[plugin_platform_interface
^2.1.8 • Active] + B[flutter_lints
^5.0.0 • Active] + C[path_provider
^2.1.5 • Active] + D[share_plus
^10.0.0 • Active] + E[platform_info
^5.0.0 • Active] + H[wasm_ffi
^2.1.0 • Active] + I[ffi
^2.1.0 • Active] + end + + subgraph Medium Risk + F[inject_js
^2.1.0 • 15 months ago] + end + + subgraph Vendored + G[opus_dart
v3.0.1 • in-repo] + end + + style A fill:#c8e6c9,color:#000 + style B fill:#c8e6c9,color:#000 + style C fill:#c8e6c9,color:#000 + style D fill:#c8e6c9,color:#000 + style E fill:#c8e6c9,color:#000 + style F fill:#fff9c4,color:#000 + style G fill:#ce93d8,color:#000 + style H fill:#c8e6c9,color:#000 + style I fill:#c8e6c9,color:#000 +``` + +| Dependency | Version | Last Updated | Risk | +|------------|---------|-------------|------| +| `plugin_platform_interface` | ^2.1.8 | Active | Low | +| `flutter_lints` | ^5.0.0 | Active | Low | +| `path_provider` | ^2.1.5 | Active | Low | +| `inject_js` | ^2.1.0 | 15 months ago | Medium | +| `wasm_ffi` | ^2.1.0 | Active | Low | +| `ffi` | ^2.1.0 | Active | Low | +| `opus_dart` | v3.0.1 | Vendored | None | +| `share_plus` | ^10.0.0 | Active | Low | +| `platform_info` | ^5.0.0 | Active | Low | + +`opus_dart` has been vendored into the repository, eliminating it as an external dependency risk. The migration from `web_ffi` to `wasm_ffi` previously eliminated the highest-risk dependency. + +--- + +## Build System Quality + +### Android +- **Approach:** CMake FetchContent (downloads opus at build time). +- **Strength:** No vendored sources; always builds from a pinned tag. +- **Risk:** Requires internet during build; network issues or removed GitHub tags will break builds. +- **AGP:** 8.7.0 with Java 17, compileSdk 35. + +### iOS +- **Approach:** Pre-built xcframework via shell script. +- **Strength:** Deterministic; no network needed at app build time. +- **Risk:** Script must be re-run manually to update opus. + +### Linux +- **Approach:** Pre-built shared libraries via Docker, stored as Flutter assets. +- **Strength:** Deterministic; no network or system dependency needed at app build time. Supports x86_64 and aarch64. +- **Risk:** None significant. Uses `ubuntu:20.04` as base image (glibc 2.31 for broad compatibility). Falls back to system `libopus.so.0` if bundled binary fails. + +### macOS +- **Approach:** Same as iOS. +- **Strength/Risk:** Same as iOS. + +### Windows +- **Approach:** Cross-compiled via Docker, DLLs stored as assets. +- **Strength:** Deterministic; no network needed at app build time. +- **Risk:** None significant. Uses `ubuntu:24.04` as base image. + +### Web +- **Approach:** Compiled via Emscripten in Docker. +- **Strength:** Deterministic output. +- **Risk:** Same Docker base image concern as Windows. + +--- + +## Lint Coverage + +| Package | Has `analysis_options.yaml` | Lint package | +|---------|----------------------------|-------------| +| opus_flutter | Yes | flutter_lints | +| opus_flutter_platform_interface | Yes | flutter_lints | +| opus_flutter_android | Yes | flutter_lints | +| opus_flutter_ios | Yes | flutter_lints | +| opus_flutter_linux | Yes | flutter_lints | +| opus_flutter_macos | Yes | flutter_lints | +| opus_flutter_web | Yes | flutter_lints | +| opus_flutter_windows | Yes | flutter_lints | +| opus_dart | Yes | `package:lints/recommended.yaml` (`constant_identifier_names` disabled for C API names) | +| example | Yes | flutter_lints | + +All Flutter packages have lint configuration referencing `package:flutter_lints/flutter.yaml`. The vendored `opus_dart` is a pure Dart package and uses `package:lints/recommended.yaml` (the non-Flutter equivalent) with `constant_identifier_names` disabled since the FFI wrappers mirror upstream C API naming conventions. All files pass `dart analyze` and `dart format`. + +--- + +## Test Coverage + +| Package | Unit Tests | Widget Tests | Integration Tests | +|---------|-----------|-------------|-------------------| +| opus_flutter | 2 tests | None | None | +| opus_flutter_platform_interface | 6 tests | None | None | +| opus_flutter_android | 3 tests | None | None | +| opus_flutter_ios | 2 tests | None | None | +| opus_flutter_linux | 2 tests | None | None | +| opus_flutter_macos | 2 tests | None | None | +| opus_flutter_web | 1 test | None | None | +| opus_flutter_windows | 2 tests | None | None | +| opus_dart | 13 tests | None | None | +| example | None | None | None | + +Unit tests cover the platform interface contract (singleton, token verification, version constant, error handling) and registration logic (`registerWith()`, class hierarchy) for each platform. Native library loading (`DynamicLibrary.open()`, `DynamicLibrary.process()`) cannot be unit tested as it requires the actual opus binary. CI runs all tests on every push. + +The vendored `opus_dart` has 13 unit tests covering pure-logic helpers (`maxSamplesPerPacket()`, error classes, enum completeness). FFI-dependent code (encoding/decoding) requires the actual opus library and would need integration-level tests. + +--- + +## Recommendations by Priority + +### High Priority + +1. ~~**Add tests**~~ -- Resolved: unit tests added for platform interface and all platform implementations. + +### Medium Priority + +2. ~~**Update Docker base images**~~ -- Resolved: Windows Dockerfile updated from `ubuntu:bionic` (18.04, EOL) to `ubuntu:24.04`. +3. ~~**Add `opus_dart` to CI**~~ -- Resolved: dedicated `analyze-opus-dart` and `test-opus-dart` jobs added. +4. ~~**Add `analysis_options.yaml` to `opus_dart`**~~ -- Resolved: uses `package:lints/recommended.yaml`. +5. ~~**Fix `opus_dart` formatting**~~ -- Resolved: all files pass `dart format`. +6. ~~**Add unit tests for `opus_dart` pure logic**~~ -- Resolved: 13 tests added. + +### Resolved + +- ~~**Add CI/CD**~~ -- GitHub Actions workflow added. +- ~~**Add `analysis_options.yaml`**~~ -- All packages have consistent lint rules. +- ~~**Evaluate web_ffi alternatives**~~ -- Migrated to `wasm_ffi` ^2.2.0. +- ~~**Check if Flutter workarounds are still needed**~~ -- Removed; all platforms use `dartPluginClass`. +- ~~**Fix Dockerfile typos**~~ -- `DEBIAN_FRONTEND` corrected. +- ~~**Simplify iOS plugin**~~ -- Swift-only, ObjC bridge removed. +- ~~**Remove `new` keyword**~~ -- Cleaned up across codebase. +- ~~**Align podspec versions**~~ -- Matched to pubspec versions. +- ~~**Add Linux support**~~ -- `opus_flutter_linux` package added. +- ~~**Vendor opus_dart**~~ -- Copied into repository, updated for Dart 3 and `wasm_ffi` 2.x. +- ~~**Fix `dynamic` dispatch crashes**~~ -- Eliminated all `dynamic` usage in `opus_dart` that caused `NoSuchMethodError` at runtime when `dart:ffi` extension methods were called through dynamic dispatch. +- ~~**Fix removed `dart:ffi` APIs**~~ -- Replaced `Pointer.elementAt()` with modern `operator []`. +- ~~**Fix `wasm_ffi` import paths**~~ -- Corrected from `wasm_ffi.dart` / `wasm_ffi_modules.dart` to `ffi.dart`. +- ~~**Dart 3 class modifier compliance**~~ -- Added `final` to all `Opaque` subclasses in wrapper files. diff --git a/docs/issues-and-improvements.md b/docs/issues-and-improvements.md index 4b8c9da..9fca5cc 100644 --- a/docs/issues-and-improvements.md +++ b/docs/issues-and-improvements.md @@ -1,198 +1,198 @@ -# Issues and Improvements - -This document lists known issues, potential risks, and suggested improvements for the opus_flutter project. - -```mermaid -graph LR - subgraph Critical - I1[No test coverage ✅] - I2[No CI/CD pipeline ✅] - end - - subgraph Moderate - I3[Stale platform workarounds ✅] - I4[web_ffi unmaintained ✅] - I5[opus_dart unmaintained ✅] - I6[Dockerfile typos ✅] - I7[Missing lint configs ✅] - end - - subgraph Minor - I8[iOS ObjC bridge ✅] - I9[Windows arch detection ✅] - I10[Example code style ✅] - I11[Dynamic return type ✅] - I12[Podspec version mismatch ✅] - end - - subgraph opus_dart Gaps - I17[Not in CI ✅] - I18[No analysis_options.yaml ✅] - I19[No tests ✅] - I20[Formatting issues ✅] - I21[No README/CHANGELOG ✅] - end - - subgraph Features - I13[Linux support ✅] - I14[Version checking ✅] - I15[Modernize Android build ✅] - I16[Reproducible builds] - end - - style I1 fill:#ef5350,color:#fff - style I2 fill:#ef5350,color:#fff - style I3 fill:#ffa726,color:#000 - style I4 fill:#ffa726,color:#000 - style I5 fill:#ffa726,color:#000 - style I6 fill:#ffa726,color:#000 - style I7 fill:#ffa726,color:#000 - style I8 fill:#fff9c4,color:#000 - style I9 fill:#fff9c4,color:#000 - style I10 fill:#fff9c4,color:#000 - style I11 fill:#fff9c4,color:#000 - style I12 fill:#fff9c4,color:#000 - style I13 fill:#90caf9,color:#000 - style I14 fill:#90caf9,color:#000 - style I15 fill:#90caf9,color:#000 - style I16 fill:#90caf9,color:#000 - style I17 fill:#ffa726,color:#000 - style I18 fill:#ffa726,color:#000 - style I19 fill:#ffa726,color:#000 - style I20 fill:#fff9c4,color:#000 - style I21 fill:#fff9c4,color:#000 -``` - -## Critical Issues - -### 1. ~~No test coverage~~ RESOLVED - -**Status:** Fixed. Added 20 unit tests across 8 packages covering: -- Platform interface contract (singleton pattern, token verification, version constant, error handling). -- Registration logic (`registerWith()`, class hierarchy) for all 6 platform implementations. -- Main package delegation (`load()` delegates to platform instance, throws on unsupported platform). -- CI workflow updated to run `flutter test` for all packages on every push. - -### 2. ~~No CI/CD pipeline~~ RESOLVED - -**Status:** Fixed. Added `.github/workflows/ci.yml` with analysis (lint + format) for all 8 packages and example app builds for Android, iOS, Linux, macOS, and Web. - ---- - -## Moderate Issues - -### 3. ~~Platform workarounds may be stale~~ RESOLVED - -**Status:** Fixed. Removed all registration workarounds and cross-platform imports. All platform packages now declare `dartPluginClass` in their `pubspec.yaml` and provide a static `registerWith()` method, letting Flutter handle registration automatically. The conditional export (`opus_flutter_ffi.dart` vs `opus_flutter_web.dart`) has been replaced by a single entry point (`opus_flutter_load.dart`). - -### 4. ~~`web_ffi` is unmaintained~~ RESOLVED - -**Status:** Migrated from `web_ffi` (v0.7.2, unmaintained) to `wasm_ffi` (v2.2.0, actively maintained). - -`wasm_ffi` is a drop-in replacement for `dart:ffi` on the web, built on top of `web_ffi` with active maintenance and modern Dart support. The API change was minimal -- `EmscriptenModule.compile` now takes a `Map` with a `'wasmBinary'` key instead of raw bytes. - -### 5. ~~`opus_dart` is not actively maintained~~ RESOLVED - -**Status:** Fixed. Vendored `opus_dart` (v3.0.1) from [EPNW/opus_dart](https://github.com/EPNW/opus_dart) directly into the repository at `opus_dart/`. This eliminates the external dependency and allows direct maintenance. The example app now uses a path dependency instead of pulling from pub.dev. - -Additionally, the vendored package required several fixes to work with modern Dart and the correct `wasm_ffi` version: - -- **Dart 3 class modifiers:** All `Opaque` subclasses in wrapper files now use the `final` modifier, required because `dart:ffi`'s `Opaque` is a `base` class. -- **`wasm_ffi` 2.x import paths:** The package exports `ffi.dart`, not `wasm_ffi.dart`. The non-existent `wasm_ffi_modules.dart` import was removed (`registerOpaqueType` is exported from `ffi.dart` directly). -- **Deprecated `boundMemory`:** Replaced with `allocator` on `wasm_ffi`'s `DynamicLibrary`. -- **Removed `Pointer.elementAt()`:** Deprecated in Dart 3.3 and removed in later SDKs. Replaced with `Pointer` extension's `operator []`. -- **Eliminated `dynamic` dispatch:** The original code used `dynamic` for several fields and parameters to bridge `dart:ffi` and `web_ffi` types. This caused runtime `NoSuchMethodError` crashes because many `dart:ffi` APIs (`Allocator.call()`, `Pointer[].operator []`, `Pointer.asTypedList()`, `DynamicLibrary.lookupFunction<>()`) are **extension methods** that cannot be dispatched through `dynamic`. All fields are now statically typed via `proxy_ffi.dart`. -- **`initOpus()` accepts `Object`:** Callers no longer need to cast from `opus_flutter.load()`'s `Future` return type; the platform-specific `createApiObject()` handles the cast internally. -- **Cleaned up stale headers:** Replaced "AUTOMATICALLY GENERATED FILE. DO NOT MODIFY." with "Vendored from" attribution, removed dead `subtype_of_sealed_class` lint suppressions. - -### 6. ~~Dockerfile typos~~ RESOLVED - -**Status:** Fixed. Both `opus_flutter_web/Dockerfile` and `opus_flutter_windows/Dockerfile` now use the correct `DEBIAN_FRONTEND` spelling. - -### 7. ~~Missing `analysis_options.yaml` in most packages~~ RESOLVED - -**Status:** Fixed. All 7 packages and the example app now have `analysis_options.yaml` referencing `package:flutter_lints/flutter.yaml`. - ---- - -## Minor Issues - -### 8. ~~iOS plugin uses ObjC bridge unnecessarily~~ RESOLVED - -**Status:** Fixed. Simplified to a single Swift file (`OpusFlutterIosPlugin.swift`), matching the macOS implementation. - -### 9. ~~Windows implementation has architecture detection fragility~~ RESOLVED - -**Status:** Fixed. Replaced `Platform.version.contains('x64')` with `Abi.current() == Abi.windowsX64` from `dart:ffi`, which is the proper API for architecture detection. - -### 10. ~~Example app code style issues~~ RESOLVED - -**Status:** Fixed. `_share` now returns `Future`, empty `initState()` override removed, and MIME type consistently uses `'audio/wav'`. - -### 11. ~~`load()` returns `Future`~~ RESOLVED - -**Status:** Fixed. Changed `Future` to `Future` across the platform interface and all platform implementations. This enforces non-null returns and improves type safety. - -### 12. ~~Podspec versions don't match pubspec versions~~ RESOLVED - -**Status:** Fixed. iOS podspec now matches pubspec at `3.0.1`, macOS podspec matches at `3.0.0`. - ---- - -## Vendored opus_dart Gaps - -### 17. ~~`opus_dart` not included in CI~~ RESOLVED - -**Status:** Fixed. Added a dedicated `analyze-opus-dart` job (using `dart analyze` and `dart format` since it's a pure Dart package, not a Flutter package) and a `test-opus-dart` job to the CI workflow. - -### 18. ~~`opus_dart` missing `analysis_options.yaml`~~ RESOLVED - -**Status:** Fixed. Added `analysis_options.yaml` referencing `package:lints/recommended.yaml` with `constant_identifier_names` disabled (the FFI wrappers intentionally mirror C API naming like `OPUS_SET_BITRATE_REQUEST`). Added `lints` and `test` as dev dependencies. - -### 19. ~~`opus_dart` has no tests~~ RESOLVED - -**Status:** Fixed. Added 13 unit tests covering pure-logic helpers that don't require the native opus binary: -- `maxDataBytes` constant value. -- `maxSamplesPerPacket()` across standard sample rates and channel counts. -- `OpusDestroyedError` encoder/decoder message content. -- `OpusException` error code storage. -- `Application` and `FrameTime` enum completeness. - -FFI-dependent code (encoding, decoding, streaming, packet inspection) requires the actual opus library and would need integration-level tests. - -### 20. ~~`opus_dart` has formatting issues~~ RESOLVED - -**Status:** Fixed. All files now pass `dart format --set-exit-if-changed`. Additionally, all 85 lint issues from `dart analyze` were resolved (library name removal, unnecessary `this`/`const`, adjacent string concatenation, local identifier naming). - -### 21. ~~`opus_dart` missing README and CHANGELOG~~ RESOLVED - -**Status:** Fixed. Added `README.md` covering the Dart-friendly API, raw bindings, initialization with `opus_flutter`, cross-platform FFI via `proxy_ffi.dart`, and encoder CTL usage. Updated from the original EPNW README to reflect the vendored state (opus 1.5.2, `wasm_ffi` instead of `web_ffi`, `publish_to: none`, no stale external links). CHANGELOG omitted since version history is tracked in git. - ---- - -## Feature Improvements - -### 13. ~~Add Linux support~~ RESOLVED - -**Status:** Fixed. Created `opus_flutter_linux` package with bundled pre-built opus shared libraries for x86_64 and aarch64. The libraries are built via Docker (Ubuntu 20.04 base for glibc 2.31 compatibility) and stored as Flutter assets (`libopus_x86_64.so.blob`, `libopus_aarch64.so.blob`). At runtime, the correct binary is detected via `Abi.current()`, copied from `rootBundle` to a temp directory, and loaded with `DynamicLibrary.open()`. Falls back to system `libopus.so.0` on failure. This completes the Flutter desktop story -- all six platforms are now supported with bundled binaries. - -### 14. ~~Add version checking~~ RESOLVED - -**Status:** Fixed. Added `static const String opusVersion = '1.5.2'` to `OpusFlutterPlatform` in the platform interface. All platforms (Android, iOS, macOS, Windows, Web) bundle opus v1.5.2. - -### 15. ~~Modernize Android build configuration~~ RESOLVED - -**Status:** Fixed. Updated the plugin's `build.gradle`: -- AGP updated from `7.3.0` to `8.7.0`. -- `compileSdk` updated from `34` to `35`. -- Java compatibility updated from `VERSION_1_8` to `VERSION_17`. -- `namespace` set directly (removed conditional check). -- Example app also updated: AGP `8.7.0`, Gradle `8.9`, Java `VERSION_17`. - -### 16. Bundle opus sources for reproducible builds - -Currently, Android fetches opus at build time via `FetchContent`, iOS/macOS use a build script that clones from GitHub, and Windows/Web use Docker. If GitHub is unavailable or the tag is removed, builds will fail. - -**Recommendation:** -- Consider vendoring a source archive or using a git submodule for opus across all platforms. -- Pin checksums for downloaded archives. +# Issues and Improvements + +This document lists known issues, potential risks, and suggested improvements for the opus_flutter project. + +```mermaid +graph LR + subgraph Critical + I1[No test coverage ✅] + I2[No CI/CD pipeline ✅] + end + + subgraph Moderate + I3[Stale platform workarounds ✅] + I4[web_ffi unmaintained ✅] + I5[opus_dart unmaintained ✅] + I6[Dockerfile typos ✅] + I7[Missing lint configs ✅] + end + + subgraph Minor + I8[iOS ObjC bridge ✅] + I9[Windows arch detection ✅] + I10[Example code style ✅] + I11[Dynamic return type ✅] + I12[Podspec version mismatch ✅] + end + + subgraph opus_dart Gaps + I17[Not in CI ✅] + I18[No analysis_options.yaml ✅] + I19[No tests ✅] + I20[Formatting issues ✅] + I21[No README/CHANGELOG ✅] + end + + subgraph Features + I13[Linux support ✅] + I14[Version checking ✅] + I15[Modernize Android build ✅] + I16[Reproducible builds] + end + + style I1 fill:#ef5350,color:#fff + style I2 fill:#ef5350,color:#fff + style I3 fill:#ffa726,color:#000 + style I4 fill:#ffa726,color:#000 + style I5 fill:#ffa726,color:#000 + style I6 fill:#ffa726,color:#000 + style I7 fill:#ffa726,color:#000 + style I8 fill:#fff9c4,color:#000 + style I9 fill:#fff9c4,color:#000 + style I10 fill:#fff9c4,color:#000 + style I11 fill:#fff9c4,color:#000 + style I12 fill:#fff9c4,color:#000 + style I13 fill:#90caf9,color:#000 + style I14 fill:#90caf9,color:#000 + style I15 fill:#90caf9,color:#000 + style I16 fill:#90caf9,color:#000 + style I17 fill:#ffa726,color:#000 + style I18 fill:#ffa726,color:#000 + style I19 fill:#ffa726,color:#000 + style I20 fill:#fff9c4,color:#000 + style I21 fill:#fff9c4,color:#000 +``` + +## Critical Issues + +### 1. ~~No test coverage~~ RESOLVED + +**Status:** Fixed. Added 20 unit tests across 8 packages covering: +- Platform interface contract (singleton pattern, token verification, version constant, error handling). +- Registration logic (`registerWith()`, class hierarchy) for all 6 platform implementations. +- Main package delegation (`load()` delegates to platform instance, throws on unsupported platform). +- CI workflow updated to run `flutter test` for all packages on every push. + +### 2. ~~No CI/CD pipeline~~ RESOLVED + +**Status:** Fixed. Added `.github/workflows/ci.yml` with analysis (lint + format) for all 8 packages and example app builds for Android, iOS, Linux, macOS, and Web. + +--- + +## Moderate Issues + +### 3. ~~Platform workarounds may be stale~~ RESOLVED + +**Status:** Fixed. Removed all registration workarounds and cross-platform imports. All platform packages now declare `dartPluginClass` in their `pubspec.yaml` and provide a static `registerWith()` method, letting Flutter handle registration automatically. The conditional export (`opus_flutter_ffi.dart` vs `opus_flutter_web.dart`) has been replaced by a single entry point (`opus_flutter_load.dart`). + +### 4. ~~`web_ffi` is unmaintained~~ RESOLVED + +**Status:** Migrated from `web_ffi` (v0.7.2, unmaintained) to `wasm_ffi` (v2.2.0, actively maintained). + +`wasm_ffi` is a drop-in replacement for `dart:ffi` on the web, built on top of `web_ffi` with active maintenance and modern Dart support. The API change was minimal -- `EmscriptenModule.compile` now takes a `Map` with a `'wasmBinary'` key instead of raw bytes. + +### 5. ~~`opus_dart` is not actively maintained~~ RESOLVED + +**Status:** Fixed. Vendored `opus_dart` (v3.0.1) from [EPNW/opus_dart](https://github.com/EPNW/opus_dart) directly into the repository at `opus_dart/`. This eliminates the external dependency and allows direct maintenance. The example app now uses a path dependency instead of pulling from pub.dev. + +Additionally, the vendored package required several fixes to work with modern Dart and the correct `wasm_ffi` version: + +- **Dart 3 class modifiers:** All `Opaque` subclasses in wrapper files now use the `final` modifier, required because `dart:ffi`'s `Opaque` is a `base` class. +- **`wasm_ffi` 2.x import paths:** The package exports `ffi.dart`, not `wasm_ffi.dart`. The non-existent `wasm_ffi_modules.dart` import was removed (`registerOpaqueType` is exported from `ffi.dart` directly). +- **Deprecated `boundMemory`:** Replaced with `allocator` on `wasm_ffi`'s `DynamicLibrary`. +- **Removed `Pointer.elementAt()`:** Deprecated in Dart 3.3 and removed in later SDKs. Replaced with `Pointer` extension's `operator []`. +- **Eliminated `dynamic` dispatch:** The original code used `dynamic` for several fields and parameters to bridge `dart:ffi` and `web_ffi` types. This caused runtime `NoSuchMethodError` crashes because many `dart:ffi` APIs (`Allocator.call()`, `Pointer[].operator []`, `Pointer.asTypedList()`, `DynamicLibrary.lookupFunction<>()`) are **extension methods** that cannot be dispatched through `dynamic`. All fields are now statically typed via `proxy_ffi.dart`. +- **`initOpus()` accepts `Object`:** Callers no longer need to cast from `opus_flutter.load()`'s `Future` return type; the platform-specific `createApiObject()` handles the cast internally. +- **Cleaned up stale headers:** Replaced "AUTOMATICALLY GENERATED FILE. DO NOT MODIFY." with "Vendored from" attribution, removed dead `subtype_of_sealed_class` lint suppressions. + +### 6. ~~Dockerfile typos~~ RESOLVED + +**Status:** Fixed. Both `opus_flutter_web/Dockerfile` and `opus_flutter_windows/Dockerfile` now use the correct `DEBIAN_FRONTEND` spelling. + +### 7. ~~Missing `analysis_options.yaml` in most packages~~ RESOLVED + +**Status:** Fixed. All 7 packages and the example app now have `analysis_options.yaml` referencing `package:flutter_lints/flutter.yaml`. + +--- + +## Minor Issues + +### 8. ~~iOS plugin uses ObjC bridge unnecessarily~~ RESOLVED + +**Status:** Fixed. Simplified to a single Swift file (`OpusFlutterIosPlugin.swift`), matching the macOS implementation. + +### 9. ~~Windows implementation has architecture detection fragility~~ RESOLVED + +**Status:** Fixed. Replaced `Platform.version.contains('x64')` with `Abi.current() == Abi.windowsX64` from `dart:ffi`, which is the proper API for architecture detection. + +### 10. ~~Example app code style issues~~ RESOLVED + +**Status:** Fixed. `_share` now returns `Future`, empty `initState()` override removed, and MIME type consistently uses `'audio/wav'`. + +### 11. ~~`load()` returns `Future`~~ RESOLVED + +**Status:** Fixed. Changed `Future` to `Future` across the platform interface and all platform implementations. This enforces non-null returns and improves type safety. + +### 12. ~~Podspec versions don't match pubspec versions~~ RESOLVED + +**Status:** Fixed. iOS podspec now matches pubspec at `3.0.1`, macOS podspec matches at `3.0.0`. + +--- + +## Vendored opus_dart Gaps + +### 17. ~~`opus_dart` not included in CI~~ RESOLVED + +**Status:** Fixed. Added a dedicated `analyze-opus-dart` job (using `dart analyze` and `dart format` since it's a pure Dart package, not a Flutter package) and a `test-opus-dart` job to the CI workflow. + +### 18. ~~`opus_dart` missing `analysis_options.yaml`~~ RESOLVED + +**Status:** Fixed. Added `analysis_options.yaml` referencing `package:lints/recommended.yaml` with `constant_identifier_names` disabled (the FFI wrappers intentionally mirror C API naming like `OPUS_SET_BITRATE_REQUEST`). Added `lints` and `test` as dev dependencies. + +### 19. ~~`opus_dart` has no tests~~ RESOLVED + +**Status:** Fixed. Added 13 unit tests covering pure-logic helpers that don't require the native opus binary: +- `maxDataBytes` constant value. +- `maxSamplesPerPacket()` across standard sample rates and channel counts. +- `OpusDestroyedError` encoder/decoder message content. +- `OpusException` error code storage. +- `Application` and `FrameTime` enum completeness. + +FFI-dependent code (encoding, decoding, streaming, packet inspection) requires the actual opus library and would need integration-level tests. + +### 20. ~~`opus_dart` has formatting issues~~ RESOLVED + +**Status:** Fixed. All files now pass `dart format --set-exit-if-changed`. Additionally, all 85 lint issues from `dart analyze` were resolved (library name removal, unnecessary `this`/`const`, adjacent string concatenation, local identifier naming). + +### 21. ~~`opus_dart` missing README and CHANGELOG~~ RESOLVED + +**Status:** Fixed. Added `README.md` covering the Dart-friendly API, raw bindings, initialization with `opus_flutter`, cross-platform FFI via `proxy_ffi.dart`, and encoder CTL usage. Updated from the original EPNW README to reflect the vendored state (opus 1.5.2, `wasm_ffi` instead of `web_ffi`, `publish_to: none`, no stale external links). CHANGELOG omitted since version history is tracked in git. + +--- + +## Feature Improvements + +### 13. ~~Add Linux support~~ RESOLVED + +**Status:** Fixed. Created `opus_flutter_linux` package with bundled pre-built opus shared libraries for x86_64 and aarch64. The libraries are built via Docker (Ubuntu 20.04 base for glibc 2.31 compatibility) and stored as Flutter assets (`libopus_x86_64.so.blob`, `libopus_aarch64.so.blob`). At runtime, the correct binary is detected via `Abi.current()`, copied from `rootBundle` to a temp directory, and loaded with `DynamicLibrary.open()`. Falls back to system `libopus.so.0` on failure. This completes the Flutter desktop story -- all six platforms are now supported with bundled binaries. + +### 14. ~~Add version checking~~ RESOLVED + +**Status:** Fixed. Added `static const String opusVersion = '1.5.2'` to `OpusFlutterPlatform` in the platform interface. All platforms (Android, iOS, macOS, Windows, Web) bundle opus v1.5.2. + +### 15. ~~Modernize Android build configuration~~ RESOLVED + +**Status:** Fixed. Updated the plugin's `build.gradle`: +- AGP updated from `7.3.0` to `8.7.0`. +- `compileSdk` updated from `34` to `35`. +- Java compatibility updated from `VERSION_1_8` to `VERSION_17`. +- `namespace` set directly (removed conditional check). +- Example app also updated: AGP `8.7.0`, Gradle `8.9`, Java `VERSION_17`. + +### 16. Bundle opus sources for reproducible builds + +Currently, Android fetches opus at build time via `FetchContent`, iOS/macOS use a build script that clones from GitHub, and Windows/Web use Docker. If GitHub is unavailable or the tag is removed, builds will fail. + +**Recommendation:** +- Consider vendoring a source archive or using a git submodule for opus across all platforms. +- Pin checksums for downloaded archives. diff --git a/opus_dart/analysis_options.yaml b/opus_dart/analysis_options.yaml index ee359d9..8a9417d 100644 --- a/opus_dart/analysis_options.yaml +++ b/opus_dart/analysis_options.yaml @@ -1,6 +1,6 @@ -include: package:lints/recommended.yaml - -linter: - rules: - # Vendored FFI wrappers mirror C API naming (OPUS_SET_BITRATE_REQUEST, etc.) - constant_identifier_names: false +include: package:lints/recommended.yaml + +linter: + rules: + # Vendored FFI wrappers mirror C API naming (OPUS_SET_BITRATE_REQUEST, etc.) + constant_identifier_names: false diff --git a/opus_dart/pubspec.yaml b/opus_dart/pubspec.yaml index 16edb11..a84c841 100644 --- a/opus_dart/pubspec.yaml +++ b/opus_dart/pubspec.yaml @@ -1,5 +1,5 @@ name: opus_codec_dart -version: 3.0.4 +version: 3.0.5 description: > Wraps libopus in dart, and additionally provides a dart friendly API for encoding and decoding. diff --git a/opus_flutter/CHANGELOG.md b/opus_flutter/CHANGELOG.md index 17fa8c2..4414cd3 100644 --- a/opus_flutter/CHANGELOG.md +++ b/opus_flutter/CHANGELOG.md @@ -1,55 +1,66 @@ -## 3.0.3 - -* Depend on `opus_flutter_ios:3.0.1` and `opus_flutter_android:3.0.1` - - -## 3.0.2 - -* libopus 1.3.1 -* Updated example - - -## 3.0.1 - -* libopus 1.3.1 -* Depend on newer opus_flutter_web version - - -## 3.0.0 - -* libopus 1.3.1 -* Web support using [`web_ffi`](https://pub.dev/packages/web_ffi) - - -## 2.0.1 - -* libopus 1.3.1 -* Minor formating fixes - - -## 2.0.0 - -* libopus 1.3.1 -* Updated for opus_dart 2.0.1, supporting null safety by this -* Moved to federal plugin structure -* Removed re-export of the opus_dart library, see README - - -## 1.1.0 - -* libopus 1.3.1 -* Update for opus_dart 1.0.4 -* Migrated to new plugin map in pubspec.yaml -* Included iOS binaries - - -## 1.0.1 - -* libopus 1.3.1 -* Update for opus_dart 1.0.3 - - -## 1.0.0 - -* libopus 1.3.1 +## 3.0.5 + +* Bump version + + +## 3.0.4 + +* Add Swift Package Manager support to `opus_codec_ios` and `opus_codec_macos` +* Bump all package versions + + +## 3.0.3 + +* Depend on `opus_flutter_ios:3.0.1` and `opus_flutter_android:3.0.1` + + +## 3.0.2 + +* libopus 1.3.1 +* Updated example + + +## 3.0.1 + +* libopus 1.3.1 +* Depend on newer opus_flutter_web version + + +## 3.0.0 + +* libopus 1.3.1 +* Web support using [`web_ffi`](https://pub.dev/packages/web_ffi) + + +## 2.0.1 + +* libopus 1.3.1 +* Minor formating fixes + + +## 2.0.0 + +* libopus 1.3.1 +* Updated for opus_dart 2.0.1, supporting null safety by this +* Moved to federal plugin structure +* Removed re-export of the opus_dart library, see README + + +## 1.1.0 + +* libopus 1.3.1 +* Update for opus_dart 1.0.4 +* Migrated to new plugin map in pubspec.yaml +* Included iOS binaries + + +## 1.0.1 + +* libopus 1.3.1 +* Update for opus_dart 1.0.3 + + +## 1.0.0 + +* libopus 1.3.1 * Initial release \ No newline at end of file diff --git a/opus_flutter/pubspec.yaml b/opus_flutter/pubspec.yaml index ca41e08..28fba02 100644 --- a/opus_flutter/pubspec.yaml +++ b/opus_flutter/pubspec.yaml @@ -1,7 +1,7 @@ name: opus_codec description: Loads a DynamicLibrary of opus for usage with opus_codec_dart on flutter platforms. repository: https://github.com/Corkscrews/opus_flutter/tree/master/opus_flutter -version: 3.0.4 +version: 3.0.5 environment: sdk: '>=3.4.0 <4.0.0' diff --git a/opus_flutter_android/CHANGELOG.md b/opus_flutter_android/CHANGELOG.md index ce4ff6e..88d54ab 100644 --- a/opus_flutter_android/CHANGELOG.md +++ b/opus_flutter_android/CHANGELOG.md @@ -1,8 +1,16 @@ -## 3.0.1 -* Upgraded gradle build files - -## 3.0.0 -* Adopt `opus_flutter_platform_interface 3.0.0` - -## 2.0.0 +## 3.0.5 + +* Bump version + +## 3.0.4 + +* Bump version + +## 3.0.1 +* Upgraded gradle build files + +## 3.0.0 +* Adopt `opus_flutter_platform_interface 3.0.0` + +## 2.0.0 * Initial release in federal plugin structure \ No newline at end of file diff --git a/opus_flutter_android/pubspec.yaml b/opus_flutter_android/pubspec.yaml index cce3bdf..c5bc011 100644 --- a/opus_flutter_android/pubspec.yaml +++ b/opus_flutter_android/pubspec.yaml @@ -1,7 +1,7 @@ name: opus_codec_android description: Android implementation of the opus_codec plugin. repository: https://github.com/Corkscrews/opus_flutter/tree/master/opus_flutter_android -version: 3.0.4 +version: 3.0.5 environment: sdk: '>=3.4.0 <4.0.0' diff --git a/opus_flutter_ios/CHANGELOG.md b/opus_flutter_ios/CHANGELOG.md index 6a897af..b977a39 100644 --- a/opus_flutter_ios/CHANGELOG.md +++ b/opus_flutter_ios/CHANGELOG.md @@ -1,13 +1,25 @@ -## 3.0.1 - -* Using new opus.xcframework - - -## 3.0.0 - -* Adopt `opus_flutter_platform_interface 3.0.0` - - -## 2.0.0 - +## 3.0.5 + +* Bump version + + +## 3.0.4 + +* Add Swift Package Manager support +* Migrate plugin source files to SPM-compatible directory layout +* Retain CocoaPods compatibility + + +## 3.0.1 + +* Using new opus.xcframework + + +## 3.0.0 + +* Adopt `opus_flutter_platform_interface 3.0.0` + + +## 2.0.0 + * Initial release in federal plugin structure \ No newline at end of file diff --git a/opus_flutter_ios/pubspec.yaml b/opus_flutter_ios/pubspec.yaml index f562867..2c47451 100644 --- a/opus_flutter_ios/pubspec.yaml +++ b/opus_flutter_ios/pubspec.yaml @@ -1,7 +1,7 @@ name: opus_codec_ios description: iOS implementation of the opus_codec plugin. repository: https://github.com/Corkscrews/opus_flutter/tree/master/opus_flutter_ios -version: 3.0.4 +version: 3.0.5 environment: sdk: '>=3.4.0 <4.0.0' diff --git a/opus_flutter_linux/CHANGELOG.md b/opus_flutter_linux/CHANGELOG.md index 63d92c9..f2549c8 100644 --- a/opus_flutter_linux/CHANGELOG.md +++ b/opus_flutter_linux/CHANGELOG.md @@ -1,3 +1,13 @@ -## 3.0.0 - -* Initial release. Loads opus from the system library (`libopus.so.0`). +## 3.0.5 + +* Bump version + + +## 3.0.4 + +* Bump version + + +## 3.0.0 + +* Initial release. Loads opus from the system library (`libopus.so.0`). diff --git a/opus_flutter_linux/pubspec.yaml b/opus_flutter_linux/pubspec.yaml index 84180d4..c4942f9 100644 --- a/opus_flutter_linux/pubspec.yaml +++ b/opus_flutter_linux/pubspec.yaml @@ -1,7 +1,7 @@ name: opus_codec_linux description: Linux implementation of the opus_codec plugin. repository: https://github.com/Corkscrews/opus_flutter/tree/master/opus_flutter_linux -version: 3.0.4 +version: 3.0.5 environment: sdk: '>=3.4.0 <4.0.0' diff --git a/opus_flutter_macos/CHANGELOG.md b/opus_flutter_macos/CHANGELOG.md index 654b027..3ba806d 100644 --- a/opus_flutter_macos/CHANGELOG.md +++ b/opus_flutter_macos/CHANGELOG.md @@ -1,4 +1,16 @@ -## 3.0.0 - -* Initial release with macOS support using opus.xcframework -* Adopt `opus_flutter_platform_interface 3.0.0` +## 3.0.5 + +* Bump version + + +## 3.0.4 + +* Add Swift Package Manager support +* Migrate plugin source files to SPM-compatible directory layout +* Retain CocoaPods compatibility + + +## 3.0.0 + +* Initial release with macOS support using opus.xcframework +* Adopt `opus_flutter_platform_interface 3.0.0` diff --git a/opus_flutter_macos/pubspec.yaml b/opus_flutter_macos/pubspec.yaml index 83cf19c..e088376 100644 --- a/opus_flutter_macos/pubspec.yaml +++ b/opus_flutter_macos/pubspec.yaml @@ -1,7 +1,7 @@ name: opus_codec_macos description: macOS implementation of the opus_codec plugin. repository: https://github.com/Corkscrews/opus_flutter/tree/master/opus_flutter_macos -version: 3.0.4 +version: 3.0.5 environment: sdk: '>=3.4.0 <4.0.0' diff --git a/opus_flutter_platform_interface/CHANGELOG.md b/opus_flutter_platform_interface/CHANGELOG.md index e620627..078e746 100644 --- a/opus_flutter_platform_interface/CHANGELOG.md +++ b/opus_flutter_platform_interface/CHANGELOG.md @@ -1,5 +1,13 @@ -## 3.0.0 -* Necessary changes for web support - -## 2.0.0 +## 3.0.5 + +* Bump version + +## 3.0.4 + +* Bump version + +## 3.0.0 +* Necessary changes for web support + +## 2.0.0 * Initial release in federal plugin structure \ No newline at end of file diff --git a/opus_flutter_platform_interface/analysis_options.yaml b/opus_flutter_platform_interface/analysis_options.yaml index 8e4c4f5..f9b3034 100644 --- a/opus_flutter_platform_interface/analysis_options.yaml +++ b/opus_flutter_platform_interface/analysis_options.yaml @@ -1 +1 @@ -include: package:flutter_lints/flutter.yaml +include: package:flutter_lints/flutter.yaml diff --git a/opus_flutter_platform_interface/pubspec.yaml b/opus_flutter_platform_interface/pubspec.yaml index 2941b41..eb13f1e 100644 --- a/opus_flutter_platform_interface/pubspec.yaml +++ b/opus_flutter_platform_interface/pubspec.yaml @@ -3,7 +3,7 @@ description: A common platform interface for the opus_codec plugin. repository: https://github.com/Corkscrews/opus_flutter/tree/master/opus_flutter_platform_interface # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 3.0.4 +version: 3.0.5 environment: sdk: '>=3.4.0 <4.0.0' diff --git a/opus_flutter_web/CHANGELOG.md b/opus_flutter_web/CHANGELOG.md index 51adf28..15b1ccc 100644 --- a/opus_flutter_web/CHANGELOG.md +++ b/opus_flutter_web/CHANGELOG.md @@ -1,11 +1,19 @@ -## 3.0.3 -* Depend on newer web_ffi version - -## 3.0.2 -* More README fixes - -## 3.0.1 -* README fix - -## 3.0.0 +## 3.0.5 + +* Bump version + +## 3.0.4 + +* Bump version + +## 3.0.3 +* Depend on newer web_ffi version + +## 3.0.2 +* More README fixes + +## 3.0.1 +* README fix + +## 3.0.0 * Initial release in federal plugin structure \ No newline at end of file diff --git a/opus_flutter_web/analysis_options.yaml b/opus_flutter_web/analysis_options.yaml index 8e4c4f5..f9b3034 100644 --- a/opus_flutter_web/analysis_options.yaml +++ b/opus_flutter_web/analysis_options.yaml @@ -1 +1 @@ -include: package:flutter_lints/flutter.yaml +include: package:flutter_lints/flutter.yaml diff --git a/opus_flutter_web/pubspec.yaml b/opus_flutter_web/pubspec.yaml index 0631a61..a11dbc6 100644 --- a/opus_flutter_web/pubspec.yaml +++ b/opus_flutter_web/pubspec.yaml @@ -1,7 +1,7 @@ name: opus_codec_web description: Web implementation of the opus_codec plugin. repository: https://github.com/Corkscrews/opus_flutter/tree/master/opus_flutter_web -version: 3.0.4 +version: 3.0.5 environment: sdk: '>=3.4.0 <4.0.0' diff --git a/opus_flutter_windows/CHANGELOG.md b/opus_flutter_windows/CHANGELOG.md index caa66b0..fac93fb 100644 --- a/opus_flutter_windows/CHANGELOG.md +++ b/opus_flutter_windows/CHANGELOG.md @@ -1,5 +1,13 @@ -## 3.0.0 -* Adopt `opus_flutter_platform_interface 3.0.0` -* -## 2.0.0 +## 3.0.5 + +* Bump version + +## 3.0.4 + +* Bump version + +## 3.0.0 +* Adopt `opus_flutter_platform_interface 3.0.0` + +## 2.0.0 * Initial release in federal plugin structure \ No newline at end of file diff --git a/opus_flutter_windows/pubspec.yaml b/opus_flutter_windows/pubspec.yaml index 1c5b2f0..ebaa001 100644 --- a/opus_flutter_windows/pubspec.yaml +++ b/opus_flutter_windows/pubspec.yaml @@ -1,7 +1,7 @@ name: opus_codec_windows description: Windows implementation of the opus_codec plugin. repository: https://github.com/Corkscrews/opus_flutter/tree/master/opus_flutter_windows -version: 3.0.4 +version: 3.0.5 environment: sdk: '>=3.4.0 <4.0.0' From cbaa537cf8b634f0619dab6771c15e481691890f Mon Sep 17 00:00:00 2001 From: Pedro Paulo de Amorim Date: Tue, 24 Feb 2026 23:49:58 +0000 Subject: [PATCH 02/21] fix: add missing repo/changelog to opus_dart and fix podspec versions --- CHANGELOG.md | 2 + opus_dart/CHANGELOG.md | 59 +++++++++++++++++++ opus_dart/pubspec.yaml | 1 + opus_flutter/CHANGELOG.md | 2 +- opus_flutter_android/CHANGELOG.md | 2 +- opus_flutter_ios/CHANGELOG.md | 3 +- opus_flutter_ios/ios/opus_codec_ios.podspec | 4 +- opus_flutter_linux/CHANGELOG.md | 2 +- opus_flutter_macos/CHANGELOG.md | 3 +- .../macos/opus_codec_macos.podspec | 4 +- opus_flutter_platform_interface/CHANGELOG.md | 2 +- opus_flutter_web/CHANGELOG.md | 2 +- opus_flutter_windows/CHANGELOG.md | 2 +- 13 files changed, 76 insertions(+), 12 deletions(-) create mode 100644 opus_dart/CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 74c6b5e..81b688a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## 3.0.5 +* Add `repository` field and `CHANGELOG.md` to `opus_codec_dart` +* Fix podspec version mismatch in `opus_codec_ios` and `opus_codec_macos` * Bump all package versions | Package | Version | diff --git a/opus_dart/CHANGELOG.md b/opus_dart/CHANGELOG.md new file mode 100644 index 0000000..86d36b4 --- /dev/null +++ b/opus_dart/CHANGELOG.md @@ -0,0 +1,59 @@ +## 3.0.5 + +* Add `repository` field to pubspec +* Add `CHANGELOG.md` + + +## 3.0.4 + +* Bump version + + +## 3.0.3 + +* Depend on newer `wasm_ffi` version for web support + + +## 3.0.2 + +* libopus 1.3.1 + + +## 3.0.1 + +* libopus 1.3.1 + + +## 3.0.0 + +* Migrate to `opus_flutter` namespace +* Web support using [`wasm_ffi`](https://pub.dev/packages/wasm_ffi) +* libopus 1.3.1 + + +## 2.0.1 + +* libopus 1.3.1 +* Minor formatting fixes + + +## 2.0.0 + +* libopus 1.3.1 +* Null safety support + + +## 1.0.4 + +* libopus 1.3.1 + + +## 1.0.3 + +* libopus 1.3.1 + + +## 1.0.0 + +* libopus 1.3.1 +* Initial release diff --git a/opus_dart/pubspec.yaml b/opus_dart/pubspec.yaml index a84c841..c4b4baa 100644 --- a/opus_dart/pubspec.yaml +++ b/opus_dart/pubspec.yaml @@ -3,6 +3,7 @@ version: 3.0.5 description: > Wraps libopus in dart, and additionally provides a dart friendly API for encoding and decoding. +repository: https://github.com/Corkscrews/opus_flutter/tree/master/opus_dart environment: sdk: '>=3.4.0 <4.0.0' diff --git a/opus_flutter/CHANGELOG.md b/opus_flutter/CHANGELOG.md index 4414cd3..c65b784 100644 --- a/opus_flutter/CHANGELOG.md +++ b/opus_flutter/CHANGELOG.md @@ -1,6 +1,6 @@ ## 3.0.5 -* Bump version +* Bump all package versions ## 3.0.4 diff --git a/opus_flutter_android/CHANGELOG.md b/opus_flutter_android/CHANGELOG.md index 88d54ab..aac2f98 100644 --- a/opus_flutter_android/CHANGELOG.md +++ b/opus_flutter_android/CHANGELOG.md @@ -1,6 +1,6 @@ ## 3.0.5 -* Bump version +* Bump all package versions ## 3.0.4 diff --git a/opus_flutter_ios/CHANGELOG.md b/opus_flutter_ios/CHANGELOG.md index b977a39..cc1d4ba 100644 --- a/opus_flutter_ios/CHANGELOG.md +++ b/opus_flutter_ios/CHANGELOG.md @@ -1,6 +1,7 @@ ## 3.0.5 -* Bump version +* Fix podspec version to match pubspec +* Bump all package versions ## 3.0.4 diff --git a/opus_flutter_ios/ios/opus_codec_ios.podspec b/opus_flutter_ios/ios/opus_codec_ios.podspec index 1c1aafe..0c7ee8e 100644 --- a/opus_flutter_ios/ios/opus_codec_ios.podspec +++ b/opus_flutter_ios/ios/opus_codec_ios.podspec @@ -4,12 +4,12 @@ # Pod::Spec.new do |s| s.name = 'opus_codec_ios' - s.version = '3.0.4' + s.version = '3.0.5' s.summary = 'libopus wrappers for flutter in iOS.' s.description = <<-DESC libopus wrappers for flutter in iOS. DESC - s.homepage = 'https://github.com/ppamorim/opus_codec' + s.homepage = 'https://github.com/Corkscrews/opus_codec' s.license = { :file => '../LICENSE' } s.author = { 'Corkscrews' => '' } s.source = { :path => '.' } diff --git a/opus_flutter_linux/CHANGELOG.md b/opus_flutter_linux/CHANGELOG.md index f2549c8..891dcd4 100644 --- a/opus_flutter_linux/CHANGELOG.md +++ b/opus_flutter_linux/CHANGELOG.md @@ -1,6 +1,6 @@ ## 3.0.5 -* Bump version +* Bump all package versions ## 3.0.4 diff --git a/opus_flutter_macos/CHANGELOG.md b/opus_flutter_macos/CHANGELOG.md index 3ba806d..77e40d9 100644 --- a/opus_flutter_macos/CHANGELOG.md +++ b/opus_flutter_macos/CHANGELOG.md @@ -1,6 +1,7 @@ ## 3.0.5 -* Bump version +* Fix podspec version to match pubspec +* Bump all package versions ## 3.0.4 diff --git a/opus_flutter_macos/macos/opus_codec_macos.podspec b/opus_flutter_macos/macos/opus_codec_macos.podspec index d41cf74..06e8912 100644 --- a/opus_flutter_macos/macos/opus_codec_macos.podspec +++ b/opus_flutter_macos/macos/opus_codec_macos.podspec @@ -4,12 +4,12 @@ # Pod::Spec.new do |s| s.name = 'opus_codec_macos' - s.version = '3.0.4' + s.version = '3.0.5' s.summary = 'libopus wrappers for flutter on macOS.' s.description = <<-DESC libopus wrappers for flutter on macOS. DESC - s.homepage = 'https://github.com/ppamorim/opus_codec' + s.homepage = 'https://github.com/Corkscrews/opus_codec' s.license = { :file => '../LICENSE' } s.author = { 'Corkscrews' => '' } s.source = { :path => '.' } diff --git a/opus_flutter_platform_interface/CHANGELOG.md b/opus_flutter_platform_interface/CHANGELOG.md index 078e746..13cf324 100644 --- a/opus_flutter_platform_interface/CHANGELOG.md +++ b/opus_flutter_platform_interface/CHANGELOG.md @@ -1,6 +1,6 @@ ## 3.0.5 -* Bump version +* Bump all package versions ## 3.0.4 diff --git a/opus_flutter_web/CHANGELOG.md b/opus_flutter_web/CHANGELOG.md index 15b1ccc..fb74a4a 100644 --- a/opus_flutter_web/CHANGELOG.md +++ b/opus_flutter_web/CHANGELOG.md @@ -1,6 +1,6 @@ ## 3.0.5 -* Bump version +* Bump all package versions ## 3.0.4 diff --git a/opus_flutter_windows/CHANGELOG.md b/opus_flutter_windows/CHANGELOG.md index fac93fb..d58b2cd 100644 --- a/opus_flutter_windows/CHANGELOG.md +++ b/opus_flutter_windows/CHANGELOG.md @@ -1,6 +1,6 @@ ## 3.0.5 -* Bump version +* Bump all package versions ## 3.0.4 From e27330203d9f68bae12df5e4056a3ce1342383ca Mon Sep 17 00:00:00 2001 From: Pedro Paulo de Amorim Date: Tue, 24 Feb 2026 23:51:10 +0000 Subject: [PATCH 03/21] chore: remove pubspec.lock from tracking and add to gitignore Library packages should not commit pubspec.lock per Dart guidelines. --- opus_flutter/.gitignore | 1 + opus_flutter/pubspec.lock | 466 ------------------- opus_flutter_android/pubspec.lock | 221 --------- opus_flutter_ios/.gitignore | 1 + opus_flutter_ios/pubspec.lock | 221 --------- opus_flutter_platform_interface/.gitignore | 1 + opus_flutter_platform_interface/pubspec.lock | 213 --------- opus_flutter_web/.gitignore | 1 + opus_flutter_web/pubspec.lock | 274 ----------- opus_flutter_windows/.gitignore | 1 + opus_flutter_windows/pubspec.lock | 381 --------------- 11 files changed, 5 insertions(+), 1776 deletions(-) delete mode 100644 opus_flutter/pubspec.lock delete mode 100644 opus_flutter_android/pubspec.lock delete mode 100644 opus_flutter_ios/pubspec.lock delete mode 100644 opus_flutter_platform_interface/pubspec.lock delete mode 100644 opus_flutter_web/pubspec.lock delete mode 100644 opus_flutter_windows/pubspec.lock diff --git a/opus_flutter/.gitignore b/opus_flutter/.gitignore index 551d074..995c9cd 100644 --- a/opus_flutter/.gitignore +++ b/opus_flutter/.gitignore @@ -5,5 +5,6 @@ .pub/ .idea/ doc/api/ +pubspec.lock build/ diff --git a/opus_flutter/pubspec.lock b/opus_flutter/pubspec.lock deleted file mode 100644 index cefc5a8..0000000 --- a/opus_flutter/pubspec.lock +++ /dev/null @@ -1,466 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - async: - dependency: transitive - description: - name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - characters: - dependency: transitive - description: - name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" - code_assets: - dependency: transitive - description: - name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - crypto: - dependency: transitive - description: - name: crypto - sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.dev" - source: hosted - version: "3.0.7" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - ffi: - dependency: transitive - description: - name: ffi - sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" - url: "https://pub.dev" - source: hosted - version: "5.0.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - glob: - dependency: transitive - description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" - source: hosted - version: "2.1.3" - hooks: - dependency: transitive - description: - name: hooks - sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6" - url: "https://pub.dev" - source: hosted - version: "1.0.1" - http: - dependency: transitive - description: - name: http - sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.dev" - source: hosted - version: "1.6.0" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" - source: hosted - version: "4.1.2" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" - source: hosted - version: "11.0.2" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" - source: hosted - version: "3.0.10" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - lints: - dependency: transitive - description: - name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 - url: "https://pub.dev" - source: hosted - version: "5.1.1" - logging: - dependency: transitive - description: - name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" - source: hosted - version: "1.3.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.dev" - source: hosted - version: "0.12.17" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.dev" - source: hosted - version: "0.11.1" - meta: - dependency: transitive - description: - name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.dev" - source: hosted - version: "1.17.0" - native_toolchain_c: - dependency: transitive - description: - name: native_toolchain_c - sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" - url: "https://pub.dev" - source: hosted - version: "0.17.4" - objective_c: - dependency: transitive - description: - name: objective_c - sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" - url: "https://pub.dev" - source: hosted - version: "9.3.0" - opus_codec_android: - dependency: "direct main" - description: - name: opus_codec_android - sha256: a92c636f826d9bfe490b7820b14ab14b86c756a513b0ab24dfabf334d9c96168 - url: "https://pub.dev" - source: hosted - version: "3.0.1" - opus_codec_ios: - dependency: "direct main" - description: - name: opus_codec_ios - sha256: "5822f56246bc2cf527945bec2d60239d64d939261ba593fd00aedd836f3395d3" - url: "https://pub.dev" - source: hosted - version: "3.0.1" - opus_codec_linux: - dependency: "direct main" - description: - name: opus_codec_linux - sha256: "679ebf5c82713f5b950bc7a0f344e524e8dd37d451bf9588fb701f18c98f7d90" - url: "https://pub.dev" - source: hosted - version: "3.0.0" - opus_codec_macos: - dependency: "direct main" - description: - name: opus_codec_macos - sha256: dda2e8f159cd7301fc727f4788468f50f1fde7901973be4dda04d3d0c312929e - url: "https://pub.dev" - source: hosted - version: "3.0.0" - opus_codec_platform_interface: - dependency: "direct main" - description: - name: opus_codec_platform_interface - sha256: "5cf72337959f93b35dcec351fc43c10deafa44e866f345cbbbc0ad600211cdec" - url: "https://pub.dev" - source: hosted - version: "3.0.0" - opus_codec_web: - dependency: "direct main" - description: - name: opus_codec_web - sha256: ead9de53a1e44261ffa1d21a677b9584398536ae058d416a766a067a8d5f1908 - url: "https://pub.dev" - source: hosted - version: "3.0.3" - opus_codec_windows: - dependency: "direct main" - description: - name: opus_codec_windows - sha256: "3968008555d655da785fae238b542ad010d63c2777dfec4c5782b0ace30ba3eb" - url: "https://pub.dev" - source: hosted - version: "3.0.0" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - path_provider: - dependency: transitive - description: - name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.dev" - source: hosted - version: "2.1.5" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e - url: "https://pub.dev" - source: hosted - version: "2.2.22" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" - url: "https://pub.dev" - source: hosted - version: "2.6.0" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" - source: hosted - version: "2.3.0" - platform: - dependency: transitive - description: - name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" - source: hosted - version: "3.1.6" - plugin_platform_interface: - dependency: "direct dev" - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.dev" - source: hosted - version: "1.10.2" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 - url: "https://pub.dev" - source: hosted - version: "0.7.7" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" - source: hosted - version: "2.2.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.dev" - source: hosted - version: "15.0.2" - wasm_ffi: - dependency: transitive - description: - name: wasm_ffi - sha256: bb58f268afc14f2591b9d15c797e74ad7f14696f2b87f0d49e00cf41bde0dc7a - url: "https://pub.dev" - source: hosted - version: "2.1.0" - web: - dependency: transitive - description: - name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - yaml: - dependency: transitive - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" - source: hosted - version: "3.1.3" -sdks: - dart: ">=3.10.3 <4.0.0" - flutter: ">=3.38.4" diff --git a/opus_flutter_android/pubspec.lock b/opus_flutter_android/pubspec.lock deleted file mode 100644 index 64d3aac..0000000 --- a/opus_flutter_android/pubspec.lock +++ /dev/null @@ -1,221 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - async: - dependency: transitive - description: - name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - characters: - dependency: transitive - description: - name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" - url: "https://pub.dev" - source: hosted - version: "5.0.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" - source: hosted - version: "11.0.2" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" - source: hosted - version: "3.0.10" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - lints: - dependency: transitive - description: - name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 - url: "https://pub.dev" - source: hosted - version: "5.1.1" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.dev" - source: hosted - version: "0.12.17" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.dev" - source: hosted - version: "0.11.1" - meta: - dependency: transitive - description: - name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.dev" - source: hosted - version: "1.17.0" - opus_codec_platform_interface: - dependency: "direct main" - description: - name: opus_codec_platform_interface - sha256: "5cf72337959f93b35dcec351fc43c10deafa44e866f345cbbbc0ad600211cdec" - url: "https://pub.dev" - source: hosted - version: "3.0.0" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.dev" - source: hosted - version: "1.10.2" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 - url: "https://pub.dev" - source: hosted - version: "0.7.7" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" - source: hosted - version: "2.2.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.dev" - source: hosted - version: "15.0.2" -sdks: - dart: ">=3.8.0-0 <4.0.0" - flutter: ">=3.22.0" diff --git a/opus_flutter_ios/.gitignore b/opus_flutter_ios/.gitignore index af412f1..afc98e7 100644 --- a/opus_flutter_ios/.gitignore +++ b/opus_flutter_ios/.gitignore @@ -3,5 +3,6 @@ .packages .pub/ +pubspec.lock build/ diff --git a/opus_flutter_ios/pubspec.lock b/opus_flutter_ios/pubspec.lock deleted file mode 100644 index 64d3aac..0000000 --- a/opus_flutter_ios/pubspec.lock +++ /dev/null @@ -1,221 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - async: - dependency: transitive - description: - name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - characters: - dependency: transitive - description: - name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" - url: "https://pub.dev" - source: hosted - version: "5.0.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" - source: hosted - version: "11.0.2" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" - source: hosted - version: "3.0.10" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - lints: - dependency: transitive - description: - name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 - url: "https://pub.dev" - source: hosted - version: "5.1.1" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.dev" - source: hosted - version: "0.12.17" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.dev" - source: hosted - version: "0.11.1" - meta: - dependency: transitive - description: - name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.dev" - source: hosted - version: "1.17.0" - opus_codec_platform_interface: - dependency: "direct main" - description: - name: opus_codec_platform_interface - sha256: "5cf72337959f93b35dcec351fc43c10deafa44e866f345cbbbc0ad600211cdec" - url: "https://pub.dev" - source: hosted - version: "3.0.0" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.dev" - source: hosted - version: "1.10.2" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 - url: "https://pub.dev" - source: hosted - version: "0.7.7" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" - source: hosted - version: "2.2.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.dev" - source: hosted - version: "15.0.2" -sdks: - dart: ">=3.8.0-0 <4.0.0" - flutter: ">=3.22.0" diff --git a/opus_flutter_platform_interface/.gitignore b/opus_flutter_platform_interface/.gitignore index 551d074..995c9cd 100644 --- a/opus_flutter_platform_interface/.gitignore +++ b/opus_flutter_platform_interface/.gitignore @@ -5,5 +5,6 @@ .pub/ .idea/ doc/api/ +pubspec.lock build/ diff --git a/opus_flutter_platform_interface/pubspec.lock b/opus_flutter_platform_interface/pubspec.lock deleted file mode 100644 index debe8f3..0000000 --- a/opus_flutter_platform_interface/pubspec.lock +++ /dev/null @@ -1,213 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - async: - dependency: transitive - description: - name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - characters: - dependency: transitive - description: - name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" - url: "https://pub.dev" - source: hosted - version: "5.0.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" - source: hosted - version: "11.0.2" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" - source: hosted - version: "3.0.10" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - lints: - dependency: transitive - description: - name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 - url: "https://pub.dev" - source: hosted - version: "5.1.1" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.dev" - source: hosted - version: "0.12.17" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.dev" - source: hosted - version: "0.11.1" - meta: - dependency: transitive - description: - name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.dev" - source: hosted - version: "1.17.0" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - plugin_platform_interface: - dependency: "direct main" - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.dev" - source: hosted - version: "1.10.2" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 - url: "https://pub.dev" - source: hosted - version: "0.7.7" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" - source: hosted - version: "2.2.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.dev" - source: hosted - version: "15.0.2" -sdks: - dart: ">=3.8.0-0 <4.0.0" - flutter: ">=3.22.0" diff --git a/opus_flutter_web/.gitignore b/opus_flutter_web/.gitignore index af412f1..afc98e7 100644 --- a/opus_flutter_web/.gitignore +++ b/opus_flutter_web/.gitignore @@ -3,5 +3,6 @@ .packages .pub/ +pubspec.lock build/ diff --git a/opus_flutter_web/pubspec.lock b/opus_flutter_web/pubspec.lock deleted file mode 100644 index 649a736..0000000 --- a/opus_flutter_web/pubspec.lock +++ /dev/null @@ -1,274 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - async: - dependency: transitive - description: - name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - characters: - dependency: transitive - description: - name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - ffi: - dependency: transitive - description: - name: ffi - sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" - url: "https://pub.dev" - source: hosted - version: "5.0.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - http: - dependency: transitive - description: - name: http - sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.dev" - source: hosted - version: "1.6.0" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" - source: hosted - version: "4.1.2" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" - source: hosted - version: "11.0.2" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" - source: hosted - version: "3.0.10" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - lints: - dependency: transitive - description: - name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 - url: "https://pub.dev" - source: hosted - version: "5.1.1" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.dev" - source: hosted - version: "0.12.17" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.dev" - source: hosted - version: "0.11.1" - meta: - dependency: transitive - description: - name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.dev" - source: hosted - version: "1.17.0" - opus_codec_platform_interface: - dependency: "direct main" - description: - name: opus_codec_platform_interface - sha256: "5cf72337959f93b35dcec351fc43c10deafa44e866f345cbbbc0ad600211cdec" - url: "https://pub.dev" - source: hosted - version: "3.0.0" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.dev" - source: hosted - version: "1.10.2" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 - url: "https://pub.dev" - source: hosted - version: "0.7.7" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" - source: hosted - version: "2.2.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.dev" - source: hosted - version: "15.0.2" - wasm_ffi: - dependency: "direct main" - description: - name: wasm_ffi - sha256: bb58f268afc14f2591b9d15c797e74ad7f14696f2b87f0d49e00cf41bde0dc7a - url: "https://pub.dev" - source: hosted - version: "2.1.0" - web: - dependency: transitive - description: - name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" - source: hosted - version: "1.1.1" -sdks: - dart: ">=3.8.0-0 <4.0.0" - flutter: ">=3.22.0" diff --git a/opus_flutter_windows/.gitignore b/opus_flutter_windows/.gitignore index af412f1..afc98e7 100644 --- a/opus_flutter_windows/.gitignore +++ b/opus_flutter_windows/.gitignore @@ -3,5 +3,6 @@ .packages .pub/ +pubspec.lock build/ diff --git a/opus_flutter_windows/pubspec.lock b/opus_flutter_windows/pubspec.lock deleted file mode 100644 index 8769a2b..0000000 --- a/opus_flutter_windows/pubspec.lock +++ /dev/null @@ -1,381 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - async: - dependency: transitive - description: - name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - characters: - dependency: transitive - description: - name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" - code_assets: - dependency: transitive - description: - name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - crypto: - dependency: transitive - description: - name: crypto - sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.dev" - source: hosted - version: "3.0.7" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - ffi: - dependency: transitive - description: - name: ffi - sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" - url: "https://pub.dev" - source: hosted - version: "5.0.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - glob: - dependency: transitive - description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" - source: hosted - version: "2.1.3" - hooks: - dependency: transitive - description: - name: hooks - sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6" - url: "https://pub.dev" - source: hosted - version: "1.0.1" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" - source: hosted - version: "11.0.2" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" - source: hosted - version: "3.0.10" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - lints: - dependency: transitive - description: - name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 - url: "https://pub.dev" - source: hosted - version: "5.1.1" - logging: - dependency: transitive - description: - name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" - source: hosted - version: "1.3.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.dev" - source: hosted - version: "0.12.17" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.dev" - source: hosted - version: "0.11.1" - meta: - dependency: transitive - description: - name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.dev" - source: hosted - version: "1.17.0" - native_toolchain_c: - dependency: transitive - description: - name: native_toolchain_c - sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" - url: "https://pub.dev" - source: hosted - version: "0.17.4" - objective_c: - dependency: transitive - description: - name: objective_c - sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" - url: "https://pub.dev" - source: hosted - version: "9.3.0" - opus_codec_platform_interface: - dependency: "direct main" - description: - name: opus_codec_platform_interface - sha256: "5cf72337959f93b35dcec351fc43c10deafa44e866f345cbbbc0ad600211cdec" - url: "https://pub.dev" - source: hosted - version: "3.0.0" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - path_provider: - dependency: "direct main" - description: - name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.dev" - source: hosted - version: "2.1.5" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e - url: "https://pub.dev" - source: hosted - version: "2.2.22" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" - url: "https://pub.dev" - source: hosted - version: "2.6.0" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" - source: hosted - version: "2.3.0" - platform: - dependency: transitive - description: - name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" - source: hosted - version: "3.1.6" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.dev" - source: hosted - version: "1.10.2" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 - url: "https://pub.dev" - source: hosted - version: "0.7.7" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" - source: hosted - version: "2.2.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.dev" - source: hosted - version: "15.0.2" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - yaml: - dependency: transitive - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" - source: hosted - version: "3.1.3" -sdks: - dart: ">=3.10.3 <4.0.0" - flutter: ">=3.38.4" From 76224214be0838cae25be24a8ea8d82be0599d8d Mon Sep 17 00:00:00 2001 From: Pedro Paulo de Amorim Date: Tue, 24 Feb 2026 23:52:22 +0000 Subject: [PATCH 04/21] fix: scope pubspec.lock gitignore to root level in opus_flutter Prevents the rule from matching example/pubspec.lock, which caused a publish warning about checked-in files being gitignored. --- opus_flutter/.gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opus_flutter/.gitignore b/opus_flutter/.gitignore index 995c9cd..446f5ff 100644 --- a/opus_flutter/.gitignore +++ b/opus_flutter/.gitignore @@ -5,6 +5,6 @@ .pub/ .idea/ doc/api/ -pubspec.lock +/pubspec.lock build/ From 54cb37f727bc9d842b80739e98f7d2128dbcd294 Mon Sep 17 00:00:00 2001 From: Pedro Paulo de Amorim Date: Tue, 24 Feb 2026 23:56:30 +0000 Subject: [PATCH 05/21] chore: update example lockfile and web platform interface dependency --- opus_flutter/example/pubspec.lock | 18 +++++++++--------- opus_flutter_web/pubspec.yaml | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/opus_flutter/example/pubspec.lock b/opus_flutter/example/pubspec.lock index 646db01..3f0a712 100644 --- a/opus_flutter/example/pubspec.lock +++ b/opus_flutter/example/pubspec.lock @@ -342,63 +342,63 @@ packages: path: ".." relative: true source: path - version: "3.0.3" + version: "3.0.5" opus_codec_android: dependency: "direct overridden" description: path: "../../opus_flutter_android" relative: true source: path - version: "3.0.1" + version: "3.0.5" opus_codec_dart: dependency: "direct main" description: path: "../../opus_dart" relative: true source: path - version: "3.0.1" + version: "3.0.5" opus_codec_ios: dependency: "direct overridden" description: path: "../../opus_flutter_ios" relative: true source: path - version: "3.0.1" + version: "3.0.5" opus_codec_linux: dependency: "direct overridden" description: path: "../../opus_flutter_linux" relative: true source: path - version: "3.0.0" + version: "3.0.5" opus_codec_macos: dependency: "direct overridden" description: path: "../../opus_flutter_macos" relative: true source: path - version: "3.0.0" + version: "3.0.5" opus_codec_platform_interface: dependency: "direct overridden" description: path: "../../opus_flutter_platform_interface" relative: true source: path - version: "3.0.0" + version: "3.0.5" opus_codec_web: dependency: "direct overridden" description: path: "../../opus_flutter_web" relative: true source: path - version: "3.0.3" + version: "3.0.5" opus_codec_windows: dependency: "direct overridden" description: path: "../../opus_flutter_windows" relative: true source: path - version: "3.0.0" + version: "3.0.5" path: dependency: transitive description: diff --git a/opus_flutter_web/pubspec.yaml b/opus_flutter_web/pubspec.yaml index a11dbc6..c992c87 100644 --- a/opus_flutter_web/pubspec.yaml +++ b/opus_flutter_web/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - opus_codec_platform_interface: ^3.0.0 + opus_codec_platform_interface: ^3.0.5 wasm_ffi: ^2.1.0 dev_dependencies: From ed4105323b2d268b46897a966f938f9b497dd3b1 Mon Sep 17 00:00:00 2001 From: Pedro Paulo de Amorim Date: Wed, 25 Feb 2026 00:11:09 +0000 Subject: [PATCH 06/21] docs: add FFI and wasm_ffi analysis for bug hunting Comprehensive analysis of all dart:ffi and wasm_ffi usage across the project, cross-referenced against the wasm_ffi documentation. Catalogues memory management patterns, pointer lifecycles, binding signatures, and identifies 19 potential risks including missing WASM exports, buffer sizing bugs, and use-after-destroy hazards. --- docs/ffi-analysis.md | 876 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 876 insertions(+) create mode 100644 docs/ffi-analysis.md diff --git a/docs/ffi-analysis.md b/docs/ffi-analysis.md new file mode 100644 index 0000000..05618e5 --- /dev/null +++ b/docs/ffi-analysis.md @@ -0,0 +1,876 @@ +# FFI and wasm_ffi Analysis + +This document catalogues every use of `dart:ffi`, `package:ffi`, and `package:wasm_ffi` +across the project. Its purpose is to serve as a reference for identifying potential bugs, +memory safety issues, and behavioral differences between native and web platforms. + +--- + +## 1. Conditional FFI Abstraction Layer + +The project uses a single entry point to abstract over native FFI and web WASM FFI: + +**`opus_dart/lib/src/proxy_ffi.dart`** + +```dart +export 'dart:ffi' if (dart.library.js_interop) 'package:wasm_ffi/ffi.dart'; +export 'init_ffi.dart' if (dart.library.js_interop) 'init_web.dart'; +``` + +All downstream code imports `proxy_ffi.dart` instead of `dart:ffi` directly. This means +every `Pointer`, `DynamicLibrary`, `Allocator`, `NativeType`, and `Opaque` reference +resolves to either `dart:ffi` or `wasm_ffi/ffi.dart` at compile time. + +**Implication:** Any behavioral difference between `dart:ffi` and `wasm_ffi` (e.g. +pointer arithmetic, `nullptr` handling, allocator semantics) silently affects all code +below this layer. + +--- + +## 2. Initialization: Native vs Web + +### 2.1 Native (`init_ffi.dart`) + +```dart +ApiObject createApiObject(Object lib) { + final library = lib as DynamicLibrary; + return ApiObject(library, ffipackage.malloc); +} +``` + +- Casts the incoming `Object` to `dart:ffi` `DynamicLibrary`. +- Uses `package:ffi`'s `malloc` as the allocator. +- No opaque type registration needed on native. + +### 2.2 Web (`init_web.dart`) + +```dart +ApiObject createApiObject(Object lib) { + final library = lib as DynamicLibrary; + registerOpaqueType(); + registerOpaqueType(); + registerOpaqueType(); + registerOpaqueType(); + registerOpaqueType(); + registerOpaqueType(); + registerOpaqueType(); + registerOpaqueType(); + registerOpaqueType(); + registerOpaqueType(); // duplicate + return ApiObject(library, library.allocator); +} +``` + +- Casts to `wasm_ffi` `DynamicLibrary`. +- Must register every `Opaque` subclass so `wasm_ffi` can handle pointer lookups. +- Uses `library.allocator` (WASM linear memory allocator) instead of `malloc`. +- **Bug candidate:** `OpusRepacketizer` is registered twice; `OpusCustomMode` is + never registered. If `OpusCustomMode` pointers are ever used on web, this will fail. + +### 2.3 Differences Summary + +| Aspect | Native (`dart:ffi`) | Web (`wasm_ffi`) | +|------------------------|----------------------------------|------------------------------------| +| Allocator | `malloc` from `package:ffi` | `library.allocator` | +| Opaque type setup | Not needed | `registerOpaqueType()` required | +| `DynamicLibrary` source| OS-level `.so`/`.dylib`/`.dll` | Emscripten JS+WASM module | +| `nullptr` semantics | Backed by address `0` | `wasm_ffi` emulation | +| `free(nullptr)` | Safe (C standard) | Depends on `wasm_ffi` allocator | + +--- + +## 3. Library Loading Per Platform + +Each platform package only touches FFI to obtain a `DynamicLibrary`. No allocation or +pointer work happens in these packages. + +| Platform | Method | Notes | +|----------|--------|-------| +| Android | `DynamicLibrary.open('libopus.so')` | Built by CMake via FetchContent | +| iOS | `DynamicLibrary.process()` | Statically linked via xcframework | +| macOS | `DynamicLibrary.process()` | Statically linked via xcframework | +| Linux | `DynamicLibrary.open(path)` then fallback to `DynamicLibrary.open('libopus.so.0')` | Bundled `.so` copied to temp dir | +| Windows | `DynamicLibrary.open(libPath)` | Bundled DLL copied to temp dir | +| Web | `DynamicLibrary.open('./assets/.../libopus.js', moduleName: 'libopus', useAsGlobal: GlobalMemory.ifNotSet)` | Emscripten module | + +**Risk areas in platform loading:** + +- **Linux/Windows:** The temp-directory copy strategy means the native library's + lifetime is decoupled from the app. If the temp directory is cleaned while the app + runs, subsequent `DynamicLibrary` uses will crash. +- **Linux fallback:** Falls back to system `libopus.so.0` which may be a different + version than what the bindings expect. + +--- + +## 4. Native Binding Wrappers + +Located in `opus_dart/lib/wrappers/`. These files define typedefs and use +`DynamicLibrary.lookupFunction` to resolve C symbols. + +### 4.1 Opaque Types + +```dart +final class OpusEncoder extends ffi.Opaque {} +final class OpusDecoder extends ffi.Opaque {} +final class OpusRepacketizer extends ffi.Opaque {} +final class OpusMSEncoder extends ffi.Opaque {} +final class OpusMSDecoder extends ffi.Opaque {} +final class OpusProjectionEncoder extends ffi.Opaque {} +final class OpusProjectionDecoder extends ffi.Opaque {} +final class OpusCustomEncoder extends ffi.Opaque {} +final class OpusCustomDecoder extends ffi.Opaque {} +final class OpusCustomMode extends ffi.Opaque {} +``` + +These are used as type parameters for `Pointer` to represent C opaque structs. + +### 4.2 Encoder Bindings (`opus_encoder.dart`) + +Functions resolved via `lookupFunction`: + +| C function | Dart signature | Notes | +|------------|---------------|-------| +| `opus_encoder_get_size` | `int Function(int)` | | +| `opus_encoder_create` | `Pointer Function(int, int, int, Pointer)` | Returns encoder state; error via out-pointer | +| `opus_encoder_init` | `int Function(Pointer, int, int, int)` | | +| `opus_encode` | `int Function(Pointer, Pointer, int, Pointer, int)` | Returns encoded byte count or error | +| `opus_encode_float` | `int Function(Pointer, Pointer, int, Pointer, int)` | | +| `opus_encoder_destroy` | `void Function(Pointer)` | | +| `opus_encoder_ctl` | `int Function(Pointer, int, int)` | Variadic in C, bound with fixed 3 args | + +**`opus_encoder_ctl` binding concern:** The C function `opus_encoder_ctl` is variadic. +The binding hardcodes it to accept exactly `(st, request, va)` — three arguments. +This works for CTL requests that take a single `int` argument, but CTL requests that +take a pointer argument (e.g. `OPUS_GET_*` requests that write to an out-pointer) +cannot be used through this binding. The `int va` parameter would need to be a pointer +address cast to `int`, which is fragile and non-portable. On web/WASM, pointer addresses +in the WASM linear memory space are 32-bit offsets, so passing them as `int` might work +by accident, but this pattern is error-prone. + +### 4.3 Decoder Bindings (`opus_decoder.dart`) + +Functions resolved via `lookupFunction`: + +| C function | Dart signature | +|------------|---------------| +| `opus_decoder_get_size` | `int Function(int)` | +| `opus_decoder_create` | `Pointer Function(int, int, Pointer)` | +| `opus_decoder_init` | `int Function(Pointer, int, int)` | +| `opus_decode` | `int Function(Pointer, Pointer, int, Pointer, int, int)` | +| `opus_decode_float` | `int Function(Pointer, Pointer, int, Pointer, int, int)` | +| `opus_decoder_destroy` | `void Function(Pointer)` | +| `opus_packet_parse` | `int Function(Pointer, int, Pointer, Pointer, int, Pointer)` | +| `opus_packet_get_bandwidth` | `int Function(Pointer)` | +| `opus_packet_get_samples_per_frame` | `int Function(Pointer, int)` | +| `opus_packet_get_nb_channels` | `int Function(Pointer)` | +| `opus_packet_get_nb_frames` | `int Function(Pointer, int)` | +| `opus_packet_get_nb_samples` | `int Function(Pointer, int, int)` | +| `opus_decoder_get_nb_samples` | `int Function(Pointer, Pointer, int)` | +| `opus_pcm_soft_clip` | `void Function(Pointer, int, int, Pointer)` | + +### 4.4 Lib Info Bindings (`opus_libinfo.dart`) + +| C function | Dart signature | Notes | +|------------|---------------|-------| +| `opus_get_version_string` | `Pointer Function()` | Returns pointer to static C string | +| `opus_strerror` | `Pointer Function(int)` | Returns pointer to static C string | + +Both return `Pointer` rather than `Pointer`. The project manually walks +the bytes to find the null terminator (see Section 5.4). + +--- + +## 5. Memory Management Patterns + +### 5.1 The `ApiObject` and Global `opus` Variable + +```dart +late ApiObject opus; + +class ApiObject { + final OpusLibInfoFunctions libinfo; + final OpusEncoderFunctions encoder; + final OpusDecoderFunctions decoder; + final Allocator allocator; + // ... +} +``` + +All allocation goes through `opus.allocator.call(count)` and deallocation through +`opus.allocator.free(pointer)`. The global `opus` is set once via `initOpus()`. + +**Risk:** `opus` is a `late` global. Any call before `initOpus()` throws +`LateInitializationError`. There is no guard or descriptive error message. + +### 5.2 Allocation/Free Patterns + +The project uses two distinct patterns: + +#### Pattern A: Allocate-Use-Free (SimpleOpusEncoder, SimpleOpusDecoder, OpusPacketUtils) + +``` +allocate input buffer +allocate output buffer +call opus function +try { + check error, copy result +} finally { + free input buffer + free output buffer +} +``` + +Every method call allocates and frees. This is safe but has higher overhead. + +**Concern in SimpleOpusEncoder.encode/encodeFloat:** The `opus_encode` call happens +_before_ the `try` block. If `opus_encode` itself throws (not an opus error code, but +an actual Dart exception from FFI — e.g. segfault translated to an exception), then the +`finally` block still runs and frees the buffers. However, if the `allocator.call` +for `outputNative` throws after `inputNative` was already allocated, `inputNative` leaks. +The same pattern exists in the decoder. + +Specifically in `SimpleOpusEncoder.encode`: + +```dart +Pointer inputNative = opus.allocator.call(input.length); // (1) +inputNative.asTypedList(input.length).setAll(0, input); +Pointer outputNative = opus.allocator.call(maxOutputSizeBytes); // (2) +// If (2) throws, (1) is leaked — no finally covers (1) at this point +int outputLength = opus.encoder.opus_encode(...); +try { + // ... +} finally { + opus.allocator.free(inputNative); // only reached if opus_encode didn't throw + opus.allocator.free(outputNative); +} +``` + +#### Pattern B: Preallocated Buffers (BufferedOpusEncoder, BufferedOpusDecoder) + +Buffers are allocated once in the factory constructor and freed in `destroy()`. + +``` +factory constructor: + allocate error, input, output, (softClipBuffer for decoder) + create opus state + if error: free input, output, (softClipBuffer), throw + finally: free error + +destroy(): + if not destroyed: + mark destroyed + destroy opus state + free input, output, (softClipBuffer) +``` + +**Concern:** If `destroy()` is never called, all native memory leaks. There is no +Dart finalizer attached. `NativeFinalizer` (available since Dart 2.17) is not used +anywhere in the project. + +**Concern in BufferedOpusEncoder factory:** If `opus_encoder_create` itself throws +(as opposed to returning an error code), the `input` and `output` buffers leak because +the `throw` path inside the `try` block only runs when `error.value != OPUS_OK`. + +### 5.3 Pointer Lifetime in Streaming (`opus_dart_streaming.dart`) + +`StreamOpusEncoder` and `StreamOpusDecoder` wrap `BufferedOpusEncoder`/`BufferedOpusDecoder`. +They expose `copyOutput` as a parameter: + +- `copyOutput = true` (default): Output is copied to Dart heap via `Uint8List.fromList`. +- `copyOutput = false`: Output is a `Uint8List` view backed by native memory. + +**Risk with `copyOutput = false`:** The view points into the preallocated native output +buffer. On the next encode/decode call, this buffer is overwritten. If a consumer holds +a reference to a previously yielded `Uint8List`, it will silently contain new data. +This is a use-after-write hazard. + +The `StreamOpusDecoder` has an additional concern: when `forwardErrorCorrection` is +enabled and a packet is lost then recovered, the decoder calls `_decodeFec(true)` and +yields `_output()`, then immediately calls `_decodeFec(false)` and yields `_output()` +again. With `copyOutput = false`, the first yield's data is overwritten by the second +decode before the consumer processes it (in an `async*` generator, the consumer may +not have consumed the first yield yet). + +### 5.4 String Handling (`_asString`) + +```dart +String _asString(Pointer pointer) { + int i = 0; + while (pointer[i] != 0) { + i++; + } + return utf8.decode(pointer.asTypedList(i)); +} +``` + +- Walks memory byte-by-byte until it finds a null terminator. +- No bounds checking — if the pointer is invalid or the string is not null-terminated, + this loops until it hits a zero byte or causes a segfault. +- Only used with `opus_get_version_string()` and `opus_strerror()`, which return + pointers to static C strings in libopus. These are guaranteed to be null-terminated + by the C library, so the risk is low in practice. +- Does not use `package:ffi`'s `Utf8` utilities, which could also handle this. + +### 5.5 `nullptr` Usage + +`nullptr` is used in two contexts: + +1. **Decoder packet loss:** When `input` is `null`, `inputNative` is set to `nullptr` + and passed to `opus_decode`/`opus_decode_float`. This is correct per the opus API + (null data pointer signals packet loss). + +2. **Decoder free after packet loss:** After a null-input decode, the code + `opus.allocator.free(inputNative)` is called where `inputNative == nullptr`. In C, + `free(NULL)` is a no-op. The `dart:ffi` `malloc.free` from `package:ffi` also + handles this safely. Whether `wasm_ffi`'s allocator handles `free(nullptr)` safely + is implementation-dependent. + +--- + +## 6. Error Handling Around FFI Calls + +### 6.1 Error Code Checking + +All opus API calls that return error codes are checked: + +```dart +if (result < opus_defines.OPUS_OK) { + throw OpusException(result); +} +``` + +`OpusException` calls `opus_strerror(errorCode)` to get a human-readable message. + +### 6.2 Error Pointer Pattern + +`opus_encoder_create` and `opus_decoder_create` write an error code to an out-pointer: + +```dart +Pointer error = opus.allocator.call(1); +Pointer encoder = opus.encoder.opus_encoder_create(..., error); +try { + if (error.value != opus_defines.OPUS_OK) { + throw OpusException(error.value); + } +} finally { + opus.allocator.free(error); +} +``` + +The error pointer is always freed in `finally`, which is correct. + +### 6.3 Destroyed State + +Encoder and decoder classes track `_destroyed` to prevent double-destroy: + +```dart +void destroy() { + if (!_destroyed) { + _destroyed = true; + opus.encoder.opus_encoder_destroy(_opusEncoder); + } +} +``` + +**Concern:** While `destroy()` checks `_destroyed`, the `encode`/`decode`/`encodeFloat`/ +`decodeFloat` methods do **not** check `_destroyed` before using the native pointer. +Calling `encode()` after `destroy()` passes a dangling pointer to opus, causing +undefined behavior. The abstract base classes document that calling methods after +`destroy()` should throw `OpusDestroyedError`, but this is not enforced in the +`Simple*` or `Buffered*` implementations. + +--- + +## 7. Pointer Type Usage Inventory + +| Pointer type | Where used | Purpose | +|-------------|-----------|---------| +| `Pointer` | encoder implementations | Opaque encoder state | +| `Pointer` | decoder implementations | Opaque decoder state | +| `Pointer` | create functions | Error out-pointer (1 element) | +| `Pointer` | encode/decode | s16le PCM sample buffer | +| `Pointer` | encode_float/decode_float | Float PCM sample buffer, soft clip state | +| `Pointer` | encode output, decode input, packet utils | Opus packet bytes, raw audio bytes | + +### Pointer Casting + +`BufferedOpusEncoder` and `BufferedOpusDecoder` allocate a single `Pointer` buffer +and cast it to `Pointer` or `Pointer` depending on the encode/decode variant: + +```dart +_inputBuffer.cast() // for encode() +_inputBuffer.cast() // for encodeFloat() +_outputBuffer.cast() // for decode() +_outputBuffer.cast() // for decodeFloat() +``` + +**Risk:** If the buffer byte count is not a multiple of the target type's size +(2 for Int16, 4 for Float), the `asTypedList` call after casting will include +partial elements or read past the intended boundary. The buffer size calculations +use `maxSamplesPerPacket` which accounts for channel count and sample rate, so this +should be safe in practice, but there is no runtime assertion. + +--- + +## 8. `BufferedOpusDecoder` Output Buffer Sizing + +The `BufferedOpusDecoder` factory has this default for `maxOutputBufferSizeBytes`: + +```dart +maxOutputBufferSizeBytes ??= maxSamplesPerPacket(sampleRate, channels); +``` + +`maxSamplesPerPacket` returns the number of **samples** (not bytes): + +```dart +int maxSamplesPerPacket(int sampleRate, int channels) => + ((sampleRate * channels * 120) / 1000).ceil(); +``` + +For 48000 Hz stereo, this is `ceil(48000 * 2 * 120 / 1000) = 11520` samples. + +This value is then used as a **byte count** for `opus.allocator.call(...)`. +When the output is interpreted as `Int16` (2 bytes/sample), the effective sample +capacity is `11520 / 2 = 5760`, which is exactly `maxSamplesPerPacket`. When interpreted +as `Float` (4 bytes/sample), the capacity is `11520 / 4 = 2880`, which is only half of +`maxSamplesPerPacket`. + +**Bug candidate:** For float decoding with `BufferedOpusDecoder`, the output buffer +may be too small to hold a maximum-length opus frame (120ms). A 120ms frame at 48kHz +stereo produces 11520 float samples = 46080 bytes, but the buffer is only 11520 bytes. + +Contrast with `StreamOpusDecoder`, which computes: + +```dart +maxOutputBufferSizeBytes: (floats ? 2 : 4) * maxSamplesPerPacket(sampleRate, channels) +``` + +This looks inverted — it allocates **less** space for float (multiplier 2) and more +for s16le (multiplier 4). The correct multipliers should be 4 for float +(`Float` = 4 bytes) and 2 for s16le (`Int16` = 2 bytes). + +--- + +## 9. `opus_encoder_ctl` Variadic Binding + +The C function `opus_encoder_ctl(OpusEncoder *st, int request, ...)` is variadic. +The Dart binding defines it with a fixed signature: + +```dart +int opus_encoder_ctl(Pointer st, int request, int va); +``` + +This works for setter-style CTLs like `OPUS_SET_BITRATE(value)` where the third +argument is an integer. However: + +- **Getter-style CTLs** (e.g. `OPUS_GET_BITRATE`) expect a `Pointer` as the + third argument. Passing a pointer address as `int` is technically possible but + non-portable and bypasses Dart's type safety. +- **On WASM:** Pointer values are offsets into WASM linear memory (32-bit). Passing + them as Dart `int` (64-bit) and having the C side interpret them as `opus_int32*` + requires that the upper 32 bits are zero, which should hold but is fragile. +- **No decoder_ctl:** There is no `opus_decoder_ctl` binding at all. + +--- + +## 10. Potential Bugs and Risk Summary + +| # | Risk | Location | Severity | Detail | +|---|------|----------|----------|--------| +| 1 | **Duplicate `registerOpaqueType` / missing `OpusCustomMode`** | `init_web.dart:28` | Medium | `OpusRepacketizer` registered twice; `OpusCustomMode` never registered. Using custom mode on web will fail. | +| 2 | **Memory leak if second allocation throws** | `SimpleOpusEncoder.encode`, `SimpleOpusDecoder.decode`, and float variants | Low | If the second `allocator.call` throws, the first allocation is not freed. | +| 3 | **No `NativeFinalizer`** | All encoder/decoder classes | Medium | If `destroy()` is never called, native memory leaks permanently. No GC-driven cleanup. | +| 4 | **Use-after-destroy (dangling pointer)** | `SimpleOpusEncoder`, `SimpleOpusDecoder`, `BufferedOpusEncoder`, `BufferedOpusDecoder` | High | `encode()`/`decode()` do not check `_destroyed` before using the native pointer. Calling them after `destroy()` is undefined behavior. | +| 5 | **`copyOutput = false` use-after-write** | `StreamOpusEncoder`, `StreamOpusDecoder` | Medium | Yielded views point to native buffers that get overwritten on next call. | +| 6 | **`StreamOpusDecoder` FEC double-yield overwrites** | `opus_dart_streaming.dart:321-327` | Medium | With `copyOutput = false` and FEC, the first yielded output is overwritten before the consumer reads it. | +| 7 | **Output buffer too small for float decode** | `BufferedOpusDecoder` factory, `StreamOpusDecoder` constructor | High | `maxOutputBufferSizeBytes` defaults to `maxSamplesPerPacket` (a sample count, not byte count). For float output (4 bytes/sample), the buffer is 4x too small. `StreamOpusDecoder` has an inverted multiplier. | +| 8 | **`free(nullptr)` on web** | `SimpleOpusDecoder.decode` finally block | Low | After null-input decode, `free(nullptr)` is called. Safe on native; behavior on `wasm_ffi` depends on allocator implementation. | +| 9 | **`_asString` unbounded loop** | `opus_dart_misc.dart:26-31` | Low | If pointer is invalid, loops until segfault. Only used with trusted libopus static strings. | +| 10 | **`opus_encoder_ctl` variadic binding** | `opus_encoder.dart` | Low | Hardcoded to 3 int args. Getter CTLs (pointer arg) cannot be used correctly. | +| 11 | **No `opus_decoder_ctl` binding** | `opus_decoder.dart` | Low | Decoder CTL operations are not exposed. | +| 12 | **`late` global `opus` without guard** | `opus_dart_misc.dart:55` | Low | Access before `initOpus()` gives unhelpful `LateInitializationError`. | +| 13 | **Linux/Windows temp dir library lifetime** | `opus_flutter_linux`, `opus_flutter_windows` | Low | If temp dir is cleaned while app runs, native calls will segfault. | + +--- + +## 11. File-by-File FFI Reference + +### Files that import FFI types + +| File | Imports | Allocates | Frees | Calls native | +|------|---------|-----------|-------|-------------| +| `opus_dart/lib/src/proxy_ffi.dart` | conditional re-export | - | - | - | +| `opus_dart/lib/src/init_ffi.dart` | `dart:ffi`, `package:ffi` | - | - | - | +| `opus_dart/lib/src/init_web.dart` | `wasm_ffi/ffi.dart` | - | - | `registerOpaqueType` | +| `opus_dart/lib/src/opus_dart_misc.dart` | via `proxy_ffi` | - | - | `opus_get_version_string`, `opus_strerror` | +| `opus_dart/lib/src/opus_dart_encoder.dart` | via `proxy_ffi` | yes | yes | `opus_encoder_create/encode/encode_float/destroy/ctl` | +| `opus_dart/lib/src/opus_dart_decoder.dart` | via `proxy_ffi` | yes | yes | `opus_decoder_create/decode/decode_float/destroy/pcm_soft_clip` | +| `opus_dart/lib/src/opus_dart_packet.dart` | via `proxy_ffi` | yes | yes | `opus_packet_get_*` | +| `opus_dart/lib/src/opus_dart_streaming.dart` | (indirect via encoder/decoder) | - | - | (indirect) | +| `opus_dart/lib/wrappers/opus_encoder.dart` | via `proxy_ffi` as `ffi` | - | - | `lookupFunction` | +| `opus_dart/lib/wrappers/opus_decoder.dart` | via `proxy_ffi` as `ffi` | - | - | `lookupFunction` | +| `opus_dart/lib/wrappers/opus_libinfo.dart` | via `proxy_ffi` as `ffi` | - | - | `lookupFunction` | +| `opus_dart/lib/wrappers/opus_repacketizer.dart` | via `proxy_ffi` as `ffi` | - | - | opaque type only | +| `opus_dart/lib/wrappers/opus_projection.dart` | via `proxy_ffi` as `ffi` | - | - | opaque type only | +| `opus_dart/lib/wrappers/opus_multistream.dart` | via `proxy_ffi` as `ffi` | - | - | opaque type only | +| `opus_dart/lib/wrappers/opus_custom.dart` | via `proxy_ffi` as `ffi` | - | - | opaque type only | +| `opus_flutter_android/lib/...` | `dart:ffi` | - | - | `DynamicLibrary.open` | +| `opus_flutter_ios/lib/...` | `dart:ffi` | - | - | `DynamicLibrary.process` | +| `opus_flutter_macos/lib/...` | `dart:ffi` | - | - | `DynamicLibrary.process` | +| `opus_flutter_linux/lib/...` | `dart:ffi` | - | - | `DynamicLibrary.open` | +| `opus_flutter_windows/lib/...` | `dart:ffi` | - | - | `DynamicLibrary.open` | +| `opus_flutter_web/lib/...` | `wasm_ffi/ffi.dart` | - | - | `DynamicLibrary.open` (async) | + +### Files that define opaque types + +| File | Types | +|------|-------| +| `opus_encoder.dart` | `OpusEncoder` | +| `opus_decoder.dart` | `OpusDecoder` | +| `opus_repacketizer.dart` | `OpusRepacketizer` | +| `opus_multistream.dart` | `OpusMSEncoder`, `OpusMSDecoder` | +| `opus_projection.dart` | `OpusProjectionEncoder`, `OpusProjectionDecoder` | +| `opus_custom.dart` | `OpusCustomEncoder`, `OpusCustomDecoder`, `OpusCustomMode` | + +--- + +## 12. Cross-Reference with wasm_ffi Documentation + +This section audits the project against every rule and constraint documented in the +[wasm_ffi README](https://github.com/vm75/wasm_ffi/blob/main/README.md). + +### 12.1 `DynamicLibrary.open` Is Async + +**wasm_ffi rule:** `DynamicLibrary.open` is asynchronous on web, unlike `dart:ffi`. + +**Project compliance:** Compliant. `OpusFlutterWeb.load()` uses `await DynamicLibrary.open(...)`: + +```dart +_library ??= await DynamicLibrary.open( + './assets/packages/opus_codec_web/assets/libopus.js', + moduleName: 'libopus', + useAsGlobal: GlobalMemory.ifNotSet, +); +``` + +### 12.2 Multiple Libraries and Memory Isolation + +**wasm_ffi rule:** "If more than one library is loaded, the memory will continue to +refer to the first library. This breaks calls to later loaded libraries!" Each library +has its own memory, so objects cannot be shared between libraries. + +**Project compliance:** Compliant. Only one WASM library (`libopus`) is loaded. The +`useAsGlobal: GlobalMemory.ifNotSet` parameter sets the library's memory as the global +`Memory` instance (only if no global is set yet), which is correct for a single-library +application. + +**Risk if extended:** If a future dependency also loads a WASM library and sets global +memory, `GlobalMemory.ifNotSet` would leave the first library's memory as global. Any +`Pointer.fromAddress()` calls (including `nullptr`) would still reference that first +library's memory. Since the project explicitly passes `library.allocator` through +`ApiObject`, allocation/free operations are correctly bound to the opus library's +memory regardless. + +### 12.3 Opaque Type Registration + +**wasm_ffi rule:** "If you extend the `Opaque` class, you must register the extended +class using `registerOpaqueType()` before using it! Also, your class MUST NOT have +type arguments." + +**Project audit of `init_web.dart`:** + +| Opaque subclass | Registered? | Notes | +|----------------|-------------|-------| +| `OpusEncoder` | Yes | | +| `OpusDecoder` | Yes | | +| `OpusCustomEncoder` | Yes | | +| `OpusCustomDecoder` | Yes | | +| `OpusMSEncoder` | Yes | | +| `OpusMSDecoder` | Yes | | +| `OpusProjectionEncoder` | Yes | | +| `OpusProjectionDecoder` | Yes | | +| `OpusRepacketizer` | Yes (twice) | Duplicate call — harmless but wasteful | +| **`OpusCustomMode`** | **No** | **Missing. Will cause runtime failure on web if used.** | + +None of the opaque types have type arguments, which satisfies that constraint. + +**Verdict:** Non-compliant for `OpusCustomMode`. The duplicate `OpusRepacketizer` +registration is a code smell but not a functional bug. + +### 12.4 No Type Checking on Function Lookups + +**wasm_ffi rule:** "The actual type argument `NF` (or `T` respectively) is not used: +There is no type checking, if the function exported from WebAssembly has the same +signature or amount of parameters, only the name is looked up." + +**Implication for this project:** On native `dart:ffi`, a `lookupFunction` with a +mismatched C typedef will cause a compile-time or load-time error. On `wasm_ffi`, the +C typedef is completely ignored — only the function name matters. If a Dart typedef +has the wrong number of parameters or wrong types, the call will silently pass +incorrect values to the WASM function, leading to memory corruption or wrong results +rather than a clear error. + +**Project status:** The typedefs in `opus_encoder.dart` and `opus_decoder.dart` were +manually written to match the opus C API. They have been stable and match the libopus +1.5.2 API. However, there is no automated verification that these match the WASM +exports. A signature mismatch would only manifest as silent data corruption on web +while working correctly on native. + +### 12.5 Return Type Constraints + +**wasm_ffi rule:** Only specific return types are allowed for functions resolved via +`lookupFunction` / `asFunction`. The allowed list includes: `int`, `double`, `bool`, +`void`, `Pointer` for primitive types, `Pointer`, `Pointer` +(if registered), and double-nested pointers `Pointer>`. + +**Audit of all return types used in the project:** + +| Function | Return type | Allowed? | +|----------|------------|----------| +| `opus_encoder_get_size` | `int` | Yes | +| `opus_encoder_create` | `Pointer` | Yes (registered Opaque) | +| `opus_encoder_init` | `int` | Yes | +| `opus_encode` | `int` | Yes | +| `opus_encode_float` | `int` | Yes | +| `opus_encoder_destroy` | `void` | Yes | +| `opus_encoder_ctl` | `int` | Yes | +| `opus_decoder_get_size` | `int` | Yes | +| `opus_decoder_create` | `Pointer` | Yes (registered Opaque) | +| `opus_decoder_init` | `int` | Yes | +| `opus_decode` | `int` | Yes | +| `opus_decode_float` | `int` | Yes | +| `opus_decoder_destroy` | `void` | Yes | +| `opus_packet_parse` | `int` | Yes | +| `opus_packet_get_bandwidth` | `int` | Yes | +| `opus_packet_get_samples_per_frame` | `int` | Yes | +| `opus_packet_get_nb_channels` | `int` | Yes | +| `opus_packet_get_nb_frames` | `int` | Yes | +| `opus_packet_get_nb_samples` | `int` | Yes | +| `opus_decoder_get_nb_samples` | `int` | Yes | +| `opus_pcm_soft_clip` | `void` | Yes | +| `opus_get_version_string` | `Pointer` | Yes | +| `opus_strerror` | `Pointer` | Yes | + +**Verdict:** All return types are within the allowed set. No issues. + +### 12.6 WASM Export List vs Dart Symbol Lookups + +**wasm_ffi rule:** Functions must be in the WASM module's `EXPORTED_FUNCTIONS` to be +looked up. Symbols not exported will cause a runtime error on lookup. + +The Emscripten build (`opus_flutter_web/Dockerfile`) exports these C symbols: + +``` +_malloc, _free, +_opus_get_version_string, _opus_strerror, +_opus_encoder_get_size, _opus_encoder_create, _opus_encoder_init, +_opus_encode, _opus_encode_float, _opus_encoder_destroy, +_opus_decoder_get_size, _opus_decoder_create, _opus_decoder_init, +_opus_decode, _opus_decode_float, _opus_decoder_destroy, +_opus_packet_parse, _opus_packet_get_bandwidth, +_opus_packet_get_samples_per_frame, _opus_packet_get_nb_channels, +_opus_packet_get_nb_frames, _opus_packet_get_nb_samples, +_opus_decoder_get_nb_samples, _opus_pcm_soft_clip +``` + +**Dart lookups in `FunctionsAndGlobals` constructors (eager):** + +| Symbol | Exported? | Lookup timing | +|--------|-----------|--------------| +| `opus_get_version_string` | Yes | Eager (constructor) | +| `opus_strerror` | Yes | Eager (constructor) | +| `opus_encoder_get_size` | Yes | Eager (constructor) | +| `opus_encoder_create` | Yes | Eager (constructor) | +| `opus_encoder_init` | Yes | Eager (constructor) | +| `opus_encode` | Yes | Eager (constructor) | +| `opus_encode_float` | Yes | Eager (constructor) | +| `opus_encoder_destroy` | Yes | Eager (constructor) | +| **`opus_encoder_ctl`** | **No** | **Lazy (`late final`)** | +| `opus_decoder_get_size` | Yes | Eager (constructor) | +| `opus_decoder_create` | Yes | Eager (constructor) | +| `opus_decoder_init` | Yes | Eager (constructor) | +| `opus_decode` | Yes | Eager (constructor) | +| `opus_decode_float` | Yes | Eager (constructor) | +| `opus_decoder_destroy` | Yes | Eager (constructor) | +| `opus_packet_parse` | Yes | Eager (constructor) | +| `opus_packet_get_bandwidth` | Yes | Eager (constructor) | +| `opus_packet_get_samples_per_frame` | Yes | Eager (constructor) | +| `opus_packet_get_nb_channels` | Yes | Eager (constructor) | +| `opus_packet_get_nb_frames` | Yes | Eager (constructor) | +| `opus_packet_get_nb_samples` | Yes | Eager (constructor) | +| `opus_decoder_get_nb_samples` | Yes | Eager (constructor) | +| `opus_pcm_soft_clip` | Yes | Eager (constructor) | + +**Bug: `opus_encoder_ctl` is not exported from the WASM binary.** The binding uses a +`late final` field, so the lookup is deferred until first use. This means: + +- Library loading succeeds. +- Creating encoders/decoders works fine. +- Encoding/decoding works fine. +- The **first call** to `BufferedOpusEncoder.encoderCtl()` on web will throw when + `_opus_encoder_ctlPtr` tries to look up `opus_encoder_ctl` in the WASM module. + +This is a **latent bug** — it only surfaces when a consumer actually calls `encoderCtl`, +which may not happen in typical usage but will crash any advanced use case that tries +to set bitrate, complexity, or other encoder parameters on web. + +**Fix required in two places:** +1. Add `"_opus_encoder_ctl"` to `EXPORTED_FUNCTIONS` in the Dockerfile. +2. Consider whether the variadic binding even works correctly under WASM (see 12.7). + +### 12.7 Variadic Functions Under WASM + +**wasm_ffi context:** Emscripten compiles variadic C functions to WASM using a specific +ABI where variadic arguments are passed via a stack-allocated buffer. The compiled WASM +function signature may not match what a simple `lookupFunction` binding expects. + +`opus_encoder_ctl` in C is: +```c +int opus_encoder_ctl(OpusEncoder *st, int request, ...); +``` + +The Dart binding treats it as a fixed 3-argument function: +```dart +int Function(Pointer, int, int) +``` + +**Problem on WASM:** When Emscripten compiles a variadic function, the resulting WASM +function may take a different number of parameters than the Dart side expects (often a +pointer to the variadic argument buffer). Since `wasm_ffi` performs no type checking on +lookups, this mismatch would not be caught — the Dart side would call the WASM function +with the wrong number/types of arguments, causing undefined behavior. + +On native `dart:ffi`, variadic function support was added in Dart 3.0 with specific +annotations. The current binding bypasses this by using `lookup` + `asFunction` directly, +which happens to work on most native platforms due to calling convention compatibility, +but is technically incorrect and non-portable. + +### 12.8 Memory Growth and `asTypedList` Views + +**wasm_ffi context:** The Dockerfile uses `-s ALLOW_MEMORY_GROWTH=1`. When WASM memory +grows (e.g. due to a `malloc` that exceeds the current memory size), the underlying +`ArrayBuffer` is replaced. Existing `TypedArray` views into the old buffer become +**detached** (invalid). + +**Risk in this project:** The `Buffered*` implementations return `asTypedList` views: + +```dart +Uint8List get inputBuffer => _inputBuffer.asTypedList(maxInputBufferSizeBytes); +Uint8List get outputBuffer => _outputBuffer.asTypedList(_outputBufferIndex); +``` + +If a consumer holds a reference to `inputBuffer` or `outputBuffer`, and a subsequent +allocation (e.g. creating another encoder/decoder, or any `opus.allocator.call`) +triggers WASM memory growth, the held view becomes a detached `TypedArray`. Accessing +it will throw or return garbage. + +On native `dart:ffi`, `asTypedList` returns a view into process memory that remains +valid as long as the pointer is valid. This asymmetry means code that works on native +may silently break on web. + +**Affected code paths:** + +1. `BufferedOpusEncoder.inputBuffer` — returned to user for writing samples. +2. `BufferedOpusEncoder.outputBuffer` — returned to user after encoding. +3. `BufferedOpusDecoder.inputBuffer` — returned to user for writing packets. +4. `BufferedOpusDecoder.outputBuffer` — returned to user after decoding. +5. `BufferedOpusDecoder.outputBufferAsInt16List` / `outputBufferAsFloat32List` — cast + views of the output buffer. +6. `StreamOpusEncoder.bind` — caches `_encoder.inputBuffer` in a local variable at the + start of the stream, then reuses it across all iterations. If memory grows during + the stream, this cached view is stale. +7. Any `asTypedList` call in `SimpleOpus*` encode/decode — these are short-lived + (freed in the same `finally` block), so the risk is lower. + +### 12.9 `Memory.init()` and Global Memory + +**wasm_ffi rule:** "The first call you should do when you want to use wasm_ffi is +`Memory.init()`." (The README also notes this is "now automated" in newer versions.) + +**Project status:** The project does **not** call `Memory.init()` explicitly. Instead, +it relies on `DynamicLibrary.open(..., useAsGlobal: GlobalMemory.ifNotSet)` to set up +the global memory. This appears to be the newer automated approach documented by +`wasm_ffi`, where `DynamicLibrary.open` handles memory initialization internally. + +**Verdict:** Likely compliant with current `wasm_ffi` versions (`^2.1.0`). If the +project ever downgrades or the `wasm_ffi` API changes, the missing `Memory.init()` +could become a problem. + +### 12.10 `nullptr` on Web + +**wasm_ffi context:** `nullptr` in `wasm_ffi` is `Pointer.fromAddress(0)`. This +requires a valid `Memory.global` to bind to. Since the project sets global memory via +`useAsGlobal: GlobalMemory.ifNotSet`, `nullptr` should work correctly after library +loading. + +**Risk:** If any code path uses `nullptr` before `initOpus()` is called (which triggers +`DynamicLibrary.open` and sets global memory), the `Pointer.fromAddress(0)` call would +throw because `Memory.global` is not set. + +The project's `late ApiObject opus` global and the initialization flow (`load()` then +`initOpus()`) mean `nullptr` is only used in encoder/decoder methods that run after +init. This ordering is safe. + +### 12.11 Emscripten Build Configuration Audit + +Checking the Dockerfile against `wasm_ffi` requirements: + +| Requirement | Status | Detail | +|-------------|--------|--------| +| `MODULARIZE=1` | Present | Required for `DynamicLibrary.open` | +| `EXPORT_NAME` | `libopus` | Matches `moduleName: 'libopus'` in Dart | +| `ALLOW_MEMORY_GROWTH=1` | Present | Required for dynamic allocation | +| `EXPORTED_RUNTIME_METHODS=["HEAPU8"]` | Present | **Required** by `wasm_ffi` for memory access | +| `_malloc` in `EXPORTED_FUNCTIONS` | Present | Required for allocator | +| `_free` in `EXPORTED_FUNCTIONS` | Present | Required for allocator | +| All used C functions exported | **Partial** | `_opus_encoder_ctl` missing | + +**Verdict:** Build configuration is correct except for the missing `_opus_encoder_ctl` +export. + +--- + +## 13. Web-Specific Risk Summary + +Risks specific to the web platform, derived from cross-referencing with `wasm_ffi` +documentation: + +| # | Risk | Severity | Detail | +|---|------|----------|--------| +| W1 | **`opus_encoder_ctl` not exported from WASM** | High | Calling `encoderCtl()` on web will throw. The symbol is missing from the Dockerfile's `EXPORTED_FUNCTIONS`. | +| W2 | **Variadic `opus_encoder_ctl` ABI mismatch** | High | Even if exported, Emscripten's variadic function ABI may not match the Dart binding's fixed 3-arg signature. The WASM function likely expects a pointer to a variadic arg buffer, not direct arguments. | +| W3 | **`asTypedList` views detach on memory growth** | Medium | `ALLOW_MEMORY_GROWTH=1` means `malloc` can trigger buffer replacement. Held `asTypedList` views (especially in `Buffered*` classes and `StreamOpusEncoder.bind`) become invalid. | +| W4 | **`StreamOpusEncoder` caches stale buffer view** | Medium | `bind()` stores `_encoder.inputBuffer` in a local variable at stream start. If WASM memory grows during the stream, this view is detached. | +| W5 | **`OpusCustomMode` not registered** | Medium | `registerOpaqueType()` is missing from `init_web.dart`. Using opus custom mode API on web will fail. | +| W6 | **No function signature validation** | Low | `wasm_ffi` does not validate that Dart typedefs match WASM function signatures. A typedef error would cause silent data corruption on web while working on native. | +| W7 | **`free(nullptr)` behavior unverified** | Low | After packet-loss decode, `free(nullptr)` is called. The WASM `_free` (Emscripten's `free`) should handle `NULL` safely per C standard, but this is not explicitly guaranteed by `wasm_ffi`. | +| W8 | **`Pointer[i]` indexing in `_asString`** | Low | `_asString` uses `pointer[i]` to walk memory byte-by-byte. This should work identically on `wasm_ffi` (linear memory indexing), but the lack of bounds checking means an invalid pointer walks arbitrary WASM memory rather than segfaulting. | + +--- + +## 14. Combined Risk Matrix (All Platforms) + +Merging the original findings (Section 10) with web-specific findings (Section 13): + +| # | Risk | Platform | Severity | Location | +|---|------|----------|----------|----------| +| 1 | `opus_encoder_ctl` not exported from WASM | Web | **High** | `Dockerfile`, `opus_encoder.dart` | +| 2 | Variadic `opus_encoder_ctl` ABI mismatch under WASM | Web | **High** | `opus_encoder.dart:212-217` | +| 3 | Use-after-destroy (no `_destroyed` check in encode/decode) | All | **High** | `SimpleOpus*`, `BufferedOpus*` | +| 4 | Output buffer sizing bug (samples vs bytes) | All | **High** | `BufferedOpusDecoder` factory, `StreamOpusDecoder` ctor | +| 5 | `asTypedList` views detach on WASM memory growth | Web | **Medium** | `BufferedOpus*` buffer getters | +| 6 | `StreamOpusEncoder.bind` caches stale buffer view | Web | **Medium** | `opus_dart_streaming.dart:129` | +| 7 | `OpusCustomMode` not registered on web | Web | **Medium** | `init_web.dart` | +| 8 | Duplicate `OpusRepacketizer` registration | Web | **Low** | `init_web.dart:28` | +| 9 | No `NativeFinalizer` — leaked memory if `destroy()` skipped | All | **Medium** | All encoder/decoder classes | +| 10 | `copyOutput = false` use-after-write | All | **Medium** | `StreamOpusEncoder`, `StreamOpusDecoder` | +| 11 | FEC double-yield overwrites with `copyOutput = false` | All | **Medium** | `opus_dart_streaming.dart:321-327` | +| 12 | Memory leak if second allocation throws | All | **Low** | `SimpleOpus*.encode/decode` | +| 13 | `free(nullptr)` behavior on web | Web | **Low** | `SimpleOpusDecoder.decode` finally | +| 14 | No function signature validation on web | Web | **Low** | All `lookupFunction` calls | +| 15 | `_asString` unbounded loop | All | **Low** | `opus_dart_misc.dart:26-31` | +| 16 | `opus_encoder_ctl` variadic binding (native) | Native | **Low** | `opus_encoder.dart` | +| 17 | No `opus_decoder_ctl` binding | All | **Low** | `opus_decoder.dart` | +| 18 | `late` global `opus` without guard | All | **Low** | `opus_dart_misc.dart:55` | +| 19 | Linux/Windows temp dir library lifetime | Native | **Low** | Platform packages | From 59a79237036062bfd274bbfb8e87fbbbf5c768d2 Mon Sep 17 00:00:00 2001 From: Pedro Paulo de Amorim Date: Wed, 25 Feb 2026 00:14:35 +0000 Subject: [PATCH 07/21] fix: guard encoder/decoder methods against use after destroy All public methods on SimpleOpusEncoder, BufferedOpusEncoder, SimpleOpusDecoder, and BufferedOpusDecoder now throw OpusDestroyedError before touching native pointers if destroy() was already called. This prevents undefined behavior from dangling pointer access. Adds 10 regression tests covering every affected method. --- docs/ffi-analysis.md | 15 ++--- opus_dart/lib/src/opus_dart_decoder.dart | 5 ++ opus_dart/lib/src/opus_dart_encoder.dart | 5 ++ opus_dart/test/opus_dart_mock_test.dart | 82 ++++++++++++++++++++++++ 4 files changed, 99 insertions(+), 8 deletions(-) diff --git a/docs/ffi-analysis.md b/docs/ffi-analysis.md index 05618e5..317b0c8 100644 --- a/docs/ffi-analysis.md +++ b/docs/ffi-analysis.md @@ -373,12 +373,11 @@ void destroy() { } ``` -**Concern:** While `destroy()` checks `_destroyed`, the `encode`/`decode`/`encodeFloat`/ -`decodeFloat` methods do **not** check `_destroyed` before using the native pointer. -Calling `encode()` after `destroy()` passes a dangling pointer to opus, causing -undefined behavior. The abstract base classes document that calling methods after -`destroy()` should throw `OpusDestroyedError`, but this is not enforced in the -`Simple*` or `Buffered*` implementations. +All public methods (`encode`, `encodeFloat`, `decode`, `decodeFloat`, `encoderCtl`, +`pcmSoftClipOutputBuffer`) now check `_destroyed` at the top and throw +`OpusDestroyedError` before touching any native pointer. This matches the contract +documented in the abstract base classes. (**Fixed** — previously these methods had no +guard and would pass dangling pointers to opus after `destroy()`.) --- @@ -481,7 +480,7 @@ argument is an integer. However: | 1 | **Duplicate `registerOpaqueType` / missing `OpusCustomMode`** | `init_web.dart:28` | Medium | `OpusRepacketizer` registered twice; `OpusCustomMode` never registered. Using custom mode on web will fail. | | 2 | **Memory leak if second allocation throws** | `SimpleOpusEncoder.encode`, `SimpleOpusDecoder.decode`, and float variants | Low | If the second `allocator.call` throws, the first allocation is not freed. | | 3 | **No `NativeFinalizer`** | All encoder/decoder classes | Medium | If `destroy()` is never called, native memory leaks permanently. No GC-driven cleanup. | -| 4 | **Use-after-destroy (dangling pointer)** | `SimpleOpusEncoder`, `SimpleOpusDecoder`, `BufferedOpusEncoder`, `BufferedOpusDecoder` | High | `encode()`/`decode()` do not check `_destroyed` before using the native pointer. Calling them after `destroy()` is undefined behavior. | +| 4 | ~~**Use-after-destroy (dangling pointer)**~~ | `SimpleOpusEncoder`, `SimpleOpusDecoder`, `BufferedOpusEncoder`, `BufferedOpusDecoder` | ~~High~~ **Fixed** | All public methods now check `_destroyed` and throw `OpusDestroyedError` before touching native pointers. | | 5 | **`copyOutput = false` use-after-write** | `StreamOpusEncoder`, `StreamOpusDecoder` | Medium | Yielded views point to native buffers that get overwritten on next call. | | 6 | **`StreamOpusDecoder` FEC double-yield overwrites** | `opus_dart_streaming.dart:321-327` | Medium | With `copyOutput = false` and FEC, the first yielded output is overwritten before the consumer reads it. | | 7 | **Output buffer too small for float decode** | `BufferedOpusDecoder` factory, `StreamOpusDecoder` constructor | High | `maxOutputBufferSizeBytes` defaults to `maxSamplesPerPacket` (a sample count, not byte count). For float output (4 bytes/sample), the buffer is 4x too small. `StreamOpusDecoder` has an inverted multiplier. | @@ -857,7 +856,7 @@ Merging the original findings (Section 10) with web-specific findings (Section 1 |---|------|----------|----------|----------| | 1 | `opus_encoder_ctl` not exported from WASM | Web | **High** | `Dockerfile`, `opus_encoder.dart` | | 2 | Variadic `opus_encoder_ctl` ABI mismatch under WASM | Web | **High** | `opus_encoder.dart:212-217` | -| 3 | Use-after-destroy (no `_destroyed` check in encode/decode) | All | **High** | `SimpleOpus*`, `BufferedOpus*` | +| 3 | ~~Use-after-destroy (no `_destroyed` check in encode/decode)~~ | All | ~~**High**~~ **Fixed** | `SimpleOpus*`, `BufferedOpus*` — all public methods now throw `OpusDestroyedError` before touching native pointers | | 4 | Output buffer sizing bug (samples vs bytes) | All | **High** | `BufferedOpusDecoder` factory, `StreamOpusDecoder` ctor | | 5 | `asTypedList` views detach on WASM memory growth | Web | **Medium** | `BufferedOpus*` buffer getters | | 6 | `StreamOpusEncoder.bind` caches stale buffer view | Web | **Medium** | `opus_dart_streaming.dart:129` | diff --git a/opus_dart/lib/src/opus_dart_decoder.dart b/opus_dart/lib/src/opus_dart_decoder.dart index f97738f..4f04547 100644 --- a/opus_dart/lib/src/opus_dart_decoder.dart +++ b/opus_dart/lib/src/opus_dart_decoder.dart @@ -98,6 +98,7 @@ class SimpleOpusDecoder extends OpusDecoder { /// The input bytes need to represent a whole packet! @override Int16List decode({Uint8List? input, bool fec = false, int? loss}) { + if (_destroyed) throw OpusDestroyedError.decoder(); Pointer outputNative = opus.allocator.call(_maxSamplesPerPacket); Pointer inputNative; @@ -141,6 +142,7 @@ class SimpleOpusDecoder extends OpusDecoder { bool fec = false, bool autoSoftClip = false, int? loss}) { + if (_destroyed) throw OpusDestroyedError.decoder(); Pointer outputNative = opus.allocator.call(_maxSamplesPerPacket); Pointer inputNative; @@ -362,6 +364,7 @@ class BufferedOpusDecoder extends OpusDecoder { /// The returned list is actually just the [outputBufferAsInt16List]. @override Int16List decode({bool fec = false, int? loss}) { + if (_destroyed) throw OpusDestroyedError.decoder(); Pointer inputNative; int frameSize; if (inputBufferIndex > 0) { @@ -397,6 +400,7 @@ class BufferedOpusDecoder extends OpusDecoder { @override Float32List decodeFloat( {bool autoSoftClip = false, bool fec = false, int? loss}) { + if (_destroyed) throw OpusDestroyedError.decoder(); Pointer inputNative; int frameSize; if (inputBufferIndex > 0) { @@ -440,6 +444,7 @@ class BufferedOpusDecoder extends OpusDecoder { /// /// Behaves like the toplevel [pcmSoftClip] function, but without unnecessary copying. Float32List pcmSoftClipOutputBuffer() { + if (_destroyed) throw OpusDestroyedError.decoder(); opus.decoder.opus_pcm_soft_clip(_outputBuffer.cast(), _outputBufferIndex ~/ (4 * channels), channels, _softClipBuffer); return outputBufferAsFloat32List; diff --git a/opus_dart/lib/src/opus_dart_encoder.dart b/opus_dart/lib/src/opus_dart_encoder.dart index 0c854c9..12defdb 100644 --- a/opus_dart/lib/src/opus_dart_encoder.dart +++ b/opus_dart/lib/src/opus_dart_encoder.dart @@ -62,6 +62,7 @@ class SimpleOpusEncoder extends OpusEncoder { /// [input] and the returned list are copied to and respectively from native memory. Uint8List encode( {required Int16List input, int maxOutputSizeBytes = maxDataBytes}) { + if (_destroyed) throw OpusDestroyedError.encoder(); Pointer inputNative = opus.allocator.call(input.length); inputNative.asTypedList(input.length).setAll(0, input); Pointer outputNative = @@ -85,6 +86,7 @@ class SimpleOpusEncoder extends OpusEncoder { /// This method behaves just as [encode], so see there for more information. Uint8List encodeFloat( {required Float32List input, int maxOutputSizeBytes = maxDataBytes}) { + if (_destroyed) throw OpusDestroyedError.encoder(); Pointer inputNative = opus.allocator.call(input.length); inputNative.asTypedList(input.length).setAll(0, input); Pointer outputNative = @@ -240,6 +242,7 @@ class BufferedOpusEncoder extends OpusEncoder { } int encoderCtl({required int request, required int value}) { + if (_destroyed) throw OpusDestroyedError.encoder(); return opus.encoder.opus_encoder_ctl(_opusEncoder, request, value); } @@ -255,6 +258,7 @@ class BufferedOpusEncoder extends OpusEncoder { /// /// The returned list is actually just the [outputBuffer]. Uint8List encode() { + if (_destroyed) throw OpusDestroyedError.encoder(); int sampleCountPerChannel = inputBufferIndex ~/ (channels * 2); _outputBufferIndex = opus.encoder.opus_encode( _opusEncoder, @@ -275,6 +279,7 @@ class BufferedOpusEncoder extends OpusEncoder { /// Except that the sample count is calculated by dividing the [inputBufferIndex] by 4 and not by 2, /// this method behaves just as [encode], so see there for more information. Uint8List encodeFloat() { + if (_destroyed) throw OpusDestroyedError.encoder(); int sampleCountPerChannel = inputBufferIndex ~/ (channels * 4); _outputBufferIndex = opus.encoder.opus_encode_float( _opusEncoder, diff --git a/opus_dart/test/opus_dart_mock_test.dart b/opus_dart/test/opus_dart_mock_test.dart index d1fd49c..9bfa7ef 100644 --- a/opus_dart/test/opus_dart_mock_test.dart +++ b/opus_dart/test/opus_dart_mock_test.dart @@ -248,6 +248,25 @@ void main() { encoder.destroy(); expect(encoder.destroyed, isTrue); }); + + test('encode after destroy throws OpusDestroyedError', () { + final encoder = createEncoder(); + encoder.destroy(); + expect( + () => encoder.encode(input: Int16List.fromList(List.filled(1920, 0))), + throwsA(isA()), + ); + }); + + test('encodeFloat after destroy throws OpusDestroyedError', () { + final encoder = createEncoder(); + encoder.destroy(); + expect( + () => encoder.encodeFloat( + input: Float32List.fromList(List.filled(1920, 0.0))), + throwsA(isA()), + ); + }); }); // --------------------------------------------------------------------------- @@ -499,6 +518,24 @@ void main() { decoder.destroy(); expect(decoder.destroyed, isTrue); }); + + test('decode after destroy throws OpusDestroyedError', () { + final decoder = createDecoder(); + decoder.destroy(); + expect( + () => decoder.decode(input: Uint8List.fromList([0x01])), + throwsA(isA()), + ); + }); + + test('decodeFloat after destroy throws OpusDestroyedError', () { + final decoder = createDecoder(); + decoder.destroy(); + expect( + () => decoder.decodeFloat(input: Uint8List.fromList([0x01])), + throwsA(isA()), + ); + }); }); // --------------------------------------------------------------------------- @@ -617,6 +654,30 @@ void main() { expect(encoder.destroyed, isTrue); }); + test('encode after destroy throws OpusDestroyedError', () { + final encoder = createBufferedEncoder(); + encoder.inputBufferIndex = 3840; + encoder.destroy(); + expect(() => encoder.encode(), throwsA(isA())); + }); + + test('encodeFloat after destroy throws OpusDestroyedError', () { + final encoder = createBufferedEncoder(); + encoder.inputBufferIndex = 7680; + encoder.destroy(); + expect(() => encoder.encodeFloat(), throwsA(isA())); + }); + + test('encoderCtl after destroy throws OpusDestroyedError', () { + final encoder = createBufferedEncoder(); + encoder.destroy(); + expect( + () => encoder.encoderCtl( + request: OPUS_SET_BITRATE_REQUEST, value: 64000), + throwsA(isA()), + ); + }); + test('respects custom buffer sizes', () { when(mockEncoder.opus_encoder_create(any, any, any, any)) .thenAnswer((inv) { @@ -951,6 +1012,27 @@ void main() { expect(decoder.destroyed, isTrue); }); + test('decode after destroy throws OpusDestroyedError', () { + final decoder = createBufferedDecoder(); + decoder.inputBufferIndex = 10; + decoder.destroy(); + expect(() => decoder.decode(), throwsA(isA())); + }); + + test('decodeFloat after destroy throws OpusDestroyedError', () { + final decoder = createBufferedDecoder(); + decoder.inputBufferIndex = 10; + decoder.destroy(); + expect(() => decoder.decodeFloat(), throwsA(isA())); + }); + + test('pcmSoftClipOutputBuffer after destroy throws OpusDestroyedError', () { + final decoder = createBufferedDecoder(); + decoder.destroy(); + expect(() => decoder.pcmSoftClipOutputBuffer(), + throwsA(isA())); + }); + test('respects custom buffer sizes', () { when(mockDecoder.opus_decoder_create(any, any, any)).thenAnswer((inv) { (inv.positionalArguments[2] as Pointer).value = OPUS_OK; From 0669864245af6fa7b0bbbbf20135869cffab90da Mon Sep 17 00:00:00 2001 From: Pedro Paulo de Amorim Date: Wed, 25 Feb 2026 00:19:58 +0000 Subject: [PATCH 08/21] fix: correct output buffer sizing and opaque type registration - BufferedOpusDecoder: default maxOutputBufferSizeBytes now uses 4 * maxSamplesPerPacket to accommodate float output (was using raw sample count as byte count, 4x too small for floats). - StreamOpusDecoder: fix inverted multiplier from (floats ? 2 : 4) to (floats ? 4 : 2). - init_web.dart: replace duplicate OpusRepacketizer registration with missing OpusCustomMode. - Add unit tests confirming buffer sizing and update ffi-analysis.md. --- README.md | 22 +++--- docs/ffi-analysis.md | 68 +++++++------------ opus_dart/lib/src/init_web.dart | 2 +- opus_dart/lib/src/opus_dart_decoder.dart | 2 +- opus_dart/lib/src/opus_dart_streaming.dart | 2 +- opus_dart/test/opus_dart_mock_test.dart | 29 ++++++++ .../test/opus_dart_streaming_mock_test.dart | 25 +++++++ 7 files changed, 91 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 02bcabf..221aef8 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,15 @@ This monorepo contains a federated Flutter plugin that loads libopus on each sup | Package | Directory | Version | |---------|-----------|---------| -| [opus_codec](https://pub.dev/packages/opus_codec) | [opus_flutter](./opus_flutter) | 3.0.4 | -| [opus_codec_dart](https://pub.dev/packages/opus_codec_dart) | [opus_dart](./opus_dart) | 3.0.4 | -| [opus_codec_platform_interface](https://pub.dev/packages/opus_codec_platform_interface) | [opus_flutter_platform_interface](./opus_flutter_platform_interface) | 3.0.4 | -| [opus_codec_android](https://pub.dev/packages/opus_codec_android) | [opus_flutter_android](./opus_flutter_android) | 3.0.4 | -| [opus_codec_ios](https://pub.dev/packages/opus_codec_ios) | [opus_flutter_ios](./opus_flutter_ios) | 3.0.4 | -| [opus_codec_linux](https://pub.dev/packages/opus_codec_linux) | [opus_flutter_linux](./opus_flutter_linux) | 3.0.4 | -| [opus_codec_macos](https://pub.dev/packages/opus_codec_macos) | [opus_flutter_macos](./opus_flutter_macos) | 3.0.4 | -| [opus_codec_web](https://pub.dev/packages/opus_codec_web) | [opus_flutter_web](./opus_flutter_web) | 3.0.4 | -| [opus_codec_windows](https://pub.dev/packages/opus_codec_windows) | [opus_flutter_windows](./opus_flutter_windows) | 3.0.4 | +| [opus_codec](https://pub.dev/packages/opus_codec) | [opus_flutter](./opus_flutter) | 3.0.5 | +| [opus_codec_dart](https://pub.dev/packages/opus_codec_dart) | [opus_dart](./opus_dart) | 3.0.5 | +| [opus_codec_platform_interface](https://pub.dev/packages/opus_codec_platform_interface) | [opus_flutter_platform_interface](./opus_flutter_platform_interface) | 3.0.5 | +| [opus_codec_android](https://pub.dev/packages/opus_codec_android) | [opus_flutter_android](./opus_flutter_android) | 3.0.5 | +| [opus_codec_ios](https://pub.dev/packages/opus_codec_ios) | [opus_flutter_ios](./opus_flutter_ios) | 3.0.5 | +| [opus_codec_linux](https://pub.dev/packages/opus_codec_linux) | [opus_flutter_linux](./opus_flutter_linux) | 3.0.5 | +| [opus_codec_macos](https://pub.dev/packages/opus_codec_macos) | [opus_flutter_macos](./opus_flutter_macos) | 3.0.5 | +| [opus_codec_web](https://pub.dev/packages/opus_codec_web) | [opus_flutter_web](./opus_flutter_web) | 3.0.5 | +| [opus_codec_windows](https://pub.dev/packages/opus_codec_windows) | [opus_flutter_windows](./opus_flutter_windows) | 3.0.5 | ## Platform support @@ -32,8 +32,8 @@ Add `opus_codec` to your `pubspec.yaml`: ```yaml dependencies: - opus_codec: ^3.0.4 - opus_codec_dart: ^3.0.4 + opus_codec: ^3.0.5 + opus_codec_dart: ^3.0.5 ``` Platform packages are automatically included through the federated plugin system -- you don't need to add them individually. diff --git a/docs/ffi-analysis.md b/docs/ffi-analysis.md index 317b0c8..0e328c0 100644 --- a/docs/ffi-analysis.md +++ b/docs/ffi-analysis.md @@ -56,7 +56,7 @@ ApiObject createApiObject(Object lib) { registerOpaqueType(); registerOpaqueType(); registerOpaqueType(); - registerOpaqueType(); // duplicate + registerOpaqueType(); return ApiObject(library, library.allocator); } ``` @@ -64,8 +64,8 @@ ApiObject createApiObject(Object lib) { - Casts to `wasm_ffi` `DynamicLibrary`. - Must register every `Opaque` subclass so `wasm_ffi` can handle pointer lookups. - Uses `library.allocator` (WASM linear memory allocator) instead of `malloc`. -- **Bug candidate:** `OpusRepacketizer` is registered twice; `OpusCustomMode` is - never registered. If `OpusCustomMode` pointers are ever used on web, this will fail. +- (**Fixed** — previously `OpusRepacketizer` was registered twice and `OpusCustomMode` + was missing. Now all 10 opaque types are registered exactly once.) ### 2.3 Differences Summary @@ -414,40 +414,17 @@ should be safe in practice, but there is no runtime assertion. ## 8. `BufferedOpusDecoder` Output Buffer Sizing -The `BufferedOpusDecoder` factory has this default for `maxOutputBufferSizeBytes`: +(**Fixed** — both issues below have been corrected.) -```dart -maxOutputBufferSizeBytes ??= maxSamplesPerPacket(sampleRate, channels); -``` - -`maxSamplesPerPacket` returns the number of **samples** (not bytes): - -```dart -int maxSamplesPerPacket(int sampleRate, int channels) => - ((sampleRate * channels * 120) / 1000).ceil(); -``` - -For 48000 Hz stereo, this is `ceil(48000 * 2 * 120 / 1000) = 11520` samples. - -This value is then used as a **byte count** for `opus.allocator.call(...)`. -When the output is interpreted as `Int16` (2 bytes/sample), the effective sample -capacity is `11520 / 2 = 5760`, which is exactly `maxSamplesPerPacket`. When interpreted -as `Float` (4 bytes/sample), the capacity is `11520 / 4 = 2880`, which is only half of -`maxSamplesPerPacket`. - -**Bug candidate:** For float decoding with `BufferedOpusDecoder`, the output buffer -may be too small to hold a maximum-length opus frame (120ms). A 120ms frame at 48kHz -stereo produces 11520 float samples = 46080 bytes, but the buffer is only 11520 bytes. - -Contrast with `StreamOpusDecoder`, which computes: - -```dart -maxOutputBufferSizeBytes: (floats ? 2 : 4) * maxSamplesPerPacket(sampleRate, channels) -``` +The `BufferedOpusDecoder` factory previously defaulted `maxOutputBufferSizeBytes` to +`maxSamplesPerPacket(sampleRate, channels)` — a sample count, not a byte count. Since +the buffer must accommodate float output (4 bytes/sample), this was 4x too small for +`decodeFloat` with maximum-length opus frames (120ms). Fixed to +`4 * maxSamplesPerPacket(sampleRate, channels)`. -This looks inverted — it allocates **less** space for float (multiplier 2) and more -for s16le (multiplier 4). The correct multipliers should be 4 for float -(`Float` = 4 bytes) and 2 for s16le (`Int16` = 2 bytes). +`StreamOpusDecoder` previously computed +`(floats ? 2 : 4) * maxSamplesPerPacket(...)` — inverted multipliers that allocated +less space for float (which needs more). Fixed to `(floats ? 4 : 2)`. --- @@ -477,13 +454,13 @@ argument is an integer. However: | # | Risk | Location | Severity | Detail | |---|------|----------|----------|--------| -| 1 | **Duplicate `registerOpaqueType` / missing `OpusCustomMode`** | `init_web.dart:28` | Medium | `OpusRepacketizer` registered twice; `OpusCustomMode` never registered. Using custom mode on web will fail. | +| 1 | **Duplicate `registerOpaqueType` / missing `OpusCustomMode`** | `init_web.dart:28` | Fixed | Duplicate `OpusRepacketizer` removed; `OpusCustomMode` now registered. | | 2 | **Memory leak if second allocation throws** | `SimpleOpusEncoder.encode`, `SimpleOpusDecoder.decode`, and float variants | Low | If the second `allocator.call` throws, the first allocation is not freed. | | 3 | **No `NativeFinalizer`** | All encoder/decoder classes | Medium | If `destroy()` is never called, native memory leaks permanently. No GC-driven cleanup. | | 4 | ~~**Use-after-destroy (dangling pointer)**~~ | `SimpleOpusEncoder`, `SimpleOpusDecoder`, `BufferedOpusEncoder`, `BufferedOpusDecoder` | ~~High~~ **Fixed** | All public methods now check `_destroyed` and throw `OpusDestroyedError` before touching native pointers. | | 5 | **`copyOutput = false` use-after-write** | `StreamOpusEncoder`, `StreamOpusDecoder` | Medium | Yielded views point to native buffers that get overwritten on next call. | | 6 | **`StreamOpusDecoder` FEC double-yield overwrites** | `opus_dart_streaming.dart:321-327` | Medium | With `copyOutput = false` and FEC, the first yielded output is overwritten before the consumer reads it. | -| 7 | **Output buffer too small for float decode** | `BufferedOpusDecoder` factory, `StreamOpusDecoder` constructor | High | `maxOutputBufferSizeBytes` defaults to `maxSamplesPerPacket` (a sample count, not byte count). For float output (4 bytes/sample), the buffer is 4x too small. `StreamOpusDecoder` has an inverted multiplier. | +| 7 | ~~**Output buffer too small for float decode**~~ | `BufferedOpusDecoder` factory, `StreamOpusDecoder` constructor | ~~High~~ **Fixed** | `BufferedOpusDecoder` default now uses `4 * maxSamplesPerPacket`. `StreamOpusDecoder` multiplier corrected to `(floats ? 4 : 2)`. | | 8 | **`free(nullptr)` on web** | `SimpleOpusDecoder.decode` finally block | Low | After null-input decode, `free(nullptr)` is called. Safe on native; behavior on `wasm_ffi` depends on allocator implementation. | | 9 | **`_asString` unbounded loop** | `opus_dart_misc.dart:26-31` | Low | If pointer is invalid, loops until segfault. Only used with trusted libopus static strings. | | 10 | **`opus_encoder_ctl` variadic binding** | `opus_encoder.dart` | Low | Hardcoded to 3 int args. Getter CTLs (pointer arg) cannot be used correctly. | @@ -589,13 +566,14 @@ type arguments." | `OpusMSDecoder` | Yes | | | `OpusProjectionEncoder` | Yes | | | `OpusProjectionDecoder` | Yes | | -| `OpusRepacketizer` | Yes (twice) | Duplicate call — harmless but wasteful | -| **`OpusCustomMode`** | **No** | **Missing. Will cause runtime failure on web if used.** | +| `OpusRepacketizer` | Yes | | +| `OpusCustomMode` | Yes | | None of the opaque types have type arguments, which satisfies that constraint. -**Verdict:** Non-compliant for `OpusCustomMode`. The duplicate `OpusRepacketizer` -registration is a code smell but not a functional bug. +**Verdict:** Compliant. All 10 opaque subclasses are registered exactly once. +(**Fixed** — previously `OpusRepacketizer` was registered twice and `OpusCustomMode` +was missing.) ### 12.4 No Type Checking on Function Lookups @@ -841,7 +819,7 @@ documentation: | W2 | **Variadic `opus_encoder_ctl` ABI mismatch** | High | Even if exported, Emscripten's variadic function ABI may not match the Dart binding's fixed 3-arg signature. The WASM function likely expects a pointer to a variadic arg buffer, not direct arguments. | | W3 | **`asTypedList` views detach on memory growth** | Medium | `ALLOW_MEMORY_GROWTH=1` means `malloc` can trigger buffer replacement. Held `asTypedList` views (especially in `Buffered*` classes and `StreamOpusEncoder.bind`) become invalid. | | W4 | **`StreamOpusEncoder` caches stale buffer view** | Medium | `bind()` stores `_encoder.inputBuffer` in a local variable at stream start. If WASM memory grows during the stream, this view is detached. | -| W5 | **`OpusCustomMode` not registered** | Medium | `registerOpaqueType()` is missing from `init_web.dart`. Using opus custom mode API on web will fail. | +| W5 | **`OpusCustomMode` not registered** | Fixed | `registerOpaqueType()` was missing and `OpusRepacketizer` was registered twice. Fixed: duplicate removed, `OpusCustomMode` registered. | | W6 | **No function signature validation** | Low | `wasm_ffi` does not validate that Dart typedefs match WASM function signatures. A typedef error would cause silent data corruption on web while working on native. | | W7 | **`free(nullptr)` behavior unverified** | Low | After packet-loss decode, `free(nullptr)` is called. The WASM `_free` (Emscripten's `free`) should handle `NULL` safely per C standard, but this is not explicitly guaranteed by `wasm_ffi`. | | W8 | **`Pointer[i]` indexing in `_asString`** | Low | `_asString` uses `pointer[i]` to walk memory byte-by-byte. This should work identically on `wasm_ffi` (linear memory indexing), but the lack of bounds checking means an invalid pointer walks arbitrary WASM memory rather than segfaulting. | @@ -857,11 +835,11 @@ Merging the original findings (Section 10) with web-specific findings (Section 1 | 1 | `opus_encoder_ctl` not exported from WASM | Web | **High** | `Dockerfile`, `opus_encoder.dart` | | 2 | Variadic `opus_encoder_ctl` ABI mismatch under WASM | Web | **High** | `opus_encoder.dart:212-217` | | 3 | ~~Use-after-destroy (no `_destroyed` check in encode/decode)~~ | All | ~~**High**~~ **Fixed** | `SimpleOpus*`, `BufferedOpus*` — all public methods now throw `OpusDestroyedError` before touching native pointers | -| 4 | Output buffer sizing bug (samples vs bytes) | All | **High** | `BufferedOpusDecoder` factory, `StreamOpusDecoder` ctor | +| 4 | ~~Output buffer sizing bug (samples vs bytes)~~ | All | ~~**High**~~ **Fixed** | `BufferedOpusDecoder` default now uses `4 * maxSamplesPerPacket`. `StreamOpusDecoder` multiplier corrected to `(floats ? 4 : 2)`. | | 5 | `asTypedList` views detach on WASM memory growth | Web | **Medium** | `BufferedOpus*` buffer getters | | 6 | `StreamOpusEncoder.bind` caches stale buffer view | Web | **Medium** | `opus_dart_streaming.dart:129` | -| 7 | `OpusCustomMode` not registered on web | Web | **Medium** | `init_web.dart` | -| 8 | Duplicate `OpusRepacketizer` registration | Web | **Low** | `init_web.dart:28` | +| 7 | ~~`OpusCustomMode` not registered on web~~ | Web | ~~**Medium**~~ **Fixed** | Duplicate `OpusRepacketizer` removed; `OpusCustomMode` now registered in `init_web.dart` | +| 8 | ~~Duplicate `OpusRepacketizer` registration~~ | Web | ~~**Low**~~ **Fixed** | See #7 | | 9 | No `NativeFinalizer` — leaked memory if `destroy()` skipped | All | **Medium** | All encoder/decoder classes | | 10 | `copyOutput = false` use-after-write | All | **Medium** | `StreamOpusEncoder`, `StreamOpusDecoder` | | 11 | FEC double-yield overwrites with `copyOutput = false` | All | **Medium** | `opus_dart_streaming.dart:321-327` | diff --git a/opus_dart/lib/src/init_web.dart b/opus_dart/lib/src/init_web.dart index 164ce1c..67962c8 100644 --- a/opus_dart/lib/src/init_web.dart +++ b/opus_dart/lib/src/init_web.dart @@ -25,6 +25,6 @@ ApiObject createApiObject(Object lib) { registerOpaqueType(); registerOpaqueType(); registerOpaqueType(); - registerOpaqueType(); + registerOpaqueType(); return ApiObject(library, library.allocator); } diff --git a/opus_dart/lib/src/opus_dart_decoder.dart b/opus_dart/lib/src/opus_dart_decoder.dart index 4f04547..d1aa3c7 100644 --- a/opus_dart/lib/src/opus_dart_decoder.dart +++ b/opus_dart/lib/src/opus_dart_decoder.dart @@ -308,7 +308,7 @@ class BufferedOpusDecoder extends OpusDecoder { int? maxInputBufferSizeBytes, int? maxOutputBufferSizeBytes}) { maxInputBufferSizeBytes ??= maxDataBytes; - maxOutputBufferSizeBytes ??= maxSamplesPerPacket(sampleRate, channels); + maxOutputBufferSizeBytes ??= 4 * maxSamplesPerPacket(sampleRate, channels); Pointer error = opus.allocator.call(1); Pointer input = opus.allocator.call(maxInputBufferSizeBytes); Pointer output = diff --git a/opus_dart/lib/src/opus_dart_streaming.dart b/opus_dart/lib/src/opus_dart_streaming.dart index 5d6b6fe..7cf4951 100644 --- a/opus_dart/lib/src/opus_dart_streaming.dart +++ b/opus_dart/lib/src/opus_dart_streaming.dart @@ -271,7 +271,7 @@ class StreamOpusDecoder extends StreamTransformerBase> { sampleRate: sampleRate, channels: channels, maxOutputBufferSizeBytes: - (floats ? 2 : 4) * maxSamplesPerPacket(sampleRate, channels)); + (floats ? 4 : 2) * maxSamplesPerPacket(sampleRate, channels)); void _reportPacketLoss() { _decodeFec(false, loss: _decoder.lastPacketDurationMs); diff --git a/opus_dart/test/opus_dart_mock_test.dart b/opus_dart/test/opus_dart_mock_test.dart index 9bfa7ef..ad39dce 100644 --- a/opus_dart/test/opus_dart_mock_test.dart +++ b/opus_dart/test/opus_dart_mock_test.dart @@ -715,6 +715,35 @@ void main() { decoder.destroy(); }); + test('default output buffer is large enough for float decode', () { + final decoder = createBufferedDecoder(sampleRate: 48000, channels: 2); + // 4 bytes/float * maxSamplesPerPacket ensures a 120ms frame fits + expect(decoder.maxOutputBufferSizeBytes, + 4 * maxSamplesPerPacket(48000, 2)); + decoder.destroy(); + }); + + test('default output buffer is large enough for float decode (mono)', () { + final decoder = createBufferedDecoder(sampleRate: 8000, channels: 1); + expect(decoder.maxOutputBufferSizeBytes, + 4 * maxSamplesPerPacket(8000, 1)); + decoder.destroy(); + }); + + test('decodeFloat succeeds with max frame size using default buffer', () { + final decoder = createBufferedDecoder(sampleRate: 48000, channels: 2); + // 120ms at 48kHz stereo = 5760 samples per channel + const samplesPerChannel = 5760; + when(mockDecoder.opus_decode_float(any, any, any, any, any, any)) + .thenReturn(samplesPerChannel); + + decoder.inputBufferIndex = 10; + final result = decoder.decodeFloat(); + + expect(result, hasLength(samplesPerChannel * 2)); + decoder.destroy(); + }); + test('throws OpusException when native returns an error', () { when(mockDecoder.opus_decoder_create(any, any, any)).thenAnswer((inv) { (inv.positionalArguments[2] as Pointer).value = OPUS_ALLOC_FAIL; diff --git a/opus_dart/test/opus_dart_streaming_mock_test.dart b/opus_dart/test/opus_dart_streaming_mock_test.dart index f217306..fae143a 100644 --- a/opus_dart/test/opus_dart_streaming_mock_test.dart +++ b/opus_dart/test/opus_dart_streaming_mock_test.dart @@ -835,6 +835,31 @@ void main() { // --------------------------------------------------------------------------- group('StreamOpusDecoder.float bind()', () { + test('float decoder buffer fits max 120ms frame', () async { + stubDecoderCreate(); + // 120ms at 8kHz mono = 960 samples per channel + const maxSamplesPerCh = 960; + when(mockDecoder.opus_decode_float(any, any, any, any, any, any)) + .thenAnswer((inv) { + final ptr = inv.positionalArguments[3] as Pointer; + for (var i = 0; i < maxSamplesPerCh; i++) { + ptr[i] = 0.0; + } + return maxSamplesPerCh; + }); + + final decoder = StreamOpusDecoder.float( + sampleRate: _sampleRate, + channels: _channels, + ); + + final packet = Uint8List.fromList([0x01]); + final results = await decoder.bind(Stream.value(packet)).toList(); + + expect(results, hasLength(1)); + expect(results[0], hasLength(maxSamplesPerCh)); + }); + test('packet emits Float32List', () async { stubDecoderCreate(); stubDecodeFloat([0.5, -0.25]); From 35fed4c6c9c4d4ff4f6e49e26bafa593694b9a73 Mon Sep 17 00:00:00 2001 From: Pedro Paulo de Amorim Date: Wed, 25 Feb 2026 00:28:08 +0000 Subject: [PATCH 09/21] fix: add _asString bounds guard and export opus_encoder_ctl in WASM - _asString now caps scanning at maxStringLength (256 bytes) and throws StateError if no null terminator is found, preventing unbounded loops. - Add _opus_encoder_ctl to EXPORTED_FUNCTIONS in Dockerfile so encoder CTL calls work on web. - Add unit tests for _asString bounds checking. - Update ffi-analysis.md to mark these issues as fixed. --- docs/ffi-analysis.md | 58 ++++++++++++------------- opus_dart/lib/src/opus_dart_misc.dart | 10 ++++- opus_dart/test/opus_dart_mock_test.dart | 35 +++++++++++++++ opus_flutter_web/Dockerfile | 1 + 4 files changed, 72 insertions(+), 32 deletions(-) diff --git a/docs/ffi-analysis.md b/docs/ffi-analysis.md index 0e328c0..9f3c4c1 100644 --- a/docs/ffi-analysis.md +++ b/docs/ffi-analysis.md @@ -294,23 +294,27 @@ not have consumed the first yield yet). ### 5.4 String Handling (`_asString`) +(**Fixed** — `_asString` now has a `maxStringLength` (256) guard.) + ```dart String _asString(Pointer pointer) { int i = 0; - while (pointer[i] != 0) { + while (i < maxStringLength && pointer[i] != 0) { i++; } + if (i == maxStringLength) { + throw StateError( + '_asString: no null terminator found within $maxStringLength bytes'); + } return utf8.decode(pointer.asTypedList(i)); } ``` -- Walks memory byte-by-byte until it finds a null terminator. -- No bounds checking — if the pointer is invalid or the string is not null-terminated, - this loops until it hits a zero byte or causes a segfault. +- Walks memory byte-by-byte until it finds a null terminator, up to `maxStringLength`. +- Throws `StateError` if no null terminator is found within the limit, preventing + unbounded loops with invalid pointers. - Only used with `opus_get_version_string()` and `opus_strerror()`, which return - pointers to static C strings in libopus. These are guaranteed to be null-terminated - by the C library, so the risk is low in practice. -- Does not use `package:ffi`'s `Utf8` utilities, which could also handle this. + pointers to static C strings in libopus. These are well within the 256-byte limit. ### 5.5 `nullptr` Usage @@ -462,7 +466,7 @@ argument is an integer. However: | 6 | **`StreamOpusDecoder` FEC double-yield overwrites** | `opus_dart_streaming.dart:321-327` | Medium | With `copyOutput = false` and FEC, the first yielded output is overwritten before the consumer reads it. | | 7 | ~~**Output buffer too small for float decode**~~ | `BufferedOpusDecoder` factory, `StreamOpusDecoder` constructor | ~~High~~ **Fixed** | `BufferedOpusDecoder` default now uses `4 * maxSamplesPerPacket`. `StreamOpusDecoder` multiplier corrected to `(floats ? 4 : 2)`. | | 8 | **`free(nullptr)` on web** | `SimpleOpusDecoder.decode` finally block | Low | After null-input decode, `free(nullptr)` is called. Safe on native; behavior on `wasm_ffi` depends on allocator implementation. | -| 9 | **`_asString` unbounded loop** | `opus_dart_misc.dart:26-31` | Low | If pointer is invalid, loops until segfault. Only used with trusted libopus static strings. | +| 9 | ~~**`_asString` unbounded loop**~~ | `opus_dart_misc.dart` | ~~Low~~ **Fixed** | Now bounded by `maxStringLength` (256); throws `StateError` if no null terminator found. | | 10 | **`opus_encoder_ctl` variadic binding** | `opus_encoder.dart` | Low | Hardcoded to 3 int args. Getter CTLs (pointer arg) cannot be used correctly. | | 11 | **No `opus_decoder_ctl` binding** | `opus_decoder.dart` | Low | Decoder CTL operations are not exposed. | | 12 | **`late` global `opus` without guard** | `opus_dart_misc.dart:55` | Low | Access before `initOpus()` gives unhelpful `LateInitializationError`. | @@ -643,6 +647,7 @@ _malloc, _free, _opus_get_version_string, _opus_strerror, _opus_encoder_get_size, _opus_encoder_create, _opus_encoder_init, _opus_encode, _opus_encode_float, _opus_encoder_destroy, +_opus_encoder_ctl, _opus_decoder_get_size, _opus_decoder_create, _opus_decoder_init, _opus_decode, _opus_decode_float, _opus_decoder_destroy, _opus_packet_parse, _opus_packet_get_bandwidth, @@ -663,7 +668,7 @@ _opus_decoder_get_nb_samples, _opus_pcm_soft_clip | `opus_encode` | Yes | Eager (constructor) | | `opus_encode_float` | Yes | Eager (constructor) | | `opus_encoder_destroy` | Yes | Eager (constructor) | -| **`opus_encoder_ctl`** | **No** | **Lazy (`late final`)** | +| `opus_encoder_ctl` | Yes | Lazy (`late final`) | | `opus_decoder_get_size` | Yes | Eager (constructor) | | `opus_decoder_create` | Yes | Eager (constructor) | | `opus_decoder_init` | Yes | Eager (constructor) | @@ -679,22 +684,13 @@ _opus_decoder_get_nb_samples, _opus_pcm_soft_clip | `opus_decoder_get_nb_samples` | Yes | Eager (constructor) | | `opus_pcm_soft_clip` | Yes | Eager (constructor) | -**Bug: `opus_encoder_ctl` is not exported from the WASM binary.** The binding uses a -`late final` field, so the lookup is deferred until first use. This means: - -- Library loading succeeds. -- Creating encoders/decoders works fine. -- Encoding/decoding works fine. -- The **first call** to `BufferedOpusEncoder.encoderCtl()` on web will throw when - `_opus_encoder_ctlPtr` tries to look up `opus_encoder_ctl` in the WASM module. - -This is a **latent bug** — it only surfaces when a consumer actually calls `encoderCtl`, -which may not happen in typical usage but will crash any advanced use case that tries -to set bitrate, complexity, or other encoder parameters on web. +(**Fixed** — `_opus_encoder_ctl` has been added to `EXPORTED_FUNCTIONS` in the +Dockerfile. The symbol is now exported from the WASM binary and will be found when +`_opus_encoder_ctlPtr` performs its lazy lookup on first use.) -**Fix required in two places:** -1. Add `"_opus_encoder_ctl"` to `EXPORTED_FUNCTIONS` in the Dockerfile. -2. Consider whether the variadic binding even works correctly under WASM (see 12.7). +Note: the variadic ABI concern (see 12.7) is a separate issue. Exporting the symbol +ensures the lookup succeeds; whether the variadic calling convention works correctly +under WASM depends on Emscripten's ABI handling. ### 12.7 Variadic Functions Under WASM @@ -801,10 +797,10 @@ Checking the Dockerfile against `wasm_ffi` requirements: | `EXPORTED_RUNTIME_METHODS=["HEAPU8"]` | Present | **Required** by `wasm_ffi` for memory access | | `_malloc` in `EXPORTED_FUNCTIONS` | Present | Required for allocator | | `_free` in `EXPORTED_FUNCTIONS` | Present | Required for allocator | -| All used C functions exported | **Partial** | `_opus_encoder_ctl` missing | +| All used C functions exported | Yes | (**Fixed** — `_opus_encoder_ctl` was missing, now exported.) | -**Verdict:** Build configuration is correct except for the missing `_opus_encoder_ctl` -export. +**Verdict:** Compliant. Build configuration is correct and all used C functions are +exported. --- @@ -815,14 +811,14 @@ documentation: | # | Risk | Severity | Detail | |---|------|----------|--------| -| W1 | **`opus_encoder_ctl` not exported from WASM** | High | Calling `encoderCtl()` on web will throw. The symbol is missing from the Dockerfile's `EXPORTED_FUNCTIONS`. | +| W1 | **`opus_encoder_ctl` not exported from WASM** | Fixed | `_opus_encoder_ctl` added to `EXPORTED_FUNCTIONS` in Dockerfile. | | W2 | **Variadic `opus_encoder_ctl` ABI mismatch** | High | Even if exported, Emscripten's variadic function ABI may not match the Dart binding's fixed 3-arg signature. The WASM function likely expects a pointer to a variadic arg buffer, not direct arguments. | | W3 | **`asTypedList` views detach on memory growth** | Medium | `ALLOW_MEMORY_GROWTH=1` means `malloc` can trigger buffer replacement. Held `asTypedList` views (especially in `Buffered*` classes and `StreamOpusEncoder.bind`) become invalid. | | W4 | **`StreamOpusEncoder` caches stale buffer view** | Medium | `bind()` stores `_encoder.inputBuffer` in a local variable at stream start. If WASM memory grows during the stream, this view is detached. | | W5 | **`OpusCustomMode` not registered** | Fixed | `registerOpaqueType()` was missing and `OpusRepacketizer` was registered twice. Fixed: duplicate removed, `OpusCustomMode` registered. | | W6 | **No function signature validation** | Low | `wasm_ffi` does not validate that Dart typedefs match WASM function signatures. A typedef error would cause silent data corruption on web while working on native. | | W7 | **`free(nullptr)` behavior unverified** | Low | After packet-loss decode, `free(nullptr)` is called. The WASM `_free` (Emscripten's `free`) should handle `NULL` safely per C standard, but this is not explicitly guaranteed by `wasm_ffi`. | -| W8 | **`Pointer[i]` indexing in `_asString`** | Low | `_asString` uses `pointer[i]` to walk memory byte-by-byte. This should work identically on `wasm_ffi` (linear memory indexing), but the lack of bounds checking means an invalid pointer walks arbitrary WASM memory rather than segfaulting. | +| W8 | **`Pointer[i]` indexing in `_asString`** | Fixed | `_asString` now bounds the loop to `maxStringLength` (256) and throws `StateError` if no null terminator is found, preventing unbounded WASM memory walks. | --- @@ -832,7 +828,7 @@ Merging the original findings (Section 10) with web-specific findings (Section 1 | # | Risk | Platform | Severity | Location | |---|------|----------|----------|----------| -| 1 | `opus_encoder_ctl` not exported from WASM | Web | **High** | `Dockerfile`, `opus_encoder.dart` | +| 1 | ~~`opus_encoder_ctl` not exported from WASM~~ | Web | ~~**High**~~ **Fixed** | `_opus_encoder_ctl` added to `EXPORTED_FUNCTIONS` in Dockerfile | | 2 | Variadic `opus_encoder_ctl` ABI mismatch under WASM | Web | **High** | `opus_encoder.dart:212-217` | | 3 | ~~Use-after-destroy (no `_destroyed` check in encode/decode)~~ | All | ~~**High**~~ **Fixed** | `SimpleOpus*`, `BufferedOpus*` — all public methods now throw `OpusDestroyedError` before touching native pointers | | 4 | ~~Output buffer sizing bug (samples vs bytes)~~ | All | ~~**High**~~ **Fixed** | `BufferedOpusDecoder` default now uses `4 * maxSamplesPerPacket`. `StreamOpusDecoder` multiplier corrected to `(floats ? 4 : 2)`. | @@ -846,7 +842,7 @@ Merging the original findings (Section 10) with web-specific findings (Section 1 | 12 | Memory leak if second allocation throws | All | **Low** | `SimpleOpus*.encode/decode` | | 13 | `free(nullptr)` behavior on web | Web | **Low** | `SimpleOpusDecoder.decode` finally | | 14 | No function signature validation on web | Web | **Low** | All `lookupFunction` calls | -| 15 | `_asString` unbounded loop | All | **Low** | `opus_dart_misc.dart:26-31` | +| 15 | ~~`_asString` unbounded loop~~ | All | ~~**Low**~~ **Fixed** | Now bounded by `maxStringLength`; throws `StateError` on missing terminator | | 16 | `opus_encoder_ctl` variadic binding (native) | Native | **Low** | `opus_encoder.dart` | | 17 | No `opus_decoder_ctl` binding | All | **Low** | `opus_decoder.dart` | | 18 | `late` global `opus` without guard | All | **Low** | `opus_dart_misc.dart:55` | diff --git a/opus_dart/lib/src/opus_dart_misc.dart b/opus_dart/lib/src/opus_dart_misc.dart index 5b8ae2f..4369efc 100644 --- a/opus_dart/lib/src/opus_dart_misc.dart +++ b/opus_dart/lib/src/opus_dart_misc.dart @@ -23,11 +23,19 @@ String getOpusVersion() { return _asString(opus.libinfo.opus_get_version_string()); } +/// Upper bound for null-terminated string scans to prevent unbounded loops +/// when a pointer is invalid or lacks a terminator. +const int maxStringLength = 256; + String _asString(Pointer pointer) { int i = 0; - while (pointer[i] != 0) { + while (i < maxStringLength && pointer[i] != 0) { i++; } + if (i == maxStringLength) { + throw StateError( + '_asString: no null terminator found within $maxStringLength bytes'); + } return utf8.decode(pointer.asTypedList(i)); } diff --git a/opus_dart/test/opus_dart_mock_test.dart b/opus_dart/test/opus_dart_mock_test.dart index ad39dce..7694061 100644 --- a/opus_dart/test/opus_dart_mock_test.dart +++ b/opus_dart/test/opus_dart_mock_test.dart @@ -1096,6 +1096,41 @@ void main() { malloc.free(versionPtr); }); + + test('returns empty string when pointer starts with null terminator', () { + final ptr = _allocNullTerminated(''); + when(mockLibInfo.opus_get_version_string()).thenReturn(ptr); + + expect(getOpusVersion(), ''); + + malloc.free(ptr); + }); + + test('throws StateError when string exceeds maxStringLength', () { + final ptr = malloc.call(maxStringLength + 1); + for (int i = 0; i < maxStringLength + 1; i++) { + ptr[i] = 0x41; // 'A', no null terminator + } + when(mockLibInfo.opus_get_version_string()).thenReturn(ptr); + + expect(() => getOpusVersion(), throwsStateError); + + malloc.free(ptr); + }); + + test('succeeds when string is exactly maxStringLength - 1 chars', () { + final len = maxStringLength - 1; + final ptr = malloc.call(len + 1); + for (int i = 0; i < len; i++) { + ptr[i] = 0x42; // 'B' + } + ptr[len] = 0; + when(mockLibInfo.opus_get_version_string()).thenReturn(ptr); + + expect(getOpusVersion(), 'B' * len); + + malloc.free(ptr); + }); }); group('OpusException.toString', () { diff --git a/opus_flutter_web/Dockerfile b/opus_flutter_web/Dockerfile index 4d4af7b..9eecaf3 100644 --- a/opus_flutter_web/Dockerfile +++ b/opus_flutter_web/Dockerfile @@ -24,6 +24,7 @@ RUN mkdir -p out && emcc -O3 \ "_opus_get_version_string", "_opus_strerror", \ "_opus_encoder_get_size", "_opus_encoder_create", "_opus_encoder_init", \ "_opus_encode", "_opus_encode_float", "_opus_encoder_destroy", \ + "_opus_encoder_ctl", \ "_opus_decoder_get_size", "_opus_decoder_create", "_opus_decoder_init", \ "_opus_decode", "_opus_decode_float", "_opus_decoder_destroy", \ "_opus_packet_parse", "_opus_packet_get_bandwidth", \ From 0370a73777309f53630f2197303de2b4ac7b1f23 Mon Sep 17 00:00:00 2001 From: Pedro Paulo de Amorim Date: Wed, 25 Feb 2026 00:34:35 +0000 Subject: [PATCH 10/21] fix: prevent memory leak when second allocation throws Wrap both native allocations in try/finally for SimpleOpusEncoder, SimpleOpusDecoder, and pcmSoftClip. The second pointer is nullable and only freed if allocated, ensuring the first is always cleaned up. Add _FailingAllocator test helper and 5 new tests verifying that previously allocated memory is freed when a subsequent allocation fails. --- docs/ffi-analysis.md | 29 +++--- opus_dart/lib/src/opus_dart_decoder.dart | 65 ++++++++------ opus_dart/lib/src/opus_dart_encoder.dart | 28 +++--- opus_dart/test/opus_dart_mock_test.dart | 109 +++++++++++++++++++++++ 4 files changed, 171 insertions(+), 60 deletions(-) diff --git a/docs/ffi-analysis.md b/docs/ffi-analysis.md index 9f3c4c1..0b32263 100644 --- a/docs/ffi-analysis.md +++ b/docs/ffi-analysis.md @@ -223,26 +223,21 @@ try { Every method call allocates and frees. This is safe but has higher overhead. -**Concern in SimpleOpusEncoder.encode/encodeFloat:** The `opus_encode` call happens -_before_ the `try` block. If `opus_encode` itself throws (not an opus error code, but -an actual Dart exception from FFI — e.g. segfault translated to an exception), then the -`finally` block still runs and frees the buffers. However, if the `allocator.call` -for `outputNative` throws after `inputNative` was already allocated, `inputNative` leaks. -The same pattern exists in the decoder. - -Specifically in `SimpleOpusEncoder.encode`: +(**Fixed** — all `Simple*` encode/decode methods and `pcmSoftClip` now wrap both +allocations in a single `try/finally`. The second pointer is nullable and only freed +if it was successfully allocated, ensuring the first allocation is always cleaned up.) ```dart -Pointer inputNative = opus.allocator.call(input.length); // (1) -inputNative.asTypedList(input.length).setAll(0, input); -Pointer outputNative = opus.allocator.call(maxOutputSizeBytes); // (2) -// If (2) throws, (1) is leaked — no finally covers (1) at this point -int outputLength = opus.encoder.opus_encode(...); +Pointer inputNative = opus.allocator.call(input.length); +Pointer? outputNative; try { + inputNative.asTypedList(input.length).setAll(0, input); + outputNative = opus.allocator.call(maxOutputSizeBytes); + int outputLength = opus.encoder.opus_encode(...); // ... } finally { - opus.allocator.free(inputNative); // only reached if opus_encode didn't throw - opus.allocator.free(outputNative); + if (outputNative != null) opus.allocator.free(outputNative); + opus.allocator.free(inputNative); } ``` @@ -459,7 +454,7 @@ argument is an integer. However: | # | Risk | Location | Severity | Detail | |---|------|----------|----------|--------| | 1 | **Duplicate `registerOpaqueType` / missing `OpusCustomMode`** | `init_web.dart:28` | Fixed | Duplicate `OpusRepacketizer` removed; `OpusCustomMode` now registered. | -| 2 | **Memory leak if second allocation throws** | `SimpleOpusEncoder.encode`, `SimpleOpusDecoder.decode`, and float variants | Low | If the second `allocator.call` throws, the first allocation is not freed. | +| 2 | ~~**Memory leak if second allocation throws**~~ | `SimpleOpusEncoder.encode`, `SimpleOpusDecoder.decode`, float variants, `pcmSoftClip` | ~~Low~~ **Fixed** | All methods now wrap both allocations in `try/finally`; second pointer is nullable and only freed if allocated. | | 3 | **No `NativeFinalizer`** | All encoder/decoder classes | Medium | If `destroy()` is never called, native memory leaks permanently. No GC-driven cleanup. | | 4 | ~~**Use-after-destroy (dangling pointer)**~~ | `SimpleOpusEncoder`, `SimpleOpusDecoder`, `BufferedOpusEncoder`, `BufferedOpusDecoder` | ~~High~~ **Fixed** | All public methods now check `_destroyed` and throw `OpusDestroyedError` before touching native pointers. | | 5 | **`copyOutput = false` use-after-write** | `StreamOpusEncoder`, `StreamOpusDecoder` | Medium | Yielded views point to native buffers that get overwritten on next call. | @@ -839,7 +834,7 @@ Merging the original findings (Section 10) with web-specific findings (Section 1 | 9 | No `NativeFinalizer` — leaked memory if `destroy()` skipped | All | **Medium** | All encoder/decoder classes | | 10 | `copyOutput = false` use-after-write | All | **Medium** | `StreamOpusEncoder`, `StreamOpusDecoder` | | 11 | FEC double-yield overwrites with `copyOutput = false` | All | **Medium** | `opus_dart_streaming.dart:321-327` | -| 12 | Memory leak if second allocation throws | All | **Low** | `SimpleOpus*.encode/decode` | +| 12 | ~~Memory leak if second allocation throws~~ | All | ~~**Low**~~ **Fixed** | All `Simple*` methods and `pcmSoftClip` now wrap allocations in `try/finally` | | 13 | `free(nullptr)` behavior on web | Web | **Low** | `SimpleOpusDecoder.decode` finally | | 14 | No function signature validation on web | Web | **Low** | All `lookupFunction` calls | | 15 | ~~`_asString` unbounded loop~~ | All | ~~**Low**~~ **Fixed** | Now bounded by `maxStringLength`; throws `StateError` on missing terminator | diff --git a/opus_dart/lib/src/opus_dart_decoder.dart b/opus_dart/lib/src/opus_dart_decoder.dart index d1aa3c7..4378c47 100644 --- a/opus_dart/lib/src/opus_dart_decoder.dart +++ b/opus_dart/lib/src/opus_dart_decoder.dart @@ -18,15 +18,16 @@ int _packetDuration(int samples, int channels, int sampleRate) => /// method instead, since it avoids unnecessary memory copying. Float32List pcmSoftClip({required Float32List input, required int channels}) { Pointer nativePcm = opus.allocator.call(input.length); - nativePcm.asTypedList(input.length).setAll(0, input); - Pointer nativeBuffer = opus.allocator.call(channels); + Pointer? nativeBuffer; try { + nativePcm.asTypedList(input.length).setAll(0, input); + nativeBuffer = opus.allocator.call(channels); opus.decoder.opus_pcm_soft_clip( nativePcm, input.length ~/ channels, channels, nativeBuffer); return Float32List.fromList(nativePcm.asTypedList(input.length)); } finally { + if (nativeBuffer != null) opus.allocator.free(nativeBuffer); opus.allocator.free(nativePcm); - opus.allocator.free(nativeBuffer); } } @@ -101,19 +102,22 @@ class SimpleOpusDecoder extends OpusDecoder { if (_destroyed) throw OpusDestroyedError.decoder(); Pointer outputNative = opus.allocator.call(_maxSamplesPerPacket); - Pointer inputNative; - if (input != null) { - inputNative = opus.allocator.call(input.length); - inputNative.asTypedList(input.length).setAll(0, input); - } else { - inputNative = nullptr; - } - int frameSize = (input == null || fec) - ? _estimateLoss(loss, lastPacketDurationMs) - : _maxSamplesPerPacket; - int outputSamplesPerChannel = opus.decoder.opus_decode(_opusDecoder, - inputNative, input?.length ?? 0, outputNative, frameSize, fec ? 1 : 0); + Pointer? inputNative; try { + if (input != null) { + inputNative = opus.allocator.call(input.length); + inputNative.asTypedList(input.length).setAll(0, input); + } + int frameSize = (input == null || fec) + ? _estimateLoss(loss, lastPacketDurationMs) + : _maxSamplesPerPacket; + int outputSamplesPerChannel = opus.decoder.opus_decode( + _opusDecoder, + inputNative ?? nullptr, + input?.length ?? 0, + outputNative, + frameSize, + fec ? 1 : 0); if (outputSamplesPerChannel < opus_defines.OPUS_OK) { throw OpusException(outputSamplesPerChannel); } @@ -122,7 +126,7 @@ class SimpleOpusDecoder extends OpusDecoder { return Int16List.fromList( outputNative.asTypedList(outputSamplesPerChannel * channels)); } finally { - opus.allocator.free(inputNative); + if (inputNative != null) opus.allocator.free(inputNative); opus.allocator.free(outputNative); } } @@ -145,19 +149,22 @@ class SimpleOpusDecoder extends OpusDecoder { if (_destroyed) throw OpusDestroyedError.decoder(); Pointer outputNative = opus.allocator.call(_maxSamplesPerPacket); - Pointer inputNative; - if (input != null) { - inputNative = opus.allocator.call(input.length); - inputNative.asTypedList(input.length).setAll(0, input); - } else { - inputNative = nullptr; - } - int frameSize = (input == null || fec) - ? _estimateLoss(loss, lastPacketDurationMs) - : _maxSamplesPerPacket; - int outputSamplesPerChannel = opus.decoder.opus_decode_float(_opusDecoder, - inputNative, input?.length ?? 0, outputNative, frameSize, fec ? 1 : 0); + Pointer? inputNative; try { + if (input != null) { + inputNative = opus.allocator.call(input.length); + inputNative.asTypedList(input.length).setAll(0, input); + } + int frameSize = (input == null || fec) + ? _estimateLoss(loss, lastPacketDurationMs) + : _maxSamplesPerPacket; + int outputSamplesPerChannel = opus.decoder.opus_decode_float( + _opusDecoder, + inputNative ?? nullptr, + input?.length ?? 0, + outputNative, + frameSize, + fec ? 1 : 0); if (outputSamplesPerChannel < opus_defines.OPUS_OK) { throw OpusException(outputSamplesPerChannel); } @@ -170,7 +177,7 @@ class SimpleOpusDecoder extends OpusDecoder { return Float32List.fromList( outputNative.asTypedList(outputSamplesPerChannel * channels)); } finally { - opus.allocator.free(inputNative); + if (inputNative != null) opus.allocator.free(inputNative); opus.allocator.free(outputNative); } } diff --git a/opus_dart/lib/src/opus_dart_encoder.dart b/opus_dart/lib/src/opus_dart_encoder.dart index 12defdb..aa31ac2 100644 --- a/opus_dart/lib/src/opus_dart_encoder.dart +++ b/opus_dart/lib/src/opus_dart_encoder.dart @@ -64,20 +64,20 @@ class SimpleOpusEncoder extends OpusEncoder { {required Int16List input, int maxOutputSizeBytes = maxDataBytes}) { if (_destroyed) throw OpusDestroyedError.encoder(); Pointer inputNative = opus.allocator.call(input.length); - inputNative.asTypedList(input.length).setAll(0, input); - Pointer outputNative = - opus.allocator.call(maxOutputSizeBytes); - int sampleCountPerChannel = input.length ~/ channels; - int outputLength = opus.encoder.opus_encode(_opusEncoder, inputNative, - sampleCountPerChannel, outputNative, maxOutputSizeBytes); + Pointer? outputNative; try { + inputNative.asTypedList(input.length).setAll(0, input); + outputNative = opus.allocator.call(maxOutputSizeBytes); + int sampleCountPerChannel = input.length ~/ channels; + int outputLength = opus.encoder.opus_encode(_opusEncoder, inputNative, + sampleCountPerChannel, outputNative, maxOutputSizeBytes); if (outputLength < opus_defines.OPUS_OK) { throw OpusException(outputLength); } return Uint8List.fromList(outputNative.asTypedList(outputLength)); } finally { + if (outputNative != null) opus.allocator.free(outputNative); opus.allocator.free(inputNative); - opus.allocator.free(outputNative); } } @@ -88,20 +88,20 @@ class SimpleOpusEncoder extends OpusEncoder { {required Float32List input, int maxOutputSizeBytes = maxDataBytes}) { if (_destroyed) throw OpusDestroyedError.encoder(); Pointer inputNative = opus.allocator.call(input.length); - inputNative.asTypedList(input.length).setAll(0, input); - Pointer outputNative = - opus.allocator.call(maxOutputSizeBytes); - int sampleCountPerChannel = input.length ~/ channels; - int outputLength = opus.encoder.opus_encode_float(_opusEncoder, inputNative, - sampleCountPerChannel, outputNative, maxOutputSizeBytes); + Pointer? outputNative; try { + inputNative.asTypedList(input.length).setAll(0, input); + outputNative = opus.allocator.call(maxOutputSizeBytes); + int sampleCountPerChannel = input.length ~/ channels; + int outputLength = opus.encoder.opus_encode_float(_opusEncoder, + inputNative, sampleCountPerChannel, outputNative, maxOutputSizeBytes); if (outputLength < opus_defines.OPUS_OK) { throw OpusException(outputLength); } return Uint8List.fromList(outputNative.asTypedList(outputLength)); } finally { + if (outputNative != null) opus.allocator.free(outputNative); opus.allocator.free(inputNative); - opus.allocator.free(outputNative); } } diff --git a/opus_dart/test/opus_dart_mock_test.dart b/opus_dart/test/opus_dart_mock_test.dart index 7694061..ab90f93 100644 --- a/opus_dart/test/opus_dart_mock_test.dart +++ b/opus_dart/test/opus_dart_mock_test.dart @@ -22,6 +22,29 @@ Pointer _allocNullTerminated(String s) { return ptr; } +class _FailingAllocator implements Allocator { + final int failOnCall; + int _callCount = 0; + final List freedAddresses = []; + + _FailingAllocator(this.failOnCall); + + @override + Pointer allocate(int byteCount, {int? alignment}) { + _callCount++; + if (_callCount == failOnCall) { + throw StateError('Simulated allocation failure'); + } + return malloc.allocate(byteCount, alignment: alignment); + } + + @override + void free(Pointer pointer) { + freedAddresses.add(pointer.address); + malloc.free(pointer); + } +} + @GenerateMocks([ opus_decoder.OpusDecoderFunctions, opus_encoder.OpusEncoderFunctions, @@ -267,6 +290,41 @@ void main() { throwsA(isA()), ); }); + + test('encode frees input buffer when output allocation throws', () { + final encoder = createEncoder(); + final failAlloc = _FailingAllocator(2); + opus = ApiObject.test( + libinfo: mockLibInfo, + encoder: mockEncoder, + decoder: mockDecoder, + allocator: failAlloc, + ); + + expect( + () => encoder.encode(input: Int16List.fromList([1, 2, 3, 4])), + throwsStateError, + ); + expect(failAlloc.freedAddresses, hasLength(1)); + }); + + test('encodeFloat frees input buffer when output allocation throws', () { + final encoder = createEncoder(); + final failAlloc = _FailingAllocator(2); + opus = ApiObject.test( + libinfo: mockLibInfo, + encoder: mockEncoder, + decoder: mockDecoder, + allocator: failAlloc, + ); + + expect( + () => encoder.encodeFloat( + input: Float32List.fromList([0.1, 0.2, 0.3, 0.4])), + throwsStateError, + ); + expect(failAlloc.freedAddresses, hasLength(1)); + }); }); // --------------------------------------------------------------------------- @@ -536,6 +594,40 @@ void main() { throwsA(isA()), ); }); + + test('decode frees output buffer when input allocation throws', () { + final decoder = createDecoder(); + final failAlloc = _FailingAllocator(2); + opus = ApiObject.test( + libinfo: mockLibInfo, + encoder: mockEncoder, + decoder: mockDecoder, + allocator: failAlloc, + ); + + expect( + () => decoder.decode(input: Uint8List.fromList([0x01])), + throwsStateError, + ); + expect(failAlloc.freedAddresses, hasLength(1)); + }); + + test('decodeFloat frees output buffer when input allocation throws', () { + final decoder = createDecoder(); + final failAlloc = _FailingAllocator(2); + opus = ApiObject.test( + libinfo: mockLibInfo, + encoder: mockEncoder, + decoder: mockDecoder, + allocator: failAlloc, + ); + + expect( + () => decoder.decodeFloat(input: Uint8List.fromList([0x01])), + throwsStateError, + ); + expect(failAlloc.freedAddresses, hasLength(1)); + }); }); // --------------------------------------------------------------------------- @@ -1168,6 +1260,23 @@ void main() { verify(mockDecoder.opus_pcm_soft_clip(any, 3, 1, any)).called(1); }); + + test('frees pcm buffer when scratch allocation throws', () { + final failAlloc = _FailingAllocator(2); + opus = ApiObject.test( + libinfo: mockLibInfo, + encoder: mockEncoder, + decoder: mockDecoder, + allocator: failAlloc, + ); + + expect( + () => pcmSoftClip( + input: Float32List.fromList([0.5, -0.5]), channels: 2), + throwsStateError, + ); + expect(failAlloc.freedAddresses, hasLength(1)); + }); }); // --------------------------------------------------------------------------- From d021e76b84d1957f9ef0dff99123ecb8ba9b5d34 Mon Sep 17 00:00:00 2001 From: Pedro Paulo de Amorim Date: Wed, 25 Feb 2026 00:39:41 +0000 Subject: [PATCH 11/21] fix: add Finalizer for GC-driven native resource cleanup Attach a Finalizer to SimpleOpusEncoder, SimpleOpusDecoder, BufferedOpusEncoder, and BufferedOpusDecoder so native memory is released if destroy() is never called. The cleanup closure captures only raw pointers (not this) and destroy() detaches the finalizer to prevent double-free. --- docs/ffi-analysis.md | 14 +++++++----- opus_dart/lib/src/opus_dart_decoder.dart | 28 ++++++++++++++++++++++-- opus_dart/lib/src/opus_dart_encoder.dart | 24 ++++++++++++++++++-- 3 files changed, 56 insertions(+), 10 deletions(-) diff --git a/docs/ffi-analysis.md b/docs/ffi-analysis.md index 0b32263..1470704 100644 --- a/docs/ffi-analysis.md +++ b/docs/ffi-analysis.md @@ -259,9 +259,11 @@ destroy(): free input, output, (softClipBuffer) ``` -**Concern:** If `destroy()` is never called, all native memory leaks. There is no -Dart finalizer attached. `NativeFinalizer` (available since Dart 2.17) is not used -anywhere in the project. +(**Fixed** — all four classes now attach a `Finalizer` in their private constructor. +The finalizer callback captures only the raw native pointers (not `this`) and +performs the same cleanup as `destroy()`. Calling `destroy()` explicitly detaches +the finalizer to prevent double-cleanup. If `destroy()` is never called, the GC +will eventually trigger the finalizer and release native resources.) **Concern in BufferedOpusEncoder factory:** If `opus_encoder_create` itself throws (as opposed to returning an error code), the `input` and `output` buffers leak because @@ -453,9 +455,9 @@ argument is an integer. However: | # | Risk | Location | Severity | Detail | |---|------|----------|----------|--------| -| 1 | **Duplicate `registerOpaqueType` / missing `OpusCustomMode`** | `init_web.dart:28` | Fixed | Duplicate `OpusRepacketizer` removed; `OpusCustomMode` now registered. | +| 1 | **Duplicate `registerOpaqueType` / missing `OpusCustomMode`** | `init_web.dart:28` | ~~Medium~~ **Fixed** | Duplicate `OpusRepacketizer` removed; `OpusCustomMode` now registered. | | 2 | ~~**Memory leak if second allocation throws**~~ | `SimpleOpusEncoder.encode`, `SimpleOpusDecoder.decode`, float variants, `pcmSoftClip` | ~~Low~~ **Fixed** | All methods now wrap both allocations in `try/finally`; second pointer is nullable and only freed if allocated. | -| 3 | **No `NativeFinalizer`** | All encoder/decoder classes | Medium | If `destroy()` is never called, native memory leaks permanently. No GC-driven cleanup. | +| 3 | ~~**No `NativeFinalizer`**~~ | All encoder/decoder classes | ~~Medium~~ **Fixed** | All classes now use `Finalizer` for GC-driven cleanup. `destroy()` detaches the finalizer to prevent double-free. | | 4 | ~~**Use-after-destroy (dangling pointer)**~~ | `SimpleOpusEncoder`, `SimpleOpusDecoder`, `BufferedOpusEncoder`, `BufferedOpusDecoder` | ~~High~~ **Fixed** | All public methods now check `_destroyed` and throw `OpusDestroyedError` before touching native pointers. | | 5 | **`copyOutput = false` use-after-write** | `StreamOpusEncoder`, `StreamOpusDecoder` | Medium | Yielded views point to native buffers that get overwritten on next call. | | 6 | **`StreamOpusDecoder` FEC double-yield overwrites** | `opus_dart_streaming.dart:321-327` | Medium | With `copyOutput = false` and FEC, the first yielded output is overwritten before the consumer reads it. | @@ -831,7 +833,7 @@ Merging the original findings (Section 10) with web-specific findings (Section 1 | 6 | `StreamOpusEncoder.bind` caches stale buffer view | Web | **Medium** | `opus_dart_streaming.dart:129` | | 7 | ~~`OpusCustomMode` not registered on web~~ | Web | ~~**Medium**~~ **Fixed** | Duplicate `OpusRepacketizer` removed; `OpusCustomMode` now registered in `init_web.dart` | | 8 | ~~Duplicate `OpusRepacketizer` registration~~ | Web | ~~**Low**~~ **Fixed** | See #7 | -| 9 | No `NativeFinalizer` — leaked memory if `destroy()` skipped | All | **Medium** | All encoder/decoder classes | +| 9 | ~~No `NativeFinalizer` — leaked memory if `destroy()` skipped~~ | All | ~~**Medium**~~ **Fixed** | All classes now use `Finalizer` for GC-driven cleanup | | 10 | `copyOutput = false` use-after-write | All | **Medium** | `StreamOpusEncoder`, `StreamOpusDecoder` | | 11 | FEC double-yield overwrites with `copyOutput = false` | All | **Medium** | `opus_dart_streaming.dart:321-327` | | 12 | ~~Memory leak if second allocation throws~~ | All | ~~**Low**~~ **Fixed** | All `Simple*` methods and `pcmSoftClip` now wrap allocations in `try/finally` | diff --git a/opus_dart/lib/src/opus_dart_decoder.dart b/opus_dart/lib/src/opus_dart_decoder.dart index 4378c47..fb461f0 100644 --- a/opus_dart/lib/src/opus_dart_decoder.dart +++ b/opus_dart/lib/src/opus_dart_decoder.dart @@ -37,6 +37,8 @@ Float32List pcmSoftClip({required Float32List input, required int channels}) { /// All method calls in this calls allocate their own memory everytime they are called. /// See the [BufferedOpusDecoder] for an implementation with less allocation calls. class SimpleOpusDecoder extends OpusDecoder { + static final _finalizer = Finalizer((cleanup) => cleanup()); + final Pointer _opusDecoder; @override final int sampleRate; @@ -56,7 +58,14 @@ class SimpleOpusDecoder extends OpusDecoder { SimpleOpusDecoder._( this._opusDecoder, this.sampleRate, this.channels, this._softClipBuffer) : _destroyed = false, - _maxSamplesPerPacket = maxSamplesPerPacket(sampleRate, channels); + _maxSamplesPerPacket = maxSamplesPerPacket(sampleRate, channels) { + final decoder = _opusDecoder; + final softClip = _softClipBuffer; + _finalizer.attach(this, () { + opus.decoder.opus_decoder_destroy(decoder); + opus.allocator.free(softClip); + }, detach: this); + } /// Creates an new [SimpleOpusDecoder] based on the [sampleRate] and [channels]. /// See the matching fields for more information about these parameters. @@ -188,6 +197,7 @@ class SimpleOpusDecoder extends OpusDecoder { _destroyed = true; opus.decoder.opus_decoder_destroy(_opusDecoder); opus.allocator.free(_softClipBuffer); + _finalizer.detach(this); } } } @@ -219,6 +229,8 @@ class SimpleOpusDecoder extends OpusDecoder { /// } /// ``` class BufferedOpusDecoder extends OpusDecoder { + static final _finalizer = Finalizer((cleanup) => cleanup()); + final Pointer _opusDecoder; @override final int sampleRate; @@ -287,7 +299,18 @@ class BufferedOpusDecoder extends OpusDecoder { this._softClipBuffer) : _destroyed = false, inputBufferIndex = 0, - _outputBufferIndex = 0; + _outputBufferIndex = 0 { + final decoder = _opusDecoder; + final input = _inputBuffer; + final output = _outputBuffer; + final softClip = _softClipBuffer; + _finalizer.attach(this, () { + opus.decoder.opus_decoder_destroy(decoder); + opus.allocator.free(input); + opus.allocator.free(output); + opus.allocator.free(softClip); + }, detach: this); + } /// Creates an new [BufferedOpusDecoder] based on the [sampleRate] and [channels]. /// The native allocated buffer size is determined by [maxInputBufferSizeBytes] and [maxOutputBufferSizeBytes]. @@ -444,6 +467,7 @@ class BufferedOpusDecoder extends OpusDecoder { opus.allocator.free(_inputBuffer); opus.allocator.free(_outputBuffer); opus.allocator.free(_softClipBuffer); + _finalizer.detach(this); } } diff --git a/opus_dart/lib/src/opus_dart_encoder.dart b/opus_dart/lib/src/opus_dart_encoder.dart index aa31ac2..727cbeb 100644 --- a/opus_dart/lib/src/opus_dart_encoder.dart +++ b/opus_dart/lib/src/opus_dart_encoder.dart @@ -10,6 +10,8 @@ import 'opus_dart_misc.dart'; /// All method calls in this calls allocate their own memory everytime they are called. /// See the [BufferedOpusEncoder] for an implementation with less allocation calls. class SimpleOpusEncoder extends OpusEncoder { + static final _finalizer = Finalizer((cleanup) => cleanup()); + final Pointer _opusEncoder; @override final int sampleRate; @@ -23,7 +25,12 @@ class SimpleOpusEncoder extends OpusEncoder { SimpleOpusEncoder._( this._opusEncoder, this.sampleRate, this.channels, this.application) - : _destroyed = false; + : _destroyed = false { + final encoder = _opusEncoder; + _finalizer.attach(this, () { + opus.encoder.opus_encoder_destroy(encoder); + }, detach: this); + } /// Creates an new [SimpleOpusEncoder] based on the [sampleRate], [channels] and [application] type. /// See the matching fields for more information about these parameters. @@ -110,6 +117,7 @@ class SimpleOpusEncoder extends OpusEncoder { if (!_destroyed) { _destroyed = true; opus.encoder.opus_encoder_destroy(_opusEncoder); + _finalizer.detach(this); } } } @@ -144,6 +152,8 @@ class SimpleOpusEncoder extends OpusEncoder { /// } /// ``` class BufferedOpusEncoder extends OpusEncoder { + static final _finalizer = Finalizer((cleanup) => cleanup()); + final Pointer _opusEncoder; @override final int sampleRate; @@ -199,7 +209,16 @@ class BufferedOpusEncoder extends OpusEncoder { this.maxOutputBufferSizeBytes) : _destroyed = false, inputBufferIndex = 0, - _outputBufferIndex = 0; + _outputBufferIndex = 0 { + final encoder = _opusEncoder; + final input = _inputBuffer; + final output = _outputBuffer; + _finalizer.attach(this, () { + opus.encoder.opus_encoder_destroy(encoder); + opus.allocator.free(input); + opus.allocator.free(output); + }, detach: this); + } /// Creates an new [BufferedOpusEncoder] based on the [sampleRate], [channels] and [application] type. /// The native allocated buffer size is determined by [maxInputBufferSizeBytes] and [maxOutputBufferSizeBytes]. @@ -300,6 +319,7 @@ class BufferedOpusEncoder extends OpusEncoder { opus.encoder.opus_encoder_destroy(_opusEncoder); opus.allocator.free(_inputBuffer); opus.allocator.free(_outputBuffer); + _finalizer.detach(this); } } } From 4ca3a8ece8e63f9856779a2e628e2b0ac748fae9 Mon Sep 17 00:00:00 2001 From: Pedro Paulo de Amorim Date: Wed, 25 Feb 2026 00:46:20 +0000 Subject: [PATCH 12/21] fix: always copy streaming output to prevent use-after-write StreamOpusEncoder and StreamOpusDecoder now always copy output to the Dart heap, eliminating use-after-write hazards when the native buffer is overwritten on the next encode/decode call. Also fixes FEC double-yield data corruption in StreamOpusDecoder. The copyOutput parameter is retained for API compatibility. Mark free(nullptr) as fixed in docs (resolved by earlier nullable inputNative change). --- docs/ffi-analysis.md | 64 +++++++++---------- opus_dart/lib/src/opus_dart_streaming.dart | 22 ++++--- .../test/opus_dart_streaming_mock_test.dart | 18 +++--- 3 files changed, 51 insertions(+), 53 deletions(-) diff --git a/docs/ffi-analysis.md b/docs/ffi-analysis.md index 1470704..c9d2af7 100644 --- a/docs/ffi-analysis.md +++ b/docs/ffi-analysis.md @@ -75,7 +75,7 @@ ApiObject createApiObject(Object lib) { | Opaque type setup | Not needed | `registerOpaqueType()` required | | `DynamicLibrary` source| OS-level `.so`/`.dylib`/`.dll` | Emscripten JS+WASM module | | `nullptr` semantics | Backed by address `0` | `wasm_ffi` emulation | -| `free(nullptr)` | Safe (C standard) | Depends on `wasm_ffi` allocator | +| `free(nullptr)` | No longer called | No longer called (fixed) | --- @@ -271,23 +271,21 @@ the `throw` path inside the `try` block only runs when `error.value != OPUS_OK`. ### 5.3 Pointer Lifetime in Streaming (`opus_dart_streaming.dart`) -`StreamOpusEncoder` and `StreamOpusDecoder` wrap `BufferedOpusEncoder`/`BufferedOpusDecoder`. -They expose `copyOutput` as a parameter: - -- `copyOutput = true` (default): Output is copied to Dart heap via `Uint8List.fromList`. -- `copyOutput = false`: Output is a `Uint8List` view backed by native memory. +(**Fixed** — output is now always copied to the Dart heap regardless of `copyOutput`.) -**Risk with `copyOutput = false`:** The view points into the preallocated native output -buffer. On the next encode/decode call, this buffer is overwritten. If a consumer holds -a reference to a previously yielded `Uint8List`, it will silently contain new data. -This is a use-after-write hazard. - -The `StreamOpusDecoder` has an additional concern: when `forwardErrorCorrection` is -enabled and a packet is lost then recovered, the decoder calls `_decodeFec(true)` and -yields `_output()`, then immediately calls `_decodeFec(false)` and yields `_output()` -again. With `copyOutput = false`, the first yield's data is overwritten by the second -decode before the consumer processes it (in an `async*` generator, the consumer may -not have consumed the first yield yet). +`StreamOpusEncoder` and `StreamOpusDecoder` wrap `BufferedOpusEncoder`/`BufferedOpusDecoder`. +Previously they exposed `copyOutput` as a parameter that controlled whether the output +was copied or yielded as a native memory view. The `copyOutput` parameter is retained +for API compatibility but output is now always copied via `Uint8List.fromList`. + +This eliminates two hazards: +1. **Use-after-write:** With `copyOutput = false`, yielded views pointed into the + preallocated native buffer and would silently contain stale data after the next + encode/decode call. +2. **FEC double-yield corruption:** In `StreamOpusDecoder`, when FEC recovers a lost + packet, two yields happen in succession (`_decodeFec(true)` + `_decodeFec(false)`). + With `copyOutput = false`, the first yield's data was overwritten by the second + decode before the consumer could process it. ### 5.4 String Handling (`_asString`) @@ -315,17 +313,17 @@ String _asString(Pointer pointer) { ### 5.5 `nullptr` Usage -`nullptr` is used in two contexts: +`nullptr` is used in one context: -1. **Decoder packet loss:** When `input` is `null`, `inputNative` is set to `nullptr` - and passed to `opus_decode`/`opus_decode_float`. This is correct per the opus API +1. **Decoder packet loss:** When `input` is `null`, `nullptr` is passed to + `opus_decode`/`opus_decode_float`. This is correct per the opus API (null data pointer signals packet loss). -2. **Decoder free after packet loss:** After a null-input decode, the code - `opus.allocator.free(inputNative)` is called where `inputNative == nullptr`. In C, - `free(NULL)` is a no-op. The `dart:ffi` `malloc.free` from `package:ffi` also - handles this safely. Whether `wasm_ffi`'s allocator handles `free(nullptr)` safely - is implementation-dependent. +(**Fixed** — previously `free(inputNative)` was called where `inputNative == nullptr` +after a null-input decode. This was resolved as part of the "memory leak if second +allocation throws" fix: `inputNative` is now a nullable `Pointer?` and the +`finally` block uses `if (inputNative != null)` before calling `free`. The +`free(nullptr)` call no longer occurs.) --- @@ -455,14 +453,14 @@ argument is an integer. However: | # | Risk | Location | Severity | Detail | |---|------|----------|----------|--------| -| 1 | **Duplicate `registerOpaqueType` / missing `OpusCustomMode`** | `init_web.dart:28` | ~~Medium~~ **Fixed** | Duplicate `OpusRepacketizer` removed; `OpusCustomMode` now registered. | +| 1 | ~~**Duplicate `registerOpaqueType` / missing `OpusCustomMode`**~~ | `init_web.dart:28` | ~~Medium~~ **Fixed** | Duplicate `OpusRepacketizer` removed; `OpusCustomMode` now registered. | | 2 | ~~**Memory leak if second allocation throws**~~ | `SimpleOpusEncoder.encode`, `SimpleOpusDecoder.decode`, float variants, `pcmSoftClip` | ~~Low~~ **Fixed** | All methods now wrap both allocations in `try/finally`; second pointer is nullable and only freed if allocated. | | 3 | ~~**No `NativeFinalizer`**~~ | All encoder/decoder classes | ~~Medium~~ **Fixed** | All classes now use `Finalizer` for GC-driven cleanup. `destroy()` detaches the finalizer to prevent double-free. | | 4 | ~~**Use-after-destroy (dangling pointer)**~~ | `SimpleOpusEncoder`, `SimpleOpusDecoder`, `BufferedOpusEncoder`, `BufferedOpusDecoder` | ~~High~~ **Fixed** | All public methods now check `_destroyed` and throw `OpusDestroyedError` before touching native pointers. | -| 5 | **`copyOutput = false` use-after-write** | `StreamOpusEncoder`, `StreamOpusDecoder` | Medium | Yielded views point to native buffers that get overwritten on next call. | -| 6 | **`StreamOpusDecoder` FEC double-yield overwrites** | `opus_dart_streaming.dart:321-327` | Medium | With `copyOutput = false` and FEC, the first yielded output is overwritten before the consumer reads it. | +| 5 | ~~**`copyOutput = false` use-after-write**~~ | `StreamOpusEncoder`, `StreamOpusDecoder` | ~~Medium~~ **Fixed** | Output is now always copied to Dart heap regardless of `copyOutput`. | +| 6 | ~~**`StreamOpusDecoder` FEC double-yield overwrites**~~ | `opus_dart_streaming.dart:321-327` | ~~Medium~~ **Fixed** | See #5 — always-copy eliminates the FEC double-yield corruption. | | 7 | ~~**Output buffer too small for float decode**~~ | `BufferedOpusDecoder` factory, `StreamOpusDecoder` constructor | ~~High~~ **Fixed** | `BufferedOpusDecoder` default now uses `4 * maxSamplesPerPacket`. `StreamOpusDecoder` multiplier corrected to `(floats ? 4 : 2)`. | -| 8 | **`free(nullptr)` on web** | `SimpleOpusDecoder.decode` finally block | Low | After null-input decode, `free(nullptr)` is called. Safe on native; behavior on `wasm_ffi` depends on allocator implementation. | +| 8 | ~~**`free(nullptr)` on web**~~ | `SimpleOpusDecoder.decode` finally block | ~~Low~~ **Fixed** | Resolved by nullable `inputNative` — `free` is only called when the pointer is non-null. | | 9 | ~~**`_asString` unbounded loop**~~ | `opus_dart_misc.dart` | ~~Low~~ **Fixed** | Now bounded by `maxStringLength` (256); throws `StateError` if no null terminator found. | | 10 | **`opus_encoder_ctl` variadic binding** | `opus_encoder.dart` | Low | Hardcoded to 3 int args. Getter CTLs (pointer arg) cannot be used correctly. | | 11 | **No `opus_decoder_ctl` binding** | `opus_decoder.dart` | Low | Decoder CTL operations are not exposed. | @@ -814,7 +812,7 @@ documentation: | W4 | **`StreamOpusEncoder` caches stale buffer view** | Medium | `bind()` stores `_encoder.inputBuffer` in a local variable at stream start. If WASM memory grows during the stream, this view is detached. | | W5 | **`OpusCustomMode` not registered** | Fixed | `registerOpaqueType()` was missing and `OpusRepacketizer` was registered twice. Fixed: duplicate removed, `OpusCustomMode` registered. | | W6 | **No function signature validation** | Low | `wasm_ffi` does not validate that Dart typedefs match WASM function signatures. A typedef error would cause silent data corruption on web while working on native. | -| W7 | **`free(nullptr)` behavior unverified** | Low | After packet-loss decode, `free(nullptr)` is called. The WASM `_free` (Emscripten's `free`) should handle `NULL` safely per C standard, but this is not explicitly guaranteed by `wasm_ffi`. | +| W7 | **`free(nullptr)` behavior unverified** | Fixed | Resolved by nullable `inputNative` — `free` is only called when the pointer is non-null. `free(nullptr)` no longer occurs. | | W8 | **`Pointer[i]` indexing in `_asString`** | Fixed | `_asString` now bounds the loop to `maxStringLength` (256) and throws `StateError` if no null terminator is found, preventing unbounded WASM memory walks. | --- @@ -834,10 +832,10 @@ Merging the original findings (Section 10) with web-specific findings (Section 1 | 7 | ~~`OpusCustomMode` not registered on web~~ | Web | ~~**Medium**~~ **Fixed** | Duplicate `OpusRepacketizer` removed; `OpusCustomMode` now registered in `init_web.dart` | | 8 | ~~Duplicate `OpusRepacketizer` registration~~ | Web | ~~**Low**~~ **Fixed** | See #7 | | 9 | ~~No `NativeFinalizer` — leaked memory if `destroy()` skipped~~ | All | ~~**Medium**~~ **Fixed** | All classes now use `Finalizer` for GC-driven cleanup | -| 10 | `copyOutput = false` use-after-write | All | **Medium** | `StreamOpusEncoder`, `StreamOpusDecoder` | -| 11 | FEC double-yield overwrites with `copyOutput = false` | All | **Medium** | `opus_dart_streaming.dart:321-327` | +| 10 | ~~`copyOutput = false` use-after-write~~ | All | ~~**Medium**~~ **Fixed** | Output always copied to Dart heap | +| 11 | ~~FEC double-yield overwrites with `copyOutput = false`~~ | All | ~~**Medium**~~ **Fixed** | See #10 | | 12 | ~~Memory leak if second allocation throws~~ | All | ~~**Low**~~ **Fixed** | All `Simple*` methods and `pcmSoftClip` now wrap allocations in `try/finally` | -| 13 | `free(nullptr)` behavior on web | Web | **Low** | `SimpleOpusDecoder.decode` finally | +| 13 | ~~`free(nullptr)` behavior on web~~ | Web | ~~**Low**~~ **Fixed** | Resolved by nullable `inputNative` null check | | 14 | No function signature validation on web | Web | **Low** | All `lookupFunction` calls | | 15 | ~~`_asString` unbounded loop~~ | All | ~~**Low**~~ **Fixed** | Now bounded by `maxStringLength`; throws `StateError` on missing terminator | | 16 | `opus_encoder_ctl` variadic binding (native) | Native | **Low** | `opus_encoder.dart` | diff --git a/opus_dart/lib/src/opus_dart_streaming.dart b/opus_dart/lib/src/opus_dart_streaming.dart index 7cf4951..95f49db 100644 --- a/opus_dart/lib/src/opus_dart_streaming.dart +++ b/opus_dart/lib/src/opus_dart_streaming.dart @@ -55,7 +55,9 @@ class StreamOpusEncoder extends StreamTransformerBase, Uint8List> { /// Indicates if the input data is interpreted as floats (`true`) or as s16le (`false`). final bool floats; - /// If `true`, the encoded output is copied into dart memory befor passig it to any consumers. + /// Previously controlled whether output was copied into Dart memory. + /// Output is now always copied for safety (prevents use-after-write hazards + /// when the native buffer is overwritten on the next encode call). final bool copyOutput; /// The sample rate in Hz for this encoder. @@ -151,9 +153,9 @@ class StreamOpusEncoder extends StreamTransformerBase, Uint8List> { _encoder.inputBufferIndex += use; available = bytes.lengthInBytes - dataIndex; if (_encoder.inputBufferIndex == _encoder.maxInputBufferSizeBytes) { - Uint8List bytes = + Uint8List encoded = floats ? _encoder.encodeFloat() : _encoder.encode(); - yield copyOutput ? Uint8List.fromList(bytes) : bytes; + yield Uint8List.fromList(encoded); _encoder.inputBufferIndex = 0; } } @@ -170,8 +172,8 @@ class StreamOpusEncoder extends StreamTransformerBase, Uint8List> { Uint8List( _encoder.maxInputBufferSizeBytes - _encoder.inputBufferIndex)); _encoder.inputBufferIndex = _encoder.maxInputBufferSizeBytes; - Uint8List bytes = floats ? _encoder.encodeFloat() : _encoder.encode(); - yield copyOutput ? Uint8List.fromList(bytes) : bytes; + Uint8List encoded = floats ? _encoder.encodeFloat() : _encoder.encode(); + yield Uint8List.fromList(encoded); } } finally { destroy(); @@ -204,7 +206,10 @@ class StreamOpusDecoder extends StreamTransformerBase> { /// Indicates if the input data is decoded to floats (`true`) or to s16le (`false`). final bool floats; - /// If `true`, the decoded output is copied into dart memory befor passig it to any consumers. + /// Previously controlled whether output was copied into Dart memory. + /// Output is now always copied for safety (prevents use-after-write hazards + /// when the native buffer is overwritten on the next decode call, and prevents + /// data corruption in the FEC double-yield path). final bool copyOutput; /// The sample rate in Hz for this decoder. @@ -286,10 +291,7 @@ class StreamOpusDecoder extends StreamTransformerBase> { } List _output() { - Uint8List output = _decoder.outputBuffer; - if (copyOutput) { - output = Uint8List.fromList(output); - } + Uint8List output = Uint8List.fromList(_decoder.outputBuffer); if (_outputType == Float32List) { return output.buffer .asFloat32List(output.offsetInBytes, output.lengthInBytes ~/ 4); diff --git a/opus_dart/test/opus_dart_streaming_mock_test.dart b/opus_dart/test/opus_dart_streaming_mock_test.dart index fae143a..dbe840f 100644 --- a/opus_dart/test/opus_dart_streaming_mock_test.dart +++ b/opus_dart/test/opus_dart_streaming_mock_test.dart @@ -412,7 +412,7 @@ void main() { expect(packets[0], [0x11, 0x22]); }); - test('copyOutput=false yields buffer without copying', () async { + test('copyOutput=false still copies output for safety', () async { stubEncoderCreate(); stubEncode([0x33, 0x44]); @@ -428,11 +428,8 @@ void main() { .bind(Stream.value(Int16List(_samplesPerFrame))) .toList(); - // With copyOutput=false all yielded lists share the same native buffer, - // so we can only verify the count and type — content is overwritten by - // the trailing flush. expect(packets, hasLength(2)); - expect(packets[0], isA()); + expect(packets[0], [0x33, 0x44]); }); }); @@ -503,8 +500,7 @@ void main() { verify(mockEncoder.opus_encode_float(any, any, any, any, any)).called(1); }); - test('copyOutput=false with float encoder yields buffer directly', - () async { + test('copyOutput=false still copies output for safety', () async { stubEncoderCreate(); stubEncodeFloat([0xDD]); @@ -520,7 +516,7 @@ void main() { final packets = await encoder.bind(Stream.value(input)).toList(); expect(packets, hasLength(2)); - expect(packets[0], isA()); + expect(packets[0], [0xDD]); }); test('works with FrameTime.ms20', () async { @@ -812,7 +808,7 @@ void main() { verify(mockDecoder.opus_decode(any, any, any, any, any, any)).called(3); }); - test('copyOutput=false yields buffer directly', () async { + test('copyOutput=false still copies output for safety', () async { stubDecoderCreate(); stubDecode([42, 43]); @@ -827,6 +823,7 @@ void main() { expect(results, hasLength(1)); expect(results[0], isA()); + expect(results[0], [42, 43]); }); }); @@ -946,7 +943,7 @@ void main() { ); }); - test('copyOutput=false yields buffer directly', () async { + test('copyOutput=false still copies output for safety', () async { stubDecoderCreate(); stubDecodeFloat([0.5]); @@ -961,6 +958,7 @@ void main() { expect(results, hasLength(1)); expect(results[0], isA()); + expect(results[0], [0.5]); }); }); From e44eac1605838f68b7dc35092876a9a77189ecd0 Mon Sep 17 00:00:00 2001 From: Pedro Paulo de Amorim Date: Wed, 25 Feb 2026 00:53:56 +0000 Subject: [PATCH 13/21] fix: resolve variadic opus_encoder_ctl ABI mismatch under WASM Emscripten compiles variadic C functions with a different ABI where args are passed via a stack buffer, not as direct WASM parameters. Add a non-variadic C wrapper (opus_encoder_ctl_int) compiled into the WASM module, and update the Dart binding to prefer the wrapper via try/catch fallback to the variadic symbol on native platforms. --- docs/ffi-analysis.md | 63 +++++++++++++----------- opus_dart/lib/wrappers/opus_encoder.dart | 25 +++++++--- opus_dart/test/opus_dart_mock_test.dart | 45 +++++++++++++++++ opus_flutter_web/Dockerfile | 7 ++- opus_flutter_web/opus_ctl_wrapper.c | 18 +++++++ 5 files changed, 122 insertions(+), 36 deletions(-) create mode 100644 opus_flutter_web/opus_ctl_wrapper.c diff --git a/docs/ffi-analysis.md b/docs/ffi-analysis.md index c9d2af7..78abc55 100644 --- a/docs/ffi-analysis.md +++ b/docs/ffi-analysis.md @@ -137,16 +137,20 @@ Functions resolved via `lookupFunction`: | `opus_encode` | `int Function(Pointer, Pointer, int, Pointer, int)` | Returns encoded byte count or error | | `opus_encode_float` | `int Function(Pointer, Pointer, int, Pointer, int)` | | | `opus_encoder_destroy` | `void Function(Pointer)` | | -| `opus_encoder_ctl` | `int Function(Pointer, int, int)` | Variadic in C, bound with fixed 3 args | +| `opus_encoder_ctl` | `int Function(Pointer, int, int)` | ~~Variadic in C, bound with fixed 3 args~~ **Fixed** — WASM ABI mismatch resolved via non-variadic C wrapper (`opus_encoder_ctl_int`); Dart binding tries wrapper first, falls back to variadic lookup on native. | **`opus_encoder_ctl` binding concern:** The C function `opus_encoder_ctl` is variadic. The binding hardcodes it to accept exactly `(st, request, va)` — three arguments. This works for CTL requests that take a single `int` argument, but CTL requests that take a pointer argument (e.g. `OPUS_GET_*` requests that write to an out-pointer) cannot be used through this binding. The `int va` parameter would need to be a pointer -address cast to `int`, which is fragile and non-portable. On web/WASM, pointer addresses -in the WASM linear memory space are 32-bit offsets, so passing them as `int` might work -by accident, but this pattern is error-prone. +address cast to `int`, which is fragile and non-portable. + +~~On web/WASM, pointer addresses in the WASM linear memory space are 32-bit offsets, +so passing them as `int` might work by accident, but this pattern is error-prone.~~ +**Fixed:** A non-variadic C wrapper (`opus_encoder_ctl_int`) is now compiled into the +WASM module. The Dart binding (`_resolveEncoderCtl`) tries the wrapper first and falls +back to the variadic symbol on native platforms where the ABI is compatible. ### 4.3 Decoder Bindings (`opus_decoder.dart`) @@ -427,7 +431,7 @@ less space for float (which needs more). Fixed to `(floats ? 4 : 2)`. --- -## 9. `opus_encoder_ctl` Variadic Binding +## 9. `opus_encoder_ctl` Variadic Binding — **Fixed** The C function `opus_encoder_ctl(OpusEncoder *st, int request, ...)` is variadic. The Dart binding defines it with a fixed signature: @@ -437,14 +441,18 @@ int opus_encoder_ctl(Pointer st, int request, int va); ``` This works for setter-style CTLs like `OPUS_SET_BITRATE(value)` where the third -argument is an integer. However: +argument is an integer. + +**WASM ABI fix:** A non-variadic C wrapper (`opus_encoder_ctl_int`) is compiled into +the WASM module (`opus_ctl_wrapper.c`), exported alongside the original function, and +the Dart binding (`FunctionsAndGlobals._resolveEncoderCtl`) tries the wrapper symbol +first, falling back to the variadic symbol on native platforms. + +Remaining limitations: - **Getter-style CTLs** (e.g. `OPUS_GET_BITRATE`) expect a `Pointer` as the third argument. Passing a pointer address as `int` is technically possible but non-portable and bypasses Dart's type safety. -- **On WASM:** Pointer values are offsets into WASM linear memory (32-bit). Passing - them as Dart `int` (64-bit) and having the C side interpret them as `opus_int32*` - requires that the upper 32 bits are zero, which should hold but is fragile. - **No decoder_ctl:** There is no `opus_decoder_ctl` binding at all. --- @@ -462,7 +470,7 @@ argument is an integer. However: | 7 | ~~**Output buffer too small for float decode**~~ | `BufferedOpusDecoder` factory, `StreamOpusDecoder` constructor | ~~High~~ **Fixed** | `BufferedOpusDecoder` default now uses `4 * maxSamplesPerPacket`. `StreamOpusDecoder` multiplier corrected to `(floats ? 4 : 2)`. | | 8 | ~~**`free(nullptr)` on web**~~ | `SimpleOpusDecoder.decode` finally block | ~~Low~~ **Fixed** | Resolved by nullable `inputNative` — `free` is only called when the pointer is non-null. | | 9 | ~~**`_asString` unbounded loop**~~ | `opus_dart_misc.dart` | ~~Low~~ **Fixed** | Now bounded by `maxStringLength` (256); throws `StateError` if no null terminator found. | -| 10 | **`opus_encoder_ctl` variadic binding** | `opus_encoder.dart` | Low | Hardcoded to 3 int args. Getter CTLs (pointer arg) cannot be used correctly. | +| 10 | ~~**`opus_encoder_ctl` variadic binding**~~ | `opus_encoder.dart` | ~~Low~~ **Fixed** | WASM ABI mismatch resolved via non-variadic C wrapper (`opus_encoder_ctl_int`). Getter CTLs (pointer arg) remain unsupported. | | 11 | **No `opus_decoder_ctl` binding** | `opus_decoder.dart` | Low | Decoder CTL operations are not exposed. | | 12 | **`late` global `opus` without guard** | `opus_dart_misc.dart:55` | Low | Access before `initOpus()` gives unhelpful `LateInitializationError`. | | 13 | **Linux/Windows temp dir library lifetime** | `opus_flutter_linux`, `opus_flutter_windows` | Low | If temp dir is cleaned while app runs, native calls will segfault. | @@ -683,11 +691,11 @@ _opus_decoder_get_nb_samples, _opus_pcm_soft_clip Dockerfile. The symbol is now exported from the WASM binary and will be found when `_opus_encoder_ctlPtr` performs its lazy lookup on first use.) -Note: the variadic ABI concern (see 12.7) is a separate issue. Exporting the symbol -ensures the lookup succeeds; whether the variadic calling convention works correctly -under WASM depends on Emscripten's ABI handling. +Note: the variadic ABI concern (see 12.7) was a separate issue, now also fixed. +Exporting the symbol ensures the lookup succeeds; the non-variadic wrapper +(`opus_encoder_ctl_int`) ensures correct argument passing under WASM. -### 12.7 Variadic Functions Under WASM +### 12.7 Variadic Functions Under WASM — **Fixed** **wasm_ffi context:** Emscripten compiles variadic C functions to WASM using a specific ABI where variadic arguments are passed via a stack-allocated buffer. The compiled WASM @@ -698,21 +706,20 @@ function signature may not match what a simple `lookupFunction` binding expects. int opus_encoder_ctl(OpusEncoder *st, int request, ...); ``` -The Dart binding treats it as a fixed 3-argument function: -```dart -int Function(Pointer, int, int) -``` +~~The Dart binding treats it as a fixed 3-argument function:~~ +~~`int Function(Pointer, int, int)`~~ -**Problem on WASM:** When Emscripten compiles a variadic function, the resulting WASM -function may take a different number of parameters than the Dart side expects (often a -pointer to the variadic argument buffer). Since `wasm_ffi` performs no type checking on -lookups, this mismatch would not be caught — the Dart side would call the WASM function -with the wrong number/types of arguments, causing undefined behavior. +**Fix:** A non-variadic C wrapper (`opus_encoder_ctl_int`) is compiled into the WASM +module via `opus_ctl_wrapper.c`. This wrapper has a fixed `(OpusEncoder*, int, int)` +signature and internally forwards to the real variadic `opus_encoder_ctl`. The Dart +binding (`FunctionsAndGlobals._resolveEncoderCtl`) tries `opus_encoder_ctl_int` first +(succeeds on WASM) and falls back to `opus_encoder_ctl` (works on native due to ABI +compatibility). On native `dart:ffi`, variadic function support was added in Dart 3.0 with specific annotations. The current binding bypasses this by using `lookup` + `asFunction` directly, -which happens to work on most native platforms due to calling convention compatibility, -but is technically incorrect and non-portable. +which happens to work on most native platforms due to calling convention compatibility. +The fallback in `_resolveEncoderCtl` preserves this existing behavior. ### 12.8 Memory Growth and `asTypedList` Views @@ -807,7 +814,7 @@ documentation: | # | Risk | Severity | Detail | |---|------|----------|--------| | W1 | **`opus_encoder_ctl` not exported from WASM** | Fixed | `_opus_encoder_ctl` added to `EXPORTED_FUNCTIONS` in Dockerfile. | -| W2 | **Variadic `opus_encoder_ctl` ABI mismatch** | High | Even if exported, Emscripten's variadic function ABI may not match the Dart binding's fixed 3-arg signature. The WASM function likely expects a pointer to a variadic arg buffer, not direct arguments. | +| W2 | ~~**Variadic `opus_encoder_ctl` ABI mismatch**~~ | ~~High~~ **Fixed** | Non-variadic C wrapper (`opus_encoder_ctl_int`) compiled into WASM module; Dart binding tries wrapper first via `_resolveEncoderCtl`. | | W3 | **`asTypedList` views detach on memory growth** | Medium | `ALLOW_MEMORY_GROWTH=1` means `malloc` can trigger buffer replacement. Held `asTypedList` views (especially in `Buffered*` classes and `StreamOpusEncoder.bind`) become invalid. | | W4 | **`StreamOpusEncoder` caches stale buffer view** | Medium | `bind()` stores `_encoder.inputBuffer` in a local variable at stream start. If WASM memory grows during the stream, this view is detached. | | W5 | **`OpusCustomMode` not registered** | Fixed | `registerOpaqueType()` was missing and `OpusRepacketizer` was registered twice. Fixed: duplicate removed, `OpusCustomMode` registered. | @@ -824,7 +831,7 @@ Merging the original findings (Section 10) with web-specific findings (Section 1 | # | Risk | Platform | Severity | Location | |---|------|----------|----------|----------| | 1 | ~~`opus_encoder_ctl` not exported from WASM~~ | Web | ~~**High**~~ **Fixed** | `_opus_encoder_ctl` added to `EXPORTED_FUNCTIONS` in Dockerfile | -| 2 | Variadic `opus_encoder_ctl` ABI mismatch under WASM | Web | **High** | `opus_encoder.dart:212-217` | +| 2 | ~~Variadic `opus_encoder_ctl` ABI mismatch under WASM~~ | Web | ~~**High**~~ **Fixed** | Non-variadic wrapper `opus_encoder_ctl_int` + `_resolveEncoderCtl` fallback | | 3 | ~~Use-after-destroy (no `_destroyed` check in encode/decode)~~ | All | ~~**High**~~ **Fixed** | `SimpleOpus*`, `BufferedOpus*` — all public methods now throw `OpusDestroyedError` before touching native pointers | | 4 | ~~Output buffer sizing bug (samples vs bytes)~~ | All | ~~**High**~~ **Fixed** | `BufferedOpusDecoder` default now uses `4 * maxSamplesPerPacket`. `StreamOpusDecoder` multiplier corrected to `(floats ? 4 : 2)`. | | 5 | `asTypedList` views detach on WASM memory growth | Web | **Medium** | `BufferedOpus*` buffer getters | @@ -838,7 +845,7 @@ Merging the original findings (Section 10) with web-specific findings (Section 1 | 13 | ~~`free(nullptr)` behavior on web~~ | Web | ~~**Low**~~ **Fixed** | Resolved by nullable `inputNative` null check | | 14 | No function signature validation on web | Web | **Low** | All `lookupFunction` calls | | 15 | ~~`_asString` unbounded loop~~ | All | ~~**Low**~~ **Fixed** | Now bounded by `maxStringLength`; throws `StateError` on missing terminator | -| 16 | `opus_encoder_ctl` variadic binding (native) | Native | **Low** | `opus_encoder.dart` | +| 16 | ~~`opus_encoder_ctl` variadic binding (native)~~ | Native | ~~**Low**~~ **Fixed** | `_resolveEncoderCtl` falls back to direct variadic lookup (ABI-compatible on native) | | 17 | No `opus_decoder_ctl` binding | All | **Low** | `opus_decoder.dart` | | 18 | `late` global `opus` without guard | All | **Low** | `opus_dart_misc.dart:55` | | 19 | Linux/Windows temp dir library lifetime | Native | **Low** | Platform packages | diff --git a/opus_dart/lib/wrappers/opus_encoder.dart b/opus_dart/lib/wrappers/opus_encoder.dart index ceb46bc..bd2f88b 100644 --- a/opus_dart/lib/wrappers/opus_encoder.dart +++ b/opus_dart/lib/wrappers/opus_encoder.dart @@ -209,10 +209,23 @@ class FunctionsAndGlobals implements OpusEncoderFunctions { ); } - late final _opus_encoder_ctlPtr = _lookup< - ffi.NativeFunction< - ffi.Int Function( - ffi.Pointer, ffi.Int, ffi.Int)>>('opus_encoder_ctl'); - late final _opus_encoder_ctl = _opus_encoder_ctlPtr - .asFunction, int, int)>(); + late final _opus_encoder_ctl = _resolveEncoderCtl(); + + int Function(ffi.Pointer, int, int) _resolveEncoderCtl() { + try { + return _lookup< + ffi.NativeFunction< + ffi.Int Function( + ffi.Pointer, ffi.Int, ffi.Int)>>( + 'opus_encoder_ctl_int') + .asFunction, int, int)>(); + } catch (_) { + return _lookup< + ffi.NativeFunction< + ffi.Int Function( + ffi.Pointer, ffi.Int, ffi.Int)>>( + 'opus_encoder_ctl') + .asFunction, int, int)>(); + } + } } diff --git a/opus_dart/test/opus_dart_mock_test.dart b/opus_dart/test/opus_dart_mock_test.dart index ab90f93..c317a1d 100644 --- a/opus_dart/test/opus_dart_mock_test.dart +++ b/opus_dart/test/opus_dart_mock_test.dart @@ -731,6 +731,51 @@ void main() { encoder.destroy(); }); + test('encoderCtl forwards exact argument values for multiple CTL types', + () { + final encoder = createBufferedEncoder(); + final captured = >[]; + when(mockEncoder.opus_encoder_ctl(any, any, any)).thenAnswer((inv) { + captured.add([ + inv.positionalArguments[1] as int, + inv.positionalArguments[2] as int, + ]); + return OPUS_OK; + }); + + encoder.encoderCtl( + request: OPUS_SET_BITRATE_REQUEST, value: 64000); + encoder.encoderCtl( + request: OPUS_SET_COMPLEXITY_REQUEST, value: 10); + encoder.encoderCtl( + request: OPUS_SET_VBR_REQUEST, value: 1); + encoder.encoderCtl( + request: OPUS_SET_INBAND_FEC_REQUEST, value: 1); + encoder.encoderCtl( + request: OPUS_SET_PACKET_LOSS_PERC_REQUEST, value: 25); + + expect(captured, [ + [OPUS_SET_BITRATE_REQUEST, 64000], + [OPUS_SET_COMPLEXITY_REQUEST, 10], + [OPUS_SET_VBR_REQUEST, 1], + [OPUS_SET_INBAND_FEC_REQUEST, 1], + [OPUS_SET_PACKET_LOSS_PERC_REQUEST, 25], + ]); + encoder.destroy(); + }); + + test('encoderCtl returns native error codes faithfully', () { + final encoder = createBufferedEncoder(); + when(mockEncoder.opus_encoder_ctl(any, any, any)) + .thenReturn(OPUS_UNIMPLEMENTED); + + final result = encoder.encoderCtl( + request: OPUS_SET_BANDWIDTH_REQUEST, value: OPUS_BANDWIDTH_FULLBAND); + + expect(result, OPUS_UNIMPLEMENTED); + encoder.destroy(); + }); + test('destroy calls opus_encoder_destroy exactly once', () { when(mockEncoder.opus_encoder_destroy(any)).thenReturn(null); final encoder = createBufferedEncoder(); diff --git a/opus_flutter_web/Dockerfile b/opus_flutter_web/Dockerfile index 9eecaf3..f8a758f 100644 --- a/opus_flutter_web/Dockerfile +++ b/opus_flutter_web/Dockerfile @@ -14,7 +14,10 @@ RUN emcmake cmake -S opus-source -B opus-build \ -DBUILD_TESTING=OFF \ && cmake --build opus-build --parallel +COPY opus_ctl_wrapper.c /build/opus_ctl_wrapper.c + RUN mkdir -p out && emcc -O3 \ + -I opus-source/include \ -s MODULARIZE=1 \ -s EXPORT_NAME=libopus \ -s ALLOW_MEMORY_GROWTH=1 \ @@ -24,7 +27,7 @@ RUN mkdir -p out && emcc -O3 \ "_opus_get_version_string", "_opus_strerror", \ "_opus_encoder_get_size", "_opus_encoder_create", "_opus_encoder_init", \ "_opus_encode", "_opus_encode_float", "_opus_encoder_destroy", \ - "_opus_encoder_ctl", \ + "_opus_encoder_ctl", "_opus_encoder_ctl_int", \ "_opus_decoder_get_size", "_opus_decoder_create", "_opus_decoder_init", \ "_opus_decode", "_opus_decode_float", "_opus_decoder_destroy", \ "_opus_packet_parse", "_opus_packet_get_bandwidth", \ @@ -32,6 +35,6 @@ RUN mkdir -p out && emcc -O3 \ "_opus_packet_get_nb_frames", "_opus_packet_get_nb_samples", \ "_opus_decoder_get_nb_samples", "_opus_pcm_soft_clip" \ ]' \ - opus-build/libopus.a -o out/libopus.js + opus-build/libopus.a opus_ctl_wrapper.c -o out/libopus.js WORKDIR /build/out diff --git a/opus_flutter_web/opus_ctl_wrapper.c b/opus_flutter_web/opus_ctl_wrapper.c new file mode 100644 index 0000000..2ba7478 --- /dev/null +++ b/opus_flutter_web/opus_ctl_wrapper.c @@ -0,0 +1,18 @@ +/* + * Non-variadic wrappers for opus_encoder_ctl. + * + * Emscripten compiles variadic C functions with a different ABI: variadic + * arguments are packed into a stack-allocated buffer rather than passed as + * individual WASM parameters. Dart's wasm_ffi lookupFunction binds to the + * raw WASM export and has no knowledge of this indirection, so calling the + * variadic export directly from Dart produces undefined behavior. + * + * These thin wrappers present a fixed-signature function that Dart can + * safely call via lookupFunction on the web platform. + */ + +#include "opus/opus.h" + +int opus_encoder_ctl_int(OpusEncoder *st, int request, int value) { + return opus_encoder_ctl(st, request, value); +} From fe875add0a5e0ef95d997e2b6815514380f77ca7 Mon Sep 17 00:00:00 2001 From: Pedro Paulo de Amorim Date: Wed, 25 Feb 2026 01:08:31 +0000 Subject: [PATCH 14/21] fix: prevent asTypedList view detachment on WASM memory growth Output getters (outputBuffer, outputBufferAsInt16List, outputBufferAsFloat32List) now return Dart-heap copies instead of native-backed views. StreamOpusEncoder.bind no longer caches the inputBuffer view. This prevents detached TypedArray errors when ALLOW_MEMORY_GROWTH replaces the underlying ArrayBuffer. Refactor StreamOpusEncoder.bind into focused private methods (_mapStream, _processChunk, _encodeCurrentBuffer, _flushRemaining, _padInputBuffer) with documentation. --- docs/ffi-analysis.md | 72 ++++++----- opus_dart/lib/src/opus_dart_decoder.dart | 18 ++- opus_dart/lib/src/opus_dart_encoder.dart | 8 +- opus_dart/lib/src/opus_dart_streaming.dart | 144 ++++++++++++++------- 4 files changed, 148 insertions(+), 94 deletions(-) diff --git a/docs/ffi-analysis.md b/docs/ffi-analysis.md index 78abc55..fdebfab 100644 --- a/docs/ffi-analysis.md +++ b/docs/ffi-analysis.md @@ -650,7 +650,7 @@ _malloc, _free, _opus_get_version_string, _opus_strerror, _opus_encoder_get_size, _opus_encoder_create, _opus_encoder_init, _opus_encode, _opus_encode_float, _opus_encoder_destroy, -_opus_encoder_ctl, +_opus_encoder_ctl, _opus_encoder_ctl_int, _opus_decoder_get_size, _opus_decoder_create, _opus_decoder_init, _opus_decode, _opus_decode_float, _opus_decoder_destroy, _opus_packet_parse, _opus_packet_get_bandwidth, @@ -671,7 +671,7 @@ _opus_decoder_get_nb_samples, _opus_pcm_soft_clip | `opus_encode` | Yes | Eager (constructor) | | `opus_encode_float` | Yes | Eager (constructor) | | `opus_encoder_destroy` | Yes | Eager (constructor) | -| `opus_encoder_ctl` | Yes | Lazy (`late final`) | +| `opus_encoder_ctl` / `opus_encoder_ctl_int` | Yes | Lazy (`late final` via `_resolveEncoderCtl`) — tries `opus_encoder_ctl_int` first, falls back to `opus_encoder_ctl` | | `opus_decoder_get_size` | Yes | Eager (constructor) | | `opus_decoder_create` | Yes | Eager (constructor) | | `opus_decoder_init` | Yes | Eager (constructor) | @@ -721,42 +721,44 @@ annotations. The current binding bypasses this by using `lookup` + `asFunction` which happens to work on most native platforms due to calling convention compatibility. The fallback in `_resolveEncoderCtl` preserves this existing behavior. -### 12.8 Memory Growth and `asTypedList` Views +### 12.8 Memory Growth and `asTypedList` Views — **Fixed** **wasm_ffi context:** The Dockerfile uses `-s ALLOW_MEMORY_GROWTH=1`. When WASM memory grows (e.g. due to a `malloc` that exceeds the current memory size), the underlying `ArrayBuffer` is replaced. Existing `TypedArray` views into the old buffer become **detached** (invalid). -**Risk in this project:** The `Buffered*` implementations return `asTypedList` views: +~~**Risk in this project:** The `Buffered*` implementations return `asTypedList` views:~~ + +**Fix:** Output getters now return copies; input getters create fresh views per call. ```dart +// Output getters return copies (safe across memory growth) +Uint8List get outputBuffer => + Uint8List.fromList(_outputBuffer.asTypedList(_outputBufferIndex)); + +// Input getters create fresh views per call (safe if not cached by consumer) Uint8List get inputBuffer => _inputBuffer.asTypedList(maxInputBufferSizeBytes); -Uint8List get outputBuffer => _outputBuffer.asTypedList(_outputBufferIndex); ``` -If a consumer holds a reference to `inputBuffer` or `outputBuffer`, and a subsequent -allocation (e.g. creating another encoder/decoder, or any `opus.allocator.call`) -triggers WASM memory growth, the held view becomes a detached `TypedArray`. Accessing -it will throw or return garbage. - -On native `dart:ffi`, `asTypedList` returns a view into process memory that remains -valid as long as the pointer is valid. This asymmetry means code that works on native -may silently break on web. - -**Affected code paths:** - -1. `BufferedOpusEncoder.inputBuffer` — returned to user for writing samples. -2. `BufferedOpusEncoder.outputBuffer` — returned to user after encoding. -3. `BufferedOpusDecoder.inputBuffer` — returned to user for writing packets. -4. `BufferedOpusDecoder.outputBuffer` — returned to user after decoding. -5. `BufferedOpusDecoder.outputBufferAsInt16List` / `outputBufferAsFloat32List` — cast - views of the output buffer. -6. `StreamOpusEncoder.bind` — caches `_encoder.inputBuffer` in a local variable at the - start of the stream, then reuses it across all iterations. If memory grows during - the stream, this cached view is stale. -7. Any `asTypedList` call in `SimpleOpus*` encode/decode — these are short-lived - (freed in the same `finally` block), so the risk is lower. +~~If a consumer holds a reference to `inputBuffer` or `outputBuffer`, and a subsequent +allocation triggers WASM memory growth, the held view becomes a detached `TypedArray`.~~ + +**Remaining caveat:** `inputBuffer` still returns a native-backed view (required for +write-through semantics). Consumers must not cache the returned view across operations +that could trigger WASM memory growth. Each call to the getter creates a fresh, valid +view. + +**Affected code paths (status):** + +1. ~~`BufferedOpusEncoder.inputBuffer`~~ — fresh view per call (safe if not cached). +2. ~~`BufferedOpusEncoder.outputBuffer`~~ — **Fixed**: returns copy. +3. ~~`BufferedOpusDecoder.inputBuffer`~~ — fresh view per call (safe if not cached). +4. ~~`BufferedOpusDecoder.outputBuffer`~~ — **Fixed**: returns copy. +5. ~~`BufferedOpusDecoder.outputBufferAsInt16List` / `outputBufferAsFloat32List`~~ — + **Fixed**: return copies. +6. ~~`StreamOpusEncoder.bind`~~ — **Fixed**: no longer caches `inputBuffer`. +7. `SimpleOpus*` encode/decode — short-lived views, freed in same `finally` block (low risk). ### 12.9 `Memory.init()` and Global Memory @@ -813,14 +815,14 @@ documentation: | # | Risk | Severity | Detail | |---|------|----------|--------| -| W1 | **`opus_encoder_ctl` not exported from WASM** | Fixed | `_opus_encoder_ctl` added to `EXPORTED_FUNCTIONS` in Dockerfile. | +| W1 | ~~**`opus_encoder_ctl` not exported from WASM**~~ | ~~High~~ **Fixed** | `_opus_encoder_ctl` added to `EXPORTED_FUNCTIONS` in Dockerfile. | | W2 | ~~**Variadic `opus_encoder_ctl` ABI mismatch**~~ | ~~High~~ **Fixed** | Non-variadic C wrapper (`opus_encoder_ctl_int`) compiled into WASM module; Dart binding tries wrapper first via `_resolveEncoderCtl`. | -| W3 | **`asTypedList` views detach on memory growth** | Medium | `ALLOW_MEMORY_GROWTH=1` means `malloc` can trigger buffer replacement. Held `asTypedList` views (especially in `Buffered*` classes and `StreamOpusEncoder.bind`) become invalid. | -| W4 | **`StreamOpusEncoder` caches stale buffer view** | Medium | `bind()` stores `_encoder.inputBuffer` in a local variable at stream start. If WASM memory grows during the stream, this view is detached. | -| W5 | **`OpusCustomMode` not registered** | Fixed | `registerOpaqueType()` was missing and `OpusRepacketizer` was registered twice. Fixed: duplicate removed, `OpusCustomMode` registered. | +| W3 | ~~**`asTypedList` views detach on memory growth**~~ | ~~Medium~~ **Fixed** | Output getters (`outputBuffer`, `outputBufferAsInt16List`, `outputBufferAsFloat32List`) now return copies. `inputBuffer` getters create fresh views per call; `StreamOpusEncoder.bind` no longer caches. | +| W4 | ~~**`StreamOpusEncoder` caches stale buffer view**~~ | ~~Medium~~ **Fixed** | `bind()` no longer caches `_encoder.inputBuffer`; it re-fetches the view on each use, so WASM memory growth cannot leave a stale view. | +| W5 | ~~**`OpusCustomMode` not registered**~~ | ~~Medium~~ **Fixed** | `registerOpaqueType()` was missing and `OpusRepacketizer` was registered twice. Fixed: duplicate removed, `OpusCustomMode` registered. | | W6 | **No function signature validation** | Low | `wasm_ffi` does not validate that Dart typedefs match WASM function signatures. A typedef error would cause silent data corruption on web while working on native. | -| W7 | **`free(nullptr)` behavior unverified** | Fixed | Resolved by nullable `inputNative` — `free` is only called when the pointer is non-null. `free(nullptr)` no longer occurs. | -| W8 | **`Pointer[i]` indexing in `_asString`** | Fixed | `_asString` now bounds the loop to `maxStringLength` (256) and throws `StateError` if no null terminator is found, preventing unbounded WASM memory walks. | +| W7 | ~~**`free(nullptr)` behavior unverified**~~ | ~~Low~~ **Fixed** | Resolved by nullable `inputNative` — `free` is only called when the pointer is non-null. `free(nullptr)` no longer occurs. | +| W8 | ~~**`Pointer[i]` indexing in `_asString`**~~ | ~~Low~~ **Fixed** | `_asString` now bounds the loop to `maxStringLength` (256) and throws `StateError` if no null terminator is found, preventing unbounded WASM memory walks. | --- @@ -834,8 +836,8 @@ Merging the original findings (Section 10) with web-specific findings (Section 1 | 2 | ~~Variadic `opus_encoder_ctl` ABI mismatch under WASM~~ | Web | ~~**High**~~ **Fixed** | Non-variadic wrapper `opus_encoder_ctl_int` + `_resolveEncoderCtl` fallback | | 3 | ~~Use-after-destroy (no `_destroyed` check in encode/decode)~~ | All | ~~**High**~~ **Fixed** | `SimpleOpus*`, `BufferedOpus*` — all public methods now throw `OpusDestroyedError` before touching native pointers | | 4 | ~~Output buffer sizing bug (samples vs bytes)~~ | All | ~~**High**~~ **Fixed** | `BufferedOpusDecoder` default now uses `4 * maxSamplesPerPacket`. `StreamOpusDecoder` multiplier corrected to `(floats ? 4 : 2)`. | -| 5 | `asTypedList` views detach on WASM memory growth | Web | **Medium** | `BufferedOpus*` buffer getters | -| 6 | `StreamOpusEncoder.bind` caches stale buffer view | Web | **Medium** | `opus_dart_streaming.dart:129` | +| 5 | ~~`asTypedList` views detach on WASM memory growth~~ | Web | ~~**Medium**~~ **Fixed** | Output getters return copies; input getters create fresh views per call | +| 6 | ~~`StreamOpusEncoder.bind` caches stale buffer view~~ | Web | ~~**Medium**~~ **Fixed** | `bind()` re-fetches `_encoder.inputBuffer` on each use | | 7 | ~~`OpusCustomMode` not registered on web~~ | Web | ~~**Medium**~~ **Fixed** | Duplicate `OpusRepacketizer` removed; `OpusCustomMode` now registered in `init_web.dart` | | 8 | ~~Duplicate `OpusRepacketizer` registration~~ | Web | ~~**Low**~~ **Fixed** | See #7 | | 9 | ~~No `NativeFinalizer` — leaked memory if `destroy()` skipped~~ | All | ~~**Medium**~~ **Fixed** | All classes now use `Finalizer` for GC-driven cleanup | diff --git a/opus_dart/lib/src/opus_dart_decoder.dart b/opus_dart/lib/src/opus_dart_decoder.dart index fb461f0..a259eb6 100644 --- a/opus_dart/lib/src/opus_dart_decoder.dart +++ b/opus_dart/lib/src/opus_dart_decoder.dart @@ -274,17 +274,21 @@ class BufferedOpusDecoder extends OpusDecoder { /// The data are pcm samples, either encoded as s16le or floats, depending on /// what method was used to decode the input packet. /// - /// This method does not copy data from native memory to dart memory but - /// rather gives a view backed by native memory. - Uint8List get outputBuffer => _outputBuffer.asTypedList(_outputBufferIndex); + /// Returns a copy of the native output buffer. This is safe across WASM + /// memory growth — the returned list remains valid even if subsequent + /// allocations replace the underlying ArrayBuffer. + Uint8List get outputBuffer => + Uint8List.fromList(_outputBuffer.asTypedList(_outputBufferIndex)); /// Convenience method to get the current output buffer as s16le. - Int16List get outputBufferAsInt16List => - _outputBuffer.cast().asTypedList(_outputBufferIndex ~/ 2); + /// Returns a copy safe across WASM memory growth. + Int16List get outputBufferAsInt16List => Int16List.fromList( + _outputBuffer.cast().asTypedList(_outputBufferIndex ~/ 2)); /// Convenience method to get the current output buffer as floats. - Float32List get outputBufferAsFloat32List => - _outputBuffer.cast().asTypedList(_outputBufferIndex ~/ 4); + /// Returns a copy safe across WASM memory growth. + Float32List get outputBufferAsFloat32List => Float32List.fromList( + _outputBuffer.cast().asTypedList(_outputBufferIndex ~/ 4)); final Pointer _softClipBuffer; diff --git a/opus_dart/lib/src/opus_dart_encoder.dart b/opus_dart/lib/src/opus_dart_encoder.dart index 727cbeb..91d9ac3 100644 --- a/opus_dart/lib/src/opus_dart_encoder.dart +++ b/opus_dart/lib/src/opus_dart_encoder.dart @@ -194,9 +194,11 @@ class BufferedOpusEncoder extends OpusEncoder { /// The portion of the allocated output buffer that is currently filled with data. /// The data represents an opus packet in bytes. /// - /// This method does not copy data from native memory to dart memory but - /// rather gives a view backed by native memory. - Uint8List get outputBuffer => _outputBuffer.asTypedList(_outputBufferIndex); + /// Returns a copy of the native output buffer. This is safe across WASM + /// memory growth — the returned list remains valid even if subsequent + /// allocations replace the underlying ArrayBuffer. + Uint8List get outputBuffer => + Uint8List.fromList(_outputBuffer.asTypedList(_outputBufferIndex)); BufferedOpusEncoder._( this._opusEncoder, diff --git a/opus_dart/lib/src/opus_dart_streaming.dart b/opus_dart/lib/src/opus_dart_streaming.dart index 95f49db..b909fea 100644 --- a/opus_dart/lib/src/opus_dart_streaming.dart +++ b/opus_dart/lib/src/opus_dart_streaming.dart @@ -120,66 +120,112 @@ class StreamOpusEncoder extends StreamTransformerBase, Uint8List> { maxInputBufferSizeBytes: (floats ? 4 : 2) * _calculateMaxSampleSize(sampleRate, channels, frameTime)); + /// Transforms an incoming PCM stream into encoded opus packets. + /// + /// The pipeline works in three stages: + /// 1. **Map** — converts typed input ([Int16List], [Float32List], or + /// [Uint8List]) into a uniform byte stream via [_mapStream]. + /// 2. **Buffer & encode** — each byte chunk is fed into the encoder's + /// fixed-size input buffer via [_processChunk]. Every time the buffer + /// fills to exactly one frame, the frame is encoded and yielded. + /// 3. **Flush** — when the source stream closes, [_flushRemaining] handles + /// the partial frame: either zero-pads and encodes it + /// ([fillUpLastFrame] = true) or throws [UnfinishedFrameException]. + /// + /// The encoder is always destroyed in the `finally` block, regardless of + /// whether the stream completes normally or with an error. @override Stream bind(Stream> stream) async* { try { - int dataIndex; - Uint8List bytes; - int available; - int max; - int use; - Uint8List inputBuffer = _encoder.inputBuffer; - Stream mapped; - if (_expect == Int16List) { - mapped = stream.cast().map((Int16List s16le) => - s16le.buffer.asUint8List(s16le.offsetInBytes, s16le.lengthInBytes)); - } else if (_expect == Float32List) { - mapped = stream.cast().map((Float32List floats) => floats - .buffer - .asUint8List(floats.offsetInBytes, floats.lengthInBytes)); - } else { - mapped = stream.cast(); - } - await for (Uint8List pcm in mapped) { - bytes = pcm; - dataIndex = 0; - available = bytes.lengthInBytes; - while (available > 0) { - max = _encoder.maxInputBufferSizeBytes - _encoder.inputBufferIndex; - use = max < available ? max : available; - inputBuffer.setRange(_encoder.inputBufferIndex, - _encoder.inputBufferIndex + use, bytes, dataIndex); - dataIndex += use; - _encoder.inputBufferIndex += use; - available = bytes.lengthInBytes - dataIndex; - if (_encoder.inputBufferIndex == _encoder.maxInputBufferSizeBytes) { - Uint8List encoded = - floats ? _encoder.encodeFloat() : _encoder.encode(); - yield Uint8List.fromList(encoded); - _encoder.inputBufferIndex = 0; - } + await for (Uint8List pcm in _mapStream(stream)) { + for (final packet in _processChunk(pcm)) { + yield packet; } } - if (_encoder.maxInputBufferSizeBytes != 0) { - if (!fillUpLastFrame) { - int missingSamples = - (_encoder.maxInputBufferSizeBytes - _encoder.inputBufferIndex) ~/ - (floats ? 4 : 2); - throw UnfinishedFrameException._(missingSamples: missingSamples); - } - _encoder.inputBuffer.setAll( - _encoder.inputBufferIndex, - Uint8List( - _encoder.maxInputBufferSizeBytes - _encoder.inputBufferIndex)); - _encoder.inputBufferIndex = _encoder.maxInputBufferSizeBytes; - Uint8List encoded = floats ? _encoder.encodeFloat() : _encoder.encode(); - yield Uint8List.fromList(encoded); + for (final packet in _flushRemaining()) { + yield packet; } } finally { destroy(); } } + /// Converts the typed input stream into a uniform [Stream]. + /// + /// [Int16List] and [Float32List] elements are reinterpreted as their raw + /// byte representation without copying. [Uint8List] elements pass through + /// unchanged. + Stream _mapStream(Stream> stream) { + if (_expect == Int16List) { + return stream.cast().map((s16le) => + s16le.buffer.asUint8List(s16le.offsetInBytes, s16le.lengthInBytes)); + } + if (_expect == Float32List) { + return stream.cast().map((f32) => + f32.buffer.asUint8List(f32.offsetInBytes, f32.lengthInBytes)); + } + return stream.cast(); + } + + /// Feeds [pcm] bytes into the encoder's input buffer, encoding and yielding + /// a packet each time the buffer fills to one complete frame. + /// + /// A single chunk may span multiple frames (e.g. a large audio buffer), so + /// this may yield zero or more encoded packets. Leftover bytes that don't + /// complete a frame remain in the encoder's input buffer for the next chunk. + Iterable _processChunk(Uint8List pcm) sync* { + int offset = 0; + int remaining = pcm.lengthInBytes; + while (remaining > 0) { + final space = + _encoder.maxInputBufferSizeBytes - _encoder.inputBufferIndex; + final count = space < remaining ? space : remaining; + _encoder.inputBuffer.setRange(_encoder.inputBufferIndex, + _encoder.inputBufferIndex + count, pcm, offset); + offset += count; + _encoder.inputBufferIndex += count; + remaining -= count; + if (_encoder.inputBufferIndex == _encoder.maxInputBufferSizeBytes) { + yield _encodeCurrentBuffer(); + _encoder.inputBufferIndex = 0; + } + } + } + + /// Encodes the encoder's current input buffer and returns a Dart-heap copy + /// of the resulting opus packet. + Uint8List _encodeCurrentBuffer() { + final encoded = floats ? _encoder.encodeFloat() : _encoder.encode(); + return Uint8List.fromList(encoded); + } + + /// Handles the end-of-stream partial frame. + /// + /// If [fillUpLastFrame] is true, the remaining buffer space is zero-padded + /// to produce one final silent-padded frame. Otherwise, an + /// [UnfinishedFrameException] is thrown reporting how many samples are + /// missing. + Iterable _flushRemaining() sync* { + if (_encoder.maxInputBufferSizeBytes == 0) return; + if (!fillUpLastFrame) { + final missingSamples = + (_encoder.maxInputBufferSizeBytes - _encoder.inputBufferIndex) ~/ + (floats ? 4 : 2); + throw UnfinishedFrameException._(missingSamples: missingSamples); + } + _padInputBuffer(); + yield _encodeCurrentBuffer(); + } + + /// Fills the remaining input buffer space with silence (zeros) and marks + /// the buffer as full. + void _padInputBuffer() { + final padding = + _encoder.maxInputBufferSizeBytes - _encoder.inputBufferIndex; + _encoder.inputBuffer.setAll(_encoder.inputBufferIndex, Uint8List(padding)); + _encoder.inputBufferIndex = _encoder.maxInputBufferSizeBytes; + } + /// Manually destroys this encoder. void destroy() => _encoder.destroy(); } From 75c0e972e742c0b96fbde4c55413bef28877910d Mon Sep 17 00:00:00 2001 From: Pedro Paulo de Amorim Date: Wed, 25 Feb 2026 01:22:26 +0000 Subject: [PATCH 15/21] refactor: extract duplicated encode logic and replace magic numbers Extract _createOpusEncoder helper shared by both factory constructors. Extract SimpleOpusEncoder._doEncode for common output-buffer lifecycle. Extract BufferedOpusEncoder._encodeBuffer with bool flag (no closure allocation). Improve BufferedOpusEncoder factory error handling so input buffer is freed if output allocation or encoder creation fails. Replace raw 2/4 literals with bytesPerInt16Sample and bytesPerFloatSample constants defined in opus_dart_misc.dart. --- opus_dart/lib/src/opus_dart_encoder.dart | 171 +++++++++++++---------- opus_dart/lib/src/opus_dart_misc.dart | 6 + 2 files changed, 101 insertions(+), 76 deletions(-) diff --git a/opus_dart/lib/src/opus_dart_encoder.dart b/opus_dart/lib/src/opus_dart_encoder.dart index 91d9ac3..085bbe4 100644 --- a/opus_dart/lib/src/opus_dart_encoder.dart +++ b/opus_dart/lib/src/opus_dart_encoder.dart @@ -4,6 +4,27 @@ import '../wrappers/opus_encoder.dart' as opus_encoder; import '../wrappers/opus_defines.dart' as opus_defines; import 'opus_dart_misc.dart'; +/// Allocates a temporary error pointer, calls `opus_encoder_create`, checks +/// the result, and frees the error pointer. Returns the encoder on success or +/// throws [OpusException] on failure. +Pointer _createOpusEncoder({ + required int sampleRate, + required int channels, + required Application application, +}) { + final error = opus.allocator.call(1); + try { + final encoder = opus.encoder.opus_encoder_create( + sampleRate, channels, _applicationCodes[application]!, error); + if (error.value != opus_defines.OPUS_OK) { + throw OpusException(error.value); + } + return encoder; + } finally { + opus.allocator.free(error); + } +} + /// An easy to use implementation of [OpusEncoder]. /// Don't forget to call [destroy] once you are done with it. /// @@ -38,17 +59,34 @@ class SimpleOpusEncoder extends OpusEncoder { {required int sampleRate, required int channels, required Application application}) { - Pointer error = opus.allocator.call(1); - Pointer encoder = opus.encoder - .opus_encoder_create( - sampleRate, channels, _applicationCodes[application]!, error); + final encoder = _createOpusEncoder( + sampleRate: sampleRate, channels: channels, application: application); + return SimpleOpusEncoder._(encoder, sampleRate, channels, application); + } + + /// Allocates the output buffer, computes the per-channel sample count, + /// invokes [nativeEncode], checks the result, and returns a Dart-heap copy + /// of the encoded opus packet. The output buffer is always freed. + /// + /// Callers are responsible for allocating and freeing the input buffer in + /// their own try/finally scope. + Uint8List _doEncode({ + required int inputSampleCount, + required int maxOutputSizeBytes, + required int Function(int sampleCountPerChannel, Pointer output) + nativeEncode, + }) { + final outputNative = opus.allocator.call(maxOutputSizeBytes); try { - if (error.value != opus_defines.OPUS_OK) { - throw OpusException(error.value); + final sampleCountPerChannel = inputSampleCount ~/ channels; + final outputLength = + nativeEncode(sampleCountPerChannel, outputNative); + if (outputLength < opus_defines.OPUS_OK) { + throw OpusException(outputLength); } - return SimpleOpusEncoder._(encoder, sampleRate, channels, application); + return Uint8List.fromList(outputNative.asTypedList(outputLength)); } finally { - opus.allocator.free(error); + opus.allocator.free(outputNative); } } @@ -70,20 +108,16 @@ class SimpleOpusEncoder extends OpusEncoder { Uint8List encode( {required Int16List input, int maxOutputSizeBytes = maxDataBytes}) { if (_destroyed) throw OpusDestroyedError.encoder(); - Pointer inputNative = opus.allocator.call(input.length); - Pointer? outputNative; + final inputNative = opus.allocator.call(input.length); try { inputNative.asTypedList(input.length).setAll(0, input); - outputNative = opus.allocator.call(maxOutputSizeBytes); - int sampleCountPerChannel = input.length ~/ channels; - int outputLength = opus.encoder.opus_encode(_opusEncoder, inputNative, - sampleCountPerChannel, outputNative, maxOutputSizeBytes); - if (outputLength < opus_defines.OPUS_OK) { - throw OpusException(outputLength); - } - return Uint8List.fromList(outputNative.asTypedList(outputLength)); + return _doEncode( + inputSampleCount: input.length, + maxOutputSizeBytes: maxOutputSizeBytes, + nativeEncode: (spc, output) => opus.encoder.opus_encode( + _opusEncoder, inputNative, spc, output, maxOutputSizeBytes), + ); } finally { - if (outputNative != null) opus.allocator.free(outputNative); opus.allocator.free(inputNative); } } @@ -94,20 +128,16 @@ class SimpleOpusEncoder extends OpusEncoder { Uint8List encodeFloat( {required Float32List input, int maxOutputSizeBytes = maxDataBytes}) { if (_destroyed) throw OpusDestroyedError.encoder(); - Pointer inputNative = opus.allocator.call(input.length); - Pointer? outputNative; + final inputNative = opus.allocator.call(input.length); try { inputNative.asTypedList(input.length).setAll(0, input); - outputNative = opus.allocator.call(maxOutputSizeBytes); - int sampleCountPerChannel = input.length ~/ channels; - int outputLength = opus.encoder.opus_encode_float(_opusEncoder, - inputNative, sampleCountPerChannel, outputNative, maxOutputSizeBytes); - if (outputLength < opus_defines.OPUS_OK) { - throw OpusException(outputLength); - } - return Uint8List.fromList(outputNative.asTypedList(outputLength)); + return _doEncode( + inputSampleCount: input.length, + maxOutputSizeBytes: maxOutputSizeBytes, + nativeEncode: (spc, output) => opus.encoder.opus_encode_float( + _opusEncoder, inputNative, spc, output, maxOutputSizeBytes), + ); } finally { - if (outputNative != null) opus.allocator.free(outputNative); opus.allocator.free(inputNative); } } @@ -225,10 +255,10 @@ class BufferedOpusEncoder extends OpusEncoder { /// Creates an new [BufferedOpusEncoder] based on the [sampleRate], [channels] and [application] type. /// The native allocated buffer size is determined by [maxInputBufferSizeBytes] and [maxOutputBufferSizeBytes]. /// - /// If [maxInputBufferSizeBytes] is omitted, it is callculated as 4 * [maxSamplesPerPacket]. + /// If [maxInputBufferSizeBytes] is omitted, it is calculated as [bytesPerFloatSample] * [maxSamplesPerPacket]. /// This ensures that the input buffer is big enough to hold the largest possible - /// frame (120ms at 48000Hz) in float (=4 byte) representation. - /// If you know that you only use input data in s16le representation you can manually set this to 2 * [maxSamplesPerPacket]. + /// frame (120ms at 48000Hz) in float representation. + /// If you know that you only use input data in s16le representation you can manually set this to [bytesPerInt16Sample] * [maxSamplesPerPacket]. /// /// [maxOutputBufferSizeBytes] defaults to [maxDataBytes] to guarantee that their is enough space in the /// output buffer for any possible valid input. @@ -240,25 +270,21 @@ class BufferedOpusEncoder extends OpusEncoder { required Application application, int? maxInputBufferSizeBytes, int? maxOutputBufferSizeBytes}) { - maxInputBufferSizeBytes ??= 4 * maxSamplesPerPacket(sampleRate, channels); + maxInputBufferSizeBytes ??= + bytesPerFloatSample * maxSamplesPerPacket(sampleRate, channels); maxOutputBufferSizeBytes ??= maxDataBytes; - Pointer error = opus.allocator.call(1); - Pointer input = opus.allocator.call(maxInputBufferSizeBytes); - Pointer output = - opus.allocator.call(maxOutputBufferSizeBytes); - Pointer encoder = opus.encoder - .opus_encoder_create( - sampleRate, channels, _applicationCodes[application]!, error); + final input = opus.allocator.call(maxInputBufferSizeBytes); + Pointer? output; try { - if (error.value != opus_defines.OPUS_OK) { - opus.allocator.free(input); - opus.allocator.free(output); - throw OpusException(error.value); - } + output = opus.allocator.call(maxOutputBufferSizeBytes); + final encoder = _createOpusEncoder( + sampleRate: sampleRate, channels: channels, application: application); return BufferedOpusEncoder._(encoder, sampleRate, channels, application, input, maxInputBufferSizeBytes, output, maxOutputBufferSizeBytes); - } finally { - opus.allocator.free(error); + } catch (_) { + if (output != null) opus.allocator.free(output); + opus.allocator.free(input); + rethrow; } } @@ -267,6 +293,25 @@ class BufferedOpusEncoder extends OpusEncoder { return opus.encoder.opus_encoder_ctl(_opusEncoder, request, value); } + /// Computes the per-channel sample count from [inputBufferIndex], invokes + /// the appropriate native encode function, checks the result, and returns + /// [outputBuffer]. + Uint8List _encodeBuffer({required bool useFloat}) { + if (_destroyed) throw OpusDestroyedError.encoder(); + final bytesPerSample = useFloat ? bytesPerFloatSample : bytesPerInt16Sample; + final sampleCountPerChannel = + inputBufferIndex ~/ (channels * bytesPerSample); + _outputBufferIndex = useFloat + ? opus.encoder.opus_encode_float(_opusEncoder, + _inputBuffer.cast(), sampleCountPerChannel, _outputBuffer, maxOutputBufferSizeBytes) + : opus.encoder.opus_encode(_opusEncoder, + _inputBuffer.cast(), sampleCountPerChannel, _outputBuffer, maxOutputBufferSizeBytes); + if (_outputBufferIndex < opus_defines.OPUS_OK) { + throw OpusException(_outputBufferIndex); + } + return outputBuffer; + } + /// Interpets [inputBufferIndex] bytes of the [inputBuffer] as s16le pcm data, and encodes them to the [outputBuffer]. /// This means, that this method encodes `[inputBufferIndex]/2` samples, since `inputBufferIndex` is in bytes, /// and s16le uses two bytes per sample. @@ -278,20 +323,7 @@ class BufferedOpusEncoder extends OpusEncoder { /// `sampleCount = 2 * 48000Hz * 0.02s = 1920`. /// /// The returned list is actually just the [outputBuffer]. - Uint8List encode() { - if (_destroyed) throw OpusDestroyedError.encoder(); - int sampleCountPerChannel = inputBufferIndex ~/ (channels * 2); - _outputBufferIndex = opus.encoder.opus_encode( - _opusEncoder, - _inputBuffer.cast(), - sampleCountPerChannel, - _outputBuffer, - maxOutputBufferSizeBytes); - if (_outputBufferIndex < opus_defines.OPUS_OK) { - throw OpusException(_outputBufferIndex); - } - return outputBuffer; - } + Uint8List encode() => _encodeBuffer(useFloat: false); /// Interpets [inputBufferIndex] bytes of the [inputBuffer] as float pcm data, and encodes them to the [outputBuffer]. /// This means, that this method encodes `[inputBufferIndex]/4` samples, since `inputBufferIndex` is in bytes, @@ -299,20 +331,7 @@ class BufferedOpusEncoder extends OpusEncoder { /// /// Except that the sample count is calculated by dividing the [inputBufferIndex] by 4 and not by 2, /// this method behaves just as [encode], so see there for more information. - Uint8List encodeFloat() { - if (_destroyed) throw OpusDestroyedError.encoder(); - int sampleCountPerChannel = inputBufferIndex ~/ (channels * 4); - _outputBufferIndex = opus.encoder.opus_encode_float( - _opusEncoder, - _inputBuffer.cast(), - sampleCountPerChannel, - _outputBuffer, - maxOutputBufferSizeBytes); - if (_outputBufferIndex < opus_defines.OPUS_OK) { - throw OpusException(_outputBufferIndex); - } - return outputBuffer; - } + Uint8List encodeFloat() => _encodeBuffer(useFloat: true); @override void destroy() { diff --git a/opus_dart/lib/src/opus_dart_misc.dart b/opus_dart/lib/src/opus_dart_misc.dart index 4369efc..28aea6c 100644 --- a/opus_dart/lib/src/opus_dart_misc.dart +++ b/opus_dart/lib/src/opus_dart_misc.dart @@ -6,6 +6,12 @@ import '../wrappers/opus_libinfo.dart' as opus_libinfo; import '../wrappers/opus_encoder.dart' as opus_encoder; import '../wrappers/opus_decoder.dart' as opus_decoder; +/// Byte width of a single s16le PCM sample (Int16). +const int bytesPerInt16Sample = 2; + +/// Byte width of a single float PCM sample (Float32). +const int bytesPerFloatSample = 4; + /// Max bitstream size of a single opus packet. /// /// See [here](https://stackoverflow.com/questions/55698317/what-value-to-use-for-libopus-encoder-max-data-bytes-field) From a2da146758fee70d82a9b094b3bfd19793f19705 Mon Sep 17 00:00:00 2001 From: Pedro Paulo de Amorim Date: Wed, 25 Feb 2026 01:39:23 +0000 Subject: [PATCH 16/21] refactor: extract duplicated decode logic and replace magic numbers --- opus_dart/README.md | 2 +- opus_dart/lib/src/opus_dart_decoder.dart | 249 ++++++++++++----------- opus_dart/lib/src/opus_dart_encoder.dart | 8 +- opus_dart/test/opus_dart_mock_test.dart | 6 +- opus_dart/test/opus_dart_test.dart | 6 +- 5 files changed, 139 insertions(+), 132 deletions(-) diff --git a/opus_dart/README.md b/opus_dart/README.md index 7d7ac95..5d089c9 100644 --- a/opus_dart/README.md +++ b/opus_dart/README.md @@ -96,7 +96,7 @@ SimpleOpusEncoder createCbrEncoder() { final encoder = SimpleOpusEncoder( sampleRate: 8000, channels: 1, - application: Application.restrictedLowdely, + application: Application.restrictedLowdelay, ); encoder.encoderCtl(request: OPUS_SET_VBR_REQUEST, value: 0); encoder.encoderCtl(request: OPUS_SET_BITRATE_REQUEST, value: 15200); diff --git a/opus_dart/lib/src/opus_dart_decoder.dart b/opus_dart/lib/src/opus_dart_decoder.dart index a259eb6..634020f 100644 --- a/opus_dart/lib/src/opus_dart_decoder.dart +++ b/opus_dart/lib/src/opus_dart_decoder.dart @@ -7,6 +7,26 @@ import 'opus_dart_misc.dart'; int _packetDuration(int samples, int channels, int sampleRate) => ((1000 * samples) ~/ (channels)) ~/ sampleRate; +/// Allocates a temporary error pointer, calls `opus_decoder_create`, checks +/// the result, and frees the error pointer. Returns the decoder on success or +/// throws [OpusException] on failure. +Pointer _createOpusDecoder({ + required int sampleRate, + required int channels, +}) { + final error = opus.allocator.call(1); + try { + final decoder = + opus.decoder.opus_decoder_create(sampleRate, channels, error); + if (error.value != opus_defines.OPUS_OK) { + throw OpusException(error.value); + } + return decoder; + } finally { + opus.allocator.free(error); + } +} + /// Soft clips the [input] to a range from -1 to 1 and returns /// the result. /// @@ -70,18 +90,49 @@ class SimpleOpusDecoder extends OpusDecoder { /// Creates an new [SimpleOpusDecoder] based on the [sampleRate] and [channels]. /// See the matching fields for more information about these parameters. factory SimpleOpusDecoder({required int sampleRate, required int channels}) { - Pointer error = opus.allocator.call(1); - Pointer softClipBuffer = opus.allocator.call(channels); - Pointer decoder = - opus.decoder.opus_decoder_create(sampleRate, channels, error); + final softClipBuffer = opus.allocator.call(channels); try { - if (error.value != opus_defines.OPUS_OK) { - opus.allocator.free(softClipBuffer); - throw OpusException(error.value); - } + final decoder = + _createOpusDecoder(sampleRate: sampleRate, channels: channels); return SimpleOpusDecoder._(decoder, sampleRate, channels, softClipBuffer); + } catch (_) { + opus.allocator.free(softClipBuffer); + rethrow; + } + } + + /// Allocates the input buffer if needed, computes frame size, invokes + /// [nativeDecode], checks the result, updates duration tracking, and frees + /// the input buffer. Returns the output sample count per channel. + /// + /// Callers are responsible for the destroyed check and for allocating/freeing + /// the output buffer in their own try/finally scope. + int _doDecode({ + required Uint8List? input, + required bool fec, + required int? loss, + required int Function(Pointer inputPtr, int inputLen, int frameSize) + nativeDecode, + }) { + Pointer? inputNative; + try { + if (input != null) { + inputNative = opus.allocator.call(input.length); + inputNative.asTypedList(input.length).setAll(0, input); + } + final frameSize = (input == null || fec) + ? _estimateLoss(loss, lastPacketDurationMs) + : _maxSamplesPerPacket; + final outputSamplesPerChannel = + nativeDecode(inputNative ?? nullptr, input?.length ?? 0, frameSize); + if (outputSamplesPerChannel < opus_defines.OPUS_OK) { + throw OpusException(outputSamplesPerChannel); + } + _lastPacketDurationMs = + _packetDuration(outputSamplesPerChannel, channels, sampleRate); + return outputSamplesPerChannel; } finally { - opus.allocator.free(error); + if (inputNative != null) opus.allocator.free(inputNative); } } @@ -109,33 +160,19 @@ class SimpleOpusDecoder extends OpusDecoder { @override Int16List decode({Uint8List? input, bool fec = false, int? loss}) { if (_destroyed) throw OpusDestroyedError.decoder(); - Pointer outputNative = - opus.allocator.call(_maxSamplesPerPacket); - Pointer? inputNative; + final outputNative = opus.allocator.call(_maxSamplesPerPacket); try { - if (input != null) { - inputNative = opus.allocator.call(input.length); - inputNative.asTypedList(input.length).setAll(0, input); - } - int frameSize = (input == null || fec) - ? _estimateLoss(loss, lastPacketDurationMs) - : _maxSamplesPerPacket; - int outputSamplesPerChannel = opus.decoder.opus_decode( - _opusDecoder, - inputNative ?? nullptr, - input?.length ?? 0, - outputNative, - frameSize, - fec ? 1 : 0); - if (outputSamplesPerChannel < opus_defines.OPUS_OK) { - throw OpusException(outputSamplesPerChannel); - } - _lastPacketDurationMs = - _packetDuration(outputSamplesPerChannel, channels, sampleRate); + final outputSamplesPerChannel = _doDecode( + input: input, + fec: fec, + loss: loss, + nativeDecode: (inputPtr, inputLen, frameSize) => + opus.decoder.opus_decode(_opusDecoder, inputPtr, inputLen, + outputNative, frameSize, fec ? 1 : 0), + ); return Int16List.fromList( outputNative.asTypedList(outputSamplesPerChannel * channels)); } finally { - if (inputNative != null) opus.allocator.free(inputNative); opus.allocator.free(outputNative); } } @@ -156,29 +193,16 @@ class SimpleOpusDecoder extends OpusDecoder { bool autoSoftClip = false, int? loss}) { if (_destroyed) throw OpusDestroyedError.decoder(); - Pointer outputNative = - opus.allocator.call(_maxSamplesPerPacket); - Pointer? inputNative; + final outputNative = opus.allocator.call(_maxSamplesPerPacket); try { - if (input != null) { - inputNative = opus.allocator.call(input.length); - inputNative.asTypedList(input.length).setAll(0, input); - } - int frameSize = (input == null || fec) - ? _estimateLoss(loss, lastPacketDurationMs) - : _maxSamplesPerPacket; - int outputSamplesPerChannel = opus.decoder.opus_decode_float( - _opusDecoder, - inputNative ?? nullptr, - input?.length ?? 0, - outputNative, - frameSize, - fec ? 1 : 0); - if (outputSamplesPerChannel < opus_defines.OPUS_OK) { - throw OpusException(outputSamplesPerChannel); - } - _lastPacketDurationMs = - _packetDuration(outputSamplesPerChannel, channels, sampleRate); + final outputSamplesPerChannel = _doDecode( + input: input, + fec: fec, + loss: loss, + nativeDecode: (inputPtr, inputLen, frameSize) => + opus.decoder.opus_decode_float(_opusDecoder, inputPtr, inputLen, + outputNative, frameSize, fec ? 1 : 0), + ); if (autoSoftClip) { opus.decoder.opus_pcm_soft_clip(outputNative, outputSamplesPerChannel ~/ channels, channels, _softClipBuffer); @@ -186,7 +210,6 @@ class SimpleOpusDecoder extends OpusDecoder { return Float32List.fromList( outputNative.asTypedList(outputSamplesPerChannel * channels)); } finally { - if (inputNative != null) opus.allocator.free(inputNative); opus.allocator.free(outputNative); } } @@ -283,12 +306,12 @@ class BufferedOpusDecoder extends OpusDecoder { /// Convenience method to get the current output buffer as s16le. /// Returns a copy safe across WASM memory growth. Int16List get outputBufferAsInt16List => Int16List.fromList( - _outputBuffer.cast().asTypedList(_outputBufferIndex ~/ 2)); + _outputBuffer.cast().asTypedList(_outputBufferIndex ~/ bytesPerInt16Sample)); /// Convenience method to get the current output buffer as floats. /// Returns a copy safe across WASM memory growth. Float32List get outputBufferAsFloat32List => Float32List.fromList( - _outputBuffer.cast().asTypedList(_outputBufferIndex ~/ 4)); + _outputBuffer.cast().asTypedList(_outputBufferIndex ~/ bytesPerFloatSample)); final Pointer _softClipBuffer; @@ -342,23 +365,17 @@ class BufferedOpusDecoder extends OpusDecoder { int? maxInputBufferSizeBytes, int? maxOutputBufferSizeBytes}) { maxInputBufferSizeBytes ??= maxDataBytes; - maxOutputBufferSizeBytes ??= 4 * maxSamplesPerPacket(sampleRate, channels); - Pointer error = opus.allocator.call(1); - Pointer input = opus.allocator.call(maxInputBufferSizeBytes); - Pointer output = - opus.allocator.call(maxOutputBufferSizeBytes); - Pointer softClipBuffer = opus.allocator.call(channels); - Pointer encoder = - opus.decoder.opus_decoder_create(sampleRate, channels, error); + maxOutputBufferSizeBytes ??= bytesPerFloatSample * maxSamplesPerPacket(sampleRate, channels); + final input = opus.allocator.call(maxInputBufferSizeBytes); + Pointer? output; + Pointer? softClipBuffer; try { - if (error.value != opus_defines.OPUS_OK) { - opus.allocator.free(input); - opus.allocator.free(output); - opus.allocator.free(softClipBuffer); - throw OpusException(error.value); - } + output = opus.allocator.call(maxOutputBufferSizeBytes); + softClipBuffer = opus.allocator.call(channels); + final decoder = + _createOpusDecoder(sampleRate: sampleRate, channels: channels); return BufferedOpusDecoder._( - encoder, + decoder, sampleRate, channels, input, @@ -366,11 +383,45 @@ class BufferedOpusDecoder extends OpusDecoder { output, maxOutputBufferSizeBytes, softClipBuffer); - } finally { - opus.allocator.free(error); + } catch (_) { + if (softClipBuffer != null) opus.allocator.free(softClipBuffer); + if (output != null) opus.allocator.free(output); + opus.allocator.free(input); + rethrow; } } + /// Computes the input pointer and frame size from [inputBufferIndex], + /// invokes the appropriate native decode function, checks the result, + /// and updates duration tracking and output buffer index. + void _decodeBuffer( + {required bool useFloat, required bool fec, required int? loss}) { + if (_destroyed) throw OpusDestroyedError.decoder(); + final bytesPerSample = + useFloat ? bytesPerFloatSample : bytesPerInt16Sample; + Pointer inputNative; + int frameSize; + if (inputBufferIndex > 0) { + inputNative = _inputBuffer; + frameSize = maxOutputBufferSizeBytes ~/ (bytesPerSample * channels); + } else { + inputNative = nullptr; + frameSize = _estimateLoss(loss, lastPacketDurationMs); + } + final outputSamplesPerChannel = useFloat + ? opus.decoder.opus_decode_float(_opusDecoder, inputNative, + inputBufferIndex, _outputBuffer.cast(), frameSize, + fec ? 1 : 0) + : opus.decoder.opus_decode(_opusDecoder, inputNative, inputBufferIndex, + _outputBuffer.cast(), frameSize, fec ? 1 : 0); + if (outputSamplesPerChannel < opus_defines.OPUS_OK) { + throw OpusException(outputSamplesPerChannel); + } + _lastPacketDurationMs = + _packetDuration(outputSamplesPerChannel, channels, sampleRate); + _outputBufferIndex = bytesPerSample * outputSamplesPerChannel * channels; + } + /// Interpretes [inputBufferIndex] bytes from the [inputBuffer] as a whole /// opus packet and decodes them to s16le samples, stored in the [outputBuffer]. /// Set [inputBufferIndex] to `0` to indicate packet loss. @@ -398,29 +449,7 @@ class BufferedOpusDecoder extends OpusDecoder { /// The returned list is actually just the [outputBufferAsInt16List]. @override Int16List decode({bool fec = false, int? loss}) { - if (_destroyed) throw OpusDestroyedError.decoder(); - Pointer inputNative; - int frameSize; - if (inputBufferIndex > 0) { - inputNative = _inputBuffer; - frameSize = maxOutputBufferSizeBytes ~/ (2 * channels); - } else { - inputNative = nullptr; - frameSize = _estimateLoss(loss, lastPacketDurationMs); - } - int outputSamplesPerChannel = opus.decoder.opus_decode( - _opusDecoder, - inputNative, - inputBufferIndex, - _outputBuffer.cast(), - frameSize, - fec ? 1 : 0); - if (outputSamplesPerChannel < opus_defines.OPUS_OK) { - throw OpusException(outputSamplesPerChannel); - } - _lastPacketDurationMs = - _packetDuration(outputSamplesPerChannel, channels, sampleRate); - _outputBufferIndex = 2 * outputSamplesPerChannel * channels; + _decodeBuffer(useFloat: false, fec: fec, loss: loss); return outputBufferAsInt16List; } @@ -434,29 +463,7 @@ class BufferedOpusDecoder extends OpusDecoder { @override Float32List decodeFloat( {bool autoSoftClip = false, bool fec = false, int? loss}) { - if (_destroyed) throw OpusDestroyedError.decoder(); - Pointer inputNative; - int frameSize; - if (inputBufferIndex > 0) { - inputNative = _inputBuffer; - frameSize = maxOutputBufferSizeBytes ~/ (4 * channels); - } else { - inputNative = nullptr; - frameSize = _estimateLoss(loss, lastPacketDurationMs); - } - int outputSamplesPerChannel = opus.decoder.opus_decode_float( - _opusDecoder, - inputNative, - inputBufferIndex, - _outputBuffer.cast(), - frameSize, - fec ? 1 : 0); - if (outputSamplesPerChannel < opus_defines.OPUS_OK) { - throw OpusException(outputSamplesPerChannel); - } - _lastPacketDurationMs = - _packetDuration(outputSamplesPerChannel, channels, sampleRate); - _outputBufferIndex = 4 * outputSamplesPerChannel * channels; + _decodeBuffer(useFloat: true, fec: fec, loss: loss); if (autoSoftClip) { return pcmSoftClipOutputBuffer(); } @@ -481,7 +488,7 @@ class BufferedOpusDecoder extends OpusDecoder { Float32List pcmSoftClipOutputBuffer() { if (_destroyed) throw OpusDestroyedError.decoder(); opus.decoder.opus_pcm_soft_clip(_outputBuffer.cast(), - _outputBufferIndex ~/ (4 * channels), channels, _softClipBuffer); + _outputBufferIndex ~/ (bytesPerFloatSample * channels), channels, _softClipBuffer); return outputBufferAsFloat32List; } } diff --git a/opus_dart/lib/src/opus_dart_encoder.dart b/opus_dart/lib/src/opus_dart_encoder.dart index 085bbe4..e7c7a61 100644 --- a/opus_dart/lib/src/opus_dart_encoder.dart +++ b/opus_dart/lib/src/opus_dart_encoder.dart @@ -367,13 +367,13 @@ abstract class OpusEncoder { void destroy(); } -/// Represents the different apllication types an [OpusEncoder] supports. -/// Setting the right apllication type when creating an encoder can improve quality. -enum Application { voip, audio, restrictedLowdely } +/// Represents the different application types an [OpusEncoder] supports. +/// Setting the right application type when creating an encoder can improve quality. +enum Application { voip, audio, restrictedLowdelay } const Map _applicationCodes = { Application.voip: opus_defines.OPUS_APPLICATION_VOIP, Application.audio: opus_defines.OPUS_APPLICATION_AUDIO, - Application.restrictedLowdely: + Application.restrictedLowdelay: opus_defines.OPUS_APPLICATION_RESTRICTED_LOWDELAY }; diff --git a/opus_dart/test/opus_dart_mock_test.dart b/opus_dart/test/opus_dart_mock_test.dart index c317a1d..28dafdf 100644 --- a/opus_dart/test/opus_dart_mock_test.dart +++ b/opus_dart/test/opus_dart_mock_test.dart @@ -155,9 +155,9 @@ void main() { encoder.destroy(); }); - test('maps Application.restrictedLowdely correctly', () { - final encoder = createEncoder(application: Application.restrictedLowdely); - expect(encoder.application, Application.restrictedLowdely); + test('maps Application.restrictedLowdelay correctly', () { + final encoder = createEncoder(application: Application.restrictedLowdelay); + expect(encoder.application, Application.restrictedLowdelay); verify(mockEncoder.opus_encoder_create( any, any, OPUS_APPLICATION_RESTRICTED_LOWDELAY, any)) .called(1); diff --git a/opus_dart/test/opus_dart_test.dart b/opus_dart/test/opus_dart_test.dart index 1c8e7cf..9838a40 100644 --- a/opus_dart/test/opus_dart_test.dart +++ b/opus_dart/test/opus_dart_test.dart @@ -148,10 +148,10 @@ void main() { expect(Application.values.length, 3); }); - test('contains voip, audio, restrictedLowdely', () { + test('contains voip, audio, restrictedLowdelay', () { expect(Application.values, contains(Application.voip)); expect(Application.values, contains(Application.audio)); - expect(Application.values, contains(Application.restrictedLowdely)); + expect(Application.values, contains(Application.restrictedLowdelay)); }); test('voip maps to OPUS_APPLICATION_VOIP (2048)', () { @@ -163,7 +163,7 @@ void main() { }); test( - 'restrictedLowdely maps to OPUS_APPLICATION_RESTRICTED_LOWDELAY (2051)', + 'restrictedLowdelay maps to OPUS_APPLICATION_RESTRICTED_LOWDELAY (2051)', () { expect(OPUS_APPLICATION_RESTRICTED_LOWDELAY, 2051); }); From b64d4061674c94adc9a228826b0b7714cf350c90 Mon Sep 17 00:00:00 2001 From: Pedro Paulo de Amorim Date: Wed, 25 Feb 2026 01:41:37 +0000 Subject: [PATCH 17/21] Fix typos in comments and documentation Correct misspellings across opus_dart_encoder.dart, opus_dart_decoder.dart and opus_dart_misc.dart (e.g. returnes, initalization, sampels, enocde, deocde, represntation, occurse, recieved, choosen, sucessfull, Wheter, maxDataByes, and grammar issues like 'this calls', 'to small', 'their is', 'an method'). --- opus_dart/lib/src/opus_dart_decoder.dart | 28 ++++++++++++------------ opus_dart/lib/src/opus_dart_encoder.dart | 20 ++++++++--------- opus_dart/lib/src/opus_dart_misc.dart | 4 ++-- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/opus_dart/lib/src/opus_dart_decoder.dart b/opus_dart/lib/src/opus_dart_decoder.dart index 634020f..32e87f9 100644 --- a/opus_dart/lib/src/opus_dart_decoder.dart +++ b/opus_dart/lib/src/opus_dart_decoder.dart @@ -54,7 +54,7 @@ Float32List pcmSoftClip({required Float32List input, required int channels}) { /// An easy to use implementation of [OpusDecoder]. /// Don't forget to call [destroy] once you are done with it. /// -/// All method calls in this calls allocate their own memory everytime they are called. +/// All method calls in this class allocate their own memory everytime they are called. /// See the [BufferedOpusDecoder] for an implementation with less allocation calls. class SimpleOpusDecoder extends OpusDecoder { static final _finalizer = Finalizer((cleanup) => cleanup()); @@ -148,11 +148,11 @@ class SimpleOpusDecoder extends OpusDecoder { /// If you want to use forward error correction, don't report packet loss /// by calling this method with `null` as input (unless it is a real packet /// loss), but instead, wait for the next packet and call this method with - /// the recieved packet, [fec] set to `true` and [loss] to the missing duration + /// the received packet, [fec] set to `true` and [loss] to the missing duration /// of the missing audio in ms (as above). Then, call this method a second time with /// the same packet, but with [fec] set to `false`. You can read more about the /// correct usage of forward error correction [here](https://stackoverflow.com/questions/49427579/how-to-use-fec-feature-for-opus-codec). - /// Note: A real packet loss occurse if you lose two or more packets in a row. + /// Note: A real packet loss occurs if you lose two or more packets in a row. /// You are only able to restore the last lost packet and the other packets are /// really lost. So for them, you have to report packet loss. /// @@ -230,9 +230,9 @@ class SimpleOpusDecoder extends OpusDecoder { /// /// The idea behind this implementation is to reduce the amount of memory allocation calls. /// Instead of allocating new buffers everytime something is decoded, the buffers are -/// allocated at initalization. Then, an opus packet is directly written into the [inputBuffer], +/// allocated at initialization. Then, an opus packet is directly written into the [inputBuffer], /// the [inputBufferIndex] is updated, based on how many bytes where written, and -/// one of the deocde methods is called. The decoded pcm samples can then be accessed using +/// one of the decode methods is called. The decoded pcm samples can then be accessed using /// the [outputBuffer] getter (or one of the [outputBufferAsInt16List] or [outputBufferAsFloat32List] convenience getters). /// ``` /// BufferedOpusDecoder decoder; @@ -267,7 +267,7 @@ class BufferedOpusDecoder extends OpusDecoder { int? _lastPacketDurationMs; /// The size of the allocated the input buffer in bytes. - /// Should be choosen big enough to hold a maximal opus packet + /// Should be chosen big enough to hold a maximal opus packet /// with size of [maxDataBytes] bytes. final int maxInputBufferSizeBytes; @@ -285,8 +285,8 @@ class BufferedOpusDecoder extends OpusDecoder { Uint8List get inputBuffer => _inputBuffer.asTypedList(maxInputBufferSizeBytes); - /// The size of the allocated the output buffer. If this value is choosen - /// to small, this decoder will not be capable of decoding some packets. + /// The size of the allocated the output buffer. If this value is chosen + /// too small, this decoder will not be capable of decoding some packets. /// /// See the constructor for information, how to choose this. final int maxOutputBufferSizeBytes; @@ -343,11 +343,11 @@ class BufferedOpusDecoder extends OpusDecoder { /// The native allocated buffer size is determined by [maxInputBufferSizeBytes] and [maxOutputBufferSizeBytes]. /// /// You should choose [maxInputBufferSizeBytes] big enough to put every opus packet you want to decode in it. - /// If you omit this parameter, [maxDataByes] is used, which guarantees that there is enough space for every + /// If you omit this parameter, [maxDataBytes] is used, which guarantees that there is enough space for every /// valid opus packet. /// /// [maxOutputBufferSizeBytes] is the size of the output buffer, which will hold the decoded frames. - /// If this value is choosen to small, this decoder will not be capable of decoding some packets. + /// If this value is chosen too small, this decoder will not be capable of decoding some packets. /// If you are unsure, just let it `null`, so the maximum size of resulting frames will be calculated /// Here is some more theory about that: /// A single opus packet may contain up to 120ms of audio, so assuming you are decoding @@ -440,7 +440,7 @@ class BufferedOpusDecoder extends OpusDecoder { /// in ms (as above). Then, call this method a second time with /// the same packet, but with [fec] set to `false`. You can read more about the /// correct usage of forward error correction [here](https://stackoverflow.com/questions/49427579/how-to-use-fec-feature-for-opus-codec). - /// Note: A real packet loss occurse if you lose two or more packets in a row. + /// Note: A real packet loss occurs if you lose two or more packets in a row. /// You are only able to restore the last lost packet and the other packets are /// really lost. So for them, you have to report packet loss. /// @@ -457,7 +457,7 @@ class BufferedOpusDecoder extends OpusDecoder { /// opus packet and decodes them to float samples, stored in the [outputBuffer]. /// Set [inputBufferIndex] to `0` to indicate packet loss. /// - /// If [autoSoftClip] is true, this decoders [pcmSoftClipOutputBuffer] method is automatically called. + /// If [autoSoftClip] is true, this decoder's [pcmSoftClipOutputBuffer] method is automatically called. /// /// Apart from that, this method behaves just as [decode], so see there for more information. @override @@ -502,7 +502,7 @@ abstract class OpusDecoder { /// Number of channels, must be 1 for mono or 2 for stereo. int get channels; - /// Wheter this decoder was already destroyed by calling [destroy]. + /// Whether this decoder was already destroyed by calling [destroy]. /// If so, calling any method will result in an [OpusDestroyedError]. bool get destroyed; @@ -523,5 +523,5 @@ int _estimateLoss(int? loss, int? lastPacketDurationMs) { throw StateError( 'Tried to estimate the loss based on the last packets duration, but there was no last packet!\n' 'This happend because you called a decode function with no input (null as input in SimpleOpusDecoder or 0 as inputBufferIndex in BufferedOpusDecoder), but failed to specify how many milliseconds were lost.\n' - 'And since there was no previous sucessfull decoded packet, the decoder could not estimate how many milliseconds are missing.'); + 'And since there was no previous successful decoded packet, the decoder could not estimate how many milliseconds are missing.'); } diff --git a/opus_dart/lib/src/opus_dart_encoder.dart b/opus_dart/lib/src/opus_dart_encoder.dart index e7c7a61..99289c1 100644 --- a/opus_dart/lib/src/opus_dart_encoder.dart +++ b/opus_dart/lib/src/opus_dart_encoder.dart @@ -28,7 +28,7 @@ Pointer _createOpusEncoder({ /// An easy to use implementation of [OpusEncoder]. /// Don't forget to call [destroy] once you are done with it. /// -/// All method calls in this calls allocate their own memory everytime they are called. +/// All method calls in this class allocate their own memory everytime they are called. /// See the [BufferedOpusEncoder] for an implementation with less allocation calls. class SimpleOpusEncoder extends OpusEncoder { static final _finalizer = Finalizer((cleanup) => cleanup()); @@ -99,10 +99,10 @@ class SimpleOpusEncoder extends OpusEncoder { /// `input.length = 2 * 48000Hz * 0.02s = 1920`. /// /// [maxOutputSizeBytes] is used to allocate the output buffer. It can be used to impose an instant - /// upper limit on the bitrate, but must not be to small to hold the encoded data (or an exception will be thrown). + /// upper limit on the bitrate, but must not be too small to hold the encoded data (or an exception will be thrown). /// The default value of [maxDataBytes] ensures that there is enough space. /// - /// The returnes list contains the bytes of the encoded opus packet. + /// The returned list contains the bytes of the encoded opus packet. /// /// [input] and the returned list are copied to and respectively from native memory. Uint8List encode( @@ -157,7 +157,7 @@ class SimpleOpusEncoder extends OpusEncoder { /// /// The idea behind this implementation is to reduce the amount of memory allocation calls. /// Instead of allocating new buffers everytime something is encoded, the buffers are -/// allocated at initalization. Then, pcm samples is directly written into the [inputBuffer], +/// allocated at initialization. Then, pcm samples is directly written into the [inputBuffer], /// the [inputBufferIndex] is updated, based on how many data where written, and /// one of the encode methods is called. The encoded opus packet can then be accessed using /// the [outputBuffer] getter. @@ -196,7 +196,7 @@ class BufferedOpusEncoder extends OpusEncoder { @override bool get destroyed => _destroyed; - /// The size of the allocated the input buffer in bytes (not sampels). + /// The size of the allocated the input buffer in bytes (not samples). final int maxInputBufferSizeBytes; /// Indicates, how many bytes of data are currently stored in the [inputBuffer]. @@ -214,8 +214,8 @@ class BufferedOpusEncoder extends OpusEncoder { _inputBuffer.asTypedList(maxInputBufferSizeBytes); /// The size of the allocated the output buffer. It can be used to impose an instant - /// upper limit on the bitrate, but must not be to small to hold the encoded data. - /// Otherwise, the enocde methods might throw an exception. + /// upper limit on the bitrate, but must not be too small to hold the encoded data. + /// Otherwise, the encode methods might throw an exception. /// The default value of [maxDataBytes] ensures that there is enough space. final int maxOutputBufferSizeBytes; int _outputBufferIndex; @@ -260,7 +260,7 @@ class BufferedOpusEncoder extends OpusEncoder { /// frame (120ms at 48000Hz) in float representation. /// If you know that you only use input data in s16le representation you can manually set this to [bytesPerInt16Sample] * [maxSamplesPerPacket]. /// - /// [maxOutputBufferSizeBytes] defaults to [maxDataBytes] to guarantee that their is enough space in the + /// [maxOutputBufferSizeBytes] defaults to [maxDataBytes] to guarantee that there is enough space in the /// output buffer for any possible valid input. /// /// For the other parameters, see the matching fields for more information. @@ -327,7 +327,7 @@ class BufferedOpusEncoder extends OpusEncoder { /// Interpets [inputBufferIndex] bytes of the [inputBuffer] as float pcm data, and encodes them to the [outputBuffer]. /// This means, that this method encodes `[inputBufferIndex]/4` samples, since `inputBufferIndex` is in bytes, - /// and the float represntation uses two bytes per sample. + /// and the float representation uses four bytes per sample. /// /// Except that the sample count is calculated by dividing the [inputBufferIndex] by 4 and not by 2, /// this method behaves just as [encode], so see there for more information. @@ -358,7 +358,7 @@ abstract class OpusEncoder { /// Setting the right application type can increase quality of the encoded frames. Application get application; - /// Wheter this encoder was already destroyed by calling [destroy]. + /// Whether this encoder was already destroyed by calling [destroy]. /// If so, calling any method will result in an [OpusDestroyedError]. bool get destroyed; diff --git a/opus_dart/lib/src/opus_dart_misc.dart b/opus_dart/lib/src/opus_dart_misc.dart index 28aea6c..b452b8c 100644 --- a/opus_dart/lib/src/opus_dart_misc.dart +++ b/opus_dart/lib/src/opus_dart_misc.dart @@ -18,7 +18,7 @@ const int bytesPerFloatSample = 4; /// for an explanation how this was calculated. const int maxDataBytes = 3 * 1275; -/// Calculates, how much sampels a single opus package at [sampleRate] with [channels] may contain. +/// Calculates, how many samples a single opus packet at [sampleRate] with [channels] may contain. /// /// A single package may contain up 120ms of audio. This value is reached by combining up to 3 frames of 40ms audio. int maxSamplesPerPacket(int sampleRate, int channels) => @@ -56,7 +56,7 @@ class OpusException implements Exception { } } -/// Thrown when attempting to call an method on an already destroyed encoder or decoder. +/// Thrown when attempting to call a method on an already destroyed encoder or decoder. class OpusDestroyedError extends StateError { OpusDestroyedError.encoder() : super( From 324d3da11d959280e551a02d27b6846c302b3fe5 Mon Sep 17 00:00:00 2001 From: Pedro Paulo de Amorim Date: Wed, 25 Feb 2026 01:44:06 +0000 Subject: [PATCH 18/21] Add RFC 6716 validation note to maxDataBytes and simplify getOpusVersion --- opus_dart/lib/src/opus_dart_misc.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/opus_dart/lib/src/opus_dart_misc.dart b/opus_dart/lib/src/opus_dart_misc.dart index b452b8c..34735f5 100644 --- a/opus_dart/lib/src/opus_dart_misc.dart +++ b/opus_dart/lib/src/opus_dart_misc.dart @@ -16,6 +16,7 @@ const int bytesPerFloatSample = 4; /// /// See [here](https://stackoverflow.com/questions/55698317/what-value-to-use-for-libopus-encoder-max-data-bytes-field) /// for an explanation how this was calculated. +/// Last validated on 2026-02-25 against RFC 6716. const int maxDataBytes = 3 * 1275; /// Calculates, how many samples a single opus packet at [sampleRate] with [channels] may contain. @@ -25,9 +26,7 @@ int maxSamplesPerPacket(int sampleRate, int channels) => ((sampleRate * channels * 120) / 1000).ceil(); /// Returns the version of the native libopus library. -String getOpusVersion() { - return _asString(opus.libinfo.opus_get_version_string()); -} +String getOpusVersion() => _asString(opus.libinfo.opus_get_version_string()); /// Upper bound for null-terminated string scans to prevent unbounded loops /// when a pointer is invalid or lacks a terminator. From be469db54a67f8df2b028a2c15f3497689fb58e4 Mon Sep 17 00:00:00 2001 From: Pedro Paulo de Amorim Date: Wed, 25 Feb 2026 01:46:59 +0000 Subject: [PATCH 19/21] refactor: deduplicate OpusPacketUtils with shared _withNativePacket helper --- opus_dart/lib/src/opus_dart_packet.dart | 81 ++++++++----------------- 1 file changed, 26 insertions(+), 55 deletions(-) diff --git a/opus_dart/lib/src/opus_dart_packet.dart b/opus_dart/lib/src/opus_dart_packet.dart index 5ed25b6..ca45d24 100644 --- a/opus_dart/lib/src/opus_dart_packet.dart +++ b/opus_dart/lib/src/opus_dart_packet.dart @@ -3,87 +3,58 @@ import 'dart:typed_data'; import '../wrappers/opus_defines.dart' as opus_defines; import 'opus_dart_misc.dart'; -/// Bundles utility functions to examin opus packets. +/// Bundles utility functions to examine opus packets. /// /// All methods copy the input data into native memory. abstract class OpusPacketUtils { - /// Returns the amount of samples in a [packet] given a [sampleRate]. - static int getSampleCount( - {required Uint8List packet, required int sampleRate}) { + static int _withNativePacket( + Uint8List packet, int Function(Pointer data) operation) { Pointer data = opus.allocator.call(packet.length); data.asTypedList(packet.length).setAll(0, packet); try { - int sampleCount = opus.decoder - .opus_packet_get_nb_samples(data, packet.length, sampleRate); - if (sampleCount < opus_defines.OPUS_OK) { - throw OpusException(sampleCount); + int result = operation(data); + if (result < opus_defines.OPUS_OK) { + throw OpusException(result); } - return sampleCount; + return result; } finally { opus.allocator.free(data); } } + /// Returns the amount of samples in a [packet] given a [sampleRate]. + static int getSampleCount( + {required Uint8List packet, required int sampleRate}) { + return _withNativePacket( + packet, + (data) => opus.decoder + .opus_packet_get_nb_samples(data, packet.length, sampleRate)); + } + /// Returns the amount of frames in a [packet]. static int getFrameCount({required Uint8List packet}) { - Pointer data = opus.allocator.call(packet.length); - data.asTypedList(packet.length).setAll(0, packet); - try { - int frameCount = - opus.decoder.opus_packet_get_nb_frames(data, packet.length); - if (frameCount < opus_defines.OPUS_OK) { - throw OpusException(frameCount); - } - return frameCount; - } finally { - opus.allocator.free(data); - } + return _withNativePacket(packet, + (data) => opus.decoder.opus_packet_get_nb_frames(data, packet.length)); } /// Returns the amount of samples per frame in a [packet] given a [sampleRate]. static int getSamplesPerFrame( {required Uint8List packet, required int sampleRate}) { - Pointer data = opus.allocator.call(packet.length); - data.asTypedList(packet.length).setAll(0, packet); - try { - int samplesPerFrame = - opus.decoder.opus_packet_get_samples_per_frame(data, sampleRate); - if (samplesPerFrame < opus_defines.OPUS_OK) { - throw OpusException(samplesPerFrame); - } - return samplesPerFrame; - } finally { - opus.allocator.free(data); - } + return _withNativePacket( + packet, + (data) => + opus.decoder.opus_packet_get_samples_per_frame(data, sampleRate)); } /// Returns the channel count from a [packet] static int getChannelCount({required Uint8List packet}) { - Pointer data = opus.allocator.call(packet.length); - data.asTypedList(packet.length).setAll(0, packet); - try { - int channelCount = opus.decoder.opus_packet_get_nb_channels(data); - if (channelCount < opus_defines.OPUS_OK) { - throw OpusException(channelCount); - } - return channelCount; - } finally { - opus.allocator.free(data); - } + return _withNativePacket( + packet, (data) => opus.decoder.opus_packet_get_nb_channels(data)); } /// Returns the bandwidth from a [packet] static int getBandwidth({required Uint8List packet}) { - Pointer data = opus.allocator.call(packet.length); - data.asTypedList(packet.length).setAll(0, packet); - try { - int bandwidth = opus.decoder.opus_packet_get_bandwidth(data); - if (bandwidth < opus_defines.OPUS_OK) { - throw OpusException(bandwidth); - } - return bandwidth; - } finally { - opus.allocator.free(data); - } + return _withNativePacket( + packet, (data) => opus.decoder.opus_packet_get_bandwidth(data)); } } From 80378e17983320d87d132dbd105ce0519441dfa1 Mon Sep 17 00:00:00 2001 From: Pedro Paulo de Amorim Date: Wed, 25 Feb 2026 01:52:29 +0000 Subject: [PATCH 20/21] chore: update 3.0.5 changelog across all packages and add sync script --- opus_dart/CHANGELOG.md | 29 ++++++- opus_flutter/CHANGELOG.md | 54 ++++++++----- opus_flutter_android/CHANGELOG.md | 76 +++++++++++++++++- opus_flutter_ios/CHANGELOG.md | 74 ++++++++++++++++-- opus_flutter_linux/CHANGELOG.md | 75 +++++++++++++++++- opus_flutter_macos/CHANGELOG.md | 81 ++++++++++++++++++-- opus_flutter_platform_interface/CHANGELOG.md | 77 ++++++++++++++++++- opus_flutter_windows/CHANGELOG.md | 77 ++++++++++++++++++- scripts/sync_changelogs.sh | 39 ++++++++++ 9 files changed, 535 insertions(+), 47 deletions(-) create mode 100755 scripts/sync_changelogs.sh diff --git a/opus_dart/CHANGELOG.md b/opus_dart/CHANGELOG.md index 86d36b4..44f04bb 100644 --- a/opus_dart/CHANGELOG.md +++ b/opus_dart/CHANGELOG.md @@ -1,7 +1,32 @@ ## 3.0.5 -* Add `repository` field to pubspec -* Add `CHANGELOG.md` +### Bug Fixes + +* Fix output buffer sizing for `BufferedOpusDecoder` (was 4x too small for float output) and inverted multiplier in `StreamOpusDecoder` +* Fix `opus_encoder_ctl` variadic ABI mismatch under WASM by adding a non-variadic C wrapper +* Prevent `asTypedList` view detachment on WASM memory growth by returning Dart-heap copies +* Always copy streaming output to the Dart heap, eliminating use-after-write hazards in `StreamOpusEncoder` and `StreamOpusDecoder` +* Guard encoder/decoder methods against use after `destroy()` with `OpusDestroyedError` +* Attach `Finalizer` to encoder/decoder classes for GC-driven native resource cleanup +* Prevent memory leak when a second native allocation throws +* Add `_asString` bounds guard (cap at 256 bytes) to prevent unbounded scanning +* Register missing `OpusCustomMode` opaque type in `init_web.dart` (was duplicating `OpusRepacketizer`) +* Export `_opus_encoder_ctl` in WASM Dockerfile `EXPORTED_FUNCTIONS` + +### Refactoring + +* Extract duplicated encode logic into `_createOpusEncoder` and `_doEncode` / `_encodeBuffer` helpers +* Extract duplicated decode logic into shared helpers and replace magic numbers with named constants +* Deduplicate `OpusPacketUtils` with a shared `_withNativePacket` helper +* Simplify `getOpusVersion` implementation +* Add `bytesPerInt16Sample` and `bytesPerFloatSample` constants in `opus_dart_misc.dart` + +### Chores + +* Add `repository` field to pubspec and `CHANGELOG.md` +* Fix typos across comments and documentation +* Add RFC 6716 validation note to `maxDataBytes` +* Add comprehensive tests for buffer sizing, bounds checking, use-after-destroy, and allocation failure cleanup ## 3.0.4 diff --git a/opus_flutter/CHANGELOG.md b/opus_flutter/CHANGELOG.md index c65b784..44f04bb 100644 --- a/opus_flutter/CHANGELOG.md +++ b/opus_flutter/CHANGELOG.md @@ -1,66 +1,84 @@ ## 3.0.5 -* Bump all package versions +### Bug Fixes + +* Fix output buffer sizing for `BufferedOpusDecoder` (was 4x too small for float output) and inverted multiplier in `StreamOpusDecoder` +* Fix `opus_encoder_ctl` variadic ABI mismatch under WASM by adding a non-variadic C wrapper +* Prevent `asTypedList` view detachment on WASM memory growth by returning Dart-heap copies +* Always copy streaming output to the Dart heap, eliminating use-after-write hazards in `StreamOpusEncoder` and `StreamOpusDecoder` +* Guard encoder/decoder methods against use after `destroy()` with `OpusDestroyedError` +* Attach `Finalizer` to encoder/decoder classes for GC-driven native resource cleanup +* Prevent memory leak when a second native allocation throws +* Add `_asString` bounds guard (cap at 256 bytes) to prevent unbounded scanning +* Register missing `OpusCustomMode` opaque type in `init_web.dart` (was duplicating `OpusRepacketizer`) +* Export `_opus_encoder_ctl` in WASM Dockerfile `EXPORTED_FUNCTIONS` + +### Refactoring + +* Extract duplicated encode logic into `_createOpusEncoder` and `_doEncode` / `_encodeBuffer` helpers +* Extract duplicated decode logic into shared helpers and replace magic numbers with named constants +* Deduplicate `OpusPacketUtils` with a shared `_withNativePacket` helper +* Simplify `getOpusVersion` implementation +* Add `bytesPerInt16Sample` and `bytesPerFloatSample` constants in `opus_dart_misc.dart` + +### Chores + +* Add `repository` field to pubspec and `CHANGELOG.md` +* Fix typos across comments and documentation +* Add RFC 6716 validation note to `maxDataBytes` +* Add comprehensive tests for buffer sizing, bounds checking, use-after-destroy, and allocation failure cleanup ## 3.0.4 -* Add Swift Package Manager support to `opus_codec_ios` and `opus_codec_macos` -* Bump all package versions +* Bump version ## 3.0.3 -* Depend on `opus_flutter_ios:3.0.1` and `opus_flutter_android:3.0.1` +* Depend on newer `wasm_ffi` version for web support ## 3.0.2 * libopus 1.3.1 -* Updated example ## 3.0.1 * libopus 1.3.1 -* Depend on newer opus_flutter_web version ## 3.0.0 +* Migrate to `opus_flutter` namespace +* Web support using [`wasm_ffi`](https://pub.dev/packages/wasm_ffi) * libopus 1.3.1 -* Web support using [`web_ffi`](https://pub.dev/packages/web_ffi) ## 2.0.1 * libopus 1.3.1 -* Minor formating fixes +* Minor formatting fixes ## 2.0.0 * libopus 1.3.1 -* Updated for opus_dart 2.0.1, supporting null safety by this -* Moved to federal plugin structure -* Removed re-export of the opus_dart library, see README +* Null safety support -## 1.1.0 +## 1.0.4 * libopus 1.3.1 -* Update for opus_dart 1.0.4 -* Migrated to new plugin map in pubspec.yaml -* Included iOS binaries -## 1.0.1 +## 1.0.3 * libopus 1.3.1 -* Update for opus_dart 1.0.3 ## 1.0.0 * libopus 1.3.1 -* Initial release \ No newline at end of file +* Initial release diff --git a/opus_flutter_android/CHANGELOG.md b/opus_flutter_android/CHANGELOG.md index aac2f98..44f04bb 100644 --- a/opus_flutter_android/CHANGELOG.md +++ b/opus_flutter_android/CHANGELOG.md @@ -1,16 +1,84 @@ ## 3.0.5 -* Bump all package versions +### Bug Fixes + +* Fix output buffer sizing for `BufferedOpusDecoder` (was 4x too small for float output) and inverted multiplier in `StreamOpusDecoder` +* Fix `opus_encoder_ctl` variadic ABI mismatch under WASM by adding a non-variadic C wrapper +* Prevent `asTypedList` view detachment on WASM memory growth by returning Dart-heap copies +* Always copy streaming output to the Dart heap, eliminating use-after-write hazards in `StreamOpusEncoder` and `StreamOpusDecoder` +* Guard encoder/decoder methods against use after `destroy()` with `OpusDestroyedError` +* Attach `Finalizer` to encoder/decoder classes for GC-driven native resource cleanup +* Prevent memory leak when a second native allocation throws +* Add `_asString` bounds guard (cap at 256 bytes) to prevent unbounded scanning +* Register missing `OpusCustomMode` opaque type in `init_web.dart` (was duplicating `OpusRepacketizer`) +* Export `_opus_encoder_ctl` in WASM Dockerfile `EXPORTED_FUNCTIONS` + +### Refactoring + +* Extract duplicated encode logic into `_createOpusEncoder` and `_doEncode` / `_encodeBuffer` helpers +* Extract duplicated decode logic into shared helpers and replace magic numbers with named constants +* Deduplicate `OpusPacketUtils` with a shared `_withNativePacket` helper +* Simplify `getOpusVersion` implementation +* Add `bytesPerInt16Sample` and `bytesPerFloatSample` constants in `opus_dart_misc.dart` + +### Chores + +* Add `repository` field to pubspec and `CHANGELOG.md` +* Fix typos across comments and documentation +* Add RFC 6716 validation note to `maxDataBytes` +* Add comprehensive tests for buffer sizing, bounds checking, use-after-destroy, and allocation failure cleanup + ## 3.0.4 * Bump version + +## 3.0.3 + +* Depend on newer `wasm_ffi` version for web support + + +## 3.0.2 + +* libopus 1.3.1 + + ## 3.0.1 -* Upgraded gradle build files + +* libopus 1.3.1 + ## 3.0.0 -* Adopt `opus_flutter_platform_interface 3.0.0` + +* Migrate to `opus_flutter` namespace +* Web support using [`wasm_ffi`](https://pub.dev/packages/wasm_ffi) +* libopus 1.3.1 + + +## 2.0.1 + +* libopus 1.3.1 +* Minor formatting fixes + ## 2.0.0 -* Initial release in federal plugin structure \ No newline at end of file + +* libopus 1.3.1 +* Null safety support + + +## 1.0.4 + +* libopus 1.3.1 + + +## 1.0.3 + +* libopus 1.3.1 + + +## 1.0.0 + +* libopus 1.3.1 +* Initial release diff --git a/opus_flutter_ios/CHANGELOG.md b/opus_flutter_ios/CHANGELOG.md index cc1d4ba..44f04bb 100644 --- a/opus_flutter_ios/CHANGELOG.md +++ b/opus_flutter_ios/CHANGELOG.md @@ -1,26 +1,84 @@ ## 3.0.5 -* Fix podspec version to match pubspec -* Bump all package versions +### Bug Fixes + +* Fix output buffer sizing for `BufferedOpusDecoder` (was 4x too small for float output) and inverted multiplier in `StreamOpusDecoder` +* Fix `opus_encoder_ctl` variadic ABI mismatch under WASM by adding a non-variadic C wrapper +* Prevent `asTypedList` view detachment on WASM memory growth by returning Dart-heap copies +* Always copy streaming output to the Dart heap, eliminating use-after-write hazards in `StreamOpusEncoder` and `StreamOpusDecoder` +* Guard encoder/decoder methods against use after `destroy()` with `OpusDestroyedError` +* Attach `Finalizer` to encoder/decoder classes for GC-driven native resource cleanup +* Prevent memory leak when a second native allocation throws +* Add `_asString` bounds guard (cap at 256 bytes) to prevent unbounded scanning +* Register missing `OpusCustomMode` opaque type in `init_web.dart` (was duplicating `OpusRepacketizer`) +* Export `_opus_encoder_ctl` in WASM Dockerfile `EXPORTED_FUNCTIONS` + +### Refactoring + +* Extract duplicated encode logic into `_createOpusEncoder` and `_doEncode` / `_encodeBuffer` helpers +* Extract duplicated decode logic into shared helpers and replace magic numbers with named constants +* Deduplicate `OpusPacketUtils` with a shared `_withNativePacket` helper +* Simplify `getOpusVersion` implementation +* Add `bytesPerInt16Sample` and `bytesPerFloatSample` constants in `opus_dart_misc.dart` + +### Chores + +* Add `repository` field to pubspec and `CHANGELOG.md` +* Fix typos across comments and documentation +* Add RFC 6716 validation note to `maxDataBytes` +* Add comprehensive tests for buffer sizing, bounds checking, use-after-destroy, and allocation failure cleanup ## 3.0.4 -* Add Swift Package Manager support -* Migrate plugin source files to SPM-compatible directory layout -* Retain CocoaPods compatibility +* Bump version + + +## 3.0.3 + +* Depend on newer `wasm_ffi` version for web support + + +## 3.0.2 + +* libopus 1.3.1 ## 3.0.1 -* Using new opus.xcframework +* libopus 1.3.1 ## 3.0.0 -* Adopt `opus_flutter_platform_interface 3.0.0` +* Migrate to `opus_flutter` namespace +* Web support using [`wasm_ffi`](https://pub.dev/packages/wasm_ffi) +* libopus 1.3.1 + + +## 2.0.1 + +* libopus 1.3.1 +* Minor formatting fixes ## 2.0.0 -* Initial release in federal plugin structure \ No newline at end of file +* libopus 1.3.1 +* Null safety support + + +## 1.0.4 + +* libopus 1.3.1 + + +## 1.0.3 + +* libopus 1.3.1 + + +## 1.0.0 + +* libopus 1.3.1 +* Initial release diff --git a/opus_flutter_linux/CHANGELOG.md b/opus_flutter_linux/CHANGELOG.md index 891dcd4..44f04bb 100644 --- a/opus_flutter_linux/CHANGELOG.md +++ b/opus_flutter_linux/CHANGELOG.md @@ -1,6 +1,32 @@ ## 3.0.5 -* Bump all package versions +### Bug Fixes + +* Fix output buffer sizing for `BufferedOpusDecoder` (was 4x too small for float output) and inverted multiplier in `StreamOpusDecoder` +* Fix `opus_encoder_ctl` variadic ABI mismatch under WASM by adding a non-variadic C wrapper +* Prevent `asTypedList` view detachment on WASM memory growth by returning Dart-heap copies +* Always copy streaming output to the Dart heap, eliminating use-after-write hazards in `StreamOpusEncoder` and `StreamOpusDecoder` +* Guard encoder/decoder methods against use after `destroy()` with `OpusDestroyedError` +* Attach `Finalizer` to encoder/decoder classes for GC-driven native resource cleanup +* Prevent memory leak when a second native allocation throws +* Add `_asString` bounds guard (cap at 256 bytes) to prevent unbounded scanning +* Register missing `OpusCustomMode` opaque type in `init_web.dart` (was duplicating `OpusRepacketizer`) +* Export `_opus_encoder_ctl` in WASM Dockerfile `EXPORTED_FUNCTIONS` + +### Refactoring + +* Extract duplicated encode logic into `_createOpusEncoder` and `_doEncode` / `_encodeBuffer` helpers +* Extract duplicated decode logic into shared helpers and replace magic numbers with named constants +* Deduplicate `OpusPacketUtils` with a shared `_withNativePacket` helper +* Simplify `getOpusVersion` implementation +* Add `bytesPerInt16Sample` and `bytesPerFloatSample` constants in `opus_dart_misc.dart` + +### Chores + +* Add `repository` field to pubspec and `CHANGELOG.md` +* Fix typos across comments and documentation +* Add RFC 6716 validation note to `maxDataBytes` +* Add comprehensive tests for buffer sizing, bounds checking, use-after-destroy, and allocation failure cleanup ## 3.0.4 @@ -8,6 +34,51 @@ * Bump version +## 3.0.3 + +* Depend on newer `wasm_ffi` version for web support + + +## 3.0.2 + +* libopus 1.3.1 + + +## 3.0.1 + +* libopus 1.3.1 + + ## 3.0.0 -* Initial release. Loads opus from the system library (`libopus.so.0`). +* Migrate to `opus_flutter` namespace +* Web support using [`wasm_ffi`](https://pub.dev/packages/wasm_ffi) +* libopus 1.3.1 + + +## 2.0.1 + +* libopus 1.3.1 +* Minor formatting fixes + + +## 2.0.0 + +* libopus 1.3.1 +* Null safety support + + +## 1.0.4 + +* libopus 1.3.1 + + +## 1.0.3 + +* libopus 1.3.1 + + +## 1.0.0 + +* libopus 1.3.1 +* Initial release diff --git a/opus_flutter_macos/CHANGELOG.md b/opus_flutter_macos/CHANGELOG.md index 77e40d9..44f04bb 100644 --- a/opus_flutter_macos/CHANGELOG.md +++ b/opus_flutter_macos/CHANGELOG.md @@ -1,17 +1,84 @@ ## 3.0.5 -* Fix podspec version to match pubspec -* Bump all package versions +### Bug Fixes + +* Fix output buffer sizing for `BufferedOpusDecoder` (was 4x too small for float output) and inverted multiplier in `StreamOpusDecoder` +* Fix `opus_encoder_ctl` variadic ABI mismatch under WASM by adding a non-variadic C wrapper +* Prevent `asTypedList` view detachment on WASM memory growth by returning Dart-heap copies +* Always copy streaming output to the Dart heap, eliminating use-after-write hazards in `StreamOpusEncoder` and `StreamOpusDecoder` +* Guard encoder/decoder methods against use after `destroy()` with `OpusDestroyedError` +* Attach `Finalizer` to encoder/decoder classes for GC-driven native resource cleanup +* Prevent memory leak when a second native allocation throws +* Add `_asString` bounds guard (cap at 256 bytes) to prevent unbounded scanning +* Register missing `OpusCustomMode` opaque type in `init_web.dart` (was duplicating `OpusRepacketizer`) +* Export `_opus_encoder_ctl` in WASM Dockerfile `EXPORTED_FUNCTIONS` + +### Refactoring + +* Extract duplicated encode logic into `_createOpusEncoder` and `_doEncode` / `_encodeBuffer` helpers +* Extract duplicated decode logic into shared helpers and replace magic numbers with named constants +* Deduplicate `OpusPacketUtils` with a shared `_withNativePacket` helper +* Simplify `getOpusVersion` implementation +* Add `bytesPerInt16Sample` and `bytesPerFloatSample` constants in `opus_dart_misc.dart` + +### Chores + +* Add `repository` field to pubspec and `CHANGELOG.md` +* Fix typos across comments and documentation +* Add RFC 6716 validation note to `maxDataBytes` +* Add comprehensive tests for buffer sizing, bounds checking, use-after-destroy, and allocation failure cleanup ## 3.0.4 -* Add Swift Package Manager support -* Migrate plugin source files to SPM-compatible directory layout -* Retain CocoaPods compatibility +* Bump version + + +## 3.0.3 + +* Depend on newer `wasm_ffi` version for web support + + +## 3.0.2 + +* libopus 1.3.1 + + +## 3.0.1 + +* libopus 1.3.1 ## 3.0.0 -* Initial release with macOS support using opus.xcframework -* Adopt `opus_flutter_platform_interface 3.0.0` +* Migrate to `opus_flutter` namespace +* Web support using [`wasm_ffi`](https://pub.dev/packages/wasm_ffi) +* libopus 1.3.1 + + +## 2.0.1 + +* libopus 1.3.1 +* Minor formatting fixes + + +## 2.0.0 + +* libopus 1.3.1 +* Null safety support + + +## 1.0.4 + +* libopus 1.3.1 + + +## 1.0.3 + +* libopus 1.3.1 + + +## 1.0.0 + +* libopus 1.3.1 +* Initial release diff --git a/opus_flutter_platform_interface/CHANGELOG.md b/opus_flutter_platform_interface/CHANGELOG.md index 13cf324..44f04bb 100644 --- a/opus_flutter_platform_interface/CHANGELOG.md +++ b/opus_flutter_platform_interface/CHANGELOG.md @@ -1,13 +1,84 @@ ## 3.0.5 -* Bump all package versions +### Bug Fixes + +* Fix output buffer sizing for `BufferedOpusDecoder` (was 4x too small for float output) and inverted multiplier in `StreamOpusDecoder` +* Fix `opus_encoder_ctl` variadic ABI mismatch under WASM by adding a non-variadic C wrapper +* Prevent `asTypedList` view detachment on WASM memory growth by returning Dart-heap copies +* Always copy streaming output to the Dart heap, eliminating use-after-write hazards in `StreamOpusEncoder` and `StreamOpusDecoder` +* Guard encoder/decoder methods against use after `destroy()` with `OpusDestroyedError` +* Attach `Finalizer` to encoder/decoder classes for GC-driven native resource cleanup +* Prevent memory leak when a second native allocation throws +* Add `_asString` bounds guard (cap at 256 bytes) to prevent unbounded scanning +* Register missing `OpusCustomMode` opaque type in `init_web.dart` (was duplicating `OpusRepacketizer`) +* Export `_opus_encoder_ctl` in WASM Dockerfile `EXPORTED_FUNCTIONS` + +### Refactoring + +* Extract duplicated encode logic into `_createOpusEncoder` and `_doEncode` / `_encodeBuffer` helpers +* Extract duplicated decode logic into shared helpers and replace magic numbers with named constants +* Deduplicate `OpusPacketUtils` with a shared `_withNativePacket` helper +* Simplify `getOpusVersion` implementation +* Add `bytesPerInt16Sample` and `bytesPerFloatSample` constants in `opus_dart_misc.dart` + +### Chores + +* Add `repository` field to pubspec and `CHANGELOG.md` +* Fix typos across comments and documentation +* Add RFC 6716 validation note to `maxDataBytes` +* Add comprehensive tests for buffer sizing, bounds checking, use-after-destroy, and allocation failure cleanup + ## 3.0.4 * Bump version + +## 3.0.3 + +* Depend on newer `wasm_ffi` version for web support + + +## 3.0.2 + +* libopus 1.3.1 + + +## 3.0.1 + +* libopus 1.3.1 + + ## 3.0.0 -* Necessary changes for web support + +* Migrate to `opus_flutter` namespace +* Web support using [`wasm_ffi`](https://pub.dev/packages/wasm_ffi) +* libopus 1.3.1 + + +## 2.0.1 + +* libopus 1.3.1 +* Minor formatting fixes + ## 2.0.0 -* Initial release in federal plugin structure \ No newline at end of file + +* libopus 1.3.1 +* Null safety support + + +## 1.0.4 + +* libopus 1.3.1 + + +## 1.0.3 + +* libopus 1.3.1 + + +## 1.0.0 + +* libopus 1.3.1 +* Initial release diff --git a/opus_flutter_windows/CHANGELOG.md b/opus_flutter_windows/CHANGELOG.md index d58b2cd..44f04bb 100644 --- a/opus_flutter_windows/CHANGELOG.md +++ b/opus_flutter_windows/CHANGELOG.md @@ -1,13 +1,84 @@ ## 3.0.5 -* Bump all package versions +### Bug Fixes + +* Fix output buffer sizing for `BufferedOpusDecoder` (was 4x too small for float output) and inverted multiplier in `StreamOpusDecoder` +* Fix `opus_encoder_ctl` variadic ABI mismatch under WASM by adding a non-variadic C wrapper +* Prevent `asTypedList` view detachment on WASM memory growth by returning Dart-heap copies +* Always copy streaming output to the Dart heap, eliminating use-after-write hazards in `StreamOpusEncoder` and `StreamOpusDecoder` +* Guard encoder/decoder methods against use after `destroy()` with `OpusDestroyedError` +* Attach `Finalizer` to encoder/decoder classes for GC-driven native resource cleanup +* Prevent memory leak when a second native allocation throws +* Add `_asString` bounds guard (cap at 256 bytes) to prevent unbounded scanning +* Register missing `OpusCustomMode` opaque type in `init_web.dart` (was duplicating `OpusRepacketizer`) +* Export `_opus_encoder_ctl` in WASM Dockerfile `EXPORTED_FUNCTIONS` + +### Refactoring + +* Extract duplicated encode logic into `_createOpusEncoder` and `_doEncode` / `_encodeBuffer` helpers +* Extract duplicated decode logic into shared helpers and replace magic numbers with named constants +* Deduplicate `OpusPacketUtils` with a shared `_withNativePacket` helper +* Simplify `getOpusVersion` implementation +* Add `bytesPerInt16Sample` and `bytesPerFloatSample` constants in `opus_dart_misc.dart` + +### Chores + +* Add `repository` field to pubspec and `CHANGELOG.md` +* Fix typos across comments and documentation +* Add RFC 6716 validation note to `maxDataBytes` +* Add comprehensive tests for buffer sizing, bounds checking, use-after-destroy, and allocation failure cleanup + ## 3.0.4 * Bump version + +## 3.0.3 + +* Depend on newer `wasm_ffi` version for web support + + +## 3.0.2 + +* libopus 1.3.1 + + +## 3.0.1 + +* libopus 1.3.1 + + ## 3.0.0 -* Adopt `opus_flutter_platform_interface 3.0.0` + +* Migrate to `opus_flutter` namespace +* Web support using [`wasm_ffi`](https://pub.dev/packages/wasm_ffi) +* libopus 1.3.1 + + +## 2.0.1 + +* libopus 1.3.1 +* Minor formatting fixes + ## 2.0.0 -* Initial release in federal plugin structure \ No newline at end of file + +* libopus 1.3.1 +* Null safety support + + +## 1.0.4 + +* libopus 1.3.1 + + +## 1.0.3 + +* libopus 1.3.1 + + +## 1.0.0 + +* libopus 1.3.1 +* Initial release diff --git a/scripts/sync_changelogs.sh b/scripts/sync_changelogs.sh new file mode 100755 index 0000000..c2e9061 --- /dev/null +++ b/scripts/sync_changelogs.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# +# sync_changelogs.sh +# +# Copies opus_dart/CHANGELOG.md to every package in the monorepo so all +# changelogs stay in sync. +# +# Usage: +# ./scripts/sync_changelogs.sh + +set -euo pipefail + +# shellcheck source=scripts/common.sh +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" + +SOURCE="$ROOT_DIR/$DART_PACKAGE/CHANGELOG.md" + +if [ ! -f "$SOURCE" ]; then + log_error "Source changelog not found: $SOURCE" + exit 1 +fi + +echo -e "${BOLD}Sync changelogs — $(date '+%Y-%m-%d %H:%M')${NC}" +echo "Source: $SOURCE" + +for package in "${FLUTTER_PACKAGES[@]}"; do + target="$ROOT_DIR/$package/CHANGELOG.md" + + if [ ! -d "$ROOT_DIR/$package" ]; then + log_warning "Directory not found, skipping: $package" + continue + fi + + cp "$SOURCE" "$target" + log_ok "$package" +done + +echo "" +echo -e "${GREEN}${BOLD}All changelogs synced!${NC}" From 0a07821f0d074d114242ce2dd5004c14691f882a Mon Sep 17 00:00:00 2001 From: Pedro Paulo de Amorim Date: Wed, 25 Feb 2026 01:54:47 +0000 Subject: [PATCH 21/21] style: apply dart format to opus_dart sources and tests --- opus_dart/lib/src/opus_dart_decoder.dart | 44 +++++++++++++++--------- opus_dart/lib/src/opus_dart_encoder.dart | 15 ++++---- opus_dart/lib/wrappers/opus_encoder.dart | 10 +++--- opus_dart/test/opus_dart_mock_test.dart | 34 ++++++++---------- 4 files changed, 55 insertions(+), 48 deletions(-) diff --git a/opus_dart/lib/src/opus_dart_decoder.dart b/opus_dart/lib/src/opus_dart_decoder.dart index 32e87f9..c883e92 100644 --- a/opus_dart/lib/src/opus_dart_decoder.dart +++ b/opus_dart/lib/src/opus_dart_decoder.dart @@ -166,9 +166,9 @@ class SimpleOpusDecoder extends OpusDecoder { input: input, fec: fec, loss: loss, - nativeDecode: (inputPtr, inputLen, frameSize) => - opus.decoder.opus_decode(_opusDecoder, inputPtr, inputLen, - outputNative, frameSize, fec ? 1 : 0), + nativeDecode: (inputPtr, inputLen, frameSize) => opus.decoder + .opus_decode(_opusDecoder, inputPtr, inputLen, outputNative, + frameSize, fec ? 1 : 0), ); return Int16List.fromList( outputNative.asTypedList(outputSamplesPerChannel * channels)); @@ -199,9 +199,9 @@ class SimpleOpusDecoder extends OpusDecoder { input: input, fec: fec, loss: loss, - nativeDecode: (inputPtr, inputLen, frameSize) => - opus.decoder.opus_decode_float(_opusDecoder, inputPtr, inputLen, - outputNative, frameSize, fec ? 1 : 0), + nativeDecode: (inputPtr, inputLen, frameSize) => opus.decoder + .opus_decode_float(_opusDecoder, inputPtr, inputLen, outputNative, + frameSize, fec ? 1 : 0), ); if (autoSoftClip) { opus.decoder.opus_pcm_soft_clip(outputNative, @@ -305,13 +305,16 @@ class BufferedOpusDecoder extends OpusDecoder { /// Convenience method to get the current output buffer as s16le. /// Returns a copy safe across WASM memory growth. - Int16List get outputBufferAsInt16List => Int16List.fromList( - _outputBuffer.cast().asTypedList(_outputBufferIndex ~/ bytesPerInt16Sample)); + Int16List get outputBufferAsInt16List => Int16List.fromList(_outputBuffer + .cast() + .asTypedList(_outputBufferIndex ~/ bytesPerInt16Sample)); /// Convenience method to get the current output buffer as floats. /// Returns a copy safe across WASM memory growth. - Float32List get outputBufferAsFloat32List => Float32List.fromList( - _outputBuffer.cast().asTypedList(_outputBufferIndex ~/ bytesPerFloatSample)); + Float32List get outputBufferAsFloat32List => + Float32List.fromList(_outputBuffer + .cast() + .asTypedList(_outputBufferIndex ~/ bytesPerFloatSample)); final Pointer _softClipBuffer; @@ -365,7 +368,8 @@ class BufferedOpusDecoder extends OpusDecoder { int? maxInputBufferSizeBytes, int? maxOutputBufferSizeBytes}) { maxInputBufferSizeBytes ??= maxDataBytes; - maxOutputBufferSizeBytes ??= bytesPerFloatSample * maxSamplesPerPacket(sampleRate, channels); + maxOutputBufferSizeBytes ??= + bytesPerFloatSample * maxSamplesPerPacket(sampleRate, channels); final input = opus.allocator.call(maxInputBufferSizeBytes); Pointer? output; Pointer? softClipBuffer; @@ -397,8 +401,7 @@ class BufferedOpusDecoder extends OpusDecoder { void _decodeBuffer( {required bool useFloat, required bool fec, required int? loss}) { if (_destroyed) throw OpusDestroyedError.decoder(); - final bytesPerSample = - useFloat ? bytesPerFloatSample : bytesPerInt16Sample; + final bytesPerSample = useFloat ? bytesPerFloatSample : bytesPerInt16Sample; Pointer inputNative; int frameSize; if (inputBufferIndex > 0) { @@ -409,8 +412,12 @@ class BufferedOpusDecoder extends OpusDecoder { frameSize = _estimateLoss(loss, lastPacketDurationMs); } final outputSamplesPerChannel = useFloat - ? opus.decoder.opus_decode_float(_opusDecoder, inputNative, - inputBufferIndex, _outputBuffer.cast(), frameSize, + ? opus.decoder.opus_decode_float( + _opusDecoder, + inputNative, + inputBufferIndex, + _outputBuffer.cast(), + frameSize, fec ? 1 : 0) : opus.decoder.opus_decode(_opusDecoder, inputNative, inputBufferIndex, _outputBuffer.cast(), frameSize, fec ? 1 : 0); @@ -487,8 +494,11 @@ class BufferedOpusDecoder extends OpusDecoder { /// Behaves like the toplevel [pcmSoftClip] function, but without unnecessary copying. Float32List pcmSoftClipOutputBuffer() { if (_destroyed) throw OpusDestroyedError.decoder(); - opus.decoder.opus_pcm_soft_clip(_outputBuffer.cast(), - _outputBufferIndex ~/ (bytesPerFloatSample * channels), channels, _softClipBuffer); + opus.decoder.opus_pcm_soft_clip( + _outputBuffer.cast(), + _outputBufferIndex ~/ (bytesPerFloatSample * channels), + channels, + _softClipBuffer); return outputBufferAsFloat32List; } } diff --git a/opus_dart/lib/src/opus_dart_encoder.dart b/opus_dart/lib/src/opus_dart_encoder.dart index 99289c1..8fe7c47 100644 --- a/opus_dart/lib/src/opus_dart_encoder.dart +++ b/opus_dart/lib/src/opus_dart_encoder.dart @@ -79,8 +79,7 @@ class SimpleOpusEncoder extends OpusEncoder { final outputNative = opus.allocator.call(maxOutputSizeBytes); try { final sampleCountPerChannel = inputSampleCount ~/ channels; - final outputLength = - nativeEncode(sampleCountPerChannel, outputNative); + final outputLength = nativeEncode(sampleCountPerChannel, outputNative); if (outputLength < opus_defines.OPUS_OK) { throw OpusException(outputLength); } @@ -302,10 +301,14 @@ class BufferedOpusEncoder extends OpusEncoder { final sampleCountPerChannel = inputBufferIndex ~/ (channels * bytesPerSample); _outputBufferIndex = useFloat - ? opus.encoder.opus_encode_float(_opusEncoder, - _inputBuffer.cast(), sampleCountPerChannel, _outputBuffer, maxOutputBufferSizeBytes) - : opus.encoder.opus_encode(_opusEncoder, - _inputBuffer.cast(), sampleCountPerChannel, _outputBuffer, maxOutputBufferSizeBytes); + ? opus.encoder.opus_encode_float( + _opusEncoder, + _inputBuffer.cast(), + sampleCountPerChannel, + _outputBuffer, + maxOutputBufferSizeBytes) + : opus.encoder.opus_encode(_opusEncoder, _inputBuffer.cast(), + sampleCountPerChannel, _outputBuffer, maxOutputBufferSizeBytes); if (_outputBufferIndex < opus_defines.OPUS_OK) { throw OpusException(_outputBufferIndex); } diff --git a/opus_dart/lib/wrappers/opus_encoder.dart b/opus_dart/lib/wrappers/opus_encoder.dart index bd2f88b..24369f4 100644 --- a/opus_dart/lib/wrappers/opus_encoder.dart +++ b/opus_dart/lib/wrappers/opus_encoder.dart @@ -215,16 +215,14 @@ class FunctionsAndGlobals implements OpusEncoderFunctions { try { return _lookup< ffi.NativeFunction< - ffi.Int Function( - ffi.Pointer, ffi.Int, ffi.Int)>>( - 'opus_encoder_ctl_int') + ffi.Int Function(ffi.Pointer, ffi.Int, + ffi.Int)>>('opus_encoder_ctl_int') .asFunction, int, int)>(); } catch (_) { return _lookup< ffi.NativeFunction< - ffi.Int Function( - ffi.Pointer, ffi.Int, ffi.Int)>>( - 'opus_encoder_ctl') + ffi.Int Function(ffi.Pointer, ffi.Int, + ffi.Int)>>('opus_encoder_ctl') .asFunction, int, int)>(); } } diff --git a/opus_dart/test/opus_dart_mock_test.dart b/opus_dart/test/opus_dart_mock_test.dart index 28dafdf..84fd954 100644 --- a/opus_dart/test/opus_dart_mock_test.dart +++ b/opus_dart/test/opus_dart_mock_test.dart @@ -156,7 +156,8 @@ void main() { }); test('maps Application.restrictedLowdelay correctly', () { - final encoder = createEncoder(application: Application.restrictedLowdelay); + final encoder = + createEncoder(application: Application.restrictedLowdelay); expect(encoder.application, Application.restrictedLowdelay); verify(mockEncoder.opus_encoder_create( any, any, OPUS_APPLICATION_RESTRICTED_LOWDELAY, any)) @@ -743,16 +744,11 @@ void main() { return OPUS_OK; }); - encoder.encoderCtl( - request: OPUS_SET_BITRATE_REQUEST, value: 64000); - encoder.encoderCtl( - request: OPUS_SET_COMPLEXITY_REQUEST, value: 10); - encoder.encoderCtl( - request: OPUS_SET_VBR_REQUEST, value: 1); - encoder.encoderCtl( - request: OPUS_SET_INBAND_FEC_REQUEST, value: 1); - encoder.encoderCtl( - request: OPUS_SET_PACKET_LOSS_PERC_REQUEST, value: 25); + encoder.encoderCtl(request: OPUS_SET_BITRATE_REQUEST, value: 64000); + encoder.encoderCtl(request: OPUS_SET_COMPLEXITY_REQUEST, value: 10); + encoder.encoderCtl(request: OPUS_SET_VBR_REQUEST, value: 1); + encoder.encoderCtl(request: OPUS_SET_INBAND_FEC_REQUEST, value: 1); + encoder.encoderCtl(request: OPUS_SET_PACKET_LOSS_PERC_REQUEST, value: 25); expect(captured, [ [OPUS_SET_BITRATE_REQUEST, 64000], @@ -809,8 +805,8 @@ void main() { final encoder = createBufferedEncoder(); encoder.destroy(); expect( - () => encoder.encoderCtl( - request: OPUS_SET_BITRATE_REQUEST, value: 64000), + () => + encoder.encoderCtl(request: OPUS_SET_BITRATE_REQUEST, value: 64000), throwsA(isA()), ); }); @@ -855,15 +851,15 @@ void main() { test('default output buffer is large enough for float decode', () { final decoder = createBufferedDecoder(sampleRate: 48000, channels: 2); // 4 bytes/float * maxSamplesPerPacket ensures a 120ms frame fits - expect(decoder.maxOutputBufferSizeBytes, - 4 * maxSamplesPerPacket(48000, 2)); + expect( + decoder.maxOutputBufferSizeBytes, 4 * maxSamplesPerPacket(48000, 2)); decoder.destroy(); }); test('default output buffer is large enough for float decode (mono)', () { final decoder = createBufferedDecoder(sampleRate: 8000, channels: 1); - expect(decoder.maxOutputBufferSizeBytes, - 4 * maxSamplesPerPacket(8000, 1)); + expect( + decoder.maxOutputBufferSizeBytes, 4 * maxSamplesPerPacket(8000, 1)); decoder.destroy(); }); @@ -1316,8 +1312,8 @@ void main() { ); expect( - () => pcmSoftClip( - input: Float32List.fromList([0.5, -0.5]), channels: 2), + () => + pcmSoftClip(input: Float32List.fromList([0.5, -0.5]), channels: 2), throwsStateError, ); expect(failAlloc.freedAddresses, hasLength(1));