From dc5ec29bd8d2dd75471cf42006a56e33ac6c126b Mon Sep 17 00:00:00 2001 From: Aiden Vigue Date: Sun, 20 Jun 2021 15:05:55 -0400 Subject: [PATCH] switch to official cast sdk. --- .github/workflows/ci.yml | 19 - .gitignore | 4 +- Cartfile | 2 - JellyfinPlayer.xcodeproj/project.pbxproj | 421 +++++------- .../xcshareddata/swiftpm/Package.resolved | 9 - .../xcschemes/JellyfinPlayer.xcscheme | 2 +- .../xcschemes/WidgetExtension.xcscheme | 2 +- .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/swiftpm/Package.resolved | 97 +++ JellyfinPlayer/EpisodeItemView.swift | 2 +- JellyfinPlayer/Info.plist | 2 + JellyfinPlayer/LatestMediaView.swift | 3 - JellyfinPlayer/MovieItemView.swift | 2 +- .../Definitions/CASTV2Protocol.swift | 96 --- .../Helpers/CastV2PlatformReader.swift | 83 --- .../Helpers/Proto/cast_channel.pb.swift | 618 ----------------- .../Helpers/Proto/cast_channel.proto | 80 --- .../Models/AppAvailability.swift | 24 - .../OpenCastSwift/Models/CastApp.swift | 62 -- .../OpenCastSwift/Models/CastDevice.swift | 66 -- .../OpenCastSwift/Models/CastMedia.swift | 92 --- .../Models/CastMediaStatus.swift | 60 -- .../OpenCastSwift/Models/CastMessage.swift | 36 - .../Models/CastMultizoneDevice.swift | 42 -- .../Models/CastMultizoneStatus.swift | 29 - .../OpenCastSwift/Models/CastStatus.swift | 44 -- .../OpenCastSwift/Networking/CastClient.swift | 626 ------------------ .../Networking/CastDeviceScanner.swift | 219 ------ .../Networking/Channels/CastChannel.swift | 31 - .../Networking/Channels/Channelable.swift | 39 -- .../Channels/DeviceAuthChannel.swift | 30 - .../Channels/DeviceConnectionChannel.swift | 47 -- .../Channels/DeviceDiscoveryChannel.swift | 31 - .../Channels/DeviceSetupChannel.swift | 97 --- .../Channels/HeartbeatChannel.swift | 97 --- .../Channels/MediaControlChannel.swift | 132 ---- .../Channels/MultizoneControlChannel.swift | 115 ---- .../Channels/ReceiverControlChannel.swift | 157 ----- .../Networking/Channels/RequestSink.swift | 38 -- .../Supporting Files/ChromeCastCore.h | 12 - JellyfinPlayer/VideoPlayer.storyboard | 24 - JellyfinPlayer/VideoPlayer.swift | 554 +++++++++------- .../VideoPlayerCastDeviceSelector.swift | 41 +- Podfile | 11 + 45 files changed, 627 insertions(+), 3589 deletions(-) delete mode 100644 Cartfile create mode 100644 JellyfinPlayer.xcworkspace/contents.xcworkspacedata create mode 100644 JellyfinPlayer.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved delete mode 100644 JellyfinPlayer/OpenCastSwift/Definitions/CASTV2Protocol.swift delete mode 100644 JellyfinPlayer/OpenCastSwift/Helpers/CastV2PlatformReader.swift delete mode 100644 JellyfinPlayer/OpenCastSwift/Helpers/Proto/cast_channel.pb.swift delete mode 100644 JellyfinPlayer/OpenCastSwift/Helpers/Proto/cast_channel.proto delete mode 100644 JellyfinPlayer/OpenCastSwift/Models/AppAvailability.swift delete mode 100644 JellyfinPlayer/OpenCastSwift/Models/CastApp.swift delete mode 100644 JellyfinPlayer/OpenCastSwift/Models/CastDevice.swift delete mode 100644 JellyfinPlayer/OpenCastSwift/Models/CastMedia.swift delete mode 100644 JellyfinPlayer/OpenCastSwift/Models/CastMediaStatus.swift delete mode 100644 JellyfinPlayer/OpenCastSwift/Models/CastMessage.swift delete mode 100644 JellyfinPlayer/OpenCastSwift/Models/CastMultizoneDevice.swift delete mode 100644 JellyfinPlayer/OpenCastSwift/Models/CastMultizoneStatus.swift delete mode 100644 JellyfinPlayer/OpenCastSwift/Models/CastStatus.swift delete mode 100644 JellyfinPlayer/OpenCastSwift/Networking/CastClient.swift delete mode 100644 JellyfinPlayer/OpenCastSwift/Networking/CastDeviceScanner.swift delete mode 100644 JellyfinPlayer/OpenCastSwift/Networking/Channels/CastChannel.swift delete mode 100644 JellyfinPlayer/OpenCastSwift/Networking/Channels/Channelable.swift delete mode 100644 JellyfinPlayer/OpenCastSwift/Networking/Channels/DeviceAuthChannel.swift delete mode 100644 JellyfinPlayer/OpenCastSwift/Networking/Channels/DeviceConnectionChannel.swift delete mode 100644 JellyfinPlayer/OpenCastSwift/Networking/Channels/DeviceDiscoveryChannel.swift delete mode 100644 JellyfinPlayer/OpenCastSwift/Networking/Channels/DeviceSetupChannel.swift delete mode 100644 JellyfinPlayer/OpenCastSwift/Networking/Channels/HeartbeatChannel.swift delete mode 100644 JellyfinPlayer/OpenCastSwift/Networking/Channels/MediaControlChannel.swift delete mode 100644 JellyfinPlayer/OpenCastSwift/Networking/Channels/MultizoneControlChannel.swift delete mode 100644 JellyfinPlayer/OpenCastSwift/Networking/Channels/ReceiverControlChannel.swift delete mode 100644 JellyfinPlayer/OpenCastSwift/Networking/Channels/RequestSink.swift delete mode 100644 JellyfinPlayer/OpenCastSwift/Supporting Files/ChromeCastCore.h create mode 100644 Podfile diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc8ea8b9..a663a01c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,15 +25,6 @@ jobs: - name: Checkout uses: actions/checkout@v1 - - name: Cache Carthage dependencies - id: cache-vlckit - uses: actions/cache@v2 - with: - path: Carthage - key: ${{ runner.os }}-${{ matrix.scheme }}-carthage-${{ hashFiles('**/Cartfile.resolved') }} - restore-keys: | - ${{ runner.os }}-${{ matrix.scheme }}-carthage- - - name: Cache Swift packages uses: actions/cache@v2 with: @@ -48,16 +39,6 @@ jobs: path: "~/Library/Developer/Xcode/DerivedData" key: ${{ runner.os }}-${{ matrix.scheme }}-deriveddata - - name: Install Ruby (for Carthage) - if: steps.cache-vlckit.outputs.cache-hit != 'true' - uses: ruby/setup-ruby@v1 - with: - ruby-version: 2.6 # Not needed with a .ruby-version file - bundler-cache: true # runs 'bundle install' and caches installed gems automatically - - name: Update Carthage dependencies - if: steps.cache-vlckit.outputs.cache-hit != 'true' - run: "carthage update" - - name: xcodebuild! run: | xcodebuild build -project "JellyfinPlayer.xcodeproj" \ diff --git a/.gitignore b/.gitignore index 55a674a9..db1ee361 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ build/ DerivedData/ +Pods/ +Podfile.lock dynatraceSymbols.zip Cartfile.resolved Gemfile.lock @@ -93,4 +95,4 @@ iOSInjectionProject/ .Spotlight-V100 .Trashes ehthumbs.db -Thumbs.db \ No newline at end of file +Thumbs.db diff --git a/Cartfile b/Cartfile deleted file mode 100644 index d949255d..00000000 --- a/Cartfile +++ /dev/null @@ -1,2 +0,0 @@ -binary "https://code.videolan.org/videolan/VLCKit/raw/master/Packaging/MobileVLCKit.json" ~> 3.3.0 -binary "https://code.videolan.org/videolan/VLCKit/raw/master/Packaging/TVVLCKit.json" ~> 3.3.0 \ No newline at end of file diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index ff3ba448..b7cfb356 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -41,35 +41,6 @@ 535870AD2669D8DD00D05A09 /* Typings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870AC2669D8DD00D05A09 /* Typings.swift */; }; 535BAE9F2649E569005FA86D /* ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAE9E2649E569005FA86D /* ItemView.swift */; }; 535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAEA4264A151C005FA86D /* VideoPlayer.swift */; }; - 5362E4D3267D461F000E2F71 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = 5362E4D2267D461F000E2F71 /* SwiftProtobuf */; }; - 5362E4D6267D4671000E2F71 /* Result in Frameworks */ = {isa = PBXBuildFile; productRef = 5362E4D5267D4671000E2F71 /* Result */; }; - 5362E4D9267D4695000E2F71 /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 5362E4D8267D4695000E2F71 /* SwiftyJSON */; }; - 5362E500267D4707000E2F71 /* CastClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5362E4DE267D4707000E2F71 /* CastClient.swift */; }; - 5362E501267D4707000E2F71 /* CastDeviceScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5362E4DF267D4707000E2F71 /* CastDeviceScanner.swift */; }; - 5362E502267D4707000E2F71 /* ReceiverControlChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5362E4E1267D4707000E2F71 /* ReceiverControlChannel.swift */; }; - 5362E503267D4707000E2F71 /* Channelable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5362E4E2267D4707000E2F71 /* Channelable.swift */; }; - 5362E504267D4707000E2F71 /* CastChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5362E4E3267D4707000E2F71 /* CastChannel.swift */; }; - 5362E505267D4707000E2F71 /* DeviceAuthChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5362E4E4267D4707000E2F71 /* DeviceAuthChannel.swift */; }; - 5362E506267D4707000E2F71 /* MediaControlChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5362E4E5267D4707000E2F71 /* MediaControlChannel.swift */; }; - 5362E507267D4707000E2F71 /* HeartbeatChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5362E4E6267D4707000E2F71 /* HeartbeatChannel.swift */; }; - 5362E508267D4707000E2F71 /* DeviceSetupChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5362E4E7267D4707000E2F71 /* DeviceSetupChannel.swift */; }; - 5362E509267D4707000E2F71 /* DeviceConnectionChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5362E4E8267D4707000E2F71 /* DeviceConnectionChannel.swift */; }; - 5362E50A267D4707000E2F71 /* RequestSink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5362E4E9267D4707000E2F71 /* RequestSink.swift */; }; - 5362E50B267D4707000E2F71 /* MultizoneControlChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5362E4EA267D4707000E2F71 /* MultizoneControlChannel.swift */; }; - 5362E50C267D4707000E2F71 /* DeviceDiscoveryChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5362E4EB267D4707000E2F71 /* DeviceDiscoveryChannel.swift */; }; - 5362E50D267D4707000E2F71 /* CastDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5362E4ED267D4707000E2F71 /* CastDevice.swift */; }; - 5362E50E267D4707000E2F71 /* CastStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5362E4EE267D4707000E2F71 /* CastStatus.swift */; }; - 5362E50F267D4707000E2F71 /* CastApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5362E4EF267D4707000E2F71 /* CastApp.swift */; }; - 5362E510267D4707000E2F71 /* CastMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5362E4F0267D4707000E2F71 /* CastMessage.swift */; }; - 5362E511267D4707000E2F71 /* CastMediaStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5362E4F1267D4707000E2F71 /* CastMediaStatus.swift */; }; - 5362E512267D4707000E2F71 /* CastMedia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5362E4F2267D4707000E2F71 /* CastMedia.swift */; }; - 5362E513267D4707000E2F71 /* CastMultizoneDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5362E4F3267D4707000E2F71 /* CastMultizoneDevice.swift */; }; - 5362E514267D4707000E2F71 /* CastMultizoneStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5362E4F4267D4707000E2F71 /* CastMultizoneStatus.swift */; }; - 5362E515267D4707000E2F71 /* AppAvailability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5362E4F5267D4707000E2F71 /* AppAvailability.swift */; }; - 5362E517267D4707000E2F71 /* CASTV2Protocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5362E4FA267D4707000E2F71 /* CASTV2Protocol.swift */; }; - 5362E518267D4707000E2F71 /* cast_channel.proto in Sources */ = {isa = PBXBuildFile; fileRef = 5362E4FD267D4707000E2F71 /* cast_channel.proto */; }; - 5362E519267D4707000E2F71 /* cast_channel.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5362E4FE267D4707000E2F71 /* cast_channel.pb.swift */; }; - 5362E51A267D4707000E2F71 /* CastV2PlatformReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5362E4FF267D4707000E2F71 /* CastV2PlatformReader.swift */; }; 5364F455266CA0DC0026ECBA /* APIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* APIExtensions.swift */; }; 5364F456266CA0DC0026ECBA /* APIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* APIExtensions.swift */; }; 536D3D74267BA8170004248C /* BackgroundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536D3D73267BA8170004248C /* BackgroundManager.swift */; }; @@ -111,6 +82,9 @@ 53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53DE4BD1267098F300739748 /* SearchBarView.swift */; }; 53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53DF641D263D9C0600A7CD1A /* LibraryView.swift */; }; 53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */; }; + 53EC6E1E267E80AC006DD26A /* Pods_JellyfinPlayer_tvOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EBFE1F64394BCC2EFFF1610D /* Pods_JellyfinPlayer_tvOS.framework */; }; + 53EC6E21267E80B1006DD26A /* Pods_JellyfinPlayer_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F905C1D3D3A0C9E13E7A0BC /* Pods_JellyfinPlayer_iOS.framework */; }; + 53EC6E25267EB10F006DD26A /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 53EC6E24267EB10F006DD26A /* SwiftyJSON */; }; 53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EE24E5265060780068F029 /* LibrarySearchView.swift */; }; 53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */; }; 53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */; }; @@ -128,8 +102,6 @@ 625CB5752678C33500530A6E /* LibraryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5742678C33500530A6E /* LibraryListViewModel.swift */; }; 625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */; }; 625CB57A2678C4A400530A6E /* ActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 625CB5792678C4A400530A6E /* ActivityIndicator */; }; - 625CB57E2678E81E00530A6E /* TVVLCKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 625CB57D2678E81E00530A6E /* TVVLCKit.xcframework */; }; - 625CB57F2678E81E00530A6E /* TVVLCKit.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 625CB57D2678E81E00530A6E /* TVVLCKit.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 6267B3D42671024A00A7371D /* APIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* APIExtensions.swift */; }; 6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D526710B8900A7371D /* CollectionExtensions.swift */; }; 6267B3D726710B9700A7371D /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D526710B8900A7371D /* CollectionExtensions.swift */; }; @@ -165,8 +137,6 @@ 62E632F0267D43320063E547 /* LibraryFilterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632EE267D43320063E547 /* LibraryFilterViewModel.swift */; }; 62E632F3267D54030063E547 /* DetailItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632F2267D54030063E547 /* DetailItemViewModel.swift */; }; 62E632F4267D54030063E547 /* DetailItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632F2267D54030063E547 /* DetailItemViewModel.swift */; }; - 62EC3527267665D8000E9F2D /* MobileVLCKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */; }; - 62EC3528267665D8000E9F2D /* MobileVLCKit.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 62EC352C26766675000E9F2D /* ServerEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352B26766675000E9F2D /* ServerEnvironment.swift */; }; 62EC352D26766675000E9F2D /* ServerEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352B26766675000E9F2D /* ServerEnvironment.swift */; }; 62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352E267666A5000E9F2D /* SessionManager.swift */; }; @@ -197,17 +167,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 625CB5802678E81E00530A6E /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 625CB57F2678E81E00530A6E /* TVVLCKit.xcframework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; 628B95312670CABE0091AF3B /* Embed App Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -219,20 +178,11 @@ name = "Embed App Extensions"; runOnlyForDeploymentPostprocessing = 0; }; - 62EC3529267665D8000E9F2D /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 62EC3528267665D8000E9F2D /* MobileVLCKit.xcframework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 3773C07648173CE7FEC083D5 /* Pods-JellyfinPlayer iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer iOS.debug.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS.debug.xcconfig"; sourceTree = ""; }; + 3F905C1D3D3A0C9E13E7A0BC /* Pods_JellyfinPlayer_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_JellyfinPlayer_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 531690E4267ABD5C005D8AB9 /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = ""; }; 531690E6267ABD79005D8AB9 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; 531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingView.swift; sourceTree = ""; }; @@ -272,33 +222,6 @@ 5362E4C4267D40F0000E2F71 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; 5362E4C6267D40F4000E2F71 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; 5362E4C8267D40F7000E2F71 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; - 5362E4DE267D4707000E2F71 /* CastClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CastClient.swift; sourceTree = ""; }; - 5362E4DF267D4707000E2F71 /* CastDeviceScanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CastDeviceScanner.swift; sourceTree = ""; }; - 5362E4E1267D4707000E2F71 /* ReceiverControlChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiverControlChannel.swift; sourceTree = ""; }; - 5362E4E2267D4707000E2F71 /* Channelable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Channelable.swift; sourceTree = ""; }; - 5362E4E3267D4707000E2F71 /* CastChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CastChannel.swift; sourceTree = ""; }; - 5362E4E4267D4707000E2F71 /* DeviceAuthChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceAuthChannel.swift; sourceTree = ""; }; - 5362E4E5267D4707000E2F71 /* MediaControlChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaControlChannel.swift; sourceTree = ""; }; - 5362E4E6267D4707000E2F71 /* HeartbeatChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeartbeatChannel.swift; sourceTree = ""; }; - 5362E4E7267D4707000E2F71 /* DeviceSetupChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceSetupChannel.swift; sourceTree = ""; }; - 5362E4E8267D4707000E2F71 /* DeviceConnectionChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceConnectionChannel.swift; sourceTree = ""; }; - 5362E4E9267D4707000E2F71 /* RequestSink.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestSink.swift; sourceTree = ""; }; - 5362E4EA267D4707000E2F71 /* MultizoneControlChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultizoneControlChannel.swift; sourceTree = ""; }; - 5362E4EB267D4707000E2F71 /* DeviceDiscoveryChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceDiscoveryChannel.swift; sourceTree = ""; }; - 5362E4ED267D4707000E2F71 /* CastDevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CastDevice.swift; sourceTree = ""; }; - 5362E4EE267D4707000E2F71 /* CastStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CastStatus.swift; sourceTree = ""; }; - 5362E4EF267D4707000E2F71 /* CastApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CastApp.swift; sourceTree = ""; }; - 5362E4F0267D4707000E2F71 /* CastMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CastMessage.swift; sourceTree = ""; }; - 5362E4F1267D4707000E2F71 /* CastMediaStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CastMediaStatus.swift; sourceTree = ""; }; - 5362E4F2267D4707000E2F71 /* CastMedia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CastMedia.swift; sourceTree = ""; }; - 5362E4F3267D4707000E2F71 /* CastMultizoneDevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CastMultizoneDevice.swift; sourceTree = ""; }; - 5362E4F4267D4707000E2F71 /* CastMultizoneStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CastMultizoneStatus.swift; sourceTree = ""; }; - 5362E4F5267D4707000E2F71 /* AppAvailability.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppAvailability.swift; sourceTree = ""; }; - 5362E4F7267D4707000E2F71 /* ChromeCastCore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ChromeCastCore.h; sourceTree = ""; }; - 5362E4FA267D4707000E2F71 /* CASTV2Protocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CASTV2Protocol.swift; sourceTree = ""; }; - 5362E4FD267D4707000E2F71 /* cast_channel.proto */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.protobuf; path = cast_channel.proto; sourceTree = ""; }; - 5362E4FE267D4707000E2F71 /* cast_channel.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = cast_channel.pb.swift; sourceTree = ""; }; - 5362E4FF267D4707000E2F71 /* CastV2PlatformReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CastV2PlatformReader.swift; sourceTree = ""; }; 5364F454266CA0DC0026ECBA /* APIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIExtensions.swift; sourceTree = ""; }; 536D3D73267BA8170004248C /* BackgroundManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundManager.swift; sourceTree = ""; }; 536D3D75267BA9BB0004248C /* MainTabViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabViewModel.swift; sourceTree = ""; }; @@ -326,7 +249,6 @@ 53ABFDDD267974E300886593 /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = ""; }; 53ABFDEA2679753200886593 /* ConnectToServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerView.swift; sourceTree = ""; }; 53AD124C2670278D0094A276 /* JellyfinPlayer.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = JellyfinPlayer.entitlements; sourceTree = ""; }; - 53D5E3DA264B460200BADDC8 /* Cartfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Cartfile; sourceTree = ""; }; 53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MobileVLCKit.xcframework; path = Carthage/Build/MobileVLCKit.xcframework; sourceTree = ""; }; 53DE4BD1267098F300739748 /* SearchBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBarView.swift; sourceTree = ""; }; 53DF641D263D9C0600A7CD1A /* LibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = ""; }; @@ -371,6 +293,10 @@ 62EC352E267666A5000E9F2D /* SessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionManager.swift; sourceTree = ""; }; 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRotationViewModifier.swift; sourceTree = ""; }; AE8C3158265D6F90008AA076 /* bitrates.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = bitrates.json; sourceTree = ""; }; + BEEC50E7EFD4848C0E320941 /* Pods-JellyfinPlayer iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer iOS.release.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS.release.xcconfig"; sourceTree = ""; }; + D79953919FED0C4DF72BA578 /* Pods-JellyfinPlayer tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer tvOS.release.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS.release.xcconfig"; sourceTree = ""; }; + DE5004F745B19E28744A7DE7 /* Pods-JellyfinPlayer tvOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer tvOS.debug.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS.debug.xcconfig"; sourceTree = ""; }; + EBFE1F64394BCC2EFFF1610D /* Pods_JellyfinPlayer_tvOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_JellyfinPlayer_tvOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -378,9 +304,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 53EC6E1E267E80AC006DD26A /* Pods_JellyfinPlayer_tvOS.framework in Frameworks */, 53A431BF266B0FFE0016769F /* JellyfinAPI in Frameworks */, 535870912669D7A800D05A09 /* Introspect in Frameworks */, - 625CB57E2678E81E00530A6E /* TVVLCKit.xcframework in Frameworks */, 5358708D2669D7A800D05A09 /* KeychainSwift in Frameworks */, 536D3D84267BEA550004248C /* ParallaxView in Frameworks */, 53ABFDDC267972BF00886593 /* TVServices.framework in Frameworks */, @@ -393,15 +319,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5362E4D9267D4695000E2F71 /* SwiftyJSON in Frameworks */, 5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */, + 53EC6E25267EB10F006DD26A /* SwiftyJSON in Frameworks */, + 53EC6E21267E80B1006DD26A /* Pods_JellyfinPlayer_iOS.framework in Frameworks */, 53352571265EA0A0006CCA86 /* Introspect in Frameworks */, 621C638026672A30004216EA /* NukeUI in Frameworks */, 625CB57A2678C4A400530A6E /* ActivityIndicator in Frameworks */, 53A431BD266B0FF20016769F /* JellyfinAPI in Frameworks */, - 5362E4D6267D4671000E2F71 /* Result in Frameworks */, - 5362E4D3267D461F000E2F71 /* SwiftProtobuf in Frameworks */, - 62EC3527267665D8000E9F2D /* MobileVLCKit.xcframework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -495,96 +419,6 @@ path = Typings; sourceTree = ""; }; - 5362E4DC267D4707000E2F71 /* OpenCastSwift */ = { - isa = PBXGroup; - children = ( - 5362E4DD267D4707000E2F71 /* Networking */, - 5362E4EC267D4707000E2F71 /* Models */, - 5362E4F6267D4707000E2F71 /* Supporting Files */, - 5362E4F9267D4707000E2F71 /* Definitions */, - 5362E4FB267D4707000E2F71 /* Helpers */, - ); - path = OpenCastSwift; - sourceTree = ""; - }; - 5362E4DD267D4707000E2F71 /* Networking */ = { - isa = PBXGroup; - children = ( - 5362E4DE267D4707000E2F71 /* CastClient.swift */, - 5362E4DF267D4707000E2F71 /* CastDeviceScanner.swift */, - 5362E4E0267D4707000E2F71 /* Channels */, - ); - path = Networking; - sourceTree = ""; - }; - 5362E4E0267D4707000E2F71 /* Channels */ = { - isa = PBXGroup; - children = ( - 5362E4E1267D4707000E2F71 /* ReceiverControlChannel.swift */, - 5362E4E2267D4707000E2F71 /* Channelable.swift */, - 5362E4E3267D4707000E2F71 /* CastChannel.swift */, - 5362E4E4267D4707000E2F71 /* DeviceAuthChannel.swift */, - 5362E4E5267D4707000E2F71 /* MediaControlChannel.swift */, - 5362E4E6267D4707000E2F71 /* HeartbeatChannel.swift */, - 5362E4E7267D4707000E2F71 /* DeviceSetupChannel.swift */, - 5362E4E8267D4707000E2F71 /* DeviceConnectionChannel.swift */, - 5362E4E9267D4707000E2F71 /* RequestSink.swift */, - 5362E4EA267D4707000E2F71 /* MultizoneControlChannel.swift */, - 5362E4EB267D4707000E2F71 /* DeviceDiscoveryChannel.swift */, - ); - path = Channels; - sourceTree = ""; - }; - 5362E4EC267D4707000E2F71 /* Models */ = { - isa = PBXGroup; - children = ( - 5362E4ED267D4707000E2F71 /* CastDevice.swift */, - 5362E4EE267D4707000E2F71 /* CastStatus.swift */, - 5362E4EF267D4707000E2F71 /* CastApp.swift */, - 5362E4F0267D4707000E2F71 /* CastMessage.swift */, - 5362E4F1267D4707000E2F71 /* CastMediaStatus.swift */, - 5362E4F2267D4707000E2F71 /* CastMedia.swift */, - 5362E4F3267D4707000E2F71 /* CastMultizoneDevice.swift */, - 5362E4F4267D4707000E2F71 /* CastMultizoneStatus.swift */, - 5362E4F5267D4707000E2F71 /* AppAvailability.swift */, - ); - path = Models; - sourceTree = ""; - }; - 5362E4F6267D4707000E2F71 /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 5362E4F7267D4707000E2F71 /* ChromeCastCore.h */, - ); - path = "Supporting Files"; - sourceTree = ""; - }; - 5362E4F9267D4707000E2F71 /* Definitions */ = { - isa = PBXGroup; - children = ( - 5362E4FA267D4707000E2F71 /* CASTV2Protocol.swift */, - ); - path = Definitions; - sourceTree = ""; - }; - 5362E4FB267D4707000E2F71 /* Helpers */ = { - isa = PBXGroup; - children = ( - 5362E4FC267D4707000E2F71 /* Proto */, - 5362E4FF267D4707000E2F71 /* CastV2PlatformReader.swift */, - ); - path = Helpers; - sourceTree = ""; - }; - 5362E4FC267D4707000E2F71 /* Proto */ = { - isa = PBXGroup; - children = ( - 5362E4FD267D4707000E2F71 /* cast_channel.proto */, - 5362E4FE267D4707000E2F71 /* cast_channel.pb.swift */, - ); - path = Proto; - sourceTree = ""; - }; 536D3D77267BB9650004248C /* Components */ = { isa = PBXGroup; children = ( @@ -598,13 +432,13 @@ 5377CBE8263B596A003A4E83 = { isa = PBXGroup; children = ( - 53D5E3DA264B460200BADDC8 /* Cartfile */, 53D5E3DB264B47EE00BADDC8 /* Frameworks */, 5377CBF3263B596A003A4E83 /* JellyfinPlayer */, 535870612669D21600D05A09 /* JellyfinPlayer tvOS */, 5377CBF2263B596A003A4E83 /* Products */, 535870752669D60C00D05A09 /* Shared */, 628B95252670CABD0091AF3B /* WidgetExtension */, + C78797A232E2B8774099D1E9 /* Pods */, ); sourceTree = ""; }; @@ -621,7 +455,6 @@ 5377CBF3263B596A003A4E83 /* JellyfinPlayer */ = { isa = PBXGroup; children = ( - 5362E4DC267D4707000E2F71 /* OpenCastSwift */, 53AD124C2670278D0094A276 /* JellyfinPlayer.entitlements */, 5377CBF8263B596B003A4E83 /* Assets.xcassets */, 5338F74D263B61370014BF09 /* ConnectToServerView.swift */, @@ -689,6 +522,8 @@ 53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */, 628B95212670CABD0091AF3B /* WidgetKit.framework */, 628B95232670CABD0091AF3B /* SwiftUI.framework */, + 3F905C1D3D3A0C9E13E7A0BC /* Pods_JellyfinPlayer_iOS.framework */, + EBFE1F64394BCC2EFFF1610D /* Pods_JellyfinPlayer_tvOS.framework */, ); name = Frameworks; sourceTree = ""; @@ -741,6 +576,17 @@ path = Resources; sourceTree = ""; }; + C78797A232E2B8774099D1E9 /* Pods */ = { + isa = PBXGroup; + children = ( + 3773C07648173CE7FEC083D5 /* Pods-JellyfinPlayer iOS.debug.xcconfig */, + BEEC50E7EFD4848C0E320941 /* Pods-JellyfinPlayer iOS.release.xcconfig */, + DE5004F745B19E28744A7DE7 /* Pods-JellyfinPlayer tvOS.debug.xcconfig */, + D79953919FED0C4DF72BA578 /* Pods-JellyfinPlayer tvOS.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -748,10 +594,11 @@ isa = PBXNativeTarget; buildConfigurationList = 535870712669D21700D05A09 /* Build configuration list for PBXNativeTarget "JellyfinPlayer tvOS" */; buildPhases = ( + E7370E1AA68C6CB254E46F2C /* [CP] Check Pods Manifest.lock */, 5358705C2669D21600D05A09 /* Sources */, 5358705D2669D21600D05A09 /* Frameworks */, 5358705E2669D21600D05A09 /* Resources */, - 625CB5802678E81E00530A6E /* Embed Frameworks */, + 6AB6F1DD2C8AD942F71C8A32 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -774,12 +621,14 @@ isa = PBXNativeTarget; buildConfigurationList = 5377CC1B263B596B003A4E83 /* Build configuration list for PBXNativeTarget "JellyfinPlayer iOS" */; buildPhases = ( + 6435C3C2E610FE34AD537AC1 /* [CP] Check Pods Manifest.lock */, 5377CBED263B596A003A4E83 /* Sources */, 5377CBEE263B596A003A4E83 /* Frameworks */, 5377CBEF263B596A003A4E83 /* Resources */, 5302F8322658B74800647A2E /* CopyFiles */, 628B95312670CABE0091AF3B /* Embed App Extensions */, - 62EC3529267665D8000E9F2D /* Embed Frameworks */, + E8DDF21F62DFCE8CE76666BA /* [CP] Embed Pods Frameworks */, + 83FD120CA10FD0E91DAD83C9 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -793,9 +642,7 @@ 621C637F26672A30004216EA /* NukeUI */, 53A431BC266B0FF20016769F /* JellyfinAPI */, 625CB5792678C4A400530A6E /* ActivityIndicator */, - 5362E4D2267D461F000E2F71 /* SwiftProtobuf */, - 5362E4D5267D4671000E2F71 /* Result */, - 5362E4D8267D4695000E2F71 /* SwiftyJSON */, + 53EC6E24267EB10F006DD26A /* SwiftyJSON */, ); productName = JellyfinPlayer; productReference = 5377CBF1263B596A003A4E83 /* JellyfinPlayer iOS.app */; @@ -863,9 +710,7 @@ 53A431BB266B0FF20016769F /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */, 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */, 536D3D82267BEA550004248C /* XCRemoteSwiftPackageReference "ParallaxView" */, - 5362E4D1267D461F000E2F71 /* XCRemoteSwiftPackageReference "swift-protobuf" */, - 5362E4D4267D4671000E2F71 /* XCRemoteSwiftPackageReference "Result" */, - 5362E4D7267D4695000E2F71 /* XCRemoteSwiftPackageReference "SwiftyJSON" */, + 53EC6E23267EB10F006DD26A /* XCRemoteSwiftPackageReference "SwiftyJSON" */, ); productRefGroup = 5377CBF2263B596A003A4E83 /* Products */; projectDirPath = ""; @@ -910,6 +755,104 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 6435C3C2E610FE34AD537AC1 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-JellyfinPlayer iOS-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 6AB6F1DD2C8AD942F71C8A32 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 83FD120CA10FD0E91DAD83C9 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + E7370E1AA68C6CB254E46F2C /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-JellyfinPlayer tvOS-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + E8DDF21F62DFCE8CE76666BA /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 5358705C2669D21600D05A09 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -967,80 +910,54 @@ files = ( 5364F455266CA0DC0026ECBA /* APIExtensions.swift in Sources */, 621338932660107500A81A2A /* StringExtensions.swift in Sources */, - 5362E507267D4707000E2F71 /* HeartbeatChannel.swift in Sources */, 53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */, 62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */, - 5362E506267D4707000E2F71 /* MediaControlChannel.swift in Sources */, 625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */, 62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */, 5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */, - 5362E500267D4707000E2F71 /* CastClient.swift in Sources */, 5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */, 53AD124E26702B8A0094A276 /* SeasonItemView.swift in Sources */, 535BAE9F2649E569005FA86D /* ItemView.swift in Sources */, 6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */, 625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */, 536D3D78267BD5C30004248C /* ViewModel.swift in Sources */, - 5362E508267D4707000E2F71 /* DeviceSetupChannel.swift in Sources */, 53DE4BD02670961400739748 /* EpisodeItemView.swift in Sources */, 53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */, 53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */, - 5362E50E267D4707000E2F71 /* CastStatus.swift in Sources */, 62133890265F83A900A81A2A /* LibraryListView.swift in Sources */, 62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */, 625CB56F2678C23300530A6E /* HomeView.swift in Sources */, 53892770263C25230035E14B /* NextUpView.swift in Sources */, - 5362E513267D4707000E2F71 /* CastMultizoneDevice.swift in Sources */, 625CB5682678B6FB00530A6E /* SplashView.swift in Sources */, 535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */, 62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */, 5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */, 532E68CF267D9F6B007B9F13 /* VideoPlayerCastDeviceSelector.swift in Sources */, 532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */, - 5362E50A267D4707000E2F71 /* RequestSink.swift in Sources */, 5377CC01263B596B003A4E83 /* Model.xcdatamodeld in Sources */, 53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */, - 5362E514267D4707000E2F71 /* CastMultizoneStatus.swift in Sources */, 6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */, - 5362E503267D4707000E2F71 /* Channelable.swift in Sources */, 53A089D0264DA9DA00D57806 /* MovieItemView.swift in Sources */, 62E632E9267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */, 625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */, 62E632F3267D54030063E547 /* DetailItemViewModel.swift in Sources */, - 5362E50C267D4707000E2F71 /* DeviceDiscoveryChannel.swift in Sources */, 53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */, - 5362E50D267D4707000E2F71 /* CastDevice.swift in Sources */, 53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */, - 5362E509267D4707000E2F71 /* DeviceConnectionChannel.swift in Sources */, 621338B32660A07800A81A2A /* LazyView.swift in Sources */, 531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */, 62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */, 62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */, 62E632E3267D3BA60063E547 /* MovieItemViewModel.swift in Sources */, 62E632EF267D43320063E547 /* LibraryFilterViewModel.swift in Sources */, - 5362E512267D4707000E2F71 /* CastMedia.swift in Sources */, - 5362E519267D4707000E2F71 /* cast_channel.pb.swift in Sources */, 535870AD2669D8DD00D05A09 /* Typings.swift in Sources */, 62EC352C26766675000E9F2D /* ServerEnvironment.swift in Sources */, 6267B3DA2671138200A7371D /* ImageExtensions.swift in Sources */, - 5362E50B267D4707000E2F71 /* MultizoneControlChannel.swift in Sources */, - 5362E517267D4707000E2F71 /* CASTV2Protocol.swift in Sources */, 62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */, 5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */, - 5362E518267D4707000E2F71 /* cast_channel.proto in Sources */, 625CB56C2678C0FD00530A6E /* MainTabView.swift in Sources */, 539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */, - 5362E511267D4707000E2F71 /* CastMediaStatus.swift in Sources */, 5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */, - 5362E505267D4707000E2F71 /* DeviceAuthChannel.swift in Sources */, 53AD124D267029D60094A276 /* SeriesItemView.swift in Sources */, - 5362E50F267D4707000E2F71 /* CastApp.swift in Sources */, - 5362E501267D4707000E2F71 /* CastDeviceScanner.swift in Sources */, - 5362E502267D4707000E2F71 /* ReceiverControlChannel.swift in Sources */, - 5362E504267D4707000E2F71 /* CastChannel.swift in Sources */, - 5362E515267D4707000E2F71 /* AppAvailability.swift in Sources */, - 5362E51A267D4707000E2F71 /* CastV2PlatformReader.swift in Sources */, - 5362E510267D4707000E2F71 /* CastMessage.swift in Sources */, 5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */, 53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */, 53892772263C8C6F0035E14B /* LoadingView.swift in Sources */, @@ -1079,6 +996,7 @@ /* Begin XCBuildConfiguration section */ 535870722669D21700D05A09 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = DE5004F745B19E28744A7DE7 /* Pods-JellyfinPlayer tvOS.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -1088,10 +1006,7 @@ DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\""; DEVELOPMENT_TEAM = 9R8RREG67J; ENABLE_PREVIEWS = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Carthage/Build/TVVLCKit.xcframework/tvos-arm64", - ); + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "JellyfinPlayer tvOS/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -1109,6 +1024,7 @@ }; 535870732669D21700D05A09 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = D79953919FED0C4DF72BA578 /* Pods-JellyfinPlayer tvOS.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -1118,10 +1034,7 @@ DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\""; DEVELOPMENT_TEAM = 9R8RREG67J; ENABLE_PREVIEWS = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Carthage/Build/TVVLCKit.xcframework/tvos-arm64", - ); + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "JellyfinPlayer tvOS/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -1258,6 +1171,7 @@ }; 5377CC1C263B596B003A4E83 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 3773C07648173CE7FEC083D5 /* Pods-JellyfinPlayer iOS.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -1271,10 +1185,7 @@ ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; EXCLUDED_ARCHS = ""; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Carthage/Build/MobileVLCKit.xcframework/ios-arm64_armv7_armv7s", - ); + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = JellyfinPlayer/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.1; LD_RUNPATH_SEARCH_PATHS = ( @@ -1294,6 +1205,7 @@ }; 5377CC1D263B596B003A4E83 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = BEEC50E7EFD4848C0E320941 /* Pods-JellyfinPlayer iOS.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -1308,10 +1220,7 @@ ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; EXCLUDED_ARCHS = ""; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Carthage/Build/MobileVLCKit.xcframework/ios-arm64_armv7_armv7s", - ); + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = JellyfinPlayer/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.1; LD_RUNPATH_SEARCH_PATHS = ( @@ -1437,30 +1346,6 @@ minimumVersion = 19.0.0; }; }; - 5362E4D1267D461F000E2F71 /* XCRemoteSwiftPackageReference "swift-protobuf" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/apple/swift-protobuf.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.0.0; - }; - }; - 5362E4D4267D4671000E2F71 /* XCRemoteSwiftPackageReference "Result" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/antitypical/Result"; - requirement = { - branch = master; - kind = branch; - }; - }; - 5362E4D7267D4695000E2F71 /* XCRemoteSwiftPackageReference "SwiftyJSON" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/SwiftyJSON/SwiftyJSON"; - requirement = { - branch = master; - kind = branch; - }; - }; 536D3D82267BEA550004248C /* XCRemoteSwiftPackageReference "ParallaxView" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/PGSSoft/ParallaxView"; @@ -1477,6 +1362,14 @@ kind = branch; }; }; + 53EC6E23267EB10F006DD26A /* XCRemoteSwiftPackageReference "SwiftyJSON" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/SwiftyJSON/SwiftyJSON"; + requirement = { + branch = master; + kind = branch; + }; + }; 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/kean/NukeUI"; @@ -1521,21 +1414,6 @@ package = 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */; productName = NukeUI; }; - 5362E4D2267D461F000E2F71 /* SwiftProtobuf */ = { - isa = XCSwiftPackageProductDependency; - package = 5362E4D1267D461F000E2F71 /* XCRemoteSwiftPackageReference "swift-protobuf" */; - productName = SwiftProtobuf; - }; - 5362E4D5267D4671000E2F71 /* Result */ = { - isa = XCSwiftPackageProductDependency; - package = 5362E4D4267D4671000E2F71 /* XCRemoteSwiftPackageReference "Result" */; - productName = Result; - }; - 5362E4D8267D4695000E2F71 /* SwiftyJSON */ = { - isa = XCSwiftPackageProductDependency; - package = 5362E4D7267D4695000E2F71 /* XCRemoteSwiftPackageReference "SwiftyJSON" */; - productName = SwiftyJSON; - }; 536D3D7C267BD5F90004248C /* ActivityIndicator */ = { isa = XCSwiftPackageProductDependency; package = 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */; @@ -1561,6 +1439,11 @@ package = 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */; productName = ActivityIndicator; }; + 53EC6E24267EB10F006DD26A /* SwiftyJSON */ = { + isa = XCSwiftPackageProductDependency; + package = 53EC6E23267EB10F006DD26A /* XCRemoteSwiftPackageReference "SwiftyJSON" */; + productName = SwiftyJSON; + }; 621C637F26672A30004216EA /* NukeUI */ = { isa = XCSwiftPackageProductDependency; package = 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */; diff --git a/JellyfinPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/JellyfinPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 77c500d2..3f989d64 100644 --- a/JellyfinPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/JellyfinPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -73,15 +73,6 @@ "version": "3.1.2" } }, - { - "package": "Result", - "repositoryURL": "https://github.com/antitypical/Result", - "state": { - "branch": "master", - "revision": "c30700bfcab7f555bf1d386fdd609407a94f369c", - "version": null - } - }, { "package": "swift-protobuf", "repositoryURL": "https://github.com/apple/swift-protobuf.git", diff --git a/JellyfinPlayer.xcodeproj/xcshareddata/xcschemes/JellyfinPlayer.xcscheme b/JellyfinPlayer.xcodeproj/xcshareddata/xcschemes/JellyfinPlayer.xcscheme index 496cbad2..ebd8a372 100644 --- a/JellyfinPlayer.xcodeproj/xcshareddata/xcschemes/JellyfinPlayer.xcscheme +++ b/JellyfinPlayer.xcodeproj/xcshareddata/xcschemes/JellyfinPlayer.xcscheme @@ -1,6 +1,6 @@ + + + + + + diff --git a/JellyfinPlayer.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/JellyfinPlayer.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/JellyfinPlayer.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved b/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..94c97f78 --- /dev/null +++ b/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,97 @@ +{ + "object": { + "pins": [ + { + "package": "ActivityIndicator", + "repositoryURL": "https://github.com/duyquang91/ActivityIndicator", + "state": { + "branch": null, + "revision": "0101a02196f6a67cf26f6434b007d3db6bd07fee", + "version": "1.1.0" + } + }, + { + "package": "AnyCodable", + "repositoryURL": "https://github.com/Flight-School/AnyCodable", + "state": { + "branch": null, + "revision": "876d162385e9862ae8b3c8d65dc301312b040005", + "version": "0.6.0" + } + }, + { + "package": "Gifu", + "repositoryURL": "https://github.com/kaishin/Gifu", + "state": { + "branch": null, + "revision": "51f2eab32903e336f590c013267cfa4d7f8b06c4", + "version": "3.3.1" + } + }, + { + "package": "jellyfin-sdk-swift", + "repositoryURL": "https://github.com/jellyfin/jellyfin-sdk-swift", + "state": { + "branch": "main", + "revision": "5cdc2419f547b3f31dc96f5eccaf3f303f44184b", + "version": null + } + }, + { + "package": "keychain-swift", + "repositoryURL": "https://github.com/evgenyneu/keychain-swift", + "state": { + "branch": null, + "revision": "96fb84f45a96630e7583903bd7e08cf095c7a7ef", + "version": "19.0.0" + } + }, + { + "package": "Nuke", + "repositoryURL": "https://github.com/kean/Nuke.git", + "state": { + "branch": null, + "revision": "69ae6d5b8c4b898450432f94bd35f863d3830cfc", + "version": "10.3.0" + } + }, + { + "package": "NukeUI", + "repositoryURL": "https://github.com/kean/NukeUI", + "state": { + "branch": null, + "revision": "d2580b8d22b29c6244418d8e4b568f3162191460", + "version": "0.3.0" + } + }, + { + "package": "ParallaxView", + "repositoryURL": "https://github.com/PGSSoft/ParallaxView", + "state": { + "branch": null, + "revision": "a4165b0edd9c9c923a1d6e3e4c9a807302a1a475", + "version": "3.1.2" + } + }, + { + "package": "SwiftUI-Introspect", + "repositoryURL": "https://github.com/siteline/SwiftUI-Introspect", + "state": { + "branch": null, + "revision": "2e09be8af614401bc9f87d40093ec19ce56ccaf2", + "version": "0.1.3" + } + }, + { + "package": "SwiftyJSON", + "repositoryURL": "https://github.com/SwiftyJSON/SwiftyJSON", + "state": { + "branch": "master", + "revision": "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07", + "version": null + } + } + ] + }, + "version": 1 +} diff --git a/JellyfinPlayer/EpisodeItemView.swift b/JellyfinPlayer/EpisodeItemView.swift index 38fc44bc..6f4f5c8b 100644 --- a/JellyfinPlayer/EpisodeItemView.swift +++ b/JellyfinPlayer/EpisodeItemView.swift @@ -64,7 +64,7 @@ struct EpisodeItemView: View { self.playbackInfo.shouldShowPlayer = true } label: { HStack { - Text(viewModel.item.getItemProgressString() == "" ? "Play" : "\(viewModel.item.getItemProgressString()) left") + Text(viewModel.item.getItemProgressString() == "" ? "Play" : viewModel.item.getItemProgressString()) .foregroundColor(Color.white).font(.callout).fontWeight(.semibold) Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20)) } diff --git a/JellyfinPlayer/Info.plist b/JellyfinPlayer/Info.plist index 57bf7db3..a31d2e3f 100644 --- a/JellyfinPlayer/Info.plist +++ b/JellyfinPlayer/Info.plist @@ -24,6 +24,8 @@ LSRequiresIPhoneOS + NSBluetoothAlwaysUsageDescription + ${PRODUCT_NAME} uses Bluetooth to discover nearby Cast devices. NSAppTransportSecurity NSAllowsArbitraryLoadsForMedia diff --git a/JellyfinPlayer/LatestMediaView.swift b/JellyfinPlayer/LatestMediaView.swift index 740524ce..137fc910 100644 --- a/JellyfinPlayer/LatestMediaView.swift +++ b/JellyfinPlayer/LatestMediaView.swift @@ -44,9 +44,6 @@ struct LatestMediaView: View { } } } - if viewModel.isLoading { - ProgressView() - } } } .frame(height: 190) diff --git a/JellyfinPlayer/MovieItemView.swift b/JellyfinPlayer/MovieItemView.swift index f9e917f2..d60243c2 100644 --- a/JellyfinPlayer/MovieItemView.swift +++ b/JellyfinPlayer/MovieItemView.swift @@ -70,7 +70,7 @@ struct MovieItemView: View { self.playbackInfo.shouldShowPlayer = true } label: { HStack { - Text(viewModel.item.getItemProgressString() == "" ? "Play" : "\(viewModel.item.getItemProgressString()) left") + Text(viewModel.item.getItemProgressString() == "" ? "Play" : viewModel.item.getItemProgressString()) .foregroundColor(Color.white).font(.callout).fontWeight(.semibold) Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20)) } diff --git a/JellyfinPlayer/OpenCastSwift/Definitions/CASTV2Protocol.swift b/JellyfinPlayer/OpenCastSwift/Definitions/CASTV2Protocol.swift deleted file mode 100644 index 3f629cb1..00000000 --- a/JellyfinPlayer/OpenCastSwift/Definitions/CASTV2Protocol.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// CASTV2Protocol.swift -// OpenCastSwift -// -// Created by Miles Hollingsworth on 4/22/18 -// Copyright © 2018 Miles Hollingsworth. All rights reserved. -// - -import Foundation - -struct CastNamespace { - static let auth = "urn:x-cast:com.google.cast.tp.deviceauth" - static let connection = "urn:x-cast:com.google.cast.tp.connection" - static let heartbeat = "urn:x-cast:com.google.cast.tp.heartbeat" - static let receiver = "urn:x-cast:com.google.cast.receiver" - static let media = "urn:x-cast:com.google.cast.media" - static let discovery = "urn:x-cast:com.google.cast.receiver.discovery" - static let setup = "urn:x-cast:com.google.cast.setup" - static let multizone = "urn:x-cast:com.google.cast.multizone" -} - -enum CastMessageType: String { - case ping = "PING" - case pong = "PONG" - case connect = "CONNECT" - case close = "CLOSE" - case status = "RECEIVER_STATUS" - case launch = "LAUNCH" - case stop = "STOP" - case load = "LOAD" - case pause = "PAUSE" - case play = "PLAY" - case seek = "SEEK" - case setVolume = "SET_VOLUME" - case setDeviceVolume = "SET_DEVICE_VOLUME" - case statusRequest = "GET_STATUS" - case availableApps = "GET_APP_AVAILABILITY" - case mediaStatus = "MEDIA_STATUS" - case getDeviceInfo = "GET_DEVICE_INFO" - case deviceInfo = "DEVICE_INFO" - case getDeviceConfig = "eureka_info" - case setDeviceConfig = "set_eureka_info" - case getAppDeviceId = "get_app_device_id" - case multizoneStatus = "MULTIZONE_STATUS" - case deviceAdded = "DEVICE_ADDED" - case deviceUpdated = "DEVICE_UPDATED" - case deviceRemoved = "DEVICE_REMOVED" - case invalidRequest = "INVALID_REQUEST" - case mdxSessionStatus = "mdxSessionStatus" -} - -struct CastJSONPayloadKeys { - static let type = "type" - static let requestId = "requestId" - static let status = "status" - static let applications = "applications" - static let appId = "appId" - static let displayName = "displayName" - static let sessionId = "sessionId" - static let transportId = "transportId" - static let statusText = "statusText" - static let isIdleScreen = "isIdleScreen" - static let namespaces = "namespaces" - static let volume = "volume" - static let controlType = "controlType" - static let level = "level" - static let muted = "muted" - static let mediaSessionId = "mediaSessionId" - static let availability = "availability" - static let name = "name" - static let currentTime = "currentTime" - static let media = "media" - static let repeatMode = "repeatMode" - static let autoplay = "autoplay" - static let contentId = "contentId" - static let contentType = "contentType" - static let streamType = "streamType" - static let metadata = "metadata" - static let metadataType = "metadataType" - static let title = "title" - static let images = "images" - static let url = "url" - static let activeTrackIds = "activeTrackIds" - static let playbackRate = "playbackRate" - static let playerState = "playerState" - static let deviceId = "deviceId" - static let device = "device" - static let devices = "devices" - static let capabilities = "capabilities" -} - -struct CastConstants { - static let sender = "sender-0" - static let receiver = "receiver-0" - static let transport = "transport-0" -} diff --git a/JellyfinPlayer/OpenCastSwift/Helpers/CastV2PlatformReader.swift b/JellyfinPlayer/OpenCastSwift/Helpers/CastV2PlatformReader.swift deleted file mode 100644 index a96bb4a1..00000000 --- a/JellyfinPlayer/OpenCastSwift/Helpers/CastV2PlatformReader.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// CastV2PlatformReader.swift -// OpenCastSwift Mac -// -// Created by Miles Hollingsworth on 4/22/18 -// Copyright © 2018 Miles Hollingsworth. All rights reserved. -// - -let maxBufferLength = 8192 - -import Foundation - -class CastV2PlatformReader { - let stream: InputStream - var readPosition = 0 - var buffer = Data(capacity: maxBufferLength) - - init(stream: InputStream) { - self.stream = stream - } - - func readStream() { - objc_sync_enter(self) - defer { objc_sync_exit(self) } - - var totalBytesRead = 0 - let bufferSize = 32 - - while stream.hasBytesAvailable { - var bytes = [UInt8](repeating: 0, count: bufferSize) - - let bytesRead = stream.read(&bytes, maxLength: bufferSize) - - if bytesRead < 0 { continue } - - buffer.append(Data(bytes: &bytes, count: bytesRead)) - - totalBytesRead += bytesRead - } - } - - func nextMessage() -> Data? { - objc_sync_enter(self) - defer { objc_sync_exit(self) } - - let headerSize = MemoryLayout.size - guard buffer.count - readPosition >= headerSize else { return nil } - let header = buffer.withUnsafeBytes({ (pointer: UnsafePointer) -> UInt32 in - return pointer.advanced(by: self.readPosition).withMemoryRebound(to: UInt32.self, capacity: 1, { $0.pointee }) - }) - - let payloadSize = Int(CFSwapInt32BigToHost(header)) - - readPosition += headerSize - - guard buffer.count >= readPosition + payloadSize, buffer.count - readPosition >= payloadSize, payloadSize >= 0 else { - // Message hasn't arrived - readPosition -= headerSize - return nil - } - - let payload = buffer.withUnsafeBytes({ (pointer: UnsafePointer) -> Data in - return Data(bytes: pointer.advanced(by: self.readPosition), count: payloadSize) - }) - readPosition += payloadSize - - resetBufferIfNeeded() - - return payload - } - - private func resetBufferIfNeeded() { - guard buffer.count >= maxBufferLength else { return } - - if readPosition == buffer.count { - buffer = Data(capacity: maxBufferLength) - } else { - buffer = buffer.advanced(by: readPosition) - } - - readPosition = 0 - } -} diff --git a/JellyfinPlayer/OpenCastSwift/Helpers/Proto/cast_channel.pb.swift b/JellyfinPlayer/OpenCastSwift/Helpers/Proto/cast_channel.pb.swift deleted file mode 100644 index 25a61633..00000000 --- a/JellyfinPlayer/OpenCastSwift/Helpers/Proto/cast_channel.pb.swift +++ /dev/null @@ -1,618 +0,0 @@ -// DO NOT EDIT. -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: cast_channel.proto -// -// For information on using the generated types, please see the documenation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2013 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import Foundation -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that your are building against the same version of the API -// that was used to generate this file. -private struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -struct Extensions_Api_CastChannel_CastMessage: SwiftProtobuf.Message { - static let protoMessageName: String = _protobuf_package + ".CastMessage" - - var protocolVersion: Extensions_Api_CastChannel_CastMessage.ProtocolVersion { - get {return _protocolVersion ?? .castv210} - set {_protocolVersion = newValue} - } - /// Returns true if `protocolVersion` has been explicitly set. - var hasProtocolVersion: Bool {return self._protocolVersion != nil} - /// Clears the value of `protocolVersion`. Subsequent reads from it will return its default value. - mutating func clearProtocolVersion() {self._protocolVersion = nil} - - /// source and destination ids identify the origin and destination of the - /// message. They are used to route messages between endpoints that share a - /// device-to-device channel. - /// - /// For messages between applications: - /// - The sender application id is a unique identifier generated on behalf of - /// the sender application. - /// - The receiver id is always the the session id for the application. - /// - /// For messages to or from the sender or receiver platform, the special ids - /// 'sender-0' and 'receiver-0' can be used. - /// - /// For messages intended for all endpoints using a given channel, the - /// wildcard destination_id '*' can be used. - var sourceID: String { - get {return _sourceID ?? String()} - set {_sourceID = newValue} - } - /// Returns true if `sourceID` has been explicitly set. - var hasSourceID: Bool {return self._sourceID != nil} - /// Clears the value of `sourceID`. Subsequent reads from it will return its default value. - mutating func clearSourceID() {self._sourceID = nil} - - var destinationID: String { - get {return _destinationID ?? String()} - set {_destinationID = newValue} - } - /// Returns true if `destinationID` has been explicitly set. - var hasDestinationID: Bool {return self._destinationID != nil} - /// Clears the value of `destinationID`. Subsequent reads from it will return its default value. - mutating func clearDestinationID() {self._destinationID = nil} - - /// This is the core multiplexing key. All messages are sent on a namespace - /// and endpoints sharing a channel listen on one or more namespaces. The - /// namespace defines the protocol and semantics of the message. - var namespace: String { - get {return _namespace ?? String()} - set {_namespace = newValue} - } - /// Returns true if `namespace` has been explicitly set. - var hasNamespace: Bool {return self._namespace != nil} - /// Clears the value of `namespace`. Subsequent reads from it will return its default value. - mutating func clearNamespace() {self._namespace = nil} - - var payloadType: Extensions_Api_CastChannel_CastMessage.PayloadType { - get {return _payloadType ?? .string} - set {_payloadType = newValue} - } - /// Returns true if `payloadType` has been explicitly set. - var hasPayloadType: Bool {return self._payloadType != nil} - /// Clears the value of `payloadType`. Subsequent reads from it will return its default value. - mutating func clearPayloadType() {self._payloadType = nil} - - /// Depending on payload_type, exactly one of the following optional fields - /// will always be set. - var payloadUtf8: String { - get {return _payloadUtf8 ?? String()} - set {_payloadUtf8 = newValue} - } - /// Returns true if `payloadUtf8` has been explicitly set. - var hasPayloadUtf8: Bool {return self._payloadUtf8 != nil} - /// Clears the value of `payloadUtf8`. Subsequent reads from it will return its default value. - mutating func clearPayloadUtf8() {self._payloadUtf8 = nil} - - var payloadBinary: Data { - get {return _payloadBinary ?? SwiftProtobuf.Internal.emptyData} - set {_payloadBinary = newValue} - } - /// Returns true if `payloadBinary` has been explicitly set. - var hasPayloadBinary: Bool {return self._payloadBinary != nil} - /// Clears the value of `payloadBinary`. Subsequent reads from it will return its default value. - mutating func clearPayloadBinary() {self._payloadBinary = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - /// Always pass a version of the protocol for future compatibility - /// requirements. - enum ProtocolVersion: SwiftProtobuf.Enum { - typealias RawValue = Int - case castv210 // = 0 - - init() { - self = .castv210 - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .castv210 - default: return nil - } - } - - var rawValue: Int { - switch self { - case .castv210: return 0 - } - } - - } - - /// What type of data do we have in this message. - enum PayloadType: SwiftProtobuf.Enum { - typealias RawValue = Int - case string // = 0 - case binary // = 1 - - init() { - self = .string - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .string - case 1: self = .binary - default: return nil - } - } - - var rawValue: Int { - switch self { - case .string: return 0 - case .binary: return 1 - } - } - - } - - init() {} - - public var isInitialized: Bool { - if self._protocolVersion == nil {return false} - if self._sourceID == nil {return false} - if self._destinationID == nil {return false} - if self._namespace == nil {return false} - if self._payloadType == nil {return false} - return true - } - - /// Used by the decoding initializers in the SwiftProtobuf library, not generally - /// used directly. `init(serializedData:)`, `init(jsonUTF8Data:)`, and other decoding - /// initializers are defined in the SwiftProtobuf library. See the Message and - /// Message+*Additions` files. - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - switch fieldNumber { - case 1: try decoder.decodeSingularEnumField(value: &self._protocolVersion) - case 2: try decoder.decodeSingularStringField(value: &self._sourceID) - case 3: try decoder.decodeSingularStringField(value: &self._destinationID) - case 4: try decoder.decodeSingularStringField(value: &self._namespace) - case 5: try decoder.decodeSingularEnumField(value: &self._payloadType) - case 6: try decoder.decodeSingularStringField(value: &self._payloadUtf8) - case 7: try decoder.decodeSingularBytesField(value: &self._payloadBinary) - default: break - } - } - } - - /// Used by the encoding methods of the SwiftProtobuf library, not generally - /// used directly. `Message.serializedData()`, `Message.jsonUTF8Data()`, and - /// other serializer methods are defined in the SwiftProtobuf library. See the - /// `Message` and `Message+*Additions` files. - func traverse(visitor: inout V) throws { - if let v = self._protocolVersion { - try visitor.visitSingularEnumField(value: v, fieldNumber: 1) - } - if let v = self._sourceID { - try visitor.visitSingularStringField(value: v, fieldNumber: 2) - } - if let v = self._destinationID { - try visitor.visitSingularStringField(value: v, fieldNumber: 3) - } - if let v = self._namespace { - try visitor.visitSingularStringField(value: v, fieldNumber: 4) - } - if let v = self._payloadType { - try visitor.visitSingularEnumField(value: v, fieldNumber: 5) - } - if let v = self._payloadUtf8 { - try visitor.visitSingularStringField(value: v, fieldNumber: 6) - } - if let v = self._payloadBinary { - try visitor.visitSingularBytesField(value: v, fieldNumber: 7) - } - try unknownFields.traverse(visitor: &visitor) - } - - fileprivate var _protocolVersion: Extensions_Api_CastChannel_CastMessage.ProtocolVersion? - fileprivate var _sourceID: String? - fileprivate var _destinationID: String? - fileprivate var _namespace: String? - fileprivate var _payloadType: Extensions_Api_CastChannel_CastMessage.PayloadType? - fileprivate var _payloadUtf8: String? - fileprivate var _payloadBinary: Data? -} - -/// Messages for authentication protocol between a sender and a receiver. -struct Extensions_Api_CastChannel_AuthChallenge: SwiftProtobuf.Message { - static let protoMessageName: String = _protobuf_package + ".AuthChallenge" - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - /// Used by the decoding initializers in the SwiftProtobuf library, not generally - /// used directly. `init(serializedData:)`, `init(jsonUTF8Data:)`, and other decoding - /// initializers are defined in the SwiftProtobuf library. See the Message and - /// Message+*Additions` files. - mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } - } - - /// Used by the encoding methods of the SwiftProtobuf library, not generally - /// used directly. `Message.serializedData()`, `Message.jsonUTF8Data()`, and - /// other serializer methods are defined in the SwiftProtobuf library. See the - /// `Message` and `Message+*Additions` files. - func traverse(visitor: inout V) throws { - try unknownFields.traverse(visitor: &visitor) - } -} - -struct Extensions_Api_CastChannel_AuthResponse: SwiftProtobuf.Message { - static let protoMessageName: String = _protobuf_package + ".AuthResponse" - - var signature: Data { - get {return _signature ?? SwiftProtobuf.Internal.emptyData} - set {_signature = newValue} - } - /// Returns true if `signature` has been explicitly set. - var hasSignature: Bool {return self._signature != nil} - /// Clears the value of `signature`. Subsequent reads from it will return its default value. - mutating func clearSignature() {self._signature = nil} - - var clientAuthCertificate: Data { - get {return _clientAuthCertificate ?? SwiftProtobuf.Internal.emptyData} - set {_clientAuthCertificate = newValue} - } - /// Returns true if `clientAuthCertificate` has been explicitly set. - var hasClientAuthCertificate: Bool {return self._clientAuthCertificate != nil} - /// Clears the value of `clientAuthCertificate`. Subsequent reads from it will return its default value. - mutating func clearClientAuthCertificate() {self._clientAuthCertificate = nil} - - var clientCa: [Data] = [] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - public var isInitialized: Bool { - if self._signature == nil {return false} - if self._clientAuthCertificate == nil {return false} - return true - } - - /// Used by the decoding initializers in the SwiftProtobuf library, not generally - /// used directly. `init(serializedData:)`, `init(jsonUTF8Data:)`, and other decoding - /// initializers are defined in the SwiftProtobuf library. See the Message and - /// Message+*Additions` files. - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - switch fieldNumber { - case 1: try decoder.decodeSingularBytesField(value: &self._signature) - case 2: try decoder.decodeSingularBytesField(value: &self._clientAuthCertificate) - case 3: try decoder.decodeRepeatedBytesField(value: &self.clientCa) - default: break - } - } - } - - /// Used by the encoding methods of the SwiftProtobuf library, not generally - /// used directly. `Message.serializedData()`, `Message.jsonUTF8Data()`, and - /// other serializer methods are defined in the SwiftProtobuf library. See the - /// `Message` and `Message+*Additions` files. - func traverse(visitor: inout V) throws { - if let v = self._signature { - try visitor.visitSingularBytesField(value: v, fieldNumber: 1) - } - if let v = self._clientAuthCertificate { - try visitor.visitSingularBytesField(value: v, fieldNumber: 2) - } - if !self.clientCa.isEmpty { - try visitor.visitRepeatedBytesField(value: self.clientCa, fieldNumber: 3) - } - try unknownFields.traverse(visitor: &visitor) - } - - fileprivate var _signature: Data? - fileprivate var _clientAuthCertificate: Data? -} - -struct Extensions_Api_CastChannel_AuthError: SwiftProtobuf.Message { - static let protoMessageName: String = _protobuf_package + ".AuthError" - - var errorType: Extensions_Api_CastChannel_AuthError.ErrorType { - get {return _errorType ?? .internalError} - set {_errorType = newValue} - } - /// Returns true if `errorType` has been explicitly set. - var hasErrorType: Bool {return self._errorType != nil} - /// Clears the value of `errorType`. Subsequent reads from it will return its default value. - mutating func clearErrorType() {self._errorType = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - enum ErrorType: SwiftProtobuf.Enum { - typealias RawValue = Int - case internalError // = 0 - - /// The underlying connection is not TLS - case noTls // = 1 - - init() { - self = .internalError - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .internalError - case 1: self = .noTls - default: return nil - } - } - - var rawValue: Int { - switch self { - case .internalError: return 0 - case .noTls: return 1 - } - } - - } - - init() {} - - public var isInitialized: Bool { - if self._errorType == nil {return false} - return true - } - - /// Used by the decoding initializers in the SwiftProtobuf library, not generally - /// used directly. `init(serializedData:)`, `init(jsonUTF8Data:)`, and other decoding - /// initializers are defined in the SwiftProtobuf library. See the Message and - /// Message+*Additions` files. - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - switch fieldNumber { - case 1: try decoder.decodeSingularEnumField(value: &self._errorType) - default: break - } - } - } - - /// Used by the encoding methods of the SwiftProtobuf library, not generally - /// used directly. `Message.serializedData()`, `Message.jsonUTF8Data()`, and - /// other serializer methods are defined in the SwiftProtobuf library. See the - /// `Message` and `Message+*Additions` files. - func traverse(visitor: inout V) throws { - if let v = self._errorType { - try visitor.visitSingularEnumField(value: v, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - fileprivate var _errorType: Extensions_Api_CastChannel_AuthError.ErrorType? -} - -struct Extensions_Api_CastChannel_DeviceAuthMessage: SwiftProtobuf.Message { - static let protoMessageName: String = _protobuf_package + ".DeviceAuthMessage" - - /// Request fields - var challenge: Extensions_Api_CastChannel_AuthChallenge { - get {return _storage._challenge ?? Extensions_Api_CastChannel_AuthChallenge()} - set {_uniqueStorage()._challenge = newValue} - } - /// Returns true if `challenge` has been explicitly set. - var hasChallenge: Bool {return _storage._challenge != nil} - /// Clears the value of `challenge`. Subsequent reads from it will return its default value. - mutating func clearChallenge() {_storage._challenge = nil} - - /// Response fields - var response: Extensions_Api_CastChannel_AuthResponse { - get {return _storage._response ?? Extensions_Api_CastChannel_AuthResponse()} - set {_uniqueStorage()._response = newValue} - } - /// Returns true if `response` has been explicitly set. - var hasResponse: Bool {return _storage._response != nil} - /// Clears the value of `response`. Subsequent reads from it will return its default value. - mutating func clearResponse() {_storage._response = nil} - - var error: Extensions_Api_CastChannel_AuthError { - get {return _storage._error ?? Extensions_Api_CastChannel_AuthError()} - set {_uniqueStorage()._error = newValue} - } - /// Returns true if `error` has been explicitly set. - var hasError: Bool {return _storage._error != nil} - /// Clears the value of `error`. Subsequent reads from it will return its default value. - mutating func clearError() {_storage._error = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - public var isInitialized: Bool { - return withExtendedLifetime(_storage) { (_storage: _StorageClass) in - if let v = _storage._response, !v.isInitialized {return false} - if let v = _storage._error, !v.isInitialized {return false} - return true - } - } - - /// Used by the decoding initializers in the SwiftProtobuf library, not generally - /// used directly. `init(serializedData:)`, `init(jsonUTF8Data:)`, and other decoding - /// initializers are defined in the SwiftProtobuf library. See the Message and - /// Message+*Additions` files. - mutating func decodeMessage(decoder: inout D) throws { - _ = _uniqueStorage() - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - while let fieldNumber = try decoder.nextFieldNumber() { - switch fieldNumber { - case 1: try decoder.decodeSingularMessageField(value: &_storage._challenge) - case 2: try decoder.decodeSingularMessageField(value: &_storage._response) - case 3: try decoder.decodeSingularMessageField(value: &_storage._error) - default: break - } - } - } - } - - /// Used by the encoding methods of the SwiftProtobuf library, not generally - /// used directly. `Message.serializedData()`, `Message.jsonUTF8Data()`, and - /// other serializer methods are defined in the SwiftProtobuf library. See the - /// `Message` and `Message+*Additions` files. - func traverse(visitor: inout V) throws { - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - if let v = _storage._challenge { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } - if let v = _storage._response { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } - if let v = _storage._error { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } - } - try unknownFields.traverse(visitor: &visitor) - } - - fileprivate var _storage = _StorageClass.defaultInstance -} - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -private let _protobuf_package = "extensions.api.cast_channel" - -extension Extensions_Api_CastChannel_CastMessage: SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "protocol_version"), - 2: .standard(proto: "source_id"), - 3: .standard(proto: "destination_id"), - 4: .same(proto: "namespace"), - 5: .standard(proto: "payload_type"), - 6: .standard(proto: "payload_utf8"), - 7: .standard(proto: "payload_binary") - ] - - func _protobuf_generated_isEqualTo(other: Extensions_Api_CastChannel_CastMessage) -> Bool { - if self._protocolVersion != other._protocolVersion {return false} - if self._sourceID != other._sourceID {return false} - if self._destinationID != other._destinationID {return false} - if self._namespace != other._namespace {return false} - if self._payloadType != other._payloadType {return false} - if self._payloadUtf8 != other._payloadUtf8 {return false} - if self._payloadBinary != other._payloadBinary {return false} - if unknownFields != other.unknownFields {return false} - return true - } -} - -extension Extensions_Api_CastChannel_CastMessage.ProtocolVersion: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "CASTV2_1_0") - ] -} - -extension Extensions_Api_CastChannel_CastMessage.PayloadType: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "STRING"), - 1: .same(proto: "BINARY") - ] -} - -extension Extensions_Api_CastChannel_AuthChallenge: SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap = SwiftProtobuf._NameMap() - - func _protobuf_generated_isEqualTo(other: Extensions_Api_CastChannel_AuthChallenge) -> Bool { - if unknownFields != other.unknownFields {return false} - return true - } -} - -extension Extensions_Api_CastChannel_AuthResponse: SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "signature"), - 2: .standard(proto: "client_auth_certificate"), - 3: .standard(proto: "client_ca") - ] - - func _protobuf_generated_isEqualTo(other: Extensions_Api_CastChannel_AuthResponse) -> Bool { - if self._signature != other._signature {return false} - if self._clientAuthCertificate != other._clientAuthCertificate {return false} - if self.clientCa != other.clientCa {return false} - if unknownFields != other.unknownFields {return false} - return true - } -} - -extension Extensions_Api_CastChannel_AuthError: SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "error_type") - ] - - func _protobuf_generated_isEqualTo(other: Extensions_Api_CastChannel_AuthError) -> Bool { - if self._errorType != other._errorType {return false} - if unknownFields != other.unknownFields {return false} - return true - } -} - -extension Extensions_Api_CastChannel_AuthError.ErrorType: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "INTERNAL_ERROR"), - 1: .same(proto: "NO_TLS") - ] -} - -extension Extensions_Api_CastChannel_DeviceAuthMessage: SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "challenge"), - 2: .same(proto: "response"), - 3: .same(proto: "error") - ] - - fileprivate class _StorageClass { - var _challenge: Extensions_Api_CastChannel_AuthChallenge? - var _response: Extensions_Api_CastChannel_AuthResponse? - var _error: Extensions_Api_CastChannel_AuthError? - - static let defaultInstance = _StorageClass() - - private init() {} - - init(copying source: _StorageClass) { - _challenge = source._challenge - _response = source._response - _error = source._error - } - } - - fileprivate mutating func _uniqueStorage() -> _StorageClass { - if !isKnownUniquelyReferenced(&_storage) { - _storage = _StorageClass(copying: _storage) - } - return _storage - } - - func _protobuf_generated_isEqualTo(other: Extensions_Api_CastChannel_DeviceAuthMessage) -> Bool { - if _storage !== other._storage { - let storagesAreEqual: Bool = withExtendedLifetime((_storage, other._storage)) { (_args: (_StorageClass, _StorageClass)) in - let _storage = _args.0 - let other_storage = _args.1 - if _storage._challenge != other_storage._challenge {return false} - if _storage._response != other_storage._response {return false} - if _storage._error != other_storage._error {return false} - return true - } - if !storagesAreEqual {return false} - } - if unknownFields != other.unknownFields {return false} - return true - } -} diff --git a/JellyfinPlayer/OpenCastSwift/Helpers/Proto/cast_channel.proto b/JellyfinPlayer/OpenCastSwift/Helpers/Proto/cast_channel.proto deleted file mode 100644 index 47d63940..00000000 --- a/JellyfinPlayer/OpenCastSwift/Helpers/Proto/cast_channel.proto +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2013 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -syntax = "proto2"; - -option optimize_for = LITE_RUNTIME; - -package extensions.api.cast_channel; - -message CastMessage { - // Always pass a version of the protocol for future compatibility - // requirements. - enum ProtocolVersion { - CASTV2_1_0 = 0; - } - required ProtocolVersion protocol_version = 1; - - // source and destination ids identify the origin and destination of the - // message. They are used to route messages between endpoints that share a - // device-to-device channel. - // - // For messages between applications: - // - The sender application id is a unique identifier generated on behalf of - // the sender application. - // - The receiver id is always the the session id for the application. - // - // For messages to or from the sender or receiver platform, the special ids - // 'sender-0' and 'receiver-0' can be used. - // - // For messages intended for all endpoints using a given channel, the - // wildcard destination_id '*' can be used. - required string source_id = 2; - required string destination_id = 3; - - // This is the core multiplexing key. All messages are sent on a namespace - // and endpoints sharing a channel listen on one or more namespaces. The - // namespace defines the protocol and semantics of the message. - required string namespace = 4; - - // Encoding and payload info follows. - - // What type of data do we have in this message. - enum PayloadType { - STRING = 0; - BINARY = 1; - } - required PayloadType payload_type = 5; - - // Depending on payload_type, exactly one of the following optional fields - // will always be set. - optional string payload_utf8 = 6; - optional bytes payload_binary = 7; -} - -// Messages for authentication protocol between a sender and a receiver. -message AuthChallenge { -} - -message AuthResponse { - required bytes signature = 1; - required bytes client_auth_certificate = 2; - repeated bytes client_ca = 3; -} - -message AuthError { - enum ErrorType { - INTERNAL_ERROR = 0; - NO_TLS = 1; // The underlying connection is not TLS - } - required ErrorType error_type = 1; -} - -message DeviceAuthMessage { - // Request fields - optional AuthChallenge challenge = 1; - // Response fields - optional AuthResponse response = 2; - optional AuthError error = 3; -} diff --git a/JellyfinPlayer/OpenCastSwift/Models/AppAvailability.swift b/JellyfinPlayer/OpenCastSwift/Models/AppAvailability.swift deleted file mode 100644 index 1ad12496..00000000 --- a/JellyfinPlayer/OpenCastSwift/Models/AppAvailability.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// AppAvailability.swift -// OpenCastSwift -// -// Created by Miles Hollingsworth on 4/22/18 -// Copyright © 2018 Miles Hollingsworth. All rights reserved. -// - -import SwiftyJSON -import Foundation - -public class AppAvailability: NSObject { - public var availability = [String: Bool]() -} - -extension AppAvailability { - convenience init(json: JSON) { - self.init() - - if let availability = json[CastJSONPayloadKeys.availability].dictionaryObject as? [String: String] { - self.availability = availability.mapValues { $0 == "APP_AVAILABLE" } - } - } -} diff --git a/JellyfinPlayer/OpenCastSwift/Models/CastApp.swift b/JellyfinPlayer/OpenCastSwift/Models/CastApp.swift deleted file mode 100644 index 8ac4f0f0..00000000 --- a/JellyfinPlayer/OpenCastSwift/Models/CastApp.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// CastApp.swift -// OpenCastSwift -// -// Created by Miles Hollingsworth on 4/22/18 -// Copyright © 2018 Miles Hollingsworth. All rights reserved. -// - -import Foundation -import SwiftyJSON - -public struct CastAppIdentifier { - public static let defaultMediaPlayer = "CC1AD845" - public static let youTube = "YouTube" - public static let googleAssistant = "97216CB6" -} - -public final class CastApp: NSObject { - public var id: String = "" - public var displayName: String = "" - public var isIdleScreen: Bool = false - public var sessionId: String = "" - public var statusText: String = "" - public var transportId: String = "" - public var namespaces = [String]() - - convenience init(json: JSON) { - self.init() - - if let id = json[CastJSONPayloadKeys.appId].string { - self.id = id - } - - if let displayName = json[CastJSONPayloadKeys.displayName].string { - self.displayName = displayName - } - - if let isIdleScreen = json[CastJSONPayloadKeys.isIdleScreen].bool { - self.isIdleScreen = isIdleScreen - } - - if let sessionId = json[CastJSONPayloadKeys.sessionId].string { - self.sessionId = sessionId - } - - if let statusText = json[CastJSONPayloadKeys.statusText].string { - self.statusText = statusText - } - - if let transportId = json[CastJSONPayloadKeys.transportId].string { - self.transportId = transportId - } - - if let namespaces = json[CastJSONPayloadKeys.namespaces].array { - self.namespaces = namespaces.compactMap { $0[CastJSONPayloadKeys.name].string } - } - } - - public override var description: String { - return "CastApp(id: \(id), displayName: \(displayName), isIdleScreen: \(isIdleScreen), sessionId: \(sessionId), statusText: \(statusText), transportId: \(transportId), namespaces: \(namespaces)" - } -} diff --git a/JellyfinPlayer/OpenCastSwift/Models/CastDevice.swift b/JellyfinPlayer/OpenCastSwift/Models/CastDevice.swift deleted file mode 100644 index b155a20b..00000000 --- a/JellyfinPlayer/OpenCastSwift/Models/CastDevice.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// CastDevice.swift -// OpenCastSwift -// -// Created by Miles Hollingsworth on 4/22/18 -// Copyright © 2018 Miles Hollingsworth. All rights reserved. -// - -import Foundation - -public struct DeviceCapabilities: OptionSet { - public let rawValue: Int - public init(rawValue: Int) { self.rawValue = rawValue } - - public static let none = DeviceCapabilities([]) - public static let videoOut = DeviceCapabilities(rawValue: 1 << 0) - public static let videoIn = DeviceCapabilities(rawValue: 1 << 1) - public static let audioOut = DeviceCapabilities(rawValue: 1 << 2) - public static let audioIn = DeviceCapabilities(rawValue: 1 << 3) - public static let multizoneGroup = DeviceCapabilities(rawValue: 1 << 5) - public static let masterVolume = DeviceCapabilities(rawValue: 1 << 11) - public static let attenuationVolume = DeviceCapabilities(rawValue: 1 << 12) -} - -public final class CastDevice: NSObject, NSCopying { - - public private(set) var id: String - public private(set) var name: String - public private(set) var modelName: String - public private(set) var hostName: String - public private(set) var ipAddress: String - public private(set) var port: Int - public private(set) var capabilities: DeviceCapabilities - public private(set) var status: String - public private(set) var iconPath: String - - init(id: String, name: String, modelName: String, hostName: String, ipAddress: String, port: Int, capabilitiesMask: Int, status: String, iconPath: String) { - self.id = id - self.name = name - self.modelName = modelName - self.hostName = hostName - self.ipAddress = ipAddress - self.port = port - capabilities = DeviceCapabilities(rawValue: capabilitiesMask) - self.status = status - self.iconPath = iconPath - - super.init() - } - - public func copy(with zone: NSZone? = nil) -> Any { - return CastDevice(id: self.id, - name: self.name, - modelName: self.modelName, - hostName: self.hostName, - ipAddress: self.ipAddress, - port: self.port, - capabilitiesMask: capabilities.rawValue, - status: self.status, - iconPath: iconPath) - } - - public override var description: String { - return "CastDevice(id: \(id), name: \(name), hostName:\(hostName), ipAddress:\(ipAddress), port:\(port))" - } -} diff --git a/JellyfinPlayer/OpenCastSwift/Models/CastMedia.swift b/JellyfinPlayer/OpenCastSwift/Models/CastMedia.swift deleted file mode 100644 index cee1db85..00000000 --- a/JellyfinPlayer/OpenCastSwift/Models/CastMedia.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// CastMedia.swift -// OpenCastSwift -// -// Created by Miles Hollingsworth on 4/22/18 -// Copyright © 2018 Miles Hollingsworth. All rights reserved. -// - -import Foundation - -public let CastMediaStreamTypeBuffered = "BUFFERED" -public let CastMediaStreamTypeLive = "LIVE" - -public enum CastMediaStreamType: String { - case buffered = "BUFFERED" - case live = "LIVE" -} - -public final class CastMedia: NSObject { - public let title: String - public let url: URL - public let poster: URL? - - public let autoplay: Bool - public let currentTime: Double - - public let contentType: String - public let streamType: CastMediaStreamType - - public init(title: String, url: URL, poster: URL? = nil, contentType: String, streamType: CastMediaStreamType = .buffered, autoplay: Bool = true, currentTime: Double = 0) { - self.title = title - self.url = url - self.poster = poster - self.contentType = contentType - self.streamType = streamType - self.autoplay = autoplay - self.currentTime = currentTime - } - -// public convenience init(title: String, url: URL, poster: URL, contentType: String, streamType: String, autoplay: Bool, currentTime: Double) { -// guard let type = CastMediaStreamType(rawValue: streamType) else { -// fatalError("Invalid media stream type \(streamType)") -// } -// -// self.init(title: title, url: url, poster: poster, contentType: contentType, streamType: type, autoplay: autoplay, currentTime: currentTime) -// } -} - -extension CastMedia { - - var dict: [String: Any] { - if let poster = poster { - return [ - CastJSONPayloadKeys.autoplay: autoplay, - CastJSONPayloadKeys.activeTrackIds: [], - CastJSONPayloadKeys.repeatMode: "REPEAT_OFF", - CastJSONPayloadKeys.currentTime: currentTime, - CastJSONPayloadKeys.media: [ - CastJSONPayloadKeys.contentId: url.absoluteString, - CastJSONPayloadKeys.contentType: contentType, - CastJSONPayloadKeys.streamType: streamType.rawValue, - CastJSONPayloadKeys.metadata: [ - CastJSONPayloadKeys.type: 0, - CastJSONPayloadKeys.metadataType: 0, - CastJSONPayloadKeys.title: title, - CastJSONPayloadKeys.images: [ - [CastJSONPayloadKeys.url: poster.absoluteString] - ] - ] - ] - ] - } else { - return [ - CastJSONPayloadKeys.autoplay: autoplay, - CastJSONPayloadKeys.activeTrackIds: [], - CastJSONPayloadKeys.repeatMode: "REPEAT_OFF", - CastJSONPayloadKeys.currentTime: currentTime, - CastJSONPayloadKeys.media: [ - CastJSONPayloadKeys.contentId: url.absoluteString, - CastJSONPayloadKeys.contentType: contentType, - CastJSONPayloadKeys.streamType: streamType.rawValue, - CastJSONPayloadKeys.metadata: [ - CastJSONPayloadKeys.type: 0, - CastJSONPayloadKeys.metadataType: 0, - CastJSONPayloadKeys.title: title - ] - ] - ] - } - } - -} diff --git a/JellyfinPlayer/OpenCastSwift/Models/CastMediaStatus.swift b/JellyfinPlayer/OpenCastSwift/Models/CastMediaStatus.swift deleted file mode 100644 index ccd18a93..00000000 --- a/JellyfinPlayer/OpenCastSwift/Models/CastMediaStatus.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// CastMediaStatus.swift -// OpenCastSwift -// -// Created by Miles Hollingsworth on 4/22/18 -// Copyright © 2018 Miles Hollingsworth. All rights reserved. -// - -import Foundation -import SwiftyJSON - -public enum CastMediaPlayerState: String { - case buffering = "BUFFERING" - case playing = "PLAYING" - case paused = "PAUSED" - case stopped = "STOPPED" -} - -public final class CastMediaStatus: NSObject { - - public let mediaSessionId: Int - public let playbackRate: Int - public let playerState: CastMediaPlayerState - public let currentTime: Double - public let metadata: JSON? - public let contentID: String? - private let createdDate = Date() - - public var adjustedCurrentTime: Double { - return currentTime - Double(playbackRate)*createdDate.timeIntervalSinceNow - } - - public var state: String { - return playerState.rawValue - } - - public override var description: String { - return "MediaStatus(mediaSessionId: \(mediaSessionId), playbackRate: \(playbackRate), playerState: \(playerState.rawValue), currentTime: \(currentTime))" - } - - init(json: JSON) { - mediaSessionId = json[CastJSONPayloadKeys.mediaSessionId].int ?? 0 - - playbackRate = json[CastJSONPayloadKeys.playbackRate].int ?? 1 - - playerState = json[CastJSONPayloadKeys.playerState].string.flatMap(CastMediaPlayerState.init) ?? .buffering - - currentTime = json[CastJSONPayloadKeys.currentTime].double ?? 0 - - metadata = json[CastJSONPayloadKeys.media][CastJSONPayloadKeys.metadata] - - if let contentID = json[CastJSONPayloadKeys.media][CastJSONPayloadKeys.contentId].string, let data = contentID.data(using: .utf8) { - self.contentID = (try? JSON(data: data))?[CastJSONPayloadKeys.contentId].string ?? contentID - } else { - contentID = nil - } - - super.init() - } -} diff --git a/JellyfinPlayer/OpenCastSwift/Models/CastMessage.swift b/JellyfinPlayer/OpenCastSwift/Models/CastMessage.swift deleted file mode 100644 index 4957d737..00000000 --- a/JellyfinPlayer/OpenCastSwift/Models/CastMessage.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// CastMessage.swift -// OpenCastSwift -// -// Created by Miles Hollingsworth on 4/22/18 -// Copyright © 2018 Miles Hollingsworth. All rights reserved. -// - -import Foundation - -extension CastMessage { - static func encodedMessage(payload: CastPayload, namespace: String, sourceId: String, destinationId: String) throws -> Data { - var message = CastMessage() - message.protocolVersion = .castv210 - message.sourceID = sourceId - message.destinationID = destinationId - message.namespace = namespace - - switch payload { - case .json(let payload): - let json = try JSONSerialization.data(withJSONObject: payload, options: []) - - guard let jsonString = String(data: json, encoding: .utf8) else { - fatalError("error forming json string") - } - - message.payloadType = .string - message.payloadUtf8 = jsonString - case .data(let payload): - message.payloadType = .binary - message.payloadBinary = payload - } - - return try message.serializedData() - } -} diff --git a/JellyfinPlayer/OpenCastSwift/Models/CastMultizoneDevice.swift b/JellyfinPlayer/OpenCastSwift/Models/CastMultizoneDevice.swift deleted file mode 100644 index eb79b919..00000000 --- a/JellyfinPlayer/OpenCastSwift/Models/CastMultizoneDevice.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// CastMultizoneDevice.swift -// OpenCastSwift Mac -// -// Created by Miles Hollingsworth on 4/22/18 -// Copyright © 2018 Miles Hollingsworth. All rights reserved. -// - -import Foundation -import SwiftyJSON - -public class CastMultizoneDevice { - public let name: String - public let volume: Float - public let isMuted: Bool - public let capabilities: DeviceCapabilities - public let id: String - - public init(name: String, volume: Float, isMuted: Bool, capabilitiesMask: Int, id: String) { - self.name = name - self.volume = volume - self.isMuted = isMuted - capabilities = DeviceCapabilities(rawValue: capabilitiesMask) - self.id = id - } -} - -extension CastMultizoneDevice { - convenience init(json: JSON) { - let name = json[CastJSONPayloadKeys.name].stringValue - - let volumeValues = json[CastJSONPayloadKeys.volume] - - let volume = volumeValues[CastJSONPayloadKeys.level].floatValue - let isMuted = volumeValues[CastJSONPayloadKeys.muted].boolValue - let capabilitiesMask = json[CastJSONPayloadKeys.capabilities].intValue - let deviceId = json[CastJSONPayloadKeys.deviceId].stringValue - - self.init(name: name, volume: volume, isMuted: isMuted, capabilitiesMask: capabilitiesMask, id: deviceId) - } - -} diff --git a/JellyfinPlayer/OpenCastSwift/Models/CastMultizoneStatus.swift b/JellyfinPlayer/OpenCastSwift/Models/CastMultizoneStatus.swift deleted file mode 100644 index d2584b59..00000000 --- a/JellyfinPlayer/OpenCastSwift/Models/CastMultizoneStatus.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// CastMultizoneStatus.swift -// OpenCastSwift Mac -// -// Created by Miles Hollingsworth on 4/22/18 -// Copyright © 2018 Miles Hollingsworth. All rights reserved. -// - -import Foundation -import SwiftyJSON - -public class CastMultizoneStatus { - public let devices: [CastMultizoneDevice] - - public init(devices: [CastMultizoneDevice]) { - self.devices = devices - } -} - -extension CastMultizoneStatus { - - convenience init(json: JSON) { - let status = json[CastJSONPayloadKeys.status] - let devices = status[CastJSONPayloadKeys.devices].array?.map(CastMultizoneDevice.init) ?? [] - - self.init(devices: devices) - } - -} diff --git a/JellyfinPlayer/OpenCastSwift/Models/CastStatus.swift b/JellyfinPlayer/OpenCastSwift/Models/CastStatus.swift deleted file mode 100644 index 1d7dabda..00000000 --- a/JellyfinPlayer/OpenCastSwift/Models/CastStatus.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// CastStatus.swift -// OpenCastSwift -// -// Created by Miles Hollingsworth on 4/22/18 -// Copyright © 2018 Miles Hollingsworth. All rights reserved. -// - -import Foundation -import SwiftyJSON - -public final class CastStatus: NSObject { - - public var volume: Double = 1 - public var muted: Bool = false - public var apps: [CastApp] = [] - - public override var description: String { - return "CastStatus(volume: \(volume), muted: \(muted), apps: \(apps))" - } - -} - -extension CastStatus { - - convenience init(json: JSON) { - self.init() -// print(json) - let status = json[CastJSONPayloadKeys.status] - let volume = status[CastJSONPayloadKeys.volume] - - if let volume = volume[CastJSONPayloadKeys.level].double { - self.volume = volume - } - if let muted = volume[CastJSONPayloadKeys.muted].bool { - self.muted = muted - } - - if let apps = status[CastJSONPayloadKeys.applications].array { - self.apps = apps.compactMap(CastApp.init) - } - } - -} diff --git a/JellyfinPlayer/OpenCastSwift/Networking/CastClient.swift b/JellyfinPlayer/OpenCastSwift/Networking/CastClient.swift deleted file mode 100644 index e592a8f4..00000000 --- a/JellyfinPlayer/OpenCastSwift/Networking/CastClient.swift +++ /dev/null @@ -1,626 +0,0 @@ -// -// CastClient.swift -// OpenCastSwift -// -// Created by Miles Hollingsworth on 4/22/18 -// Copyright © 2018 Miles Hollingsworth. All rights reserved. -// - -import Foundation -import SwiftProtobuf -import SwiftyJSON -import Result - -public enum CastPayload { - case json([String: Any]) - case data(Data) - - init(_ json: [String: Any]) { - self = .json(json) - } - - init(_ data: Data) { - self = .data(data) - } -} - -typealias CastMessage = Extensions_Api_CastChannel_CastMessage -public typealias CastResponseHandler = (Result) -> Void - -public enum CastError: Error { - case connection(String) - case write(String) - case session(String) - case request(String) - case launch(String) - case load(String) -} - -public class CastRequest: NSObject { - var id: Int - var namespace: String - var destinationId: String - var payload: CastPayload - - init(id: Int, namespace: String, destinationId: String, payload: [String: Any]) { - self.id = id - self.namespace = namespace - self.destinationId = destinationId - self.payload = CastPayload(payload) - } - - init(id: Int, namespace: String, destinationId: String, payload: Data) { - self.id = id - self.namespace = namespace - self.destinationId = destinationId - self.payload = CastPayload(payload) - } -} - -@objc public protocol CastClientDelegate: AnyObject { - - @objc optional func castClient(_ client: CastClient, willConnectTo device: CastDevice) - @objc optional func castClient(_ client: CastClient, didConnectTo device: CastDevice) - @objc optional func castClient(_ client: CastClient, didDisconnectFrom device: CastDevice) - @objc optional func castClient(_ client: CastClient, connectionTo device: CastDevice, didFailWith error: Error?) - - @objc optional func castClient(_ client: CastClient, deviceStatusDidChange status: CastStatus) - @objc optional func castClient(_ client: CastClient, mediaStatusDidChange status: CastMediaStatus) - -} - -public final class CastClient: NSObject, RequestDispatchable, Channelable { - - public let device: CastDevice - public weak var delegate: CastClientDelegate? - public var connectedApp: CastApp? - - public private(set) var currentStatus: CastStatus? { - didSet { - guard let status = currentStatus else { return } - - if oldValue != status { - DispatchQueue.main.async { - self.delegate?.castClient?(self, deviceStatusDidChange: status) - self.statusDidChange?(status) - } - } - } - } - - public private(set) var currentMediaStatus: CastMediaStatus? { - didSet { - guard let status = currentMediaStatus else { return } - - if oldValue != status { - DispatchQueue.main.async { - self.delegate?.castClient?(self, mediaStatusDidChange: status) - self.mediaStatusDidChange?(status) - } - } - } - } - - public private(set) var currentMultizoneStatus: CastMultizoneStatus? - - public var statusDidChange: ((CastStatus) -> Void)? - public var mediaStatusDidChange: ((CastMediaStatus) -> Void)? - - public init(device: CastDevice) { - self.device = device - - super.init() - } - - deinit { - disconnect() - } - - // MARK: - Socket Setup - - public var isConnected = false { - didSet { - if oldValue != isConnected { - if isConnected { - DispatchQueue.main.async { self.delegate?.castClient?(self, didConnectTo: self.device) } - } else { - DispatchQueue.main.async { self.delegate?.castClient?(self, didDisconnectFrom: self.device) } - } - } - } - } - - private var inputStream: InputStream! { - didSet { - if let inputStream = inputStream { - reader = CastV2PlatformReader(stream: inputStream) - } else { - reader = nil - } - } - } - - private var outputStream: OutputStream! - - fileprivate lazy var socketQueue = DispatchQueue.global(qos: .userInitiated) - - public func connect() { - socketQueue.async { - do { - var readStream: Unmanaged? - var writeStream: Unmanaged? - - let settings: [String: Any] = [ - kCFStreamSSLValidatesCertificateChain as String: false, - kCFStreamSSLLevel as String: kCFStreamSocketSecurityLevelTLSv1, - kCFStreamPropertyShouldCloseNativeSocket as String: true - ] - - CFStreamCreatePairWithSocketToHost(nil, self.device.hostName as CFString, UInt32(self.device.port), &readStream, &writeStream) - - guard let readStreamRetained = readStream?.takeRetainedValue() else { - throw CastError.connection("Unable to create input stream") - } - - guard let writeStreamRetained = writeStream?.takeRetainedValue() else { - throw CastError.connection("Unable to create output stream") - } - - DispatchQueue.main.async { self.delegate?.castClient?(self, willConnectTo: self.device) } - - CFReadStreamSetProperty(readStreamRetained, CFStreamPropertyKey(kCFStreamPropertySSLSettings), settings as CFTypeRef?) - CFWriteStreamSetProperty(writeStreamRetained, CFStreamPropertyKey(kCFStreamPropertySSLSettings), settings as CFTypeRef?) - - self.inputStream = readStreamRetained - self.outputStream = writeStreamRetained - - self.inputStream.delegate = self - - self.inputStream.schedule(in: .current, forMode: RunLoop.Mode.default) - self.outputStream.schedule(in: .current, forMode: RunLoop.Mode.default) - - self.inputStream.open() - self.outputStream.open() - - RunLoop.current.run() - } catch { - DispatchQueue.main.async { self.delegate?.castClient?(self, connectionTo: self.device, didFailWith: error as NSError) } - } - } - } - - public func disconnect() { - if isConnected { - isConnected = false - } - - channels.values.forEach(remove) - - socketQueue.async { - if self.inputStream != nil { - self.inputStream.close() - self.inputStream.remove(from: RunLoop.current, forMode: RunLoop.Mode.default) - self.inputStream = nil - } - - if self.outputStream != nil { - self.outputStream.close() - self.outputStream.remove(from: RunLoop.current, forMode: RunLoop.Mode.default) - self.outputStream = nil - } - } - } - - // MARK: - Socket Lifecycle - - private func write(data: Data) throws { - var payloadSize = UInt32(data.count).bigEndian - let packet = NSMutableData(bytes: &payloadSize, length: MemoryLayout.size) - packet.append(data) - - let streamBytes = packet.bytes.bindMemory(to: UInt8.self, capacity: data.count) - - if outputStream.write(streamBytes, maxLength: packet.length) < 0 { - if let error = outputStream.streamError { - throw CastError.write("Error writing \(packet.length) byte(s) to stream: \(error)") - } else { - throw CastError.write("Unknown error writing \(packet.length) byte(s) to stream") - } - } - } - - fileprivate func sendConnectMessage() throws { - guard outputStream != nil else { return } - - _ = connectionChannel - - DispatchQueue.main.async { - _ = self.receiverControlChannel - _ = self.mediaControlChannel - _ = self.heartbeatChannel - - if self.device.capabilities.contains(.multizoneGroup) { - _ = self.multizoneControlChannel - } - } - } - - private var reader: CastV2PlatformReader? - - fileprivate func readStream() { - do { - reader?.readStream() - - while let payload = reader?.nextMessage() { - let message = try CastMessage(serializedData: payload) - - guard let channel = channels[message.namespace] else { - return - } - - switch message.payloadType { - case .string: - if let messageData = message.payloadUtf8.data(using: .utf8) { - let json = JSON(messageData) - - channel.handleResponse(json, - sourceId: message.sourceID) - - if let requestId = json[CastJSONPayloadKeys.requestId].int { - callResponseHandler(for: requestId, with: Result(value: json)) - } - } else { - NSLog("Unable to get UTF8 JSON data from message") - } - case .binary: - channel.handleResponse(message.payloadBinary, - sourceId: message.sourceID) - } - } - } catch { - NSLog("Error reading: \(error)") - } - } - - // MARK: - Channelable - - var channels = [String: CastChannel]() - - private lazy var heartbeatChannel: HeartbeatChannel = { - let channel = HeartbeatChannel() - self.add(channel: channel) - - return channel - }() - - private lazy var connectionChannel: DeviceConnectionChannel = { - let channel = DeviceConnectionChannel() - self.add(channel: channel) - - return channel - }() - - private lazy var receiverControlChannel: ReceiverControlChannel = { - let channel = ReceiverControlChannel() - self.add(channel: channel) - - return channel - }() - - private lazy var mediaControlChannel: MediaControlChannel = { - let channel = MediaControlChannel() - self.add(channel: channel) - - return channel - }() - - private lazy var multizoneControlChannel: MultizoneControlChannel = { - let channel = MultizoneControlChannel() - self.add(channel: channel) - - return channel - }() - - // MARK: - Request response - - private lazy var currentRequestId = Int(arc4random_uniform(800)) - - func nextRequestId() -> Int { - currentRequestId += 1 - - return currentRequestId - } - - private let senderName: String = "sender-\(UUID().uuidString)" - - private var responseHandlers = [Int: CastResponseHandler]() - - func send(_ request: CastRequest, response: CastResponseHandler?) { - if let response = response { - responseHandlers[request.id] = response - } - - do { - let messageData = try CastMessage.encodedMessage(payload: request.payload, - namespace: request.namespace, - sourceId: senderName, - destinationId: request.destinationId) - try write(data: messageData) - } catch { - callResponseHandler(for: request.id, with: Result(error: .request(error.localizedDescription))) - } - } - - private func callResponseHandler(for requestId: Int, with result: Result) { - DispatchQueue.main.async { - if let handler = self.responseHandlers.removeValue(forKey: requestId) { - handler(result) - } - } - } - - // MARK: - Public messages - - public func getAppAvailability(apps: [CastApp], completion: @escaping (Result) -> Void) { - guard outputStream != nil else { return } - - receiverControlChannel.getAppAvailability(apps: apps, completion: completion) - } - - public func join(app: CastApp? = nil, completion: @escaping (Result) -> Void) { - guard outputStream != nil, - let target = app ?? currentStatus?.apps.first else { - completion(Result(error: CastError.session("No Apps Running"))) - return - } - - if target == connectedApp { - completion(Result(value: target)) - } else if let existing = currentStatus?.apps.first(where: { $0.id == target.id }) { - connect(to: existing) - completion(Result(value: existing)) - } else { - receiverControlChannel.requestStatus { [weak self] result in - switch result { - case .success(let status): - guard let app = status.apps.first else { - completion(Result(error: CastError.launch("Unable to get launched app instance"))) - return - } - - self?.connect(to: app) - completion(Result(value: app)) - - case .failure(let error): - completion(Result(error: error)) - } - } - } - } - - public func launch(appId: String, completion: @escaping (Result) -> Void) { - guard outputStream != nil else { return } - - receiverControlChannel.launch(appId: appId) { [weak self] result in - switch result { - case .success(let app): - self?.connect(to: app) - fallthrough - - default: - completion(result) - } - } - } - - public func stopCurrentApp() { - guard outputStream != nil, let app = currentStatus?.apps.first else { return } - - receiverControlChannel.stop(app: app) - } - - public func leave(_ app: CastApp) { - guard outputStream != nil else { return } - - connectionChannel.leave(app) - connectedApp = nil - } - - public func load(media: CastMedia, with app: CastApp, completion: @escaping (Result) -> Void) { - guard outputStream != nil else { return } - - mediaControlChannel.load(media: media, with: app, completion: completion) - } - - public func requestMediaStatus(for app: CastApp, completion: ((Result) -> Void)? = nil) { - guard outputStream != nil else { return } - - mediaControlChannel.requestMediaStatus(for: app) - } - - private func connect(to app: CastApp) { - guard outputStream != nil else { return } - - connectionChannel.connect(to: app) - connectedApp = app - } - - public func pause() { - guard outputStream != nil, let app = connectedApp else { return } - - if let mediaStatus = currentMediaStatus { - mediaControlChannel.sendPause(for: app, mediaSessionId: mediaStatus.mediaSessionId) - } else { - mediaControlChannel.requestMediaStatus(for: app) { result in - switch result { - case .success(let mediaStatus): - self.mediaControlChannel.sendPause(for: app, mediaSessionId: mediaStatus.mediaSessionId) - - case .failure(let error): - print(error) - } - } - } - } - - public func play() { - guard outputStream != nil, let app = connectedApp else { return } - - if let mediaStatus = currentMediaStatus { - mediaControlChannel.sendPlay(for: app, mediaSessionId: mediaStatus.mediaSessionId) - } else { - mediaControlChannel.requestMediaStatus(for: app) { result in - switch result { - case .success(let mediaStatus): - self.mediaControlChannel.sendPlay(for: app, mediaSessionId: mediaStatus.mediaSessionId) - - case .failure(let error): - print(error) - } - } - } - } - - public func stop() { - guard outputStream != nil, let app = connectedApp else { return } - - if let mediaStatus = currentMediaStatus { - mediaControlChannel.sendStop(for: app, mediaSessionId: mediaStatus.mediaSessionId) - } else { - mediaControlChannel.requestMediaStatus(for: app) { result in - switch result { - case .success(let mediaStatus): - self.mediaControlChannel.sendStop(for: app, mediaSessionId: mediaStatus.mediaSessionId) - - case .failure(let error): - print(error) - } - } - } - } - - public func seek(to currentTime: Float) { - guard outputStream != nil, let app = connectedApp else { return } - - if let mediaStatus = currentMediaStatus { - mediaControlChannel.sendSeek(to: currentTime, for: app, mediaSessionId: mediaStatus.mediaSessionId) - } else { - mediaControlChannel.requestMediaStatus(for: app) { result in - switch result { - case .success(let mediaStatus): - self.mediaControlChannel.sendSeek(to: currentTime, for: app, mediaSessionId: mediaStatus.mediaSessionId) - - case .failure(let error): - print(error) - } - } - } - } - - public func setVolume(_ volume: Float) { - guard outputStream != nil else { return } - - receiverControlChannel.setVolume(volume) - } - - public func setMuted(_ muted: Bool) { - guard outputStream != nil else { return } - - receiverControlChannel.setMuted(muted) - } - - public func setVolume(_ volume: Float, for device: CastMultizoneDevice) { - guard device.capabilities.contains(.multizoneGroup) else { - print("Attempted to set zone volume on non-multizone device") - return - } - - multizoneControlChannel.setVolume(volume, for: device) - } - - public func setMuted(_ isMuted: Bool, for device: CastMultizoneDevice) { - guard device.capabilities.contains(.multizoneGroup) else { - print("Attempted to mute zone on non-multizone device") - return - } - - multizoneControlChannel.setMuted(isMuted, for: device) - } -} - -extension CastClient: StreamDelegate { - public func stream(_ aStream: Stream, handle eventCode: Stream.Event) { - switch eventCode { - case Stream.Event.openCompleted: - guard !isConnected else { return } - socketQueue.async { - do { - try self.sendConnectMessage() - - } catch { - NSLog("Error sending connect message: \(error)") - } - } - case Stream.Event.errorOccurred: - NSLog("Stream error occurred: \(aStream.streamError.debugDescription)") - - DispatchQueue.main.async { - self.delegate?.castClient?(self, connectionTo: self.device, didFailWith: aStream.streamError) - } - case Stream.Event.hasBytesAvailable: - socketQueue.async { - self.readStream() - } - case Stream.Event.endEncountered: - NSLog("Input stream ended") - disconnect() - - default: break - } - } -} - -extension CastClient: ReceiverControlChannelDelegate { - func channel(_ channel: ReceiverControlChannel, didReceive status: CastStatus) { - currentStatus = status - } -} - -extension CastClient: MediaControlChannelDelegate { - func channel(_ channel: MediaControlChannel, didReceive mediaStatus: CastMediaStatus) { - currentMediaStatus = mediaStatus - } -} - -extension CastClient: HeartbeatChannelDelegate { - func channelDidConnect(_ channel: HeartbeatChannel) { - if !isConnected { - isConnected = true - } - } - - func channelDidTimeout(_ channel: HeartbeatChannel) { - disconnect() - currentStatus = nil - currentMediaStatus = nil - connectedApp = nil - } -} - -extension CastClient: MultizoneControlChannelDelegate { - func channel(_ channel: MultizoneControlChannel, added device: CastMultizoneDevice) { - - } - - func channel(_ channel: MultizoneControlChannel, updated device: CastMultizoneDevice) { - - } - - func channel(_ channel: MultizoneControlChannel, removed deviceId: String) { - - } - - func channel(_ channel: MultizoneControlChannel, didReceive status: CastMultizoneStatus) { - currentMultizoneStatus = status - } -} diff --git a/JellyfinPlayer/OpenCastSwift/Networking/CastDeviceScanner.swift b/JellyfinPlayer/OpenCastSwift/Networking/CastDeviceScanner.swift deleted file mode 100644 index 38d45bb6..00000000 --- a/JellyfinPlayer/OpenCastSwift/Networking/CastDeviceScanner.swift +++ /dev/null @@ -1,219 +0,0 @@ -// -// CastDeviceScanner.swift -// OpenCastSwift -// -// Created by Miles Hollingsworth on 4/22/18 -// Copyright © 2018 Miles Hollingsworth. All rights reserved. -// - -import Foundation - -extension CastDevice { - convenience init(service: NetService, info: [String: String]) { - var ipAddress: String? - - if let address = service.addresses?.first { - ipAddress = address.withUnsafeBytes { (pointer: UnsafePointer) -> String? in - var hostName = [CChar](repeating: 0, count: Int(NI_MAXHOST)) - - return getnameinfo(pointer, socklen_t(address.count), &hostName, socklen_t(NI_MAXHOST), nil, 0, NI_NUMERICHOST) == 0 ? String.init(cString: hostName) : nil - } - } - - self.init(id: info["id"] ?? "", - name: info["fn"] ?? service.name, - modelName: info["md"] ?? "Google Cast", - hostName: service.hostName!, - ipAddress: ipAddress ?? "", - port: service.port, - capabilitiesMask: info["ca"].flatMap(Int.init) ?? 0 , - status: info["rs"] ?? "", - iconPath: info["ic"] ?? "") - } - -} - -public final class CastDeviceScanner: NSObject { - public weak var delegate: CastDeviceScannerDelegate? - - public static let deviceListDidChange = Notification.Name(rawValue: "DeviceScannerDeviceListDidChangeNotification") - - private lazy var browser: NetServiceBrowser = configureBrowser() - - public var isScanning = false - - fileprivate var services = [NetService]() - - public fileprivate(set) var devices = [CastDevice]() { - didSet { - NotificationCenter.default.post(name: CastDeviceScanner.deviceListDidChange, object: self) - } - } - - private func configureBrowser() -> NetServiceBrowser { - let b = NetServiceBrowser() - - b.includesPeerToPeer = true - b.delegate = self - - return b - } - - public func startScanning() { - guard !isScanning else { return } - - browser.stop() - browser.searchForServices(ofType: "_googlecast._tcp", inDomain: "local") - - #if DEBUG - NSLog("Started scanning") - #endif - } - - public func stopScanning() { - guard isScanning else { return } - - browser.stop() - - #if DEBUG - NSLog("Stopped scanning") - #endif - } - - public func reset() { - stopScanning() - devices.removeAll() - } - - deinit { - stopScanning() - } - -} - -extension CastDeviceScanner: NetServiceBrowserDelegate { - - public func netServiceBrowserWillSearch(_ browser: NetServiceBrowser) { - isScanning = true - } - - public func netServiceBrowserDidStopSearch(_ browser: NetServiceBrowser) { - isScanning = false - } - - public func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) { - removeService(service) - - service.delegate = self - service.resolve(withTimeout: 30.0) - services.append(service) - - #if DEBUG - NSLog("Did find service: \(service) more: \(moreComing)") - #endif - } - - public func netServiceBrowser(_ browser: NetServiceBrowser, didRemove service: NetService, moreComing: Bool) { - guard let service = removeService(service) else { return } - - #if DEBUG - NSLog("Did remove service: \(service)") - #endif - - guard let deviceId = service.id, - let index = devices.firstIndex(where: { $0.id == deviceId }) else { - #if DEBUG - NSLog("No device") - #endif - - return - } - - #if DEBUG - NSLog("Removing device: \(devices[index])") - #endif - let device = devices.remove(at: index) - delegate?.deviceDidGoOffline(device) - } - - @discardableResult func removeService(_ service: NetService) -> NetService? { - if let index = services.firstIndex(of: service) { - return services.remove(at: index) - } - - return nil - } - - func addDevice(_ device: CastDevice) { - if let index = devices.firstIndex(where: { $0.id == device.id }) { - let existing = devices[index] - - guard existing.name != device.name || existing.hostName != device.hostName else { return } - - devices.remove(at: index) - devices.insert(device, at: index) - - delegate?.deviceDidChange(device) - } else { - devices.append(device) - delegate?.deviceDidComeOnline(device) - } - } -} - -extension CastDeviceScanner: NetServiceDelegate { - - public func netServiceDidResolveAddress(_ sender: NetService) { - guard let infoDict = sender.infoDict else { - #if DEBUG - NSLog("No TXT record for \(sender), skipping") - #endif - return - } - - #if DEBUG - NSLog("Did resolve service: \(sender)") - NSLog("\(infoDict)") - #endif - - guard infoDict["id"] != nil else { - #if DEBUG - NSLog("No id for device \(sender), skipping") - #endif - return - } - - addDevice(CastDevice(service: sender, info: infoDict)) - } - - public func netService(_ sender: NetService, didNotResolve errorDict: [String: NSNumber]) { - removeService(sender) - - #if DEBUG - NSLog("!! Failed to resolve service: \(sender) - \(errorDict) !!") - #endif - } -} - -extension NetService { - var infoDict: [String: String]? { - guard let data = txtRecordData() else { - return nil - } - - var dict = [String: String]() - NetService.dictionary(fromTXTRecord: data).forEach({ dict[$0.key] = String(data: $0.value, encoding: .utf8)! }) - - return dict - } - - var id: String? { - return infoDict?["id"] - } -} - -public protocol CastDeviceScannerDelegate: AnyObject { - func deviceDidComeOnline(_ device: CastDevice) - func deviceDidChange(_ device: CastDevice) - func deviceDidGoOffline(_ device: CastDevice) -} diff --git a/JellyfinPlayer/OpenCastSwift/Networking/Channels/CastChannel.swift b/JellyfinPlayer/OpenCastSwift/Networking/Channels/CastChannel.swift deleted file mode 100644 index fce246bb..00000000 --- a/JellyfinPlayer/OpenCastSwift/Networking/Channels/CastChannel.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// CastChannel.swift -// OpenCastSwift -// -// Created by Miles Hollingsworth on 4/22/18 -// Copyright © 2018 Miles Hollingsworth. All rights reserved. -// - -import Foundation -import SwiftyJSON - -open class CastChannel { - let namespace: String - weak var requestDispatcher: RequestDispatchable! - - init(namespace: String) { - self.namespace = namespace - } - - open func handleResponse(_ json: JSON, sourceId: String) { -// print(json) - } - - open func handleResponse(_ data: Data, sourceId: String) { - print("\n--Binary response--\n") - } - - public func send(_ request: CastRequest, response: CastResponseHandler? = nil) { - requestDispatcher.send(request, response: response) - } -} diff --git a/JellyfinPlayer/OpenCastSwift/Networking/Channels/Channelable.swift b/JellyfinPlayer/OpenCastSwift/Networking/Channels/Channelable.swift deleted file mode 100644 index 549be0b7..00000000 --- a/JellyfinPlayer/OpenCastSwift/Networking/Channels/Channelable.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// Channelable.swift -// OpenCastSwift Mac -// -// Created by Miles Hollingsworth on 4/22/18 -// Copyright © 2018 Miles Hollingsworth. All rights reserved. -// - -import Foundation - -protocol Channelable: RequestDispatchable { - var channels: [String: CastChannel] { get set } - - func add(channel: CastChannel) - func remove(channel: CastChannel) -} - -extension Channelable { - public func add(channel: CastChannel) { - let namespace = channel.namespace - guard channels[namespace] == nil else { - print("Channel already attached for \(namespace)") - return - } - - channels[namespace] = channel - channel.requestDispatcher = self - } - - public func remove(channel: CastChannel) { - let namespace = channel.namespace - guard let channel = channels.removeValue(forKey: namespace) else { - print("No channel attached for \(namespace)") - return - } - - channel.requestDispatcher = nil - } -} diff --git a/JellyfinPlayer/OpenCastSwift/Networking/Channels/DeviceAuthChannel.swift b/JellyfinPlayer/OpenCastSwift/Networking/Channels/DeviceAuthChannel.swift deleted file mode 100644 index af7656dc..00000000 --- a/JellyfinPlayer/OpenCastSwift/Networking/Channels/DeviceAuthChannel.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// DeviceAuthChannel.swift -// OpenCastSwift Mac -// -// Created by Miles Hollingsworth on 4/22/18 -// Copyright © 2018 Miles Hollingsworth. All rights reserved. -// - -import Foundation - -class DeviceAuthChannel: CastChannel { - typealias CastAuthChallenge = Extensions_Api_CastChannel_AuthChallenge - typealias CastAuthMessage = Extensions_Api_CastChannel_DeviceAuthMessage - - init() { - super.init(namespace: CastNamespace.auth) - } - - public func sendAuthChallenge() throws { - let message = CastAuthMessage.with { - $0.challenge = CastAuthChallenge() - } - - let request = requestDispatcher.request(withNamespace: namespace, - destinationId: CastConstants.receiver, - payload: try message.serializedData()) - - send(request) - } -} diff --git a/JellyfinPlayer/OpenCastSwift/Networking/Channels/DeviceConnectionChannel.swift b/JellyfinPlayer/OpenCastSwift/Networking/Channels/DeviceConnectionChannel.swift deleted file mode 100644 index 936ceb1e..00000000 --- a/JellyfinPlayer/OpenCastSwift/Networking/Channels/DeviceConnectionChannel.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// DeviceConnectionChannel.swift -// OpenCastSwift Mac -// -// Created by Miles Hollingsworth on 4/22/18 -// Copyright © 2018 Miles Hollingsworth. All rights reserved. -// - -import Foundation - -class DeviceConnectionChannel: CastChannel { - override weak var requestDispatcher: RequestDispatchable! { - didSet { - if let _ = requestDispatcher { - connect() - } - } - } - - init() { - super.init(namespace: CastNamespace.connection) - } - - func connect() { - let request = requestDispatcher.request(withNamespace: namespace, - destinationId: CastConstants.receiver, - payload: [CastJSONPayloadKeys.type: CastMessageType.connect.rawValue]) - - send(request) - } - - func connect(to app: CastApp) { - let request = requestDispatcher.request(withNamespace: namespace, - destinationId: app.transportId, - payload: [CastJSONPayloadKeys.type: CastMessageType.connect.rawValue]) - - send(request) - } - - public func leave(_ app: CastApp) { - let request = requestDispatcher.request(withNamespace: namespace, - destinationId: app.transportId, - payload: [CastJSONPayloadKeys.type: CastMessageType.close.rawValue]) - - send(request) - } -} diff --git a/JellyfinPlayer/OpenCastSwift/Networking/Channels/DeviceDiscoveryChannel.swift b/JellyfinPlayer/OpenCastSwift/Networking/Channels/DeviceDiscoveryChannel.swift deleted file mode 100644 index 6b044631..00000000 --- a/JellyfinPlayer/OpenCastSwift/Networking/Channels/DeviceDiscoveryChannel.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// DeviceDiscoveryChannel.swift -// OpenCastSwift -// -// Created by Miles Hollingsworth on 4/22/18 -// Copyright © 2018 Miles Hollingsworth. All rights reserved. -// - -import Foundation - -class DeviceDiscoveryChannel: CastChannel { - init() { - super.init(namespace: CastNamespace.discovery) - } - - func requestDeviceInfo() { - let request = requestDispatcher.request(withNamespace: namespace, - destinationId: CastConstants.receiver, - payload: [CastJSONPayloadKeys.type: CastMessageType.getDeviceInfo.rawValue]) - - send(request) { result in - switch result { - case .success(let json): - print(json) - - case .failure(let error): - print(error) - } - } - } -} diff --git a/JellyfinPlayer/OpenCastSwift/Networking/Channels/DeviceSetupChannel.swift b/JellyfinPlayer/OpenCastSwift/Networking/Channels/DeviceSetupChannel.swift deleted file mode 100644 index 078674f0..00000000 --- a/JellyfinPlayer/OpenCastSwift/Networking/Channels/DeviceSetupChannel.swift +++ /dev/null @@ -1,97 +0,0 @@ -// -// DeviceSetupChannel.swift -// OpenCastSwift -// -// Created by Miles Hollingsworth on 4/22/18 -// Copyright © 2018 Miles Hollingsworth. All rights reserved. -// - -import Foundation - -class DeviceSetupChannel: CastChannel { - init() { - super.init(namespace: CastNamespace.setup) - } - - public func requestDeviceConfig() { - let params = [ - "version", - "name", - "build_info.cast_build_revision", - "net.ip_address", - "net.online", - "net.ssid", - "wifi.signal_level", - "wifi.noise_level" - ] - - let payload: [String: Any] = [ - CastJSONPayloadKeys.type: CastMessageType.getDeviceConfig.rawValue, - "params": params, - "data": [:] - ] - - let request = requestDispatcher.request(withNamespace: namespace, - destinationId: CastConstants.receiver, - payload: payload) - - send(request) { result in - switch result { - case .success(let json): - print(json) - - case .failure(let error): - print(error) - } - } - } - - public func requestSetDeviceConfig() { -// let data: [String: Any] = [ -// "name": "JUNK", -// "settings": [ -// -// ] -// ] - - let payload: [String: Any] = [ - CastJSONPayloadKeys.type: CastMessageType.getDeviceConfig.rawValue, - "data": [:] - ] - - let request = requestDispatcher.request(withNamespace: namespace, - destinationId: CastConstants.receiver, - payload: payload) - - send(request) { result in - switch result { - case .success(let json): - print(json) - - case .failure(let error): - print(error) - } - } - } - - public func requestAppDeviceId(app: CastApp) { - let payload: [String: Any] = [ - CastJSONPayloadKeys.type: CastMessageType.getAppDeviceId.rawValue, - "data": ["app_id": app.id] - ] - - let request = requestDispatcher.request(withNamespace: namespace, - destinationId: CastConstants.receiver, - payload: payload) - - send(request) { result in - switch result { - case .success(let json): - print(json) - - case .failure(let error): - print(error) - } - } - } -} diff --git a/JellyfinPlayer/OpenCastSwift/Networking/Channels/HeartbeatChannel.swift b/JellyfinPlayer/OpenCastSwift/Networking/Channels/HeartbeatChannel.swift deleted file mode 100644 index 38bf5a5f..00000000 --- a/JellyfinPlayer/OpenCastSwift/Networking/Channels/HeartbeatChannel.swift +++ /dev/null @@ -1,97 +0,0 @@ -// -// CastHeartbeatChannel.swift -// OpenCastSwift -// -// Created by Miles Hollingsworth on 4/22/18 -// Copyright © 2018 Miles Hollingsworth. All rights reserved. -// - -import Foundation -import SwiftyJSON - -class HeartbeatChannel: CastChannel { - private lazy var timer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(sendPing), userInfo: nil, repeats: true) - - private let disconnectTimeout: TimeInterval = 10 - private var disconnectTimer: Timer? { - willSet { - disconnectTimer?.invalidate() - } - didSet { - guard let timer = disconnectTimer else { return } - - RunLoop.main.add(timer, forMode: RunLoop.Mode.common) - } - } - - override weak var requestDispatcher: RequestDispatchable! { - didSet { - if let _ = requestDispatcher { - startBeating() - } else { - timer.invalidate() - } - } - } - - private var delegate: HeartbeatChannelDelegate? { - return requestDispatcher as? HeartbeatChannelDelegate - } - - init() { - super.init(namespace: CastNamespace.heartbeat) - } - - override func handleResponse(_ json: JSON, sourceId: String) { - delegate?.channelDidConnect(self) - - guard let rawType = json["type"].string else { return } - - guard let type = CastMessageType(rawValue: rawType) else { - print("Unknown type: \(rawType)") - print(json) - return - } - - if type == .ping { - sendPong(to: sourceId) - print("PING from \(sourceId)") - } - - disconnectTimer = Timer(timeInterval: disconnectTimeout, - target: self, - selector: #selector(handleTimeout), - userInfo: nil, - repeats: false) - } - - private func startBeating() { - _ = timer - sendPing() - } - - @objc private func sendPing() { - let request = requestDispatcher.request(withNamespace: namespace, - destinationId: CastConstants.transport, - payload: [CastJSONPayloadKeys.type: CastMessageType.ping.rawValue]) - - send(request) - } - - private func sendPong(to destinationId: String) { - let request = requestDispatcher.request(withNamespace: namespace, - destinationId: destinationId, - payload: [CastJSONPayloadKeys.type: CastMessageType.pong.rawValue]) - - send(request) - } - - @objc private func handleTimeout() { - delegate?.channelDidTimeout(self) - } -} - -protocol HeartbeatChannelDelegate: AnyObject { - func channelDidConnect(_ channel: HeartbeatChannel) - func channelDidTimeout(_ channel: HeartbeatChannel) -} diff --git a/JellyfinPlayer/OpenCastSwift/Networking/Channels/MediaControlChannel.swift b/JellyfinPlayer/OpenCastSwift/Networking/Channels/MediaControlChannel.swift deleted file mode 100644 index 0d0884d8..00000000 --- a/JellyfinPlayer/OpenCastSwift/Networking/Channels/MediaControlChannel.swift +++ /dev/null @@ -1,132 +0,0 @@ -// -// MediaControlChannel.swift -// OpenCastSwift -// -// Created by Miles Hollingsworth on 4/22/18 -// Copyright © 2018 Miles Hollingsworth. All rights reserved. -// - -import Foundation -import Result -import SwiftyJSON - -class MediaControlChannel: CastChannel { - private var delegate: MediaControlChannelDelegate? { - return requestDispatcher as? MediaControlChannelDelegate - } - - init() { - super.init(namespace: CastNamespace.media) - } - - override func handleResponse(_ json: JSON, sourceId: String) { - guard let rawType = json["type"].string else { return } - - guard let type = CastMessageType(rawValue: rawType) else { - print("Unknown type: \(rawType)") - print(json) - return - } - - switch type { - case .mediaStatus: - guard let status = json["status"].array?.first else { return } - - delegate?.channel(self, didReceive: CastMediaStatus(json: status)) - - default: - print(rawType) - } - } - - public func requestMediaStatus(for app: CastApp, completion: ((Result) -> Void)? = nil) { - let payload: [String: Any] = [ - CastJSONPayloadKeys.type: CastMessageType.statusRequest.rawValue, - CastJSONPayloadKeys.sessionId: app.sessionId - ] - - let request = requestDispatcher.request(withNamespace: namespace, - destinationId: app.transportId, - payload: payload) - - if let completion = completion { - send(request) { result in - switch result { - case .success(let json): - completion(Result(value: CastMediaStatus(json: json))) - - case .failure(let error): - completion(Result(error: error)) - } - } - } else { - send(request) - } - } - - public func sendPause(for app: CastApp, mediaSessionId: Int) { - send(.pause, for: app, mediaSessionId: mediaSessionId) - } - - public func sendPlay(for app: CastApp, mediaSessionId: Int) { - send(.play, for: app, mediaSessionId: mediaSessionId) - } - - public func sendStop(for app: CastApp, mediaSessionId: Int) { - send(.stop, for: app, mediaSessionId: mediaSessionId) - } - - public func sendSeek(to currentTime: Float, for app: CastApp, mediaSessionId: Int) { - let payload: [String: Any] = [ - CastJSONPayloadKeys.type: CastMessageType.seek.rawValue, - CastJSONPayloadKeys.sessionId: app.sessionId, - CastJSONPayloadKeys.currentTime: currentTime, - CastJSONPayloadKeys.mediaSessionId: mediaSessionId - ] - - let request = requestDispatcher.request(withNamespace: namespace, - destinationId: app.transportId, - payload: payload) - - send(request) - } - - private func send(_ message: CastMessageType, for app: CastApp, mediaSessionId: Int) { - let payload: [String: Any] = [ - CastJSONPayloadKeys.type: message.rawValue, - CastJSONPayloadKeys.mediaSessionId: mediaSessionId - ] - - let request = requestDispatcher.request(withNamespace: namespace, - destinationId: app.transportId, - payload: payload) - - send(request) - } - - public func load(media: CastMedia, with app: CastApp, completion: @escaping (Result) -> Void) { - var payload = media.dict - payload[CastJSONPayloadKeys.type] = CastMessageType.load.rawValue - payload[CastJSONPayloadKeys.sessionId] = app.sessionId - - let request = requestDispatcher.request(withNamespace: namespace, - destinationId: app.transportId, - payload: payload) - - send(request) { result in - switch result { - case .success(let json): - guard let status = json["status"].array?.first else { return } - - completion(Result(value: CastMediaStatus(json: status))) - - case .failure(let error): - completion(Result(error: CastError.load(error.localizedDescription))) - } - } - } -} - -protocol MediaControlChannelDelegate: AnyObject { - func channel(_ channel: MediaControlChannel, didReceive mediaStatus: CastMediaStatus) -} diff --git a/JellyfinPlayer/OpenCastSwift/Networking/Channels/MultizoneControlChannel.swift b/JellyfinPlayer/OpenCastSwift/Networking/Channels/MultizoneControlChannel.swift deleted file mode 100644 index 010259f5..00000000 --- a/JellyfinPlayer/OpenCastSwift/Networking/Channels/MultizoneControlChannel.swift +++ /dev/null @@ -1,115 +0,0 @@ -// -// MultizoneControlChannel.swift -// OpenCastSwift Mac -// -// Created by Miles Hollingsworth on 4/22/18 -// Copyright © 2018 Miles Hollingsworth. All rights reserved. -// - -import Foundation -import Result -import SwiftyJSON - -class MultizoneControlChannel: CastChannel { - override weak var requestDispatcher: RequestDispatchable! { - didSet { - if let _ = requestDispatcher { - requestStatus() - } - } - } - - private var delegate: MultizoneControlChannelDelegate? { - return requestDispatcher as? MultizoneControlChannelDelegate - } - - init() { - super.init(namespace: CastNamespace.multizone) - } - - override func handleResponse(_ json: JSON, sourceId: String) { - guard let rawType = json["type"].string else { return } - - guard let type = CastMessageType(rawValue: rawType) else { - print("Unknown type: \(rawType)") - print(json) - return - } - - switch type { - case .multizoneStatus: - delegate?.channel(self, didReceive: CastMultizoneStatus(json: json)) - - case .deviceAdded: - let device = CastMultizoneDevice(json: json[CastJSONPayloadKeys.device]) - delegate?.channel(self, added: device) - - case .deviceUpdated: - let device = CastMultizoneDevice(json: json[CastJSONPayloadKeys.device]) - delegate?.channel(self, updated: device) - - case .deviceRemoved: - guard let deviceId = json[CastJSONPayloadKeys.deviceId].string else { return } - delegate?.channel(self, removed: deviceId) - - default: - print(rawType) - print(json) - } - } - - public func requestStatus(completion: ((Result) -> Void)? = nil) { - let request = requestDispatcher.request(withNamespace: namespace, - destinationId: CastConstants.receiver, - payload: [CastJSONPayloadKeys.type: CastMessageType.statusRequest.rawValue]) - - if let completion = completion { - send(request) { result in - switch result { - case .success(let json): - completion(Result(value: CastStatus(json: json))) - - case .failure(let error): - completion(Result(error: error)) - } - } - } else { - send(request) - } - } - - public func setVolume(_ volume: Float, for device: CastMultizoneDevice) { - let payload: [String: Any] = [ - CastJSONPayloadKeys.type: CastMessageType.setDeviceVolume.rawValue, - CastJSONPayloadKeys.volume: [CastJSONPayloadKeys.level: volume], - CastJSONPayloadKeys.deviceId: device.id - ] - - let request = requestDispatcher.request(withNamespace: namespace, - destinationId: CastConstants.receiver, - payload: payload) - - send(request) - } - - public func setMuted(_ isMuted: Bool, for device: CastMultizoneDevice) { - let payload: [String: Any] = [ - CastJSONPayloadKeys.type: CastMessageType.setVolume.rawValue, - CastJSONPayloadKeys.volume: [CastJSONPayloadKeys.muted: isMuted], - CastJSONPayloadKeys.deviceId: device.id - ] - - let request = requestDispatcher.request(withNamespace: namespace, - destinationId: CastConstants.receiver, - payload: payload) - - send(request) - } -} - -protocol MultizoneControlChannelDelegate: AnyObject { - func channel(_ channel: MultizoneControlChannel, didReceive status: CastMultizoneStatus) - func channel(_ channel: MultizoneControlChannel, added device: CastMultizoneDevice) - func channel(_ channel: MultizoneControlChannel, updated device: CastMultizoneDevice) - func channel(_ channel: MultizoneControlChannel, removed deviceId: String) -} diff --git a/JellyfinPlayer/OpenCastSwift/Networking/Channels/ReceiverControlChannel.swift b/JellyfinPlayer/OpenCastSwift/Networking/Channels/ReceiverControlChannel.swift deleted file mode 100644 index 85a738a1..00000000 --- a/JellyfinPlayer/OpenCastSwift/Networking/Channels/ReceiverControlChannel.swift +++ /dev/null @@ -1,157 +0,0 @@ -// -// ReceiverControlChannel.swift -// OpenCastSwift -// -// Created by Miles Hollingsworth on 4/22/18 -// Copyright © 2018 Miles Hollingsworth. All rights reserved. -// - -import Foundation -import Result -import SwiftyJSON - -class ReceiverControlChannel: CastChannel { - override weak var requestDispatcher: RequestDispatchable! { - didSet { - if let _ = requestDispatcher { - requestStatus() - } - } - } - - private var delegate: ReceiverControlChannelDelegate? { - return requestDispatcher as? ReceiverControlChannelDelegate - } - - init() { - super.init(namespace: CastNamespace.receiver) - } - - override func handleResponse(_ json: JSON, sourceId: String) { - guard let rawType = json["type"].string else { return } - - guard let type = CastMessageType(rawValue: rawType) else { - print("Unknown type: \(rawType)") - print(json) - return - } - - switch type { - case .status: - delegate?.channel(self, didReceive: CastStatus(json: json)) - - default: - print(rawType) - } - } - - public func getAppAvailability(apps: [CastApp], completion: @escaping (Result) -> Void) { - let payload: [String: Any] = [ - CastJSONPayloadKeys.type: CastMessageType.availableApps.rawValue, - CastJSONPayloadKeys.appId: apps.map { $0.id } - ] - - let request = requestDispatcher.request(withNamespace: namespace, - destinationId: CastConstants.receiver, - payload: payload) - - send(request) { result in - switch result { - case .success(let json): - completion(Result(value: AppAvailability(json: json))) - case .failure(let error): - completion(Result(error: CastError.launch(error.localizedDescription))) - } - } - } - - public func requestStatus(completion: ((Result) -> Void)? = nil) { - let request = requestDispatcher.request(withNamespace: namespace, - destinationId: CastConstants.receiver, - payload: [CastJSONPayloadKeys.type: CastMessageType.statusRequest.rawValue]) - - if let completion = completion { - send(request) { result in - switch result { - case .success(let json): - completion(Result(value: CastStatus(json: json))) - - case .failure(let error): - completion(Result(error: error)) - } - } - } else { - send(request) - } - } - - func launch(appId: String, completion: @escaping (Result) -> Void) { - let payload: [String: Any] = [ - CastJSONPayloadKeys.type: CastMessageType.launch.rawValue, - CastJSONPayloadKeys.appId: appId - ] - - let request = requestDispatcher.request(withNamespace: namespace, - destinationId: CastConstants.receiver, - payload: payload) - - send(request) { result in - switch result { - case .success(let json): - guard let app = CastStatus(json: json).apps.first else { - completion(Result(error: CastError.launch("Unable to get launched app instance"))) - return - } - - completion(Result(value: app)) - - case .failure(let error): - completion(Result(error: error)) - } - - } - } - - public func stop(app: CastApp) { - let payload: [String: Any] = [ - CastJSONPayloadKeys.type: CastMessageType.stop.rawValue, - CastJSONPayloadKeys.sessionId: app.sessionId - ] - - let request = requestDispatcher.request(withNamespace: namespace, - destinationId: CastConstants.receiver, - payload: payload) - - send(request) - } - - public func setVolume(_ volume: Float) { - let payload: [String: Any] = [ - CastJSONPayloadKeys.type: CastMessageType.setVolume.rawValue, - CastJSONPayloadKeys.volume: [CastJSONPayloadKeys.level: volume] - ] - - let request = requestDispatcher.request(withNamespace: namespace, - destinationId: CastConstants.receiver, - payload: payload) - - send(request) - } - - public func setMuted(_ isMuted: Bool) { - let payload: [String: Any] = [ - CastJSONPayloadKeys.type: CastMessageType.setVolume.rawValue, - CastJSONPayloadKeys.volume: [CastJSONPayloadKeys.muted: isMuted] - ] - - let request = requestDispatcher.request(withNamespace: namespace, - destinationId: CastConstants.receiver, - payload: payload) - - send(request) - } -} - -protocol ReceiverControlChannelDelegate: RequestDispatchable { - func channel(_ channel: ReceiverControlChannel, didReceive status: CastStatus) -} diff --git a/JellyfinPlayer/OpenCastSwift/Networking/Channels/RequestSink.swift b/JellyfinPlayer/OpenCastSwift/Networking/Channels/RequestSink.swift deleted file mode 100644 index 25492c84..00000000 --- a/JellyfinPlayer/OpenCastSwift/Networking/Channels/RequestSink.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// RequestDispatchable.swift -// OpenCastSwift Mac -// -// Created by Miles Hollingsworth on 4/22/18 -// Copyright © 2018 Miles Hollingsworth. All rights reserved. -// - -import Foundation - -protocol RequestDispatchable: AnyObject { - func nextRequestId() -> Int - - func request(withNamespace namespace: String, destinationId: String, payload: [String: Any]) -> CastRequest - func request(withNamespace namespace: String, destinationId: String, payload: Data) -> CastRequest - - func send(_ request: CastRequest, response: CastResponseHandler?) -} - -extension RequestDispatchable { - func request(withNamespace namespace: String, destinationId: String, payload: [String: Any]) -> CastRequest { - var payload = payload - let requestId = nextRequestId() - payload[CastJSONPayloadKeys.requestId] = requestId - - return CastRequest(id: requestId, - namespace: namespace, - destinationId: destinationId, - payload: payload) - } - - func request(withNamespace namespace: String, destinationId: String, payload: Data) -> CastRequest { - return CastRequest(id: nextRequestId(), - namespace: namespace, - destinationId: destinationId, - payload: payload) - } -} diff --git a/JellyfinPlayer/OpenCastSwift/Supporting Files/ChromeCastCore.h b/JellyfinPlayer/OpenCastSwift/Supporting Files/ChromeCastCore.h deleted file mode 100644 index aa85e2c7..00000000 --- a/JellyfinPlayer/OpenCastSwift/Supporting Files/ChromeCastCore.h +++ /dev/null @@ -1,12 +0,0 @@ -// -// OpenCastSwift.h -// OpenCastSwift -// -// Created by Miles Hollingsworth on 4/22/18 -// Copyright © 2018 Miles Hollingsworth. All rights reserved. -// - -@import Foundation; - -FOUNDATION_EXPORT double ChromeCastCoreVersionNumber; -FOUNDATION_EXPORT const unsigned char ChromeCastCoreVersionString[]; diff --git a/JellyfinPlayer/VideoPlayer.storyboard b/JellyfinPlayer/VideoPlayer.storyboard index 09975b3e..a4c0ad6c 100644 --- a/JellyfinPlayer/VideoPlayer.storyboard +++ b/JellyfinPlayer/VideoPlayer.storyboard @@ -17,30 +17,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/JellyfinPlayer/VideoPlayer.swift b/JellyfinPlayer/VideoPlayer.swift index 1e5b30c8..dc5a4ecd 100644 --- a/JellyfinPlayer/VideoPlayer.swift +++ b/JellyfinPlayer/VideoPlayer.swift @@ -10,6 +10,7 @@ import MobileVLCKit import JellyfinAPI import MediaPlayer import Combine +import GoogleCast import SwiftyJSON struct Subtitle { @@ -41,7 +42,7 @@ protocol PlayerViewControllerDelegate: AnyObject { func exitPlayer(_ viewController: PlayerViewController) } -class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDelegate, CastClientDelegate { +class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRemoteMediaClientListener { weak var delegate: PlayerViewControllerDelegate? @@ -69,33 +70,28 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe var startTime: Int = 0 var controlsAppearTime: Double = 0 - var discoveredCastDevices: [CastDevice] = [] //not private due to VPCDS using it. - var selectedCastDevice: CastDevice? //same here - private var castClient: CastClient? - private var playerDestination: PlayerDestination = .local; - private var castAppTransportID: String = ""; - private var remotePlayIsPlaying: Bool = false; - private var remotePlaySeekState: Int = 0; - private let castScanner: CastDeviceScanner = CastDeviceScanner(); + var playerDestination: PlayerDestination = .local; + var discoveredCastDevices: [GCKDevice] = []; + var selectedCastDevice: GCKDevice?; + var jellyfinCastChannel: GCKGenericChannel? + var remotePositionTicks: Int = 0 + private var castDiscoveryManager: GCKDiscoveryManager { + return GCKCastContext.sharedInstance().discoveryManager + } + private var castSessionManager: GCKSessionManager { + return GCKCastContext.sharedInstance().sessionManager + } - var selectedAudioTrack: Int32 = -1 { - didSet { - print(selectedAudioTrack) - } - } - var selectedCaptionTrack: Int32 = -1 { - didSet { - print(selectedCaptionTrack) - } - } + var selectedAudioTrack: Int32 = -1 + var selectedCaptionTrack: Int32 = -1 var playSessionId: String = "" var lastProgressReportTime: Double = 0 - var subtitleTrackArray: [Subtitle] = [] var audioTrackArray: [AudioTrack] = [] var manifest: BaseItemDto = BaseItemDto() var playbackItem = PlaybackItem() + // MARK: IBActions @IBAction func seekSliderStart(_ sender: Any) { @@ -106,7 +102,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe } @IBAction func seekSliderValueChanged(_ sender: Any) { - let videoDuration = Double(mediaPlayer.time.intValue + abs(mediaPlayer.remainingTime.intValue))/1000 + let videoDuration: Double = Double(manifest.runTimeTicks! / Int64(10_000_000)) let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration) let scrubRemaining = videoDuration - secondsScrubbedTo let remainingTime = scrubRemaining @@ -121,21 +117,31 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe } @IBAction func seekSliderEnd(_ sender: Any) { - print("ss end") - let videoPosition = Double(mediaPlayer.time.intValue) - let videoDuration = Double(mediaPlayer.time.intValue + abs(mediaPlayer.remainingTime.intValue)) + let videoPosition = playerDestination == .local ? Double(mediaPlayer.time.intValue / 1000) : Double(remotePositionTicks / Int(10_000_000)) + let videoDuration = Double(manifest.runTimeTicks! / Int64(10_000_000)) // Scrub is value from 0..1 - find position in video and add / or remove. let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration) let offset = secondsScrubbedTo - videoPosition + print(videoPosition) + print(videoDuration) + print(secondsScrubbedTo) + print(offset) + if(playerDestination == .local) { - mediaPlayer.play() if offset > 0 { - mediaPlayer.jumpForward(Int32(offset)/1000) + mediaPlayer.jumpForward(Int32(offset)) } else { - mediaPlayer.jumpBackward(Int32(abs(offset))/1000) + mediaPlayer.jumpBackward(Int32(abs(offset))) } + mediaPlayer.play() sendProgressReport(eventName: "unpause") + } else { + /* + sendJellyfinCommand(command: "Seek", options: [ + "position": secondsScrubbedTo + ]) + */ } } @@ -143,12 +149,8 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe sendStopReport() mediaPlayer.stop() - if(playerDestination == .remote) { - castClient?.stopCurrentApp() - castClient?.disconnect() - castClient = nil - selectedCastDevice = nil - playerDestination = .local + if(castSessionManager.hasConnectedCastSession()) { + castSessionManager.endSessionAndStopCasting(true) } delegate?.exitPlayer(self) @@ -194,7 +196,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) paused = false } else { - sendCastCommand(cmd: "Unpause") + sendJellyfinCommand(command: "Unpause", options: [:]) mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) paused = false } @@ -204,7 +206,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe mainActionButton.setImage(UIImage(systemName: "play"), for: .normal) paused = true } else { - sendCastCommand(cmd: "Pause") + sendJellyfinCommand(command: "Pause", options: [:]) mainActionButton.setImage(UIImage(systemName: "play"), for: .normal) paused = true } @@ -226,7 +228,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe } } - //MARK: Cast start + //MARK: Cast methods @IBAction func castButtonPressed(_ sender: Any) { if(selectedCastDevice == nil) { castDeviceVC = VideoPlayerCastDeviceSelectorView() @@ -242,115 +244,31 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal) } } else { - castClient?.stopCurrentApp() - castClient?.disconnect() + castSessionManager.endSessionAndStopCasting(true) selectedCastDevice = nil; - castClient = nil; self.castButton.isEnabled = true self.castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal) - - //disconnect cast device. - + playerDestination = .local + startLocalPlaybackEngine() } } func castPopoverDismissed() { castDeviceVC?.dismiss(animated: true, completion: nil) - self.mediaPlayer.play() + if(playerDestination == .local) { + self.mediaPlayer.play() + } self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) } func castDeviceChanged() { if(selectedCastDevice != nil) { - castClient = CastClient(device: selectedCastDevice!) - castClient!.delegate = self - castClient!.connect() + playerDestination = .remote + castSessionManager.add(self) + castSessionManager.startSession(with: selectedCastDevice!) } } - func sendCastCommand(cmd: String) { - let payload: [String: Any] = [ - "options": [], - "command": cmd, - "userId": SessionManager.current.user.user_id!, - "deviceId": SessionManager.current.deviceID, - "accessToken": SessionManager.current.accessToken, - "serverAddress": ServerEnvironment.current.server.baseURI!, - "serverId": ServerEnvironment.current.server.server_id!, - "serverVersion": "10.8.0", - "receiverName": self.selectedCastDevice!.name - ] - let req = CastRequest(id: castClient!.nextRequestId(), namespace: "urn:x-cast:com.connectsdk", destinationId: castAppTransportID, payload: payload) - castClient!.send(req, response: nil) - } - - func castClient(_ client: CastClient, connectionTo device: CastDevice, didFailWith error: Error?) { - dump(error) - } - - func castClient(_ client: CastClient, willConnectTo device: CastDevice) { - print("Connecting") - mediaPlayer.pause() - castScanner.stopScanning() - self.castButton.setImage(UIImage(named: "CastConnecting1"), for: .normal) - } - - func castClient(_ client: CastClient, didConnectTo device: CastDevice) { - print("Connected") - self.castButton.setImage(UIImage(named: "CastConnected"), for: .normal) - - //Launch player - client.launch(appId: "F007D354") { result in - switch result { - case .success(let app): - // here you would probably call client.load() to load some media - let payload: [String: Any] = [ - "options": [ - "items": [[ - "Id": self.manifest.id!, - "ServerId": ServerEnvironment.current.server.server_id!, - "Name": self.manifest.name!, - "Type": self.manifest.type!, - "MediaType": self.manifest.mediaType!, - "IsFolder": self.manifest.isFolder! - ]] - ], - "command": "PlayNow", - "userId": SessionManager.current.user.user_id!, - "deviceId": SessionManager.current.deviceID, - "accessToken": SessionManager.current.accessToken, - "serverAddress": ServerEnvironment.current.server.baseURI!, - "serverId": ServerEnvironment.current.server.server_id!, - "serverVersion": "10.8.0", - "receiverName": self.selectedCastDevice!.name, - "subtitleBurnIn": false - ] - self.castAppTransportID = app.transportId - let req = CastRequest(id: client.nextRequestId(), namespace: "urn:x-cast:com.connectsdk", destinationId: app.transportId, payload: payload) - client.send(req, response: self.castResponseHandler) - case .failure(let error): - print(error) - } - } - - //Hide VLC player - videoContentView.isHidden = true; - playerDestination = .remote; - - } - - func castClient(_ client: CastClient, didDisconnectFrom device: CastDevice) { - print("Disconnected") - castScanner.startScanning() - playerDestination = .local; - videoContentView.isHidden = false; - self.castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal) - } - - func castResponseHandler(result: Result) { - dump(result) - } - //MARK: Cast End func settingsPopoverDismissed() { optionsVC?.dismiss(animated: true, completion: nil) @@ -382,7 +300,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe self.mediaPlayer.pause() self.sendProgressReport(eventName: "pause") } else { - self.sendCastCommand(cmd: "Pause") + self.sendJellyfinCommand(command: "Pause", options: [:]) } self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal) return .success @@ -394,7 +312,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe self.mediaPlayer.play() self.sendProgressReport(eventName: "unpause") } else { - self.sendCastCommand(cmd: "Unpause") + self.sendJellyfinCommand(command: "Unpause", options: [:]) } self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) return .success @@ -475,20 +393,37 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe } func mediaHasStartedPlaying() { - NotificationCenter.default.addObserver(forName: CastDeviceScanner.deviceListDidChange, object: castScanner, queue: nil) { _ in - self.discoveredCastDevices = self.castScanner.devices - if !self.castScanner.devices.isEmpty { - self.castButton.isEnabled = true - self.castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal) - } else { - self.castButton.isEnabled = false - self.castButton.setImage(nil, for: .normal) + castButton.isHidden = true; + let discoveryCriteria = GCKDiscoveryCriteria(applicationID: "F007D354") + let gckCastOptions = GCKCastOptions(discoveryCriteria: discoveryCriteria) + GCKCastContext.setSharedInstanceWith(gckCastOptions) + castDiscoveryManager.passiveScan = true + castDiscoveryManager.add(self) + castDiscoveryManager.startDiscovery() + } + + func didUpdateDeviceList() { + let totalDevices = castDiscoveryManager.deviceCount; + discoveredCastDevices = [] + if(totalDevices > 0) { + for i in 0...totalDevices-1 { + let device = castDiscoveryManager.device(at: i) + discoveredCastDevices.append(device) } } - castScanner.startScanning() + if !discoveredCastDevices.isEmpty { + castButton.isHidden = false + castButton.isEnabled = true + castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal) + } else { + castButton.isHidden = true + castButton.isEnabled = false + castButton.setImage(nil, for: .normal) + } } - + + //MARK: viewDidAppear override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) overrideUserInterfaceStyle = .dark @@ -606,51 +541,53 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe selectedAudioTrack = audioTrackArray[0].id } } - - print("gotToEnd") - + self.sendPlayReport() playbackItem = item } - - mediaPlayer.media = VLCMedia(url: playbackItem.videoUrl) - mediaPlayer.play() - - // 1 second = 10,000,000 ticks - - let rawStartTicks = manifest.userData?.playbackPositionTicks ?? 0 - - if rawStartTicks != 0 { - let startSeconds = rawStartTicks / 10_000_000 - mediaPlayer.jumpForward(Int32(startSeconds)) - } - - // Pause and load captions into memory. - mediaPlayer.pause() - - var shouldHaveSubtitleTracks = 0 - subtitleTrackArray.forEach { sub in - if sub.id != -1 && sub.delivery == .external && sub.codec != "subrip" { - shouldHaveSubtitleTracks = shouldHaveSubtitleTracks + 1 - mediaPlayer.addPlaybackSlave(sub.url!, type: .subtitle, enforce: false) - } - } - - // Wait for captions to load - delegate?.showLoadingView(self) - - while mediaPlayer.numberOfSubtitlesTracks != shouldHaveSubtitleTracks {} - - // Select default track & resume playback - mediaPlayer.currentVideoSubTitleIndex = selectedCaptionTrack - mediaPlayer.pause() - mediaPlayer.play() - self.mediaHasStartedPlaying() + startLocalPlaybackEngine() }) .store(in: &cancellables) } } + func startLocalPlaybackEngine() { + mediaPlayer.media = VLCMedia(url: playbackItem.videoUrl) + mediaPlayer.play() + + // 1 second = 10,000,000 ticks + + let rawStartTicks = manifest.userData?.playbackPositionTicks ?? 0 + + if rawStartTicks != 0 { + let startSeconds = rawStartTicks / 10_000_000 + mediaPlayer.jumpForward(Int32(startSeconds)) + } + + // Pause and load captions into memory. + mediaPlayer.pause() + + var shouldHaveSubtitleTracks = 0 + subtitleTrackArray.forEach { sub in + if sub.id != -1 && sub.delivery == .external && sub.codec != "subrip" { + shouldHaveSubtitleTracks = shouldHaveSubtitleTracks + 1 + mediaPlayer.addPlaybackSlave(sub.url!, type: .subtitle, enforce: false) + } + } + + // Wait for captions to load + delegate?.showLoadingView(self) + + while mediaPlayer.numberOfSubtitlesTracks != shouldHaveSubtitleTracks {} + + // Select default track & resume playback + mediaPlayer.currentVideoSubTitleIndex = selectedCaptionTrack + mediaPlayer.pause() + mediaPlayer.play() + self.mediaHasStartedPlaying() + delegate?.hideLoadingView(self) + } + // MARK: VideoPlayerSettings Delegate func subtitleTrackChanged(newTrackID: Int32) { selectedCaptionTrack = newTrackID @@ -661,8 +598,144 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe selectedAudioTrack = newTrackID mediaPlayer.currentAudioTrackIndex = newTrackID } +} - // MARK: VLCMediaPlayer Delegates +//MARK: - GCKGenericChannelDelegate +extension PlayerViewController: GCKGenericChannelDelegate { + func cast(_ channel: GCKGenericChannel, didReceiveTextMessage message: String, withNamespace protocolNamespace: String) { + print("~~~~~~~~~~~~~~~~~") + print(message) + print("~~~~~~~~~~~~~~~~~") + print("") + + if let data = message.data(using: .utf8) { + if let json = try? JSON(data: data) { + let messageType = json["type"].string ?? "" + if(messageType == "playbackprogress") { + print("Playback progress received from Cast!"); + + self.remotePositionTicks = json["data"]["PlayState"]["PositionTicks"].int ?? 0; + print(remotePositionTicks) + + let remainingTime = (manifest.runTimeTicks! - Int64(remotePositionTicks))/10_000_000 + print(remainingTime) + let hours = remainingTime / 3600 + let minutes = (remainingTime % 3600) / 60 + let seconds = (remainingTime % 3600) % 60 + var timeTextStr = "" + if hours != 0 { + timeTextStr = "\(Int(hours)):\(String(Int((minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int((seconds))).leftPad(toWidth: 2, withString: "0"))" + } else { + timeTextStr = "\(String(Int((minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int((seconds))).leftPad(toWidth: 2, withString: "0"))" + } + timeText.text = timeTextStr + + let playbackProgress = Int64(remotePositionTicks) / manifest.runTimeTicks! + seekSlider.setValue(Float(playbackProgress), animated: true) + } + } + } + } + + func sendJellyfinCommand(command: String, options: [String: Any]) { + let payload: [String: Any] = [ + "options": options, + "command": command, + "userId": SessionManager.current.user.user_id!, + "deviceId": SessionManager.current.deviceID, + "accessToken": SessionManager.current.accessToken, + "serverAddress": ServerEnvironment.current.server.baseURI!, + "serverId": ServerEnvironment.current.server.server_id!, + "serverVersion": "10.8.0", + "receiverName": castSessionManager.currentCastSession!.device.friendlyName!, + "subtitleBurnIn": false + ] + let jsonData = JSON(payload) + + jellyfinCastChannel?.sendTextMessage(jsonData.rawString()!, error: nil) + } +} + +//MARK: - GCKSessionManagerListener +extension PlayerViewController: GCKSessionManagerListener { + func sessionDidStart(manager: GCKSessionManager, didStart session: GCKCastSession) { + self.sendStopReport() + mediaPlayer.stop() + + playerDestination = .remote + videoContentView.isHidden = true; + videoControlsView.isHidden = false; + castButton.setImage(UIImage(named: "CastConnected"), for: .normal) + manager.currentCastSession?.start() + + jellyfinCastChannel!.delegate = self + session.add(jellyfinCastChannel!) + + if let client = session.remoteMediaClient { + client.add(self) + } + + let playNowOptions: [String: Any] = [ + "items": [[ + "Id": self.manifest.id!, + "ServerId": ServerEnvironment.current.server.server_id!, + "Name": self.manifest.name!, + "Type": self.manifest.type!, + "MediaType": self.manifest.mediaType!, + "IsFolder": self.manifest.isFolder! + ]] + ] + sendJellyfinCommand(command: "PlayNow", options: playNowOptions) + sendJellyfinCommand(command: "Seek", options: [ + "position": (manifest.runTimeTicks! - (Int64(mediaPlayer.remainingTime.intValue) * 10000)) / 10_000_000 + ]) + } + + func sessionManager(_ sessionManager: GCKSessionManager, didStart session: GCKCastSession) { + print("starting session") + self.jellyfinCastChannel = GCKGenericChannel(namespace: "urn:x-cast:com.connectsdk") + self.sessionDidStart(manager: sessionManager, didStart: session) + } + + func sessionManager(_ sessionManager: GCKSessionManager, didResumeCastSession session: GCKCastSession) { + self.jellyfinCastChannel = GCKGenericChannel(namespace: "urn:x-cast:com.connectsdk") + print("resuming session") + self.sessionDidStart(manager: sessionManager, didStart: session) + } + + func sessionManager(_ sessionManager: GCKSessionManager, didFailToStart session: GCKCastSession, withError error: Error) { + dump(error) + } + + + func sessionManager(_ sessionManager: GCKSessionManager, didEnd session: GCKCastSession, withError error: Error?) { + print("didEnd") + playerDestination = .local; + videoContentView.isHidden = false; + castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal) + startLocalPlaybackEngine() + } + + func sessionManager(_ sessionManager: GCKSessionManager, didSuspend session: GCKCastSession, with reason: GCKConnectionSuspendReason) { + print("didSuspend") + playerDestination = .local; + videoContentView.isHidden = false; + castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal) + startLocalPlaybackEngine() + } + + func sessionManager(_ sessionManager: GCKSessionManager, castSession session: GCKCastSession, didReceiveDeviceStatus statusText: String?) { + print("96") + dump(statusText) + } + + func sessionManager(_ sessionManager: GCKSessionManager, castSession session: GCKCastSession, didReceiveDeviceVolume volume: Float, muted: Bool) { + print("100") + } +} + +//MARK: - VLCMediaPlayer Delegates +extension PlayerViewController: VLCMediaPlayerDelegate { func mediaPlayerStateChanged(_ aNotification: Notification!) { let currentState: VLCMediaPlayerState = mediaPlayer.state switch currentState { @@ -690,7 +763,8 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe mediaPlayer.pause() usleep(10000) mediaPlayer.play() - + delegate?.hideLoadingView(self) + paused = false case .error : print("Video has error)") sendStopReport() @@ -700,26 +774,16 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe break } } - + func mediaPlayerTimeChanged(_ aNotification: Notification!) { let time = mediaPlayer.position - if time != lastTime { + if abs(time-lastTime) > 0.00005 { paused = false mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) seekSlider.setValue(mediaPlayer.position, animated: true) delegate?.hideLoadingView(self) - let remainingTime = abs(mediaPlayer.remainingTime.intValue)/1000 - let hours = remainingTime / 3600 - let minutes = (remainingTime % 3600) / 60 - let seconds = (remainingTime % 3600) % 60 - var timeTextStr = "" - if hours != 0 { - timeTextStr = "\(Int(hours)):\(String(Int((minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int((seconds))).leftPad(toWidth: 2, withString: "0"))" - } else { - timeTextStr = "\(String(Int((minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int((seconds))).leftPad(toWidth: 2, withString: "0"))" - } - timeText.text = timeTextStr + timeText.text = String(mediaPlayer.remainingTime.stringValue.dropFirst()) if CACurrentMediaTime() - controlsAppearTime > 5 { UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseOut, animations: { @@ -730,61 +794,28 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe }) controlsAppearTime = 999_999_999_999_999 } - } else { - paused = true + lastTime = time } - lastTime = time if CACurrentMediaTime() - lastProgressReportTime > 5 { sendProgressReport(eventName: "timeupdate") lastProgressReportTime = CACurrentMediaTime() } } - - // MARK: Jellyfin Playstate updates - func sendProgressReport(eventName: String) { - if (eventName == "timeupdate" && mediaPlayer.state == .playing) || eventName != "timeupdate" { - let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: (mediaPlayer.state == .paused), isMuted: false, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0") - - PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: progressInfo) - .sink(receiveCompletion: { result in - print(result) - }, receiveValue: { _ in - print("Playback progress report sent!") - }) - .store(in: &cancellables) - } - } - - func sendStopReport() { - let stopInfo = PlaybackStopInfo(item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), liveStreamId: nil, playSessionId: playSessionId, failed: nil, nextMediaType: nil, playlistItemId: "playlistItem0", nowPlayingQueue: []) - - PlaystateAPI.reportPlaybackStopped(playbackStopInfo: stopInfo) - .sink(receiveCompletion: { result in - print(result) - }, receiveValue: { _ in - print("Playback stop report sent!") - }) - .store(in: &cancellables) - } - - func sendPlayReport() { - startTime = Int(Date().timeIntervalSince1970) * 10000000 - - print("sending play report!") - - let startInfo = PlaybackStartInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: false, isMuted: false, positionTicks: manifest.userData?.playbackPositionTicks, playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0") - - PlaystateAPI.reportPlaybackStart(playbackStartInfo: startInfo) - .sink(receiveCompletion: { result in - print(result) - }, receiveValue: { _ in - print("Playback start report sent!") - }) - .store(in: &cancellables) - } } + + + + + + + + + + + +//MARK: End VideoPlayerVC struct VLCPlayerWithControls: UIViewControllerRepresentable { var item: BaseItemDto @Environment(\.presentationMode) var presentationMode @@ -830,3 +861,48 @@ struct VLCPlayerWithControls: UIViewControllerRepresentable { func updateUIViewController(_ uiViewController: VLCPlayerWithControls.UIViewControllerType, context: UIViewControllerRepresentableContext) { } } + +//MARK: - Play State Update Methods +extension PlayerViewController { + func sendProgressReport(eventName: String) { + if (eventName == "timeupdate" && mediaPlayer.state == .playing) || eventName != "timeupdate" { + let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: (mediaPlayer.state == .paused), isMuted: false, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0") + + PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: progressInfo) + .sink(receiveCompletion: { result in + print(result) + }, receiveValue: { _ in + print("Playback progress report sent!") + }) + .store(in: &cancellables) + } + } + + func sendStopReport() { + let stopInfo = PlaybackStopInfo(item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), liveStreamId: nil, playSessionId: playSessionId, failed: nil, nextMediaType: nil, playlistItemId: "playlistItem0", nowPlayingQueue: []) + + PlaystateAPI.reportPlaybackStopped(playbackStopInfo: stopInfo) + .sink(receiveCompletion: { result in + print(result) + }, receiveValue: { _ in + print("Playback stop report sent!") + }) + .store(in: &cancellables) + } + + func sendPlayReport() { + startTime = Int(Date().timeIntervalSince1970) * 10000000 + + print("sending play report!") + + let startInfo = PlaybackStartInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: false, isMuted: false, positionTicks: manifest.userData?.playbackPositionTicks, playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0") + + PlaystateAPI.reportPlaybackStart(playbackStartInfo: startInfo) + .sink(receiveCompletion: { result in + print(result) + }, receiveValue: { _ in + print("Playback start report sent!") + }) + .store(in: &cancellables) + } +} diff --git a/JellyfinPlayer/VideoPlayerCastDeviceSelector.swift b/JellyfinPlayer/VideoPlayerCastDeviceSelector.swift index 1ebccda6..02a253b5 100644 --- a/JellyfinPlayer/VideoPlayerCastDeviceSelector.swift +++ b/JellyfinPlayer/VideoPlayerCastDeviceSelector.swift @@ -42,25 +42,34 @@ struct VideoPlayerCastDeviceSelector: View { var body: some View { NavigationView { - List(delegate.discoveredCastDevices, id: \.id) { device in - HStack() { - Text("\(device.name)") - .font(.subheadline) - .fontWeight(.medium) - Spacer() - Button { - delegate.selectedCastDevice = device - self.delegate?.castDeviceChanged() - self.delegate?.castPopoverDismissed() - } label: { + Group { + if(!delegate.discoveredCastDevices.isEmpty) { + List(delegate.discoveredCastDevices, id: \.deviceID) { device in HStack() { - Text("Connect") - .font(.caption) + Text(device.friendlyName!) + .font(.subheadline) .fontWeight(.medium) - Image(systemName: "bonjour") - .font(.caption) + Spacer() + Button { + delegate.selectedCastDevice = device + delegate?.castDeviceChanged() + delegate?.castPopoverDismissed() + } label: { + HStack() { + Text("Connect") + .font(.caption) + .fontWeight(.medium) + Image(systemName: "bonjour") + .font(.caption) + } + } } } + } else { + Text("No Cast devices found..") + .foregroundColor(.secondary) + .font(.subheadline) + .fontWeight(.medium) } } .navigationBarTitleDisplayMode(.inline) @@ -69,7 +78,7 @@ struct VideoPlayerCastDeviceSelector: View { ToolbarItemGroup(placement: .navigationBarLeading) { if UIDevice.current.userInterfaceIdiom == .phone { Button { - self.delegate?.castPopoverDismissed() + delegate?.castPopoverDismissed() } label: { HStack { Image(systemName: "chevron.left") diff --git a/Podfile b/Podfile new file mode 100644 index 00000000..603c1693 --- /dev/null +++ b/Podfile @@ -0,0 +1,11 @@ +target 'JellyfinPlayer iOS' do + platform :ios, '14.0' + use_frameworks! + pod 'google-cast-sdk' + pod 'MobileVLCKit' +end +target 'JellyfinPlayer tvOS' do + platform :tvos, '14.0' + use_frameworks! + pod 'TVVLCKit' +end \ No newline at end of file