Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: report low power mode and thermal info to stats #1583

Merged
merged 27 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1d474a1
add initial implementation for thermal info for android
kristian-mkd Nov 19, 2024
b6b7e9b
add PowerModeModule and optimized thermal status event reading
kristian-mkd Nov 20, 2024
2eb759c
update power mode module for android
kristian-mkd Nov 21, 2024
6bcf26e
Merge branch 'main' into report-low-power-and-thermal-info
kristian-mkd Nov 28, 2024
95099ae
lobby changes
kristian-mkd Nov 29, 2024
3285f62
Merge branch 'main' into report-low-power-and-thermal-info
kristian-mkd Nov 29, 2024
4e3c055
complete feature
kristian-mkd Dec 4, 2024
2a0a7e3
Merge branch 'main' into report-low-power-and-thermal-info
kristian-mkd Dec 4, 2024
6fb9434
code clean up
kristian-mkd Dec 4, 2024
ba72fca
fix review remarks
kristian-mkd Dec 10, 2024
947747b
Merge branch 'main' into report-low-power-and-thermal-info
kristian-mkd Dec 10, 2024
2125dff
ios fixes
kristian-mkd Dec 11, 2024
ea718a9
fix streamCall review remarks
kristian-mkd Dec 12, 2024
188dbe3
revert the objc version of the main module
kristian-mkd Dec 12, 2024
f547910
revert some changes
kristian-mkd Dec 12, 2024
20bb896
Merge branch 'main' into report-low-power-and-thermal-info
kristian-mkd Dec 12, 2024
c50a76d
new device stats methods
kristian-mkd Dec 13, 2024
22a2c58
remove device stats swift
kristian-mkd Dec 13, 2024
edba555
Merge branch 'main' into report-low-power-and-thermal-info
kristian-mkd Dec 13, 2024
1219bfd
Merge branch 'main' into report-low-power-and-thermal-info
kristian-mkd Dec 16, 2024
317dc24
ios fixes
kristian-mkd Dec 16, 2024
ab99c7c
fix android device stats
kristian-mkd Dec 17, 2024
d9626f3
fix tests
kristian-mkd Dec 17, 2024
c36a1f7
Merge branch 'main' into report-low-power-and-thermal-info
kristian-mkd Dec 18, 2024
5056b05
fix review remarks
kristian-mkd Dec 18, 2024
7559c6b
review fixes
kristian-mkd Dec 18, 2024
ba8454c
remove unused import
kristian-mkd Dec 18, 2024
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
80 changes: 80 additions & 0 deletions packages/client/src/client-details.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import {
AndroidThermalState,
AppleThermalState,
ClientDetails,
Device,
OS,
Sdk,
SdkType,
} from './gen/video/sfu/models/models';
import { SendStatsRequest } from './gen/video/sfu/signal_rpc/signal';
import { isReactNative } from './helpers/platforms';
import { UAParser } from 'ua-parser-js';

