Fix thread sync

Reformat Metal files

Rewrite Metal render loop to not use DisplayLink so we can use the amazing afterMinimumDuration API call

Improve frame drop algorithm

Misc fixes to HDR <-> SDR switching, deprecation warning cleanup

Fix AVSB mode

Fix renderingBackend selected value

Add Rendering Backend setting to iPhone

Let users choose which renderer to use. Refactor metrics into a singleton

Fix tvOS build

Port moonlight-qt's Metal renderer

Bump imgui to latest

Misc tweaks and refactoring

Switch iOS to use Opus float32 audio, in line with other clients.  It was already being sent from the server as float32, so this is simply decoding it with less loss of dynamic range.

Give FrameQueue a faster data structure and full test coverage. Remove pts frame pacing, it's not a fit for iOS.

Don't allow very transparent opacity for middle stats overlay box

Change wording to select button instead of remote button

Add tvOS gesture for double-clicking remote button to show/hide stats

Remove kVTDecodeFrame_1xRealTimePlayback, this was found to seriously impact decode times

Move decode time stat out of async block

Enabe FQ logging

Misc cleanup/fixes

iPad: 26 will require UIRequiresFullScreen to be false, and for all orientations to be supported

Updated iPhone and iPad settings panels

Add setting for graph opacity. Enable stats and graphs to be toggled via two finger swipe

Fix wait time in displaylink, fix settings spacing, stats fixes

Settings for frame queue size, graphs. Misc refactoring and cleanup

Bump moonlight-common-c

Improve logging readability

Fix host frametime and frame queue stats to be recorded during input not output. Show min/max/avg decode time

Add bytes per frame graph

Started on settings changes, both modes working very well

Misc refactoring, move graphs around, both pacing functions working well

Set frameDropTarget to 2 always

Use raw RTP timestamps without converting to floating point for even more accurate playback

Standard frame pacing: remove +3 logic

Switch to NSRunLoopCommonModes for displayLink

Add frame pacing algorithm based on moonlight-qt's Pacer class as a simpler baseline

Switch to os_unfair_lock, misc tweaks

Large refactor that now uses VTDecompressionSession based on PR535 from felipejfc. This now allows more control over queue size and allows dropping arbitrary frames, but introduces a lot of new problems

bump moonlight-common-c to fix 32-bit rollover issue

LiWait timing graph

Add displaylink interval graph

Graphs checkpoint

ImGui graph working checkpoint

ImGui working checkpoint

Add pending frames to stats, add a define for verbose logging

DisplayLink WIP

Add imgui & implot modules

Frame pacing: alternate method based on thieman's pullFrames thread

Frame pacing: simplify displayLink, support passing targetTimestamp down to the sample buffer

Point to my common-c with some timing changes.

Add stats gestures: 2-finger swipe down to show stats, and 2-finger swipe up to hide

Limit refresh rate to 60 when in low power mode

Display screen refresh rate in stats overlay, remove decode time and explain why it can't be measured

Adjust the refresh rate of the display to match the stream framerate and improve battery life

Don't use presentationTimeMs when creating CMSampleTimingInfo

Fix overlayView build for iOS

Refactor CADisplayLink callback for improved frame delivery. Support a user-configurable buffered frame count.

[opt] Use UILabel for stats overlay
This commit is contained in:
Travis Thieman authored and Acaki committed 2025-07-11 21:02:13 +08:00
1 parent 51e915b598
commit df2b67ac99
61 files changed
+6080 -458

No files matched your search

