diff --git a/CHANGELOG.md b/CHANGELOG.md index 123e597..81b688a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## 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 | +|---------|---------| +| 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/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/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/ffi-analysis.md b/docs/ffi-analysis.md new file mode 100644 index 0000000..fdebfab --- /dev/null +++ b/docs/ffi-analysis.md @@ -0,0 +1,853 @@ +# 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(); + 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`. +- (**Fixed** — previously `OpusRepacketizer` was registered twice and `OpusCustomMode` + was missing. Now all 10 opaque types are registered exactly once.) + +### 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)` | No longer called | No longer called (fixed) | + +--- + +## 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~~ **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.~~ +**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`) + +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. + +(**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); +Pointer? outputNative; +try { + inputNative.asTypedList(input.length).setAll(0, input); + outputNative = opus.allocator.call(maxOutputSizeBytes); + int outputLength = opus.encoder.opus_encode(...); + // ... +} finally { + if (outputNative != null) opus.allocator.free(outputNative); + opus.allocator.free(inputNative); +} +``` + +#### 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) +``` + +(**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 +the `throw` path inside the `try` block only runs when `error.value != OPUS_OK`. + +### 5.3 Pointer Lifetime in Streaming (`opus_dart_streaming.dart`) + +(**Fixed** — output is now always copied to the Dart heap regardless of `copyOutput`.) + +`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`) + +(**Fixed** — `_asString` now has a `maxStringLength` (256) guard.) + +```dart +String _asString(Pointer pointer) { + int 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, 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 well within the 256-byte limit. + +### 5.5 `nullptr` Usage + +`nullptr` is used in one context: + +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). + +(**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.) + +--- + +## 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); + } +} +``` + +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()`.) + +--- + +## 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 + +(**Fixed** — both issues below have been corrected.) + +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)`. + +`StreamOpusDecoder` previously computed +`(floats ? 2 : 4) * maxSamplesPerPacket(...)` — inverted multipliers that allocated +less space for float (which needs more). Fixed to `(floats ? 4 : 2)`. + +--- + +## 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: + +```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. + +**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. +- **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~~ **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~~ **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~~ **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~~ **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. | + +--- + +## 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 | | +| `OpusCustomMode` | Yes | | + +None of the opaque types have type arguments, which satisfies that constraint. + +**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 + +**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_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, +_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` / `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) | +| `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) | + +(**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.) + +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 — **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 +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:~~ +~~`int Function(Pointer, int, int)`~~ + +**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. +The fallback in `_resolveEncoderCtl` preserves this existing behavior. + +### 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:~~ + +**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); +``` + +~~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 + +**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 | Yes | (**Fixed** — `_opus_encoder_ctl` was missing, now exported.) | + +**Verdict:** Compliant. Build configuration is correct and all used C functions are +exported. + +--- + +## 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~~ **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~~ **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**~~ | ~~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. | + +--- + +## 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**~~ **Fixed** | `_opus_encoder_ctl` added to `EXPORTED_FUNCTIONS` in Dockerfile | +| 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**~~ **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 | +| 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**~~ **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**~~ **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/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/CHANGELOG.md b/opus_dart/CHANGELOG.md new file mode 100644 index 0000000..44f04bb --- /dev/null +++ b/opus_dart/CHANGELOG.md @@ -0,0 +1,84 @@ +## 3.0.5 + +### 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 + +* 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/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/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/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 f97738f..c883e92 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. /// @@ -18,24 +38,27 @@ 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); } } /// 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()); + final Pointer _opusDecoder; @override final int sampleRate; @@ -55,23 +78,61 @@ 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. 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); } } @@ -87,41 +148,31 @@ 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. /// /// The input bytes need to represent a whole packet! @override Int16List decode({Uint8List? input, bool fec = false, int? loss}) { - 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); + if (_destroyed) throw OpusDestroyedError.decoder(); + final outputNative = opus.allocator.call(_maxSamplesPerPacket); try { - 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 { - opus.allocator.free(inputNative); opus.allocator.free(outputNative); } } @@ -141,26 +192,17 @@ class SimpleOpusDecoder extends OpusDecoder { bool fec = false, bool autoSoftClip = false, int? loss}) { - 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); + if (_destroyed) throw OpusDestroyedError.decoder(); + final outputNative = opus.allocator.call(_maxSamplesPerPacket); try { - 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); @@ -168,7 +210,6 @@ class SimpleOpusDecoder extends OpusDecoder { return Float32List.fromList( outputNative.asTypedList(outputSamplesPerChannel * channels)); } finally { - opus.allocator.free(inputNative); opus.allocator.free(outputNative); } } @@ -179,6 +220,7 @@ class SimpleOpusDecoder extends OpusDecoder { _destroyed = true; opus.decoder.opus_decoder_destroy(_opusDecoder); opus.allocator.free(_softClipBuffer); + _finalizer.detach(this); } } } @@ -188,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; @@ -210,6 +252,8 @@ class SimpleOpusDecoder extends OpusDecoder { /// } /// ``` class BufferedOpusDecoder extends OpusDecoder { + static final _finalizer = Finalizer((cleanup) => cleanup()); + final Pointer _opusDecoder; @override final int sampleRate; @@ -223,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; @@ -241,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; @@ -253,17 +297,24 @@ 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 ~/ bytesPerInt16Sample)); /// Convenience method to get the current output buffer as floats. + /// Returns a copy safe across WASM memory growth. Float32List get outputBufferAsFloat32List => - _outputBuffer.cast().asTypedList(_outputBufferIndex ~/ 4); + Float32List.fromList(_outputBuffer + .cast() + .asTypedList(_outputBufferIndex ~/ bytesPerFloatSample)); final Pointer _softClipBuffer; @@ -278,17 +329,28 @@ 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]. /// /// 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 @@ -306,23 +368,18 @@ class BufferedOpusDecoder extends OpusDecoder { int? maxInputBufferSizeBytes, int? maxOutputBufferSizeBytes}) { maxInputBufferSizeBytes ??= maxDataBytes; - maxOutputBufferSizeBytes ??= 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, @@ -330,11 +387,48 @@ 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. @@ -353,7 +447,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. /// @@ -362,28 +456,7 @@ class BufferedOpusDecoder extends OpusDecoder { /// The returned list is actually just the [outputBufferAsInt16List]. @override Int16List decode({bool fec = false, int? loss}) { - 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; } @@ -391,34 +464,13 @@ 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 Float32List decodeFloat( {bool autoSoftClip = false, bool fec = false, int? loss}) { - 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(); } @@ -433,6 +485,7 @@ class BufferedOpusDecoder extends OpusDecoder { opus.allocator.free(_inputBuffer); opus.allocator.free(_outputBuffer); opus.allocator.free(_softClipBuffer); + _finalizer.detach(this); } } @@ -440,8 +493,12 @@ class BufferedOpusDecoder extends OpusDecoder { /// /// Behaves like the toplevel [pcmSoftClip] function, but without unnecessary copying. Float32List pcmSoftClipOutputBuffer() { - opus.decoder.opus_pcm_soft_clip(_outputBuffer.cast(), - _outputBufferIndex ~/ (4 * channels), channels, _softClipBuffer); + if (_destroyed) throw OpusDestroyedError.decoder(); + opus.decoder.opus_pcm_soft_clip( + _outputBuffer.cast(), + _outputBufferIndex ~/ (bytesPerFloatSample * channels), + channels, + _softClipBuffer); return outputBufferAsFloat32List; } } @@ -455,7 +512,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; @@ -476,5 +533,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 0c854c9..8fe7c47 100644 --- a/opus_dart/lib/src/opus_dart_encoder.dart +++ b/opus_dart/lib/src/opus_dart_encoder.dart @@ -4,12 +4,35 @@ 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. /// -/// 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()); + final Pointer _opusEncoder; @override final int sampleRate; @@ -23,7 +46,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. @@ -31,17 +59,33 @@ 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); } } @@ -54,29 +98,26 @@ 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( {required Int16List input, int maxOutputSizeBytes = maxDataBytes}) { - 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); + if (_destroyed) throw OpusDestroyedError.encoder(); + final inputNative = opus.allocator.call(input.length); try { - if (outputLength < opus_defines.OPUS_OK) { - throw OpusException(outputLength); - } - return Uint8List.fromList(outputNative.asTypedList(outputLength)); + inputNative.asTypedList(input.length).setAll(0, input); + return _doEncode( + inputSampleCount: input.length, + maxOutputSizeBytes: maxOutputSizeBytes, + nativeEncode: (spc, output) => opus.encoder.opus_encode( + _opusEncoder, inputNative, spc, output, maxOutputSizeBytes), + ); } finally { opus.allocator.free(inputNative); - opus.allocator.free(outputNative); } } @@ -85,21 +126,18 @@ class SimpleOpusEncoder extends OpusEncoder { /// This method behaves just as [encode], so see there for more information. Uint8List encodeFloat( {required Float32List input, int maxOutputSizeBytes = maxDataBytes}) { - 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); + if (_destroyed) throw OpusDestroyedError.encoder(); + final inputNative = opus.allocator.call(input.length); try { - if (outputLength < opus_defines.OPUS_OK) { - throw OpusException(outputLength); - } - return Uint8List.fromList(outputNative.asTypedList(outputLength)); + inputNative.asTypedList(input.length).setAll(0, input); + return _doEncode( + inputSampleCount: input.length, + maxOutputSizeBytes: maxOutputSizeBytes, + nativeEncode: (spc, output) => opus.encoder.opus_encode_float( + _opusEncoder, inputNative, spc, output, maxOutputSizeBytes), + ); } finally { opus.allocator.free(inputNative); - opus.allocator.free(outputNative); } } @@ -108,6 +146,7 @@ class SimpleOpusEncoder extends OpusEncoder { if (!_destroyed) { _destroyed = true; opus.encoder.opus_encoder_destroy(_opusEncoder); + _finalizer.detach(this); } } } @@ -117,7 +156,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. @@ -142,6 +181,8 @@ class SimpleOpusEncoder extends OpusEncoder { /// } /// ``` class BufferedOpusEncoder extends OpusEncoder { + static final _finalizer = Finalizer((cleanup) => cleanup()); + final Pointer _opusEncoder; @override final int sampleRate; @@ -154,7 +195,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]. @@ -172,8 +213,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; @@ -182,9 +223,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, @@ -197,17 +240,26 @@ 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]. /// - /// 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 + /// [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. @@ -217,32 +269,52 @@ 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; } } int encoderCtl({required int request, required int value}) { + if (_destroyed) throw OpusDestroyedError.encoder(); 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. @@ -254,39 +326,15 @@ class BufferedOpusEncoder extends OpusEncoder { /// `sampleCount = 2 * 48000Hz * 0.02s = 1920`. /// /// The returned list is actually just the [outputBuffer]. - Uint8List encode() { - 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, - /// 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. - Uint8List encodeFloat() { - 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() { @@ -295,6 +343,7 @@ class BufferedOpusEncoder extends OpusEncoder { opus.encoder.opus_encoder_destroy(_opusEncoder); opus.allocator.free(_inputBuffer); opus.allocator.free(_outputBuffer); + _finalizer.detach(this); } } } @@ -312,7 +361,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; @@ -321,13 +370,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/lib/src/opus_dart_misc.dart b/opus_dart/lib/src/opus_dart_misc.dart index 5b8ae2f..34735f5 100644 --- a/opus_dart/lib/src/opus_dart_misc.dart +++ b/opus_dart/lib/src/opus_dart_misc.dart @@ -6,28 +6,41 @@ 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) /// for an explanation how this was calculated. +/// Last validated on 2026-02-25 against RFC 6716. 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) => ((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. +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)); } @@ -42,7 +55,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( 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)); } } diff --git a/opus_dart/lib/src/opus_dart_streaming.dart b/opus_dart/lib/src/opus_dart_streaming.dart index 5d6b6fe..b909fea 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. @@ -118,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 bytes = - floats ? _encoder.encodeFloat() : _encoder.encode(); - yield copyOutput ? Uint8List.fromList(bytes) : bytes; - _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 bytes = floats ? _encoder.encodeFloat() : _encoder.encode(); - yield copyOutput ? Uint8List.fromList(bytes) : bytes; + 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(); } @@ -204,7 +252,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. @@ -271,7 +322,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); @@ -286,10 +337,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/lib/wrappers/opus_encoder.dart b/opus_dart/lib/wrappers/opus_encoder.dart index ceb46bc..24369f4 100644 --- a/opus_dart/lib/wrappers/opus_encoder.dart +++ b/opus_dart/lib/wrappers/opus_encoder.dart @@ -209,10 +209,21 @@ 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/pubspec.yaml b/opus_dart/pubspec.yaml index 16edb11..c4b4baa 100644 --- a/opus_dart/pubspec.yaml +++ b/opus_dart/pubspec.yaml @@ -1,8 +1,9 @@ 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. +repository: https://github.com/Corkscrews/opus_flutter/tree/master/opus_dart environment: sdk: '>=3.4.0 <4.0.0' diff --git a/opus_dart/test/opus_dart_mock_test.dart b/opus_dart/test/opus_dart_mock_test.dart index d1fd49c..84fd954 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, @@ -132,9 +155,10 @@ 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); @@ -248,6 +272,60 @@ 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()), + ); + }); + + 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)); + }); }); // --------------------------------------------------------------------------- @@ -499,6 +577,58 @@ 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()), + ); + }); + + 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)); + }); }); // --------------------------------------------------------------------------- @@ -602,6 +732,46 @@ 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(); @@ -617,6 +787,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) { @@ -654,6 +848,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; @@ -951,6 +1174,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; @@ -985,6 +1229,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', () { @@ -1022,6 +1301,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)); + }); }); // --------------------------------------------------------------------------- diff --git a/opus_dart/test/opus_dart_streaming_mock_test.dart b/opus_dart/test/opus_dart_streaming_mock_test.dart index f217306..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]); }); }); @@ -835,6 +832,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]); @@ -921,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]); @@ -936,6 +958,7 @@ void main() { expect(results, hasLength(1)); expect(results[0], isA()); + expect(results[0], [0.5]); }); }); 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); }); diff --git a/opus_flutter/.gitignore b/opus_flutter/.gitignore index 551d074..446f5ff 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/CHANGELOG.md b/opus_flutter/CHANGELOG.md index 17fa8c2..44f04bb 100644 --- a/opus_flutter/CHANGELOG.md +++ b/opus_flutter/CHANGELOG.md @@ -1,55 +1,84 @@ -## 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 +## 3.0.5 + +### 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 + +* 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/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/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/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..44f04bb 100644 --- a/opus_flutter_android/CHANGELOG.md +++ b/opus_flutter_android/CHANGELOG.md @@ -1,8 +1,84 @@ -## 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 +## 3.0.5 + +### 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 + +* 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_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_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/.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/CHANGELOG.md b/opus_flutter_ios/CHANGELOG.md index 6a897af..44f04bb 100644 --- a/opus_flutter_ios/CHANGELOG.md +++ b/opus_flutter_ios/CHANGELOG.md @@ -1,13 +1,84 @@ -## 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 +## 3.0.5 + +### 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 + +* 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_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_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_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..44f04bb 100644 --- a/opus_flutter_linux/CHANGELOG.md +++ b/opus_flutter_linux/CHANGELOG.md @@ -1,3 +1,84 @@ -## 3.0.0 - -* Initial release. Loads opus from the system library (`libopus.so.0`). +## 3.0.5 + +### 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 + +* 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_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..44f04bb 100644 --- a/opus_flutter_macos/CHANGELOG.md +++ b/opus_flutter_macos/CHANGELOG.md @@ -1,4 +1,84 @@ -## 3.0.0 - -* Initial release with macOS support using opus.xcframework -* Adopt `opus_flutter_platform_interface 3.0.0` +## 3.0.5 + +### 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 + +* 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/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_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/.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/CHANGELOG.md b/opus_flutter_platform_interface/CHANGELOG.md index e620627..44f04bb 100644 --- a/opus_flutter_platform_interface/CHANGELOG.md +++ b/opus_flutter_platform_interface/CHANGELOG.md @@ -1,5 +1,84 @@ -## 3.0.0 -* Necessary changes for web support - -## 2.0.0 -* Initial release in federal plugin structure \ No newline at end of file +## 3.0.5 + +### 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 + +* 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/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.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_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/.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/CHANGELOG.md b/opus_flutter_web/CHANGELOG.md index 51adf28..fb74a4a 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 all package versions + +## 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/Dockerfile b/opus_flutter_web/Dockerfile index 4d4af7b..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,6 +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_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", \ @@ -31,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/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/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); +} 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_web/pubspec.yaml b/opus_flutter_web/pubspec.yaml index 0631a61..c992c87 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' @@ -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: 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/CHANGELOG.md b/opus_flutter_windows/CHANGELOG.md index caa66b0..44f04bb 100644 --- a/opus_flutter_windows/CHANGELOG.md +++ b/opus_flutter_windows/CHANGELOG.md @@ -1,5 +1,84 @@ -## 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 +## 3.0.5 + +### 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 + +* 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_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" 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' 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}"