Skip to content

Commit

Permalink
Merge branch 'release/1.0.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
0xced committed May 17, 2013
2 parents 0233105 + c0cc872 commit 1efdd60
Show file tree
Hide file tree
Showing 19 changed files with 1,523 additions and 0 deletions.
22 changes: 22 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
The MIT License (MIT)

Copyright (c) 2013 Cédric Luthi

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

73 changes: 73 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
## About

**XCDYouTubeVideoPlayerViewController** is a YouTube video player for iPhone and iPad.

<img src="Screenshots/XCDYouTubeVideoPlayerViewController.png" width="480" height="320">

To the best of my knowledge, the only *official* way of playing a YouTube video on iOS is with a UIWebView and the [iframe player API](https://developers.google.com/youtube/iframe_api_reference). Unfortunately, this is very slow and quite ugly, so I wrote XCDYouTubeVideoPlayerViewController which gives the user a better viewing experience.

XCDYouTubeVideoPlayerViewController uses progressive download, so remember that some restrictions apply if you submit your app to the App Store, as stated in
[HTTP Live Streaming — Requirements for Apps](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/StreamingMediaGuide/UsingHTTPLiveStreaming/UsingHTTPLiveStreaming.html#//apple_ref/doc/uid/TP40008332-CH102-SW5):
> **Warning**: iOS apps submitted for distribution in the App Store must conform to these requirements.
>
> If your app delivers video over cellular networks, and the video exceeds either 10 minutes duration or 5 MB of data in a five minute period, you are required to use HTTP Live Streaming. (Progressive download may be used for smaller clips.)
## Requirements

- Runs on iOS 5.0 and later
- Must be compiled with ARC

## Installation

XCDYouTubeVideoPlayerViewController is available through CocoaPods.

Alternatively, you can install it manually:

1. Copy the `XCDYouTubeVideoPlayerViewController.h` and `XCDYouTubeVideoPlayerViewController.m` files in your project.
2. Add `MediaPlayer.framework` and `AVFoundation.framework` in your project.

## Usage

Use `XCDYouTubeVideoPlayerViewController` the same way you use a `MPMoviePlayerViewController`, except you initialize it with a YouTube video identifier instead of a content URL.

#### Present the video in full-screen

```
XCDYouTubeVideoPlayerViewController *videoPlayerViewController = [[XCDYouTubeVideoPlayerViewController alloc] initWithVideoIdentifier:@"9bZkp7q19f0"];
[self presentMoviePlayerViewControllerAnimated:videoPlayerViewController];
```

#### Fetch the video identifier asynchronously

```
XCDYouTubeVideoPlayerViewController *videoPlayerViewController = [XCDYouTubeVideoPlayerViewController new];
[self presentMoviePlayerViewControllerAnimated:videoPlayerViewController];
NSURL *url = [NSURL URLWithString:@"https://gdata.youtube.com/feeds/api/standardfeeds/on_the_web?v=2&alt=json&max-results=1"];
[NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:url] queue:[NSOperationQueue new] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
id json = [NSJSONSerialization JSONObjectWithData:data ?: [NSData new] options:0 error:NULL];
NSString *videoIdentifier = [[[json valueForKeyPath:@"feed.entry.media$group.yt$videoid.$t"] lastObject] description];
videoPlayerViewController.videoIdentifier = videoIdentifier;
}];
```

#### Present the video in a non full-screen view

```
XCDYouTubeVideoPlayerViewController *videoPlayerViewController = [[XCDYouTubeVideoPlayerViewController alloc] initWithVideoIdentifier:@"9bZkp7q19f0"];
[videoPlayerViewController presentInView:self.videoContainerView];
```

See the demo project for more sample code.

## Contact

Cédric Luthi

- http://github.com/0xced
- http://twitter.com/0xced
- [email protected]

## License

XCDYouTubeVideoPlayerViewController is available under the MIT license. See the LICENSE file for more information.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions XCDYouTubeVideoPlayerViewController.podspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Pod::Spec.new do |s|
s.name = "XCDYouTubeVideoPlayerViewController"
s.version = "1.0.0"
s.summary = "YouTube video player for iPhone and iPad."
s.homepage = "https://github.com/0xced/XCDYouTubeVideoPlayerViewController"
s.license = 'MIT'
s.author = { "Cédric Luthi" => "[email protected]" }
s.source = { :git => "https://github.com/0xced/XCDYouTubeVideoPlayerViewController.git", :tag => "1.0.0" }
s.platform = :ios, '5.0'
s.source_files = 'XCDYouTubeVideoPlayerViewController'
s.frameworks = 'AVFoundation', 'MediaPlayer'
s.requires_arc = true
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// XCDYouTubeVideoPlayerViewController.h
// YouTube Video Player Demo
//
// Created by Cédric Luthi on 02.05.13.
// Copyright (c) 2013 Cédric Luthi. All rights reserved.
//

#import <MediaPlayer/MediaPlayer.h>

typedef NS_ENUM(NSUInteger, XCDYouTubeVideoQuality) {
XCDYouTubeVideoQualitySmall240 = 36,
XCDYouTubeVideoQualityMedium360 = 18,
XCDYouTubeVideoQualityHD720 = 22,
XCDYouTubeVideoQualityHD1080 = 37,
};

MP_EXTERN NSString *const XCDYouTubeVideoErrorDomain;
MP_EXTERN NSString *const XCDMoviePlayerPlaybackDidFinishErrorUserInfoKey; // NSError

@interface XCDYouTubeVideoPlayerViewController : MPMoviePlayerViewController

- (id) initWithVideoIdentifier:(NSString *)videoIdentifier;

@property (nonatomic, copy) NSString *videoIdentifier;

// On iPhone, defaults to @[ @(XCDYouTubeVideoQualityHD720), @(XCDYouTubeVideoQualityMedium360), @(XCDYouTubeVideoQualitySmall240) ]
// On iPad, defaults to @[ @(XCDYouTubeVideoQualityHD1080), @(XCDYouTubeVideoQualityHD720), @(XCDYouTubeVideoQualityMedium360), @(XCDYouTubeVideoQualitySmall240) ]
// 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.
- (void) presentInView:(UIView *)view;

@end
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
//
// XCDYouTubeVideoPlayerViewController.m
// YouTube Video Player Demo
//
// Created by Cédric Luthi on 02.05.13.
// Copyright (c) 2013 Cédric Luthi. All rights reserved.
//

#import "XCDYouTubeVideoPlayerViewController.h"

#import <AVFoundation/AVFoundation.h>
#import <objc/runtime.h>

NSString *const XCDYouTubeVideoErrorDomain = @"XCDYouTubeVideoErrorDomain";
NSString *const XCDMoviePlayerPlaybackDidFinishErrorUserInfoKey = @"XCDMoviePlayerPlaybackDidFinishErrorUserInfoKey";

static NSDictionary *DictionaryWithQueryString(NSString *string, NSStringEncoding encoding)
{
NSMutableDictionary *dictionary = [NSMutableDictionary new];
NSArray *fields = [string componentsSeparatedByString:@"&"];
for (NSString *field in fields)
{
NSArray *pair = [field componentsSeparatedByString:@"="];
if (pair.count == 2)
{
NSString *key = pair[0];
NSString *value = [pair[1] stringByReplacingPercentEscapesUsingEncoding:encoding];
value = [value stringByReplacingOccurrencesOfString:@"+" withString:@" "];
dictionary[key] = value;
}
}
return dictionary;
}

@interface XCDYouTubeVideoPlayerViewController ()
@property (nonatomic, strong) NSURLConnection *connection;
@property (nonatomic, strong) NSMutableData *connectionData;
@property (nonatomic, strong) NSMutableArray *elFields;
@property (nonatomic, assign, getter = isEmbedded) BOOL embedded;
@end

@implementation XCDYouTubeVideoPlayerViewController

static void *MoviePlayerKey = &MoviePlayerKey;

- (id) init
{
return [self initWithVideoIdentifier:nil];
}

- (id) initWithContentURL:(NSURL *)contentURL
{
@throw [NSException exceptionWithName:NSGenericException reason:@"Use the `initWithVideoIdentifier:` method instead." userInfo:nil];
}

- (id) initWithVideoIdentifier:(NSString *)videoIdentifier
{
if (!(self = [super init]))
return nil;

if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone)
_preferredVideoQualities = @[ @(XCDYouTubeVideoQualityHD720), @(XCDYouTubeVideoQualityMedium360), @(XCDYouTubeVideoQualitySmall240) ];
else
_preferredVideoQualities = @[ @(XCDYouTubeVideoQualityHD1080), @(XCDYouTubeVideoQualityHD720), @(XCDYouTubeVideoQualityMedium360), @(XCDYouTubeVideoQualitySmall240) ];

if (videoIdentifier)
self.videoIdentifier = videoIdentifier;

return self;
}

- (void) setVideoIdentifier:(NSString *)videoIdentifier
{
if (![NSThread isMainThread])
{
[self performSelectorOnMainThread:_cmd withObject:videoIdentifier waitUntilDone:NO];
return;
}

if ([videoIdentifier isEqual:self.videoIdentifier])
return;

_videoIdentifier = [videoIdentifier copy];

self.elFields = [[NSMutableArray alloc] initWithArray:@[ @"embedded", @"detailpage", @"vevo", @"" ]];

[self startVideoInfoRequest];
}

- (void) presentInView:(UIView *)view
{
self.embedded = YES;

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);
}

