You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

481 lines
15 KiB

/*****************************************************************************
* VLCVideoUIView.m: iOS UIView vout window provider
*****************************************************************************
* Copyright (C) 2001-2017 VLC authors and VideoLAN
* Copyright (C) 2020 Videolabs
*
* 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>
* Alexandre Janniaux <ajanni@videolabs.io>
*
* 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.
*****************************************************************************/
/**
* @file VLCVideoUIView.m
* @brief UIView-based vout_window_t provider
*
* This UIView window provider mostly handles resizing constraints from parent
* views and provides event forwarding to VLC. It is usable for any kind of
* subview and in particular can be used to implement a CAEAGLLayer in a
* vlc_gl_t provider as well as a CAMetalLayer, or other CALayer based video
* output in general.
*
* In particular, UI event will be forwarded to the core without the display
* lock thanks to this implementation, but vout display implementation will
* need to let the event pass through to this UIView.
*
* Note that this module is asynchronous with the usual VLC execution flow:
* except during Open(), where a status code is needed and synchronization
* must be done with the main thread, everything is forwarded to and
* asynchronously executed by the main thread. In particular, the closing
* of this module must be done asynchronously to not require the main thread
* to run, and the hosting application will need to drain the main thread
* dispatch queue. For iOS, it basically means nothing more than running the
* usual UIApplicationMain.
*/
#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_dialog.h>
#import <vlc_mouse.h>
#import <vlc_vout_window.h>
#import <assert.h>
#import "../../../src/darwin/runloop.h"
@interface VLCVideoUIView : UIView {
/* VLC window object, set to NULL under _eventq sync dispatch when closed. */
vout_window_t *_wnd;
/* Parent view defined by libvlc_media_player_set_nsobject. */
UIView *_viewContainer;
/* Window observer for mouse-like events. */
UITapGestureRecognizer *_tapRecognizer;
/* Window state */
BOOL _enabled;
atomic_bool _avstatEnabled;
/* Constraints */
NSArray<NSLayoutConstraint*> *_constraints;
/* Other */
CADisplayLink *_displayLink;
/* Serial queue used to report events to _wnd */
dispatch_queue_t _eventq;
vlc_tick_t _last_ca_target_ts;
}
- (id)initWithWindow:(vout_window_t *)wnd;
- (UIView *)fetchViewContainer;
- (void)detachFromParent;
- (void)tapRecognized:(UITapGestureRecognizer *)tapRecognizer;
- (void)enable;
- (void)disable;
- (void)applicationStateChanged:(NSNotification*)notification;
- (void)displayLinkUpdate:(CADisplayLink *)sender;
- (void)setAvstatEnabled:(bool)enabled;
@end
/*****************************************************************************
* Our UIView object
*****************************************************************************/
@implementation VLCVideoUIView
- (id)initWithWindow:(vout_window_t *)wnd
{
_wnd = wnd;
_enabled = NO;
_last_ca_target_ts = VLC_TICK_INVALID;
atomic_init(&_avstatEnabled, false);
UIView *superview = [self fetchViewContainer];
if (superview == nil)
return nil;
self = [super initWithFrame:[superview frame]];
if (!self)
return nil;
_eventq = dispatch_queue_create("vlc_eventq", DISPATCH_QUEUE_SERIAL);
/* The window is controlled by the host application through the UIView
* sizing mechanisms. */
self.translatesAutoresizingMaskIntoConstraints = false;
/* add tap gesture recognizer for DVD menus and stuff */
_tapRecognizer = [[UITapGestureRecognizer alloc]
initWithTarget:self action:@selector(tapRecognized:)];
_tapRecognizer.cancelsTouchesInView = NO;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationStateChanged:)
name:UIApplicationWillEnterForegroundNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationStateChanged:)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
_viewContainer = superview;
CGSize size = superview.bounds.size;
[self reportEvent:^{
vout_window_ReportSize(_wnd, size.width, size.height);
}];
return self;
}
- (void)didMoveToSuperview
{
if ([self superview] == nil)
return;
if (_constraints != nil)
return;
_constraints = @[
[self.centerXAnchor constraintEqualToAnchor:[[self superview] centerXAnchor]],
[self.centerYAnchor constraintEqualToAnchor:[[self superview] centerYAnchor]],
[self.widthAnchor constraintEqualToAnchor:[[self superview] widthAnchor]],
[self.heightAnchor constraintEqualToAnchor:[[self superview] heightAnchor]],
];
[NSLayoutConstraint activateConstraints:_constraints];
}
- (UIView *)fetchViewContainer
{
@try {
/* get the object we will draw into */
UIView *viewContainer = (__bridge UIView*)var_InheritAddress (_wnd, "drawable-nsobject");
if (unlikely(viewContainer == nil)) {
msg_Err(_wnd, "provided view container is nil");
return nil;
}
if (unlikely(![viewContainer respondsToSelector:@selector(isKindOfClass:)])) {
msg_Err(_wnd, "void pointer not an ObjC object");
return nil;
}
if (![viewContainer isKindOfClass:[UIView class]]) {
msg_Err(_wnd, "passed ObjC object not of class UIView");
return nil;
}
return viewContainer;
} @catch (NSException *exception) {
msg_Err(_wnd, "Handling the view container failed due to an Obj-C exception (%s, %s", [exception.name UTF8String], [exception.reason UTF8String]);
return nil;
}
}
- (void)displayLinkUpdate:(CADisplayLink *)sender
{
vlc_tick_t now = vlc_tick_now();
CFTimeInterval ca_now = CACurrentMediaTime();
CFTimeInterval ca_current = [sender timestamp];
CFTimeInterval ca_target = [sender targetTimestamp];
vlc_tick_t ca_now_ts = vlc_tick_from_sec(ca_now);
vlc_tick_t ca_current_ts = vlc_tick_from_sec(ca_current);
vlc_tick_t ca_target_ts = vlc_tick_from_sec(ca_target);
[self reportEvent:^{
/*if (@available(iOS 10, *))
target_ts = [sender targetTimestamp]; */
/* this compute the length of the VSYNC and the next date for the
* VSYNC. */
vlc_tick_t last_ca_target_ts =_last_ca_target_ts == VLC_TICK_INVALID ?
ca_current_ts : _last_ca_target_ts;
vlc_tick_t clock_offset = ca_current_ts - ca_now_ts;
vlc_tick_t vsync_length = ca_target_ts - last_ca_target_ts;
_last_ca_target_ts = ca_target_ts;
if (atomic_load(&_avstatEnabled))
msg_Info(_wnd, "avstats: [RENDER][CADISPLAYLINK] ts=%" PRId64 " "
"clock_offset=%" PRId64 " prev_ts=%" PRId64 " target_ts=%" PRId64,
NS_FROM_VLC_TICK(now),
NS_FROM_VLC_TICK(clock_offset),
NS_FROM_VLC_TICK(ca_current_ts),
NS_FROM_VLC_TICK(ca_target_ts));
vout_window_ReportVsyncReached(_wnd, now + clock_offset + vsync_length);
}];
}
- (void)reportEvent:(void(^)())eventBlock
{
CFRunLoopRef runloop = CFRunLoopGetCurrent();
vlc_darwin_runloop_PerformBlock(runloop, ^{
/* Execute the event in a different thread, we don't
* want to block the main CFRunLoop since the vout
* display module typically needs it to Open(). */
dispatch_async(_eventq, ^{
/* We need to dispatch to ensure _wnd is still valid,
* see detachFromParent. */
if (_wnd != NULL)
(eventBlock)();
vlc_darwin_runloop_Stop(runloop);
});
});
vlc_darwin_runloop_RunUntilStopped(runloop);
}
- (void)detachFromParent
{
/* We need to dispatch synchronously to ensure _wnd is set to null after
* all events have been reported in the _eventq
*/
dispatch_sync(_eventq, ^{
/* The UIView must not be attached before releasing. Disable() is doing
* exactly this asynchronously in the main thread so ensure it was called
* here before detaching from the parent. */
_wnd = NULL;
});
}
/**
* Vout window operations implemention, which are expected to be run on
* the main thread only. Core C wrappers below must typically use
* dispatch_async with dispatch_get_main_queue() to call them.
*
* The addition of the UIView to the parent UIView might happen later
* if there's no subview attached yet.
*/
- (void)enable
{
assert(!_enabled);
_enabled = YES;
/* Bind tapRecognizer. */
[self addGestureRecognizer:_tapRecognizer];
[_viewContainer addSubview:self];
_displayLink = [CADisplayLink displayLinkWithTarget:self
selector:@selector(displayLinkUpdate:)];
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)disable
{
assert(_enabled);
_enabled = NO;
[self removeFromSuperview];
_constraints = nil;
[_tapRecognizer.view removeGestureRecognizer:_tapRecognizer];
}
/**
* Window state tracking and reporting
*/
- (void)didMoveToWindow
{
self.contentScaleFactor = self.window.screen.scale;
}
- (void)layoutSubviews
{
[self reshape];
}
- (void)reshape
{
assert([NSThread isMainThread]);
CGSize viewSize = [self bounds].size;
CGFloat scaleFactor = self.contentScaleFactor;
[self reportEvent:^{
vout_window_ReportSize(_wnd,
viewSize.width * scaleFactor,
viewSize.height * scaleFactor);
}];
}
- (void)tapRecognized:(UITapGestureRecognizer *)tapRecognizer
{
UIGestureRecognizerState state = [tapRecognizer state];
CGPoint touchPoint = [tapRecognizer locationInView:self];
CGFloat scaleFactor = self.contentScaleFactor;
[self reportEvent:^{
vout_window_ReportMouseMoved(_wnd,
(int)touchPoint.x * scaleFactor, (int)touchPoint.y * scaleFactor);
vout_window_ReportMousePressed(_wnd, MOUSE_BUTTON_LEFT);
vout_window_ReportMouseReleased(_wnd, MOUSE_BUTTON_LEFT);
}];
}
- (void)updateConstraints
{
[super updateConstraints];
[self reshape];
}
- (void)pause
{
dispatch_sync(_eventq, ^{
[_displayLink invalidate];
_displayLink = nil;
});
}
- (void)applicationStateChanged:(NSNotification *)notification
{
[self reportEvent:^{
if ([[notification name] isEqualToString:UIApplicationWillEnterForegroundNotification])
{
vout_window_ReportVisibilityChanged(_wnd, VOUT_WINDOW_VISIBLE);
_displayLink.paused = NO;
}
else if ([[notification name] isEqualToString:UIApplicationDidEnterBackgroundNotification])
{
vout_window_ReportVisibilityChanged(_wnd, VOUT_WINDOW_NOT_VISIBLE);
_displayLink.paused = YES;
}
}];
}
/* Subview are expected to fill the whole frame so tell the compositor
* that it doesn't have to bother with what's behind the window. */
- (BOOL)isOpaque
{
return YES;
}
/* Prevent the subviews (which are renderers only) to get events so that
* they can be dispatched from this vout_window module. */
- (BOOL)acceptsFirstResponder
{
return YES;
}
- (void)setAvstatEnabled:(BOOL)enabled
{
atomic_store(&_avstatEnabled, enabled);
}
@end
/**
* C core wrapper of the vout window operations for the ObjC module.
*/
static int Enable(vout_window_t *wnd, const vout_window_cfg_t *cfg)
{
VLCVideoUIView *sys = (__bridge VLCVideoUIView *)wnd->sys;
dispatch_async(dispatch_get_main_queue(), ^{
[sys enable];
});
return VLC_SUCCESS;
}
static void Disable(vout_window_t *wnd)
{
VLCVideoUIView *sys = (__bridge VLCVideoUIView *)wnd->sys;
[sys pause];
dispatch_async(dispatch_get_main_queue(), ^{
[sys disable];
});
}
static int OnAvstatChanged(vlc_object_t *obj, const char *name, vlc_value_t oldv,
vlc_value_t newv, void *opaque)
{
VLCVideoUIView *view = (__bridge VLCVideoUIView*)opaque;
[view setAvstatEnabled: newv.b_bool ? YES : NO];
return VLC_SUCCESS;
}
static void Close(vout_window_t *wnd)
{
VLCVideoUIView *sys = (__bridge_transfer VLCVideoUIView*)wnd->sys;
var_DelCallback(wnd, "avstat", OnAvstatChanged, (__bridge void*)sys);
/* We need to signal the asynchronous implementation that we have been
* closed and cannot use _wnd anymore. */
[sys detachFromParent];
}
static const struct vout_window_operations window_ops =
{
.enable = Enable,
.disable = Disable,
.destroy = Close,
};
static int Open(vout_window_t *wnd)
{
dispatch_sync(dispatch_get_main_queue(), ^{
VLCVideoUIView *sys = [[VLCVideoUIView alloc] initWithWindow:wnd];
wnd->sys = (__bridge_retained void*)sys;
});
if (wnd->sys == NULL)
{
msg_Err(wnd, "Creating UIView window provider failed");
return VLC_EGENERIC;
}
var_Create(wnd, "avstat", VLC_VAR_BOOL | VLC_VAR_DOINHERIT);
var_AddCallback(wnd, "avstat", OnAvstatChanged, wnd->sys);
var_TriggerCallback(wnd, "avstat");
wnd->type = VOUT_WINDOW_TYPE_NSOBJECT;
wnd->handle.nsobject = wnd->sys;
wnd->ops = &window_ops;
return VLC_SUCCESS;
}
vlc_module_begin ()
set_shortname("UIView")
set_description("iOS UIView vout window provider")
set_category(CAT_VIDEO)
set_subcategory(SUBCAT_VIDEO_VOUT)
set_capability("vout window", 300)
set_callback(Open)
add_shortcut("uiview", "ios")
vlc_module_end ()