Merge branch 'master' of github.com:limelight-stream/limelight-ios

# By Cameron Gutman
# Via Cameron Gutman
* 'master' of github.com:limelight-stream/limelight-ios:
  Video is mostly working now. It's just chopped off in the view now.
  Add connection listener callbacks to fix crash on starting streaming
  Implement code for new video decoder (untested) based on http://stackoverflow.com/questions/25980070/how-to-use-avsamplebufferdisplaylayer-in-ios-8-for-rtp-h264-streams-with-gstream
  Target 8.0 so we can use Metal for decoding
  Rip out the old video renderer and decoder
This commit is contained in:
Diego Waxemberg
2014-10-19 04:20:21 -04:00
112 changed files with 243 additions and 25194 deletions

View File

@@ -7,10 +7,11 @@
//
#import <Foundation/Foundation.h>
#import "VideoDecoderRenderer.h"
@interface Connection : NSOperation <NSStreamDelegate>
-(id) initWithHost:(int)ipaddr width:(int)width height:(int)height;
-(id) initWithHost:(int)ipaddr width:(int)width height:(int)height renderer:(VideoDecoderRenderer*)renderer;
-(void) main;
@end

View File

@@ -7,13 +7,12 @@
//
#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;
@@ -31,23 +30,20 @@ static OpusDecoder *opusDecoder;
static short* decodedPcmBuffer;
static int filledPcmBuffer;
NSLock* audioRendererBlock;
AudioComponentInstance audioUnit;
bool started = false;
static AudioComponentInstance audioUnit;
static bool started = false;
static VideoDecoderRenderer* renderer;
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());
unsigned char* data = (unsigned char*) malloc(decodeUnit->fullLength);
if (data != NULL) {
int offset = 0;
int err;
PLENTRY entry = decodeUnit->bufferList;
while (entry != NULL) {
@@ -56,10 +52,7 @@ void DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
entry = entry->next;
}
err = nv_avc_decode(data, decodeUnit->fullLength);
if (err != 0) {
printf("Decode failed: %d\n", err);
}
[renderer submitDecodeBuffer:data length:decodeUnit->fullLength];
free(data);
}
@@ -68,19 +61,16 @@ void DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
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)
@@ -131,20 +121,57 @@ void ArDecodeAndPlaySample(char* sampleData, int sampleLength)
// Return of opus_decode is samples per channel
filledPcmBuffer *= 4;
NSLog(@"pcmBuffer: %d", filledPcmBuffer);
//[audioRendererBlock lock];
NSLog(@"pcmBuffer: %d", filledPcmBuffer);
}
}
-(id) initWithHost:(int)ipaddr width:(int)width height:(int)height
void ClStageStarting(int stage)
{
NSLog(@"Starting stage: %d", stage);
}
void ClStageComplete(int stage)
{
NSLog(@"Stage %d complete", stage);
}
void ClStageFailed(int stage, long errorCode)
{
NSLog(@"Stage %d failed: %ld", stage, errorCode);
}
void ClConnectionStarted(void)
{
NSLog(@"Connection started");
}
void ClConnectionTerminated(long errorCode)
{
NSLog(@"ConnectionTerminated: %ld", errorCode);
}
void ClDisplayMessage(char* message)
{
NSLog(@"DisplayMessage: %s", message);
}
void ClDisplayTransientMessage(char* message)
{
NSLog(@"DisplayTransientMessage: %s", message);
}
-(id) initWithHost:(int)ipaddr width:(int)width height:(int)height renderer:(VideoDecoderRenderer*)myRenderer
{
self = [super init];
host = ipaddr;
renderer = myRenderer;
streamConfig.width = width;
streamConfig.height = height;
streamConfig.fps = 30;
streamConfig.bitrate = 5000;
streamConfig.packetSize = 1024;
// FIXME: RI AES members
drCallbacks.setup = DrSetup;
drCallbacks.start = DrStart;
@@ -158,6 +185,13 @@ void ArDecodeAndPlaySample(char* sampleData, int sampleLength)
arCallbacks.release = ArRelease;
arCallbacks.decodeAndPlaySample = ArDecodeAndPlaySample;
clCallbacks.stageStarting = ClStageStarting;
clCallbacks.stageComplete = ClStageComplete;
clCallbacks.stageFailed = ClStageFailed;
clCallbacks.connectionStarted = ClConnectionStarted;
clCallbacks.connectionTerminated = ClConnectionTerminated;
clCallbacks.displayMessage = ClDisplayMessage;
clCallbacks.displayTransientMessage = ClDisplayTransientMessage;
//////// Don't think any of this is used /////////
NSError *audioSessionError = nil;
@@ -249,7 +283,6 @@ static OSStatus playbackCallback(void *inRefCon,
filledPcmBuffer -= min;
}
//[audioRendererBlock unlock];
return noErr;
}

