Merge branch 'main' into PangMo5/refactoring-2

This commit is contained in:
aiden vigue 2021-06-18 19:00:40 -04:00 committed by GitHub
commit 60414101ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 3307 additions and 12 deletions

View File

@ -40,6 +40,35 @@
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 */; };
@ -224,6 +253,50 @@
535870AC2669D8DD00D05A09 /* Typings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Typings.swift; sourceTree = "<group>"; };
535BAE9E2649E569005FA86D /* ItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemView.swift; sourceTree = "<group>"; };
535BAEA4264A151C005FA86D /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = "<group>"; };
5362E4A7267D4067000E2F71 /* GoogleCast.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GoogleCast.framework; path = "../../Downloads/GoogleCastSDK-ios-4.6.0_dynamic/GoogleCast.framework"; sourceTree = "<group>"; };
5362E4AA267D40AD000E2F71 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; };
5362E4AC267D40B1000E2F71 /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = System/Library/Frameworks/Accelerate.framework; sourceTree = SDKROOT; };
5362E4AE267D40B5000E2F71 /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; };
5362E4B0267D40B9000E2F71 /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; };
5362E4B2267D40BE000E2F71 /* CoreBluetooth.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreBluetooth.framework; path = System/Library/Frameworks/CoreBluetooth.framework; sourceTree = SDKROOT; };
5362E4B4267D40C5000E2F71 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; };
5362E4B6267D40CA000E2F71 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; };
5362E4B8267D40CE000E2F71 /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; };
5362E4BA267D40D2000E2F71 /* CoreText.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreText.framework; path = System/Library/Frameworks/CoreText.framework; sourceTree = SDKROOT; };
5362E4BC267D40D8000E2F71 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
5362E4BE267D40E4000E2F71 /* MediaAccessibility.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaAccessibility.framework; path = System/Library/Frameworks/MediaAccessibility.framework; sourceTree = SDKROOT; };
5362E4C0267D40E8000E2F71 /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = System/Library/Frameworks/MediaPlayer.framework; sourceTree = SDKROOT; };
5362E4C2267D40EC000E2F71 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
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 = "<group>"; };
5362E4DF267D4707000E2F71 /* CastDeviceScanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CastDeviceScanner.swift; sourceTree = "<group>"; };
5362E4E1267D4707000E2F71 /* ReceiverControlChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiverControlChannel.swift; sourceTree = "<group>"; };
5362E4E2267D4707000E2F71 /* Channelable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Channelable.swift; sourceTree = "<group>"; };
5362E4E3267D4707000E2F71 /* CastChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CastChannel.swift; sourceTree = "<group>"; };
5362E4E4267D4707000E2F71 /* DeviceAuthChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceAuthChannel.swift; sourceTree = "<group>"; };
5362E4E5267D4707000E2F71 /* MediaControlChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaControlChannel.swift; sourceTree = "<group>"; };
5362E4E6267D4707000E2F71 /* HeartbeatChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeartbeatChannel.swift; sourceTree = "<group>"; };
5362E4E7267D4707000E2F71 /* DeviceSetupChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceSetupChannel.swift; sourceTree = "<group>"; };
5362E4E8267D4707000E2F71 /* DeviceConnectionChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceConnectionChannel.swift; sourceTree = "<group>"; };
5362E4E9267D4707000E2F71 /* RequestSink.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestSink.swift; sourceTree = "<group>"; };
5362E4EA267D4707000E2F71 /* MultizoneControlChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultizoneControlChannel.swift; sourceTree = "<group>"; };
5362E4EB267D4707000E2F71 /* DeviceDiscoveryChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceDiscoveryChannel.swift; sourceTree = "<group>"; };
5362E4ED267D4707000E2F71 /* CastDevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CastDevice.swift; sourceTree = "<group>"; };
5362E4EE267D4707000E2F71 /* CastStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CastStatus.swift; sourceTree = "<group>"; };
5362E4EF267D4707000E2F71 /* CastApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CastApp.swift; sourceTree = "<group>"; };
5362E4F0267D4707000E2F71 /* CastMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CastMessage.swift; sourceTree = "<group>"; };
5362E4F1267D4707000E2F71 /* CastMediaStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CastMediaStatus.swift; sourceTree = "<group>"; };
5362E4F2267D4707000E2F71 /* CastMedia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CastMedia.swift; sourceTree = "<group>"; };
5362E4F3267D4707000E2F71 /* CastMultizoneDevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CastMultizoneDevice.swift; sourceTree = "<group>"; };
5362E4F4267D4707000E2F71 /* CastMultizoneStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CastMultizoneStatus.swift; sourceTree = "<group>"; };
5362E4F5267D4707000E2F71 /* AppAvailability.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppAvailability.swift; sourceTree = "<group>"; };
5362E4F7267D4707000E2F71 /* ChromeCastCore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ChromeCastCore.h; sourceTree = "<group>"; };
5362E4FA267D4707000E2F71 /* CASTV2Protocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CASTV2Protocol.swift; sourceTree = "<group>"; };
5362E4FD267D4707000E2F71 /* cast_channel.proto */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.protobuf; path = cast_channel.proto; sourceTree = "<group>"; };
5362E4FE267D4707000E2F71 /* cast_channel.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = cast_channel.pb.swift; sourceTree = "<group>"; };
5362E4FF267D4707000E2F71 /* CastV2PlatformReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CastV2PlatformReader.swift; sourceTree = "<group>"; };
5364F454266CA0DC0026ECBA /* APIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIExtensions.swift; sourceTree = "<group>"; };
536D3D73267BA8170004248C /* BackgroundManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundManager.swift; sourceTree = "<group>"; };
536D3D75267BA9BB0004248C /* MainTabViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabViewModel.swift; sourceTree = "<group>"; };
@ -318,11 +391,14 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5362E4D9267D4695000E2F71 /* SwiftyJSON in Frameworks */,
5338F757263B7E2E0014BF09 /* KeychainSwift 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;
@ -418,6 +494,96 @@
path = Typings;
sourceTree = "<group>";
};
5362E4DC267D4707000E2F71 /* OpenCastSwift */ = {
isa = PBXGroup;
children = (
5362E4DD267D4707000E2F71 /* Networking */,
5362E4EC267D4707000E2F71 /* Models */,
5362E4F6267D4707000E2F71 /* Supporting Files */,
5362E4F9267D4707000E2F71 /* Definitions */,
5362E4FB267D4707000E2F71 /* Helpers */,
);
path = OpenCastSwift;
sourceTree = "<group>";
};
5362E4DD267D4707000E2F71 /* Networking */ = {
isa = PBXGroup;
children = (
5362E4DE267D4707000E2F71 /* CastClient.swift */,
5362E4DF267D4707000E2F71 /* CastDeviceScanner.swift */,
5362E4E0267D4707000E2F71 /* Channels */,
);
path = Networking;
sourceTree = "<group>";
};
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 = "<group>";
};
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 = "<group>";
};
5362E4F6267D4707000E2F71 /* Supporting Files */ = {
isa = PBXGroup;
children = (
5362E4F7267D4707000E2F71 /* ChromeCastCore.h */,
);
path = "Supporting Files";
sourceTree = "<group>";
};
5362E4F9267D4707000E2F71 /* Definitions */ = {
isa = PBXGroup;
children = (
5362E4FA267D4707000E2F71 /* CASTV2Protocol.swift */,
);
path = Definitions;
sourceTree = "<group>";
};
5362E4FB267D4707000E2F71 /* Helpers */ = {
isa = PBXGroup;
children = (
5362E4FC267D4707000E2F71 /* Proto */,
5362E4FF267D4707000E2F71 /* CastV2PlatformReader.swift */,
);
path = Helpers;
sourceTree = "<group>";
};
5362E4FC267D4707000E2F71 /* Proto */ = {
isa = PBXGroup;
children = (
5362E4FD267D4707000E2F71 /* cast_channel.proto */,
5362E4FE267D4707000E2F71 /* cast_channel.pb.swift */,
);
path = Proto;
sourceTree = "<group>";
};
536D3D77267BB9650004248C /* Components */ = {
isa = PBXGroup;
children = (
@ -454,6 +620,7 @@
5377CBF3263B596A003A4E83 /* JellyfinPlayer */ = {
isa = PBXGroup;
children = (
5362E4DC267D4707000E2F71 /* OpenCastSwift */,
53AD124C2670278D0094A276 /* JellyfinPlayer.entitlements */,
5377CBF8263B596B003A4E83 /* Assets.xcassets */,
5338F74D263B61370014BF09 /* ConnectToServerView.swift */,
@ -498,6 +665,23 @@
53D5E3DB264B47EE00BADDC8 /* Frameworks */ = {
isa = PBXGroup;
children = (
5362E4C8267D40F7000E2F71 /* UIKit.framework */,
5362E4C6267D40F4000E2F71 /* SystemConfiguration.framework */,
5362E4C4267D40F0000E2F71 /* Security.framework */,
5362E4C2267D40EC000E2F71 /* QuartzCore.framework */,
5362E4C0267D40E8000E2F71 /* MediaPlayer.framework */,
5362E4BE267D40E4000E2F71 /* MediaAccessibility.framework */,
5362E4BC267D40D8000E2F71 /* Foundation.framework */,
5362E4BA267D40D2000E2F71 /* CoreText.framework */,
5362E4B8267D40CE000E2F71 /* CoreMedia.framework */,
5362E4B6267D40CA000E2F71 /* CoreGraphics.framework */,
5362E4B4267D40C5000E2F71 /* CoreData.framework */,
5362E4B2267D40BE000E2F71 /* CoreBluetooth.framework */,
5362E4B0267D40B9000E2F71 /* CFNetwork.framework */,
5362E4AE267D40B5000E2F71 /* AudioToolbox.framework */,
5362E4AC267D40B1000E2F71 /* Accelerate.framework */,
5362E4AA267D40AD000E2F71 /* AVFoundation.framework */,
5362E4A7267D4067000E2F71 /* GoogleCast.framework */,
53ABFDDB267972BF00886593 /* TVServices.framework */,
625CB57D2678E81E00530A6E /* TVVLCKit.xcframework */,
53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */,
@ -614,6 +798,9 @@
621C637F26672A30004216EA /* NukeUI */,
53A431BC266B0FF20016769F /* JellyfinAPI */,
625CB5792678C4A400530A6E /* ActivityIndicator */,
5362E4D2267D461F000E2F71 /* SwiftProtobuf */,
5362E4D5267D4671000E2F71 /* Result */,
5362E4D8267D4695000E2F71 /* SwiftyJSON */,
);
productName = JellyfinPlayer;
productReference = 5377CBF1263B596A003A4E83 /* JellyfinPlayer iOS.app */;
@ -681,6 +868,9 @@
53A431BB266B0FF20016769F /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */,
625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */,
536D3D82267BEA550004248C /* XCRemoteSwiftPackageReference "ParallaxView" */,
5362E4D1267D461F000E2F71 /* XCRemoteSwiftPackageReference "swift-protobuf" */,
5362E4D4267D4671000E2F71 /* XCRemoteSwiftPackageReference "Result" */,
5362E4D7267D4695000E2F71 /* XCRemoteSwiftPackageReference "SwiftyJSON" */,
);
productRefGroup = 5377CBF2263B596A003A4E83 /* Products */;
projectDirPath = "";
@ -782,53 +972,79 @@
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 */,
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 */,
@ -1225,6 +1441,30 @@
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";
@ -1285,6 +1525,21 @@
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" */;

View File

@ -73,6 +73,24 @@
"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",
"state": {
"branch": null,
"revision": "1f62db409f2c9b0223a3f68567b4a01333aae778",
"version": "1.17.0"
}
},
{
"package": "Introspect",
"repositoryURL": "https://github.com/siteline/SwiftUI-Introspect",
@ -81,6 +99,15 @@
"revision": "2e09be8af614401bc9f87d40093ec19ce56ccaf2",
"version": "0.1.3"
}
},
{
"package": "SwiftyJSON",
"repositoryURL": "https://github.com/SwiftyJSON/SwiftyJSON",
"state": {
"branch": "master",
"revision": "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07",
"version": null
}
}
]
},