Expand All @@ -25,6 +28,7 @@ let sdkInfo: Sdk | undefined = {
let osInfo: OS | undefined;
let deviceInfo: Device | undefined;
let webRtcInfo: WebRTCInfoType | undefined;
let deviceState: SendStatsRequest['deviceState'];

export const setSdkInfo = (info: Sdk) => {
sdkInfo = info;
Expand Down Expand Up @@ -62,6 +66,82 @@ export type LocalClientDetailsType = ClientDetails & {
webRTCInfo?: WebRTCInfoType;
};

export const setThermalState = (state: string) => {
if (!osInfo) {
deviceState = { oneofKind: undefined };
return;
}

if (osInfo.name === 'android') {
const thermalState =
AndroidThermalState[state as keyof typeof AndroidThermalState] ||
AndroidThermalState.UNSPECIFIED;

deviceState = {
oneofKind: 'android',
android: {
thermalState,
isPowerSaverMode:
deviceState?.oneofKind === 'android' &&
deviceState.android.isPowerSaverMode,
},
};
}

if (osInfo.name.toLowerCase() === 'ios') {
const thermalState =
AppleThermalState[state as keyof typeof AppleThermalState] ||
AppleThermalState.UNSPECIFIED;

deviceState = {
oneofKind: 'apple',
apple: {
thermalState,
isLowPowerModeEnabled:
deviceState?.oneofKind === 'apple' &&
deviceState.apple.isLowPowerModeEnabled,
},
};
}
};

export const setPowerState = (powerMode: boolean) => {
if (!osInfo) {
deviceState = { oneofKind: undefined };
return;
}

if (osInfo.name === 'android') {
deviceState = {
oneofKind: 'android',
android: {
thermalState:
deviceState?.oneofKind === 'android'
? deviceState.android.thermalState
: AndroidThermalState.UNSPECIFIED,
isPowerSaverMode: powerMode,
},
};
}

if (osInfo.name.toLowerCase() === 'ios') {
deviceState = {
oneofKind: 'apple',
apple: {
thermalState:
deviceState?.oneofKind === 'apple'
? deviceState.apple.thermalState
: AppleThermalState.UNSPECIFIED,
isLowPowerModeEnabled: powerMode,
},
};
}
};

export const getDeviceState = () => {
return deviceState;
};

export const getClientDetails = (): LocalClientDetailsType => {
if (isReactNative()) {
// Since RN doesn't support web, sharing browser info is not required
Expand Down
8 changes: 6 additions & 2 deletions packages/client/src/stats/SfuStatsReporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { OwnCapability, StatsOptions } from '../gen/coordinator';
import { getLogger } from '../logger';
import { Publisher, Subscriber } from '../rtc';
import { flatten, getSdkName, getSdkVersion } from './utils';
import { getWebRTCInfo, LocalClientDetailsType } from '../client-details';
import {
getDeviceState,
getWebRTCInfo,
LocalClientDetailsType,
} from '../client-details';
import { InputDevices } from '../gen/video/sfu/models/models';
import { CameraManager, MicrophoneManager } from '../devices';
import { createSubscription } from '../store/rxUtils';
Expand Down Expand Up @@ -132,7 +136,7 @@ export class SfuStatsReporter {
publisherStats,
audioDevices: this.inputDevices.get('mic'),
videoDevices: this.inputDevices.get('camera'),
deviceState: { oneofKind: undefined },
deviceState: getDeviceState(),
telemetry: telemetryData,
});
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
package="com.streamvideo.reactnative">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.DEVICE_POWER" />
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import android.app.AppOpsManager
import android.app.PictureInPictureParams
import android.content.Context
import android.content.pm.PackageManager
import android.content.BroadcastReceiver
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.os.Process
import android.util.Rational
import androidx.annotation.RequiresApi
Expand All @@ -23,13 +27,18 @@ class StreamVideoReactNativeModule(reactContext: ReactApplicationContext) : Reac
return NAME;
}

private var thermalStatusListener: PowerManager.OnThermalStatusChangedListener? = null

override fun initialize() {
super.initialize()
StreamVideoReactNative.pipListeners.add { isInPictureInPictureMode ->
reactApplicationContext.getJSModule(
RCTDeviceEventEmitter::class.java
).emit(PIP_CHANGE_EVENT, isInPictureInPictureMode)
}

val filter = IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)
reactApplicationContext.registerReceiver(powerReceiver, filter)
}

@ReactMethod
Expand Down Expand Up @@ -81,6 +90,111 @@ class StreamVideoReactNativeModule(reactContext: ReactApplicationContext) : Reac
StreamVideoReactNative.canAutoEnterPictureInPictureMode = value
}

@ReactMethod
fun startThermalStatusUpdates(promise: Promise) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val powerManager = reactApplicationContext.getSystemService(ReactApplicationContext.POWER_SERVICE) as PowerManager

val listener = PowerManager.OnThermalStatusChangedListener { status ->
val thermalStatus = when (status) {
PowerManager.THERMAL_STATUS_NONE -> "NONE"
PowerManager.THERMAL_STATUS_LIGHT -> "LIGHT"
PowerManager.THERMAL_STATUS_MODERATE -> "MODERATE"
PowerManager.THERMAL_STATUS_SEVERE -> "SEVERE"
PowerManager.THERMAL_STATUS_CRITICAL -> "CRITICAL"
PowerManager.THERMAL_STATUS_EMERGENCY -> "EMERGENCY"
PowerManager.THERMAL_STATUS_SHUTDOWN -> "SHUTDOWN"
else -> "UNKNOWN"
}

reactApplicationContext
.getJSModule(RCTDeviceEventEmitter::class.java)
.emit("thermalStateDidChange", thermalStatus)
}

thermalStatusListener = listener
powerManager.addThermalStatusListener(listener)
// Get initial status
currentThermalState(promise)
} else {
promise.resolve("NOT_SUPPORTED")
}
} catch (e: Exception) {
promise.reject("THERMAL_ERROR", e.message)
}
}

@ReactMethod
fun stopThermalStatusUpdates() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val powerManager = reactApplicationContext.getSystemService(ReactApplicationContext.POWER_SERVICE) as PowerManager
// Store the current listener in a local val for safe null checking
val currentListener = thermalStatusListener
if (currentListener != null) {
powerManager.removeThermalStatusListener(currentListener)
thermalStatusListener = null
}
}
}