- (void) startVideoInfoRequest
{
NSString *elField = [self.elFields objectAtIndex:0];
[self.elFields removeObjectAtIndex:0];
if (elField.length > 0)
elField = [@"&el=" stringByAppendingString:elField];

NSURL *videoInfoURL = [NSURL URLWithString:[NSString stringWithFormat:@"https://www.youtube.com/get_video_info?video_id=%@%@&ps=default&eurl=&gl=US&hl=en", self.videoIdentifier ?: @"", elField]];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:videoInfoURL cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10];
[self.connection cancel];
self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
}

- (void) finishWithError:(NSError *)error
{
NSDictionary *userInfo = @{ MPMoviePlayerPlaybackDidFinishReasonUserInfoKey: @(MPMovieFinishReasonPlaybackError),
XCDMoviePlayerPlaybackDidFinishErrorUserInfoKey: error };
[[NSNotificationCenter defaultCenter] postNotificationName:MPMoviePlayerPlaybackDidFinishNotification object:self.moviePlayer userInfo:userInfo];

if (self.isEmbedded)
[self.moviePlayer.view removeFromSuperview];
else
[self.presentingViewController dismissMoviePlayerViewControllerAnimated];
}

#pragma mark - UIViewController

- (void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];

if ([self isBeingPresented])
self.moviePlayer.controlStyle = MPMovieControlStyleFullscreen;
}