View File

@ -142,13 +142,23 @@ extension View {
struct JellyfinPlayerApp: App {
let persistenceController = PersistenceController.shared
func test_cast() {
let scanner = CastDeviceScanner()
NotificationCenter.default.addObserver(forName: CastDeviceScanner.deviceListDidChange, object: scanner, queue: nil) { _ in
dump(scanner.devices)
}
scanner.startScanning()
}
var body: some Scene {
WindowGroup {
SplashView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.withHostingWindow { window in
window?.rootViewController = PreferenceUIHostingController(wrappedView: SplashView().environment(\.managedObjectContext, persistenceController.container.viewContext))
}
}.onAppear(perform: test_cast)
}
}
}

View File

@ -0,0 +1,96 @@
//
// 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"
}

View File

@ -0,0 +1,83 @@
//
// 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<UInt32>.size
guard buffer.count - readPosition >= headerSize else { return nil }
let header = buffer.withUnsafeBytes({ (pointer: UnsafePointer<Int8>) -> 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<Int8>) -> 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
}
}

View File

@ -0,0 +1,618 @@
// 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<D: SwiftProtobuf.Decoder>(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<V: SwiftProtobuf.Visitor>(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<D: SwiftProtobuf.Decoder>(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<V: SwiftProtobuf.Visitor>(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<D: SwiftProtobuf.Decoder>(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<V: SwiftProtobuf.Visitor>(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<D: SwiftProtobuf.Decoder>(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<V: SwiftProtobuf.Visitor>(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<D: SwiftProtobuf.Decoder>(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<V: SwiftProtobuf.Visitor>(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
}
}

