Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/stream_video/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- Fixed an issue where ringing a member during an ongoing call could prematurely end the call if they declined.
- Fixed an issue where a failed call accept attempt left the CallKit call active on iOS.
- Fixed `X-Stream-Client` header and SFU `ClientDetails` being sent with stale or incomplete device/app info.

## 1.4.0

Expand Down
13 changes: 8 additions & 5 deletions packages/stream_video/lib/globals.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -12,11 +13,13 @@ const String iosWebRTCVersion = webrtc.iosWebRTCVersion;

const String streamDefaultUserAgent =
'stream-video-flutter-v$streamVideoVersion';
final 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;
174 changes: 78 additions & 96 deletions packages/stream_video/lib/src/stream_video.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ 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 '../open_api/video/coordinator/api.dart';
import 'audio_processing/audio_processor.dart';
Expand Down Expand Up @@ -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';

Expand Down Expand Up @@ -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',
);
_collectEnvironment().then((env) {

@xsahil03x xsahil03x Jun 24, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure, but can you make sure we get the .then callback and calls connect even if the _collectEnvironment throws?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe better to add a test for this

_updateVideoEnvironment(env);
if (options.autoConnect) {
connect(
includeUserDetails: options.includeUserDetailsForAutoConnect,
).catchError((dynamic error, StackTrace stackTrace) {
_logger.e(
() =>
'[StreamVideo] failed to auto connect: $error with stackTrace: $stackTrace',
);

return null;
return Result<UserToken>.error('Failed to auto connect: $error');
});
}
}),
);
}

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');
}),
);
}
void _updateVideoEnvironment(VideoEnvironment environment) {
videoEnvironmentManager.updateEnvironment(environment);
Comment thread
Brazol marked this conversation as resolved.
}

static final InstanceHolder _instanceHolder = InstanceHolder();
Expand Down Expand Up @@ -1311,96 +1307,82 @@ void _setupLogger(Priority logPriority, LogHandlerFunction logHandlerFunction) {
}
}

Future<String?> _setClientDetails() async {
/// Collects platform and app info and returns a fully-populated
/// [VideoEnvironment].
Future<VideoEnvironment> _collectEnvironment() async {
String? appName;
String? appVersion;
try {
final packageInfo = await PackageInfo.fromPlatform();
appName = packageInfo.appName;
appVersion = packageInfo.version;
} catch (e, stk) {
streamLog.e(
_tag,
() => '[_collectEnvironment] 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);
String? osVersion;
String? osArchitecture;
String? deviceModel;
String? deviceVersion;
String? browserName;
String? browserVersion;
String? webrtcVersion;

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;
}

final versionSplit = 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,
),
os: os,
device: device,
browser: browser,
webrtcVersion: webrtcVersion,
} catch (e, stk) {
streamLog.e(
_tag,
() => '[_collectEnvironment] platform info failed: $e\n$stk',
);

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 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.
Expand Down
80 changes: 80 additions & 0 deletions packages/stream_video/lib/src/video_environment.dart
Original file line number Diff line number Diff line change
@@ -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,
);
Comment thread
Brazol marked this conversation as resolved.
}
}

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('|');
}
Loading
Loading