├── Cartfile ├── Examples └── iOS │ ├── Assets.xcassets │ ├── Contents.json │ ├── AppIcon.appiconset │ │ ├── 152.png │ │ ├── 167.png │ │ ├── 180.png │ │ ├── 20.png │ │ ├── 29.png │ │ ├── 40.png │ │ ├── 58.png │ │ ├── 60.png │ │ ├── 76.png │ │ ├── 80.png │ │ ├── 87.png │ │ ├── 1024.png │ │ ├── 120 1.png │ │ ├── 120 2.png │ │ ├── 40 1.png │ │ ├── 40 2.png │ │ ├── 58 1.png │ │ ├── 80 1.png │ │ └── Contents.json │ └── AccentColor.colorset │ │ └── Contents.json │ ├── Info.plist │ ├── ViewControllers │ ├── SettingsViewController.swift │ └── MainViewController.swift │ ├── AppDelegate.swift │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── SceneDelegate.swift │ ├── SettingsManager.swift │ └── Settings.bundle │ └── Root.plist ├── ApiVideoLiveStream.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── project.pbxproj ├── Tests └── ApiVideoLiveStream │ ├── Parameters.swift │ └── ApiVideoLiveStreamTests.swift ├── .github └── workflows │ ├── create-release-from-changelog.yml │ ├── release.yml │ ├── create-documentation-pr.yml │ └── build.yml ├── Package.resolved ├── LICENSE.md ├── ApiVideoLiveStream.podspec ├── Package.swift ├── .swiftlint.yml ├── .swiftformat ├── Sources └── ApiVideoLiveStream │ ├── models │ ├── Resolution.swift │ └── Configuration.swift │ └── ApiVideoLiveStream.swift ├── CHANGELOG.md ├── .gitignore ├── README.md └── CONTRIBUTING.md /Cartfile: -------------------------------------------------------------------------------- 1 | github "shogo4405/HaishinKit.swift" ~> 1.9.3 2 | -------------------------------------------------------------------------------- /Examples/iOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/iOS/Assets.xcassets/AppIcon.appiconset/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-swift-live-stream/HEAD/Examples/iOS/Assets.xcassets/AppIcon.appiconset/152.png -------------------------------------------------------------------------------- /Examples/iOS/Assets.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-swift-live-stream/HEAD/Examples/iOS/Assets.xcassets/AppIcon.appiconset/167.png -------------------------------------------------------------------------------- /Examples/iOS/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-swift-live-stream/HEAD/Examples/iOS/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /Examples/iOS/Assets.xcassets/AppIcon.appiconset/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-swift-live-stream/HEAD/Examples/iOS/Assets.xcassets/AppIcon.appiconset/20.png -------------------------------------------------------------------------------- /Examples/iOS/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-swift-live-stream/HEAD/Examples/iOS/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /Examples/iOS/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-swift-live-stream/HEAD/Examples/iOS/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /Examples/iOS/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-swift-live-stream/HEAD/Examples/iOS/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /Examples/iOS/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-swift-live-stream/HEAD/Examples/iOS/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /Examples/iOS/Assets.xcassets/AppIcon.appiconset/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-swift-live-stream/HEAD/Examples/iOS/Assets.xcassets/AppIcon.appiconset/76.png -------------------------------------------------------------------------------- /Examples/iOS/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-swift-live-stream/HEAD/Examples/iOS/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /Examples/iOS/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-swift-live-stream/HEAD/Examples/iOS/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /Examples/iOS/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-swift-live-stream/HEAD/Examples/iOS/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /Examples/iOS/Assets.xcassets/AppIcon.appiconset/120 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-swift-live-stream/HEAD/Examples/iOS/Assets.xcassets/AppIcon.appiconset/120 1.png -------------------------------------------------------------------------------- /Examples/iOS/Assets.xcassets/AppIcon.appiconset/120 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-swift-live-stream/HEAD/Examples/iOS/Assets.xcassets/AppIcon.appiconset/120 2.png -------------------------------------------------------------------------------- /Examples/iOS/Assets.xcassets/AppIcon.appiconset/40 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-swift-live-stream/HEAD/Examples/iOS/Assets.xcassets/AppIcon.appiconset/40 1.png -------------------------------------------------------------------------------- /Examples/iOS/Assets.xcassets/AppIcon.appiconset/40 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-swift-live-stream/HEAD/Examples/iOS/Assets.xcassets/AppIcon.appiconset/40 2.png -------------------------------------------------------------------------------- /Examples/iOS/Assets.xcassets/AppIcon.appiconset/58 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-swift-live-stream/HEAD/Examples/iOS/Assets.xcassets/AppIcon.appiconset/58 1.png -------------------------------------------------------------------------------- /Examples/iOS/Assets.xcassets/AppIcon.appiconset/80 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-swift-live-stream/HEAD/Examples/iOS/Assets.xcassets/AppIcon.appiconset/80 1.png -------------------------------------------------------------------------------- /ApiVideoLiveStream.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 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 | -------------------------------------------------------------------------------- /Tests/ApiVideoLiveStream/Parameters.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum Parameters { 4 | public static var rtmpUrl: String = "INTEGRATION_TESTS_RTMP_URL" 5 | public static var streamKey: String = "INTEGRATION_TESTS_RTMP_STREAM_KEY" 6 | } 7 | -------------------------------------------------------------------------------- /ApiVideoLiveStream.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release to Cocoapods 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | deploy: 7 | runs-on: macos-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - name: Install Cocoapods 11 | run: gem install cocoapods 12 | - name: Deploy to Cocoapods 13 | run: | 14 | xcodebuild -downloadPlatform iOS 15 | pod trunk push --allow-warnings 16 | env: 17 | COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "haishinkit.swift", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/shogo4405/HaishinKit.swift", 7 | "state" : { 8 | "revision" : "3d69e5a8514d7aead27aa4241dd859fc219acd3f", 9 | "version" : "1.9.3" 10 | } 11 | }, 12 | { 13 | "identity" : "logboard", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/shogo4405/Logboard.git", 16 | "state" : { 17 | "revision" : "272976e1f3e8873e60ffe4b08fe50df48a93751b", 18 | "version" : "2.5.0" 19 | } 20 | } 21 | ], 22 | "version" : 2 23 | } 24 | -------------------------------------------------------------------------------- /.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/livestream 25 | destination-filename: swift-livestream-library.md 26 | pat: "${{ secrets.PAT }}" 27 | -------------------------------------------------------------------------------- /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 | NSCameraUsageDescription 25 | Your own description of the purpose 26 | NSMicrophoneUsageDescription 27 | Your own description of the purpose 28 | 29 | 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 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 | -------------------------------------------------------------------------------- /ApiVideoLiveStream.podspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | Pod::Spec.new do |spec| 4 | spec.name = "ApiVideoLiveStream" 5 | spec.version = "1.4.6" 6 | spec.summary = "The api.video live stream framework allow easy integration of a live stream broadcast into your application." 7 | 8 | spec.description = <<-DESC 9 | Quickly add the ability to broadcast a live stream from your application with this module. Within minutes, your app will be streaming RTMP live video to api.video where it can be broadcasted to anyone around the world. 10 | DESC 11 | 12 | spec.homepage = "https://docs.api.video" 13 | spec.license = { :type => 'MIT' } 14 | 15 | spec.author = { "Ecosystem Team" => "ecosystem@api.video" } 16 | spec.social_media_url = "https://x.com/api_video" 17 | 18 | spec.swift_versions = ["5.6"] 19 | spec.ios.deployment_target = "13.0" 20 | 21 | spec.source = { :git => "https://github.com/apivideo/api.video-swift-live-stream.git", :tag => "v" + spec.version.to_s } 22 | 23 | spec.source_files = "Sources/**/*.{h,m,swift}" 24 | spec.exclude_files = "Sources/Exclude" 25 | 26 | spec.dependency "HaishinKit", "1.9.3" 27 | 28 | end 29 | -------------------------------------------------------------------------------- /Tests/ApiVideoLiveStream/ApiVideoLiveStreamTests.swift: -------------------------------------------------------------------------------- 1 | @testable import ApiVideoLiveStream 2 | import XCTest 3 | 4 | class ApiVideoLiveStreamTests: XCTestCase { 5 | private let connectionExpectation = XCTestExpectation(description: "connectionExpectation") 6 | private let disconnectionExpectation = XCTestExpectation(description: "disconnectionExpectation") 7 | 8 | func testSingleLiveStream() throws { 9 | let liveStream = try ApiVideoLiveStream() 10 | liveStream.delegate = self 11 | try liveStream.startStreaming(streamKey: Parameters.streamKey, url: Parameters.rtmpUrl) 12 | wait(for: [self.connectionExpectation], timeout: 10.0) 13 | liveStream.stopStreaming() 14 | wait(for: [self.disconnectionExpectation], timeout: 10.0) 15 | } 16 | } 17 | 18 | // MARK: ApiVideoLiveStreamDelegate 19 | 20 | extension ApiVideoLiveStreamTests: ApiVideoLiveStreamDelegate { 21 | func connectionSuccess() { 22 | self.connectionExpectation.fulfill() 23 | } 24 | 25 | func connectionFailed(_: String) {} 26 | 27 | func disconnection() { 28 | self.disconnectionExpectation.fulfill() 29 | } 30 | 31 | func audioError(_: Error) {} 32 | 33 | func videoError(_: Error) {} 34 | } 35 | -------------------------------------------------------------------------------- /Examples/iOS/ViewControllers/SettingsViewController.swift: -------------------------------------------------------------------------------- 1 | import ApiVideoLiveStream 2 | import Foundation 3 | import InAppSettingsKit 4 | import UIKit 5 | 6 | class SettingsViewController: IASKAppSettingsViewController { 7 | 8 | override func viewDidLoad() { 9 | super.viewDidLoad() 10 | 11 | showDoneButton = true 12 | showCreditsFooter = false 13 | 14 | NotificationCenter.default.addObserver( 15 | self, 16 | selector: #selector(self.settingDidChange(notification:)), 17 | name: Notification.Name.IASKSettingChanged, 18 | object: nil 19 | ) 20 | } 21 | 22 | @objc 23 | func settingDidChange(notification: Notification?) { 24 | guard let notification = notification, 25 | let paramChanged = notification.userInfo?.first, 26 | let key = paramChanged.key as? String else 27 | { 28 | return 29 | } 30 | 31 | switch key { 32 | case "VideoBitrateSlider": 33 | if let value = paramChanged.value as? Float { 34 | UserDefaults.standard.set(Int(value), forKey: "VideoBitrateValue") 35 | } 36 | 37 | default: 38 | // Do nothing 39 | break 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Examples/iOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @main 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | func application( 6 | _: UIApplication, 7 | didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? 8 | ) -> Bool { 9 | // Override point for customization after application launch. 10 | return true 11 | } 12 | 13 | // MARK: UISceneSession Lifecycle 14 | 15 | func application( 16 | _: UIApplication, 17 | configurationForConnecting connectingSceneSession: UISceneSession, 18 | options _: UIScene.ConnectionOptions 19 | ) -> UISceneConfiguration { 20 | // Called when a new scene session is being created. 21 | // Use this method to select a configuration to create the new scene with. 22 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 23 | } 24 | 25 | func application(_: UIApplication, didDiscardSceneSessions _: Set) { 26 | // Called when the user discards a scene session. 27 | // If any sessions were discarded while the application was not running, this will be called shortly after 28 | // application:didFinishLaunchingWithOptions. 29 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "ApiVideoLiveStream", 8 | platforms: [ 9 | .macOS(.v11), .iOS(.v13) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, and make them visible to other packages. 13 | .library( 14 | name: "ApiVideoLiveStream", 15 | targets: ["ApiVideoLiveStream"] 16 | ) 17 | ], 18 | dependencies: [ 19 | // Dependencies declare other packages that this package depends on. 20 | // .package(url: /* package url */, from: "1.0.0"), 21 | .package(url: "https://github.com/shogo4405/HaishinKit.swift", exact: "1.9.3") 22 | ], 23 | targets: [ 24 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 25 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 26 | .target( 27 | name: "ApiVideoLiveStream", 28 | dependencies: [ 29 | .product(name: "HaishinKit", package: "HaishinKit.swift") 30 | ] 31 | ), 32 | .testTarget( 33 | name: "ApiVideoLiveStreamTests", 34 | dependencies: ["ApiVideoLiveStream"], 35 | path: "Tests/ApiVideoLiveStream" 36 | ) 37 | ] 38 | ) 39 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Examples/iOS/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 4 | var window: UIWindow? 5 | 6 | func scene(_: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) { 7 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene 8 | // `scene`. 9 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 10 | // This delegate does not imply the connecting scene or session are new (see 11 | // `application:configurationForConnectingSceneSession` instead). 12 | } 13 | 14 | func sceneDidDisconnect(_: UIScene) { 15 | // Called as the scene is being released by the system. 16 | // This occurs shortly after the scene enters the background, or when its session is discarded. 17 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 18 | // The scene may re-connect later, as its session was not necessarily discarded (see 19 | // `application:didDiscardSceneSessions` instead). 20 | } 21 | 22 | func sceneDidBecomeActive(_: UIScene) { 23 | // Called when the scene has moved from an inactive state to an active state. 24 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 25 | } 26 | 27 | func sceneWillResignActive(_: UIScene) { 28 | // Called when the scene will move from an active state to an inactive state. 29 | // This may occur due to temporary interruptions (ex. an incoming phone call). 30 | } 31 | 32 | func sceneWillEnterForeground(_: UIScene) { 33 | // Called as the scene transitions from the background to the foreground. 34 | // Use this method to undo the changes made on entering the background. 35 | } 36 | 37 | func sceneDidEnterBackground(_: UIScene) { 38 | // Called as the scene transitions from the foreground to the background. 39 | // Use this method to save data, release shared resources, and store enough scene-specific state information 40 | // to restore the scene back to its current state. 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.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-latest 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-latest 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-latest 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: | 51 | xcodebuild -downloadPlatform iOS 52 | xcodebuild clean build -project ApiVideoLiveStream.xcodeproj -scheme "${{matrix.scheme}}" -sdk iphoneos CODE_SIGNING_ALLOWED=NO 53 | verify: 54 | name: Verify package sanity 55 | runs-on: macos-latest 56 | steps: 57 | - name: Checkout 58 | uses: actions/checkout@v4 59 | - name: xcode version 60 | uses: maxim-lobanov/setup-xcode@v1 61 | with: 62 | xcode-version: latest-stable 63 | - name: Verify cocoapods 64 | run: | 65 | xcodebuild -downloadPlatform iOS 66 | pod lib lint --allow-warnings 67 | - name: Install swiftlint 68 | run: brew install swiftlint 69 | - name: Execute swiftlint 70 | run: swiftlint 71 | -------------------------------------------------------------------------------- /Examples/iOS/SettingsManager.swift: -------------------------------------------------------------------------------- 1 | import ApiVideoLiveStream 2 | import Foundation 3 | 4 | enum SettingsManager { 5 | // MARK: Endpoint 6 | 7 | static var rtmpUrl: String { 8 | UserDefaults.standard.string(forKey: "RtmpUrl") ?? "rtmp://broadcast.api.video/s/" 9 | } 10 | 11 | static var streamKey: String { 12 | UserDefaults.standard.string(forKey: "StreamKey") ?? "" 13 | } 14 | 15 | // MARK: Video 16 | 17 | private static var videoBitrate: Int { 18 | UserDefaults.standard.integer(forKey: "VideoBitrateValue") 19 | } 20 | 21 | private static var resolution: Resolution { 22 | do { 23 | return try UserDefaults.standard.string(forKey: "Resolution")?.toResolution() ?? Resolution 24 | .RESOLUTION_16_9_720P 25 | } catch { 26 | fatalError("Can't get resolution from user defaults") 27 | } 28 | } 29 | 30 | private static var framerate: Float64 { 31 | UserDefaults.standard.double(forKey: "Framerate") 32 | } 33 | 34 | public static var videoConfig: VideoConfig { 35 | VideoConfig(bitrate: videoBitrate * 1_000, resolution: resolution, fps: framerate) 36 | } 37 | 38 | // MARK: Audio 39 | 40 | private static var audioBitrate: Int { 41 | UserDefaults.standard.integer(forKey: "AudioBitrate") 42 | } 43 | 44 | public static var audioConfig: AudioConfig { 45 | AudioConfig(bitrate: audioBitrate) 46 | } 47 | } 48 | 49 | // MARK: Convert functions 50 | 51 | extension String { 52 | func toResolution() throws -> Resolution { 53 | let resolutionArray = self.components(separatedBy: "x") 54 | guard let width = Int(resolutionArray[0]) else { 55 | throw ParameterError.Invalid("Width is invalid") 56 | } 57 | guard let height = Int(resolutionArray[1]) else { 58 | throw ParameterError.Invalid("Height is invalid") 59 | } 60 | let resolution = Resolution(rawValue: CGSize(width: width, height: height)) 61 | if let resolution { 62 | return resolution 63 | } else { 64 | throw ParameterError.Invalid("Resolution is invalid for \(width)x\(height)") 65 | } 66 | } 67 | } 68 | 69 | extension Resolution { 70 | func toString() -> String { 71 | "\(rawValue.width)x\(rawValue.height)" 72 | } 73 | } 74 | 75 | public enum ParameterError: Error { 76 | case Invalid(String) 77 | } 78 | -------------------------------------------------------------------------------- /Examples/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "40.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "60.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "58 1.png", 17 | "idiom" : "iphone", 18 | "scale" : "2x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "87.png", 23 | "idiom" : "iphone", 24 | "scale" : "3x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "80 1.png", 29 | "idiom" : "iphone", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "120 2.png", 35 | "idiom" : "iphone", 36 | "scale" : "3x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "120 1.png", 41 | "idiom" : "iphone", 42 | "scale" : "2x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "filename" : "180.png", 47 | "idiom" : "iphone", 48 | "scale" : "3x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "20.png", 53 | "idiom" : "ipad", 54 | "scale" : "1x", 55 | "size" : "20x20" 56 | }, 57 | { 58 | "filename" : "40 1.png", 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "29.png", 65 | "idiom" : "ipad", 66 | "scale" : "1x", 67 | "size" : "29x29" 68 | }, 69 | { 70 | "filename" : "58.png", 71 | "idiom" : "ipad", 72 | "scale" : "2x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "40 2.png", 77 | "idiom" : "ipad", 78 | "scale" : "1x", 79 | "size" : "40x40" 80 | }, 81 | { 82 | "filename" : "80.png", 83 | "idiom" : "ipad", 84 | "scale" : "2x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "76.png", 89 | "idiom" : "ipad", 90 | "scale" : "1x", 91 | "size" : "76x76" 92 | }, 93 | { 94 | "filename" : "152.png", 95 | "idiom" : "ipad", 96 | "scale" : "2x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "167.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "83.5x83.5" 104 | }, 105 | { 106 | "filename" : "1024.png", 107 | "idiom" : "ios-marketing", 108 | "scale" : "1x", 109 | "size" : "1024x1024" 110 | } 111 | ], 112 | "info" : { 113 | "author" : "xcode", 114 | "version" : 1 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /.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: 350 113 | error: 450 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 | --enable 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 | --varattributes same-line 84 | 85 | ### Else 86 | --elseposition same-line 87 | --guardelse same-line 88 | 89 | ### Others 90 | --commas inline 91 | --ifdef noindent 92 | --indent 4 93 | --linebreaks lf ##default 94 | --maxwidth 120 95 | --patternlet hoist ##default 96 | --self insert 97 | --semicolons never 98 | --stripunusedargs always 99 | --trimwhitespace always 100 | --voidtype void 101 | 102 | 103 | ## Disabled rules 104 | 105 | ### SwiftFormat also removes from funcs, so let's let SwiftLint takes care of it. 106 | --disable redundantReturn 107 | 108 | ### SwiftFormat also removes from end of scope, which is against our empty line guidelines. 109 | --disable blankLinesAtEndOfScope 110 | 111 | ### SwiftFormat also removes from start of scope, which is against our empty line guidelines. 112 | --disable blankLinesAtStartOfScope -------------------------------------------------------------------------------- /Sources/ApiVideoLiveStream/models/Resolution.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Resolution of the video 4 | public enum Resolution { 5 | // 16:9 6 | /// 426x240 7 | case RESOLUTION_16_9_240P 8 | /// 640x360: nHD 9 | case RESOLUTION_16_9_360P 10 | /// 854x480: FWVGA 11 | case RESOLUTION_16_9_480P 12 | /// 1280x720: WXGA 13 | case RESOLUTION_16_9_720P 14 | /// 1920x1080: FHD 15 | case RESOLUTION_16_9_1080P 16 | 17 | // 4:3 18 | /// 320x240: QVGA 19 | case RESOLUTION_4_3_240P 20 | /// 640x480: VGA 21 | case RESOLUTION_4_3_480P 22 | /// 800x600: SVGA 23 | case RESOLUTION_4_3_600P 24 | /// 1024x768: XGA 25 | case RESOLUTION_4_3_768P 26 | /// 1440x1080 27 | case RESOLUTION_4_3_1080P 28 | } 29 | 30 | // MARK: RawRepresentable 31 | 32 | extension Resolution: RawRepresentable { 33 | public typealias RawValue = CGSize 34 | 35 | public init?(rawValue: RawValue) { 36 | let widerWidth = max(rawValue.width, rawValue.height) 37 | let widerHeight = min(rawValue.width, rawValue.height) 38 | switch (widerWidth, widerHeight) { 39 | case (426, 240): 40 | self = .RESOLUTION_16_9_240P 41 | 42 | case (640, 360): 43 | self = .RESOLUTION_16_9_360P 44 | 45 | case (854, 480): 46 | self = .RESOLUTION_16_9_480P 47 | 48 | case (1_280, 720): 49 | self = .RESOLUTION_16_9_720P 50 | 51 | case (1_920, 1_080): 52 | self = .RESOLUTION_16_9_1080P 53 | 54 | case (320, 240): 55 | self = .RESOLUTION_4_3_240P 56 | 57 | case (640, 480): 58 | self = .RESOLUTION_4_3_480P 59 | 60 | case (800, 600): 61 | self = .RESOLUTION_4_3_600P 62 | 63 | case (1_024, 768): 64 | self = .RESOLUTION_4_3_768P 65 | 66 | case (1_440, 1_080): 67 | self = .RESOLUTION_4_3_1080P 68 | 69 | default: return nil 70 | } 71 | } 72 | 73 | public var rawValue: RawValue { 74 | switch self { 75 | case .RESOLUTION_16_9_240P: 76 | return CGSize(width: 426, height: 240) 77 | 78 | case .RESOLUTION_16_9_360P: 79 | return CGSize(width: 640, height: 360) 80 | 81 | case .RESOLUTION_16_9_480P: 82 | return CGSize(width: 854, height: 480) 83 | 84 | case .RESOLUTION_16_9_720P: 85 | return CGSize(width: 1_280, height: 720) 86 | 87 | case .RESOLUTION_16_9_1080P: 88 | return CGSize(width: 1_920, height: 1_080) 89 | 90 | case .RESOLUTION_4_3_240P: 91 | return CGSize(width: 320, height: 240) 92 | 93 | case .RESOLUTION_4_3_480P: 94 | return CGSize(width: 640, height: 480) 95 | 96 | case .RESOLUTION_4_3_600P: 97 | return CGSize(width: 800, height: 600) 98 | 99 | case .RESOLUTION_4_3_768P: 100 | return CGSize(width: 1_024, height: 768) 101 | 102 | case .RESOLUTION_4_3_1080P: 103 | return CGSize(width: 1_440, height: 1_080) 104 | } 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All changes to this project will be documented in this file. 3 | 4 | ## [1.4.6] - 2025-01-29 5 | - Fix `isMuted` property 6 | 7 | ## [1.4.5] - 2024-10-04 8 | - Add an API to get the zoom ratio range 9 | - Fix zoom ratio max value 10 | 11 | ## [1.4.4] - 2024-09-11 12 | - Upgrade HaishinKit to 1.9.3 13 | 14 | ## [1.4.3] - 2024-06-10 15 | - Add publish name to the stream to trigger `FCUnpublish` 16 | 17 | ## [1.4.2] - 2024-06-07 18 | - Fix settings keys on iOS example 19 | - Upgrade HaishinKit to 1.8.1 20 | - Create a global pbxproj 21 | 22 | ## [1.4.1] - 2024-02-12 23 | - Fix regression on device orientation 24 | 25 | ## [1.4.0] - 2024-02-09 26 | - Improve the way Resolution are handled to avoid misusage. 27 | 28 | ## [1.3.6] - 2024-02-08 29 | - Improve RTMP error handling 30 | - Avoid disptaching `connect` because it is slower 31 | - Fixes on zoom ratio 32 | - Upgrade HaishinKit to 1.7.3 33 | - Example: reduce default bitrate to improve experience 34 | 35 | ## [1.3.5] - 2023-06-21 36 | - Fix detach camera 37 | - Upgrade HaishinKit to 1.5.2 38 | 39 | ## [1.3.4] - 2023-03-02 40 | - Fix front camera mirroring 41 | - Upgrade HaishinKit to 1.4.3 42 | 43 | ## [1.3.3] - 2023-01-19 44 | - Fix streaming orientation when application is opened in portrait and device is turned to landscape 45 | - Upgrade HaishinKit to 1.4.2 46 | 47 | ## [1.3.2] - 2023-01-11 48 | - Fix `lastCamera` when camera is set with `cameraPosition` 49 | - Improve `startStreaming` errors 50 | 51 | ## [1.3.1] - 2023-01-09 52 | - Reduce startup time by fixing synchronization issues 53 | - Fix `isMuted` value. The property was inverted 54 | 55 | ## [1.3.0] - 2023-01-06 56 | - Add a camera parameter in the ApiVideoLiveStream constructors 57 | - Add an API to set the duration between two key frames 58 | - Introducing the new `camera` API. Previous `camera` has been renamed to `cameraPosition` 59 | - Add swift PM support 60 | - Upgrade HaishinKit to 1.4.1 61 | - Example: refactor and clean the UIKit example 62 | 63 | ## [1.2.2] - 2022-09-29 64 | - Only register event listener in constructor to avoid multiple callback called 65 | 66 | ## [1.2.1] - 2022-09-29 67 | - fix(lib): use RtmpStream lock instead of a custom lock 68 | 69 | ## [1.2.0] - 2022-09-29 70 | - Add startPreview/stopPreview API 71 | - Try to synchronize `startStreaming` abd video configuration 72 | - Force HaishinKit default resolution to 720p 73 | - Add constructor for HaishinKit NetStreamDrawable 74 | - Upgrade to HaishinKit 1.3.0 75 | - Release workflow is triggered on release published (instead of created) 76 | 77 | ## [1.1.0] - 2022-08-18 78 | - Adds API to set zoom ratio 79 | 80 | ## [1.0.0] - 2022-08-15 81 | - Allows nil initialVideoConfig and initialAudioConfig 82 | - Fix landscape orientation 83 | - Return onDisconnect when user call `stopStreaming` 84 | 85 | ## [0.2.1] - 2022-04-22 86 | - Stop streaming on didEnterBackgroundNotification 87 | 88 | ## [0.2.0] - 2022-04-13 89 | - Add default parameters for audio and video config 90 | - Fix continous auto focus 91 | - Call `onDisconnect` when connection is closed 92 | - Rename `Resolution` to `Size` 93 | - Few fixes on example 94 | 95 | ## [0.1.0] - 2022-01-27 96 | - Large refactor and clean 97 | - Add a sample application 98 | -------------------------------------------------------------------------------- /.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 | ## Gcc Patch 91 | /*.gcno 92 | 93 | ### Xcode Patch ### 94 | *.xcodeproj/* 95 | !*.xcodeproj/project.pbxproj 96 | !*.xcodeproj/xcshareddata/ 97 | !*.xcworkspace/contents.xcworkspacedata 98 | **/xcshareddata/WorkspaceSettings.xcsettings 99 | 100 | .DS_Store 101 | /.build 102 | /Packages 103 | /*.xcodeproj 104 | .swiftpm/config/registries.json 105 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 106 | .netrc 107 | 108 | # Android studio 3.1+ serialized cache file or AppCode 109 | .idea/ 110 | 111 | 112 | -------------------------------------------------------------------------------- /Sources/ApiVideoLiveStream/models/Configuration.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | import Foundation 3 | import Network 4 | 5 | public struct AudioConfig { 6 | public let bitrate: Int 7 | 8 | /// Creates an audio configuration object 9 | /// - Parameters: 10 | /// - bitrate: The audio bitrate in bits per second 11 | public init(bitrate: Int = 128_000) { 12 | self.bitrate = bitrate 13 | } 14 | } 15 | 16 | public struct VideoConfig { 17 | public let bitrate: Int 18 | public let resolution: CGSize 19 | public let fps: Float64 20 | public let gopDuration: TimeInterval 21 | 22 | /// Creates a video configuration object with explicit video bitrate and CGSize resolution 23 | /// - Parameters: 24 | /// - bitrate: The video bitrate in bits per second 25 | /// - resolution: The video resolution 26 | /// - fps: The video framerate 27 | /// - gopDuration: The time interval between two key frames 28 | public init( 29 | bitrate: Int, 30 | resolution: CGSize = CGSize(width: 1_280, height: 720), 31 | fps: Float64 = 30, 32 | gopDuration: TimeInterval = 1.0 33 | ) { 34 | self.bitrate = bitrate 35 | self.resolution = resolution 36 | self.fps = fps 37 | self.gopDuration = gopDuration 38 | } 39 | 40 | /// Creates a video configuration object with default video bitrate and CGSize resolution 41 | /// - Parameters: 42 | /// - resolution: The video resolution 43 | /// - fps: The video framerate 44 | /// - gopDuration: The time interval between two key frames 45 | public init( 46 | resolution: CGSize = CGSize(width: 1_280, height: 720), 47 | fps: Float64 = 30, 48 | gopDuration: TimeInterval = 1.0 49 | ) { 50 | self.bitrate = VideoConfig.getDefaultBitrate(resolution) 51 | self.resolution = resolution 52 | self.fps = fps 53 | self.gopDuration = gopDuration 54 | } 55 | 56 | /// Creates a video configuration object with default video bitrate 57 | /// - Parameters: 58 | /// - resolution: The video resolution 59 | /// - fps: The video framerate 60 | /// - gopDuration: The time interval between two key frames 61 | public init( 62 | resolution: Resolution, 63 | fps: Float64 = 30, 64 | gopDuration: TimeInterval = 1.0 65 | ) { 66 | self.init(resolution: resolution.rawValue, fps: fps, gopDuration: gopDuration) 67 | } 68 | 69 | /// Creates a video configuration object with explicit video bitrate 70 | /// - Parameters: 71 | /// - bitrate: The video bitrate in bits per second 72 | /// - resolution: The video resolution 73 | /// - fps: The video framerate 74 | /// - gopDuration: The time interval between two key frames 75 | public init( 76 | bitrate: Int, 77 | resolution: Resolution, 78 | fps: Float64 = 30, 79 | gopDuration: TimeInterval = 1.0 80 | ) { 81 | self.init(bitrate: bitrate, resolution: resolution.rawValue, fps: fps, gopDuration: gopDuration) 82 | } 83 | 84 | static func getDefaultBitrate(_ size: CGSize) -> Int { 85 | let numOfPixels = size.width * size.height 86 | switch numOfPixels { 87 | case 0 ... 102_240: return 800_000 // for 4/3 and 16/9 240p 88 | case 102_241 ... 230_400: return 1_000_000 // for 16/9 360p 89 | case 230_401 ... 409_920: return 1_300_000 // for 4/3 and 16/9 480p 90 | case 409_921 ... 921_600: return 2_000_000 // for 4/3 600p, 4/3 768p and 16/9 720p 91 | default: return 3_000_000 // for 16/9 1080p 92 | } 93 | } 94 | } 95 | 96 | enum ConfigurationError: Error { 97 | case invalidParameter(String) 98 | } 99 | -------------------------------------------------------------------------------- /Examples/iOS/Settings.bundle/Root.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | Settings 7 | StringsTable 8 | Root 9 | PreferenceSpecifiers 10 | 11 | 12 | Type 13 | PSGroupSpecifier 14 | Title 15 | Endpoint 16 | 17 | 18 | Type 19 | PSTextFieldSpecifier 20 | Title 21 | RTMP endpoint 22 | DefaultValue 23 | rtmp://broadcast.api.video/s/ 24 | Key 25 | RtmpUrl 26 | 27 | 28 | Type 29 | PSTextFieldSpecifier 30 | Title 31 | Stream key 32 | DefaultValue 33 | 34 | Key 35 | StreamKey 36 | 37 | 38 | Type 39 | PSGroupSpecifier 40 | Title 41 | Video 42 | 43 | 44 | Type 45 | PSMultiValueSpecifier 46 | Title 47 | Resolution 48 | Key 49 | Resolution 50 | DefaultValue 51 | 854x480 52 | Values 53 | 54 | 426x240 55 | 640x360 56 | 854x480 57 | 1280x720 58 | 1920x1080 59 | 60 | Titles 61 | 62 | 426x240 63 | 640x360 64 | 854x480 65 | 1280x720 66 | 1920x1080 67 | 68 | 69 | 70 | Type 71 | PSMultiValueSpecifier 72 | Title 73 | Framerate 74 | Key 75 | Framerate 76 | DefaultValue 77 | 30 78 | Values 79 | 80 | 24 81 | 30 82 | 60 83 | 84 | Titles 85 | 86 | 24 87 | 30 88 | 60 89 | 90 | 91 | 92 | Type 93 | PSGroupSpecifier 94 | Title 95 | Bitrate 96 | 97 | 98 | Type 99 | PSTitleValueSpecifier 100 | Title 101 | Bitrate 102 | DefaultValue 103 | 1000 104 | Key 105 | VideoBitrateValue 106 | 107 | 108 | Type 109 | PSSliderSpecifier 110 | Key 111 | VideoBitrateSlider 112 | DefaultValue 113 | 1000 114 | MinimumValue 115 | 1000 116 | MaximumValue 117 | 10000 118 | 119 | 120 | Type 121 | PSGroupSpecifier 122 | Title 123 | Audio 124 | 125 | 126 | Type 127 | PSMultiValueSpecifier 128 | Title 129 | Bitrate 130 | Key 131 | AudioBitrate 132 | DefaultValue 133 | 128000 134 | Values 135 | 136 | 24000 137 | 64000 138 | 128000 139 | 192000 140 | 141 | Titles 142 | 143 | 24 Kbps 144 | 64 Kbps 145 | 128 Kbps 146 | 192 Kbps 147 | 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![badge](https://img.shields.io/twitter/follow/api_video?style=social)](https://twitter.com/intent/follow?screen_name=api_video)   [![badge](https://img.shields.io/github/stars/apivideo/api.video-swift-live-stream?style=social)](https://github.com/apivideo/api.video-swift-live-stream)   [![badge](https://img.shields.io/discourse/topics?server=https%3A%2F%2Fcommunity.api.video)](https://community.api.video) 3 | ![](https://github.com/apivideo/.github/blob/main/assets/apivideo_banner.png) 4 |

