Complete refactor of project.

- now runs universally on iPhone, iPad, and simulator
- all paths should now be relative
This commit is contained in:
Diego Waxemberg
2014-08-31 13:13:46 -04:00
parent 457b6b13cc
commit 764f051318
114 changed files with 27797 additions and 16 deletions
+4 -3
View File
@@ -1,9 +1,9 @@
//
// AppDelegate.h
// Limelight
// Limelight-iOS
//
// Created by Diego Waxemberg on 8/30/14.
// Copyright (c) 2014 Limelight Stream. All rights reserved.
// Created by Diego Waxemberg on 1/17/14.
// Copyright (c) 2014 Diego Waxemberg. All rights reserved.
//
#import <UIKit/UIKit.h>
@@ -18,5 +18,6 @@
- (void)saveContext;
- (NSURL *)applicationDocumentsDirectory;
+ (NSOperationQueue*) getMainOpQueue;
@end
+14 -10
View File
@@ -1,9 +1,9 @@
//
// AppDelegate.m
// Limelight
// Limelight-iOS
//
// Created by Diego Waxemberg on 8/30/14.
// Copyright (c) 2014 Limelight Stream. All rights reserved.
// Created by Diego Waxemberg on 1/17/14.
// Copyright (c) 2014 Diego Waxemberg. All rights reserved.
//
#import "AppDelegate.h"
@@ -14,15 +14,19 @@
@synthesize managedObjectModel = _managedObjectModel;
@synthesize persistentStoreCoordinator = _persistentStoreCoordinator;
static NSOperationQueue* mainQueue;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
mainQueue = [[NSOperationQueue alloc]init];
return YES;
}
+ (NSOperationQueue*) getMainOpQueue
{
return mainQueue;
}
- (void)applicationWillResignActive:(UIApplication *)application
{
// 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.
@@ -58,7 +62,7 @@
if (managedObjectContext != nil) {
if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
@@ -90,7 +94,7 @@
if (_managedObjectModel != nil) {
return _managedObjectModel;
}
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"Limelight" withExtension:@"momd"];
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"Limelight_iOS" withExtension:@"momd"];
_managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
return _managedObjectModel;
}
@@ -103,7 +107,7 @@
return _persistentStoreCoordinator;
}
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"Limelight.sqlite"];
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"Limelight_iOS.sqlite"];
NSError *error = nil;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
+16
View File
@@ -0,0 +1,16 @@
//
// Connection.h
// Limelight-iOS
//
// Created by Diego Waxemberg on 1/19/14.
// Copyright (c) 2014 Diego Waxemberg. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface Connection : NSOperation <NSStreamDelegate>
-(id) initWithHost:(int)ipaddr width:(int)width height:(int)height;
-(void) main;
@end
+264
View File
@@ -0,0 +1,264 @@
//
// Connection.m
// Limelight-iOS
//
// Created by Diego Waxemberg on 1/19/14.
// Copyright (c) 2014 Diego Waxemberg. All rights reserved.
//
#import "Connection.h"
#import <AudioUnit/AudioUnit.h>
#import <AVFoundation/AVFoundation.h>
#include "Limelight.h"
#include "opus.h"
#include "VideoDecoder.h"
#include "VideoRenderer.h"
@implementation Connection {
IP_ADDRESS host;
STREAM_CONFIGURATION streamConfig;
CONNECTION_LISTENER_CALLBACKS clCallbacks;
DECODER_RENDERER_CALLBACKS drCallbacks;
AUDIO_RENDERER_CALLBACKS arCallbacks;
}
static OpusDecoder *opusDecoder;
#define PCM_BUFFER_SIZE 1024
#define OUTPUT_BUS 0
static short* decodedPcmBuffer;
static int filledPcmBuffer;
NSLock* audioRendererBlock;
AudioComponentInstance audioUnit;
bool started = false;
void DrSetup(int width, int height, int fps, void* context, int drFlags)
{
printf("Setup video\n");
nv_avc_init(width, height, DISABLE_LOOP_FILTER | FAST_DECODE | FAST_BILINEAR_FILTERING, 2);
}
void DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
{
unsigned char* data = (unsigned char*) malloc(decodeUnit->fullLength + nv_avc_get_input_padding_size());
if (data != NULL) {
int offset = 0;
int err;
PLENTRY entry = decodeUnit->bufferList;
while (entry != NULL) {
memcpy(&data[offset], entry->data, entry->length);
offset += entry->length;
entry = entry->next;
}
err = nv_avc_decode(data, decodeUnit->fullLength);
if (err != 0) {
printf("Decode failed: %d\n", err);
}
free(data);
}
}
void DrStart(void)
{
printf("Start video\n");
[VideoRenderer startRendering];
}
void DrStop(void)
{
printf("Stop video\n");
[VideoRenderer stopRendering];
}
void DrRelease(void)
{
printf("Release video\n");
nv_avc_destroy();
}
void ArInit(void)
{
int err;
printf("Init audio\n");
opusDecoder = opus_decoder_create(48000, 2, &err);
decodedPcmBuffer = malloc(PCM_BUFFER_SIZE);
}
void ArRelease(void)
{
printf("Release audio\n");
if (decodedPcmBuffer != NULL) {
free(decodedPcmBuffer);
decodedPcmBuffer = NULL;
}
if (opusDecoder != NULL) {
opus_decoder_destroy(opusDecoder);
opusDecoder = NULL;
}
}
void ArStart(void)
{
printf("Start audio\n");
}
void ArStop(void)
{
printf("Stop audio\n");
}
void ArDecodeAndPlaySample(char* sampleData, int sampleLength)
{
if (!started) {
AudioOutputUnitStart(audioUnit);
started = true;
}
filledPcmBuffer = opus_decode(opusDecoder, (unsigned char*)sampleData, sampleLength, decodedPcmBuffer, PCM_BUFFER_SIZE / 2, 0);
if (filledPcmBuffer > 0) {
// Return of opus_decode is samples per channel
filledPcmBuffer *= 4;
NSLog(@"pcmBuffer: %d", filledPcmBuffer);
//[audioRendererBlock lock];
}
}
-(id) initWithHost:(int)ipaddr width:(int)width height:(int)height
{
self = [super init];
host = ipaddr;
streamConfig.width = width;
streamConfig.height = height;
streamConfig.fps = 30;
drCallbacks.setup = DrSetup;
drCallbacks.start = DrStart;
drCallbacks.stop = DrStop;
drCallbacks.release = DrRelease;
drCallbacks.submitDecodeUnit = DrSubmitDecodeUnit;
arCallbacks.init = ArInit;
arCallbacks.start = ArStart;
arCallbacks.stop = ArStop;
arCallbacks.release = ArRelease;
arCallbacks.decodeAndPlaySample = ArDecodeAndPlaySample;
//////// Don't think any of this is used /////////
NSError *audioSessionError = nil;
AVAudioSession* audioSession = [AVAudioSession sharedInstance];
[audioSession setPreferredSampleRate:48000.0 error:&audioSessionError];
[audioSession setCategory: AVAudioSessionCategoryPlayAndRecord error: &audioSessionError];
[audioSession setPreferredOutputNumberOfChannels:2 error:&audioSessionError];
[audioSession setActive: YES error: &audioSessionError];
//////////////////////////////////////////////////
OSStatus status;
AudioComponentDescription audioDesc;
audioDesc.componentType = kAudioUnitType_Output;
audioDesc.componentSubType = kAudioUnitSubType_RemoteIO;
audioDesc.componentFlags = 0;
audioDesc.componentFlagsMask = 0;
audioDesc.componentManufacturer = kAudioUnitManufacturer_Apple;
status = AudioComponentInstanceNew(AudioComponentFindNext(NULL, &audioDesc), &audioUnit);
if (status) {
NSLog(@"Unable to instantiate new AudioComponent: %d", (int32_t)status);
}
AudioStreamBasicDescription audioFormat = {0};
audioFormat.mSampleRate = 48000;
audioFormat.mBitsPerChannel = 16;
audioFormat.mFormatID = kAudioFormatLinearPCM;
audioFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger;
audioFormat.mFramesPerPacket = 1;
audioFormat.mChannelsPerFrame = 2;
audioFormat.mBytesPerFrame = 960;
audioFormat.mBytesPerPacket = 960;
audioFormat.mReserved = 0;
status = AudioUnitSetProperty(audioUnit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Output,
OUTPUT_BUS,
&audioFormat,
sizeof(audioFormat));
if (status) {
NSLog(@"Unable to set audio unit to output: %d", (int32_t)status);
}
AURenderCallbackStruct callbackStruct = {0};
callbackStruct.inputProc = playbackCallback;
callbackStruct.inputProcRefCon = NULL;
status = AudioUnitSetProperty(audioUnit,
kAudioUnitProperty_SetRenderCallback,
kAudioUnitScope_Global,
OUTPUT_BUS,
&callbackStruct,
sizeof(callbackStruct));
if (status) {
NSLog(@"Unable to set audio unit callback: %d", (int32_t)status);
}
status = AudioUnitInitialize(audioUnit);
if (status) {
NSLog(@"Unable to initialize audioUnit: %d", (int32_t)status);
}
return self;
}
static OSStatus playbackCallback(void *inRefCon,
AudioUnitRenderActionFlags *ioActionFlags,
const AudioTimeStamp *inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList *ioData) {
// Notes: ioData contains buffers (may be more than one!)
// Fill them up as much as you can. Remember to set the size value in each buffer to match how
// much data is in the buffer.
NSLog(@"Playback callback");
for (int i = 0; i < ioData->mNumberBuffers; i++) {
ioData->mBuffers[i].mNumberChannels = 2;
int min = MIN(ioData->mBuffers[i].mDataByteSize, filledPcmBuffer);
NSLog(@"Min: %d", min);
memcpy(ioData->mBuffers[i].mData, decodedPcmBuffer, min);
ioData->mBuffers[i].mDataByteSize = min;
filledPcmBuffer -= min;
}
//[audioRendererBlock unlock];
return noErr;
}
-(void) main
{
LiStartConnection(host, &streamConfig, &clCallbacks, &drCallbacks, &arCallbacks, NULL, 0);
}
@end
+19
View File
@@ -0,0 +1,19 @@
//
// ConnectionHandler.h
// Limelight-iOS
//
// Created by Diego Waxemberg on 1/27/14.
// Copyright (c) 2014 Diego Waxemberg. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface ConnectionHandler : NSOperation
@property NSString* hostName;
@property int mode;
+ (void) pairWithHost:(NSString*)host;
+ (void) streamWithHost:(NSString*)host viewController:(UIViewController*)viewCont;
+ (NSString*) resolveHost:(NSString*)host;
@end
+336
View File
@@ -0,0 +1,336 @@
//
// ConnectionHandler.m
// Limelight-iOS
//
// Created by Diego Waxemberg on 1/27/14.
// Copyright (c) 2014 Diego Waxemberg. All rights reserved.
//
#import "ConnectionHandler.h"
#import "AppDelegate.h"
#import <AdSupport/ASIdentifierManager.h>
#import "MainFrameViewController.h"
#include <libxml2/libxml/xmlreader.h>
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
@implementation ConnectionHandler
static const int PAIR_MODE = 0x1;
static const int STREAM_MODE = 0x2;
static MainFrameViewController* viewCont;
+ (void)streamWithHost:(NSString *)host viewController:(MainFrameViewController *)viewController
{
viewCont = viewController;
ConnectionHandler* streamHandler = [[ConnectionHandler alloc] initForStream:host];
[[AppDelegate getMainOpQueue] addOperation:streamHandler];
}
+ (void)pairWithHost:(NSString *)host
{
ConnectionHandler* pairHandler = [[ConnectionHandler alloc] initForPair:host];
[[AppDelegate getMainOpQueue]addOperation:pairHandler];
}
+ (NSString*) resolveHost:(NSString*)host
{
struct hostent *hostent;
if (inet_addr([host UTF8String]) != INADDR_NONE)
{
// Already an IP address
return host;
}
else
{
hostent = gethostbyname([host UTF8String]);
if (hostent != NULL)
{
char* ipstr = inet_ntoa(*(struct in_addr*)hostent->h_addr_list[0]);
NSLog(@"Resolved %@ -> %s", host, ipstr);
return [NSString stringWithUTF8String:ipstr];
}
else
{
NSLog(@"Failed to resolve host: %d", h_errno);
return nil;
}
}
}
+ (NSString*) getId
{
// we need to generate some unique id
NSUUID* uniqueId = [ASIdentifierManager sharedManager].advertisingIdentifier;
NSString* idString = [NSString stringWithString:[uniqueId UUIDString]];
//use just the last 12 digits because GameStream truncates id's
return [idString substringFromIndex:24];
}
+ (NSString*) getName
{
NSString* name = [UIDevice currentDevice].name;
NSLog(@"Device Name: %@", name);
//sanitize name
name = [name stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
return name;
}
- (id) initForStream:(NSString*)host
{
self = [super init];
self.hostName = host;
self.mode = STREAM_MODE;
return self;
}
- (id) initForPair:(NSString*)host
{
self = [super init];
self.hostName = host;
self.mode = PAIR_MODE;
return self;
}
- (void) main
{
switch (self.mode)
{
case STREAM_MODE:
{
bool pairState;
int err = [self pairState:&pairState];
if (err) {
NSLog(@"ERROR: Pair state error: %d", err);
break;
}
if (!pairState)
{
NSLog(@"WARN: Not paired with host!");
UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"Not Paired"
message:@"This device is not paired with the host."
delegate:nil
cancelButtonTitle:@"Okay"
otherButtonTitles:nil, nil];
[self showAlert:alert];
return;
}
//TODO: parse this from http request
unsigned int appId = 67339056;
NSData* applist = [self httpRequest:[NSString stringWithFormat:@"applist?uniqueid=%@", [ConnectionHandler getId]]];
NSString* streamString = [NSString stringWithFormat:@"http://%@:47989/launch?uniqueid=%@&appid=%u", [ConnectionHandler resolveHost:self.hostName], [ConnectionHandler getId], appId];
NSLog(@"host: %@", self.hostName);
NSURL* url = [[NSURL alloc] initWithString:streamString];
NSMutableURLRequest* request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setHTTPMethod:@"GET"];
NSURLResponse* response = nil;
NSData *response1 = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:NULL];
[viewCont performSelector:@selector(segueIntoStream) onThread:[NSThread mainThread] withObject:nil waitUntilDone:NO];
break;
}
case PAIR_MODE:
{
NSLog(@"Trying to pair to %@...", self.hostName);
bool pairState;
int err = [self pairState:&pairState];
if (err) {
NSLog(@"ERROR: Pair state error: %d", err);
break;
}
NSLog(@"Pair state: %i", pairState);
if(pairState)
{
UIAlertView* alert = [[UIAlertView alloc]initWithTitle:@"Pairing"
message:[NSString stringWithFormat:@"Already paired to:\n %@", self.hostName]
delegate:nil
cancelButtonTitle:@"Okay"
otherButtonTitles:nil, nil];
[self showAlert:alert];
NSLog(@"Showing alertview");
break;
}
// this will hang until it pairs successfully or unsuccessfully
NSData* pairResponse = [self httpRequest:[NSString stringWithFormat:@"pair?uniqueid=%@&devicename=%@", [ConnectionHandler getId], [ConnectionHandler getName]]];
void* buffer = [self getXMLBufferFromData:pairResponse];
if (buffer == NULL) {
NSLog(@"ERROR: Unable to allocate pair buffer.");
UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"Pairing"
message:[NSString stringWithFormat:@"Failed to pair with host:\n %@", self.hostName]
delegate:nil
cancelButtonTitle:@"Okay"
otherButtonTitles:nil, nil];
[self showAlert:alert];
break;
}
xmlDocPtr doc = xmlParseMemory(buffer, [pairResponse length]-1);
if (doc == NULL) {
NSLog(@"ERROR: An error occured trying to parse pair response.");
break;
}
xmlNodePtr node = xmlDocGetRootElement(doc);
node = node->xmlChildrenNode;
while (node != NULL) {
NSLog(@"Node: %s", node->name);
if (!xmlStrcmp(node->name, (const xmlChar*)"sessionid"))
{
xmlChar* sessionid = xmlNodeListGetString(doc, node->xmlChildrenNode, 1);
if (xmlStrcmp(sessionid, (xmlChar*)"0")) {
UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"Pairing"
message:[NSString stringWithFormat:@"Successfully paired to host:\n %@", self.hostName]
delegate:nil
cancelButtonTitle:@"Okay"
otherButtonTitles:nil, nil];
[self showAlert:alert];
} else {
UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"Pairing"
message:[NSString stringWithFormat:@"Pairing was declined by host:\n %@", self.hostName]
delegate:nil
cancelButtonTitle:@"Okay"
otherButtonTitles:nil, nil];
[self showAlert:alert];
}
xmlFree(sessionid);
goto cleanup;
}
node = node->next;
}
NSLog(@"ERROR: Could not find \"sessionid\" element in XML");
cleanup:
xmlFreeNode(node);
break;
}
default:
{
NSLog(@"Invalid connection mode specified!!!");
}
}
}
- (int) pairState: (bool*)paired
{
int err;
NSData* pairData = [self httpRequest:[NSString stringWithFormat:@"pairstate?uniqueid=%@", [ConnectionHandler getId]]];
void* buffer = [self getXMLBufferFromData:pairData];
if (buffer == NULL) {
NSLog(@"ERROR: Unable to allocate pair state buffer.");
UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"Pairing"
message:[NSString stringWithFormat:@"Failed to get pair state with host:\n %@", self.hostName]
delegate:nil
cancelButtonTitle:@"Okay"
otherButtonTitles:nil, nil];
[self showAlert:alert];
return -1;
}
xmlDocPtr doc = xmlParseMemory(buffer, [pairData length]-1);
if (doc == NULL) {
NSLog(@"ERROR: An error occured trying to parse pair state.");
return -1;
}
xmlNodePtr node = xmlDocGetRootElement(doc);
while (node != NULL) {
NSLog(@"Node: %s", node->name);
if (!xmlStrcmp(node->name, (const xmlChar*)"paired"))
{
xmlChar* pairState = xmlNodeListGetString(doc, node->xmlChildrenNode, 1);
*paired = !xmlStrcmp(pairState, (const xmlChar*)"1");
err = 0;
xmlFree(pairState);
goto cleanup;
}
node = node->xmlChildrenNode;
}
NSLog(@"ERROR: Could not find \"paired\" element in XML");
err = -2;
cleanup:
xmlFreeNode(node);
return err;
}
- (NSData*) httpRequest:(NSString*)args
{
NSString* requestString = [NSString stringWithFormat:@"http://%@:47989/%@", [ConnectionHandler resolveHost:self.hostName], args];
NSLog(@"Making HTTP request: %@", requestString);
NSURL* url = [[NSURL alloc] initWithString:requestString];
NSMutableURLRequest* request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setHTTPMethod:@"GET"];
[request setTimeoutInterval:7];
NSURLResponse* response = nil;
NSError* error = nil;
NSData* responseData = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
if (error != nil) {
NSLog(@"An error occured making HTTP request: %@\n Caused by: %@", [error localizedDescription], [error localizedFailureReason]);
}
return responseData;
}
- (void*) getXMLBufferFromData:(NSData*)data
{
if (data == nil) {
NSLog(@"ERROR: No data to get XML from!");
return NULL;
}
char* buffer = malloc([data length] + 1);
if (buffer == NULL) {
NSLog(@"ERROR: Unable to allocate XML buffer.");
return NULL;
}
[data getBytes:buffer length:[data length]];
buffer[[data length]] = 0;
NSLog(@"Buffer: %s", buffer);
//#AllTheJank
void* newBuffer = (void*)[[[NSString stringWithUTF8String:buffer]stringByReplacingOccurrencesOfString:@"UTF-16" withString:@"UTF-8" options:NSCaseInsensitiveSearch range:NSMakeRange(0, [data length])] UTF8String];
NSLog(@"New Buffer: %s", newBuffer);
free(buffer);
return newBuffer;
}
- (void) showAlert:(UIAlertView*) alert
{
[alert performSelector:@selector(show) onThread:[NSThread mainThread] withObject:nil waitUntilDone:NO];
}
@end
+8 -3
View File
@@ -24,20 +24,25 @@
<string>1.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIMainStoryboardFile</key>
<string>MainFrame-iPhone</string>
<key>UIMainStoryboardFile~ipad</key>
<string>MainFrame-iPad</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UIStatusBarHidden</key>
<true/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
+21
View File
@@ -0,0 +1,21 @@
//
// MainFrameViewController.h
// Limelight-iOS
//
// Created by Diego Waxemberg on 1/17/14.
// Copyright (c) 2014 Diego Waxemberg. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface MainFrameViewController : UIViewController <UIPickerViewDataSource,UIPickerViewDelegate>
- (IBAction)StreamButton:(UIButton *)sender;
- (IBAction)PairButton:(UIButton *)sender;
@property (strong, nonatomic) IBOutlet UITextField *HostField;
@property (strong, nonatomic) IBOutlet UIPickerView *StreamConfigs;
@property (strong, nonatomic) NSArray* streamConfigVals;
+ (const char*)getHostAddr;
- (void) segueIntoStream;
@end
+78
View File
@@ -0,0 +1,78 @@
//
// MainFrameViewController.m
// Limelight-iOS
//
// Created by Diego Waxemberg on 1/17/14.
// Copyright (c) 2014 Diego Waxemberg. All rights reserved.
//
#import "MainFrameViewController.h"
#import "VideoDepacketizer.h"
#import "ConnectionHandler.h"
@interface MainFrameViewController ()
@end
@implementation MainFrameViewController
static NSString* hostAddr;
+ (const char*)getHostAddr
{
return [hostAddr UTF8String];
}
- (void)PairButton:(UIButton *)sender
{
NSLog(@"Pair Button Pressed!");
hostAddr = self.HostField.text;
[ConnectionHandler pairWithHost:hostAddr];
}
- (void)StreamButton:(UIButton *)sender
{
NSLog(@"Stream Button Pressed!");
hostAddr = self.HostField.text;
[ConnectionHandler streamWithHost:hostAddr viewController:self];
}
- (void) segueIntoStream {
[self performSegueWithIdentifier:@"createStreamFrame" sender:self];
}
- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component
{
return [self.streamConfigVals objectAtIndex:row];
}
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
{
//TODO: figure out how to save this info!!
}
// returns the number of 'columns' to display.
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
{
return 1;
}
// returns the # of rows in each component..
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
{
return 4;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
self.streamConfigVals = [[NSArray alloc] initWithObjects:@"1280x720 (30Hz)",@"1280x720 (60Hz)",@"1920x1080 (30Hz)",@"1920x1080 (60Hz)",nil];
self.HostField.text = @"neyer.case.edu";
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
+14
View File
@@ -0,0 +1,14 @@
//
// StreamFrameViewController.h
// Limelight-iOS
//
// Created by Diego Waxemberg on 1/18/14.
// Copyright (c) 2014 Diego Waxemberg. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "StreamView.h"
@interface StreamFrameViewController : UIViewController
@end
+61
View File
@@ -0,0 +1,61 @@
//
// StreamFrameViewController.m
// Limelight-iOS
//
// Created by Diego Waxemberg on 1/18/14.
// Copyright (c) 2014 Diego Waxemberg. All rights reserved.
//
#import "StreamFrameViewController.h"
#import "MainFrameViewController.h"
#import "VideoDepacketizer.h"
#import "Connection.h"
#import "VideoRenderer.h"
#import "ConnectionHandler.h"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
@interface StreamFrameViewController ()
@end
@implementation StreamFrameViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[UIApplication sharedApplication].idleTimerDisabled = YES;
StreamView* streamView = [[StreamView alloc] initWithFrame:self.view.frame];
streamView.backgroundColor = [UIColor blackColor];
[self.view addSubview:streamView];
[streamView setNeedsDisplay];
CGAffineTransform transform = CGAffineTransformMakeTranslation((streamView.frame.size.height/2) - (streamView.frame.size.width/2), (streamView.frame.size.width/2) - (streamView.frame.size.height/2));
transform = CGAffineTransformRotate(transform, M_PI_2);
transform = CGAffineTransformScale(transform, -1, -1);
streamView.transform = transform;
// Repositions and resizes the view.
CGRect contentRect = CGRectMake(0,0, self.view.frame.size.width, self.view.frame.size.height);
streamView.bounds = contentRect;
Connection* conn = [[Connection alloc] initWithHost:inet_addr([[ConnectionHandler resolveHost:[NSString stringWithUTF8String:[MainFrameViewController getHostAddr]]] UTF8String]) width:1280 height:720];
NSOperationQueue* opQueue = [[NSOperationQueue alloc] init];
[opQueue addOperation:conn];
[opQueue addOperation:[[VideoRenderer alloc]initWithTarget:streamView]];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
+13
View File
@@ -0,0 +1,13 @@
//
// StreamView.h
// Limelight-iOS
//
// Created by Diego Waxemberg on 1/18/14.
// Copyright (c) 2014 Diego Waxemberg. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface StreamView : UIView
@end
+73
View File
@@ -0,0 +1,73 @@
//
// StreamView.m
// Limelight-iOS
//
// Created by Diego Waxemberg on 1/18/14.
// Copyright (c) 2014 Diego Waxemberg. All rights reserved.
//
#import "StreamView.h"
#import "VideoDecoder.h"
#import "VideoRenderer.h"
@implementation StreamView {
size_t width;
size_t height;
size_t bitsPerComponent;
size_t bytesPerRow;
CGColorSpaceRef colorSpace;
CGContextRef bitmapContext;
CGImageRef image;
unsigned char* pixelData;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
// Initialization code
width = 1280;
height = 720;
bitsPerComponent = 8;
bytesPerRow = (bitsPerComponent / 8) * width * 4;
pixelData = malloc(width * height * 4);
colorSpace = CGColorSpaceCreateDeviceRGB();
return self;
}
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
if (![VideoRenderer isRendering]) {
return;
}
if (!nv_avc_get_rgb_frame((char*)pixelData, width*height*4))
{
//NSLog(@"no new decoded frame!");
//return;
}
bitmapContext = CGBitmapContextCreate(pixelData, width, height, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Little);
image = CGBitmapContextCreateImage(bitmapContext);
struct CGContext* context = UIGraphicsGetCurrentContext();
CGContextSetBlendMode(context, kCGBlendModeCopy);
CGContextSetInterpolationQuality(context, kCGInterpolationNone);
CGContextSetShouldAntialias(context, false);
CGContextRotateCTM(context, -M_PI_2);
CGContextScaleCTM(context, -(float)self.frame.size.width/self.frame.size.height, (float)self.frame.size.height/self.frame.size.width);
CGContextDrawImage(context, rect, image);
CGImageRelease(image);
[super drawRect:rect];
}
@end
+46
View File
@@ -0,0 +1,46 @@
//
// VideoDecoder.h
// Limelight-iOS
//
// Created by Diego Waxemberg on 1/18/14.
// Copyright (c) 2014 Diego Waxemberg. All rights reserved.
//
#import <Foundation/Foundation.h>
// Disables the deblocking filter at the cost of image quality
#define DISABLE_LOOP_FILTER 0x1
// Uses the low latency decode flag (disables multithreading)
#define LOW_LATENCY_DECODE 0x2
// Threads process each slice, rather than each frame
#define SLICE_THREADING 0x4
// Uses nonstandard speedup tricks
#define FAST_DECODE 0x8
// Uses bilinear filtering instead of bicubic
#define BILINEAR_FILTERING 0x10
// Uses a faster bilinear filtering with lower image quality
#define FAST_BILINEAR_FILTERING 0x20
// Disables color conversion (output is NV21)
#define NO_COLOR_CONVERSION 0x40
// Native color format: RGB0
#define NATIVE_COLOR_RGB0 0x80
// Native color format: 0RGB
#define NATIVE_COLOR_0RGB 0x100
// Native color format: ARGB
#define NATIVE_COLOR_ARGB 0x200
// Native color format: RGBA
#define NATIVE_COLOR_RGBA 0x400
@interface VideoDecoder : NSObject
int nv_avc_init(int width, int height, int perf_lvl, int thread_count);
void nv_avc_destroy(void);
int nv_avc_get_raw_frame(char* buffer, int size);
int nv_avc_get_rgb_frame(char* buffer, int size);
int nv_avc_get_rgb_frame_int(int* buffer, int size);
int nv_avc_redraw(void);
int nv_avc_get_input_padding_size(void);
int nv_avc_decode(unsigned char* indata, int inlen);
@end
+335
View File
@@ -0,0 +1,335 @@
//
// VideoDecoder.m
// Limelight-iOS
//
// Created by Diego Waxemberg on 1/18/14.
// Copyright (c) 2014 Diego Waxemberg. All rights reserved.
//
#import "VideoDecoder.h"
#import "avcodec.h"
#import "swscale.h"
#include <pthread.h>
@implementation VideoDecoder
- (id) init
{
self = [super init];
return self;
}
// General decoder and renderer state
AVPacket pkt;
AVCodec* decoder;
AVCodecContext* decoder_ctx;
AVFrame* yuv_frame;
AVFrame* dec_frame;
pthread_mutex_t mutex;
// Color conversion and rendering
AVFrame* rgb_frame;
char* rgb_frame_buf;
struct SwsContext* scaler_ctx;
int render_pix_fmt;
#define BYTES_PER_PIXEL 4
// This function must be called before
// any other decoding functions
int nv_avc_init(int width, int height, int perf_lvl, int thread_count) {
int err;
int filtering;
pthread_mutex_init(&mutex, NULL);
// Initialize the avcodec library and register codecs
av_log_set_level(AV_LOG_QUIET);
avcodec_register_all();
av_init_packet(&pkt);
decoder = avcodec_find_decoder(AV_CODEC_ID_H264);
if (decoder == NULL) {
// __android_log_write(ANDROID_LOG_ERROR, "NVAVCDEC",
// "Couldn't find H264 decoder");
return -1;
}
decoder_ctx = avcodec_alloc_context3(decoder);
if (decoder_ctx == NULL) {
//__android_log_write(ANDROID_LOG_ERROR, "NVAVCDEC",
// "Couldn't allocate context");
return -1;
}
// Show frames even before a reference frame
decoder_ctx->flags2 |= CODEC_FLAG2_SHOW_ALL;
if (perf_lvl & DISABLE_LOOP_FILTER) {
// Skip the loop filter for performance reasons
decoder_ctx->skip_loop_filter = AVDISCARD_ALL;
}
if (perf_lvl & LOW_LATENCY_DECODE) {
// Use low delay single threaded encoding
decoder_ctx->flags |= CODEC_FLAG_LOW_DELAY;
}
if (perf_lvl & SLICE_THREADING) {
decoder_ctx->thread_type = FF_THREAD_SLICE;
}
else {
decoder_ctx->thread_type = FF_THREAD_FRAME;
}
decoder_ctx->thread_count = thread_count;
decoder_ctx->width = width;
decoder_ctx->height = height;
decoder_ctx->pix_fmt = PIX_FMT_YUV420P;
render_pix_fmt = AV_PIX_FMT_BGR0;
err = avcodec_open2(decoder_ctx, decoder, NULL);
if (err < 0) {
//__android_log_write(ANDROID_LOG_ERROR, "NVAVCDEC",
// "Couldn't open codec");
return err;
}
dec_frame = av_frame_alloc();
if (dec_frame == NULL) {
//__android_log_write(ANDROID_LOG_ERROR, "NVAVCDEC",
// "Couldn't allocate frame");
return -1;
}
if (!(perf_lvl & NO_COLOR_CONVERSION)) {
rgb_frame = av_frame_alloc();
if (rgb_frame == NULL) {
//__android_log_write(ANDROID_LOG_ERROR, "NVAVCDEC",
// "Couldn't allocate frame");
return -1;
}
rgb_frame_buf = (char*)av_malloc(width * height * BYTES_PER_PIXEL);
if (rgb_frame_buf == NULL) {
//__android_log_write(ANDROID_LOG_ERROR, "NVAVCDEC",
// "Couldn't allocate picture");
return -1;
}
err = avpicture_fill((AVPicture*)rgb_frame,
rgb_frame_buf,
render_pix_fmt,
decoder_ctx->width,
decoder_ctx->height);
if (err < 0) {
//__android_log_write(ANDROID_LOG_ERROR, "NVAVCDEC",
// "Couldn't fill picture");
return err;
}
if (perf_lvl & FAST_BILINEAR_FILTERING) {
filtering = SWS_FAST_BILINEAR;
}
else if (perf_lvl & BILINEAR_FILTERING) {
filtering = SWS_BILINEAR;
}
else {
filtering = SWS_BICUBIC;
}
scaler_ctx = sws_getContext(decoder_ctx->width,
decoder_ctx->height,
decoder_ctx->pix_fmt,
decoder_ctx->width,
decoder_ctx->height,
render_pix_fmt,
filtering,
NULL, NULL, NULL);
if (scaler_ctx == NULL) {
//__android_log_write(ANDROID_LOG_ERROR, "NVAVCDEC",
// "Couldn't get scaler context");
return -1;
}
}
return 0;
}
// This function must be called after
// decoding is finished
void nv_avc_destroy(void) {
if (decoder_ctx) {
avcodec_close(decoder_ctx);
av_free(decoder_ctx);
decoder_ctx = NULL;
}
if (scaler_ctx) {
sws_freeContext(scaler_ctx);
scaler_ctx = NULL;
}
if (dec_frame) {
av_frame_free(&dec_frame);
dec_frame = NULL;
}
if (yuv_frame) {
av_frame_free(&yuv_frame);
yuv_frame = NULL;
}
if (rgb_frame) {
av_frame_free(&rgb_frame);
rgb_frame = NULL;
}
if (rgb_frame_buf) {
av_free(rgb_frame_buf);
rgb_frame_buf = NULL;
}
pthread_mutex_destroy(&mutex);
}
static AVFrame* dequeue_new_frame(void) {
AVFrame *our_yuv_frame = NULL;
pthread_mutex_lock(&mutex);
// Check if there's a new frame
if (yuv_frame) {
// We now own the decoder's frame and are
// responsible for freeing it when we're done
our_yuv_frame = yuv_frame;
yuv_frame = NULL;
}
pthread_mutex_unlock(&mutex);
return our_yuv_frame;
}
static int update_rgb_frame(void) {
AVFrame *our_yuv_frame;
int err;
our_yuv_frame = dequeue_new_frame();
if (our_yuv_frame == NULL) {
return 0;
}
// Convert the YUV image to RGB
err = sws_scale(scaler_ctx,
our_yuv_frame->data,
our_yuv_frame->linesize,
0,
decoder_ctx->height,
rgb_frame->data,
rgb_frame->linesize);
av_frame_free(&our_yuv_frame);
if (err != decoder_ctx->height) {
//__android_log_write(ANDROID_LOG_ERROR, "NVAVCDEC",
// "Scaling failed");
return 0;
}
return 1;
}
static int render_rgb_to_buffer(char* buffer, int size) {
int err;
// Draw the frame to the buffer
err = avpicture_layout((AVPicture*)rgb_frame,
render_pix_fmt,
decoder_ctx->width,
decoder_ctx->height,
buffer,
size);
if (err < 0) {
// __android_log_write(ANDROID_LOG_ERROR, "NVAVCDEC",
// "Picture fill failed");
return 0;
}
return 1;
}
int nv_avc_get_raw_frame(char* buffer, int size) {
AVFrame *our_yuv_frame;
int err;
our_yuv_frame = dequeue_new_frame();
if (our_yuv_frame == NULL) {
return 0;
}
err = avpicture_layout((AVPicture*)our_yuv_frame,
decoder_ctx->pix_fmt,
decoder_ctx->width,
decoder_ctx->height,
buffer,
size);
av_frame_free(&our_yuv_frame);
return (err >= 0);
}
int nv_avc_get_rgb_frame(char* buffer, int size) {
return (update_rgb_frame() && render_rgb_to_buffer(buffer, size));
}
int nv_avc_get_input_padding_size(void) {
return FF_INPUT_BUFFER_PADDING_SIZE;
}
// packets must be decoded in order
// indata must be inlen + FF_INPUT_BUFFER_PADDING_SIZE in length
int nv_avc_decode(unsigned char* indata, int inlen) {
int err = 0;
int got_pic = 0;
pkt.data = indata;
pkt.size = inlen;
while (pkt.size > 0) {
got_pic = 0;
err = avcodec_decode_video2(
decoder_ctx,
dec_frame,
&got_pic,
&pkt);
if (err < 0) {
//__android_log_write(ANDROID_LOG_ERROR, "NVAVCDEC",
// "Decode failed");
got_pic = 0;
break;
}
pkt.size -= err;
pkt.data += err;
}
// Only copy the picture at the end of decoding the packet
if (got_pic) {
pthread_mutex_lock(&mutex);
// Only clone this frame if the last frame was taken.
// This saves on extra copies for frames that don't get
// rendered.
if (yuv_frame == NULL) {
// Clone a new frame
yuv_frame = av_frame_clone(dec_frame);
}
pthread_mutex_unlock(&mutex);
}
return err < 0 ? err : 0;
}
@end
+21
View File
@@ -0,0 +1,21 @@
//
// VideoDepacketizer.h
// Limelight-iOS
//
// Created by Diego Waxemberg on 1/18/14.
// Copyright (c) 2014 Diego Waxemberg. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "VideoDecoder.h"
@interface VideoDepacketizer : NSOperation <NSStreamDelegate>
@property uint8_t* byteBuffer;
@property unsigned int offset;
@property VideoDecoder* decoder;
@property NSString* file;
@property UIView* target;
- (id) initWithFile:(NSString*) file renderTarget:(UIView*)renderTarget;
@end
+78
View File
@@ -0,0 +1,78 @@
//
// VideoDepacketizer.m
// Limelight-iOS
//
// Created by Diego Waxemberg on 1/18/14.
// Copyright (c) 2014 Diego Waxemberg. All rights reserved.
//
#import "VideoDepacketizer.h"
@implementation VideoDepacketizer
static int BUFFER_LENGTH = 131072;
- (id)initWithFile:(NSString *)file renderTarget:(UIView*)renderTarget
{
self = [super init];
self.file = file;
self.target = renderTarget;
return self;
}
- (void)main
{
NSInputStream* inStream = [[NSInputStream alloc] initWithFileAtPath:self.file];
self.byteBuffer = malloc(BUFFER_LENGTH);
self.decoder = [[VideoDecoder alloc]init];
[inStream open];
while ([inStream streamStatus] != NSStreamStatusOpen) {
NSLog(@"stream status: %d", [inStream streamStatus]);
sleep(1);
}
while (true)
{
unsigned int len = 0;
len = [inStream read:self.byteBuffer maxLength:BUFFER_LENGTH];
if (len)
{
BOOL firstStart = false;
for (int i = 0; i < len - 4; i++) {
self.offset++;
if (self.byteBuffer[i] == 0 && self.byteBuffer[i+1] == 0
&& self.byteBuffer[i+2] == 0 && self.byteBuffer[i+3] == 1)
{
if (firstStart)
{
// decode the first i-1 bytes and render a frame
//[self.decoder decode:self.byteBuffer length:i];
[self.target performSelectorOnMainThread:@selector(setNeedsDisplay) withObject:NULL waitUntilDone:FALSE];
// move offset back to beginning of start sequence
[inStream setProperty:[[NSNumber alloc] initWithInt:self.offset-4] forKey:NSStreamFileCurrentOffsetKey];
self.offset -= 1;
break;
} else
{
firstStart = true;
}
}
}
}
else
{
NSLog(@"No Buffer! restarting file!");
// move offset back to beginning of start sequence
self.offset = 0;
[inStream close];
inStream = [[NSInputStream alloc] initWithFileAtPath:self.file];
[inStream open];
}
}
}
@end
+18
View File
@@ -0,0 +1,18 @@
//
// VideoRenderer.h
// Limelight-iOS
//
// Created by Diego Waxemberg on 1/19/14.
// Copyright (c) 2014 Diego Waxemberg. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface VideoRenderer : NSOperation
@property UIView* renderTarget;
- (id) initWithTarget:(UIView*)target;
+ (void) startRendering;
+ (void) stopRendering;
+ (BOOL) isRendering;
@end
+51
View File
@@ -0,0 +1,51 @@
//
// VideoRenderer.m
// Limelight-iOS
//
// Created by Diego Waxemberg on 1/19/14.
// Copyright (c) 2014 Diego Waxemberg. All rights reserved.
//
#import "VideoRenderer.h"
@implementation VideoRenderer
static bool render = false;
- (id)initWithTarget:(UIView *)target
{
self = [super init];
self.renderTarget = target;
return self;
}
- (void)main
{
while (true)
{
if (render)
{
[self.renderTarget performSelectorOnMainThread:@selector(setNeedsDisplay) withObject:NULL waitUntilDone:TRUE];
usleep(10000);
} else {
sleep(1);
}
}
}
+ (void) startRendering
{
render = true;
}
+ (void) stopRendering
{
render = false;
}
+ (BOOL) isRendering
{
return render;
}
@end