View File

@ -0,0 +1,80 @@
// 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;
}

View File

@ -0,0 +1,24 @@
//
// 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" }
}
}
}

View File

@ -0,0 +1,62 @@
//
// 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)"
}
}

View File

@ -0,0 +1,66 @@
//
// 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))"
}
}

View File

@ -0,0 +1,92 @@
//
// 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
]
]
]
}
}
}

View File

@ -0,0 +1,60 @@
//
// 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()
}
}

View File

@ -0,0 +1,36 @@
//
// 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()
}
}

View File

@ -0,0 +1,42 @@
//
// 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)
}
}

View File

@ -0,0 +1,29 @@
//
// 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)
}
}

View File

@ -0,0 +1,44 @@
//
// 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)
}
}
}

View File

@ -0,0 +1,628 @@
//
// 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<JSON, CastError>) -> 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<CFReadStream>?
var writeStream: Unmanaged<CFWriteStream>?
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<UInt32>.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 {
print("No channel attached for namespace \(message.namespace)")
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<JSON, CastError>) {
DispatchQueue.main.async {
if let handler = self.responseHandlers.removeValue(forKey: requestId) {
handler(result)
}
}
}
// MARK: - Public messages
public func getAppAvailability(apps: [CastApp], completion: @escaping (Result<AppAvailability, CastError>) -> Void) {
guard outputStream != nil else { return }
receiverControlChannel.getAppAvailability(apps: apps, completion: completion)
}
public func join(app: CastApp? = nil, completion: @escaping (Result<CastApp, CastError>) -> 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<CastApp, CastError>) -> 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<CastMediaStatus, CastError>) -> Void) {
guard outputStream != nil else { return }
mediaControlChannel.load(media: media, with: app, completion: completion)
}
public func requestMediaStatus(for app: CastApp, completion: ((Result<CastMediaStatus, CastError>) -> 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
}
}

