diff --git a/lib/pods/call.dart b/lib/pods/call.dart new file mode 100644 index 0000000..4ff95ec --- /dev/null +++ b/lib/pods/call.dart @@ -0,0 +1,94 @@ +import 'package:flutter_webrtc/flutter_webrtc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:dio/dio.dart'; +import 'package:island/pods/network.dart'; + +part 'call.g.dart'; +part 'call.freezed.dart'; + +@freezed +sealed class CallState with _$CallState { + const factory CallState({ + required bool isMuted, + required bool isConnected, + String? error, + }) = _CallState; +} + +@riverpod +class CallNotifier extends _$CallNotifier { + RTCPeerConnection? _peerConnection; + MediaStream? _localStream; + final _localRenderer = RTCVideoRenderer(); + + @override + CallState build() { + return const CallState(isMuted: false, isConnected: false); + } + + Future initialize() async { + try { + await _localRenderer.initialize(); + + // Get user media (audio) + _localStream = await navigator.mediaDevices.getUserMedia({ + 'audio': true, + 'video': false, + }); + + // Create peer connection + _peerConnection = await createPeerConnection({ + 'iceServers': [ + {'urls': 'stun:stun.l.google.com:19302'}, + // Add your Cloudflare TURN servers here + ], + }); + + // Add local stream to peer connection + _localStream!.getTracks().forEach((track) { + _peerConnection!.addTrack(track, _localStream!); + }); + + // Handle incoming tracks + _peerConnection!.onTrack = (RTCTrackEvent event) { + if (event.track.kind == 'audio') { + // Handle remote audio track + } + }; + + state = state.copyWith(isConnected: true); + } catch (e) { + state = state.copyWith(error: e.toString()); + } + } + + Future createSession() async { + try { + final apiClient = ref.read(apiClientProvider); + final response = await apiClient.post( + 'YOUR_CLOUDFLARE_CALLS_ENDPOINT/sessions', + options: Options(headers: {'Content-Type': 'application/json'}), + ); + + if (response.statusCode == 200) { + // Handle session creation + } + } catch (e) { + state = state.copyWith(error: e.toString()); + } + } + + void toggleMute() { + state = state.copyWith(isMuted: !state.isMuted); + _localStream?.getAudioTracks().forEach((track) { + track.enabled = !state.isMuted; + }); + } + + void dispose() { + _localStream?.dispose(); + _peerConnection?.dispose(); + _localRenderer.dispose(); + } +} diff --git a/lib/pods/call.freezed.dart b/lib/pods/call.freezed.dart new file mode 100644 index 0000000..7e237ea --- /dev/null +++ b/lib/pods/call.freezed.dart @@ -0,0 +1,148 @@ +// dart format width=80 +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'call.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$CallState { + + bool get isMuted; bool get isConnected; String? get error; +/// Create a copy of CallState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$CallStateCopyWith get copyWith => _$CallStateCopyWithImpl(this as CallState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is CallState&&(identical(other.isMuted, isMuted) || other.isMuted == isMuted)&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.error, error) || other.error == error)); +} + + +@override +int get hashCode => Object.hash(runtimeType,isMuted,isConnected,error); + +@override +String toString() { + return 'CallState(isMuted: $isMuted, isConnected: $isConnected, error: $error)'; +} + + +} + +/// @nodoc +abstract mixin class $CallStateCopyWith<$Res> { + factory $CallStateCopyWith(CallState value, $Res Function(CallState) _then) = _$CallStateCopyWithImpl; +@useResult +$Res call({ + bool isMuted, bool isConnected, String? error +}); + + + + +} +/// @nodoc +class _$CallStateCopyWithImpl<$Res> + implements $CallStateCopyWith<$Res> { + _$CallStateCopyWithImpl(this._self, this._then); + + final CallState _self; + final $Res Function(CallState) _then; + +/// Create a copy of CallState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? isMuted = null,Object? isConnected = null,Object? error = freezed,}) { + return _then(_self.copyWith( +isMuted: null == isMuted ? _self.isMuted : isMuted // ignore: cast_nullable_to_non_nullable +as bool,isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable +as bool,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + +} + + +/// @nodoc + + +class _CallState implements CallState { + const _CallState({required this.isMuted, required this.isConnected, this.error}); + + +@override final bool isMuted; +@override final bool isConnected; +@override final String? error; + +/// Create a copy of CallState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$CallStateCopyWith<_CallState> get copyWith => __$CallStateCopyWithImpl<_CallState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _CallState&&(identical(other.isMuted, isMuted) || other.isMuted == isMuted)&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.error, error) || other.error == error)); +} + + +@override +int get hashCode => Object.hash(runtimeType,isMuted,isConnected,error); + +@override +String toString() { + return 'CallState(isMuted: $isMuted, isConnected: $isConnected, error: $error)'; +} + + +} + +/// @nodoc +abstract mixin class _$CallStateCopyWith<$Res> implements $CallStateCopyWith<$Res> { + factory _$CallStateCopyWith(_CallState value, $Res Function(_CallState) _then) = __$CallStateCopyWithImpl; +@override @useResult +$Res call({ + bool isMuted, bool isConnected, String? error +}); + + + + +} +/// @nodoc +class __$CallStateCopyWithImpl<$Res> + implements _$CallStateCopyWith<$Res> { + __$CallStateCopyWithImpl(this._self, this._then); + + final _CallState _self; + final $Res Function(_CallState) _then; + +/// Create a copy of CallState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? isMuted = null,Object? isConnected = null,Object? error = freezed,}) { + return _then(_CallState( +isMuted: null == isMuted ? _self.isMuted : isMuted // ignore: cast_nullable_to_non_nullable +as bool,isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable +as bool,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + + +} + +// dart format on diff --git a/lib/pods/call.g.dart b/lib/pods/call.g.dart new file mode 100644 index 0000000..baf747f --- /dev/null +++ b/lib/pods/call.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'call.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$callNotifierHash() => r'a910dfa778c42888f8b8e5ab199b25ce8a74392b'; + +/// See also [CallNotifier]. +@ProviderFor(CallNotifier) +final callNotifierProvider = + AutoDisposeNotifierProvider.internal( + CallNotifier.new, + name: r'callNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$callNotifierHash, + dependencies: null, + allTransitiveDependencies: null, + ); + +typedef _$CallNotifier = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/screens/chat/call.dart b/lib/screens/chat/call.dart new file mode 100644 index 0000000..7bc1141 --- /dev/null +++ b/lib/screens/chat/call.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/pods/call.dart'; + +class CallScreen extends HookConsumerWidget { + const CallScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final callState = ref.watch(callNotifierProvider); + final callNotifier = ref.read(callNotifierProvider.notifier); + + useEffect(() { + callNotifier.initialize(); + return null; + }, []); + + return Scaffold( + appBar: AppBar(title: const Text('Audio Call')), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (callState.error != null) + Text(callState.error!, style: const TextStyle(color: Colors.red)), + IconButton( + icon: Icon(callState.isMuted ? Icons.mic_off : Icons.mic), + onPressed: callNotifier.toggleMute, + ), + if (callState.isConnected) + const Text('Connected') + else + const CircularProgressIndicator(), + ], + ), + ), + ); + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index d55b720..084fe7c 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -10,12 +10,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) bitsdojo_window_linux_registrar = @@ -30,6 +32,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flutter_udid_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterUdidPlugin"); flutter_udid_plugin_register_with_registrar(flutter_udid_registrar); + g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin"); + flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar); g_autoptr(FlPluginRegistrar) irondash_engine_context_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "IrondashEngineContextPlugin"); irondash_engine_context_plugin_register_with_registrar(irondash_engine_context_registrar); @@ -48,4 +53,7 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) volume_controller_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "VolumeControllerPlugin"); + volume_controller_plugin_register_with_registrar(volume_controller_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index a3f64ab..e4e9336 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -7,12 +7,14 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux flutter_platform_alert flutter_udid + flutter_webrtc irondash_engine_context media_kit_libs_linux media_kit_video sqlite3_flutter_libs super_native_extensions url_launcher_linux + volume_controller ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 960c824..c06bb17 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -14,6 +14,7 @@ import firebase_messaging import flutter_inappwebview_macos import flutter_platform_alert import flutter_udid +import flutter_webrtc import irondash_engine_context import media_kit_libs_macos_video import media_kit_video @@ -37,6 +38,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) FlutterPlatformAlertPlugin.register(with: registry.registrar(forPlugin: "FlutterPlatformAlertPlugin")) FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin")) + FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 1fceac3..de5f888 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -18,7 +18,7 @@ packages: source: hosted version: "1.3.55" analyzer: - dependency: transitive + dependency: "direct overridden" description: name: analyzer sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" @@ -26,13 +26,13 @@ packages: source: hosted version: "7.3.0" analyzer_plugin: - dependency: transitive + dependency: "direct overridden" description: name: analyzer_plugin - sha256: b3075265c5ab222f8b3188342dcb50b476286394a40323e85d1fa725035d40a4 + sha256: "1d460d14e3c2ae36dc2b32cef847c4479198cf87704f63c3c3c8150ee50c3916" url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.12.0" animations: dependency: "direct main" description: @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" auto_route: dependency: "direct main" description: @@ -386,7 +386,7 @@ packages: source: hosted version: "0.7.5" custom_lint_visitor: - dependency: transitive + dependency: "direct overridden" description: name: custom_lint_visitor sha256: "36282d85714af494ee2d7da8c8913630aa6694da99f104fb2ed4afcf8fc857d8" @@ -401,6 +401,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" + dart_webrtc: + dependency: transitive + description: + name: dart_webrtc + sha256: "5b76fd85ac95d6f5dee3e7d7de8d4b51bfbec1dc73804647c6aebb52d6297116" + url: "https://pub.dev" + source: hosted + version: "1.5.3+hotfix.2" dbus: dependency: transitive description: @@ -525,10 +533,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -845,6 +853,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_webrtc: + dependency: "direct main" + description: + name: flutter_webrtc + sha256: dd47ca103b5b6217771e6277882674276d9621bbf9eb23da3c03898b507844e3 + url: "https://pub.dev" + source: hosted + version: "0.14.1" font_awesome_flutter: dependency: transitive description: @@ -1041,10 +1057,10 @@ packages: dependency: transitive description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" io: dependency: transitive description: @@ -1097,10 +1113,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -1734,10 +1750,10 @@ packages: dependency: transitive description: name: sqlite3 - sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e" + sha256: c0503c69b44d5714e6abbf4c1f51a3c3cc42b75ce785f44404765e4635481d38 url: "https://pub.dev" source: hosted - version: "2.7.5" + version: "2.7.6" sqlite3_flutter_libs: dependency: transitive description: @@ -1854,10 +1870,10 @@ packages: dependency: "direct main" description: name: table_calendar - sha256: b2896b7c86adf3a4d9c911d860120fe3dbe03c85db43b22fd61f14ee78cdbb63 + sha256: "0c0c6219878b363a2d5f40c7afb159d845f253d061dc3c822aa0d5fe0f721982" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.2.0" term_glyph: dependency: transitive description: @@ -2023,10 +2039,10 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" + sha256: "557a315b7d2a6dbb0aaaff84d857967ce6bdc96a63dc6ee2a57ce5a6ee5d3331" url: "https://pub.dev" source: hosted - version: "1.1.16" + version: "1.1.17" vector_math: dependency: transitive description: @@ -2055,18 +2071,18 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" volume_controller: dependency: transitive description: name: volume_controller - sha256: e82fd689bb8e1fe8e64be3fa5946ff8699058f8cf9f4c1679acdba20cda7f5bd + sha256: d75039e69c0d90e7810bfd47e3eedf29ff8543ea7a10392792e81f9bded7edf5 url: "https://pub.dev" source: hosted - version: "3.3.3" + version: "3.4.0" wakelock_plus: dependency: transitive description: @@ -2115,6 +2131,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + webrtc_interface: + dependency: transitive + description: + name: webrtc_interface + sha256: "86fe3afc81a08481dfb25cf14a5a94e27062ecef25544783f352c914e0bbc1ca" + url: "https://pub.dev" + source: hosted + version: "1.2.2+hotfix.2" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 332ec1d..cb23c46 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -101,6 +101,7 @@ dependencies: dismissible_page: ^1.0.2 super_sliver_list: ^0.4.1 super_clipboard: ^0.9.0-dev.6 + flutter_webrtc: ^0.14.1 dev_dependencies: flutter_test: @@ -122,6 +123,13 @@ dev_dependencies: drift_dev: ^2.26.0 flutter_launcher_icons: ^0.14.3 +dependency_overrides: + # https://github.com/dart-lang/sdk/issues/60784#issuecomment-2904784415 + analyzer: 7.3.0 + analyzer_plugin: 0.12.0 + # https://github.com/dart-lang/sdk/issues/60784#issuecomment-2906872272 + custom_lint_visitor: 1.0.0+7.3.0 + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index c0ff750..47678b9 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -33,6 +34,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FlutterPlatformAlertPlugin")); FlutterUdidPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterUdidPluginCApi")); + FlutterWebRTCPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterWebRTCPlugin")); IrondashEngineContextPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("IrondashEngineContextPluginCApi")); MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index ecdf13b..c298702 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_inappwebview_windows flutter_platform_alert flutter_udid + flutter_webrtc irondash_engine_context media_kit_libs_windows_video media_kit_video