Swift RTMP live stream client for iOS

5 | 6 | [api.video](https://api.video) is the video infrastructure for product builders. Lightning fast video APIs for integrating, scaling, and managing on-demand & low latency live streaming features in your app. 7 | 8 | ## Table of contents 9 | 10 | - [Table of contents](#table-of-contents) 11 | - [Project description](#project-description) 12 | - [Getting started](#getting-started) 13 | - [Installation](#installation) 14 | - [With Cocoapods](#with-cocoapods) 15 | - [Permissions](#permissions) 16 | - [Code sample](#code-sample) 17 | - [Plugins](#plugins) 18 | - [FAQ](#faq) 19 | 20 | 21 | 33 | ## Project description 34 | 35 | This module is an easy way to broadcast RTMP live stream to [api.video](https://api.video) platform 36 | 37 | ## Getting started 38 | 39 | ### Installation 40 | 41 | #### Swift Package Manager 42 | 43 | In the Project Navigator select your own project. Then select the project in the Project section and click on the Package Dependencies tab. Click on the "+" button at the bottom. Paste the below url on the search bar on the top right. Finaly click on "Add package" button. 44 | 45 | ``` 46 | https://github.com/api.video-swift-live-stream 47 | ``` 48 | Or add this in your Package.swift 49 | ``` 50 | dependencies: [ 51 | .package(url: "https://github.com/api.video-swift-live-stream.git", from: "1.4.6"), 52 | ], 53 | ``` 54 | 55 | #### Cocoapods 56 | 57 | 1. Add the following entry to your Podfile: 58 | ```swift 59 | pod 'ApiVideoLiveStream', '1.4.6' 60 | ``` 61 | 2. Then run `pod install` 62 | 3. Don’t forget to import `ApiVideoLiveStream` in every file you’d like to use api.video live stream library 63 | 64 | #### Carthage 65 | ``` 66 | github "apivideo/ApiVideoLiveStream.swift" ~> 1.4.6 67 | ``` 68 | 69 | ### Permissions 70 | To be able to broadcast, you must update Info.plist with a usage description for camera and microphone 71 | 72 | ```xml 73 | ... 74 | NSCameraUsageDescription 75 | Your own description of the purpose 76 | NSMicrophoneUsageDescription 77 | Your own description of the purpose 78 | ... 79 | ``` 80 | 81 | ### Code sample 82 | 1. In ViewController.swift import the library 83 | ```swift 84 | import ApiVideoLiveStream 85 | ``` 86 | 2. Create a `ApiVideoLiveStream` object with your default audio and video configuration 87 | ```swift 88 | class ViewController: UIViewController { 89 | var liveStream: ApiVideoLiveStream? 90 | @IBOutlet var viewCamera: UIView! 91 | override func viewDidLoad() { 92 | super.viewDidLoad() 93 | let audioConfig = AudioConfig(bitrate: 32 * 1000) 94 | let videoConfig = VideoConfig(bitrate: 2 * 1024 * 1024, resolution: Resolutions.RESOLUTION_720, fps: 30) 95 | do { 96 | liveStream = try ApiVideoLiveStream(initialAudioConfig: audioConfig, initialVideoConfig: videoConfig, preview: preview) 97 | } catch { 98 | print (error) 99 | } 100 | } 101 | } 102 | ``` 103 | 3. Start your live stream with `startStreaming` 104 | ```swift 105 | liveStream?.startStreaming(streamKey: "YOUR_STREAM_KEY") 106 | ``` 107 | Alternatively, you can use `startStreaming` `url` parameter to set the URL of your RTMP server. 108 | 109 | ## Plugins 110 | 111 | api.video sdk is using external library 112 | 113 | | Plugin | README | 114 | | ------ | ------ | 115 | | HaishinKit | [https://github.com/shogo4405/HaishinKit.swift][HaishinKit] | 116 | 117 | ## FAQ 118 | If you have any questions, ask us here: https://community.api.video . 119 | Or use [Issues]. 120 | 121 | Also feel free to test our [Sample app]. 122 | 123 | [//]: # (These are reference links used in the body of this note and get stripped out when the markdown processor does its job. There is no need to format nicely because it shouldn't be seen. Thanks SO - http://stackoverflow.com/questions/4823468/store-comments-in-markdown-syntax) 124 | 125 | [Issues]: 126 | [HaishinKit]: 127 | [Sample app]: 128 | 129 | 130 | -------------------------------------------------------------------------------- /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 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /Examples/iOS/ViewControllers/MainViewController.swift: -------------------------------------------------------------------------------- 1 | import ApiVideoLiveStream 2 | import AVKit 3 | import InAppSettingsKit 4 | import UIKit 5 | 6 | class MainViewController: UIViewController { 7 | @IBOutlet private var preview: UIView! 8 | private lazy var liveStream: ApiVideoLiveStream = { 9 | var liveStream: ApiVideoLiveStream 10 | do { 11 | // Create liveStream object 12 | let liveStream = try ApiVideoLiveStream( 13 | preview: preview, 14 | initialAudioConfig: SettingsManager.audioConfig, 15 | initialVideoConfig: SettingsManager.videoConfig 16 | ) 17 | 18 | return liveStream 19 | } catch { 20 | fatalError("Can't create liveStream: \(error)") 21 | } 22 | }() 23 | 24 | private let front = UIImage(systemName: "camera.rotate.fill") 25 | private let back = UIImage(systemName: "camera.rotate") 26 | private let mute = UIImage(systemName: "mic.slash.fill") 27 | private let unMute = UIImage(systemName: "mic.fill") 28 | private let parameter = UIImage(systemName: "ellipsis") 29 | 30 | private lazy var zoomGesture: UIPinchGestureRecognizer = .init(target: self, action: #selector(zoom(sender:))) 31 | private let pinchZoomMultiplier: CGFloat = 2.2 32 | 33 | private let muteButton: UIButton = { 34 | let muteButton = UIButton() 35 | muteButton.setTitleColor(.orange, for: .normal) 36 | return muteButton 37 | }() 38 | 39 | private let streamingButton: UIButton = { 40 | let start = UIButton() 41 | start.setTitle("Start", for: .normal) 42 | start.setTitleColor(.orange, for: .normal) 43 | return start 44 | }() 45 | 46 | private let switchButton: UIButton = { 47 | let switchBtn = UIButton() 48 | switchBtn.setTitleColor(.orange, for: .normal) 49 | return switchBtn 50 | }() 51 | 52 | private let parameterButton: UIButton = { 53 | let paramBtn = UIButton() 54 | paramBtn.setTitleColor(.orange, for: .normal) 55 | return paramBtn 56 | }() 57 | 58 | private func callAlert(_ message: String, title: String = "Error", action: @escaping () -> Void = {}) { 59 | let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) 60 | let okAction = UIAlertAction(title: "OK", style: .default) { _ in 61 | action() 62 | } 63 | 64 | alert.addAction(okAction) 65 | DispatchQueue.main.async { 66 | self.present(alert, animated: true, completion: nil) 67 | } 68 | } 69 | 70 | override func viewDidLoad() { 71 | super.viewDidLoad() 72 | 73 | // Register default NSUserDefaults 74 | if let defaultDict = SettingsViewController().settingsReader?.gatherDefaultsLimited(toEditableFields: true) { 75 | UserDefaults.standard.register(defaults: defaultDict) 76 | } 77 | 78 | // Call to explicitly create liveStream 79 | self.liveStream.delegate = self 80 | 81 | view.addSubview(self.muteButton) 82 | view.addSubview(self.streamingButton) 83 | view.addSubview(self.switchButton) 84 | view.addSubview(self.parameterButton) 85 | self.muteButton.setImage(self.unMute, for: .normal) 86 | self.switchButton.setImage(self.back, for: .normal) 87 | self.parameterButton.setImage(self.parameter, for: .normal) 88 | 89 | self.switchButton.addTarget(self, action: #selector(self.toggleSwitch), for: .touchUpInside) 90 | self.muteButton.addTarget(self, action: #selector(self.toggleMute), for: .touchUpInside) 91 | self.streamingButton.addTarget(self, action: #selector(self.toggleStreaming), for: .touchUpInside) 92 | self.parameterButton.addTarget(self, action: #selector(self.navigateToParam), for: .touchUpInside) 93 | 94 | self.preview.addGestureRecognizer(self.zoomGesture) 95 | 96 | self.constraints() 97 | } 98 | 99 | func constraints() { 100 | self.muteButton.translatesAutoresizingMaskIntoConstraints = false 101 | self.streamingButton.translatesAutoresizingMaskIntoConstraints = false 102 | self.switchButton.translatesAutoresizingMaskIntoConstraints = false 103 | self.parameterButton.translatesAutoresizingMaskIntoConstraints = false 104 | 105 | self.muteButton.widthAnchor.constraint(equalToConstant: 70).isActive = true 106 | self.muteButton.heightAnchor.constraint(equalToConstant: 40).isActive = true 107 | self.streamingButton.widthAnchor.constraint(equalToConstant: 70).isActive = true 108 | self.streamingButton.heightAnchor.constraint(equalToConstant: 40).isActive = true 109 | self.switchButton.widthAnchor.constraint(equalToConstant: 80).isActive = true 110 | self.switchButton.heightAnchor.constraint(equalToConstant: 40).isActive = true 111 | self.parameterButton.widthAnchor.constraint(equalToConstant: 40).isActive = true 112 | self.parameterButton.heightAnchor.constraint(equalToConstant: 40).isActive = true 113 | 114 | self.parameterButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 70).isActive = true 115 | self.parameterButton.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20).isActive = true 116 | 117 | self.streamingButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true 118 | self.streamingButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -40).isActive = true 119 | 120 | self.muteButton.centerYAnchor.constraint(equalTo: self.streamingButton.centerYAnchor).isActive = true 121 | self.switchButton.centerYAnchor.constraint(equalTo: self.streamingButton.centerYAnchor).isActive = true 122 | 123 | self.muteButton.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 30).isActive = true 124 | self.switchButton.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -30).isActive = true 125 | } 126 | 127 | private func resetStartButton() { 128 | DispatchQueue.main.async { 129 | self.streamingButton.setTitle("Start", for: []) 130 | self.streamingButton.isSelected = false 131 | } 132 | } 133 | 134 | @objc 135 | func toggleStreaming() { 136 | if self.streamingButton.isSelected { 137 | UIApplication.shared.isIdleTimerDisabled = false 138 | self.liveStream.stopStreaming() 139 | self.resetStartButton() 140 | } else { 141 | UIApplication.shared.isIdleTimerDisabled = true 142 | do { 143 | if SettingsManager.streamKey.isEmpty { 144 | self.callAlert("The stream key is not set. Please set it in Settings.") 145 | return 146 | } 147 | if SettingsManager.rtmpUrl.isEmpty { 148 | self.callAlert("The stream key is not set. Please set it in Settings.") 149 | return 150 | } 151 | 152 | try self.liveStream.startStreaming(streamKey: SettingsManager.streamKey, url: SettingsManager.rtmpUrl) 153 | 154 | self.streamingButton.setTitle("Stop", for: []) 155 | self.streamingButton.isSelected = true 156 | } catch { 157 | self.callAlert("Failed to start streaming: \(error)") 158 | } 159 | } 160 | } 161 | 162 | @objc 163 | func toggleSwitch() { 164 | if self.liveStream.cameraPosition == .front { 165 | self.liveStream.cameraPosition = .back 166 | self.switchButton.setImage(self.back, for: .normal) 167 | } else if self.liveStream.cameraPosition == .back { 168 | self.liveStream.cameraPosition = .front 169 | self.switchButton.setImage(self.front, for: .normal) 170 | } 171 | } 172 | 173 | @objc 174 | func toggleMute() { 175 | self.liveStream.isMuted.toggle() 176 | if self.liveStream.isMuted { 177 | self.muteButton.setImage(self.unMute, for: .normal) 178 | } else { 179 | self.muteButton.setImage(self.mute, for: .normal) 180 | } 181 | } 182 | 183 | @objc 184 | func navigateToParam() { 185 | performSegue(withIdentifier: "paramSegue", sender: self) 186 | } 187 | 188 | private func updateConfig() { 189 | self.liveStream.audioConfig = SettingsManager.audioConfig 190 | self.liveStream.videoConfig = SettingsManager.videoConfig 191 | } 192 | 193 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 194 | super.prepare(for: segue, sender: sender) 195 | if segue.identifier == "paramSegue" { 196 | guard let navigationController = segue.destination as? UINavigationController else { 197 | print("Can't get navigation controller") 198 | return 199 | } 200 | let settingsViewController = SettingsViewController() 201 | settingsViewController.delegate = self 202 | navigationController.pushViewController(settingsViewController, animated: true) 203 | } 204 | } 205 | 206 | @objc 207 | private func zoom(sender: UIPinchGestureRecognizer) { 208 | if sender.state == .changed { 209 | self.liveStream.zoomRatio += (sender.scale - 1) * self.pinchZoomMultiplier 210 | sender.scale = 1 211 | } 212 | } 213 | } 214 | 215 | // MARK: IASKSettingsDelegate 216 | 217 | extension MainViewController: IASKSettingsDelegate { 218 | func settingsViewControllerDidEnd(_ settingsViewController: IASKAppSettingsViewController) { 219 | settingsViewController.dismiss(animated: true, completion: nil) 220 | self.updateConfig() 221 | } 222 | } 223 | 224 | // MARK: ApiVideoLiveStreamDelegate 225 | 226 | extension MainViewController: ApiVideoLiveStreamDelegate { 227 | /// Called when the connection to the rtmp server is successful 228 | func connectionSuccess() { 229 | print("onConnectionSuccess") 230 | } 231 | 232 | /// Called when the connection to the rtmp server failed 233 | func connectionFailed(_: String) { 234 | self.callAlert("Failed to connect to the server") 235 | self.resetStartButton() 236 | } 237 | 238 | /// Called when the connection to the rtmp server is closed 239 | func disconnection() { 240 | print("onDisconnect") 241 | self.resetStartButton() 242 | } 243 | 244 | /// Called if an error happened during the audio configuration 245 | func audioError(_ error: Error) { 246 | self.callAlert("Audio error: \(error.localizedDescription)") 247 | } 248 | 249 | /// Called if an error happened during the video configuration 250 | func videoError(_ error: Error) { 251 | self.callAlert("Video error: \(error.localizedDescription)") 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Sources/ApiVideoLiveStream/ApiVideoLiveStream.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApiVideoLiveStream.swift 3 | // 4 | 5 | import AVFoundation 6 | import Foundation 7 | import HaishinKit 8 | #if !os(macOS) 9 | import UIKit 10 | #endif 11 | import VideoToolbox 12 | 13 | public class ApiVideoLiveStream { 14 | private let rtmpStream: RTMPStream 15 | private let rtmpConnection = RTMPConnection() 16 | 17 | private var streamKey: String = "" 18 | private var url: String = "" 19 | 20 | private var isAudioConfigured = false 21 | private var isVideoConfigured = false 22 | 23 | /// The delegate of the ApiVideoLiveStream 24 | public weak var delegate: ApiVideoLiveStreamDelegate? 25 | 26 | /// Getter and Setter for an AudioConfig 27 | public var audioConfig: AudioConfig { 28 | get { 29 | AudioConfig(bitrate: self.rtmpStream.audioSettings.bitRate) 30 | } 31 | set { 32 | self.prepareAudio(audioConfig: newValue) 33 | } 34 | } 35 | 36 | /// Getter and Setter for a VideoConfig 37 | public var videoConfig: VideoConfig { 38 | get { 39 | VideoConfig( 40 | bitrate: Int(self.rtmpStream.videoSettings.bitRate), 41 | resolution: CGSize( 42 | width: Int(self.rtmpStream.videoSettings.videoSize.width), 43 | height: Int(self.rtmpStream.videoSettings.videoSize.height) 44 | ), 45 | fps: self.rtmpStream.frameRate, 46 | gopDuration: TimeInterval(self.rtmpStream.videoSettings.maxKeyFrameIntervalDuration) 47 | ) 48 | } 49 | set { 50 | self.prepareVideo(videoConfig: newValue) 51 | } 52 | } 53 | 54 | /// Getter and Setter for the Bitrate number for the video 55 | public var videoBitrate: Int { 56 | get { 57 | self.rtmpStream.videoSettings.bitRate 58 | } 59 | set(newValue) { 60 | self.rtmpStream.videoSettings.bitRate = newValue 61 | } 62 | } 63 | 64 | private var lastCamera: AVCaptureDevice? 65 | 66 | /// Camera position 67 | public var cameraPosition: AVCaptureDevice.Position { 68 | get { 69 | guard let position = rtmpStream.videoCapture(for: 0)?.device?.position else { 70 | return AVCaptureDevice.Position.unspecified 71 | } 72 | return position 73 | } 74 | set(newValue) { 75 | self.attachCamera(newValue) 76 | } 77 | } 78 | 79 | /// Camera device 80 | public var camera: AVCaptureDevice? { 81 | get { 82 | self.rtmpStream.videoCapture(for: 0)?.device 83 | } 84 | set(newValue) { 85 | self.attachCamera(newValue) 86 | } 87 | } 88 | 89 | /// Mutes or unmutes audio capture. 90 | public var isMuted: Bool { 91 | get { 92 | self.rtmpStream.audioMixerSettings.isMuted 93 | } 94 | set(newValue) { 95 | self.rtmpStream.audioMixerSettings.isMuted = newValue 96 | } 97 | } 98 | 99 | #if os(iOS) 100 | public var zoomRatioRange: Range { 101 | guard let device = rtmpStream.videoCapture(for: 0)?.device else { 102 | return 1.0 ..< 1.0 103 | } 104 | return 1.0 ..< device.activeFormat.videoMaxZoomFactor 105 | } 106 | 107 | /// Zoom on the video capture 108 | public var zoomRatio: CGFloat { 109 | get { 110 | guard let device = rtmpStream.videoCapture(for: 0)?.device else { 111 | return 1.0 112 | } 113 | return device.videoZoomFactor 114 | } 115 | set(newValue) { 116 | guard let device = rtmpStream.videoCapture(for: 0)?.device, newValue >= 1, 117 | newValue <= device.activeFormat.videoMaxZoomFactor else 118 | { 119 | return 120 | } 121 | do { 122 | try device.lockForConfiguration() 123 | device.videoZoomFactor = newValue 124 | device.unlockForConfiguration() 125 | } catch let error as NSError { 126 | print("Error while locking device for zoom ramp: \(error)") 127 | } 128 | } 129 | } 130 | #endif 131 | 132 | /// Creates a new ApiVideoLiveStream object without a preview 133 | /// - Parameters: 134 | /// - initialAudioConfig: The ApiVideoLiveStream's initial AudioConfig 135 | /// - initialVideoConfig: The ApiVideoLiveStream's initial VideoConfig 136 | /// - initialCamera: The ApiVideoLiveStream's initial camera device 137 | public init( 138 | initialAudioConfig: AudioConfig? = AudioConfig(), 139 | initialVideoConfig: VideoConfig? = VideoConfig(), 140 | initialCamera: AVCaptureDevice? = AVCaptureDevice.default( 141 | .builtInWideAngleCamera, 142 | for: .video, 143 | position: .back 144 | ) 145 | ) throws { 146 | #if os(iOS) 147 | let session = AVAudioSession.sharedInstance() 148 | 149 | // https://stackoverflow.com/questions/51010390/avaudiosession-setcategory-swift-4-2-ios-12-play-sound-on-silent 150 | try session.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker, .allowBluetooth]) 151 | try session.setActive(true) 152 | #endif 153 | 154 | self.rtmpStream = RTMPStream(connection: self.rtmpConnection) 155 | 156 | // Force default resolution because HK default resolution is not supported (480x272) 157 | self.rtmpStream.videoSettings = VideoCodecSettings(videoSize: .init(width: 1_280, height: 720)) 158 | 159 | #if os(iOS) 160 | if let orientation = DeviceUtil.videoOrientation(by: UIApplication.shared.statusBarOrientation) { 161 | self.rtmpStream.videoOrientation = orientation 162 | } 163 | #endif 164 | 165 | if let initialCamera = initialCamera { 166 | self.attachCamera(initialCamera) 167 | } 168 | if let initialVideoConfig = initialVideoConfig { 169 | self.prepareVideo(videoConfig: initialVideoConfig) 170 | } 171 | 172 | self.attachAudio() 173 | if let initialAudioConfig = initialAudioConfig { 174 | self.prepareAudio(audioConfig: initialAudioConfig) 175 | } 176 | 177 | #if !os(macOS) 178 | NotificationCenter.default.addObserver( 179 | self, 180 | selector: #selector(self.didEnterBackground(_:)), 181 | name: UIApplication.didEnterBackgroundNotification, 182 | object: nil 183 | ) 184 | #endif 185 | 186 | self.rtmpConnection.addEventListener(.rtmpStatus, selector: #selector(self.rtmpStatusHandler), observer: self) 187 | self.rtmpConnection.addEventListener(.ioError, selector: #selector(self.rtmpErrorHandler), observer: self) 188 | 189 | #if os(iOS) 190 | NotificationCenter.default.addObserver( 191 | self, 192 | selector: #selector(self.orientationDidChange(_:)), 193 | name: UIDevice.orientationDidChangeNotification, 194 | object: nil 195 | ) 196 | #endif 197 | } 198 | 199 | #if !os(macOS) 200 | /// Creates a new ApiVideoLiveStream object with a UIView as preview 201 | /// - Parameters: 202 | /// - preview: The UIView where to display the preview of camera 203 | /// - initialAudioConfig: The ApiVideoLiveStream's new AudioConfig 204 | /// - initialVideoConfig: The ApiVideoLiveStream's new VideoConfig 205 | /// - initialCamera: The ApiVideoLiveStream's initial camera device 206 | public convenience init( 207 | preview: UIView, 208 | initialAudioConfig: AudioConfig? = AudioConfig(), 209 | initialVideoConfig: VideoConfig? = VideoConfig(), 210 | initialCamera: AVCaptureDevice? = AVCaptureDevice.default( 211 | .builtInWideAngleCamera, 212 | for: .video, 213 | position: .back 214 | ) 215 | ) throws { 216 | try self.init( 217 | initialAudioConfig: initialAudioConfig, 218 | initialVideoConfig: initialVideoConfig, 219 | initialCamera: initialCamera 220 | ) 221 | 222 | let mthkView = MTHKView(frame: preview.bounds) 223 | mthkView.translatesAutoresizingMaskIntoConstraints = false 224 | mthkView.videoGravity = AVLayerVideoGravity.resizeAspectFill 225 | mthkView.attachStream(self.rtmpStream) 226 | 227 | preview.addSubview(mthkView) 228 | 229 | let maxWidth = mthkView.widthAnchor.constraint(lessThanOrEqualTo: preview.widthAnchor) 230 | let maxHeight = mthkView.heightAnchor.constraint(lessThanOrEqualTo: preview.heightAnchor) 231 | let width = mthkView.widthAnchor.constraint(equalTo: preview.widthAnchor) 232 | let height = mthkView.heightAnchor.constraint(equalTo: preview.heightAnchor) 233 | let centerX = mthkView.centerXAnchor.constraint(equalTo: preview.centerXAnchor) 234 | let centerY = mthkView.centerYAnchor.constraint(equalTo: preview.centerYAnchor) 235 | 236 | width.priority = .defaultHigh 237 | height.priority = .defaultHigh 238 | 239 | NSLayoutConstraint.activate([ 240 | maxWidth, maxHeight, width, height, centerX, centerY 241 | ]) 242 | } 243 | #endif 244 | 245 | /// Creates a new ApiVideoLiveStream object with a NetStreamDrawable 246 | /// - Parameters: 247 | /// - preview: The NetStreamDrawable where to display the preview of camera 248 | /// - initialAudioConfig: The ApiVideoLiveStream's new AudioConfig 249 | /// - initialVideoConfig: The ApiVideoLiveStream's new VideoConfig 250 | /// - initialCamera: The ApiVideoLiveStream's initial camera device 251 | public convenience init( 252 | preview: IOStreamView, 253 | initialAudioConfig: AudioConfig? = AudioConfig(), 254 | initialVideoConfig: VideoConfig? = VideoConfig(), 255 | initialCamera: AVCaptureDevice? = AVCaptureDevice.default( 256 | .builtInWideAngleCamera, 257 | for: .video, 258 | position: .back 259 | ) 260 | ) throws { 261 | try self.init( 262 | initialAudioConfig: initialAudioConfig, 263 | initialVideoConfig: initialVideoConfig, 264 | initialCamera: initialCamera 265 | ) 266 | preview.attachStream(self.rtmpStream) 267 | } 268 | 269 | deinit { 270 | #if os(iOS) 271 | NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil) 272 | #endif 273 | #if !os(macOS) 274 | NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil) 275 | #endif 276 | rtmpConnection.removeEventListener(.rtmpStatus, selector: #selector(rtmpStatusHandler), observer: self) 277 | rtmpConnection.removeEventListener(.ioError, selector: #selector(rtmpErrorHandler), observer: self) 278 | } 279 | 280 | private func attachCamera(_ cameraPosition: AVCaptureDevice.Position) { 281 | let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: cameraPosition) 282 | self.attachCamera(camera) 283 | } 284 | 285 | private func attachCamera(_ camera: AVCaptureDevice?) { 286 | self.lastCamera = camera 287 | 288 | self.rtmpStream.attachCamera(camera) { videoCaptureUnit, error in 289 | if let error { 290 | print("======== Camera error ==========") 291 | print(error) 292 | self.delegate?.videoError(error) 293 | return 294 | } 295 | 296 | if let camera { 297 | videoCaptureUnit?.isVideoMirrored = camera.position == .front 298 | } 299 | #if os(iOS) 300 | // videoCaptureUnit.preferredVideoStabilizationMode = AVCaptureVideoStabilizationMode 301 | // .auto // Add latency to video 302 | #endif 303 | 304 | guard let device = videoCaptureUnit?.device else { 305 | return 306 | } 307 | self.rtmpStream.lockQueue.async { 308 | do { 309 | try device.lockForConfiguration() 310 | if device.isExposureModeSupported(.continuousAutoExposure) { 311 | device.exposureMode = .continuousAutoExposure 312 | } 313 | if device.isFocusModeSupported(.continuousAutoFocus) { 314 | device.focusMode = .continuousAutoFocus 315 | } 316 | device.unlockForConfiguration() 317 | } catch { 318 | print("Could not lock device for exposure and focus: \(error)") 319 | } 320 | } 321 | } 322 | } 323 | 324 | private func prepareVideo(videoConfig: VideoConfig) { 325 | self.rtmpStream.frameRate = videoConfig.fps 326 | self.rtmpStream.sessionPreset = AVCaptureSession.Preset.high 327 | 328 | let resolution = videoConfig.resolution 329 | let width = self.rtmpStream.videoOrientation 330 | .isLandscape ? max(resolution.width, resolution.height) : min(resolution.width, resolution.height) 331 | let height = self.rtmpStream.videoOrientation 332 | .isLandscape ? min(resolution.width, resolution.height) : max(resolution.width, resolution.height) 333 | 334 | self.rtmpStream.videoSettings = VideoCodecSettings( 335 | videoSize: CGSize(width: width, height: height), 336 | bitRate: videoConfig.bitrate, 337 | profileLevel: kVTProfileLevel_H264_Baseline_5_2 as String, 338 | maxKeyFrameIntervalDuration: Int32(videoConfig.gopDuration) 339 | ) 340 | 341 | self.isVideoConfigured = true 342 | } 343 | 344 | private func attachAudio() { 345 | self.rtmpStream.attachAudio(AVCaptureDevice.default(for: AVMediaType.audio)) { _, error in 346 | if let error { 347 | print("======== Audio error ==========") 348 | print(error) 349 | self.delegate?.audioError(error) 350 | } 351 | } 352 | } 353 | 354 | private func prepareAudio(audioConfig: AudioConfig) { 355 | self.rtmpStream.audioSettings.bitRate = audioConfig.bitrate 356 | 357 | self.isAudioConfigured = true 358 | } 359 | 360 | /// Start your livestream 361 | /// - Parameters: 362 | /// - streamKey: The key of your live 363 | /// - url: The url of your rtmp server, by default it's rtmp://broadcast.api.video/s 364 | /// - Returns: Void 365 | public func startStreaming(streamKey: String, url: String = "rtmp://broadcast.api.video/s") throws { 366 | if streamKey.isEmpty { 367 | throw LiveStreamError.IllegalArgumentError("Stream key must not be empty") 368 | } 369 | if url.isEmpty { 370 | throw LiveStreamError.IllegalArgumentError("URL must not be empty") 371 | } 372 | if !self.isAudioConfigured || !self.isVideoConfigured { 373 | throw LiveStreamError.IllegalOperationError("Missing audio and/or video configuration") 374 | } 375 | 376 | self.streamKey = streamKey 377 | self.url = url 378 | 379 | self.rtmpStream.fcPublishName = streamKey 380 | self.rtmpConnection.connect(url) 381 | } 382 | 383 | /// Stop your livestream 384 | /// - Returns: Void 385 | public func stopStreaming() { 386 | let isConnected = self.rtmpConnection.connected 387 | self.rtmpConnection.close() 388 | if isConnected { 389 | self.delegate?.disconnection() 390 | } 391 | } 392 | 393 | public func startPreview() { 394 | guard let lastCamera = lastCamera else { 395 | print("No camera has been set") 396 | return 397 | } 398 | self.attachCamera(lastCamera) 399 | self.attachAudio() 400 | } 401 | 402 | public func stopPreview() { 403 | self.rtmpStream.attachCamera(nil) 404 | self.rtmpStream.attachAudio(nil) 405 | } 406 | 407 | @objc 408 | private func rtmpStatusHandler(_ notification: Notification) { 409 | let e = Event.from(notification) 410 | guard let data: ASObject = e.data as? ASObject, 411 | let code: String = data["code"] as? String, 412 | let level: String = data["level"] as? String else 413 | { 414 | print("rtmpStatusHandler: failed to parse event: \(e)") 415 | return 416 | } 417 | switch code { 418 | case RTMPConnection.Code.connectSuccess.rawValue: 419 | self.rtmpStream.publish(self.streamKey) 420 | 421 | case RTMPStream.Code.publishStart.rawValue: 422 | self.delegate?.connectionSuccess() 423 | 424 | case RTMPConnection.Code.connectClosed.rawValue: 425 | self.delegate?.disconnection() 426 | 427 | default: 428 | if level == "error" { 429 | self.delegate?.connectionFailed(code) 430 | } 431 | } 432 | } 433 | 434 | @objc 435 | private func rtmpErrorHandler(_ notification: Notification) { 436 | let e = Event.from(notification) 437 | print("rtmpErrorHandler: \(e)") 438 | DispatchQueue.main.async { 439 | self.rtmpConnection.connect(self.url) 440 | } 441 | } 442 | 443 | #if os(iOS) 444 | @objc 445 | private func orientationDidChange(_: Notification) { 446 | guard let orientation = DeviceUtil.videoOrientation(by: UIApplication.shared.statusBarOrientation) else { 447 | return 448 | } 449 | 450 | self.rtmpStream.lockQueue.async { 451 | self.rtmpStream.videoOrientation = orientation 452 | 453 | let currentVideoSize = self.rtmpStream.videoSettings.videoSize 454 | var newVideoSize: CGSize 455 | if self.rtmpStream.videoOrientation.isLandscape { 456 | newVideoSize = CGSize( 457 | width: max(currentVideoSize.width, currentVideoSize.height), 458 | height: min(currentVideoSize.width, currentVideoSize.height) 459 | ) 460 | } else { 461 | newVideoSize = CGSize( 462 | width: min(currentVideoSize.width, currentVideoSize.height), 463 | height: max(currentVideoSize.width, currentVideoSize.height) 464 | ) 465 | } 466 | self.rtmpStream.videoSettings.videoSize = newVideoSize 467 | } 468 | } 469 | #endif 470 | 471 | #if !os(macOS) 472 | @objc 473 | private func didEnterBackground(_: Notification) { 474 | self.stopStreaming() 475 | } 476 | #endif 477 | } 478 | 479 | public protocol ApiVideoLiveStreamDelegate: AnyObject { 480 | /// Called when the connection to the rtmp server is successful 481 | func connectionSuccess() 482 | 483 | /// Called when the connection to the rtmp server failed 484 | func connectionFailed(_ code: String) 485 | 486 | /// Called when the connection to the rtmp server is closed 487 | func disconnection() 488 | 489 | /// Called if an error happened during the audio configuration 490 | func audioError(_ error: Error) 491 | 492 | /// Called if an error happened during the video configuration 493 | func videoError(_ error: Error) 494 | } 495 | 496 | extension AVCaptureVideoOrientation { 497 | var isLandscape: Bool { 498 | self == .landscapeLeft || self == .landscapeRight 499 | } 500 | } 501 | 502 | public enum LiveStreamError: Error { 503 | case IllegalArgumentError(String) 504 | case IllegalOperationError(String) 505 | } 506 | -------------------------------------------------------------------------------- /ApiVideoLiveStream.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2102CD142BA9E0E700D0EBAD /* ApiVideoLiveStream.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2102CD092BA9E0E700D0EBAD /* ApiVideoLiveStream.framework */; }; 11 | 21BF485B2C10B54300EAEB5B /* HaishinKit in Frameworks */ = {isa = PBXBuildFile; productRef = 21BF485A2C10B54300EAEB5B /* HaishinKit */; }; 12 | 21BF485E2C10BAAF00EAEB5B /* InAppSettingsKit in Frameworks */ = {isa = PBXBuildFile; productRef = 21BF485D2C10BAAF00EAEB5B /* InAppSettingsKit */; }; 13 | 21CCD6A02BBC44EE00E58F5D /* ApiVideoLiveStream.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2102CD092BA9E0E700D0EBAD /* ApiVideoLiveStream.framework */; }; 14 | 21CCD6A12BBC44EE00E58F5D /* ApiVideoLiveStream.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 2102CD092BA9E0E700D0EBAD /* ApiVideoLiveStream.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 15 | 21E3588F2C10A35300D856D9 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21E358802C10A35300D856D9 /* MainViewController.swift */; }; 16 | 21E358902C10A35300D856D9 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21E358812C10A35300D856D9 /* SettingsViewController.swift */; }; 17 | 21E358912C10A35300D856D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21E358832C10A35300D856D9 /* AppDelegate.swift */; }; 18 | 21E358922C10A35300D856D9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 21E358842C10A35300D856D9 /* Assets.xcassets */; }; 19 | 21E358942C10A35300D856D9 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 21E358872C10A35300D856D9 /* LaunchScreen.storyboard */; }; 20 | 21E358952C10A35300D856D9 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 21E358892C10A35300D856D9 /* Main.storyboard */; }; 21 | 21E358962C10A35300D856D9 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21E3588A2C10A35300D856D9 /* SceneDelegate.swift */; }; 22 | 21E358972C10A35300D856D9 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 21E3588B2C10A35300D856D9 /* Settings.bundle */; }; 23 | 21E358982C10A35300D856D9 /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21E3588C2C10A35300D856D9 /* SettingsManager.swift */; }; 24 | 21E3589D2C10A37100D856D9 /* ApiVideoLiveStreamTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21E358992C10A37000D856D9 /* ApiVideoLiveStreamTests.swift */; }; 25 | 21E3589E2C10A37100D856D9 /* Parameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21E3589A2C10A37000D856D9 /* Parameters.swift */; }; 26 | 21E358A52C10A39400D856D9 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21E3589F2C10A39400D856D9 /* Configuration.swift */; }; 27 | 21E358A62C10A39400D856D9 /* Resolution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21E358A02C10A39400D856D9 /* Resolution.swift */; }; 28 | 21E358A72C10A39400D856D9 /* ApiVideoLiveStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21E358A22C10A39400D856D9 /* ApiVideoLiveStream.swift */; }; 29 | /* End PBXBuildFile section */ 30 | 31 | /* Begin PBXBuildRule section */ 32 | 21CCD6A92BBC4A9800E58F5D /* PBXBuildRule */ = { 33 | isa = PBXBuildRule; 34 | compilerSpec = com.apple.compilers.proxy.script; 35 | fileType = text.plist; 36 | inputFiles = ( 37 | ); 38 | isEditable = 1; 39 | outputFiles = ( 40 | ); 41 | script = "# builtin-copyPlist\n"; 42 | }; 43 | /* End PBXBuildRule section */ 44 | 45 | /* Begin PBXContainerItemProxy section */ 46 | 2102CD152BA9E0E700D0EBAD /* PBXContainerItemProxy */ = { 47 | isa = PBXContainerItemProxy; 48 | containerPortal = 2102CD002BA9E0E600D0EBAD /* Project object */; 49 | proxyType = 1; 50 | remoteGlobalIDString = 2102CD082BA9E0E700D0EBAD; 51 | remoteInfo = Databus; 52 | }; 53 | 21CCD6A22BBC44EF00E58F5D /* PBXContainerItemProxy */ = { 54 | isa = PBXContainerItemProxy; 55 | containerPortal = 2102CD002BA9E0E600D0EBAD /* Project object */; 56 | proxyType = 1; 57 | remoteGlobalIDString = 2102CD082BA9E0E700D0EBAD; 58 | remoteInfo = ApiVideoDatabus; 59 | }; 60 | /* End PBXContainerItemProxy section */ 61 | 62 | /* Begin PBXCopyFilesBuildPhase section */ 63 | 21CCD6A42BBC44EF00E58F5D /* Embed Frameworks */ = { 64 | isa = PBXCopyFilesBuildPhase; 65 | buildActionMask = 2147483647; 66 | dstPath = ""; 67 | dstSubfolderSpec = 10; 68 | files = ( 69 | 21CCD6A12BBC44EE00E58F5D /* ApiVideoLiveStream.framework in Embed Frameworks */, 70 | ); 71 | name = "Embed Frameworks"; 72 | runOnlyForDeploymentPostprocessing = 0; 73 | }; 74 | /* End PBXCopyFilesBuildPhase section */ 75 | 76 | /* Begin PBXFileReference section */ 77 | 2102CD092BA9E0E700D0EBAD /* ApiVideoLiveStream.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ApiVideoLiveStream.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 78 | 2102CD132BA9E0E700D0EBAD /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 79 | 2117CBCE2BB1EBCA00B85BF0 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; 80 | 2117CBD02BB1EBCA00B85BF0 /* CONTRIBUTING.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = ""; }; 81 | 2117CBD12BB1EBCA00B85BF0 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 82 | 2117CBDC2BB1EE3100B85BF0 /* Example iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Example iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 83 | 2117CC072BB1F1F900B85BF0 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 84 | 2126C5132BCD47DA006BEDF3 /* LICENSE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = ""; }; 85 | 2126C5142BCD4D6E006BEDF3 /* build.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = build.yml; sourceTree = ""; }; 86 | 2126C5152BCD4D6E006BEDF3 /* create-documentation-pr.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = "create-documentation-pr.yml"; sourceTree = ""; }; 87 | 2126C5162BCD4D6E006BEDF3 /* create-release-from-changelog.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = "create-release-from-changelog.yml"; sourceTree = ""; }; 88 | 2126C5172BCD4D6E006BEDF3 /* release.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = release.yml; sourceTree = ""; }; 89 | 21E3585C2C10A27000D856D9 /* Cartfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Cartfile; sourceTree = ""; }; 90 | 21E3585D2C10A27000D856D9 /* ApiVideoLiveStream.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = ApiVideoLiveStream.podspec; sourceTree = ""; }; 91 | 21E358802C10A35300D856D9 /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; 92 | 21E358812C10A35300D856D9 /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; 93 | 21E358832C10A35300D856D9 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 94 | 21E358842C10A35300D856D9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 95 | 21E358852C10A35300D856D9 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 96 | 21E358862C10A35300D856D9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97 | 21E358882C10A35300D856D9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 98 | 21E3588A2C10A35300D856D9 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 99 | 21E3588B2C10A35300D856D9 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; 100 | 21E3588C2C10A35300D856D9 /* SettingsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsManager.swift; sourceTree = ""; }; 101 | 21E358992C10A37000D856D9 /* ApiVideoLiveStreamTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiVideoLiveStreamTests.swift; sourceTree = ""; }; 102 | 21E3589A2C10A37000D856D9 /* Parameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Parameters.swift; sourceTree = ""; }; 103 | 21E3589F2C10A39400D856D9 /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; 104 | 21E358A02C10A39400D856D9 /* Resolution.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Resolution.swift; sourceTree = ""; }; 105 | 21E358A22C10A39400D856D9 /* ApiVideoLiveStream.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiVideoLiveStream.swift; sourceTree = ""; }; 106 | /* End PBXFileReference section */ 107 | 108 | /* Begin PBXFrameworksBuildPhase section */ 109 | 2102CD062BA9E0E700D0EBAD /* Frameworks */ = { 110 | isa = PBXFrameworksBuildPhase; 111 | buildActionMask = 2147483647; 112 | files = ( 113 | 21BF485B2C10B54300EAEB5B /* HaishinKit in Frameworks */, 114 | ); 115 | runOnlyForDeploymentPostprocessing = 0; 116 | }; 117 | 2102CD102BA9E0E700D0EBAD /* Frameworks */ = { 118 | isa = PBXFrameworksBuildPhase; 119 | buildActionMask = 2147483647; 120 | files = ( 121 | 2102CD142BA9E0E700D0EBAD /* ApiVideoLiveStream.framework in Frameworks */, 122 | ); 123 | runOnlyForDeploymentPostprocessing = 0; 124 | }; 125 | 2117CBD92BB1EE3100B85BF0 /* Frameworks */ = { 126 | isa = PBXFrameworksBuildPhase; 127 | buildActionMask = 2147483647; 128 | files = ( 129 | 21CCD6A02BBC44EE00E58F5D /* ApiVideoLiveStream.framework in Frameworks */, 130 | 21BF485E2C10BAAF00EAEB5B /* InAppSettingsKit in Frameworks */, 131 | ); 132 | runOnlyForDeploymentPostprocessing = 0; 133 | }; 134 | /* End PBXFrameworksBuildPhase section */ 135 | 136 | /* Begin PBXGroup section */ 137 | 2102CCFF2BA9E0E600D0EBAD = { 138 | isa = PBXGroup; 139 | children = ( 140 | 2126C51B2BCD4D6E006BEDF3 /* .github */, 141 | 21E358A42C10A39400D856D9 /* Sources */, 142 | 21E3588E2C10A35300D856D9 /* Examples */, 143 | 21E3589C2C10A37000D856D9 /* Tests */, 144 | 2117CBCE2BB1EBCA00B85BF0 /* CHANGELOG.md */, 145 | 2117CBD02BB1EBCA00B85BF0 /* CONTRIBUTING.md */, 146 | 2126C5132BCD47DA006BEDF3 /* LICENSE.md */, 147 | 2117CBD12BB1EBCA00B85BF0 /* README.md */, 148 | 21E3585C2C10A27000D856D9 /* Cartfile */, 149 | 21E3585D2C10A27000D856D9 /* ApiVideoLiveStream.podspec */, 150 | 2117CC072BB1F1F900B85BF0 /* Package.swift */, 151 | 2102CD0A2BA9E0E700D0EBAD /* Products */, 152 | 21CCD69F2BBC44EE00E58F5D /* Frameworks */, 153 | ); 154 | sourceTree = ""; 155 | }; 156 | 2102CD0A2BA9E0E700D0EBAD /* Products */ = { 157 | isa = PBXGroup; 158 | children = ( 159 | 2102CD092BA9E0E700D0EBAD /* ApiVideoLiveStream.framework */, 160 | 2102CD132BA9E0E700D0EBAD /* Tests.xctest */, 161 | 2117CBDC2BB1EE3100B85BF0 /* Example iOS.app */, 162 | ); 163 | name = Products; 164 | sourceTree = ""; 165 | }; 166 | 2126C51A2BCD4D6E006BEDF3 /* workflows */ = { 167 | isa = PBXGroup; 168 | children = ( 169 | 2126C5142BCD4D6E006BEDF3 /* build.yml */, 170 | 2126C5152BCD4D6E006BEDF3 /* create-documentation-pr.yml */, 171 | 2126C5162BCD4D6E006BEDF3 /* create-release-from-changelog.yml */, 172 | 2126C5172BCD4D6E006BEDF3 /* release.yml */, 173 | ); 174 | path = workflows; 175 | sourceTree = ""; 176 | }; 177 | 2126C51B2BCD4D6E006BEDF3 /* .github */ = { 178 | isa = PBXGroup; 179 | children = ( 180 | 2126C51A2BCD4D6E006BEDF3 /* workflows */, 181 | ); 182 | path = .github; 183 | sourceTree = ""; 184 | }; 185 | 21CCD69F2BBC44EE00E58F5D /* Frameworks */ = { 186 | isa = PBXGroup; 187 | children = ( 188 | ); 189 | name = Frameworks; 190 | sourceTree = ""; 191 | }; 192 | 21E358822C10A35300D856D9 /* ViewControllers */ = { 193 | isa = PBXGroup; 194 | children = ( 195 | 21E358802C10A35300D856D9 /* MainViewController.swift */, 196 | 21E358812C10A35300D856D9 /* SettingsViewController.swift */, 197 | ); 198 | path = ViewControllers; 199 | sourceTree = ""; 200 | }; 201 | 21E3588D2C10A35300D856D9 /* iOS */ = { 202 | isa = PBXGroup; 203 | children = ( 204 | 21E358822C10A35300D856D9 /* ViewControllers */, 205 | 21E358832C10A35300D856D9 /* AppDelegate.swift */, 206 | 21E358842C10A35300D856D9 /* Assets.xcassets */, 207 | 21E358852C10A35300D856D9 /* Info.plist */, 208 | 21E358872C10A35300D856D9 /* LaunchScreen.storyboard */, 209 | 21E358892C10A35300D856D9 /* Main.storyboard */, 210 | 21E3588A2C10A35300D856D9 /* SceneDelegate.swift */, 211 | 21E3588B2C10A35300D856D9 /* Settings.bundle */, 212 | 21E3588C2C10A35300D856D9 /* SettingsManager.swift */, 213 | ); 214 | path = iOS; 215 | sourceTree = ""; 216 | }; 217 | 21E3588E2C10A35300D856D9 /* Examples */ = { 218 | isa = PBXGroup; 219 | children = ( 220 | 21E3588D2C10A35300D856D9 /* iOS */, 221 | ); 222 | path = Examples; 223 | sourceTree = ""; 224 | }; 225 | 21E3589B2C10A37000D856D9 /* ApiVideoLiveStream */ = { 226 | isa = PBXGroup; 227 | children = ( 228 | 21E358992C10A37000D856D9 /* ApiVideoLiveStreamTests.swift */, 229 | 21E3589A2C10A37000D856D9 /* Parameters.swift */, 230 | ); 231 | path = ApiVideoLiveStream; 232 | sourceTree = ""; 233 | }; 234 | 21E3589C2C10A37000D856D9 /* Tests */ = { 235 | isa = PBXGroup; 236 | children = ( 237 | 21E3589B2C10A37000D856D9 /* ApiVideoLiveStream */, 238 | ); 239 | path = Tests; 240 | sourceTree = ""; 241 | }; 242 | 21E358A12C10A39400D856D9 /* models */ = { 243 | isa = PBXGroup; 244 | children = ( 245 | 21E3589F2C10A39400D856D9 /* Configuration.swift */, 246 | 21E358A02C10A39400D856D9 /* Resolution.swift */, 247 | ); 248 | path = models; 249 | sourceTree = ""; 250 | }; 251 | 21E358A32C10A39400D856D9 /* ApiVideoLiveStream */ = { 252 | isa = PBXGroup; 253 | children = ( 254 | 21E358A12C10A39400D856D9 /* models */, 255 | 21E358A22C10A39400D856D9 /* ApiVideoLiveStream.swift */, 256 | ); 257 | path = ApiVideoLiveStream; 258 | sourceTree = ""; 259 | }; 260 | 21E358A42C10A39400D856D9 /* Sources */ = { 261 | isa = PBXGroup; 262 | children = ( 263 | 21E358A32C10A39400D856D9 /* ApiVideoLiveStream */, 264 | ); 265 | path = Sources; 266 | sourceTree = ""; 267 | }; 268 | /* End PBXGroup section */ 269 | 270 | /* Begin PBXHeadersBuildPhase section */ 271 | 2102CD042BA9E0E700D0EBAD /* Headers */ = { 272 | isa = PBXHeadersBuildPhase; 273 | buildActionMask = 2147483647; 274 | files = ( 275 | ); 276 | runOnlyForDeploymentPostprocessing = 0; 277 | }; 278 | /* End PBXHeadersBuildPhase section */ 279 | 280 | /* Begin PBXNativeTarget section */ 281 | 2102CD082BA9E0E700D0EBAD /* ApiVideoLiveStream */ = { 282 | isa = PBXNativeTarget; 283 | buildConfigurationList = 2102CD1D2BA9E0E700D0EBAD /* Build configuration list for PBXNativeTarget "ApiVideoLiveStream" */; 284 | buildPhases = ( 285 | 2102CD042BA9E0E700D0EBAD /* Headers */, 286 | 2102CD052BA9E0E700D0EBAD /* Sources */, 287 | 2102CD062BA9E0E700D0EBAD /* Frameworks */, 288 | 2102CD072BA9E0E700D0EBAD /* Resources */, 289 | ); 290 | buildRules = ( 291 | ); 292 | dependencies = ( 293 | ); 294 | name = ApiVideoLiveStream; 295 | packageProductDependencies = ( 296 | 21BF485A2C10B54300EAEB5B /* HaishinKit */, 297 | ); 298 | productName = Databus; 299 | productReference = 2102CD092BA9E0E700D0EBAD /* ApiVideoLiveStream.framework */; 300 | productType = "com.apple.product-type.framework"; 301 | }; 302 | 2102CD122BA9E0E700D0EBAD /* Tests */ = { 303 | isa = PBXNativeTarget; 304 | buildConfigurationList = 2102CD202BA9E0E700D0EBAD /* Build configuration list for PBXNativeTarget "Tests" */; 305 | buildPhases = ( 306 | 2102CD0F2BA9E0E700D0EBAD /* Sources */, 307 | 2102CD102BA9E0E700D0EBAD /* Frameworks */, 308 | 2102CD112BA9E0E700D0EBAD /* Resources */, 309 | ); 310 | buildRules = ( 311 | ); 312 | dependencies = ( 313 | 2102CD162BA9E0E700D0EBAD /* PBXTargetDependency */, 314 | ); 315 | name = Tests; 316 | productName = DatabusTests; 317 | productReference = 2102CD132BA9E0E700D0EBAD /* Tests.xctest */; 318 | productType = "com.apple.product-type.bundle.unit-test"; 319 | }; 320 | 2117CBDB2BB1EE3100B85BF0 /* Example iOS */ = { 321 | isa = PBXNativeTarget; 322 | buildConfigurationList = 2117CBED2BB1EE3300B85BF0 /* Build configuration list for PBXNativeTarget "Example iOS" */; 323 | buildPhases = ( 324 | 2117CBD82BB1EE3100B85BF0 /* Sources */, 325 | 2117CBD92BB1EE3100B85BF0 /* Frameworks */, 326 | 2117CBDA2BB1EE3100B85BF0 /* Resources */, 327 | 21CCD6A42BBC44EF00E58F5D /* Embed Frameworks */, 328 | ); 329 | buildRules = ( 330 | 21CCD6A92BBC4A9800E58F5D /* PBXBuildRule */, 331 | ); 332 | dependencies = ( 333 | 21CCD6A32BBC44EF00E58F5D /* PBXTargetDependency */, 334 | ); 335 | name = "Example iOS"; 336 | packageProductDependencies = ( 337 | 21BF485D2C10BAAF00EAEB5B /* InAppSettingsKit */, 338 | ); 339 | productName = iOS; 340 | productReference = 2117CBDC2BB1EE3100B85BF0 /* Example iOS.app */; 341 | productType = "com.apple.product-type.application"; 342 | }; 343 | /* End PBXNativeTarget section */ 344 | 345 | /* Begin PBXProject section */ 346 | 2102CD002BA9E0E600D0EBAD /* Project object */ = { 347 | isa = PBXProject; 348 | attributes = { 349 | BuildIndependentTargetsInParallel = 1; 350 | LastSwiftUpdateCheck = 1530; 351 | LastUpgradeCheck = 1540; 352 | TargetAttributes = { 353 | 2102CD082BA9E0E700D0EBAD = { 354 | CreatedOnToolsVersion = 15.2; 355 | }; 356 | 2102CD122BA9E0E700D0EBAD = { 357 | CreatedOnToolsVersion = 15.2; 358 | }; 359 | 2117CBDB2BB1EE3100B85BF0 = { 360 | CreatedOnToolsVersion = 15.3; 361 | }; 362 | }; 363 | }; 364 | buildConfigurationList = 2102CD032BA9E0E600D0EBAD /* Build configuration list for PBXProject "ApiVideoLiveStream" */; 365 | compatibilityVersion = "Xcode 14.0"; 366 | developmentRegion = en; 367 | hasScannedForEncodings = 0; 368 | knownRegions = ( 369 | en, 370 | Base, 371 | ); 372 | mainGroup = 2102CCFF2BA9E0E600D0EBAD; 373 | packageReferences = ( 374 | 21E358A82C10A54D00D856D9 /* XCRemoteSwiftPackageReference "HaishinKit" */, 375 | 21BF485C2C10BA9A00EAEB5B /* XCRemoteSwiftPackageReference "InAppSettingsKit" */, 376 | ); 377 | productRefGroup = 2102CD0A2BA9E0E700D0EBAD /* Products */; 378 | projectDirPath = ""; 379 | projectRoot = ""; 380 | targets = ( 381 | 2102CD082BA9E0E700D0EBAD /* ApiVideoLiveStream */, 382 | 2102CD122BA9E0E700D0EBAD /* Tests */, 383 | 2117CBDB2BB1EE3100B85BF0 /* Example iOS */, 384 | ); 385 | }; 386 | /* End PBXProject section */ 387 | 388 | /* Begin PBXResourcesBuildPhase section */ 389 | 2102CD072BA9E0E700D0EBAD /* Resources */ = { 390 | isa = PBXResourcesBuildPhase; 391 | buildActionMask = 2147483647; 392 | files = ( 393 | ); 394 | runOnlyForDeploymentPostprocessing = 0; 395 | }; 396 | 2102CD112BA9E0E700D0EBAD /* Resources */ = { 397 | isa = PBXResourcesBuildPhase; 398 | buildActionMask = 2147483647; 399 | files = ( 400 | ); 401 | runOnlyForDeploymentPostprocessing = 0; 402 | }; 403 | 2117CBDA2BB1EE3100B85BF0 /* Resources */ = { 404 | isa = PBXResourcesBuildPhase; 405 | buildActionMask = 2147483647; 406 | files = ( 407 | 21E358942C10A35300D856D9 /* LaunchScreen.storyboard in Resources */, 408 | 21E358972C10A35300D856D9 /* Settings.bundle in Resources */, 409 | 21E358922C10A35300D856D9 /* Assets.xcassets in Resources */, 410 | 21E358952C10A35300D856D9 /* Main.storyboard in Resources */, 411 | ); 412 | runOnlyForDeploymentPostprocessing = 0; 413 | }; 414 | /* End PBXResourcesBuildPhase section */ 415 | 416 | /* Begin PBXSourcesBuildPhase section */ 417 | 2102CD052BA9E0E700D0EBAD /* Sources */ = { 418 | isa = PBXSourcesBuildPhase; 419 | buildActionMask = 2147483647; 420 | files = ( 421 | 21E358A52C10A39400D856D9 /* Configuration.swift in Sources */, 422 | 21E358A62C10A39400D856D9 /* Resolution.swift in Sources */, 423 | 21E358A72C10A39400D856D9 /* ApiVideoLiveStream.swift in Sources */, 424 | ); 425 | runOnlyForDeploymentPostprocessing = 0; 426 | }; 427 | 2102CD0F2BA9E0E700D0EBAD /* Sources */ = { 428 | isa = PBXSourcesBuildPhase; 429 | buildActionMask = 2147483647; 430 | files = ( 431 | 21E3589D2C10A37100D856D9 /* ApiVideoLiveStreamTests.swift in Sources */, 432 | 21E3589E2C10A37100D856D9 /* Parameters.swift in Sources */, 433 | ); 434 | runOnlyForDeploymentPostprocessing = 0; 435 | }; 436 | 2117CBD82BB1EE3100B85BF0 /* Sources */ = { 437 | isa = PBXSourcesBuildPhase; 438 | buildActionMask = 2147483647; 439 | files = ( 440 | 21E358912C10A35300D856D9 /* AppDelegate.swift in Sources */, 441 | 21E358982C10A35300D856D9 /* SettingsManager.swift in Sources */, 442 | 21E3588F2C10A35300D856D9 /* MainViewController.swift in Sources */, 443 | 21E358902C10A35300D856D9 /* SettingsViewController.swift in Sources */, 444 | 21E358962C10A35300D856D9 /* SceneDelegate.swift in Sources */, 445 | ); 446 | runOnlyForDeploymentPostprocessing = 0; 447 | }; 448 | /* End PBXSourcesBuildPhase section */ 449 | 450 | /* Begin PBXTargetDependency section */ 451 | 2102CD162BA9E0E700D0EBAD /* PBXTargetDependency */ = { 452 | isa = PBXTargetDependency; 453 | target = 2102CD082BA9E0E700D0EBAD /* ApiVideoLiveStream */; 454 | targetProxy = 2102CD152BA9E0E700D0EBAD /* PBXContainerItemProxy */; 455 | }; 456 | 21CCD6A32BBC44EF00E58F5D /* PBXTargetDependency */ = { 457 | isa = PBXTargetDependency; 458 | target = 2102CD082BA9E0E700D0EBAD /* ApiVideoLiveStream */; 459 | targetProxy = 21CCD6A22BBC44EF00E58F5D /* PBXContainerItemProxy */; 460 | }; 461 | /* End PBXTargetDependency section */ 462 | 463 | /* Begin PBXVariantGroup section */ 464 | 21E358872C10A35300D856D9 /* LaunchScreen.storyboard */ = { 465 | isa = PBXVariantGroup; 466 | children = ( 467 | 21E358862C10A35300D856D9 /* Base */, 468 | ); 469 | name = LaunchScreen.storyboard; 470 | sourceTree = ""; 471 | }; 472 | 21E358892C10A35300D856D9 /* Main.storyboard */ = { 473 | isa = PBXVariantGroup; 474 | children = ( 475 | 21E358882C10A35300D856D9 /* Base */, 476 | ); 477 | name = Main.storyboard; 478 | sourceTree = ""; 479 | }; 480 | /* End PBXVariantGroup section */ 481 | 482 | /* Begin XCBuildConfiguration section */ 483 | 2102CD1B2BA9E0E700D0EBAD /* Debug */ = { 484 | isa = XCBuildConfiguration; 485 | buildSettings = { 486 | ALWAYS_SEARCH_USER_PATHS = NO; 487 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 488 | CLANG_ANALYZER_NONNULL = YES; 489 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 490 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 491 | CLANG_ENABLE_MODULES = YES; 492 | CLANG_ENABLE_OBJC_ARC = YES; 493 | CLANG_ENABLE_OBJC_WEAK = YES; 494 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 495 | CLANG_WARN_BOOL_CONVERSION = YES; 496 | CLANG_WARN_COMMA = YES; 497 | CLANG_WARN_CONSTANT_CONVERSION = YES; 498 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 499 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 500 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 501 | CLANG_WARN_EMPTY_BODY = YES; 502 | CLANG_WARN_ENUM_CONVERSION = YES; 503 | CLANG_WARN_INFINITE_RECURSION = YES; 504 | CLANG_WARN_INT_CONVERSION = YES; 505 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 506 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 507 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 508 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 509 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 510 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 511 | CLANG_WARN_STRICT_PROTOTYPES = YES; 512 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 513 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 514 | CLANG_WARN_UNREACHABLE_CODE = YES; 515 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 516 | COPY_PHASE_STRIP = NO; 517 | CURRENT_PROJECT_VERSION = 1; 518 | DEAD_CODE_STRIPPING = YES; 519 | DEBUG_INFORMATION_FORMAT = dwarf; 520 | ENABLE_STRICT_OBJC_MSGSEND = YES; 521 | ENABLE_TESTABILITY = YES; 522 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 523 | GCC_C_LANGUAGE_STANDARD = gnu17; 524 | GCC_DYNAMIC_NO_PIC = NO; 525 | GCC_NO_COMMON_BLOCKS = YES; 526 | GCC_OPTIMIZATION_LEVEL = 0; 527 | GCC_PREPROCESSOR_DEFINITIONS = ( 528 | "DEBUG=1", 529 | "$(inherited)", 530 | ); 531 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 532 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 533 | GCC_WARN_UNDECLARED_SELECTOR = YES; 534 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 535 | GCC_WARN_UNUSED_FUNCTION = YES; 536 | GCC_WARN_UNUSED_VARIABLE = YES; 537 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 538 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 539 | MTL_FAST_MATH = YES; 540 | ONLY_ACTIVE_ARCH = YES; 541 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 542 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 543 | VERSIONING_SYSTEM = "apple-generic"; 544 | VERSION_INFO_PREFIX = ""; 545 | }; 546 | name = Debug; 547 | }; 548 | 2102CD1C2BA9E0E700D0EBAD /* Release */ = { 549 | isa = XCBuildConfiguration; 550 | buildSettings = { 551 | ALWAYS_SEARCH_USER_PATHS = NO; 552 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 553 | CLANG_ANALYZER_NONNULL = YES; 554 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 555 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 556 | CLANG_ENABLE_MODULES = YES; 557 | CLANG_ENABLE_OBJC_ARC = YES; 558 | CLANG_ENABLE_OBJC_WEAK = YES; 559 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 560 | CLANG_WARN_BOOL_CONVERSION = YES; 561 | CLANG_WARN_COMMA = YES; 562 | CLANG_WARN_CONSTANT_CONVERSION = YES; 563 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 564 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 565 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 566 | CLANG_WARN_EMPTY_BODY = YES; 567 | CLANG_WARN_ENUM_CONVERSION = YES; 568 | CLANG_WARN_INFINITE_RECURSION = YES; 569 | CLANG_WARN_INT_CONVERSION = YES; 570 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 571 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 572 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 573 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 574 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 575 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 576 | CLANG_WARN_STRICT_PROTOTYPES = YES; 577 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 578 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 579 | CLANG_WARN_UNREACHABLE_CODE = YES; 580 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 581 | COPY_PHASE_STRIP = NO; 582 | CURRENT_PROJECT_VERSION = 1; 583 | DEAD_CODE_STRIPPING = YES; 584 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 585 | ENABLE_NS_ASSERTIONS = NO; 586 | ENABLE_STRICT_OBJC_MSGSEND = YES; 587 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 588 | GCC_C_LANGUAGE_STANDARD = gnu17; 589 | GCC_NO_COMMON_BLOCKS = YES; 590 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 591 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 592 | GCC_WARN_UNDECLARED_SELECTOR = YES; 593 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 594 | GCC_WARN_UNUSED_FUNCTION = YES; 595 | GCC_WARN_UNUSED_VARIABLE = YES; 596 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 597 | MTL_ENABLE_DEBUG_INFO = NO; 598 | MTL_FAST_MATH = YES; 599 | SWIFT_COMPILATION_MODE = wholemodule; 600 | VERSIONING_SYSTEM = "apple-generic"; 601 | VERSION_INFO_PREFIX = ""; 602 | }; 603 | name = Release; 604 | }; 605 | 2102CD1E2BA9E0E700D0EBAD /* Debug */ = { 606 | isa = XCBuildConfiguration; 607 | buildSettings = { 608 | CODE_SIGN_STYLE = Automatic; 609 | CURRENT_PROJECT_VERSION = 1; 610 | DEAD_CODE_STRIPPING = YES; 611 | DEFINES_MODULE = YES; 612 | DYLIB_COMPATIBILITY_VERSION = 1; 613 | DYLIB_CURRENT_VERSION = 1; 614 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 615 | ENABLE_MODULE_VERIFIER = YES; 616 | GENERATE_INFOPLIST_FILE = YES; 617 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 618 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 619 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 620 | LD_RUNPATH_SEARCH_PATHS = ( 621 | "@executable_path/Frameworks", 622 | "@loader_path/Frameworks", 623 | ); 624 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = ( 625 | "@executable_path/../Frameworks", 626 | "@loader_path/Frameworks", 627 | ); 628 | MACOSX_DEPLOYMENT_TARGET = 14.1; 629 | MARKETING_VERSION = 1.0; 630 | MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; 631 | MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; 632 | PRODUCT_BUNDLE_IDENTIFIER = video.api.Databus; 633 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 634 | SDKROOT = auto; 635 | SKIP_INSTALL = YES; 636 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 637 | SWIFT_EMIT_LOC_STRINGS = YES; 638 | SWIFT_VERSION = 5.0; 639 | TARGETED_DEVICE_FAMILY = "1,2"; 640 | }; 641 | name = Debug; 642 | }; 643 | 2102CD1F2BA9E0E700D0EBAD /* Release */ = { 644 | isa = XCBuildConfiguration; 645 | buildSettings = { 646 | CODE_SIGN_STYLE = Automatic; 647 | CURRENT_PROJECT_VERSION = 1; 648 | DEAD_CODE_STRIPPING = YES; 649 | DEFINES_MODULE = YES; 650 | DYLIB_COMPATIBILITY_VERSION = 1; 651 | DYLIB_CURRENT_VERSION = 1; 652 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 653 | ENABLE_MODULE_VERIFIER = YES; 654 | GENERATE_INFOPLIST_FILE = YES; 655 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 656 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 657 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 658 | LD_RUNPATH_SEARCH_PATHS = ( 659 | "@executable_path/Frameworks", 660 | "@loader_path/Frameworks", 661 | ); 662 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = ( 663 | "@executable_path/../Frameworks", 664 | "@loader_path/Frameworks", 665 | ); 666 | MACOSX_DEPLOYMENT_TARGET = 14.1; 667 | MARKETING_VERSION = 1.0; 668 | MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; 669 | MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; 670 | PRODUCT_BUNDLE_IDENTIFIER = video.api.Databus; 671 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 672 | SDKROOT = auto; 673 | SKIP_INSTALL = YES; 674 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 675 | SWIFT_EMIT_LOC_STRINGS = YES; 676 | SWIFT_VERSION = 5.0; 677 | TARGETED_DEVICE_FAMILY = "1,2"; 678 | }; 679 | name = Release; 680 | }; 681 | 2102CD212BA9E0E700D0EBAD /* Debug */ = { 682 | isa = XCBuildConfiguration; 683 | buildSettings = { 684 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 685 | CODE_SIGN_STYLE = Automatic; 686 | CURRENT_PROJECT_VERSION = 1; 687 | DEAD_CODE_STRIPPING = YES; 688 | GENERATE_INFOPLIST_FILE = YES; 689 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 690 | MACOSX_DEPLOYMENT_TARGET = 14.1; 691 | MARKETING_VERSION = 1.0; 692 | PRODUCT_BUNDLE_IDENTIFIER = video.api.DatabusTests; 693 | PRODUCT_NAME = "$(TARGET_NAME)"; 694 | SDKROOT = auto; 695 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 696 | SWIFT_EMIT_LOC_STRINGS = NO; 697 | SWIFT_VERSION = 5.0; 698 | TARGETED_DEVICE_FAMILY = "1,2"; 699 | }; 700 | name = Debug; 701 | }; 702 | 2102CD222BA9E0E700D0EBAD /* Release */ = { 703 | isa = XCBuildConfiguration; 704 | buildSettings = { 705 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 706 | CODE_SIGN_STYLE = Automatic; 707 | CURRENT_PROJECT_VERSION = 1; 708 | DEAD_CODE_STRIPPING = YES; 709 | GENERATE_INFOPLIST_FILE = YES; 710 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 711 | MACOSX_DEPLOYMENT_TARGET = 14.1; 712 | MARKETING_VERSION = 1.0; 713 | PRODUCT_BUNDLE_IDENTIFIER = video.api.DatabusTests; 714 | PRODUCT_NAME = "$(TARGET_NAME)"; 715 | SDKROOT = auto; 716 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 717 | SWIFT_EMIT_LOC_STRINGS = NO; 718 | SWIFT_VERSION = 5.0; 719 | TARGETED_DEVICE_FAMILY = "1,2"; 720 | }; 721 | name = Release; 722 | }; 723 | 2117CBEE2BB1EE3300B85BF0 /* Debug */ = { 724 | isa = XCBuildConfiguration; 725 | buildSettings = { 726 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 727 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 728 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 729 | CODE_SIGN_STYLE = Automatic; 730 | CURRENT_PROJECT_VERSION = 1; 731 | DEVELOPMENT_TEAM = GBC36KP98K; 732 | GENERATE_INFOPLIST_FILE = YES; 733 | INFOPLIST_FILE = Examples/iOS/Info.plist; 734 | INFOPLIST_KEY_CFBundleDisplayName = ApiVideoLiveStream; 735 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 736 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 737 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 738 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 739 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 740 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 741 | LD_RUNPATH_SEARCH_PATHS = ( 742 | "$(inherited)", 743 | "@executable_path/Frameworks", 744 | ); 745 | MARKETING_VERSION = 1.0; 746 | PRODUCT_BUNDLE_IDENTIFIER = video.api.livestream.examples.iOS; 747 | PRODUCT_NAME = "$(TARGET_NAME)"; 748 | SDKROOT = iphoneos; 749 | SWIFT_EMIT_LOC_STRINGS = YES; 750 | SWIFT_VERSION = 5.0; 751 | TARGETED_DEVICE_FAMILY = "1,2"; 752 | }; 753 | name = Debug; 754 | }; 755 | 2117CBEF2BB1EE3300B85BF0 /* Release */ = { 756 | isa = XCBuildConfiguration; 757 | buildSettings = { 758 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 759 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 760 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 761 | CODE_SIGN_STYLE = Automatic; 762 | CURRENT_PROJECT_VERSION = 1; 763 | DEVELOPMENT_TEAM = GBC36KP98K; 764 | GENERATE_INFOPLIST_FILE = YES; 765 | INFOPLIST_FILE = Examples/iOS/Info.plist; 766 | INFOPLIST_KEY_CFBundleDisplayName = ApiVideoLiveStream; 767 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 768 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 769 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 770 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 771 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 772 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 773 | LD_RUNPATH_SEARCH_PATHS = ( 774 | "$(inherited)", 775 | "@executable_path/Frameworks", 776 | ); 777 | MARKETING_VERSION = 1.0; 778 | PRODUCT_BUNDLE_IDENTIFIER = video.api.livestream.examples.iOS; 779 | PRODUCT_NAME = "$(TARGET_NAME)"; 780 | SDKROOT = iphoneos; 781 | SWIFT_EMIT_LOC_STRINGS = YES; 782 | SWIFT_VERSION = 5.0; 783 | TARGETED_DEVICE_FAMILY = "1,2"; 784 | VALIDATE_PRODUCT = YES; 785 | }; 786 | name = Release; 787 | }; 788 | /* End XCBuildConfiguration section */ 789 | 790 | /* Begin XCConfigurationList section */ 791 | 2102CD032BA9E0E600D0EBAD /* Build configuration list for PBXProject "ApiVideoLiveStream" */ = { 792 | isa = XCConfigurationList; 793 | buildConfigurations = ( 794 | 2102CD1B2BA9E0E700D0EBAD /* Debug */, 795 | 2102CD1C2BA9E0E700D0EBAD /* Release */, 796 | ); 797 | defaultConfigurationIsVisible = 0; 798 | defaultConfigurationName = Release; 799 | }; 800 | 2102CD1D2BA9E0E700D0EBAD /* Build configuration list for PBXNativeTarget "ApiVideoLiveStream" */ = { 801 | isa = XCConfigurationList; 802 | buildConfigurations = ( 803 | 2102CD1E2BA9E0E700D0EBAD /* Debug */, 804 | 2102CD1F2BA9E0E700D0EBAD /* Release */, 805 | ); 806 | defaultConfigurationIsVisible = 0; 807 | defaultConfigurationName = Release; 808 | }; 809 | 2102CD202BA9E0E700D0EBAD /* Build configuration list for PBXNativeTarget "Tests" */ = { 810 | isa = XCConfigurationList; 811 | buildConfigurations = ( 812 | 2102CD212BA9E0E700D0EBAD /* Debug */, 813 | 2102CD222BA9E0E700D0EBAD /* Release */, 814 | ); 815 | defaultConfigurationIsVisible = 0; 816 | defaultConfigurationName = Release; 817 | }; 818 | 2117CBED2BB1EE3300B85BF0 /* Build configuration list for PBXNativeTarget "Example iOS" */ = { 819 | isa = XCConfigurationList; 820 | buildConfigurations = ( 821 | 2117CBEE2BB1EE3300B85BF0 /* Debug */, 822 | 2117CBEF2BB1EE3300B85BF0 /* Release */, 823 | ); 824 | defaultConfigurationIsVisible = 0; 825 | defaultConfigurationName = Release; 826 | }; 827 | /* End XCConfigurationList section */ 828 | 829 | /* Begin XCRemoteSwiftPackageReference section */ 830 | 21BF485C2C10BA9A00EAEB5B /* XCRemoteSwiftPackageReference "InAppSettingsKit" */ = { 831 | isa = XCRemoteSwiftPackageReference; 832 | repositoryURL = "https://github.com/futuretap/InAppSettingsKit.git"; 833 | requirement = { 834 | kind = upToNextMajorVersion; 835 | minimumVersion = 3.7.1; 836 | }; 837 | }; 838 | 21E358A82C10A54D00D856D9 /* XCRemoteSwiftPackageReference "HaishinKit" */ = { 839 | isa = XCRemoteSwiftPackageReference; 840 | repositoryURL = "https://github.com/shogo4405/HaishinKit.swift"; 841 | requirement = { 842 | kind = exactVersion; 843 | version = 1.9.3; 844 | }; 845 | }; 846 | /* End XCRemoteSwiftPackageReference section */ 847 | 848 | /* Begin XCSwiftPackageProductDependency section */ 849 | 21BF485A2C10B54300EAEB5B /* HaishinKit */ = { 850 | isa = XCSwiftPackageProductDependency; 851 | package = 21E358A82C10A54D00D856D9 /* XCRemoteSwiftPackageReference "HaishinKit" */; 852 | productName = HaishinKit; 853 | }; 854 | 21BF485D2C10BAAF00EAEB5B /* InAppSettingsKit */ = { 855 | isa = XCSwiftPackageProductDependency; 856 | package = 21BF485C2C10BA9A00EAEB5B /* XCRemoteSwiftPackageReference "InAppSettingsKit" */; 857 | productName = InAppSettingsKit; 858 | }; 859 | /* End XCSwiftPackageProductDependency section */ 860 | }; 861 | rootObject = 2102CD002BA9E0E600D0EBAD /* Project object */; 862 | } 863 | --------------------------------------------------------------------------------