Permalink
Cannot retrieve contributors at this time
1133 lines (923 sloc)
36.9 KB
| /***************************************************************************** | |
| * ios2.m: iOS OpenGL ES 2 provider | |
| ***************************************************************************** | |
| * Copyright (C) 2001-2015 VLC authors and VideoLAN | |
| * $Id$ | |
| * | |
| * Authors: Pierre d'Herbemont <pdherbemont at videolan dot org> | |
| * Felix Paul Kühne <fkuehne at videolan dot org> | |
| * David Fuhrmann <david dot fuhrmann at googlemail dot com> | |
| * Rémi Denis-Courmont | |
| * Laurent Aimar <fenrir _AT_ videolan _DOT_ org> | |
| * Eric Petit <titer@m0k.org> | |
| * | |
| * This program is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU Lesser General Public License as published by | |
| * the Free Software Foundation; either version 2.1 of the License, or | |
| * (at your option) any later version. | |
| * | |
| * This program is distributed in the hope that it will be useful, | |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| * GNU Lesser General Public License for more details. | |
| * | |
| * You should have received a copy of the GNU Lesser General Public License | |
| * along with this program; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. | |
| *****************************************************************************/ | |
| /***************************************************************************** | |
| * Preamble | |
| *****************************************************************************/ | |
| #import <UIKit/UIKit.h> | |
| #import <OpenGLES/EAGL.h> | |
| #import <OpenGLES/ES2/gl.h> | |
| #import <OpenGLES/ES2/glext.h> | |
| #import <QuartzCore/QuartzCore.h> | |
| #import <dlfcn.h> | |
| #ifdef HAVE_CONFIG_H | |
| # import "config.h" | |
| #endif | |
| #import <vlc_common.h> | |
| #import <vlc_plugin.h> | |
| #import <vlc_vout_display.h> | |
| #import <vlc_opengl.h> | |
| #import <vlc_dialog.h> | |
| #import "opengl.h" | |
| /** | |
| * Forward declarations | |
| */ | |
| enum | |
| { | |
| UNIFORM_Y, | |
| UNIFORM_UV, | |
| UNIFORM_COLOR_CONVERSION_MATRIX, | |
| UNIFORM_TRANSFORM_MATRIX, | |
| NUM_UNIFORMS | |
| }; | |
| GLint uniforms[NUM_UNIFORMS]; | |
| // Attribute index. | |
| enum | |
| { | |
| ATTRIB_VERTEX, | |
| ATTRIB_TEXCOORD, | |
| NUM_ATTRIBUTES | |
| }; | |
| struct picture_sys_t { | |
| CVPixelBufferRef pixelBuffer; | |
| }; | |
| // BT.601, which is the standard for SDTV. | |
| static const GLfloat kColorConversion601[] = { | |
| 1.164383561643836, 1.164383561643836, 1.164383561643836, | |
| 0.0, -0.391762290094914, 2.017232142857142, | |
| 1.596026785714286, -0.812967647237771, 0.0, | |
| }; | |
| // BT.709, which is the standard for HDTV. | |
| static const GLfloat kColorConversion709[] = { | |
| 1.164383561643836, 1.164383561643836, 1.164383561643836, | |
| 0.0, -0.21324861427373, 2.112401785714286, | |
| 1.792741071428571, -0.532909328559444, 0.0, | |
| }; | |
| static NSString *const fragmentShaderString = @" \ | |
| varying highp vec2 texCoordVarying; \ | |
| precision mediump float; \ | |
| \ | |
| uniform sampler2D SamplerY; \ | |
| uniform sampler2D SamplerUV; \ | |
| uniform mat3 colorConversionMatrix; \ | |
| \ | |
| void main() \ | |
| { \ | |
| mediump vec3 yuv; \ | |
| lowp vec3 rgb; \ | |
| \ | |
| yuv.x = (texture2D(SamplerY, texCoordVarying).r - (16.0/255.0)); \ | |
| yuv.yz = (texture2D(SamplerUV, texCoordVarying).rg - vec2(0.5, 0.5)); \ | |
| \ | |
| rgb = colorConversionMatrix * yuv; \ | |
| \ | |
| gl_FragColor = vec4(rgb,1); \ | |
| } \ | |
| "; | |
| static NSString *const vertexShaderString = @" \ | |
| attribute vec4 position; \ | |
| attribute vec2 texCoord; \ | |
| uniform mat4 transformMatrix; \ | |
| \ | |
| varying vec2 texCoordVarying; \ | |
| \ | |
| void main() \ | |
| { \ | |
| gl_Position = position * transformMatrix; \ | |
| texCoordVarying = texCoord; \ | |
| } \ | |
| "; | |
| static int Open(vlc_object_t *); | |
| static void Close(vlc_object_t *); | |
| static picture_pool_t* PicturePool(vout_display_t *, unsigned); | |
| static void PictureRender(vout_display_t *, picture_t *, subpicture_t *); | |
| static void PictureDisplay(vout_display_t *, picture_t *, subpicture_t *); | |
| static int Control(vout_display_t*, int, va_list); | |
| static void *OurGetProcAddress(vlc_gl_t *, const char *); | |
| static int OpenglESClean(vlc_gl_t *); | |
| static void OpenglESSwap(vlc_gl_t *); | |
| static picture_pool_t *ZeroCopyPicturePool(vout_display_t *, unsigned); | |
| static void DestroyZeroCopyPoolPicture(picture_t *); | |
| static void ZeroCopyClean(vout_display_t *vd, picture_t *pic, subpicture_t *subpicture); | |
| static void ZeroCopyDisplay(vout_display_t *, picture_t *, subpicture_t *); | |
| /** | |
| * Module declaration | |
| */ | |
| vlc_module_begin () | |
| set_shortname("iOS vout") | |
| set_description(N_("iOS OpenGL video output")) | |
| set_category(CAT_VIDEO) | |
| set_subcategory(SUBCAT_VIDEO_VOUT) | |
| set_capability("vout display", 300) | |
| set_callbacks(Open, Close) | |
| add_shortcut("vout_ios2") | |
| vlc_module_end () | |
| @interface VLCOpenGLES2VideoView : UIView { | |
| vout_display_t *_voutDisplay; | |
| EAGLContext *_eaglContext; | |
| GLuint _renderBuffer; | |
| GLuint _frameBuffer; | |
| BOOL _bufferNeedReset; | |
| BOOL _appActive; | |
| bool _zeroCopy; | |
| CVOpenGLESTextureCacheRef _videoTextureCache; | |
| CVOpenGLESTextureRef _lumaTexture; | |
| CVOpenGLESTextureRef _chromaTexture; | |
| const GLfloat *_preferredConversion; | |
| } | |
| @property (readonly) GLuint renderBuffer; | |
| @property (readonly) GLuint frameBuffer; | |
| @property (readwrite) vout_display_t* voutDisplay; | |
| @property (readonly) EAGLContext* eaglContext; | |
| @property (readonly) BOOL isAppActive; | |
| @property GLuint shaderProgram; | |
| - (id)initWithFrame:(CGRect)frame zeroCopy:(bool)zero_copy voutDisplay:(vout_display_t *)vd; | |
| - (void)createBuffers; | |
| - (void)destroyBuffers; | |
| - (void)resetBuffers; | |
| - (void)reshape; | |
| - (void)setupZeroCopyGL; | |
| - (void)displayPixelBuffer:(CVPixelBufferRef)pixelBuffer; | |
| @end | |
| struct vout_display_sys_t | |
| { | |
| VLCOpenGLES2VideoView *glESView; | |
| UIView* viewContainer; | |
| UITapGestureRecognizer *tapRecognizer; | |
| vlc_gl_t gl; | |
| vout_display_opengl_t *vgl; | |
| picture_pool_t *picturePool; | |
| bool has_first_frame; | |
| bool zero_copy; | |
| vout_display_place_t place; | |
| }; | |
| static void *OurGetProcAddress(vlc_gl_t *gl, const char *name) | |
| { | |
| VLC_UNUSED(gl); | |
| return dlsym(RTLD_DEFAULT, name); | |
| } | |
| static int Open(vlc_object_t *this) | |
| { | |
| vout_display_t *vd = (vout_display_t *)this; | |
| if (vout_display_IsWindowed(vd)) | |
| return VLC_EGENERIC; | |
| vout_display_sys_t *sys = calloc (1, sizeof(*sys)); | |
| if (!sys) | |
| return VLC_ENOMEM; | |
| vd->sys = sys; | |
| sys->picturePool = NULL; | |
| sys->gl.sys = NULL; | |
| @autoreleasepool { | |
| /* get the object we will draw into */ | |
| UIView* viewContainer = var_CreateGetAddress (vd, "drawable-nsobject"); | |
| if (unlikely(viewContainer == nil)) | |
| goto bailout; | |
| [viewContainer retain]; | |
| @synchronized(viewContainer) { | |
| if (unlikely(![viewContainer respondsToSelector:@selector(isKindOfClass:)])) | |
| goto bailout; | |
| if (![viewContainer isKindOfClass:[UIView class]]) | |
| goto bailout; | |
| /* This will be released in Close(), on | |
| * main thread, after we are done using it. */ | |
| sys->viewContainer = viewContainer; | |
| if (vd->fmt.i_chroma == VLC_CODEC_CVPX_OPAQUE) { | |
| msg_Dbg(vd, "will use zero-copy rendering"); | |
| sys->zero_copy = true; | |
| } | |
| /* setup the actual OpenGL ES view */ | |
| sys->glESView = [[VLCOpenGLES2VideoView alloc] initWithFrame:[viewContainer bounds] zeroCopy:sys->zero_copy voutDisplay:vd]; | |
| sys->glESView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; | |
| if (!sys->glESView) | |
| goto bailout; | |
| [sys->viewContainer performSelectorOnMainThread:@selector(addSubview:) | |
| withObject:sys->glESView | |
| waitUntilDone:YES]; | |
| /* add tap gesture recognizer for DVD menus and stuff */ | |
| sys->tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:sys->glESView | |
| action:@selector(tapRecognized:)]; | |
| if (sys->viewContainer.window) { | |
| if (sys->viewContainer.window.rootViewController) { | |
| if (sys->viewContainer.window.rootViewController.view) | |
| [sys->viewContainer.superview addGestureRecognizer:sys->tapRecognizer]; | |
| } | |
| } | |
| sys->tapRecognizer.cancelsTouchesInView = NO; | |
| } | |
| const vlc_fourcc_t *subpicture_chromas; | |
| video_format_t fmt = vd->fmt; | |
| if (!sys->zero_copy) { | |
| msg_Dbg(vd, "will use regular OpenGL rendering"); | |
| /* Initialize common OpenGL video display */ | |
| sys->gl.lock = OpenglESClean; | |
| sys->gl.unlock = nil; | |
| sys->gl.swap = OpenglESSwap; | |
| sys->gl.getProcAddress = OurGetProcAddress; | |
| sys->gl.sys = sys; | |
| sys->vgl = vout_display_opengl_New(&vd->fmt, &subpicture_chromas, &sys->gl); | |
| if (!sys->vgl) { | |
| sys->gl.sys = NULL; | |
| goto bailout; | |
| } | |
| } else { | |
| subpicture_chromas = gl_subpicture_chromas; | |
| } | |
| /* */ | |
| vout_display_info_t info = vd->info; | |
| info.has_pictures_invalid = false; | |
| info.has_event_thread = true; | |
| info.subpicture_chromas = subpicture_chromas; | |
| info.is_slow = !sys->zero_copy; | |
| info.has_hide_mouse = false; | |
| /* Setup vout_display_t once everything is fine */ | |
| vd->info = info; | |
| if (sys->zero_copy) { | |
| vd->pool = ZeroCopyPicturePool; | |
| vd->prepare = ZeroCopyClean; | |
| vd->display = ZeroCopyDisplay; | |
| } else { | |
| vd->pool = PicturePool; | |
| vd->prepare = PictureRender; | |
| vd->display = PictureDisplay; | |
| } | |
| vd->control = Control; | |
| vd->manage = NULL; | |
| /* forward our dimensions to the vout core */ | |
| CGFloat scaleFactor; | |
| CGSize viewSize; | |
| @synchronized(sys->viewContainer) { | |
| scaleFactor = sys->viewContainer.contentScaleFactor; | |
| viewSize = sys->viewContainer.bounds.size; | |
| } | |
| vout_display_SendEventFullscreen(vd, false); | |
| vout_display_SendEventDisplaySize(vd, viewSize.width * scaleFactor, viewSize.height * scaleFactor); | |
| /* */ | |
| [[NSNotificationCenter defaultCenter] addObserver:sys->glESView | |
| selector:@selector(applicationStateChanged:) | |
| name:UIApplicationWillResignActiveNotification | |
| object:nil]; | |
| [[NSNotificationCenter defaultCenter] addObserver:sys->glESView | |
| selector:@selector(applicationStateChanged:) | |
| name:UIApplicationDidBecomeActiveNotification | |
| object:nil]; | |
| [sys->glESView reshape]; | |
| return VLC_SUCCESS; | |
| bailout: | |
| Close(this); | |
| return VLC_EGENERIC; | |
| } | |
| } | |
| void Close (vlc_object_t *this) | |
| { | |
| vout_display_t *vd = (vout_display_t *)this; | |
| vout_display_sys_t *sys = vd->sys; | |
| @autoreleasepool { | |
| if (sys->tapRecognizer) { | |
| [sys->tapRecognizer.view removeGestureRecognizer:sys->tapRecognizer]; | |
| [sys->tapRecognizer release]; | |
| } | |
| [sys->glESView setVoutDisplay:nil]; | |
| var_Destroy (vd, "drawable-nsobject"); | |
| @synchronized(sys->viewContainer) { | |
| [sys->glESView performSelectorOnMainThread:@selector(removeFromSuperview) withObject:nil waitUntilDone:NO]; | |
| [sys->viewContainer performSelectorOnMainThread:@selector(release) withObject:nil waitUntilDone:NO]; | |
| } | |
| sys->viewContainer = nil; | |
| if (sys->gl.sys != NULL) { | |
| @synchronized (sys->glESView) { | |
| msg_Dbg(this, "deleting display"); | |
| if (likely([sys->glESView isAppActive])) | |
| vout_display_opengl_Delete(sys->vgl); | |
| } | |
| } | |
| [sys->glESView release]; | |
| /* when using the traditional pipeline, the cross-platform code will free the the pool */ | |
| if (sys->zero_copy) { | |
| if (sys->picturePool) | |
| picture_pool_Release(sys->picturePool); | |
| sys->picturePool = NULL; | |
| } | |
| free(sys); | |
| } | |
| } | |
| /***************************************************************************** | |
| * vout display callbacks | |
| *****************************************************************************/ | |
| static int Control(vout_display_t *vd, int query, va_list ap) | |
| { | |
| vout_display_sys_t *sys = vd->sys; | |
| switch (query) { | |
| case VOUT_DISPLAY_HIDE_MOUSE: | |
| return VLC_EGENERIC; | |
| case VOUT_DISPLAY_CHANGE_DISPLAY_FILLED: | |
| case VOUT_DISPLAY_CHANGE_ZOOM: | |
| case VOUT_DISPLAY_CHANGE_SOURCE_ASPECT: | |
| case VOUT_DISPLAY_CHANGE_SOURCE_CROP: | |
| case VOUT_DISPLAY_CHANGE_DISPLAY_SIZE: | |
| { | |
| if (!vd->sys) | |
| return VLC_EGENERIC; | |
| @autoreleasepool { | |
| const vout_display_cfg_t *cfg; | |
| const video_format_t *source; | |
| if (query == VOUT_DISPLAY_CHANGE_SOURCE_ASPECT || | |
| query == VOUT_DISPLAY_CHANGE_SOURCE_CROP) { | |
| source = (const video_format_t *)va_arg(ap, const video_format_t *); | |
| cfg = vd->cfg; | |
| } else { | |
| source = &vd->source; | |
| cfg = (const vout_display_cfg_t*)va_arg(ap, const vout_display_cfg_t *); | |
| } | |
| /* we don't adapt anything here regardless of what the vout core | |
| * wants since we are not in a traditional desktop window */ | |
| if (!cfg) | |
| return VLC_EGENERIC; | |
| vout_display_cfg_t cfg_tmp = *cfg; | |
| CGSize viewSize; | |
| viewSize = [sys->glESView bounds].size; | |
| /* on HiDPI displays, the point bounds don't equal the actual pixels */ | |
| CGFloat scaleFactor = sys->glESView.contentScaleFactor; | |
| cfg_tmp.display.width = viewSize.width * scaleFactor; | |
| cfg_tmp.display.height = viewSize.height * scaleFactor; | |
| vout_display_place_t place; | |
| vout_display_PlacePicture(&place, source, &cfg_tmp, false); | |
| @synchronized (sys->glESView) { | |
| sys->place = place; | |
| } | |
| // x / y are top left corner, but we need the lower left one | |
| if (query != VOUT_DISPLAY_CHANGE_DISPLAY_SIZE) | |
| glViewport(place.x, cfg_tmp.display.height - (place.y + place.height), place.width, place.height); | |
| } | |
| return VLC_SUCCESS; | |
| } | |
| case VOUT_DISPLAY_RESET_PICTURES: | |
| vlc_assert_unreachable (); | |
| default: | |
| msg_Err(vd, "Unknown request %d", query); | |
| case VOUT_DISPLAY_CHANGE_FULLSCREEN: | |
| return VLC_EGENERIC; | |
| } | |
| } | |
| static void PictureDisplay(vout_display_t *vd, picture_t *pic, subpicture_t *subpicture) | |
| { | |
| vout_display_sys_t *sys = vd->sys; | |
| sys->has_first_frame = true; | |
| @synchronized (sys->glESView) { | |
| if (likely([sys->glESView isAppActive])) { | |
| vout_display_opengl_Display(sys->vgl, &vd->source); | |
| } | |
| } | |
| picture_Release(pic); | |
| if (subpicture) | |
| subpicture_Delete(subpicture); | |
| } | |
| static void PictureRender(vout_display_t *vd, picture_t *pic, subpicture_t *subpicture) | |
| { | |
| vout_display_sys_t *sys = vd->sys; | |
| if (likely([sys->glESView isAppActive])) | |
| vout_display_opengl_Prepare(sys->vgl, pic, subpicture); | |
| } | |
| static picture_pool_t *PicturePool(vout_display_t *vd, unsigned requested_count) | |
| { | |
| vout_display_sys_t *sys = vd->sys; | |
| if (!sys->picturePool) | |
| sys->picturePool = vout_display_opengl_GetPool(sys->vgl, requested_count); | |
| assert(sys->picturePool); | |
| return sys->picturePool; | |
| } | |
| /***************************************************************************** | |
| * vout opengl callbacks | |
| *****************************************************************************/ | |
| static int OpenglESClean(vlc_gl_t *gl) | |
| { | |
| vout_display_sys_t *sys = (vout_display_sys_t *)gl->sys; | |
| if (likely([sys->glESView isAppActive])) | |
| [sys->glESView resetBuffers]; | |
| return 0; | |
| } | |
| static void OpenglESSwap(vlc_gl_t *gl) | |
| { | |
| vout_display_sys_t *sys = (vout_display_sys_t *)gl->sys; | |
| if (likely([sys->glESView isAppActive])) | |
| [[sys->glESView eaglContext] presentRenderbuffer:GL_RENDERBUFFER]; | |
| } | |
| /***************************************************************************** | |
| * zero copy display callbacks | |
| *****************************************************************************/ | |
| static picture_pool_t *ZeroCopyPicturePool(vout_display_t *vd, unsigned requested_count) | |
| { | |
| vout_display_sys_t *sys = vd->sys; | |
| if (sys->picturePool != NULL) | |
| return sys->picturePool; | |
| picture_t** pictures = calloc(requested_count, sizeof(*pictures)); | |
| if (!pictures) | |
| goto bailout; | |
| for (unsigned x = 0; x < requested_count; x++) { | |
| picture_sys_t *picsys = calloc(1, sizeof(*picsys)); | |
| if (unlikely(!picsys)) { | |
| goto bailout; | |
| } | |
| picture_resource_t picture_resource; | |
| picture_resource.p_sys = picsys; | |
| picture_resource.pf_destroy = DestroyZeroCopyPoolPicture; | |
| picture_t *picture = picture_NewFromResource(&vd->fmt, &picture_resource); | |
| if (unlikely(picture == NULL)) { | |
| free(picsys); | |
| goto bailout; | |
| } | |
| pictures[x] = picture; | |
| } | |
| picture_pool_configuration_t pool_config; | |
| memset(&pool_config, 0, sizeof(pool_config)); | |
| pool_config.picture_count = requested_count; | |
| pool_config.picture = pictures; | |
| sys->picturePool = picture_pool_NewExtended(&pool_config); | |
| bailout: | |
| if (sys->picturePool == NULL && pictures) { | |
| for (unsigned x = 0; x < requested_count; x++) | |
| DestroyZeroCopyPoolPicture(pictures[x]); | |
| free(pictures); | |
| } | |
| return sys->picturePool; | |
| } | |
| static void DestroyZeroCopyPoolPicture(picture_t *picture) | |
| { | |
| picture_sys_t *p_sys = (picture_sys_t *)picture->p_sys; | |
| if (p_sys->pixelBuffer != nil) { | |
| CFRelease(p_sys->pixelBuffer); | |
| p_sys->pixelBuffer = nil; | |
| } | |
| free(p_sys); | |
| free(picture); | |
| } | |
| static void ZeroCopyClean(vout_display_t *vd, picture_t *pic, subpicture_t *subpicture) | |
| { | |
| vout_display_sys_t *sys = vd->sys; | |
| if (likely([sys->glESView isAppActive])) | |
| [sys->glESView resetBuffers]; | |
| } | |
| static void ZeroCopyDisplay(vout_display_t *vd, picture_t *pic, subpicture_t *subpicture) | |
| { | |
| vout_display_sys_t *sys = vd->sys; | |
| sys->has_first_frame = true; | |
| @synchronized (sys->glESView) { | |
| if (likely([sys->glESView isAppActive])) { | |
| if (pic->p_sys != NULL) { | |
| picture_sys_t *picsys = pic->p_sys; | |
| if (picsys->pixelBuffer != nil) { | |
| [sys->glESView displayPixelBuffer: picsys->pixelBuffer]; | |
| } | |
| } | |
| } | |
| } | |
| picture_Release(pic); | |
| if (subpicture) | |
| subpicture_Delete(subpicture); | |
| } | |
| /***************************************************************************** | |
| * Our UIView object | |
| *****************************************************************************/ | |
| @implementation VLCOpenGLES2VideoView | |
| @synthesize voutDisplay = _voutDisplay, eaglContext = _eaglContext, isAppActive = _appActive; | |
| + (Class)layerClass | |
| { | |
| return [CAEAGLLayer class]; | |
| } | |
| - (id)initWithFrame:(CGRect)frame zeroCopy:(bool)zero_copy voutDisplay:(vout_display_t *)vd | |
| { | |
| self = [super initWithFrame:frame]; | |
| if (!self) | |
| return nil; | |
| _appActive = ([UIApplication sharedApplication].applicationState == UIApplicationStateActive); | |
| if (unlikely(!_appActive)) | |
| return nil; | |
| _eaglContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; | |
| if (unlikely(!_eaglContext)) | |
| return nil; | |
| if (unlikely(![EAGLContext setCurrentContext:_eaglContext])) | |
| return nil; | |
| CAEAGLLayer *layer = (CAEAGLLayer *)self.layer; | |
| layer.drawableProperties = [NSDictionary dictionaryWithObject:kEAGLColorFormatRGBA8 forKey: kEAGLDrawablePropertyColorFormat]; | |
| layer.opaque = YES; | |
| _voutDisplay = vd; | |
| [self createBuffers]; | |
| if (zero_copy) { | |
| _preferredConversion = kColorConversion709; | |
| [self setupZeroCopyGL]; | |
| } | |
| [self reshape]; | |
| [self setAutoresizingMask: UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight]; | |
| _zeroCopy = zero_copy; | |
| return self; | |
| } | |
| - (void)dealloc | |
| { | |
| if (_zeroCopy) { | |
| [self cleanUpTextures]; | |
| if(likely(_videoTextureCache)) | |
| CFRelease(_videoTextureCache); | |
| } | |
| [[NSNotificationCenter defaultCenter] removeObserver:self]; | |
| [_eaglContext release]; | |
| [super dealloc]; | |
| } | |
| - (void)didMoveToWindow | |
| { | |
| self.contentScaleFactor = self.window.screen.scale; | |
| _bufferNeedReset = YES; | |
| } | |
| - (void)createBuffers | |
| { | |
| glDisable(GL_DEPTH_TEST); | |
| glEnableVertexAttribArray(ATTRIB_VERTEX); | |
| glVertexAttribPointer(ATTRIB_VERTEX, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), 0); | |
| glEnableVertexAttribArray(ATTRIB_TEXCOORD); | |
| glVertexAttribPointer(ATTRIB_TEXCOORD, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), 0); | |
| glGenFramebuffers(1, &_frameBuffer); | |
| glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer); | |
| glGenRenderbuffers(1, &_renderBuffer); | |
| glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer); | |
| [_eaglContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer *)self.layer]; | |
| glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _renderBuffer); | |
| if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { | |
| if (_voutDisplay) | |
| msg_Err(_voutDisplay, "Failed to make complete framebuffer object %x", glCheckFramebufferStatus(GL_FRAMEBUFFER)); | |
| } | |
| } | |
| - (void)destroyBuffers | |
| { | |
| /* re-set current context */ | |
| EAGLContext *previousContext = [EAGLContext currentContext]; | |
| [EAGLContext setCurrentContext:_eaglContext]; | |
| /* clear frame buffer */ | |
| glDeleteFramebuffers(1, &_frameBuffer); | |
| _frameBuffer = 0; | |
| /* clear render buffer */ | |
| glDeleteRenderbuffers(1, &_renderBuffer); | |
| _renderBuffer = 0; | |
| [EAGLContext setCurrentContext:previousContext]; | |
| } | |
| - (void)resetBuffers | |
| { | |
| if (unlikely(_bufferNeedReset)) { | |
| EAGLContext *previousContext = [EAGLContext currentContext]; | |
| [EAGLContext setCurrentContext:_eaglContext]; | |
| [self destroyBuffers]; | |
| [self createBuffers]; | |
| _bufferNeedReset = NO; | |
| [EAGLContext setCurrentContext:previousContext]; | |
| } | |
| } | |
| - (void)layoutSubviews | |
| { | |
| [self reshape]; | |
| _bufferNeedReset = YES; | |
| } | |
| - (void)reshape | |
| { | |
| EAGLContext *previousContext = [EAGLContext currentContext]; | |
| [EAGLContext setCurrentContext:_eaglContext]; | |
| CGSize viewSize = [self bounds].size; | |
| vout_display_place_t place; | |
| @synchronized (self) { | |
| if (_voutDisplay) { | |
| vout_display_cfg_t cfg_tmp = *(_voutDisplay->cfg); | |
| CGFloat scaleFactor = self.contentScaleFactor; | |
| cfg_tmp.display.width = viewSize.width * scaleFactor; | |
| cfg_tmp.display.height = viewSize.height * scaleFactor; | |
| vout_display_PlacePicture(&place, &_voutDisplay->source, &cfg_tmp, false); | |
| _voutDisplay->sys->place = place; | |
| vout_display_SendEventDisplaySize(_voutDisplay, viewSize.width * scaleFactor, | |
| viewSize.height * scaleFactor); | |
| } | |
| } | |
| // x / y are top left corner, but we need the lower left one | |
| glViewport(place.x, place.y, place.width, place.height); | |
| [EAGLContext setCurrentContext:previousContext]; | |
| } | |
| - (void)tapRecognized:(UITapGestureRecognizer *)tapRecognizer | |
| { | |
| UIGestureRecognizerState state = [tapRecognizer state]; | |
| CGPoint touchPoint = [tapRecognizer locationInView:self]; | |
| CGFloat scaleFactor = self.contentScaleFactor; | |
| vout_display_SendMouseMovedDisplayCoordinates(_voutDisplay, ORIENT_NORMAL, | |
| (int)touchPoint.x * scaleFactor, (int)touchPoint.y * scaleFactor, | |
| &_voutDisplay->sys->place); | |
| vout_display_SendEventMousePressed(_voutDisplay, MOUSE_BUTTON_LEFT); | |
| vout_display_SendEventMouseReleased(_voutDisplay, MOUSE_BUTTON_LEFT); | |
| } | |
| - (void)applicationStateChanged:(NSNotification *)notification | |
| { | |
| @synchronized (self) { | |
| if ([[notification name] isEqualToString:UIApplicationWillResignActiveNotification] | |
| || [[notification name] isEqualToString:UIApplicationDidEnterBackgroundNotification] | |
| || [[notification name] isEqualToString:UIApplicationWillTerminateNotification]) | |
| _appActive = NO; | |
| else | |
| _appActive = YES; | |
| } | |
| } | |
| - (void)updateConstraints | |
| { | |
| [super updateConstraints]; | |
| [self reshape]; | |
| } | |
| - (BOOL)isOpaque | |
| { | |
| return YES; | |
| } | |
| - (BOOL)acceptsFirstResponder | |
| { | |
| return YES; | |
| } | |
| - (void)displayPixelBuffer:(CVPixelBufferRef)pixelBuffer | |
| { | |
| /* the currently current context may not be ours, so cache it and restore it later */ | |
| EAGLContext *previousContext = [EAGLContext currentContext]; | |
| [EAGLContext setCurrentContext:_eaglContext]; | |
| CVReturn err; | |
| if (pixelBuffer != NULL) { | |
| int frameWidth = (int)CVPixelBufferGetWidth(pixelBuffer); | |
| int frameHeight = (int)CVPixelBufferGetHeight(pixelBuffer); | |
| if (!_videoTextureCache) { | |
| if (_voutDisplay) | |
| msg_Err(_voutDisplay, "No video texture cache"); | |
| goto done; | |
| } | |
| [self cleanUpTextures]; | |
| /* Use the color attachment of the pixel buffer to determine the appropriate color conversion matrix. */ | |
| CFTypeRef colorAttachments = CVBufferGetAttachment(pixelBuffer, kCVImageBufferYCbCrMatrixKey, NULL); | |
| if (colorAttachments == kCVImageBufferYCbCrMatrix_ITU_R_601_4) | |
| _preferredConversion = kColorConversion601; | |
| else | |
| _preferredConversion = kColorConversion709; | |
| /* Create Y and UV textures from the pixel buffer. | |
| * These textures will be drawn on the frame buffer Y-plane. */ | |
| glActiveTexture(GL_TEXTURE0); | |
| err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, | |
| _videoTextureCache, | |
| pixelBuffer, | |
| NULL, | |
| GL_TEXTURE_2D, | |
| GL_RED_EXT, | |
| frameWidth, | |
| frameHeight, | |
| GL_RED_EXT, | |
| GL_UNSIGNED_BYTE, | |
| 0, | |
| &_lumaTexture); | |
| if (err) { | |
| if (_voutDisplay) | |
| msg_Err(_voutDisplay, "Error at CVOpenGLESTextureCacheCreateTextureFromImage %d", err); | |
| } | |
| glBindTexture(CVOpenGLESTextureGetTarget(_lumaTexture), CVOpenGLESTextureGetName(_lumaTexture)); | |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); | |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); | |
| glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); | |
| glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); | |
| // UV-plane. | |
| glActiveTexture(GL_TEXTURE1); | |
| err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, | |
| _videoTextureCache, | |
| pixelBuffer, | |
| NULL, | |
| GL_TEXTURE_2D, | |
| GL_RG_EXT, | |
| frameWidth / 2, | |
| frameHeight / 2, | |
| GL_RG_EXT, | |
| GL_UNSIGNED_BYTE, | |
| 1, | |
| &_chromaTexture); | |
| if (err) { | |
| if (_voutDisplay) | |
| msg_Err(_voutDisplay, "Error at CVOpenGLESTextureCacheCreateTextureFromImage %d", err); | |
| } | |
| glBindTexture(CVOpenGLESTextureGetTarget(_chromaTexture), CVOpenGLESTextureGetName(_chromaTexture)); | |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); | |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); | |
| glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); | |
| glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); | |
| glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer); | |
| } | |
| glClearColor(0.0f, 0.0f, 0.0f, 1.0f); | |
| glClear(GL_COLOR_BUFFER_BIT); | |
| // Use shader program. | |
| glUseProgram(self.shaderProgram); | |
| glUniformMatrix3fv(uniforms[UNIFORM_COLOR_CONVERSION_MATRIX], 1, GL_FALSE, _preferredConversion); | |
| GLfloat transformMatrix[16]; | |
| orientationTransformMatrix(transformMatrix, _voutDisplay->fmt.orientation); | |
| glUniformMatrix4fv(uniforms[UNIFORM_TRANSFORM_MATRIX], 1, GL_FALSE, transformMatrix); | |
| // Set up the quad vertices with respect to the orientation and aspect ratio of the video. | |
| CGRect vertexSamplingRect = self.bounds; | |
| // Compute normalized quad coordinates to draw the frame into. | |
| CGSize normalizedSamplingSize = CGSizeMake(0.0, 0.0); | |
| CGSize cropScaleAmount = CGSizeMake(vertexSamplingRect.size.width/self.bounds.size.width, vertexSamplingRect.size.height/self.bounds.size.height); | |
| // Normalize the quad vertices. | |
| if (cropScaleAmount.width > cropScaleAmount.height) { | |
| normalizedSamplingSize.width = 1.0; | |
| normalizedSamplingSize.height = cropScaleAmount.height/cropScaleAmount.width; | |
| } | |
| else { | |
| normalizedSamplingSize.width = 1.0; | |
| normalizedSamplingSize.height = cropScaleAmount.width/cropScaleAmount.height; | |
| } | |
| /* | |
| The quad vertex data defines the region of 2D plane onto which we draw our pixel buffers. | |
| Vertex data formed using (-1,-1) and (1,1) as the bottom left and top right coordinates respectively, covers the entire screen. | |
| */ | |
| GLfloat quadVertexData [] = { | |
| -1 * normalizedSamplingSize.width, -1 * normalizedSamplingSize.height, | |
| normalizedSamplingSize.width, -1 * normalizedSamplingSize.height, | |
| -1 * normalizedSamplingSize.width, normalizedSamplingSize.height, | |
| normalizedSamplingSize.width, normalizedSamplingSize.height, | |
| }; | |
| // Update attribute values. | |
| glVertexAttribPointer(ATTRIB_VERTEX, 2, GL_FLOAT, 0, 0, quadVertexData); | |
| glEnableVertexAttribArray(ATTRIB_VERTEX); | |
| /* | |
| The texture vertices are set up such that we flip the texture vertically. This is so that our top left origin buffers match OpenGL's bottom left texture coordinate system. | |
| */ | |
| CGRect textureSamplingRect = CGRectMake(0, 0, 1, 1); | |
| GLfloat quadTextureData[] = { | |
| CGRectGetMinX(textureSamplingRect), CGRectGetMaxY(textureSamplingRect), | |
| CGRectGetMaxX(textureSamplingRect), CGRectGetMaxY(textureSamplingRect), | |
| CGRectGetMinX(textureSamplingRect), CGRectGetMinY(textureSamplingRect), | |
| CGRectGetMaxX(textureSamplingRect), CGRectGetMinY(textureSamplingRect) | |
| }; | |
| glVertexAttribPointer(ATTRIB_TEXCOORD, 2, GL_FLOAT, 0, 0, quadTextureData); | |
| glEnableVertexAttribArray(ATTRIB_TEXCOORD); | |
| glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); | |
| glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer); | |
| [_eaglContext presentRenderbuffer:GL_RENDERBUFFER]; | |
| glFlush(); | |
| done: | |
| /* restore previous eagl context which we cached on entry */ | |
| [EAGLContext setCurrentContext:previousContext]; | |
| } | |
| - (void)setupZeroCopyGL | |
| { | |
| EAGLContext *previousContext = [EAGLContext currentContext]; | |
| [EAGLContext setCurrentContext:_eaglContext]; | |
| [self loadShaders]; | |
| glUseProgram(self.shaderProgram); | |
| // 0 and 1 are the texture IDs of _lumaTexture and _chromaTexture respectively. | |
| glUniform1i(uniforms[UNIFORM_Y], 0); | |
| glUniform1i(uniforms[UNIFORM_UV], 1); | |
| GLfloat transformMatrix[16]; | |
| orientationTransformMatrix(transformMatrix, _voutDisplay->fmt.orientation); | |
| glUniformMatrix4fv(uniforms[UNIFORM_TRANSFORM_MATRIX], 1, GL_FALSE, transformMatrix); | |
| glUniformMatrix3fv(uniforms[UNIFORM_COLOR_CONVERSION_MATRIX], 1, GL_FALSE, _preferredConversion); | |
| if (!_videoTextureCache) { | |
| CVReturn err = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault, NULL, _eaglContext, NULL, &_videoTextureCache); | |
| if (err != noErr) { | |
| if (_voutDisplay) | |
| msg_Err(_voutDisplay, "Error at CVOpenGLESTextureCacheCreate %d", err); | |
| } | |
| } | |
| [EAGLContext setCurrentContext:previousContext]; | |
| } | |
| - (void)cleanUpTextures | |
| { | |
| if (_lumaTexture) { | |
| CFRelease(_lumaTexture); | |
| _lumaTexture = NULL; | |
| } | |
| if (_chromaTexture) { | |
| CFRelease(_chromaTexture); | |
| _chromaTexture = NULL; | |
| } | |
| // Periodic texture cache flush every frame | |
| CVOpenGLESTextureCacheFlush(_videoTextureCache, 0); | |
| } | |
| - (BOOL)loadShaders | |
| { | |
| GLuint vertShader, fragShader; | |
| NSURL *vertShaderURL, *fragShaderURL; | |
| // Create the shader program. | |
| self.shaderProgram = glCreateProgram(); | |
| // Create and compile the vertex shader. | |
| if (![self compileShader:&vertShader type:GL_VERTEX_SHADER sourceString:vertexShaderString]) { | |
| if (_voutDisplay) | |
| msg_Err(_voutDisplay, "Failed to compile vertex shader"); | |
| return NO; | |
| } | |
| // Create and compile fragment shader. | |
| if (![self compileShader:&fragShader type:GL_FRAGMENT_SHADER sourceString:fragmentShaderString]) { | |
| if (_voutDisplay) | |
| msg_Err(_voutDisplay, "Failed to compile fragment shader"); | |
| return NO; | |
| } | |
| // Attach vertex shader to program. | |
| glAttachShader(self.shaderProgram, vertShader); | |
| // Attach fragment shader to program. | |
| glAttachShader(self.shaderProgram, fragShader); | |
| // Bind attribute locations. This needs to be done prior to linking. | |
| glBindAttribLocation(self.shaderProgram, ATTRIB_VERTEX, "position"); | |
| glBindAttribLocation(self.shaderProgram, ATTRIB_TEXCOORD, "texCoord"); | |
| // Link the program. | |
| if (![self linkProgram:self.shaderProgram]) { | |
| if (_voutDisplay) | |
| msg_Err(_voutDisplay, "Failed to link program: %d", self.shaderProgram); | |
| if (vertShader) { | |
| glDeleteShader(vertShader); | |
| vertShader = 0; | |
| } | |
| if (fragShader) { | |
| glDeleteShader(fragShader); | |
| fragShader = 0; | |
| } | |
| if (self.shaderProgram) { | |
| glDeleteProgram(self.shaderProgram); | |
| self.shaderProgram = 0; | |
| } | |
| return NO; | |
| } | |
| // Get uniform locations. | |
| uniforms[UNIFORM_Y] = glGetUniformLocation(self.shaderProgram, "SamplerY"); | |
| uniforms[UNIFORM_UV] = glGetUniformLocation(self.shaderProgram, "SamplerUV"); | |
| uniforms[UNIFORM_COLOR_CONVERSION_MATRIX] = glGetUniformLocation(self.shaderProgram, "colorConversionMatrix"); | |
| uniforms[UNIFORM_TRANSFORM_MATRIX] = glGetUniformLocation(self.shaderProgram, "transformMatrix"); | |
| // Release vertex and fragment shaders. | |
| if (vertShader) { | |
| glDetachShader(self.shaderProgram, vertShader); | |
| glDeleteShader(vertShader); | |
| } | |
| if (fragShader) { | |
| glDetachShader(self.shaderProgram, fragShader); | |
| glDeleteShader(fragShader); | |
| } | |
| return YES; | |
| } | |
| - (BOOL)compileShader:(GLuint *)shader type:(GLenum)type sourceString:sourceString | |
| { | |
| GLint status; | |
| const GLchar *source; | |
| source = (GLchar *)[sourceString UTF8String]; | |
| *shader = glCreateShader(type); | |
| glShaderSource(*shader, 1, &source, NULL); | |
| glCompileShader(*shader); | |
| #ifndef NDEBUG | |
| GLint logLength; | |
| glGetShaderiv(*shader, GL_INFO_LOG_LENGTH, &logLength); | |
| if (logLength > 0) { | |
| GLchar *log = (GLchar *)malloc(logLength); | |
| glGetShaderInfoLog(*shader, logLength, &logLength, log); | |
| if (_voutDisplay) | |
| msg_Dbg(_voutDisplay, "Shader compile log:\n%s", log); | |
| free(log); | |
| } | |
| #endif | |
| glGetShaderiv(*shader, GL_COMPILE_STATUS, &status); | |
| if (status == 0) { | |
| glDeleteShader(*shader); | |
| return NO; | |
| } | |
| return YES; | |
| } | |
| - (BOOL)linkProgram:(GLuint)prog | |
| { | |
| GLint status; | |
| glLinkProgram(prog); | |
| #ifndef NDEBUG | |
| GLint logLength; | |
| glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &logLength); | |
| if (logLength > 0) { | |
| GLchar *log = (GLchar *)malloc(logLength); | |
| glGetProgramInfoLog(prog, logLength, &logLength, log); | |
| if (_voutDisplay) | |
| msg_Dbg(_voutDisplay, "Program link log:\n%s", log); | |
| free(log); | |
| } | |
| #endif | |
| glGetProgramiv(prog, GL_LINK_STATUS, &status); | |
| if (status == 0) { | |
| return NO; | |
| } | |
| return YES; | |
| } | |
| @end |