From 427ad41da0b7f34c2d127112ec5b290c66f0f1c0 Mon Sep 17 00:00:00 2001 From: koen Date: Thu, 21 Oct 2021 13:15:28 +0200 Subject: [PATCH 1/3] Updated repo with all fixes so far --- .../xcschemes/xcschememanagement.plist | 22 -- Package.swift | 34 +-- .../Playground.playground/Contents.swift | 30 +-- .../AVPlayerViewControllerManager.swift | 235 +++++++++--------- .../DemoFullScreenViewController.swift | 27 +- XCDYouTubeKit Demo/iOS Demo/Utilities.swift | 2 +- XCDYouTubeKit/XCDYouTubeClient.h | 3 + XCDYouTubeKit/XCDYouTubeClient.m | 11 + XCDYouTubeKit/XCDYouTubeVideo.m | 7 +- XCDYouTubeKit/XCDYouTubeVideoOperation.m | 129 +++++++++- 10 files changed, 304 insertions(+), 196 deletions(-) delete mode 100644 .swiftpm/xcode/xcuserdata/soneejohn.xcuserdatad/xcschemes/xcschememanagement.plist diff --git a/.swiftpm/xcode/xcuserdata/soneejohn.xcuserdatad/xcschemes/xcschememanagement.plist b/.swiftpm/xcode/xcuserdata/soneejohn.xcuserdatad/xcschemes/xcschememanagement.plist deleted file mode 100644 index d1054dc45..000000000 --- a/.swiftpm/xcode/xcuserdata/soneejohn.xcuserdatad/xcschemes/xcschememanagement.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - SchemeUserState - - XCDYouTubeKit.xcscheme_^#shared#^_ - - orderHint - 0 - - - SuppressBuildableAutocreation - - XCDYouTubeKit - - primary - - - - - diff --git a/Package.swift b/Package.swift index cc066ac27..6f2f7de7f 100644 --- a/Package.swift +++ b/Package.swift @@ -2,21 +2,21 @@ import PackageDescription let package = Package( - name: "XCDYouTubeKit", - products: [ - .library(name: "XCDYouTubeKit", targets: ["XCDYouTubeKit"]) - ], - targets: [ - .target( - name: "XCDYouTubeKit", - path: ".", - exclude: [ - "XCDYouTubeKit/Info.plist", - "XCDYouTubeKit/Configuration.plist", - "XCDYouTubeKit/AppledocSettings.plist" - ], - sources: ["XCDYouTubeKit"], - publicHeadersPath: "XCDYouTubeKit" - ) - ] + name: "XCDYouTubeKit", + products: [ + .library(name: "XCDYouTubeKit", targets: ["XCDYouTubeKit"]) + ], + targets: [ + .target( + name: "XCDYouTubeKit", + path: ".", + exclude: [ + "XCDYouTubeKit/Info.plist", + "XCDYouTubeKit/Configuration.plist", + "XCDYouTubeKit/AppledocSettings.plist" + ], + sources: ["XCDYouTubeKit"], + publicHeadersPath: "XCDYouTubeKit" + ) + ] ) diff --git a/XCDYouTubeKit Demo/Playground.playground/Contents.swift b/XCDYouTubeKit Demo/Playground.playground/Contents.swift index 8be03b019..c2c7535e9 100644 --- a/XCDYouTubeKit Demo/Playground.playground/Contents.swift +++ b/XCDYouTubeKit Demo/Playground.playground/Contents.swift @@ -1,19 +1,19 @@ import XCDYouTubeKit #if swift(>=3.0) - import PlaygroundSupport - struct YouTubeVideoQuality { - static let hd720 = NSNumber(value: XCDYouTubeVideoQuality.HD720.rawValue) - static let medium360 = NSNumber(value: XCDYouTubeVideoQuality.medium360.rawValue) - static let small240 = NSNumber(value: XCDYouTubeVideoQuality.small240.rawValue) - } +import PlaygroundSupport +struct YouTubeVideoQuality { + static let hd720 = NSNumber(value: XCDYouTubeVideoQuality.HD720.rawValue) + static let medium360 = NSNumber(value: XCDYouTubeVideoQuality.medium360.rawValue) + static let small240 = NSNumber(value: XCDYouTubeVideoQuality.small240.rawValue) +} #else - import XCPlayground - typealias Error = NSError - struct YouTubeVideoQuality { - static let hd720 = NSNumber(unsignedLong: XCDYouTubeVideoQuality.HD720.rawValue) - static let medium360 = NSNumber(unsignedLong: XCDYouTubeVideoQuality.Medium360.rawValue) - static let small240 = NSNumber(unsignedLong: XCDYouTubeVideoQuality.Small240.rawValue) - } +import XCPlayground +typealias Error = NSError +enum YouTubeVideoQuality { + static let hd720 = NSNumber(unsignedLong: XCDYouTubeVideoQuality.HD720.rawValue) + static let medium360 = NSNumber(unsignedLong: XCDYouTubeVideoQuality.Medium360.rawValue) + static let small240 = NSNumber(unsignedLong: XCDYouTubeVideoQuality.Small240.rawValue) +} #endif setenv("XCDYouTubeKitLogLevel", "0", 1) @@ -38,7 +38,7 @@ client.getVideoWithIdentifier("xxxxxxxxxxx") { (video: XCDYouTubeVideo?, error: } #if swift(>=3.0) - PlaygroundPage.current.needsIndefiniteExecution = true +PlaygroundPage.current.needsIndefiniteExecution = true #else - XCPlaygroundPage.currentPage.needsIndefiniteExecution = true +XCPlaygroundPage.currentPage.needsIndefiniteExecution = true #endif diff --git a/XCDYouTubeKit Demo/iOS Demo/AVPlayerViewControllerManager.swift b/XCDYouTubeKit Demo/iOS Demo/AVPlayerViewControllerManager.swift index 53eaeb18b..93cb7df48 100644 --- a/XCDYouTubeKit Demo/iOS Demo/AVPlayerViewControllerManager.swift +++ b/XCDYouTubeKit Demo/iOS Demo/AVPlayerViewControllerManager.swift @@ -6,85 +6,86 @@ // Copyright © 2019 Cédric Luthi. All rights reserved. // -import Foundation import AVKit +import Foundation import MediaPlayer extension UIViewController { - func topMostViewController() -> UIViewController { - if self.presentedViewController == nil { - return self - } - if let navigation = self.presentedViewController as? UINavigationController { - return navigation.visibleViewController!.topMostViewController() - } - if let tab = self.presentedViewController as? UITabBarController { - if let selectedTab = tab.selectedViewController { - return selectedTab.topMostViewController() - } - return tab.topMostViewController() - } - return self.presentedViewController!.topMostViewController() - } + func topMostViewController() -> UIViewController { + if presentedViewController == nil { + return self + } + if let navigation = presentedViewController as? UINavigationController { + return navigation.visibleViewController!.topMostViewController() + } + if let tab = presentedViewController as? UITabBarController { + if let selectedTab = tab.selectedViewController { + return selectedTab.topMostViewController() + } + return tab.topMostViewController() + } + return presentedViewController!.topMostViewController() + } } extension UIView { - var parentViewController: UIViewController? { - var parentResponder: UIResponder? = self - while parentResponder != nil { - parentResponder = parentResponder!.next - if let viewController = parentResponder as? UIViewController { - return viewController - } - } - return nil - } + var parentViewController: UIViewController? { + var parentResponder: UIResponder? = self + while parentResponder != nil { + parentResponder = parentResponder!.next + if let viewController = parentResponder as? UIViewController { + return viewController + } + } + return nil + } } @objcMembers class AVPlayerViewControllerManager: NSObject { - //MARK: - Public - public static let shared = AVPlayerViewControllerManager() + // MARK: - Public + + public static let shared = AVPlayerViewControllerManager() public var lowQualityMode = false public dynamic var duration: Float = 0 - + public var video: XCDYouTubeVideo? { didSet { guard let video = video else { return } guard lowQualityMode == false else { guard let streamURL = video.streamURLs[XCDYouTubeVideoQualityHTTPLiveStreaming] ?? video.streamURLs[XCDYouTubeVideoQuality.medium360.rawValue] ?? video.streamURLs[XCDYouTubeVideoQuality.small240.rawValue] else { fatalError("No stream URL") } - - self.player = AVPlayer(url: streamURL) - self.controller.player = self.player + + player = AVPlayer(url: streamURL) + controller.player = player return } - guard let streamURL = video.streamURL else { fatalError("No stream URL")} - self.player = AVPlayer(url: streamURL) - self.controller.player = self.player + guard let streamURL = video.streamURL else { fatalError("No stream URL") } + player = AVPlayer(url: streamURL) + controller.player = player } } - public var player: AVPlayer? { - didSet { + public var player: AVPlayer? { + didSet { if let playerRateObserverToken = playerRateObserverToken { playerRateObserverToken.invalidate() self.playerRateObserverToken = nil } - - self.playerRateObserverToken = player?.observe(\.rate, changeHandler: { (item, value) in - self.updatePlaybackRateMetadata() - }) - + + playerRateObserverToken = player?.observe(\.rate, changeHandler: { _, _ in + self.updatePlaybackRateMetadata() + }) + guard let video = self.video else { return } if let token = timeObserverToken { oldValue?.removeTimeObserver(token) timeObserverToken = nil } - self.setupRemoteTransportControls() - self.updateGeneralMetadata(video: video) - self.updatePlaybackDuration() - } - } - + setupRemoteTransportControls() + updateGeneralMetadata(video: video) + updatePlaybackDuration() + } + } + public lazy var controller: AVPlayerViewController = { let controller = AVPlayerViewController() if #available(iOS 10.0, *) { @@ -92,22 +93,22 @@ extension UIView { } return controller }() - + override init() { super.init() - - NotificationCenter.default.addObserver(forName: AVAudioSession.interruptionNotification, object: AVAudioSession.sharedInstance(), queue: .main) { (notification) in - + + NotificationCenter.default.addObserver(forName: AVAudioSession.interruptionNotification, object: AVAudioSession.sharedInstance(), queue: .main) { notification in + guard let userInfo = notification.userInfo, - let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt, - let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { - return + let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt, + let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { + return } - + if type == .began { self.player?.pause() } else if type == .ended { - guard ((try? AVAudioSession.sharedInstance().setActive(true)) != nil) else { return } + guard (try? AVAudioSession.sharedInstance().setActive(true)) != nil else { return } guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return } let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue) guard options.contains(.shouldResume) else { return } @@ -115,84 +116,84 @@ extension UIView { } } } - + public func disconnectPlayer() { - self.controller.player = nil - } - + controller.player = nil + } + public func reconnectPlayer(rootViewController: UIViewController) { let viewController = rootViewController.topMostViewController() guard let playerViewController = viewController as? AVPlayerViewController else { if rootViewController is UINavigationController { guard let vc = (rootViewController as! UINavigationController).visibleViewController else { return } - for childVC in vc.children { + for childVC in vc.children { guard let playerViewController = childVC as? AVPlayerViewController else { continue } - playerViewController.player = self.player + playerViewController.player = player break } } return } - playerViewController.player = self.player + playerViewController.player = player } - - //MARK: Private - + + // MARK: Private + fileprivate var playerRateObserverToken: NSKeyValueObservation? fileprivate var timeObserverToken: Any? - fileprivate let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default() - - fileprivate func setupRemoteTransportControls() { - let commandCenter = MPRemoteCommandCenter.shared() - commandCenter.playCommand.addTarget { [unowned self] event in - if self.player?.rate == 0.0 { - self.player?.play() - return .success - } - return .commandFailed - } - - commandCenter.pauseCommand.addTarget { event in - if self.player?.rate == 1.0 { - self.player?.pause() - return .success - } - return .commandFailed - } - } - + fileprivate let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default() + + fileprivate func setupRemoteTransportControls() { + let commandCenter = MPRemoteCommandCenter.shared() + commandCenter.playCommand.addTarget { [unowned self] _ in + if self.player?.rate == 0.0 { + self.player?.play() + return .success + } + return .commandFailed + } + + commandCenter.pauseCommand.addTarget { _ in + if self.player?.rate == 1.0 { + self.player?.pause() + return .success + } + return .commandFailed + } + } + fileprivate func updateGeneralMetadata(video: XCDYouTubeVideo) { - guard player?.currentItem != nil else { - nowPlayingInfoCenter.nowPlayingInfo = nil - return - } - - var nowPlayingInfo = nowPlayingInfoCenter.nowPlayingInfo ?? [String: Any]() + guard player?.currentItem != nil else { + nowPlayingInfoCenter.nowPlayingInfo = nil + return + } + + var nowPlayingInfo = nowPlayingInfoCenter.nowPlayingInfo ?? [String: Any]() let title = video.title - + if let thumbnailURL = video.thumbnailURL { - URLSession.shared.dataTask(with: thumbnailURL) { (data, _, error) in + URLSession.shared.dataTask(with: thumbnailURL) { data, _, error in guard error == nil else { return } guard data != nil else { return } guard let image = UIImage(data: data!) else { return } - + let artwork = MPMediaItemArtwork(image: image) nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork self.nowPlayingInfoCenter.nowPlayingInfo = nowPlayingInfo }.resume() } - - nowPlayingInfo[MPMediaItemPropertyTitle] = title - nowPlayingInfoCenter.nowPlayingInfo = nowPlayingInfo - } - + + nowPlayingInfo[MPMediaItemPropertyTitle] = title + nowPlayingInfoCenter.nowPlayingInfo = nowPlayingInfo + } + fileprivate func updatePlaybackDuration() { let interval = CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) - - timeObserverToken = self.player?.addPeriodicTimeObserver(forInterval: interval, queue: .main, using: { [weak self] (time) in + + timeObserverToken = player?.addPeriodicTimeObserver(forInterval: interval, queue: .main, using: { [weak self] _ in guard let player = self?.player else { return } guard player.currentItem != nil else { return } - + var nowPlayingInfo = self!.nowPlayingInfoCenter.nowPlayingInfo ?? [String: Any]() self!.duration = Float(CMTimeGetSeconds(player.currentItem!.duration)) nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = self!.duration @@ -200,16 +201,16 @@ extension UIView { self!.nowPlayingInfoCenter.nowPlayingInfo = nowPlayingInfo }) } - - fileprivate func updatePlaybackRateMetadata() { - guard player?.currentItem != nil else { - duration = 0 - nowPlayingInfoCenter.nowPlayingInfo = nil - return - } - - var nowPlayingInfo = nowPlayingInfoCenter.nowPlayingInfo ?? [String: Any]() - nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = player!.rate - nowPlayingInfo[MPNowPlayingInfoPropertyDefaultPlaybackRate] = player!.rate - } + + fileprivate func updatePlaybackRateMetadata() { + guard player?.currentItem != nil else { + duration = 0 + nowPlayingInfoCenter.nowPlayingInfo = nil + return + } + + var nowPlayingInfo = nowPlayingInfoCenter.nowPlayingInfo ?? [String: Any]() + nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = player!.rate + nowPlayingInfo[MPNowPlayingInfoPropertyDefaultPlaybackRate] = player!.rate + } } diff --git a/XCDYouTubeKit Demo/iOS Demo/DemoFullScreenViewController.swift b/XCDYouTubeKit Demo/iOS Demo/DemoFullScreenViewController.swift index 0a42b55b9..5f9c64c4c 100644 --- a/XCDYouTubeKit Demo/iOS Demo/DemoFullScreenViewController.swift +++ b/XCDYouTubeKit Demo/iOS Demo/DemoFullScreenViewController.swift @@ -6,46 +6,45 @@ // Copyright © 2019 Cédric Luthi. All rights reserved. // -import UIKit import AVKit +import UIKit import XCDYouTubeKit extension DemoFullScreenViewController: VideoPickerControllerDelegate { func videoPickerController(_ videoPickerController: VideoPickerController!, didSelectVideoWithIdentifier videoIdentifier: String!) { - self.videoIdentifierTextField.text = videoIdentifier + videoIdentifierTextField.text = videoIdentifier UserDefaults.standard.set(videoIdentifier, forKey: "VideoIdentifier") } } extension DemoFullScreenViewController: UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { - self.play(textField) + play(textField) return true } - + func textFieldDidEndEditing(_ textField: UITextField) { - UserDefaults.standard.set(self.videoIdentifierTextField.text, forKey: "VideoIdentifier") + UserDefaults.standard.set(videoIdentifierTextField.text, forKey: "VideoIdentifier") } } class DemoFullScreenViewController: UIViewController { - - @IBOutlet weak open var lowQualitySwitch: UISwitch! - @IBOutlet weak open var videoIdentifierTextField: UITextField! + @IBOutlet open var lowQualitySwitch: UISwitch! + @IBOutlet open var videoIdentifierTextField: UITextField! var ob: NSKeyValueObservation? private var timeObserverToken: Any? - + override func viewDidLoad() { super.viewDidLoad() - self.videoIdentifierTextField.text = UserDefaults.standard.string(forKey: "VideoIdentifier") + videoIdentifierTextField.text = UserDefaults.standard.string(forKey: "VideoIdentifier") } - + @IBAction open func endEditing(_ sender: Any!) { - self.view.endEditing(true) + view.endEditing(true) } - + @IBAction open func play(_ sender: Any!) { - XCDYouTubeClient.default().getVideoWithIdentifier(self.videoIdentifierTextField.text) { (video, error) in + XCDYouTubeClient.default().getVideoWithIdentifier(videoIdentifierTextField.text) { video, error in guard error == nil else { Utilities.shared.displayError(error! as NSError, originViewController: self) return diff --git a/XCDYouTubeKit Demo/iOS Demo/Utilities.swift b/XCDYouTubeKit Demo/iOS Demo/Utilities.swift index a43939296..7a9bc398f 100644 --- a/XCDYouTubeKit Demo/iOS Demo/Utilities.swift +++ b/XCDYouTubeKit Demo/iOS Demo/Utilities.swift @@ -10,7 +10,7 @@ import UIKit @objcMembers class Utilities: NSObject { static let shared = Utilities() - + func displayError(_ error: NSError, originViewController: UIViewController) { OperationQueue.main.addOperation { originViewController.dismiss(animated: true) { diff --git a/XCDYouTubeKit/XCDYouTubeClient.h b/XCDYouTubeKit/XCDYouTubeClient.h index 573a97155..1fba33cab 100644 --- a/XCDYouTubeKit/XCDYouTubeClient.h +++ b/XCDYouTubeKit/XCDYouTubeClient.h @@ -25,6 +25,9 @@ NS_ASSUME_NONNULL_BEGIN */ @interface XCDYouTubeClient : NSObject ++ (NSString *)innertubeApiKey; ++ (void)setInnertubeApiKey:(NSString *)key; + /** * ------------------ * @name Initializing diff --git a/XCDYouTubeKit/XCDYouTubeClient.m b/XCDYouTubeKit/XCDYouTubeClient.m index 5305d1aea..2ae8e2bd4 100644 --- a/XCDYouTubeKit/XCDYouTubeClient.m +++ b/XCDYouTubeKit/XCDYouTubeClient.m @@ -14,6 +14,8 @@ @implementation XCDYouTubeClient @synthesize languageIdentifier = _languageIdentifier; +static NSString * _innertubeApiKey = @"AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"; + + (instancetype) defaultClient { static XCDYouTubeClient *defaultClient; @@ -29,6 +31,15 @@ - (instancetype) init return [self initWithLanguageIdentifier:nil]; } + ++ (NSString *)innertubeApiKey { + return _innertubeApiKey; +} + ++ (void)setInnertubeApiKey:(NSString *)key { + _innertubeApiKey = key; +} + - (instancetype) initWithLanguageIdentifier:(NSString *)languageIdentifier { if (!(self = [super init])) diff --git a/XCDYouTubeKit/XCDYouTubeVideo.m b/XCDYouTubeKit/XCDYouTubeVideo.m index 637fafe20..84a86ff11 100644 --- a/XCDYouTubeKit/XCDYouTubeVideo.m +++ b/XCDYouTubeKit/XCDYouTubeVideo.m @@ -171,6 +171,9 @@ - (instancetype) initWithIdentifier:(NSString *)identifier info:(NSDictionary *) NSString *streamMap = info[@"url_encoded_fmt_stream_map"]; NSArray *alternativeStreamMap = XCDStreamingDataWithString(playerResponse)[@"formats"] == nil ? info[@"streamingData"][@"formats"] : XCDStreamingDataWithString(playerResponse)[@"formats"]; NSString *httpLiveStream = info[@"hlsvp"] ?: XCDHTTPLiveStreamingStringWithString(playerResponse); + if(httpLiveStream.length == 0){ + httpLiveStream = info[@"streamingData"][@"hlsManifestUrl"]; + } NSString *adaptiveFormats = info[@"adaptive_fmts"]; NSArray *alternativeAdaptiveFormats = XCDStreamingDataWithString(playerResponse)[@"adaptiveFormats"] == nil ? info[@"streamingData"][@"adaptiveFormats"] : XCDStreamingDataWithString(playerResponse)[@"adaptiveFormats"]; NSDictionary *videoDetails = XCDDictionaryWithString(playerResponse)[@"videoDetails"] == nil ? info[@"videoDetails"] : XCDDictionaryWithString(playerResponse)[@"videoDetails"]; @@ -251,8 +254,10 @@ - (instancetype) initWithIdentifier:(NSString *)identifier info:(NSDictionary *) NSMutableDictionary *streamURLs = [NSMutableDictionary new]; - if (httpLiveStream) + if (httpLiveStream != nil) { + _streamURL = [NSURL URLWithString:httpLiveStream]; streamURLs[XCDYouTubeVideoQualityHTTPLiveStreaming] = [NSURL URLWithString:httpLiveStream]; + } NSMutableDictionary *captionURLs = [NSMutableDictionary new]; NSMutableDictionary *autoGeneratedCaptionURLs = [NSMutableDictionary new]; diff --git a/XCDYouTubeKit/XCDYouTubeVideoOperation.m b/XCDYouTubeKit/XCDYouTubeVideoOperation.m index be4ce9ee1..dbaa8c1b6 100644 --- a/XCDYouTubeKit/XCDYouTubeVideoOperation.m +++ b/XCDYouTubeKit/XCDYouTubeVideoOperation.m @@ -12,6 +12,7 @@ #import "XCDYouTubeDashManifestXML.h" #import "XCDYouTubePlayerScript.h" #import "XCDYouTubeLogger+Private.h" +#import "XCDYouTubeClient.h" typedef NS_ENUM(NSUInteger, XCDYouTubeRequestType) { XCDYouTubeRequestTypeGetVideoInfo = 1, @@ -35,6 +36,8 @@ @interface XCDYouTubeVideoOperation () @property (atomic, readonly) NSURLSession *session; @property (atomic, strong) NSURLSessionDataTask *dataTask; ++(NSDateFormatter*) dateFormatter; + @property (atomic, assign) BOOL isExecuting; @property (atomic, assign) BOOL isFinished; @property (atomic, readonly) dispatch_semaphore_t operationStartSemaphore; @@ -146,19 +149,30 @@ - (void) startNextRequest } else { - NSString *eventLabel = [self.eventLabels objectAtIndex:0]; [self.eventLabels removeObjectAtIndex:0]; - NSDictionary *query = @{ @"video_id": self.videoIdentifier, @"hl": self.languageIdentifier, @"el": eventLabel, @"ps": @"default" }; - NSString *queryString = XCDQueryStringWithDictionary(query); - NSURL *videoInfoURL = [NSURL URLWithString:[@"https://www.youtube.com/get_video_info?" stringByAppendingString:queryString]]; - [self startRequestWithURL:videoInfoURL type:XCDYouTubeRequestTypeGetVideoInfo]; + NSString *urlString = [NSString stringWithFormat:@"https://www.youtube.com/youtubei/v1/player?key=%@", XCDYouTubeClient.innertubeApiKey]; + NSURL *url = [NSURL URLWithString:urlString]; + + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10]; + + [request setHTTPMethod:@"POST"]; + + NSString *string = [NSString stringWithFormat:@"{'context': {'client': {'hl': 'en','clientName': 'ANDROID','clientVersion': '16.20','playbackContext': {'contentPlaybackContext': {'html5Preference': 'HTML5_PREF_WANTS'}}}},'contentCheckOk': true,'racyCheckOk': true,'videoId': '%@'}", self.videoIdentifier]; + + NSData *postData = [string dataUsingEncoding:NSASCIIStringEncoding]; + + [request setHTTPBody:postData]; + + [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; + + [self startRequestWith:request type:XCDYouTubeRequestTypeGetVideoInfo]; } } - (void) startWatchPageRequest { - NSDictionary *query = @{ @"v": self.videoIdentifier, @"hl": self.languageIdentifier, @"has_verified": @YES, @"bpctr": @9999999999 }; + NSDictionary *query = @{ @"v": self.videoIdentifier, @"hl": self.languageIdentifier, @"has_verified": @YES, @"bpctr": @9999999999, @"c": @"ANDROID" }; NSString *queryString = XCDQueryStringWithDictionary(query); NSURL *webpageURL = [NSURL URLWithString:[@"https://www.youtube.com/watch?" stringByAppendingString:queryString]]; [self startRequestWithURL:webpageURL type:XCDYouTubeRequestTypeWatchPage]; @@ -206,6 +220,39 @@ - (void) startRequestWithURL:(NSURL *)url type:(XCDYouTubeRequestType)requestTyp self.requestType = requestType; } +- (void) startRequestWith:(NSMutableURLRequest *)request type:(XCDYouTubeRequestType)requestType +{ + if (self.isCancelled) + return; + + // Max (age-restricted VEVO) = 2×GetVideoInfo + 1×WatchPage + 2×EmbedPage + 1×JavaScriptPlayer + 1×GetVideoInfo + 1xDashManifest + if (++self.requestCount > 8) + { + // This condition should never happen but the request flow is quite complex so better abort here than go into an infinite loop of requests + [self finishWithError]; + return; + } + + XCDYouTubeLogDebug(@"Starting request: %@", [request URL]); + + [request setValue:self.languageIdentifier forHTTPHeaderField:@"Accept-Language"]; + [request setValue:[NSString stringWithFormat:@"https://youtube.com/watch?v=%@", self.videoIdentifier] forHTTPHeaderField:@"Referer"]; + + self.dataTask = [self.session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) + { + if (self.isCancelled) + return; + + if (error) + [self handleConnectionError:error requestType:requestType]; + else + [self handleConnectionSuccessWithData:data response:response requestType:requestType]; + }]; + [self.dataTask resume]; + + self.requestType = requestType; +} + #pragma mark - Response Dispatch - (void) handleConnectionSuccessWithData:(NSData *)data response:(NSURLResponse *)response requestType:(XCDYouTubeRequestType)requestType @@ -222,6 +269,11 @@ - (void) handleConnectionSuccessWithData:(NSData *)data response:(NSURLResponse [self handleConnectionError:[NSError errorWithDomain:XCDYouTubeVideoErrorDomain code:XCDYouTubeErrorTooManyRequests userInfo:@{NSLocalizedDescriptionKey : @"The operation couldn’t be completed because too many requests were sent."}] requestType:requestType]; return; } + if ([(NSHTTPURLResponse *)response statusCode] == 404 && responseString.length == 0) + { + [self handleConnectionError:[NSError errorWithDomain:XCDYouTubeVideoErrorDomain code:XCDYouTubeErrorEmptyResponse userInfo:@{NSLocalizedDescriptionKey : @"The response is empty."}] requestType:requestType]; + return; + } if (responseString.length == 0) { //Previously we would throw an assertion here, however, this has been changed to an error @@ -235,7 +287,7 @@ - (void) handleConnectionSuccessWithData:(NSData *)data response:(NSURLResponse switch (requestType) { case XCDYouTubeRequestTypeGetVideoInfo: - [self handleVideoInfoResponseWithInfo:XCDDictionaryWithQueryString(responseString) response:response]; + [self handleVideoInfoResponseWithInfo:XCDDictionaryWithString(responseString) response:response]; break; case XCDYouTubeRequestTypeWatchPage: [self handleWebPageWithHTMLString:responseString]; @@ -270,10 +322,69 @@ - (void) handleConnectionError:(NSError *)connectionError requestType:(XCDYouTub #pragma mark - Response Parsing +- (void) initializeConsentWithResponse:(NSURLResponse *)response { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + if (httpResponse && response.URL) { + NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:httpResponse.allHeaderFields forURL:(NSURL *_Nonnull)response.URL]; + + for (NSHTTPCookie *cookie in cookies) { + if ([cookie.name isEqualToString:@"__Secure-3PSID"]) return; + } + + for (NSHTTPCookie *cookie in cookies) { + if ([cookie.name isEqualToString:@"CONSENT"]) { + if ([cookie.value isEqualToString:@"YES"]) return; + + NSString *rawConsentID = [cookie.value stringByReplacingOccurrencesOfString:@"PENDING+" withString:@""]; + int consentID = [rawConsentID intValue]; + + // generate random consent id, if doesn't match expected format + if (consentID < 100 || consentID > 999) { + consentID = 100 + (int)arc4random_uniform((uint32_t)(999 - 100 + 1)); + } + + NSString *cookieValue = [[NSString alloc] initWithFormat:@"YES+cb.%@-17-p0.en+FX+%i", [self youtubeConsentDateString], consentID]; + NSHTTPCookie *consentCookie = [NSHTTPCookie cookieWithProperties:@{ + NSHTTPCookiePath: @"/", + NSHTTPCookieName: @"CONSENT", + NSHTTPCookieValue: cookieValue, + NSHTTPCookieDomain:@".youtube.com", + NSHTTPCookieSecure:@"TRUE" + }]; + [self.session.configuration.HTTPCookieStorage setCookie:consentCookie]; + return; + } + } + + } +} + +- (NSString *) youtubeConsentDateString { + NSDateComponents *offset = [NSDateComponents new]; + [offset setDay: -1]; + NSDate *yesterday = [NSCalendar.currentCalendar dateByAddingComponents:offset toDate:[NSDate new] options:0]; + return [XCDYouTubeVideoOperation.dateFormatter stringFromDate: yesterday]; +} + ++ (NSDateFormatter*) dateFormatter { + static NSDateFormatter *formatter = nil; + + static dispatch_once_t oncePredicate; + + dispatch_once(&oncePredicate, ^{ + formatter = [NSDateFormatter new]; + [formatter setDateFormat:@"yyyyMMdd"]; + }); + + return formatter; +} + - (void) handleVideoInfoResponseWithInfo:(NSDictionary *)info response:(NSURLResponse *)response { XCDYouTubeLogDebug(@"Handling video info response"); + [self initializeConsentWithResponse:response]; + NSError *error = nil; XCDYouTubeVideo *video = [[XCDYouTubeVideo alloc] initWithIdentifier:self.videoIdentifier info:info playerScript:self.playerScript response:response error:&error]; if (video) @@ -358,7 +469,7 @@ - (void) handleJavaScriptPlayerWithScript:(NSString *)script { NSString *eurl = [@"https://youtube.googleapis.com/v/" stringByAppendingString:self.videoIdentifier]; NSString *sts = self.embedWebpage.sts ?: self.webpage.sts ?: @""; - NSDictionary *query = @{ @"video_id": self.videoIdentifier, @"hl": self.languageIdentifier, @"eurl": eurl, @"sts": sts}; + NSDictionary *query = @{ @"video_id": self.videoIdentifier, @"hl": self.languageIdentifier, @"eurl": eurl, @"sts": sts, @"html5" : @"1", @"c": @"ANDROID", @"cver": @"16.05.7"}; NSString *queryString = XCDQueryStringWithDictionary(query); NSURL *videoInfoURL = [NSURL URLWithString:[@"https://www.youtube.com/get_video_info?" stringByAppendingString:queryString]]; [self startRequestWithURL:videoInfoURL type:XCDYouTubeRequestTypeGetVideoInfo]; @@ -439,7 +550,7 @@ - (void) start self.isExecuting = YES; - self.eventLabels = [[NSMutableArray alloc] initWithArray:@[ @"embedded", @"detailpage" ]]; + self.eventLabels = [[NSMutableArray alloc] initWithArray:@[ @"embedded", @"detailpage", @"embedded" ]]; [self startNextRequest]; } From a35b5fe91c2f6ea8d1ec1e0d2abba18fb8615e9e Mon Sep 17 00:00:00 2001 From: koen Date: Tue, 16 Nov 2021 10:00:48 +0100 Subject: [PATCH 2/3] Added fix for thumbnails Thanks @kunalsood --- XCDYouTubeKit/XCDYouTubeVideo.m | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/XCDYouTubeKit/XCDYouTubeVideo.m b/XCDYouTubeKit/XCDYouTubeVideo.m index 84a86ff11..761ed770f 100644 --- a/XCDYouTubeKit/XCDYouTubeVideo.m +++ b/XCDYouTubeKit/XCDYouTubeVideo.m @@ -223,9 +223,18 @@ - (instancetype) initWithIdentifier:(NSString *)identifier info:(NSDictionary *) NSString *thumbnail = info[@"thumbnail_url"] ?: info[@"iurl"]; NSURL *thumbnailURL = thumbnail ? [NSURL URLWithString:thumbnail] : nil; _thumbnailURL = thumbnailURL; - + if (!_thumbnailURL) { NSArray *thumbnails = XCDThumnailArrayWithString(playerResponse); + if (!thumbnails) { + NSDictionary *thumbnailDictionary = videoDetails[@"thumbnail"]; + if (thumbnailDictionary && [thumbnailDictionary isKindOfClass:[NSDictionary class]]) { + NSArray *thumbnailArray = thumbnailDictionary[@"thumbnails"]; + if (thumbnailArray && [thumbnailArray isKindOfClass:[NSArray class]]) { + thumbnails = thumbnailArray; + } + } + } if (thumbnails.count >= 1) { // Prepare array of thumbnails URLs. NSMutableArray *thumbnailURLs = [[NSMutableArray alloc] initWithCapacity:thumbnails.count]; From f2b1b685c375edca711cc2b229f6bf2bb4588611 Mon Sep 17 00:00:00 2001 From: koen Date: Tue, 15 Mar 2022 14:46:08 +0100 Subject: [PATCH 3/3] trivial podspec --- ...ubeKit.podspec => XCDYouTubeKit-kbexdev.podspec | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) rename XCDYouTubeKit.podspec => XCDYouTubeKit-kbexdev.podspec (65%) diff --git a/XCDYouTubeKit.podspec b/XCDYouTubeKit-kbexdev.podspec similarity index 65% rename from XCDYouTubeKit.podspec rename to XCDYouTubeKit-kbexdev.podspec index 01e9a26d3..d10e1ca9e 100644 --- a/XCDYouTubeKit.podspec +++ b/XCDYouTubeKit-kbexdev.podspec @@ -1,13 +1,13 @@ Pod::Spec.new do |s| - s.name = "XCDYouTubeKit" - s.version = "2.15.2" - s.summary = "YouTube video player for iOS and OS X." - s.homepage = "https://github.com/0xced/XCDYouTubeKit" + s.name = "XCDYouTubeKit-kbexdev" + s.version = "2.16.0" + s.summary = "Fork of YouTube video player for iOS and OS X." + s.homepage = "https://github.com/kbex-dev/XCDYouTubeKit" s.screenshot = "https://raw.github.com/0xced/XCDYouTubeKit/#{s.version}/Screenshots/XCDYouTubeVideoPlayerViewController.png" s.license = { :type => "MIT", :file => "LICENSE" } - s.author = { "Cédric Luthi" => "cedric.luthi@gmail.com" } - s.social_media_url = "https://twitter.com/0xced" - s.source = { :git => "https://github.com/0xced/XCDYouTubeKit.git", :tag => s.version.to_s } + s.author = { "kbexdev" => "kbexdev@gmail.com" } + s.social_media_url = "" + s.source = { :git => "https://github.com/kbex-dev/XCDYouTubeKit.git", :tag => s.version.to_s } s.ios.deployment_target = "8.0" s.osx.deployment_target = "10.9" s.tvos.deployment_target = "9.0"