├── Classes ├── .gitkeep ├── Player │ ├── PKController.swift │ ├── VR │ │ ├── VRData.swift │ │ └── VRPlayerEngine.swift │ ├── Tracks │ │ ├── PKTracks.swift │ │ └── Track.swift │ ├── PKAsset.swift │ ├── Protocols │ │ ├── PlayerEngine.swift │ │ └── BasicPlayer.swift │ ├── Media │ │ ├── PKMediaEntry+Copying.swift │ │ └── SourceSelector.swift │ ├── Playlist │ │ └── PKPlaylist.swift │ ├── PlayerState.swift │ ├── Ads │ │ ├── AdsDAIPlugin.swift │ │ └── AdsPlugin.swift │ ├── AVAsset │ │ └── PKAssetResourceLoaderDelegate.swift │ ├── PlayerController+TimeMonitor.swift │ ├── PlayerConfig.swift │ ├── PKVRController.swift │ └── PlayerView.swift ├── Providers │ ├── PlaylistProvider.swift │ ├── MediaEntryProviderResponseDelegate.swift │ ├── MediaEntryProvider.swift │ └── Mock │ │ └── MockMediaProvider.swift ├── Platform.swift ├── Plugins │ ├── TimeIntervalExtension.swift │ ├── PlayerPluginsDataSource.swift │ ├── BasePlugin.swift │ └── PKPlugin.swift ├── Events │ ├── PKMessageBus.swift │ ├── EventsPayloads │ │ ├── PKAdDAICuePoints.swift │ │ ├── PKAdCuePoints.swift │ │ └── PKPlaybackInfo.swift │ ├── PluginEvent.swift │ ├── InterceptorEvent.swift │ └── PlaylistEvent.swift ├── Extensions │ └── ArrayExtension.swift ├── Models │ ├── PKTimeRange.swift │ └── PKBoundary.swift ├── PKRequestParams.swift ├── PKLog.swift ├── Managers │ ├── NotificationsManager.swift │ ├── LocalDataStore.swift │ ├── AppState │ │ └── AppStateProvider.swift │ └── PlayKitManager.swift ├── PKStateMachine.swift ├── Utils │ └── NetworkUtils.swift ├── FairPlayDRM │ └── FPSContentKeyManager.swift ├── Network │ ├── KalturaPlaybackRequestAdapter.swift │ └── KalturaUDRMLicenseRequestAdapter.swift └── MessageBus.swift ├── icon.png ├── TestApp ├── Default-568h@2x.png ├── TestApp │ ├── Assets.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── ViewController.swift │ ├── Info.plist │ ├── TestApp_v5-Info.plist │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── AppDelegate.swift ├── TestApp.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ ├── TestApp_v5.xcscheme │ │ └── TestApp_v4_2.xcscheme ├── TestApp.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── Podfile ├── tvOSTestApp ├── tvOSTestApp │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── App Icon & Top Shelf Image.brandassets │ │ │ ├── App Icon.imagestack │ │ │ │ ├── Back.imagestacklayer │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ ├── Front.imagestacklayer │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ ├── Middle.imagestacklayer │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── App Icon - App Store.imagestack │ │ │ │ ├── Back.imagestacklayer │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ ├── Front.imagestacklayer │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ ├── Middle.imagestacklayer │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Top Shelf Image.imageset │ │ │ │ └── Contents.json │ │ │ ├── Top Shelf Image Wide.imageset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ └── AccentColor.colorset │ │ │ └── Contents.json │ ├── ViewController.swift │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ └── AppDelegate.swift ├── tvOSTestApp.xcodeproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Podfile └── tvOSTestApp.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Example ├── Tests │ ├── Resources │ │ └── Video │ │ │ └── big_buck_bunny_short.mp4 │ ├── Info.plist │ ├── Helpers │ │ └── PluginTestConfiguration.swift │ ├── Analytics │ │ ├── AnalyticsPluginConfig.swift │ │ ├── TVPAPI │ │ │ └── TVPAPIPluginTest.swift │ │ └── Phoenix │ │ │ └── PhoenixPluginTest.swift │ ├── Basic │ │ ├── SourceSelectorTest.swift │ │ └── TracksTest.swift │ └── MediaEntryProvider │ │ ├── RequestBuilder │ │ └── KalturaRequestBuilderTest.swift │ │ ├── MediaEntryProviderMock.swift │ │ └── PhoenixMediaProviderTest.swift ├── PlayKit.xcodeproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── PlayKit-Tests.xcscheme ├── PlayKit.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Sources │ └── Entries.json ├── PlayKit │ ├── ViewController.swift │ ├── Images.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Info.plist │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.xib │ └── AppDelegate.swift ├── Podfile └── Podfile.lock ├── .github ├── release_notes_template.md ├── cocoapods_publish.sh ├── release_mode.sh ├── release_notes.sh ├── PULL_REQUEST_TEMPLATE.md ├── tag.sh ├── ISSUE_TEMPLATE.md └── workflows │ ├── ci.yml │ ├── pr.yml │ └── release.yml ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── .travis.yml ├── .gitignore ├── Plugins └── AnalyticsCommon │ ├── AnalyticsConfig.swift │ ├── AnalyticsPluginProtocol.swift │ └── BaseAnalyticsPlugin.swift ├── travis-build.sh ├── banner-helper.py ├── PlayKit.podspec ├── Package.swift └── README.md /Classes/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaltura/playkit-ios/HEAD/icon.png -------------------------------------------------------------------------------- /TestApp/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaltura/playkit-ios/HEAD/TestApp/Default-568h@2x.png -------------------------------------------------------------------------------- /TestApp/TestApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /tvOSTestApp/tvOSTestApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Tests/Resources/Video/big_buck_bunny_short.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaltura/playkit-ios/HEAD/Example/Tests/Resources/Video/big_buck_bunny_short.mp4 -------------------------------------------------------------------------------- /.github/release_notes_template.md: -------------------------------------------------------------------------------- 1 | ## New in this version: 2 | 3 | @@RELEASE_NOTES@@ 4 | 5 | 6 | ## Cocoapods install 7 | `pod '@@POD_NAME@@', '~> @@POD_PREFERRED_VERSION@@'` 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tvOSTestApp/tvOSTestApp/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tvOSTestApp/tvOSTestApp/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tvOSTestApp/tvOSTestApp/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tvOSTestApp/tvOSTestApp/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tvOSTestApp/tvOSTestApp/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tvOSTestApp/tvOSTestApp/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tvOSTestApp/tvOSTestApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/PlayKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /TestApp/TestApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tvOSTestApp/tvOSTestApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.github/cocoapods_publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -u 4 | 5 | cat << EOF > ~/.netrc 6 | machine trunk.cocoapods.org 7 | login $PODS_USER 8 | password $PODS_PASS 9 | EOF 10 | 11 | chmod 0600 ~/.netrc 12 | 13 | pod trunk push --verbose --allow-warnings 14 | -------------------------------------------------------------------------------- /tvOSTestApp/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | platform :tvos, '9.0' 3 | 4 | target 'tvOSTestApp' do 5 | # Comment the next line if you don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | pod 'PlayKit', :path => '..' 9 | 10 | end 11 | -------------------------------------------------------------------------------- /Example/PlayKit.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /TestApp/TestApp.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/PlayKit.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /TestApp/TestApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tvOSTestApp/tvOSTestApp/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tvOSTestApp/tvOSTestApp/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tvOSTestApp/tvOSTestApp/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tvOSTestApp/tvOSTestApp.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tvOSTestApp/tvOSTestApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /TestApp/TestApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tvOSTestApp/tvOSTestApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.github/release_mode.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -u 4 | 5 | PODSPEC=*.podspec 6 | POD=$(basename $PODSPEC .podspec) 7 | 8 | sed -e "s#.0000##1" -i '' $POD.podspec 9 | 10 | cat $POD.podspec 11 | 12 | git add . 13 | git config user.name "Kaltura Dev" 14 | git config user.email dev@kaltura.com 15 | 16 | git commit -m "Set to release mode" 17 | 18 | git push 19 | -------------------------------------------------------------------------------- /tvOSTestApp/tvOSTestApp/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // tvOSTestApp 4 | // 5 | // Created by Nilit Danan on 14/06/2022. 6 | // 7 | 8 | import UIKit 9 | 10 | class ViewController: UIViewController { 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | // Do any additional setup after loading the view. 15 | } 16 | 17 | 18 | } 19 | 20 | -------------------------------------------------------------------------------- /tvOSTestApp/tvOSTestApp/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tvOSTestApp/tvOSTestApp/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tvOSTestApp/tvOSTestApp/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tvOSTestApp/tvOSTestApp/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.imagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.imagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.imagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tvOSTestApp/tvOSTestApp/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.imagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.imagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.imagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: swift 2 | osx_image: xcode13.1 3 | before_install: 4 | - gem install cocoapods xcpretty 5 | - pod repo update 6 | script: 7 | - ./travis-build.sh 8 | notifications: 9 | email: 10 | recipients: 11 | - noam.tamim@kaltura.com 12 | - nilit.danan@kaltura.com 13 | - gilad.nadav@kaltura.com 14 | - oren.melamed@kaltura.com 15 | - sergey.chausov@kaltura.com 16 | on_success: change 17 | on_failure: always 18 | -------------------------------------------------------------------------------- /TestApp/TestApp/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // TestApp 4 | // 5 | // Created by Noam Tamim on 21/10/2018. 6 | // Copyright © 2018 Kaltura. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | // Do any additional setup after loading the view, typically from a nib. 16 | } 17 | 18 | 19 | } 20 | 21 | -------------------------------------------------------------------------------- /.github/release_notes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -u 4 | 5 | PODSPEC=*.podspec 6 | POD=$(basename $PODSPEC .podspec) 7 | 8 | pod ipc spec $POD.podspec > spec.json 9 | 10 | POD_NAME=$(jq '.name' --raw-output spec.json) 11 | VERSION=$(jq '.version' --raw-output spec.json) 12 | 13 | sed -e "s#@@RELEASE_NOTES@@#- TBA#g" -e "s#@@POD_NAME@@#$POD_NAME#g" -e "s#@@POD_PREFERRED_VERSION@@#$VERSION#g" -i '' .github/release_notes_template.md 14 | 15 | cat .github/release_notes_template.md 16 | -------------------------------------------------------------------------------- /TestApp/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | platform :ios, '9.0' 3 | 4 | # Comment the next line if you're not using Swift and don't want to use dynamic frameworks 5 | use_frameworks! 6 | 7 | def sharedPods 8 | pod 'PlayKit', :path => '..' 9 | #pod 'KalturaNetKit'#, :path => '../../netkit-ios' 10 | #pod 'PlayKitUtils'#, :path => '../../playkit-ios-utils' 11 | end 12 | 13 | target 'TestApp_v4_2' do 14 | sharedPods 15 | end 16 | 17 | target 'TestApp_v5' do 18 | sharedPods 19 | end 20 | -------------------------------------------------------------------------------- /tvOSTestApp/tvOSTestApp/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "tv-marketing", 13 | "scale" : "1x" 14 | }, 15 | { 16 | "idiom" : "tv-marketing", 17 | "scale" : "2x" 18 | } 19 | ], 20 | "info" : { 21 | "author" : "xcode", 22 | "version" : 1 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tvOSTestApp/tvOSTestApp/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "tv-marketing", 13 | "scale" : "1x" 14 | }, 15 | { 16 | "idiom" : "tv-marketing", 17 | "scale" : "2x" 18 | } 19 | ], 20 | "info" : { 21 | "author" : "xcode", 22 | "version" : 1 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description of the Changes 2 | 3 | Please add a detailed description of the change, weather it's an enhancement or a bugfix. 4 | If the PR is related to an open issue please link to it. 5 | 6 | ### CheckLists 7 | 8 | - [ ] changes have been done against master branch, and PR does not conflict 9 | - [ ] new unit / functional tests have been added (whenever applicable) 10 | - [ ] test are passing in local environment 11 | - [ ] Travis tests are passing (or test results are not worse than on master branch :)) 12 | - [ ] Docs have been updated 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | ## Build generated 4 | build/ 5 | DerivedData/ 6 | 7 | ## Various settings 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata/ 17 | 18 | ## Other 19 | *.moved-aside 20 | *.xccheckout 21 | *.xcscmblueprint 22 | 23 | # CocoaPods 24 | Pods/ 25 | 26 | # Swift Package Manager 27 | # 28 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 29 | # Packages/ 30 | # Package.pins 31 | Package.resolved 32 | .build/ 33 | -------------------------------------------------------------------------------- /Example/Sources/Entries.json: -------------------------------------------------------------------------------- 1 | { 2 | "m001": { 3 | "duration": 60000, 4 | "id": "m001", 5 | "name": "demo1", 6 | "sources": [ 7 | { 8 | "id": "m001s001", 9 | "mimeType": "application/dash+xml", 10 | "url": "http://cfvod.kaltura.com/dasha/p/1851571/sp/185157100/serveFlavor/entryId/0_pl5lbfo0/v/2/flavorId/0_,zwq3l44r,otmaqpnf,ywkmqnkg,/forceproxy/true/name/a.mp4.urlset/manifest.mpd" 11 | } 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Classes/Player/PKController.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | 13 | @objc public protocol PKController { 14 | init(player: PlayerEngine?) 15 | } 16 | -------------------------------------------------------------------------------- /Classes/Providers/PlaylistProvider.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2021 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | // 11 | 12 | import Foundation 13 | 14 | @objc public protocol PlaylistProvider { 15 | 16 | func loadPlaylist(callback: @escaping (PKPlaylist?, Error?) -> Void) 17 | 18 | func cancel() 19 | } 20 | -------------------------------------------------------------------------------- /Classes/Player/VR/VRData.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | 13 | 14 | /// This class will be filled with some relevant properties at the future. 15 | /// For now this class is allocated and uses as indicator when VR content is detected. 16 | class VRData { 17 | } 18 | -------------------------------------------------------------------------------- /Classes/Platform.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | 13 | struct Platform { 14 | static let isSimulator: Bool = { 15 | var isSim = false 16 | #if arch(i386) || arch(x86_64) 17 | isSim = true 18 | #endif 19 | return isSim 20 | }() 21 | } 22 | -------------------------------------------------------------------------------- /Classes/Plugins/TimeIntervalExtension.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import UIKit 12 | 13 | extension TimeInterval { 14 | 15 | public func toInt32() -> Int32 { 16 | if !self.isNaN && !self.isInfinite { 17 | return Int32(self) 18 | } 19 | return 0 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/tag.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -u 4 | 5 | PODSPEC=*.podspec 6 | POD=$(basename $PODSPEC .podspec) 7 | 8 | pod ipc spec $POD.podspec > spec.json 9 | 10 | TARGET_TAG=$(jq '.source.tag' --raw-output spec.json) 11 | NAME=$(jq '.name' --raw-output spec.json) 12 | COMMIT_SHA=$(git rev-parse HEAD) 13 | 14 | cat << EOF > post.json 15 | { 16 | "ref": "refs/tags/$TARGET_TAG", 17 | "sha": "$COMMIT_SHA" 18 | } 19 | EOF 20 | 21 | POST_URL=https://api.github.com/repos/$GITHUB_REPOSITORY/git/refs 22 | 23 | curl $POST_URL -X POST -H "Content-Type: application/json" -H "authorization: Bearer $GITHUB_TOKEN" -d@post.json 24 | 25 | #Add current tag to job output 26 | echo "tag=${TARGET_TAG}" >> $GITHUB_OUTPUT 27 | 28 | echo "$NAME release tag added and it is ready for CocoaPods distribution, upcoming version is going to be $TARGET_TAG" 29 | -------------------------------------------------------------------------------- /Example/Tests/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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /tvOSTestApp/tvOSTestApp/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets" : [ 3 | { 4 | "filename" : "App Icon - App Store.imagestack", 5 | "idiom" : "tv", 6 | "role" : "primary-app-icon", 7 | "size" : "1280x768" 8 | }, 9 | { 10 | "filename" : "App Icon.imagestack", 11 | "idiom" : "tv", 12 | "role" : "primary-app-icon", 13 | "size" : "400x240" 14 | }, 15 | { 16 | "filename" : "Top Shelf Image Wide.imageset", 17 | "idiom" : "tv", 18 | "role" : "top-shelf-image-wide", 19 | "size" : "2320x720" 20 | }, 21 | { 22 | "filename" : "Top Shelf Image.imageset", 23 | "idiom" : "tv", 24 | "role" : "top-shelf-image", 25 | "size" : "1920x720" 26 | } 27 | ], 28 | "info" : { 29 | "author" : "xcode", 30 | "version" : 1 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Classes/Player/Tracks/PKTracks.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | 13 | @objc public class PKTracks: NSObject { 14 | @objc public var audioTracks: [Track]? 15 | @objc public var textTracks: [Track]? 16 | 17 | init(audioTracks: [Track]?, textTracks: [Track]?) { 18 | PKLog.verbose("init::") 19 | self.audioTracks = audioTracks 20 | self.textTracks = textTracks 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /Classes/Events/PKMessageBus.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2021 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | // 11 | // PKMessageBus.swift 12 | // PlayKit 13 | // 14 | // Created by Sergii Chausov on 07.11.2021. 15 | // 16 | 17 | import Foundation 18 | 19 | public protocol PKMessageBus { 20 | func getMessageBus() -> MessageBus 21 | } 22 | 23 | extension PlayerLoader: PKMessageBus { 24 | 25 | func getMessageBus() -> MessageBus { 26 | return self.messageBus 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Plugins/AnalyticsCommon/AnalyticsConfig.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import UIKit 12 | 13 | @objc public class AnalyticsConfig: NSObject { 14 | 15 | @objc public var params: [String: Any] 16 | 17 | @objc public init(params: [String: Any]) { 18 | self.params = params 19 | super.init() 20 | } 21 | 22 | @objc public override init() { 23 | self.params = [:] 24 | super.init() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | ##### Prerequisites 3 | - [ ] Have you checked for duplicate [issues](https://github.com/kaltura/playkit-ios/issues): ______ 4 | - [ ] Which Player [version](https://github.com/kaltura/playkit-ios/releases) are you using: ______ 5 | - [ ] Can you reproduce the issue with our latest release version: ______ 6 | - [ ] Can you reproduce the issue with the latest code from master: ______ 7 | - [ ] What devices and OS versions are you using: ______ 8 | - [ ] If applicable, add test code or test page to reproduce: 9 | ``` 10 | Paste test code here 11 | ``` 12 | 13 | ##### Expected behavior 14 | What you expected to happen 15 | 16 | ##### Actual behavior 17 | What actually happened 18 | 19 | ##### Console output 20 | ``` 21 | Paste the contents of the Xcode console here. 22 | ``` 23 | -------------------------------------------------------------------------------- /Example/PlayKit/ViewController.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, 5 | // unless a different license for a particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import UIKit 12 | import PlayKit 13 | 14 | 15 | 16 | 17 | class ViewController: UIViewController { 18 | 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | 23 | } 24 | 25 | override func didReceiveMemoryWarning() { 26 | super.didReceiveMemoryWarning() 27 | // Dispose of any resources that can be recreated. 28 | } 29 | 30 | } 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Classes/Providers/MediaEntryProviderResponseDelegate.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | import KalturaNetKit 13 | 14 | //This protocol provides a way to use the response data of the requests are being sent by MediaEntryProvider. 15 | //For example the response of getPlaybackContext, or additional meta data. 16 | 17 | public protocol PKMediaEntryProviderResponseDelegate: AnyObject { 18 | 19 | func providerGotResponse(sender: MediaEntryProvider?, response: Response) -> Void 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Example/PlayKit/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | } 43 | ], 44 | "info" : { 45 | "version" : 1, 46 | "author" : "xcode" 47 | } 48 | } -------------------------------------------------------------------------------- /Classes/Events/EventsPayloads/PKAdDAICuePoints.swift: -------------------------------------------------------------------------------- 1 | 2 | 3 | import Foundation 4 | 5 | @objc public class CuePoint: NSObject { 6 | @objc public private(set) var startTime: TimeInterval 7 | @objc public private(set) var endTime: TimeInterval 8 | @objc public private(set) var played: Bool 9 | 10 | @objc public init(startTime: TimeInterval, endTime: TimeInterval, played: Bool) { 11 | self.startTime = startTime 12 | self.endTime = endTime 13 | self.played = played 14 | } 15 | 16 | public override var description: String { 17 | return "StartTime:\(startTime) EndTime:\(endTime) Played:\(played)" 18 | } 19 | } 20 | 21 | @objc public class PKAdDAICuePoints: NSObject { 22 | 23 | @objc public private(set) var cuePoints: [CuePoint] 24 | 25 | @objc public init(_ cuePoints: [CuePoint]) { 26 | self.cuePoints = cuePoints 27 | } 28 | 29 | @objc public var hasPreRoll: Bool { 30 | return self.cuePoints.filter { $0.startTime == 0 }.count > 0 // pre-roll ads values = 0 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Plugins/AnalyticsCommon/AnalyticsPluginProtocol.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | import PlayKit 13 | 14 | @objc public protocol AnalyticsPluginProtocol: PKPlugin { 15 | 16 | /// Indicates if it first play. 17 | var isFirstPlay: Bool { get set } 18 | 19 | /// List of events should be handled on plugin. 20 | var playerEventsToRegister: [PlayerEvent.Type] { get } 21 | 22 | /// Event registrasion based on playerEventsToRegister array. 23 | func registerEvents() 24 | 25 | /// unregister all registered events. 26 | func unregisterEvents() 27 | } 28 | -------------------------------------------------------------------------------- /Classes/Events/PluginEvent.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2018 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | // 12 | // PluginEvent.swift 13 | // PlayKit 14 | // 15 | // Created by Nilit Danan on 11/14/18. 16 | // 17 | 18 | import Foundation 19 | 20 | @objc public class PluginEvent: PKEvent { 21 | 22 | /// Sent when a plugin error occurs. 23 | @objc public static let error: PluginEvent.Type = Error.self 24 | 25 | public class Error: PluginEvent { 26 | public convenience init(nsError: NSError) { 27 | self.init([EventDataKeys.error: nsError]) 28 | } 29 | 30 | public convenience init(error: PKError) { 31 | self.init([EventDataKeys.error: error.asNSError]) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Classes/Player/Tracks/Track.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | 13 | internal enum TrackType { 14 | case audio 15 | case text 16 | } 17 | 18 | @objc public class Track: NSObject { 19 | @objc public var id: String 20 | @objc public var title: String 21 | @objc public var language: String? 22 | 23 | var type: TrackType 24 | 25 | init(id: String, title: String, type: TrackType, language: String?) { 26 | PKLog.verbose("init:: id:\(String(describing: id)) title:\(String(describing: title)) language: \(String(describing: language))") 27 | 28 | self.id = id 29 | self.title = title 30 | self.type = type 31 | self.language = language 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Classes/Plugins/PlayerPluginsDataSource.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2020 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | 13 | public protocol PlayerPluginsDataSource { 14 | /// Filtering loaded plugins by certain type. 15 | func getLoadedPlugins(ofType type: T.Type) -> [T] 16 | 17 | func isPluginLoaded(pluginName name: String) -> Bool 18 | } 19 | 20 | extension PlayerLoader: PlayerPluginsDataSource { 21 | public func getLoadedPlugins(ofType type: T.Type) -> [T] { 22 | return self.loadedPlugins 23 | .filter { $0.value.plugin is T } 24 | .compactMap { $0.value.plugin as? T } 25 | } 26 | 27 | public func isPluginLoaded(pluginName name: String) -> Bool { 28 | return self.loadedPlugins.contains { $0.key == name } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | use_frameworks! 2 | 3 | source 'https://github.com/CocoaPods/Specs.git' 4 | 5 | platform :ios, '9.0' 6 | 7 | abstract_target 'PlayKit' do 8 | 9 | pod 'PlayKit', :path => '../../playkit-ios' 10 | pod 'PlayKit/YouboraPlugin', :path => '../../playkit-ios' 11 | pod 'PlayKit/PhoenixPlugin', :path => '../../playkit-ios' 12 | pod 'PlayKit/KalturaStatsPlugin', :path => '../../playkit-ios' 13 | pod 'PlayKit/KalturaLiveStatsPlugin', :path => '../../playkit-ios' 14 | pod 'PlayKit/GoogleCastAddon', :path => '../../playkit-ios' 15 | target 'PlayKit_Example' do 16 | end 17 | 18 | target 'PlayKit_Tests' do 19 | pod 'Quick', '1.1.0' 20 | pod 'Nimble', '7.0.1' 21 | end 22 | 23 | end 24 | 25 | pre_install do |installer| 26 | def installer.verify_no_static_framework_transitive_dependencies; end 27 | end 28 | 29 | post_install do |installer| 30 | installer.pods_project.targets.each do |target| 31 | target.build_configurations.each do |config| 32 | config.build_settings['ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES'] = 'NO' 33 | if target.name == 'PlayKit' 34 | config.build_settings['SWIFT_VERSION'] = '4.0' 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /Classes/Extensions/ArrayExtension.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | 13 | extension Array where Element: Equatable { 14 | 15 | /** 16 | Remove an element from the array and invalidates all indexes. 17 | 18 | - parameter element: The element to remove. 19 | */ 20 | mutating func remove(element: Element) { 21 | if let index = self.firstIndex(of: element) { 22 | self.remove(at: index) 23 | } 24 | } 25 | 26 | /** 27 | Removes elements from the array and invalidates all indexes. 28 | 29 | - parameter elements: The array with the elements to delete. 30 | */ 31 | mutating func remove(elements: [Element]) { 32 | for element in elements { 33 | remove(element: element) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Classes/Events/EventsPayloads/PKAdCuePoints.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | 13 | @objc public class PKAdCuePoints: NSObject { 14 | 15 | @objc public private(set) var cuePoints: [TimeInterval] 16 | 17 | @objc public init(cuePoints: [TimeInterval]) { 18 | self.cuePoints = cuePoints.sorted() // makes sure array is sorted 19 | } 20 | 21 | @objc public var count: Int { 22 | return self.cuePoints.count 23 | } 24 | 25 | @objc public var hasPreRoll: Bool { 26 | return self.cuePoints.filter { $0 == 0 }.count > 0 // pre-roll ads values = 0 27 | } 28 | 29 | @objc public var hasMidRoll: Bool { 30 | return self.cuePoints.filter { $0 > 0 }.count > 0 31 | } 32 | 33 | @objc public var hasPostRoll: Bool { 34 | return self.cuePoints.filter { $0 < 0 }.count > 0 // post-roll ads values = -1 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Classes/Models/PKTimeRange.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | import CoreMedia 13 | 14 | @objc public class PKTimeRange: NSObject { 15 | @objc public let start: TimeInterval 16 | @objc public let end: TimeInterval 17 | @objc public let duration: TimeInterval 18 | 19 | @objc public override var description: String { 20 | return "[\(String(describing: type(of: self)))] - start: \(self.start), end: \(self.end), duration: \(self.duration)" 21 | } 22 | 23 | init(start: TimeInterval, duration: TimeInterval) { 24 | self.start = start 25 | self.duration = duration 26 | self.end = start + duration 27 | } 28 | 29 | convenience init(timeRange: CMTimeRange) { 30 | let start = CMTimeGetSeconds(timeRange.start) 31 | let duration = CMTimeGetSeconds(timeRange.duration) 32 | self.init(start: start, duration: duration) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Classes/Player/PKAsset.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | import AVFoundation 13 | 14 | @objc enum PKAssetStatus: Int, CustomStringConvertible { 15 | case new 16 | case preparing 17 | case prepared 18 | case faild 19 | 20 | var description: String { 21 | switch self { 22 | case .new: return "new" 23 | case .preparing: return "preparing" 24 | case .prepared: return "prepared" 25 | case .faild: return "faild" 26 | } 27 | } 28 | } 29 | 30 | class PKAsset: NSObject { 31 | let avAsset: AVURLAsset 32 | let playerSettings: PKPlayerSettings 33 | let autoBuffer: Bool 34 | @objc dynamic var status: PKAssetStatus = .new 35 | 36 | init(avAsset: AVURLAsset, playerSettings: PKPlayerSettings, autoBuffer: Bool) { 37 | self.avAsset = avAsset 38 | self.playerSettings = playerSettings.createCopy() 39 | self.autoBuffer = autoBuffer 40 | 41 | super.init() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Classes/Player/Protocols/PlayerEngine.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | 13 | @objc public protocol PlayerEngine: BasicPlayer { 14 | /// Fired when an event is triggred. 15 | var onEventBlock: ((PKEvent) -> Void)? { get set } 16 | 17 | /// The player's start position. 18 | var startPosition: TimeInterval { get set } 19 | 20 | /// The player's current position. 21 | var currentPosition: TimeInterval { get set } 22 | 23 | /// The current media config that was set. 24 | var mediaConfig: MediaConfig? { get set } 25 | 26 | /// The media playback type. 27 | var playbackType: String? { get } 28 | 29 | /// Load the media to the player. 30 | func loadMedia(from mediaSource: PKMediaSource?, handler: AssetHandler) 31 | 32 | /// Plays the live media from the live edge. 33 | func playFromLiveEdge() 34 | 35 | /// Update the text tracks styling. 36 | func updateTextTrackStyling(_ textTrackStyling: PKTextTrackStyling) 37 | } 38 | -------------------------------------------------------------------------------- /Example/PlayKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | PlayKit 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 | 3.2.x-dev 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSAllowsArbitraryLoads 30 | 31 | 32 | UILaunchStoryboardName 33 | LaunchScreen 34 | UIMainStoryboardFile 35 | Main 36 | UIRequiredDeviceCapabilities 37 | 38 | armv7 39 | 40 | UISupportedInterfaceOrientations 41 | 42 | UIInterfaceOrientationPortrait 43 | UIInterfaceOrientationLandscapeLeft 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /travis-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eou pipefail 4 | 5 | # Travis aborts the build if it doesn't get output for 10 minutes. 6 | keepAlive() { 7 | while [ -f $1 ] 8 | do 9 | sleep 10 10 | echo . 11 | done 12 | } 13 | 14 | buildiOSApp() { 15 | echo Building the iOS TestApp 16 | cd TestApp 17 | pod install 18 | CODE=0 19 | xcodebuild clean build -workspace TestApp.xcworkspace -scheme TestApp_v4_2 ONLY_ACTIVE_ARCH=NO -destination 'platform=iOS Simulator,name=iPhone 11' | tee xcodebuild.log | xcpretty -r html || CODE=$? 20 | xcodebuild clean build -workspace TestApp.xcworkspace -scheme TestApp_v5 ONLY_ACTIVE_ARCH=NO -destination 'platform=iOS Simulator,name=iPhone 11' | tee xcodebuild.log | xcpretty -r html || CODE=$? 21 | cd ../ 22 | export CODE 23 | } 24 | 25 | buildtvOSApp() { 26 | echo Building the tvOS TestApp 27 | cd tvOSTestApp 28 | pod install 29 | CODE=0 30 | xcodebuild clean build -workspace tvOSTestApp.xcworkspace -scheme tvOSTestApp ONLY_ACTIVE_ARCH=NO -destination 'platform=tvOS Simulator,name=Apple TV' | tee xcodebuild.log | xcpretty -r html || CODE=$? 31 | cd ../ 32 | export CODE 33 | } 34 | 35 | libLint() { 36 | echo Linting the pod 37 | pod lib lint --fail-fast --allow-warnings 38 | } 39 | 40 | FLAG=$(mktemp) 41 | 42 | if [ "$TRAVIS_EVENT_TYPE" == "cron" ] || [ -n "$TRAVIS_TAG" ]; then 43 | # Full build at night and tags 44 | keepAlive $FLAG & 45 | libLint 46 | 47 | else 48 | # Else just build the test app 49 | buildiOSApp 50 | buildtvOSApp 51 | fi 52 | 53 | rm $FLAG # stop keepAlive 54 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ "develop" ] 6 | paths-ignore: 7 | - 'docs/**' 8 | - 'iOSTestApp/**' 9 | - 'tvOSTestApp/**' 10 | - 'Example/**' 11 | - 'TestApp/**' 12 | - 'LICENSE' 13 | - '*.md' 14 | 15 | concurrency: 16 | group: ci-${{ github.ref }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | build: 21 | name: "CocoaPods linting" 22 | runs-on: macOS-13 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - name: Updating Cocoapods 28 | run: gem install cocoapods 29 | 30 | - name: Updating CocoaPods repo 31 | run: pod repo update 32 | 33 | - name: Pod linting 34 | run: pod lib lint --fail-fast --verbose --allow-warnings 35 | 36 | SPM: 37 | name: "Build SPM" 38 | runs-on: macOS-13 39 | 40 | strategy: 41 | fail-fast: false 42 | matrix: 43 | include: 44 | - destination: "OS=17.2,name=iPhone 15" 45 | name: "iOS" 46 | scheme: "PlayKit-Package" 47 | - destination: "OS=17.0,name=Apple TV" 48 | name: "tvOS" 49 | scheme: "PlayKit-Package" 50 | steps: 51 | - name: Force Xcode 15 52 | run: sudo xcode-select -switch /Applications/Xcode_15.2.app 53 | - uses: actions/checkout@v4 54 | - name: ${{ matrix.name }} 55 | run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -scheme "${{ matrix.scheme }}" -destination "${{ matrix.destination }}" clean test | xcpretty 56 | 57 | -------------------------------------------------------------------------------- /Classes/Providers/MediaEntryProvider.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import UIKit 12 | 13 | @objc public protocol MediaEntryProvider { 14 | /** 15 | This method is triggering the creation of media base on custom parameters and actions. 16 | 17 | ## Important: 18 | - In order to write custom provider you should implement this method 19 | - In order to send an informative error in the ResponseElement 20 | you should implement an error enum with the relevnat errors 21 | 22 | - parameter callback - a block that called on completion and returing response object wich contain the PKMediaEntry 23 | ``` 24 | // example of usage: 25 | let cp : MediaEntryProvider = 26 | CustomMediaEntryProvider(customParameters) 27 | 28 | customMediaProvider.loadMedia { 29 | (r:ResponseElement) in 30 | if (r.succedded){ 31 | ... 32 | ``` 33 | 34 | */ 35 | func loadMedia(callback: @escaping (PKMediaEntry?, Error?) -> Void) 36 | 37 | func cancel() 38 | 39 | } 40 | -------------------------------------------------------------------------------- /banner-helper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | BANNER = """// =================================================================================================== 4 | // Copyright (C) %d Kaltura Inc. 5 | // 6 | // Licensed under the AGPLv3 license, unless a different license for a 7 | // particular library is specified in the applicable library path. 8 | // 9 | // You may obtain a copy of the License at 10 | // https://www.gnu.org/licenses/agpl-3.0.html 11 | // =================================================================================================== 12 | """ 13 | 14 | import os 15 | from sys import exit 16 | from datetime import datetime 17 | 18 | FIRST_YEAR = 2017 19 | THIS_YEAR = datetime.now().year 20 | 21 | def has_banner(d): 22 | d = d.lstrip() 23 | for year in xrange(FIRST_YEAR, THIS_YEAR + 1): 24 | if d.startswith(BANNER % year): 25 | return True 26 | return False 27 | 28 | def process(dir): 29 | global modified_files 30 | banner = BANNER % THIS_YEAR 31 | for root, dirs, files in os.walk(dir): 32 | for name in files: 33 | fileExt = os.path.splitext(name)[1] 34 | if fileExt != '.swift': 35 | continue 36 | 37 | fullPath = os.path.join(root, name) 38 | d = file(fullPath, 'rb').read() 39 | 40 | if has_banner(d): 41 | continue 42 | 43 | d = banner + '\n' + d 44 | 45 | file(fullPath, 'wb').write(d) 46 | 47 | modified_files += 1 48 | 49 | 50 | modified_files = 0 51 | 52 | for dir in ('Addons', 'Classes', 'Plugins', 'Widevine'): 53 | process(dir) 54 | 55 | # exit with 0 (OK) if there was at least one change. 56 | exit(0 if modified_files > 0 else 1) 57 | -------------------------------------------------------------------------------- /Example/Tests/Helpers/PluginTestConfiguration.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, 5 | // unless a different license for a particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | 13 | enum PluginTestConfiguration { 14 | case TVPAPI 15 | case Phoenix 16 | 17 | mutating func next() { 18 | switch self { 19 | case .TVPAPI: self = .Phoenix 20 | case .Phoenix: self = .TVPAPI 21 | } 22 | } 23 | 24 | var pluginName: String { 25 | switch self { 26 | case .TVPAPI: return "TVPAPIAnalyticsPluginMock" 27 | case .Phoenix: return "PhoenixAnalyticsPluginMock" 28 | } 29 | } 30 | 31 | var paramsDict: [String : Any] { 32 | switch self { 33 | case .TVPAPI: return [ 34 | "fileId": "464302", 35 | "baseUrl": "http://tvpapi-preprod.ott.kaltura.com/v3_9/gateways/jsonpostgw.aspx?", 36 | "timerInterval":30, 37 | "initObj": ["": ""] 38 | ] 39 | case .Phoenix: return [ 40 | "fileId": "464302", 41 | "baseUrl": "http://api-preprod.ott.kaltura.com/v4_1/api_v3/", 42 | "partnerId": 198, 43 | "timerInterval": 30 44 | ] 45 | } 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /TestApp/TestApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /TestApp/TestApp/TestApp_v5-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: 4 | pull_request: 5 | branches: [ "develop" ] 6 | paths-ignore: 7 | - 'docs/**' 8 | - 'iOSTestApp/**' 9 | - 'tvOSTestApp/**' 10 | - 'Example/**' 11 | - 'TestApp/**' 12 | - 'LICENSE' 13 | - '*.md' 14 | 15 | concurrency: 16 | group: PR_${{ github.head_ref }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | build: 21 | name: "CocoaPods linting" 22 | environment: PR 23 | runs-on: macos-13 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | 28 | - name: Updating Cocoapods 29 | run: gem install cocoapods 30 | 31 | - name: Updating CocoaPods repo 32 | run: pod repo update 33 | 34 | - name: Pod linting 35 | run: pod lib lint --fail-fast --verbose --allow-warnings 36 | 37 | SPM: 38 | name: "Build SPM" 39 | environment: PR 40 | runs-on: macos-13 41 | strategy: 42 | fail-fast: false 43 | matrix: 44 | include: 45 | - destination: "OS=17.2,name=iPhone 15" 46 | name: "iOS" 47 | scheme: "PlayKit-Package" 48 | - destination: "OS=17.0,name=Apple TV" 49 | name: "tvOS" 50 | scheme: "PlayKit-Package" 51 | steps: 52 | - name: Force Xcode 15 53 | run: sudo xcode-select -switch /Applications/Xcode_15.2.app 54 | - uses: actions/checkout@v4 55 | - name: ${{ matrix.name }} 56 | run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -scheme "${{ matrix.scheme }}" -destination "${{ matrix.destination }}" clean test | xcpretty 57 | 58 | 59 | -------------------------------------------------------------------------------- /Example/PlayKit/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tvOSTestApp/tvOSTestApp/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /TestApp/TestApp/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Classes/PKRequestParams.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | 13 | /// `PKRequestParamsDecorator` used for getting updated request info 14 | @objc public protocol PKRequestParamsAdapter { 15 | /// Called when need to update the request adapter with information from the player. 16 | /// Use this to update the adapter with any information available from the player. 17 | /// 18 | /// For example, when media session id changes. 19 | @objc func updateRequestAdapter(with player: Player) 20 | @objc func adapt(requestParams: PKRequestParams) -> PKRequestParams 21 | } 22 | 23 | @objc public class PKRequestParams: NSObject { 24 | 25 | @objc public let url: URL 26 | @objc public let headers: [String: String]? 27 | 28 | @objc public init(url: URL, headers: [String: String]?) { 29 | self.url = url 30 | self.headers = headers 31 | } 32 | } 33 | 34 | extension PKRequestParamsAdapter { 35 | func base64(from: String) -> String { 36 | return from.data(using: .utf8)?.base64EncodedString() ?? "" 37 | } 38 | 39 | func percentEncoded(_ string: String) -> String { 40 | string.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowedCharacterSet) ?? string 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /TestApp/TestApp/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Example/Tests/Analytics/AnalyticsPluginConfig.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, 5 | // unless a different license for a particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | 13 | enum AnalyticsPluginConfig { 14 | case TVPAPI 15 | case Phoenix 16 | 17 | mutating func next() { 18 | switch self { 19 | case .TVPAPI: self = .Phoenix 20 | case .Phoenix: self = .TVPAPI 21 | } 22 | } 23 | 24 | var pluginName: String { 25 | switch self { 26 | case .TVPAPI: return PhoenixAnalyticsPluginMock.pluginName 27 | case .Phoenix: return TVPAPIAnalyticsPluginMock.pluginName 28 | } 29 | } 30 | 31 | var paramsDict: [String : Any] { 32 | switch self { 33 | case .TVPAPI: return [ 34 | "fileId": "464302", 35 | "baseUrl": "http://tvpapi-preprod.ott.kaltura.com/v3_9/gateways/jsonpostgw.aspx?", 36 | "timerInterval":30000, 37 | "initObj": "" 38 | ] 39 | case .Phoenix: return [ 40 | "fileId": "464302", 41 | "baseUrl": "http://api-preprod.ott.kaltura.com/v4_1/api_v3/", 42 | "ks": "djJ8MTk4fL1W9Rs4udDqNt_CpUT9dJKk1laPk9_XnBtUaq7PXVcVPYrXz2shTbKSW1G5Lhn_Hvbbnh0snheANOmSodl7Puowxhk2WYkpmNugi9vNAg5C", 43 | "partnerId": 198, 44 | "timerInterval": 30 45 | ] 46 | } 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /PlayKit.podspec: -------------------------------------------------------------------------------- 1 | suffix = '.0000' # Dev mode 2 | # suffix = '' # Release 3 | 4 | Pod::Spec.new do |s| 5 | 6 | s.name = 'PlayKit' 7 | s.version = '3.31.0' + suffix 8 | s.summary = 'PlayKit: Kaltura Mobile Player SDK - iOS' 9 | s.homepage = 'https://github.com/kaltura/playkit-ios' 10 | s.license = { :type => 'AGPLv3', :text => 'AGPLv3' } 11 | s.author = { 'Kaltura' => 'community@kaltura.com' } 12 | s.source = { :git => 'https://github.com/kaltura/playkit-ios.git', :tag => 'v' + s.version.to_s } 13 | s.swift_version = '5.0' 14 | 15 | s.ios.deployment_target = '15.0' 16 | s.tvos.deployment_target = '15.0' 17 | 18 | s.subspec 'Core' do |sp| 19 | sp.source_files = 'Classes/**/*' 20 | sp.dependency 'kSwiftyJSON', '5.0.8' 21 | sp.dependency 'XCGLogger', '7.1.5' 22 | sp.dependency 'KalturaNetKit', '~> 1.7.0' 23 | sp.dependency 'PlayKitUtils', '~> 0.7' 24 | end 25 | 26 | s.subspec 'WidevineClassic' do |ssp| 27 | ssp.ios.deployment_target = '15.0' 28 | ssp.source_files = 'Widevine' 29 | ssp.dependency 'PlayKit/Core' 30 | #ssp.dependency 'PlayKitWV' 31 | #ssp.pod_target_xcconfig = { 'ENABLE_BITCODE' => 'NO', 'GCC_PREPROCESSOR_DEFINITIONS'=>'WIDEVINE_ENABLED=1', 32 | # 'OTHER_SWIFT_FLAGS' => '$(inherited) -DWIDEVINE_ENABLED' } 33 | end 34 | 35 | s.subspec 'AnalyticsCommon' do |ssp| 36 | ssp.source_files = 'Plugins/AnalyticsCommon' 37 | ssp.xcconfig = { 38 | 'CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES' => 'YES', 39 | 'OTHER_LDFLAGS' => '$(inherited)', 40 | 'FRAMEWORK_SEARCH_PATHS' => '$(inherited) "${PODS_ROOT}"/**', 41 | 'LIBRARY_SEARCH_PATHS' => '$(inherited) "${PODS_ROOT}"/**' 42 | } 43 | ssp.dependency 'PlayKit/Core' 44 | end 45 | 46 | s.default_subspec = 'Core' 47 | 48 | end 49 | -------------------------------------------------------------------------------- /Example/Tests/Basic/SourceSelectorTest.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, 5 | // unless a different license for a particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import XCTest 12 | import AVFoundation 13 | @testable import PlayKit 14 | 15 | class SourceSelectorTest: XCTestCase { 16 | 17 | let mp4 = PKMediaSource("mp4", contentUrl: URL(string: "https://example.com/a.mp4"), mediaFormat: .mp4) 18 | let hls = PKMediaSource("hls", contentUrl: URL(string: "https://example.com/hls.m3u8"), mediaFormat: .hls) 19 | let fps = PKMediaSource("fps", contentUrl: URL(string: "https://example.com/fps.m3u8"), mediaFormat: .hls) 20 | let wvm = PKMediaSource("wvm", contentUrl: URL(string: "https://example.com/a.wvm"), mediaFormat: .wvm ) 21 | 22 | func testSelectedSource() { 23 | 24 | guard let preferredMedia = DefaultAssetHandler.getPreferredMediaSource(from: PKMediaEntry("e", 25 | sources: [mp4, hls, fps])) else { 26 | XCTFail() 27 | return 28 | } 29 | 30 | DefaultAssetHandler().build(from: preferredMedia.0) { (error: Error?, asset: AVURLAsset?) in 31 | XCTAssertNil(error) 32 | 33 | guard let asset = asset else { 34 | XCTFail() 35 | return 36 | } 37 | 38 | XCTAssertEqual(asset.url.lastPathComponent, "hls.m3u8") 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Classes/Player/Media/PKMediaEntry+Copying.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | 13 | extension PKMediaEntry: NSCopying { 14 | 15 | public func copy(with zone: NSZone? = nil) -> Any { 16 | 17 | let sources = self.sources?.compactMap{ $0.copy() as? PKMediaSource } 18 | 19 | let entry = PKMediaEntry(self.id, 20 | sources: sources, 21 | duration: self.duration) 22 | entry.mediaType = self.mediaType 23 | entry.metadata = self.metadata 24 | entry.name = self.name 25 | entry.externalSubtitles = self.externalSubtitles 26 | entry.thumbnailUrl = self.thumbnailUrl 27 | entry.tags = self.tags 28 | 29 | return entry 30 | } 31 | } 32 | 33 | extension PKMediaSource: NSCopying { 34 | 35 | public func copy(with zone: NSZone? = nil) -> Any { 36 | let source = PKMediaSource(self.id, 37 | contentUrl: self.contentUrl, 38 | mimeType: self.mimeType, 39 | drmData: self.drmData, 40 | mediaFormat: self.mediaFormat) 41 | 42 | source.externalSubtitle = self.externalSubtitle 43 | source.contentRequestAdapter = self.contentRequestAdapter 44 | 45 | return source 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Classes/Player/Playlist/PKPlaylist.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2021 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | // 11 | 12 | import Foundation 13 | import kSwiftyJSON 14 | 15 | fileprivate let idKey = "id" 16 | fileprivate let nameKey = "name" 17 | fileprivate let thumbnailUrlKey = "thumbnailUrl" 18 | fileprivate let mediasKey = "medias" 19 | 20 | @objc public class PKPlaylist: NSObject { 21 | 22 | @objc public var id: String? 23 | @objc public var name: String? 24 | @objc public var thumbnailUrl: String? 25 | @objc public var medias: [PKMediaEntry]? 26 | 27 | @objc override public var description: String { 28 | get { 29 | return "id : \(self.id ?? "empty")," + 30 | " name: \(self.name ?? "empty")," + 31 | " thumbnailUrl: \(self.thumbnailUrl ?? "empty")" 32 | } 33 | } 34 | 35 | internal init(id: String?) { 36 | self.id = id 37 | super.init() 38 | } 39 | 40 | @objc public init(id: String?, name: String?, thumbnailUrl: String?, medias: [PKMediaEntry]) { 41 | self.id = id 42 | self.name = name 43 | self.thumbnailUrl = thumbnailUrl 44 | self.medias = medias 45 | super.init() 46 | } 47 | 48 | @objc public init(json: Any) { 49 | let jsonObject = json as? JSON ?? JSON(json) 50 | self.id = jsonObject[idKey].string 51 | self.name = jsonObject[nameKey].string 52 | 53 | super.init() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Example/Tests/MediaEntryProvider/RequestBuilder/KalturaRequestBuilderTest.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, 5 | // unless a different license for a particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import XCTest 12 | import Quick 13 | import Nimble 14 | @testable import PlayKit 15 | @testable import KalturaNetKit 16 | import kSwiftyJSON 17 | 18 | class KalturaRequestBuilderTest: QuickSpec { 19 | 20 | override func spec() { 21 | describe("KalturaRequestBuilder Test") { 22 | it("should build a request") { 23 | let expectedUrl = "https://cdnapisec.kaltura.com/service/test/action/add" 24 | let expectedBody = JSON(["test": "test"]) 25 | 26 | let url = "https://cdnapisec.kaltura.com" 27 | let service = "test" 28 | let action = "add" 29 | let kalturaRequestBuilder: KalturaRequestBuilder! = KalturaRequestBuilder(url: url, service: service, action: action) 30 | expect(kalturaRequestBuilder).toNot(beNil()) 31 | // the result 32 | let request = kalturaRequestBuilder.setBody(key: "test", value: "test").build() 33 | // check the result against expected 34 | let parsedRequestBody = JSON.init(parseJSON: String.init(data: request.dataBody!, encoding: .utf8)!) 35 | expect(expectedBody).to(equal(parsedRequestBody)) 36 | expect(expectedUrl).to(equal(request.url.absoluteString)) 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tvOSTestApp/tvOSTestApp/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // tvOSTestApp 4 | // 5 | // Created by Nilit Danan on 14/06/2022. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | func applicationWillResignActive(_ application: UIApplication) { 22 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 23 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 24 | } 25 | 26 | func applicationDidEnterBackground(_ application: UIApplication) { 27 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 28 | } 29 | 30 | func applicationWillEnterForeground(_ application: UIApplication) { 31 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 32 | } 33 | 34 | func applicationDidBecomeActive(_ application: UIApplication) { 35 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 36 | } 37 | 38 | 39 | } 40 | 41 | -------------------------------------------------------------------------------- /Classes/PKLog.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import XCGLogger 12 | import Foundation 13 | 14 | /// `PKLogLevel` describes the available log levels. 15 | @objc public enum PKLogLevel: Int, CustomStringConvertible { 16 | case verbose, debug, info, warning, error 17 | 18 | static let `default` = PKLogLevel.debug 19 | 20 | public var description: String { 21 | return String(describing: self).uppercased() 22 | } 23 | 24 | /// converts our levels to the levels of logger we wrap 25 | var toLoggerLevel: XCGLogger.Level { 26 | switch self { 27 | case .verbose: return .verbose 28 | case .debug: return .debug 29 | case .info: return .info 30 | case .warning: return .warning 31 | case .error: return .error 32 | } 33 | } 34 | } 35 | 36 | public let PKLog: XCGLogger = { 37 | let logger = XCGLogger(identifier: "PlayKit") 38 | logger.outputLevel = PKLogLevel.default.toLoggerLevel 39 | return logger 40 | }() 41 | 42 | // For compatibility with the old logger -- in XCGLogger 'trace' is called 'verbose'. 43 | public extension XCGLogger { 44 | func trace(_ closure: @autoclosure () -> Any?, functionName: StaticString = #function, fileName: StaticString = #file, lineNumber: Int = #line, userInfo: [String: Any] = [:]) { 45 | self.logln(.verbose, functionName: functionName, fileName: fileName, lineNumber: lineNumber, userInfo: userInfo, closure: closure) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tvOSTestApp/tvOSTestApp/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /TestApp/TestApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Classes/Player/PlayerState.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2018 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | // =================================================================================================== 12 | // Copyright (C) 2017 Kaltura Inc. 13 | // 14 | // Licensed under the AGPLv3 license, unless a different license for a 15 | // particular library is specified in the applicable library path. 16 | // 17 | // You may obtain a copy of the License at 18 | // https://www.gnu.org/licenses/agpl-3.0.html 19 | // =================================================================================================== 20 | 21 | import Foundation 22 | 23 | /// An PlayerState is an enum of different player states 24 | @objc public enum PlayerState: Int, CustomStringConvertible { 25 | /// Sent when player's state idle. 26 | case idle 27 | /// Sent when player's state ready. 28 | case ready 29 | /// Sent when player's state buffering. 30 | case buffering 31 | /// Sent when player's state ended. 32 | /// Same event sent when observing PlayerEvent.ended. 33 | /// This state was attached to reflect current state and avoid unrelevant boolean. 34 | case ended 35 | /// Sent when player's state errored. 36 | case error 37 | /// Sent when player's state unknown. 38 | case unknown = -1 39 | 40 | public var description: String { 41 | switch self { 42 | case .idle: return "Idle" 43 | case .ready: return "Ready" 44 | case .buffering: return "Buffering" 45 | case .ended: return "Ended" 46 | case .error: return "Error" 47 | case .unknown: return "Unknown" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Classes/Plugins/BasePlugin.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | 13 | /// class `BasePlugin` is a base plugin object used for plugin subclasses 14 | @objc open class BasePlugin: NSObject, PKPlugin { 15 | 16 | /// abstract implementation subclasses will have names 17 | @objc open class var pluginName: String { 18 | fatalError("abstract property must be overriden in subclass") 19 | } 20 | 21 | @objc open class var pluginVersion: String { 22 | guard let version = Bundle(for: self).object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String else { 23 | return "?.?.?" 24 | } 25 | return version 26 | } 27 | 28 | @objc public weak var player: Player? 29 | @objc public weak var messageBus: MessageBus? 30 | 31 | @objc public required init(player: Player, pluginConfig: Any?, messageBus: MessageBus) throws { 32 | let pluginClass = type(of: self) 33 | PKLog.verbose("initializing plugin \(pluginClass)") 34 | self.player = player 35 | self.messageBus = messageBus 36 | } 37 | 38 | @objc open func onUpdateMedia(mediaConfig: MediaConfig) { 39 | PKLog.verbose("plugin \(type(of:self)) onUpdateMedia with media config: \(String(describing: mediaConfig))") 40 | } 41 | 42 | @objc open func onUpdateConfig(pluginConfig: Any) { 43 | PKLog.verbose("plugin \(type(of:self)) onUpdateConfig with media config: \(String(describing: pluginConfig))") 44 | } 45 | 46 | @objc open func destroy() { 47 | PKLog.verbose("destroying plugin \(type(of:self))") 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "PlayKit", 7 | platforms: [.iOS(.v14), 8 | .tvOS(.v14)], 9 | products: [.library(name: "PlayKit", 10 | targets: ["PlayKit"]), 11 | .library(name: "AnalyticsCommon", 12 | targets: ["AnalyticsCommon"])], 13 | dependencies: [ 14 | .package(url: "https://github.com/imberezin/kSwiftyJSON.git", .upToNextMajor(from: "5.0.8")), 15 | .package(url: "https://github.com/DaveWoodCom/XCGLogger.git", .upToNextMajor(from: "7.1.5")), 16 | .package(name: "PlayKitUtils", 17 | url: "https://github.com/kaltura/playkit-ios-utils.git", 18 | .upToNextMinor(from: "0.7.0")), 19 | .package(name: "KalturaNetKit", 20 | url: "https://github.com/kaltura/netkit-ios.git", 21 | .upToNextMinor(from: "1.7.0")), 22 | .package(url: "https://github.com/Quick/Quick.git", .upToNextMajor(from: "7.5.0")), 23 | .package(url: "https://github.com/Quick/Nimble.git", .upToNextMajor(from: "13.3.0")), 24 | ], 25 | targets: [.target(name: "PlayKit", 26 | dependencies: 27 | [ 28 | "kSwiftyJSON", 29 | "XCGLogger", 30 | .product(name: "PlayKitUtils", package: "PlayKitUtils"), 31 | .product(name: "KalturaNetKit", package: "KalturaNetKit"), 32 | ], 33 | path: "Classes/"), 34 | .target(name: "AnalyticsCommon", 35 | dependencies: ["PlayKit"], 36 | path: "Plugins/AnalyticsCommon/"), 37 | .testTarget(name: "PlayKitTests", 38 | dependencies: ["PlayKit", "Quick", "Nimble"], 39 | path: "Example/Tests/Basic/", 40 | exclude: [ 41 | 42 | ]) 43 | ] 44 | ) 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![Swift 5.0+](https://img.shields.io/badge/Swift-5.0+-orange.svg) 3 | [![CI Status](https://github.com/kaltura/playkit-ios/actions/workflows/ci.yml/badge.svg)](https://github.com/kaltura/playkit-ios/actions/workflows/ci.yml) 4 | [![Version](https://img.shields.io/cocoapods/v/PlayKit.svg?style=flat)](https://cocoapods.org/pods/PlayKit) 5 | [![License](https://img.shields.io/cocoapods/l/PlayKit.svg?style=flat)](https://cocoapods.org/pods/PlayKit) 6 | [![Platform](https://img.shields.io/cocoapods/p/PlayKit.svg?style=flat)](https://cocoapods.org/pods/PlayKit) 7 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/3c95a3b285d9447cb525be42647af4f8)](https://www.codacy.com/app/PlayKit/playkit-ios?utm_source=github.com&utm_medium=referral&utm_content=kaltura/playkit-ios&utm_campaign=badger) 8 | 9 | # Kaltura Player SDK 10 | 11 | ### Demo: [Demo repo](https://github.com/kaltura/playkit-ios-samples). 12 | 13 | *If you are a Kaltura customer, please contact your Kaltura Customer Success Manager to help facilitate use of this component.* 14 | 15 | ## Overview 16 | The **Kaltura Player SDK** is fully native and introduces significant performance improvements. The SDK is intended to be integrated in any iOS application and includes the following features: 17 | 18 | * Online and Offline Playback 19 | * Live 20 | * Multi audio tracks 21 | * Multiple captions 22 | * Kaltura’s uDRM support (FairPlay, Widevine Classic) 23 | * VAST Support (IMA) 24 | * Kaltura analytics 25 | * Youbora 26 | * Chromecast and AirPlay support 27 | 28 | ## Usage Guides 29 | Please see our [VPaaS Documentation site](https://vpaas.kaltura.com/documentation/Mobile-Video-Player-SDKs/v3_iOS_Introduction.html). 30 | 31 | ## License and Copyright Information 32 | All code in this project is released under the [AGPLv3 license](http://www.gnu.org/licenses/agpl-3.0.html) unless a different license for a particular library is specified in the applicable library path. 33 | 34 | Copyright © Kaltura Inc. All rights reserved. 35 | Authors and contributors: See [GitHub contributors list](https://github.com/kaltura/playkit-ios/graphs/contributors). 36 | -------------------------------------------------------------------------------- /Classes/Player/Ads/AdsDAIPlugin.swift: -------------------------------------------------------------------------------- 1 | 2 | 3 | import Foundation 4 | 5 | public protocol AdsDAIPlugin: AdsPlugin { 6 | 7 | /** 8 | * Returns the content time without ads for a given stream time. Returns the given stream time 9 | * for live streams. 10 | * 11 | * @param streamTime the stream time with inserted ads (in seconds) 12 | * 13 | * @return the content time that corresponds with the given stream time once ads are removed 14 | */ 15 | func contentTime(forStreamTime streamTime: TimeInterval) -> TimeInterval 16 | 17 | /** 18 | * Returns the stream time with ads for a given content time. Returns the given content time 19 | * for live streams. 20 | * 21 | * @param contentTime the content time without any ads (in seconds) 22 | * 23 | * @return the stream time that corresponds with the given content time once ads are inserted 24 | */ 25 | func streamTime(forContentTime contentTime: TimeInterval) -> TimeInterval 26 | 27 | /** 28 | * Returns the previous cuepoint for the given stream time. Retuns nil if no such cuepoint exists. 29 | * This is used to implement features like snap back, and called when the publisher detects that 30 | * the user seeked in order to force the user to watch an ad break they may have skipped over. 31 | * 32 | * @param streamTime the stream time that was seeked to. 33 | * 34 | * @return the previous Cuepoint for the given stream time. 35 | */ 36 | func previousCuepoint(forStreamTime streamTime: TimeInterval) -> CuePoint? 37 | 38 | /** 39 | * Returns if the upcoming ad can be played, the duration of the ad and the ad's end time. 40 | * Returns nil in case no ad was found. 41 | * 42 | * @param streamTime the stream time to check for specific ad. 43 | * 44 | * @return (canPlay, duration, endTime) if the ad can be played or not, duration of the ad, the end time of the ad. 45 | */ 46 | func canPlayAd(atStreamTime streamTime: TimeInterval) -> (canPlay: Bool, duration: TimeInterval, endTime: TimeInterval)? 47 | } 48 | -------------------------------------------------------------------------------- /Classes/Plugins/PKPlugin.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import UIKit 12 | import AVFoundation 13 | 14 | /// The `PKPlugin` protocol defines all the properties and methods required to define a plugin object. 15 | @objc public protocol PKPlugin { 16 | /// The plugin name. 17 | static var pluginName: String { get } 18 | 19 | /** 20 | The plugin version. The default (implemented in BasePlugin) is the plugin's bundle `CFBundleShortVersionString`: 21 | `Bundle(for: pluginClass).object(forInfoDictionaryKey: "CFBundleShortVersionString")` 22 | Override this function to provide a version from a different source. 23 | 24 | Example for overriding: 25 | ``` 26 | @objc override public class var pluginVersion: String { 27 | return "1.2.3" 28 | } 29 | ``` 30 | 31 | If `CFBundleShortVersionString` wasn't found (or is not a string), and no alternative implementation is 32 | provided, the string "?.?.?" is used. 33 | 34 | */ 35 | static var pluginVersion: String { get } 36 | 37 | /// The player associated with the plugin 38 | weak var player: Player? { get } 39 | /// The messageBus associated with the plugin 40 | weak var messageBus: MessageBus? { get } 41 | /// On first load. used for doing initialization for the first time with the media config. 42 | init(player: Player, pluginConfig: Any?, messageBus: MessageBus) throws 43 | /// On update media. used to update the plugin with new media config when available. 44 | func onUpdateMedia(mediaConfig: MediaConfig) 45 | /// On update config. used to update the plugin config. 46 | func onUpdateConfig(pluginConfig: Any) 47 | /// Called on player destroy. 48 | func destroy() 49 | } 50 | 51 | @objc public protocol PKPluginWarmUp { 52 | static func warmUp() 53 | } 54 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | 8 | SPM: 9 | name: "Build SPM" 10 | environment: Tag 11 | runs-on: macos-13 12 | 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | include: 17 | - destination: "OS=17.2,name=iPhone 15" 18 | name: "iOS" 19 | scheme: "PlayKit-Package" 20 | - destination: "OS=17.0,name=Apple TV" 21 | name: "tvOS" 22 | scheme: "PlayKit-Package" 23 | steps: 24 | - name: Force Xcode 15 25 | run: sudo xcode-select -switch /Applications/Xcode_15.2.app 26 | - uses: actions/checkout@v4 27 | - name: ${{ matrix.name }} 28 | run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild docbuild -scheme "${{ matrix.scheme }}" -destination "${{ matrix.destination }}" clean test | xcpretty 29 | 30 | TAGGING: 31 | name: "Add Git Tag" 32 | runs-on: macos-13 33 | environment: Tag 34 | needs: SPM 35 | 36 | outputs: 37 | output1: ${{ steps.tagging.outputs.tag }} 38 | 39 | steps: 40 | - uses: actions/checkout@v4 41 | 42 | - name: Switching Podspec to release mode 43 | run: | 44 | sh .github/release_mode.sh 45 | 46 | - id: tagging 47 | name: Add git tag 48 | run: | 49 | GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} sh .github/tag.sh 50 | 51 | PODS_PUSH: 52 | name: "CocoaPods push" 53 | runs-on: macos-13 54 | environment: CocoaPods 55 | needs: TAGGING 56 | 57 | steps: 58 | - uses: actions/checkout@v4 59 | - run: git pull 60 | 61 | - name: Updating Cocoapods 62 | run: gem install cocoapods 63 | 64 | - name: Updating CocoaPods repo 65 | run: pod repo update 66 | 67 | - name: CocoaPods push 68 | run: | 69 | PODS_USER=${{ secrets.PODS_USER }} PODS_PASS=${{ secrets.PODS_PASS }} sh .github/cocoapods_publish.sh 70 | 71 | - name: Prepare release notes 72 | run: | 73 | sh .github/release_notes.sh 74 | 75 | - uses: ncipollo/release-action@v1 76 | with: 77 | tag: ${{needs.TAGGING.outputs.output1}} 78 | bodyFile: ".github/release_notes_template.md" 79 | token: ${{ secrets.GITHUB_TOKEN }} 80 | draft: true 81 | -------------------------------------------------------------------------------- /TestApp/TestApp/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // TestApp 4 | // 5 | // Created by Noam Tamim on 21/10/2018. 6 | // Copyright © 2018 Kaltura. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Classes/Player/VR/VRPlayerEngine.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | import AVFoundation 13 | import UIKit 14 | 15 | /************************************************************/ 16 | // MARK: - ViewState 17 | /************************************************************/ 18 | 19 | @objc public enum ViewState: Int, CustomStringConvertible { 20 | /// Sent when player's view state panorama. 21 | case panorama 22 | /// Sent when player's view state vr. 23 | case stereo 24 | /// Sent when player's view state errored. 25 | case error 26 | /// Sent when player's view state unknown. 27 | case unknown = -1 28 | 29 | public var description: String { 30 | switch self { 31 | case .panorama: return "Panorama" 32 | case .stereo: return "Stereo" 33 | case .error: return "Error" 34 | case .unknown: return "Unknown" 35 | } 36 | } 37 | } 38 | 39 | /************************************************************/ 40 | // MARK: - VRPlayerEngine 41 | /************************************************************/ 42 | 43 | /// `VRPlayerEngine` protocol defines the methods needed to implement in order to work with the vr player engine. 44 | @objc public protocol VRPlayerEngine: PlayerEngine { 45 | /// VRPlayerEngine initializer 46 | /// 47 | init() 48 | 49 | /// Current View State 50 | var currentViewState: ViewState { get } 51 | 52 | /// Enable VR Mode - Stereo display for Google's Cardboard. 53 | /// 54 | /// - Parameter isEnabled: Toggle to enable vr mode. 55 | func setVRModeEnabled(_ isEnabled: Bool) 56 | 57 | /// Requests reset of rotation in the next rendering frame. 58 | func centerViewPoint() 59 | 60 | /// Creates the orientation indicator view. 61 | /// 62 | /// - Parameter frame: The frame of orientation indicator view. 63 | func createOrientationIndicatorView(frame: CGRect) -> UIView? 64 | } 65 | -------------------------------------------------------------------------------- /Example/PlayKit.xcodeproj/xcshareddata/xcschemes/PlayKit-Tests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 16 | 18 | 24 | 25 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 44 | 45 | 46 | 47 | 53 | 54 | 56 | 57 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - google-cast-sdk (3.5.0): 3 | - google-cast-sdk/Core (= 3.5.0) 4 | - google-cast-sdk/Core (3.5.0) 5 | - KalturaNetKit (0.0.17): 6 | - KalturaNetKit/Core (= 0.0.17) 7 | - KalturaNetKit/Core (0.0.17): 8 | - SwiftyJSON 9 | - Log (1.0) 10 | - Nimble (7.0.1) 11 | - PlayKit (3.4.x-dev): 12 | - PlayKit/Core (= 3.4.x-dev) 13 | - PlayKit/AnalyticsCommon (3.4.x-dev): 14 | - PlayKit/Core 15 | - PlayKit/Core (3.4.x-dev): 16 | - KalturaNetKit (~> 0.0) 17 | - Log (= 1.0) 18 | - PlayKitUtils (= 0.1.4) 19 | - SwiftyJSON (= 3.1.4) 20 | - SwiftyXMLParser (= 3.0.3) 21 | - PlayKit/GoogleCastAddon (3.4.x-dev): 22 | - google-cast-sdk (= 3.5) 23 | - PlayKit/Core 24 | - PlayKit/KalturaLiveStatsPlugin (3.4.x-dev): 25 | - PlayKit/AnalyticsCommon 26 | - PlayKit/Core 27 | - PlayKit/KalturaStatsPlugin (3.4.x-dev): 28 | - PlayKit/AnalyticsCommon 29 | - PlayKit/Core 30 | - PlayKit/PhoenixPlugin (3.4.x-dev): 31 | - PlayKit/AnalyticsCommon 32 | - PlayKit/Core 33 | - PlayKit/YouboraPlugin (3.4.x-dev): 34 | - PlayKit/AnalyticsCommon 35 | - PlayKit/Core 36 | - Youbora-AVPlayer/dynamic (= 5.4.18) 37 | - PlayKitUtils (0.1.4) 38 | - Quick (1.1.0) 39 | - SwiftyJSON (3.1.4) 40 | - SwiftyXMLParser (3.0.3) 41 | - Youbora-AVPlayer/dynamic (5.4.18): 42 | - Youbora-YouboraLib/dynamic (= 5.4.4) 43 | - Youbora-YouboraLib/dynamic (5.4.4) 44 | 45 | DEPENDENCIES: 46 | - Nimble (= 7.0.1) 47 | - PlayKit (from `../../playkit-ios`) 48 | - PlayKit/GoogleCastAddon (from `../../playkit-ios`) 49 | - PlayKit/KalturaLiveStatsPlugin (from `../../playkit-ios`) 50 | - PlayKit/KalturaStatsPlugin (from `../../playkit-ios`) 51 | - PlayKit/PhoenixPlugin (from `../../playkit-ios`) 52 | - PlayKit/YouboraPlugin (from `../../playkit-ios`) 53 | - Quick (= 1.1.0) 54 | 55 | EXTERNAL SOURCES: 56 | PlayKit: 57 | :path: ../../playkit-ios 58 | 59 | SPEC CHECKSUMS: 60 | google-cast-sdk: fc3728ead66150ac746e001ba03a44e4b10e359e 61 | KalturaNetKit: d37b3c6fc08da9935677fc547c59b25abc3574a3 62 | Log: 5e368c9528db07517d18d2d04ff5fe2b6f5a1e21 63 | Nimble: 657d000e11df8aebe27cdaf9d244de7f30ed87f7 64 | PlayKit: 099466211a5f9df804308d170cbf6a1cec8c3bda 65 | PlayKitUtils: 8f03f72690805e20718be6f913547e0966c835f9 66 | Quick: dafc587e21eed9f4cab3249b9f9015b0b7a7f71d 67 | SwiftyJSON: c2842d878f95482ffceec5709abc3d05680c0220 68 | SwiftyXMLParser: 9c6cc0310e778266a63d8eba50d9e07925aab0fb 69 | Youbora-AVPlayer: e824d4332e65825e9e79347bc4990a407b96ca9d 70 | Youbora-YouboraLib: 90bfccf52b9e918c5cb8543a5a5c015c0f02c4f2 71 | 72 | PODFILE CHECKSUM: 24ed6305dcb645a9df435e307d6a6f160e7b6259 73 | 74 | COCOAPODS: 1.4.0 75 | -------------------------------------------------------------------------------- /Example/PlayKit/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, 5 | // unless a different license for a particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import UIKit 12 | 13 | @UIApplicationMain 14 | class AppDelegate: UIResponder, UIApplicationDelegate { 15 | 16 | var window: UIWindow? 17 | 18 | 19 | 20 | 21 | 22 | 23 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 24 | // Override point for customization after application launch. 25 | return true 26 | } 27 | 28 | func applicationWillResignActive(_ application: UIApplication) { 29 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 30 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 31 | } 32 | 33 | func applicationDidEnterBackground(_ application: UIApplication) { 34 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 35 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 36 | } 37 | 38 | func applicationWillEnterForeground(_ application: UIApplication) { 39 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 40 | } 41 | 42 | func applicationDidBecomeActive(_ application: UIApplication) { 43 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 44 | } 45 | 46 | func applicationWillTerminate(_ application: UIApplication) { 47 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 48 | } 49 | 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /Example/Tests/Analytics/TVPAPI/TVPAPIPluginTest.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, 5 | // unless a different license for a particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import XCTest 12 | @testable import PlayKit 13 | import Quick 14 | import Nimble 15 | import KalturaNetKit 16 | 17 | class TVPAPIPluginTest: QuickSpec { 18 | 19 | /************************************************************/ 20 | // MARK: - Mocks 21 | /************************************************************/ 22 | 23 | class TVPAPIAnalyticsPluginMock: TVPAPIAnalyticsPlugin { 24 | 25 | public override class var pluginName: String { return PluginTestConfiguration.TVPAPI.pluginName } 26 | } 27 | 28 | /************************************************************/ 29 | // MARK: - Tests 30 | /************************************************************/ 31 | 32 | override func spec() { 33 | describe("TVPAPIPluginTest") { 34 | var player: PlayerLoader! 35 | var tvpapiPluginMock: TVPAPIPluginTest.TVPAPIAnalyticsPluginMock! 36 | 37 | beforeEach { 38 | PlayKitManager.shared.registerPlugin(TVPAPIPluginTest.TVPAPIAnalyticsPluginMock.self) 39 | player = self.createPlayerForTVPAPI() 40 | tvpapiPluginMock = player.loadedPlugins[TVPAPIPluginTest.TVPAPIAnalyticsPluginMock.pluginName]!.plugin as! TVPAPIPluginTest.TVPAPIAnalyticsPluginMock 41 | } 42 | 43 | afterEach { 44 | self.destroyPlayer(player) 45 | } 46 | 47 | it("can build play event request") { 48 | let expectedDataBody = "{\"iMediaID\":\"test\",\"mediaType\":0,\"Action\":\"play\",\"initObj\":{\"\":\"\"},\"iLocation\":0,\"iFileID\":\"464302\"}".data(using: .utf8) 49 | let expectedUrl = "http://tvpapi-preprod.ott.kaltura.com/v3_9/gateways/jsonpostgw.aspx?m=MediaMark" 50 | let expectedMethod: RequestMethod = .post 51 | 52 | let request = tvpapiPluginMock.buildRequest(ofType: .play) 53 | 54 | expect(expectedUrl).to(equal(request?.url.absoluteString)) 55 | expect(expectedDataBody).to(equal(request?.dataBody)) 56 | expect(expectedMethod).to(equal(request?.method)) 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Classes/Player/AVAsset/PKAssetResourceLoaderDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PKAssetResourceLoaderDelegate.swift 3 | // PlayKit 4 | // 5 | // Created by Nilit Danan on 8/28/19. 6 | // 7 | 8 | import Foundation 9 | import AVFoundation 10 | 11 | class PKAssetResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate { 12 | 13 | var delegates: [String : AVAssetResourceLoaderDelegate] = [:] 14 | 15 | func setDelegate(_ delegate : AVAssetResourceLoaderDelegate, forScheme scheme: String) { 16 | delegates[scheme] = delegate 17 | } 18 | 19 | // MARK: - AVAssetResourceLoaderDelegate 20 | 21 | func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool { 22 | 23 | PKLog.verbose("\(#function) was called in PKAssetResourceLoaderDelegate with loadingRequest: \(loadingRequest)") 24 | 25 | guard let scheme = loadingRequest.request.url?.scheme else { return false } 26 | guard let delegate = delegates[scheme] else { return false } 27 | 28 | return delegate.resourceLoader?(resourceLoader, shouldWaitForLoadingOfRequestedResource: loadingRequest) ?? false 29 | } 30 | 31 | func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForRenewalOfRequestedResource renewalRequest: AVAssetResourceRenewalRequest) -> Bool { 32 | 33 | PKLog.verbose("\(#function) was called in PKAssetResourceLoaderDelegate with renewalRequest: \(renewalRequest)") 34 | 35 | guard let scheme = renewalRequest.request.url?.scheme else { return false } 36 | guard let delegate = delegates[scheme] else { return false } 37 | 38 | return delegate.resourceLoader?(resourceLoader, shouldWaitForRenewalOfRequestedResource: renewalRequest) ?? false 39 | } 40 | 41 | func resourceLoader(_ resourceLoader: AVAssetResourceLoader, didCancel loadingRequest: AVAssetResourceLoadingRequest) { 42 | 43 | PKLog.verbose("\(#function) was called in PKAssetResourceLoaderDelegate with loadingRequest: \(loadingRequest)") 44 | } 45 | 46 | func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForResponseTo authenticationChallenge: URLAuthenticationChallenge) -> Bool { 47 | 48 | PKLog.verbose("\(#function) was called in PKAssetResourceLoaderDelegate with authenticationChallenge: \(authenticationChallenge)") 49 | return false 50 | } 51 | 52 | func resourceLoader(_ resourceLoader: AVAssetResourceLoader, didCancel authenticationChallenge: URLAuthenticationChallenge) { 53 | 54 | PKLog.verbose("\(#function) was called in PKAssetResourceLoaderDelegate with authenticationChallenge: \(authenticationChallenge)") 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Example/Tests/Analytics/Phoenix/PhoenixPluginTest.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, 5 | // unless a different license for a particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | import Quick 13 | import Nimble 14 | @testable import PlayKit 15 | 16 | class PhoenixPluginTest: QuickSpec { 17 | 18 | /************************************************************/ 19 | // MARK: - Mocks 20 | /************************************************************/ 21 | 22 | class PhoenixAnalyticsPluginMock: PhoenixAnalyticsPlugin { 23 | 24 | public override class var pluginName: String { return PluginTestConfiguration.Phoenix.pluginName } 25 | } 26 | 27 | /************************************************************/ 28 | // MARK: - Tests 29 | /************************************************************/ 30 | 31 | override func spec() { 32 | describe("PhoenixPluginTest") { 33 | var player: PlayerLoader! 34 | var phoenixPluginMock: PhoenixPluginTest.PhoenixAnalyticsPluginMock! 35 | 36 | beforeEach { 37 | PlayKitManager.shared.registerPlugin(PhoenixPluginTest.PhoenixAnalyticsPluginMock.self) 38 | player = self.createPlayerForPhoenix() 39 | phoenixPluginMock = player.loadedPlugins[PhoenixPluginTest.PhoenixAnalyticsPluginMock.pluginName]!.plugin as! PhoenixPluginTest.PhoenixAnalyticsPluginMock 40 | } 41 | 42 | afterEach { 43 | self.destroyPlayer(player) 44 | } 45 | 46 | it("can build play event request") { 47 | let expectedDataBody = "{\"clientTag\":\"java:16-09-10\",\"apiVersion\":\"3.6.1078.11798\",\"bookmark\":{\"position\":0,\"objectType\":\"KalturaBookmark\",\"type\":\"media\",\"id\":\"test\",\"playerData\":{\"fileId\":\"464302\",\"action\":\"PLAY\",\"objectType\":\"KalturaBookmarkPlayerData\"}},\"ks\":\"\"}".data(using: .utf8) 48 | let expectedUrl = "http://api-preprod.ott.kaltura.com/v4_1/api_v3//service/bookmark/action/add" 49 | 50 | let request = phoenixPluginMock.buildRequest(ofType: .play) 51 | 52 | expect(expectedUrl).to(equal(request?.url.absoluteString)) 53 | expect(expectedDataBody).to(equal(request?.dataBody)) 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Classes/Player/PlayerController+TimeMonitor.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | 13 | extension PlayerController: TimeMonitor, TimeProvider { 14 | 15 | @objc public func addPeriodicObserver(interval: TimeInterval, observeOn dispatchQueue: DispatchQueue?, using eventHandler: @escaping (TimeInterval) -> Void) -> UUID { 16 | PKLog.debug("add periodic observer with interval: \(interval), on queue: \(String(describing: dispatchQueue))") 17 | let token = self.timeObserver.addPeriodicObserver(interval: interval, observeOn: dispatchQueue, using: eventHandler) 18 | PKLog.debug("periodic observer added with token: \(token.uuidString)") 19 | return token 20 | } 21 | 22 | @objc public func addBoundaryObserver(boundaries: [PKBoundary], observeOn dispatchQueue: DispatchQueue?, using block: @escaping (TimeInterval, Double) -> Void) -> UUID { 23 | return self.addBoundaryObserver(times: boundaries.map { $0.time }, observeOn: dispatchQueue, using: block) 24 | } 25 | 26 | @objc public func addBoundaryObserver(times: [TimeInterval], observeOn dispatchQueue: DispatchQueue?, using eventHandler: @escaping (TimeInterval, Double) -> Void) -> UUID { 27 | PKLog.debug("add boundary observer with times: \(times), on queue: \(String(describing: dispatchQueue))") 28 | let token = self.timeObserver.addBoundaryObserver(times: times, observeOn: dispatchQueue, using: eventHandler) 29 | PKLog.debug("boundary observer added with token: \(token.uuidString)") 30 | return token 31 | } 32 | 33 | @objc public func removePeriodicObserver(_ token: UUID) { 34 | PKLog.debug("remove periodic observer with token: \(token.uuidString)") 35 | self.timeObserver.removePeriodicObserver(token) 36 | } 37 | 38 | @objc public func removeBoundaryObserver(_ token: UUID) { 39 | PKLog.debug("remove boundary observer with token: \(token.uuidString)") 40 | self.timeObserver.removeBoundaryObserver(token) 41 | } 42 | 43 | func removePeriodicObservers() { 44 | PKLog.debug("remove all periodic observers") 45 | self.timeObserver.removePeriodicObservers() 46 | } 47 | 48 | func removeBoundaryObservers() { 49 | PKLog.debug("remove all boundary observers") 50 | self.timeObserver.removeBoundaryObservers() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Example/Tests/Basic/TracksTest.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, 5 | // unless a different license for a particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import XCTest 12 | import PlayKit 13 | import kSwiftyJSON 14 | 15 | class TracksTest: XCTestCase { 16 | var player : Player! 17 | var tracks: PKTracks? 18 | 19 | override func setUp() { 20 | super.setUp() 21 | 22 | let url = URL(string: "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8") 23 | 24 | let mediaEntry = PKMediaEntry("test-id", 25 | sources: [PKMediaSource("test", contentUrl: url)], 26 | duration: -1) 27 | 28 | self.player = PlayKitManager.shared.loadPlayer(pluginConfig: nil) 29 | self.player.prepare(MediaConfig(mediaEntry: mediaEntry)) 30 | } 31 | 32 | func testGetTracksByEvent() { 33 | self.player.play() 34 | 35 | let theExeption = expectation(description: "get tracks") 36 | 37 | self.player.addObserver(self, 38 | events: [PlayerEvent.tracksAvailable]) { event in 39 | 40 | if event is PlayerEvent.TracksAvailable { 41 | self.tracks = event.tracks 42 | 43 | theExeption.fulfill() 44 | } else { 45 | XCTFail() 46 | } 47 | } 48 | 49 | waitForExpectations(timeout: 30.0) { (_) -> Void in} 50 | } 51 | 52 | func testSelectTrack() { 53 | 54 | self.player.play() 55 | 56 | let theExeption = expectation(description: "select track") 57 | 58 | self.player.addObserver(self, 59 | events: [PlayerEvent.tracksAvailable]) { event in 60 | if event is PlayerEvent.TracksAvailable { 61 | print(event) 62 | self.player.selectTrack(trackId: "sbtl:0") 63 | 64 | theExeption.fulfill() 65 | } else { 66 | XCTFail() 67 | } 68 | } 69 | 70 | waitForExpectations(timeout: 30.0) { (_) -> Void in} 71 | } 72 | 73 | override func tearDown() { 74 | // Put teardown code here. This method is called after the invocation of each test method in the class. 75 | super.tearDown() 76 | self.destroyPlayer(player) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Classes/Managers/NotificationsManager.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | 13 | /// The `NotificationsManager` objects provides a mechanism for adding/removing observers within a program 14 | public final class NotificationsManager { 15 | 16 | let notificationCenter = NotificationCenter.default 17 | 18 | /// lock object for synchronizing access 19 | let lock: AnyObject = UUID().uuidString as AnyObject 20 | 21 | /// Holds all the notification observers tokens 22 | var observerTokens = [NSObjectProtocol]() 23 | 24 | 25 | /// Adds an observer for notification name, appends the token to the token list and returns it. 26 | /// - returns: The added observer token 27 | @discardableResult 28 | func addObserver(notificationName: Notification.Name, 29 | object: Any? = nil, 30 | queue: DispatchQueue = DispatchQueue.main, 31 | using block: @escaping (Notification) -> Void) -> NSObjectProtocol { 32 | 33 | let observerToken = notificationCenter.addObserver(forName: notificationName, 34 | object: object, 35 | queue: OperationQueue.main, 36 | using: block) 37 | sync { 38 | observerTokens.append(observerToken) 39 | } 40 | return observerToken 41 | } 42 | 43 | /// Removes an observer 44 | func remove(observer: AnyObject) { 45 | sync { 46 | notificationCenter.removeObserver(observer) 47 | for i in 0.. ()) { 69 | objc_sync_enter(lock) 70 | block() 71 | objc_sync_exit(lock) 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /Classes/Models/PKBoundary.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | 13 | /// `PKBoundary` used as abstract for boundary types (% and time). 14 | @objc public protocol PKBoundary { 15 | var time: TimeInterval { get } 16 | } 17 | 18 | /// `PKBoundaryFactory` factory class used to create boundary objects easily. 19 | @objc public class PKBoundaryFactory: NSObject { 20 | 21 | let duration: TimeInterval 22 | 23 | @objc public init (duration: TimeInterval) { 24 | self.duration = duration 25 | } 26 | 27 | @objc public func percentageTimeBoundary(boundary: Int) -> PKPercentageTimeBoundary { 28 | return PKPercentageTimeBoundary(boundary: boundary, duration: self.duration) 29 | } 30 | 31 | @objc public func timeBoundary(boundaryTime: TimeInterval) -> PKTimeBoundary { 32 | return PKTimeBoundary(boundaryTime: boundaryTime, duration: self.duration) 33 | } 34 | } 35 | 36 | /// `PKPercentageTimeBoundary` represents a time boundary in % against the media duration. 37 | @objc public class PKPercentageTimeBoundary: NSObject, PKBoundary { 38 | 39 | /// The time to set the boundary on. 40 | public let time: TimeInterval 41 | 42 | /// Creates a new `PKPercentageTimeBoundary` object from %. 43 | /// - Attention: boundary value should be between 1 and 100 otherwise will use default values! 44 | @objc public init(boundary: Int, duration: TimeInterval) { 45 | switch boundary { 46 | case 1...100: self.time = duration * TimeInterval(boundary) / TimeInterval(100) 47 | case Int.min...0: self.time = 0 48 | case 101...Int.max: self.time = duration 49 | default: self.time = 0 50 | } 51 | } 52 | } 53 | 54 | /// `PKTimeBoundary` represents a time boundary in seconds. 55 | @objc public class PKTimeBoundary: NSObject, PKBoundary { 56 | 57 | /// The time to set the boundary on. 58 | @objc public let time: TimeInterval 59 | 60 | /// Creates a new `PKTimeBoundary` object from seconds. 61 | /// - Attention: boundary value should be between 0 and duration otherwise will use default values! 62 | @objc public init(boundaryTime: TimeInterval, duration: TimeInterval) { 63 | if boundaryTime <= 0 { 64 | self.time = 0 65 | } else if boundaryTime >= duration { 66 | self.time = duration 67 | } else { 68 | self.time = boundaryTime 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Classes/Player/Ads/AdsPlugin.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, 5 | // unless a different license for a particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | import AVKit 13 | 14 | public enum PlayType: CustomStringConvertible { 15 | case play, resume 16 | 17 | public var description: String { 18 | switch self { 19 | case .play: 20 | return "Play" 21 | case .resume: 22 | return "Resume" 23 | } 24 | } 25 | } 26 | 27 | public protocol AdsPluginDataSource: AnyObject { 28 | /// The player's media config start time. 29 | var playAdsAfterTime: TimeInterval { get } 30 | } 31 | 32 | public protocol AdsPluginDelegate: AnyObject { 33 | func adsPlugin(_ adsPlugin: AdsPlugin, loaderFailedWith error: String) 34 | func adsPlugin(_ adsPlugin: AdsPlugin, managerFailedWith error: String) 35 | func adsPlugin(_ adsPlugin: AdsPlugin, didReceive event: PKEvent) 36 | 37 | /// called when ads request was timed out, telling the player if it should start play afterwards. 38 | func adsRequestTimedOut(shouldPlay: Bool) 39 | 40 | /// called when the plugin wants the player to start play. 41 | func play(_ playType: PlayType) 42 | } 43 | 44 | public protocol AdsPlugin: PKPlugin { 45 | var dataSource: AdsPluginDataSource? { get set } 46 | var delegate: AdsPluginDelegate? { get set } 47 | /// Is ad currently playing. 48 | var isAdPlaying: Bool { get } 49 | /// Whether or not the pre-roll should be played upon start position different than 0. 50 | var startWithPreroll: Bool { get } 51 | /// Request ads from the server. 52 | func requestAds() throws 53 | /// Resume ad. 54 | func resume() 55 | /// Pause ad. 56 | func pause() 57 | /// Ad content complete. 58 | func contentComplete() 59 | /// Destroy the ads manager. 60 | func destroyManager() 61 | /// Called after player called `super.play()`. 62 | func didPlay() 63 | /// Called when play() or resume() was called. 64 | /// Used to make the neccery checks with the ads plugin if can play or resume the content. 65 | func didRequestPlay(ofType type: PlayType) 66 | /// Called when entering the background. 67 | func didEnterBackground() 68 | /// Called when coming back from background. 69 | func willEnterForeground() 70 | } 71 | 72 | #if os(iOS) 73 | public protocol PIPEnabledAdsPlugin: AdsPlugin, AVPictureInPictureControllerDelegate { 74 | var pipDelegate: AVPictureInPictureControllerDelegate? { get set } 75 | } 76 | #endif 77 | -------------------------------------------------------------------------------- /Classes/Events/InterceptorEvent.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2021 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | // 11 | // InterceptorEvent.swift 12 | // PlayKit 13 | // 14 | // Created by Sergey Chausov on 07.07.2021. 15 | // 16 | 17 | import Foundation 18 | 19 | /// InterceptorEvent is a class that is used to reflect specific PKMediaEntryInterceptor events. 20 | @objc public class InterceptorEvent: PKEvent { 21 | 22 | @objc public static let allEventTypes: [InterceptorEvent.Type] = [ 23 | cdnSwitched, 24 | sourceUrlSwitched 25 | ] 26 | 27 | // MARK: - Interceptor Events Static References. 28 | 29 | /// Sent by SmartSwitch interceptor plugin, when the playback URL changed with attached CDN code. 30 | /// Currently used by Youbora contentCdn options. 31 | @objc public static let cdnSwitched: InterceptorEvent.Type = CDNSwitched.self 32 | 33 | public class CDNSwitched: InterceptorEvent { 34 | public convenience init(cdnCode: String) { 35 | self.init([InterceptorEventDataKeys.cdnCode: cdnCode]) 36 | } 37 | } 38 | 39 | // Can be sent by any interceptor plugin, when the playback URL is changed with updated url. 40 | @objc public static let sourceUrlSwitched: InterceptorEvent.Type = SourceUrlSwitched.self 41 | 42 | public class SourceUrlSwitched: InterceptorEvent { 43 | public convenience init(originalUrl: String, updatedUrl: String) { 44 | self.init([InterceptorEventDataKeys.originalUrl: originalUrl, InterceptorEventDataKeys.updatedUrl: updatedUrl]) 45 | } 46 | } 47 | } 48 | 49 | @objc public class InterceptorEventDataKeys: NSObject { 50 | 51 | public static let cdnCode = "CDNCode" 52 | public static let originalUrl = "OriginalUrl" 53 | public static let updatedUrl = "UpdatedUrl" 54 | } 55 | 56 | // MARK: - CDNSwitched 57 | extension PKEvent { 58 | 59 | /// CDN code provided by plugins (SmartSwitch) 60 | @objc public var cdnCode: String? { 61 | return self.data?[InterceptorEventDataKeys.cdnCode] as? String 62 | } 63 | } 64 | 65 | // MARK: - SourceUrlSwitched 66 | extension PKEvent { 67 | 68 | /// SourceUrlSwitched values provided by interceptor plugins 69 | @objc public var originalUrl: String? { 70 | return self.data?[InterceptorEventDataKeys.originalUrl] as? String 71 | } 72 | 73 | @objc public var updatedUrl: String? { 74 | return self.data?[InterceptorEventDataKeys.updatedUrl] as? String 75 | } 76 | } 77 | 78 | -------------------------------------------------------------------------------- /Classes/PKStateMachine.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | 13 | public protocol IntRawRepresentable: RawRepresentable { 14 | var rawValue: Int { get } 15 | } 16 | 17 | public protocol StateProtocol: IntRawRepresentable, Hashable {} 18 | 19 | extension StateProtocol { 20 | var hashValue: Int { 21 | return rawValue 22 | } 23 | } 24 | 25 | public class BasicStateMachine { 26 | /// the current state. 27 | private var state: T 28 | /// the queue to make changes and fetches on. 29 | let dispatchQueue: DispatchQueue 30 | /// the initial state of the state machine. 31 | let initialState: T 32 | /// indicates whether it is allowed to change the state to the initial one. 33 | var allowTransitionToInitialState: Bool 34 | /// a block to perform on every state changing (performed on the main queue). 35 | var onStateChange: ((T) -> Void)? 36 | 37 | public init(initialState: T, allowTransitionToInitialState: Bool = true) { 38 | self.state = initialState 39 | self.initialState = initialState 40 | self.allowTransitionToInitialState = allowTransitionToInitialState 41 | self.dispatchQueue = DispatchQueue(label: "com.kaltura.playkit.dispatch-queue.\(String(describing: type(of: self)))") 42 | } 43 | 44 | /// gets the current state. 45 | public func getState() -> T { 46 | return self.dispatchQueue.sync { 47 | return self.state 48 | } 49 | } 50 | 51 | /// sets the state to a new value. 52 | public func set(state: T) { 53 | self.dispatchQueue.sync { 54 | if state == self.initialState && !self.allowTransitionToInitialState { 55 | PKLog.error("\(String(describing: type(of: self))) was set to initial state, this is not allowed") 56 | return 57 | } 58 | // only set state when changed 59 | if self.state != state { 60 | self.state = state 61 | DispatchQueue.main.async { 62 | self.onStateChange?(state) 63 | } 64 | } 65 | } 66 | } 67 | 68 | /// sets the state machine to the initial value. 69 | public func reset() { 70 | dispatchQueue.sync { 71 | self.state = self.initialState 72 | DispatchQueue.main.async { 73 | self.onStateChange?(self.state) 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Classes/Player/PlayerConfig.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | 13 | /// A `MediaConfig` object defines behavior and info to use when preparing a `Player` object. 14 | @objc public class MediaConfig: NSObject { 15 | 16 | @objc public var mediaEntry: PKMediaEntry 17 | @objc public var startTime: TimeInterval = TimeInterval.nan 18 | 19 | @objc public override var description: String { 20 | return "Media config, mediaEntry: \(self.mediaEntry) startTime: \(self.startTime)" 21 | } 22 | 23 | @objc public init(mediaEntry: PKMediaEntry) { 24 | self.mediaEntry = mediaEntry 25 | } 26 | 27 | @objc public init(mediaEntry: PKMediaEntry, startTime: TimeInterval) { 28 | self.mediaEntry = mediaEntry 29 | self.startTime = startTime 30 | } 31 | 32 | @objc public static func config(mediaEntry: PKMediaEntry) -> MediaConfig { 33 | return MediaConfig.init(mediaEntry: mediaEntry) 34 | } 35 | 36 | @objc public static func config(mediaEntry: PKMediaEntry, startTime: TimeInterval) -> MediaConfig { 37 | return MediaConfig.init(mediaEntry: mediaEntry, startTime: startTime) 38 | } 39 | 40 | /// Private init. 41 | private override init() { 42 | fatalError("Private initializer, use `init(mediaEntry:startTime:)`") 43 | } 44 | } 45 | 46 | extension MediaConfig: NSCopying { 47 | 48 | @objc public func copy(with zone: NSZone? = nil) -> Any { 49 | let copy = MediaConfig(mediaEntry: self.mediaEntry, startTime: self.startTime) 50 | return copy 51 | } 52 | } 53 | 54 | /// A `PluginConfig` object defines config to use when loading a plugin object. 55 | @objc public class PluginConfig: NSObject { 56 | /// Plugins config dictionary holds [plugin name : plugin config] 57 | @objc public var config: [String: Any] 58 | 59 | @objc public override var description: String { 60 | return "Plugin config:\n\(self.config)" 61 | } 62 | 63 | @objc public init(config: [String: Any]) { 64 | self.config = config 65 | } 66 | 67 | /// Private init. 68 | private override init() { 69 | fatalError("Private initializer, use `init(config:)`") 70 | } 71 | } 72 | 73 | extension PluginConfig: NSCopying { 74 | 75 | @objc public func copy(with zone: NSZone? = nil) -> Any { 76 | let copy = PluginConfig(config: self.config) 77 | return copy 78 | } 79 | } 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /Classes/Player/PKVRController.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | import UIKit 13 | 14 | @objc public class PKVRController: NSObject, PKController { 15 | /************************************************************/ 16 | // MARK: - Properties 17 | /************************************************************/ 18 | 19 | var currentPlayer: VRPlayerEngine? 20 | 21 | /// Represents current PKVRController view state 22 | /// 23 | /// ViewState Options 24 | /// - panorama 25 | /// - stereo 26 | /// - error 27 | /// - unknown 28 | @objc public var currentViewState: ViewState { 29 | 30 | guard let player = self.currentPlayer else { 31 | PKLog.warning("player doesn't exist") 32 | return ViewState.error 33 | } 34 | 35 | PKLog.debug("currentViewState: \(player.currentViewState)") 36 | return player.currentViewState 37 | } 38 | 39 | /************************************************************/ 40 | // MARK: - Initialization 41 | /************************************************************/ 42 | 43 | @objc required public init(player: PlayerEngine?) { 44 | self.currentPlayer = player as? VRPlayerEngine 45 | } 46 | 47 | /************************************************************/ 48 | // MARK: - Functions 49 | /************************************************************/ 50 | 51 | /// Enable VR Mode - For stereo display for Google's Cardboard. 52 | /// 53 | /// - Parameter isVREnabled: Toggle to enable vr mode. 54 | @objc public func setVRModeEnabled(_ isVREnabled: Bool) { 55 | PKLog.debug("isVREnabled: \(isVREnabled)") 56 | self.currentPlayer?.setVRModeEnabled(isVREnabled) 57 | } 58 | 59 | /// Requests reset of rotation in the next rendering frame. 60 | @objc public func centerViewPoint() { 61 | self.currentPlayer?.centerViewPoint() 62 | } 63 | 64 | /// Creates eye view indicator. 65 | /// 66 | /// - Parameter frame: eye view frame 67 | /// - Returns: eye view indicator 68 | @objc public func createOrientationIndicatorView(frame: CGRect) -> UIView? { 69 | guard let player = self.currentPlayer else { 70 | PKLog.warning("player doesn't exist") 71 | return nil 72 | } 73 | 74 | PKLog.debug("frame: \(frame)") 75 | return (player.createOrientationIndicatorView(frame: frame)) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Classes/Player/Media/SourceSelector.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | 13 | /// Media Source Type 14 | enum SourceType { 15 | case mp3 16 | case mp4 17 | case m3u8 18 | case wvm 19 | 20 | var asString: String { 21 | switch self { 22 | case .mp3: 23 | return "mp3" 24 | case .mp4: 25 | return "mp4" 26 | case .m3u8: 27 | return "m3u8" 28 | case .wvm: 29 | return "wvm" 30 | } 31 | } 32 | } 33 | 34 | /// Selects the preffered media source 35 | class SourceSelector { 36 | static func selectSource(from mediaEntry: PKMediaEntry) -> (PKMediaSource, AssetHandler)? { 37 | guard let sources = mediaEntry.sources else { 38 | PKLog.error("no media sources in mediaEntry!") 39 | return nil 40 | } 41 | 42 | let defaultHandler = DefaultAssetHandler.self 43 | 44 | // Preference: Local, HLS, FPS*, MP4, WVM*, MP3 45 | 46 | if let source = sources.first(where: {$0 is LocalMediaSource}) { 47 | if source.mediaFormat == .wvm { 48 | return (source, DRMSupport.widevineClassicHandler!.init()) 49 | } else { 50 | return (source, defaultHandler.init()) 51 | } 52 | } 53 | 54 | if DRMSupport.fairplay { 55 | if let source = sources.first(where: {$0.mediaFormat == .hls}) { 56 | return (source, defaultHandler.init()) 57 | } 58 | } else { 59 | if let source = sources.first(where: {$0.mediaFormat == .hls && ($0.drmData == nil || $0.drmData!.isEmpty) }) { 60 | return (source, defaultHandler.init()) 61 | } 62 | } 63 | 64 | if let source = sources.first(where: {$0.mediaFormat == .mp4}) { 65 | return (source, defaultHandler.init()) 66 | } 67 | 68 | if DRMSupport.widevineClassic, let source = sources.first(where: {$0.mediaFormat == .wvm}) { 69 | return (source, DRMSupport.widevineClassicHandler!.init()) 70 | } 71 | 72 | if let source = sources.first(where: {$0.mediaFormat == .mp3}) { 73 | return (source, defaultHandler.init()) 74 | } 75 | 76 | PKLog.error("No playable media sources!") 77 | if !DRMSupport.fairplay { 78 | PKLog.warning("Note: FairPlay is not supported on simulators") 79 | } 80 | return nil 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Classes/Player/PlayerView.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import UIKit 12 | import AVFoundation 13 | 14 | /// A simple `UIView` subclass that is backed by an `AVPlayerLayer` layer. 15 | @objc public class PlayerView: UIView { 16 | 17 | var player: AVPlayer? { 18 | get { 19 | return playerLayer.player 20 | } 21 | set { 22 | playerLayer.player = newValue 23 | } 24 | } 25 | 26 | public override var contentMode: UIView.ContentMode { 27 | didSet { 28 | switch self.contentMode { 29 | case .scaleAspectFill: 30 | playerLayer.videoGravity = .resizeAspectFill 31 | case .scaleAspectFit: 32 | playerLayer.videoGravity = .resizeAspect 33 | case .scaleToFill: 34 | playerLayer.videoGravity = .resize 35 | default: 36 | playerLayer.videoGravity = .resizeAspect 37 | } 38 | } 39 | } 40 | 41 | var playerLayer: AVPlayerLayer { 42 | return self.layer as! AVPlayerLayer 43 | } 44 | 45 | // Override UIView property 46 | override public static var layerClass: AnyClass { 47 | return AVPlayerLayer.self 48 | } 49 | 50 | /// adds the player view as a subview to the container view and sets up constraints 51 | @objc public func add(toContainer container: UIView) { 52 | self.translatesAutoresizingMaskIntoConstraints = false 53 | container.addSubview(self) 54 | 55 | let views = ["playerView": self] 56 | 57 | let horizontalConstraint = NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[playerView]-0-|", options: [], metrics: nil, views: views) 58 | let verticalConstraint = NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[playerView]-0-|", options: [], metrics: nil, views: views) 59 | 60 | container.addConstraints(horizontalConstraint) 61 | container.addConstraints(verticalConstraint) 62 | } 63 | 64 | /// creates a new `PlayerView` instance and connects it to the player 65 | /// - important: make sure to keep strong reference for the player view instance (either from adding as subview or property), 66 | /// otherwise it will be deallocated as the framework holds a weak reference to it 67 | @objc public static func createPlayerView(forPlayer player: Player) -> PlayerView { 68 | let playerView = PlayerView() 69 | player.view = playerView 70 | return playerView 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Classes/Managers/LocalDataStore.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2018 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | 13 | @objc public protocol LocalDataStore { 14 | func save(key: String, value: Data) throws 15 | func load(key: String) throws -> Data 16 | func remove(key: String) throws 17 | func exists(key: String) -> Bool 18 | } 19 | 20 | /// Implementation of LocalDataStore that saves data to files in the Library directory. 21 | @objc public class DefaultLocalDataStore: NSObject, LocalDataStore { 22 | 23 | static let pkLocalDataStore = "pkLocalDataStore" 24 | let storageDirectory: URL 25 | 26 | @objc public static func defaultDataStore() -> DefaultLocalDataStore? { 27 | return try? DefaultLocalDataStore(directory: .libraryDirectory) 28 | } 29 | 30 | private override init() { 31 | fatalError("Private initializer, use a factory or `init(directory:)`") 32 | } 33 | 34 | static func storageDir(_ directory: FileManager.SearchPathDirectory = .libraryDirectory) throws -> URL { 35 | let baseDir = try FileManager.default.url(for: directory, in: .userDomainMask, appropriateFor: nil, create: false) 36 | let storageDirectory = baseDir.appendingPathComponent(DefaultLocalDataStore.pkLocalDataStore, isDirectory: true) 37 | 38 | try FileManager.default.createDirectory(at: storageDirectory, withIntermediateDirectories: true, attributes: nil) 39 | 40 | return storageDirectory 41 | } 42 | 43 | @objc public init(directory: FileManager.SearchPathDirectory) throws { 44 | try self.storageDirectory = type(of: self).storageDir(directory) 45 | } 46 | 47 | private func file(_ key: String) -> URL { 48 | return self.storageDirectory.appendingPathComponent(key) 49 | } 50 | 51 | @objc public func save(key: String, value: Data) throws { 52 | let f = file(key) 53 | PKLog.debug("Saving key to \(f)") 54 | try value.write(to: f, options: .atomic) 55 | } 56 | 57 | @objc public func load(key: String) throws -> Data { 58 | let f = file(key) 59 | PKLog.debug("Loading key from \(f)") 60 | return try Data.init(contentsOf: f, options: []) 61 | } 62 | 63 | @objc public func exists(key: String) -> Bool { 64 | return FileManager.default.fileExists(atPath: file(key).path) 65 | } 66 | 67 | @objc public func remove(key: String) throws { 68 | let f = file(key) 69 | PKLog.debug("Removing key at \(f)") 70 | try FileManager.default.removeItem(at: f) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Classes/Managers/AppState/AppStateProvider.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | import UIKit 13 | 14 | /// The delegate of `AppStateProvider`, allows the delegate to inform on app state notifications. 15 | public protocol AppStateProviderDelegate: AnyObject { 16 | /// fire this delegate function when received observation event. 17 | /// for every observer with the same observation event process the on observe block. 18 | func appStateEventPosted(name: ObservationName) 19 | } 20 | 21 | /// The interface of `AppStateProvider`, allows us to better divide the logic and mock easier. 22 | public protocol AppStateProviderProtocol { 23 | var notificationsManager: NotificationsManager { get } 24 | /// Holds all the observation names we will be observing. 25 | /// If you want to observe more events add them here. 26 | var observationNames: Set { get } 27 | var delegate: AppStateProviderDelegate? { get } 28 | } 29 | 30 | extension AppStateProviderProtocol { 31 | /// Add observers for the provided notification names. 32 | func addObservers() { 33 | observationNames.forEach { name in 34 | notificationsManager.addObserver(notificationName: name) { notification in 35 | self.delegate?.appStateEventPosted(name: notification.name) 36 | } 37 | } 38 | } 39 | 40 | /// Remove observers for the provided notification names. 41 | func removeObservers() { 42 | notificationsManager.removeAllObservers() 43 | } 44 | 45 | } 46 | 47 | /************************************************************/ 48 | // MARK: - AppStateProvider 49 | /************************************************************/ 50 | 51 | /// The `AppStateProvider` is a provider for receiving events from the system about app states. 52 | /// Used to seperate the events providing from the app state subject and enabling us to mock better. 53 | public final class AppStateProvider: AppStateProviderProtocol { 54 | 55 | public init(delegate: AppStateProviderDelegate? = nil) { 56 | self.delegate = delegate 57 | } 58 | 59 | public weak var delegate: AppStateProviderDelegate? 60 | 61 | public let notificationsManager = NotificationsManager() 62 | 63 | public let observationNames: Set = [ 64 | UIApplication.willTerminateNotification, 65 | UIApplication.didEnterBackgroundNotification, 66 | UIApplication.didBecomeActiveNotification, 67 | UIApplication.willResignActiveNotification, 68 | UIApplication.willEnterForegroundNotification 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /Plugins/AnalyticsCommon/BaseAnalyticsPlugin.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | import PlayKit 13 | 14 | /************************************************************/ 15 | // MARK: - BaseAnalyticsPlugin 16 | /************************************************************/ 17 | 18 | /// class `BaseAnalyticsPlugin` is a base plugin object used for analytics plugin subclasses 19 | @objc public class BaseAnalyticsPlugin: BasePlugin, AnalyticsPluginProtocol { 20 | 21 | var config: AnalyticsConfig? 22 | /// indicates whether we played for the first time or not. 23 | @objc public var isFirstPlay: Bool = true 24 | 25 | /************************************************************/ 26 | // MARK: - PKPlugin 27 | /************************************************************/ 28 | 29 | @objc public required init(player: Player, pluginConfig: Any?, messageBus: MessageBus) throws { 30 | try super.init(player: player, pluginConfig: pluginConfig, messageBus: messageBus) 31 | if let aConfig = pluginConfig as? AnalyticsConfig { 32 | self.config = aConfig 33 | } else { 34 | PKLog.warning("There is no Analytics Config! for \(type(of: self))") 35 | } 36 | self.registerEvents() 37 | } 38 | 39 | public override func onUpdateMedia(mediaConfig: MediaConfig) { 40 | super.onUpdateMedia(mediaConfig: mediaConfig) 41 | self.isFirstPlay = true 42 | } 43 | 44 | public override func onUpdateConfig(pluginConfig: Any) { 45 | super.onUpdateConfig(pluginConfig: pluginConfig) 46 | 47 | guard let config = pluginConfig as? AnalyticsConfig else { 48 | PKLog.error("plugin configis wrong") 49 | return 50 | } 51 | 52 | PKLog.verbose("new config::\(String(describing: config))") 53 | self.config = config 54 | } 55 | 56 | public override func destroy() { 57 | self.messageBus?.removeObserver(self, events: playerEventsToRegister) 58 | super.destroy() 59 | } 60 | 61 | /************************************************************/ 62 | // MARK: - AnalyticsPluginProtocol 63 | /************************************************************/ 64 | 65 | /// default events to register 66 | @objc public var playerEventsToRegister: [PlayerEvent.Type] { 67 | fatalError("abstract property should be overriden in subclass") 68 | } 69 | 70 | @objc public func registerEvents() { 71 | fatalError("abstract func should be overriden in subclass") 72 | } 73 | 74 | @objc public func unregisterEvents() { 75 | fatalError("abstract func should be overriden in subclass") 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Classes/Utils/NetworkUtils.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2021 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | import KalturaNetKit 13 | 14 | @objc public class NetworkUtils: NSObject { 15 | 16 | let defaultKavaBaseUrl: String = "https://analytics.kaltura.com/api_v3/index.php" 17 | let defaultKavaPartnerId: Int = 2504201 18 | let defaultKavaEntryId: String = "1_3bwzbc9o" 19 | 20 | static public let kavaEventImpression = "1" 21 | static public let kavaEventPlayRequest = "2" 22 | 23 | public func sendKavaAnalytics(forPartnerId partnerId: Int?, entryId: String?, eventType: String, sessionId: String) { 24 | guard let request: KalturaRequestBuilder = KalturaRequestBuilder(url: defaultKavaBaseUrl, service: nil, action: nil) else { return } 25 | 26 | var defPartnerId: Int = defaultKavaPartnerId 27 | var defEntryId: String = defaultKavaEntryId 28 | 29 | if let partnerId = partnerId, partnerId > 0, 30 | let entryId = entryId, !entryId.isEmpty { 31 | defPartnerId = partnerId 32 | defEntryId = entryId 33 | } 34 | 35 | request.set(method: .get) 36 | request.add(headerKey: "User-Agent", headerValue: PlayKitManager.userAgent) 37 | 38 | request.setParam(key: "service", value: "analytics") 39 | request.setParam(key: "action", value: "trackEvent") 40 | request.setParam(key: "eventType", value: eventType) 41 | request.setParam(key: "eventIndex", value: "1") 42 | request.setParam(key: "partnerId", value: String(defPartnerId)) 43 | request.setParam(key: "entryId", value: defEntryId) 44 | request.setParam(key: "sessionId", value: sessionId) 45 | request.setParam(key: "referrer", value: self.base64(from: Bundle.main.bundleIdentifier ?? "")) 46 | request.setParam(key: "deliveryType", value: "url") 47 | request.setParam(key: "playbackType", value: "vod") 48 | request.setParam(key: "clientVer", value: "\(PlayKitManager.clientTag)") 49 | request.setParam(key: "position", value: "0") 50 | if let bundleId = Bundle.main.bundleIdentifier { 51 | request.setParam(key: "application", value: "\(bundleId)") 52 | } 53 | 54 | request.set { (response: Response) in 55 | PKLog.debug("Response:\nStatus Code: \(response.statusCode)\nError: \(response.error?.localizedDescription ?? "")\nData: \(response.data ?? "")") 56 | } 57 | PKLog.debug("Sending Kava Event type: " + eventType) 58 | KNKRequestExecutor.shared.send(request: request.build()) 59 | } 60 | 61 | func base64(from: String) -> String { 62 | return from.data(using: .utf8)?.base64EncodedString() ?? "" 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /Classes/FairPlayDRM/FPSContentKeyManager.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2018 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import AVFoundation 12 | 13 | #if os(iOS) 14 | @available(iOS 10.3, *) 15 | class FPSContentKeyManager { 16 | 17 | static let shared: FPSContentKeyManager = FPSContentKeyManager() 18 | 19 | /// The instance of `AVContentKeySession` that is used for managing and preloading content keys. 20 | let contentKeySession: AVContentKeySession 21 | 22 | let contentKeyDelegate: FPSContentKeySessionDelegate 23 | 24 | /// The DispatchQueue to use for delegate callbacks. 25 | let contentKeyDelegateQueue = DispatchQueue(label: "com.kaltura.playkit.contentKeyDelegateQueue") 26 | 27 | // MARK: Initialization. 28 | 29 | private init() { 30 | if #available(iOS 11.0, *) { 31 | contentKeySession = AVContentKeySession(keySystem: .fairPlayStreaming) 32 | } else { 33 | // iOS 10.3 34 | let storageUrl = try! FileManager.default.url(for: .libraryDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appendingPathComponent("pkLegacyExpiredContentKeySessionReports", isDirectory: true) 35 | try? FileManager.default.createDirectory(at: storageUrl, withIntermediateDirectories: true, attributes: nil) 36 | contentKeySession = AVContentKeySession(keySystem: .fairPlayStreaming, storageDirectoryAt: storageUrl) 37 | } 38 | contentKeyDelegate = FPSContentKeySessionDelegate() 39 | 40 | contentKeySession.setDelegate(contentKeyDelegate, queue: contentKeyDelegateQueue) 41 | } 42 | 43 | func installOfflineLicense(for location: URL, mediaSource: PKMediaSource, dataStore: LocalDataStore, callback: @escaping (Error?) -> Void) throws { 44 | 45 | let drmParams = try mediaSource.fairPlayParams() 46 | 47 | guard let id = FPSUtils.extractAssetId(at: location) else { 48 | PKLog.error("Asset at \(location.absoluteString) is missing the asset id") 49 | throw FPSError.missingAssetId(location) 50 | } 51 | 52 | let skdUrl = "skd://" + id 53 | if let assetHelper = contentKeyDelegate.assetHelpersMap[skdUrl], 54 | let avContentKeyRequest = assetHelper.avContentKeyRequest as? AVContentKeyRequest { 55 | assetHelper.doneCallback = callback 56 | contentKeySession.renewExpiringResponseData(for: avContentKeyRequest) 57 | } else { 58 | let helper = FPSLicenseHelper(assetId: id, params: drmParams, dataStore: dataStore, forceDownload: true) 59 | helper?.doneCallback = callback 60 | contentKeyDelegate.assetHelpersMap[skdUrl] = helper 61 | 62 | contentKeySession.processContentKeyRequest(withIdentifier: skdUrl, initializationData: nil, options: nil) 63 | } 64 | } 65 | } 66 | #endif 67 | -------------------------------------------------------------------------------- /Classes/Providers/Mock/MockMediaProvider.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import UIKit 12 | import kSwiftyJSON 13 | 14 | @objc public class MockMediaEntryProvider: NSObject, MediaEntryProvider { 15 | 16 | public enum MockError: Error { 17 | case invalidParam(paramName:String) 18 | case fileIsEmptyOrNotFound 19 | case unableToParseJSON 20 | case mediaNotFound 21 | } 22 | 23 | @objc public var id: String? 24 | @objc public var url: URL? 25 | @objc public var content: Any? 26 | 27 | @discardableResult 28 | @nonobjc public func set(id: String?) -> Self { 29 | self.id = id 30 | return self 31 | } 32 | 33 | @discardableResult 34 | @nonobjc public func set(url: URL?) -> Self { 35 | self.url = url 36 | return self 37 | } 38 | 39 | @discardableResult 40 | @nonobjc public func set(content: Any?) -> Self { 41 | self.content = content 42 | return self 43 | } 44 | 45 | public override init() { 46 | 47 | } 48 | 49 | struct LoaderInfo { 50 | var id: String 51 | var content: JSON 52 | } 53 | 54 | @objc public func loadMedia(callback: @escaping (PKMediaEntry?, Error?) -> Void) { 55 | 56 | guard let id = self.id else { 57 | callback(nil, MockError.invalidParam(paramName: "id")) 58 | return 59 | } 60 | 61 | var json: JSON? = nil 62 | if let content = self.content { 63 | json = JSON(content) 64 | } else if self.url != nil { 65 | guard let stringPath = self.url?.absoluteString else { 66 | callback(nil, MockError.invalidParam(paramName: "url")) 67 | return 68 | } 69 | guard let data = NSData(contentsOfFile: stringPath) else { 70 | callback(nil, MockError.fileIsEmptyOrNotFound) 71 | return 72 | } 73 | json = try? JSON(data: data as Data) 74 | } 75 | 76 | guard let jsonContent = json else { 77 | callback(nil, MockError.unableToParseJSON) 78 | return 79 | } 80 | 81 | let loderInfo = LoaderInfo(id: id, content: jsonContent) 82 | 83 | guard loderInfo.content != .null else { 84 | callback(nil, MockError.unableToParseJSON) 85 | return 86 | } 87 | 88 | let jsonObject: JSON = loderInfo.content[loderInfo.id] 89 | guard jsonObject != .null else { 90 | callback(nil, MockError.mediaNotFound) 91 | return 92 | } 93 | 94 | let mediaEntry = PKMediaEntry(json: jsonObject.object) 95 | callback(mediaEntry, nil) 96 | } 97 | 98 | @objc public func cancel() { 99 | 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Classes/Player/Protocols/BasicPlayer.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2019 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | 13 | @objc public protocol BasicPlayer { 14 | /// The player's duration. 15 | @objc var duration: TimeInterval { get } 16 | 17 | /// The player's currentState. 18 | @objc var currentState: PlayerState { get } 19 | 20 | /// Indicates if the player is playing. 21 | @objc var isPlaying: Bool { get } 22 | 23 | /// The player's view component. 24 | @objc weak var view: PlayerView? { get set } 25 | 26 | /// The current player's time. 27 | @objc var currentTime: TimeInterval { get set } 28 | 29 | /// The current program time (PROGRAM-DATE-TIME). 30 | @objc var currentProgramTime: Date? { get } 31 | 32 | /// Get the player's current audio track. 33 | @objc var currentAudioTrack: String? { get } 34 | 35 | /// Get the player's current text track. 36 | @objc var currentTextTrack: String? { get } 37 | 38 | /// Indicates the desired rate of playback, 0.0 means "paused", 1.0 indicates a desire to play at the natural rate of the current item. 39 | /// Note: Do not use the rate to indicate whether to play or pause! Use the isPlaying property. 40 | @objc var rate: Float { get set } 41 | 42 | /// The audio playback volume for the player, ranging from 0.0 through 1.0 on a linear scale. 43 | @objc var volume: Float { get set } 44 | 45 | /// Provides a collection of time ranges for which the player has the media data readily available. The ranges provided might be discontinuous. 46 | @objc var loadedTimeRanges: [PKTimeRange]? { get } 47 | 48 | /// The current player's buffered time. 49 | @objc var bufferedTime: TimeInterval { get } 50 | 51 | /// Send a play action for the player. 52 | @objc func play() 53 | 54 | /// Send a pause action for the player. 55 | @objc func pause() 56 | 57 | /// Send a resume action for the player. 58 | @objc func resume() 59 | 60 | /// Send a stop action for the player. 61 | @objc func stop() 62 | 63 | /// Send a replay action for the player. 64 | @objc func replay() 65 | 66 | /// Send a seek action for the player. 67 | @objc func seek(to time: TimeInterval) 68 | 69 | /// Seek to the live edje. 70 | @objc func seekToLiveEdge() 71 | 72 | /// Select a Track 73 | @objc func selectTrack(trackId: String) 74 | 75 | /// Release the player's resources. 76 | @objc func destroy() 77 | 78 | /// Prepare for playing an entry. 79 | /// If player network setting autoBuffer is set to true, prepare starts buffering the entry. 80 | /// Otherwise, if autoBuffer is set to false, need to call startBuffering manually. 81 | @objc func prepare(_ config: MediaConfig) 82 | 83 | /// Starts buffering the entry. 84 | @objc func startBuffering() 85 | } 86 | -------------------------------------------------------------------------------- /Classes/Events/EventsPayloads/PKPlaybackInfo.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | import AVFoundation 13 | 14 | /// `PKPlaybackInfo` represents a playback info object. 15 | @objc public class PKPlaybackInfo: NSObject { 16 | 17 | /// The actual bitrate of the playback. 18 | @objc public let bitrate: Double 19 | /// The selected track indicated bitrate. 20 | @objc public let indicatedBitrate: Double 21 | /// The throughput of the playback (download speed) 22 | @objc public let observedBitrate: Double 23 | /// The average bitrate of video track if it is unmuxed 24 | @objc public let averageVideoBitrate: Double 25 | /// The average bitrate of audio track. This is not available if audio is muxed with video. 26 | @objc public let averageAudioBitrate: Double 27 | /// The URI of the playback item 28 | @objc public let uri: String? 29 | 30 | init(bitrate: Double, indicatedBitrate: Double, observedBitrate: Double, averageVideoBitrate: Double, averageAudioBitrate: Double, uri: String?) { 31 | self.bitrate = bitrate 32 | self.indicatedBitrate = indicatedBitrate 33 | self.observedBitrate = observedBitrate 34 | self.averageVideoBitrate = averageVideoBitrate 35 | self.averageAudioBitrate = averageAudioBitrate 36 | self.uri = uri 37 | } 38 | 39 | convenience init(logEvent: AVPlayerItemAccessLogEvent) { 40 | let bitrate: Double 41 | if logEvent.segmentsDownloadedDuration > 0 { 42 | // bitrate is equal to: 43 | // (amount of bytes transfered) * 8 (bits in byte) / (amount of time took to download the transfered bytes) 44 | bitrate = Double(logEvent.numberOfBytesTransferred * 8) / logEvent.segmentsDownloadedDuration 45 | } else { 46 | bitrate = logEvent.indicatedBitrate 47 | } 48 | let indicatedBitrate = logEvent.indicatedBitrate 49 | let observedBitrate = logEvent.observedBitrate 50 | let averageAudioBitrate: Double 51 | let averageVideoBitrate: Double 52 | if #available(iOS 10.0, tvOS 10.0, *) { 53 | if (logEvent.averageVideoBitrate > 0) { 54 | averageVideoBitrate = logEvent.averageVideoBitrate 55 | } else { 56 | averageVideoBitrate = bitrate 57 | } 58 | if (logEvent.averageAudioBitrate > 0) { 59 | averageAudioBitrate = logEvent.averageAudioBitrate 60 | } else { 61 | averageAudioBitrate = -1 62 | } 63 | } else { 64 | averageVideoBitrate = bitrate 65 | averageAudioBitrate = -1 66 | } 67 | 68 | self.init(bitrate: bitrate, 69 | indicatedBitrate: indicatedBitrate, 70 | observedBitrate: observedBitrate, 71 | averageVideoBitrate: averageVideoBitrate, 72 | averageAudioBitrate: averageAudioBitrate, 73 | uri: logEvent.uri ?? "") 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Classes/Events/PlaylistEvent.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2021 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | // 11 | // PlaylistEvent.swift 12 | // PlayKit 13 | // 14 | // Created by Sergii Chausov on 27.10.2021. 15 | // 16 | 17 | import Foundation 18 | 19 | @objc public class PlaylistEvent: PKEvent { 20 | 21 | @objc public static let allEventTypes: [PlaylistEvent.Type] = [ 22 | playlistLoaded, playlistStarted, playlistEnded, playlistCountdownStart, playlistCountdownEnd, playlistLoopStateChanged, playlistLoopStateChanged, playlistAutoContinueStateChanged, playlistError, playlistLoadMediaError, playlistCurrentPlayingItemChanged 23 | ] 24 | 25 | @objc public static let playlistLoaded: PlaylistEvent.Type = PlaylistLoaded.self 26 | @objc public static let playlistStarted: PlaylistEvent.Type = PlaylistStarted.self 27 | @objc public static let playlistEnded: PlaylistEvent.Type = PlaylistEnded.self 28 | @objc public static let playlistCountdownStart: PlaylistEvent.Type = PlaylistCountdownStart.self 29 | @objc public static let playlistCountdownEnd: PlaylistEvent.Type = PlaylistCountdownEnd.self 30 | @objc public static let playlistLoopStateChanged: PlaylistEvent.Type = PlaylistLoopStateChanged.self 31 | @objc public static let playlistAutoContinueStateChanged: PlaylistEvent.Type = PlaylistAutoContinueStateChanged.self 32 | @objc public static let playlistError: PlaylistEvent.Type = PlaylistError.self 33 | @objc public static let playlistLoadMediaError: PlaylistEvent.Type = PlaylistLoadMediaError.self 34 | @objc public static let playlistCurrentPlayingItemChanged: PlaylistEvent.Type = PlaylistCurrentPlayingItemChanged.self 35 | 36 | public class PlaylistLoaded: PlaylistEvent {} 37 | 38 | public class PlaylistStarted: PlaylistEvent {} 39 | 40 | public class PlaylistEnded: PlaylistEvent {} 41 | 42 | public class PlaylistCountdownStart: PlaylistEvent { 43 | public convenience init(countDownDuration: TimeInterval) { 44 | self.init([EventDataKeys.duration: NSNumber(value: countDownDuration)]) 45 | } 46 | } 47 | 48 | public class PlaylistCountdownEnd: PlaylistEvent {} 49 | 50 | public class PlaylistLoopStateChanged: PlaylistEvent {} 51 | 52 | public class PlaylistAutoContinueStateChanged: PlaylistEvent {} 53 | 54 | public class PlaylistError: PlaylistEvent { 55 | public convenience init(nsError: NSError) { 56 | self.init([EventDataKeys.error: nsError]) 57 | } 58 | 59 | public convenience init(error: PKError) { 60 | self.init([EventDataKeys.error: error.asNSError]) 61 | } 62 | } 63 | 64 | public class PlaylistLoadMediaError: PlaylistEvent { 65 | public convenience init(entryId: String, nsError: NSError) { 66 | self.init([EventDataKeys.entryId: entryId, EventDataKeys.error: nsError]) 67 | } 68 | 69 | public convenience init(error: PKError) { 70 | self.init([EventDataKeys.error: error.asNSError]) 71 | } 72 | } 73 | 74 | public class PlaylistCurrentPlayingItemChanged: PlaylistEvent {} 75 | 76 | } 77 | -------------------------------------------------------------------------------- /Example/Tests/MediaEntryProvider/MediaEntryProviderMock.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, 5 | // unless a different license for a particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import UIKit 12 | import PlayKit 13 | import KalturaNetKit 14 | 15 | 16 | 17 | class MediaEntryProviderMockExecutor: RequestExecutor { 18 | 19 | var entryID: String 20 | var domain: String 21 | 22 | init(entryID:String,domain:String) { 23 | self.entryID = entryID 24 | self.domain = domain 25 | } 26 | 27 | let serviceKey = "service" 28 | let actionKey = "action" 29 | 30 | 31 | func send(request:Request){ 32 | 33 | var fileName = self.domain 34 | 35 | let urlComponent = request.url.absoluteString.components(separatedBy: "/") 36 | 37 | var serviceName: String? = nil 38 | var actionName: String? = nil 39 | 40 | let serviceKeyIndex = urlComponent.index(of: serviceKey) 41 | 42 | if let serviceKeyIndex = serviceKeyIndex{ 43 | serviceName = urlComponent[serviceKeyIndex + 1] 44 | } 45 | 46 | 47 | let actionKeyIndex = urlComponent.index(of: actionKey) 48 | if let actionKeyIndex = actionKeyIndex{ 49 | actionName = urlComponent[actionKeyIndex+1] 50 | } 51 | 52 | 53 | if let service = serviceName{ 54 | fileName.append(".\(service)") 55 | } 56 | else{ 57 | fileName.append("._") 58 | } 59 | 60 | 61 | if let action = actionName{ 62 | fileName.append(".\(action)") 63 | } 64 | else{ 65 | fileName.append("._") 66 | } 67 | 68 | fileName.append(".\(entryID)") 69 | 70 | 71 | let bundle = Bundle.main 72 | let path = bundle.path(forResource: fileName, ofType: "json") 73 | guard let filePath = path else { 74 | 75 | if let completion = request.completion{ 76 | completion(Response(data: nil, error: nil)) 77 | } 78 | return 79 | } 80 | 81 | let content = NSData(contentsOfFile:filePath) as Data? 82 | 83 | guard let contentFile = content else { 84 | if let completion = request.completion{ 85 | completion(Response(data: nil, error: nil)) 86 | } 87 | return 88 | } 89 | 90 | do{ 91 | let result = try JSONSerialization.jsonObject(with: contentFile, options: JSONSerialization.ReadingOptions()) 92 | if let completion = request.completion{ 93 | completion(Response(data: result, error: nil)) 94 | } 95 | 96 | }catch{ 97 | if let completion = request.completion{ 98 | completion(Response(data: nil, error: nil)) 99 | } 100 | 101 | } 102 | 103 | } 104 | 105 | func cancel(request:Request){ 106 | 107 | } 108 | 109 | func clean(){ 110 | 111 | } 112 | } 113 | 114 | 115 | -------------------------------------------------------------------------------- /TestApp/TestApp.xcodeproj/xcshareddata/xcschemes/TestApp_v5.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Classes/Managers/PlayKitManager.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import UIKit 12 | import PlayKitUtils 13 | 14 | /** 15 | Manager class used for: 16 | - creating `Player` objects. 17 | - creating and registering plugins. 18 | */ 19 | @objc public class PlayKitManager: NSObject { 20 | 21 | @objc public static let versionString: String = Bundle(for: PlayKitManager.self).object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "dev" 22 | @objc public static let clientTag = "playkit/ios-\(versionString)" 23 | @objc public static let userAgent = UserAgent.build(clientTag: clientTag) 24 | 25 | @objc(sharedInstance) public static let shared: PlayKitManager = PlayKitManager() 26 | 27 | var pluginRegistry = Dictionary() 28 | 29 | // Private init to prevent initializing this singleton 30 | private override init() { 31 | if type(of: self) != PlayKitManager.self { 32 | fatalError("Private initializer, use shared instance instead") 33 | } 34 | } 35 | 36 | func createPlugin(name: String, player: Player, pluginConfig: Any?, messageBus: MessageBus) throws -> PKPlugin { 37 | guard let pluginClass = pluginRegistry[name] else { 38 | PKLog.error("plugin with name: \(name) doesn't exist in pluginRegistry") 39 | throw PKPluginError.failedToCreatePlugin(pluginName: name).asNSError 40 | } 41 | return try pluginClass.init(player: player, pluginConfig: pluginConfig, messageBus: messageBus) 42 | } 43 | 44 | // MARK: - Public 45 | 46 | /// Loads and returns a player object using a provided configuration. 47 | /// 48 | /// - Important: In order to start buffering the video after loading the player 49 | /// you must call prepare on the player with the same configuration. 50 | /// ```` 51 | /// player = PlayKitManager.sharedInstance.loadPlayer(config: config) 52 | /// player.prepare(config) 53 | /// ```` 54 | /// 55 | /// - Parameter pluginConfig: The configuration object to load the player with. 56 | /// - Returns: A player loaded using the provided configuration. 57 | @objc public func loadPlayer(pluginConfig: PluginConfig?) -> Player { 58 | let loader = PlayerLoader() 59 | loader.load(pluginConfig: pluginConfig) 60 | return loader 61 | } 62 | 63 | @objc public func registerPlugin(_ pluginClass: BasePlugin.Type) { 64 | PKLog.info("Registering plugin \(pluginClass.pluginName)/\(pluginClass.pluginVersion)") 65 | 66 | if let pluginWarmUp = pluginClass as? PKPluginWarmUp.Type { 67 | pluginWarmUp.warmUp() 68 | } 69 | pluginRegistry[pluginClass.pluginName] = pluginClass 70 | } 71 | 72 | @objc public func registeredPlugins() -> [String: String] { 73 | var dict = [String: String]() 74 | for (name, type) in pluginRegistry { 75 | dict[name] = type.pluginVersion 76 | } 77 | return dict 78 | } 79 | 80 | /// Sets the logging level for our logger. 81 | @objc public static var logLevel: PKLogLevel = .default { 82 | didSet { 83 | PKLog.outputLevel = logLevel.toLoggerLevel 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /TestApp/TestApp.xcodeproj/xcshareddata/xcschemes/TestApp_v4_2.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Example/PlayKit/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Classes/Network/KalturaPlaybackRequestAdapter.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | 13 | @objc public class KalturaPlaybackRequestAdapter: NSObject, PKRequestParamsAdapter { 14 | 15 | private var applicationName: String? 16 | private var sessionId: String? 17 | 18 | /// Installs a new kaltura request adapter on the provided player with custom application name. 19 | /// 20 | /// - Parameters: 21 | /// - player: The player you want to use with the request adapter 22 | /// - appName: the application name, if `nil` will use the bundle identifier. 23 | @objc public static func install(in player: Player, withAppName appName: String?) { 24 | let requestAdapter = KalturaPlaybackRequestAdapter() 25 | requestAdapter.sessionId = player.sessionId 26 | requestAdapter.applicationName = appName 27 | player.settings.contentRequestAdapter = requestAdapter 28 | } 29 | 30 | /// Updates the request adapter with info from the player 31 | @objc public func updateRequestAdapter(with player: Player) { 32 | self.sessionId = player.sessionId 33 | } 34 | 35 | /// Adapts the request params 36 | @objc public func adapt(requestParams: PKRequestParams) -> PKRequestParams { 37 | guard let sessionId = self.sessionId else { return requestParams } 38 | guard requestParams.url.path.contains("/playManifest/") else { return requestParams } 39 | guard var urlComponents = URLComponents(url: requestParams.url, resolvingAgainstBaseURL: false) else { return requestParams } 40 | // add query items to the request 41 | let queryItems = [ 42 | URLQueryItem(name: "playSessionId", value: sessionId), 43 | URLQueryItem(name: "clientTag", value: PlayKitManager.clientTag), 44 | URLQueryItem(name: "referrer", value: self.applicationName == nil ? self.base64(from: Bundle.main.bundleIdentifier ?? "") : self.base64(from: self.applicationName!)) 45 | ] 46 | if var urlQueryItems = urlComponents.queryItems { 47 | urlQueryItems += queryItems 48 | urlComponents.queryItems = urlQueryItems 49 | } else { 50 | urlComponents.queryItems = queryItems 51 | } 52 | // create the url 53 | guard let url = urlComponents.url else { 54 | PKLog.debug("failed to create url after appending query items") 55 | return requestParams 56 | } 57 | return PKRequestParams(url: url, headers: requestParams.headers) 58 | } 59 | 60 | } 61 | 62 | @objc public class CustomPlaybackRequestAdapter: KalturaPlaybackRequestAdapter { 63 | 64 | @objc public var httpHeaders: [String: String]? 65 | 66 | public override func adapt(requestParams: PKRequestParams) -> PKRequestParams { 67 | let parameters = super.adapt(requestParams: requestParams) 68 | 69 | if let customHeaders = self.httpHeaders { 70 | var newHeaders: [String: String] = [:] 71 | if let headers = parameters.headers { 72 | newHeaders = headers 73 | } 74 | 75 | for (key, value) in customHeaders { 76 | newHeaders[key] = value 77 | } 78 | return PKRequestParams(url: parameters.url, headers: newHeaders) 79 | } 80 | 81 | return parameters 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /Example/Tests/MediaEntryProvider/PhoenixMediaProviderTest.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, 5 | // unless a different license for a particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import XCTest 12 | import PlayKit 13 | import KalturaNetKit 14 | import Quick 15 | import Nimble 16 | 17 | class PhoenixMediaProviderTest: QuickSpec, SessionProvider { 18 | 19 | /************************************************************/ 20 | // MARK: - Mocks 21 | /************************************************************/ 22 | 23 | fileprivate class RequestExecutorMock: RequestExecutor { 24 | 25 | let onRequestSend: (Request) -> Void 26 | 27 | init(onRequestSend: @escaping (Request) -> Void) { 28 | self.onRequestSend = onRequestSend 29 | } 30 | 31 | func send(request: Request) { 32 | onRequestSend(request) 33 | } 34 | 35 | func cancel(request: Request) {} 36 | func clean() {} 37 | } 38 | 39 | /************************************************************/ 40 | // MARK: - SessionProvider 41 | /************************************************************/ 42 | 43 | let mediaID = "485293" 44 | var partnerId: Int64 = 198 45 | var serverURL: String = "http://api-preprod.ott.kaltura.com/v4_2/api_v3" 46 | 47 | public func loadKS(completion: @escaping (String?, Error?) -> Void) { 48 | completion(nil, nil) 49 | } 50 | 51 | /************************************************************/ 52 | // MARK: - Tests 53 | /************************************************************/ 54 | 55 | override func spec() { 56 | describe("PhoenixMediaProviderTest") { 57 | it("should make a request") { 58 | let expectedUrl = URL(string: "http://api-preprod.ott.kaltura.com/v4_2/api_v3/service/multirequest")! 59 | let expectedBodyData = "{\"1\":{\"service\":\"ottUser\",\"action\":\"anonymousLogin\",\"partnerId\":198},\"2\":{\"service\":\"asset\",\"assetType\":\"media\",\"action\":\"getPlaybackContext\",\"assetId\":\"485293\",\"ks\":\"{1:result:ks}\",\"contextDataParams\":{\"context\":\"PLAYBACK\",\"mediaProtocols\":[\"https\"]}},\"clientTag\":\"java:16-09-10\",\"apiVersion\":\"3.6.1078.11798\"}".data(using: .utf8) 60 | let expectedRequestMethod: RequestMethod = .post 61 | let expectedHeaders = ["Content-Type": "application/json", "Accept": "application/json"] 62 | 63 | waitUntil(timeout: 10) { (done) in 64 | let mockExecutor = RequestExecutorMock { (request) in 65 | expect(expectedUrl).to(equal(request.url)) 66 | expect(expectedRequestMethod).to(equal(request.method)) 67 | expect(expectedBodyData).to(equal(request.dataBody)) 68 | expect(expectedHeaders).to(equal(request.headers)) 69 | done() 70 | } 71 | 72 | let provider = PhoenixMediaProvider() 73 | .set(sessionProvider: self) 74 | .set(assetId: self.mediaID) 75 | .set(type: .media) 76 | .set(playbackContextType: .playback) 77 | .set(executor: mockExecutor) 78 | 79 | provider.loadMedia { (media, error) in } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | 86 | 87 | -------------------------------------------------------------------------------- /Classes/Network/KalturaUDRMLicenseRequestAdapter.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2018 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | 13 | 14 | @objc public class KalturaUDRMLicenseRequestAdapter: NSObject, PKRequestParamsAdapter { 15 | 16 | private var applicationName: String? 17 | @objc public var sessionId: String? 18 | 19 | /// Installs a new kaltura request adapter on the provided player with custom application name. 20 | /// 21 | /// - Parameters: 22 | /// - player: The player you want to use with the request adapter 23 | /// - appName: the application name, if `nil` will use the bundle identifier. 24 | @objc public static func install(in player: Player, withAppName appName: String?) { 25 | let requestAdapter = KalturaUDRMLicenseRequestAdapter() 26 | requestAdapter.sessionId = player.sessionId 27 | requestAdapter.applicationName = appName 28 | player.settings.licenseRequestAdapter = requestAdapter 29 | } 30 | 31 | /// Updates the request adapter with info from the player 32 | @objc public func updateRequestAdapter(with player: Player) { 33 | self.sessionId = player.sessionId 34 | } 35 | 36 | /// Adapts the request params 37 | @objc public func adapt(requestParams: PKRequestParams) -> PKRequestParams { 38 | guard let sessionId = self.sessionId else { return requestParams } 39 | guard let urlComponents = URLComponents(url: requestParams.url, resolvingAgainstBaseURL: false) else { return requestParams } 40 | // Prepare clientTag 41 | let clientTag = PlayKitManager.clientTag 42 | // Prepare referrer 43 | var referrer = Bundle.main.bundleIdentifier ?? "" 44 | if let appName = self.applicationName { 45 | referrer = appName 46 | } 47 | referrer = self.base64(from: referrer) 48 | 49 | if #available(iOS 11.0, tvOS 11.0, *) { 50 | let url = requestParams.url 51 | .appendingPercentEncodedQueryComponent(key: "playSessionId", value: sessionId) 52 | .appendingPercentEncodedQueryComponent(key: "clientTag", value: clientTag) 53 | .appendingPercentEncodedQueryComponent(key: "referrer", value: referrer) 54 | 55 | return PKRequestParams(url: url, headers: requestParams.headers) 56 | } else { 57 | var licenceUrlString = requestParams.url.absoluteString 58 | 59 | var queryParameters: [String: String] = [ 60 | "playSessionId": percentEncoded(sessionId), 61 | "clientTag": percentEncoded(clientTag), 62 | "referrer": percentEncoded(referrer) 63 | ] 64 | 65 | if urlComponents.queryItems == nil, 66 | let firstKey = queryParameters.keys.first { 67 | licenceUrlString = licenceUrlString.appending("?\(firstKey)=\(queryParameters.removeValue(forKey: firstKey) ?? "")") 68 | } 69 | 70 | for item in queryParameters { 71 | licenceUrlString = licenceUrlString.appending("&\(item.key)=\(item.value)") 72 | } 73 | 74 | // create the url 75 | guard let url = URL(string: licenceUrlString) else { 76 | PKLog.debug("failed to create url after appending query items") 77 | return requestParams 78 | } 79 | return PKRequestParams(url: url, headers: requestParams.headers) 80 | } 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /Classes/MessageBus.swift: -------------------------------------------------------------------------------- 1 | // =================================================================================================== 2 | // Copyright (C) 2017 Kaltura Inc. 3 | // 4 | // Licensed under the AGPLv3 license, unless a different license for a 5 | // particular library is specified in the applicable library path. 6 | // 7 | // You may obtain a copy of the License at 8 | // https://www.gnu.org/licenses/agpl-3.0.html 9 | // =================================================================================================== 10 | 11 | import Foundation 12 | 13 | private struct Observation { 14 | /// the observer of the event 15 | weak var observer: AnyObject? 16 | /// the dispatchQueue to observe the events on. 17 | let observeOn: DispatchQueue 18 | /// the block of code to be performed when event fires. 19 | let block: (PKEvent) -> Void 20 | } 21 | 22 | /// `MessageBus` object handles all event message observing and posting 23 | @objc public class MessageBus: NSObject { 24 | 25 | private var observations = [String: [Observation]]() 26 | private let dispatchQueue = DispatchQueue(label: "com.kaltura.playkit.message-bus") 27 | 28 | @objc public func addObserver(_ observer: AnyObject, events: [PKEvent.Type], block: @escaping (PKEvent) -> Void) { 29 | self.add(observer: observer, events: events, block: block) 30 | } 31 | 32 | @objc public func addObserver(_ observer: AnyObject, events: [PKEvent.Type], observeOn dispatchQueue: DispatchQueue, block: @escaping (PKEvent) -> Void) { 33 | self.add(observer: observer, events: events, observeOn: dispatchQueue, block: block) 34 | } 35 | 36 | private func add(observer: AnyObject, events: [PKEvent.Type], observeOn dispatchQueue: DispatchQueue = DispatchQueue.main, block: @escaping (PKEvent) -> Void) { 37 | self.dispatchQueue.sync { 38 | PKLog.verbose("Add observer: \(String(describing: observer)) for events: \(String(describing: events))") 39 | events.forEach { (et) in 40 | let typeId = NSStringFromClass(et) 41 | var observationList: [Observation] = observations[typeId] ?? [] 42 | observationList.append(Observation(observer: observer, observeOn: dispatchQueue, block: block)) 43 | observations[typeId] = observationList 44 | } 45 | } 46 | } 47 | 48 | @objc public func removeObserver(_ observer: AnyObject, events: [PKEvent.Type]) { 49 | self.dispatchQueue.sync { 50 | PKLog.verbose("Remove observer: \(String(describing: observer)) for events: \(String(describing: events))") 51 | events.forEach { (et) in 52 | let typeId = NSStringFromClass(et) 53 | guard let array = observations[typeId], !array.isEmpty else { 54 | PKLog.debug("removeObserver:: array is empty") 55 | return 56 | } 57 | 58 | observations[typeId] = array.filter { 59 | if let item = $0.observer { 60 | return item !== observer 61 | } 62 | return false 63 | } 64 | } 65 | } 66 | } 67 | 68 | @objc public func post(_ event: PKEvent) { 69 | self.dispatchQueue.sync { [weak self] in 70 | guard let self = self else { return } 71 | PKLog.verbose("post event: \(event), with data: \(event.data ?? [:])") 72 | let typeId = NSStringFromClass(type(of: event)) 73 | 74 | if let array = self.observations[typeId] { 75 | // remove nil observers replace current observations with new ones, and call block with the event 76 | let newObservations = array.filter { $0.observer != nil } 77 | self.observations[typeId] = newObservations 78 | newObservations.forEach { observation in 79 | observation.observeOn.async { 80 | observation.block(event) 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } 87 | --------------------------------------------------------------------------------