View File

@@ -7,7 +7,6 @@
//
#import "MainFrameViewController.h"
#import "VideoDepacketizer.h"
#import "ConnectionHandler.h"
#import "Computer.h"
#import "CryptoManager.h"

View File

@@ -7,7 +7,6 @@
//
#import <UIKit/UIKit.h>
#import "StreamView.h"
@interface StreamFrameViewController : UIViewController

View File

@@ -8,9 +8,8 @@
#import "StreamFrameViewController.h"
#import "MainFrameViewController.h"
#import "VideoDepacketizer.h"
#import "Connection.h"
#import "VideoRenderer.h"
#import "VideoDecoderRenderer.h"
#import "ConnectionHandler.h"
#include <sys/socket.h>
@@ -28,28 +27,14 @@
[super viewDidLoad];
[UIApplication sharedApplication].idleTimerDisabled = YES;
VideoDecoderRenderer* renderer = [[VideoDecoderRenderer alloc]initWithView:self.view];
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];
Connection* conn = [[Connection alloc] initWithHost:inet_addr([[ConnectionHandler resolveHost:[NSString stringWithUTF8String:[MainFrameViewController getHostAddr]]] UTF8String]) width:1280 height:720
renderer: renderer];
NSOperationQueue* opQueue = [[NSOperationQueue alloc] init];
[opQueue addOperation:conn];
[opQueue addOperation:[[VideoRenderer alloc]initWithTarget:streamView]];
}
- (void)didReceiveMemoryWarning

View File

@@ -1,13 +0,0 @@
//
// 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

View File

@@ -1,73 +0,0 @@
//
// 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

View File

@@ -1,46 +0,0 @@
//
// 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

View File

