From d33258098b99068064e0d4a8cfc6566dec8e881e Mon Sep 17 00:00:00 2001 From: Zeke Chin Date: Fri, 14 Aug 2015 11:00:02 -0700 Subject: [PATCH] Add stats overlay to iOS AppRTCDemo. BUG= R=jiayl@webrtc.org Review URL: https://codereview.webrtc.org/1289623005 . Cr-Commit-Position: refs/heads/master@{#9714} --- .../objc/AppRTCDemo/ARDAppClient+Internal.h | 3 +- .../examples/objc/AppRTCDemo/ARDAppClient.h | 6 + .../examples/objc/AppRTCDemo/ARDAppClient.m | 85 ++++- .../objc/AppRTCDemo/ARDBitrateTracker.h | 30 ++ .../objc/AppRTCDemo/ARDBitrateTracker.m | 45 +++ .../objc/AppRTCDemo/ARDStatsBuilder.h | 29 ++ .../objc/AppRTCDemo/ARDStatsBuilder.m | 321 ++++++++++++++++++ .../objc/AppRTCDemo/common/ARDUtilities.h | 3 + .../objc/AppRTCDemo/common/ARDUtilities.m | 33 ++ .../objc/AppRTCDemo/ios/ARDStatsView.h | 17 + .../objc/AppRTCDemo/ios/ARDStatsView.m | 52 +++ .../objc/AppRTCDemo/ios/ARDVideoCallView.h | 6 + .../objc/AppRTCDemo/ios/ARDVideoCallView.m | 23 ++ .../ios/ARDVideoCallViewController.m | 11 + .../AppRTCDemo/mac/APPRTCViewController.m | 4 + webrtc/libjingle_examples.gyp | 13 + 16 files changed, 674 insertions(+), 7 deletions(-) create mode 100644 webrtc/examples/objc/AppRTCDemo/ARDBitrateTracker.h create mode 100644 webrtc/examples/objc/AppRTCDemo/ARDBitrateTracker.m create mode 100644 webrtc/examples/objc/AppRTCDemo/ARDStatsBuilder.h create mode 100644 webrtc/examples/objc/AppRTCDemo/ARDStatsBuilder.m create mode 100644 webrtc/examples/objc/AppRTCDemo/ios/ARDStatsView.h create mode 100644 webrtc/examples/objc/AppRTCDemo/ios/ARDStatsView.m diff --git a/webrtc/examples/objc/AppRTCDemo/ARDAppClient+Internal.h b/webrtc/examples/objc/AppRTCDemo/ARDAppClient+Internal.h index c4a3871b89..5a7b387880 100644 --- a/webrtc/examples/objc/AppRTCDemo/ARDAppClient+Internal.h +++ b/webrtc/examples/objc/AppRTCDemo/ARDAppClient+Internal.h @@ -17,9 +17,10 @@ #import "RTCPeerConnectionDelegate.h" #import "RTCPeerConnectionFactory.h" #import "RTCSessionDescriptionDelegate.h" +#import "RTCStatsDelegate.h" @interface ARDAppClient () + RTCPeerConnectionDelegate, RTCSessionDescriptionDelegate, RTCStatsDelegate> // All properties should only be mutated from the main queue. @property(nonatomic, strong) id roomServerClient; diff --git a/webrtc/examples/objc/AppRTCDemo/ARDAppClient.h b/webrtc/examples/objc/AppRTCDemo/ARDAppClient.h index 04993e49b2..d591704985 100644 --- a/webrtc/examples/objc/AppRTCDemo/ARDAppClient.h +++ b/webrtc/examples/objc/AppRTCDemo/ARDAppClient.h @@ -41,12 +41,18 @@ typedef NS_ENUM(NSInteger, ARDAppClientState) { - (void)appClient:(ARDAppClient *)client didError:(NSError *)error; +- (void)appClient:(ARDAppClient *)client + didGetStats:(NSArray *)stats; + @end // Handles connections to the AppRTC server for a given room. Methods on this // class should only be called from the main queue. @interface ARDAppClient : NSObject +// If |shouldGetStats| is true, stats will be reported in 1s intervals through +// the delegate. +@property(nonatomic, assign) BOOL shouldGetStats; @property(nonatomic, readonly) ARDAppClientState state; @property(nonatomic, weak) id delegate; diff --git a/webrtc/examples/objc/AppRTCDemo/ARDAppClient.m b/webrtc/examples/objc/AppRTCDemo/ARDAppClient.m index bcc746050a..e53222e60e 100644 --- a/webrtc/examples/objc/AppRTCDemo/ARDAppClient.m +++ b/webrtc/examples/objc/AppRTCDemo/ARDAppClient.m @@ -33,7 +33,6 @@ #import "RTCICECandidate+JSON.h" #import "RTCSessionDescription+JSON.h" - static NSString * const kARDDefaultSTUNServerUrl = @"stun:stun.l.google.com:19302"; // TODO(tkchin): figure out a better username for CEOD statistics. @@ -49,12 +48,55 @@ static NSInteger const kARDAppClientErrorSetSDP = -4; static NSInteger const kARDAppClientErrorInvalidClient = -5; static NSInteger const kARDAppClientErrorInvalidRoom = -6; -@implementation ARDAppClient { - RTCFileLogger *_fileLogger; +// We need a proxy to NSTimer because it causes a strong retain cycle. When +// using the proxy, |invalidate| must be called before it properly deallocs. +@interface ARDTimerProxy : NSObject + +- (instancetype)initWithInterval:(NSTimeInterval)interval + repeats:(BOOL)repeats + timerHandler:(void (^)(void))timerHandler; +- (void)invalidate; + +@end + +@implementation ARDTimerProxy { + NSTimer *_timer; + void (^_timerHandler)(void); } -@synthesize delegate = _delegate; +- (instancetype)initWithInterval:(NSTimeInterval)interval + repeats:(BOOL)repeats + timerHandler:(void (^)(void))timerHandler { + NSParameterAssert(timerHandler); + if (self = [super init]) { + _timerHandler = timerHandler; + _timer = [NSTimer scheduledTimerWithTimeInterval:interval + target:self + selector:@selector(timerDidFire:) + userInfo:nil + repeats:repeats]; + } + return self; +} + +- (void)invalidate { + [_timer invalidate]; +} + +- (void)timerDidFire:(NSTimer *)timer { + _timerHandler(); +} + +@end + +@implementation ARDAppClient { + RTCFileLogger *_fileLogger; + ARDTimerProxy *_statsTimer; +} + +@synthesize shouldGetStats = _shouldGetStats; @synthesize state = _state; +@synthesize delegate = _delegate; @synthesize roomServerClient = _roomServerClient; @synthesize channel = _channel; @synthesize turnClient = _turnClient; @@ -122,9 +164,31 @@ static NSInteger const kARDAppClientErrorInvalidRoom = -6; } - (void)dealloc { + self.shouldGetStats = NO; [self disconnect]; } +- (void)setShouldGetStats:(BOOL)shouldGetStats { + if (_shouldGetStats == shouldGetStats) { + return; + } + if (shouldGetStats) { + __weak ARDAppClient *weakSelf = self; + _statsTimer = [[ARDTimerProxy alloc] initWithInterval:1 + repeats:YES + timerHandler:^{ + ARDAppClient *strongSelf = weakSelf; + [strongSelf.peerConnection getStatsWithDelegate:strongSelf + mediaStreamTrack:nil + statsOutputLevel:RTCStatsOutputLevelDebug]; + }]; + } else { + [_statsTimer invalidate]; + _statsTimer = nil; + } + _shouldGetStats = shouldGetStats; +} + - (void)setState:(ARDAppClientState)state { if (_state == state) { return; @@ -309,8 +373,17 @@ static NSInteger const kARDAppClientErrorInvalidRoom = -6; }); } -- (void)peerConnection:(RTCPeerConnection*)peerConnection - didOpenDataChannel:(RTCDataChannel*)dataChannel { +- (void)peerConnection:(RTCPeerConnection *)peerConnection + didOpenDataChannel:(RTCDataChannel *)dataChannel { +} + +#pragma mark - RTCStatsDelegate + +- (void)peerConnection:(RTCPeerConnection *)peerConnection + didGetStats:(NSArray *)stats { + dispatch_async(dispatch_get_main_queue(), ^{ + [_delegate appClient:self didGetStats:stats]; + }); } #pragma mark - RTCSessionDescriptionDelegate diff --git a/webrtc/examples/objc/AppRTCDemo/ARDBitrateTracker.h b/webrtc/examples/objc/AppRTCDemo/ARDBitrateTracker.h new file mode 100644 index 0000000000..81ac4b4bd5 --- /dev/null +++ b/webrtc/examples/objc/AppRTCDemo/ARDBitrateTracker.h @@ -0,0 +1,30 @@ +/* + * Copyright 2015 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#import + +/** Class used to estimate bitrate based on byte count. It is expected that + * byte count is monotonocially increasing. This class tracks the times that + * byte count is updated, and measures the bitrate based on the byte difference + * over the interval between updates. + */ +@interface ARDBitrateTracker : NSObject + +/** The bitrate in bits per second. */ +@property(nonatomic, readonly) double bitrate; +/** The bitrate as a formatted string in bps, Kbps or Mbps. */ +@property(nonatomic, readonly) NSString *bitrateString; + +/** Converts the bitrate to a readable format in bps, Kbps or Mbps. */ ++ (NSString *)bitrateStringForBitrate:(double)bitrate; +/** Updates the tracked bitrate with the new byte count. */ +- (void)updateBitrateWithCurrentByteCount:(NSInteger)byteCount; + +@end diff --git a/webrtc/examples/objc/AppRTCDemo/ARDBitrateTracker.m b/webrtc/examples/objc/AppRTCDemo/ARDBitrateTracker.m new file mode 100644 index 0000000000..8158229187 --- /dev/null +++ b/webrtc/examples/objc/AppRTCDemo/ARDBitrateTracker.m @@ -0,0 +1,45 @@ +/* + * Copyright 2015 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#import "ARDBitrateTracker.h" + +#import + +@implementation ARDBitrateTracker { + CFTimeInterval _prevTime; + NSInteger _prevByteCount; +} + +@synthesize bitrate = _bitrate; + ++ (NSString *)bitrateStringForBitrate:(double)bitrate { + if (bitrate > 1e6) { + return [NSString stringWithFormat:@"%.2fMbps", bitrate * 1e-6]; + } else if (bitrate > 1e3) { + return [NSString stringWithFormat:@"%.0fKbps", bitrate * 1e-3]; + } else { + return [NSString stringWithFormat:@"%.0fbps", bitrate]; + } +} + +- (NSString *)bitrateString { + return [[self class] bitrateStringForBitrate:_bitrate]; +} + +- (void)updateBitrateWithCurrentByteCount:(NSInteger)byteCount { + CFTimeInterval currentTime = CACurrentMediaTime(); + if (_prevTime && (byteCount > _prevByteCount)) { + _bitrate = (byteCount - _prevByteCount) * 8 / (currentTime - _prevTime); + } + _prevByteCount = byteCount; + _prevTime = currentTime; +} + +@end diff --git a/webrtc/examples/objc/AppRTCDemo/ARDStatsBuilder.h b/webrtc/examples/objc/AppRTCDemo/ARDStatsBuilder.h new file mode 100644 index 0000000000..29bd7fc59f --- /dev/null +++ b/webrtc/examples/objc/AppRTCDemo/ARDStatsBuilder.h @@ -0,0 +1,29 @@ +/* + * Copyright 2015 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#import + +@class RTCStatsReport; + +/** Class used to accumulate stats information into a single displayable string. + */ +@interface ARDStatsBuilder : NSObject + +/** String that represents the accumulated stats reports passed into this + * class. + */ +@property(nonatomic, readonly) NSString *statsString; + +/** Parses the information in the stats report into an appropriate internal + * format used to generate the stats string. + */ +- (void)parseStatsReport:(RTCStatsReport *)statsReport; + +@end diff --git a/webrtc/examples/objc/AppRTCDemo/ARDStatsBuilder.m b/webrtc/examples/objc/AppRTCDemo/ARDStatsBuilder.m new file mode 100644 index 0000000000..fd0b16b5d2 --- /dev/null +++ b/webrtc/examples/objc/AppRTCDemo/ARDStatsBuilder.m @@ -0,0 +1,321 @@ +/* + * Copyright 2015 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#import "ARDStatsBuilder.h" + +#import "RTCPair.h" +#import "RTCStatsReport.h" + +#import "ARDBitrateTracker.h" +#import "ARDUtilities.h" + +@implementation ARDStatsBuilder { + // Connection stats. + NSString *_connRecvBitrate; + NSString *_connRtt; + NSString *_connSendBitrate; + NSString *_localCandType; + NSString *_remoteCandType; + NSString *_transportType; + + // BWE stats. + NSString *_actualEncBitrate; + NSString *_availableRecvBw; + NSString *_availableSendBw; + NSString *_targetEncBitrate; + + // Video send stats. + NSString *_videoEncodeMs; + NSString *_videoInputFps; + NSString *_videoInputHeight; + NSString *_videoInputWidth; + NSString *_videoSendCodec; + NSString *_videoSendBitrate; + NSString *_videoSendFps; + NSString *_videoSendHeight; + NSString *_videoSendWidth; + + // Video receive stats. + NSString *_videoDecodeMs; + NSString *_videoDecodedFps; + NSString *_videoOutputFps; + NSString *_videoRecvBitrate; + NSString *_videoRecvFps; + NSString *_videoRecvHeight; + NSString *_videoRecvWidth; + + // Audio send stats. + NSString *_audioSendBitrate; + NSString *_audioSendCodec; + + // Audio receive stats. + NSString *_audioCurrentDelay; + NSString *_audioExpandRate; + NSString *_audioRecvBitrate; + NSString *_audioRecvCodec; + + // Bitrate trackers. + ARDBitrateTracker *_audioRecvBitrateTracker; + ARDBitrateTracker *_audioSendBitrateTracker; + ARDBitrateTracker *_connRecvBitrateTracker; + ARDBitrateTracker *_connSendBitrateTracker; + ARDBitrateTracker *_videoRecvBitrateTracker; + ARDBitrateTracker *_videoSendBitrateTracker; +} + +- (instancetype)init { + if (self = [super init]) { + _audioSendBitrateTracker = [[ARDBitrateTracker alloc] init]; + _audioRecvBitrateTracker = [[ARDBitrateTracker alloc] init]; + _connSendBitrateTracker = [[ARDBitrateTracker alloc] init]; + _connRecvBitrateTracker = [[ARDBitrateTracker alloc] init]; + _videoSendBitrateTracker = [[ARDBitrateTracker alloc] init]; + _videoRecvBitrateTracker = [[ARDBitrateTracker alloc] init]; + } + return self; +} + +- (NSString *)statsString { + NSMutableString *result = [NSMutableString string]; + NSString *systemStatsFormat = @"(cpu)%ld%%\n"; + [result appendString:[NSString stringWithFormat:systemStatsFormat, + (long)ARDGetCpuUsagePercentage()]]; + + // Connection stats. + NSString *connStatsFormat = @"CN %@ms | %@->%@/%@ | (s)%@ | (r)%@\n"; + [result appendString:[NSString stringWithFormat:connStatsFormat, + _connRtt, + _localCandType, _remoteCandType, _transportType, + _connSendBitrate, _connRecvBitrate]]; + + // Video send stats. + NSString *videoSendFormat = @"VS (input) %@x%@@%@fps | (sent) %@x%@@%@fps\n" + "VS (enc) %@/%@ | (sent) %@/%@ | %@ms | %@\n"; + [result appendString:[NSString stringWithFormat:videoSendFormat, + _videoInputWidth, _videoInputHeight, _videoInputFps, + _videoSendWidth, _videoSendHeight, _videoSendFps, + _actualEncBitrate, _targetEncBitrate, + _videoSendBitrate, _availableSendBw, + _videoEncodeMs, + _videoSendCodec]]; + + // Video receive stats. + NSString *videoReceiveFormat = + @"VR (recv) %@x%@@%@fps | (decoded)%@ | (output)%@fps | %@/%@ | %@ms\n"; + [result appendString:[NSString stringWithFormat:videoReceiveFormat, + _videoRecvWidth, _videoRecvHeight, _videoRecvFps, + _videoDecodedFps, + _videoOutputFps, + _videoRecvBitrate, _availableRecvBw, + _videoDecodeMs]]; + + // Audio send stats. + NSString *audioSendFormat = @"AS %@ | %@\n"; + [result appendString:[NSString stringWithFormat:audioSendFormat, + _audioSendBitrate, _audioSendCodec]]; + + // Audio receive stats. + NSString *audioReceiveFormat = @"AR %@ | %@ | %@ms | (expandrate)%@"; + [result appendString:[NSString stringWithFormat:audioReceiveFormat, + _audioRecvBitrate, _audioRecvCodec, _audioCurrentDelay, + _audioExpandRate]]; + + return result; +} + +- (void)parseStatsReport:(RTCStatsReport *)statsReport { + NSString *reportType = statsReport.type; + if ([reportType isEqualToString:@"ssrc"] && + [statsReport.reportId rangeOfString:@"ssrc"].location != NSNotFound) { + if ([statsReport.reportId rangeOfString:@"send"].location != NSNotFound) { + [self parseSendSsrcStatsReport:statsReport]; + } + if ([statsReport.reportId rangeOfString:@"recv"].location != NSNotFound) { + [self parseRecvSsrcStatsReport:statsReport]; + } + } else if ([reportType isEqualToString:@"VideoBwe"]) { + [self parseBweStatsReport:statsReport]; + } else if ([reportType isEqualToString:@"googCandidatePair"]) { + [self parseConnectionStatsReport:statsReport]; + } +} + +#pragma mark - Private + +- (void)parseBweStatsReport:(RTCStatsReport *)statsReport { + for (RTCPair *pair in statsReport.values) { + NSString *key = pair.key; + NSString *value = pair.value; + if ([key isEqualToString:@"googAvailableSendBandwidth"]) { + _availableSendBw = + [ARDBitrateTracker bitrateStringForBitrate:value.doubleValue]; + } else if ([key isEqualToString:@"googAvailableReceiveBandwidth"]) { + _availableRecvBw = + [ARDBitrateTracker bitrateStringForBitrate:value.doubleValue]; + } else if ([key isEqualToString:@"googActualEncBitrate"]) { + _actualEncBitrate = + [ARDBitrateTracker bitrateStringForBitrate:value.doubleValue]; + } else if ([key isEqualToString:@"googTargetEncBitrate"]) { + _targetEncBitrate = + [ARDBitrateTracker bitrateStringForBitrate:value.doubleValue]; + } + } +} + +- (void)parseConnectionStatsReport:(RTCStatsReport *)statsReport { + NSDictionary *values = [self dictionaryForReport:statsReport]; + NSString *activeConnection = [values[@"googActiveConnection"] firstObject]; + if (![activeConnection isEqualToString:@"true"]) { + return; + } + for (RTCPair *pair in statsReport.values) { + NSString *key = pair.key; + NSString *value = pair.value; + if ([key isEqualToString:@"googRtt"]) { + _connRtt = value; + } else if ([key isEqualToString:@"googLocalCandidateType"]) { + _localCandType = value; + } else if ([key isEqualToString:@"googRemoteCandidateType"]) { + _remoteCandType = value; + } else if ([key isEqualToString:@"googTransportType"]) { + _transportType = value; + } else if ([key isEqualToString:@"bytesReceived"]) { + NSInteger byteCount = value.integerValue; + [_connRecvBitrateTracker updateBitrateWithCurrentByteCount:byteCount]; + _connRecvBitrate = _connRecvBitrateTracker.bitrateString; + } else if ([key isEqualToString:@"bytesSent"]) { + NSInteger byteCount = value.integerValue; + [_connSendBitrateTracker updateBitrateWithCurrentByteCount:byteCount]; + _connSendBitrate = _connSendBitrateTracker.bitrateString; + } + } +} + +- (void)parseSendSsrcStatsReport:(RTCStatsReport *)statsReport { + NSDictionary *values = [self dictionaryForReport:statsReport]; + NSString *trackId = [values[@"googTrackId"] firstObject]; + if (trackId.length && [trackId hasPrefix:@"ARDAMSv0"]) { + // Video track. + [self parseVideoSendStatsReport:statsReport]; + } else { + // Audio track. + [self parseAudioSendStatsReport:statsReport]; + } +} + +- (void)parseAudioSendStatsReport:(RTCStatsReport *)statsReport { + for (RTCPair *pair in statsReport.values) { + NSString *key = pair.key; + NSString *value = pair.value; + if ([key isEqualToString:@"googCodecName"]) { + _audioSendCodec = value; + } else if ([key isEqualToString:@"bytesSent"]) { + NSInteger byteCount = value.integerValue; + [_audioSendBitrateTracker updateBitrateWithCurrentByteCount:byteCount]; + _audioSendBitrate = _audioSendBitrateTracker.bitrateString; + } + } +} + +- (void)parseVideoSendStatsReport:(RTCStatsReport *)statsReport { + for (RTCPair *pair in statsReport.values) { + NSString *key = pair.key; + NSString *value = pair.value; + if ([key isEqualToString:@"googCodecName"]) { + _videoSendCodec = value; + } else if ([key isEqualToString:@"googFrameHeightInput"]) { + _videoInputHeight = value; + } else if ([key isEqualToString:@"googFrameWidthInput"]) { + _videoInputWidth = value; + } else if ([key isEqualToString:@"googFrameRateInput"]) { + _videoInputFps = value; + } else if ([key isEqualToString:@"googFrameHeightSent"]) { + _videoSendHeight = value; + } else if ([key isEqualToString:@"googFrameWidthSent"]) { + _videoSendWidth = value; + } else if ([key isEqualToString:@"googFrameRateSent"]) { + _videoSendFps = value; + } else if ([key isEqualToString:@"googAvgEncodeMs"]) { + _videoEncodeMs = value; + } else if ([key isEqualToString:@"bytesSent"]) { + NSInteger byteCount = value.integerValue; + [_videoSendBitrateTracker updateBitrateWithCurrentByteCount:byteCount]; + _videoSendBitrate = _videoSendBitrateTracker.bitrateString; + } + } +} + +- (void)parseRecvSsrcStatsReport:(RTCStatsReport *)statsReport { + NSDictionary *values = [self dictionaryForReport:statsReport]; + NSString *transportId = [values[@"transportId"] firstObject]; + if ([values[@"googFrameWidthReceived"] firstObject]) { + [self parseVideoRecvStatsReport:statsReport]; + } else { + [self parseAudioRecvStatsReport:statsReport]; + } +} + +- (void)parseAudioRecvStatsReport:(RTCStatsReport *)statsReport { + for (RTCPair *pair in statsReport.values) { + NSString *key = pair.key; + NSString *value = pair.value; + if ([key isEqualToString:@"googCodecName"]) { + _audioRecvCodec = value; + } else if ([key isEqualToString:@"bytesReceived"]) { + NSInteger byteCount = value.integerValue; + [_audioRecvBitrateTracker updateBitrateWithCurrentByteCount:byteCount]; + _audioRecvBitrate = _audioRecvBitrateTracker.bitrateString; + } else if ([key isEqualToString:@"googSpeechExpandRate"]) { + _audioExpandRate = value; + } else if ([key isEqualToString:@"googCurrentDelayMs"]) { + _audioCurrentDelay = value; + } + } +} + +- (void)parseVideoRecvStatsReport:(RTCStatsReport *)statsReport { + for (RTCPair *pair in statsReport.values) { + NSString *key = pair.key; + NSString *value = pair.value; + if ([key isEqualToString:@"googFrameHeightReceived"]) { + _videoRecvHeight = value; + } else if ([key isEqualToString:@"googFrameWidthReceived"]) { + _videoRecvWidth = value; + } else if ([key isEqualToString:@"googFrameRateReceived"]) { + _videoRecvFps = value; + } else if ([key isEqualToString:@"googFrameRateDecoded"]) { + _videoDecodedFps = value; + } else if ([key isEqualToString:@"googFrameRateOutput"]) { + _videoOutputFps = value; + } else if ([key isEqualToString:@"googDecodeMs"]) { + _videoDecodeMs = value; + } else if ([key isEqualToString:@"bytesReceived"]) { + NSInteger byteCount = value.integerValue; + [_videoRecvBitrateTracker updateBitrateWithCurrentByteCount:byteCount]; + _videoRecvBitrate = _videoRecvBitrateTracker.bitrateString; + } + } +} + +- (NSDictionary *)dictionaryForReport:(RTCStatsReport *)statsReport { + NSMutableDictionary *dict = [NSMutableDictionary dictionary]; + for (RTCPair *pair in statsReport.values) { + NSMutableArray *values = dict[pair.key]; + if (!values) { + values = [NSMutableArray arrayWithCapacity:1]; + dict[pair.key] = values; + } + [values addObject:pair.value]; + } + return dict; +} + +@end + diff --git a/webrtc/examples/objc/AppRTCDemo/common/ARDUtilities.h b/webrtc/examples/objc/AppRTCDemo/common/ARDUtilities.h index 6f94ef7054..8a5c1262ab 100644 --- a/webrtc/examples/objc/AppRTCDemo/common/ARDUtilities.h +++ b/webrtc/examples/objc/AppRTCDemo/common/ARDUtilities.h @@ -33,3 +33,6 @@ NSData *data))completionHandler; @end + +NSInteger ARDGetCpuUsagePercentage(); + diff --git a/webrtc/examples/objc/AppRTCDemo/common/ARDUtilities.m b/webrtc/examples/objc/AppRTCDemo/common/ARDUtilities.m index 257b6a6a5c..2d03f708a7 100644 --- a/webrtc/examples/objc/AppRTCDemo/common/ARDUtilities.m +++ b/webrtc/examples/objc/AppRTCDemo/common/ARDUtilities.m @@ -10,6 +10,8 @@ #import "ARDUtilities.h" +#import + #import "RTCLogging.h" @implementation NSDictionary (ARDUtilites) @@ -93,3 +95,34 @@ } @end + +NSInteger ARDGetCpuUsagePercentage() { + // Create an array of thread ports for the current task. + const task_t task = mach_task_self(); + thread_act_array_t thread_array; + mach_msg_type_number_t thread_count; + if (task_threads(task, &thread_array, &thread_count) != KERN_SUCCESS) { + return -1; + } + + // Sum cpu usage from all threads. + float cpu_usage_percentage = 0; + thread_basic_info_data_t thread_info_data = {}; + mach_msg_type_number_t thread_info_count; + for (size_t i = 0; i < thread_count; ++i) { + thread_info_count = THREAD_BASIC_INFO_COUNT; + kern_return_t ret = thread_info(thread_array[i], + THREAD_BASIC_INFO, + (thread_info_t)&thread_info_data, + &thread_info_count); + if (ret == KERN_SUCCESS) { + cpu_usage_percentage += + 100.f * (float)thread_info_data.cpu_usage / TH_USAGE_SCALE; + } + } + + // Dealloc the created array. + vm_deallocate(task, (vm_address_t)thread_array, + sizeof(thread_act_t) * thread_count); + return lroundf(cpu_usage_percentage); +} diff --git a/webrtc/examples/objc/AppRTCDemo/ios/ARDStatsView.h b/webrtc/examples/objc/AppRTCDemo/ios/ARDStatsView.h new file mode 100644 index 0000000000..9c8636476c --- /dev/null +++ b/webrtc/examples/objc/AppRTCDemo/ios/ARDStatsView.h @@ -0,0 +1,17 @@ +/* + * Copyright 2015 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#import + +@interface ARDStatsView : UIView + +- (void)setStats:(NSArray *)stats; + +@end diff --git a/webrtc/examples/objc/AppRTCDemo/ios/ARDStatsView.m b/webrtc/examples/objc/AppRTCDemo/ios/ARDStatsView.m new file mode 100644 index 0000000000..bcc538e2dd --- /dev/null +++ b/webrtc/examples/objc/AppRTCDemo/ios/ARDStatsView.m @@ -0,0 +1,52 @@ +/* + * Copyright 2015 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#import "ARDStatsView.h" + +#import "RTCStatsReport.h" + +#import "ARDStatsBuilder.h" + +@implementation ARDStatsView { + UILabel *_statsLabel; + ARDStatsBuilder *_statsBuilder; +} + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + _statsLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + _statsLabel.numberOfLines = 0; + _statsLabel.font = [UIFont fontWithName:@"Roboto" size:12]; + _statsLabel.adjustsFontSizeToFitWidth = YES; + _statsLabel.minimumScaleFactor = 0.6; + _statsLabel.textColor = [UIColor greenColor]; + [self addSubview:_statsLabel]; + self.backgroundColor = [UIColor colorWithWhite:0 alpha:.6]; + _statsBuilder = [[ARDStatsBuilder alloc] init]; + } + return self; +} + +- (void)setStats:(NSArray *)stats { + for (RTCStatsReport *report in stats) { + [_statsBuilder parseStatsReport:report]; + } + _statsLabel.text = _statsBuilder.statsString; +} + +- (void)layoutSubviews { + _statsLabel.frame = self.bounds; +} + +- (CGSize)sizeThatFits:(CGSize)size { + return [_statsLabel sizeThatFits:size]; +} + +@end diff --git a/webrtc/examples/objc/AppRTCDemo/ios/ARDVideoCallView.h b/webrtc/examples/objc/AppRTCDemo/ios/ARDVideoCallView.h index 320892590b..209bcd462c 100644 --- a/webrtc/examples/objc/AppRTCDemo/ios/ARDVideoCallView.h +++ b/webrtc/examples/objc/AppRTCDemo/ios/ARDVideoCallView.h @@ -12,6 +12,8 @@ #import "RTCEAGLVideoView.h" +#import "ARDStatsView.h" + @class ARDVideoCallView; @protocol ARDVideoCallViewDelegate @@ -21,6 +23,9 @@ // Called when the hangup button is pressed. - (void)videoCallViewDidHangup:(ARDVideoCallView *)view; +// Called when stats are enabled by triple tapping. +- (void)videoCallViewDidEnableStats:(ARDVideoCallView *)view; + @end // Video call view that shows local and remote video, provides a label to @@ -30,6 +35,7 @@ @property(nonatomic, readonly) UILabel *statusLabel; @property(nonatomic, readonly) RTCEAGLVideoView *localVideoView; @property(nonatomic, readonly) RTCEAGLVideoView *remoteVideoView; +@property(nonatomic, readonly) ARDStatsView *statsView; @property(nonatomic, weak) id delegate; @end diff --git a/webrtc/examples/objc/AppRTCDemo/ios/ARDVideoCallView.m b/webrtc/examples/objc/AppRTCDemo/ios/ARDVideoCallView.m index 45a69cf344..4048b84bb2 100644 --- a/webrtc/examples/objc/AppRTCDemo/ios/ARDVideoCallView.m +++ b/webrtc/examples/objc/AppRTCDemo/ios/ARDVideoCallView.m @@ -17,6 +17,7 @@ static CGFloat const kButtonPadding = 16; static CGFloat const kButtonSize = 48; static CGFloat const kLocalVideoViewSize = 120; static CGFloat const kLocalVideoViewPadding = 8; +static CGFloat const kStatusBarHeight = 20; @interface ARDVideoCallView () @end @@ -32,6 +33,7 @@ static CGFloat const kLocalVideoViewPadding = 8; @synthesize statusLabel = _statusLabel; @synthesize localVideoView = _localVideoView; @synthesize remoteVideoView = _remoteVideoView; +@synthesize statsView = _statsView; @synthesize delegate = _delegate; - (instancetype)initWithFrame:(CGRect)frame { @@ -46,6 +48,10 @@ static CGFloat const kLocalVideoViewPadding = 8; _localVideoView.delegate = self; [self addSubview:_localVideoView]; + _statsView = [[ARDStatsView alloc] initWithFrame:CGRectZero]; + _statsView.hidden = YES; + [self addSubview:_statsView]; + // TODO(tkchin): don't display this if we can't actually do camera switch. _cameraSwitchButton = [UIButton buttonWithType:UIButtonTypeCustom]; _cameraSwitchButton.backgroundColor = [UIColor whiteColor]; @@ -74,6 +80,13 @@ static CGFloat const kLocalVideoViewPadding = 8; _statusLabel.font = [UIFont fontWithName:@"Roboto" size:16]; _statusLabel.textColor = [UIColor whiteColor]; [self addSubview:_statusLabel]; + + UITapGestureRecognizer *tapRecognizer = + [[UITapGestureRecognizer alloc] + initWithTarget:self + action:@selector(didTripleTap:)]; + tapRecognizer.numberOfTapsRequired = 3; + [self addGestureRecognizer:tapRecognizer]; } return self; } @@ -118,6 +131,12 @@ static CGFloat const kLocalVideoViewPadding = 8; _localVideoView.frame = bounds; } + // Place stats at the top. + CGSize statsSize = [_statsView sizeThatFits:bounds.size]; + _statsView.frame = CGRectMake(CGRectGetMinX(bounds), + CGRectGetMinY(bounds) + kStatusBarHeight, + statsSize.width, statsSize.height); + // Place hangup button in the bottom left. _hangupButton.frame = CGRectMake(CGRectGetMinX(bounds) + kButtonPadding, @@ -159,4 +178,8 @@ static CGFloat const kLocalVideoViewPadding = 8; [_delegate videoCallViewDidHangup:self]; } +- (void)didTripleTap:(UITapGestureRecognizer *)recognizer { + [_delegate videoCallViewDidEnableStats:self]; +} + @end diff --git a/webrtc/examples/objc/AppRTCDemo/ios/ARDVideoCallViewController.m b/webrtc/examples/objc/AppRTCDemo/ios/ARDVideoCallViewController.m index 36c0902981..a26068f213 100644 --- a/webrtc/examples/objc/AppRTCDemo/ios/ARDVideoCallViewController.m +++ b/webrtc/examples/objc/AppRTCDemo/ios/ARDVideoCallViewController.m @@ -87,6 +87,12 @@ _videoCallView.statusLabel.hidden = YES; } +- (void)appClient:(ARDAppClient *)client + didGetStats:(NSArray *)stats { + _videoCallView.statsView.stats = stats; + [_videoCallView setNeedsLayout]; +} + - (void)appClient:(ARDAppClient *)client didError:(NSError *)error { NSString *message = @@ -107,6 +113,11 @@ [self switchCamera]; } +- (void)videoCallViewDidEnableStats:(ARDVideoCallView *)view { + _client.shouldGetStats = YES; + _videoCallView.statsView.hidden = NO; +} + #pragma mark - Private - (void)setLocalVideoTrack:(RTCVideoTrack *)localVideoTrack { diff --git a/webrtc/examples/objc/AppRTCDemo/mac/APPRTCViewController.m b/webrtc/examples/objc/AppRTCDemo/mac/APPRTCViewController.m index 96ad7c90c1..dec41eadeb 100644 --- a/webrtc/examples/objc/AppRTCDemo/mac/APPRTCViewController.m +++ b/webrtc/examples/objc/AppRTCDemo/mac/APPRTCViewController.m @@ -270,6 +270,10 @@ static NSUInteger const kLogViewHeight = 280; [self disconnect]; } +- (void)appClient:(ARDAppClient *)client + didGetStats:(NSArray *)stats { +} + #pragma mark - APPRTCMainViewDelegate - (void)appRTCMainView:(APPRTCMainView*)mainView diff --git a/webrtc/libjingle_examples.gyp b/webrtc/libjingle_examples.gyp index 135932db62..7339cc4c55 100755 --- a/webrtc/libjingle_examples.gyp +++ b/webrtc/libjingle_examples.gyp @@ -160,6 +160,13 @@ }, }], ], + 'link_settings': { + 'xcode_settings': { + 'OTHER_LDFLAGS': [ + '-framework QuartzCore', + ], + }, + }, }, { 'target_name': 'apprtc_signaling', @@ -175,6 +182,8 @@ 'examples/objc/AppRTCDemo/ARDAppClient+Internal.h', 'examples/objc/AppRTCDemo/ARDAppEngineClient.h', 'examples/objc/AppRTCDemo/ARDAppEngineClient.m', + 'examples/objc/AppRTCDemo/ARDBitrateTracker.h', + 'examples/objc/AppRTCDemo/ARDBitrateTracker.m', 'examples/objc/AppRTCDemo/ARDCEODTURNClient.h', 'examples/objc/AppRTCDemo/ARDCEODTURNClient.m', 'examples/objc/AppRTCDemo/ARDJoinResponse.h', @@ -189,6 +198,8 @@ 'examples/objc/AppRTCDemo/ARDSignalingChannel.h', 'examples/objc/AppRTCDemo/ARDSignalingMessage.h', 'examples/objc/AppRTCDemo/ARDSignalingMessage.m', + 'examples/objc/AppRTCDemo/ARDStatsBuilder.h', + 'examples/objc/AppRTCDemo/ARDStatsBuilder.m', 'examples/objc/AppRTCDemo/ARDTURNClient.h', 'examples/objc/AppRTCDemo/ARDWebSocketChannel.h', 'examples/objc/AppRTCDemo/ARDWebSocketChannel.m', @@ -251,6 +262,8 @@ 'examples/objc/AppRTCDemo/ios/ARDMainView.m', 'examples/objc/AppRTCDemo/ios/ARDMainViewController.h', 'examples/objc/AppRTCDemo/ios/ARDMainViewController.m', + 'examples/objc/AppRTCDemo/ios/ARDStatsView.h', + 'examples/objc/AppRTCDemo/ios/ARDStatsView.m', 'examples/objc/AppRTCDemo/ios/ARDVideoCallView.h', 'examples/objc/AppRTCDemo/ios/ARDVideoCallView.m', 'examples/objc/AppRTCDemo/ios/ARDVideoCallViewController.h',