├── Examples └── iOS │ ├── Assets.xcassets │ ├── Contents.json │ ├── AccentColor.colorset │ │ └── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Utils.swift │ ├── Info.plist │ ├── ViewController.swift │ ├── AppDelegate.swift │ ├── Base.lproj │ ├── Main.storyboard │ └── LaunchScreen.storyboard │ └── SceneDelegate.swift ├── Sources ├── PlayerAnalytics │ ├── Utils │ │ ├── TimeUtils.swift │ │ ├── HTTP │ │ │ ├── Request.swift │ │ │ ├── HttpClient.swift │ │ │ └── Response.swift │ │ ├── FixedSizedArrayWithUpsert.swift │ │ └── PlayerAnalyticsError.swift │ ├── Payload │ │ ├── ErrorCode.swift │ │ ├── Batch.swift │ │ └── Event.swift │ └── ApiVideoPlayerAnalyticsAgent.swift ├── PlayerAnalytics.docc │ └── PlayerAnalytics.md └── AVPlayerAnalytics │ ├── AVErrorExtension.swift │ ├── Utils.swift │ └── ApiVideoDatabusAVPlayer.swift ├── .github └── workflows │ ├── test.yml │ ├── create-release-from-changelog.yml │ ├── release.yml │ ├── create-documentation-pr.yml │ ├── update-documentation.yml │ └── build.yml ├── ApiVideoPlayerAnalytics.podspec ├── Tests └── Databus │ ├── Utils │ └── FixedSizedArrayWithUpsertTest.swift │ └── Payload │ └── EventTest.swift ├── CHANGELOG.md ├── LICENSE.md ├── Package.swift ├── .swiftlint.yml ├── .swiftformat ├── ApiVideoPlayerAnalytics.xcodeproj ├── xcshareddata │ └── xcschemes │ │ └── Example iOS.xcscheme └── project.pbxproj ├── README.md ├── .gitignore └── CONTRIBUTING.md /Examples/iOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/PlayerAnalytics/Utils/TimeUtils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum TimeUtils { 4 | static func currentTimeInMs() -> Int64 { 5 | return Int64(Date().timeIntervalSince1970 * 1_000) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Examples/iOS/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/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run unit tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | runs-on: macos-14 8 | steps: 9 | - uses: actions/checkout@v4 10 | - name: Running tests 11 | run: swift test 12 | -------------------------------------------------------------------------------- /Examples/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/PlayerAnalytics.docc/PlayerAnalytics.md: -------------------------------------------------------------------------------- 1 | # ``PlayerAnalytics`` 2 | 3 | Summary 4 | 5 | ## Overview 6 | 7 | Text 8 | 9 | ## Topics 10 | 11 | ### Group 12 | 13 | - ``Symbol`` 14 | -------------------------------------------------------------------------------- /.github/workflows/create-release-from-changelog.yml: -------------------------------------------------------------------------------- 1 | name: Create draft release from CHANGELOG.md 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'CHANGELOG.md' 7 | 8 | jobs: 9 | update-documentation: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Create draft release if needed 14 | uses: apivideo/api.video-release-from-changelog-action@main 15 | with: 16 | github-auth-token: ${{ secrets.GITHUB_TOKEN }} 17 | prefix: v 18 | 19 | -------------------------------------------------------------------------------- /Sources/AVPlayerAnalytics/AVErrorExtension.swift: -------------------------------------------------------------------------------- 1 | import AVFoundation 2 | import Foundation 3 | 4 | extension AVError { 5 | var analyticsErrorCode: ErrorCode { 6 | switch code { 7 | case .contentIsUnavailable, .noLongerPlayable: 8 | return .network 9 | 10 | case .decodeFailed: 11 | return .decoding 12 | 13 | // case .decoderNotFound, .decoderTemporarilyUnavailable, .formatUnsupported, .incompatibleAsset: 14 | default: return .noSupport // Nothing 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release to Cocoapods 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: macos-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Install Cocoapods 13 | run: gem install cocoapods 14 | - name: Deploy to Cocoapods 15 | run: | 16 | set -eo pipefail 17 | pod lib lint --allow-warnings 18 | pod trunk push --allow-warnings 19 | env: 20 | COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} -------------------------------------------------------------------------------- /Examples/iOS/Utils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum Utils { 4 | static func inferManifestURL(mediaId: String) -> URL { 5 | let url: URL? 6 | if mediaId.hasPrefix("vi") { 7 | url = URL(string: "https://vod.api.video/vod/\(mediaId)/hls/manifest.m3u8") 8 | } else if mediaId.hasPrefix("li") { 9 | url = URL(string: "https://live.api.video/\(mediaId).m3u8") 10 | } else { 11 | fatalError("Invalid mediaId: \(mediaId)") 12 | } 13 | guard let url else { 14 | fatalError("Invalid url for \(mediaId)") 15 | } 16 | return url 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ApiVideoPlayerAnalytics.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'ApiVideoPlayerAnalytics' 3 | s.version = '2.0.0' 4 | s.summary = 'The official Swift player analytics for api.video' 5 | 6 | s.homepage = 'https://github.com/apivideo/api.video-swift-player-analytics' 7 | s.license = { :type => 'MIT', :file => 'LICENSE.md' } 8 | s.author = { 'Ecosystem' => 'ecosystem@api.video' } 9 | s.source = { :git => 'https://github.com/apivideo/api.video-swift-player-analytics.git', :tag => "v" + s.version.to_s } 10 | 11 | s.ios.deployment_target = '11.0' 12 | s.swift_version = '5.0' 13 | 14 | s.source_files = 'Sources/**/*.{swift, plist}' 15 | end 16 | -------------------------------------------------------------------------------- /Sources/PlayerAnalytics/Payload/ErrorCode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// An enum representing an error code that is emitted by the player. 4 | enum ErrorCode: Int, Codable { 5 | /// No error 6 | case none = 0 7 | /// The action was aborted by user. 8 | case abort = 1 9 | /// Some kind of network error occurred which prevented the media from being successfully fetched, despite having previously been available. 10 | case network = 2 11 | /// Despite having previously been determined to be usable, an error occurred while trying to decode the media resource, resulting in an error. 12 | case decoding = 3 13 | /// The media resource was determined to be unsuitable. 14 | case noSupport = 4 15 | } 16 | -------------------------------------------------------------------------------- /Sources/PlayerAnalytics/Utils/HTTP/Request.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol Requestable { 4 | func launch(completion: @escaping (Response) -> Void) 5 | } 6 | 7 | class Request: Requestable { 8 | private let session: URLSession 9 | private let request: URLRequest 10 | 11 | init(session: URLSession, request: URLRequest) { 12 | self.session = session 13 | self.request = request 14 | } 15 | 16 | func launch(completion: @escaping (Response) -> Void) { 17 | let task = session.dataTask(with: request, completionHandler: { data, response, error in 18 | completion(Response(data: data, response: response as? HTTPURLResponse, error: error)) 19 | }) 20 | task.resume() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/Databus/Utils/FixedSizedArrayWithUpsertTest.swift: -------------------------------------------------------------------------------- 1 | @testable import ApiVideoPlayerAnalytics 2 | import XCTest 3 | 4 | class FixedSizedArrayWithUpsertTest: XCTestCase { 5 | func testUpsert() throws { 6 | let expected = ["a", "b"] 7 | 8 | let actual = FixedSizedArrayWithUpsert(maxSize: 2) 9 | actual.upsert("a") 10 | actual.upsert("b") 11 | actual.upsert("b") 12 | XCTAssertEqual(expected, actual.array) 13 | } 14 | 15 | func testListSize() throws { 16 | let expected = ["b", "c"] 17 | 18 | let actual = FixedSizedArrayWithUpsert(maxSize: 2) 19 | actual.upsert("a") 20 | actual.upsert("b") 21 | actual.upsert("c") 22 | XCTAssertEqual(expected, actual.array) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Examples/iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | UISceneStoryboardFile 19 | Main 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Examples/iOS/ViewController.swift: -------------------------------------------------------------------------------- 1 | import ApiVideoPlayerAnalytics 2 | import AVFoundation 3 | import AVKit 4 | import UIKit 5 | 6 | class ViewController: UIViewController { 7 | private let player = ApiVideoAnalyticsAVPlayer(playerItem: nil) 8 | private let playerViewController = AVPlayerViewController() 9 | 10 | override func viewDidLoad() { 11 | super.viewDidLoad() 12 | // Replace with your media ID (either your video ID or your live stream ID) 13 | let url = Utils.inferManifestURL(mediaId: "vi77Dgk0F8eLwaFOtC5870yn") 14 | player.replaceCurrentItem(with: AVPlayerItem(url: url)) 15 | } 16 | 17 | override func viewWillAppear(_ animated: Bool) { 18 | super.viewWillAppear(animated) 19 | addChild(playerViewController) 20 | view.addSubview(playerViewController.view) 21 | playerViewController.view.frame = view.frame 22 | playerViewController.player = player 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/create-documentation-pr.yml: -------------------------------------------------------------------------------- 1 | name: Create documentation PR 2 | on: 3 | # Trigger the workflow on pull requests targeting the main branch 4 | pull_request: 5 | types: [assigned, unassigned, opened, reopened, synchronize, edited, labeled, unlabeled, edited, closed] 6 | branches: 7 | - main 8 | 9 | jobs: 10 | create_documentation_pr: 11 | if: github.event.action != 'closed' 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Check out current repository code 17 | uses: actions/checkout@v4 18 | 19 | - name: Create the documentation pull request 20 | uses: apivideo/api.video-create-readme-file-pull-request-action@main 21 | with: 22 | source-file-path: "README.md" 23 | destination-repository: apivideo/api.video-documentation 24 | destination-path: sdks/player 25 | destination-filename: apivideo-swift-player.md 26 | pat: "${{ secrets.PAT }}" 27 | -------------------------------------------------------------------------------- /Sources/AVPlayerAnalytics/Utils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum Utils { 4 | /// Parse media URL to get mediaId 5 | static func parseMediaUrl(_ mediaURL: URL) -> String? { 6 | let mediaUrlStr = mediaURL.absoluteString 7 | let pattern = "https://[^/]+/(?>(?vod|live)/)?(?>.*/)?(?(vi|li)[^/^.]*).*" 8 | let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) 9 | 10 | guard let match = regex?.firstMatch( 11 | in: mediaUrlStr, 12 | range: NSRange(location: 0, length: mediaUrlStr.utf16.count) 13 | ) else { 14 | print("No match found for \(mediaUrlStr)") 15 | return nil 16 | } 17 | 18 | if let videoIdRange = Range(match.range(withName: "id"), in: mediaUrlStr) { 19 | return String(mediaUrlStr[videoIdRange]) 20 | } else { 21 | print("Can not get video id from URL: \(mediaUrlStr)") 22 | return nil 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/PlayerAnalytics/Utils/HTTP/HttpClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class HttpClient { 4 | private let session = URLSession(configuration: .default) 5 | private let url: URL 6 | 7 | init(_ url: URL) { 8 | self.url = url 9 | } 10 | 11 | func post(_ codable: Encodable, completion: @escaping (Response) -> Void) { 12 | let encoder = JSONEncoder() 13 | do { 14 | let data = try encoder.encode(codable) 15 | post(data, completion: completion) 16 | } catch { 17 | completion(Response(data: nil, response: nil, error: error)) 18 | } 19 | } 20 | 21 | func post(_ data: Data, completion: @escaping (Response) -> Void) { 22 | var urlRequest = URLRequest(url: url) 23 | urlRequest.httpMethod = "POST" 24 | urlRequest.httpBody = data 25 | urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") 26 | 27 | let request = Request(session: session, request: urlRequest) 28 | request.launch(completion: completion) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All changes to this project will be documented in this file. 3 | 4 | ## [2.0.0] - 2024-07-24 5 | - Use Analytics endpoint v2 6 | 7 | ## [1.1.1] - 2023-10-09 8 | - Fix minor issues: seek to 0 is possible, fix value of `emittedAt`,... 9 | - Synchronize generation of ping payload 10 | - Add api.video logo as AppIcon 11 | 12 | ## [1.1.0] - 2023-03-04 13 | - Add support for custom domain 14 | - `metadata` parameter is now a `Dictionary` 15 | - Simplify Json serialization 16 | 17 | ## [1.0.6] - 2023-03-04 18 | - Fix regex to handle private live stream 19 | 20 | ## [1.0.5] - 2023-03-23 21 | - Add init method that handle VideoInfo 22 | - VideoInfo build the pingUrl 23 | 24 | ## [1.0.4] - 2022-08-03 25 | - Add Seek method with CMTime params 26 | 27 | ## [1.0.3] - 2022-07-26 28 | - Fix cocoapods release 29 | 30 | ## [1.0.2] - 2022-07-22 31 | - Fix handle server error 32 | 33 | ## [1.0.1] - 2022-07-06 34 | - Fix scheduled data send 35 | - Fix json error 36 | - Handle dublicates events 37 | - Add Docc documentation 38 | 39 | ## [1.0.0] - 2022-01-03 40 | - First version 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 api.video 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.9 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "ApiVideoPlayerAnalytics", 6 | platforms: [ 7 | .iOS(.v11), 8 | .tvOS(.v12), 9 | .visionOS(.v1), 10 | .macOS(.v10_13), 11 | .macCatalyst(.v14) 12 | ], 13 | products: [ 14 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 15 | .library( 16 | name: "ApiVideoPlayerAnalytics", 17 | targets: ["ApiVideoPlayerAnalytics"] 18 | ) 19 | ], 20 | dependencies: [ 21 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.3.0") 22 | ], 23 | targets: [ 24 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 25 | .target( 26 | name: "ApiVideoPlayerAnalytics", 27 | path: "Sources" 28 | ), 29 | // Targets for tests 30 | .testTarget( 31 | name: "ApiVideoPlayerAnalyticsTests", 32 | dependencies: ["ApiVideoPlayerAnalytics"], 33 | path: "Tests" 34 | ) 35 | ] 36 | ) 37 | -------------------------------------------------------------------------------- /Sources/PlayerAnalytics/Payload/Batch.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A struct representing a batch of events that are emitted by the player. 4 | struct Batch: Codable { 5 | let sendAtInMs: Int64 6 | let sessionId: String 7 | let playbackId: String 8 | let mediaId: String 9 | let events: [Event] 10 | let version: String 11 | let referrer: String 12 | 13 | enum CodingKeys: String, CodingKey { 14 | case sendAtInMs = "sat" 15 | case sessionId = "sid" 16 | case playbackId = "pid" 17 | case mediaId = "mid" 18 | case events = "eve" 19 | case version = "ver" 20 | case referrer = "ref" 21 | } 22 | 23 | static func createNow( 24 | sessionId: String, 25 | playbackId: String, 26 | mediaId: String, 27 | events: [Event], 28 | version: String, 29 | referrer: String 30 | ) -> Batch { 31 | return Batch( 32 | sendAtInMs: TimeUtils.currentTimeInMs(), 33 | sessionId: sessionId, 34 | playbackId: playbackId, 35 | mediaId: mediaId, 36 | events: events, 37 | version: version, 38 | referrer: referrer 39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/PlayerAnalytics/Utils/FixedSizedArrayWithUpsert.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A class representing a fixed sized array. 4 | /// The array will only contain the last `maxSize` elements. 5 | class FixedSizedArrayWithUpsert { 6 | private let maxSize: Int 7 | private var elements: [T] = [] 8 | 9 | /// True if the array is empty. 10 | var isEmpty: Bool { 11 | elements.isEmpty 12 | } 13 | 14 | /// The array of elements. 15 | /// Available for testing purposes. 16 | var array: [T] { 17 | elements 18 | } 19 | 20 | /// Initialize the array with a maximum size. 21 | init(maxSize: Int) { 22 | self.maxSize = maxSize 23 | } 24 | 25 | /// Upsert an element. 26 | func upsert(_ element: T) { 27 | if elements.last == element { 28 | elements.removeLast() 29 | elements.append(element) 30 | } else { 31 | add(element) 32 | } 33 | } 34 | 35 | private func add(_ element: T) { 36 | if elements.count >= maxSize { 37 | elements.remove(at: 0) 38 | } 39 | elements.append(element) 40 | } 41 | 42 | func removeAll() { 43 | elements.removeAll() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/update-documentation.yml: -------------------------------------------------------------------------------- 1 | name: Update Docc documentation 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | update-api-documentation: 9 | runs-on: macos-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: maxim-lobanov/setup-xcode@v1 13 | with: 14 | xcode-version: latest-stable 15 | - name: generate doc files 16 | run: xcodebuild docbuild -scheme ApiVideoPlayerAnalytics -derivedDataPath ../api.video-swift-player-analytics-build -destination 'platform=iOS Simulator,name=iPhone 12' 17 | - name: create docs folder 18 | run: mkdir ../Docs 19 | - name: process docarchive 20 | run: $(xcrun --find docc) process-archive \transform-for-static-hosting ../api.video-swift-player-analytics-build/Build/Products/Debug/ApiVideoPlayerAnalytics.doccarchive \--output-path ../Docs \--hosting-base-path /api.video-swift-player-analytics 21 | - name: Deploy documentation to Github Pages 22 | uses: JamesIves/github-pages-deploy-action@v4 23 | with: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | branch: gh-pages 26 | folder: ../Docs 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Examples/iOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // iOS 4 | // 5 | // Created by Thibault on 25/03/2024. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | func application( 13 | _: UIApplication, 14 | didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? 15 | ) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application( 23 | _: UIApplication, 24 | configurationForConnecting connectingSceneSession: UISceneSession, 25 | options _: UIScene.ConnectionOptions 26 | ) -> UISceneConfiguration { 27 | // Called when a new scene session is being created. 28 | // Use this method to select a configuration to create the new scene with. 29 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 30 | } 31 | 32 | func application(_: UIApplication, didDiscardSceneSessions _: Set) { 33 | // Called when the user discards a scene session. 34 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 35 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/PlayerAnalytics/Utils/HTTP/Response.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | func json(_ data: Data?) -> Any? { 4 | guard let data = data else { return nil } 5 | return try? JSONSerialization.jsonObject(with: data, options: []) 6 | } 7 | 8 | func string(_ data: Data?) -> String? { 9 | guard let data = data else { return nil } 10 | return String(data: data, encoding: .utf8) 11 | } 12 | 13 | struct Response { 14 | let data: Data? 15 | let response: HTTPURLResponse? 16 | let error: Error? 17 | 18 | func result() throws -> Any? { 19 | guard error == nil else { 20 | throw AnalyticsAPIError(description: "Unknown error", statusCode: response?.statusCode ?? 0) 21 | } 22 | guard let response = response else { throw AnalyticsAPIError(description: nil) } 23 | guard (200 ... 300).contains(response.statusCode) else { 24 | if let json = json(data) as? [String: Any] { 25 | throw AnalyticsAPIError(info: json, statusCode: response.statusCode) 26 | } 27 | throw AnalyticsAPIError(from: self) 28 | } 29 | guard let data = data, !data.isEmpty else { 30 | if response.statusCode == 204 { 31 | return nil 32 | } 33 | // Not using the custom initializer because data could be empty 34 | throw AnalyticsAPIError(description: nil, statusCode: response.statusCode) 35 | } 36 | if let json = json(data) { 37 | return json 38 | } 39 | throw AnalyticsAPIError(from: self) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Examples/iOS/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 | -------------------------------------------------------------------------------- /Examples/iOS/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 | -------------------------------------------------------------------------------- /Sources/PlayerAnalytics/Payload/Event.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A struct representing an event that is emitted by the player. 4 | struct Event: Codable { 5 | let emittedAtInMs: Int64 6 | let type: EventType 7 | let videoTimeInS: Float 8 | let videoWidth: Int 9 | let videoHeight: Int 10 | let paused: Bool 11 | let errorCode: ErrorCode 12 | 13 | enum CodingKeys: String, CodingKey { 14 | case emittedAtInMs = "eat" 15 | case type = "typ" 16 | case videoTimeInS = "vti" 17 | case videoWidth = "vwi" 18 | case videoHeight = "vhe" 19 | case paused = "pau" 20 | case errorCode = "eco" 21 | } 22 | 23 | enum EventType: Int, Codable { 24 | case loaded = 1 25 | case play = 2 26 | case pause = 3 27 | case timeUpdate = 4 28 | case error = 5 29 | case end = 6 30 | case seek = 7 31 | case src = 8 32 | } 33 | 34 | static func createNow( 35 | type: EventType, 36 | videoTimeInS: Float, 37 | videoWidth: Int, 38 | videoHeight: Int, 39 | paused: Bool, 40 | errorCode: ErrorCode 41 | ) -> Event { 42 | return Event( 43 | emittedAtInMs: TimeUtils.currentTimeInMs(), 44 | type: type, 45 | videoTimeInS: videoTimeInS, 46 | videoWidth: videoWidth, 47 | videoHeight: videoHeight, 48 | paused: paused, 49 | errorCode: errorCode 50 | ) 51 | } 52 | } 53 | 54 | // MARK: Equatable 55 | 56 | extension Event: Equatable { 57 | static func == (lhs: Event, rhs: Event) -> Bool { 58 | return lhs.isEqual(rhs) 59 | } 60 | 61 | func isEqual(_ other: Event) -> Bool { 62 | if type != other.type { 63 | return false 64 | } 65 | if paused != other.paused { 66 | return false 67 | } 68 | return true 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | - push 4 | jobs: 5 | find_schemes: 6 | name: Find xcode schemes 7 | runs-on: macos-14 8 | outputs: 9 | schemes: ${{ steps.getSchemes.outputs.schemes}} 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | - name: xcode version 14 | uses: maxim-lobanov/setup-xcode@v1 15 | with: 16 | xcode-version: latest-stable 17 | - name: Get schemes 18 | id: getSchemes 19 | run: | 20 | TARGETS=$(xcodebuild -list -json | tr -d "\n") 21 | SCHEMES=$(echo $TARGETS | ruby -e "require 'json'; puts JSON.generate(:scheme => JSON.parse(STDIN.gets)['project']['schemes'])") 22 | echo Found schemes: $SCHEMES 23 | echo "schemes=$SCHEMES" >> $GITHUB_OUTPUT 24 | swift_build: 25 | name: Build with swift 26 | runs-on: macos-14 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | - name: xcode version 31 | uses: maxim-lobanov/setup-xcode@v1 32 | with: 33 | xcode-version: latest-stable 34 | - name: Build Package with swift 35 | run: swift build 36 | xcode_build: 37 | name: Build with xcode 38 | needs: find_schemes 39 | runs-on: macos-14 40 | strategy: 41 | matrix: ${{ fromJson(needs.find_schemes.outputs.schemes) }} 42 | steps: 43 | - name: Checkout 44 | uses: actions/checkout@v4 45 | - name: xcode version 46 | uses: maxim-lobanov/setup-xcode@v1 47 | with: 48 | xcode-version: latest-stable 49 | - name: Build ${{matrix.scheme}} 50 | run: xcodebuild clean build -project ApiVideoPlayerAnalytics.xcodeproj -scheme "${{matrix.scheme}}" -sdk iphoneos CODE_SIGNING_ALLOWED=NO 51 | verify: 52 | name: Verify package sanity 53 | runs-on: macos-14 54 | steps: 55 | - name: Checkout 56 | uses: actions/checkout@v4 57 | - name: xcode version 58 | uses: maxim-lobanov/setup-xcode@v1 59 | with: 60 | xcode-version: latest-stable 61 | - name: Verify cocoapods 62 | run: pod lib lint --allow-warnings 63 | - name: Install swiftlint 64 | run: brew install swiftlint 65 | - name: Execute swiftlint 66 | run: swiftlint 67 | -------------------------------------------------------------------------------- /Examples/iOS/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // iOS 4 | // 5 | // Created by Thibault on 25/03/2024. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | var window: UIWindow? 12 | 13 | func scene(_: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) { 14 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 15 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 16 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 17 | } 18 | 19 | func sceneDidDisconnect(_: UIScene) { 20 | // Called as the scene is being released by the system. 21 | // This occurs shortly after the scene enters the background, or when its session is discarded. 22 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 23 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 24 | } 25 | 26 | func sceneDidBecomeActive(_: UIScene) { 27 | // Called when the scene has moved from an inactive state to an active state. 28 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 29 | } 30 | 31 | func sceneWillResignActive(_: UIScene) { 32 | // Called when the scene will move from an active state to an inactive state. 33 | // This may occur due to temporary interruptions (ex. an incoming phone call). 34 | } 35 | 36 | func sceneWillEnterForeground(_: UIScene) { 37 | // Called as the scene transitions from the background to the foreground. 38 | // Use this method to undo the changes made on entering the background. 39 | } 40 | 41 | func sceneDidEnterBackground(_: UIScene) { 42 | // Called as the scene transitions from the foreground to the background. 43 | // Use this method to save data, release shared resources, and store enough scene-specific state information 44 | // to restore the scene back to its current state. 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/Databus/Payload/EventTest.swift: -------------------------------------------------------------------------------- 1 | @testable import ApiVideoPlayerAnalytics 2 | import XCTest 3 | 4 | class EventTest: XCTestCase { 5 | private let jsonEncoder = JSONEncoder() 6 | 7 | override func setUp() { 8 | jsonEncoder.outputFormatting = .sortedKeys 9 | } 10 | 11 | func testSerialization() { 12 | let expected = """ 13 | {"eat":1710147604622,"eco":0,"pau":false,"typ":2,"vhe":1280,"vti":0.1,"vwi":720} 14 | """ 15 | let event = Event( 16 | emittedAtInMs: 1_710_147_604_622, 17 | type: .play, 18 | videoTimeInS: 0.1, 19 | videoWidth: 720, 20 | videoHeight: 1_280, 21 | paused: false, 22 | errorCode: .none 23 | ) 24 | do { 25 | let actual = try jsonEncoder.encode(event) 26 | XCTAssertEqual(expected, String(data: actual, encoding: .utf8)) 27 | } catch { 28 | XCTFail("Failed to encode Event: \(error)") 29 | } 30 | } 31 | 32 | func testEquality() { 33 | let event1 = Event( 34 | emittedAtInMs: 1_710_147_604_622, 35 | type: .play, 36 | videoTimeInS: 0.1, 37 | videoWidth: 720, 38 | videoHeight: 1_280, 39 | paused: false, 40 | errorCode: .none 41 | ) 42 | let event2 = Event( 43 | emittedAtInMs: 1_710_147_604_623, 44 | type: .play, 45 | videoTimeInS: 0.3, 46 | videoWidth: 480, 47 | videoHeight: 640, 48 | paused: false, 49 | errorCode: .abort 50 | ) 51 | XCTAssertEqual(event1, event2) 52 | 53 | // Test different types 54 | let event3 = Event( 55 | emittedAtInMs: 1_710_147_604_622, 56 | type: .pause, 57 | videoTimeInS: 0.1, 58 | videoWidth: 720, 59 | videoHeight: 1_280, 60 | paused: false, 61 | errorCode: .none 62 | ) 63 | XCTAssertNotEqual(event1, event3) 64 | 65 | // Test different paused 66 | let event4 = Event( 67 | emittedAtInMs: 1_710_147_604_622, 68 | type: .play, 69 | videoTimeInS: 0.1, 70 | videoWidth: 720, 71 | videoHeight: 1_280, 72 | paused: true, 73 | errorCode: .none 74 | ) 75 | XCTAssertNotEqual(event1, event4) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - opening_brace 3 | - todo 4 | - notification_center_detachment 5 | - block_based_kvo 6 | - identifier_name 7 | 8 | opt_in_rules: 9 | - anyobject_protocol 10 | - attributes 11 | - closure_end_indentation 12 | - closure_spacing 13 | - contains_over_filter_count 14 | - contains_over_filter_is_empty 15 | - contains_over_range_nil_comparison 16 | - convenience_type 17 | - discouraged_object_literal 18 | - empty_collection_literal 19 | - empty_count 20 | - empty_string 21 | - empty_xctest_method 22 | - enum_case_associated_values_count 23 | - explicit_init 24 | - explicit_self 25 | - fallthrough 26 | - fatal_error_message 27 | - first_where 28 | - flatmap_over_map_reduce 29 | - force_unwrapping 30 | - function_default_parameter_at_end 31 | - identical_operands 32 | - implicit_return 33 | - joined_default_parameter 34 | - last_where 35 | - legacy_multiple 36 | - modifier_order 37 | - operator_usage_whitespace 38 | - optional_enum_case_matching 39 | - overridden_super_call 40 | - pattern_matching_keywords 41 | - prefer_zero_over_explicit_init 42 | - prohibited_super_call 43 | - redundant_nil_coalescing 44 | - redundant_type_annotation 45 | - single_test_class 46 | - sorted_first_last 47 | - static_operator 48 | - toggle_bool 49 | - untyped_error_in_catch 50 | - unneeded_parentheses_in_closure_argument 51 | - unused_declaration 52 | - unused_import 53 | - vertical_whitespace_between_cases 54 | - yoda_condition 55 | 56 | excluded: 57 | - Pods 58 | - "*/Pods" 59 | - R.generated.swift 60 | - .build 61 | 62 | # We exclude function (which is present by default) 63 | implicit_return: 64 | included: 65 | - closure 66 | - getter 67 | 68 | private_over_fileprivate: 69 | validate_extensions: true 70 | severity: error 71 | 72 | force_unwrapping: 73 | severity: error 74 | 75 | redundant_type_annotation: error 76 | 77 | type_name: 78 | min_length: 3 79 | max_length: 80 | warning: 60 81 | error: 60 82 | excluded: 83 | - Id 84 | 85 | identifier_name: 86 | allowed_symbols: "_" 87 | min_length: 3 88 | max_length: 89 | warning: 60 90 | error: 60 91 | excluded: 92 | - id 93 | - x 94 | - y 95 | 96 | line_length: 97 | warning: 120 98 | error: 120 99 | ignores_function_declarations: true 100 | ignores_comments: true 101 | ignores_urls: true 102 | 103 | function_body_length: 104 | warning: 300 105 | error: 300 106 | 107 | function_parameter_count: 108 | warning: 8 109 | error: 8 110 | 111 | type_body_length: 112 | warning: 600 113 | error: 800 114 | 115 | file_length: 116 | warning: 1000 117 | error: 1000 118 | ignore_comment_only_lines: true 119 | 120 | enum_case_associated_values_count: 121 | warning: 3 122 | error: 3 123 | 124 | reporter: "xcode" 125 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | ## File options 2 | 3 | --exclude Pods 4 | --swiftversion 5.3 5 | 6 | 7 | ## Enabled rules 8 | 9 | --enable andOperator 10 | --enable anyObjectProtocol 11 | --enable blankLinesAroundMark 12 | --enable blankLinesBetweenScopes 13 | --enable braces 14 | --enable consecutiveBlankLines 15 | --enable consecutiveSpaces 16 | --enable duplicateimports 17 | --enable elseOnSameLine 18 | --enable emptybraces 19 | --enable enumnamespaces 20 | --enable extensionaccesscontrol 21 | --enable fileHeader 22 | --enable indent 23 | --enable initcoderunavailable 24 | --enable isempty 25 | --enable leadingdelimiters 26 | --enable linebreakAtEndOfFile 27 | --enable marktypes 28 | --enable numberFormatting 29 | --enable preferkeypath 30 | --enable redundantbackticks 31 | --enable redundantextensionacl 32 | --enable redundantfileprivate 33 | --enable redundantlet 34 | --enable redundantnilinit 35 | --enable redundantobjc 36 | --enable redundantparens 37 | --enable redundantpattern 38 | --enable redundantrawvalues 39 | --disable redundantself 40 | --enable redundanttype 41 | --enable redundantvoidreturntype 42 | --enable semicolons 43 | --enable sortedimports 44 | --enable spacearoundbraces 45 | --enable spacearoundbrackets 46 | --enable spacearoundcomments 47 | --enable spacearoundgenerics 48 | --enable spacearoundoperators 49 | --enable spacearoundparens 50 | --enable spaceinsidebraces 51 | --enable spaceinsidebrackets 52 | --enable spaceinsidecomments 53 | --enable spaceinsidegenerics 54 | --enable spaceinsideparens 55 | --enable trailingclosures 56 | --enable trailingCommas 57 | --enable typesugar 58 | --enable wrapArguments 59 | --enable wrapAttributes 60 | --enable wrapenumcases 61 | --enable wrapmultilinestatementbraces 62 | --enable yodaconditions
 63 | 64 | 65 | ## Configuration of specific rules 66 | 67 | ### Number formatting 68 | --decimalgrouping 3 69 | --hexgrouping 4,8 70 | --binarygrouping 4 71 | 72 | ### MARK 73 | --marktypes never 74 | 75 | ### Wrap 76 | --wraparguments before-first 77 | --wrapparameters before-first 78 | --wrapcollections before-first 79 | 80 | ### Attributes 81 | --funcattributes prev-line 82 | --typeattributes prev-line 83 | 84 | ### Else 85 | --elseposition same-line 86 | --guardelse same-line 87 | 88 | ### Others 89 | --commas inline 90 | --ifdef noindent 91 | --indent 4 92 | --linebreaks lf ##default 93 | --maxwidth 120 94 | --patternlet hoist ##default 95 | --self insert 96 | --semicolons never 97 | --stripunusedargs always 98 | --trimwhitespace always 99 | --voidtype void 100 | 101 | 102 | ## Disabled rules 103 | 104 | ### SwiftFormat also removes from funcs, so let's let SwiftLint takes care of it. 105 | --disable redundantReturn 106 | 107 | ### SwiftFormat also removes from end of scope, which is against our empty line guidelines. 108 | --disable blankLinesAtEndOfScope 109 | 110 | ### SwiftFormat also removes from start of scope, which is against our empty line guidelines. 111 | --disable blankLinesAtStartOfScope -------------------------------------------------------------------------------- /ApiVideoPlayerAnalytics.xcodeproj/xcshareddata/xcschemes/Example iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 58 | 59 | 60 | 61 | 67 | 69 | 75 | 76 | 77 | 78 | 80 | 81 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /Sources/PlayerAnalytics/Utils/PlayerAnalyticsError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol AnalyticsError: LocalizedError, CustomDebugStringConvertible { 4 | /// The underlying `Error` value, if any. 5 | var cause: Error? { get } 6 | } 7 | 8 | extension AnalyticsError { 9 | /// The underlying `Error` value, if any. Defaults to `nil`. 10 | var cause: Error? { nil } 11 | 12 | /// Description of the error for debugging. 13 | var localizedDescription: String { 14 | debugDescription 15 | } 16 | 17 | /// Description of the error for debugging. 18 | var errorDescription: String? { 19 | debugDescription 20 | } 21 | } 22 | 23 | extension AnalyticsError { 24 | func appendCause(to errorMessage: String) -> String { 25 | guard let cause = cause else { 26 | return errorMessage 27 | } 28 | 29 | let separator = errorMessage.hasSuffix(".") ? "" : "." 30 | return "\(errorMessage)\(separator) Cause: \(String(describing: cause))" 31 | } 32 | } 33 | 34 | protocol AnalyticsAPIErrorProtocol: AnalyticsError { 35 | /// The HTTP status code associated with the error. 36 | var statusCode: Int { get } 37 | 38 | /// Additional information about the error. 39 | var info: [String: Any] { get } 40 | 41 | /// Creates an error 42 | init(info: [String: Any], statusCode: Int) 43 | } 44 | 45 | extension AnalyticsAPIErrorProtocol { 46 | init(info: [String: Any], statusCode: Int = 0) { 47 | self.init(info: info, statusCode: statusCode) 48 | } 49 | 50 | init(cause error: Error, statusCode: Int = 0) { 51 | let info: [String: Any] = [ 52 | "description": "Unable to complete the operation.", 53 | "cause": error 54 | ] 55 | self.init(info: info, statusCode: statusCode) 56 | } 57 | 58 | init(description: String?, statusCode: Int = 0) { 59 | let info: [String: Any] = [ 60 | "description": description ?? "Empty response body." 61 | ] 62 | self.init(info: info, statusCode: statusCode) 63 | } 64 | 65 | init(from response: Response) { 66 | self.init(description: string(response.data), statusCode: response.response?.statusCode ?? 0) 67 | } 68 | } 69 | 70 | struct AnalyticsAPIError: AnalyticsAPIErrorProtocol { 71 | /// Additional information about the error. 72 | let info: [String: Any] 73 | 74 | /// HTTP status code of the response. 75 | let statusCode: Int 76 | 77 | /// Creates an error from a JSON response. 78 | /// 79 | /// - Parameters: 80 | /// - info: JSON response from Auth0. 81 | /// - statusCode: HTTP status code of the response. 82 | /// 83 | /// - Returns: A new `ManagementError`. 84 | init(info: [String: Any], statusCode: Int) { 85 | var values = info 86 | values["statusCode"] = statusCode 87 | self.info = values 88 | self.statusCode = statusCode 89 | } 90 | 91 | /// The underlying `Error` value, if any. Defaults to `nil`. 92 | var cause: Error? { 93 | info["cause"] as? Error 94 | } 95 | 96 | var message: String { 97 | if let string = info["description"] as? String { 98 | return string 99 | } 100 | return "Failed with unknown error \(info)." 101 | } 102 | 103 | /// Description of the error for debugging. 104 | public var debugDescription: String { 105 | appendCause(to: message) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![badge](https://img.shields.io/twitter/follow/api_video?style=social)](https://twitter.com/intent/follow?screen_name=api_video) 3 |   [![badge](https://img.shields.io/github/stars/apivideo/api.video-swift-player-analytics?style=social)](https://github.com/apivideo/api.video-swift-player-analytics) 4 |   [![badge](https://img.shields.io/discourse/topics?server=https%3A%2F%2Fcommunity.api.video)](https://community.api.video) 5 | ![](https://github.com/apivideo/.github/blob/main/assets/apivideo_banner.png) 6 |

api.video Swift player analytics

7 | 8 | [api.video](https://api.video) is the video infrastructure for product builders. Lightning fast 9 | video APIs for integrating, scaling, and managing on-demand & low latency live streaming features in 10 | your app. 11 | 12 | ## Table of contents 13 | 14 | - [Table of contents](#table-of-contents) 15 | - [Project description](#project-description) 16 | - [Getting started](#getting-started) 17 | - [Installation](#installation) 18 | - [Code sample](#code-sample) 19 | - [Documentation](#documentation) 20 | - [FAQ](#faq) 21 | 22 | 23 | 31 | 32 | ## Project description 33 | 34 | This library send player events from the player to api.video. 35 | 36 | ## Getting started 37 | 38 | ### Installation 39 | 40 | #### Swift Package Manager 41 | 42 | Add the following dependency to your `Package.swift` file: 43 | 44 | ```swift 45 | dependencies: [ 46 | .package(url: "https://github.com/apivideo/api.video-swift-player-analytics.git", from: "2.0.0"), 47 | ], 48 | ``` 49 | 50 | #### CocoaPods 51 | 52 | Add the following line to your `Podfile`: 53 | 54 | ```ruby 55 | pod 'ApiVideoPlayerAnalytics', '~> 2.0.0 56 | ``` 57 | 58 | Then, run `pod install`. 59 | 60 | ### Code sample 61 | 62 | Create a `ApiVideoAnalyticsAVPlayer` instance. 63 | 64 | ```swift 65 | import ApiVideoPlayerAnalytics 66 | 67 | let player = ApiVideoAnalyticsAVPlayer(url: "URL of the HLS manifest") // HLS manifest from api.video. Example: https://vod.api.video/vod/YOUR_VIDEO_ID/hls/manifest.m3u8 68 | ``` 69 | 70 | Then, use it like a regular `AVPlayer`. 71 | 72 | ```swift 73 | player.play() 74 | ``` 75 | 76 | For a custom domain collector, use: 77 | 78 | ```swift 79 | let player = ApiVideoAnalyticsAVPlayer(url: "URL of the HLS manifest", collectorUrl: "https://collector.mycustomdomain.com") // Register the player analytics listener so it sends player events to api.video. 80 | ``` 81 | 82 | ## Sample application 83 | 84 | Open `ApiVideoPlayerAnalytics.xcodeproj`. 85 | 86 | Replace the default media ID in `Examples/iOS/ViewController.swift` with your own. 87 | 88 | ```swift 89 | let url = Utils.inferManifestURL(mediaId: "vi77Dgk0F8eLwaFOtC5870yn") // replace `vi77Dgk0F8eLwaFOtC5870yn` with your own media ID 90 | ``` 91 | 92 | Then, run the `Example iOS` target. 93 | 94 | 95 | ## Documentation 96 | 97 | - [Player documentation](https://apivideo.github.io/api.video-swift-player-analytics/documentation/apivideoplayeranalytics/) 98 | - [api.video analytics documentation](https://docs.api.video/analytics) 99 | 100 | ## FAQ 101 | 102 | If you have any questions, ask us in the [community](https://community.api.video) or 103 | use [issues](https://github.com/apivideo/api.video-swift-player-analytics/issues). 104 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ### Swift ### 3 | # Xcode 4 | # 5 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 6 | 7 | ## User settings 8 | xcuserdata/ 9 | 10 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 11 | *.xcscmblueprint 12 | *.xccheckout 13 | 14 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 15 | build/ 16 | DerivedData/ 17 | *.moved-aside 18 | *.pbxuser 19 | !default.pbxuser 20 | *.mode1v3 21 | !default.mode1v3 22 | *.mode2v3 23 | !default.mode2v3 24 | *.perspectivev3 25 | !default.perspectivev3 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | 30 | ## App packaging 31 | *.ipa 32 | *.dSYM.zip 33 | *.dSYM 34 | 35 | ## Playgrounds 36 | timeline.xctimeline 37 | playground.xcworkspace 38 | 39 | # Swift Package Manager 40 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 41 | # Packages/ 42 | # Package.pins 43 | Package.resolved 44 | # *.xcodeproj 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # We recommend against adding the Pods directory to your .gitignore. However 53 | # you should judge for yourself, the pros and cons are mentioned at: 54 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 55 | # Pods/ 56 | # Add this line if you want to avoid checking in source code from the Xcode workspace 57 | # *.xcworkspace 58 | 59 | # Carthage 60 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 61 | # Carthage/Checkouts 62 | 63 | Carthage/Build/ 64 | 65 | # Add this lines if you are using Accio dependency management (Deprecated since Xcode 12) 66 | # Dependencies/ 67 | # .accio/ 68 | 69 | # fastlane 70 | # It is recommended to not store the screenshots in the git repo. 71 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 72 | # For more information about the recommended setup visit: 73 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 74 | 75 | fastlane/report.xml 76 | fastlane/Preview.html 77 | fastlane/screenshots/**/*.png 78 | fastlane/test_output 79 | 80 | # Code Injection 81 | # After new code Injection tools there's a generated folder /iOSInjectionProject 82 | # https://github.com/johnno1962/injectionforxcode 83 | 84 | iOSInjectionProject/ 85 | 86 | ### Xcode ### 87 | # Xcode 88 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 89 | 90 | 91 | 92 | 93 | ## Gcc Patch 94 | /*.gcno 95 | 96 | ### Xcode Patch ### 97 | *.xcodeproj/* 98 | !*.xcodeproj/project.pbxproj 99 | !*.xcodeproj/xcshareddata/ 100 | !*.xcworkspace/contents.xcworkspacedata 101 | **/xcshareddata/WorkspaceSettings.xcsettings 102 | 103 | .DS_Store 104 | /.build 105 | /Packages 106 | .swiftpm/config/registries.json 107 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 108 | .netrc 109 | 110 | # End of https://www.toptal.com/developers/gitignore/api/swift,xcode 111 | 112 | © 2022 GitHub, Inc. 113 | 114 | Terms 115 | Privacy 116 | Security 117 | Status 118 | Docs 119 | Contact GitHub 120 | Pricing 121 | API 122 | Training 123 | Blog 124 | About 125 | 126 | Loading complete 127 | 128 | .idea/workspace.xml 129 | # User-specific stuff 130 | .idea/**/workspace.xml 131 | .idea/**/tasks.xml 132 | .idea/**/usage.statistics.xml 133 | .idea/**/dictionaries 134 | .idea/**/shelf 135 | 136 | # AWS User-specific 137 | .idea/**/aws.xml 138 | 139 | # Generated files 140 | .idea/**/contentModel.xml 141 | 142 | # Sensitive or high-churn files 143 | .idea/**/dataSources/ 144 | .idea/**/dataSources.ids 145 | .idea/**/dataSources.local.xml 146 | .idea/**/sqlDataSources.xml 147 | .idea/**/dynamic.xml 148 | .idea/**/uiDesigner.xml 149 | .idea/**/dbnavigator.xml 150 | 151 | # Gradle 152 | .idea/**/gradle.xml 153 | .idea/**/libraries 154 | 155 | # Gradle and Maven with auto-import 156 | # When using Gradle or Maven with auto-import, you should exclude module files, 157 | # since they will be recreated, and may cause churn. Uncomment if using 158 | # auto-import. 159 | # .idea/artifacts 160 | # .idea/compiler.xml 161 | # .idea/jarRepositories.xml 162 | # .idea/modules.xml 163 | # .idea/*.iml 164 | # .idea/modules 165 | # *.iml 166 | # *.ipr 167 | 168 | # CMake 169 | cmake-build-*/ 170 | 171 | # Mongo Explorer plugin 172 | .idea/**/mongoSettings.xml 173 | 174 | # File-based project format 175 | *.iws 176 | 177 | # IntelliJ 178 | out/ 179 | 180 | # mpeltonen/sbt-idea plugin 181 | .idea_modules/ 182 | 183 | # JIRA plugin 184 | atlassian-ide-plugin.xml 185 | 186 | # Cursive Clojure plugin 187 | .idea/replstate.xml 188 | 189 | # Crashlytics plugin (for Android Studio and IntelliJ) 190 | com_crashlytics_export_strings.xml 191 | crashlytics.properties 192 | crashlytics-build.properties 193 | fabric.properties 194 | 195 | # Editor-based Rest Client 196 | .idea/httpRequests 197 | 198 | # Android studio 3.1+ serialized cache file 199 | .idea/caches/build_file_checksums.ser 200 | .idea/misc.xml 201 | .idea/modules.xml 202 | .idea/vcs.xml 203 | -------------------------------------------------------------------------------- /Sources/PlayerAnalytics/ApiVideoPlayerAnalyticsAgent.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// The player analytics agent that sends events to the collector. 4 | class ApiVideoPlayerAnalyticsAgent { 5 | private let options: ApiVideoPlayerAnalyticsOptions 6 | private let client: HttpClient 7 | 8 | private var events = FixedSizedArrayWithUpsert(maxSize: eventsQueueSize) 9 | private let serialQueue = DispatchQueue(label: "video.api.player.analytics.serialQueue") 10 | 11 | private let sessionId: String 12 | 13 | private var batchGenerator: BatchGenerator? 14 | 15 | private init(options: ApiVideoPlayerAnalyticsOptions, sessionId: String) { 16 | self.options = options 17 | self.sessionId = sessionId 18 | client = HttpClient(options.collectorUrl.appendingPathComponent(options.collectorPath)) 19 | } 20 | 21 | /// Sets the mediaId for the player analytics agent. 22 | /// - Parameters: 23 | /// - mediaId: The mediaId of the video. Either a videoId or a liveStreamId 24 | func setMediaId(_ mediaId: String) { 25 | serialQueue.sync { 26 | batchGenerator?.close() 27 | batchGenerator = BatchGenerator( 28 | sessionId: sessionId, 29 | mediaId: mediaId, 30 | playbackId: UUID().uuidString, 31 | batchReportIntervalInS: self.options.batchReportIntervalInS, 32 | serialQueue: serialQueue, 33 | onNewBatch: { batch in 34 | self.reportBatch(batch) 35 | } 36 | ) 37 | } 38 | } 39 | 40 | /// Temporary disables the player analytics agent. 41 | /// It is usefull when the player reads a non-api.video media. 42 | func disable() { 43 | serialQueue.sync { 44 | batchGenerator?.close() 45 | batchGenerator = nil 46 | } 47 | } 48 | 49 | /// Adds an event to the player analytics agent. 50 | func addEvent(_ event: Event) { 51 | batchGenerator?.addEvent(event) 52 | } 53 | 54 | private func reportBatch(_ batch: Batch) { 55 | client.post(batch, completion: ({ response in 56 | do { 57 | _ = try response.result() 58 | } catch { 59 | print("Error: \(error) for payload: \(batch)") 60 | } 61 | })) 62 | } 63 | 64 | deinit { 65 | batchGenerator?.close() 66 | } 67 | 68 | /// Create a new instance of the ApiVideoPlayerAnalyticsAgent. 69 | /// - Parameters: 70 | /// - collectorUrl: The URL of the collector. Only for custom domain. Expected format: URL(string: "https://collector.mycustomdomain.com")! 71 | /// - Returns: The ApiVideoPlayerAnalyticsAgent instance 72 | public static func create(collectorUrl: URL? = nil) -> ApiVideoPlayerAnalyticsAgent { 73 | let sessionId = SessionStorage.sessionId 74 | let options: ApiVideoPlayerAnalyticsOptions 75 | if let collectorUrl { 76 | options = ApiVideoPlayerAnalyticsOptions(collectorUrl: collectorUrl) 77 | } else { 78 | options = ApiVideoPlayerAnalyticsOptions() 79 | } 80 | return ApiVideoPlayerAnalyticsAgent(options: options, sessionId: sessionId) 81 | } 82 | 83 | private static let version = "2.0.0" 84 | private static let eventsQueueSize = 20 85 | 86 | private class BatchGenerator { 87 | private let sessionId: String 88 | private let mediaId: String 89 | private let playbackId: String 90 | private let batchReportIntervalInS: TimeInterval 91 | private let serialQueue: DispatchQueue 92 | 93 | private let onNewBatch: (Batch) -> Void 94 | 95 | private var events = FixedSizedArrayWithUpsert(maxSize: eventsQueueSize) 96 | 97 | private var scheduler: Timer? 98 | 99 | private var hasFirstPlay = false 100 | 101 | init( 102 | sessionId: String, 103 | mediaId: String, 104 | playbackId: String, 105 | batchReportIntervalInS: TimeInterval, 106 | serialQueue: DispatchQueue, 107 | onNewBatch: @escaping (Batch) -> Void 108 | ) { 109 | self.sessionId = sessionId 110 | self.mediaId = mediaId 111 | self.playbackId = playbackId 112 | self.batchReportIntervalInS = batchReportIntervalInS 113 | self.serialQueue = serialQueue 114 | self.onNewBatch = onNewBatch 115 | } 116 | 117 | private func startScheduler() { 118 | DispatchQueue.global(qos: .background).async { 119 | self.scheduler = Timer 120 | .scheduledTimer(withTimeInterval: self.batchReportIntervalInS, repeats: true) { _ in 121 | self.trySendBatch() 122 | } 123 | RunLoop.current.run() 124 | } 125 | } 126 | 127 | private func getBatch() -> Batch? { 128 | let events = serialQueue.sync { 129 | let events = self.events.array 130 | self.events.removeAll() 131 | return events 132 | } 133 | if events.isEmpty { 134 | return nil 135 | } 136 | 137 | return Batch.createNow( 138 | sessionId: sessionId, 139 | playbackId: playbackId, 140 | mediaId: mediaId, 141 | events: events, 142 | version: ApiVideoPlayerAnalyticsAgent.version, 143 | referrer: "" 144 | ) 145 | } 146 | 147 | private func trySendBatch() { 148 | guard let batch = getBatch() else { 149 | return 150 | } 151 | onNewBatch(batch) 152 | } 153 | 154 | func addEvent(_ event: Event) { 155 | if scheduler == nil { 156 | startScheduler() 157 | } 158 | serialQueue.sync { 159 | events.upsert(event) 160 | } 161 | if event.type == .play, !hasFirstPlay { 162 | hasFirstPlay = true 163 | trySendBatch() 164 | } 165 | } 166 | 167 | func close() { 168 | scheduler?.invalidate() 169 | trySendBatch() 170 | } 171 | 172 | deinit { 173 | close() 174 | } 175 | } 176 | } 177 | 178 | struct ApiVideoPlayerAnalyticsOptions { 179 | let collectorUrl: URL 180 | let collectorPath: String 181 | let batchReportIntervalInS: TimeInterval 182 | 183 | init( 184 | collectorUrl: URL = defaultCollectorUrl, 185 | collectorPath: String = defaultCollectorPath, 186 | batchReportIntervalInS: TimeInterval = defaultBatchReportIntervalInS 187 | ) { 188 | self.collectorUrl = collectorUrl 189 | self.collectorPath = collectorPath 190 | self.batchReportIntervalInS = batchReportIntervalInS 191 | } 192 | 193 | private static var defaultCollectorUrl: URL { 194 | let url = URL(string: "https://collector.api.video") 195 | guard let url else { fatalError("Invalid default collector URL") } 196 | return url 197 | } 198 | 199 | private static let defaultCollectorPath = "watch" 200 | private static let defaultBatchReportIntervalInS = 5.0 201 | } 202 | 203 | private enum SessionStorage { 204 | static let sessionId = UUID().uuidString 205 | } 206 | -------------------------------------------------------------------------------- /Sources/AVPlayerAnalytics/ApiVideoDatabusAVPlayer.swift: -------------------------------------------------------------------------------- 1 | import AVFoundation 2 | import Foundation 3 | 4 | /// An AVPlayer with api.video player analytics integration. 5 | public class ApiVideoAnalyticsAVPlayer: AVPlayer { 6 | private let agent: ApiVideoPlayerAnalyticsAgent 7 | private var timeObserver: Any? 8 | 9 | private var currentItemObserver: NSKeyValueObservation? 10 | private var statusObserver: NSKeyValueObservation? 11 | private var timeControlStatusObserver: NSKeyValueObservation? 12 | 13 | /// Initializes an AVPlayer that plays a single audiovisual resource referenced by URL with a custom collector. 14 | /// Parameters: 15 | /// - url: The URL that points to the api.video media. 16 | /// - collectorUrl: The URL of the collector. Only for custom domain. Expected format: URL(string: "https://collector.mycustomdomain.com")! 17 | public init(url: URL, collectorUrl: URL) { 18 | agent = ApiVideoPlayerAnalyticsAgent.create(collectorUrl: collectorUrl) 19 | super.init(url: url) 20 | addObservers() 21 | } 22 | 23 | /// Initializes an AVPlayer that plays a single audiovisual resource referenced by URL. 24 | /// Parameters: 25 | /// - url: The URL that points to the api.video media. 26 | override public init(url: URL) { 27 | agent = ApiVideoPlayerAnalyticsAgent.create() 28 | super.init(url: url) 29 | addObservers() 30 | } 31 | 32 | /// Initializes an AVPlayer that plays a single media item with a custom collector. 33 | /// Parameters: 34 | /// - playerItem: The AVPlayerItem that the player will play. 35 | /// - collectorUrl: The URL of the collector. Only for custom domain. Expected format: URL(string: "https://collector.mycustomdomain.com")! 36 | public init(playerItem: AVPlayerItem?, collectorUrl: URL) { 37 | agent = ApiVideoPlayerAnalyticsAgent.create(collectorUrl: collectorUrl) 38 | super.init(playerItem: playerItem) 39 | addObservers() 40 | } 41 | 42 | /// Initializes an AVPlayer that plays a single media item. 43 | /// Parameters: 44 | /// - playerItem: The AVPlayerItem that the player will play. 45 | override public init(playerItem: AVPlayerItem?) { 46 | agent = ApiVideoPlayerAnalyticsAgent.create() 47 | super.init(playerItem: playerItem) 48 | addObservers() 49 | } 50 | 51 | override public init() { 52 | agent = ApiVideoPlayerAnalyticsAgent.create() 53 | super.init() 54 | addObservers() 55 | } 56 | 57 | override public func seek(to time: CMTime) { 58 | super.seek(to: time) 59 | addEvent(.seek) 60 | } 61 | 62 | override public func seek(to time: CMTime, toleranceBefore: CMTime, toleranceAfter: CMTime) { 63 | super.seek(to: time, toleranceBefore: toleranceBefore, toleranceAfter: toleranceAfter) 64 | addEvent(.seek) 65 | } 66 | 67 | override public func seek(to date: Date) { 68 | super.seek(to: date) 69 | addEvent(.seek) 70 | } 71 | 72 | override public func seek(to time: CMTime, completionHandler: @escaping (Bool) -> Void) { 73 | super.seek(to: time, completionHandler: completionHandler) 74 | addEvent(.seek) 75 | } 76 | 77 | override public func seek(to date: Date, completionHandler: @escaping (Bool) -> Void) { 78 | super.seek(to: date, completionHandler: completionHandler) 79 | addEvent(.seek) 80 | } 81 | 82 | override public func seek( 83 | to time: CMTime, 84 | toleranceBefore: CMTime, 85 | toleranceAfter: CMTime, 86 | completionHandler: @escaping (Bool) -> Void 87 | ) { 88 | super.seek( 89 | to: time, 90 | toleranceBefore: toleranceBefore, 91 | toleranceAfter: toleranceAfter, 92 | completionHandler: completionHandler 93 | ) 94 | addEvent(.seek) 95 | } 96 | 97 | private func addEvent(_ eventType: Event.EventType) { 98 | let errorCode: ErrorCode 99 | if let error = error as? AVError { 100 | errorCode = error.analyticsErrorCode 101 | } else { 102 | errorCode = .none 103 | } 104 | let videoSize = currentItem?.presentationSize ?? CGSize.zero 105 | let paused = timeControlStatus != .playing 106 | let event = Event.createNow( 107 | type: eventType, 108 | videoTimeInS: Float(currentTime().seconds), 109 | videoWidth: Int(videoSize.width), 110 | videoHeight: Int(videoSize.height), 111 | paused: paused, 112 | errorCode: errorCode 113 | ) 114 | agent.addEvent(event) 115 | } 116 | 117 | deinit { 118 | removeObservers() 119 | } 120 | 121 | @objc 122 | func playerItemDidPlayToEnd() { 123 | addEvent(.end) 124 | } 125 | 126 | private func addPlayerItemObserver(_ playerItem: AVPlayerItem) { 127 | NotificationCenter.default.addObserver( 128 | self, 129 | selector: #selector(playerItemDidPlayToEnd), 130 | name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, 131 | object: playerItem 132 | ) 133 | } 134 | 135 | private func removePlayerItemObserver() { 136 | if let playerItem = currentItem { 137 | removePlayerItemObserver(playerItem) 138 | } 139 | } 140 | 141 | private func removePlayerItemObserver(_ playerItem: AVPlayerItem) { 142 | NotificationCenter.default.removeObserver( 143 | self, 144 | name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, 145 | object: playerItem 146 | ) 147 | } 148 | 149 | private func addPlayerObserver() { 150 | currentItemObserver = observe(\.currentItem, options: [.new, .old], changeHandler: { [weak self] _, change in 151 | self?.onCurrentItem(change) 152 | }) 153 | statusObserver = observe(\.status, options: [.new], changeHandler: { [weak self] player, change in 154 | self?.onStatus(player: player, change: change) 155 | }) 156 | timeControlStatusObserver = observe( 157 | \.timeControlStatus, 158 | options: [.new, .old], 159 | changeHandler: { [weak self] player, change in 160 | self?.onTimeControlStatus(player: player, change: change) 161 | } 162 | ) 163 | } 164 | 165 | private func removePlayerObserver() { 166 | currentItemObserver?.invalidate() 167 | currentItemObserver = nil 168 | statusObserver?.invalidate() 169 | statusObserver = nil 170 | timeControlStatusObserver?.invalidate() 171 | timeControlStatusObserver = nil 172 | } 173 | 174 | private func addTimeObserver() { 175 | if timeObserver != nil { 176 | return 177 | } 178 | timeObserver = addPeriodicTimeObserver( 179 | forInterval: CMTimeMake(value: 250, timescale: 1_000), 180 | queue: nil, 181 | using: { _ in 182 | self.addEvent(.timeUpdate) 183 | } 184 | ) 185 | } 186 | 187 | private func removeTimeObserver() { 188 | if let timeObserver = timeObserver { 189 | removeTimeObserver(timeObserver) 190 | } 191 | timeObserver = nil 192 | } 193 | 194 | private func addObservers() { 195 | addPlayerObserver() 196 | if let playerItem = currentItem { 197 | addPlayerItemObserver(playerItem) 198 | } 199 | addTimeObserver() 200 | } 201 | 202 | private func removeObservers() { 203 | removeTimeObserver() 204 | removePlayerItemObserver() 205 | removePlayerObserver() 206 | } 207 | 208 | private func onCurrentItem(_ change: NSKeyValueObservedChange) { 209 | if let oldPlayerItem = change.oldValue, 210 | let oldPlayerItem 211 | { 212 | removePlayerItemObserver(oldPlayerItem) 213 | } 214 | if let newPlayerItem = change.newValue, 215 | let newPlayerItem 216 | { 217 | guard let asset = newPlayerItem.asset as? AVURLAsset else { 218 | agent.disable() 219 | return 220 | } 221 | guard let mediaId = Utils.parseMediaUrl(asset.url) else { 222 | agent.disable() 223 | return 224 | } 225 | 226 | agent.setMediaId(mediaId) 227 | addEvent(.src) 228 | addPlayerItemObserver(newPlayerItem) 229 | } else { 230 | agent.disable() 231 | } 232 | } 233 | 234 | private func onStatus(player: AVPlayer, change _: NSKeyValueObservedChange) { 235 | switch player.status { 236 | case .failed: 237 | addEvent(.error) 238 | 239 | case .readyToPlay: 240 | addEvent(.loaded) 241 | 242 | default: 243 | break 244 | } 245 | } 246 | 247 | private func onTimeControlStatus(player: AVPlayer, change _: NSKeyValueObservedChange) { 248 | let timeControlStatus = player.timeControlStatus 249 | if timeControlStatus == AVPlayer.TimeControlStatus.playing { 250 | addEvent(.play) 251 | addTimeObserver() 252 | } else if timeControlStatus == AVPlayer.TimeControlStatus.paused { 253 | addEvent(.pause) 254 | removeTimeObserver() 255 | } 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to api.video 2 | 3 | :movie_camera::+1::tada: Thank you for taking the time to contribute and participate in the implementation of a Video First World! :tada::+1::movie_camera: 4 | 5 | The following is a set of guidelines for contributing to api.video and its packages, which are hosted in the [api.video Organization](https://github.com/apivideo) on GitHub. 6 | 7 | #### Table of contents 8 | 9 | - [Contributing to api.video](#contributing-to-apivideo) 10 | - [Table of contents](#table-of-contents) 11 | - [Code of conduct](#code-of-conduct) 12 | - [I just have a question!](#i-just-have-a-question) 13 | - [How can I contribute?](#how-can-i-contribute) 14 | - [Reporting bugs](#reporting-bugs) 15 | - [Before submitting a bug report](#before-submitting-a-bug-report) 16 | - [How do I submit a (good) bug report?](#how-do-i-submit-a-good-bug-report) 17 | - [Suggesting enhancements](#suggesting-enhancements) 18 | - [How do I submit a (good) enhancement suggestion?](#how-do-i-submit-a-good-enhancement-suggestion) 19 | - [Pull requests](#pull-requests) 20 | - [Style guides](#style-guides) 21 | - [Git commit messages](#git-commit-messages) 22 | - [Documentation style guide](#documentation-style-guide) 23 | - [Additional notes](#additional-notes) 24 | - [Issue and pull request labels](#issue-and-pull-request-labels) 25 | - [Type of issue and issue state](#type-of-issue-and-issue-state) 26 | - [Topic categories](#topic-categories) 27 | - [Pull request labels](#pull-request-labels) 28 | 29 | ## Code of conduct 30 | 31 | This project and everyone participating in it is governed by the [api.video Code of Conduct](https://github.com/apivideo/.github/blob/main/CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [help@api.video](mailto:help@api.video). 32 | 33 | ## I just have a question! 34 | 35 | > **Note:** [Please don't file an issue to ask a question.] You'll get faster results by using the resources below. 36 | 37 | We have an official message board with a detailed FAQ and where the community chimes in with helpful advice if you have questions. 38 | 39 | * [The official api.video's Community](https://community.api.video/) 40 | * [api.video FAQ](https://community.api.video/c/faq/) 41 | 42 | 43 | ## How can I contribute? 44 | 45 | ### Reporting bugs 46 | 47 | This section guides you through submitting a bug report for api.video. Following these guidelines helps maintainers and the community understand your report :pencil:, reproduce the behavior :computer:, and find related reports :mag_right:. 48 | 49 | Before creating bug reports, please check [this list](#before-submitting-a-bug-report) as you might find out that you don't need to create one. When you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report). Fill out [the required template](https://github.com/apivideo/.github/blob/main/.github/ISSUE_TEMPLATE/bug_report.yml), the information it asks for helps us resolve issues faster. 50 | 51 | > **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one. 52 | 53 | #### Before submitting a bug report 54 | 55 | * **Check the [The official api.video's Community](https://community.api.video/)** for a list of common questions and problems. 56 | * **Determine which repository the problem should be reported in**. 57 | * **Perform a [cursory search](https://github.com/search?q=is%3Aissue+user%3Aapivideo)** to see if the problem has already been reported. If it has **and the issue is still open**, add a comment to the existing issue instead of opening a new one. 58 | 59 | #### How do I submit a (good) bug report? 60 | 61 | Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/). After you've determined which repository your bug is related to, create an issue on that repository and provide the following information by filling in [the template](https://github.com/apivideo/.github/blob/main/.github/ISSUE_TEMPLATE/bug_report.yml). 62 | 63 | Explain the problem and include additional details to help maintainers reproduce the problem: 64 | 65 | * **Use a clear and descriptive title** for the issue to identify the problem. 66 | * **Describe the exact steps which reproduce the problem** in as many details as possible. When listing steps, **don't just say what you did, but explain how you did it**. 67 | * **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). 68 | * **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. 69 | * **Explain which behavior you expected to see instead and why.** 70 | * **Include screenshots or videos** which show you following the described steps and clearly demonstrate the problem. 71 | * **If the problem wasn't triggered by a specific action**, describe what you were doing before the problem happened and share more information using the guidelines below. 72 | 73 | Provide more context by answering these questions: 74 | 75 | * **Did the problem start happening recently** (e.g. after updating to a new version of api.video) or was this always a problem? 76 | * If the problem started happening recently.** 77 | * **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens. 78 | 79 | Include details about your configuration and environment: 80 | 81 | * **Which version of the api.video package are you using?** 82 | * **What's the name and version of the OS you're using?** 83 | 84 | ### Suggesting enhancements 85 | 86 | This section guides you through submitting an enhancement suggestion for api.video project, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion :pencil: and find related suggestions :mag_right:. 87 | 88 | When you are creating an enhancement suggestion, please [include as many details as possible](#how-do-i-submit-a-good-enhancement-suggestion). Fill in [the template](https://github.com/apivideo/.github/blob/main/.github/ISSUE_TEMPLATE/feature_request.yml), including the steps that you imagine you would take if the feature you're requesting existed. 89 | 90 | 91 | #### How do I submit a (good) enhancement suggestion? 92 | 93 | Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/). After you've determined which repository your enhancement suggestion is related to, create an issue on that repository and provide the following information: 94 | 95 | * **Use a clear and descriptive title** for the issue to identify the suggestion. 96 | * **Provide a step-by-step description of the suggested enhancement** in as many details as possible. 97 | * **Provide specific examples to demonstrate the steps**. Include copy/pasteable snippets which you use in those examples, as [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). 98 | * **Describe the current behavior** and **explain which behavior you expected to see instead** and why. 99 | * **Include screenshots** which help you demonstrate the steps or point out the part of api.video which the suggestion is related to. 100 | * **Explain why this enhancement would be useful** to most api.video users and isn't something that can or should be implemented as a community package. 101 | * **Specify which version of the api.video package you're using.** 102 | * **Specify the name and version of the OS you're using.** 103 | 104 | 105 | ### Pull requests 106 | 107 | The process described here has several goals: 108 | 109 | - Maintain api.video's quality 110 | - Fix problems that are important to users 111 | - Engage the community in working toward the best possible api.video 112 | - Enable a sustainable system for api.video's maintainers to review contributions 113 | 114 | Please follow these steps to have your contribution considered by the maintainers: 115 | 116 | 1. Explain what, why and how you resolved the issue. If you have a related issue, please mention it. 117 | 2. Follow the [style guides](#style-guides) 118 | 3. After you submit your pull request, verify that all [status checks](https://help.github.com/articles/about-status-checks/) are passing
What if the status checks are failing?If a status check is failing, and you believe that the failure is unrelated to your change, please leave a comment on the pull request explaining why you believe the failure is unrelated. A maintainer will re-run the status check for you. If we conclude that the failure was a false positive, then we will open an issue to track that problem with our status check suite.
119 | 120 | While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted. 121 | 122 | ## Style guides 123 | 124 | ### Git commit messages 125 | 126 | * Use the present tense ("Add feature" not "Added feature") 127 | * Limit the first line to 72 characters or less 128 | * Reference issues and pull requests after the first line 129 | * Consider starting the commit message with an applicable emoji: 130 | * :art: `:art:` when improving the format/structure of the code 131 | * :racehorse: `:racehorse:` when improving performance 132 | * :non-potable_water: `:non-potable_water:` when plugging memory leaks 133 | * :memo: `:memo:` when writing docs 134 | * :penguin: `:penguin:` when fixing something on Linux 135 | * :apple: `:apple:` when fixing something on macOS 136 | * :checkered_flag: `:checkered_flag:` when fixing something on Windows 137 | * :bug: `:bug:` when fixing a bug 138 | * :fire: `:fire:` when removing code or files 139 | * :green_heart: `:green_heart:` when fixing the CI build 140 | * :white_check_mark: `:white_check_mark:` when adding tests 141 | * :lock: `:lock:` when dealing with security 142 | * :arrow_up: `:arrow_up:` when upgrading dependencies 143 | * :arrow_down: `:arrow_down:` when downgrading dependencies 144 | * :shirt: `:shirt:` when removing linter warnings 145 | 146 | ### Documentation style guide 147 | 148 | * Use [Markdown](https://daringfireball.net/projects/markdown). 149 | 150 | 151 | ## Additional notes 152 | 153 | ### Issue and pull request labels 154 | 155 | This section lists the labels we use to help us track and manage issues and pull requests on all api.video repositories. 156 | 157 | [GitHub search](https://help.github.com/articles/searching-issues/) makes it easy to use labels for finding groups of issues or pull requests you're interested in. We encourage you to read about [other search filters](https://help.github.com/articles/searching-issues/) which will help you write more focused queries. 158 | 159 | 160 | #### Type of issue and issue state 161 | 162 | | Label name | `apivideo` :mag_right: | Description | 163 | | --- | --- | --- | 164 | | `enhancement` | [search][search-apivideo-org-label-enhancement] | Feature requests. | 165 | | `bug` | [search][search-apivideo-org-label-bug] | Confirmed bugs or reports that are very likely to be bugs. | 166 | | `question` | [search][search-apivideo-org-label-question] | Questions more than bug reports or feature requests (e.g. how do I do X). | 167 | | `feedback` | [search][search-apivideo-org-label-feedback] | General feedback more than bug reports or feature requests. | 168 | | `help-wanted` | [search][search-apivideo-org-label-help-wanted] | The api.video team would appreciate help from the community in resolving these issues. | 169 | | `more-information-needed` | [search][search-apivideo-org-label-more-information-needed] | More information needs to be collected about these problems or feature requests (e.g. steps to reproduce). | 170 | | `needs-reproduction` | [search][search-apivideo-org-label-needs-reproduction] | Likely bugs, but haven't been reliably reproduced. | 171 | | `blocked` | [search][search-apivideo-org-label-blocked] | Issues blocked on other issues. | 172 | | `duplicate` | [search][search-apivideo-org-label-duplicate] | Issues which are duplicates of other issues, i.e. they have been reported before. | 173 | | `wontfix` | [search][search-apivideo-org-label-wontfix] | The api.video team has decided not to fix these issues for now, either because they're working as intended or for some other reason. | 174 | | `invalid` | [search][search-apivideo-org-label-invalid] | Issues which aren't valid (e.g. user errors). | 175 | | `package-idea` | [search][search-apivideo-org-label-package-idea] | Feature request which might be good candidates for new packages, instead of extending api.video packages. | 176 | | `wrong-repo` | [search][search-apivideo-org-label-wrong-repo] | Issues reported on the wrong repository. | 177 | 178 | #### Topic categories 179 | 180 | | Label name | `apivideo` :mag_right: | Description | 181 | | --- | --- | --- | 182 | | `windows` | [search][search-apivideo-org-label-windows] | Related to api.video running on Windows. | 183 | | `linux` | [search][search-apivideo-org-label-linux] | Related to api.video running on Linux. | 184 | | `mac` | [search][search-apivideo-org-label-mac] | Related to api.video running on macOS. | 185 | | `documentation` | [search][search-apivideo-org-label-documentation] | Related to any type of documentation. | 186 | | `performance` | [search][search-apivideo-org-label-performance] | Related to performance. | 187 | | `security` | [search][search-apivideo-org-label-security] | Related to security. | 188 | | `ui` | [search][search-apivideo-org-label-ui] | Related to visual design. | 189 | | `api` | [search][search-apivideo-org-label-api] | Related to api.video's public APIs. | 190 | 191 | #### Pull request labels 192 | 193 | | Label name | `apivideo` :mag_right: | Description 194 | | --- | --- | --- | 195 | | `work-in-progress` | [search][search-apivideo-org-label-work-in-progress] | Pull requests which are still being worked on, more changes will follow. | 196 | | `needs-review` | [search][search-apivideo-org-label-needs-review] | Pull requests which need code review, and approval from maintainers or api.video team. | 197 | | `under-review` | [search][search-apivideo-org-label-under-review] | Pull requests being reviewed by maintainers or api.video team. | 198 | | `requires-changes` | [search][search-apivideo-org-label-requires-changes] | Pull requests which need to be updated based on review comments and then reviewed again. | 199 | | `needs-testing` | [search][search-apivideo-org-label-needs-testing] | Pull requests which need manual testing. | 200 | 201 | [search-apivideo-org-label-enhancement]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Aenhancement 202 | [search-apivideo-org-label-bug]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Abug 203 | [search-apivideo-org-label-question]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Aquestion 204 | [search-apivideo-org-label-feedback]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Afeedback 205 | [search-apivideo-org-label-help-wanted]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Ahelp-wanted 206 | [search-apivideo-org-label-more-information-needed]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Amore-information-needed 207 | [search-apivideo-org-label-needs-reproduction]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Aneeds-reproduction 208 | [search-apivideo-org-label-windows]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Awindows 209 | [search-apivideo-org-label-linux]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Alinux 210 | [search-apivideo-org-label-mac]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Amac 211 | [search-apivideo-org-label-documentation]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Adocumentation 212 | [search-apivideo-org-label-performance]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Aperformance 213 | [search-apivideo-org-label-security]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Asecurity 214 | [search-apivideo-org-label-ui]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Aui 215 | [search-apivideo-org-label-api]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Aapi 216 | [search-apivideo-org-label-blocked]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Ablocked 217 | [search-apivideo-org-label-duplicate]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Aduplicate 218 | [search-apivideo-org-label-wontfix]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Awontfix 219 | [search-apivideo-org-label-invalid]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Ainvalid 220 | [search-apivideo-org-label-package-idea]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Apackage-idea 221 | [search-apivideo-org-label-wrong-repo]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Awrong-repo 222 | [search-apivideo-org-label-work-in-progress]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3Aapivideo%2Fapivideo+label%3Awork-in-progress 223 | [search-apivideo-org-label-needs-review]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3Aapivideo%2Fapivideo+label%3Aneeds-review 224 | [search-apivideo-org-label-under-review]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3Aapivideo%2Fapivideo+label%3Aunder-review 225 | [search-apivideo-org-label-requires-changes]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3Aapivideo%2Fapivideo+label%3Arequires-changes 226 | [search-apivideo-org-label-needs-testing]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3Aapivideo%2Fapivideo+label%3Aneeds-testing 227 | 228 | [help-wanted]:https://github.com/search?q=is%3Aopen+is%3Aissue+label%3Ahelp-wanted+user%3Aapivideo+sort%3Acomments-desc 229 | -------------------------------------------------------------------------------- /ApiVideoPlayerAnalytics.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2102CD142BA9E0E700D0EBAD /* ApiVideoPlayerAnalytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2102CD092BA9E0E700D0EBAD /* ApiVideoPlayerAnalytics.framework */; }; 11 | 2117CBD42BB1EBD900B85BF0 /* PlayerAnalytics.docc in Sources */ = {isa = PBXBuildFile; fileRef = 2117CBD22BB1EBD900B85BF0 /* PlayerAnalytics.docc */; }; 12 | 2117CBFA2BB1EE6300B85BF0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2117CBF02BB1EE6300B85BF0 /* AppDelegate.swift */; }; 13 | 2117CBFB2BB1EE6300B85BF0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2117CBF12BB1EE6300B85BF0 /* Assets.xcassets */; }; 14 | 2117CBFD2BB1EE6300B85BF0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2117CBF42BB1EE6300B85BF0 /* LaunchScreen.storyboard */; }; 15 | 2117CBFE2BB1EE6300B85BF0 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2117CBF62BB1EE6300B85BF0 /* Main.storyboard */; }; 16 | 2117CBFF2BB1EE6300B85BF0 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2117CBF72BB1EE6300B85BF0 /* SceneDelegate.swift */; }; 17 | 2117CC002BB1EE6300B85BF0 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2117CBF82BB1EE6300B85BF0 /* ViewController.swift */; }; 18 | 2117CC152BB2E2E500B85BF0 /* FixedSizedArrayWithUpsertTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2117CC132BB2E23A00B85BF0 /* FixedSizedArrayWithUpsertTest.swift */; }; 19 | 2117CC182BB2E3BB00B85BF0 /* EventTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2117CC172BB2E3BB00B85BF0 /* EventTest.swift */; }; 20 | 21395B872C4EA4DF0098E236 /* Batch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21395B752C4EA4DF0098E236 /* Batch.swift */; }; 21 | 21395B882C4EA4DF0098E236 /* ErrorCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21395B762C4EA4DF0098E236 /* ErrorCode.swift */; }; 22 | 21395B892C4EA4DF0098E236 /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21395B772C4EA4DF0098E236 /* Event.swift */; }; 23 | 21395B8A2C4EA4DF0098E236 /* HttpClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21395B792C4EA4DF0098E236 /* HttpClient.swift */; }; 24 | 21395B8B2C4EA4DF0098E236 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21395B7A2C4EA4DF0098E236 /* Request.swift */; }; 25 | 21395B8C2C4EA4DF0098E236 /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21395B7B2C4EA4DF0098E236 /* Response.swift */; }; 26 | 21395B8D2C4EA4DF0098E236 /* FixedSizedArrayWithUpsert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21395B7D2C4EA4DF0098E236 /* FixedSizedArrayWithUpsert.swift */; }; 27 | 21395B8E2C4EA4DF0098E236 /* PlayerAnalyticsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21395B7E2C4EA4DF0098E236 /* PlayerAnalyticsError.swift */; }; 28 | 21395B8F2C4EA4DF0098E236 /* TimeUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21395B7F2C4EA4DF0098E236 /* TimeUtils.swift */; }; 29 | 21395B902C4EA4DF0098E236 /* ApiVideoPlayerAnalyticsAgent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21395B812C4EA4DF0098E236 /* ApiVideoPlayerAnalyticsAgent.swift */; }; 30 | 21395B912C4EA4DF0098E236 /* ApiVideoDatabusAVPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21395B832C4EA4DF0098E236 /* ApiVideoDatabusAVPlayer.swift */; }; 31 | 21395B922C4EA4DF0098E236 /* AVErrorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21395B842C4EA4DF0098E236 /* AVErrorExtension.swift */; }; 32 | 21395B932C4EA4DF0098E236 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21395B852C4EA4DF0098E236 /* Utils.swift */; }; 33 | 21CCD6A02BBC44EE00E58F5D /* ApiVideoPlayerAnalytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2102CD092BA9E0E700D0EBAD /* ApiVideoPlayerAnalytics.framework */; }; 34 | 21CCD6A12BBC44EE00E58F5D /* ApiVideoPlayerAnalytics.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 2102CD092BA9E0E700D0EBAD /* ApiVideoPlayerAnalytics.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 35 | 21CCD6AC2BBC4FF600E58F5D /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21CCD6AA2BBC4EB400E58F5D /* Utils.swift */; }; 36 | /* End PBXBuildFile section */ 37 | 38 | /* Begin PBXBuildRule section */ 39 | 21CCD6A92BBC4A9800E58F5D /* PBXBuildRule */ = { 40 | isa = PBXBuildRule; 41 | compilerSpec = com.apple.compilers.proxy.script; 42 | fileType = text.plist; 43 | inputFiles = ( 44 | ); 45 | isEditable = 1; 46 | outputFiles = ( 47 | ); 48 | script = "# builtin-copyPlist\n"; 49 | }; 50 | /* End PBXBuildRule section */ 51 | 52 | /* Begin PBXContainerItemProxy section */ 53 | 2102CD152BA9E0E700D0EBAD /* PBXContainerItemProxy */ = { 54 | isa = PBXContainerItemProxy; 55 | containerPortal = 2102CD002BA9E0E600D0EBAD /* Project object */; 56 | proxyType = 1; 57 | remoteGlobalIDString = 2102CD082BA9E0E700D0EBAD; 58 | remoteInfo = Databus; 59 | }; 60 | 21CCD6A22BBC44EF00E58F5D /* PBXContainerItemProxy */ = { 61 | isa = PBXContainerItemProxy; 62 | containerPortal = 2102CD002BA9E0E600D0EBAD /* Project object */; 63 | proxyType = 1; 64 | remoteGlobalIDString = 2102CD082BA9E0E700D0EBAD; 65 | remoteInfo = ApiVideoDatabus; 66 | }; 67 | /* End PBXContainerItemProxy section */ 68 | 69 | /* Begin PBXCopyFilesBuildPhase section */ 70 | 21CCD6A42BBC44EF00E58F5D /* Embed Frameworks */ = { 71 | isa = PBXCopyFilesBuildPhase; 72 | buildActionMask = 2147483647; 73 | dstPath = ""; 74 | dstSubfolderSpec = 10; 75 | files = ( 76 | 21CCD6A12BBC44EE00E58F5D /* ApiVideoPlayerAnalytics.framework in Embed Frameworks */, 77 | ); 78 | name = "Embed Frameworks"; 79 | runOnlyForDeploymentPostprocessing = 0; 80 | }; 81 | /* End PBXCopyFilesBuildPhase section */ 82 | 83 | /* Begin PBXFileReference section */ 84 | 2102CD092BA9E0E700D0EBAD /* ApiVideoPlayerAnalytics.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ApiVideoPlayerAnalytics.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 85 | 2102CD132BA9E0E700D0EBAD /* ApiVideoPlayerAnalyticsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ApiVideoPlayerAnalyticsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 86 | 2117CBCE2BB1EBCA00B85BF0 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; 87 | 2117CBD02BB1EBCA00B85BF0 /* CONTRIBUTING.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = ""; }; 88 | 2117CBD12BB1EBCA00B85BF0 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 89 | 2117CBD22BB1EBD900B85BF0 /* PlayerAnalytics.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = PlayerAnalytics.docc; sourceTree = ""; }; 90 | 2117CBDC2BB1EE3100B85BF0 /* Example iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Example iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 91 | 2117CBF02BB1EE6300B85BF0 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 92 | 2117CBF12BB1EE6300B85BF0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 93 | 2117CBF22BB1EE6300B85BF0 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 94 | 2117CBF32BB1EE6300B85BF0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 95 | 2117CBF52BB1EE6300B85BF0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 96 | 2117CBF72BB1EE6300B85BF0 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 97 | 2117CBF82BB1EE6300B85BF0 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 98 | 2117CC072BB1F1F900B85BF0 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 99 | 2117CC132BB2E23A00B85BF0 /* FixedSizedArrayWithUpsertTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FixedSizedArrayWithUpsertTest.swift; sourceTree = ""; }; 100 | 2117CC172BB2E3BB00B85BF0 /* EventTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTest.swift; sourceTree = ""; }; 101 | 2126C5132BCD47DA006BEDF3 /* LICENSE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = ""; }; 102 | 2126C5142BCD4D6E006BEDF3 /* build.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = build.yml; sourceTree = ""; }; 103 | 2126C5152BCD4D6E006BEDF3 /* create-documentation-pr.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = "create-documentation-pr.yml"; sourceTree = ""; }; 104 | 2126C5162BCD4D6E006BEDF3 /* create-release-from-changelog.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = "create-release-from-changelog.yml"; sourceTree = ""; }; 105 | 2126C5172BCD4D6E006BEDF3 /* release.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = release.yml; sourceTree = ""; }; 106 | 2126C5182BCD4D6E006BEDF3 /* test.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = test.yml; sourceTree = ""; }; 107 | 2126C5192BCD4D6E006BEDF3 /* update-documentation.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = "update-documentation.yml"; sourceTree = ""; }; 108 | 21395B742C4E4D210098E236 /* ApiVideoPlayerAnalytics.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = ApiVideoPlayerAnalytics.podspec; sourceTree = ""; }; 109 | 21395B752C4EA4DF0098E236 /* Batch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Batch.swift; sourceTree = ""; }; 110 | 21395B762C4EA4DF0098E236 /* ErrorCode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorCode.swift; sourceTree = ""; }; 111 | 21395B772C4EA4DF0098E236 /* Event.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = ""; }; 112 | 21395B792C4EA4DF0098E236 /* HttpClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpClient.swift; sourceTree = ""; }; 113 | 21395B7A2C4EA4DF0098E236 /* Request.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; 114 | 21395B7B2C4EA4DF0098E236 /* Response.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = ""; }; 115 | 21395B7D2C4EA4DF0098E236 /* FixedSizedArrayWithUpsert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FixedSizedArrayWithUpsert.swift; sourceTree = ""; }; 116 | 21395B7E2C4EA4DF0098E236 /* PlayerAnalyticsError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerAnalyticsError.swift; sourceTree = ""; }; 117 | 21395B7F2C4EA4DF0098E236 /* TimeUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimeUtils.swift; sourceTree = ""; }; 118 | 21395B812C4EA4DF0098E236 /* ApiVideoPlayerAnalyticsAgent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiVideoPlayerAnalyticsAgent.swift; sourceTree = ""; }; 119 | 21395B832C4EA4DF0098E236 /* ApiVideoDatabusAVPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiVideoDatabusAVPlayer.swift; sourceTree = ""; }; 120 | 21395B842C4EA4DF0098E236 /* AVErrorExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AVErrorExtension.swift; sourceTree = ""; }; 121 | 21395B852C4EA4DF0098E236 /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; 122 | 21CCD6AA2BBC4EB400E58F5D /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; 123 | /* End PBXFileReference section */ 124 | 125 | /* Begin PBXFrameworksBuildPhase section */ 126 | 2102CD062BA9E0E700D0EBAD /* Frameworks */ = { 127 | isa = PBXFrameworksBuildPhase; 128 | buildActionMask = 2147483647; 129 | files = ( 130 | ); 131 | runOnlyForDeploymentPostprocessing = 0; 132 | }; 133 | 2102CD102BA9E0E700D0EBAD /* Frameworks */ = { 134 | isa = PBXFrameworksBuildPhase; 135 | buildActionMask = 2147483647; 136 | files = ( 137 | 2102CD142BA9E0E700D0EBAD /* ApiVideoPlayerAnalytics.framework in Frameworks */, 138 | ); 139 | runOnlyForDeploymentPostprocessing = 0; 140 | }; 141 | 2117CBD92BB1EE3100B85BF0 /* Frameworks */ = { 142 | isa = PBXFrameworksBuildPhase; 143 | buildActionMask = 2147483647; 144 | files = ( 145 | 21CCD6A02BBC44EE00E58F5D /* ApiVideoPlayerAnalytics.framework in Frameworks */, 146 | ); 147 | runOnlyForDeploymentPostprocessing = 0; 148 | }; 149 | /* End PBXFrameworksBuildPhase section */ 150 | 151 | /* Begin PBXGroup section */ 152 | 2102CCFF2BA9E0E600D0EBAD = { 153 | isa = PBXGroup; 154 | children = ( 155 | 2126C51B2BCD4D6E006BEDF3 /* .github */, 156 | 2117CC012BB1EE7B00B85BF0 /* Examples */, 157 | 2117CBD62BB1EBE500B85BF0 /* Tests */, 158 | 2117CBD32BB1EBD900B85BF0 /* Sources */, 159 | 2117CBCE2BB1EBCA00B85BF0 /* CHANGELOG.md */, 160 | 2117CBD02BB1EBCA00B85BF0 /* CONTRIBUTING.md */, 161 | 2126C5132BCD47DA006BEDF3 /* LICENSE.md */, 162 | 2117CBD12BB1EBCA00B85BF0 /* README.md */, 163 | 2117CC072BB1F1F900B85BF0 /* Package.swift */, 164 | 21395B742C4E4D210098E236 /* ApiVideoPlayerAnalytics.podspec */, 165 | 2102CD0A2BA9E0E700D0EBAD /* Products */, 166 | 21CCD69F2BBC44EE00E58F5D /* Frameworks */, 167 | ); 168 | sourceTree = ""; 169 | }; 170 | 2102CD0A2BA9E0E700D0EBAD /* Products */ = { 171 | isa = PBXGroup; 172 | children = ( 173 | 2102CD092BA9E0E700D0EBAD /* ApiVideoPlayerAnalytics.framework */, 174 | 2102CD132BA9E0E700D0EBAD /* ApiVideoPlayerAnalyticsTests.xctest */, 175 | 2117CBDC2BB1EE3100B85BF0 /* Example iOS.app */, 176 | ); 177 | name = Products; 178 | sourceTree = ""; 179 | }; 180 | 2117CBD32BB1EBD900B85BF0 /* Sources */ = { 181 | isa = PBXGroup; 182 | children = ( 183 | 21395B862C4EA4DF0098E236 /* AVPlayerAnalytics */, 184 | 21395B822C4EA4DF0098E236 /* PlayerAnalytics */, 185 | 2117CBD22BB1EBD900B85BF0 /* PlayerAnalytics.docc */, 186 | ); 187 | path = Sources; 188 | sourceTree = ""; 189 | }; 190 | 2117CBD62BB1EBE500B85BF0 /* Tests */ = { 191 | isa = PBXGroup; 192 | children = ( 193 | 2117CC112BB2E1ED00B85BF0 /* Databus */, 194 | ); 195 | path = Tests; 196 | sourceTree = ""; 197 | }; 198 | 2117CBF92BB1EE6300B85BF0 /* iOS */ = { 199 | isa = PBXGroup; 200 | children = ( 201 | 2117CBF02BB1EE6300B85BF0 /* AppDelegate.swift */, 202 | 2117CBF12BB1EE6300B85BF0 /* Assets.xcassets */, 203 | 2117CBF22BB1EE6300B85BF0 /* Info.plist */, 204 | 2117CBF42BB1EE6300B85BF0 /* LaunchScreen.storyboard */, 205 | 2117CBF62BB1EE6300B85BF0 /* Main.storyboard */, 206 | 2117CBF72BB1EE6300B85BF0 /* SceneDelegate.swift */, 207 | 2117CBF82BB1EE6300B85BF0 /* ViewController.swift */, 208 | 21CCD6AA2BBC4EB400E58F5D /* Utils.swift */, 209 | ); 210 | name = iOS; 211 | path = ..; 212 | sourceTree = ""; 213 | }; 214 | 2117CC012BB1EE7B00B85BF0 /* Examples */ = { 215 | isa = PBXGroup; 216 | children = ( 217 | 2117CBF92BB1EE6300B85BF0 /* iOS */, 218 | ); 219 | name = Examples; 220 | path = Examples/iOS/Examples; 221 | sourceTree = ""; 222 | }; 223 | 2117CC112BB2E1ED00B85BF0 /* Databus */ = { 224 | isa = PBXGroup; 225 | children = ( 226 | 2117CC162BB2E3A300B85BF0 /* Payload */, 227 | 2117CC122BB2E20F00B85BF0 /* Utils */, 228 | ); 229 | path = Databus; 230 | sourceTree = ""; 231 | }; 232 | 2117CC122BB2E20F00B85BF0 /* Utils */ = { 233 | isa = PBXGroup; 234 | children = ( 235 | 2117CC132BB2E23A00B85BF0 /* FixedSizedArrayWithUpsertTest.swift */, 236 | ); 237 | path = Utils; 238 | sourceTree = ""; 239 | }; 240 | 2117CC162BB2E3A300B85BF0 /* Payload */ = { 241 | isa = PBXGroup; 242 | children = ( 243 | 2117CC172BB2E3BB00B85BF0 /* EventTest.swift */, 244 | ); 245 | path = Payload; 246 | sourceTree = ""; 247 | }; 248 | 2126C51A2BCD4D6E006BEDF3 /* workflows */ = { 249 | isa = PBXGroup; 250 | children = ( 251 | 2126C5142BCD4D6E006BEDF3 /* build.yml */, 252 | 2126C5152BCD4D6E006BEDF3 /* create-documentation-pr.yml */, 253 | 2126C5162BCD4D6E006BEDF3 /* create-release-from-changelog.yml */, 254 | 2126C5172BCD4D6E006BEDF3 /* release.yml */, 255 | 2126C5182BCD4D6E006BEDF3 /* test.yml */, 256 | 2126C5192BCD4D6E006BEDF3 /* update-documentation.yml */, 257 | ); 258 | path = workflows; 259 | sourceTree = ""; 260 | }; 261 | 2126C51B2BCD4D6E006BEDF3 /* .github */ = { 262 | isa = PBXGroup; 263 | children = ( 264 | 2126C51A2BCD4D6E006BEDF3 /* workflows */, 265 | ); 266 | path = .github; 267 | sourceTree = ""; 268 | }; 269 | 21395B782C4EA4DF0098E236 /* Payload */ = { 270 | isa = PBXGroup; 271 | children = ( 272 | 21395B752C4EA4DF0098E236 /* Batch.swift */, 273 | 21395B762C4EA4DF0098E236 /* ErrorCode.swift */, 274 | 21395B772C4EA4DF0098E236 /* Event.swift */, 275 | ); 276 | path = Payload; 277 | sourceTree = ""; 278 | }; 279 | 21395B7C2C4EA4DF0098E236 /* HTTP */ = { 280 | isa = PBXGroup; 281 | children = ( 282 | 21395B792C4EA4DF0098E236 /* HttpClient.swift */, 283 | 21395B7A2C4EA4DF0098E236 /* Request.swift */, 284 | 21395B7B2C4EA4DF0098E236 /* Response.swift */, 285 | ); 286 | path = HTTP; 287 | sourceTree = ""; 288 | }; 289 | 21395B802C4EA4DF0098E236 /* Utils */ = { 290 | isa = PBXGroup; 291 | children = ( 292 | 21395B7C2C4EA4DF0098E236 /* HTTP */, 293 | 21395B7D2C4EA4DF0098E236 /* FixedSizedArrayWithUpsert.swift */, 294 | 21395B7E2C4EA4DF0098E236 /* PlayerAnalyticsError.swift */, 295 | 21395B7F2C4EA4DF0098E236 /* TimeUtils.swift */, 296 | ); 297 | path = Utils; 298 | sourceTree = ""; 299 | }; 300 | 21395B822C4EA4DF0098E236 /* PlayerAnalytics */ = { 301 | isa = PBXGroup; 302 | children = ( 303 | 21395B782C4EA4DF0098E236 /* Payload */, 304 | 21395B802C4EA4DF0098E236 /* Utils */, 305 | 21395B812C4EA4DF0098E236 /* ApiVideoPlayerAnalyticsAgent.swift */, 306 | ); 307 | path = PlayerAnalytics; 308 | sourceTree = ""; 309 | }; 310 | 21395B862C4EA4DF0098E236 /* AVPlayerAnalytics */ = { 311 | isa = PBXGroup; 312 | children = ( 313 | 21395B832C4EA4DF0098E236 /* ApiVideoDatabusAVPlayer.swift */, 314 | 21395B842C4EA4DF0098E236 /* AVErrorExtension.swift */, 315 | 21395B852C4EA4DF0098E236 /* Utils.swift */, 316 | ); 317 | path = AVPlayerAnalytics; 318 | sourceTree = ""; 319 | }; 320 | 21CCD69F2BBC44EE00E58F5D /* Frameworks */ = { 321 | isa = PBXGroup; 322 | children = ( 323 | ); 324 | name = Frameworks; 325 | sourceTree = ""; 326 | }; 327 | /* End PBXGroup section */ 328 | 329 | /* Begin PBXHeadersBuildPhase section */ 330 | 2102CD042BA9E0E700D0EBAD /* Headers */ = { 331 | isa = PBXHeadersBuildPhase; 332 | buildActionMask = 2147483647; 333 | files = ( 334 | ); 335 | runOnlyForDeploymentPostprocessing = 0; 336 | }; 337 | /* End PBXHeadersBuildPhase section */ 338 | 339 | /* Begin PBXNativeTarget section */ 340 | 2102CD082BA9E0E700D0EBAD /* ApiVideoPlayerAnalytics */ = { 341 | isa = PBXNativeTarget; 342 | buildConfigurationList = 2102CD1D2BA9E0E700D0EBAD /* Build configuration list for PBXNativeTarget "ApiVideoPlayerAnalytics" */; 343 | buildPhases = ( 344 | 2102CD042BA9E0E700D0EBAD /* Headers */, 345 | 2102CD052BA9E0E700D0EBAD /* Sources */, 346 | 2102CD062BA9E0E700D0EBAD /* Frameworks */, 347 | 2102CD072BA9E0E700D0EBAD /* Resources */, 348 | ); 349 | buildRules = ( 350 | ); 351 | dependencies = ( 352 | ); 353 | name = ApiVideoPlayerAnalytics; 354 | productName = Databus; 355 | productReference = 2102CD092BA9E0E700D0EBAD /* ApiVideoPlayerAnalytics.framework */; 356 | productType = "com.apple.product-type.framework"; 357 | }; 358 | 2102CD122BA9E0E700D0EBAD /* ApiVideoPlayerAnalyticsTests */ = { 359 | isa = PBXNativeTarget; 360 | buildConfigurationList = 2102CD202BA9E0E700D0EBAD /* Build configuration list for PBXNativeTarget "ApiVideoPlayerAnalyticsTests" */; 361 | buildPhases = ( 362 | 2102CD0F2BA9E0E700D0EBAD /* Sources */, 363 | 2102CD102BA9E0E700D0EBAD /* Frameworks */, 364 | 2102CD112BA9E0E700D0EBAD /* Resources */, 365 | ); 366 | buildRules = ( 367 | ); 368 | dependencies = ( 369 | 2102CD162BA9E0E700D0EBAD /* PBXTargetDependency */, 370 | ); 371 | name = ApiVideoPlayerAnalyticsTests; 372 | productName = DatabusTests; 373 | productReference = 2102CD132BA9E0E700D0EBAD /* ApiVideoPlayerAnalyticsTests.xctest */; 374 | productType = "com.apple.product-type.bundle.unit-test"; 375 | }; 376 | 2117CBDB2BB1EE3100B85BF0 /* Example iOS */ = { 377 | isa = PBXNativeTarget; 378 | buildConfigurationList = 2117CBED2BB1EE3300B85BF0 /* Build configuration list for PBXNativeTarget "Example iOS" */; 379 | buildPhases = ( 380 | 2117CBD82BB1EE3100B85BF0 /* Sources */, 381 | 2117CBD92BB1EE3100B85BF0 /* Frameworks */, 382 | 2117CBDA2BB1EE3100B85BF0 /* Resources */, 383 | 21CCD6A42BBC44EF00E58F5D /* Embed Frameworks */, 384 | ); 385 | buildRules = ( 386 | 21CCD6A92BBC4A9800E58F5D /* PBXBuildRule */, 387 | ); 388 | dependencies = ( 389 | 21CCD6A32BBC44EF00E58F5D /* PBXTargetDependency */, 390 | ); 391 | name = "Example iOS"; 392 | productName = iOS; 393 | productReference = 2117CBDC2BB1EE3100B85BF0 /* Example iOS.app */; 394 | productType = "com.apple.product-type.application"; 395 | }; 396 | /* End PBXNativeTarget section */ 397 | 398 | /* Begin PBXProject section */ 399 | 2102CD002BA9E0E600D0EBAD /* Project object */ = { 400 | isa = PBXProject; 401 | attributes = { 402 | BuildIndependentTargetsInParallel = 1; 403 | LastSwiftUpdateCheck = 1530; 404 | LastUpgradeCheck = 1530; 405 | TargetAttributes = { 406 | 2102CD082BA9E0E700D0EBAD = { 407 | CreatedOnToolsVersion = 15.2; 408 | }; 409 | 2102CD122BA9E0E700D0EBAD = { 410 | CreatedOnToolsVersion = 15.2; 411 | }; 412 | 2117CBDB2BB1EE3100B85BF0 = { 413 | CreatedOnToolsVersion = 15.3; 414 | }; 415 | }; 416 | }; 417 | buildConfigurationList = 2102CD032BA9E0E600D0EBAD /* Build configuration list for PBXProject "ApiVideoPlayerAnalytics" */; 418 | compatibilityVersion = "Xcode 14.0"; 419 | developmentRegion = en; 420 | hasScannedForEncodings = 0; 421 | knownRegions = ( 422 | en, 423 | Base, 424 | ); 425 | mainGroup = 2102CCFF2BA9E0E600D0EBAD; 426 | productRefGroup = 2102CD0A2BA9E0E700D0EBAD /* Products */; 427 | projectDirPath = ""; 428 | projectRoot = ""; 429 | targets = ( 430 | 2102CD082BA9E0E700D0EBAD /* ApiVideoPlayerAnalytics */, 431 | 2102CD122BA9E0E700D0EBAD /* ApiVideoPlayerAnalyticsTests */, 432 | 2117CBDB2BB1EE3100B85BF0 /* Example iOS */, 433 | ); 434 | }; 435 | /* End PBXProject section */ 436 | 437 | /* Begin PBXResourcesBuildPhase section */ 438 | 2102CD072BA9E0E700D0EBAD /* Resources */ = { 439 | isa = PBXResourcesBuildPhase; 440 | buildActionMask = 2147483647; 441 | files = ( 442 | ); 443 | runOnlyForDeploymentPostprocessing = 0; 444 | }; 445 | 2102CD112BA9E0E700D0EBAD /* Resources */ = { 446 | isa = PBXResourcesBuildPhase; 447 | buildActionMask = 2147483647; 448 | files = ( 449 | ); 450 | runOnlyForDeploymentPostprocessing = 0; 451 | }; 452 | 2117CBDA2BB1EE3100B85BF0 /* Resources */ = { 453 | isa = PBXResourcesBuildPhase; 454 | buildActionMask = 2147483647; 455 | files = ( 456 | 2117CBFD2BB1EE6300B85BF0 /* LaunchScreen.storyboard in Resources */, 457 | 2117CBFB2BB1EE6300B85BF0 /* Assets.xcassets in Resources */, 458 | 2117CBFE2BB1EE6300B85BF0 /* Main.storyboard in Resources */, 459 | ); 460 | runOnlyForDeploymentPostprocessing = 0; 461 | }; 462 | /* End PBXResourcesBuildPhase section */ 463 | 464 | /* Begin PBXSourcesBuildPhase section */ 465 | 2102CD052BA9E0E700D0EBAD /* Sources */ = { 466 | isa = PBXSourcesBuildPhase; 467 | buildActionMask = 2147483647; 468 | files = ( 469 | 21395B8A2C4EA4DF0098E236 /* HttpClient.swift in Sources */, 470 | 21395B912C4EA4DF0098E236 /* ApiVideoDatabusAVPlayer.swift in Sources */, 471 | 21395B8B2C4EA4DF0098E236 /* Request.swift in Sources */, 472 | 21395B932C4EA4DF0098E236 /* Utils.swift in Sources */, 473 | 21395B892C4EA4DF0098E236 /* Event.swift in Sources */, 474 | 21395B872C4EA4DF0098E236 /* Batch.swift in Sources */, 475 | 2117CBD42BB1EBD900B85BF0 /* PlayerAnalytics.docc in Sources */, 476 | 21395B882C4EA4DF0098E236 /* ErrorCode.swift in Sources */, 477 | 21395B8F2C4EA4DF0098E236 /* TimeUtils.swift in Sources */, 478 | 21395B8E2C4EA4DF0098E236 /* PlayerAnalyticsError.swift in Sources */, 479 | 21395B922C4EA4DF0098E236 /* AVErrorExtension.swift in Sources */, 480 | 21395B902C4EA4DF0098E236 /* ApiVideoPlayerAnalyticsAgent.swift in Sources */, 481 | 21395B8C2C4EA4DF0098E236 /* Response.swift in Sources */, 482 | 21395B8D2C4EA4DF0098E236 /* FixedSizedArrayWithUpsert.swift in Sources */, 483 | ); 484 | runOnlyForDeploymentPostprocessing = 0; 485 | }; 486 | 2102CD0F2BA9E0E700D0EBAD /* Sources */ = { 487 | isa = PBXSourcesBuildPhase; 488 | buildActionMask = 2147483647; 489 | files = ( 490 | 2117CC182BB2E3BB00B85BF0 /* EventTest.swift in Sources */, 491 | 2117CC152BB2E2E500B85BF0 /* FixedSizedArrayWithUpsertTest.swift in Sources */, 492 | ); 493 | runOnlyForDeploymentPostprocessing = 0; 494 | }; 495 | 2117CBD82BB1EE3100B85BF0 /* Sources */ = { 496 | isa = PBXSourcesBuildPhase; 497 | buildActionMask = 2147483647; 498 | files = ( 499 | 2117CC002BB1EE6300B85BF0 /* ViewController.swift in Sources */, 500 | 2117CBFA2BB1EE6300B85BF0 /* AppDelegate.swift in Sources */, 501 | 2117CBFF2BB1EE6300B85BF0 /* SceneDelegate.swift in Sources */, 502 | 21CCD6AC2BBC4FF600E58F5D /* Utils.swift in Sources */, 503 | ); 504 | runOnlyForDeploymentPostprocessing = 0; 505 | }; 506 | /* End PBXSourcesBuildPhase section */ 507 | 508 | /* Begin PBXTargetDependency section */ 509 | 2102CD162BA9E0E700D0EBAD /* PBXTargetDependency */ = { 510 | isa = PBXTargetDependency; 511 | target = 2102CD082BA9E0E700D0EBAD /* ApiVideoPlayerAnalytics */; 512 | targetProxy = 2102CD152BA9E0E700D0EBAD /* PBXContainerItemProxy */; 513 | }; 514 | 21CCD6A32BBC44EF00E58F5D /* PBXTargetDependency */ = { 515 | isa = PBXTargetDependency; 516 | target = 2102CD082BA9E0E700D0EBAD /* ApiVideoPlayerAnalytics */; 517 | targetProxy = 21CCD6A22BBC44EF00E58F5D /* PBXContainerItemProxy */; 518 | }; 519 | /* End PBXTargetDependency section */ 520 | 521 | /* Begin PBXVariantGroup section */ 522 | 2117CBF42BB1EE6300B85BF0 /* LaunchScreen.storyboard */ = { 523 | isa = PBXVariantGroup; 524 | children = ( 525 | 2117CBF32BB1EE6300B85BF0 /* Base */, 526 | ); 527 | name = LaunchScreen.storyboard; 528 | sourceTree = ""; 529 | }; 530 | 2117CBF62BB1EE6300B85BF0 /* Main.storyboard */ = { 531 | isa = PBXVariantGroup; 532 | children = ( 533 | 2117CBF52BB1EE6300B85BF0 /* Base */, 534 | ); 535 | name = Main.storyboard; 536 | sourceTree = ""; 537 | }; 538 | /* End PBXVariantGroup section */ 539 | 540 | /* Begin XCBuildConfiguration section */ 541 | 2102CD1B2BA9E0E700D0EBAD /* Debug */ = { 542 | isa = XCBuildConfiguration; 543 | buildSettings = { 544 | ALWAYS_SEARCH_USER_PATHS = NO; 545 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 546 | CLANG_ANALYZER_NONNULL = YES; 547 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 548 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 549 | CLANG_ENABLE_MODULES = YES; 550 | CLANG_ENABLE_OBJC_ARC = YES; 551 | CLANG_ENABLE_OBJC_WEAK = YES; 552 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 553 | CLANG_WARN_BOOL_CONVERSION = YES; 554 | CLANG_WARN_COMMA = YES; 555 | CLANG_WARN_CONSTANT_CONVERSION = YES; 556 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 557 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 558 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 559 | CLANG_WARN_EMPTY_BODY = YES; 560 | CLANG_WARN_ENUM_CONVERSION = YES; 561 | CLANG_WARN_INFINITE_RECURSION = YES; 562 | CLANG_WARN_INT_CONVERSION = YES; 563 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 564 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 565 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 566 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 567 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 568 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 569 | CLANG_WARN_STRICT_PROTOTYPES = YES; 570 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 571 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 572 | CLANG_WARN_UNREACHABLE_CODE = YES; 573 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 574 | COPY_PHASE_STRIP = NO; 575 | CURRENT_PROJECT_VERSION = 1; 576 | DEAD_CODE_STRIPPING = YES; 577 | DEBUG_INFORMATION_FORMAT = dwarf; 578 | ENABLE_STRICT_OBJC_MSGSEND = YES; 579 | ENABLE_TESTABILITY = YES; 580 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 581 | GCC_C_LANGUAGE_STANDARD = gnu17; 582 | GCC_DYNAMIC_NO_PIC = NO; 583 | GCC_NO_COMMON_BLOCKS = YES; 584 | GCC_OPTIMIZATION_LEVEL = 0; 585 | GCC_PREPROCESSOR_DEFINITIONS = ( 586 | "DEBUG=1", 587 | "$(inherited)", 588 | ); 589 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 590 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 591 | GCC_WARN_UNDECLARED_SELECTOR = YES; 592 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 593 | GCC_WARN_UNUSED_FUNCTION = YES; 594 | GCC_WARN_UNUSED_VARIABLE = YES; 595 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 596 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 597 | MTL_FAST_MATH = YES; 598 | ONLY_ACTIVE_ARCH = YES; 599 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 600 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 601 | VERSIONING_SYSTEM = "apple-generic"; 602 | VERSION_INFO_PREFIX = ""; 603 | }; 604 | name = Debug; 605 | }; 606 | 2102CD1C2BA9E0E700D0EBAD /* Release */ = { 607 | isa = XCBuildConfiguration; 608 | buildSettings = { 609 | ALWAYS_SEARCH_USER_PATHS = NO; 610 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 611 | CLANG_ANALYZER_NONNULL = YES; 612 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 613 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 614 | CLANG_ENABLE_MODULES = YES; 615 | CLANG_ENABLE_OBJC_ARC = YES; 616 | CLANG_ENABLE_OBJC_WEAK = YES; 617 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 618 | CLANG_WARN_BOOL_CONVERSION = YES; 619 | CLANG_WARN_COMMA = YES; 620 | CLANG_WARN_CONSTANT_CONVERSION = YES; 621 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 622 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 623 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 624 | CLANG_WARN_EMPTY_BODY = YES; 625 | CLANG_WARN_ENUM_CONVERSION = YES; 626 | CLANG_WARN_INFINITE_RECURSION = YES; 627 | CLANG_WARN_INT_CONVERSION = YES; 628 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 629 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 630 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 631 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 632 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 633 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 634 | CLANG_WARN_STRICT_PROTOTYPES = YES; 635 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 636 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 637 | CLANG_WARN_UNREACHABLE_CODE = YES; 638 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 639 | COPY_PHASE_STRIP = NO; 640 | CURRENT_PROJECT_VERSION = 1; 641 | DEAD_CODE_STRIPPING = YES; 642 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 643 | ENABLE_NS_ASSERTIONS = NO; 644 | ENABLE_STRICT_OBJC_MSGSEND = YES; 645 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 646 | GCC_C_LANGUAGE_STANDARD = gnu17; 647 | GCC_NO_COMMON_BLOCKS = YES; 648 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 649 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 650 | GCC_WARN_UNDECLARED_SELECTOR = YES; 651 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 652 | GCC_WARN_UNUSED_FUNCTION = YES; 653 | GCC_WARN_UNUSED_VARIABLE = YES; 654 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 655 | MTL_ENABLE_DEBUG_INFO = NO; 656 | MTL_FAST_MATH = YES; 657 | SWIFT_COMPILATION_MODE = wholemodule; 658 | VERSIONING_SYSTEM = "apple-generic"; 659 | VERSION_INFO_PREFIX = ""; 660 | }; 661 | name = Release; 662 | }; 663 | 2102CD1E2BA9E0E700D0EBAD /* Debug */ = { 664 | isa = XCBuildConfiguration; 665 | buildSettings = { 666 | CODE_SIGN_STYLE = Automatic; 667 | CURRENT_PROJECT_VERSION = 1; 668 | DEAD_CODE_STRIPPING = YES; 669 | DEFINES_MODULE = YES; 670 | DYLIB_COMPATIBILITY_VERSION = 1; 671 | DYLIB_CURRENT_VERSION = 1; 672 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 673 | ENABLE_MODULE_VERIFIER = YES; 674 | GENERATE_INFOPLIST_FILE = YES; 675 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 676 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 677 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 678 | LD_RUNPATH_SEARCH_PATHS = ( 679 | "@executable_path/Frameworks", 680 | "@loader_path/Frameworks", 681 | ); 682 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = ( 683 | "@executable_path/../Frameworks", 684 | "@loader_path/Frameworks", 685 | ); 686 | MACOSX_DEPLOYMENT_TARGET = 11.0; 687 | MARKETING_VERSION = 1.0; 688 | MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; 689 | MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; 690 | PRODUCT_BUNDLE_IDENTIFIER = video.api.player.analytics; 691 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 692 | SDKROOT = auto; 693 | SKIP_INSTALL = YES; 694 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 695 | SWIFT_EMIT_LOC_STRINGS = YES; 696 | SWIFT_VERSION = 5.0; 697 | TARGETED_DEVICE_FAMILY = "1,2"; 698 | }; 699 | name = Debug; 700 | }; 701 | 2102CD1F2BA9E0E700D0EBAD /* Release */ = { 702 | isa = XCBuildConfiguration; 703 | buildSettings = { 704 | CODE_SIGN_STYLE = Automatic; 705 | CURRENT_PROJECT_VERSION = 1; 706 | DEAD_CODE_STRIPPING = YES; 707 | DEFINES_MODULE = YES; 708 | DYLIB_COMPATIBILITY_VERSION = 1; 709 | DYLIB_CURRENT_VERSION = 1; 710 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 711 | ENABLE_MODULE_VERIFIER = YES; 712 | GENERATE_INFOPLIST_FILE = YES; 713 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 714 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 715 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 716 | LD_RUNPATH_SEARCH_PATHS = ( 717 | "@executable_path/Frameworks", 718 | "@loader_path/Frameworks", 719 | ); 720 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = ( 721 | "@executable_path/../Frameworks", 722 | "@loader_path/Frameworks", 723 | ); 724 | MACOSX_DEPLOYMENT_TARGET = 11.0; 725 | MARKETING_VERSION = 1.0; 726 | MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; 727 | MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; 728 | PRODUCT_BUNDLE_IDENTIFIER = video.api.player.analytics; 729 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 730 | SDKROOT = auto; 731 | SKIP_INSTALL = YES; 732 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 733 | SWIFT_EMIT_LOC_STRINGS = YES; 734 | SWIFT_VERSION = 5.0; 735 | TARGETED_DEVICE_FAMILY = "1,2"; 736 | }; 737 | name = Release; 738 | }; 739 | 2102CD212BA9E0E700D0EBAD /* Debug */ = { 740 | isa = XCBuildConfiguration; 741 | buildSettings = { 742 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 743 | CODE_SIGN_STYLE = Automatic; 744 | CURRENT_PROJECT_VERSION = 1; 745 | DEAD_CODE_STRIPPING = YES; 746 | GENERATE_INFOPLIST_FILE = YES; 747 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 748 | MACOSX_DEPLOYMENT_TARGET = 11.0; 749 | MARKETING_VERSION = 1.0; 750 | PRODUCT_BUNDLE_IDENTIFIER = video.api.player.analytics.Example; 751 | PRODUCT_NAME = "$(TARGET_NAME)"; 752 | SDKROOT = auto; 753 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 754 | SWIFT_EMIT_LOC_STRINGS = NO; 755 | SWIFT_VERSION = 5.0; 756 | TARGETED_DEVICE_FAMILY = "1,2"; 757 | }; 758 | name = Debug; 759 | }; 760 | 2102CD222BA9E0E700D0EBAD /* Release */ = { 761 | isa = XCBuildConfiguration; 762 | buildSettings = { 763 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 764 | CODE_SIGN_STYLE = Automatic; 765 | CURRENT_PROJECT_VERSION = 1; 766 | DEAD_CODE_STRIPPING = YES; 767 | GENERATE_INFOPLIST_FILE = YES; 768 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 769 | MACOSX_DEPLOYMENT_TARGET = 11.0; 770 | MARKETING_VERSION = 1.0; 771 | PRODUCT_BUNDLE_IDENTIFIER = video.api.player.analytics.Example; 772 | PRODUCT_NAME = "$(TARGET_NAME)"; 773 | SDKROOT = auto; 774 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 775 | SWIFT_EMIT_LOC_STRINGS = NO; 776 | SWIFT_VERSION = 5.0; 777 | TARGETED_DEVICE_FAMILY = "1,2"; 778 | }; 779 | name = Release; 780 | }; 781 | 2117CBEE2BB1EE3300B85BF0 /* Debug */ = { 782 | isa = XCBuildConfiguration; 783 | buildSettings = { 784 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 785 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 786 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 787 | CODE_SIGN_STYLE = Automatic; 788 | CURRENT_PROJECT_VERSION = 1; 789 | DEVELOPMENT_TEAM = GBC36KP98K; 790 | GENERATE_INFOPLIST_FILE = YES; 791 | INFOPLIST_FILE = Examples/iOS/Info.plist; 792 | INFOPLIST_KEY_CFBundleDisplayName = PlayerAnalytics; 793 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 794 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 795 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 796 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 797 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 798 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 799 | LD_RUNPATH_SEARCH_PATHS = ( 800 | "$(inherited)", 801 | "@executable_path/Frameworks", 802 | ); 803 | MARKETING_VERSION = 1.0; 804 | PRODUCT_BUNDLE_IDENTIFIER = video.api.player.analytics.examples.iOS; 805 | PRODUCT_NAME = "$(TARGET_NAME)"; 806 | SDKROOT = iphoneos; 807 | SWIFT_EMIT_LOC_STRINGS = YES; 808 | SWIFT_VERSION = 5.0; 809 | TARGETED_DEVICE_FAMILY = "1,2"; 810 | }; 811 | name = Debug; 812 | }; 813 | 2117CBEF2BB1EE3300B85BF0 /* Release */ = { 814 | isa = XCBuildConfiguration; 815 | buildSettings = { 816 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 817 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 818 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 819 | CODE_SIGN_STYLE = Automatic; 820 | CURRENT_PROJECT_VERSION = 1; 821 | DEVELOPMENT_TEAM = GBC36KP98K; 822 | GENERATE_INFOPLIST_FILE = YES; 823 | INFOPLIST_FILE = Examples/iOS/Info.plist; 824 | INFOPLIST_KEY_CFBundleDisplayName = PlayerAnalytics; 825 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 826 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 827 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 828 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 829 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 830 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 831 | LD_RUNPATH_SEARCH_PATHS = ( 832 | "$(inherited)", 833 | "@executable_path/Frameworks", 834 | ); 835 | MARKETING_VERSION = 1.0; 836 | PRODUCT_BUNDLE_IDENTIFIER = video.api.player.analytics.examples.iOS; 837 | PRODUCT_NAME = "$(TARGET_NAME)"; 838 | SDKROOT = iphoneos; 839 | SWIFT_EMIT_LOC_STRINGS = YES; 840 | SWIFT_VERSION = 5.0; 841 | TARGETED_DEVICE_FAMILY = "1,2"; 842 | VALIDATE_PRODUCT = YES; 843 | }; 844 | name = Release; 845 | }; 846 | /* End XCBuildConfiguration section */ 847 | 848 | /* Begin XCConfigurationList section */ 849 | 2102CD032BA9E0E600D0EBAD /* Build configuration list for PBXProject "ApiVideoPlayerAnalytics" */ = { 850 | isa = XCConfigurationList; 851 | buildConfigurations = ( 852 | 2102CD1B2BA9E0E700D0EBAD /* Debug */, 853 | 2102CD1C2BA9E0E700D0EBAD /* Release */, 854 | ); 855 | defaultConfigurationIsVisible = 0; 856 | defaultConfigurationName = Release; 857 | }; 858 | 2102CD1D2BA9E0E700D0EBAD /* Build configuration list for PBXNativeTarget "ApiVideoPlayerAnalytics" */ = { 859 | isa = XCConfigurationList; 860 | buildConfigurations = ( 861 | 2102CD1E2BA9E0E700D0EBAD /* Debug */, 862 | 2102CD1F2BA9E0E700D0EBAD /* Release */, 863 | ); 864 | defaultConfigurationIsVisible = 0; 865 | defaultConfigurationName = Release; 866 | }; 867 | 2102CD202BA9E0E700D0EBAD /* Build configuration list for PBXNativeTarget "ApiVideoPlayerAnalyticsTests" */ = { 868 | isa = XCConfigurationList; 869 | buildConfigurations = ( 870 | 2102CD212BA9E0E700D0EBAD /* Debug */, 871 | 2102CD222BA9E0E700D0EBAD /* Release */, 872 | ); 873 | defaultConfigurationIsVisible = 0; 874 | defaultConfigurationName = Release; 875 | }; 876 | 2117CBED2BB1EE3300B85BF0 /* Build configuration list for PBXNativeTarget "Example iOS" */ = { 877 | isa = XCConfigurationList; 878 | buildConfigurations = ( 879 | 2117CBEE2BB1EE3300B85BF0 /* Debug */, 880 | 2117CBEF2BB1EE3300B85BF0 /* Release */, 881 | ); 882 | defaultConfigurationIsVisible = 0; 883 | defaultConfigurationName = Release; 884 | }; 885 | /* End XCConfigurationList section */ 886 | }; 887 | rootObject = 2102CD002BA9E0E600D0EBAD /* Project object */; 888 | } 889 | --------------------------------------------------------------------------------