@@ -1,337 +0,0 @@
//
// 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,
(unsigned char*)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;
}
const unsigned char* data = (unsigned char*)our_yuv_frame->data;
// Convert the YUV image to RGB
err = sws_scale(scaler_ctx,
&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,
(unsigned char*)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,
(unsigned char*)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

View File

@@ -1,21 +0,0 @@
//
// 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

View File

@@ -1,78 +0,0 @@
//
// 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

View File

@@ -1,18 +0,0 @@
//
// 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

View File

@@ -1,51 +0,0 @@
//
// 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

View File

@@ -0,0 +1,19 @@
//
// VideoDecoderRenderer.h
// Limelight
//
// Created by Cameron Gutman on 10/18/14.
// Copyright (c) 2014 Limelight Stream. All rights reserved.
//
#import <Foundation/Foundation.h>
@import AVFoundation;
@interface VideoDecoderRenderer : NSObject
- (id)initWithView:(UIView*)view;
- (void)submitDecodeBuffer:(unsigned char *)data length:(int)length;
@end

View File

@@ -0,0 +1,152 @@
//
// VideoDecoderRenderer.m
// Limelight
//
// Created by Cameron Gutman on 10/18/14.
// Copyright (c) 2014 Limelight Stream. All rights reserved.
//
#import "VideoDecoderRenderer.h"
@implementation VideoDecoderRenderer {
AVSampleBufferDisplayLayer* displayLayer;
Boolean waitingForSps, waitingForPpsA, waitingForPpsB;
NSData *spsData, *ppsDataA, *ppsDataB;
unsigned char ppsDataAFirstByte;
CMVideoFormatDescriptionRef formatDesc;
}
- (id)initWithView:(UIView*)view
{
self = [super init];
displayLayer = [[AVSampleBufferDisplayLayer alloc] init];
displayLayer.bounds = view.bounds;
displayLayer.backgroundColor = [UIColor greenColor].CGColor;
displayLayer.position = CGPointMake(CGRectGetMidX(view.bounds), CGRectGetMidY(view.bounds));
displayLayer.videoGravity = AVLayerVideoGravityResize;
[view.layer addSublayer:displayLayer];
// We need some parameter sets before we can properly start decoding frames
waitingForSps = true;
waitingForPpsA = true;
waitingForPpsB = true;
return self;
}
#define ES_START_PREFIX_SIZE 4
- (void)submitDecodeBuffer:(unsigned char *)data length:(int)length
{
unsigned char nalType = data[ES_START_PREFIX_SIZE] & 0x1F;
OSStatus status;
if (formatDesc == NULL && (nalType == 0x7 || nalType == 0x8)) {
if (waitingForSps && nalType == 0x7) {
NSLog(@"Got SPS");
spsData = [NSData dataWithBytes:&data[ES_START_PREFIX_SIZE] length:length - ES_START_PREFIX_SIZE];
waitingForSps = false;
}
// Nvidia's stream has 2 PPS NALUs so we'll wait for both of them
else if ((waitingForPpsA || waitingForPpsB) && nalType == 0x8) {
// Read the NALU's PPS index to figure out which PPS this is
printf("PPS BYTE: %02x", data[ES_START_PREFIX_SIZE + 1]);
if (waitingForPpsA) {
NSLog(@"Got PPS 1");
ppsDataA = [NSData dataWithBytes:&data[ES_START_PREFIX_SIZE] length:length - ES_START_PREFIX_SIZE];
waitingForPpsA = false;
ppsDataAFirstByte = data[ES_START_PREFIX_SIZE + 1];
}
else if (data[ES_START_PREFIX_SIZE + 1] != ppsDataAFirstByte) {
NSLog(@"Got PPS 2");
ppsDataA = [NSData dataWithBytes:&data[ES_START_PREFIX_SIZE] length:length - ES_START_PREFIX_SIZE];
waitingForPpsB = false;
}
}
// See if we've got all the parameter sets we need
if (!waitingForSps && !waitingForPpsA && !waitingForPpsB) {
const uint8_t* const parameterSetPointers[] = { [spsData bytes], [ppsDataA bytes], [ppsDataB bytes] };
const size_t parameterSetSizes[] = { [spsData length], [ppsDataA length], [ppsDataB length] };
NSLog(@"Constructing format description");
status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault,
2, /* count of parameter sets */
parameterSetPointers,
parameterSetSizes,
4 /* size of length prefix */,
&formatDesc);
if (status != noErr) {
NSLog(@"Failed to create format description: %d", (int)status);
formatDesc = NULL;
return;
}
}
// No frame data to submit for these NALUs
return;
}
if (formatDesc == NULL) {
// Can't decode if we haven't gotten our parameter sets yet
return;
}
if (nalType != 0x1 && nalType != 0x5) {
// Don't submit parameter set data
return;
}
// Now we're decoding actual frame data here
CMBlockBufferRef blockBuffer;
status = CMBlockBufferCreateWithMemoryBlock(NULL, data, length, kCFAllocatorNull, NULL, 0, length, 0, &blockBuffer);
if (status != noErr) {
NSLog(@"CMBlockBufferCreateWithMemoryBlock failed: %d", (int)status);
return;
}
// Compute the new length prefix to replace the 00 00 00 01
int dataLength = length - ES_START_PREFIX_SIZE;
const uint8_t lengthBytes[] = {(uint8_t)(dataLength >> 24), (uint8_t)(dataLength >> 16),
(uint8_t)(dataLength >> 8), (uint8_t)dataLength};
status = CMBlockBufferReplaceDataBytes(lengthBytes, blockBuffer, 0, 4);
if (status != noErr) {
NSLog(@"CMBlockBufferReplaceDataBytes failed: %d", (int)status);
return;
}
CMSampleBufferRef sampleBuffer;
const size_t sampleSizeArray[] = {length};
status = CMSampleBufferCreate(kCFAllocatorDefault,
blockBuffer, true, NULL,
NULL, formatDesc, 1, 0,
NULL, 1, sampleSizeArray,
&sampleBuffer);
if (status != noErr) {
NSLog(@"CMSampleBufferCreate failed: %d", (int)status);
return;
}
CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, YES);
CFMutableDictionaryRef dict = (CFMutableDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DisplayImmediately, kCFBooleanTrue);
CFDictionarySetValue(dict, kCMSampleAttachmentKey_IsDependedOnByOthers, kCFBooleanTrue);
if (nalType == 1) {
// P-frame
CFDictionarySetValue(dict, kCMSampleAttachmentKey_NotSync, kCFBooleanTrue);
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DependsOnOthers, kCFBooleanTrue);
}
else {
// I-frame
CFDictionarySetValue(dict, kCMSampleAttachmentKey_NotSync, kCFBooleanFalse);
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DependsOnOthers, kCFBooleanFalse);
}
[displayLayer enqueueSampleBuffer:sampleBuffer];
}
@end