From 56aeacc291af3e6ab8a32128263798e5734abc03 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 18 Jan 2022 16:16:05 +0700 Subject: [PATCH] Capture client info (#71) --- example/ios/Podfile.lock | 6 ++ .../Flutter/GeneratedPluginRegistrant.swift | 2 + example/macos/Podfile.lock | 6 ++ example/pubspec.lock | 56 +++++++++++ lib/livekit_client_web.dart | 20 ++-- lib/src/core/signal_client.dart | 8 +- lib/src/support/native.dart | 40 ++++++++ lib/src/support/native_audio.dart | 21 ----- lib/src/support/platform/io.dart | 3 +- lib/src/track/audio_management.dart | 3 +- lib/src/utils.dart | 94 ++++++++++++++++++- pubspec.lock | 68 ++++++++++++-- pubspec.yaml | 2 + shared_swift/LiveKitPlugin.swift | 18 ++++ 14 files changed, 306 insertions(+), 41 deletions(-) create mode 100644 lib/src/support/native.dart diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 4493f1c..fa07d7c 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,4 +1,6 @@ PODS: + - device_info_plus (0.0.1): + - Flutter - Flutter (1.0.0) - flutter_webrtc (0.7.1): - Flutter @@ -15,6 +17,7 @@ PODS: - WebRTC-SDK (93.4577.01) DEPENDENCIES: + - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - Flutter (from `Flutter`) - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) - livekit_client (from `.symlinks/plugins/livekit_client/ios`) @@ -27,6 +30,8 @@ SPEC REPOS: - WebRTC-SDK EXTERNAL SOURCES: + device_info_plus: + :path: ".symlinks/plugins/device_info_plus/ios" Flutter: :path: Flutter flutter_webrtc: @@ -39,6 +44,7 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/shared_preferences_ios/ios" SPEC CHECKSUMS: + device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a flutter_webrtc: 611cab01a48b1d20e2b2409783c182134b8729e5 Libyuv: 5f79ced0ee66e60a612ca97de1e6ccacd187a437 diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift index d965146..8310140 100644 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,12 +5,14 @@ import FlutterMacOS import Foundation +import device_info_plus_macos import flutter_webrtc import livekit_client import path_provider_macos import shared_preferences_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock index d181f9f..0494b6a 100644 --- a/example/macos/Podfile.lock +++ b/example/macos/Podfile.lock @@ -1,4 +1,6 @@ PODS: + - device_info_plus_macos (0.0.1): + - FlutterMacOS - flutter_webrtc (0.7.1): - FlutterMacOS - WebRTC-SDK (= 93.4577.01) @@ -13,6 +15,7 @@ PODS: - WebRTC-SDK (93.4577.01) DEPENDENCIES: + - device_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus_macos/macos`) - flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`) @@ -24,6 +27,8 @@ SPEC REPOS: - WebRTC-SDK EXTERNAL SOURCES: + device_info_plus_macos: + :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus_macos/macos flutter_webrtc: :path: Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos FlutterMacOS: @@ -36,6 +41,7 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos SPEC CHECKSUMS: + device_info_plus_macos: 1ad388a1ef433505c4038e7dd9605aadd1e2e9c7 flutter_webrtc: b5a65d35686e113195a2da282dfe11fa827c5e6b FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 livekit_client: d6920cc9a77586581b1109dd57a3b2b39d630e49 diff --git a/example/pubspec.lock b/example/pubspec.lock index 44bb08f..75e84ff 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -57,6 +57,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.3" + device_info_plus: + dependency: transitive + description: + name: device_info_plus + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.1" + device_info_plus_linux: + dependency: transitive + description: + name: device_info_plus_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + device_info_plus_macos: + dependency: transitive + description: + name: device_info_plus_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.1" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" + device_info_plus_web: + dependency: transitive + description: + name: device_info_plus_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + device_info_plus_windows: + dependency: transitive + description: + name: device_info_plus_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" eva_icons_flutter: dependency: "direct main" description: @@ -296,6 +338,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.1.0" + platform_detect: + dependency: transitive + description: + name: platform_detect + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" plugin_platform_interface: dependency: transitive description: @@ -324,6 +373,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.0.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" shared_preferences: dependency: "direct main" description: diff --git a/lib/livekit_client_web.dart b/lib/livekit_client_web.dart index c0ae194..31ceac4 100644 --- a/lib/livekit_client_web.dart +++ b/lib/livekit_client_web.dart @@ -1,14 +1,13 @@ import 'dart:async'; - -import 'package:flutter/services.dart'; - -import 'package:flutter_web_plugins/flutter_web_plugins.dart'; - // In order to *not* need this ignore, consider extracting the "web" version // of your plugin as a separate package, instead of inlining it in the same // package as the core of your plugin. -// ignore: avoid_web_libraries_in_flutter -// import 'dart:html' as html show window; + +// ignore: unused_import +import 'dart:html' as html show document, ScriptElement; + +import 'package:flutter/services.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; /// A web implementation of the Livekit plugin. class LiveKitWebPlugin { @@ -21,6 +20,13 @@ class LiveKitWebPlugin { final pluginInstance = LiveKitWebPlugin(); channel.setMethodCallHandler(pluginInstance.handleMethodCall); + + // Unofficial method load js as flutter assets (unreliable) + // html.document.head!.append(html.ScriptElement() + // ..src = 'assets/packages/livekit_client/assets/ua-parser.min.js' + // ..type = 'application/javascript' + // ..defer = true + // ); } /// Handles method calls over the MethodChannel of this plugin. diff --git a/lib/src/core/signal_client.dart b/lib/src/core/signal_client.dart index 68ae7f1..fce0608 100644 --- a/lib/src/core/signal_client.dart +++ b/lib/src/core/signal_client.dart @@ -39,12 +39,14 @@ class SignalClient extends Disposable with EventsEmittable { String token, { ConnectOptions? connectOptions, }) async { - final rtcUri = Utils.buildUri( + final rtcUri = await Utils.buildUri( uriString, token: token, connectOptions: connectOptions, ); + logger.fine('SignalClient connecting with url: $rtcUri'); + try { _ws = await LiveKitWebSocket.connect( rtcUri, @@ -56,7 +58,7 @@ class SignalClient extends Disposable with EventsEmittable { ); } catch (socketError) { // Re-build same uri for validate mode - final validateUri = Utils.buildUri( + final validateUri = await Utils.buildUri( uriString, token: token, connectOptions: connectOptions, @@ -90,7 +92,7 @@ class SignalClient extends Disposable with EventsEmittable { await _ws?.dispose(); _ws = null; - final rtcUri = Utils.buildUri( + final rtcUri = await Utils.buildUri( uriString, token: token, reconnect: true, diff --git a/lib/src/support/native.dart b/lib/src/support/native.dart new file mode 100644 index 0000000..1704754 --- /dev/null +++ b/lib/src/support/native.dart @@ -0,0 +1,40 @@ +import 'package:flutter/services.dart'; +import 'package:meta/meta.dart'; + +import '../logger.dart'; +import 'native_audio.dart'; + +// Method channel methods to call native code. +class Native { + @internal + static const channel = MethodChannel('livekit_client'); + + @internal + static Future configureAudio( + NativeAudioConfiguration configuration) async { + try { + final result = await channel.invokeMethod( + 'configureNativeAudio', + configuration.toMap(), + ); + return result == true; + } catch (error) { + logger.warning('configureNativeAudio did throw $error'); + return false; + } + } + + /// Returns OS's version as a string + /// Currently only for iOS, macOS + @internal + static Future osVersionString() async { + try { + return await channel.invokeMethod( + 'osVersionString', + {}, + ); + } catch (error) { + logger.warning('appleOSVersionString did throw error: ${error}'); + } + } +} diff --git a/lib/src/support/native_audio.dart b/lib/src/support/native_audio.dart index 5082dfa..0e41a50 100644 --- a/lib/src/support/native_audio.dart +++ b/lib/src/support/native_audio.dart @@ -1,9 +1,4 @@ // https://developer.apple.com/documentation/avfaudio/avaudiosession/category - -import 'package:flutter/services.dart'; - -import '../logger.dart'; - enum AppleAudioCategory { soloAmbient, playback, @@ -109,19 +104,3 @@ class NativeAudioConfiguration { appleAudioMode: appleAudioMode ?? this.appleAudioMode, ); } - -const _lkMethodChannel = MethodChannel('livekit_client'); - -Future configureNativeAudio( - NativeAudioConfiguration configuration) async { - try { - final result = await _lkMethodChannel.invokeMethod( - 'configureNativeAudio', - configuration.toMap(), - ); - return result == true; - } catch (_) { - logger.warning('configureAudioSession did throw $_'); - return false; - } -} diff --git a/lib/src/support/platform/io.dart b/lib/src/support/platform/io.dart index 769b776..2c13a92 100644 --- a/lib/src/support/platform/io.dart +++ b/lib/src/support/platform/io.dart @@ -8,5 +8,6 @@ PlatformType lkPlatformImplementation() { if (Platform.isMacOS) return PlatformType.macOS; if (Platform.isLinux) return PlatformType.linux; if (Platform.isIOS) return PlatformType.iOS; - return PlatformType.android; + if (Platform.isAndroid) return PlatformType.android; + throw UnsupportedError('Unknown Platform'); } diff --git a/lib/src/track/audio_management.dart b/lib/src/track/audio_management.dart index bd9b0be..a2d1da4 100644 --- a/lib/src/track/audio_management.dart +++ b/lib/src/track/audio_management.dart @@ -1,6 +1,7 @@ import 'package:synchronized/synchronized.dart' as sync; import '../logger.dart'; +import '../support/native.dart'; import '../support/native_audio.dart'; import '../support/platform.dart'; import 'local/audio.dart'; @@ -83,7 +84,7 @@ mixin AudioManagementMixin on AudioTrack { logger.fine( '[$runtimeType] configuring for ${audioTrackState} using ${config}...'); try { - await configureNativeAudio(config); + await Native.configureAudio(config); } catch (error) { logger.warning('[$runtimeType] Failed to configure ${error}'); } diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 1ccb407..a7c7007 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -1,30 +1,103 @@ import 'dart:async'; import 'package:collection/collection.dart'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; +import 'package:meta/meta.dart'; +import 'package:platform_detect/platform_detect.dart' as pd; import './proto/livekit_models.pb.dart' as lk_models; +import './support/native.dart'; import 'extensions.dart'; import 'livekit.dart'; import 'logger.dart'; import 'options.dart'; +import 'support/platform.dart'; import 'track/options.dart'; import 'types.dart'; extension UriExt on Uri { + @internal bool get isSecureScheme => ['https', 'wss'].contains(scheme); } // Collection of state-less static methods class Utils { - static Uri buildUri( + // DeviceInfoPlugin caches internally + static final _deviceInfoPlugin = DeviceInfoPlugin(); + + static Future _clientInfo() async { + switch (lkPlatform()) { + case PlatformType.web: + return lk_models.ClientInfo( + os: pd.operatingSystem.name.toLowerCase(), + browser: pd.browser.name.toLowerCase(), + browserVersion: pd.browser.version.canonicalizedVersion, + ); + case PlatformType.windows: + return lk_models.ClientInfo( + os: 'windows', + + /// [WindowsDeviceInfo] does not provide details... + ); + + case PlatformType.macOS: + final info = await _deviceInfoPlugin.macOsInfo; + + /// [MacOsDeviceInfo.osRelease] returns Darwin version instead of macOS version + /// So call native code to get os version + String? osVersionString = await Native.osVersionString(); + + return lk_models.ClientInfo( + os: 'macOS', + osVersion: osVersionString, + // Confirmed + deviceModel: info.model, + ); + + case PlatformType.android: + final info = await _deviceInfoPlugin.androidInfo; + return lk_models.ClientInfo( + os: 'android', + osVersion: info.version.release, + deviceModel: info.model, + ); + + case PlatformType.iOS: + final info = await _deviceInfoPlugin.iosInfo; + String? model = info.utsname.machine; + if (model != null && ['i386', 'x86_64', 'arm64'].contains(model)) { + model = 'iOSSimulator,${model}'; + } + return lk_models.ClientInfo( + os: 'iOS', + // Confirmed + osVersion: info.systemVersion, + deviceModel: model, + ); + + case PlatformType.linux: + final info = await _deviceInfoPlugin.linuxInfo; + return lk_models.ClientInfo( + os: 'linux', + osVersion: info.versionId, + deviceModel: info.machineId, + ); + + default: + // case PlatformType.fuchsia: + } + } + + @internal + static Future buildUri( String uriString, { required String token, ConnectOptions? connectOptions, bool reconnect = false, bool validate = false, bool forceSecure = false, - }) { + }) async { connectOptions ??= const ConnectOptions(); final Uri uri = Uri.parse(uriString); @@ -44,6 +117,8 @@ class Utils { } pathSegments.add(lastSegment); + final clientInfo = await _clientInfo(); + return uri.replace( scheme: validate ? httpScheme : wsScheme, pathSegments: pathSegments, @@ -54,6 +129,16 @@ class Utils { 'protocol': connectOptions.protocolVersion.toStringValue(), 'sdk': 'flutter', 'version': LiveKitClient.version, + // client info + if (clientInfo != null) ...{ + if (clientInfo.hasOs()) 'os': clientInfo.os, + if (clientInfo.hasOsVersion()) 'os_version': clientInfo.osVersion, + if (clientInfo.hasDeviceModel()) + 'device_model': clientInfo.deviceModel, + if (clientInfo.hasBrowser()) 'browser': clientInfo.browser, + if (clientInfo.hasBrowserVersion()) + 'browser_version': clientInfo.browserVersion, + }, }, ); } @@ -94,6 +179,7 @@ class Utils { static final videoRids = ['q', 'h', 'f']; + @internal static List encodingsFromPresets( VideoDimensions dimensions, { required List presets, @@ -113,6 +199,7 @@ class Utils { return result; } + @internal static List? computeVideoEncodings({ required bool isScreenShare, VideoDimensions? dimensions, @@ -173,6 +260,7 @@ class Utils { ); } + @internal static List computeVideoLayers( VideoDimensions dimensions, List? encodings, @@ -204,6 +292,7 @@ class Utils { }).toList(); } + @internal static lk_models.VideoQuality? videoQualityForRid(String? rid) => { 'f': lk_models.VideoQuality.HIGH, 'h': lk_models.VideoQuality.MEDIUM, @@ -211,6 +300,7 @@ class Utils { }[rid]; // makes a debounce func, with 1 param + @internal static Function(T) createDebounceFunc( Function(T) f, { Function(Function)? cancelFunc, diff --git a/pubspec.lock b/pubspec.lock index f4ecbd7..1cdf0b0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -64,6 +64,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.3" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.1" + device_info_plus_linux: + dependency: transitive + description: + name: device_info_plus_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + device_info_plus_macos: + dependency: transitive + description: + name: device_info_plus_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.1" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0+1" + device_info_plus_web: + dependency: transitive + description: + name: device_info_plus_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + device_info_plus_windows: + dependency: transitive + description: + name: device_info_plus_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" fake_async: dependency: transitive description: @@ -211,28 +253,28 @@ packages: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "2.1.4" + version: "2.1.5" path_provider_macos: dependency: transitive description: name: path_provider_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.5" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.3" path_provider_windows: dependency: transitive description: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.5" platform: dependency: transitive description: @@ -240,13 +282,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.1.0" + platform_detect: + dependency: "direct main" + description: + name: platform_detect + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.1.2" process: dependency: transitive description: @@ -261,6 +310,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" sky_engine: dependency: transitive description: flutter @@ -356,7 +412,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.3.3" + version: "2.3.4" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 508ca8a..6f8924c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,6 +24,8 @@ dependencies: protobuf: ^2.0.1 flutter_webrtc: ^0.8.1 dart_webrtc: ^1.0.3 + device_info_plus: ^3.2.1 + platform_detect: ^2.0.3 dev_dependencies: flutter_test: diff --git a/shared_swift/LiveKitPlugin.swift b/shared_swift/LiveKitPlugin.swift index faf42a3..59b449b 100644 --- a/shared_swift/LiveKitPlugin.swift +++ b/shared_swift/LiveKitPlugin.swift @@ -140,6 +140,22 @@ public class LiveKitPlugin: NSObject, FlutterPlugin { #endif } + private static let processInfo = ProcessInfo() + + /// Returns os version as a string. + /// format: `12.1`, `15.3.1`, `15.0.1` + private static func osVersionString() -> String { + let osVersion = processInfo.operatingSystemVersion + var versions = [osVersion.majorVersion] + if osVersion.minorVersion != 0 || osVersion.patchVersion != 0 { + versions.append(osVersion.minorVersion) + } + if osVersion.patchVersion != 0 { + versions.append(osVersion.patchVersion) + } + return versions.map({ String($0) }).joined(separator: ".") + } + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { guard let args = call.arguments as? [String: Any?] else { @@ -151,6 +167,8 @@ public class LiveKitPlugin: NSObject, FlutterPlugin { switch call.method { case "configureNativeAudio": handleConfigureNativeAudio(args: args, result: result) + case "osVersionString": + result(LiveKitPlugin.osVersionString()) default: print("[LiveKit] method not found: ", call.method) result(FlutterMethodNotImplemented)