@ReactMethod
fun currentThermalState(promise: Promise) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val powerManager = reactApplicationContext.getSystemService(ReactApplicationContext.POWER_SERVICE) as PowerManager
val status = powerManager.currentThermalStatus
val thermalStatus = when (status) {
PowerManager.THERMAL_STATUS_NONE -> "NONE"
PowerManager.THERMAL_STATUS_LIGHT -> "LIGHT"
PowerManager.THERMAL_STATUS_MODERATE -> "MODERATE"
PowerManager.THERMAL_STATUS_SEVERE -> "SEVERE"
PowerManager.THERMAL_STATUS_CRITICAL -> "CRITICAL"
PowerManager.THERMAL_STATUS_EMERGENCY -> "EMERGENCY"
PowerManager.THERMAL_STATUS_SHUTDOWN -> "SHUTDOWN"
else -> "UNKNOWN"
}
promise.resolve(thermalStatus)
} else {
promise.resolve("NOT_SUPPORTED")
}
} catch (e: Exception) {
promise.reject("THERMAL_ERROR", e.message)
}
}

private val powerReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == PowerManager.ACTION_POWER_SAVE_MODE_CHANGED) {
sendPowerModeEvent()
}
}
}

override fun onCatalystInstanceDestroy() {
super.onCatalystInstanceDestroy()
reactApplicationContext.unregisterReceiver(powerReceiver)
stopThermalStatusUpdates()
}

private fun sendPowerModeEvent() {
val powerManager = reactApplicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
val isLowPowerMode = powerManager.isPowerSaveMode
reactApplicationContext
.getJSModule(RCTDeviceEventEmitter::class.java)
.emit("isLowPowerModeEnabled", isLowPowerMode)
}

@ReactMethod
fun isLowPowerModeEnabled(promise: Promise) {
try {
val powerManager = reactApplicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
promise.resolve(powerManager.isPowerSaveMode)
} catch (e: Exception) {
promise.reject("ERROR", e.message)
}
}

private fun hasPermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && reactApplicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
val appOps =
Expand Down
49 changes: 45 additions & 4 deletions packages/react-native-sdk/ios/StreamVideoReactNative.m
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ void broadcastNotificationCallback(CFNotificationCenterRef center,
StreamVideoReactNative *this = (__bridge StreamVideoReactNative*)observer;
NSString *eventName = (__bridge NSString*)name;
[this screenShareEventReceived: eventName];

}

@implementation StreamVideoReactNative
Expand All @@ -44,10 +44,21 @@ -(instancetype)init {
_notificationCenter = CFNotificationCenterGetDarwinNotifyCenter();
[self setupObserver];
}

if (self) {
[UIDevice currentDevice].batteryMonitoringEnabled = YES;
}

return self;
}

RCT_EXPORT_METHOD(isLowPowerModeEnabled:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
resolve(@([NSProcessInfo processInfo].lowPowerModeEnabled));
}

RCT_EXPORT_METHOD(currentThermalState:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
resolve(@([NSProcessInfo processInfo].thermalState));
}

-(void)dealloc {
[self clearObserver];
}
Expand Down Expand Up @@ -81,10 +92,40 @@ -(void)clearObserver {

-(void)startObserving {
hasListeners = YES;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(powerModeDidChange)
name:NSProcessInfoPowerStateDidChangeNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(thermalStateDidChange)
name:NSProcessInfoThermalStateDidChangeNotification
object:nil];
}

-(void)stopObserving {
hasListeners = NO;
[[NSNotificationCenter defaultCenter] removeObserver:self
name:NSProcessInfoPowerStateDidChangeNotification
object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:NSProcessInfoThermalStateDidChangeNotification
object:nil];
}

- (void)powerModeDidChange {
if (!hasListeners) {
return;
}
BOOL lowPowerEnabled = [NSProcessInfo processInfo].lowPowerModeEnabled;
[self sendEventWithName:@"isLowPowerModeEnabled" body:@(lowPowerEnabled)];
}

- (void)thermalStateDidChange {
if (!hasListeners) {
return;
}
NSInteger thermalState = [NSProcessInfo processInfo].thermalState;
[self sendEventWithName:@"thermalStateDidChange" body:@(thermalState)];
}

-(void)screenShareEventReceived:(NSString*)event {
Expand Down Expand Up @@ -113,11 +154,11 @@ +(void)registerIncomingCall:(NSString *)cid uuid:(NSString *)uuid {
} else {
reject(@"access_failure", @"requested incoming call found", nil);
}

}

-(NSArray<NSString *> *)supportedEvents {
return @[@"StreamVideoReactNative_Ios_Screenshare_Event"];
return @[@"StreamVideoReactNative_Ios_Screenshare_Event", @"isLowPowerModeEnabled", @"thermalStateDidChange"];
}

@end
Loading
Loading