From e00d94c7ad1b7c1a0e9ef84c62c2e63dbf8d8c43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Braz=CC=87ewicz?= Date: Wed, 24 Jun 2026 10:50:43 +0200 Subject: [PATCH 1/4] fix stream header string creation --- packages/stream_video/lib/globals.dart | 2 +- .../stream_video/lib/src/stream_video.dart | 102 +++++++++++------- 2 files changed, 63 insertions(+), 41 deletions(-) diff --git a/packages/stream_video/lib/globals.dart b/packages/stream_video/lib/globals.dart index 626880181..a57e7c862 100644 --- a/packages/stream_video/lib/globals.dart +++ b/packages/stream_video/lib/globals.dart @@ -12,7 +12,7 @@ const String iosWebRTCVersion = webrtc.iosWebRTCVersion; const String streamDefaultUserAgent = 'stream-video-flutter-v$streamVideoVersion'; -final xStreamClientHeader = '$streamDefaultUserAgent|$clientVersionDetails'; +String get xStreamClientHeader => '$streamDefaultUserAgent|$clientVersionDetails'; /// Details regarding app name, version, os and device. Is set during initialization of StreamVideo instance. @internal diff --git a/packages/stream_video/lib/src/stream_video.dart b/packages/stream_video/lib/src/stream_video.dart index 291b57650..8c3a4f890 100644 --- a/packages/stream_video/lib/src/stream_video.dart +++ b/packages/stream_video/lib/src/stream_video.dart @@ -235,30 +235,26 @@ class StreamVideo extends Disposable { _setupLogger(options.logPriority, options.logHandlerFunction); unawaited( - _setClientDetails().onError((dynamic error, StackTrace stackTrace) { - _logger.e( - () => - '[StreamVideo] failed to set client details: $error with stackTrace: $stackTrace', - ); - - return null; - }), + _setClientDetails() + .catchError((dynamic error, StackTrace stackTrace) { + _logger.e( + () => + '[StreamVideo] failed to set client details: $error with stackTrace: $stackTrace', + ); + }) + .then((_) { + if (options.autoConnect) { + connect( + includeUserDetails: options.includeUserDetailsForAutoConnect, + ).catchError((dynamic error, StackTrace stackTrace) { + _logger.e( + () => + '[StreamVideo] failed to auto connect: $error with stackTrace: $stackTrace', + ); + }); + } + }), ); - - if (options.autoConnect) { - unawaited( - connect( - includeUserDetails: options.includeUserDetailsForAutoConnect, - ).onError((dynamic error, StackTrace stackTrace) { - _logger.e( - () => - '[StreamVideo] failed to auto connect: $error with stackTrace: $stackTrace', - ); - - return Result.error('Failed to auto connect: $error'); - }), - ); - } } static final InstanceHolder _instanceHolder = InstanceHolder(); @@ -1298,18 +1294,25 @@ void _setupLogger(Priority logPriority, LogHandlerFunction logHandlerFunction) { } Future _setClientDetails() async { + String? appName; + String? appVersion; try { final packageInfo = await PackageInfo.fromPlatform(); + appName = packageInfo.appName; + appVersion = packageInfo.version; + } catch (e, stk) { + streamLog.e( + _tag, + () => '[_setClientDetails] package info failed: $e\n$stk', + ); + } - final appName = packageInfo.appName; - final appVersion = packageInfo.version; - - sfu_models.Device? device; - sfu_models.Browser? browser; - String? webrtcVersion; - - var os = sfu_models.OS(name: CurrentPlatform.name); + sfu_models.Device? device; + sfu_models.Browser? browser; + String? webrtcVersion; + var os = sfu_models.OS(name: CurrentPlatform.name); + try { if (CurrentPlatform.isAndroid) { final deviceInfo = await DeviceInfoPlugin().androidInfo; os = sfu_models.OS( @@ -1362,30 +1365,49 @@ Future _setClientDetails() async { version: browserInfo.appVersion, ); } + } catch (e, stk) { + streamLog.e( + _tag, + () => '[_setClientDetails] platform info failed: $e\n$stk', + ); + } - final versionSplit = streamVideoVersion.split('.'); + try { + final versionParts = streamVideoVersion.split('.'); clientDetails = sfu_models.ClientDetails( sdk: sfu_models.Sdk( type: sfu_models.SdkType.SDK_TYPE_FLUTTER, - major: versionSplit.first, - minor: versionSplit.skip(1).first, - patch: versionSplit.last, + major: versionParts.isNotEmpty ? versionParts[0] : '0', + minor: versionParts.length > 1 ? versionParts[1] : '0', + patch: versionParts.length > 2 ? versionParts[2] : '0', ), os: os, device: device, browser: browser, webrtcVersion: webrtcVersion, ); + } catch (e, stk) { + streamLog.e( + _tag, + () => '[_setClientDetails] client details proto failed: $e\n$stk', + ); + } + try { + final resolvedAppName = appName ?? 'unknown'; + final resolvedAppVersion = appVersion ?? 'unknown'; final deviceName = (device?.name != null && device!.name.isNotEmpty) ? device.name : null; - return clientVersionDetails ??= - 'app=$appName|app_version=$appVersion|os=${CurrentPlatform.name} ${os.version}${deviceName != null ? '|device_model=$deviceName' : ''}'; - } catch (e) { - streamLog.e(_tag, () => '[_setClientDetails] failed: $e'); - return null; + return clientVersionDetails = + 'app=$resolvedAppName|app_version=$resolvedAppVersion|os=${CurrentPlatform.name} ${os.version}${deviceName != null ? '|device_model=$deviceName' : ''}'; + } catch (e, stk) { + streamLog.e( + _tag, + () => '[_setClientDetails] client version string failed: $e\n$stk', + ); + return clientVersionDetails; } } From e3104e3120bfde7ae1ffd1b4119d8a9f34c543b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Braz=CC=87ewicz?= Date: Wed, 24 Jun 2026 12:04:43 +0200 Subject: [PATCH 2/4] use environment object for client details --- packages/stream_video/lib/globals.dart | 13 +- .../stream_video/lib/src/stream_video.dart | 164 +++++++----------- .../lib/src/video_environment.dart | 80 +++++++++ .../lib/src/video_environment_manager.dart | 66 +++++++ 4 files changed, 215 insertions(+), 108 deletions(-) create mode 100644 packages/stream_video/lib/src/video_environment.dart create mode 100644 packages/stream_video/lib/src/video_environment_manager.dart diff --git a/packages/stream_video/lib/globals.dart b/packages/stream_video/lib/globals.dart index a57e7c862..0bc439b7c 100644 --- a/packages/stream_video/lib/globals.dart +++ b/packages/stream_video/lib/globals.dart @@ -2,6 +2,7 @@ import 'package:meta/meta.dart'; import 'package:stream_webrtc_flutter/stream_webrtc_flutter.dart' as webrtc; import 'protobuf/video/sfu/models/models.pb.dart'; +import 'src/video_environment_manager.dart'; const String streamSdkName = 'stream-flutter'; const String streamVideoVersion = '1.4.0'; @@ -12,11 +13,13 @@ const String iosWebRTCVersion = webrtc.iosWebRTCVersion; const String streamDefaultUserAgent = 'stream-video-flutter-v$streamVideoVersion'; -String get xStreamClientHeader => '$streamDefaultUserAgent|$clientVersionDetails'; -/// Details regarding app name, version, os and device. Is set during initialization of StreamVideo instance. +/// Manages the current video environment (OS, device, app info). @internal -String? clientVersionDetails; +final videoEnvironmentManager = VideoEnvironmentManager(); -@internal -ClientDetails? clientDetails; +/// The `X-Stream-Client` header value. +String get xStreamClientHeader => videoEnvironmentManager.xStreamClientHeader; + +/// The SFU `ClientDetails` proto for the current environment. +ClientDetails get clientDetails => videoEnvironmentManager.clientDetails; diff --git a/packages/stream_video/lib/src/stream_video.dart b/packages/stream_video/lib/src/stream_video.dart index 8c3a4f890..a6ab5ba54 100644 --- a/packages/stream_video/lib/src/stream_video.dart +++ b/packages/stream_video/lib/src/stream_video.dart @@ -12,8 +12,8 @@ import 'package:stream_webrtc_flutter/stream_webrtc_flutter.dart' as rtc; import 'package:system_info2/system_info2.dart'; import 'package:uuid/uuid.dart'; -import '../../../protobuf/video/sfu/models/models.pb.dart' as sfu_models; import '../globals.dart'; +import 'video_environment.dart'; import '../open_api/video/coordinator/api.dart'; import 'audio_processing/audio_processor.dart'; import 'call/call.dart'; @@ -235,28 +235,26 @@ class StreamVideo extends Disposable { _setupLogger(options.logPriority, options.logHandlerFunction); unawaited( - _setClientDetails() - .catchError((dynamic error, StackTrace stackTrace) { + _collectEnvironment().then((env) { + _updateVideoEnvironment(env); + if (options.autoConnect) { + connect( + includeUserDetails: options.includeUserDetailsForAutoConnect, + ).catchError((dynamic error, StackTrace stackTrace) { _logger.e( () => - '[StreamVideo] failed to set client details: $error with stackTrace: $stackTrace', + '[StreamVideo] failed to auto connect: $error with stackTrace: $stackTrace', ); - }) - .then((_) { - if (options.autoConnect) { - connect( - includeUserDetails: options.includeUserDetailsForAutoConnect, - ).catchError((dynamic error, StackTrace stackTrace) { - _logger.e( - () => - '[StreamVideo] failed to auto connect: $error with stackTrace: $stackTrace', - ); - }); - } - }), + }); + } + }), ); } + void _updateVideoEnvironment(VideoEnvironment environment) { + videoEnvironmentManager.environment = environment; + } + static final InstanceHolder _instanceHolder = InstanceHolder(); /// The singleton instance of the Stream Video client. @@ -1293,7 +1291,9 @@ void _setupLogger(Priority logPriority, LogHandlerFunction logHandlerFunction) { } } -Future _setClientDetails() async { +/// Collects platform and app info and returns a fully-populated +/// [VideoEnvironment]. +Future _collectEnvironment() async { String? appName; String? appVersion; try { @@ -1303,112 +1303,70 @@ Future _setClientDetails() async { } catch (e, stk) { streamLog.e( _tag, - () => '[_setClientDetails] package info failed: $e\n$stk', + () => '[_collectEnvironment] package info failed: $e\n$stk', ); } - sfu_models.Device? device; - sfu_models.Browser? browser; + String? osVersion; + String? osArchitecture; + String? deviceModel; + String? deviceVersion; + String? browserName; + String? browserVersion; String? webrtcVersion; - var os = sfu_models.OS(name: CurrentPlatform.name); try { if (CurrentPlatform.isAndroid) { - final deviceInfo = await DeviceInfoPlugin().androidInfo; - os = sfu_models.OS( - name: CurrentPlatform.name, - version: deviceInfo.version.release, - architecture: SysInfo.rawKernelArchitecture, - ); - device = sfu_models.Device( - name: '${deviceInfo.manufacturer} : ${deviceInfo.model}', - ); + final info = await DeviceInfoPlugin().androidInfo; + osVersion = info.version.release; + osArchitecture = SysInfo.rawKernelArchitecture; + deviceModel = '${info.manufacturer} ${info.model}'; webrtcVersion = androidWebRTCVersion; } else if (CurrentPlatform.isIos) { - final deviceInfo = await DeviceInfoPlugin().iosInfo; - os = sfu_models.OS( - name: CurrentPlatform.name, - version: deviceInfo.systemVersion, - ); - device = sfu_models.Device(name: deviceInfo.utsname.machine); + final info = await DeviceInfoPlugin().iosInfo; + osVersion = info.systemVersion; + deviceModel = info.utsname.machine; webrtcVersion = iosWebRTCVersion; } else if (CurrentPlatform.isMacOS) { - final deviceInfo = await DeviceInfoPlugin().macOsInfo; - os = sfu_models.OS( - name: CurrentPlatform.name, - version: - '${deviceInfo.majorVersion}.${deviceInfo.minorVersion}.${deviceInfo.patchVersion}', - architecture: deviceInfo.arch, - ); - device = sfu_models.Device( - name: deviceInfo.model, - version: deviceInfo.osRelease, - ); + final info = await DeviceInfoPlugin().macOsInfo; + osVersion = + '${info.majorVersion}.${info.minorVersion}.${info.patchVersion}'; + osArchitecture = info.arch; + deviceModel = info.model; + deviceVersion = info.osRelease; } else if (CurrentPlatform.isWindows) { - final deviceInfo = await DeviceInfoPlugin().windowsInfo; - os = sfu_models.OS( - name: CurrentPlatform.name, - version: - '${deviceInfo.majorVersion}.${deviceInfo.minorVersion}.${deviceInfo.buildNumber}', - architecture: deviceInfo.buildLabEx, - ); + final info = await DeviceInfoPlugin().windowsInfo; + osVersion = + '${info.majorVersion}.${info.minorVersion}.${info.buildNumber}'; + osArchitecture = info.buildLabEx; } else if (CurrentPlatform.isLinux) { - final deviceInfo = await DeviceInfoPlugin().linuxInfo; - os = sfu_models.OS( - name: CurrentPlatform.name, - version: '${deviceInfo.name} ${deviceInfo.version}', - ); + final info = await DeviceInfoPlugin().linuxInfo; + osVersion = '${info.name} ${info.version}'; } else if (CurrentPlatform.isWeb) { - final browserInfo = await DeviceInfoPlugin().webBrowserInfo; - browser = sfu_models.Browser( - name: browserInfo.browserName.name, - version: browserInfo.appVersion, - ); + final info = await DeviceInfoPlugin().webBrowserInfo; + browserName = info.browserName.name; + browserVersion = info.appVersion; } } catch (e, stk) { streamLog.e( _tag, - () => '[_setClientDetails] platform info failed: $e\n$stk', + () => '[_collectEnvironment] platform info failed: $e\n$stk', ); } - try { - final versionParts = streamVideoVersion.split('.'); - clientDetails = sfu_models.ClientDetails( - sdk: sfu_models.Sdk( - type: sfu_models.SdkType.SDK_TYPE_FLUTTER, - major: versionParts.isNotEmpty ? versionParts[0] : '0', - minor: versionParts.length > 1 ? versionParts[1] : '0', - patch: versionParts.length > 2 ? versionParts[2] : '0', - ), - os: os, - device: device, - browser: browser, - webrtcVersion: webrtcVersion, - ); - } catch (e, stk) { - streamLog.e( - _tag, - () => '[_setClientDetails] client details proto failed: $e\n$stk', - ); - } - - try { - final resolvedAppName = appName ?? 'unknown'; - final resolvedAppVersion = appVersion ?? 'unknown'; - final deviceName = (device?.name != null && device!.name.isNotEmpty) - ? device.name - : null; - - return clientVersionDetails = - 'app=$resolvedAppName|app_version=$resolvedAppVersion|os=${CurrentPlatform.name} ${os.version}${deviceName != null ? '|device_model=$deviceName' : ''}'; - } catch (e, stk) { - streamLog.e( - _tag, - () => '[_setClientDetails] client version string failed: $e\n$stk', - ); - return clientVersionDetails; - } + return VideoEnvironment( + sdkVersion: streamVideoVersion, + osName: CurrentPlatform.name, + appName: appName, + appVersion: appVersion, + osVersion: osVersion, + osArchitecture: osArchitecture, + deviceModel: deviceModel, + deviceVersion: deviceVersion, + browserName: browserName, + browserVersion: browserVersion, + webrtcVersion: webrtcVersion, + ); } /// Default log handler function for the [StreamVideo] logger. diff --git a/packages/stream_video/lib/src/video_environment.dart b/packages/stream_video/lib/src/video_environment.dart new file mode 100644 index 000000000..56339f5c2 --- /dev/null +++ b/packages/stream_video/lib/src/video_environment.dart @@ -0,0 +1,80 @@ +/// Immutable snapshot of the environment in which the Stream Video SDK runs. +class VideoEnvironment { + const VideoEnvironment({ + required this.sdkVersion, + required this.osName, + this.appName, + this.appVersion, + this.osVersion, + this.osArchitecture, + this.deviceModel, + this.deviceVersion, + this.browserName, + this.browserVersion, + this.webrtcVersion, + }); + + final String sdkVersion; + final String osName; + + final String? appName; + final String? appVersion; + + final String? osVersion; + final String? osArchitecture; + + /// Device model name (mobile / desktop). + final String? deviceModel; + + /// OS release string — populated on macOS only. + final String? deviceVersion; + + /// Browser name / version — populated on web only. + final String? browserName; + final String? browserVersion; + + final String? webrtcVersion; + + VideoEnvironment copyWith({ + String? sdkVersion, + String? osName, + String? appName, + String? appVersion, + String? osVersion, + String? osArchitecture, + String? deviceModel, + String? deviceVersion, + String? browserName, + String? browserVersion, + String? webrtcVersion, + }) { + return VideoEnvironment( + sdkVersion: sdkVersion ?? this.sdkVersion, + osName: osName ?? this.osName, + appName: appName ?? this.appName, + appVersion: appVersion ?? this.appVersion, + osVersion: osVersion ?? this.osVersion, + osArchitecture: osArchitecture ?? this.osArchitecture, + deviceModel: deviceModel ?? this.deviceModel, + deviceVersion: deviceVersion ?? this.deviceVersion, + browserName: browserName ?? this.browserName, + browserVersion: browserVersion ?? this.browserVersion, + webrtcVersion: webrtcVersion ?? this.webrtcVersion, + ); + } +} + +extension VideoEnvironmentHeader on VideoEnvironment { + /// Builds the `X-Stream-Client` header value. + String get xStreamClientHeader => [ + 'stream-video-flutter-v$sdkVersion', + if (appName case final name?) 'app=$name', + if (appVersion case final version?) 'app_version=$version', + switch ((osName, osVersion)) { + (final name, final version?) => 'os=$name $version', + (final name, null) => 'os=$name', + }, + if (deviceModel case final model?) 'device_model=$model', + if (browserName case final name?) 'browser=$name', + ].join('|'); +} diff --git a/packages/stream_video/lib/src/video_environment_manager.dart b/packages/stream_video/lib/src/video_environment_manager.dart new file mode 100644 index 000000000..afd794702 --- /dev/null +++ b/packages/stream_video/lib/src/video_environment_manager.dart @@ -0,0 +1,66 @@ +import '../globals.dart' show streamVideoVersion; +import '../protobuf/video/sfu/models/models.pb.dart' as sfu_models; +import 'platform_detector/platform_detector.dart'; +import 'video_environment.dart'; + +/// Manages the current [VideoEnvironment] and derives the header / proto objects from it. +class VideoEnvironmentManager { + VideoEnvironmentManager({VideoEnvironment? environment}) + : _environment = + environment ?? + VideoEnvironment( + sdkVersion: streamVideoVersion, + osName: CurrentPlatform.name, + ); + + VideoEnvironment _environment; + + VideoEnvironment get environment => _environment; + + // ignore: use_setters_to_change_properties + void updateEnvironment(VideoEnvironment environment) { + _environment = environment; + } + + /// The `X-Stream-Client` header value for the current environment. + String get xStreamClientHeader => _environment.xStreamClientHeader; + + /// The SFU `ClientDetails` proto for the current environment. + sfu_models.ClientDetails get clientDetails { + final env = _environment; + final versionParts = env.sdkVersion.split('.'); + + sfu_models.Device? device; + if (env.deviceModel case final model?) { + device = sfu_models.Device( + name: model, + version: env.deviceVersion ?? '', + ); + } + + sfu_models.Browser? browser; + if (env.browserName case final name?) { + browser = sfu_models.Browser( + name: name, + version: env.browserVersion, + ); + } + + return sfu_models.ClientDetails( + sdk: sfu_models.Sdk( + type: sfu_models.SdkType.SDK_TYPE_FLUTTER, + major: versionParts.isNotEmpty ? versionParts[0] : '0', + minor: versionParts.length > 1 ? versionParts[1] : '0', + patch: versionParts.length > 2 ? versionParts[2] : '0', + ), + os: sfu_models.OS( + name: env.osName, + version: env.osVersion ?? '', + architecture: env.osArchitecture ?? '', + ), + device: device, + browser: browser, + webrtcVersion: env.webrtcVersion ?? '', + ); + } +} From ffd55e81fdf95cd27fe5587d1b331e1165771025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Braz=CC=87ewicz?= Date: Wed, 24 Jun 2026 12:06:31 +0200 Subject: [PATCH 3/4] tweak --- packages/stream_video/lib/src/stream_video.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/stream_video/lib/src/stream_video.dart b/packages/stream_video/lib/src/stream_video.dart index a6ab5ba54..291676a82 100644 --- a/packages/stream_video/lib/src/stream_video.dart +++ b/packages/stream_video/lib/src/stream_video.dart @@ -13,7 +13,6 @@ import 'package:system_info2/system_info2.dart'; import 'package:uuid/uuid.dart'; import '../globals.dart'; -import 'video_environment.dart'; import '../open_api/video/coordinator/api.dart'; import 'audio_processing/audio_processor.dart'; import 'call/call.dart'; @@ -67,6 +66,7 @@ import 'utils/none.dart'; import 'utils/result.dart'; import 'utils/standard.dart'; import 'utils/subscriptions.dart'; +import 'video_environment.dart'; import 'webrtc/rtc_media_device/rtc_media_device_notifier.dart'; import 'webrtc/sdp/policy/sdp_policy.dart'; @@ -245,6 +245,8 @@ class StreamVideo extends Disposable { () => '[StreamVideo] failed to auto connect: $error with stackTrace: $stackTrace', ); + + return Result.error('Failed to auto connect: $error'); }); } }), @@ -252,7 +254,7 @@ class StreamVideo extends Disposable { } void _updateVideoEnvironment(VideoEnvironment environment) { - videoEnvironmentManager.environment = environment; + videoEnvironmentManager.updateEnvironment(environment); } static final InstanceHolder _instanceHolder = InstanceHolder(); From 26f052658be25eb03ae0563e5972d2be4e5e5a42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Braz=CC=87ewicz?= Date: Wed, 24 Jun 2026 12:23:33 +0200 Subject: [PATCH 4/4] changelog --- packages/stream_video/CHANGELOG.md | 4 ++++ packages/stream_video_flutter/CHANGELOG.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/packages/stream_video/CHANGELOG.md b/packages/stream_video/CHANGELOG.md index 7c959bbc2..c263bd5cc 100644 --- a/packages/stream_video/CHANGELOG.md +++ b/packages/stream_video/CHANGELOG.md @@ -1,3 +1,7 @@ +## Upcoming + +- Fixed `X-Stream-Client` header and SFU `ClientDetails` being sent with stale or incomplete device/app info. + ## 1.4.0 Each call now owns an isolated native `PeerConnectionFactory`. This fixes cross-call audio interference, sibling-call microphone capture loss, and noise cancellation failing to engage during lobby preview. diff --git a/packages/stream_video_flutter/CHANGELOG.md b/packages/stream_video_flutter/CHANGELOG.md index 49da9befb..faf51a172 100644 --- a/packages/stream_video_flutter/CHANGELOG.md +++ b/packages/stream_video_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## Upcoming + +- Fixed `X-Stream-Client` header and SFU `ClientDetails` being sent with stale or incomplete device/app info. + ## 1.4.0 Each call now owns an isolated native `PeerConnectionFactory`. This fixes cross-call audio interference, sibling-call microphone capture loss, and noise cancellation failing to engage during lobby preview.