- (void) viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];

if ([self isBeingDismissed])
[self.connection cancel];
}

#pragma mark - NSURLConnectionDataDelegate / NSURLConnectionDelegate

- (void) connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
NSUInteger capacity = response.expectedContentLength == NSURLResponseUnknownLength ? 0 : response.expectedContentLength;
self.connectionData = [[NSMutableData alloc] initWithCapacity:capacity];
}

- (void) connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[self.connectionData appendData:data];
}

- (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
{
[self finishWithError:error];
}

#pragma mark - URL Parsing

- (NSURL *) videoURLWithData:(NSData *)data error:(NSError **)error
{
NSString *videoQuery = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
NSStringEncoding queryEncoding = NSUTF8StringEncoding;
NSDictionary *video = DictionaryWithQueryString(videoQuery, queryEncoding);
NSArray *streamQueries = [video[@"url_encoded_fmt_stream_map"] componentsSeparatedByString:@","];

NSMutableDictionary *streamURLs = [NSMutableDictionary new];
for (NSString *streamQuery in streamQueries)
{
NSDictionary *stream = DictionaryWithQueryString(streamQuery, queryEncoding);
NSString *type = stream[@"type"];
if ([AVURLAsset isPlayableExtendedMIMEType:type])
{
NSURL *streamURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@&signature=%@", stream[@"url"], stream[@"sig"]]];
streamURLs[@([stream[@"itag"] integerValue])] = streamURL;
}
}

for (NSNumber *videoQuality in self.preferredVideoQualities)
{
NSURL *streamURL = streamURLs[videoQuality];
if (streamURL)
return streamURL;
}

if (error)
{
NSMutableDictionary *userInfo = [@{ NSURLErrorKey: self.connection.originalRequest.URL } mutableCopy];
NSString *reason = video[@"reason"];
if (reason)
userInfo[NSLocalizedDescriptionKey] = reason;

NSInteger code = [video[@"errorcode"] integerValue];
*error = [NSError errorWithDomain:XCDYouTubeVideoErrorDomain code:code userInfo:userInfo];
}

return nil;
}

@end
Loading

0 comments on commit 1efdd60

Please sign in to comment.