Files
VoidLink/VoidLink/Stream/StreamManager.m
T

297 lines
12 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 "DataManager.h"
#import "StreamView.h"
#import "ServerInfoResponse.h"
#import "HttpResponse.h"
#import "HttpRequest.h"
#import "IdManager.h"
#import "LocalizationHelper.h"
#import "VoidLink-Swift.h"
#include <Limelight.h>
@implementation StreamManager {
StreamConfiguration* _config;
UIView* _renderView;
id<ConnectionCallbacks> _callbacks;
Connection* _connection;
VideoDecoderRenderer* _videoRenderer;
}
- (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(), ^{
self->_videoRenderer = [[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:self->_videoRenderer connectionCallbacks:self->_callbacks];
NSOperationQueue* opQueue = [[NSOperationQueue alloc] init];
[opQueue addOperation:self->_connection];
});
}
- (void) setNeedRequeuing:(bool)needRequeuing{
if (_videoRenderer.queueSize==0) return;
[_videoRenderer.frameQueue clear];
_videoRenderer.needRequeuing = needRequeuing;
}
- (void) stopStream
{
[_connection terminate];
_callbacks = nil;
}
- (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;
if (!_connection) {
return nil;
}
if (![_connection getVideoStats:&stats]) {
return nil;
}
uint32_t rtt, variance;
NSString* latencyString;
if (LiGetEstimatedRttInfo(&rtt, &variance)) {
latencyString = [LocalizationHelper localizedStringForKey:@"%3u ms (var: %3u ms)", rtt, variance];
}
else {
latencyString = @"N/A";
}
NSString* hostProcessingString;
if (stats.framesWithHostProcessingLatency != 0) {
hostProcessingString = [LocalizationHelper localizedStringForKey:@"Host processing latency min/max/avg: %.1f/%.1f/%.1f ms\n",
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;
// Get frame pacing mode
DataManager* dataMan = [[DataManager alloc] init];
FramePacingMode framePacingMode = [[dataMan getSettings].framePacingMode integerValue];
// Calculate FPS differently based on pacing mode
float fps;
if (framePacingMode == FramePacingModeLegacy || framePacingMode == FramePacingModeOff) {
fps = stats.totalFrames / interval;
} else {
float scalePlotMetrics = stats.frameDropMetrics.nsamples > 0 ? ((float)stats.frameDropMetrics.nsamples / stats.totalFrames) : 1.0f;
fps = (stats.totalFrames - stats.networkDroppedFrames - (stats.frameDropMetrics.total / scalePlotMetrics)) / interval;
}
float interpolatedFps = stats.interpolatedFrames / interval;
NSString *renderingFpsString;
if (framePacingMode == FramePacingModeInterpolation) {
renderingFpsString = [NSString stringWithFormat:@"Rendering FPS: %dx%d %.2f+%.2f",
_config.width, _config.height, fps, interpolatedFps];
}
else {
renderingFpsString = [NSString stringWithFormat:@"Rendering FPS: %dx%d %.2f",
_config.width, _config.height, fps];
}
double avgVideoMbps = [_connection getBwTracker].averageMbps;
double peakVideoMbps = [_connection getBwTracker].peakMbps;
NSString* colorRange = _config.fullColorRange ? @"Full" : @"Limited";
if (overlayLevel == 1) {
if (framePacingMode == FramePacingModeInterpolation) {
return [LocalizationHelper localizedStringForKey:@"simplifiedOsdTextWithInterpolation",
fps,
interpolatedFps,
stats.networkDroppedFrames / interval,
avgVideoMbps,
latencyString];
}
return [LocalizationHelper localizedStringForKey:@"simplifiedOsdText",
fps,
stats.networkDroppedFrames / interval,
avgVideoMbps,
latencyString];
}
else {
if (framePacingMode == FramePacingModeLegacy || framePacingMode == FramePacingModeOff) {
NSString* rendererWithPacing = stats.renderingBackendString;
if ([stats.renderingBackendString isEqualToString:@"AVSampleBuffer"]) {
if (framePacingMode == FramePacingModeOff) {
rendererWithPacing = @"AVSampleBuffer (No Pacing)";
} else {
rendererWithPacing = @"AVSampleBuffer (Legacy Pacing)";
}
}
return [LocalizationHelper localizedStringForKey:@"%@ (Codec: %@, %@)\n"
"Bitrate: %.1f Mbps, Peak: %.1f\n"
"%@"
"Renderer: %@\n"
"Frames dropped by network: %.1f%%\n"
"Average network latency: %@",
renderingFpsString,
[_connection getActiveCodecName],
colorRange,
avgVideoMbps, peakVideoMbps,
hostProcessingString,
rendererWithPacing,
(stats.networkDroppedFrames / stats.totalFrames) * 100.0,
latencyString];
} else {
NSString* rendererWithPacing = stats.renderingBackendString;
if ([stats.renderingBackendString isEqualToString:@"AVSampleBuffer"]) {
rendererWithPacing = @"AVSampleBuffer (Queue Pacing)";
}
return [LocalizationHelper localizedStringForKey:@"%@ (Codec: %@, %@)\n"
"Bitrate: %.1f Mbps, Peak: %.1f, Frames buffered: %.1f\n"
"%@"
"Renderer: %@\n"
"Frames dropped by network/pacing jitter: %.1f%% / %.1f%%\n"
"Average network latency: %@\n"
"Decode time: %.2f/%.2f/%.2f ms %@",
renderingFpsString,
[_connection getActiveCodecName],
colorRange,
avgVideoMbps, peakVideoMbps, stats.frameQueueMetrics.avg,
hostProcessingString,
rendererWithPacing,
(stats.networkDroppedFrames / stats.totalFrames) * 100.0,
stats.frameDropMetrics.nsamples > 0 ? (stats.frameDropMetrics.total / stats.frameDropMetrics.nsamples) * 100.0 : 0.0f,
latencyString,
stats.decodeMetrics.min, stats.decodeMetrics.max, stats.decodeMetrics.avg, PublicUtils.onScreenRuntimeStats];
}
}
}
@end