diff --git a/Classes/YTPlayerView.h b/Classes/YTPlayerView.h index 7923c15..a145e57 100644 --- a/Classes/YTPlayerView.h +++ b/Classes/YTPlayerView.h @@ -13,6 +13,7 @@ // limitations under the License. #import +#import @class YTPlayerView; @@ -138,9 +139,9 @@ typedef NS_ENUM(NSInteger, YTPlayerError) { * YTPlayerView::loadWithPlaylistId: or their variants to set the video or playlist * to populate the view with. */ -@interface YTPlayerView : UIView +@interface YTPlayerView : UIView -@property(nonatomic, strong, nullable, readonly) UIWebView *webView; +@property(nonatomic, strong, nullable, readonly) WKWebView *webView; /** A delegate to be notified on playback events. */ @property(nonatomic, weak, nullable) id delegate; @@ -150,8 +151,8 @@ typedef NS_ENUM(NSInteger, YTPlayerError) { * This is a convenience method for calling YTPlayerView::loadPlayerWithVideoId:withPlayerVars: * without player variables. * - * This method reloads the entire contents of the UIWebView and regenerates its HTML contents. - * To change the currently loaded video without reloading the entire UIWebView, use the + * This method reloads the entire contents of the WKWebView and regenerates its HTML contents. + * To change the currently loaded video without reloading the entire WKWebView, use the * YTPlayerView::cueVideoById:startSeconds:suggestedQuality: family of methods. * * @param videoId The YouTube video ID of the video to load in the player view. @@ -164,8 +165,8 @@ typedef NS_ENUM(NSInteger, YTPlayerError) { * This is a convenience method for calling YTPlayerView::loadWithPlaylistId:withPlayerVars: * without player variables. * - * This method reloads the entire contents of the UIWebView and regenerates its HTML contents. - * To change the currently loaded video without reloading the entire UIWebView, use the + * This method reloads the entire contents of the WKWebView and regenerates its HTML contents. + * To change the currently loaded video without reloading the entire WKWebView, use the * YTPlayerView::cuePlaylistByPlaylistId:index:startSeconds:suggestedQuality: * family of methods. * @@ -187,8 +188,8 @@ typedef NS_ENUM(NSInteger, YTPlayerError) { * both strings and integers are valid values. The full list of parameters is defined at: * https://developers.google.com/youtube/player_parameters?playerVersion=HTML5. * - * This method reloads the entire contents of the UIWebView and regenerates its HTML contents. - * To change the currently loaded video without reloading the entire UIWebView, use the + * This method reloads the entire contents of the WKWebView and regenerates its HTML contents. + * To change the currently loaded video without reloading the entire WKWebView, use the * YTPlayerView::cueVideoById:startSeconds:suggestedQuality: family of methods. * * @param videoId The YouTube video ID of the video to load in the player view. @@ -210,8 +211,8 @@ typedef NS_ENUM(NSInteger, YTPlayerError) { * both strings and integers are valid values. The full list of parameters is defined at: * https://developers.google.com/youtube/player_parameters?playerVersion=HTML5. * - * This method reloads the entire contents of the UIWebView and regenerates its HTML contents. - * To change the currently loaded video without reloading the entire UIWebView, use the + * This method reloads the entire contents of the WKWebView and regenerates its HTML contents. + * To change the currently loaded video without reloading the entire WKWebView, use the * YTPlayerView::cuePlaylistByPlaylistId:index:startSeconds:suggestedQuality: * family of methods. * diff --git a/Classes/YTPlayerView.m b/Classes/YTPlayerView.m index b2e2df4..5e99d60 100644 --- a/Classes/YTPlayerView.m +++ b/Classes/YTPlayerView.m @@ -104,23 +104,23 @@ - (BOOL)loadWithPlaylistId:(NSString *)playlistId playerVars:(NSDictionary *)pla #pragma mark - Player methods - (void)playVideo { - [self stringFromEvaluatingJavaScript:@"player.playVideo();"]; + [self evaluateJavaScript:@"player.playVideo();"]; } - (void)pauseVideo { [self notifyDelegateOfYouTubeCallbackUrl:[NSURL URLWithString:[NSString stringWithFormat:@"ytplayer://onStateChange?data=%@", kYTPlayerStatePausedCode]]]; - [self stringFromEvaluatingJavaScript:@"player.pauseVideo();"]; + [self evaluateJavaScript:@"player.pauseVideo();"]; } - (void)stopVideo { - [self stringFromEvaluatingJavaScript:@"player.stopVideo();"]; + [self evaluateJavaScript:@"player.stopVideo();"]; } - (void)seekToSeconds:(float)seekToSeconds allowSeekAhead:(BOOL)allowSeekAhead { NSNumber *secondsValue = [NSNumber numberWithFloat:seekToSeconds]; NSString *allowSeekAheadValue = [self stringForJSBoolean:allowSeekAhead]; NSString *command = [NSString stringWithFormat:@"player.seekTo(%@, %@);", secondsValue, allowSeekAheadValue]; - [self stringFromEvaluatingJavaScript:command]; + [self evaluateJavaScript:command]; } #pragma mark - Cueing methods @@ -132,7 +132,7 @@ - (void)cueVideoById:(NSString *)videoId NSString *qualityValue = [YTPlayerView stringForPlaybackQuality:suggestedQuality]; NSString *command = [NSString stringWithFormat:@"player.cueVideoById('%@', %@, '%@');", videoId, startSecondsValue, qualityValue]; - [self stringFromEvaluatingJavaScript:command]; + [self evaluateJavaScript:command]; } - (void)cueVideoById:(NSString *)videoId @@ -143,7 +143,7 @@ - (void)cueVideoById:(NSString *)videoId NSNumber *endSecondsValue = [NSNumber numberWithFloat:endSeconds]; NSString *qualityValue = [YTPlayerView stringForPlaybackQuality:suggestedQuality]; NSString *command = [NSString stringWithFormat:@"player.cueVideoById({'videoId': '%@', 'startSeconds': %@, 'endSeconds': %@, 'suggestedQuality': '%@'});", videoId, startSecondsValue, endSecondsValue, qualityValue]; - [self stringFromEvaluatingJavaScript:command]; + [self evaluateJavaScript:command]; } - (void)loadVideoById:(NSString *)videoId @@ -153,7 +153,7 @@ - (void)loadVideoById:(NSString *)videoId NSString *qualityValue = [YTPlayerView stringForPlaybackQuality:suggestedQuality]; NSString *command = [NSString stringWithFormat:@"player.loadVideoById('%@', %@, '%@');", videoId, startSecondsValue, qualityValue]; - [self stringFromEvaluatingJavaScript:command]; + [self evaluateJavaScript:command]; } - (void)loadVideoById:(NSString *)videoId @@ -164,7 +164,7 @@ - (void)loadVideoById:(NSString *)videoId NSNumber *endSecondsValue = [NSNumber numberWithFloat:endSeconds]; NSString *qualityValue = [YTPlayerView stringForPlaybackQuality:suggestedQuality]; NSString *command = [NSString stringWithFormat:@"player.loadVideoById({'videoId': '%@', 'startSeconds': %@, 'endSeconds': %@, 'suggestedQuality': '%@'});",videoId, startSecondsValue, endSecondsValue, qualityValue]; - [self stringFromEvaluatingJavaScript:command]; + [self evaluateJavaScript:command]; } - (void)cueVideoByURL:(NSString *)videoURL @@ -174,7 +174,7 @@ - (void)cueVideoByURL:(NSString *)videoURL NSString *qualityValue = [YTPlayerView stringForPlaybackQuality:suggestedQuality]; NSString *command = [NSString stringWithFormat:@"player.cueVideoByUrl('%@', %@, '%@');", videoURL, startSecondsValue, qualityValue]; - [self stringFromEvaluatingJavaScript:command]; + [self evaluateJavaScript:command]; } - (void)cueVideoByURL:(NSString *)videoURL @@ -186,7 +186,7 @@ - (void)cueVideoByURL:(NSString *)videoURL NSString *qualityValue = [YTPlayerView stringForPlaybackQuality:suggestedQuality]; NSString *command = [NSString stringWithFormat:@"player.cueVideoByUrl('%@', %@, %@, '%@');", videoURL, startSecondsValue, endSecondsValue, qualityValue]; - [self stringFromEvaluatingJavaScript:command]; + [self evaluateJavaScript:command]; } - (void)loadVideoByURL:(NSString *)videoURL @@ -196,7 +196,7 @@ - (void)loadVideoByURL:(NSString *)videoURL NSString *qualityValue = [YTPlayerView stringForPlaybackQuality:suggestedQuality]; NSString *command = [NSString stringWithFormat:@"player.loadVideoByUrl('%@', %@, '%@');", videoURL, startSecondsValue, qualityValue]; - [self stringFromEvaluatingJavaScript:command]; + [self evaluateJavaScript:command]; } - (void)loadVideoByURL:(NSString *)videoURL @@ -208,7 +208,7 @@ - (void)loadVideoByURL:(NSString *)videoURL NSString *qualityValue = [YTPlayerView stringForPlaybackQuality:suggestedQuality]; NSString *command = [NSString stringWithFormat:@"player.loadVideoByUrl('%@', %@, %@, '%@');", videoURL, startSecondsValue, endSecondsValue, qualityValue]; - [self stringFromEvaluatingJavaScript:command]; + [self evaluateJavaScript:command]; } #pragma mark - Cueing methods for lists @@ -217,11 +217,13 @@ - (void)cuePlaylistByPlaylistId:(NSString *)playlistId index:(int)index startSeconds:(float)startSeconds suggestedQuality:(YTPlaybackQuality)suggestedQuality { - NSString *playlistIdString = [NSString stringWithFormat:@"'%@'", playlistId]; - [self cuePlaylist:playlistIdString - index:index - startSeconds:startSeconds - suggestedQuality:suggestedQuality]; + NSString *playlistIdString = [NSString stringWithFormat:@"'%@'", playlistId]; + NSNumber *indexValue = [NSNumber numberWithInt:index]; + NSNumber *startSecondsValue = [NSNumber numberWithFloat:startSeconds]; + NSString *qualityValue = [YTPlayerView stringForPlaybackQuality:suggestedQuality]; + NSString *command = [NSString stringWithFormat:@"player.cuePlaylist({listType: 'playlist', list: %@,index: %@, startSeconds: %@, suggestedQuality: '%@'});", + playlistIdString, indexValue, startSecondsValue, qualityValue]; + [self stringFromEvaluatingJavaScript:command]; } - (void)cuePlaylistByVideos:(NSArray *)videoIds @@ -238,11 +240,13 @@ - (void)loadPlaylistByPlaylistId:(NSString *)playlistId index:(int)index startSeconds:(float)startSeconds suggestedQuality:(YTPlaybackQuality)suggestedQuality { - NSString *playlistIdString = [NSString stringWithFormat:@"'%@'", playlistId]; - [self loadPlaylist:playlistIdString - index:index - startSeconds:startSeconds - suggestedQuality:suggestedQuality]; + NSString *playlistIdString = [NSString stringWithFormat:@"'%@'", playlistId]; + NSNumber *indexValue = [NSNumber numberWithInt:index]; + NSNumber *startSecondsValue = [NSNumber numberWithFloat:startSeconds]; + NSString *qualityValue = [YTPlayerView stringForPlaybackQuality:suggestedQuality]; + NSString *command = [NSString stringWithFormat:@"player.loadPlaylist({listType: 'playlist', list: %@, index: %@, startSeconds: %@, suggestedQuality: '%@'});", + playlistIdString, indexValue, startSecondsValue, qualityValue]; + [self stringFromEvaluatingJavaScript:command]; } - (void)loadPlaylistByVideos:(NSArray *)videoIds @@ -264,7 +268,7 @@ - (float)playbackRate { - (void)setPlaybackRate:(float)suggestedRate { NSString *command = [NSString stringWithFormat:@"player.setPlaybackRate(%f);", suggestedRate]; - [self stringFromEvaluatingJavaScript:command]; + [self evaluateJavaScript:command]; } - (NSArray *)availablePlaybackRates { @@ -288,13 +292,13 @@ - (NSArray *)availablePlaybackRates { - (void)setLoop:(BOOL)loop { NSString *loopPlayListValue = [self stringForJSBoolean:loop]; NSString *command = [NSString stringWithFormat:@"player.setLoop(%@);", loopPlayListValue]; - [self stringFromEvaluatingJavaScript:command]; + [self evaluateJavaScript:command]; } - (void)setShuffle:(BOOL)shuffle { NSString *shufflePlayListValue = [self stringForJSBoolean:shuffle]; NSString *command = [NSString stringWithFormat:@"player.setShuffle(%@);", shufflePlayListValue]; - [self stringFromEvaluatingJavaScript:command]; + [self evaluateJavaScript:command]; } #pragma mark - Playback status @@ -321,7 +325,7 @@ - (YTPlaybackQuality)playbackQuality { - (void)setPlaybackQuality:(YTPlaybackQuality)suggestedQuality { NSString *qualityValue = [YTPlayerView stringForPlaybackQuality:suggestedQuality]; NSString *command = [NSString stringWithFormat:@"player.setPlaybackQuality('%@');", qualityValue]; - [self stringFromEvaluatingJavaScript:command]; + [self evaluateJavaScript:command]; } #pragma mark - Video information methods @@ -363,17 +367,17 @@ - (int)playlistIndex { #pragma mark - Playing a video in a playlist - (void)nextVideo { - [self stringFromEvaluatingJavaScript:@"player.nextVideo();"]; + [self evaluateJavaScript:@"player.nextVideo();"]; } - (void)previousVideo { - [self stringFromEvaluatingJavaScript:@"player.previousVideo();"]; + [self evaluateJavaScript:@"player.previousVideo();"]; } - (void)playVideoAt:(int)index { NSString *command = [NSString stringWithFormat:@"player.playVideoAt(%@);", [NSNumber numberWithInt:index]]; - [self stringFromEvaluatingJavaScript:command]; + [self evaluateJavaScript:command]; } #pragma mark - Helper methods @@ -392,24 +396,41 @@ - (NSArray *)availableQualityLevels { return levels; } -- (BOOL)webView:(UIWebView *)webView - shouldStartLoadWithRequest:(NSURLRequest *)request - navigationType:(UIWebViewNavigationType)navigationType { - if ([request.URL.host isEqual: self.originURL.host]) { - return YES; - } else if ([request.URL.scheme isEqual:@"ytplayer"]) { - [self notifyDelegateOfYouTubeCallbackUrl:request.URL]; - return NO; - } else if ([request.URL.scheme isEqual: @"http"] || [request.URL.scheme isEqual:@"https"]) { - return [self handleHttpNavigationToUrl:request.URL]; - } - return YES; +#pragma mark - WKNavigationDelegate methods + +- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { + NSURLRequest *request = navigationAction.request; + BOOL shouldLoad = YES; + if ([request.URL.host isEqual: self.originURL.host]) { + shouldLoad = YES; + } else if ([request.URL.scheme isEqual:@"ytplayer"]) { + [self notifyDelegateOfYouTubeCallbackUrl:request.URL]; + shouldLoad = NO; + } else if ([request.URL.scheme isEqual: @"http"] || [request.URL.scheme isEqual:@"https"]) { + shouldLoad = [self handleHttpNavigationToUrl:request.URL]; + } + if (decisionHandler) { + WKNavigationActionPolicy policy = (shouldLoad ? WKNavigationActionPolicyAllow : WKNavigationActionPolicyCancel); + decisionHandler(policy); + } } -- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error { - if (self.initialLoadingView) { - [self.initialLoadingView removeFromSuperview]; - } +- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error { + [self removeInitialLoadingViewIfNecessary]; +} + +- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation { + [self removeInitialLoadingViewIfNecessary]; +} + +- (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error { + [self removeInitialLoadingViewIfNecessary]; +} + +- (void)removeInitialLoadingViewIfNecessary { + if (self.initialLoadingView) { + [self.initialLoadingView removeFromSuperview]; + } } /** @@ -521,7 +542,7 @@ + (NSString *)stringForPlayerState:(YTPlayerState)state { /** * Private method to handle "navigation" to a callback URL of the format * ytplayer://action?data=someData - * This is how the UIWebView communicates with the containing Objective-C code. + * This is how the WKWebView communicates with the containing Objective-C code. * Side effects of this method are that it calls methods on this class's delegate. * * @param url A URL of the format ytplayer://action?data=value. @@ -602,7 +623,7 @@ - (void)notifyDelegateOfYouTubeCallbackUrl: (NSURL *) url { - (BOOL)handleHttpNavigationToUrl:(NSURL *) url { // Usually this means the user has clicked on the YouTube logo or an error message in the // player. Most URLs should open in the browser. The only http(s) URL that should open in this - // UIWebView is the URL for the embed, which is of the format: + // WKWebView is the URL for the embed, which is of the format: // http(s)://www.youtube.com/embed/[VIDEO ID]?[PARAMETERS] NSError *error = NULL; NSRegularExpression *ytRegex = @@ -705,8 +726,14 @@ - (BOOL)loadWithPlayerParams:(NSDictionary *)additionalPlayerParams { // Remove the existing webView to reset any state [self.webView removeFromSuperview]; _webView = [self createNewWebView]; + _webView.translatesAutoresizingMaskIntoConstraints = NO; [self addSubview:self.webView]; + [self.topAnchor constraintEqualToAnchor:self.webView.topAnchor].active = YES; + [self.bottomAnchor constraintEqualToAnchor:self.webView.bottomAnchor].active = YES; + [self.leadingAnchor constraintEqualToAnchor:self.webView.leadingAnchor].active = YES; + [self.trailingAnchor constraintEqualToAnchor:self.webView.trailingAnchor].active = YES; + NSError *error = nil; NSString *path = [[NSBundle bundleForClass:[YTPlayerView class]] pathForResource:@"YTPlayerView-iframe-player" ofType:@"html" @@ -745,10 +772,8 @@ - (BOOL)loadWithPlayerParams:(NSDictionary *)additionalPlayerParams { NSString *embedHTML = [NSString stringWithFormat:embedHTMLTemplate, playerVarsJsonString]; [self.webView loadHTMLString:embedHTML baseURL: self.originURL]; - [self.webView setDelegate:self]; - self.webView.allowsInlineMediaPlayback = YES; - self.webView.mediaPlaybackRequiresUserAction = NO; - + [self.webView setNavigationDelegate:self]; + if ([self.delegate respondsToSelector:@selector(playerViewPreferredInitialLoadingView:)]) { UIView *initialLoadingView = [self.delegate playerViewPreferredInitialLoadingView:self]; if (initialLoadingView) { @@ -782,7 +807,7 @@ - (void)cuePlaylist:(NSString *)cueingString NSString *qualityValue = [YTPlayerView stringForPlaybackQuality:suggestedQuality]; NSString *command = [NSString stringWithFormat:@"player.cuePlaylist(%@, %@, %@, '%@');", cueingString, indexValue, startSecondsValue, qualityValue]; - [self stringFromEvaluatingJavaScript:command]; + [self evaluateJavaScript:command]; } /** @@ -805,7 +830,7 @@ - (void)loadPlaylist:(NSString *)cueingString NSString *qualityValue = [YTPlayerView stringForPlaybackQuality:suggestedQuality]; NSString *command = [NSString stringWithFormat:@"player.loadPlaylist(%@, %@, %@, '%@');", cueingString, indexValue, startSecondsValue, qualityValue]; - [self stringFromEvaluatingJavaScript:command]; + [self evaluateJavaScript:command]; } /** @@ -831,7 +856,31 @@ - (NSString *)stringFromVideoIdArray:(NSArray *)videoIds { * @return JavaScript response from evaluating code. */ - (NSString *)stringFromEvaluatingJavaScript:(NSString *)jsToExecute { - return [self.webView stringByEvaluatingJavaScriptFromString:jsToExecute]; + dispatch_semaphore_t sema4 = dispatch_semaphore_create(0); + __block NSString *resultString = nil; + + [self.webView evaluateJavaScript:jsToExecute completionHandler:^(id result, NSError *error) { + if (error == nil && result != nil) { + resultString = [NSString stringWithFormat:@"%@", result]; + } + dispatch_semaphore_signal(sema4); + }]; + // WkWebView always calls completion handler on the main thread, so this keeps us from blocking ourselves. + // c.f. http://stackoverflow.com/questions/28388197/wkwebview-trying-to-query-javascript-synchronously-from-the-main-thread + while (dispatch_semaphore_wait(sema4, DISPATCH_TIME_NOW)) { + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode + beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]; + } + return resultString; +} + +/** + * This evaluates javascript when no return value is needed. So therefore, + * we can use the normal asynchronous -[WKWebView evaluateJavaScript:completionHandler:] method, + * just with no completion handler. + */ +- (void)evaluateJavaScript:(NSString *)jsToExecute { + [self.webView evaluateJavaScript:jsToExecute completionHandler:nil]; } /** @@ -846,12 +895,25 @@ - (NSString *)stringForJSBoolean:(BOOL)boolValue { #pragma mark - Exposed for Testing -- (void)setWebView:(UIWebView *)webView { +- (void)setWebView:(WKWebView *)webView { _webView = webView; } -- (UIWebView *)createNewWebView { - UIWebView *webView = [[UIWebView alloc] initWithFrame:self.bounds]; +- (WKWebView *)createNewWebView { + // WKWebView equivalent for UIWebView's scalesPageToFit + // http://stackoverflow.com/questions/26295277/wkwebview-equivalent-for-uiwebviews-scalespagetofit + NSString *jScript = @"var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta);"; + + WKUserScript *wkUScript = [[WKUserScript alloc] initWithSource:jScript injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES]; + WKUserContentController *wkUController = [[WKUserContentController alloc] init]; + [wkUController addUserScript:wkUScript]; + + WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init]; + config.userContentController = wkUController; + config.applicationNameForUserAgent = @"app-embedded-web-view"; + config.allowsInlineMediaPlayback = YES; + config.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; + WKWebView *webView = [[WKWebView alloc] initWithFrame:self.bounds configuration:config]; webView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); webView.scrollView.scrollEnabled = NO; webView.scrollView.bounces = NO; diff --git a/youtube-ios-player-helper.podspec b/youtube-ios-player-helper.podspec index 8eee90f..123ea43 100644 --- a/youtube-ios-player-helper.podspec +++ b/youtube-ios-player-helper.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "youtube-ios-player-helper" - s.version = "0.1.6" + s.version = "0.1.7" s.summary = "Helper library for iOS developers that want to embed YouTube videos in their iOS apps with the iframe player API." @@ -41,9 +41,9 @@ Pod::Spec.new do |s| "Ibrahim Ulukaya" => "ulukaya@google.com", "Yoshifumi Yamaguchi" => "yoshifumi@google.com" } s.social_media_url = "https://twitter.com/YouTubeDev" - s.source = { :git => "https://github.com/youtube/youtube-ios-player-helper.git", :tag => "0.1.6" } + s.source = { :git => "https://github.com/youtube/youtube-ios-player-helper.git", :tag => "0.1.7" } - s.platform = :ios, '6.0' + s.platform = :ios, '9.0' s.requires_arc = true s.source_files = 'Classes'