diff --git a/lib/flutter_midi_command_web.dart b/lib/flutter_midi_command_web.dart new file mode 100644 index 00000000..81249b84 --- /dev/null +++ b/lib/flutter_midi_command_web.dart @@ -0,0 +1,229 @@ +import 'dart:async'; +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; +import 'dart:typed_data'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:flutter_midi_command_platform_interface/flutter_midi_command_platform_interface.dart'; +import 'package:web/web.dart' as web; + +@JS('Array.from') +external JSArray _jsArrayFrom(JSObject obj); + +class FlutterMidiCommandWeb extends MidiCommandPlatform { + static void registerWith(Registrar registrar) { + MidiCommandPlatform.instance = FlutterMidiCommandWeb(); + } + + web.MIDIAccess? _midiAccess; + final _rxStreamController = StreamController.broadcast(); + final _setupStreamController = StreamController.broadcast(); + final _bluetoothStateStreamController = StreamController.broadcast(); + + Future _initMidi() async { + if (_midiAccess != null) return; + try { + final promise = web.window.navigator.requestMIDIAccess(web.MIDIOptions(sysex: true)); + _midiAccess = await promise.toDart; + + _midiAccess?.onstatechange = (web.Event event) { + _setupStreamController.add("deviceFound"); + }.toJS; + } catch (e) { + print("Failed to initialize Web MIDI: $e"); + } + } + + List _getMapValues(JSObject mapObject) { + final valuesIterator = mapObject.callMethod('values'.toJS); + if (valuesIterator == null) return []; + final jsArray = _jsArrayFrom(valuesIterator as JSObject); + + final list = []; + for (int i = 0; i < jsArray.length; i++) { + final item = jsArray.getProperty(i.toJS); + if (item != null) { + list.add(item as T); + } + } + return list; + } + + @override + Future?> get devices async { + await _initMidi(); + final access = _midiAccess; + if (access == null) return []; + + final list = []; + + // Read inputs + final inputs = _getMapValues(access.inputs); + for (final input in inputs) { + final device = MidiDevice( + input.id, + input.name ?? 'Unknown Input', + 'web', + input.connection == 'open', + ); + device.inputPorts.add(MidiPort(0, MidiPortType.IN)); + list.add(device); + } + + // Read outputs + final outputs = _getMapValues(access.outputs); + for (final output in outputs) { + final device = MidiDevice( + output.id, + output.name ?? 'Unknown Output', + 'web', + output.connection == 'open', + ); + device.outputPorts.add(MidiPort(0, MidiPortType.OUT)); + list.add(device); + } + + return list; + } + + @override + Future connectToDevice(MidiDevice device, {List? ports}) async { + await _initMidi(); + final access = _midiAccess; + if (access == null) return; + + // Search in inputs + final inputs = _getMapValues(access.inputs); + final input = inputs.where((i) => i.id == device.id).firstOrNull; + if (input != null) { + await input.open().toDart; + + input.onmidimessage = (web.Event event) { + final messageEvent = event as web.MIDIMessageEvent; + final jsData = messageEvent.data; + if (jsData != null) { + final dartData = jsData.toDart; + final activeDevice = MidiDevice(device.id, device.name, device.type, true); + activeDevice.inputPorts.add(MidiPort(0, MidiPortType.IN)); + _rxStreamController.add(MidiPacket( + dartData, + messageEvent.timeStamp.toInt(), + activeDevice, + )); + } + }.toJS; + + device.connected = true; + _setupStreamController.add("deviceConnected"); + return; + } + + // Search in outputs + final outputs = _getMapValues(access.outputs); + final output = outputs.where((o) => o.id == device.id).firstOrNull; + if (output != null) { + await output.open().toDart; + device.connected = true; + _setupStreamController.add("deviceConnected"); + return; + } + } + + @override + void disconnectDevice(MidiDevice device) { + final access = _midiAccess; + if (access == null) return; + + final inputs = _getMapValues(access.inputs); + final input = inputs.where((i) => i.id == device.id).firstOrNull; + if (input != null) { + input.onmidimessage = null; + input.close(); + device.connected = false; + _setupStreamController.add("deviceDisconnected"); + return; + } + + final outputs = _getMapValues(access.outputs); + final output = outputs.where((o) => o.id == device.id).firstOrNull; + if (output != null) { + output.close(); + device.connected = false; + _setupStreamController.add("deviceDisconnected"); + return; + } + } + + @override + void sendData(Uint8List data, {int? timestamp, String? deviceId}) { + final access = _midiAccess; + if (access == null) return; + + final outputs = _getMapValues(access.outputs); + final JSArray jsData = data.map((b) => b.toJS).toList().toJS; + + if (deviceId != null) { + final output = outputs.where((o) => o.id == deviceId).firstOrNull; + if (output != null && output.connection == 'open') { + output.send(jsData); + } + } else { + for (final output in outputs) { + if (output.connection == 'open') { + output.send(jsData); + } + } + } + } + + @override + void teardown() { + final access = _midiAccess; + if (access == null) return; + + final inputs = _getMapValues(access.inputs); + for (final input in inputs) { + input.onmidimessage = null; + input.close(); + } + + final outputs = _getMapValues(access.outputs); + for (final output in outputs) { + output.close(); + } + } + + @override + Stream? get onMidiDataReceived => _rxStreamController.stream; + + @override + Stream? get onMidiSetupChanged => _setupStreamController.stream; + + @override + Stream? get onBluetoothStateChanged => _bluetoothStateStreamController.stream; + + @override + Future startBluetoothCentral() async { + _bluetoothStateStreamController.add("unsupported"); + } + + @override + Future bluetoothState() async => "unsupported"; + + @override + Future startScanningForBluetoothDevices() async {} + + @override + void stopScanningForBluetoothDevices() {} + + @override + void addVirtualDevice({String? name}) {} + + @override + void removeVirtualDevice({String? name}) {} + + @override + Future get isNetworkSessionEnabled async => false; + + @override + void setNetworkSessionEnabled(bool enabled) {} +} diff --git a/pubspec.yaml b/pubspec.yaml index 79b87fbf..1e130847 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,6 +14,9 @@ dependencies: flutter_midi_command_platform_interface: ^0.4.1 flutter_midi_command_linux: ^0.3.0 flutter_midi_command_windows: ^0.3.0 + flutter_web_plugins: + sdk: flutter + web: ^1.1.0 dev_dependencies: flutter_test: @@ -40,4 +43,7 @@ flutter: linux: default_package: flutter_midi_command_linux windows: - default_package: flutter_midi_command_windows \ No newline at end of file + default_package: flutter_midi_command_windows + web: + pluginClass: FlutterMidiCommandWeb + fileName: flutter_midi_command_web.dart \ No newline at end of file