diff --git a/XCDYouTubeVideoPlayerViewController.podspec b/XCDYouTubeVideoPlayerViewController.podspec index 599c4ef05..d50de6424 100644 --- a/XCDYouTubeVideoPlayerViewController.podspec +++ b/XCDYouTubeVideoPlayerViewController.podspec @@ -1,11 +1,11 @@ Pod::Spec.new do |s| s.name = "XCDYouTubeVideoPlayerViewController" - s.version = "1.0.0" + s.version = "1.1.0" s.summary = "YouTube video player for iPhone and iPad." s.homepage = "https://github.com/0xced/XCDYouTubeVideoPlayerViewController" s.license = 'MIT' s.author = { "CeĢdric Luthi" => "cedric.luthi@gmail.com" } - s.source = { :git => "https://github.com/0xced/XCDYouTubeVideoPlayerViewController.git", :tag => "1.0.0" } + s.source = { :git => "https://github.com/0xced/XCDYouTubeVideoPlayerViewController.git", :tag => "1.1.0" } s.platform = :ios, '5.0' s.source_files = 'XCDYouTubeVideoPlayerViewController' s.frameworks = 'AVFoundation', 'MediaPlayer' diff --git a/XCDYouTubeVideoPlayerViewController/XCDYouTubeVideoPlayerViewController.h b/XCDYouTubeVideoPlayerViewController/XCDYouTubeVideoPlayerViewController.h index fcabfe631..76d937084 100644 --- a/XCDYouTubeVideoPlayerViewController/XCDYouTubeVideoPlayerViewController.h +++ b/XCDYouTubeVideoPlayerViewController/XCDYouTubeVideoPlayerViewController.h @@ -18,6 +18,19 @@ typedef NS_ENUM(NSUInteger, XCDYouTubeVideoQuality) { MP_EXTERN NSString *const XCDYouTubeVideoErrorDomain; MP_EXTERN NSString *const XCDMoviePlayerPlaybackDidFinishErrorUserInfoKey; // NSError +enum { + XCDYouTubeErrorInvalidVideoIdentifier = 2, + XCDYouTubeErrorRemovedVideo = 100, + XCDYouTubeErrorRestrictedPlayback = 150 +}; + +MP_EXTERN NSString *const XCDYouTubeVideoPlayerViewControllerDidReceiveMetadataNotification; +// Metadata notification userInfo keys, they are all optional +MP_EXTERN NSString *const XCDMetadataKeyTitle; +MP_EXTERN NSString *const XCDMetadataKeySmallThumbnailURL; +MP_EXTERN NSString *const XCDMetadataKeyMediumThumbnailURL; +MP_EXTERN NSString *const XCDMetadataKeyLargeThumbnailURL; + @interface XCDYouTubeVideoPlayerViewController : MPMoviePlayerViewController - (id) initWithVideoIdentifier:(NSString *)videoIdentifier; @@ -29,7 +42,7 @@ MP_EXTERN NSString *const XCDMoviePlayerPlaybackDidFinishErrorUserInfoKey; // NS // If you really know what you are doing, you can use the `itag` values as described on http://en.wikipedia.org/wiki/YouTube#Quality_and_codecs @property (nonatomic, copy) NSArray *preferredVideoQualities; -// Ownership of the `moviePlayer` property is transferred to the view. +// Ownership of the XCDYouTubeVideoPlayerViewController instance is transferred to the view. - (void) presentInView:(UIView *)view; @end diff --git a/XCDYouTubeVideoPlayerViewController/XCDYouTubeVideoPlayerViewController.m b/XCDYouTubeVideoPlayerViewController/XCDYouTubeVideoPlayerViewController.m index 23beb15c1..36dfc239f 100644 --- a/XCDYouTubeVideoPlayerViewController/XCDYouTubeVideoPlayerViewController.m +++ b/XCDYouTubeVideoPlayerViewController/XCDYouTubeVideoPlayerViewController.m @@ -14,6 +14,12 @@ NSString *const XCDYouTubeVideoErrorDomain = @"XCDYouTubeVideoErrorDomain"; NSString *const XCDMoviePlayerPlaybackDidFinishErrorUserInfoKey = @"XCDMoviePlayerPlaybackDidFinishErrorUserInfoKey"; +NSString *const XCDYouTubeVideoPlayerViewControllerDidReceiveMetadataNotification = @"XCDYouTubeVideoPlayerViewControllerDidReceiveMetadataNotification"; +NSString *const XCDMetadataKeyTitle = @"Title"; +NSString *const XCDMetadataKeySmallThumbnailURL = @"SmallThumbnailURL"; +NSString *const XCDMetadataKeyMediumThumbnailURL = @"MediumThumbnailURL"; +NSString *const XCDMetadataKeyLargeThumbnailURL = @"LargeThumbnailURL"; + static NSDictionary *DictionaryWithQueryString(NSString *string, NSStringEncoding encoding) { NSMutableDictionary *dictionary = [NSMutableDictionary new]; @@ -37,11 +43,13 @@ @interface XCDYouTubeVideoPlayerViewController () @property (nonatomic, strong) NSMutableData *connectionData; @property (nonatomic, strong) NSMutableArray *elFields; @property (nonatomic, assign, getter = isEmbedded) BOOL embedded; +@property (nonatomic, assign) BOOL statusBarHidden; +@property (nonatomic, assign) UIStatusBarStyle statusBarStyle; @end @implementation XCDYouTubeVideoPlayerViewController -static void *MoviePlayerKey = &MoviePlayerKey; +static void *XCDYouTubeVideoPlayerViewControllerKey = &XCDYouTubeVideoPlayerViewControllerKey; - (id) init { @@ -66,9 +74,17 @@ - (id) initWithVideoIdentifier:(NSString *)videoIdentifier if (videoIdentifier) self.videoIdentifier = videoIdentifier; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(moviePlayerWillEnterFullscreen:) name:MPMoviePlayerWillEnterFullscreenNotification object:self.moviePlayer]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(moviePlayerWillExitFullscreen:) name:MPMoviePlayerWillExitFullscreenNotification object:self.moviePlayer]; + return self; } +- (void) dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + - (void) setVideoIdentifier:(NSString *)videoIdentifier { if (![NSThread isMainThread]) @@ -94,8 +110,9 @@ - (void) presentInView:(UIView *)view self.moviePlayer.controlStyle = MPMovieControlStyleEmbedded; self.moviePlayer.view.frame = CGRectMake(0.f, 0.f, view.bounds.size.width, view.bounds.size.height); self.moviePlayer.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - [view addSubview:self.moviePlayer.view]; - objc_setAssociatedObject(view, MoviePlayerKey, self.moviePlayer, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (![view.subviews containsObject:self.moviePlayer.view]) + [view addSubview:self.moviePlayer.view]; + objc_setAssociatedObject(view, XCDYouTubeVideoPlayerViewControllerKey, self, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (void) startVideoInfoRequest @@ -129,16 +146,21 @@ - (void) viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - if ([self isBeingPresented]) - self.moviePlayer.controlStyle = MPMovieControlStyleFullscreen; + if (![self isBeingPresented]) + return; + + self.moviePlayer.controlStyle = MPMovieControlStyleFullscreen; + [self.moviePlayer play]; } - (void) viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; - if ([self isBeingDismissed]) - [self.connection cancel]; + if (![self isBeingDismissed]) + return; + + [self.connection cancel]; } #pragma mark - NSURLConnectionDataDelegate / NSURLConnectionDelegate @@ -159,18 +181,11 @@ - (void) connectionDidFinishLoading:(NSURLConnection *)connection NSError *error = nil; NSURL *videoURL = [self videoURLWithData:self.connectionData error:&error]; if (videoURL) - { self.moviePlayer.contentURL = videoURL; - [self.moviePlayer prepareToPlay]; - } else if (self.elFields.count > 0) - { [self startVideoInfoRequest]; - } else - { [self finishWithError:error]; - } } - (void) connection:(NSURLConnection *)connection didFailWithError:(NSError *)error @@ -178,6 +193,22 @@ - (void) connection:(NSURLConnection *)connection didFailWithError:(NSError *)er [self finishWithError:error]; } +#pragma mark - Notifications + +- (void) moviePlayerWillEnterFullscreen:(NSNotification *)notification +{ + UIApplication *application = [UIApplication sharedApplication]; + self.statusBarHidden = application.statusBarHidden; + self.statusBarStyle = application.statusBarStyle; +} + +- (void) moviePlayerWillExitFullscreen:(NSNotification *)notification +{ + UIApplication *application = [UIApplication sharedApplication]; + [application setStatusBarHidden:self.statusBarHidden withAnimation:UIStatusBarAnimationFade]; + [application setStatusBarStyle:self.statusBarStyle animated:YES]; +} + #pragma mark - URL Parsing - (NSURL *) videoURLWithData:(NSData *)data error:(NSError **)error @@ -192,9 +223,11 @@ - (NSURL *) videoURLWithData:(NSData *)data error:(NSError **)error { NSDictionary *stream = DictionaryWithQueryString(streamQuery, queryEncoding); NSString *type = stream[@"type"]; - if ([AVURLAsset isPlayableExtendedMIMEType:type]) + NSString *urlString = stream[@"url"]; + NSString *signature = stream[@"sig"]; + if (urlString && signature && [AVURLAsset isPlayableExtendedMIMEType:type]) { - NSURL *streamURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@&signature=%@", stream[@"url"], stream[@"sig"]]]; + NSURL *streamURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@&signature=%@", urlString, signature]]; streamURLs[@([stream[@"itag"] integerValue])] = streamURL; } } @@ -203,7 +236,24 @@ - (NSURL *) videoURLWithData:(NSData *)data error:(NSError **)error { NSURL *streamURL = streamURLs[videoQuality]; if (streamURL) + { + NSString *title = video[@"title"]; + NSString *thumbnailSmall = video[@"thumbnail_url"]; + NSString *thumbnailMedium = video[@"iurlsd"]; + NSString *thumbnailLarge = video[@"iurlmaxres"]; + NSMutableDictionary *userInfo = [NSMutableDictionary new]; + if (title) + userInfo[XCDMetadataKeyTitle] = title; + if (thumbnailSmall) + userInfo[XCDMetadataKeySmallThumbnailURL] = [NSURL URLWithString:thumbnailSmall]; + if (thumbnailMedium) + userInfo[XCDMetadataKeyMediumThumbnailURL] = [NSURL URLWithString:thumbnailMedium]; + if (thumbnailLarge) + userInfo[XCDMetadataKeyLargeThumbnailURL] = [NSURL URLWithString:thumbnailLarge]; + + [[NSNotificationCenter defaultCenter] postNotificationName:XCDYouTubeVideoPlayerViewControllerDidReceiveMetadataNotification object:self userInfo:userInfo]; return streamURL; + } } if (error) diff --git a/YouTube Video Player Demo.xcodeproj/project.pbxproj b/YouTube Video Player Demo.xcodeproj/project.pbxproj index 1af531d39..6eddd6fec 100644 --- a/YouTube Video Player Demo.xcodeproj/project.pbxproj +++ b/YouTube Video Player Demo.xcodeproj/project.pbxproj @@ -213,7 +213,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_OPTIMIZATION_LEVEL = 0; @@ -247,7 +247,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = YES; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_PREPROCESSOR_DEFINITIONS = "NS_BLOCK_ASSERTIONS=1"; GCC_WARN_ABOUT_RETURN_TYPE = YES; diff --git a/YouTube Video Player Demo/AppDelegate.m b/YouTube Video Player Demo/AppDelegate.m index 33bfefccf..f9362dd8b 100644 --- a/YouTube Video Player Demo/AppDelegate.m +++ b/YouTube Video Player Demo/AppDelegate.m @@ -8,6 +8,7 @@ #import "AppDelegate.h" +#import "XCDYouTubeVideoPlayerViewController.h" #import "DemoViewController.h" @implementation AppDelegate @@ -16,10 +17,86 @@ @implementation AppDelegate - (BOOL) application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(videoPlayerViewControllerDidReceiveMetadata:) name:XCDYouTubeVideoPlayerViewControllerDidReceiveMetadataNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(moviePlayerPlaybackDidFinish:) name:MPMoviePlayerPlaybackDidFinishNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(moviePlayerPlaybackStateDidChange:) name:MPMoviePlayerPlaybackStateDidChangeNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(moviePlayerLoadStateDidChange:) name:MPMoviePlayerLoadStateDidChangeNotification object:nil]; + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; self.window.rootViewController = [[UINavigationController alloc] initWithRootViewController:[DemoViewController new]]; [self.window makeKeyAndVisible]; return YES; } +#pragma mark - Notifications + +- (void) moviePlayerPlaybackDidFinish:(NSNotification *)notification +{ + MPMovieFinishReason finishReason = [notification.userInfo[MPMoviePlayerPlaybackDidFinishReasonUserInfoKey] integerValue]; + NSError *error = notification.userInfo[XCDMoviePlayerPlaybackDidFinishErrorUserInfoKey]; + NSString *reason = @"Unknown"; + switch (finishReason) + { + case MPMovieFinishReasonPlaybackEnded: + reason = @"Playback Ended"; + break; + case MPMovieFinishReasonPlaybackError: + reason = @"Playback Error"; + break; + case MPMovieFinishReasonUserExited: + reason = @"User Exited"; + break; + } + NSLog(@"Finish Reason: %@%@", reason, error ? [@"\n" stringByAppendingString:[error description]] : @""); +} + +- (void) moviePlayerPlaybackStateDidChange:(NSNotification *)notification +{ + MPMoviePlayerController *moviePlayerController = notification.object; + NSString *playbackState = @"Unknown"; + switch (moviePlayerController.playbackState) + { + case MPMoviePlaybackStateStopped: + playbackState = @"Stopped"; + break; + case MPMoviePlaybackStatePlaying: + playbackState = @"Playing"; + break; + case MPMoviePlaybackStatePaused: + playbackState = @"Paused"; + break; + case MPMoviePlaybackStateInterrupted: + playbackState = @"Interrupted"; + break; + case MPMoviePlaybackStateSeekingForward: + playbackState = @"Seeking Forward"; + break; + case MPMoviePlaybackStateSeekingBackward: + playbackState = @"Seeking Backward"; + break; + } + NSLog(@"Playback State: %@", playbackState); +} + +- (void) moviePlayerLoadStateDidChange:(NSNotification *)notification +{ + MPMoviePlayerController *moviePlayerController = notification.object; + + NSMutableString *loadState = [NSMutableString new]; + MPMovieLoadState state = moviePlayerController.loadState; + if (state & MPMovieLoadStatePlayable) + [loadState appendString:@" | Playable"]; + if (state & MPMovieLoadStatePlaythroughOK) + [loadState appendString:@" | Playthrough OK"]; + if (state & MPMovieLoadStateStalled) + [loadState appendString:@" | Stalled"]; + + NSLog(@"Load State: %@", loadState.length > 0 ? [loadState substringFromIndex:3] : @"N/A"); +} + +- (void) videoPlayerViewControllerDidReceiveMetadata:(NSNotification *)notification +{ + NSLog(@"Metadata: %@", notification.userInfo); +} + @end diff --git a/YouTube Video Player Demo/DemoViewController.m b/YouTube Video Player Demo/DemoViewController.m index deef9a8e4..50b9f09ee 100644 --- a/YouTube Video Player Demo/DemoViewController.m +++ b/YouTube Video Player Demo/DemoViewController.m @@ -26,9 +26,7 @@ - (id) initWithNibName:(NSString *)nibName bundle:(NSBundle *)nibBundle if (!(self = [super initWithNibName:nibName bundle:nibBundle])) return nil; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(moviePlayerPlaybackDidFinish:) name:MPMoviePlayerPlaybackDidFinishNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(moviePlayerPlaybackStateDidChange:) name:MPMoviePlayerPlaybackStateDidChangeNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(moviePlayerLoadStateDidChange:) name:MPMoviePlayerLoadStateDidChangeNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(videoPlayerViewControllerDidReceiveMetadata:) name:XCDYouTubeVideoPlayerViewControllerDidReceiveMetadataNotification object:nil]; return self; } @@ -65,7 +63,7 @@ - (IBAction) playYouTubeVideo:(id)sender if (self.fullScreenSwitch.on) [self presentMoviePlayerViewControllerAnimated:self.videoPlayerViewController]; - else if (![self.videoContainerView.subviews containsObject:self.videoPlayerViewController.moviePlayer.view]) + else [self.videoPlayerViewController presentInView:self.videoContainerView]; } @@ -80,6 +78,7 @@ - (IBAction) playTrendingVideo:(id)sender { [self.videoContainerView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; [videoPlayerViewController presentInView:self.videoContainerView]; + [videoPlayerViewController.moviePlayer prepareToPlay]; } // https://developers.google.com/youtube/2.0/developers_guide_protocol_video_feeds#Standard_feeds @@ -99,68 +98,44 @@ - (BOOL) textFieldShouldReturn:(UITextField *)textField #pragma mark - Notifications -- (void) moviePlayerPlaybackDidFinish:(NSNotification *)notification -{ - MPMovieFinishReason finishReason = [notification.userInfo[MPMoviePlayerPlaybackDidFinishReasonUserInfoKey] integerValue]; - NSError *error = notification.userInfo[XCDMoviePlayerPlaybackDidFinishErrorUserInfoKey]; - NSString *reason = @"Unknown"; - switch (finishReason) - { - case MPMovieFinishReasonPlaybackEnded: - reason = @"Playback Ended"; - break; - case MPMovieFinishReasonPlaybackError: - reason = @"Playback Error"; - break; - case MPMovieFinishReasonUserExited: - reason = @"User Exited"; - break; - } - NSLog(@"Finish Reason: %@%@", reason, error ? [@"\n" stringByAppendingString:[error description]] : @""); -} - -- (void) moviePlayerPlaybackStateDidChange:(NSNotification *)notification +- (void) videoPlayerViewControllerDidReceiveMetadata:(NSNotification *)notification { - MPMoviePlayerController *moviePlayerController = notification.object; - NSString *playbackState = @"Unknown"; - switch (moviePlayerController.playbackState) - { - case MPMoviePlaybackStateStopped: - playbackState = @"Stopped"; - break; - case MPMoviePlaybackStatePlaying: - playbackState = @"Playing"; - break; - case MPMoviePlaybackStatePaused: - playbackState = @"Paused"; - break; - case MPMoviePlaybackStateInterrupted: - playbackState = @"Interrupted"; - break; - case MPMoviePlaybackStateSeekingForward: - playbackState = @"Seeking Forward"; - break; - case MPMoviePlaybackStateSeekingBackward: - playbackState = @"Seeking Backward"; - break; - } - NSLog(@"Playback State: %@", playbackState); + if (notification.object != self.videoPlayerViewController) + return; + + NSURL *thumbnailURL = notification.userInfo[XCDMetadataKeyMediumThumbnailURL] ?: notification.userInfo[XCDMetadataKeySmallThumbnailURL]; + [NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:thumbnailURL] queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) { + UIImageView *thumbnailImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, self.videoContainerView.bounds.size.width, self.videoContainerView.bounds.size.height)]; + thumbnailImageView.image = [UIImage imageWithData:data]; + thumbnailImageView.backgroundColor = [UIColor blackColor]; + thumbnailImageView.contentMode = UIViewContentModeScaleAspectFit; + thumbnailImageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + thumbnailImageView.userInteractionEnabled = YES; + [self.videoContainerView addSubview:thumbnailImageView]; + + // Do not get the `Play` image like this in production code + NSString *simulatorRoot = [[[NSProcessInfo processInfo] environment] objectForKey:@"IPHONE_SIMULATOR_ROOT"] ?: @""; + NSBundle *quickTimePlugin = [NSBundle bundleWithPath:[simulatorRoot stringByAppendingPathComponent:@"/System/Library/Internet Plug-Ins/QuickTime Plugin.webplugin"]]; + NSURL *playURL = [quickTimePlugin URLForResource:@"Play" withExtension:@"png"]; + UIImage *playImage = [UIImage imageWithContentsOfFile:playURL.path]; + UIButton *playButton = [UIButton buttonWithType:UIButtonTypeCustom]; + playButton.frame = thumbnailImageView.frame; + playButton.autoresizingMask = thumbnailImageView.autoresizingMask; + [playButton setImage:playImage forState:UIControlStateNormal]; + [playButton addTarget:self action:@selector(play:) forControlEvents:UIControlEventTouchUpInside]; + [thumbnailImageView addSubview:playButton]; + }]; } -- (void) moviePlayerLoadStateDidChange:(NSNotification *)notification +- (void) play:(UIButton *)sender { - MPMoviePlayerController *moviePlayerController = notification.object; - - NSMutableString *loadState = [NSMutableString new]; - MPMovieLoadState state = moviePlayerController.loadState; - if (state & MPMovieLoadStatePlayable) - [loadState appendString:@" | Playable"]; - if (state & MPMovieLoadStatePlaythroughOK) - [loadState appendString:@" | Playthrough OK"]; - if (state & MPMovieLoadStateStalled) - [loadState appendString:@" | Stalled"]; + [UIView animateWithDuration:0.3f animations:^{ + sender.superview.alpha = 0.f; + } completion:^(BOOL finished) { + [sender.superview removeFromSuperview]; + }]; - NSLog(@"Load State: %@", loadState.length > 0 ? [loadState substringFromIndex:3] : @"N/A"); + [self.videoPlayerViewController.moviePlayer play]; } @end diff --git a/YouTube Video Player Demo/YouTube Video Player Demo-Info.plist b/YouTube Video Player Demo/YouTube Video Player Demo-Info.plist index 12528ffb4..c60cca643 100644 --- a/YouTube Video Player Demo/YouTube Video Player Demo-Info.plist +++ b/YouTube Video Player Demo/YouTube Video Player Demo-Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0.0 + 1.1.0 CFBundleVersion ${CURRENT_PROJECT_VERSION} LSRequiresIPhoneOS