mirror of
https://github.com/moonlight-stream/moonlight-ios.git
synced 2026-04-12 19:06:21 +00:00
Port for macOS (#311)
* merged moonlight-mac with moonlight-ios * reverted to the original project.pbxproj * cleaned up the code, fixed lots of unnecessary code duplications * multicontroller support (not tested) * new class that can be used for further modularization of the MainFrameViewController
This commit is contained in:
committed by
Cameron Gutman
parent
1c86c4485d
commit
6cc165b589
23
Moonlight macOS/ViewController/SettingsViewController.h
Normal file
23
Moonlight macOS/ViewController/SettingsViewController.h
Normal file
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// SettingsViewController.h
|
||||
// Moonlight macOS
|
||||
//
|
||||
// Created by Felix Kratz on 11.03.18.
|
||||
// Copyright © 2018 Felix Kratz. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@interface SettingsViewController : NSViewController
|
||||
@property (weak) IBOutlet NSTextField *textFieldResolutionHeight;
|
||||
@property (weak) IBOutlet NSTextField *textFieldResolutionWidth;
|
||||
@property (weak) IBOutlet NSTextField *textFieldBitrate;
|
||||
@property (weak) IBOutlet NSTextField *textFieldFPS;
|
||||
@property (weak) IBOutlet NSButton *buttonStreamingRemotelyToggle;
|
||||
- (NSString*) getCurrentHost;
|
||||
- (NSInteger) getChosenBitrate;
|
||||
- (NSInteger) getChosenStreamWidth;
|
||||
- (NSInteger) getChosenStreamHeight;
|
||||
- (NSInteger) getChosenFrameRate;
|
||||
- (NSInteger) getRemoteOptions;
|
||||
@end
|
||||
71
Moonlight macOS/ViewController/SettingsViewController.m
Normal file
71
Moonlight macOS/ViewController/SettingsViewController.m
Normal file
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// SettingsViewController.m
|
||||
// Moonlight macOS
|
||||
//
|
||||
// Created by Felix Kratz on 11.03.18.
|
||||
// Copyright © 2018 Felix Kratz. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SettingsViewController.h"
|
||||
#import "DataManager.h"
|
||||
|
||||
@interface SettingsViewController ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation SettingsViewController
|
||||
|
||||
NSString* host;
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
[self loadSettings];
|
||||
// Do view setup here.
|
||||
}
|
||||
|
||||
- (void) controlTextDidChange:(NSNotification *)obj {
|
||||
//[self saveSettings];
|
||||
}
|
||||
|
||||
- (NSInteger) getRemoteOptions {
|
||||
return _buttonStreamingRemotelyToggle.state == NSOnState ? 1 : 0;
|
||||
}
|
||||
|
||||
- (NSInteger) getChosenFrameRate {
|
||||
return _textFieldFPS.integerValue;
|
||||
}
|
||||
|
||||
- (NSInteger) getChosenStreamHeight {
|
||||
return _textFieldResolutionHeight.integerValue;
|
||||
}
|
||||
|
||||
- (NSInteger) getChosenStreamWidth {
|
||||
return _textFieldResolutionWidth.integerValue;
|
||||
}
|
||||
|
||||
- (NSInteger) getChosenBitrate {
|
||||
return _textFieldBitrate.integerValue;
|
||||
}
|
||||
|
||||
- (void) loadSettings {
|
||||
DataManager* dataMan = [[DataManager alloc] init];
|
||||
TemporarySettings* currentSettings = [dataMan getSettings];
|
||||
|
||||
// Bitrate is persisted in kbps
|
||||
_textFieldBitrate.integerValue = [currentSettings.bitrate integerValue];
|
||||
_textFieldFPS.integerValue = [currentSettings.framerate integerValue];
|
||||
_textFieldResolutionHeight.integerValue = [currentSettings.height integerValue];
|
||||
_textFieldResolutionWidth.integerValue = [currentSettings.width integerValue];
|
||||
_buttonStreamingRemotelyToggle.state = [currentSettings.streamingRemotely integerValue] == 0 ? NSOffState: NSOnState;
|
||||
}
|
||||
|
||||
- (void)didReceiveMemoryWarning {
|
||||
// Dispose of any resources that can be recreated.
|
||||
}
|
||||
|
||||
-(NSString*) getCurrentHost {
|
||||
return host;
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
26
Moonlight macOS/ViewController/StreamFrameViewController.h
Normal file
26
Moonlight macOS/ViewController/StreamFrameViewController.h
Normal file
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// StreamFrameViewController.h
|
||||
// Moonlight macOS
|
||||
//
|
||||
// Created by Felix Kratz on 09.03.18.
|
||||
// Copyright © 2018 Felix Kratz. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import "Connection.h"
|
||||
#import "StreamConfiguration.h"
|
||||
#import "StreamView.h"
|
||||
#import "ViewController.h"
|
||||
|
||||
@interface StreamFrameViewController : NSViewController <ConnectionCallbacks>
|
||||
|
||||
- (ViewController*) _origin;
|
||||
|
||||
- (void)setOrigin: (ViewController*) viewController;
|
||||
|
||||
@property (nonatomic) StreamConfiguration* streamConfig;
|
||||
@property (strong) IBOutlet StreamView *streamView;
|
||||
@property (weak) IBOutlet NSProgressIndicator *progressIndicator;
|
||||
@property (weak) IBOutlet NSTextField *stageLabel;
|
||||
|
||||
@end
|
||||
140
Moonlight macOS/ViewController/StreamFrameViewController.m
Normal file
140
Moonlight macOS/ViewController/StreamFrameViewController.m
Normal file
@@ -0,0 +1,140 @@
|
||||
//
|
||||
// StreamFrameViewController.m
|
||||
// Moonlight macOS
|
||||
//
|
||||
// Created by Felix Kratz on 09.03.18.
|
||||
// Copyright © 2018 Felix Kratz. All rights reserved.
|
||||
//
|
||||
|
||||
#import "StreamFrameViewController.h"
|
||||
#import "VideoDecoderRenderer.h"
|
||||
#import "StreamManager.h"
|
||||
#import "Gamepad.h"
|
||||
#import "keepAlive.h"
|
||||
#import "ControllerSupport.h"
|
||||
|
||||
@interface StreamFrameViewController ()
|
||||
@end
|
||||
|
||||
@implementation StreamFrameViewController {
|
||||
StreamManager *_streamMan;
|
||||
StreamConfiguration *_streamConfig;
|
||||
NSTimer* _eventTimer;
|
||||
NSTimer* _searchTimer;
|
||||
ViewController* _origin;
|
||||
ControllerSupport* _controllerSupport;
|
||||
}
|
||||
|
||||
-(ViewController*) _origin {
|
||||
return _origin;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
[keepAlive keepSystemAlive];
|
||||
self.streamConfig = _streamConfig;
|
||||
|
||||
_streamMan = [[StreamManager alloc] initWithConfig:self.streamConfig
|
||||
renderView:self.view
|
||||
connectionCallbacks:self];
|
||||
NSOperationQueue* opQueue = [[NSOperationQueue alloc] init];
|
||||
[opQueue addOperation:_streamMan];
|
||||
|
||||
// Initialize the controllers (GC and IOHID)
|
||||
// I have not tested it, but i think there will be a bug, when mixing GC and IOHID Controllers, because all GCControllers also register as IOHID Controllers.
|
||||
// To fix this, we need to get the IOHIDDeviceRef for the GCController and compare it with every IOHID Controller.
|
||||
// This shouldn't be a problem as long as this will not be put on the mac app store, as the IOHIDDeviceRef is a private field.
|
||||
// The other "fix" would be to disable explicit GC support for the mac version for now.
|
||||
// Can someone test this?
|
||||
_controllerSupport = [[ControllerSupport alloc] init];
|
||||
|
||||
// The gamepad currently gets polled at 60Hz, this could very well be set as 1/Framerate in the future.
|
||||
_eventTimer = [NSTimer scheduledTimerWithTimeInterval:1.0/60.0 target:self selector:@selector(eventTimerTick) userInfo:nil repeats:true];
|
||||
|
||||
// We search for new devices every second.
|
||||
_searchTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(searchTimerTick) userInfo:nil repeats:true];
|
||||
}
|
||||
|
||||
- (void)eventTimerTick {
|
||||
Gamepad_processEvents();
|
||||
}
|
||||
|
||||
- (void)searchTimerTick {
|
||||
Gamepad_detectDevices();
|
||||
}
|
||||
|
||||
- (void) viewDidAppear {
|
||||
[super viewDidAppear];
|
||||
|
||||
// Hide the cursor and disconnect it from the mouse movement
|
||||
[NSCursor hide];
|
||||
CGAssociateMouseAndMouseCursorPosition(false);
|
||||
|
||||
//During the setup the window should not be resizable, but to use the fullscreen feature of macOS it has to be resizable.
|
||||
[self.view.window setStyleMask:[self.view.window styleMask] | NSWindowStyleMaskResizable];
|
||||
|
||||
if (self.view.bounds.size.height != NSScreen.mainScreen.frame.size.height || self.view.bounds.size.width != NSScreen.mainScreen.frame.size.width) {
|
||||
[self.view.window toggleFullScreen:self];
|
||||
}
|
||||
[_progressIndicator startAnimation:nil];
|
||||
[_origin dismissController:nil];
|
||||
_origin = nil;
|
||||
}
|
||||
|
||||
-(void)viewWillDisappear {
|
||||
[NSCursor unhide];
|
||||
[keepAlive allowSleep];
|
||||
[_streamMan stopStream];
|
||||
CGAssociateMouseAndMouseCursorPosition(true);
|
||||
if (self.view.bounds.size.height == NSScreen.mainScreen.frame.size.height && self.view.bounds.size.width == NSScreen.mainScreen.frame.size.width) {
|
||||
[self.view.window toggleFullScreen:self];
|
||||
[self.view.window setStyleMask:[self.view.window styleMask] & ~NSWindowStyleMaskResizable];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)connectionStarted {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[_progressIndicator stopAnimation:nil];
|
||||
_progressIndicator.hidden = true;
|
||||
_stageLabel.stringValue = @"Waiting for the first frame";
|
||||
});
|
||||
}
|
||||
|
||||
- (void)connectionTerminated:(long)errorCode {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSLog(@"error has occured: %ld", errorCode);
|
||||
NSStoryboard *storyBoard = [NSStoryboard storyboardWithName:@"Mac" bundle:nil];
|
||||
ViewController* view = (ViewController*)[storyBoard instantiateControllerWithIdentifier :@"setupFrameVC"];
|
||||
[view setError:1];
|
||||
self.view.window.contentViewController = view;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)setOrigin: (ViewController*) viewController
|
||||
{
|
||||
_origin = viewController;
|
||||
}
|
||||
|
||||
- (void)displayMessage:(const char *)message {
|
||||
|
||||
}
|
||||
|
||||
- (void)displayTransientMessage:(const char *)message {
|
||||
}
|
||||
|
||||
- (void)launchFailed:(NSString *)message {
|
||||
|
||||
}
|
||||
|
||||
- (void)stageComplete:(const char *)stageName {
|
||||
|
||||
}
|
||||
|
||||
- (void)stageFailed:(const char *)stageName withError:(long)errorCode {
|
||||
|
||||
}
|
||||
|
||||
- (void)stageStarting:(const char *)stageName {
|
||||
}
|
||||
|
||||
@end
|
||||
35
Moonlight macOS/ViewController/ViewController.h
Normal file
35
Moonlight macOS/ViewController/ViewController.h
Normal file
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// ViewController.h
|
||||
// Moonlight macOS
|
||||
//
|
||||
// Created by Felix Kratz on 09.03.18.
|
||||
// Copyright © 2018 Felix Kratz. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <AppKit/AppKit.h>
|
||||
#import "PairManager.h"
|
||||
#import "StreamConfiguration.h"
|
||||
|
||||
@interface ViewController : NSViewController <PairCallback, NSURLConnectionDelegate>
|
||||
|
||||
- (IBAction)buttonLaunchPressed:(id)sender;
|
||||
- (IBAction)textFieldAction:(id)sender;
|
||||
- (IBAction)buttonConnectPressed:(id)sender;
|
||||
- (IBAction)buttonSettingsPressed:(id)sender;
|
||||
- (IBAction)popupButtonSelectionPressed:(id)sender;
|
||||
|
||||
- (void)setError:(long)errorCode;
|
||||
- (long) error;
|
||||
|
||||
@property (weak) IBOutlet NSLayoutConstraint *layoutConstraintSetupFrame;
|
||||
@property (weak) IBOutlet NSButton *buttonConnect;
|
||||
@property (weak) IBOutlet NSTextField *textFieldHost;
|
||||
@property (weak) IBOutlet NSButton *buttonSettings;
|
||||
@property (weak) IBOutlet NSButton *buttonLaunch;
|
||||
@property (weak) IBOutlet NSPopUpButton *popupButtonSelection;
|
||||
@property (weak) IBOutlet NSView *containerViewController;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
228
Moonlight macOS/ViewController/ViewController.m
Normal file
228
Moonlight macOS/ViewController/ViewController.m
Normal file
@@ -0,0 +1,228 @@
|
||||
//
|
||||
// ViewController.m
|
||||
// Moonlight macOS
|
||||
//
|
||||
// Created by Felix Kratz on 09.03.18.
|
||||
// Copyright © 2018 Felix Kratz. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ViewController.h"
|
||||
|
||||
#import "CryptoManager.h"
|
||||
#import "HttpManager.h"
|
||||
#import "Connection.h"
|
||||
#import "StreamManager.h"
|
||||
#import "Utils.h"
|
||||
#import "DataManager.h"
|
||||
#import "TemporarySettings.h"
|
||||
#import "WakeOnLanManager.h"
|
||||
#import "AppListResponse.h"
|
||||
#import "ServerInfoResponse.h"
|
||||
#import "StreamFrameViewController.h"
|
||||
#import "TemporaryApp.h"
|
||||
#import "IdManager.h"
|
||||
#import "SettingsViewController.h"
|
||||
#import "ConnectionHelper.h"
|
||||
|
||||
@implementation ViewController{
|
||||
NSOperationQueue* _opQueue;
|
||||
TemporaryHost* _selectedHost;
|
||||
NSString* _uniqueId;
|
||||
NSData* _cert;
|
||||
StreamConfiguration* _streamConfig;
|
||||
NSArray* _sortedAppList;
|
||||
NSSet* _appList;
|
||||
NSString* _host;
|
||||
SettingsViewController *_settingsView;
|
||||
CGFloat settingsFrameHeight;
|
||||
bool showSettings;
|
||||
NSAlert* _alert;
|
||||
long error;
|
||||
}
|
||||
|
||||
- (long)error {
|
||||
return error;
|
||||
}
|
||||
|
||||
- (void)setError:(long)errorCode {
|
||||
error = errorCode;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
[CryptoManager generateKeyPairUsingSSl];
|
||||
_uniqueId = [IdManager getUniqueId];
|
||||
_cert = [CryptoManager readCertFromFile];
|
||||
|
||||
_opQueue = [[NSOperationQueue alloc] init];
|
||||
|
||||
// Do any additional setup after loading the view.
|
||||
}
|
||||
- (void)viewDidAppear {
|
||||
[super viewDidAppear];
|
||||
|
||||
if (self.view.bounds.size.height == NSScreen.mainScreen.frame.size.height && self.view.bounds.size.width == NSScreen.mainScreen.frame.size.width)
|
||||
{
|
||||
[self.view.window toggleFullScreen:self];
|
||||
[self.view.window setStyleMask:[self.view.window styleMask] & ~NSWindowStyleMaskResizable];
|
||||
return;
|
||||
}
|
||||
[_buttonLaunch setEnabled:false];
|
||||
[_popupButtonSelection removeAllItems];
|
||||
_settingsView = [self.childViewControllers lastObject];
|
||||
|
||||
if (_settingsView.getCurrentHost != nil)
|
||||
_textFieldHost.stringValue = _settingsView.getCurrentHost;
|
||||
settingsFrameHeight = _layoutConstraintSetupFrame.constant;
|
||||
_layoutConstraintSetupFrame.constant = 0;
|
||||
showSettings = false;
|
||||
|
||||
if (error != 0) {
|
||||
[self showAlert:[NSString stringWithFormat: @"The connection terminated."]];
|
||||
}
|
||||
}
|
||||
|
||||
-(void) showAlert:(NSString*) message {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
_alert = [NSAlert new];
|
||||
_alert.messageText = message;
|
||||
[_alert beginSheetModalForWindow:[self.view window] completionHandler:^(NSInteger result) {
|
||||
NSLog(@"Success");
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
- (void)setRepresentedObject:(id)representedObject {
|
||||
[super setRepresentedObject:representedObject];
|
||||
|
||||
// Update the view, if already loaded.
|
||||
}
|
||||
|
||||
- (void) saveSettings {
|
||||
DataManager* dataMan = [[DataManager alloc] init];
|
||||
NSInteger framerate = [_settingsView getChosenFrameRate];
|
||||
NSInteger height = [_settingsView getChosenStreamHeight];
|
||||
NSInteger width = [_settingsView getChosenStreamWidth];
|
||||
NSInteger streamingRemotely = [_settingsView getRemoteOptions];
|
||||
NSInteger bitrate = [_settingsView getChosenBitrate];
|
||||
[dataMan saveSettingsWithBitrate:bitrate framerate:framerate height:height width:width
|
||||
onscreenControls:0 remote:streamingRemotely];
|
||||
}
|
||||
|
||||
|
||||
- (void)controlTextDidChange:(NSNotification *)obj {
|
||||
|
||||
}
|
||||
|
||||
- (IBAction)buttonLaunchPressed:(id)sender {
|
||||
[self saveSettings];
|
||||
DataManager* dataMan = [[DataManager alloc] init];
|
||||
TemporarySettings* streamSettings = [dataMan getSettings];
|
||||
_streamConfig = [[StreamConfiguration alloc] init];
|
||||
_streamConfig.frameRate = [streamSettings.framerate intValue];
|
||||
_streamConfig.bitRate = [streamSettings.bitrate intValue];
|
||||
_streamConfig.height = [streamSettings.height intValue];
|
||||
_streamConfig.width = [streamSettings.width intValue];
|
||||
_streamConfig.streamingRemotely = [streamSettings.streamingRemotely intValue];
|
||||
_streamConfig.host = _textFieldHost.stringValue;
|
||||
_streamConfig.appID = [_sortedAppList[_popupButtonSelection.indexOfSelectedItem] id];
|
||||
[self transitionToStreamView];
|
||||
}
|
||||
|
||||
- (IBAction)textFieldAction:(id)sender {
|
||||
[self buttonConnectPressed:self];
|
||||
}
|
||||
|
||||
- (IBAction)buttonConnectPressed:(id)sender {
|
||||
_host = _textFieldHost.stringValue;
|
||||
HttpManager* hMan = [[HttpManager alloc] initWithHost:_textFieldHost.stringValue
|
||||
uniqueId:_uniqueId
|
||||
deviceName:@"roth"
|
||||
cert:_cert];
|
||||
|
||||
ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
|
||||
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[hMan newServerInfoRequest]
|
||||
fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
|
||||
|
||||
if ([[serverInfoResp getStringTag:@"PairStatus"] isEqualToString:@"1"]) {
|
||||
NSLog(@"alreadyPaired");
|
||||
[self alreadyPaired];
|
||||
} else {
|
||||
// Polling the server while pairing causes the server to screw up
|
||||
NSLog(@"Pairing");
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
HttpManager* hMan = [[HttpManager alloc] initWithHost:_host uniqueId:_uniqueId deviceName:deviceName cert:_cert];
|
||||
PairManager* pMan = [[PairManager alloc] initWithManager:hMan andCert:_cert callback:self];
|
||||
[_opQueue addOperation:pMan];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (IBAction)buttonSettingsPressed:(id)sender {
|
||||
showSettings = !showSettings;
|
||||
if(showSettings) {
|
||||
_layoutConstraintSetupFrame.constant = settingsFrameHeight;
|
||||
}
|
||||
else {
|
||||
_layoutConstraintSetupFrame.constant = 0;
|
||||
}
|
||||
}
|
||||
|
||||
- (IBAction)popupButtonSelectionPressed:(id)sender {
|
||||
}
|
||||
|
||||
- (void)alreadyPaired {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[_popupButtonSelection setEnabled:true];
|
||||
[_popupButtonSelection setHidden:false];
|
||||
[_buttonConnect setEnabled:false];
|
||||
[_buttonConnect setHidden:true];
|
||||
[_buttonLaunch setEnabled:true];
|
||||
[_textFieldHost setEnabled:false];
|
||||
});
|
||||
[self searchForHost:_host];
|
||||
[self updateAppsForHost];
|
||||
[self populatePopupButton];
|
||||
}
|
||||
|
||||
- (void)searchForHost:(NSString*) hostAddress {
|
||||
_appList = [ConnectionHelper getAppListForHostWithHostIP:_textFieldHost.stringValue deviceName:deviceName cert:_cert uniqueID:_uniqueId].getAppList;
|
||||
}
|
||||
|
||||
- (void)populatePopupButton {
|
||||
for (int i = 0; i < _appList.count; i++) {
|
||||
[_popupButtonSelection addItemWithTitle:[_sortedAppList[i] name]];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)pairFailed:(NSString *)message {
|
||||
[self showAlert:[NSString stringWithFormat: @"%@", message]];
|
||||
}
|
||||
|
||||
- (void)pairSuccessful {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.view.window endSheet:_alert.window];
|
||||
_alert = nil;
|
||||
[self alreadyPaired];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)showPIN:(NSString *)PIN {
|
||||
[self showAlert:[NSString stringWithFormat: @"PIN: %@", PIN]];
|
||||
}
|
||||
|
||||
- (void) updateAppsForHost {
|
||||
_sortedAppList = [_appList allObjects];
|
||||
_sortedAppList = [_sortedAppList sortedArrayUsingSelector:@selector(compareName:)];
|
||||
}
|
||||
|
||||
- (void)transitionToStreamView {
|
||||
NSStoryboard *storyBoard = [NSStoryboard storyboardWithName:@"Mac" bundle:nil];
|
||||
StreamFrameViewController* streamFrame = (StreamFrameViewController*)[storyBoard instantiateControllerWithIdentifier :@"streamFrameVC"];
|
||||
streamFrame.streamConfig = _streamConfig;
|
||||
[streamFrame setOrigin:self];
|
||||
self.view.window.contentViewController = streamFrame;
|
||||
}
|
||||
|
||||
@end
|
||||
Reference in New Issue
Block a user