From 44f713f5c947f9d8f4c98877629503204d14eb96 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sun, 1 Nov 2020 13:27:10 -0600 Subject: [PATCH] Add a basic stats overlay --- Limelight/Stream/Connection.h | 10 ++++ Limelight/Stream/Connection.m | 59 +++++++++++++++++++ Limelight/Stream/StreamManager.h | 2 + Limelight/Stream/StreamManager.m | 21 +++++++ .../StreamFrameViewController.m | 34 ++++++++++- 5 files changed, 125 insertions(+), 1 deletion(-) diff --git a/Limelight/Stream/Connection.h b/Limelight/Stream/Connection.h index 8e7438c3..850ca506 100644 --- a/Limelight/Stream/Connection.h +++ b/Limelight/Stream/Connection.h @@ -11,6 +11,14 @@ #define CONN_TEST_SERVER "ios.conntest.moonlight-stream.org" +typedef struct { + CFTimeInterval startTime; + CFTimeInterval endTime; + int totalFrames; + int receivedFrames; + int networkDroppedFrames; +} video_stats_t; + @protocol ConnectionCallbacks - (void) connectionStarted; @@ -29,5 +37,7 @@ -(id) initWithConfig:(StreamConfiguration*)config renderer:(VideoDecoderRenderer*)myRenderer connectionCallbacks:(id)callbacks; -(void) terminate; -(void) main; +-(BOOL) getVideoStats:(video_stats_t*)stats; +-(NSString*) getActiveCodecName; @end diff --git a/Limelight/Stream/Connection.m b/Limelight/Stream/Connection.m index 86160dc4..38045432 100644 --- a/Limelight/Stream/Connection.m +++ b/Limelight/Stream/Connection.m @@ -30,6 +30,10 @@ static NSLock* initLock; static OpusMSDecoder* opusDecoder; static id _callbacks; +static int lastFrameNumber; +static int activeVideoFormat; +static video_stats_t currentVideoStats; +static video_stats_t lastVideoStats; #define OUTPUT_BUS 0 @@ -54,6 +58,10 @@ static VideoDecoderRenderer* renderer; int DrDecoderSetup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) { [renderer setupWithVideoFormat:videoFormat refreshRate:redrawRate]; + lastFrameNumber = 0; + activeVideoFormat = videoFormat; + memset(¤tVideoStats, 0, sizeof(currentVideoStats)); + memset(&lastVideoStats, 0, sizeof(lastVideoStats)); return 0; } @@ -62,6 +70,33 @@ void DrCleanup(void) [renderer cleanup]; } +-(BOOL) getVideoStats:(video_stats_t*)stats +{ + // We return lastVideoStats because it is a complete 1 second window + if (lastVideoStats.endTime != 0) { + memcpy(stats, &lastVideoStats, sizeof(*stats)); + return YES; + } + + // No stats yet + return NO; +} + +-(NSString*) getActiveCodecName +{ + switch (activeVideoFormat) + { + case VIDEO_FORMAT_H264: + return @"H.264"; + case VIDEO_FORMAT_H265: + return @"HEVC"; + case VIDEO_FORMAT_H265_MAIN10: + return @"HEVC Main 10"; + default: + return @"UNKNOWN"; + } +} + int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit) { int offset = 0; @@ -71,6 +106,30 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit) // A frame was lost due to OOM condition return DR_NEED_IDR; } + + CFTimeInterval now = CACurrentMediaTime(); + if (!lastFrameNumber) { + currentVideoStats.startTime = now; + lastFrameNumber = decodeUnit->frameNumber; + } + else { + // Flip stats roughly every second + if (now - currentVideoStats.startTime > 1.0f) { + lastVideoStats = currentVideoStats; + + memset(¤tVideoStats, 0, sizeof(currentVideoStats)); + currentVideoStats.startTime = now; + } + + // Any frame number greater than m_LastFrameNumber + 1 represents a dropped frame + currentVideoStats.networkDroppedFrames += decodeUnit->frameNumber - (lastFrameNumber + 1); + currentVideoStats.totalFrames += decodeUnit->frameNumber - (lastFrameNumber + 1); + lastFrameNumber = decodeUnit->frameNumber; + } + + currentVideoStats.receivedFrames++; + currentVideoStats.totalFrames++; + currentVideoStats.endTime = now; PLENTRY entry = decodeUnit->bufferList; while (entry != NULL) { diff --git a/Limelight/Stream/StreamManager.h b/Limelight/Stream/StreamManager.h index 44954564..806b4d4f 100644 --- a/Limelight/Stream/StreamManager.h +++ b/Limelight/Stream/StreamManager.h @@ -15,4 +15,6 @@ - (void) stopStream; +- (NSString*) getStatsOverlayText; + @end diff --git a/Limelight/Stream/StreamManager.m b/Limelight/Stream/StreamManager.m index 5cacae87..fc06e6b1 100644 --- a/Limelight/Stream/StreamManager.m +++ b/Limelight/Stream/StreamManager.m @@ -130,4 +130,25 @@ return TRUE; } +- (NSString*) getStatsOverlayText { + video_stats_t stats; + + if (!_connection) { + return nil; + } + + if (![_connection getVideoStats:&stats]) { + return nil; + } + + float interval = stats.endTime - stats.startTime; + return [NSString stringWithFormat:@"Video stream: %dx%d %.2f FPS (Codec: %@)\nIncoming frame rate from network: %.2f FPS\nFrames dropped by your network connection: %.2f%%\n", + _config.width, + _config.height, + stats.totalFrames / interval, + [_connection getActiveCodecName], + stats.receivedFrames / interval, + stats.networkDroppedFrames / interval]; +} + @end diff --git a/Limelight/ViewControllers/StreamFrameViewController.m b/Limelight/ViewControllers/StreamFrameViewController.m index 8820efd4..08964011 100644 --- a/Limelight/ViewControllers/StreamFrameViewController.m +++ b/Limelight/ViewControllers/StreamFrameViewController.m @@ -11,6 +11,7 @@ #import "VideoDecoderRenderer.h" #import "StreamManager.h" #import "ControllerSupport.h" +#import "DataManager.h" #include #include @@ -21,6 +22,7 @@ ControllerSupport *_controllerSupport; StreamManager *_streamMan; NSTimer *_inactivityTimer; + NSTimer *_statsUpdateTimer; UITapGestureRecognizer *_menuGestureRecognizer; UITapGestureRecognizer *_menuDoubleTapGestureRecognizer; UITextView *_overlayView; @@ -126,6 +128,14 @@ } } +- (void)updateStatsOverlay { + NSString* overlayText = [self->_streamMan getStatsOverlayText]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [self updateOverlayText:overlayText]; + }); +} + - (void)updateOverlayText:(NSString*)text { if (_overlayView == nil) { _overlayView = [[UITextView alloc] init]; @@ -135,7 +145,12 @@ [_overlayView setUserInteractionEnabled:NO]; [_overlayView setSelectable:NO]; [_overlayView setScrollEnabled:NO]; - [_overlayView setTextAlignment:NSTextAlignmentCenter]; + + // HACK: If not using stats overlay, center the text + if (_statsUpdateTimer == nil) { + [_overlayView setTextAlignment:NSTextAlignmentCenter]; + } + [_overlayView setTextColor:[OSColor lightGrayColor]]; [_overlayView setBackgroundColor:[OSColor blackColor]]; #if TARGET_OS_TV @@ -159,6 +174,9 @@ } - (void) returnToMainFrame { + [_statsUpdateTimer invalidate]; + _statsUpdateTimer = nil; + [self.navigationController popToRootViewControllerAnimated:YES]; } @@ -223,6 +241,15 @@ self.tipLabel.hidden = YES; [self->_streamView showOnScreenControls]; + + TemporarySettings* settings = [[[DataManager alloc] init] getSettings]; + if (settings.statsOverlay) { + self->_statsUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:1.0f + target:self + selector:@selector(updateStatsOverlay) + userInfo:nil + repeats:YES]; + } }); } @@ -347,6 +374,11 @@ - (void)connectionStatusUpdate:(int)status { Log(LOG_W, @"Connection status update: %d", status); + // The stats overlay takes precedence over these warnings + if (_statsUpdateTimer != nil) { + return; + } + dispatch_async(dispatch_get_main_queue(), ^{ switch (status) { case CONN_STATUS_OKAY: