Skip to content

Commit

Permalink
Merge branch 'feature/specifyStreamURL' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
SoneeJohn committed Feb 24, 2020
2 parents 8ada0ca + 318209f commit 9ca9924
Show file tree
Hide file tree
Showing 9 changed files with 2,325 additions and 10 deletions.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

160 changes: 159 additions & 1 deletion XCDYouTubeKit Tests/XCDYouTubeClientTestCase.m
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,165 @@ - (void) testThatQueryingLiveVideoReturnsPlayableStreams
}];
}];

[self waitForExpectationsWithTimeout:900 handler:nil];
[self waitForExpectationsWithTimeout:5 handler:nil];
}

- (void) testQueryingWithSpecifiedStreamURLs
{
/**
* This video `NZzQQ1090wc` {itag 137 & 22) are reachable
* This test ensures that when specifying streamURLs that are in the `video` object that the operation returns only streamURLs that we specified when complete
*/
__weak XCTestExpectation *expectation = [self expectationWithDescription:@""];
[[XCDYouTubeClient defaultClient] getVideoWithIdentifier:@"NZzQQ1090wc" completionHandler:^(XCDYouTubeVideo *video, NSError *error)
{
XCTAssertNotNil(video);
XCTAssertNil(error);

NSArray<NSNumber *>*specifiedStreamiTags = @[@137, @22];
NSMutableDictionary *specifiedStreamURLs = [NSMutableDictionary new];

for (NSNumber *itag in specifiedStreamiTags)
{
if (video.streamURLs[itag])
{
specifiedStreamURLs[itag] = video.streamURLs[itag];
}
}

[[XCDYouTubeClient defaultClient] queryVideo:video streamURLsToQuery:specifiedStreamURLs options:nil cookies:nil completionHandler:^(NSDictionary * _Nullable streamURLs, NSError * _Nullable queryError, NSDictionary<id,NSError *> * _Nullable streamErrors)
{
XCTAssertNil(queryError);
XCTAssertNil(streamErrors);
XCTAssertNotNil(streamURLs);
XCTAssertTrue([NSThread isMainThread]);

for (id key in streamURLs.allKeys)
{
XCTAssertNotNil(streamURLs[key]);
}

XCTAssertEqual(specifiedStreamURLs.count, streamURLs.count, @"`streamURLs` count should be equal since we specified two streams that we know are reachable.");

[expectation fulfill];
}];
}];

[self waitForExpectationsWithTimeout:5 handler:nil];
}

- (void) testQueryingWithSpecifiedStreamURLsSomeNotBeingInVideoObject
{
/**
* This video `NZzQQ1090wc` {itag 137 & 22) are reachable
* This test ensures that when specifying streamURLs (with some not being in `video.streamURLs` that operation completes)
*/
__weak XCTestExpectation *expectation = [self expectationWithDescription:@""];
[[XCDYouTubeClient defaultClient] getVideoWithIdentifier:@"NZzQQ1090wc" completionHandler:^(XCDYouTubeVideo *video, NSError *error)
{
XCTAssertNotNil(video);
XCTAssertNil(error);

NSArray<NSNumber *>*specifiedStreamiTags = @[@137, @22, @1111111];
NSMutableDictionary *specifiedStreamURLs = [NSMutableDictionary new];

for (NSNumber *itag in specifiedStreamiTags)
{
if ([itag isEqual:@1111111] || [itag isEqual:@137])
{
//This will ensure the we do not query keys that are not in the `video` object's `streamURLs` and will ensure that the URL is the same was the value of specified key
specifiedStreamURLs[itag] = [NSURL URLWithString:@"https://www.youtube.com"];
continue;
}

specifiedStreamURLs[itag] = video.streamURLs[itag];
}

[[XCDYouTubeClient defaultClient] queryVideo:video streamURLsToQuery:specifiedStreamURLs options:nil cookies:nil completionHandler:^(NSDictionary * _Nullable streamURLs, NSError * _Nullable queryError, NSDictionary<id,NSError *> * _Nullable streamErrors)
{
XCTAssertNil(queryError);
XCTAssertNil(streamErrors);
XCTAssertNotNil(streamURLs);
XCTAssertTrue([NSThread isMainThread]);

for (id key in streamURLs.allKeys)
{
XCTAssertNotNil(streamURLs[key]);
}

XCTAssertEqual(1, streamURLs.count, @"`streamURLs` should be equal to 1 since we know only 1 of the specified streams would be queried.");
XCTAssertNotNil(streamURLs[@22]);

[expectation fulfill];
}];
}];

[self waitForExpectationsWithTimeout:5 handler:nil];
}

- (void) testQueryingWhenNoSpecifiedURLsAreInVideoObject
{
/**
* This video `NZzQQ1090wc` contains 24 streamURLs that are reachable ( all the URLs returned in `video.streamURLs` that are reachable)
* This test ensure that when none of the specified URLs are contained in `video.streamURLs` that we fallback to using `video.streamURLs` for querying.
*/
__weak XCTestExpectation *expectation = [self expectationWithDescription:@""];
[[XCDYouTubeClient defaultClient] getVideoWithIdentifier:@"NZzQQ1090wc" completionHandler:^(XCDYouTubeVideo *video, NSError *error)
{
XCTAssertNotNil(video);
XCTAssertNil(error);

NSMutableDictionary *specifiedStreamURLs = [NSMutableDictionary new];
specifiedStreamURLs[@1111111] = [NSURL URLWithString:@"https://www.youtube.com"];

[[XCDYouTubeClient defaultClient] queryVideo:video streamURLsToQuery:specifiedStreamURLs options:nil cookies:nil completionHandler:^(NSDictionary * _Nullable streamURLs, NSError * _Nullable queryError, NSDictionary<id,NSError *> * _Nullable streamErrors)
{
XCTAssertNil(queryError);
XCTAssertNil(streamErrors);
XCTAssertNotNil(streamURLs);
XCTAssertTrue([NSThread isMainThread]);

for (id key in streamURLs.allKeys)
{
XCTAssertNotNil(streamURLs[key]);
}

XCTAssertNotEqual(specifiedStreamURLs.count, streamURLs.count, @"`specifiedStreamURLs` should not be equal to `streamURLs` since when no streamURL is contained in `video.streamURLs` we use the `video.streamURLs` for querying. In this test we know `video.streamURLs` contains 24 objects.");

[expectation fulfill];
}];
}];

[self waitForExpectationsWithTimeout:5 handler:nil];
}

-(void) testQueryingWhenSpecifiedURLsAreEmpty
{
__weak XCTestExpectation *expectation = [self expectationWithDescription:@""];
[[XCDYouTubeClient defaultClient] getVideoWithIdentifier:@"NZzQQ1090wc" completionHandler:^(XCDYouTubeVideo *video, NSError *error)
{
XCTAssertNotNil(video);
XCTAssertNil(error);

[[XCDYouTubeClient defaultClient] queryVideo:video streamURLsToQuery:@{} options:nil cookies:nil completionHandler:^(NSDictionary * _Nullable streamURLs, NSError * _Nullable queryError, NSDictionary<id,NSError *> * _Nullable streamErrors)
{
XCTAssertNil(queryError);
XCTAssertNil(streamErrors);
XCTAssertNotNil(streamURLs);
XCTAssertTrue([NSThread isMainThread]);

for (id key in streamURLs.allKeys)
{
XCTAssertNotNil(streamURLs[key]);
}

XCTAssertEqual(video.streamURLs.count, streamURLs.count, @"`streamURLs` count should be equal to `video.streamURLs` since we specified an empty array and should fallback to the `video` object's `streamURLs`. We also, know all the streamsURLs are reachable.");

[expectation fulfill];
}];
}];

[self waitForExpectationsWithTimeout:5 handler:nil];
}

- (void) testExpiredLiveVideo
Expand Down
17 changes: 17 additions & 0 deletions XCDYouTubeKit/XCDYouTubeClient.h
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,23 @@ NS_ASSUME_NONNULL_BEGIN
* @return A newly initialized`<XCDYouTubeVideoQueryOperation>` object for canceling the asynchronous query operation. If you call the `cancel` method before the operation is finished, the completion handler will not be called.
*/
- (XCDYouTubeVideoQueryOperation *) queryVideo:(XCDYouTubeVideo *)video cookies:(nullable NSArray <NSHTTPCookie *>*)cookies completionHandler:(void (^)(NSDictionary * __nullable streamURLs, NSError * __nullable error, NSDictionary<id, NSError *> * __nullable streamErrors))completionHandler;
/**
* Starts an asynchronous operation for the specified `XCDYouTubeVide` object`, stream URLs to query and cookies, then calls a handler upon completion.
*
* @param video The `<XCDYouTubeVideo>` object that this operation will query. Passing a `nil` video will throw an `NSInvalidArgumentException` exception.
* @param streamURLsToQuery The specific stream URLs to query, can be nil. These URLs and keys must be contained in the `streamURLs` property of the `video` object, if none of the values in `streamURLsToQuery` match then all of the `streamURLs` will be queried.
* @param options Options that are reserved for future use.
* @param cookies An array of `NSHTTPCookie` objects, can be nil. These cookies can be used for certain videos that require a login.
* @param completionHandler A block to execute when the client finishes the operation. The completion handler is executed on the main thread. If the completion handler is nil, this method throws an exception.
* @discussion If the query operation completes successfully (i.e. at least one URL stream is reachable), the `streamURLs` parameter of the completion handler block contains a `NSDictionary` object, and the error parameter is nil. If the operation fails, the `streamURLs` parameter is nil and the error parameter contains information about the failure. The error's domain is always `XCDYouTubeVideoErrorDomain`. The `streamErrors` does not indicate that the operation failed but can contain detailed information on why a specific stream failed.In addition, this parameter is dictionary of `NSError` objects. The keys are the YouTube [itag](https://en.wikipedia.org/wiki/YouTube#Quality_and_formats) values as `NSNumber` objects. In some cases the errors within this dictionary may contain `NSError` objects with the code `NSURLErrorNetworkConnectionLost`—this may indicate that the file stored on YouTube's server is incomplete—furthermore, the error will make this suggestion via the`NSLocalizedRecoverySuggestionErrorKey` key of the error's `userInfo`.
*
* @see XCDYouTubeVideoQueryOperation
*
*
* @return A newly initialized`<XCDYouTubeVideoQueryOperation>` object for canceling the asynchronous query operation. If you call the `cancel` method before the operation is finished, the completion handler will not be called.
*/
- (XCDYouTubeVideoQueryOperation *) queryVideo:(XCDYouTubeVideo *)video streamURLsToQuery:(NSDictionary<id, NSURL *> * __nullable)streamURLsToQuery options:(NSDictionary * __nullable)options cookies:(nullable NSArray <NSHTTPCookie *>*)cookies completionHandler:(void (^)(NSDictionary *__nullable streamURLs, NSError * __nullable error, NSDictionary<id, NSError *> *__nullable streamErrors))completionHandler;


@end

Expand Down
9 changes: 7 additions & 2 deletions XCDYouTubeKit/XCDYouTubeClient.m
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,17 @@ - (instancetype) initWithLanguageIdentifier:(NSString *)languageIdentifier
return [self getVideoWithIdentifier:videoIdentifier cookies:cookies customPatterns:nil completionHandler:completionHandler];
}

- (XCDYouTubeVideoQueryOperation *)queryVideo:(XCDYouTubeVideo *)video cookies:(NSArray<NSHTTPCookie *> *)cookies completionHandler:(void (^)(NSDictionary * _Nullable, NSError * _Nullable, NSDictionary<id,NSError *> * _Nullable))completionHandler
- (XCDYouTubeVideoQueryOperation *)queryVideo:(XCDYouTubeVideo *)video cookies:(NSArray<NSHTTPCookie *> *)cookies completionHandler:(void (^)(NSDictionary * _Nonnull, NSError * _Nullable, NSDictionary<id,NSError *> * _Nonnull))completionHandler
{
return [self queryVideo:video streamURLsToQuery:nil options:nil cookies:cookies completionHandler:completionHandler];
}

- (XCDYouTubeVideoQueryOperation *)queryVideo:(XCDYouTubeVideo *)video streamURLsToQuery:(NSDictionary<id,NSURL *> *)streamURLsToQuery options:(NSDictionary *)options cookies:(NSArray<NSHTTPCookie *> *)cookies completionHandler:(void (^)(NSDictionary * _Nullable, NSError * _Nullable, NSDictionary<id,NSError *> * _Nullable))completionHandler
{
if (!completionHandler)
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"The `completionHandler` argument must not be nil." userInfo:nil];

XCDYouTubeVideoQueryOperation *operation = [[XCDYouTubeVideoQueryOperation alloc]initWithVideo:video cookies:cookies];
XCDYouTubeVideoQueryOperation *operation = [[XCDYouTubeVideoQueryOperation alloc]initWithVideo:video streamURLsToQuery:streamURLsToQuery options:options cookies:cookies];
operation.completionBlock = ^{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
#pragma clang diagnostic push
Expand Down
11 changes: 10 additions & 1 deletion XCDYouTubeKit/XCDYouTubeVideoQueryOperation.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,20 @@ NS_ASSUME_NONNULL_BEGIN
/// Initializes a video query operation with the specified video and cookies.
/// @param video The `<XCDYouTubeVideo>` object that this operation will query. Passing a `nil` video will throw an `NSInvalidArgumentException` exception.
/// @param cookies An array of `NSHTTPCookie` objects, can be nil. These cookies can be used for certain videos that require a login.
- (instancetype) initWithVideo:(XCDYouTubeVideo *)video cookies:(nullable NSArray<NSHTTPCookie *> *)cookies NS_DESIGNATED_INITIALIZER;
- (instancetype) initWithVideo:(XCDYouTubeVideo *)video cookies:(nullable NSArray<NSHTTPCookie *> *)cookies;

/// Initializes a video query operation with the specified video, stream URLs to query and cookies.
/// @param video The `<XCDYouTubeVideo>` object that this operation will query. Passing a `nil` video will throw an `NSInvalidArgumentException` exception.
/// @param streamURLsToQuery The specific stream URLs to query, can be nil. These URLs and keys must be contained in the `streamURLs` property of the `video` object, if none of the values in `streamURLsToQuery` match then all of the `streamURLs` will be queried.
/// @param options Options that are reserved for future use.
/// @param cookies An array of `NSHTTPCookie` objects, can be nil. These cookies can be used for certain videos that require a login.
- (instancetype) initWithVideo:(XCDYouTubeVideo *)video streamURLsToQuery:(nullable NSDictionary<id, NSURL *>*)streamURLsToQuery options:(nullable NSDictionary *)options cookies:(nullable NSArray<NSHTTPCookie *> *)cookies NS_DESIGNATED_INITIALIZER;

/// The `video` object that the operation initialized initialized with.
@property (atomic, strong, readonly) XCDYouTubeVideo *video;

@property (atomic, strong, readonly, nullable) NSDictionary<id, NSURL *> *streamURLsToQuery;

/// The array of `NSHTTPCookie` objects passed during initialization.
@property (atomic, copy, readonly, nullable) NSArray<NSHTTPCookie *>*cookies;

Expand Down
42 changes: 36 additions & 6 deletions XCDYouTubeKit/XCDYouTubeVideoQueryOperation.m
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ @interface XCDYouTubeVideoQueryOperation ()
@property (atomic, readwrite, nullable) NSError *error;
@property (atomic, readwrite, nullable) NSDictionary<id, NSError *> *streamErrors;

@property (atomic, strong, readwrite, nullable) NSDictionary<id, NSURL *> *streamURLsToQuery;
@property (atomic, strong) NSOperationQueue *queryQueue;
@property (atomic, readonly) dispatch_semaphore_t operationStartSemaphore;
@end
Expand All @@ -35,25 +36,53 @@ - (instancetype)init
}
#pragma clang diagnostic pop

- (instancetype) initWithVideo:(XCDYouTubeVideo *)video cookies:(nullable NSArray<NSHTTPCookie *> *)cookies
- (instancetype)initWithVideo:(XCDYouTubeVideo *)video streamURLsToQuery:(NSDictionary<id,NSURL *> *)streamURLsToQuery options:(NSDictionary *)options cookies:(NSArray<NSHTTPCookie *> *)cookies
{
if (video == nil)
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"`video` must not be nil" userInfo:nil];

if (!(self = [super init]))
return nil;

_video = video;

NSMutableDictionary *streamURLsToQueryMutable = [NSMutableDictionary new];

for (id key in streamURLsToQuery)
{
//If the `video` object `streamURLs` does not contain this key we skip.
//Or, if value of the key isn't in the `video` object `streamURLs` we also skip.
if (_video.streamURLs[key] == nil || [(NSURL *)_video.streamURLs[key] isEqual:(NSURL *)streamURLsToQuery[key]] == NO)
{
continue;
}
streamURLsToQueryMutable[key] = _video.streamURLs[key];
}

if (streamURLsToQueryMutable.count == 0)
{
//No key and value matched so we disregard `streamURLs` and simply use the `streamURLs` from the `video` object
_streamURLsToQuery = _video.streamURLs;
}
else
{
_streamURLsToQuery = streamURLsToQueryMutable.copy;
}
_cookies = [cookies copy];

_queryQueue = [NSOperationQueue new];
_queryQueue.name = [NSString stringWithFormat:@"%@ Query Queue", NSStringFromClass(self.class)];
_queryQueue.maxConcurrentOperationCount = 6; // paul_irish: Chrome re-confirmed that the 6 connections-per-host limit is the right magic number: https://code.google.com/p/chromium/issues/detail?id=285567#c14 [https://twitter.com/paul_irish/status/422808635698212864]

_operationStartSemaphore = dispatch_semaphore_create(0);
return self;
}

- (instancetype) initWithVideo:(XCDYouTubeVideo *)video cookies:(nullable NSArray<NSHTTPCookie *> *)cookies
{
return [self initWithVideo:video streamURLsToQuery:nil options:nil cookies:cookies];
}

#pragma mark - NSOperation

+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key
Expand Down Expand Up @@ -104,9 +133,10 @@ - (void) startQuery
XCDYouTubeLogDebug(@"Starting query request for video: %@", self.video);

NSMutableArray <XCDURLHEADOperation *>*HEADOperations = [NSMutableArray new];
//Always use the `video` to check if it's a live stream (clients might not include `XCDYouTubeVideoQualityHTTPLiveStreaming` in `streamURLsToQuery`)
BOOL isHTTPLiveStream = self.video.streamURLs[XCDYouTubeVideoQualityHTTPLiveStreaming] != nil;

[self.video.streamURLs enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, NSURL * _Nonnull obj, BOOL * _Nonnull stop)
[self.streamURLsToQuery enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, NSURL * _Nonnull obj, BOOL * _Nonnull stop)
{

XCDURLHEADOperation *operation = [[XCDURLHEADOperation alloc]initWithURL:obj info:@{key : obj} cookes:self.cookies];
Expand Down

0 comments on commit 9ca9924

Please sign in to comment.