View File

@ -0,0 +1,219 @@
//
// 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<sockaddr>) -> 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)
}

View File

@ -0,0 +1,31 @@
//
// 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)
}
}

View File

@ -0,0 +1,39 @@
//
// 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
}
}

View File

@ -0,0 +1,30 @@
//
// 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)
}
}

View File

@ -0,0 +1,47 @@
//
// 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)
}
}

View File

@ -0,0 +1,31 @@
//
// 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)
}
}
}
}

View File

@ -0,0 +1,97 @@
//
// 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)
}
}
}
}

View File

@ -0,0 +1,97 @@
//
// 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)
}

View File

@ -0,0 +1,132 @@
//
// 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<CastMediaStatus, CastError>) -> 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<CastMediaStatus, CastError>) -> 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)
}

View File

@ -0,0 +1,115 @@
//
// 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<CastStatus, CastError>) -> 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)
}

View File

@ -0,0 +1,157 @@
//
// 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<AppAvailability, CastError>) -> 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<CastStatus, CastError>) -> 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<CastApp, CastError>) -> 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)
}

View File

@ -0,0 +1,38 @@
//
// 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)
}
}

View File

@ -0,0 +1,12 @@
//
// 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[];

View File

@ -258,8 +258,6 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
override func remoteControlReceived(with event: UIEvent?) {
dump(event)
}
// MARK: viewDidLoad
override func viewDidLoad() {
@ -268,14 +266,14 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
} else {
titleLabel.text = "S\(String(manifest.parentIndexNumber ?? 0)):E\(String(manifest.indexNumber ?? 0))\(manifest.name ?? "")"
}
super.viewDidLoad()
if(!UIDevice.current.orientation.isLandscape) {
if !UIDevice.current.orientation.isLandscape {
let value = UIInterfaceOrientation.landscapeLeft.rawValue
UIDevice.current.setValue(value, forKey: "orientation")
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
overrideUserInterfaceStyle = .dark
@ -393,9 +391,9 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
selectedAudioTrack = audioTrackArray[0].id
}
}
print("gotToEnd")
self.sendPlayReport()
playbackItem = item
}
@ -414,8 +412,8 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
// Pause and load captions into memory.
mediaPlayer.pause()
var shouldHaveSubtitleTracks = 0;
var shouldHaveSubtitleTracks = 0
subtitleTrackArray.forEach { sub in
if sub.id != -1 && sub.delivery == .external && sub.codec != "subrip" {
shouldHaveSubtitleTracks = shouldHaveSubtitleTracks + 1
@ -556,9 +554,9 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
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)