+5
View File
@@ -65,27 +65,32 @@ static NSString* DB_NAME = @"Limelight_iOS.sqlite";
{
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
Log(LOG_I, @"TODO: applicationWillResignActive");
}
- (void)applicationDidEnterBackground:(UIApplication *)application
{
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
Log(LOG_I, @"TODO: applicationDidEnterBackground");
}
- (void)applicationWillEnterForeground:(UIApplication *)application
{
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
Log(LOG_I, @"TODO: applicationWillEnterForeground");
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
Log(LOG_I, @"TODO: applicationDidBecomeActive");
}
- (void)applicationWillTerminate:(UIApplication *)application
{
// Saves changes in the application's managed object context before the application terminates.
Log(LOG_I, @"applicationWillTerminate, calling saveContext");
[self saveContext];
}
+13 -1
View File
@@ -82,6 +82,7 @@
preferredCodec:(uint32_t)preferredCodec
enableYUV444:(BOOL)enableYUV444
useFramePacing:(BOOL)useFramePacing
frameQueueSize:(NSInteger)frameQueueSize
enableHdr:(BOOL)enableHdr
btMouseSupport:(BOOL)btMouseSupport
// absoluteTouchMode:(BOOL)absoluteTouchMode
@@ -93,6 +94,12 @@
externalDisplayMode:(NSInteger)externalDisplayMode
localMousePointerMode:(NSInteger)localMousePointerMode{
absoluteTouchMode:(BOOL)absoluteTouchMode
statsOverlay:(BOOL)statsOverlay
enableGraphs:(BOOL)enableGraphs
graphOpacity:(NSInteger)graphOpacity
renderingBackend:(NSInteger)renderingBackend
{
[_managedObjectContext performBlockAndWait:^{
Settings* settingsToSave = [self retrieveSettings];
settingsToSave.framerate = [NSNumber numberWithInteger:framerate];
@@ -121,7 +128,7 @@
settingsToSave.playAudioOnPC = audioOnPC;
settingsToSave.preferredCodec = preferredCodec;
settingsToSave.enableYUV444 = enableYUV444;
settingsToSave.useFramePacing = useFramePacing;
settingsToSave.frameQueueSize = [NSNumber numberWithInteger:frameQueueSize];
settingsToSave.enableHdr = enableHdr;
settingsToSave.btMouseSupport = btMouseSupport;
// settingsToSave.absoluteTouchMode = absoluteTouchMode;
@@ -132,6 +139,11 @@
settingsToSave.resolutionSelected = [NSNumber numberWithInteger:resolutionSelected];
settingsToSave.externalDisplayMode = [NSNumber numberWithInteger:externalDisplayMode];
settingsToSave.localMousePointerMode = [NSNumber numberWithInteger:localMousePointerMode];
settingsToSave.absoluteTouchMode = absoluteTouchMode;
settingsToSave.statsOverlay = statsOverlay;
settingsToSave.enableGraphs = enableGraphs;
settingsToSave.graphOpacity = [NSNumber numberWithInteger:graphOpacity];
settingsToSave.renderingBackend = [NSNumber numberWithInteger:renderingBackend];
[self saveData];
}];
}
+11 -5
View File
@@ -35,13 +35,13 @@
self.bitrate = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"bitrate"]];
assert([self.bitrate intValue] != 0);
self.framerate = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"framerate"]];
assert([self.framerate intValue] != 0);
self.framerate = [NSNumber numberWithDouble:[[NSUserDefaults standardUserDefaults] doubleForKey:@"framerate"]];
assert([self.framerate doubleValue] != 0.0);
self.audioConfig = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"audioConfig"]];
assert([self.audioConfig intValue] != 0);
self.preferredCodec = (typeof(self.preferredCodec))[[NSUserDefaults standardUserDefaults] integerForKey:@"preferredCodec"];
self.enableYUV444 = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableYUV444"];
self.useFramePacing = [[NSUserDefaults standardUserDefaults] integerForKey:@"useFramePacing"] != 0;
self.frameQueueSize = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"frameQueueSize"]];
self.playAudioOnPC = [[NSUserDefaults standardUserDefaults] boolForKey:@"audioOnPC"];
self.enableHdr = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableHdr"];
self.optimizeGames = [[NSUserDefaults standardUserDefaults] boolForKey:@"optimizeGames"];
@@ -49,7 +49,10 @@
self.swapABXYButtons = [[NSUserDefaults standardUserDefaults] boolForKey:@"swapABXYButtons"];
self.btMouseSupport = [[NSUserDefaults standardUserDefaults] boolForKey:@"btMouseSupport"];
self.statsOverlay = [[NSUserDefaults standardUserDefaults] boolForKey:@"statsOverlay"];
self.enableGraphs = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableGraphs"];
self.graphOpacity = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"graphOpacity"]];
self.renderingBackend = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"renderingBackend"]];
NSInteger _screenSize = [[NSUserDefaults standardUserDefaults] integerForKey:@"streamResolution"];
switch (_screenSize) {
case 0:
@@ -82,7 +85,7 @@
self.audioConfig = settings.audioConfig;
self.preferredCodec = settings.preferredCodec;
self.enableYUV444 = settings.enableYUV444;
self.useFramePacing = settings.useFramePacing;
self.frameQueueSize = settings.frameQueueSize;
self.playAudioOnPC = settings.playAudioOnPC;
self.enableHdr = settings.enableHdr;
self.optimizeGames = settings.optimizeGames;
@@ -112,6 +115,9 @@
self.resolutionSelected = settings.resolutionSelected;
self.externalDisplayMode = settings.externalDisplayMode;
self.localMousePointerMode = settings.localMousePointerMode;
self.enableGraphs = settings.enableGraphs;
self.graphOpacity = settings.graphOpacity;
self.renderingBackend = settings.renderingBackend;
#endif
self.uniqueId = settings.uniqueId;
+1 -1
View File
@@ -75,7 +75,7 @@
if ([[event allTouches] count] > 1) {
return;
}
UITouch* touch = [touches anyObject];
CGPoint touchLocation = [touch locationInView:streamView];
+30 -30
View File
@@ -34,7 +34,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
@implementation StreamView {
UIView* streamFrameTopLayerView;
TemporarySettings* settings;
OnScreenControls* onScreenControls;
KeyboardInputField* keyInputField;
@@ -43,7 +43,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
float streamAspectRatio;
// iOS 13.4 mouse support
NSInteger lastMouseButtonMask;
float lastMouseX;
@@ -56,7 +56,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
double accumulatedMouseDeltaY;
int localMousePointerMode;
UIResponder* touchHandler;
id<UserInteractionDelegate> interactionDelegate;
@@ -72,7 +72,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
#endif
CGFloat HeightViewLiftedTo;
UILabel* keyboardToggleTip;
UIKeyModifierFlags comboKeyModifierFlags;
}
@@ -87,9 +87,9 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
self->interactionDelegate = interactionDelegate;
self->streamAspectRatio = (float)streamConfig.width / (float)streamConfig.height;
self.streamAspectRatio = self->streamAspectRatio;
settings = [[[DataManager alloc] init] getSettings];
localMousePointerMode = streamConfig.localMousePointerMode;
keysDown = [[NSMutableSet alloc] init];
@@ -114,18 +114,18 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
keyboardToggleTip.layer.cornerRadius = 10;
keyboardToggleTip.clipsToBounds = true;
NSLog(@"setup streamView %f", CACurrentMediaTime());
// if(settings.touchMode.intValue == NativeTouchOnly) [self addGestureRecognizer:keyboardToggleRecognizer]; //keep legacy approach in pure native mode
// else [self->streamFrameTopLayerView addGestureRecognizer:keyboardToggleRecognizer]; //add to the superview in other modes
// [self->streamFrameTopLayerView addGestureRecognizer:keyboardToggleRecognizer]; //add to the superview in other modes
#if TARGET_OS_TV
// tvOS requires RelativeTouchHandler to manage Apple Remote input
self->touchHandler = [[RelativeTouchHandler alloc] initWithView:self];
#else
// iOS uses touch Mode depending on user preference
switch (settings.touchMode.intValue) {
case NativeTouch:
keyboardToggleRecognizer.immediateTriggering = false;
@@ -142,11 +142,11 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
self->touchHandler = [[AbsoluteTouchHandler alloc] initWithView:self];
keyboardToggleRecognizer.immediateTriggering = true; //triggers signal in touchesBegan callback stage
break;
default:
break;
}
// we'll render on-screen controls on the toplayer too:
onScreenControls = [[OnScreenControls alloc] initWithView:self->streamFrameTopLayerView controllerSup:controllerSupport streamConfig:streamConfig]; // don't delete, this is mandatory
/*
@@ -155,15 +155,15 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
RelativeTouchHandler* relativeTouchHandler = (RelativeTouchHandler*) touchHandler;
onScreenControls.mouseRightClickTapRecognizer = relativeTouchHandler.mouseRightClickTapRecognizer;
} */
OnScreenControlsLevel level = (OnScreenControlsLevel)[settings.onscreenControls integerValue];
if (settings.touchMode.intValue != RelativeTouch && settings.touchMode.intValue != NativeTouch ) {
Log(LOG_I, @"On-screen controls disabled in non-relative touch mode");
[onScreenControls setLevel:OnScreenControlsLevelOff];
//pass touchesCaptureByOnScreenButtons Set to the native touchhandler, this NSSet is init witihin onscreencontrols class, don't do it again in native touch handler class
[OnScreenControls.touchAddrsCapturedByOnScreenControls removeAllObjects]; // reset the attribute to nil
/*
if(settings.touchMode.intValue == NativeTouch){
NativeTouchHandler* nativeTouchHandler = (NativeTouchHandler* )touchHandler;
@@ -302,7 +302,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
UIBarButtonItem *altBarButton = [self createButtonWithImageNamed:@"AltIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x12 isToggleable:YES];
UIBarButtonItem *deleteBarButton = [self createButtonWithImageNamed:@"DeleteIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x2E isToggleable:NO];
UIBarButtonItem *flexibleSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
[customToolbarView setItems:[NSArray arrayWithObjects:doneBarButton, windowsBarButton, escapeBarButton, tabBarButton, shiftBarButton, controlBarButton, altBarButton, deleteBarButton, flexibleSpace, nil]];
keyInputField.inputAccessoryView = customToolbarView;
}
@@ -311,7 +311,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
[keyInputField addTarget:self action:@selector(onKeyboardPressed:) forControlEvents:UIControlEventEditingChanged];
// Undo causes issues for our state management, so turn it off
[keyInputField.undoManager disableUndoRegistration];
//[keyboardToggleTip removeFromSuperview];
}
}
@@ -384,13 +384,13 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
- (void) reloadOnScreenControlsWith:(ControllerSupport*)controllerSupport
andConfig:(StreamConfiguration*)streamConfig {
// we'll render on-screen controllers on the toplayer too.
onScreenControls = [[OnScreenControls alloc] initWithView:self->streamFrameTopLayerView controllerSup:controllerSupport streamConfig:streamConfig];
/*
// pass mouseRightClickTapRecognizer to onScreenControls obj here:
if([self isOscEnabled]){
RelativeTouchHandler* relativeTouchHandler = (RelativeTouchHandler *)touchHandler;
onScreenControls.mouseRightClickTapRecognizer = relativeTouchHandler.mouseRightClickTapRecognizer;
} */
@@ -417,16 +417,16 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
}
- (void) reloadOnScreenWidgetViews{
// NSLog(@"reload on screen keyboard buttons here");
// remove all keyboard widget views first
[self clearOnScreenKeyboardButtons];
// bool customOscEnabled = [self isOscEnabled] && settings.onscreenControls.intValue == OnScreenControlsLevelCustom;
if(![self isOnScreenButtonEnabled]) return;
OSCProfilesManager* profilesManager = [OSCProfilesManager sharedManager: self.bounds];
OSCProfile *oscProfile = [profilesManager getSelectedProfile]; //returns the currently selected OSCProfile
@@ -629,7 +629,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
}
}
}
#endif
if ([self handleMouseButtonEvent:BUTTON_ACTION_PRESS
forTouches:touches
@@ -709,8 +709,8 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
}
- (BOOL)handleMouseButtonEvent:(int)buttonAction forTouches:(NSSet *)touches withEvent:(UIEvent *)event {
// NSLog(@"mouse click time: %f", CACurrentMediaTime()*1000);
#if !TARGET_OS_TV
if (@available(iOS 13.4, *)) {
@@ -774,12 +774,12 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
#if !TARGET_OS_TV
if (settings.touchMode.intValue == NativeTouchOnly) {
[touchHandler touchesMoved:touches withEvent:event];
return; //This is a native touch oriented fork, in pure native touch mode, this call back method deals with native touch only.
}
for (UITouch* touch in touches) {
if (touch.type == UITouchTypePencil) {
if ([self sendStylusEvent:touch]) return;
@@ -805,7 +805,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
}
#endif
}
hasUserInteracted = YES;
if(self->settings.touchMode.intValue == NativeTouch || self->settings.touchMode.intValue == RelativeTouch){
@@ -883,7 +883,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
[touchHandler touchesEnded:touches withEvent:event];
return; //This is a native touch oriented fork, in pure native touch mode, this call back method deals with native touch only.
}
if (@available(iOS 13.4, *)){ //now only pencil events are restricted
for (UITouch* touch in touches) {
if (touch.type == UITouchTypePencil) {
+7 -1
View File
@@ -8,7 +8,7 @@
#include <TargetConditionals.h>
#ifndef __IPHONE_3_0
#warning "This project uses features only available in iOS SDK 3.0 and later."
#error "This project uses features only available in iOS SDK 3.0 and later."
#endif
#ifdef __OBJC__
@@ -20,5 +20,11 @@
#endif
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
#if !defined(NDEBUG)
// # define FRAME_QUEUE_VERBOSE
#endif
#import "Logger.h"
#endif
#import "MetalConfig.h"
@@ -25,12 +25,16 @@
<attribute name="audioConfig" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
<attribute name="btMouseSupport" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="deviceGyroMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="enableGraphs" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="enableYUV444" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="externalDisplayMode" optional="YES" attributeType="Integer 16" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="frameQueueSize" optional="YES" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
<attribute name="gyroMode" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="gyroSensitivity" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="graphOpacity" optional="YES" attributeType="Integer 32" defaultValueString="50" usesScalarValueType="NO" syncable="YES"/>
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
<attribute name="keyboardToggleFingers" optional="YES" attributeType="Integer 16" defaultValueString="3" usesScalarValueType="NO" syncable="YES"/>
<attribute name="liftStreamViewForKeyboard" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
@@ -50,8 +54,8 @@
<attribute name="showKeyboardToolbar" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="slideToSettingsDistance" optional="YES" attributeType="Float" defaultValueString="0.25" usesScalarValueType="NO" syncable="YES"/>
<attribute name="slideToSettingsScreenEdge" optional="YES" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
<attribute name="statsOverlayEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="statsOverlayLevel" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="renderingBackend" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="statsOverlay" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="swapABXYButtons" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="touchMode" optional="YES" attributeType="Integer 16" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="touchMoveEventInterval" optional="YES" attributeType="Integer 16" defaultValueString="320" usesScalarValueType="NO" syncable="YES"/>
+4 -12
View File
@@ -6,28 +6,20 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
#import "ConnectionCallbacks.h"
#import "VideoDecoderRenderer.h"
#import "StreamConfiguration.h"
#import "BandwidthTracker.h"
#import "Plot.h"
#define CONN_TEST_SERVER "ios.conntest.moonlight-stream.org"
typedef struct {
CFTimeInterval startTime;
CFTimeInterval endTime;
int totalFrames;
int receivedFrames;
int networkDroppedFrames;
int totalHostProcessingLatency;
int framesWithHostProcessingLatency;
int maxHostProcessingLatency;
int minHostProcessingLatency;
} video_stats_t;
@interface Connection : NSOperation <NSStreamDelegate>
-(id) initWithConfig:(StreamConfiguration*)config renderer:(VideoDecoderRenderer*)myRenderer connectionCallbacks:(id<ConnectionCallbacks>)callbacks;
-(void) terminate;
-(void) main;
-(BandwidthTracker *) getBwTracker;
-(BOOL) getVideoStats:(video_stats_t*)stats;
-(NSString*) getActiveCodecName;
+58 -21
View File
@@ -7,6 +7,7 @@
//
#import "Connection.h"
#import "Plot.h"
#import "Utils.h"
#import <VideoToolbox/VideoToolbox.h>
@@ -45,24 +46,28 @@ static int audioFrameSize;
static VideoDecoderRenderer* renderer;
static BandwidthTracker *bwTracker;
int DrDecoderSetup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags)
{
[renderer setupWithVideoFormat:videoFormat width:width height:height frameRate:redrawRate];
lastFrameNumber = 0;
activeVideoFormat = videoFormat;
Log(LOG_I, @"Active video format: 0x%x", activeVideoFormat);
memset(&currentVideoStats, 0, sizeof(currentVideoStats));
memset(&lastVideoStats, 0, sizeof(lastVideoStats));
bwTracker = [[BandwidthTracker alloc] initWithWindowSeconds:10 bucketIntervalMs:250];
return 0;
}
void DrStart(void)
void DrCleanup(void)
{
[renderer start];
[renderer cleanup];
}
void DrStop(void)
-(BandwidthTracker *) getBwTracker
{
[renderer stop];
return bwTracker;
}
-(BOOL) getVideoStats:(video_stats_t*)stats
@@ -72,10 +77,12 @@ void DrStop(void)
if (lastVideoStats.endTime != 0) {
memcpy(stats, &lastVideoStats, sizeof(*stats));
[videoStatsLock unlock];
// Pull in the separately-collected renderer stats
[renderer getAllStats:stats];
return YES;
}
// No stats yet
[videoStatsLock unlock];
return NO;
}
@@ -133,6 +140,8 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
{
int offset = 0;
int ret;
CFTimeInterval decodeStartTime = CACurrentMediaTime();
unsigned char* data = (unsigned char*) malloc(decodeUnit->fullLength);
if (data == NULL) {
// A frame was lost due to OOM condition
@@ -158,8 +167,13 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
}
// Any frame number greater than m_LastFrameNumber + 1 represents a dropped frame
currentVideoStats.networkDroppedFrames += decodeUnit->frameNumber - (lastFrameNumber + 1);
currentVideoStats.totalFrames += decodeUnit->frameNumber - (lastFrameNumber + 1);
int droppedFrames = decodeUnit->frameNumber - (lastFrameNumber + 1);
if (droppedFrames > 0) {
currentVideoStats.networkDroppedFrames += droppedFrames;
currentVideoStats.totalFrames += droppedFrames;
Log(LOG_W, @"Network dropped %d frame(s): %d - %d", droppedFrames, lastFrameNumber + 1, decodeUnit->frameNumber - 1);
}
lastFrameNumber = decodeUnit->frameNumber;
}
@@ -179,6 +193,8 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
currentVideoStats.receivedFrames++;
currentVideoStats.totalFrames++;
[bwTracker addBytes:decodeUnit->fullLength];
PLENTRY entry = decodeUnit->bufferList;
while (entry != NULL) {
// Submit parameter set NALUs directly since no copy is required by the decoder
@@ -186,7 +202,8 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
ret = [renderer submitDecodeBuffer:(unsigned char*)entry->data
length:entry->length
bufferType:entry->bufferType
decodeUnit:decodeUnit];
decodeUnit:decodeUnit
decodeStartTime:decodeStartTime];
if (ret != DR_OK) {
free(data);
return ret;
@@ -204,7 +221,8 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
return [renderer submitDecodeBuffer:data
length:offset
bufferType:BUFFER_TYPE_PICDATA
decodeUnit:decodeUnit];
decodeUnit:decodeUnit
decodeStartTime:decodeStartTime];
}
int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int flags)
@@ -219,7 +237,7 @@ int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, v
SDL_zero(want);
want.freq = opusConfig->sampleRate;
want.format = AUDIO_S16;
want.format = AUDIO_F32;
want.channels = opusConfig->channelCount;
want.samples = opusConfig->samplesPerFrame;
@@ -231,7 +249,7 @@ int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, v
}
audioConfig = *opusConfig;
audioFrameSize = opusConfig->samplesPerFrame * sizeof(short) * opusConfig->channelCount;
audioFrameSize = opusConfig->samplesPerFrame * sizeof(float) * opusConfig->channelCount;
audioBuffer = SDL_malloc(audioFrameSize);
if (audioBuffer == NULL) {
Log(LOG_E, @"Failed to allocate audio frame buffer");
@@ -289,19 +307,23 @@ void ArDecodeAndPlaySample(char* sampleData, int sampleLength)
if (LiGetPendingAudioDuration() > 30) {
return;
}
decodeLen = opus_multistream_decode(opusDecoder, (unsigned char *)sampleData, sampleLength,
(short*)audioBuffer, audioConfig.samplesPerFrame, 0);
decodeLen = opus_multistream_decode_float(opusDecoder,
(unsigned char*)sampleData,
sampleLength,
(float*)audioBuffer,
audioConfig.samplesPerFrame,
0);
if (decodeLen > 0) {
// Provide backpressure on the queue to ensure too many frames don't build up
// in SDL's audio queue.
while (SDL_GetQueuedAudioSize(audioDevice) / audioFrameSize > 10) {
SDL_Delay(1);
[NSThread sleepForTimeInterval:0.001f];
}
if (SDL_QueueAudio(audioDevice,
audioBuffer,
sizeof(short) * decodeLen * audioConfig.channelCount) < 0) {
sizeof(float) * decodeLen * audioConfig.channelCount) < 0) {
Log(LOG_E, @"Failed to queue audio sample: %s\n", SDL_GetError());
}
}
@@ -435,6 +457,19 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
renderer = myRenderer;
_callbacks = callbacks;
// Check for low power mode which limits framerate to 60
if (config.frameRate > 60 && [[NSProcessInfo processInfo] isLowPowerModeEnabled]) {
Log(LOG_W, @"Limiting stream to 60fps because device is in low power mode");
config.frameRate = 60;
}
// Lower to 90fps on Vision Pro
NSInteger deviceFps = UIScreen.mainScreen.maximumFramesPerSecond;
if (deviceFps < config.frameRate) {
Log(LOG_W, @"Limiting stream to %dfps due to max refresh rate", deviceFps);
config.frameRate = (int)deviceFps;
}
LiInitializeStreamConfiguration(&_streamConfig);
_streamConfig.width = config.width;
_streamConfig.height = config.height;
@@ -442,7 +477,7 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
_streamConfig.bitrate = config.bitRate;
_streamConfig.supportedVideoFormats = config.supportedVideoFormats;
_streamConfig.audioConfiguration = config.audioConfiguration;
// Since we require iOS 12 or above, we're guaranteed to be running
// on a 64-bit device with ARMv8 crypto instructions, so we don't
// need to check for that here.
@@ -466,9 +501,9 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
LiInitializeVideoCallbacks(&_drCallbacks);
_drCallbacks.setup = DrDecoderSetup;
_drCallbacks.start = DrStart;
_drCallbacks.stop = DrStop;
_drCallbacks.capabilities = CAPABILITY_PULL_RENDERER |
_drCallbacks.cleanup = DrCleanup;
_drCallbacks.submitDecodeUnit = DrSubmitDecodeUnit;
_drCallbacks.capabilities = CAPABILITY_DIRECT_SUBMIT |
CAPABILITY_REFERENCE_FRAME_INVALIDATION_HEVC |
CAPABILITY_REFERENCE_FRAME_INVALIDATION_AV1;
@@ -484,7 +519,9 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
_clCallbacks.stageFailed = ClStageFailed;
_clCallbacks.connectionStarted = ClConnectionStarted;
_clCallbacks.connectionTerminated = ClConnectionTerminated;
#ifdef DEBUG
_clCallbacks.logMessage = ClLogMessage;
#endif
_clCallbacks.rumble = ClRumble;
_clCallbacks.connectionStatusUpdate = ClConnectionStatusUpdate;
_clCallbacks.setHdrMode = ClSetHdrMode;
+5
View File
@@ -6,6 +6,8 @@
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
//
#import "Plot.h"
@protocol ConnectionCallbacks <NSObject>
- (void) connectionStarted;
@@ -22,4 +24,7 @@
- (void) setControllerLed:(uint16_t)controllerNumber r:(uint8_t)r g:(uint8_t)g b:(uint8_t)b;
- (void) videoContentShown;
- (void) observeFloat:(int)plotId value:(CFTimeInterval)value;
- (void) observeFloatReturnMetrics:(int)plotId value:(CFTimeInterval)value plotMetrics:(PlotMetrics *)plotMetrics;
@end
+1 -1
View File
@@ -23,6 +23,7 @@
@property int width;
@property int height;
@property int frameRate;
@property int frameRateX100;
@property int bitRate;
@property int riKeyId;
@property NSData* riKey;
@@ -35,7 +36,6 @@
@property int audioConfiguration;
@property int supportedVideoFormats;
@property BOOL multiController;
@property BOOL useFramePacing;
@property NSData* serverCert;
@property int localMousePointerMode;
+25 -5
View File
@@ -12,6 +12,7 @@
#import "StreamManager.h"
#import "CryptoManager.h"
#import "HttpManager.h"
#import "Plot.h"
#import "Utils.h"
#import "StreamView.h"
@@ -103,7 +104,7 @@
// Initializing the renderer must be done on the main thread
dispatch_async(dispatch_get_main_queue(), ^{
VideoDecoderRenderer* renderer = [[VideoDecoderRenderer alloc] initWithView:self->_renderView callbacks:self->_callbacks streamAspectRatio:(float)self->_config.width / (float)self->_config.height useFramePacing:self->_config.useFramePacing];
VideoDecoderRenderer* renderer = [[VideoDecoderRenderer alloc] initWithView:self->_renderView callbacks:self->_callbacks streamAspectRatio:(float)self->_config.width / (float)self->_config.height];
self->_connection = [[Connection alloc] initWithConfig:self->_config renderer:renderer connectionCallbacks:self->_callbacks];
NSOperationQueue* opQueue = [[NSOperationQueue alloc] init];
[opQueue addOperation:self->_connection];
@@ -155,7 +156,7 @@
video_stats_t stats;
// NSLog(@"overlayLevel: %d", overlayLevel);
if (!_connection) {
return nil;
}
@@ -182,22 +183,41 @@
(float)stats.totalHostProcessingLatency / stats.framesWithHostProcessingLatency / 10.f];
}
else {
hostProcessingString = @"";
// If all frames are duplicates this can happen, but let's avoid having the whole stats area change height
hostProcessingString = @"Host processing latency min/max/avg: -/-/- ms\n";
}
float interval = stats.endTime - stats.startTime;
if(overlayLevel == 2) return [LocalizationHelper localizedStringForKey:@"Video stream: %dx%d %.2f FPS (Codec: %@)\nNetwork dropped frames: %.2f%%\nAverage network latency: %@%@",
float scalePlotMetrics = stats.frameDropMetrics.nsamples > 0 ? ((float)stats.frameDropMetrics.nsamples / stats.totalFrames) : 1.0f;
float fps = (stats.totalFrames - stats.networkDroppedFrames - (stats.frameDropMetrics.total / scalePlotMetrics)) / interval;
double avgVideoMbps = [_connection getBwTracker].averageMbps;
double peakVideoMbps = [_connection getBwTracker].peakMbps;
return [NSString stringWithFormat:@"Video stream: %dx%d %.2f FPS (Codec: %@)\n"
"Bitrate: %.1f Mbps, Peak: %.1f, Renderer: %@\n"
"%@"
"Frames buffered: %.1f\n"
"Frames dropped by network/pacing jitter: %.1f%% / %.1f%%\n"
"Average network latency: %@\n"
"Decode time: %.2f/%.2f/%.2f ms",
_config.width,
_config.height,
stats.totalFrames / interval,
fps,
[_connection getActiveCodecName],
stats.networkDroppedFrames / interval,
avgVideoMbps, peakVideoMbps, (stats.renderingBackend == RENDER_METAL) ? @"Metal" : @"AVSampleBuffer",
hostProcessingString,
stats.frameQueueMetrics.avg,
(stats.networkDroppedFrames / stats.totalFrames) * 100.0,
stats.frameDropMetrics.nsamples > 0 ? (stats.frameDropMetrics.total / stats.frameDropMetrics.nsamples) * 100.0 : 0.0f,
latencyString,
hostProcessingString];
else return [LocalizationHelper localizedStringForKey:@"FPS: %5.2f Network dropped frames: %.2f%% Network latency: %@",
stats.totalFrames / interval,
stats.networkDroppedFrames / interval,
latencyString];
stats.decodeMetrics.min, stats.decodeMetrics.max, stats.decodeMetrics.avg];
}
@end
+21 -4
View File
@@ -9,18 +9,35 @@
@import AVFoundation;
#import "ConnectionCallbacks.h"
#import "FrameQueue.h"
#import "Plot.h"
#include "Limelight.h"
@interface VideoDecoderRenderer : NSObject
- (id)initWithView:(UIView*)view callbacks:(id<ConnectionCallbacks>)callbacks streamAspectRatio:(float)aspectRatio useFramePacing:(BOOL)useFramePacing;
@property (atomic, readonly) PlotMetrics decodeMetrics;
@property (atomic, readonly) PlotMetrics frameQueueMetrics;
- (id)initWithView:(UIView*)view callbacks:(id<ConnectionCallbacks>)callbacks streamAspectRatio:(float)aspectRatio;
- (void)setupWithVideoFormat:(int)videoFormat width:(int)videoWidth height:(int)videoHeight frameRate:(int)frameRate;
- (void)start;
- (void)stop;
- (void)renderFrame:(Frame *)frame atTime:(CMTime)targetTime;
- (void)cleanup;
- (void)setHdrMode:(BOOL)enabled;
- (void)safeCopyMetricsTo:(PlotMetrics *)dst from:(PlotMetrics *)src;
- (void)getAllStats:(video_stats_t *)stats;
- (void)optimizeRefreshRate;
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length bufferType:(int)bufferType decodeUnit:(PDECODE_UNIT)du;
- (int)submitDecodeBuffer:(unsigned char *)data
length:(int)length
bufferType:(int)bufferType
decodeUnit:(PDECODE_UNIT)du
decodeStartTime:(CFTimeInterval)decodeStartTime;
- (OSStatus)decodeFrameWithSampleBuffer:(CMSampleBufferRef)sampleBuffer
frameNumber:(int)frameNumber
frameType:(int)frameType
decodeStartTime:(CFTimeInterval)decodeStartTime;
@end
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -38,7 +38,7 @@ static const int LABEL_DY = 20;
#if TARGET_OS_TV
self.frame = CGRectMake(0, 0, 400, 400);
#else
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
self.frame = CGRectMake(0, 0, 200, 200);
} else {
self.frame = CGRectMake(0, 0, 100, 100);
@@ -59,7 +59,7 @@ static const int LABEL_DY = 20;
_hostLabel.textColor = [UIColor whiteColor];
_hostOverlay = [[UIImageView alloc] initWithFrame:CGRectMake(self.frame.size.width / 3, _hostIcon.frame.size.height / 4, _hostIcon.frame.size.width / 3, self.frame.size.height / 3)];
_hostSpinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
_hostSpinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleLarge];
[_hostSpinner setFrame:_hostOverlay.frame];
_hostSpinner.userInteractionEnabled = NO;
_hostSpinner.hidesWhenStopped = YES;
+82
View File
@@ -0,0 +1,82 @@
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/**
* @brief The BandwidthTracker class tracks network bandwidth usage over a sliding time window (default 10s).
*
* Byte totals are grouped into fixed time interval buckets (default 250ms). This provides an element of smoothing
* and deals well with spikes.
*
* GetAverageMbps() is calculated using the 25% most recent fully completed buckets. The default settings will
* return an average of the past 2.5s of data, ignoring the in-progress bucket. Using only 2.5s of data for the
* average provides a good balance of reactivity and smoothness.
*
* GetPeakMbps() returns the peak bandwidth seen during any one bucket interval across the full time window.
*
* All public methods are thread safe. A typical use case is calling AddBytes() in a data processing thread while
* calling GetAverageMbps() from a UI thread.
*
* This class was mostly auto-converted from the C++ version to use Obj-C APIs and naming conventions.
*/
@interface BandwidthTracker : NSObject
/**
* @brief Constructs a new BandwidthTracker object.
*
* Initializes the tracker to maintain statistics over a sliding window of time.
* The window is divided into buckets of fixed duration (bucketIntervalMs).
*
* @param windowSeconds The duration of the tracking window in seconds. Default is 10 seconds.
* @param bucketIntervalMs The interval for each bucket in milliseconds. Default is 250 ms.
*/
- (instancetype)initWithWindowSeconds:(NSUInteger)windowSeconds
bucketIntervalMs:(NSUInteger)bucketIntervalMs NS_DESIGNATED_INITIALIZER;
/// Convenience init → defaults to a 10 s window with 250 ms buckets.
- (instancetype)init;
/**
* @brief Record bytes that were received or sent.
*
* This method updates the corresponding bucket for the current time interval with the new data.
* It is thread-safe. Bytes are associated with the bucket for "now" and it is not possible to
* submit data for old buckets. This function should be called as needed at the time the bytes
* were received. Callers should not maintain their own byte totals.
*
* @param bytes The number of bytes to add.
*/
- (void)addBytes:(NSUInteger)bytes;
/**
* @brief Computes and returns the average bandwidth in Mbps for the most recent 25% of buckets.
*
* @return The average bandwidth in megabits per second.
*/
- (double)averageMbps;
/**
* @brief Returns the peak bandwidth in Mbps observed in any single bucket within the current window.
*
* This value represents the highest instantaneous throughput measured over one bucket interval.
*
* @return The peak bandwidth in megabits per second.
*/
- (double)peakMbps;
/**
* @brief Retrieves the duration of the tracking window.
*
* This is useful when displaying the length of the peak, e.g.
* @code
* printf("Bitrate: %.1f Mbps Peak (%us): %.1f\n",
* [bw averageMbps], [bw windowSeconds], [bw peakMbps]);
* @endcode
*
* @return The window duration in seconds.
*/
- (NSUInteger)windowSeconds;
@end
NS_ASSUME_NONNULL_END
+136
View File
@@ -0,0 +1,136 @@
#import "BandwidthTracker.h"
#import <QuartzCore/QuartzCore.h> // for CACurrentMediaTime()
// A single time‐bucket: start time (ms) + bytes counted.
typedef struct {
CFTimeInterval startMs;
NSUInteger bytes;
} Bucket;
@interface BandwidthTracker () {
// Configuration
NSUInteger _windowSeconds; // e.g. 10
NSUInteger _bucketIntervalMs; // e.g. 250
NSUInteger _bucketCount; // = (_windowSeconds * 1000) / _bucketIntervalMs
// State
Bucket *_buckets; // C-array of _bucketCount entries
NSLock *_lock; // protects all bucket ops
}
@end
@implementation BandwidthTracker
- (instancetype)init {
return [self initWithWindowSeconds:10 bucketIntervalMs:250];
}
- (instancetype)initWithWindowSeconds:(NSUInteger)windowSeconds bucketIntervalMs:(NSUInteger)bucketIntervalMs {
self = [super init];
if (self) {
_windowSeconds = (windowSeconds > 0 ? windowSeconds : 10);
_bucketIntervalMs = (bucketIntervalMs > 0 ? bucketIntervalMs : 250);
_bucketCount = (_windowSeconds * 1000) / _bucketIntervalMs;
_buckets = calloc(_bucketCount, sizeof(Bucket));
_lock = [[NSLock alloc] init];
}
return self;
}
- (void)dealloc {
free(_buckets);
}
- (NSUInteger)windowSeconds {
return _windowSeconds;
}
#pragma mark – Public API
- (void)addBytes:(NSUInteger)bytes {
[_lock lock];
CFTimeInterval nowMs = CACurrentMediaTime() * 1000.0;
[self _updateBucketWithBytes:bytes nowMs:nowMs];
[_lock unlock];
}
- (double)averageMbps {
[_lock lock];
CFTimeInterval nowMs = CACurrentMediaTime() * 1000.0;
NSUInteger currentIndex = ((NSUInteger)nowMs / _bucketIntervalMs) % _bucketCount;
NSUInteger maxBuckets = _bucketCount / 4;
NSUInteger totalBytes = 0;
CFTimeInterval oldestStart = nowMs;
for (NSUInteger i = 0; i < maxBuckets; i++) {
NSUInteger idx = (currentIndex + _bucketCount - i) % _bucketCount;
Bucket b = _buckets[idx];
if ([self _isBucketValid:&b nowMs:nowMs] && (nowMs - b.startMs >= _bucketIntervalMs)) {
totalBytes += b.bytes;
if (b.startMs < oldestStart)
oldestStart = b.startMs;
}
}
// elapsed time in seconds
double elapsedSec = (nowMs - oldestStart) / 1000.0;
double avg = (elapsedSec > 0.0 ? totalBytes * 8.0 / 1000000.0 / elapsedSec : 0.0);
[_lock unlock];
return avg;
}
- (double)peakMbps {
[_lock lock];
CFTimeInterval nowMs = CACurrentMediaTime() * 1000.0;
double peak = 0.0;
for (NSUInteger i = 0; i < _bucketCount; i++) {
Bucket b = _buckets[i];
if ([self _isBucketValid:&b nowMs:nowMs]) {
double mbps = [self _bucketMbps:&b];
if (mbps > peak)
peak = mbps;
}
}
[_lock unlock];
return peak;
}
#pragma mark – Private Helpers
// Check if a bucket's data is still valid (within the window)
- (BOOL)_isBucketValid:(Bucket *)bucket nowMs:(CFTimeInterval)nowMs {
return (nowMs - bucket->startMs) <= (_windowSeconds * 1000.0);
}
- (double)_bucketMbps:(Bucket *)bucket {
// bucketIntervalMs is ms → divide by 1000 to get seconds
return bucket->bytes * 8.0 / 1000000.0 / (_bucketIntervalMs / 1000.0);
}
- (void)_updateBucketWithBytes:(NSUInteger)bytes nowMs:(CFTimeInterval)nowMs {
NSUInteger ms = (NSUInteger)nowMs;
NSUInteger idx = (ms / _bucketIntervalMs) % _bucketCount;
NSUInteger alignedMs = ms - (ms % _bucketIntervalMs);
CFTimeInterval startMs = alignedMs;
Bucket *b = &_buckets[idx];
// If this bucket is stale (older than the window), reset it
if (nowMs - b->startMs > (_windowSeconds * 1000.0)) {
b->bytes = 0;
b->startMs = startMs;
}
// If we’ve moved into a new sub-interval, overwrite; otherwise accumulate
if (b->startMs != startMs) {
b->bytes = bytes;
b->startMs = startMs;
} else {
b->bytes += bytes;
}
}
@end
+60
View File
@@ -0,0 +1,60 @@
#import <Foundation/Foundation.h>
#import "Plot.h"
NS_ASSUME_NONNULL_BEGIN
/// A thread-safe circular buffer of `float` values (capacity must be a power of two).
/// This is a mostly automated conversion of my C++ FloatBuffer.h class to Objective-C.
@interface FloatBuffer : NSObject
/// The total capacity (always a power of two).
@property(nonatomic, readonly) int capacity;
/// The current number of valid entries (≤ capacity).
@property(nonatomic, readonly) int count;
/// The minimum value among all entries currently in the buffer.
@property(nonatomic, readonly) float minValue;
/// The maximum value among all entries currently in the buffer.
@property(nonatomic, readonly) float maxValue;
/// The arithmetic average (sum / count) of all entries. Zero if count=0.
@property (nonatomic, readonly) float averageValue;
@property (nonatomic, readonly) float total;
/// Designated initializer. `capacity` must be >0 and a power of two, otherwise throws an exception.
- (instancetype)initWithCapacity:(int)capacity NS_DESIGNATED_INITIALIZER;
/// Convenience initializer: same as `initWithCapacity:`.
- (instancetype)init;
/// Adds a new float into the buffer (overwriting the oldest if full). Thread-safe.
- (void)addValue:(float)value;
/// Most recent value
- (float)newestValue;
/// Oldest timestamp
- (CFTimeInterval)oldestTimestamp;
/// Copies the buffer’s contents (oldest→newest) into `outBuffer`, which must be able to hold at least `self.count` floats.
/// Returns the number of floats written (i.e. the current `count`).
/// If non-NULL, `outMin` and `outMax` are set to the buffer’s current minimum/maximum.
- (int)copyValuesIntoBuffer:(float *)outBuffer
min:(float * _Nullable)outMin
max:(float * _Nullable)outMax;
/// Export metrics for use by the classic stats overlay
- (void)copyMetrics:(PlotMetrics *)plotMetrics;
/// Clear and reset the buffer to 0.
- (void)clear;
/// Calls `block(value, timestamp)` for each value in the buffer (from oldest to newest)
- (void)enumerateValuesWithBlock:(void (^)(float value, BOOL *stop))block;
@end
NS_ASSUME_NONNULL_END
+273
View File
@@ -0,0 +1,273 @@
#import <UIKit/UIKit.h>
#import "FloatBuffer.h"
@implementation FloatBuffer {
float *_buffer; // raw C array holding up to capacity floats
CFTimeInterval *_timestamps; // timestamp the entry was observed
int _head; // index of next write (0…capacity−1)
int _count; // how many valid entries are in the buffer (≤ capacity)
float _minValue; // current minimum across all valid entries
float _maxValue; // current maximum across all valid entries
double _sum; // running sum of all valid entries (for average)
dispatch_queue_t _sq; // serial queue for thread safety
}
@synthesize capacity = _capacity;
@synthesize count = _count;
@synthesize minValue = _minValue;
@synthesize maxValue = _maxValue;
@synthesize averageValue = _averageValue; // custom getter below
- (instancetype)init {
return [self initWithCapacity:256];
}
- (instancetype)initWithCapacity:(int)capacity {
self = [super init];
if (self) {
// Enforce capacity > 0 and a power of two
if (capacity <= 0 || (capacity & (capacity - 1)) != 0) {
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"FloatBuffer capacity must be a nonzero power of two" userInfo:nil];
}
_capacity = capacity;
_buffer = calloc(capacity, sizeof(float));
_timestamps = calloc(capacity, sizeof(CFTimeInterval));
_head = 0;
_count = 0;
_minValue = 0.0f;
_maxValue = 0.0f;
_sum = 0.0;
_sq = dispatch_queue_create("com.floatbuffer.serial", DISPATCH_QUEUE_SERIAL);
}
return self;
}
- (void)dealloc {
if (_buffer) {
free(_buffer);
_buffer = NULL;
}
if (_timestamps) {
free(_timestamps);
_timestamps = NULL;
}
}
- (void)addValue:(float)value {
dispatch_sync(_sq, ^{
[self _unsafeAddValue:value];
});
}
- (void)_unsafeAddValue:(float)value {
BOOL wasFull = (_count == _capacity);
float overwrittenValue = 0.0f;
if (wasFull) {
// The slot at _head is about to be overwritten
overwrittenValue = _buffer[_head];
}
// 1) Write the new value into the “head” slot:
_buffer[_head] = value;
_timestamps[_head] = CACurrentMediaTime();
_head = (_head + 1) & (_capacity - 1); // wrap via bitmask
// 2) Update count / sum / min / max
if (!wasFull) {
// We had room to grow
_count += 1;
_sum += value;
if (_count == 1) {
// Very first element:
_minValue = value;
_maxValue = value;
} else {
// Compare to existing min/max
if (value < _minValue)
_minValue = value;
if (value > _maxValue)
_maxValue = value;
}
} else {
// Buffer was full: dropped overwrittenValue, added new value
_sum += (double)value - (double)overwrittenValue;
// If overwrittenValue was equal to old min or max, we must rescan
if (overwrittenValue == _minValue || overwrittenValue == _maxValue) {
float newMin = _buffer[0];
float newMax = _buffer[0];
for (int i = 1; i < _capacity; i++) {
float v = _buffer[i];
if (v < newMin)
newMin = v;
if (v > newMax)
newMax = v;
}
_minValue = newMin;
_maxValue = newMax;
}
// Finally, make sure the newly written `value` updates min/max if needed
if (value < _minValue)
_minValue = value;
if (value > _maxValue)
_maxValue = value;
}
}
- (float)averageValue {
__block float result;
dispatch_sync(_sq, ^{
result = (self->_count > 0) ? (float)(self->_sum / (double)self->_count) : 0.0f;
});
return result;
}
- (float)total {
__block float result;
dispatch_sync(_sq, ^{
result = (self->_count > 0) ? (float)(self->_sum) : 0.0f;
});
return result;
}
- (float)newestValue {
__block float result;
dispatch_sync(_sq, ^{
result = (self->_count > 0) ? _buffer[_head] : 0.0f;
});
return result;
}
- (CFTimeInterval)oldestTimestamp {
__block CFTimeInterval result;
dispatch_sync(_sq, ^{
NSUInteger tail = (_head + _capacity - _count) & (_capacity - 1);
result = (self->_count > 0) ? _timestamps[tail] : 0.0f;
});
return result;
}
- (int)copyValuesIntoBuffer:(float *)outBuffer min:(nullable float *)outMin max:(nullable float *)outMax {
__block int result;
dispatch_sync(_sq, ^{
result = [self _unsafeCopyValuesIntoBuffer:outBuffer min:outMin max:outMax];
});
return result;
}
- (int)_unsafeCopyValuesIntoBuffer:(float *)outBuffer min:(nullable float *)outMin max:(nullable float *)outMax {
if (_count == 0) {
// Empty buffer → zero, and set min/max to zero if requested
if (outMin)
*outMin = 0.0f;
if (outMax)
*outMax = 0.0f;
return 0;
}
// Compute "tail" index (oldest element)
NSUInteger tail = (_head + _capacity - _count) & (_capacity - 1);
// 1) Copy first chunk from buffer[tail] up to either end or _count elements
NSUInteger firstChunkSize = MIN(_capacity - tail, _count);
memcpy(outBuffer, &_buffer[tail], firstChunkSize * sizeof(float));
NSUInteger copiedSoFar = firstChunkSize;
// 2) If wrapped, copy remainder from index 0
if (firstChunkSize < _count) {
NSUInteger remainder = _count - firstChunkSize;
memcpy(&outBuffer[copiedSoFar], _buffer, remainder * sizeof(float));
}
if (outMin)
*outMin = _minValue;
if (outMax)
*outMax = _maxValue;
return _count;
}
- (void)copyMetrics:(PlotMetrics *)plotMetrics {
dispatch_sync(_sq, ^{
[self _unsafeCopyMetrics:plotMetrics];
});
}
- (void)_unsafeCopyMetrics:(PlotMetrics *)plotMetrics {
plotMetrics->min = _minValue;
plotMetrics->max = _maxValue;
plotMetrics->avg = (self->_count > 0) ? (float)(self->_sum / (double)self->_count) : 0.0f;
plotMetrics->total = (self->_count > 0) ? (float)(self->_sum) : 0.0f;
plotMetrics->nsamples = self->_count;
plotMetrics->samplerate = 0.0f;
if (plotMetrics->nsamples > 1) {
NSUInteger tail = (_head + _capacity - _count) & (_capacity - 1);
NSUInteger newest = (_head + _capacity - 1) & (_capacity - 1);
CFTimeInterval elapsed = _timestamps[newest] - _timestamps[tail];
if (elapsed > 0.0) {
// nsamples-1 intervals over elapsed seconds
plotMetrics->samplerate = (float)((plotMetrics->nsamples - 1) / elapsed);
}
}
}
- (void)clear {
dispatch_sync(_sq, ^{
memset(_buffer, 0, sizeof(float) * _capacity);
memset(_timestamps, 0, sizeof(CFTimeInterval) * _capacity);
_head = 0;
_count = 0;
_minValue = 0.0f;
_maxValue = 0.0f;
_sum = 0.0;
});
}
- (void)enumerateValuesWithBlock:(void (^)(float value, BOOL *stop))block {
if (!block) return;
dispatch_sync(_sq, ^{
if (_count == 0) return;
int tail = (_head + _capacity - _count) & (_capacity - 1);
BOOL stop = NO;
for (int i = 0; i < _count && !stop; i++) {
int idx = (tail + i) & (_capacity - 1);
block(_buffer[idx], &stop);
}
});
}
// Debug output for use with %@
- (NSString *)description {
__block NSString *desc;
dispatch_sync(_sq, ^{
int tail = (_head + _capacity - _count) & (_capacity - 1);
NSMutableString *values = [NSMutableString stringWithString:@"["];
for (int i = 0; i < _count; i++) {
float v = _buffer[(tail + i) & (_capacity - 1)];
[values appendFormat:@"%0.3f", v];
if (i < _count - 1) [values appendString:@", "];
}
[values appendString:@"]"];
float avg = (_count > 0) ? (float)(_sum / _count) : 0.0f;
desc = [NSString stringWithFormat:@"<%@: %p; capacity=%d; count=%d; min=%0.3f; max=%0.3f; avg=%0.3f; sum=%0.3f, values=%@>",
NSStringFromClass([self class]),
self,
_capacity,
_count,
_minValue,
_maxValue,
avg,
_sum,
values];
});
return desc;
}
@end
+25
View File
@@ -0,0 +1,25 @@
#import <Foundation/Foundation.h>
#import <VideoToolbox/VideoToolbox.h>
@interface Frame : NSObject
@property (nonatomic) int frameNumber;
@property (nonatomic) int frameType;
@property (nonatomic) CFTimeInterval decodedAt;
@property (nonatomic) CMTime pts90;
@property (nonatomic) CMTime duration90;
@property (nonatomic, assign) CMVideoFormatDescriptionRef formatDesc;
@property (nonatomic) CMSampleBufferRef sampleBuffer;
@property (nonatomic) CVPixelBufferRef pixelBuffer;
- (instancetype)initWithPixelBufffer:(CVPixelBufferRef)pixelBuffer frameNumber:(int)frameNumber frameType:(int)frameType pts:(CMTime)pts;
- (instancetype)initWithSampleBuffer:(CMSampleBufferRef)sampleBuffer frameNumber:(int)frameNumber frameType:(int)frameType;
- (void)dealloc;
- (CFTimeInterval)pts;
- (CFTimeInterval)duration;
- (void)setDurationFromNext:(Frame *)nextFrame;
- (BOOL)durationIsValid;
- (CFDictionaryRef)getFormatDescExtensions;
- (size_t)width;
- (size_t)height;
@end
+142
View File
@@ -0,0 +1,142 @@
#import <CoreVideo/CVImageBuffer.h>
#import "Frame.h"
#import "Logger.h"
#include <Limelight.h>
@implementation Frame {
CFDictionaryRef _formatDescExt;
}
- (instancetype)initWithPixelBufffer:(CVPixelBufferRef)pixelBuffer
frameNumber:(int)frameNumber
frameType:(int)frameType
pts:(CMTime)pts {
self = [super init];
if (self) {
_decodedAt = CACurrentMediaTime();
_formatDescExt = nil;
_frameNumber = frameNumber;
_frameType = frameType;
_pixelBuffer = pixelBuffer; // already retained
_sampleBuffer = nil;
// 90 kHz pts from RTP
_pts90 = pts;
_duration90 = kCMTimeInvalid;
FQLog(LOG_I, @"init Frame %d - type %@ [host pts %.3f]",
_frameNumber, _frameType == FRAME_TYPE_IDR ? @"IDR" : @"P",
CMTimeGetSeconds(_pts90));
}
return self;
}
- (instancetype)initWithSampleBuffer:(CMSampleBufferRef)sampleBuffer frameNumber:(int)frameNumber frameType:(int)frameType {
self = [super init];
if (self) {
_frameNumber = frameNumber;
_frameType = frameType;
_pixelBuffer = nil;
_sampleBuffer = sampleBuffer;
// 90 kHz pts from RTP
_pts90 = CMSampleBufferGetOutputPresentationTimeStamp(sampleBuffer);
_duration90 = kCMTimeInvalid;
FQLog(LOG_I, @"init Frame %d - type %@ [host pts %.3f]",
_frameNumber, _frameType == FRAME_TYPE_IDR ? @"IDR" : @"P",
CMTimeGetSeconds(_pts90));
}
return self;
}
- (void)dealloc {
FQLog(LOG_I, @"[%d / %f] Frame dealloc", _frameNumber, CMTimeGetSeconds(_pts90));
if (_formatDesc) {
CFRelease(_formatDesc);
_formatDesc = nil;
}
if (_formatDescExt) {
CFRelease(_formatDescExt);
_formatDescExt = nil;
}
if (_pixelBuffer) {
CVPixelBufferRelease(_pixelBuffer);
_pixelBuffer = nil;
}
// sampleBuffer comes from CMSampleBufferCreateReadyWithImageBuffer
// so we don't need to CFRetain in init, but do need to release it
if (_sampleBuffer) {
CFRelease(_sampleBuffer);
_sampleBuffer = nil;
}
}
- (void)setFormatDesc:(CMVideoFormatDescriptionRef)formatDesc {
if (_formatDesc) {
CFRelease(_formatDesc);
}
_formatDesc = CFRetain(formatDesc);
}
- (CFDictionaryRef)getFormatDescExtensions {
if (!_formatDescExt && _formatDesc) {
_formatDescExt = CFRetain(CMFormatDescriptionGetExtensions(_formatDesc));
}
return _formatDescExt;
}
- (CFTimeInterval)pts {
return CMTimeGetSeconds(_pts90);
}
- (CFTimeInterval)duration {
if (CMTIME_IS_VALID(_duration90)) {
return CMTimeGetSeconds(_duration90);
}
return NAN;
}
- (void)setDurationFromNext:(Frame *)nextFrame {
if (nextFrame.frameNumber == _frameNumber + 1) {
_duration90 = CMTimeSubtract(nextFrame.pts90, _pts90);
_duration90.flags = kCMTimeFlags_Valid;
FQLog(LOG_I, @"frame [%d / %.3f] set duration %.3f ms",
_frameNumber, CMTimeGetSeconds(_pts90),
CMTimeGetSeconds(_duration90) * 1000.0);
}
}
- (BOOL)durationIsValid {
return CMTIME_IS_VALID(_duration90);
}
- (size_t)width {
if (_pixelBuffer) {
return CVPixelBufferGetWidth(_pixelBuffer);
}
return -1;
}
- (size_t)height {
if (_pixelBuffer) {
return CVPixelBufferGetHeight(_pixelBuffer);
}
return -1;
}
// Debug output when using %@
- (NSString *)description {
return [NSString stringWithFormat:@"{Frame: %d, type %@, pts90 %lld, pts %.3f ms, duration %@}",
self.frameNumber,
self.frameType == 1 ? @"IDR" : @"PFRAME",
self.pts90.value,
self.pts * 1000.0,
[self durationIsValid] ? [ NSString stringWithFormat:@"%.3f ms", self.duration * 1000.0] : @"--"
];
}
@end
+32
View File
@@ -0,0 +1,32 @@
#import <Foundation/Foundation.h>
#import <VideoToolbox/VideoToolbox.h>
#import "Frame.h"
#import "FloatBuffer.h"
NS_ASSUME_NONNULL_BEGIN
@interface FrameQueue : NSObject
@property (nonatomic, readonly) NSUInteger count;
@property (nonatomic) FloatBuffer *frameDropMetrics;
@property (nonatomic) int highWaterMark;
@property (nonatomic, readonly) int maxCapacity;
+ (instancetype)sharedInstance;
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;
- (BOOL)isEmpty;
- (void)clear;
- (int)enqueue:(Frame *)frame;
- (int)enqueue:(Frame *)frame withSlackSize:(int)slack;
- (nullable Frame *)dequeue;
- (nullable Frame *)dequeueWithTimeout:(CFTimeInterval)timeout;
- (CFTimeInterval)estimatedFramerate;
- (int)currentSoftCap;
- (void)waitForEnqueue;
@end
NS_ASSUME_NONNULL_END
+331
View File
@@ -0,0 +1,331 @@
@import AVFoundation;
@import VideoToolbox;
#import <os/lock.h>
#import <Limelight.h>
#import "Logger.h"
#import "FloatBuffer.h"
#import "FrameQueue.h"
#import "ImGuiPlots.h"
@implementation FrameQueue {
NSMutableArray<id> *_buffer;
int _capacity;
int _head;
int _tail;
int _count;
BOOL _droppedLast;
int _framesIn;
CMTime _ptsCorrection;
os_unfair_lock _lock;
FloatBuffer *_queueSizeHistory;
int _currentSoftCap;
dispatch_queue_t _sq;
dispatch_semaphore_t _frameSemaphore;
}
+ (instancetype)sharedInstance {
static FrameQueue *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[super allocWithZone:NULL] _initSingleton];
});
return sharedInstance;
}
// Prevent others from using alloc/init directly
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
return [self sharedInstance];
}
// If someone tries to copy it, just return the same instance
- (id)copyWithZone:(NSZone *)zone {
return self;
}
- (id)mutableCopyWithZone:(NSZone *)zone {
return self;
}
- (instancetype)_initSingleton {
self = [super init];
if (self) {
_droppedLast = NO;
_frameDropMetrics = [[FloatBuffer alloc] initWithCapacity:512];
_framesIn = 0;
_highWaterMark = 2;
_maxCapacity = 15;
_ptsCorrection = CMTimeMake(0, 90000);
_queueSizeHistory = [[FloatBuffer alloc] initWithCapacity:64];
_lock = OS_UNFAIR_LOCK_INIT;
// ring buffer
_capacity = _maxCapacity;
_buffer = [NSMutableArray arrayWithCapacity:_capacity];
for (int i = 0; i < _capacity; i++) {
[_buffer addObject:[NSNull null]];
}
_head = _tail = _count = 0;
_sq = dispatch_queue_create("com.moonlight.FrameQueue",
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, 0));
_frameSemaphore = dispatch_semaphore_create(0);
// ping estimatedFramerate to set initial last value
[self estimatedFramerate];
}
return self;
}
// Push into buffer at _tail
- (void)_pushFrame:(Frame *)frame {
[_buffer replaceObjectAtIndex:_tail withObject:frame];
_tail = (_tail + 1) % _capacity;
_count++;
// I think it's ok to signal the cond from within the unfair lock, the render
// loop will just end up waiting on it in a call to dequeue.
dispatch_semaphore_signal(_frameSemaphore);
FQLog(LOG_I, @"[-> %@ %d / %f] enqueue frame, queue size %d / %d",
frame.frameType == FRAME_TYPE_IDR ? @"IDR" : @"P",
frame.frameNumber, frame.pts, _count, _highWaterMark);
}
// Pop oldest frame from _head
- (Frame *)_popFrame {
id obj = _buffer[_head];
Frame *frame = (obj == [NSNull null] ? nil : obj);
[_buffer replaceObjectAtIndex:_head withObject:[NSNull null]];
_head = (_head + 1) % _capacity;
_count--;
return frame;
}
// Peek next frame (without removing)
- (Frame *)_peekFrame {
id frame = (_count > 0) ? _buffer[_head] : nil;
return (frame == [NSNull null]) ? nil : frame;
}
// enumerate in‐buffer frames
- (void)_enumerateFrames:(void(^)(Frame *frame, NSUInteger idx, BOOL *stop))block {
BOOL stop = NO;
for (int i = 0; i < _count; i++) {
int idx = (_head + i) % _capacity;
block([_buffer objectAtIndex:idx], i, &stop);
if (stop) break;
}
}
- (void)_noteDroppedFrame:(Frame *)frame {
if ([frame durationIsValid]) {
_ptsCorrection = CMTimeAdd(_ptsCorrection, frame.duration90);
FQLog(LOG_W, @"dropped frame %d with duration %.3f ms", frame.frameNumber, frame.duration * 1000.0);
} else {
// count unknowns as 1 avg frametime
CFTimeInterval fps = (_framesIn > 1000) ? [self _unsafeEstimatedFramerate] : 0.0f;
CMTime oneFrame = (_framesIn > 1000) ? CMTimeMake((int)(90000.0f / fps), 90000) : kCMTimeZero;
_ptsCorrection = CMTimeAdd(_ptsCorrection, oneFrame);
FQLog(LOG_W, @"dropped frame %d with unknown duration, using %.3f ms (%.1f fps) instead",
frame.frameNumber, CMTimeGetSeconds(oneFrame) * 1000.0, fps);
}
}
- (int)_unsafeEnqueue:(Frame *)frame withDropTarget:(int)frameDropTarget {
int dropCount = 0;
// Always accept IDR frames, allow exceeding HWM
if (frame.frameType == FRAME_TYPE_IDR || _count < frameDropTarget) {
[self _pushFrame:frame];
_droppedLast = NO;
} else {
if (!_droppedLast) {
// alternate between: drop newest...
[self _noteDroppedFrame:frame];
dropCount = 1;
_droppedLast = YES;
} else {
// and: drop oldest & enqueue new
if ([self _peekFrame].frameType != FRAME_TYPE_IDR) {
Frame *oldest = [self _popFrame];
[oldest setDurationFromNext:[self _peekFrame]];
[self _noteDroppedFrame:oldest];
dropCount = 1;
}
[self _pushFrame:frame];
_droppedLast = NO;
}
}
// regardless of drop status, every enqueue is a frame
// for estimatedFramerate purposes
_framesIn++;
[_frameDropMetrics addValue:(float)dropCount];
// Stats displays the soft cap
_currentSoftCap = frameDropTarget;
return dropCount;
}
// enqueue with simple alternate-drop logic
- (int)enqueue:(Frame *)frame {
os_unfair_lock_lock(&_lock);
int dropCount = [self _unsafeEnqueue:frame withDropTarget:_highWaterMark];
os_unfair_lock_unlock(&_lock);
return dropCount;
}
// enqueue that is a bit more flexixble, using the same 500ms queue size history method as moonlight-qt.
- (int)enqueue:(Frame *)frame withSlackSize:(int)slack {
os_unfair_lock_lock(&_lock);
CFTimeInterval now = CACurrentMediaTime();
// new data point for queue health
[_queueSizeHistory addValue:(float)_count];
// The "target" initially starts as the size of the buffer chosen by the user. 1-5 default 2. We drop a frame
// when the queue size exceeds this amount.
int frameDropTarget = _highWaterMark;
// CFTimeInterval t0 = [_queueSizeHistory oldestTimestamp];
// if (now - t0 > 0.5f) {
// // Get the current drop percentage over the past 512 frames
// // TODO: design a better API e.g. [ImGuiPlots PLOT_DROPPED].
// float dropRate = [[ImGuiPlots sharedInstance].plots[PLOT_DROPPED].buffer averageValue];
// if (dropRate > 0.25f) {
// FQLog(LOG_I, @"queue is dropping at %.2f%%, forcing a drop", dropRate * 100.0);
// frameDropTarget--;
// } else if (_queueSizeHistory.minValue > 0 && _queueSizeHistory.minValue == _queueSizeHistory.maxValue) {
// // If the queue has been in the same state for the entire period, it's possible to get stuck there for a while.
// // Lower the FDT by 1 to force a frame to be dropped and hopefully unstick things.
// FQLog(LOG_I, @"queue stuck at %.0f, forcing a drop", _queueSizeHistory.minValue);
// frameDropTarget--;
// } else if (_queueSizeHistory.minValue < 1.0f) {
// // If the queue has cleared out at least once in the past history period, be more lenient about dropping frames.
// frameDropTarget += slack;
// // FQLog(LOG_I, @"allowing frameDropTarget of %d (history %.0f/%.0f/%.2f)", frameDropTarget,
// // _queueSizeHistory.minValue, _queueSizeHistory.maxValue, _queueSizeHistory.averageValue);
// }
//
// // TODO: New logic
// // Use timestamps, check initial interval is long enough
// // Smarter when checking min==max, a steady state can be fine if no frames are being dropped
// // If min==max && frame drops are >= 25%, force a drop
// // Maybe only look at >= 25%.
// // Test condition variables, manual present, no displaylink
// }
// original enqueue logic still applies if we're full
int dropCount = [self _unsafeEnqueue:frame withDropTarget:frameDropTarget];
os_unfair_lock_unlock(&_lock);
return dropCount;
}
// Allows the render loop to wait if the queue is empty
- (void)waitForEnqueue {
while ([self isEmpty]) {
FQLog(LOG_I, @"waitForEnqueue...");
long result = dispatch_semaphore_wait(_frameSemaphore, DISPATCH_TIME_FOREVER);
}
}
- (Frame *)dequeue {
os_unfair_lock_lock(&_lock);
Frame *frame = nil;
if (_count > 0) {
frame = [self _popFrame];
// compute duration from next
Frame *next = [self _peekFrame];
if (next) {
[frame setDurationFromNext:next];
}
FQLog(LOG_I, @"[<- %d / %f%@] dequeue frame, queue size %d",
frame.frameNumber, frame.pts,
[frame durationIsValid] ? [NSString stringWithFormat:@" dur %.3f ms", frame.duration * 1000.0] : @"",
_count);
}
os_unfair_lock_unlock(&_lock);
return frame;
}
- (Frame *)dequeueWithTimeout:(CFTimeInterval)timeout {
CFTimeInterval start = CACurrentMediaTime();
CFTimeInterval deadline = start + timeout;
int round = 0;
// Always attempt to dequeue at least once
do {
if (round > 0) {
usleep(100); // 0.1ms
}
Frame *frame = [self dequeue];
if (frame) {
return frame;
}
round++;
} while (CACurrentMediaTime() < deadline);
FQLog(LOG_I, @"dequeueWithTimeout timed out after %.3f ms", (CACurrentMediaTime() - start) * 1000.0);
return nil;
}
- (NSUInteger)count {
os_unfair_lock_lock(&_lock);
NSUInteger c = _count;
os_unfair_lock_unlock(&_lock);
return c;
}
- (BOOL)isEmpty {
return [self count] == 0;
}
- (void)clear {
os_unfair_lock_lock(&_lock);
_head = _tail = _count = 0;
_frameDropMetrics = [[FloatBuffer alloc] initWithCapacity:512];
os_unfair_lock_unlock(&_lock);
}
- (CFTimeInterval)_unsafeEstimatedFramerate {
static CFTimeInterval lastTime = 0;
static int lastFrames = 0;
static CFTimeInterval estimate = 0;
CFTimeInterval now = CACurrentMediaTime();
if (now - lastTime > 1.0) {
estimate = (_framesIn - lastFrames) / (now - lastTime);
// FQLog(LOG_I, @"fps calc using framesIn %d - lastFrames %d / now %f - lastTime %f = %.1f fps",
// _framesIn, lastFrames, now, lastTime, estimate);
lastTime = now;
lastFrames = _framesIn;
}
return estimate;
}
- (CFTimeInterval)estimatedFramerate {
os_unfair_lock_lock(&_lock);
CFTimeInterval fps = [self _unsafeEstimatedFramerate];
os_unfair_lock_unlock(&_lock);
return fps;
}
- (int)currentSoftCap {
os_unfair_lock_lock(&_lock);
int cap = _currentSoftCap;
os_unfair_lock_unlock(&_lock);
return cap;
}
// For use with NSLog("%@", franeQueue);
- (NSString *)description {
__block NSMutableArray *parts = [NSMutableArray arrayWithCapacity:_count];
[self _enumerateFrames:^(Frame *frame, NSUInteger idx, BOOL *stop) {
[parts addObject:[frame description]];
}];
return [NSString stringWithFormat:@"[%@]", [parts componentsJoinedByString:@",\n"]];
}
@end
+55
View File
@@ -9,8 +9,13 @@
#ifndef Limelight_Logger_h
#define Limelight_Logger_h
#import <dispatch/dispatch.h>
#import <stdarg.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
LOG_D,
LOG_I,
@@ -26,4 +31,54 @@ typedef enum {
void Log(LogLevel level, NSString* fmt, ...);
void LogTag(LogLevel level, NSString* tag, NSString* fmt, ...);
#ifdef __cplusplus
}
#endif
// LogOnce() is a one-time log message for use in hot areas of the code
#define CONCAT(a,b) CONCAT2(a,b)
#define CONCAT2(a,b) a##b
#define LogOnce(level, fmt, ...) \
do { \
static dispatch_once_t CONCAT(_onceToken_, __LINE__); \
dispatch_once(&CONCAT(_onceToken_, __LINE__), ^{ \
Log(level, fmt, ##__VA_ARGS__); \
}); \
} while (0)
#endif
// Disable all logging in release mode for performance
#ifdef DEBUG
#define Log(level, fmt, ...) \
LogTag(level, NULL, fmt, ##__VA_ARGS__)
#else
#define Log(level, fmt, ...) do {} while(0)
#endif
// FrameQueue log helpers FQLog() is a no-op unless FRAME_QUEUE_VERBOSE is defined
static inline NSString *FQQoSString(qos_class_t qos) {
switch (qos) {
case QOS_CLASS_USER_INTERACTIVE: return @"UI-25";
case QOS_CLASS_USER_INITIATED: return @"IN-19";
case QOS_CLASS_DEFAULT: return @"DF-15";
case QOS_CLASS_UTILITY: return @"UT-11";
case QOS_CLASS_BACKGROUND: return @"BG-09";
default: return [NSString stringWithFormat:@"??-%d", qos];
}
}
static inline NSString *FQLogPrefix(void) {
CFTimeInterval now = CACurrentMediaTime();
NSString *qos = FQQoSString(qos_class_self());
return [NSString stringWithFormat:@"[%.3f] [%@]", now, qos];
}
#if defined(FRAME_QUEUE_VERBOSE)
#define FQLog(level, fmt, ...) \
Log(level, @"%@ " fmt, FQLogPrefix(), ##__VA_ARGS__)
#else
#define FQLog(level, fmt, ...) do {} while(0)
#endif
+13 -18
View File
@@ -8,31 +8,19 @@
#import "Logger.h"
#if defined(NDEBUG)
static LogLevel LoggerLogLevel = LOG_I;
void LogTagv(LogLevel level, NSString* tag, NSString* fmt, va_list args);
void Log(LogLevel level, NSString* fmt, ...) {
va_list args;
va_start(args, fmt);
LogTagv(level, NULL, fmt, args);
va_end(args);
}
void LogTag(LogLevel level, NSString* tag, NSString* fmt, ...) {
va_list args;
va_start(args, fmt);
LogTagv(level, tag, fmt, args);
va_end(args);
}
#else
static LogLevel LoggerLogLevel = LOG_D;
#endif
void LogTagv(LogLevel level, NSString* tag, NSString* fmt, va_list args) {
NSString* levelPrefix = @"";
if (level < LoggerLogLevel) {
return;
}
switch(level) {
case LOG_D:
levelPrefix = PRFX_DEBUG;
@@ -59,3 +47,10 @@ void LogTagv(LogLevel level, NSString* tag, NSString* fmt, va_list args) {
}
NSLogv(prefixedString, args);
}
void LogTag(LogLevel level, NSString* tag, NSString* fmt, ...) {
va_list args;
va_start(args, fmt);
LogTagv(level, tag, fmt, args);
va_end(args);
}
+15
View File
@@ -0,0 +1,15 @@
//
// PaddedLabel.h
// Moonlight
//
// Created by Travis Thieman on 5/6/25.
// Copyright © 2025 Moonlight Game Streaming Project. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface PaddedLabel : UILabel
@property (nonatomic, assign) UIEdgeInsets textInsets;
@end
+37
View File
@@ -0,0 +1,37 @@
//
// PaddedLabel.m
// Moonlight
//
// Created by Travis Thieman on 5/6/25.
// Copyright © 2025 Moonlight Game Streaming Project. All rights reserved.
//
#import "PaddedLabel.h"
@implementation PaddedLabel
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.textInsets = UIEdgeInsetsMake(8, 8, 8, 8); // default insets
}
return self;
}
- (void)drawTextInRect:(CGRect)rect {
UIEdgeInsets insets = self.textInsets;
[super drawTextInRect:UIEdgeInsetsInsetRect(rect, insets)];
}
- (CGSize)intrinsicContentSize {
CGSize size = [super intrinsicContentSize];
return CGSizeMake(size.width + self.textInsets.left + self.textInsets.right,
size.height + self.textInsets.top + self.textInsets.bottom);
}
- (CGSize)sizeThatFits:(CGSize)size {
CGSize fittingSize = [super sizeThatFits:size];
return CGSizeMake(fittingSize.width + self.textInsets.left + self.textInsets.right,
fittingSize.height + self.textInsets.top + self.textInsets.bottom);
}
@end
+56
View File
@@ -0,0 +1,56 @@
typedef enum {
PACING_MODE_VSYNC,
PACING_MODE_PTS
} FramePacingMode;
typedef enum {
PLOT_FRAMETIME = 0,
PLOT_HOST_FRAMETIME,
PLOT_QUEUED_FRAMES,
PLOT_DROPPED,
PLOT_DECODE,
PlotCount
} PlotType;
typedef enum {
PLOT_LABEL_MIN_MAX_AVG = 0,
PLOT_LABEL_MIN_MAX_AVG_INT,
PLOT_LABEL_TOTAL_INT
} PlotLabelType;
typedef enum {
PLOT_HIDDEN,
PLOT_LEFT,
PLOT_RIGHT
} PlotSide;
typedef struct {
float min;
float max;
float avg;
float total;
int nsamples;
float samplerate;
} PlotMetrics;
typedef enum {
RENDER_METAL = 0,
RENDER_AVSB
} RenderingBackend;
typedef struct {
CFTimeInterval startTime;
CFTimeInterval endTime;
int totalFrames;
int receivedFrames;
int networkDroppedFrames;
int totalHostProcessingLatency;
int framesWithHostProcessingLatency;
int maxHostProcessingLatency;
int minHostProcessingLatency;
PlotMetrics decodeMetrics;
PlotMetrics frameQueueMetrics;
PlotMetrics frameDropMetrics;
RenderingBackend renderingBackend;
} video_stats_t;
@@ -205,7 +205,7 @@ static NSMutableSet* hostList;
AppListResponse* appListResp = [ConnectionHelper getAppListForHost:host];
[self->_discMan resumeDiscoveryForHost:host];
if (![appListResp isStatusOk] || [appListResp getAppList] == nil) {
Log(LOG_W, @"Failed to get applist: %@", appListResp.statusMessage);
dispatch_async(dispatch_get_main_queue(), ^{
@@ -228,7 +228,7 @@ static NSMutableSet* hostList;
} else {
dispatch_async(dispatch_get_main_queue(), ^{
[self updateApplist:[appListResp getAppList] forHost:host];
if (host != self->_selectedHost) {
[self hideLoadingFrame: nil];
return;
@@ -246,7 +246,7 @@ static NSMutableSet* hostList;
- (void) updateAppEntry:(TemporaryApp*)app forHost:(TemporaryHost*)host {
DataManager* database = [[DataManager alloc] init];
NSMutableSet* newHostAppList = [NSMutableSet setWithSet:host.appList];
for (TemporaryApp* savedApp in newHostAppList) {
if ([app.id isEqualToString:savedApp.id]) {
savedApp.name = app.name;
@@ -254,7 +254,7 @@ static NSMutableSet* hostList;
savedApp.hidden = app.hidden;
host.appList = newHostAppList;
[database updateAppsForExistingHost:host];
return;
}
@@ -313,7 +313,7 @@ static NSMutableSet* hostList;
} while (appWasRemoved);
host.appList = newHostAppList;
[database updateAppsForExistingHost:host];
// This host may be eligible for a shortcut now that the app list
@@ -427,7 +427,7 @@ static NSMutableSet* hostList;
while (host.state == StateUnknown) {
sleep(1);
}
// Don't bother polling if the server is already offline
if (host.state == StateOffline) {
dispatch_async(dispatch_get_main_queue(), ^{
@@ -437,16 +437,16 @@ static NSMutableSet* hostList;
});
return;
}
HttpManager* hMan = [[HttpManager alloc] initWithHost:host];
ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
// Exempt this host from discovery while handling the serverinfo request
[self->_discMan pauseDiscoveryForHost:host];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[hMan newServerInfoRequest:false]
fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
[self->_discMan resumeDiscoveryForHost:host];
if (![serverInfoResp isStatusOk]) {
Log(LOG_W, @"Failed to get server info: %@", serverInfoResp.statusMessage);
dispatch_async(dispatch_get_main_queue(), ^{
@@ -454,20 +454,20 @@ static NSMutableSet* hostList;
[self hideLoadingFrame:nil];
return;
}
UIAlertController* applistAlert = [UIAlertController alertControllerWithTitle:[LocalizationHelper localizedStringForKey:@"Connection Failed"]
message:serverInfoResp.statusMessage
preferredStyle:UIAlertControllerStyleAlert];
[Utils addHelpOptionToDialog:applistAlert];
[applistAlert addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:nil]];
// Only display an alert if this was the result of a real
// user action, not just passively entering the foreground again
[self hideLoadingFrame: ^{
[self switchToHostView];
[[self activeViewController] presentViewController:applistAlert animated:YES completion:nil];
}];
host.state = StateOffline;
});
} else {
@@ -568,14 +568,14 @@ static NSMutableSet* hostList;
}];
}
- (UIViewController*) activeViewController {
UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;
while (topController.presentedViewController) {
topController = topController.presentedViewController;
}
return topController;
}
@@ -603,7 +603,7 @@ static NSMutableSet* hostList;
message = [LocalizationHelper localizedStringForKey:@"Online - Not Paired"];
}
break;
case StateUnknown:
message = [LocalizationHelper localizedStringForKey:@"Connecting"];
break;
@@ -613,7 +613,7 @@ static NSMutableSet* hostList;
}
UIAlertController* longClickAlert = [UIAlertController alertControllerWithTitle:host.name message:message preferredStyle:UIAlertControllerStyleActionSheet];
if (host.state != StateOnline) {
/*[longClickAlert addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Wake PC"] style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
UIAlertController* wolAlert = [UIAlertController alertControllerWithTitle:[LocalizationHelper localizedStringForKey:@"Wake-On-LAN"] message:@"" preferredStyle:UIAlertControllerStyleAlert];
@@ -810,17 +810,16 @@ static NSMutableSet* hostList;
_streamConfig.bitRate = [streamSettings.bitrate intValue];
_streamConfig.optimizeGameSettings = streamSettings.optimizeGames;
_streamConfig.playAudioOnPC = streamSettings.playAudioOnPC;
_streamConfig.useFramePacing = streamSettings.useFramePacing;
_streamConfig.swapABXYButtons = streamSettings.swapABXYButtons;
_streamConfig.asyncNativeTouchPriority = streamSettings.asyncNativeTouchPriority; // new streamConfig segment
_streamConfig.gyroMode = [streamSettings.gyroMode intValue];
//NSLog(@"gyroMode from settings: %ld", _streamConfig.gyroMode);
// multiController must be set before calling getConnectedGamepadMask
_streamConfig.multiController = streamSettings.multiController;
_streamConfig.gamepadMask = [ControllerSupport getConnectedGamepadMask:_streamConfig];
_streamConfig.localMousePointerMode = streamSettings.localMousePointerMode.intValue;
// Probe for supported channel configurations
int physicalOutputChannels = (int)[AVAudioSession sharedInstance].maximumOutputNumberOfChannels;
Log(LOG_I, @"Audio device supports %d channels", physicalOutputChannels);
@@ -828,7 +827,7 @@ static NSMutableSet* hostList;
physicalOutputChannels = 8;
Log(LOG_I, @"System-provided spatial audio available, pretending device has %d channels", physicalOutputChannels);
}
int numberOfChannels = MIN([streamSettings.audioConfig intValue], physicalOutputChannels);
Log(LOG_I, @"Selected number of audio channels %d", numberOfChannels);
if (numberOfChannels >= 8) {
@@ -840,7 +839,7 @@ static NSMutableSet* hostList;
else {
_streamConfig.audioConfiguration = AUDIO_CONFIGURATION_STEREO;
}
switch (streamSettings.preferredCodec) {
case CODEC_PREF_AV1:
@@ -1124,9 +1123,9 @@ static NSMutableSet* hostList;
// enable / disable widgets acoordingly: in streamview, disable, outside of streamview, enable.
if(self.settingsExpandedInStreamView) [revealController buttonsInStreaming];
else [revealController buttonsNotInStreaming];
[streamFrameViewController setUserInteractionEnabledForStreamView:!_settingsExpandedInStreamView || position == FrontViewPositionLeft];
[settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.resolutionStack];
[settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.fpsStack];
[settingsViewController widget:settingsViewController.bitrateSlider setEnabled:!self.settingsExpandedInStreamView];
@@ -1140,7 +1139,7 @@ static NSMutableSet* hostList;
[settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.citrixX1MouseStack];
[settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.externalDisplayModeStack];
[settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.audioConfigStack];
}
- (void)revealController:(SWRevealViewController *)revealController didMoveToPosition:(FrontViewPosition)position {
@@ -1208,7 +1207,7 @@ static NSMutableSet* hostList;
- (BOOL)isFullScreenRequired {
NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];
NSNumber *requiresFullScreen = infoDictionary[@"UIRequiresFullScreen"];
if (requiresFullScreen != nil) {
return [requiresFullScreen boolValue];
}
@@ -1228,7 +1227,7 @@ static NSMutableSet* hostList;
self->waterMark.text = [LocalizationHelper localizedStringForKey:@"waterMarkText"];
CGFloat labelHeight = 60;
// the app is unable to automatically lock screen orientation in app window resizable mode(aka. not require fullscreen)
// if(![self isFullScreenRequired]){
if(false){
NSString* screenRotationTip = [LocalizationHelper localizedStringForKey:@"screenRotationTIp"];
@@ -1237,7 +1236,7 @@ static NSMutableSet* hostList;
self->waterMark.font = [UIFont systemFontOfSize:19];
labelHeight = 80;
}
self->waterMark.textColor = UIColor.blackColor;
self->waterMark.alpha = 0.35;
self->waterMark.textAlignment = NSTextAlignmentCenter;
@@ -1280,7 +1279,7 @@ static NSMutableSet* hostList;
button.clipsToBounds = YES;
// 设置图标(SF Symbol)
if (@available(iOS 13.0, *)) {
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:17 weight:UIImageSymbolWeightMedium];
UIImage *image = [UIImage systemImageNamed:@"plus.circle" withConfiguration:config];
@@ -1352,14 +1351,14 @@ static NSMutableSet* hostList;
- (void)setupHostViewTitle{
self->hostViewTitleLabel = [[UILabel alloc] init];
hostViewTitleLabel.translatesAutoresizingMaskIntoConstraints = NO;
hostViewTitleLabel.numberOfLines = 1;
hostViewTitleLabel.font = [UIFont systemFontOfSize:30 weight:UIFontWeightSemibold];
hostViewTitleLabel.text = [LocalizationHelper localizedStringForKey:@"Hosts"];
// CGFloat labelHeight = 60;
hostViewTitleLabel.textColor = [ThemeManager textColor];
hostViewTitleLabel.textAlignment = NSTextAlignmentCenter;
// hostViewTitleLabel.backgroundColor = [UIColor clearColor];
@@ -1409,13 +1408,13 @@ static NSMutableSet* hostList;
self->_addHostButton = [self createAddHostButton];
self->_helpButton = [self createHelpButton];
//[self setupHostViewTitle];
self.navigationItem.rightBarButtonItems = @[_helpButton, _addHostButton]; // 顺序:右边靠右的是第一个
// Set the side bar button action. When it's tapped, it'll show the sidebar.
[_settingsButton setTarget:self.revealViewController];
[_settingsButton setAction:@selector(revealToggle:)];
if (@available(iOS 13.0, *)) {
@@ -1427,7 +1426,7 @@ static NSMutableSet* hostList;
} else {
[_settingsButton setTitle:[LocalizationHelper localizedStringForKey:@"Settings"]];
}
// Set the host name button action. When it's tapped, it'll show the host selection view.
_upButton = [[UIBarButtonItem alloc] init];
[self->_upButton setTitle: [LocalizationHelper localizedStringForKey: @"Select New Host"]];
@@ -1450,7 +1449,7 @@ static NSMutableSet* hostList;
self.settingsExpandedInStreamView = false; // init this flag
self.revealViewController.isStreaming = false; //init this flag for rvlVC
self.revealViewController.mainFrameIsInHostView = true;
[self setupNavBar];
// Set the gesture
@@ -1497,7 +1496,7 @@ static NSMutableSet* hostList;
}
_boxArtCache = [[NSCache alloc] init];
//recordedScreenWidth = CGRectGetWidth([[UIScreen mainScreen] bounds]);
CGFloat screenWidthInPoints = CGRectGetWidth([[UIScreen mainScreen] bounds]);
CGFloat screenHeightInPoints = CGRectGetHeight([[UIScreen mainScreen] bounds]);
@@ -1526,22 +1525,22 @@ static NSMutableSet* hostList;
[self updateTitle];
// [self.view addSubview:hostScrollView];
// if ([hostList count] == 1) [self hostClicked:[hostList anyObject] view:nil]; // auto click for single host
//if([SettingsViewController isLandscapeNow] != _streamConfig.width > _streamConfig.height)
//[self simulateSettingsButtonPress]; //force expand setting view if orientation changed since last quit from app.
//[self simulateSettingsButtonPress]; //force expand setting view if orientation changed since last quit from app.
//[self updateResolutionAccordingly];
// SettingsViewController* settingsViewController = (SettingsViewController*)[self.revealViewController rearViewController];
// [settingsViewController updateResolutionTable];
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleMenuResize:)];
[self.view addGestureRecognizer:longPress];
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:[self isIPhone]?@"iPhone":@"iPad" bundle:nil];
SettingsViewController *viewController = [storyboard instantiateViewControllerWithIdentifier:@"settingsViewController"];
// 强制加载视图
@@ -1556,7 +1555,7 @@ static NSMutableSet* hostList;
// this will also be called back when device orientation changes
//- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
// [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
//
//
// double delayInSeconds = 0.7;
// // Convert the delay into a dispatch_time_t value
// dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
@@ -1575,7 +1574,7 @@ static NSMutableSet* hostList;
CGFloat appWindowHeight = CGRectGetHeight(window.frame) * screenScale;
CGFloat screenWidthInPoints = CGRectGetWidth([[UIScreen mainScreen] bounds]);
CGFloat screenHeightInPoints = CGRectGetHeight([[UIScreen mainScreen] bounds]);
if(currentSettings.externalDisplayMode.intValue == 1 && UIScreen.screens.count > 1){
CGRect bounds = [UIScreen.screens.lastObject bounds];
screenScale = [UIScreen.screens.lastObject scale];
@@ -1584,7 +1583,7 @@ static NSMutableSet* hostList;
}
bool needSwap = false;
if([self isFullScreenRequired]){ // if force fullscreen is enabled in app bundle, we use screen bounds to tell if a swap between width & height is needed
needSwap = (currentSettings.width.floatValue - currentSettings.height.floatValue) * (screenWidthInPoints - screenHeightInPoints) < 0; //update the current resolution accordingly
NSLog(@"need to swap width & height (non-app window mode): %d", needSwap);
@@ -1702,7 +1701,7 @@ static NSMutableSet* hostList;
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:NO];
// [self setupHostViewTitle];
// [self reloadScrollHostView]; //remove this for proper test
[self attachWaterMark];
@@ -1712,7 +1711,7 @@ static NSMutableSet* hostList;
selector:@selector(handleOrientationChange) // //force expand settings view to update resolution table, and all setting includes current fullscreen resolution will be updated.
name:UIDeviceOrientationDidChangeNotification
object:nil];
[[self revealViewController] setPrimaryViewController:self];
self.revealViewController.isStreaming = false; // tell the revealViewController streaming is finished
//[self.settingsButton setEnabled:![self isIPhonePortrait]]; //make sure settings button is disabled in iphone portrait mode.
@@ -1749,7 +1748,7 @@ static NSMutableSet* hostList;
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:NO];
/* this makes background color works*/
for (UIView *subview in self.view.subviews) {
[subview removeFromSuperview]; // 暂时移除所有子视图
@@ -1774,11 +1773,11 @@ static NSMutableSet* hostList;
[self.view addSubview:self.collectionView];
[self initHostCollection];
if(!_enteredAppView) [self switchToHostView];
[self retrieveSavedHosts];
_discMan = [[DiscoveryManager alloc] initWithHosts:[hostList allObjects] andCallback:self];
[self beginForegroundRefresh];
}
@@ -1870,7 +1869,7 @@ static NSMutableSet* hostList;
for (TemporaryHost* comp in sortedHostList) {
if(comp.state == StateOnline || comp.pairState == PairStatePaired) [self.hostCollectionVC addHost:comp];
// new host card test
// if([comp.name isEqualToString: @"ASRockPC"]) {
/*
@@ -1878,18 +1877,18 @@ static NSMutableSet* hostList;
//if([comp.name isEqualToString: @"PianoServer"]) {
//if([comp.name isEqualToString: @"TrueZj"]) {
HostCardView *testCard = [[HostCardView alloc] initWithHost:comp];
[self.view addSubview:testCard];
// [testCard resizeBySizeFactor:3];
[NSLayoutConstraint activateConstraints:@[
[testCard.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor constant:0],
[testCard.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor constant:150],
]];
}*/
//
// Start jobs to decode the box art in advance
for (TemporaryApp* app in comp.appList) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
@@ -1983,7 +1982,7 @@ static NSMutableSet* hostList;
}
_sortedAppList = visibleAppList;
}
[self.collectionView reloadData];
}
@@ -2065,7 +2064,7 @@ static NSMutableSet* hostList;
menuSeparator = [self findMenuSeparator];
if(menuSeparator.hidden || menuSeparator == nil) return;
if(gesture.state == UIGestureRecognizerStateBegan){
if(locationInView.x < 30) {
snapshot = [[UIView alloc] init];
@@ -2076,9 +2075,9 @@ static NSMutableSet* hostList;
}
return;
}
bool isPortrait = screenHeight>screenWidth;
CGFloat limitedWidth = MIN(MAX(locationInSuperView.x, isPortrait ? 200 : 280),isPortrait ? screenWidth*0.75 : screenWidth/2);
if(gesture.state == UIGestureRecognizerStateChanged){
if(snapshot) snapshot.center = CGPointMake(limitedWidth, snapshot.center.y);
@@ -2093,8 +2092,8 @@ static NSMutableSet* hostList;
Settings* settings = [dataMan retrieveSettings];
settings.settingsMenuWidth = [NSNumber numberWithFloat:self.revealViewController.rearViewRevealWidth];
[dataMan saveData];
double delayInSeconds = 0.02;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^{
@@ -2182,7 +2181,7 @@ static NSMutableSet* hostList;
// 添加为子控制器
[self addChildViewController:self.hostCollectionVC];
[self.view addSubview:self.hostCollectionVC.view];
// 设置其布局(Auto Layout 示例)
// CGFloat hostCollectionViewPadding = 75;
CGFloat leftPadding = [self isIPhone] ? 30 : 0;
@@ -2193,10 +2192,10 @@ static NSMutableSet* hostList;
[self.hostCollectionVC.view.trailingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.trailingAnchor constant:0],
// [self.hostCollectionVC.view.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor constant:0] //?
]];
// 通知子控制器已添加完成
[self.hostCollectionVC didMoveToParentViewController:self];
}
@end
@@ -10,6 +10,9 @@
//
#import "Connection.h"
#import "FloatBuffer.h"
#import "ImGuiRenderer.h"
#import "MetalViewController.h"
#import "StreamConfiguration.h"
#import "StreamView.h"
#import "LayoutOnScreenControlsViewController.h"
@@ -28,6 +31,8 @@
@property (nonatomic) StreamConfiguration* streamConfig;
@property (nonatomic, assign) MainFrameViewController *mainFrameViewcontroller;
@property (nonatomic, strong) MetalViewController *metalViewController;
@property (nonatomic, strong) ImGuiRenderer *imguiView;
-(void)updatePreferredDisplayMode:(BOOL)streamActive;
-(void)reConfigStreamViewRealtime;