├── .gitignore ├── Project └── youtube-player-ios-example │ ├── youtube-player-ios-example │ ├── en.lproj │ │ └── InfoPlist.strings │ ├── youtube-player-ios-example-Prefix.pch │ ├── Images.xcassets │ │ ├── LaunchImage.launchimage │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── AppDelegate.h │ ├── main.m │ ├── AppDelegate.m │ ├── PlaylistViewController.h │ ├── youtube-player-ios-example-Info.plist │ ├── SingleVideoViewController.h │ ├── PlaylistViewController.m │ ├── SingleVideoViewController.m │ └── Main.storyboard │ ├── youtube-player-ios-exampleTests │ ├── en.lproj │ │ └── InfoPlist.strings │ ├── youtube-player-ios-exampleTests-Info.plist │ └── youtube_player_ios_exampleTests.m │ ├── .gitignore │ ├── Podfile │ └── youtube-player-ios-example.xcodeproj │ └── project.pbxproj ├── LICENSE ├── Package.swift ├── CHANGELOG.md ├── youtube-ios-player-helper ├── YouTubeiOSPlayerHelper.h └── Info.plist ├── README.md ├── CONTRIBUTING.md ├── youtube-ios-player-helper.podspec ├── Sources ├── Assets │ └── YTPlayerView-iframe-player.html ├── YTPlayerView.h └── YTPlayerView.m ├── youtube-ios-player-helper.xcodeproj ├── xcshareddata │ └── xcschemes │ │ └── youtube-ios-player-helper.xcscheme └── project.pbxproj └── Rakefile /.gitignore: -------------------------------------------------------------------------------- 1 | # iOS 2 | .DS_Store 3 | Pods/ 4 | Podfile.lock 5 | xcuserdata/ 6 | *.xcworkspace/ 7 | -------------------------------------------------------------------------------- /Project/youtube-player-ios-example/youtube-player-ios-example/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /Project/youtube-player-ios-example/youtube-player-ios-exampleTests/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /Project/youtube-player-ios-example/.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | 22 | # CocoaPods 23 | Pods -------------------------------------------------------------------------------- /Project/youtube-player-ios-example/youtube-player-ios-example/youtube-player-ios-example-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header 3 | // 4 | // The contents of this file are implicitly included at the beginning of every source file. 5 | // 6 | 7 | #import 8 | 9 | #ifndef __IPHONE_5_0 10 | #warning "This project uses features only available in iOS SDK 5.0 and later." 11 | #endif 12 | 13 | #ifdef __OBJC__ 14 | #import 15 | #import 16 | #endif 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 Google Inc. All rights reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /Project/youtube-player-ios-example/youtube-player-ios-example/Images.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "portrait", 5 | "idiom" : "iphone", 6 | "extent" : "full-screen", 7 | "minimum-system-version" : "7.0", 8 | "scale" : "2x" 9 | }, 10 | { 11 | "orientation" : "portrait", 12 | "idiom" : "iphone", 13 | "subtype" : "retina4", 14 | "extent" : "full-screen", 15 | "minimum-system-version" : "7.0", 16 | "scale" : "2x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "YouTubeiOSPlayerHelper", 7 | platforms: [ 8 | .iOS(.v10) 9 | ], 10 | products: [ 11 | .library( 12 | name: "YouTubeiOSPlayerHelper", 13 | targets: ["YouTubeiOSPlayerHelper"] 14 | ) 15 | ], 16 | targets: [ 17 | .target( 18 | name: "YouTubeiOSPlayerHelper", 19 | path: "Sources", 20 | resources: [ 21 | .process("Assets") 22 | ], 23 | publicHeadersPath: "." 24 | ) 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # YouTube-Player-iOS-Helper CHANGELOG 16 | 17 | ## 0.1.0 18 | 19 | Initial release. 20 | -------------------------------------------------------------------------------- /youtube-ios-player-helper/YouTubeiOSPlayerHelper.h: -------------------------------------------------------------------------------- 1 | // 2 | // youtube-ios-player-helper.h 3 | // youtube-ios-player-helper 4 | // 5 | // Created by Niels van Hoorn on 02/09/15. 6 | // Copyright © 2015 YouTube Developer Relations. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for youtube-ios-player-helper. 12 | FOUNDATION_EXPORT double youtube_ios_player_helperVersionNumber; 13 | 14 | //! Project version string for youtube-ios-player-helper. 15 | FOUNDATION_EXPORT const unsigned char youtube_ios_player_helperVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | #import "YTPlayerView.h" -------------------------------------------------------------------------------- /Project/youtube-player-ios-example/youtube-player-ios-example/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #import 16 | 17 | @interface AppDelegate : UIResponder 18 | 19 | @end -------------------------------------------------------------------------------- /Project/youtube-player-ios-example/youtube-player-ios-exampleTests/youtube-player-ios-exampleTests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /youtube-ios-player-helper/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Project/youtube-player-ios-example/youtube-player-ios-example/main.m: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #import 16 | 17 | #import "AppDelegate.h" 18 | 19 | int main(int argc, char* argv[]) { 20 | @autoreleasepool { 21 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 22 | } 23 | } -------------------------------------------------------------------------------- /Project/youtube-player-ios-example/youtube-player-ios-example/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #import "AppDelegate.h" 16 | 17 | @implementation AppDelegate 18 | 19 | @synthesize window; 20 | 21 | - (BOOL)application:(UIApplication *)application 22 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 23 | return YES; 24 | } 25 | 26 | @end -------------------------------------------------------------------------------- /Project/youtube-player-ios-example/Podfile: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http:#www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | platform :ios, '11.0' 16 | inhibit_all_warnings! 17 | use_frameworks! 18 | 19 | target "youtube-player-ios-example" do 20 | pod "youtube-ios-player-helper", :path => "../../youtube-ios-player-helper.podspec" 21 | 22 | target "youtube-player-ios-exampleTests" do 23 | pod "OCMock", "3.7.1" 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /Project/youtube-player-ios-example/youtube-player-ios-example/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "scale" : "1x", 46 | "size" : "1024x1024" 47 | } 48 | ], 49 | "info" : { 50 | "author" : "xcode", 51 | "version" : 1 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Project/youtube-player-ios-example/youtube-player-ios-example/PlaylistViewController.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #import 16 | 17 | @interface PlaylistViewController : UIViewController 18 | 19 | @property(nonatomic, strong) IBOutlet YTPlayerView *playerView; 20 | @property(nonatomic, weak) IBOutlet UIButton *playButton; 21 | @property(nonatomic, weak) IBOutlet UIButton *pauseButton; 22 | @property(nonatomic, weak) IBOutlet UIButton *stopButton; 23 | @property(nonatomic, weak) IBOutlet UIButton *nextVideoButton; 24 | @property(nonatomic, weak) IBOutlet UIButton *previousVideoButton; 25 | @property(nonatomic, weak) IBOutlet UITextView *statusTextView; 26 | 27 | - (IBAction)buttonPressed:(id)sender; 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /Project/youtube-player-ios-example/youtube-player-ios-example/youtube-player-ios-example-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ${PRODUCT_NAME} 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1.0 25 | LSRequiresIPhoneOS 26 | 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Project/youtube-player-ios-example/youtube-player-ios-example/SingleVideoViewController.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #import 16 | 17 | #import 18 | 19 | @interface SingleVideoViewController : UIViewController 20 | 21 | @property(nonatomic, strong) IBOutlet YTPlayerView *playerView; 22 | @property(nonatomic, weak) IBOutlet UIButton *playButton; 23 | @property(nonatomic, weak) IBOutlet UIButton *pauseButton; 24 | @property(nonatomic, weak) IBOutlet UIButton *stopButton; 25 | @property(nonatomic, weak) IBOutlet UIButton *startButton; 26 | @property(nonatomic, weak) IBOutlet UIButton *reverseButton; 27 | @property(nonatomic, weak) IBOutlet UIButton *forwardButton; 28 | @property(nonatomic, weak) IBOutlet UITextView *statusTextView; 29 | 30 | @property(nonatomic, weak) IBOutlet UISlider *slider; 31 | 32 | - (IBAction)onSliderChange:(id)sender; 33 | 34 | - (IBAction)buttonPressed:(id)sender; 35 | 36 | @end 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YouTube-Player-iOS-Helper 2 | 3 | [![Version](https://cocoapod-badges.herokuapp.com/v/youtube-ios-player-helper/badge.png)](https://cocoapods.org/pods/youtube-ios-player-helper) 4 | [![Platform](https://cocoapod-badges.herokuapp.com/p/youtube-ios-player-helper/badge.png)](https://cocoapods.org/pods/youtube-ios-player-helper) 5 | 6 | ## Overview 7 | 8 | To run the example project; clone the repo, and run `pod install` from the Project directory first. For a simple tutorial see this Google Developers article - [Using the YouTube Helper Library to embed YouTube videos in your iOS application](https://developers.google.com/youtube/v3/guides/ios_youtube_helper). 9 | 10 | ## Installation 11 | ### CocoaPods 12 | YouTube-Player-iOS-Helper is available through [CocoaPods](http://cocoapods.org). To install 13 | the library, add the following line to your Podfile and replace "x.y.z" with the latest version. 14 | 15 | pod "youtube-ios-player-helper", "~> x.y.z" 16 | 17 | ### Swift Package Manager 18 | Add the following line to the dependencies in your `Package.swift`: 19 | ```swift 20 | .package("https://github.com/youtube/youtube-ios-player-helper.git", from: "x.y.z") 21 | ``` 22 | Add `YouTubeiOSPlayerHelper` to your target's dependencies. 23 | ```swift 24 | .target(name: "TargetName", 25 | dependencies: [ 26 | "YouTubeiOSPlayerHelper" 27 | ] 28 | ) 29 | ``` 30 | 31 | ## Usage 32 | After installing in your project, to use the library: 33 | 34 | 1. Drag a UIView the desired size of your player onto your Storyboard. 35 | 2. Change the UIView's class in the Identity Inspector tab to YTPlayerView 36 | 3. Import "YTPlayerView.h" in your ViewController. 37 | 4. Add the following property to your ViewController's header file: 38 | ```objc 39 | @property(nonatomic, strong) IBOutlet YTPlayerView *playerView; 40 | ``` 41 | 5. Load the video into the player in your controller's code with the following code: 42 | ```objc 43 | [self.playerView loadWithVideoId:@"M7lc1UVf-VE"]; 44 | ``` 45 | 6. Run your code! 46 | 47 | See the sample project for more advanced uses, including passing additional player parameters and 48 | working with callbacks via YTPlayerViewDelegate. 49 | 50 | ## Original Authors 51 | 52 | - Ikai Lan 53 | - Ibrahim Ulukaya 54 | - Yoshifumi Yamaguchi 55 | 56 | ## License 57 | 58 | YouTube-Player-iOS-Helper is available under the Apache 2.0 license. See the LICENSE file for more info. 59 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute # 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | a just a few small guidelines you need to follow. 5 | 6 | 7 | ## Contributor License Agreement ## 8 | 9 | Contributions to any Google project must be accompanied by a Contributor 10 | License Agreement. This is not a copyright **assignment**, it simply gives 11 | Google permission to use and redistribute your contributions as part of the 12 | project. 13 | 14 | * If you are an individual writing original source code and you're sure you 15 | own the intellectual property, then you'll need to sign an [individual 16 | CLA][]. 17 | 18 | * If you work for a company that wants to allow you to contribute your work, 19 | then you'll need to sign a [corporate CLA][]. 20 | 21 | You generally only need to submit a CLA once, so if you've already submitted 22 | one (even if it was for a different project), you probably don't need to do it 23 | again. 24 | 25 | [individual CLA]: https://developers.google.com/open-source/cla/individual 26 | [corporate CLA]: https://developers.google.com/open-source/cla/corporate 27 | 28 | 29 | ## Submitting a patch ## 30 | 31 | 1. It's generally best to start by opening a new issue describing the bug or 32 | feature you're intending to fix. Even if you think it's relatively minor, 33 | it's helpful to know what people are working on. Mention in the initial 34 | issue that you are planning to work on that bug or feature so that it can 35 | be assigned to you. 36 | 37 | 1. Follow the normal process of [forking][] the project, and setup a new 38 | branch to work in. It's important that each group of changes be done in 39 | separate branches in order to ensure that a pull request only includes the 40 | commits related to that bug or feature. 41 | 42 | 1. Test your code on a device if you can. There are a bunch of edge conditions 43 | that can be triggered in `UIWebView` where behavior in the simulator is 44 | going to differ from behavior on a physical device. 45 | 46 | 1. Any significant changes should almost always be accompanied by tests. The 47 | project already has good test coverage, so look at some of the existing 48 | tests if you're unsure how to go about it. 49 | 50 | 1. Do your best to have [well-formed commit messages][] for each change. 51 | This provides consistency throughout the project, and ensures that commit 52 | messages are able to be formatted properly by various git tools. 53 | 54 | 1. Finally, push the commits to your fork and submit a [pull request][]. 55 | 56 | [forking]: https://help.github.com/articles/fork-a-repo 57 | [well-formed commit messages]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 58 | [squash]: http://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits 59 | [pull request]: https://help.github.com/articles/creating-a-pull-request 60 | -------------------------------------------------------------------------------- /youtube-ios-player-helper.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "youtube-ios-player-helper" 3 | s.version = "1.0.4" 4 | s.summary = "Helper library for iOS developers that want to embed YouTube videos in 5 | their iOS apps with the iframe player API." 6 | 7 | s.description = <<-DESC 8 | Helper library for iOS developers that want to play YouTube videos in 9 | their iOS apps with the iframe player API. 10 | 11 | This library allows iOS developers to quickly embed YouTube videos within 12 | their applications via a custom UIView subclass, YTPlayerView. 13 | This library provides: 14 | 15 | * A managed WebView instance that loads the HTML code for the iframe player 16 | * Objective-C wrapper functions for the JavaScript Player API 17 | * YTPlayerViewDelegate for handling YouTube player state changes natively in 18 | your Objective-C code 19 | DESC 20 | 21 | s.homepage = "https://developers.google.com/youtube/v3/guides/ios_youtube_helper" 22 | s.license = { 23 | :type => 'Apache', 24 | :text => <<-LICENSE 25 | Copyright 2014 Google Inc. All rights reserved. 26 | 27 | Licensed under the Apache License, Version 2.0 (the "License"); 28 | you may not use this file except in compliance with the License. 29 | You may obtain a copy of the License at 30 | 31 | http://www.apache.org/licenses/LICENSE-2.0 32 | 33 | Unless required by applicable law or agreed to in writing, software 34 | distributed under the License is distributed on an "AS IS" BASIS, 35 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 36 | See the License for the specific language governing permissions and 37 | limitations under the License. 38 | LICENSE 39 | } 40 | s.author = { "Ikai Lan" => "", 41 | "Ibrahim Ulukaya" => "", 42 | "Yoshifumi Yamaguchi" => "" } 43 | s.social_media_url = "https://twitter.com/YouTubeDev" 44 | s.source = { :git => "https://github.com/youtube/youtube-ios-player-helper.git", :tag => "1.0.4" } 45 | 46 | s.platform = :ios, '10.0' 47 | s.requires_arc = true 48 | 49 | s.source_files = "Sources/**/*.{h,m}" 50 | 51 | s.resource_bundle = { 52 | 'Assets' => ['Sources/Assets/*.html'] 53 | } 54 | 55 | s.ios.exclude_files = 'Sources/osx' 56 | s.osx.exclude_files = 'Sources/ios' 57 | end 58 | -------------------------------------------------------------------------------- /Sources/Assets/YTPlayerView-iframe-player.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 34 | 35 | 36 |
37 |
38 |
39 | 40 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Project/youtube-player-ios-example/youtube-player-ios-example/PlaylistViewController.m: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #import "PlaylistViewController.h" 16 | 17 | @implementation PlaylistViewController 18 | 19 | - (void)viewDidLoad { 20 | [super viewDidLoad]; 21 | NSString* playlistId = @"PLhBgTdAWkxeCMHYCQ0uuLyhydRJGDRNo5"; 22 | 23 | // For a full list of player parameters, see the documentation for the HTML5 player 24 | // at: https://developers.google.com/youtube/player_parameters?playerVersion=HTML5 25 | NSDictionary *playerVars = @{ 26 | @"controls" : @0, 27 | @"playsinline" : @1, 28 | @"autohide" : @1, 29 | @"showinfo" : @0, 30 | @"modestbranding" : @1 31 | }; 32 | self.playerView.delegate = self; 33 | 34 | [self.playerView loadWithPlaylistId:playlistId playerVars:playerVars]; 35 | 36 | [[NSNotificationCenter defaultCenter] addObserver:self 37 | selector:@selector(receivedPlaybackStartedNotification:) 38 | name:@"Playback started" 39 | object:nil]; 40 | } 41 | 42 | - (IBAction)buttonPressed:(id)sender { 43 | if (sender == self.playButton) { 44 | [[NSNotificationCenter defaultCenter] postNotificationName:@"Playback started" object:self]; 45 | [self.playerView playVideo]; 46 | } else if (sender == self.pauseButton) { 47 | [self.playerView pauseVideo]; 48 | } else if (sender == self.stopButton) { 49 | [self.playerView stopVideo]; 50 | } else if (sender == self.nextVideoButton) { 51 | [self appendStatusText:@"Loading next video in playlist\n"]; 52 | [self.playerView nextVideo]; 53 | } else if (sender == self.previousVideoButton) { 54 | [self appendStatusText:@"Loading previous video in playlist\n"]; 55 | [self.playerView previousVideo]; 56 | } 57 | } 58 | 59 | - (void)receivedPlaybackStartedNotification:(NSNotification *) notification { 60 | if([notification.name isEqual:@"Playback started"] && notification.object != self) { 61 | [self.playerView pauseVideo]; 62 | } 63 | } 64 | 65 | /** 66 | * Private helper method to add player status in statusTextView and scroll view automatically. 67 | * 68 | * @param status a string describing current player state 69 | */ 70 | - (void)appendStatusText:(NSString*)status { 71 | [self.statusTextView setText:[self.statusTextView.text stringByAppendingString:status]]; 72 | NSRange range = NSMakeRange(self.statusTextView.text.length - 1, 1); 73 | 74 | // To avoid dizzying scrolling on appending latest status. 75 | self.statusTextView.scrollEnabled = NO; 76 | [self.statusTextView scrollRangeToVisible:range]; 77 | self.statusTextView.scrollEnabled = YES; 78 | } 79 | 80 | @end -------------------------------------------------------------------------------- /youtube-ios-player-helper.xcodeproj/xcshareddata/xcschemes/youtube-ios-player-helper.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /Project/youtube-player-ios-example/youtube-player-ios-example/SingleVideoViewController.m: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #import "SingleVideoViewController.h" 16 | 17 | @implementation SingleVideoViewController 18 | 19 | - (void)viewDidLoad { 20 | [super viewDidLoad]; 21 | NSString *videoId = @"M7lc1UVf-VE"; 22 | 23 | // For a full list of player parameters, see the documentation for the HTML5 player 24 | // at: https://developers.google.com/youtube/player_parameters?playerVersion=HTML5 25 | NSDictionary *playerVars = @{ 26 | @"controls" : @0, 27 | @"playsinline" : @1, 28 | @"autohide" : @1, 29 | @"showinfo" : @0, 30 | @"modestbranding" : @1 31 | }; 32 | self.playerView.delegate = self; 33 | [self.playerView loadWithVideoId:videoId playerVars:playerVars]; 34 | 35 | 36 | 37 | [[NSNotificationCenter defaultCenter] addObserver:self 38 | selector:@selector(receivedPlaybackStartedNotification:) 39 | name:@"Playback started" 40 | object:nil]; 41 | } 42 | 43 | - (void)playerView:(YTPlayerView *)ytPlayerView didChangeToState:(YTPlayerState)state { 44 | NSString *message = [NSString stringWithFormat:@"Player state changed: %ld\n", (long)state]; 45 | [self appendStatusText:message]; 46 | } 47 | 48 | - (void)playerView:(YTPlayerView *)playerView didPlayTime:(float)playTime { 49 | [self.playerView duration:^(double result, NSError * _Nullable error) { 50 | float progress = playTime/result; 51 | [self.slider setValue:progress]; 52 | }]; 53 | } 54 | 55 | - (IBAction)onSliderChange:(id)sender { 56 | [self.playerView duration:^(double result, NSError * _Nullable error) { 57 | float seekToTime = result * self.slider.value; 58 | [self.playerView seekToSeconds:seekToTime allowSeekAhead:YES]; 59 | [self appendStatusText:[NSString stringWithFormat:@"Seeking to time: %.0f seconds\n", seekToTime]]; 60 | }]; 61 | } 62 | 63 | - (IBAction)buttonPressed:(id)sender { 64 | if (sender == self.playButton) { 65 | [[NSNotificationCenter defaultCenter] postNotificationName:@"Playback started" object:self]; 66 | [self.playerView playVideo]; 67 | } else if (sender == self.stopButton) { 68 | [self.playerView stopVideo]; 69 | } else if (sender == self.pauseButton) { 70 | [self.playerView pauseVideo]; 71 | } else if (sender == self.reverseButton) { 72 | [self.playerView currentTime:^(float result, NSError * _Nullable error) { 73 | float seekToTime = result - 30.0; 74 | [self.playerView seekToSeconds:seekToTime allowSeekAhead:YES]; 75 | [self appendStatusText:[NSString stringWithFormat:@"Seeking to time: %.0f seconds\n", seekToTime]]; 76 | }]; 77 | } else if (sender == self.forwardButton) { 78 | [self.playerView currentTime:^(float result, NSError * _Nullable error) { 79 | float seekToTime = result + 30.0; 80 | [self.playerView seekToSeconds:seekToTime allowSeekAhead:YES]; 81 | [self appendStatusText:[NSString stringWithFormat:@"Seeking to time: %.0f seconds\n", seekToTime]]; 82 | }]; 83 | } else if (sender == self.startButton) { 84 | [self.playerView seekToSeconds:0 allowSeekAhead:YES]; 85 | [self appendStatusText:@"Seeking to beginning\n"]; 86 | } 87 | } 88 | 89 | - (void)receivedPlaybackStartedNotification:(NSNotification *) notification { 90 | if([notification.name isEqual:@"Playback started"] && notification.object != self) { 91 | [self.playerView pauseVideo]; 92 | } 93 | } 94 | 95 | /** 96 | * Private helper method to add player status in statusTextView and scroll view automatically. 97 | * 98 | * @param status a string describing current player state 99 | */ 100 | - (void)appendStatusText:(NSString *)status { 101 | [self.statusTextView setText:[self.statusTextView.text stringByAppendingString:status]]; 102 | NSRange range = NSMakeRange(self.statusTextView.text.length - 1, 1); 103 | 104 | // To avoid dizzying scrolling on appending latest status. 105 | self.statusTextView.scrollEnabled = NO; 106 | [self.statusTextView scrollRangeToVisible:range]; 107 | self.statusTextView.scrollEnabled = YES; 108 | } 109 | 110 | @end 111 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | desc "Runs the specs [EMPTY]" 16 | task :spec do 17 | # Provide your own implementation 18 | end 19 | 20 | task :version do 21 | git_remotes = `git remote`.strip.split("\n") 22 | 23 | if git_remotes.count > 0 24 | puts "-- fetching version number from github" 25 | sh 'git fetch' 26 | 27 | remote_version = remote_spec_version 28 | end 29 | 30 | if remote_version.nil? 31 | puts "There is no current released version. You're about to release a new Pod." 32 | version = "0.0.1" 33 | else 34 | puts "The current released version of your pod is " + remote_spec_version.to_s() 35 | version = suggested_version_number 36 | end 37 | 38 | puts "Enter the version you want to release (" + version + ") " 39 | new_version_number = $stdin.gets.strip 40 | if new_version_number == "" 41 | new_version_number = version 42 | end 43 | 44 | replace_version_number(new_version_number) 45 | end 46 | 47 | desc "Release a new version of the Pod" 48 | task :release do 49 | 50 | puts "* Running version" 51 | sh "rake version" 52 | 53 | unless ENV['SKIP_CHECKS'] 54 | if `git symbolic-ref HEAD 2>/dev/null`.strip.split('/').last != 'master' 55 | $stderr.puts "[!] You need to be on the `master' branch in order to be able to do a release." 56 | exit 1 57 | end 58 | 59 | if `git tag`.strip.split("\n").include?(spec_version) 60 | $stderr.puts "[!] A tag for version `#{spec_version}' already exists. Change the version in the podspec" 61 | exit 1 62 | end 63 | 64 | puts "You are about to release `#{spec_version}`, is that correct? [y/n]" 65 | exit if $stdin.gets.strip.downcase != 'y' 66 | end 67 | 68 | puts "* Running specs" 69 | sh "rake spec" 70 | 71 | puts "* Linting the podspec" 72 | sh "pod lib lint" 73 | 74 | # Then release 75 | sh "git commit #{podspec_path} CHANGELOG.md -m 'Release #{spec_version}'" 76 | sh "git tag -a #{spec_version} -m 'Release #{spec_version}'" 77 | sh "git push origin master" 78 | sh "git push origin --tags" 79 | sh "pod trunk push #{podspec_path}" 80 | end 81 | 82 | # @return [Pod::Version] The version as reported by the Podspec. 83 | # 84 | def spec_version 85 | require 'cocoapods' 86 | spec = Pod::Specification.from_file(podspec_path) 87 | spec.version 88 | end 89 | 90 | # @return [Pod::Version] The version as reported by the Podspec from remote. 91 | # 92 | def remote_spec_version 93 | require 'cocoapods-core' 94 | 95 | if spec_file_exist_on_remote? 96 | remote_spec = eval(`git show origin/master:#{podspec_path}`) 97 | remote_spec.version 98 | else 99 | nil 100 | end 101 | end 102 | 103 | # @return [Bool] If the remote repository has a copy of the podpesc file or not. 104 | # 105 | def spec_file_exist_on_remote? 106 | test_condition = `if git rev-parse --verify --quiet origin/master:#{podspec_path} >/dev/null; 107 | then 108 | echo 'true' 109 | else 110 | echo 'false' 111 | fi` 112 | 113 | 'true' == test_condition.strip 114 | end 115 | 116 | # @return [String] The relative path of the Podspec. 117 | # 118 | def podspec_path 119 | podspecs = Dir.glob('*.podspec') 120 | if podspecs.count == 1 121 | podspecs.first 122 | else 123 | raise "Could not select a podspec" 124 | end 125 | end 126 | 127 | # @return [String] The suggested version number based on the local and remote version numbers. 128 | # 129 | def suggested_version_number 130 | if spec_version != remote_spec_version 131 | spec_version.to_s() 132 | else 133 | next_version(spec_version).to_s() 134 | end 135 | end 136 | 137 | # @param [Pod::Version] version 138 | # the version for which you need the next version 139 | # 140 | # @note It is computed by bumping the last component of the versino string by 1. 141 | # 142 | # @return [Pod::Version] The version that comes next after the version supplied. 143 | # 144 | def next_version(version) 145 | version_components = version.to_s().split("."); 146 | last = (version_components.last.to_i() + 1).to_s 147 | version_components[-1] = last 148 | Pod::Version.new(version_components.join(".")) 149 | end 150 | 151 | # @param [String] new_version_number 152 | # the new version number 153 | # 154 | # @note This methods replaces the version number in the podspec file with a new version number. 155 | # 156 | # @return void 157 | # 158 | def replace_version_number(new_version_number) 159 | text = File.read(podspec_path) 160 | text.gsub!(/(s.version( )*= ")#{spec_version}(")/, "\\1#{new_version_number}\\3") 161 | text.gsub!(/(s.source( )*= \{ :git => "https:\/\/github.com\/youtube\/youtube-ios-player-helper\.git", :tag => ")#{spec_version}(")/, "\\1#{new_version_number}\\3") 162 | File.open(podspec_path, "w") { |file| file.puts text } 163 | end 164 | -------------------------------------------------------------------------------- /youtube-ios-player-helper.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | B3C76A271B975ADB00F375B4 /* YTPlayerView.h in Headers */ = {isa = PBXBuildFile; fileRef = B3C76A251B975ADB00F375B4 /* YTPlayerView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11 | B3C76A281B975ADB00F375B4 /* YTPlayerView.m in Sources */ = {isa = PBXBuildFile; fileRef = B3C76A261B975ADB00F375B4 /* YTPlayerView.m */; }; 12 | B3C76A2F1B975C0100F375B4 /* YouTubeiOSPlayerHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = B3C76A2D1B975BD500F375B4 /* YouTubeiOSPlayerHelper.h */; settings = {ATTRIBUTES = (Public, ); }; }; 13 | CC3F4BBA2514FEF200AB0A15 /* YTPlayerView-iframe-player.html in Resources */ = {isa = PBXBuildFile; fileRef = CC3F4BB92514FEF200AB0A15 /* YTPlayerView-iframe-player.html */; }; 14 | /* End PBXBuildFile section */ 15 | 16 | /* Begin PBXFileReference section */ 17 | B3C76A1A1B975AA700F375B4 /* YouTubeiOSPlayerHelper.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = YouTubeiOSPlayerHelper.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 18 | B3C76A1F1B975AA700F375B4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 19 | B3C76A251B975ADB00F375B4 /* YTPlayerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = YTPlayerView.h; path = Sources/YTPlayerView.h; sourceTree = SOURCE_ROOT; }; 20 | B3C76A261B975ADB00F375B4 /* YTPlayerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = YTPlayerView.m; path = Sources/YTPlayerView.m; sourceTree = SOURCE_ROOT; }; 21 | B3C76A2D1B975BD500F375B4 /* YouTubeiOSPlayerHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YouTubeiOSPlayerHelper.h; sourceTree = ""; }; 22 | CC3F4BB92514FEF200AB0A15 /* YTPlayerView-iframe-player.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = "YTPlayerView-iframe-player.html"; path = "../Sources/Assets/YTPlayerView-iframe-player.html"; sourceTree = ""; }; 23 | /* End PBXFileReference section */ 24 | 25 | /* Begin PBXFrameworksBuildPhase section */ 26 | B3C76A161B975AA700F375B4 /* Frameworks */ = { 27 | isa = PBXFrameworksBuildPhase; 28 | buildActionMask = 2147483647; 29 | files = ( 30 | ); 31 | runOnlyForDeploymentPostprocessing = 0; 32 | }; 33 | /* End PBXFrameworksBuildPhase section */ 34 | 35 | /* Begin PBXGroup section */ 36 | B3C76A101B975AA700F375B4 = { 37 | isa = PBXGroup; 38 | children = ( 39 | B3C76A1C1B975AA700F375B4 /* youtube-ios-player-helper */, 40 | B3C76A1B1B975AA700F375B4 /* Products */, 41 | ); 42 | sourceTree = ""; 43 | }; 44 | B3C76A1B1B975AA700F375B4 /* Products */ = { 45 | isa = PBXGroup; 46 | children = ( 47 | B3C76A1A1B975AA700F375B4 /* YouTubeiOSPlayerHelper.framework */, 48 | ); 49 | name = Products; 50 | sourceTree = ""; 51 | }; 52 | B3C76A1C1B975AA700F375B4 /* youtube-ios-player-helper */ = { 53 | isa = PBXGroup; 54 | children = ( 55 | CC3F4BBD2514FEF800AB0A15 /* Assets */, 56 | B3C76A251B975ADB00F375B4 /* YTPlayerView.h */, 57 | B3C76A261B975ADB00F375B4 /* YTPlayerView.m */, 58 | B3C76A1F1B975AA700F375B4 /* Info.plist */, 59 | B3C76A2D1B975BD500F375B4 /* YouTubeiOSPlayerHelper.h */, 60 | ); 61 | path = "youtube-ios-player-helper"; 62 | sourceTree = ""; 63 | }; 64 | CC3F4BBD2514FEF800AB0A15 /* Assets */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | CC3F4BB92514FEF200AB0A15 /* YTPlayerView-iframe-player.html */, 68 | ); 69 | name = Assets; 70 | sourceTree = ""; 71 | }; 72 | /* End PBXGroup section */ 73 | 74 | /* Begin PBXHeadersBuildPhase section */ 75 | B3C76A171B975AA700F375B4 /* Headers */ = { 76 | isa = PBXHeadersBuildPhase; 77 | buildActionMask = 2147483647; 78 | files = ( 79 | B3C76A271B975ADB00F375B4 /* YTPlayerView.h in Headers */, 80 | B3C76A2F1B975C0100F375B4 /* YouTubeiOSPlayerHelper.h in Headers */, 81 | ); 82 | runOnlyForDeploymentPostprocessing = 0; 83 | }; 84 | /* End PBXHeadersBuildPhase section */ 85 | 86 | /* Begin PBXNativeTarget section */ 87 | B3C76A191B975AA700F375B4 /* YouTubeiOSPlayerHelper */ = { 88 | isa = PBXNativeTarget; 89 | buildConfigurationList = B3C76A221B975AA700F375B4 /* Build configuration list for PBXNativeTarget "YouTubeiOSPlayerHelper" */; 90 | buildPhases = ( 91 | B3C76A151B975AA700F375B4 /* Sources */, 92 | B3C76A161B975AA700F375B4 /* Frameworks */, 93 | B3C76A171B975AA700F375B4 /* Headers */, 94 | B3C76A181B975AA700F375B4 /* Resources */, 95 | ); 96 | buildRules = ( 97 | ); 98 | dependencies = ( 99 | ); 100 | name = YouTubeiOSPlayerHelper; 101 | productName = "youtube-ios-player-helper"; 102 | productReference = B3C76A1A1B975AA700F375B4 /* YouTubeiOSPlayerHelper.framework */; 103 | productType = "com.apple.product-type.framework"; 104 | }; 105 | /* End PBXNativeTarget section */ 106 | 107 | /* Begin PBXProject section */ 108 | B3C76A111B975AA700F375B4 /* Project object */ = { 109 | isa = PBXProject; 110 | attributes = { 111 | LastUpgradeCheck = 0700; 112 | ORGANIZATIONNAME = "YouTube Developer Relations"; 113 | TargetAttributes = { 114 | B3C76A191B975AA700F375B4 = { 115 | CreatedOnToolsVersion = 7.0; 116 | }; 117 | }; 118 | }; 119 | buildConfigurationList = B3C76A141B975AA700F375B4 /* Build configuration list for PBXProject "youtube-ios-player-helper" */; 120 | compatibilityVersion = "Xcode 3.2"; 121 | developmentRegion = English; 122 | hasScannedForEncodings = 0; 123 | knownRegions = ( 124 | English, 125 | en, 126 | ); 127 | mainGroup = B3C76A101B975AA700F375B4; 128 | productRefGroup = B3C76A1B1B975AA700F375B4 /* Products */; 129 | projectDirPath = ""; 130 | projectRoot = ""; 131 | targets = ( 132 | B3C76A191B975AA700F375B4 /* YouTubeiOSPlayerHelper */, 133 | ); 134 | }; 135 | /* End PBXProject section */ 136 | 137 | /* Begin PBXResourcesBuildPhase section */ 138 | B3C76A181B975AA700F375B4 /* Resources */ = { 139 | isa = PBXResourcesBuildPhase; 140 | buildActionMask = 2147483647; 141 | files = ( 142 | CC3F4BBA2514FEF200AB0A15 /* YTPlayerView-iframe-player.html in Resources */, 143 | ); 144 | runOnlyForDeploymentPostprocessing = 0; 145 | }; 146 | /* End PBXResourcesBuildPhase section */ 147 | 148 | /* Begin PBXSourcesBuildPhase section */ 149 | B3C76A151B975AA700F375B4 /* Sources */ = { 150 | isa = PBXSourcesBuildPhase; 151 | buildActionMask = 2147483647; 152 | files = ( 153 | B3C76A281B975ADB00F375B4 /* YTPlayerView.m in Sources */, 154 | ); 155 | runOnlyForDeploymentPostprocessing = 0; 156 | }; 157 | /* End PBXSourcesBuildPhase section */ 158 | 159 | /* Begin XCBuildConfiguration section */ 160 | B3C76A201B975AA700F375B4 /* Debug */ = { 161 | isa = XCBuildConfiguration; 162 | buildSettings = { 163 | ALWAYS_SEARCH_USER_PATHS = NO; 164 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 165 | CLANG_CXX_LIBRARY = "libc++"; 166 | CLANG_ENABLE_MODULES = YES; 167 | CLANG_ENABLE_OBJC_ARC = YES; 168 | CLANG_WARN_BOOL_CONVERSION = YES; 169 | CLANG_WARN_CONSTANT_CONVERSION = YES; 170 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 171 | CLANG_WARN_EMPTY_BODY = YES; 172 | CLANG_WARN_ENUM_CONVERSION = YES; 173 | CLANG_WARN_INT_CONVERSION = YES; 174 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 175 | CLANG_WARN_UNREACHABLE_CODE = YES; 176 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 177 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 178 | COPY_PHASE_STRIP = NO; 179 | CURRENT_PROJECT_VERSION = 1; 180 | DEBUG_INFORMATION_FORMAT = dwarf; 181 | ENABLE_STRICT_OBJC_MSGSEND = YES; 182 | ENABLE_TESTABILITY = YES; 183 | GCC_C_LANGUAGE_STANDARD = gnu99; 184 | GCC_DYNAMIC_NO_PIC = NO; 185 | GCC_NO_COMMON_BLOCKS = YES; 186 | GCC_OPTIMIZATION_LEVEL = 0; 187 | GCC_PREPROCESSOR_DEFINITIONS = ( 188 | "DEBUG=1", 189 | "$(inherited)", 190 | ); 191 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 192 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 193 | GCC_WARN_UNDECLARED_SELECTOR = YES; 194 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 195 | GCC_WARN_UNUSED_FUNCTION = YES; 196 | GCC_WARN_UNUSED_VARIABLE = YES; 197 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 198 | MTL_ENABLE_DEBUG_INFO = YES; 199 | ONLY_ACTIVE_ARCH = YES; 200 | SDKROOT = iphoneos; 201 | TARGETED_DEVICE_FAMILY = "1,2"; 202 | VERSIONING_SYSTEM = "apple-generic"; 203 | VERSION_INFO_PREFIX = ""; 204 | }; 205 | name = Debug; 206 | }; 207 | B3C76A211B975AA700F375B4 /* Release */ = { 208 | isa = XCBuildConfiguration; 209 | buildSettings = { 210 | ALWAYS_SEARCH_USER_PATHS = NO; 211 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 212 | CLANG_CXX_LIBRARY = "libc++"; 213 | CLANG_ENABLE_MODULES = YES; 214 | CLANG_ENABLE_OBJC_ARC = YES; 215 | CLANG_WARN_BOOL_CONVERSION = YES; 216 | CLANG_WARN_CONSTANT_CONVERSION = YES; 217 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 218 | CLANG_WARN_EMPTY_BODY = YES; 219 | CLANG_WARN_ENUM_CONVERSION = YES; 220 | CLANG_WARN_INT_CONVERSION = YES; 221 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 222 | CLANG_WARN_UNREACHABLE_CODE = YES; 223 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 224 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 225 | COPY_PHASE_STRIP = NO; 226 | CURRENT_PROJECT_VERSION = 1; 227 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 228 | ENABLE_NS_ASSERTIONS = NO; 229 | ENABLE_STRICT_OBJC_MSGSEND = YES; 230 | GCC_C_LANGUAGE_STANDARD = gnu99; 231 | GCC_NO_COMMON_BLOCKS = YES; 232 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 233 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 234 | GCC_WARN_UNDECLARED_SELECTOR = YES; 235 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 236 | GCC_WARN_UNUSED_FUNCTION = YES; 237 | GCC_WARN_UNUSED_VARIABLE = YES; 238 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 239 | MTL_ENABLE_DEBUG_INFO = NO; 240 | SDKROOT = iphoneos; 241 | TARGETED_DEVICE_FAMILY = "1,2"; 242 | VALIDATE_PRODUCT = YES; 243 | VERSIONING_SYSTEM = "apple-generic"; 244 | VERSION_INFO_PREFIX = ""; 245 | }; 246 | name = Release; 247 | }; 248 | B3C76A231B975AA700F375B4 /* Debug */ = { 249 | isa = XCBuildConfiguration; 250 | buildSettings = { 251 | DEFINES_MODULE = YES; 252 | DYLIB_COMPATIBILITY_VERSION = 1; 253 | DYLIB_CURRENT_VERSION = 1; 254 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 255 | INFOPLIST_FILE = "youtube-ios-player-helper/Info.plist"; 256 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 257 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 258 | PRODUCT_BUNDLE_IDENTIFIER = "com.youtube.youtube-ios-player-helper"; 259 | PRODUCT_NAME = "$(TARGET_NAME)"; 260 | SKIP_INSTALL = YES; 261 | }; 262 | name = Debug; 263 | }; 264 | B3C76A241B975AA700F375B4 /* Release */ = { 265 | isa = XCBuildConfiguration; 266 | buildSettings = { 267 | DEFINES_MODULE = YES; 268 | DYLIB_COMPATIBILITY_VERSION = 1; 269 | DYLIB_CURRENT_VERSION = 1; 270 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 271 | INFOPLIST_FILE = "youtube-ios-player-helper/Info.plist"; 272 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 273 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 274 | PRODUCT_BUNDLE_IDENTIFIER = "com.youtube.youtube-ios-player-helper"; 275 | PRODUCT_NAME = "$(TARGET_NAME)"; 276 | SKIP_INSTALL = YES; 277 | }; 278 | name = Release; 279 | }; 280 | /* End XCBuildConfiguration section */ 281 | 282 | /* Begin XCConfigurationList section */ 283 | B3C76A141B975AA700F375B4 /* Build configuration list for PBXProject "youtube-ios-player-helper" */ = { 284 | isa = XCConfigurationList; 285 | buildConfigurations = ( 286 | B3C76A201B975AA700F375B4 /* Debug */, 287 | B3C76A211B975AA700F375B4 /* Release */, 288 | ); 289 | defaultConfigurationIsVisible = 0; 290 | defaultConfigurationName = Release; 291 | }; 292 | B3C76A221B975AA700F375B4 /* Build configuration list for PBXNativeTarget "YouTubeiOSPlayerHelper" */ = { 293 | isa = XCConfigurationList; 294 | buildConfigurations = ( 295 | B3C76A231B975AA700F375B4 /* Debug */, 296 | B3C76A241B975AA700F375B4 /* Release */, 297 | ); 298 | defaultConfigurationIsVisible = 0; 299 | defaultConfigurationName = Release; 300 | }; 301 | /* End XCConfigurationList section */ 302 | }; 303 | rootObject = B3C76A111B975AA700F375B4 /* Project object */; 304 | } 305 | -------------------------------------------------------------------------------- /Project/youtube-player-ios-example/youtube-player-ios-exampleTests/youtube_player_ios_exampleTests.m: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #import 16 | #import 17 | #import 18 | 19 | #import "YTPlayerView.h" 20 | 21 | @interface youtube_player_ios_exampleTests : XCTestCase 22 | 23 | @end 24 | 25 | @interface YTPlayerView (ExposedForTesting) 26 | - (void)setWebView:(WKWebView *)webView; 27 | - (WKWebView *) createNewWebView; 28 | @end 29 | 30 | @implementation youtube_player_ios_exampleTests { 31 | YTPlayerView *playerView; 32 | id mockWebView; 33 | id mockDelegate; 34 | } 35 | 36 | - (void)setUp { 37 | [super setUp]; 38 | playerView = [[YTPlayerView alloc] init]; 39 | mockWebView = [OCMockObject mockForClass:[WKWebView class]]; 40 | mockDelegate = [OCMockObject mockForProtocol:@protocol(YTPlayerViewDelegate)]; 41 | playerView.delegate = mockDelegate; 42 | playerView.webView = mockWebView; 43 | } 44 | 45 | - (void)tearDown { 46 | [super tearDown]; 47 | } 48 | 49 | #pragma mark - Player loading tests 50 | 51 | - (void)testLoadPlayerVideoId { 52 | WKWebView *webView = [[WKWebView alloc] init]; 53 | id partialWebViewMock = [OCMockObject partialMockForObject:webView]; 54 | id partailPlayer = [self makePartialPlayerMockWithWebView:partialWebViewMock]; 55 | 56 | [(WKWebView *)[partialWebViewMock expect] loadHTMLString:[OCMArg checkWithBlock:^BOOL(NSString *html) { 57 | if ([html rangeOfString:@"VIDEO_ID_HERE"].location == NSNotFound) { 58 | return NO; 59 | } else { 60 | return YES; 61 | } 62 | }] 63 | baseURL:[OCMArg any]]; 64 | [partailPlayer loadWithVideoId:@"VIDEO_ID_HERE"]; 65 | [partialWebViewMock verify]; 66 | } 67 | 68 | - (void)testLoadPlayerPlaylistId { 69 | WKWebView *webView = [[WKWebView alloc] init]; 70 | id partialWebViewMock = [OCMockObject partialMockForObject:webView]; 71 | id partialPlayer = [self makePartialPlayerMockWithWebView:partialWebViewMock]; 72 | 73 | [(WKWebView *)[partialWebViewMock expect] loadHTMLString:[OCMArg checkWithBlock:^BOOL(NSString *html) { 74 | // There are two strings to check for: 75 | // "list" : "PLAYLIST_ID_HERE" 76 | // and 77 | // "listType" : "playlist" 78 | NSString *expectedPlaylistId = @"\"list\" : \"PLAYLIST_ID_HERE\""; 79 | NSString *expectedListType = @"\"listType\" : \"playlist\""; 80 | if ([html rangeOfString:expectedPlaylistId].location == NSNotFound) { 81 | return NO; 82 | } 83 | if ([html rangeOfString:expectedListType].location == NSNotFound) { 84 | return NO; 85 | } 86 | return YES; 87 | }] 88 | baseURL:[OCMArg any]]; 89 | [partialPlayer loadWithPlaylistId:@"PLAYLIST_ID_HERE"]; 90 | [partialWebViewMock verify]; 91 | } 92 | 93 | - (void)testLoadPlayerPlayerParameters { 94 | WKWebView *webView = [[WKWebView alloc] init]; 95 | id partialWebViewMock = [OCMockObject partialMockForObject:webView]; 96 | id partialPlayer = [self makePartialPlayerMockWithWebView:partialWebViewMock]; 97 | 98 | [(WKWebView *)[partialWebViewMock expect] loadHTMLString:[OCMArg checkWithBlock:^BOOL(NSString *html) { 99 | if ([html rangeOfString:@"\"RANDOM_PARAMETER\" : 1"].location == NSNotFound) { 100 | return NO; 101 | } else { 102 | return YES; 103 | } 104 | }] 105 | baseURL:[OCMArg any]]; 106 | [partialPlayer loadWithVideoId:@"VIDEO_ID_HERE" playerVars:@{ @"RANDOM_PARAMETER" : @1 }]; 107 | [partialWebViewMock verify]; 108 | } 109 | 110 | - (id)makePartialPlayerMockWithWebView:(WKWebView *)webView { 111 | id partialPlayer = [OCMockObject partialMockForObject:playerView]; 112 | OCMStub([partialPlayer webView]).andReturn(webView); 113 | OCMStub([partialPlayer createNewWebView]).andReturn(webView); 114 | OCMStub([partialPlayer addSubview:[OCMArg isNotNil]]).andDo(nil); 115 | OCMStub([partialPlayer delegate]).andReturn(nil); 116 | 117 | return partialPlayer; 118 | } 119 | 120 | #pragma mark - Player Controls 121 | 122 | - (void)testPlayVideo { 123 | [[mockWebView expect] evaluateJavaScript:@"player.playVideo();" completionHandler:[OCMArg any]]; 124 | [playerView playVideo]; 125 | [mockWebView verify]; 126 | } 127 | 128 | - (void)testPauseVideo { 129 | [[mockDelegate expect] playerView:playerView didChangeToState:kYTPlayerStatePaused]; 130 | [[mockWebView expect] evaluateJavaScript:@"player.pauseVideo();" completionHandler:[OCMArg any]]; 131 | [playerView pauseVideo]; 132 | [mockWebView verify]; 133 | } 134 | 135 | - (void)testStopVideo { 136 | [[mockWebView expect] evaluateJavaScript:@"player.stopVideo();" completionHandler:[OCMArg any]]; 137 | [playerView stopVideo]; 138 | [mockWebView verify]; 139 | } 140 | 141 | - (void)testSeekTo { 142 | [[mockWebView expect] evaluateJavaScript:@"player.seekTo(5.5, false);" completionHandler:[OCMArg any]]; 143 | [playerView seekToSeconds:5.5 allowSeekAhead:NO]; 144 | [mockWebView verify]; 145 | } 146 | 147 | #pragma mark - Tests for cueing and loading videos 148 | 149 | - (void)testCueVideoByIdstartSeconds { 150 | [[mockWebView expect] evaluateJavaScript:@"player.cueVideoById('abc', 5.5);" 151 | completionHandler:[OCMArg any]]; 152 | 153 | [playerView cueVideoById:@"abc" startSeconds:5.5]; 154 | [mockWebView verify]; 155 | } 156 | 157 | - (void)testCueVideoByIdstartSecondsendSecondsWithQuality { 158 | [[mockWebView expect] evaluateJavaScript:@"player.cueVideoById({'videoId': 'abc','startSeconds': 5.5, 'endSeconds': 10.5});" 159 | completionHandler:[OCMArg any]]; 160 | [playerView cueVideoById:@"abc" 161 | startSeconds:5.5 162 | endSeconds:10.5]; 163 | [mockWebView verify]; 164 | } 165 | 166 | - (void)testLoadVideoByIdstartSeconds { 167 | [[mockWebView expect] evaluateJavaScript:@"player.loadVideoById('abc', 5.5);" 168 | completionHandler:[OCMArg any]]; 169 | [playerView loadVideoById:@"abc" startSeconds:5.5]; 170 | [mockWebView verify]; 171 | } 172 | 173 | - (void)testCueVideoByUrlstartSecondsWithQuality { 174 | [[mockWebView expect] evaluateJavaScript: 175 | @"player.cueVideoByUrl('http://www.youtube.com/watch?v=J0tafinyviA', 5.5);" 176 | completionHandler:[OCMArg any]]; 177 | [playerView cueVideoByURL:@"http://www.youtube.com/watch?v=J0tafinyviA" 178 | startSeconds:5.5]; 179 | [mockWebView verify]; 180 | } 181 | 182 | - (void)testCueVideoByUrlstartSecondsendSecondsWithQuality { 183 | [[mockWebView expect] 184 | evaluateJavaScript:@"player.cueVideoByUrl('http://www.youtube.com/" 185 | "watch?v=J0tafinyviA', 5.5, 10.5);" completionHandler:[OCMArg any]]; 186 | [playerView cueVideoByURL:@"http://www.youtube.com/watch?v=J0tafinyviA" 187 | startSeconds:5.5 188 | endSeconds:10.5]; 189 | [mockWebView verify]; 190 | } 191 | 192 | - (void)testLoadVideoByUrlstartSecondsWithQuality { 193 | [[mockWebView expect] evaluateJavaScript: 194 | @"player.loadVideoByUrl('http://www.youtube.com/watch?v=J0tafinyviA', 5.5);" completionHandler:[OCMArg any]]; 195 | [playerView loadVideoByURL:@"http://www.youtube.com/watch?v=J0tafinyviA" 196 | startSeconds:5.5]; 197 | [mockWebView verify]; 198 | } 199 | 200 | - (void)testLoadVideoByUrlstartSecondsendSecondsWithQuality { 201 | [[mockWebView expect] 202 | evaluateJavaScript:@"player.cueVideoByUrl('http://www.youtube.com/" 203 | "watch?v=J0tafinyviA', 5.5, 10.5);" completionHandler:[OCMArg any]]; 204 | [playerView cueVideoByURL:@"http://www.youtube.com/watch?v=J0tafinyviA" 205 | startSeconds:5.5 206 | endSeconds:10.5]; 207 | [mockWebView verify]; 208 | } 209 | 210 | #pragma mark - Tests for cueing and loading playlists 211 | 212 | - (void)testCuePlaylistIdIndexStartSecondsWithSuggestedQuality { 213 | [[mockWebView expect] evaluateJavaScript:@"player.cuePlaylist('abc', 2, 10.5);" completionHandler:[OCMArg any]]; 214 | [playerView cuePlaylistByPlaylistId:@"abc" 215 | index:2 216 | startSeconds:10.5]; 217 | [mockWebView verify]; 218 | } 219 | 220 | - (void)testCuePlaylistWithListOfVideoIds { 221 | 222 | [[mockWebView expect] evaluateJavaScript: 223 | @"player.cuePlaylist(['abc', 'def'], 2, 10.5);" completionHandler:[OCMArg any]]; 224 | 225 | NSArray *videoIds = @[ @"abc", @"def" ]; 226 | [playerView cuePlaylistByVideos:videoIds 227 | index:2 228 | startSeconds:10.5]; 229 | 230 | [mockWebView verify]; 231 | } 232 | 233 | #pragma mark - Retrieving playlist information 234 | 235 | - (void)testGetPlaylist { 236 | // The key test here is seeing if we can correctly convert a JavaScript 237 | // array into an NSArray of NSString instances 238 | XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)]; 239 | 240 | [[mockWebView stub] 241 | evaluateJavaScript:@"player.getPlaylist();" completionHandler:[OCMArg invokeBlockWithArgs:@[@"abc", @"def", @"xyz"], NSNull.null, nil]]; 242 | 243 | NSArray *expectedArray = @[ @"abc", @"def", @"xyz" ]; 244 | 245 | [playerView playlist:^(NSArray * _Nullable result, NSError * _Nullable error) { 246 | XCTAssertEqualObjects(expectedArray, result, @"Arrays are not equal."); 247 | [expectation fulfill]; 248 | }]; 249 | 250 | [self waitForExpectations:@[expectation] timeout:1.0]; 251 | } 252 | 253 | #pragma mark - Callback tests 254 | 255 | - (void)testOnPlayerReadyCallback { 256 | NSURL *url = [[NSURL alloc] initWithString:@"ytplayer://onReady"]; 257 | NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url]; 258 | 259 | id actionMock = [OCMockObject mockForClass:[WKNavigationAction class]]; 260 | OCMStub([actionMock request]).andReturn(request); 261 | 262 | [[mockDelegate expect] playerViewDidBecomeReady:[OCMArg any]]; 263 | [[mockDelegate stub] playerViewDidBecomeReady:[OCMArg any]]; 264 | 265 | [(id) playerView webView:mockWebView decidePolicyForNavigationAction:actionMock decisionHandler:^(WKNavigationActionPolicy decision) {}]; 266 | [mockDelegate verify]; 267 | } 268 | 269 | - (void)testOnPlayerStateChangeCallback { 270 | NSURL *url = [[NSURL alloc] initWithString:@"ytplayer://onStateChange?data=1"]; 271 | NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url]; 272 | 273 | id actionMock = [OCMockObject mockForClass:[WKNavigationAction class]]; 274 | OCMStub([actionMock request]).andReturn(request); 275 | 276 | [[mockDelegate expect] playerView:[OCMArg any] didChangeToState:kYTPlayerStatePlaying]; 277 | 278 | [(id) playerView webView:mockWebView decidePolicyForNavigationAction:actionMock decisionHandler:^(WKNavigationActionPolicy decision) {}]; 279 | 280 | [mockDelegate verify]; 281 | } 282 | 283 | - (void)testOnPlaybackQualityChangeCallback { 284 | NSURL *url = [[NSURL alloc] initWithString:@"ytplayer://onPlaybackQualityChange?data=hd1080"]; 285 | NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url]; 286 | 287 | id actionMock = [OCMockObject mockForClass:[WKNavigationAction class]]; 288 | OCMStub([actionMock request]).andReturn(request); 289 | 290 | [[mockDelegate expect] playerView:[OCMArg any] didChangeToQuality:kYTPlaybackQualityHD1080]; 291 | 292 | [(id) playerView webView:mockWebView decidePolicyForNavigationAction:actionMock decisionHandler:^(WKNavigationActionPolicy decision) {}]; 293 | 294 | [mockDelegate verify]; 295 | } 296 | 297 | - (void)testOnErrorCallback { 298 | NSURL *url = [[NSURL alloc] initWithString:@"ytplayer://onError?data=101"]; 299 | NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url]; 300 | 301 | id actionMock = [OCMockObject mockForClass:[WKNavigationAction class]]; 302 | OCMStub([actionMock request]).andReturn(request); 303 | 304 | [[mockDelegate expect] playerView:[OCMArg any] receivedError:kYTPlayerErrorNotEmbeddable]; 305 | 306 | [(id) playerView webView:mockWebView decidePolicyForNavigationAction:actionMock decisionHandler:^(WKNavigationActionPolicy decision) {}]; 307 | 308 | [mockDelegate verify]; 309 | } 310 | 311 | - (void)testGetAvailablePlaybackRates { 312 | XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)]; 313 | 314 | [[mockWebView stub] evaluateJavaScript:@"player.getAvailablePlaybackRates();" 315 | completionHandler:[OCMArg invokeBlockWithArgs:@[@0.25, @0.5, @1, @1.5, @2], NSNull.null, nil]]; 316 | 317 | NSArray *expectedArray = @[ @0.25, @0.5, @1, @1.5, @2 ]; 318 | 319 | [playerView availablePlaybackRates:^(NSArray * _Nullable result, NSError * _Nullable error) { 320 | XCTAssertEqualObjects(result, expectedArray, @"Arrays are not equal."); 321 | [expectation fulfill]; 322 | }]; 323 | 324 | [self waitForExpectations:@[expectation] timeout:1.0]; 325 | } 326 | 327 | - (void)testGetVideoUrl { 328 | XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)]; 329 | 330 | NSURL *expectedURL = [NSURL URLWithString:@"http://www.youtube.com/watch?v=9moAdEslwkg"]; 331 | [[mockWebView stub] evaluateJavaScript:@"player.getVideoUrl();" 332 | completionHandler:[OCMArg invokeBlockWithArgs:@"http://www.youtube.com/watch?v=9moAdEslwkg", NSNull.null, nil]]; 333 | 334 | 335 | [playerView videoUrl:^(NSURL * _Nullable result, NSError * _Nullable error) { 336 | XCTAssert([expectedURL isEqual:result]); 337 | [expectation fulfill]; 338 | }]; 339 | 340 | [self waitForExpectations:@[expectation] timeout:1.0]; 341 | } 342 | 343 | - (void)testGetVideoEmbedCode { 344 | XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)]; 345 | 346 | NSString *expectedEmbedCode = 347 | @""; 349 | [[mockWebView stub] evaluateJavaScript:@"player.getVideoEmbedCode();" 350 | completionHandler:[OCMArg invokeBlockWithArgs:@"", NSNull.null, nil]]; 352 | 353 | [playerView videoEmbedCode:^(NSString * _Nullable result, NSError * _Nullable error) { 354 | XCTAssertEqual(expectedEmbedCode, result); 355 | [expectation fulfill]; 356 | }]; 357 | 358 | [self waitForExpectations:@[expectation] timeout:1.0]; 359 | } 360 | 361 | #pragma mark - Testing catching non-embed URLs 362 | 363 | - (void)testCatchingEmbedUrls { 364 | NSURL *youTubeEmbed = 365 | [NSURL URLWithString:@"https://www.youtube.com/embed/M7lc1UVf-VE?showinfo=0"]; 366 | NSURLRequest *request = [[NSURLRequest alloc] initWithURL:youTubeEmbed]; 367 | 368 | id actionMock = [OCMockObject mockForClass:[WKNavigationAction class]]; 369 | OCMStub([actionMock request]).andReturn(request); 370 | 371 | // Application should NOT open the browser to the embed URL 372 | id mockApplication = [OCMockObject partialMockForObject:[UIApplication sharedApplication]]; 373 | [[mockApplication reject] openURL:youTubeEmbed options:[OCMArg any] completionHandler:[OCMArg any]]; 374 | 375 | [(id) playerView webView:mockWebView decidePolicyForNavigationAction:actionMock decisionHandler:^(WKNavigationActionPolicy decision) { 376 | XCTAssertEqual(WKNavigationActionPolicyAllow, decision, @"UIWebView should navigate to embed URL without opening browser"); 377 | }]; 378 | 379 | [mockApplication verify]; 380 | [mockApplication stopMocking]; 381 | } 382 | 383 | - (void)testCatchingNonEmbedUrls { 384 | NSURL *supportUrl = 385 | [NSURL URLWithString:@"https://support.google.com/youtube/answer/3037019?p=player_error1&rd=1"]; 386 | NSURLRequest *request = [[NSURLRequest alloc] initWithURL:supportUrl]; 387 | 388 | id actionMock = [OCMockObject mockForClass:[WKNavigationAction class]]; 389 | OCMStub([actionMock request]).andReturn(request); 390 | 391 | id mockApplication = [OCMockObject partialMockForObject:[UIApplication sharedApplication]]; 392 | [[mockApplication expect] openURL:supportUrl options:[OCMArg any] completionHandler:[OCMArg any]]; 393 | 394 | [(id) playerView webView:mockWebView decidePolicyForNavigationAction:actionMock decisionHandler:^(WKNavigationActionPolicy decision) { 395 | XCTAssertEqual(WKNavigationActionPolicyCancel, decision, @"UIWebView should navigate to embed URL without opening browser"); 396 | }]; 397 | 398 | [mockApplication verify]; 399 | [mockApplication stopMocking]; 400 | } 401 | 402 | @end 403 | -------------------------------------------------------------------------------- /Sources/YTPlayerView.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #import 16 | #import 17 | 18 | @class YTPlayerView; 19 | 20 | /** These enums represent the state of the current video in the player. */ 21 | typedef NS_ENUM(NSInteger, YTPlayerState) { 22 | kYTPlayerStateUnstarted, 23 | kYTPlayerStateEnded, 24 | kYTPlayerStatePlaying, 25 | kYTPlayerStatePaused, 26 | kYTPlayerStateBuffering, 27 | kYTPlayerStateCued, 28 | kYTPlayerStateUnknown 29 | }; 30 | 31 | /** These enums represent the resolution of the currently loaded video. */ 32 | typedef NS_ENUM(NSInteger, YTPlaybackQuality) { 33 | kYTPlaybackQualitySmall, 34 | kYTPlaybackQualityMedium, 35 | kYTPlaybackQualityLarge, 36 | kYTPlaybackQualityHD720, 37 | kYTPlaybackQualityHD1080, 38 | kYTPlaybackQualityHighRes, 39 | kYTPlaybackQualityAuto, /** Addition for YouTube Live Events. */ 40 | kYTPlaybackQualityDefault, 41 | kYTPlaybackQualityUnknown /** This should never be returned. It is here for future proofing. */ 42 | }; 43 | 44 | /** These enums represent error codes thrown by the player. */ 45 | typedef NS_ENUM(NSInteger, YTPlayerError) { 46 | kYTPlayerErrorInvalidParam, 47 | kYTPlayerErrorHTML5Error, 48 | kYTPlayerErrorVideoNotFound, // Functionally equivalent error codes 100 and 49 | // 105 have been collapsed into |kYTPlayerErrorVideoNotFound|. 50 | kYTPlayerErrorNotEmbeddable, // Functionally equivalent error codes 101 and 51 | // 150 have been collapsed into |kYTPlayerErrorNotEmbeddable|. 52 | kYTPlayerErrorUnknown 53 | }; 54 | 55 | /** Completion handlers for player API calls. */ 56 | typedef void (^YTIntCompletionHandler)(int result, NSError *_Nullable error); 57 | typedef void (^YTFloatCompletionHandler)(float result, NSError *_Nullable error); 58 | typedef void (^YTDoubleCompletionHandler)(double result, NSError *_Nullable error); 59 | typedef void (^YTStringCompletionHandler)(NSString *_Nullable result, NSError *_Nullable error); 60 | typedef void (^YTArrayCompletionHandler)(NSArray *_Nullable result, NSError *_Nullable error); 61 | typedef void (^YTURLCompletionHandler)(NSURL *_Nullable result, NSError *_Nullable error); 62 | typedef void (^YTPlayerStateCompletionHandler)(YTPlayerState result, NSError *_Nullable error); 63 | typedef void (^YTPlaybackQualityCompletionHandler)(YTPlaybackQuality result, 64 | NSError *_Nullable error); 65 | 66 | /** 67 | * A delegate for ViewControllers to respond to YouTube player events outside 68 | * of the view, such as changes to video playback state or playback errors. 69 | * The callback functions correlate to the events fired by the IFrame API. 70 | * For the full documentation, see the IFrame documentation here: 71 | * https://developers.google.com/youtube/iframe_api_reference#Events 72 | */ 73 | @protocol YTPlayerViewDelegate 74 | 75 | @optional 76 | /** 77 | * Invoked when the player view is ready to receive API calls. 78 | * 79 | * @param playerView The YTPlayerView instance that has become ready. 80 | */ 81 | - (void)playerViewDidBecomeReady:(nonnull YTPlayerView *)playerView; 82 | 83 | /** 84 | * Callback invoked when player state has changed, e.g. stopped or started playback. 85 | * 86 | * @param playerView The YTPlayerView instance where playback state has changed. 87 | * @param state YTPlayerState designating the new playback state. 88 | */ 89 | - (void)playerView:(nonnull YTPlayerView *)playerView didChangeToState:(YTPlayerState)state; 90 | 91 | /** 92 | * Callback invoked when playback quality has changed. 93 | * 94 | * @param playerView The YTPlayerView instance where playback quality has changed. 95 | * @param quality YTPlaybackQuality designating the new playback quality. 96 | */ 97 | - (void)playerView:(nonnull YTPlayerView *)playerView didChangeToQuality:(YTPlaybackQuality)quality; 98 | 99 | /** 100 | * Callback invoked when an error has occured. 101 | * 102 | * @param playerView The YTPlayerView instance where the error has occurred. 103 | * @param error YTPlayerError containing the error state. 104 | */ 105 | - (void)playerView:(nonnull YTPlayerView *)playerView receivedError:(YTPlayerError)error; 106 | 107 | /** 108 | * Callback invoked frequently when playBack is plaing. 109 | * 110 | * @param playerView The YTPlayerView instance where the error has occurred. 111 | * @param playTime float containing curretn playback time. 112 | */ 113 | - (void)playerView:(nonnull YTPlayerView *)playerView didPlayTime:(float)playTime; 114 | 115 | /** 116 | * Callback invoked when setting up the webview to allow custom colours so it fits in 117 | * with app color schemes. If a transparent view is required specify clearColor and 118 | * the code will handle the opacity etc. 119 | * 120 | * @param playerView The YTPlayerView instance where the error has occurred. 121 | * @return A color object that represents the background color of the webview. 122 | */ 123 | - (nonnull UIColor *)playerViewPreferredWebViewBackgroundColor:(nonnull YTPlayerView *)playerView; 124 | 125 | /** 126 | * Callback invoked when initially loading the YouTube iframe to the webview to display a custom 127 | * loading view while the player view is not ready. This loading view will be dismissed just before 128 | * -playerViewDidBecomeReady: callback is invoked. The loading view will be automatically resized 129 | * to cover the entire player view. 130 | * 131 | * The default implementation does not display any custom loading views so the player will display 132 | * a blank view with a background color of (-playerViewPreferredWebViewBackgroundColor:). 133 | * 134 | * Note that the custom loading view WILL NOT be displayed after iframe is loaded. It will be 135 | * handled by YouTube iframe API. This callback is just intended to tell users the view is actually 136 | * doing something while iframe is being loaded, which will take some time if users are in poor networks. 137 | * 138 | * @param playerView The YTPlayerView instance where the error has occurred. 139 | * @return A view object that will be displayed while YouTube iframe API is being loaded. 140 | * Pass nil to display no custom loading view. Default implementation returns nil. 141 | */ 142 | - (nullable UIView *)playerViewPreferredInitialLoadingView:(nonnull YTPlayerView *)playerView; 143 | 144 | @end 145 | 146 | /** 147 | * YTPlayerView is a custom UIView that client developers will use to include YouTube 148 | * videos in their iOS applications. It can be instantiated programmatically, or via 149 | * Interface Builder. Use the methods YTPlayerView::loadWithVideoId:, 150 | * YTPlayerView::loadWithPlaylistId: or their variants to set the video or playlist 151 | * to populate the view with. 152 | */ 153 | @interface YTPlayerView : UIView 154 | 155 | @property(nonatomic, nullable, readonly) WKWebView *webView; 156 | 157 | /** A delegate to be notified on playback events. */ 158 | @property(nonatomic, weak, nullable) id delegate; 159 | 160 | /** 161 | * This method loads the player with the given video ID. 162 | * This is a convenience method for calling YTPlayerView::loadPlayerWithVideoId:withPlayerVars: 163 | * without player variables. 164 | * 165 | * This method reloads the entire contents of the webview and regenerates its HTML contents. 166 | * To change the currently loaded video without reloading the entire webview, use the 167 | * YTPlayerView::cueVideoById:startSeconds: family of methods. 168 | * 169 | * @param videoId The YouTube video ID of the video to load in the player view. 170 | * @return YES if player has been configured correctly, NO otherwise. 171 | */ 172 | - (BOOL)loadWithVideoId:(nonnull NSString *)videoId; 173 | 174 | /** 175 | * This method loads the player with the given playlist ID. 176 | * This is a convenience method for calling YTPlayerView::loadWithPlaylistId:withPlayerVars: 177 | * without player variables. 178 | * 179 | * This method reloads the entire contents of the webview and regenerates its HTML contents. 180 | * To change the currently loaded video without reloading the entire webview, use the 181 | * YTPlayerView::cuePlaylistByPlaylistId:index:startSeconds: 182 | * family of methods. 183 | * 184 | * @param playlistId The YouTube playlist ID of the playlist to load in the player view. 185 | * @return YES if player has been configured correctly, NO otherwise. 186 | */ 187 | - (BOOL)loadWithPlaylistId:(nonnull NSString *)playlistId; 188 | 189 | /** 190 | * This method loads the player with the given video ID and player variables. Player variables 191 | * specify optional parameters for video playback. For instance, to play a YouTube 192 | * video inline, the following playerVars dictionary would be used: 193 | * 194 | * @code 195 | * @{ @"playsinline" : @1 }; 196 | * @endcode 197 | * 198 | * Note that when the documentation specifies a valid value as a number (typically 0, 1 or 2), 199 | * both strings and integers are valid values. The full list of parameters is defined at: 200 | * https://developers.google.com/youtube/player_parameters?playerVersion=HTML5. 201 | * 202 | * This method reloads the entire contents of the webview and regenerates its HTML contents. 203 | * To change the currently loaded video without reloading the entire webview, use the 204 | * YTPlayerView::cueVideoById:startSeconds: family of methods. 205 | * 206 | * @param videoId The YouTube video ID of the video to load in the player view. 207 | * @param playerVars An NSDictionary of player parameters. 208 | * @return YES if player has been configured correctly, NO otherwise. 209 | */ 210 | - (BOOL)loadWithVideoId:(nonnull NSString *)videoId playerVars:(nullable NSDictionary *)playerVars; 211 | 212 | /** 213 | * This method loads the player with the given playlist ID and player variables. Player variables 214 | * specify optional parameters for video playback. For instance, to play a YouTube 215 | * video inline, the following playerVars dictionary would be used: 216 | * 217 | * @code 218 | * @{ @"playsinline" : @1 }; 219 | * @endcode 220 | * 221 | * Note that when the documentation specifies a valid value as a number (typically 0, 1 or 2), 222 | * both strings and integers are valid values. The full list of parameters is defined at: 223 | * https://developers.google.com/youtube/player_parameters?playerVersion=HTML5. 224 | * 225 | * This method reloads the entire contents of the webview and regenerates its HTML contents. 226 | * To change the currently loaded video without reloading the entire webview, use the 227 | * YTPlayerView::cuePlaylistByPlaylistId:index:startSeconds: 228 | * family of methods. 229 | * 230 | * @param playlistId The YouTube playlist ID of the playlist to load in the player view. 231 | * @param playerVars An NSDictionary of player parameters. 232 | * @return YES if player has been configured correctly, NO otherwise. 233 | */ 234 | - (BOOL)loadWithPlaylistId:(nonnull NSString *)playlistId 235 | playerVars:(nullable NSDictionary *)playerVars; 236 | 237 | /** 238 | * This method loads an iframe player with the given player parameters. Usually you may want to use 239 | * -loadWithVideoId:playerVars: or -loadWithPlaylistId:playerVars: instead of this method does not handle 240 | * video_id or playlist_id at all. The full list of parameters is defined at: 241 | * https://developers.google.com/youtube/player_parameters?playerVersion=HTML5. 242 | * 243 | * @param additionalPlayerParams An NSDictionary of parameters in addition to required parameters 244 | * to instantiate the HTML5 player with. This differs depending on 245 | * whether a single video or playlist is being loaded. 246 | * @return YES if successful, NO if not. 247 | */ 248 | - (BOOL)loadWithPlayerParams:(nullable NSDictionary *)additionalPlayerParams; 249 | 250 | #pragma mark - Player controls 251 | 252 | // These methods correspond to their JavaScript equivalents as documented here: 253 | // https://developers.google.com/youtube/iframe_api_reference#Playback_controls 254 | 255 | /** 256 | * Starts or resumes playback on the loaded video. Corresponds to this method from 257 | * the JavaScript API: 258 | * https://developers.google.com/youtube/iframe_api_reference#playVideo 259 | */ 260 | - (void)playVideo; 261 | 262 | /** 263 | * Pauses playback on a playing video. Corresponds to this method from 264 | * the JavaScript API: 265 | * https://developers.google.com/youtube/iframe_api_reference#pauseVideo 266 | */ 267 | - (void)pauseVideo; 268 | 269 | /** 270 | * Stops playback on a playing video. Corresponds to this method from 271 | * the JavaScript API: 272 | * https://developers.google.com/youtube/iframe_api_reference#stopVideo 273 | */ 274 | - (void)stopVideo; 275 | 276 | /** 277 | * Seek to a given time on a playing video. Corresponds to this method from 278 | * the JavaScript API: 279 | * https://developers.google.com/youtube/iframe_api_reference#seekTo 280 | * 281 | * @param seekToSeconds The time in seconds to seek to in the loaded video. 282 | * @param allowSeekAhead Whether to make a new request to the server if the time is 283 | * outside what is currently buffered. Recommended to set to YES. 284 | */ 285 | - (void)seekToSeconds:(float)seekToSeconds allowSeekAhead:(BOOL)allowSeekAhead; 286 | 287 | #pragma mark - Cueing videos 288 | 289 | // Cueing functions for videos. These methods correspond to their JavaScript 290 | // equivalents as documented here: 291 | // https://developers.google.com/youtube/iframe_api_reference#Queueing_Functions 292 | 293 | /** 294 | * Cues a given video by its video ID for playback starting at the given time. 295 | * Cueing loads a video, but does not start video playback. This method 296 | * corresponds with its JavaScript API equivalent as documented here: 297 | * https://developers.google.com/youtube/iframe_api_reference#cueVideoById 298 | * 299 | * @param videoId A video ID to cue. 300 | * @param startSeconds Time in seconds to start the video when YTPlayerView::playVideo is called. 301 | */ 302 | - (void)cueVideoById:(nonnull NSString *)videoId 303 | startSeconds:(float)startSeconds; 304 | 305 | /** 306 | * Cues a given video by its video ID for playback starting and ending at the given times. 307 | * Cueing loads a video, but does not start video playback. This 308 | * method corresponds with its JavaScript API equivalent as documented here: 309 | * https://developers.google.com/youtube/iframe_api_reference#cueVideoById 310 | * 311 | * @param videoId A video ID to cue. 312 | * @param startSeconds Time in seconds to start the video when playVideo() is called. 313 | * @param endSeconds Time in seconds to end the video after it begins playing. 314 | */ 315 | - (void)cueVideoById:(nonnull NSString *)videoId 316 | startSeconds:(float)startSeconds 317 | endSeconds:(float)endSeconds; 318 | 319 | /** 320 | * Loads a given video by its video ID for playback starting at the given time. 321 | * Loading a video both loads it and begins playback. This method 322 | * corresponds with its JavaScript API equivalent as documented here: 323 | * https://developers.google.com/youtube/iframe_api_reference#loadVideoById 324 | * 325 | * @param videoId A video ID to load and begin playing. 326 | * @param startSeconds Time in seconds to start the video when it has loaded. 327 | */ 328 | - (void)loadVideoById:(nonnull NSString *)videoId 329 | startSeconds:(float)startSeconds; 330 | 331 | /** 332 | * Loads a given video by its video ID for playback starting and ending at the given times. 333 | * Loading a video both loads it and begins playback. This method 334 | * corresponds with its JavaScript API equivalent as documented here: 335 | * https://developers.google.com/youtube/iframe_api_reference#loadVideoById 336 | * 337 | * @param videoId A video ID to load and begin playing. 338 | * @param startSeconds Time in seconds to start the video when it has loaded. 339 | * @param endSeconds Time in seconds to end the video after it begins playing. 340 | */ 341 | - (void)loadVideoById:(nonnull NSString *)videoId 342 | startSeconds:(float)startSeconds 343 | endSeconds:(float)endSeconds; 344 | 345 | /** 346 | * Cues a given video by its URL on YouTube.com for playback starting at the given time. 347 | * Cueing loads a video, but does not start video playback. 348 | * This method corresponds with its JavaScript API equivalent as documented here: 349 | * https://developers.google.com/youtube/iframe_api_reference#cueVideoByUrl 350 | * 351 | * @param videoURL URL of a YouTube video to cue for playback. 352 | * @param startSeconds Time in seconds to start the video when YTPlayerView::playVideo is called. 353 | */ 354 | - (void)cueVideoByURL:(nonnull NSString *)videoURL 355 | startSeconds:(float)startSeconds; 356 | 357 | /** 358 | * Cues a given video by its URL on YouTube.com for playback starting at the given time. 359 | * Cueing loads a video, but does not start video playback. 360 | * This method corresponds with its JavaScript API equivalent as documented here: 361 | * https://developers.google.com/youtube/iframe_api_reference#cueVideoByUrl 362 | * 363 | * @param videoURL URL of a YouTube video to cue for playback. 364 | * @param startSeconds Time in seconds to start the video when YTPlayerView::playVideo is called. 365 | * @param endSeconds Time in seconds to end the video after it begins playing. 366 | */ 367 | - (void)cueVideoByURL:(nonnull NSString *)videoURL 368 | startSeconds:(float)startSeconds 369 | endSeconds:(float)endSeconds; 370 | 371 | /** 372 | * Loads a given video by its video ID for playback starting at the given time. 373 | * Loading a video both loads it and begins playback. This method 374 | * corresponds with its JavaScript API equivalent as documented here: 375 | * https://developers.google.com/youtube/iframe_api_reference#loadVideoByUrl 376 | * 377 | * @param videoURL URL of a YouTube video to load and play. 378 | * @param startSeconds Time in seconds to start the video when it has loaded. 379 | */ 380 | - (void)loadVideoByURL:(nonnull NSString *)videoURL 381 | startSeconds:(float)startSeconds; 382 | 383 | /** 384 | * Loads a given video by its video ID for playback starting and ending at the given times. 385 | * Loading a video both loads it and begins playback. This method 386 | * corresponds with its JavaScript API equivalent as documented here: 387 | * https://developers.google.com/youtube/iframe_api_reference#loadVideoByUrl 388 | * 389 | * @param videoURL URL of a YouTube video to load and play. 390 | * @param startSeconds Time in seconds to start the video when it has loaded. 391 | * @param endSeconds Time in seconds to end the video after it begins playing. 392 | */ 393 | - (void)loadVideoByURL:(nonnull NSString *)videoURL 394 | startSeconds:(float)startSeconds 395 | endSeconds:(float)endSeconds; 396 | 397 | #pragma mark - Cueing functions for playlists 398 | 399 | // Cueing functions for playlists. These methods correspond to 400 | // the JavaScript methods defined here: 401 | // https://developers.google.com/youtube/js_api_reference#Playlist_Queueing_Functions 402 | 403 | /** 404 | * Cues a given playlist with the given ID. The |index| parameter specifies the 0-indexed 405 | * position of the first video to play, starting at the given time. Cueing loads a playlist, 406 | * but does not start video playback. This method corresponds with its JavaScript API equivalent as documented here: 407 | * https://developers.google.com/youtube/iframe_api_reference#cuePlaylist 408 | * 409 | * @param playlistId Playlist ID of a YouTube playlist to cue. 410 | * @param index A 0-indexed position specifying the first video to play. 411 | * @param startSeconds Time in seconds to start the video when YTPlayerView::playVideo is called. 412 | */ 413 | - (void)cuePlaylistByPlaylistId:(nonnull NSString *)playlistId 414 | index:(int)index 415 | startSeconds:(float)startSeconds; 416 | 417 | /** 418 | * Cues a playlist of videos with the given video IDs. The |index| parameter specifies the 419 | * 0-indexed position of the first video to play, starting at the given time. 420 | * Cueing loads a playlist, but does not start video playback. This method 421 | * corresponds with its JavaScript API equivalent as documented here: 422 | * https://developers.google.com/youtube/iframe_api_reference#cuePlaylist 423 | * 424 | * @param videoIds An NSArray of video IDs to compose the playlist of. 425 | * @param index A 0-indexed position specifying the first video to play. 426 | * @param startSeconds Time in seconds to start the video when YTPlayerView::playVideo is called. 427 | */ 428 | - (void)cuePlaylistByVideos:(nonnull NSArray *)videoIds 429 | index:(int)index 430 | startSeconds:(float)startSeconds; 431 | 432 | /** 433 | * Loads a given playlist with the given ID. The |index| parameter specifies the 0-indexed 434 | * position of the first video to play, starting at the given time. Loading a playlist starts video playback. This method 435 | * corresponds with its JavaScript API equivalent as documented here: 436 | * https://developers.google.com/youtube/iframe_api_reference#loadPlaylist 437 | * 438 | * @param playlistId Playlist ID of a YouTube playlist to cue. 439 | * @param index A 0-indexed position specifying the first video to play. 440 | * @param startSeconds Time in seconds to start the video when YTPlayerView::playVideo is called. 441 | */ 442 | - (void)loadPlaylistByPlaylistId:(nonnull NSString *)playlistId 443 | index:(int)index 444 | startSeconds:(float)startSeconds; 445 | 446 | /** 447 | * Loads a playlist of videos with the given video IDs. The |index| parameter specifies the 448 | * 0-indexed position of the first video to play, starting at the given time. Loading a playlist starts video playback. 449 | * This method corresponds with its JavaScript API equivalent as documented here: 450 | * https://developers.google.com/youtube/iframe_api_reference#loadPlaylist 451 | * 452 | * @param videoIds An NSArray of video IDs to compose the playlist of. 453 | * @param index A 0-indexed position specifying the first video to play. 454 | * @param startSeconds Time in seconds to start the video when YTPlayerView::playVideo is called. 455 | */ 456 | - (void)loadPlaylistByVideos:(nonnull NSArray *)videoIds 457 | index:(int)index 458 | startSeconds:(float)startSeconds; 459 | 460 | #pragma mark - Playing a video in a playlist 461 | 462 | // These methods correspond to the JavaScript API as defined under the 463 | // "Playing a video in a playlist" section here: 464 | // https://developers.google.com/youtube/iframe_api_reference#Playback_status 465 | 466 | /** 467 | * Loads and plays the next video in the playlist. Corresponds to this method from 468 | * the JavaScript API: 469 | * https://developers.google.com/youtube/iframe_api_reference#nextVideo 470 | */ 471 | - (void)nextVideo; 472 | 473 | /** 474 | * Loads and plays the previous video in the playlist. Corresponds to this method from 475 | * the JavaScript API: 476 | * https://developers.google.com/youtube/iframe_api_reference#previousVideo 477 | */ 478 | - (void)previousVideo; 479 | 480 | /** 481 | * Loads and plays the video at the given 0-indexed position in the playlist. 482 | * Corresponds to this method from the JavaScript API: 483 | * https://developers.google.com/youtube/iframe_api_reference#playVideoAt 484 | * 485 | * @param index The 0-indexed position of the video in the playlist to load and play. 486 | */ 487 | - (void)playVideoAt:(int)index; 488 | 489 | #pragma mark - Setting the playback rate 490 | 491 | /** 492 | * Gets the playback rate. The default value is 1.0, which represents a video 493 | * playing at normal speed. Other values may include 0.25 or 0.5 for slower 494 | * speeds, and 1.5 or 2.0 for faster speeds. This method corresponds to the 495 | * JavaScript API defined here: 496 | * https://developers.google.com/youtube/iframe_api_reference#getPlaybackRate 497 | * @param completionHandler async callback block that contains a float value representing the current value 498 | * or an error. 499 | */ 500 | - (void)playbackRate:(_Nullable YTFloatCompletionHandler)completionHandler; 501 | 502 | /** 503 | * Sets the playback rate. The default value is 1.0, which represents a video 504 | * playing at normal speed. Other values may include 0.25 or 0.5 for slower 505 | * speeds, and 1.5 or 2.0 for faster speeds. To fetch a list of valid values for 506 | * this method, call YTPlayerView::getAvailablePlaybackRates. This method does not 507 | * guarantee that the playback rate will change. 508 | * This method corresponds to the JavaScript API defined here: 509 | * https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate 510 | * 511 | * @param suggestedRate A playback rate to suggest for the player. 512 | */ 513 | - (void)setPlaybackRate:(float)suggestedRate; 514 | 515 | /** 516 | * Gets a list of the valid playback rates, useful in conjunction with 517 | * YTPlayerView::setPlaybackRate. This method corresponds to the 518 | * JavaScript API defined here: 519 | * https://developers.google.com/youtube/iframe_api_reference#getPlaybackRate 520 | * 521 | * @param completionHandler async callback block that contains an array containing the available 522 | * playback rates or an error. 523 | */ 524 | - (void)availablePlaybackRates:(_Nullable YTArrayCompletionHandler)completionHandler; 525 | 526 | #pragma mark - Setting playback behavior for playlists 527 | 528 | /** 529 | * Sets whether the player should loop back to the first video in the playlist 530 | * after it has finished playing the last video. This method corresponds to the 531 | * JavaScript API defined here: 532 | * https://developers.google.com/youtube/iframe_api_reference#loopPlaylist 533 | * 534 | * @param loop A boolean representing whether the player should loop. 535 | */ 536 | - (void)setLoop:(BOOL)loop; 537 | 538 | /** 539 | * Sets whether the player should shuffle through the playlist. This method 540 | * corresponds to the JavaScript API defined here: 541 | * https://developers.google.com/youtube/iframe_api_reference#shufflePlaylist 542 | * 543 | * @param shuffle A boolean representing whether the player should 544 | * shuffle through the playlist. 545 | */ 546 | - (void)setShuffle:(BOOL)shuffle; 547 | 548 | #pragma mark - Playback status 549 | // These methods correspond to the JavaScript methods defined here: 550 | // https://developers.google.com/youtube/js_api_reference#Playback_status 551 | 552 | /** 553 | * Returns a number between 0 and 1 that specifies the percentage of the video 554 | * that the player shows as buffered. This method corresponds to the 555 | * JavaScript API defined here: 556 | * https://developers.google.com/youtube/iframe_api_reference#getVideoLoadedFraction 557 | * 558 | * @param completionHandler async callback block that contains a float number with the result or an error. 559 | */ 560 | - (void)videoLoadedFraction:(_Nullable YTFloatCompletionHandler)completionHandler; 561 | 562 | /** 563 | * Returns the state of the player. This method corresponds to the 564 | * JavaScript API defined here: 565 | * https://developers.google.com/youtube/iframe_api_reference#getPlayerState 566 | * 567 | * @param completionHandler async callback block that contains a YTPlayerState enum value 568 | * with the current player state or an error. 569 | */ 570 | - (void)playerState:(_Nullable YTPlayerStateCompletionHandler)completionHandler; 571 | 572 | /** 573 | * Returns the elapsed time in seconds since the video started playing. This 574 | * method corresponds to the JavaScript API defined here: 575 | * https://developers.google.com/youtube/iframe_api_reference#getCurrentTime 576 | * 577 | * @param completionHandler async callback block that contains float number with the result or an error. 578 | */ 579 | - (void)currentTime:(_Nullable YTFloatCompletionHandler)completionHandler; 580 | 581 | #pragma mark - Retrieving video information 582 | 583 | // Retrieving video information. These methods correspond to the JavaScript 584 | // methods defined here: 585 | // https://developers.google.com/youtube/js_api_reference#Retrieving_video_information 586 | 587 | /** 588 | * Returns the duration in seconds since the video of the video. This 589 | * method corresponds to the JavaScript API defined here: 590 | * https://developers.google.com/youtube/iframe_api_reference#getDuration 591 | * 592 | * @param completionHandler async callback block that contains a double number 593 | * with the duration of the video or an error. 594 | */ 595 | - (void)duration:(_Nullable YTDoubleCompletionHandler)completionHandler; 596 | 597 | /** 598 | * Returns the YouTube.com URL for the video. This method corresponds 599 | * to the JavaScript API defined here: 600 | * https://developers.google.com/youtube/iframe_api_reference#getVideoUrl 601 | * 602 | * @param completionHandler async callback block that contains the video URL as NSURL or an error. 603 | */ 604 | - (void)videoUrl:(_Nullable YTURLCompletionHandler)completionHandler; 605 | 606 | /** 607 | * Returns the embed code for the current video. This method corresponds 608 | * to the JavaScript API defined here: 609 | * https://developers.google.com/youtube/iframe_api_reference#getVideoEmbedCode 610 | * 611 | * @param completionHandler async callback block that contains a string with the current video 612 | * embed code or an error. 613 | */ 614 | - (void)videoEmbedCode:(_Nullable YTStringCompletionHandler)completionHandler; 615 | 616 | #pragma mark - Retrieving playlist information 617 | 618 | // Retrieving playlist information. These methods correspond to the 619 | // JavaScript defined here: 620 | // https://developers.google.com/youtube/js_api_reference#Retrieving_playlist_information 621 | 622 | /** 623 | * Returns an ordered array of video IDs in the playlist. This method corresponds 624 | * to the JavaScript API defined here: 625 | * https://developers.google.com/youtube/iframe_api_reference#getPlaylist 626 | * 627 | * @param completionHandler async callback block that contains an array of video IDs in the playlist 628 | * or an error. 629 | */ 630 | - (void)playlist:(_Nullable YTArrayCompletionHandler)completionHandler; 631 | 632 | /** 633 | * Returns the 0-based index of the currently playing item in the playlist. 634 | * This method corresponds to the JavaScript API defined here: 635 | * https://developers.google.com/youtube/iframe_api_reference#getPlaylistIndex 636 | * 637 | * @param completionHandler async callback block that contains the int value of the current playing item 638 | * in a playlist or an error. 639 | */ 640 | - (void)playlistIndex:(_Nullable YTIntCompletionHandler)completionHandler; 641 | 642 | #pragma mark - Exposed for Testing 643 | 644 | /** 645 | * Removes the internal web view from this player view. 646 | * Intended to use for testing, should not be used in production code. 647 | */ 648 | - (void)removeWebView; 649 | 650 | @end 651 | -------------------------------------------------------------------------------- /Project/youtube-player-ios-example/youtube-player-ios-example/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 41 | 53 | 66 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 188 | 197 | 206 | 215 | 221 | 230 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | -------------------------------------------------------------------------------- /Project/youtube-player-ios-example/youtube-player-ios-example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2180CB475B2FD1A3D2966D84 /* Pods_youtube_player_ios_example_youtube_player_ios_exampleTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A35FB300D92E10D899B75FCF /* Pods_youtube_player_ios_example_youtube_player_ios_exampleTests.framework */; }; 11 | 4D6993E218E22E0C0073680F /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4D6993E118E22E0C0073680F /* Foundation.framework */; }; 12 | 4D6993E418E22E0C0073680F /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4D6993E318E22E0C0073680F /* CoreGraphics.framework */; }; 13 | 4D6993E618E22E0C0073680F /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4D6993E518E22E0C0073680F /* UIKit.framework */; }; 14 | 4D6993EC18E22E0C0073680F /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4D6993EA18E22E0C0073680F /* InfoPlist.strings */; }; 15 | 4D6993FA18E22E0C0073680F /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4D6993F918E22E0C0073680F /* Images.xcassets */; }; 16 | 4D69940118E22E0C0073680F /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4D69940018E22E0C0073680F /* XCTest.framework */; }; 17 | 4D69940218E22E0C0073680F /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4D6993E118E22E0C0073680F /* Foundation.framework */; }; 18 | 4D69940318E22E0C0073680F /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4D6993E518E22E0C0073680F /* UIKit.framework */; }; 19 | 4D69940B18E22E0C0073680F /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4D69940918E22E0C0073680F /* InfoPlist.strings */; }; 20 | 4D69940D18E22E0C0073680F /* youtube_player_ios_exampleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D69940C18E22E0C0073680F /* youtube_player_ios_exampleTests.m */; }; 21 | 4D69941D18E22EE10073680F /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D69941718E22EE10073680F /* AppDelegate.m */; }; 22 | 4D69941E18E22EE10073680F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4D69941818E22EE10073680F /* Main.storyboard */; }; 23 | 4D69941F18E22EE10073680F /* SingleVideoViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D69941A18E22EE10073680F /* SingleVideoViewController.m */; }; 24 | 4D69942018E22EE10073680F /* PlaylistViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D69941C18E22EE10073680F /* PlaylistViewController.m */; }; 25 | 4D69942218E22F000073680F /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D69942118E22F000073680F /* main.m */; }; 26 | 6F41FEE03BFB1D0A2789864C /* Pods_youtube_player_ios_example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5AD3007F2E09509E85D12F3E /* Pods_youtube_player_ios_example.framework */; }; 27 | CC3F4B7F2514D1B500AB0A15 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CC3F4B7E2514D1B500AB0A15 /* WebKit.framework */; }; 28 | /* End PBXBuildFile section */ 29 | 30 | /* Begin PBXContainerItemProxy section */ 31 | 4D69940418E22E0C0073680F /* PBXContainerItemProxy */ = { 32 | isa = PBXContainerItemProxy; 33 | containerPortal = 4D6993D618E22E0C0073680F /* Project object */; 34 | proxyType = 1; 35 | remoteGlobalIDString = 4D6993DD18E22E0C0073680F; 36 | remoteInfo = "youtube-player-ios-example"; 37 | }; 38 | /* End PBXContainerItemProxy section */ 39 | 40 | /* Begin PBXFileReference section */ 41 | 1FCEBFD61815652F2688F785 /* Pods-youtube-player-ios-example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-youtube-player-ios-example.debug.xcconfig"; path = "Target Support Files/Pods-youtube-player-ios-example/Pods-youtube-player-ios-example.debug.xcconfig"; sourceTree = ""; }; 42 | 2E9AA7CE5B23225F06EB9523 /* Pods-youtube-player-ios-example-youtube-player-ios-exampleTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-youtube-player-ios-example-youtube-player-ios-exampleTests.release.xcconfig"; path = "Target Support Files/Pods-youtube-player-ios-example-youtube-player-ios-exampleTests/Pods-youtube-player-ios-example-youtube-player-ios-exampleTests.release.xcconfig"; sourceTree = ""; }; 43 | 4D6993DE18E22E0C0073680F /* youtube-player-ios-example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "youtube-player-ios-example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 44 | 4D6993E118E22E0C0073680F /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 45 | 4D6993E318E22E0C0073680F /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 46 | 4D6993E518E22E0C0073680F /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 47 | 4D6993E918E22E0C0073680F /* youtube-player-ios-example-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "youtube-player-ios-example-Info.plist"; sourceTree = ""; }; 48 | 4D6993EB18E22E0C0073680F /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 49 | 4D6993EF18E22E0C0073680F /* youtube-player-ios-example-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "youtube-player-ios-example-Prefix.pch"; sourceTree = ""; }; 50 | 4D6993F918E22E0C0073680F /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 51 | 4D6993FF18E22E0C0073680F /* youtube-player-ios-exampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "youtube-player-ios-exampleTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 52 | 4D69940018E22E0C0073680F /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; 53 | 4D69940818E22E0C0073680F /* youtube-player-ios-exampleTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "youtube-player-ios-exampleTests-Info.plist"; sourceTree = ""; }; 54 | 4D69940A18E22E0C0073680F /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 55 | 4D69940C18E22E0C0073680F /* youtube_player_ios_exampleTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = youtube_player_ios_exampleTests.m; sourceTree = ""; }; 56 | 4D69941618E22EE10073680F /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 57 | 4D69941718E22EE10073680F /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 58 | 4D69941818E22EE10073680F /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; 59 | 4D69941918E22EE10073680F /* SingleVideoViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SingleVideoViewController.h; sourceTree = ""; }; 60 | 4D69941A18E22EE10073680F /* SingleVideoViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SingleVideoViewController.m; sourceTree = ""; }; 61 | 4D69941B18E22EE10073680F /* PlaylistViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PlaylistViewController.h; sourceTree = ""; }; 62 | 4D69941C18E22EE10073680F /* PlaylistViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PlaylistViewController.m; sourceTree = ""; }; 63 | 4D69942118E22F000073680F /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 64 | 5AD3007F2E09509E85D12F3E /* Pods_youtube_player_ios_example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_youtube_player_ios_example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 65 | 7D6080CDCBA15B2A906634FD /* Pods-youtube-player-ios-example-youtube-player-ios-exampleTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-youtube-player-ios-example-youtube-player-ios-exampleTests.debug.xcconfig"; path = "Target Support Files/Pods-youtube-player-ios-example-youtube-player-ios-exampleTests/Pods-youtube-player-ios-example-youtube-player-ios-exampleTests.debug.xcconfig"; sourceTree = ""; }; 66 | A35FB300D92E10D899B75FCF /* Pods_youtube_player_ios_example_youtube_player_ios_exampleTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_youtube_player_ios_example_youtube_player_ios_exampleTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 67 | B6E173A9BA3863A5A8E9B1DD /* Pods-youtube-player-ios-example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-youtube-player-ios-example.release.xcconfig"; path = "Target Support Files/Pods-youtube-player-ios-example/Pods-youtube-player-ios-example.release.xcconfig"; sourceTree = ""; }; 68 | CC3F4B7A2514D1A500AB0A15 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; 69 | CC3F4B7E2514D1B500AB0A15 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/System/iOSSupport/System/Library/Frameworks/WebKit.framework; sourceTree = DEVELOPER_DIR; }; 70 | /* End PBXFileReference section */ 71 | 72 | /* Begin PBXFrameworksBuildPhase section */ 73 | 4D6993DB18E22E0C0073680F /* Frameworks */ = { 74 | isa = PBXFrameworksBuildPhase; 75 | buildActionMask = 2147483647; 76 | files = ( 77 | 4D6993E418E22E0C0073680F /* CoreGraphics.framework in Frameworks */, 78 | 4D6993E618E22E0C0073680F /* UIKit.framework in Frameworks */, 79 | 4D6993E218E22E0C0073680F /* Foundation.framework in Frameworks */, 80 | 6F41FEE03BFB1D0A2789864C /* Pods_youtube_player_ios_example.framework in Frameworks */, 81 | ); 82 | runOnlyForDeploymentPostprocessing = 0; 83 | }; 84 | 4D6993FC18E22E0C0073680F /* Frameworks */ = { 85 | isa = PBXFrameworksBuildPhase; 86 | buildActionMask = 2147483647; 87 | files = ( 88 | CC3F4B7F2514D1B500AB0A15 /* WebKit.framework in Frameworks */, 89 | 4D69940118E22E0C0073680F /* XCTest.framework in Frameworks */, 90 | 4D69940318E22E0C0073680F /* UIKit.framework in Frameworks */, 91 | 4D69940218E22E0C0073680F /* Foundation.framework in Frameworks */, 92 | 2180CB475B2FD1A3D2966D84 /* Pods_youtube_player_ios_example_youtube_player_ios_exampleTests.framework in Frameworks */, 93 | ); 94 | runOnlyForDeploymentPostprocessing = 0; 95 | }; 96 | /* End PBXFrameworksBuildPhase section */ 97 | 98 | /* Begin PBXGroup section */ 99 | 4D6993D518E22E0C0073680F = { 100 | isa = PBXGroup; 101 | children = ( 102 | 4D6993E718E22E0C0073680F /* youtube-player-ios-example */, 103 | 4D69940618E22E0C0073680F /* youtube-player-ios-exampleTests */, 104 | 4D6993E018E22E0C0073680F /* Frameworks */, 105 | 4D6993DF18E22E0C0073680F /* Products */, 106 | 8B9E7F1AD2D13D00E89972CE /* Pods */, 107 | ); 108 | sourceTree = ""; 109 | }; 110 | 4D6993DF18E22E0C0073680F /* Products */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | 4D6993DE18E22E0C0073680F /* youtube-player-ios-example.app */, 114 | 4D6993FF18E22E0C0073680F /* youtube-player-ios-exampleTests.xctest */, 115 | ); 116 | name = Products; 117 | sourceTree = ""; 118 | }; 119 | 4D6993E018E22E0C0073680F /* Frameworks */ = { 120 | isa = PBXGroup; 121 | children = ( 122 | CC3F4B7A2514D1A500AB0A15 /* WebKit.framework */, 123 | CC3F4B7E2514D1B500AB0A15 /* WebKit.framework */, 124 | 4D6993E118E22E0C0073680F /* Foundation.framework */, 125 | 4D6993E318E22E0C0073680F /* CoreGraphics.framework */, 126 | 4D6993E518E22E0C0073680F /* UIKit.framework */, 127 | 4D69940018E22E0C0073680F /* XCTest.framework */, 128 | 5AD3007F2E09509E85D12F3E /* Pods_youtube_player_ios_example.framework */, 129 | A35FB300D92E10D899B75FCF /* Pods_youtube_player_ios_example_youtube_player_ios_exampleTests.framework */, 130 | ); 131 | name = Frameworks; 132 | sourceTree = ""; 133 | }; 134 | 4D6993E718E22E0C0073680F /* youtube-player-ios-example */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | 4D69941618E22EE10073680F /* AppDelegate.h */, 138 | 4D69941718E22EE10073680F /* AppDelegate.m */, 139 | 4D69941818E22EE10073680F /* Main.storyboard */, 140 | 4D69941918E22EE10073680F /* SingleVideoViewController.h */, 141 | 4D69941A18E22EE10073680F /* SingleVideoViewController.m */, 142 | 4D69941B18E22EE10073680F /* PlaylistViewController.h */, 143 | 4D69941C18E22EE10073680F /* PlaylistViewController.m */, 144 | 4D6993F918E22E0C0073680F /* Images.xcassets */, 145 | 4D6993E818E22E0C0073680F /* Supporting Files */, 146 | ); 147 | path = "youtube-player-ios-example"; 148 | sourceTree = ""; 149 | }; 150 | 4D6993E818E22E0C0073680F /* Supporting Files */ = { 151 | isa = PBXGroup; 152 | children = ( 153 | 4D6993E918E22E0C0073680F /* youtube-player-ios-example-Info.plist */, 154 | 4D6993EA18E22E0C0073680F /* InfoPlist.strings */, 155 | 4D69942118E22F000073680F /* main.m */, 156 | 4D6993EF18E22E0C0073680F /* youtube-player-ios-example-Prefix.pch */, 157 | ); 158 | name = "Supporting Files"; 159 | sourceTree = ""; 160 | }; 161 | 4D69940618E22E0C0073680F /* youtube-player-ios-exampleTests */ = { 162 | isa = PBXGroup; 163 | children = ( 164 | 4D69940C18E22E0C0073680F /* youtube_player_ios_exampleTests.m */, 165 | 4D69940718E22E0C0073680F /* Supporting Files */, 166 | ); 167 | path = "youtube-player-ios-exampleTests"; 168 | sourceTree = ""; 169 | }; 170 | 4D69940718E22E0C0073680F /* Supporting Files */ = { 171 | isa = PBXGroup; 172 | children = ( 173 | 4D69940818E22E0C0073680F /* youtube-player-ios-exampleTests-Info.plist */, 174 | 4D69940918E22E0C0073680F /* InfoPlist.strings */, 175 | ); 176 | name = "Supporting Files"; 177 | sourceTree = ""; 178 | }; 179 | 8B9E7F1AD2D13D00E89972CE /* Pods */ = { 180 | isa = PBXGroup; 181 | children = ( 182 | 1FCEBFD61815652F2688F785 /* Pods-youtube-player-ios-example.debug.xcconfig */, 183 | B6E173A9BA3863A5A8E9B1DD /* Pods-youtube-player-ios-example.release.xcconfig */, 184 | 7D6080CDCBA15B2A906634FD /* Pods-youtube-player-ios-example-youtube-player-ios-exampleTests.debug.xcconfig */, 185 | 2E9AA7CE5B23225F06EB9523 /* Pods-youtube-player-ios-example-youtube-player-ios-exampleTests.release.xcconfig */, 186 | ); 187 | path = Pods; 188 | sourceTree = ""; 189 | }; 190 | /* End PBXGroup section */ 191 | 192 | /* Begin PBXNativeTarget section */ 193 | 4D6993DD18E22E0C0073680F /* youtube-player-ios-example */ = { 194 | isa = PBXNativeTarget; 195 | buildConfigurationList = 4D69941018E22E0C0073680F /* Build configuration list for PBXNativeTarget "youtube-player-ios-example" */; 196 | buildPhases = ( 197 | A9A19468858E95AB3E3EA696 /* [CP] Check Pods Manifest.lock */, 198 | 4D6993DA18E22E0C0073680F /* Sources */, 199 | 4D6993DB18E22E0C0073680F /* Frameworks */, 200 | 4D6993DC18E22E0C0073680F /* Resources */, 201 | 1048E363A4589E0F2A05EB21 /* [CP] Embed Pods Frameworks */, 202 | ); 203 | buildRules = ( 204 | ); 205 | dependencies = ( 206 | ); 207 | name = "youtube-player-ios-example"; 208 | productName = "youtube-player-ios-example"; 209 | productReference = 4D6993DE18E22E0C0073680F /* youtube-player-ios-example.app */; 210 | productType = "com.apple.product-type.application"; 211 | }; 212 | 4D6993FE18E22E0C0073680F /* youtube-player-ios-exampleTests */ = { 213 | isa = PBXNativeTarget; 214 | buildConfigurationList = 4D69941318E22E0C0073680F /* Build configuration list for PBXNativeTarget "youtube-player-ios-exampleTests" */; 215 | buildPhases = ( 216 | F01CA43A8D7FA81FC3259E7C /* [CP] Check Pods Manifest.lock */, 217 | 4D6993FB18E22E0C0073680F /* Sources */, 218 | 4D6993FC18E22E0C0073680F /* Frameworks */, 219 | 4D6993FD18E22E0C0073680F /* Resources */, 220 | EE525424A777666033A108F7 /* [CP] Embed Pods Frameworks */, 221 | ); 222 | buildRules = ( 223 | ); 224 | dependencies = ( 225 | 4D69940518E22E0C0073680F /* PBXTargetDependency */, 226 | ); 227 | name = "youtube-player-ios-exampleTests"; 228 | productName = "youtube-player-ios-exampleTests"; 229 | productReference = 4D6993FF18E22E0C0073680F /* youtube-player-ios-exampleTests.xctest */; 230 | productType = "com.apple.product-type.bundle.unit-test"; 231 | }; 232 | /* End PBXNativeTarget section */ 233 | 234 | /* Begin PBXProject section */ 235 | 4D6993D618E22E0C0073680F /* Project object */ = { 236 | isa = PBXProject; 237 | attributes = { 238 | LastUpgradeCheck = 1200; 239 | ORGANIZATIONNAME = "YouTube Developer Relations"; 240 | TargetAttributes = { 241 | 4D6993FE18E22E0C0073680F = { 242 | TestTargetID = 4D6993DD18E22E0C0073680F; 243 | }; 244 | }; 245 | }; 246 | buildConfigurationList = 4D6993D918E22E0C0073680F /* Build configuration list for PBXProject "youtube-player-ios-example" */; 247 | compatibilityVersion = "Xcode 3.2"; 248 | developmentRegion = English; 249 | hasScannedForEncodings = 0; 250 | knownRegions = ( 251 | English, 252 | en, 253 | Base, 254 | ); 255 | mainGroup = 4D6993D518E22E0C0073680F; 256 | productRefGroup = 4D6993DF18E22E0C0073680F /* Products */; 257 | projectDirPath = ""; 258 | projectRoot = ""; 259 | targets = ( 260 | 4D6993DD18E22E0C0073680F /* youtube-player-ios-example */, 261 | 4D6993FE18E22E0C0073680F /* youtube-player-ios-exampleTests */, 262 | ); 263 | }; 264 | /* End PBXProject section */ 265 | 266 | /* Begin PBXResourcesBuildPhase section */ 267 | 4D6993DC18E22E0C0073680F /* Resources */ = { 268 | isa = PBXResourcesBuildPhase; 269 | buildActionMask = 2147483647; 270 | files = ( 271 | 4D69941E18E22EE10073680F /* Main.storyboard in Resources */, 272 | 4D6993FA18E22E0C0073680F /* Images.xcassets in Resources */, 273 | 4D6993EC18E22E0C0073680F /* InfoPlist.strings in Resources */, 274 | ); 275 | runOnlyForDeploymentPostprocessing = 0; 276 | }; 277 | 4D6993FD18E22E0C0073680F /* Resources */ = { 278 | isa = PBXResourcesBuildPhase; 279 | buildActionMask = 2147483647; 280 | files = ( 281 | 4D69940B18E22E0C0073680F /* InfoPlist.strings in Resources */, 282 | ); 283 | runOnlyForDeploymentPostprocessing = 0; 284 | }; 285 | /* End PBXResourcesBuildPhase section */ 286 | 287 | /* Begin PBXShellScriptBuildPhase section */ 288 | 1048E363A4589E0F2A05EB21 /* [CP] Embed Pods Frameworks */ = { 289 | isa = PBXShellScriptBuildPhase; 290 | buildActionMask = 2147483647; 291 | files = ( 292 | ); 293 | inputPaths = ( 294 | "${PODS_ROOT}/Target Support Files/Pods-youtube-player-ios-example/Pods-youtube-player-ios-example-frameworks.sh", 295 | "${BUILT_PRODUCTS_DIR}/youtube-ios-player-helper/youtube_ios_player_helper.framework", 296 | ); 297 | name = "[CP] Embed Pods Frameworks"; 298 | outputPaths = ( 299 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/youtube_ios_player_helper.framework", 300 | ); 301 | runOnlyForDeploymentPostprocessing = 0; 302 | shellPath = /bin/sh; 303 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-youtube-player-ios-example/Pods-youtube-player-ios-example-frameworks.sh\"\n"; 304 | showEnvVarsInLog = 0; 305 | }; 306 | A9A19468858E95AB3E3EA696 /* [CP] Check Pods Manifest.lock */ = { 307 | isa = PBXShellScriptBuildPhase; 308 | buildActionMask = 2147483647; 309 | files = ( 310 | ); 311 | inputFileListPaths = ( 312 | ); 313 | inputPaths = ( 314 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 315 | "${PODS_ROOT}/Manifest.lock", 316 | ); 317 | name = "[CP] Check Pods Manifest.lock"; 318 | outputFileListPaths = ( 319 | ); 320 | outputPaths = ( 321 | "$(DERIVED_FILE_DIR)/Pods-youtube-player-ios-example-checkManifestLockResult.txt", 322 | ); 323 | runOnlyForDeploymentPostprocessing = 0; 324 | shellPath = /bin/sh; 325 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 326 | showEnvVarsInLog = 0; 327 | }; 328 | EE525424A777666033A108F7 /* [CP] Embed Pods Frameworks */ = { 329 | isa = PBXShellScriptBuildPhase; 330 | buildActionMask = 2147483647; 331 | files = ( 332 | ); 333 | inputPaths = ( 334 | "${PODS_ROOT}/Target Support Files/Pods-youtube-player-ios-example-youtube-player-ios-exampleTests/Pods-youtube-player-ios-example-youtube-player-ios-exampleTests-frameworks.sh", 335 | "${BUILT_PRODUCTS_DIR}/youtube-ios-player-helper/youtube_ios_player_helper.framework", 336 | "${BUILT_PRODUCTS_DIR}/OCMock/OCMock.framework", 337 | ); 338 | name = "[CP] Embed Pods Frameworks"; 339 | outputPaths = ( 340 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/youtube_ios_player_helper.framework", 341 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OCMock.framework", 342 | ); 343 | runOnlyForDeploymentPostprocessing = 0; 344 | shellPath = /bin/sh; 345 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-youtube-player-ios-example-youtube-player-ios-exampleTests/Pods-youtube-player-ios-example-youtube-player-ios-exampleTests-frameworks.sh\"\n"; 346 | showEnvVarsInLog = 0; 347 | }; 348 | F01CA43A8D7FA81FC3259E7C /* [CP] Check Pods Manifest.lock */ = { 349 | isa = PBXShellScriptBuildPhase; 350 | buildActionMask = 2147483647; 351 | files = ( 352 | ); 353 | inputFileListPaths = ( 354 | ); 355 | inputPaths = ( 356 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 357 | "${PODS_ROOT}/Manifest.lock", 358 | ); 359 | name = "[CP] Check Pods Manifest.lock"; 360 | outputFileListPaths = ( 361 | ); 362 | outputPaths = ( 363 | "$(DERIVED_FILE_DIR)/Pods-youtube-player-ios-example-youtube-player-ios-exampleTests-checkManifestLockResult.txt", 364 | ); 365 | runOnlyForDeploymentPostprocessing = 0; 366 | shellPath = /bin/sh; 367 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 368 | showEnvVarsInLog = 0; 369 | }; 370 | /* End PBXShellScriptBuildPhase section */ 371 | 372 | /* Begin PBXSourcesBuildPhase section */ 373 | 4D6993DA18E22E0C0073680F /* Sources */ = { 374 | isa = PBXSourcesBuildPhase; 375 | buildActionMask = 2147483647; 376 | files = ( 377 | 4D69942218E22F000073680F /* main.m in Sources */, 378 | 4D69942018E22EE10073680F /* PlaylistViewController.m in Sources */, 379 | 4D69941D18E22EE10073680F /* AppDelegate.m in Sources */, 380 | 4D69941F18E22EE10073680F /* SingleVideoViewController.m in Sources */, 381 | ); 382 | runOnlyForDeploymentPostprocessing = 0; 383 | }; 384 | 4D6993FB18E22E0C0073680F /* Sources */ = { 385 | isa = PBXSourcesBuildPhase; 386 | buildActionMask = 2147483647; 387 | files = ( 388 | 4D69940D18E22E0C0073680F /* youtube_player_ios_exampleTests.m in Sources */, 389 | ); 390 | runOnlyForDeploymentPostprocessing = 0; 391 | }; 392 | /* End PBXSourcesBuildPhase section */ 393 | 394 | /* Begin PBXTargetDependency section */ 395 | 4D69940518E22E0C0073680F /* PBXTargetDependency */ = { 396 | isa = PBXTargetDependency; 397 | target = 4D6993DD18E22E0C0073680F /* youtube-player-ios-example */; 398 | targetProxy = 4D69940418E22E0C0073680F /* PBXContainerItemProxy */; 399 | }; 400 | /* End PBXTargetDependency section */ 401 | 402 | /* Begin PBXVariantGroup section */ 403 | 4D6993EA18E22E0C0073680F /* InfoPlist.strings */ = { 404 | isa = PBXVariantGroup; 405 | children = ( 406 | 4D6993EB18E22E0C0073680F /* en */, 407 | ); 408 | name = InfoPlist.strings; 409 | sourceTree = ""; 410 | }; 411 | 4D69940918E22E0C0073680F /* InfoPlist.strings */ = { 412 | isa = PBXVariantGroup; 413 | children = ( 414 | 4D69940A18E22E0C0073680F /* en */, 415 | ); 416 | name = InfoPlist.strings; 417 | sourceTree = ""; 418 | }; 419 | /* End PBXVariantGroup section */ 420 | 421 | /* Begin XCBuildConfiguration section */ 422 | 4D69940E18E22E0C0073680F /* Debug */ = { 423 | isa = XCBuildConfiguration; 424 | buildSettings = { 425 | ALWAYS_SEARCH_USER_PATHS = NO; 426 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 427 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 428 | CLANG_CXX_LIBRARY = "libc++"; 429 | CLANG_ENABLE_MODULES = YES; 430 | CLANG_ENABLE_OBJC_ARC = YES; 431 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 432 | CLANG_WARN_BOOL_CONVERSION = YES; 433 | CLANG_WARN_COMMA = YES; 434 | CLANG_WARN_CONSTANT_CONVERSION = YES; 435 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 436 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 437 | CLANG_WARN_EMPTY_BODY = YES; 438 | CLANG_WARN_ENUM_CONVERSION = YES; 439 | CLANG_WARN_INFINITE_RECURSION = YES; 440 | CLANG_WARN_INT_CONVERSION = YES; 441 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 442 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 443 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 444 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 445 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 446 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 447 | CLANG_WARN_STRICT_PROTOTYPES = YES; 448 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 449 | CLANG_WARN_UNREACHABLE_CODE = YES; 450 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 451 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 452 | COPY_PHASE_STRIP = NO; 453 | ENABLE_STRICT_OBJC_MSGSEND = YES; 454 | ENABLE_TESTABILITY = YES; 455 | GCC_C_LANGUAGE_STANDARD = gnu99; 456 | GCC_DYNAMIC_NO_PIC = NO; 457 | GCC_NO_COMMON_BLOCKS = YES; 458 | GCC_OPTIMIZATION_LEVEL = 0; 459 | GCC_PREPROCESSOR_DEFINITIONS = ( 460 | "DEBUG=1", 461 | "$(inherited)", 462 | ); 463 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 464 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 465 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 466 | GCC_WARN_UNDECLARED_SELECTOR = YES; 467 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 468 | GCC_WARN_UNUSED_FUNCTION = YES; 469 | GCC_WARN_UNUSED_VARIABLE = YES; 470 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 471 | ONLY_ACTIVE_ARCH = YES; 472 | SDKROOT = iphoneos; 473 | }; 474 | name = Debug; 475 | }; 476 | 4D69940F18E22E0C0073680F /* Release */ = { 477 | isa = XCBuildConfiguration; 478 | buildSettings = { 479 | ALWAYS_SEARCH_USER_PATHS = NO; 480 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 481 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 482 | CLANG_CXX_LIBRARY = "libc++"; 483 | CLANG_ENABLE_MODULES = YES; 484 | CLANG_ENABLE_OBJC_ARC = YES; 485 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 486 | CLANG_WARN_BOOL_CONVERSION = YES; 487 | CLANG_WARN_COMMA = YES; 488 | CLANG_WARN_CONSTANT_CONVERSION = YES; 489 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 490 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 491 | CLANG_WARN_EMPTY_BODY = YES; 492 | CLANG_WARN_ENUM_CONVERSION = YES; 493 | CLANG_WARN_INFINITE_RECURSION = YES; 494 | CLANG_WARN_INT_CONVERSION = YES; 495 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 496 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 497 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 498 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 499 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 500 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 501 | CLANG_WARN_STRICT_PROTOTYPES = YES; 502 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 503 | CLANG_WARN_UNREACHABLE_CODE = YES; 504 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 505 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 506 | COPY_PHASE_STRIP = YES; 507 | ENABLE_NS_ASSERTIONS = NO; 508 | ENABLE_STRICT_OBJC_MSGSEND = YES; 509 | GCC_C_LANGUAGE_STANDARD = gnu99; 510 | GCC_NO_COMMON_BLOCKS = YES; 511 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 512 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 513 | GCC_WARN_UNDECLARED_SELECTOR = YES; 514 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 515 | GCC_WARN_UNUSED_FUNCTION = YES; 516 | GCC_WARN_UNUSED_VARIABLE = YES; 517 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 518 | SDKROOT = iphoneos; 519 | VALIDATE_PRODUCT = YES; 520 | }; 521 | name = Release; 522 | }; 523 | 4D69941118E22E0C0073680F /* Debug */ = { 524 | isa = XCBuildConfiguration; 525 | baseConfigurationReference = 1FCEBFD61815652F2688F785 /* Pods-youtube-player-ios-example.debug.xcconfig */; 526 | buildSettings = { 527 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 528 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 529 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 530 | GCC_PREFIX_HEADER = "youtube-player-ios-example/youtube-player-ios-example-Prefix.pch"; 531 | INFOPLIST_FILE = "youtube-player-ios-example/youtube-player-ios-example-Info.plist"; 532 | PRODUCT_BUNDLE_IDENTIFIER = "com.youtube.demo.${PRODUCT_NAME:rfc1034identifier}"; 533 | PRODUCT_NAME = "$(TARGET_NAME)"; 534 | TARGETED_DEVICE_FAMILY = "1,2"; 535 | WRAPPER_EXTENSION = app; 536 | }; 537 | name = Debug; 538 | }; 539 | 4D69941218E22E0C0073680F /* Release */ = { 540 | isa = XCBuildConfiguration; 541 | baseConfigurationReference = B6E173A9BA3863A5A8E9B1DD /* Pods-youtube-player-ios-example.release.xcconfig */; 542 | buildSettings = { 543 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 544 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 545 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 546 | GCC_PREFIX_HEADER = "youtube-player-ios-example/youtube-player-ios-example-Prefix.pch"; 547 | INFOPLIST_FILE = "youtube-player-ios-example/youtube-player-ios-example-Info.plist"; 548 | PRODUCT_BUNDLE_IDENTIFIER = "com.youtube.demo.${PRODUCT_NAME:rfc1034identifier}"; 549 | PRODUCT_NAME = "$(TARGET_NAME)"; 550 | TARGETED_DEVICE_FAMILY = "1,2"; 551 | WRAPPER_EXTENSION = app; 552 | }; 553 | name = Release; 554 | }; 555 | 4D69941418E22E0C0073680F /* Debug */ = { 556 | isa = XCBuildConfiguration; 557 | baseConfigurationReference = 7D6080CDCBA15B2A906634FD /* Pods-youtube-player-ios-example-youtube-player-ios-exampleTests.debug.xcconfig */; 558 | buildSettings = { 559 | BUNDLE_LOADER = "$(TEST_HOST)"; 560 | FRAMEWORK_SEARCH_PATHS = ( 561 | "$(SDKROOT)/Developer/Library/Frameworks", 562 | "$(inherited)", 563 | "$(DEVELOPER_FRAMEWORKS_DIR)", 564 | ); 565 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 566 | GCC_PREFIX_HEADER = "youtube-player-ios-example/youtube-player-ios-example-Prefix.pch"; 567 | GCC_PREPROCESSOR_DEFINITIONS = ( 568 | "DEBUG=1", 569 | "$(inherited)", 570 | ); 571 | INFOPLIST_FILE = "youtube-player-ios-exampleTests/youtube-player-ios-exampleTests-Info.plist"; 572 | PRODUCT_NAME = "$(TARGET_NAME)"; 573 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/youtube-player-ios-example.app/youtube-player-ios-example"; 574 | WRAPPER_EXTENSION = xctest; 575 | }; 576 | name = Debug; 577 | }; 578 | 4D69941518E22E0C0073680F /* Release */ = { 579 | isa = XCBuildConfiguration; 580 | baseConfigurationReference = 2E9AA7CE5B23225F06EB9523 /* Pods-youtube-player-ios-example-youtube-player-ios-exampleTests.release.xcconfig */; 581 | buildSettings = { 582 | BUNDLE_LOADER = "$(TEST_HOST)"; 583 | FRAMEWORK_SEARCH_PATHS = ( 584 | "$(SDKROOT)/Developer/Library/Frameworks", 585 | "$(inherited)", 586 | "$(DEVELOPER_FRAMEWORKS_DIR)", 587 | ); 588 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 589 | GCC_PREFIX_HEADER = "youtube-player-ios-example/youtube-player-ios-example-Prefix.pch"; 590 | INFOPLIST_FILE = "youtube-player-ios-exampleTests/youtube-player-ios-exampleTests-Info.plist"; 591 | PRODUCT_NAME = "$(TARGET_NAME)"; 592 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/youtube-player-ios-example.app/youtube-player-ios-example"; 593 | WRAPPER_EXTENSION = xctest; 594 | }; 595 | name = Release; 596 | }; 597 | /* End XCBuildConfiguration section */ 598 | 599 | /* Begin XCConfigurationList section */ 600 | 4D6993D918E22E0C0073680F /* Build configuration list for PBXProject "youtube-player-ios-example" */ = { 601 | isa = XCConfigurationList; 602 | buildConfigurations = ( 603 | 4D69940E18E22E0C0073680F /* Debug */, 604 | 4D69940F18E22E0C0073680F /* Release */, 605 | ); 606 | defaultConfigurationIsVisible = 0; 607 | defaultConfigurationName = Release; 608 | }; 609 | 4D69941018E22E0C0073680F /* Build configuration list for PBXNativeTarget "youtube-player-ios-example" */ = { 610 | isa = XCConfigurationList; 611 | buildConfigurations = ( 612 | 4D69941118E22E0C0073680F /* Debug */, 613 | 4D69941218E22E0C0073680F /* Release */, 614 | ); 615 | defaultConfigurationIsVisible = 0; 616 | defaultConfigurationName = Release; 617 | }; 618 | 4D69941318E22E0C0073680F /* Build configuration list for PBXNativeTarget "youtube-player-ios-exampleTests" */ = { 619 | isa = XCConfigurationList; 620 | buildConfigurations = ( 621 | 4D69941418E22E0C0073680F /* Debug */, 622 | 4D69941518E22E0C0073680F /* Release */, 623 | ); 624 | defaultConfigurationIsVisible = 0; 625 | defaultConfigurationName = Release; 626 | }; 627 | /* End XCConfigurationList section */ 628 | }; 629 | rootObject = 4D6993D618E22E0C0073680F /* Project object */; 630 | } 631 | -------------------------------------------------------------------------------- /Sources/YTPlayerView.m: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #import "YTPlayerView.h" 16 | 17 | // These are instances of NSString because we get them from parsing a URL. It would be silly to 18 | // convert these into an integer just to have to convert the URL query string value into an integer 19 | // as well for the sake of doing a value comparison. A full list of response error codes can be 20 | // found here: 21 | // https://developers.google.com/youtube/iframe_api_reference 22 | NSString static *const kYTPlayerStateUnstartedCode = @"-1"; 23 | NSString static *const kYTPlayerStateEndedCode = @"0"; 24 | NSString static *const kYTPlayerStatePlayingCode = @"1"; 25 | NSString static *const kYTPlayerStatePausedCode = @"2"; 26 | NSString static *const kYTPlayerStateBufferingCode = @"3"; 27 | NSString static *const kYTPlayerStateCuedCode = @"5"; 28 | NSString static *const kYTPlayerStateUnknownCode = @"unknown"; 29 | 30 | // Constants representing playback quality. 31 | NSString static *const kYTPlaybackQualitySmallQuality = @"small"; 32 | NSString static *const kYTPlaybackQualityMediumQuality = @"medium"; 33 | NSString static *const kYTPlaybackQualityLargeQuality = @"large"; 34 | NSString static *const kYTPlaybackQualityHD720Quality = @"hd720"; 35 | NSString static *const kYTPlaybackQualityHD1080Quality = @"hd1080"; 36 | NSString static *const kYTPlaybackQualityHighResQuality = @"highres"; 37 | NSString static *const kYTPlaybackQualityAutoQuality = @"auto"; 38 | NSString static *const kYTPlaybackQualityDefaultQuality = @"default"; 39 | NSString static *const kYTPlaybackQualityUnknownQuality = @"unknown"; 40 | 41 | // Constants representing YouTube player errors. 42 | NSString static *const kYTPlayerErrorInvalidParamErrorCode = @"2"; 43 | NSString static *const kYTPlayerErrorHTML5ErrorCode = @"5"; 44 | NSString static *const kYTPlayerErrorVideoNotFoundErrorCode = @"100"; 45 | NSString static *const kYTPlayerErrorNotEmbeddableErrorCode = @"101"; 46 | NSString static *const kYTPlayerErrorCannotFindVideoErrorCode = @"105"; 47 | NSString static *const kYTPlayerErrorSameAsNotEmbeddableErrorCode = @"150"; 48 | 49 | // Constants representing player callbacks. 50 | NSString static *const kYTPlayerCallbackOnReady = @"onReady"; 51 | NSString static *const kYTPlayerCallbackOnStateChange = @"onStateChange"; 52 | NSString static *const kYTPlayerCallbackOnPlaybackQualityChange = @"onPlaybackQualityChange"; 53 | NSString static *const kYTPlayerCallbackOnError = @"onError"; 54 | NSString static *const kYTPlayerCallbackOnPlayTime = @"onPlayTime"; 55 | 56 | NSString static *const kYTPlayerCallbackOnYouTubeIframeAPIReady = @"onYouTubeIframeAPIReady"; 57 | NSString static *const kYTPlayerCallbackOnYouTubeIframeAPIFailedToLoad = @"onYouTubeIframeAPIFailedToLoad"; 58 | 59 | NSString static *const kYTPlayerEmbedUrlRegexPattern = @"^http(s)://(www.)youtube.com/embed/(.*)$"; 60 | NSString static *const kYTPlayerAdUrlRegexPattern = @"^http(s)://pubads.g.doubleclick.net/pagead/conversion/"; 61 | NSString static *const kYTPlayerOAuthRegexPattern = @"^http(s)://accounts.google.com/o/oauth2/(.*)$"; 62 | NSString static *const kYTPlayerStaticProxyRegexPattern = @"^https://content.googleapis.com/static/proxy.html(.*)$"; 63 | NSString static *const kYTPlayerSyndicationRegexPattern = @"^https://tpc.googlesyndication.com/sodar/(.*).html$"; 64 | 65 | @interface YTPlayerView() 66 | 67 | @property (nonatomic) NSURL *originURL; 68 | @property (nonatomic, weak) UIView *initialLoadingView; 69 | 70 | @end 71 | 72 | @implementation YTPlayerView 73 | 74 | - (BOOL)loadWithVideoId:(NSString *)videoId { 75 | return [self loadWithVideoId:videoId playerVars:nil]; 76 | } 77 | 78 | - (BOOL)loadWithPlaylistId:(NSString *)playlistId { 79 | return [self loadWithPlaylistId:playlistId playerVars:nil]; 80 | } 81 | 82 | - (BOOL)loadWithVideoId:(NSString *)videoId playerVars:(NSDictionary *)playerVars { 83 | if (!playerVars) { 84 | playerVars = @{}; 85 | } 86 | NSDictionary *playerParams = @{ @"videoId" : videoId, @"playerVars" : playerVars }; 87 | return [self loadWithPlayerParams:playerParams]; 88 | } 89 | 90 | - (BOOL)loadWithPlaylistId:(NSString *)playlistId playerVars:(NSDictionary *)playerVars { 91 | 92 | // Mutable copy because we may have been passed an immutable config dictionary. 93 | NSMutableDictionary *tempPlayerVars = [[NSMutableDictionary alloc] init]; 94 | [tempPlayerVars setValue:@"playlist" forKey:@"listType"]; 95 | [tempPlayerVars setValue:playlistId forKey:@"list"]; 96 | if (playerVars) { 97 | [tempPlayerVars addEntriesFromDictionary:playerVars]; 98 | } 99 | 100 | NSDictionary *playerParams = @{ @"playerVars" : tempPlayerVars }; 101 | return [self loadWithPlayerParams:playerParams]; 102 | } 103 | 104 | #pragma mark - Player methods 105 | 106 | - (void)playVideo { 107 | [self evaluateJavaScript:@"player.playVideo();"]; 108 | } 109 | 110 | - (void)pauseVideo { 111 | [self notifyDelegateOfYouTubeCallbackUrl:[NSURL URLWithString:[NSString stringWithFormat:@"ytplayer://onStateChange?data=%@", kYTPlayerStatePausedCode]]]; 112 | [self evaluateJavaScript:@"player.pauseVideo();"]; 113 | } 114 | 115 | - (void)stopVideo { 116 | [self evaluateJavaScript:@"player.stopVideo();"]; 117 | } 118 | 119 | - (void)seekToSeconds:(float)seekToSeconds allowSeekAhead:(BOOL)allowSeekAhead { 120 | NSNumber *secondsValue = [NSNumber numberWithFloat:seekToSeconds]; 121 | NSString *allowSeekAheadValue = [self stringForJSBoolean:allowSeekAhead]; 122 | NSString *command = [NSString stringWithFormat:@"player.seekTo(%@, %@);", secondsValue, allowSeekAheadValue]; 123 | [self evaluateJavaScript:command]; 124 | } 125 | 126 | #pragma mark - Cueing methods 127 | 128 | - (void)cueVideoById:(NSString *)videoId 129 | startSeconds:(float)startSeconds { 130 | NSNumber *startSecondsValue = [NSNumber numberWithFloat:startSeconds]; 131 | NSString *command = [NSString stringWithFormat:@"player.cueVideoById('%@', %@);", 132 | videoId, startSecondsValue]; 133 | [self evaluateJavaScript:command]; 134 | } 135 | 136 | - (void)cueVideoById:(NSString *)videoId 137 | startSeconds:(float)startSeconds 138 | endSeconds:(float)endSeconds { 139 | NSNumber *startSecondsValue = [NSNumber numberWithFloat:startSeconds]; 140 | NSNumber *endSecondsValue = [NSNumber numberWithFloat:endSeconds]; 141 | NSString *command = [NSString stringWithFormat:@"player.cueVideoById({'videoId': '%@'," 142 | "'startSeconds': %@, 'endSeconds': %@});", 143 | videoId, startSecondsValue, endSecondsValue]; 144 | [self evaluateJavaScript:command]; 145 | } 146 | 147 | - (void)loadVideoById:(NSString *)videoId 148 | startSeconds:(float)startSeconds { 149 | NSNumber *startSecondsValue = [NSNumber numberWithFloat:startSeconds]; 150 | NSString *command = [NSString stringWithFormat:@"player.loadVideoById('%@', %@);", 151 | videoId, startSecondsValue]; 152 | [self evaluateJavaScript:command]; 153 | } 154 | 155 | - (void)loadVideoById:(NSString *)videoId 156 | startSeconds:(float)startSeconds 157 | endSeconds:(float)endSeconds { 158 | NSNumber *startSecondsValue = [NSNumber numberWithFloat:startSeconds]; 159 | NSNumber *endSecondsValue = [NSNumber numberWithFloat:endSeconds]; 160 | NSString *command = [NSString stringWithFormat:@"player.loadVideoById({'videoId': '%@'," 161 | "'startSeconds': %@, 'endSeconds': %@});", 162 | videoId, startSecondsValue, endSecondsValue]; 163 | [self evaluateJavaScript:command]; 164 | } 165 | 166 | - (void)cueVideoByURL:(NSString *)videoURL 167 | startSeconds:(float)startSeconds { 168 | NSNumber *startSecondsValue = [NSNumber numberWithFloat:startSeconds]; 169 | NSString *command = [NSString stringWithFormat:@"player.cueVideoByUrl('%@', %@);", 170 | videoURL, startSecondsValue]; 171 | [self evaluateJavaScript:command]; 172 | } 173 | 174 | - (void)cueVideoByURL:(NSString *)videoURL 175 | startSeconds:(float)startSeconds 176 | endSeconds:(float)endSeconds { 177 | NSNumber *startSecondsValue = [NSNumber numberWithFloat:startSeconds]; 178 | NSNumber *endSecondsValue = [NSNumber numberWithFloat:endSeconds]; 179 | NSString *command = [NSString stringWithFormat:@"player.cueVideoByUrl('%@', %@, %@);", 180 | videoURL, startSecondsValue, endSecondsValue]; 181 | [self evaluateJavaScript:command]; 182 | } 183 | 184 | - (void)loadVideoByURL:(NSString *)videoURL 185 | startSeconds:(float)startSeconds { 186 | NSNumber *startSecondsValue = [NSNumber numberWithFloat:startSeconds]; 187 | NSString *command = [NSString stringWithFormat:@"player.loadVideoByUrl('%@', %@);", 188 | videoURL, startSecondsValue]; 189 | [self evaluateJavaScript:command]; 190 | } 191 | 192 | - (void)loadVideoByURL:(NSString *)videoURL 193 | startSeconds:(float)startSeconds 194 | endSeconds:(float)endSeconds { 195 | NSNumber *startSecondsValue = [NSNumber numberWithFloat:startSeconds]; 196 | NSNumber *endSecondsValue = [NSNumber numberWithFloat:endSeconds]; 197 | NSString *command = [NSString stringWithFormat:@"player.loadVideoByUrl('%@', %@, %@);", 198 | videoURL, startSecondsValue, endSecondsValue]; 199 | [self evaluateJavaScript:command]; 200 | } 201 | 202 | #pragma mark - Cueing methods for lists 203 | 204 | - (void)cuePlaylistByPlaylistId:(NSString *)playlistId 205 | index:(int)index 206 | startSeconds:(float)startSeconds { 207 | NSString *playlistIdString = [NSString stringWithFormat:@"'%@'", playlistId]; 208 | [self cuePlaylist:playlistIdString 209 | index:index 210 | startSeconds:startSeconds]; 211 | } 212 | 213 | - (void)cuePlaylistByVideos:(NSArray *)videoIds 214 | index:(int)index 215 | startSeconds:(float)startSeconds { 216 | [self cuePlaylist:[self stringFromVideoIdArray:videoIds] 217 | index:index 218 | startSeconds:startSeconds]; 219 | } 220 | 221 | - (void)loadPlaylistByPlaylistId:(NSString *)playlistId 222 | index:(int)index 223 | startSeconds:(float)startSeconds { 224 | NSString *playlistIdString = [NSString stringWithFormat:@"'%@'", playlistId]; 225 | [self loadPlaylist:playlistIdString 226 | index:index 227 | startSeconds:startSeconds]; 228 | } 229 | 230 | - (void)loadPlaylistByVideos:(NSArray *)videoIds 231 | index:(int)index 232 | startSeconds:(float)startSeconds { 233 | [self loadPlaylist:[self stringFromVideoIdArray:videoIds] 234 | index:index 235 | startSeconds:startSeconds]; 236 | } 237 | 238 | #pragma mark - Setting the playback rate 239 | 240 | - (void)playbackRate:(_Nullable YTFloatCompletionHandler)completionHandler { 241 | [self evaluateJavaScript:@"player.getPlaybackRate();" 242 | completionHandler:^(id _Nullable result, NSError * _Nullable error) { 243 | if (!completionHandler) { 244 | return; 245 | } 246 | if (error) { 247 | completionHandler(-1, error); 248 | return; 249 | } 250 | if (!result || ![result isKindOfClass:[NSNumber class]]) { 251 | completionHandler(0, nil); 252 | return; 253 | } 254 | completionHandler([result floatValue], nil); 255 | }]; 256 | } 257 | 258 | - (void)setPlaybackRate:(float)suggestedRate { 259 | NSString *command = [NSString stringWithFormat:@"player.setPlaybackRate(%f);", suggestedRate]; 260 | [self evaluateJavaScript:command]; 261 | } 262 | 263 | - (void)availablePlaybackRates:(_Nullable YTArrayCompletionHandler)completionHandler { 264 | [self evaluateJavaScript:@"player.getAvailablePlaybackRates();" 265 | completionHandler:^(id _Nullable result, NSError * _Nullable error) { 266 | if (!completionHandler) { 267 | return; 268 | } 269 | if (error) { 270 | completionHandler(nil, error); 271 | return; 272 | } 273 | if (!result || ![result isKindOfClass:[NSArray class]]) { 274 | completionHandler(nil, nil); 275 | return; 276 | } 277 | completionHandler(result, nil); 278 | }]; 279 | } 280 | 281 | #pragma mark - Setting playback behavior for playlists 282 | 283 | - (void)setLoop:(BOOL)loop { 284 | NSString *loopPlayListValue = [self stringForJSBoolean:loop]; 285 | NSString *command = [NSString stringWithFormat:@"player.setLoop(%@);", loopPlayListValue]; 286 | [self evaluateJavaScript:command]; 287 | } 288 | 289 | - (void)setShuffle:(BOOL)shuffle { 290 | NSString *shufflePlayListValue = [self stringForJSBoolean:shuffle]; 291 | NSString *command = [NSString stringWithFormat:@"player.setShuffle(%@);", shufflePlayListValue]; 292 | [self evaluateJavaScript:command]; 293 | } 294 | 295 | #pragma mark - Playback status 296 | 297 | - (void)videoLoadedFraction:(_Nullable YTFloatCompletionHandler)completionHandler { 298 | [self evaluateJavaScript:@"player.getVideoLoadedFraction();" 299 | completionHandler:^(id _Nullable result, NSError * _Nullable error) { 300 | if (!completionHandler) { 301 | return; 302 | } 303 | if (error) { 304 | completionHandler(-1, error); 305 | return; 306 | } 307 | if (!result || ![result isKindOfClass:[NSNumber class]]) { 308 | completionHandler(0, nil); 309 | return; 310 | } 311 | completionHandler([result floatValue], nil); 312 | }]; 313 | } 314 | 315 | - (void)playerState:(_Nullable YTPlayerStateCompletionHandler)completionHandler { 316 | [self evaluateJavaScript:@"player.getPlayerState();" 317 | completionHandler:^(id _Nullable result, NSError * _Nullable error) { 318 | if (!completionHandler) { 319 | return; 320 | } 321 | if (error) { 322 | completionHandler(kYTPlayerStateUnknown, error); 323 | return; 324 | } 325 | if (!result || ![result isKindOfClass:[NSNumber class]]) { 326 | completionHandler(kYTPlayerStateUnknown, error); 327 | return; 328 | } 329 | YTPlayerState state = [YTPlayerView playerStateForString:[result stringValue]]; 330 | completionHandler(state, nil); 331 | }]; 332 | } 333 | 334 | - (void)currentTime:(_Nullable YTFloatCompletionHandler)completionHandler { 335 | [self evaluateJavaScript:@"player.getCurrentTime();" 336 | completionHandler:^(id _Nullable result, NSError * _Nullable error) { 337 | if (!completionHandler) { 338 | return; 339 | } 340 | if (error) { 341 | completionHandler(-1, error); 342 | return; 343 | } 344 | if (!result || ![result isKindOfClass:[NSNumber class]]) { 345 | completionHandler(0, nil); 346 | return; 347 | } 348 | completionHandler([result floatValue], nil); 349 | }]; 350 | } 351 | 352 | #pragma mark - Video information methods 353 | 354 | - (void)duration:(_Nullable YTDoubleCompletionHandler)completionHandler { 355 | [self evaluateJavaScript:@"player.getDuration();" 356 | completionHandler:^(id _Nullable result, NSError * _Nullable error) { 357 | if (!completionHandler) { 358 | return; 359 | } 360 | if (error) { 361 | completionHandler(-1, error); 362 | return; 363 | } 364 | if (!result || ![result isKindOfClass:[NSNumber class]]) { 365 | completionHandler(0, nil); 366 | return; 367 | } 368 | completionHandler([result doubleValue], nil); 369 | }]; 370 | } 371 | 372 | - (void)videoUrl:(_Nullable YTURLCompletionHandler)completionHandler { 373 | [self evaluateJavaScript:@"player.getVideoUrl();" 374 | completionHandler:^(id _Nullable result, NSError * _Nullable error) { 375 | if (!completionHandler) { 376 | return; 377 | } 378 | if (error) { 379 | completionHandler(nil, error); 380 | return; 381 | } 382 | if (!result || ![result isKindOfClass:[NSString class]]) { 383 | completionHandler(nil, nil); 384 | return; 385 | } 386 | completionHandler([NSURL URLWithString:result], nil); 387 | }]; 388 | } 389 | 390 | - (void)videoEmbedCode:(_Nullable YTStringCompletionHandler)completionHandler { 391 | [self evaluateJavaScript:@"player.getVideoEmbedCode();" 392 | completionHandler:^(id _Nullable result, NSError * _Nullable error) { 393 | if (!completionHandler) { 394 | return; 395 | } 396 | if (error) { 397 | completionHandler(nil, error); 398 | return; 399 | } 400 | if (!result || ![result isKindOfClass:[NSString class]]) { 401 | completionHandler(nil, nil); 402 | return; 403 | } 404 | completionHandler(result, nil); 405 | }]; 406 | } 407 | 408 | #pragma mark - Playlist methods 409 | 410 | - (void)playlist:(_Nullable YTArrayCompletionHandler)completionHandler { 411 | [self evaluateJavaScript:@"player.getPlaylist();" 412 | completionHandler:^(id _Nullable result, NSError * _Nullable error) { 413 | if (!completionHandler) { 414 | return; 415 | } 416 | if (error) { 417 | completionHandler(nil, error); 418 | } 419 | if (!result || ![result isKindOfClass:[NSArray class]]) { 420 | completionHandler(nil, nil); 421 | return; 422 | } 423 | completionHandler(result, nil); 424 | }]; 425 | } 426 | 427 | - (void)playlistIndex:(_Nullable YTIntCompletionHandler)completionHandler { 428 | [self evaluateJavaScript:@"player.getPlaylistIndex();" 429 | completionHandler:^(id _Nullable result, NSError * _Nullable error) { 430 | if (!completionHandler) { 431 | return; 432 | } 433 | if (error) { 434 | completionHandler(-1, error); 435 | return; 436 | } 437 | if (!result || ![result isKindOfClass:[NSNumber class]]) { 438 | completionHandler(0, nil); 439 | return; 440 | } 441 | completionHandler([result intValue], nil); 442 | }]; 443 | } 444 | 445 | #pragma mark - Playing a video in a playlist 446 | 447 | - (void)nextVideo { 448 | [self evaluateJavaScript:@"player.nextVideo();"]; 449 | } 450 | 451 | - (void)previousVideo { 452 | [self evaluateJavaScript:@"player.previousVideo();"]; 453 | } 454 | 455 | - (void)playVideoAt:(int)index { 456 | NSString *command = 457 | [NSString stringWithFormat:@"player.playVideoAt(%@);", [NSNumber numberWithInt:index]]; 458 | [self evaluateJavaScript:command]; 459 | } 460 | 461 | #pragma mark - Helper methods 462 | 463 | /** 464 | * Convert a quality value from NSString to the typed enum value. 465 | * 466 | * @param qualityString A string representing playback quality. Ex: "small", "medium", "hd1080". 467 | * @return An enum value representing the playback quality. 468 | */ 469 | + (YTPlaybackQuality)playbackQualityForString:(NSString *)qualityString { 470 | YTPlaybackQuality quality = kYTPlaybackQualityUnknown; 471 | 472 | if ([qualityString isEqualToString:kYTPlaybackQualitySmallQuality]) { 473 | quality = kYTPlaybackQualitySmall; 474 | } else if ([qualityString isEqualToString:kYTPlaybackQualityMediumQuality]) { 475 | quality = kYTPlaybackQualityMedium; 476 | } else if ([qualityString isEqualToString:kYTPlaybackQualityLargeQuality]) { 477 | quality = kYTPlaybackQualityLarge; 478 | } else if ([qualityString isEqualToString:kYTPlaybackQualityHD720Quality]) { 479 | quality = kYTPlaybackQualityHD720; 480 | } else if ([qualityString isEqualToString:kYTPlaybackQualityHD1080Quality]) { 481 | quality = kYTPlaybackQualityHD1080; 482 | } else if ([qualityString isEqualToString:kYTPlaybackQualityHighResQuality]) { 483 | quality = kYTPlaybackQualityHighRes; 484 | } else if ([qualityString isEqualToString:kYTPlaybackQualityAutoQuality]) { 485 | quality = kYTPlaybackQualityAuto; 486 | } 487 | 488 | return quality; 489 | } 490 | 491 | /** 492 | * Convert a state value from NSString to the typed enum value. 493 | * 494 | * @param stateString A string representing player state. Ex: "-1", "0", "1". 495 | * @return An enum value representing the player state. 496 | */ 497 | + (YTPlayerState)playerStateForString:(NSString *)stateString { 498 | YTPlayerState state = kYTPlayerStateUnknown; 499 | if ([stateString isEqualToString:kYTPlayerStateUnstartedCode]) { 500 | state = kYTPlayerStateUnstarted; 501 | } else if ([stateString isEqualToString:kYTPlayerStateEndedCode]) { 502 | state = kYTPlayerStateEnded; 503 | } else if ([stateString isEqualToString:kYTPlayerStatePlayingCode]) { 504 | state = kYTPlayerStatePlaying; 505 | } else if ([stateString isEqualToString:kYTPlayerStatePausedCode]) { 506 | state = kYTPlayerStatePaused; 507 | } else if ([stateString isEqualToString:kYTPlayerStateBufferingCode]) { 508 | state = kYTPlayerStateBuffering; 509 | } else if ([stateString isEqualToString:kYTPlayerStateCuedCode]) { 510 | state = kYTPlayerStateCued; 511 | } 512 | return state; 513 | } 514 | 515 | #pragma mark - WKNavigationDelegate 516 | 517 | - (void)webView:(WKWebView *)webView 518 | decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction 519 | decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { 520 | NSURLRequest *request = navigationAction.request; 521 | if ([request.URL.scheme isEqual:@"ytplayer"]) { 522 | [self notifyDelegateOfYouTubeCallbackUrl:request.URL]; 523 | decisionHandler(WKNavigationActionPolicyCancel); 524 | return; 525 | } else if ([request.URL.scheme isEqual: @"http"] || [request.URL.scheme isEqual:@"https"]) { 526 | if ([self handleHttpNavigationToUrl:request.URL]) { 527 | decisionHandler(WKNavigationActionPolicyAllow); 528 | } else { 529 | decisionHandler(WKNavigationActionPolicyCancel); 530 | } 531 | return; 532 | } 533 | decisionHandler(WKNavigationActionPolicyAllow); 534 | } 535 | 536 | - (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation 537 | withError:(NSError *)error { 538 | if (self.initialLoadingView) { 539 | [self.initialLoadingView removeFromSuperview]; 540 | } 541 | } 542 | 543 | #pragma mark - WKUIDelegate 544 | 545 | - (WKWebView *)webView:(WKWebView *)webView 546 | createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration 547 | forNavigationAction:(WKNavigationAction *)navigationAction 548 | windowFeatures:(WKWindowFeatures *)windowFeatures { 549 | // Handle navigation actions initiated by Javascript. 550 | [[UIApplication sharedApplication] openURL:navigationAction.request.URL 551 | options:@{} 552 | completionHandler:nil]; 553 | // Returning nil results in canceling the navigation, which has already been handled above. 554 | return nil; 555 | } 556 | 557 | #pragma mark - Private methods 558 | 559 | - (NSURL *)originURL { 560 | if (!_originURL) { 561 | NSString *bundleId = [[NSBundle mainBundle] bundleIdentifier]; 562 | NSString *stringURL = [[NSString stringWithFormat:@"http://%@", bundleId] lowercaseString]; 563 | _originURL = [NSURL URLWithString:stringURL]; 564 | } 565 | return _originURL; 566 | } 567 | 568 | /** 569 | * Private method to handle "navigation" to a callback URL of the format 570 | * ytplayer://action?data=someData 571 | * This is how the webview communicates with the containing Objective-C code. 572 | * Side effects of this method are that it calls methods on this class's delegate. 573 | * 574 | * @param url A URL of the format ytplayer://action?data=value. 575 | */ 576 | - (void)notifyDelegateOfYouTubeCallbackUrl:(NSURL *) url { 577 | NSString *action = url.host; 578 | 579 | // We know the query can only be of the format ytplayer://action?data=SOMEVALUE, 580 | // so we parse out the value. 581 | NSString *query = url.query; 582 | NSString *data; 583 | if (query) { 584 | data = [query componentsSeparatedByString:@"="][1]; 585 | } 586 | 587 | if ([action isEqual:kYTPlayerCallbackOnReady]) { 588 | if (self.initialLoadingView) { 589 | [self.initialLoadingView removeFromSuperview]; 590 | } 591 | if ([self.delegate respondsToSelector:@selector(playerViewDidBecomeReady:)]) { 592 | [self.delegate playerViewDidBecomeReady:self]; 593 | } 594 | } else if ([action isEqual:kYTPlayerCallbackOnStateChange]) { 595 | if ([self.delegate respondsToSelector:@selector(playerView:didChangeToState:)]) { 596 | YTPlayerState state = [YTPlayerView playerStateForString:data]; 597 | [self.delegate playerView:self didChangeToState:state]; 598 | } 599 | } else if ([action isEqual:kYTPlayerCallbackOnPlaybackQualityChange]) { 600 | if ([self.delegate respondsToSelector:@selector(playerView:didChangeToQuality:)]) { 601 | YTPlaybackQuality quality = [YTPlayerView playbackQualityForString:data]; 602 | [self.delegate playerView:self didChangeToQuality:quality]; 603 | } 604 | } else if ([action isEqual:kYTPlayerCallbackOnError]) { 605 | if ([self.delegate respondsToSelector:@selector(playerView:receivedError:)]) { 606 | YTPlayerError error = kYTPlayerErrorUnknown; 607 | 608 | if ([data isEqual:kYTPlayerErrorInvalidParamErrorCode]) { 609 | error = kYTPlayerErrorInvalidParam; 610 | } else if ([data isEqual:kYTPlayerErrorHTML5ErrorCode]) { 611 | error = kYTPlayerErrorHTML5Error; 612 | } else if ([data isEqual:kYTPlayerErrorNotEmbeddableErrorCode] || 613 | [data isEqual:kYTPlayerErrorSameAsNotEmbeddableErrorCode]) { 614 | error = kYTPlayerErrorNotEmbeddable; 615 | } else if ([data isEqual:kYTPlayerErrorVideoNotFoundErrorCode] || 616 | [data isEqual:kYTPlayerErrorCannotFindVideoErrorCode]) { 617 | error = kYTPlayerErrorVideoNotFound; 618 | } 619 | 620 | [self.delegate playerView:self receivedError:error]; 621 | } 622 | } else if ([action isEqualToString:kYTPlayerCallbackOnPlayTime]) { 623 | if ([self.delegate respondsToSelector:@selector(playerView:didPlayTime:)]) { 624 | float time = [data floatValue]; 625 | [self.delegate playerView:self didPlayTime:time]; 626 | } 627 | } else if ([action isEqualToString:kYTPlayerCallbackOnYouTubeIframeAPIFailedToLoad]) { 628 | if (self.initialLoadingView) { 629 | [self.initialLoadingView removeFromSuperview]; 630 | } 631 | } 632 | } 633 | 634 | - (BOOL)handleHttpNavigationToUrl:(NSURL *)url { 635 | // When loading the webView for the first time, webView tries loading the originURL 636 | // since it is set as the webView.baseURL. 637 | // In that case we want to let it load itself in the webView instead of trying 638 | // to load it in a browser. 639 | if ([[url.host lowercaseString] isEqualToString:[self.originURL.host lowercaseString]]) { 640 | return YES; 641 | } 642 | // Usually this means the user has clicked on the YouTube logo or an error message in the 643 | // player. Most URLs should open in the browser. The only http(s) URL that should open in this 644 | // webview is the URL for the embed, which is of the format: 645 | // http(s)://www.youtube.com/embed/[VIDEO ID]?[PARAMETERS] 646 | NSError *error = NULL; 647 | NSRegularExpression *ytRegex = 648 | [NSRegularExpression regularExpressionWithPattern:kYTPlayerEmbedUrlRegexPattern 649 | options:NSRegularExpressionCaseInsensitive 650 | error:&error]; 651 | NSTextCheckingResult *ytMatch = 652 | [ytRegex firstMatchInString:url.absoluteString 653 | options:0 654 | range:NSMakeRange(0, [url.absoluteString length])]; 655 | 656 | NSRegularExpression *adRegex = 657 | [NSRegularExpression regularExpressionWithPattern:kYTPlayerAdUrlRegexPattern 658 | options:NSRegularExpressionCaseInsensitive 659 | error:&error]; 660 | NSTextCheckingResult *adMatch = 661 | [adRegex firstMatchInString:url.absoluteString 662 | options:0 663 | range:NSMakeRange(0, [url.absoluteString length])]; 664 | 665 | NSRegularExpression *syndicationRegex = 666 | [NSRegularExpression regularExpressionWithPattern:kYTPlayerSyndicationRegexPattern 667 | options:NSRegularExpressionCaseInsensitive 668 | error:&error]; 669 | 670 | NSTextCheckingResult *syndicationMatch = 671 | [syndicationRegex firstMatchInString:url.absoluteString 672 | options:0 673 | range:NSMakeRange(0, [url.absoluteString length])]; 674 | 675 | NSRegularExpression *oauthRegex = 676 | [NSRegularExpression regularExpressionWithPattern:kYTPlayerOAuthRegexPattern 677 | options:NSRegularExpressionCaseInsensitive 678 | error:&error]; 679 | NSTextCheckingResult *oauthMatch = 680 | [oauthRegex firstMatchInString:url.absoluteString 681 | options:0 682 | range:NSMakeRange(0, [url.absoluteString length])]; 683 | 684 | NSRegularExpression *staticProxyRegex = 685 | [NSRegularExpression regularExpressionWithPattern:kYTPlayerStaticProxyRegexPattern 686 | options:NSRegularExpressionCaseInsensitive 687 | error:&error]; 688 | NSTextCheckingResult *staticProxyMatch = 689 | [staticProxyRegex firstMatchInString:url.absoluteString 690 | options:0 691 | range:NSMakeRange(0, [url.absoluteString length])]; 692 | 693 | if (ytMatch || adMatch || oauthMatch || staticProxyMatch || syndicationMatch) { 694 | return YES; 695 | } else { 696 | [[UIApplication sharedApplication] openURL:url 697 | options:@{UIApplicationOpenURLOptionUniversalLinksOnly: @NO} 698 | completionHandler:nil]; 699 | return NO; 700 | } 701 | } 702 | 703 | 704 | /** 705 | * Private helper method to load an iframe player with the given player parameters. 706 | * 707 | * @param additionalPlayerParams An NSDictionary of parameters in addition to required parameters 708 | * to instantiate the HTML5 player with. This differs depending on 709 | * whether a single video or playlist is being loaded. 710 | * @return YES if successful, NO if not. 711 | */ 712 | - (BOOL)loadWithPlayerParams:(NSDictionary *)additionalPlayerParams { 713 | NSDictionary *playerCallbacks = @{ 714 | @"onReady" : @"onReady", 715 | @"onStateChange" : @"onStateChange", 716 | @"onPlaybackQualityChange" : @"onPlaybackQualityChange", 717 | @"onError" : @"onPlayerError" 718 | }; 719 | NSMutableDictionary *playerParams = [[NSMutableDictionary alloc] init]; 720 | if (additionalPlayerParams) { 721 | [playerParams addEntriesFromDictionary:additionalPlayerParams]; 722 | } 723 | if (![playerParams objectForKey:@"height"]) { 724 | [playerParams setValue:@"100%" forKey:@"height"]; 725 | } 726 | if (![playerParams objectForKey:@"width"]) { 727 | [playerParams setValue:@"100%" forKey:@"width"]; 728 | } 729 | 730 | [playerParams setValue:playerCallbacks forKey:@"events"]; 731 | 732 | NSMutableDictionary *playerVars = [[playerParams objectForKey:@"playerVars"] mutableCopy]; 733 | if (!playerVars) { 734 | // playerVars must not be empty so we can render a '{}' in the output JSON 735 | playerVars = [NSMutableDictionary dictionary]; 736 | } 737 | // We always want to ovewrite the origin to self.originURL, not just for 738 | // the webView.baseURL 739 | [playerVars setObject:self.originURL.absoluteString forKey:@"origin"]; 740 | [playerParams setValue:playerVars forKey:@"playerVars"]; 741 | 742 | // Remove the existing webview to reset any state 743 | [self.webView removeFromSuperview]; 744 | _webView = [self createNewWebView]; 745 | [self addSubview:self.webView]; 746 | 747 | NSError *error = nil; 748 | NSString *path = [[NSBundle bundleForClass:[YTPlayerView class]] pathForResource:@"YTPlayerView-iframe-player" 749 | ofType:@"html"]; 750 | 751 | // in case of using Swift and embedded frameworks, resources included not in main bundle, 752 | // but in framework bundle 753 | if (!path) { 754 | path = [[[self class] frameworkBundle] pathForResource:@"YTPlayerView-iframe-player" 755 | ofType:@"html"]; 756 | } 757 | 758 | NSString *embedHTMLTemplate = 759 | [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error]; 760 | 761 | if (error) { 762 | NSLog(@"Received error rendering template: %@", error); 763 | return NO; 764 | } 765 | 766 | // Render the playerVars as a JSON dictionary. 767 | NSError *jsonRenderingError = nil; 768 | NSData *jsonData = [NSJSONSerialization dataWithJSONObject:playerParams 769 | options:NSJSONWritingPrettyPrinted 770 | error:&jsonRenderingError]; 771 | if (jsonRenderingError) { 772 | NSLog(@"Attempted configuration of player with invalid playerVars: %@ \tError: %@", 773 | playerParams, 774 | jsonRenderingError); 775 | return NO; 776 | } 777 | 778 | NSString *playerVarsJsonString = 779 | [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; 780 | 781 | NSString *embedHTML = [NSString stringWithFormat:embedHTMLTemplate, playerVarsJsonString]; 782 | 783 | [self.webView loadHTMLString:embedHTML baseURL: self.originURL]; 784 | self.webView.navigationDelegate = self; 785 | self.webView.UIDelegate = self; 786 | 787 | if ([self.delegate respondsToSelector:@selector(playerViewPreferredInitialLoadingView:)]) { 788 | UIView *initialLoadingView = [self.delegate playerViewPreferredInitialLoadingView:self]; 789 | if (initialLoadingView) { 790 | initialLoadingView.frame = self.bounds; 791 | initialLoadingView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 792 | [self addSubview:initialLoadingView]; 793 | self.initialLoadingView = initialLoadingView; 794 | } 795 | } 796 | 797 | return YES; 798 | } 799 | 800 | /** 801 | * Private method for cueing both cases of playlist ID and array of video IDs. Cueing 802 | * a playlist does not start playback. 803 | * 804 | * @param cueingString A JavaScript string representing an array, playlist ID or list of 805 | * video IDs to play with the playlist player. 806 | * @param index 0-index position of video to start playback on. 807 | * @param startSeconds Seconds after start of video to begin playback. 808 | */ 809 | - (void)cuePlaylist:(NSString *)cueingString 810 | index:(int)index 811 | startSeconds:(float)startSeconds { 812 | NSNumber *indexValue = [NSNumber numberWithInt:index]; 813 | NSNumber *startSecondsValue = [NSNumber numberWithFloat:startSeconds]; 814 | NSString *command = [NSString stringWithFormat:@"player.cuePlaylist(%@, %@, %@);", 815 | cueingString, indexValue, startSecondsValue]; 816 | [self evaluateJavaScript:command]; 817 | } 818 | 819 | /** 820 | * Private method for loading both cases of playlist ID and array of video IDs. Loading 821 | * a playlist automatically starts playback. 822 | * 823 | * @param cueingString A JavaScript string representing an array, playlist ID or list of 824 | * video IDs to play with the playlist player. 825 | * @param index 0-index position of video to start playback on. 826 | * @param startSeconds Seconds after start of video to begin playback. 827 | */ 828 | - (void)loadPlaylist:(NSString *)cueingString 829 | index:(int)index 830 | startSeconds:(float)startSeconds { 831 | NSNumber *indexValue = [NSNumber numberWithInt:index]; 832 | NSNumber *startSecondsValue = [NSNumber numberWithFloat:startSeconds]; 833 | NSString *command = [NSString stringWithFormat:@"player.loadPlaylist(%@, %@, %@);", 834 | cueingString, indexValue, startSecondsValue]; 835 | [self evaluateJavaScript:command]; 836 | } 837 | 838 | /** 839 | * Private helper method for converting an NSArray of video IDs into its JavaScript equivalent. 840 | * 841 | * @param videoIds An array of video ID strings to convert into JavaScript format. 842 | * @return A JavaScript array in String format containing video IDs. 843 | */ 844 | - (NSString *)stringFromVideoIdArray:(NSArray *)videoIds { 845 | NSMutableArray *formattedVideoIds = [[NSMutableArray alloc] init]; 846 | 847 | for (id unformattedId in videoIds) { 848 | [formattedVideoIds addObject:[NSString stringWithFormat:@"'%@'", unformattedId]]; 849 | } 850 | 851 | return [NSString stringWithFormat:@"[%@]", [formattedVideoIds componentsJoinedByString:@", "]]; 852 | } 853 | 854 | /** 855 | * Private method for evaluating JavaScript in the webview. 856 | * 857 | * @param jsToExecute The JavaScript code in string format that we want to execute. 858 | */ 859 | - (void)evaluateJavaScript:(NSString *)jsToExecute { 860 | [self evaluateJavaScript:jsToExecute completionHandler:nil]; 861 | } 862 | 863 | /** 864 | * Private method for evaluating JavaScript in the webview. 865 | * 866 | * @param jsToExecute The JavaScript code in string format that we want to execute. 867 | * @param completionHandler A block to invoke when script evaluation completes or fails. 868 | */ 869 | - (void)evaluateJavaScript:(NSString *)jsToExecute 870 | completionHandler:(void(^)(id _Nullable result, NSError *_Nullable error))completionHandler { 871 | [_webView evaluateJavaScript:jsToExecute 872 | completionHandler:^(id _Nullable result, NSError *_Nullable error) { 873 | if (!completionHandler) { 874 | return; 875 | } 876 | if (error) { 877 | completionHandler(nil, error); 878 | return; 879 | } 880 | if (!result || [result isKindOfClass:[NSNull class]]) { 881 | // we can consider this an empty result 882 | completionHandler(nil, nil); 883 | return; 884 | } 885 | 886 | completionHandler(result, nil); 887 | }]; 888 | } 889 | 890 | /** 891 | * Private method to convert a Objective-C BOOL value to JS boolean value. 892 | * 893 | * @param boolValue Objective-C BOOL value. 894 | * @return JavaScript Boolean value, i.e. "true" or "false". 895 | */ 896 | - (NSString *)stringForJSBoolean:(BOOL)boolValue { 897 | return boolValue ? @"true" : @"false"; 898 | } 899 | 900 | #pragma mark - Exposed for Testing 901 | 902 | - (void)setWebView:(WKWebView *)webView { 903 | _webView = webView; 904 | } 905 | 906 | - (WKWebView *)createNewWebView { 907 | WKWebViewConfiguration *webViewConfiguration = [[WKWebViewConfiguration alloc] init]; 908 | webViewConfiguration.allowsInlineMediaPlayback = YES; 909 | webViewConfiguration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; 910 | WKWebView *webView = [[WKWebView alloc] initWithFrame:self.bounds 911 | configuration:webViewConfiguration]; 912 | webView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); 913 | webView.scrollView.scrollEnabled = NO; 914 | webView.scrollView.bounces = NO; 915 | 916 | if ([self.delegate respondsToSelector:@selector(playerViewPreferredWebViewBackgroundColor:)]) { 917 | webView.backgroundColor = [self.delegate playerViewPreferredWebViewBackgroundColor:self]; 918 | if (webView.backgroundColor == [UIColor clearColor]) { 919 | webView.opaque = NO; 920 | } 921 | } 922 | return webView; 923 | } 924 | 925 | - (void)removeWebView { 926 | [self.webView removeFromSuperview]; 927 | self.webView = nil; 928 | } 929 | 930 | + (NSBundle *)frameworkBundle { 931 | #ifdef SWIFTPM_MODULE_BUNDLE 932 | return SWIFTPM_MODULE_BUNDLE; 933 | #else 934 | static NSBundle* frameworkBundle = nil; 935 | static dispatch_once_t predicate; 936 | dispatch_once(&predicate, ^{ 937 | NSString* mainBundlePath = [[NSBundle bundleForClass:[self class]] resourcePath]; 938 | NSString* frameworkBundlePath = [mainBundlePath stringByAppendingPathComponent:@"Assets.bundle"]; 939 | frameworkBundle = [NSBundle bundleWithPath:frameworkBundlePath]; 940 | }); 941 | return frameworkBundle; 942 | #endif 943 | } 944 | 945 | @end 946 | --------------------------------------------------------------------------------