Files
VoidLink/VoidLink/Stream/StreamManager.m
T
Travis Thieman df2b67ac99 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
2025-07-11 21:02:13 +08:00

224 lines
9.1 KiB
Objective-C

//
// StreamManager.m
// Moonlight
//
// Created by Diego Waxemberg on 10/20/14.
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
// Modified by True砖家 since 2024.7.9
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved.
//
#import "StreamManager.h"
#import "CryptoManager.h"
#import "HttpManager.h"
#import "Plot.h"
#import "Utils.h"
#import "StreamView.h"
#import "ServerInfoResponse.h"
#import "HttpResponse.h"
#import "HttpRequest.h"
#import "IdManager.h"
#import "LocalizationHelper.h"
#include <Limelight.h>
@implementation StreamManager {
StreamConfiguration* _config;
UIView* _renderView;
id<ConnectionCallbacks> _callbacks;
Connection* _connection;
}
- (id) initWithConfig:(StreamConfiguration*)config renderView:(UIView*)view connectionCallbacks:(id<ConnectionCallbacks>)callbacks {
self = [super init];
_config = config;
_renderView = view;
_callbacks = callbacks;
_config.riKey = [Utils randomBytes:16];
_config.riKeyId = arc4random();
return self;
}
- (void)main {
[CryptoManager generateKeyPairUsingSSL];
HttpManager* hMan = [[HttpManager alloc] initWithAddress:_config.host httpsPort:_config.httpsPort
serverCert:_config.serverCert];
ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[hMan newServerInfoRequest:false]
fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
NSString* pairStatus = [serverInfoResp getStringTag:@"PairStatus"];
NSString* appversion = [serverInfoResp getStringTag:@"appversion"];
NSString* gfeVersion = [serverInfoResp getStringTag:@"GfeVersion"];
NSString* serverState = [serverInfoResp getStringTag:@"state"];
if (![serverInfoResp isStatusOk]) {
[_callbacks launchFailed:serverInfoResp.statusMessage];
return;
}
else if (pairStatus == NULL || appversion == NULL || serverState == NULL) {
[_callbacks launchFailed:@"Failed to connect to PC"];
return;
}
if (![pairStatus isEqualToString:@"1"]) {
// Not paired
[_callbacks launchFailed:@"Device not paired to PC"];
return;
}
// Only perform this check on GFE (as indicated by MJOLNIR in state value)
if ((_config.width > 4096 || _config.height > 4096) && [serverState containsString:@"MJOLNIR"]) {
// Pascal added support for 8K HEVC encoding support. Maxwell 2 could encode HEVC but only up to 4K.
// We can't directly identify Pascal, but we can look for HEVC Main10 which was added in the same generation.
NSString* codecSupport = [serverInfoResp getStringTag:@"ServerCodecModeSupport"];
if (codecSupport == nil || !([codecSupport intValue] & 0x200)) {
[_callbacks launchFailed:@"Your host PC's GPU doesn't support streaming video resolutions over 4K."];
return;
}
}
// Populate the config's version fields from serverinfo
_config.appVersion = appversion;
_config.gfeVersion = gfeVersion;
// resumeApp and launchApp handle calling launchFailed
NSString* sessionUrl;
if ([serverState hasSuffix:@"_SERVER_BUSY"]) {
// App already running, resume it
if (![self resumeApp:hMan receiveSessionUrl:&sessionUrl]) {
return;
}
} else {
// Start app
if (![self launchApp:hMan receiveSessionUrl:&sessionUrl]) {
return;
}
}
// Populate RTSP session URL from launch/resume response
_config.rtspSessionUrl = sessionUrl;
// 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];
self->_connection = [[Connection alloc] initWithConfig:self->_config renderer:renderer connectionCallbacks:self->_callbacks];
NSOperationQueue* opQueue = [[NSOperationQueue alloc] init];
[opQueue addOperation:self->_connection];
});
}
- (void) stopStream
{
[_connection terminate];
}
- (BOOL) launchApp:(HttpManager*)hMan receiveSessionUrl:(NSString**)sessionUrl {
HttpResponse* launchResp = [[HttpResponse alloc] init];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:launchResp withUrlRequest:[hMan newLaunchOrResumeRequest:@"launch" config:_config]]];
NSString *gameSession = [launchResp getStringTag:@"gamesession"];
if (![launchResp isStatusOk]) {
[_callbacks launchFailed:launchResp.statusMessage];
Log(LOG_E, @"Failed Launch Response: %@", launchResp.statusMessage);
return FALSE;
} else if (gameSession == NULL || [gameSession isEqualToString:@"0"]) {
[_callbacks launchFailed:@"Failed to launch app"];
Log(LOG_E, @"Failed to parse game session");
return FALSE;
}
*sessionUrl = [launchResp getStringTag:@"sessionUrl0"];
return TRUE;
}
- (BOOL) resumeApp:(HttpManager*)hMan receiveSessionUrl:(NSString**)sessionUrl {
HttpResponse* resumeResp = [[HttpResponse alloc] init];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:resumeResp withUrlRequest:[hMan newLaunchOrResumeRequest:@"resume" config:_config]]];
NSString* resume = [resumeResp getStringTag:@"resume"];
if (![resumeResp isStatusOk]) {
[_callbacks launchFailed:resumeResp.statusMessage];
Log(LOG_E, @"Failed Resume Response: %@", resumeResp.statusMessage);
return FALSE;
} else if (resume == NULL || [resume isEqualToString:@"0"]) {
[_callbacks launchFailed:@"Failed to resume app"];
Log(LOG_E, @"Failed to parse resume response");
return FALSE;
}
*sessionUrl = [resumeResp getStringTag:@"sessionUrl0"];
return TRUE;
}
- (NSString*) getStatsOverlayText: (uint16_t) overlayLevel {
video_stats_t stats;
// NSLog(@"overlayLevel: %d", overlayLevel);
if (!_connection) {
return nil;
}
if (![_connection getVideoStats:&stats]) {
return nil;
}
uint32_t rtt, variance;
NSString* latencyString;
if (LiGetEstimatedRttInfo(&rtt, &variance)) {
latencyString = [LocalizationHelper localizedStringForKey:@"%u ms (variance: %u ms)", rtt, variance];
}
else {
latencyString = @"N/A";
}
NSString* hostProcessingString;
// overlayLevel 2: detailed
if (stats.framesWithHostProcessingLatency != 0) {
hostProcessingString = [LocalizationHelper localizedStringForKey:@"\nHost processing latency min/max/avg: %.1f/%.1f/%.1f ms",
stats.minHostProcessingLatency / 10.f,
stats.maxHostProcessingLatency / 10.f,
(float)stats.totalHostProcessingLatency / stats.framesWithHostProcessingLatency / 10.f];
}
else {
// 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,
fps,
[_connection getActiveCodecName],
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