How to intercept video when going to fullscreen in a UIWebView? - ios

I'm trying to add a button to the video player that gets added to the view when a video becomes fullscreen from inside of an embedded web browser.
I saw this: Detect when a webview video becomes fullscreen on ios8. However, I think at that point we won't have a pointer to the video player. Perhaps there's a way to loop through all the subviews of the window's view and grab whatever is an instance of AVPlayer?
Ideally I could do something like this:
NSNotificationCenter.defaultCenter().addObserverForName(
UIWindowDidResignKeyNotification,
object: self.view.window,
queue: nil
) { notification in
let window = whatever the window is now
let player = window.methodThatReturnsVideoPlayer
// do stuff with player
let button = UIButton(...)
window.view.addSubView(button)
}

There is no public API method to get a pointer to the video player in a UIWebView.
There is, however, an unsafe way to access the video player. Using reflection, you can iterate through the subviews of a UIWebView and attempt to find the player:
- (UIView *)getViewInsideView:(UIView *)view withPrefix:(NSString *)classNamePrefix {
UIView *webBrowserView = nil;
for (UIView *subview in view.subviews) {
if ([NSStringFromClass([subview class]) hasPrefix:classNamePrefix]) {
return subview;
} else {
if((webBrowserView = [self getViewInsideView:subview withPrefix:classNamePrefix])) {
break;
}
}
}
return webBrowserView;
}
- (UIViewController *)movieControllerFromWebView:(UIWebView *)webview {
UIViewController *movieController = nil;
#try {
UIView *movieView = [self getViewInsideView:webview withPrefix:[NSString stringWithFormat:#"MP%#oView", #"Vide"]];
SEL mpavControllerSel = NSSelectorFromString([NSString stringWithFormat:#"mp%#roller", #"avCont"]);
if ([movieView respondsToSelector:mpavControllerSel]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
movieController = (UIViewController *)[movieView performSelector:mpavControllerSel];
#pragma clang diagnostic pop
}
}
#catch (NSException *exception) {
NSLog(#"Failed to get movieController: %#", exception);
}
return movieController;
}
This code has been adopted from YouTubeHacks.m.
Your milage may vary, as Apple tends to change the name of their player objects with the different SDKs (MPMoviePlayerController, UIMovieView, MPVideoView, UIMoviePlayerController, etc.). I would look at the MediaPlayer.framework private headers for reference.

Related

How to prevent a UIWindow to be a key window?

When I show an alert with UIAlertController, the alert itself presented in a new window. (for now at least) And when the alert window dismisses, system seems to set a random window key-window.
I am presenting a new "banner" window to render some banners over status-bar (AppStore compatibility is out of topic here), and usually, this "banner" window becomes next key window, and causes many problems on user input and first responder management.
So, I want to prevent this "banner" window to become a key window, but I cannot figure out how. For now, as a workaround, I am just re-setting my main window to be a key window again as soon as that "banner" window becomes key window. But it doesn't feel really good.
How can I prevent a window to become a key window?
As a workaround, we can set main window key again as soon as the "banner" window becomes a key like this.
class BannerWindow: UIWindow {
weak var mainWindow: UIWindow?
override func becomeKeyWindow() {
super.becomeKeyWindow()
mainWindow?.makeKeyWindow()
}
}
Faced with this too. It seems that it's enough to just make:
class BannerWindow: UIWindow {
override func makeKey() {
// Do nothing
}
}
This way you don't need to keep a reference to a previous keyWindow, which is especially cool if it might get changed.
For Objective-C it's:
#implementation BannerWindow
- (void)makeKeyWindow {
// Do nothing
}
#end
I've been trying to solve this problem for years. I finally reported a Radar for it: http://www.openradar.me/30064691
My workaround looks something like this:
// a UIWindow subclass that I use for my overlay windows
#implementation GFStatusLevelWindow
...
#pragma mark - Never become key
// http://www.openradar.me/30064691
// these don't actually help
- (BOOL)canBecomeFirstResponder
{
return NO;
}
- (BOOL)becomeFirstResponder
{
return NO;
}
- (void)becomeKeyWindow
{
LookbackStatusWindowBecameKey(self, #"become key window");
[[self class] findAndSetSuitableKeyWindow];
}
- (void)makeKeyWindow
{
LookbackStatusWindowBecameKey(self, #"make key window");
}
- (void)makeKeyAndVisible
{
LookbackStatusWindowBecameKey(self, #"make key and visible window");
}
#pragma mark - Private API overrides for status bar appearance
// http://www.openradar.me/15573442
- (BOOL)_canAffectStatusBarAppearance
{
return NO;
}
#pragma mark - Finding better key windows
static BOOL IsAllowedKeyWindow(UIWindow *window)
{
NSString *className = [[window class] description];
if([className isEqual:#"_GFRecordingIndicatorWindow"])
return NO;
if([className isEqual:#"UIRemoteKeyboardWindow"])
return NO;
if([window isKindOfClass:[GFStatusLevelWindow class]])
return NO;
return YES;
}
void LookbackStatusWindowBecameKey(GFStatusLevelWindow *self, NSString *where)
{
GFLog(GFError, #"This window should never be key window!! %# when in %#", self, where);
GFLog(GFError, #"To developer of %#: This is likely a bug in UIKit. If you can get a stack trace at this point (by setting a breakpoint at LookbackStatusWindowBecameKey) and sending that stack trace to nevyn#lookback.io or support#lookback.io, I will report it to Apple, and there will be rainbows, unicorns and a happier world for all :) thanks!", [[NSBundle mainBundle] gf_displayName]);
}
+ (UIWindow*)suitableWindowToMakeKeyExcluding:(UIWindow*)notThis
{
NSArray *windows = [UIApplication sharedApplication].windows;
NSInteger index = windows.count-1;
UIWindow *nextWindow = [windows objectAtIndex:index];
while((!IsAllowedKeyWindow(nextWindow) || nextWindow == notThis) && index >= 0) {
nextWindow = windows[--index];
}
return nextWindow;
}
+ (UIWindow*)findAndSetSuitableKeyWindow
{
UIWindow *nextWindow = [[self class] suitableWindowToMakeKeyExcluding:nil];
GFLog(GFError, #"Choosing this as key window instead: %#", nextWindow);
[nextWindow makeKeyWindow];
return nextWindow;
}

uiimagepicker crash when calling takepicture method

I am using custom camera with the help of UIImagePickerController. I am subclassing the UIImagePickerController class and using without present/dismiss. We are just opening the customise camera view and taking picture. After sometime by clicking camera takepicture method gets called and app is getting crash. No delegate method called during this. My code is this:
-(void)viewDidLoad
{
if([UIImagePickerController isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceRear] && self.camType==2)
{
self.sourceType=UIImagePickerControllerSourceTypeCamera;
self.cameraDevice=UIImagePickerControllerCameraDeviceRear;
self.showsCameraControls = NO;
self.wantsFullScreenLayout=NO;
self.navigationBar.hidden=NO;
self.cameraOverlayView = mainView;
}
}
else
{
self.navigationBar.hidden=YES;
self.sourceType=UIImagePickerControllerSourceTypePhotoLibrary;
[notAllowBtn setHidden:TRUE];
[cmView setHidden:TRUE];
}
self.delegate=self;
[self.view addSubview:cmView];
}
-(void)cameraTakePic {
if([UIImagePickerController isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceRear]) {
[self takePicture];
}
}
- (void)imagePickerController:(UIImagePickerController*)picker didFinishPickingMediaWithInfo:(NSDictionary*)info{
if (![info objectForKey:#"UIImagePickerControllerOriginalImage"]) {
NSArray *arr=[[appDel navController] viewControllers];
id obj=[arr lastObject];
UIViewController *objContr=((UIViewController *)obj);
objContr.navigationController.navigationBar.userInteractionEnabled = YES;
[clickBtn setEnabled:YES];
return;
}
UIImage* image = nil;
UIImage *imageTemp = [info objectForKey:#"UIImagePickerControllerOriginalImage"];
}
From the Apple UIImagePickerController class reference:
The UIImagePickerController class supports portrait mode only. This
class is intended to be used as-is and does not support subclassing.
The view hierarchy for this class is private and must not be modified,
with one exception. You can assign a custom view to the
cameraOverlayView property and use that view to present additional
information or manage the interactions between the camera interface
and your code.
You shouldn't subclass the UIImagePickerController class, instead you should use AVFoundation and make your custom capture methods.

GMSSyncTileLayer tileForX:y:zoom: called only once

Summary:
I subclassed
GMSSyncTileLayer
and overwrote
tileForX:y:zoom:
but its only called once no matter how much I pan.
why?
DETAIL
We have implemented our own TileServer which is behind a secure webservice so we need to pass a login token to get the tiles.
The call is asynchronous POST which is quiet common for a webservice call.
Because I had to pass login token in the NSURLSession header I couldnt just pass GET urls to
GMSTileURLConstructor urls = http://<tileserver>/gettile?x=3&y=4&zoom=5
So I subclassed GMSSyncTileLayer
#interface SNSyncTileLayer : GMSSyncTileLayer
overwrote
- (UIImage *)tileForX:(NSUInteger)x y:(NSUInteger)y zoom:(NSUInteger)zoom {
When tileForX:y:Zoom: is called the first time I call a webservice to get the tile UIImage.
The UIImage returns on a delegate and is stored in a NSDictionary with key in format TILE_x_y_Zoom.
The call to the WS is asynch so - (UIImage *)tileForX:y:Zoom: always returns nil for that tile the first time its called.
What i've noticed is that tileForX:y:Zoom: is never called again no matter how much I pan back and forth.
For instance at the current zoom I pan across europe.
I see tileForX:y:Zoom: being called once and ws calls being mad and images being stored in my dictionary.
But if i keep panning at the same zoom I come back to Europe and tileForX:y:Zoom: is not called again.
Fix one - clear cache when new tile downloaded
I tried creating a delegate on SNSyncTileLayer and everytime a new tile downloaded it called:
[self.snSyncTileLayer clearTileCache];
But this wipes out ALL tiles and reloads them so as you pan you get a terrible flashing.
My only idea next is to measure how much the map has panned and if its more than half a width or height then to call clearTileCache.
SO the big question why isnt tileForX:y:Zoom: called everytime?
My overridden class
//
// SNSyncTileLayer.m
//
#import "SNSyncTileLayer.h"
#import "SNAppDelegate.h"
#import "GoogleTileRequest.h"
#import "SNGoogleTileRequest.h"
#import "GoogleTileImageResult.h"
#interface SNSyncTileLayer()<SNSeaNetWebServicesManagerDelegate>{
BOOL _debugOn;
}
#property (nonatomic, retain) NSMutableDictionary * tileImageCacheDict;
#end
#implementation SNSyncTileLayer
- (instancetype)init
{
self = [super init];
if (self) {
_tileImageCacheDict = [NSMutableDictionary dictionary];
_debugOn = TRUE;
}
return self;
}
- (UIImage *)tileForX:(NSUInteger)x y:(NSUInteger)y zoom:(NSUInteger)zoom {
if(_debugOn)ImportantLog(#"tileForX:(%lu) y:(%lu) zoom:(%lu)", (unsigned long)x,(unsigned long)y,(unsigned long)zoom);
UIImage *tileImage_ = nil;
//tileImage_ = [UIImage imageNamed:#"EmptyTile1.png"];
NSString * keyForTile_ = [NSString stringWithFormat:#"TILE_%lu_%lu_%lu", (unsigned long)x,(unsigned long)y,(unsigned long)zoom];
id dictObj_ = [self.tileImageCacheDict objectForKey:keyForTile_];
if (dictObj_) {
if([dictObj_ isMemberOfClass:[NSNull class]])
{
if(_debugOn)DebugLog(#"tile has been called before but image not downloaded yet:[%#]",keyForTile_);
}
else if([dictObj_ isMemberOfClass:[UIImage class]])
{
if(_debugOn)DebugLog(#"cached image found in dict_ return it:[%#]",keyForTile_);
tileImage_ = (UIImage *)dictObj_;
}
else{
ErrorLog(#"ITEM IN self.tileImageCacheDict not NSNull or UIImage:[%#]", dictObj_);
}
}else{
if(_debugOn)ImportantLog(#"tileForX: CACHED IMAGE NOT FOUND: DOWNLOAD IT[%#]",keyForTile_);
//-----------------------------------------------------------------------------------
//add in temp object - tyring to check if tileForX:Y:Zoom is called more than once
[self.tileImageCacheDict setObject:[NSNull null] forKey:keyForTile_];
//-----------------------------------------------------------------------------------
//-----------------------------------------------------------------------------------
SNAppDelegate *appDelegate = (SNAppDelegate *)[[UIApplication sharedApplication] delegate];
GoogleTileRequest * googleTileRequest_ = [[GoogleTileRequest alloc]init];
googleTileRequest_.X = [NSNumber numberWithInteger:x];
googleTileRequest_.Y = [NSNumber numberWithInteger:y];
googleTileRequest_.Zoom = [NSNumber numberWithInteger:zoom];
#pragma mark TODO - NOW - thur11dec - load from settings
googleTileRequest_.MapType = #"Dark";
//for general errors
appDelegate.snSeaNetWebServicesManager.delegate = self;
//Request should know what class to return too
googleTileRequest_.delegateForRequest = self;
[appDelegate.snSeaNetWebServicesManager ITileController_GoogleTile:googleTileRequest_];
//-----------------------------------------------------------------------------------
return kGMSTileLayerNoTile;
//-----------------------------------------------------------------------------------
}
return tileImage_;
}
#pragma mark -
#pragma mark SNSeaNetWebServicesManagerDelegate
#pragma mark -
-(void) snSeaNetWebServicesManager:(SNSeaNetWebServicesManager *)SNSeaNetWebServicesManager
wsReponseReceivedForRequest:(SNWebServiceRequest *)snWebServiceRequest_
error:(NSError *)error
{
#pragma mark TODO - NOW - thur11dec2014
if(error){
ErrorLog(#"error:%#",error);
}else{
if(snWebServiceRequest_){
if([snWebServiceRequest_ isMemberOfClass:[SNGoogleTileRequest class]])
{
//Result is JSONModel ivar in Request
if(snWebServiceRequest_.resultObject){
GoogleTileImageResult * googleTileImageResult_= (GoogleTileImageResult *)snWebServiceRequest_.resultObject;
UIImage * responseImage_ = googleTileImageResult_.responseImage;
if(responseImage_){
//-----------------------------------------------------------------------------------
//build the key from the parameters
if(snWebServiceRequest_.bodyJsonModel){
NSDictionary *paramsDict = [snWebServiceRequest_.bodyJsonModel toDictionary];
if(paramsDict){
NSString *keyX_ = [paramsDict objectForKey:#"X"];
NSString *keyY_ = [paramsDict objectForKey:#"Y"];
NSString *keyZoom_ = [paramsDict objectForKey:#"Zoom"];
if(keyX_){
if(keyY_){
if(keyZoom_){
NSString * keyForTile_ = [NSString stringWithFormat:#"TILE_%#_%#_%#", keyX_,keyY_,keyZoom_];
if(_debugOn)ImportantLog(#"TILE DOWNLOADED ADD TO CACHE[%#]",keyForTile_);
[self.tileImageCacheDict setObject:responseImage_ forKey:keyForTile_];
//if(_debugOn)DebugLog(#"[[self.tileImageCacheDict allKeys]count]:%lu", (unsigned long)[[self.tileImageCacheDict allKeys]count]);
//-----------------------------------------------------------------------------------
//I ADDED THIS SO delegate could clearTileCache but causes flashing as ALL tiles get reloaded visible ones and ones downloaded but not on map
if(self.delegate){
if([self.delegate respondsToSelector:#selector(snSyncTileLayer:tileDownloadedForX:Y:Zoom:)]){
[self.delegate snSyncTileLayer:self
tileDownloadedForX:keyX_
Y:keyY_
Zoom:keyZoom_
];
}else {
ErrorLog(#"<%# %#:(%d)> %s delegate[%#] doesnt implement snSyncTileLayer:tileDownloadedForX:Y:Zoom:", self, [[NSString stringWithUTF8String:__FILE__] lastPathComponent], __LINE__, __PRETTY_FUNCTION__ ,self.delegate);
}
}else{
ErrorLog(#"<%# %#:(%d)> %s self.delegate is nil", self, [[NSString stringWithUTF8String:__FILE__] lastPathComponent], __LINE__, __PRETTY_FUNCTION__);
}
//-----------------------------------------------------------------------------------
}else{
ErrorLog(#"keyZoom_ is nil");
}
}else{
ErrorLog(#"keyY_ is nil");
}
}else{
ErrorLog(#"keyX_ is nil");
}
}else{
ErrorLog(#"paramsDict is nil");
}
}else{
ErrorLog(#"self.downloadingTasksDictionary is nil");
}
//-----------------------------------------------------------------------------------
}else{
ErrorLog(#"responseImage_ is nil");
}
}else{
ErrorLog(#"snWebServiceRequest_.resultJsonModel is nil");
}
}
else {
ErrorLog(#"UNHANDLED snWebServiceRequest_:%#", snWebServiceRequest_.class);
}
}else{
ErrorLog(#"snWebServiceRequest_ is nil");
}
}
}
#end
I haven't done this so I'm not sure, but my guess from reading the documentation is that you should be subclassing GMSTileLayer instead of GMSSyncTileLayer.
GMSSyncTileLayer is designed for cases where you are able to synchronously (ie immediately) return the tile for that location. By returning kGMSTileLayerNoTile, you are specifically indicating that 'there is no tile here', and so it never calls your class again for that location, as you've already responded that there is no tile there. (BTW, your description says you are returning nil, which indicates a transient error, but your code is actually returning kGMSTileLayerNoTile).
The GMSTileLayer class is designed for the asynchronous approach that you're using. If you subclass GMSTileLayer, your requestTileForX:y:zoom:receiver: method should start the background process to fetch the tile. When the tile request succeeds, then it is passed off to the GMSTileReceiver that was provided in that method (you should keep a copy of that receiver along with your request).

Cannot present MoviePlayerView animation while a dismissMoviePlayerView animation is in progress

I have an app that presents images and/or movies in sequence. The problem is, I cannot dismiss one movie player and then present another. If I try, I get the message "Warning: Attempt to present MPMoviePlayerViewController on MyViewController while a presentation is in progress!" Unlike other animated present/dismiss methods, there is no completion handler, nor a non-animated version of present/dismiss.
Here is a simplified version of my code:
-(void) play
{
[[window rootViewController] presentMoviePlayerViewControllerAnimated:player];
[[_player moviePlayer] play];
}
-(void) videoNotification:(NSNotification *) notification
{
if([notification.name isEqualToString:MPMoviePlayerPlaybackDidFinishNotification])
{
[[window rootViewController] dismissMoviePlayerViewControllerAnimated];
[_canvasManager showNextCanvas]; //this calls play on the next canvas
}
}
Any thoughts/hints on how to achieve my goal?
After looking at dismissViewControllerAnimated:completion:, I was able to create a completion handler by creating a subclass of MPMoviePlayerViewController and overriding -(void)viewDidDisappear. viewDidDisappear gets called at the end of dismissMoviePlayerViewControllerAnimated.
#interface MyMoviePlayerViewController : MPMoviePlayerViewController
{
void (^_viewDidDisappearCallback)(void);
}
- (void)setViewDidDisappearCallback:(void (^)(void))callback;
#end
#implementation MyMoviePlayerViewController
- (id) initWithContentURL:videoPath
{
self = [super initWithContentURL:videoPath];
_viewDidDisappearCallback = nil;
return self;
}
- (void)setViewDidDisappearCallback:(void (^)(void))callback
{
_viewDidDisappearCallback = [callback copy];
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
if(_viewDidDisappearCallback != nil)
_viewDidDisappearCallback();
}
#end

Audio streaming playback: unable to change play/stop button image

I'm developing a radio streaming app with XCode 4.5 for iOS 6 and mainly using storyboards.
I successfully made it to be able to play in background. So wherever I go from my app, whether to other tabs or even after clicking the Home button on the simulator,it keeps playing.
I'm using Matt Gallagher's audio streamer, which I include in my app delegate .m file below
#pragma mark - audio streaming
- (void)playAudio:(indoRadio *)np withButton:(NSString *)currentButton
{
if ([currentButton isEqual:#"playbutton.png"])
{
[self.nowplayingVC.downloadSourceField resignFirstResponder];
[self createStreamer:np.URL];
[self setButtonImageNamed:#"loadingbutton.png"];
[audioStreamer start];
}
else
{
[audioStreamer stop];
}
}
- (void)createStreamer:(NSString *)url
{
if (audioStreamer)
{
return;
}
[self destroyStreamer];
NSString *escapedValue = (__bridge NSString *)CFURLCreateStringByAddingPercentEscapes(nil,(CFStringRef)url,NULL,NULL,kCFStringEncodingUTF8);
NSURL *streamurl = [NSURL URLWithString:escapedValue];
audioStreamer = [[AudioStreamer alloc] initWithURL:streamurl];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(playbackStateChanged:)
name:ASStatusChangedNotification
object:audioStreamer];
}
- (void)playbackStateChanged:(NSNotification *)aNotification
{
NSLog(#"playback state changed? %d",[audioStreamer isWaiting]);
if ([audioStreamer isWaiting])
{
[self setButtonImageNamed:#"loadingbutton.png"];
}
else if ([audioStreamer isPlaying])
{
[self setButtonImageNamed:#"stopbutton.png"];
}
else if ([audioStreamer isIdle])
{
[self destroyStreamer];
[self setButtonImageNamed:#"playbutton.png"];
}
}
- (void)destroyStreamer
{
if (audioStreamer)
{
[[NSNotificationCenter defaultCenter]
removeObserver:self
name:ASStatusChangedNotification
object:audioStreamer];
[audioStreamer stop];
audioStreamer = nil;
}
}
- (void)setButtonImageNamed:(NSString *)imageName
{
if (!imageName)
{
imageName = #"playButton";
}
self.nowplayingVC.currentImageName = imageName;
UIImage *image = [UIImage imageNamed:imageName];
[self.nowplayingVC.playBtn.layer removeAllAnimations];
[self.nowplayingVC.playBtn setImage:image forState:0];
if ([imageName isEqual:#"loadingbutton.png"])
{
[self.nowplayingVC spinButton];
}
}
The problem I got is that when I click the play button, it starts the audio streamers but doesn't change into either loading button or stop button. This makes me unable to stop the audio since the button image is not changed. And since I put an NSLog inside the playbackStateChanged: method,I know that the playback state did change. It seems like the setButtonImagedNamed: method is not firing. Any thoughts?
Please have look of my answer:Disabled buttons not changing image in
For a button there is normal, selected, highlighted and disabled state.
So in your case you have to handle the normal and selected state.
In the xib, set image to a button for normal and selected state.
Now in the function of playAudio instead of checking the image name check the button state as below:
- (void)playAudio:(indoRadio *)np withButton:(NSString *)currentButton
{
if (playButton.selected)
{
//Stop the streaming
//set selection NO
[playButton setSelected:NO];
}
else
{
//Start the streaming
//set selection YES
[playButton setSelected:YES];
}
}
playButton is the IBOutlet of play button.
As you have set the images in xib, so the image automatically get changed as per the state of the image
In case you have added the playBtn programmatically, check if the button is declared as weak or its getting released somewhere. If it is weak, make it strong.
In case the button is in the nib file, then the problem is in the IBOutlet. Redo the connection properly.
It turned out that the delegate didn't know which nowplayingVC I was calling. #sarp-kaya's question about Calling a UIViewController method from app delegate has inspired me.
I actually have put this code in my view controller viewDidLoad:
myAppDelegate* appDelegate = [UIApplication sharedApplication].delegate;
but I forgot to add this line:
appDelegate.nowplayingVC = self;
So, all I need to do is adding that one line and it works now. okay, so silly of me for not noticing such a basic thing. But thanks for your helps :D

Resources