Merge remote-tracking branch 'origin/main' into main

This commit is contained in:
Aiden Vigue 2021-06-18 19:17:32 -04:00
commit a8f5c7f947
No known key found for this signature in database
GPG Key ID: B9A09843AB079D5B
25 changed files with 390 additions and 395 deletions

View File

@ -14,61 +14,61 @@ class CastV2PlatformReader {
let stream: InputStream let stream: InputStream
var readPosition = 0 var readPosition = 0
var buffer = Data(capacity: maxBufferLength) var buffer = Data(capacity: maxBufferLength)
init(stream: InputStream) { init(stream: InputStream) {
self.stream = stream self.stream = stream
} }
func readStream() { func readStream() {
objc_sync_enter(self) objc_sync_enter(self)
defer { objc_sync_exit(self) } defer { objc_sync_exit(self) }
var totalBytesRead = 0 var totalBytesRead = 0
let bufferSize = 32 let bufferSize = 32
while stream.hasBytesAvailable { while stream.hasBytesAvailable {
var bytes = [UInt8](repeating: 0, count: bufferSize) var bytes = [UInt8](repeating: 0, count: bufferSize)
let bytesRead = stream.read(&bytes, maxLength: bufferSize) let bytesRead = stream.read(&bytes, maxLength: bufferSize)
if bytesRead < 0 { continue } if bytesRead < 0 { continue }
buffer.append(Data(bytes: &bytes, count: bytesRead)) buffer.append(Data(bytes: &bytes, count: bytesRead))
totalBytesRead += bytesRead totalBytesRead += bytesRead
} }
} }
func nextMessage() -> Data? { func nextMessage() -> Data? {
objc_sync_enter(self) objc_sync_enter(self)
defer { objc_sync_exit(self) } defer { objc_sync_exit(self) }
let headerSize = MemoryLayout<UInt32>.size let headerSize = MemoryLayout<UInt32>.size
guard buffer.count - readPosition >= headerSize else { return nil } guard buffer.count - readPosition >= headerSize else { return nil }
let header = buffer.withUnsafeBytes({ (pointer: UnsafePointer<Int8>) -> UInt32 in let header = buffer.withUnsafeBytes({ (pointer: UnsafePointer<Int8>) -> UInt32 in
return pointer.advanced(by: self.readPosition).withMemoryRebound(to: UInt32.self, capacity: 1, { $0.pointee }) return pointer.advanced(by: self.readPosition).withMemoryRebound(to: UInt32.self, capacity: 1, { $0.pointee })
}) })
let payloadSize = Int(CFSwapInt32BigToHost(header)) let payloadSize = Int(CFSwapInt32BigToHost(header))
readPosition += headerSize readPosition += headerSize
guard buffer.count >= readPosition + payloadSize, buffer.count - readPosition >= payloadSize, payloadSize >= 0 else { guard buffer.count >= readPosition + payloadSize, buffer.count - readPosition >= payloadSize, payloadSize >= 0 else {
//Message hasn't arrived // Message hasn't arrived
readPosition -= headerSize readPosition -= headerSize
return nil return nil
} }
let payload = buffer.withUnsafeBytes({ (pointer: UnsafePointer<Int8>) -> Data in let payload = buffer.withUnsafeBytes({ (pointer: UnsafePointer<Int8>) -> Data in
return Data(bytes: pointer.advanced(by: self.readPosition), count: payloadSize) return Data(bytes: pointer.advanced(by: self.readPosition), count: payloadSize)
}) })
readPosition += payloadSize readPosition += payloadSize
resetBufferIfNeeded() resetBufferIfNeeded()
return payload return payload
} }
private func resetBufferIfNeeded() { private func resetBufferIfNeeded() {
guard buffer.count >= maxBufferLength else { return } guard buffer.count >= maxBufferLength else { return }
@ -77,7 +77,7 @@ class CastV2PlatformReader {
} else { } else {
buffer = buffer.advanced(by: readPosition) buffer = buffer.advanced(by: readPosition)
} }
readPosition = 0 readPosition = 0
} }
} }

View File

@ -18,7 +18,7 @@ import SwiftProtobuf
// incompatible with the version of SwiftProtobuf to which you are linking. // incompatible with the version of SwiftProtobuf to which you are linking.
// Please ensure that your are building against the same version of the API // Please ensure that your are building against the same version of the API
// that was used to generate this file. // that was used to generate this file.
fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { private struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
typealias Version = _2 typealias Version = _2
} }
@ -221,13 +221,13 @@ struct Extensions_Api_CastChannel_CastMessage: SwiftProtobuf.Message {
try unknownFields.traverse(visitor: &visitor) try unknownFields.traverse(visitor: &visitor)
} }
fileprivate var _protocolVersion: Extensions_Api_CastChannel_CastMessage.ProtocolVersion? = nil fileprivate var _protocolVersion: Extensions_Api_CastChannel_CastMessage.ProtocolVersion?
fileprivate var _sourceID: String? = nil fileprivate var _sourceID: String?
fileprivate var _destinationID: String? = nil fileprivate var _destinationID: String?
fileprivate var _namespace: String? = nil fileprivate var _namespace: String?
fileprivate var _payloadType: Extensions_Api_CastChannel_CastMessage.PayloadType? = nil fileprivate var _payloadType: Extensions_Api_CastChannel_CastMessage.PayloadType?
fileprivate var _payloadUtf8: String? = nil fileprivate var _payloadUtf8: String?
fileprivate var _payloadBinary: Data? = nil fileprivate var _payloadBinary: Data?
} }
/// Messages for authentication protocol between a sender and a receiver. /// Messages for authentication protocol between a sender and a receiver.
@ -321,8 +321,8 @@ struct Extensions_Api_CastChannel_AuthResponse: SwiftProtobuf.Message {
try unknownFields.traverse(visitor: &visitor) try unknownFields.traverse(visitor: &visitor)
} }
fileprivate var _signature: Data? = nil fileprivate var _signature: Data?
fileprivate var _clientAuthCertificate: Data? = nil fileprivate var _clientAuthCertificate: Data?
} }
struct Extensions_Api_CastChannel_AuthError: SwiftProtobuf.Message { struct Extensions_Api_CastChannel_AuthError: SwiftProtobuf.Message {
@ -398,7 +398,7 @@ struct Extensions_Api_CastChannel_AuthError: SwiftProtobuf.Message {
try unknownFields.traverse(visitor: &visitor) try unknownFields.traverse(visitor: &visitor)
} }
fileprivate var _errorType: Extensions_Api_CastChannel_AuthError.ErrorType? = nil fileprivate var _errorType: Extensions_Api_CastChannel_AuthError.ErrorType?
} }
struct Extensions_Api_CastChannel_DeviceAuthMessage: SwiftProtobuf.Message { struct Extensions_Api_CastChannel_DeviceAuthMessage: SwiftProtobuf.Message {
@ -487,7 +487,7 @@ struct Extensions_Api_CastChannel_DeviceAuthMessage: SwiftProtobuf.Message {
// MARK: - Code below here is support for the SwiftProtobuf runtime. // MARK: - Code below here is support for the SwiftProtobuf runtime.
fileprivate let _protobuf_package = "extensions.api.cast_channel" private let _protobuf_package = "extensions.api.cast_channel"
extension Extensions_Api_CastChannel_CastMessage: SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { extension Extensions_Api_CastChannel_CastMessage: SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
@ -497,7 +497,7 @@ extension Extensions_Api_CastChannel_CastMessage: SwiftProtobuf._MessageImplemen
4: .same(proto: "namespace"), 4: .same(proto: "namespace"),
5: .standard(proto: "payload_type"), 5: .standard(proto: "payload_type"),
6: .standard(proto: "payload_utf8"), 6: .standard(proto: "payload_utf8"),
7: .standard(proto: "payload_binary"), 7: .standard(proto: "payload_binary")
] ]
func _protobuf_generated_isEqualTo(other: Extensions_Api_CastChannel_CastMessage) -> Bool { func _protobuf_generated_isEqualTo(other: Extensions_Api_CastChannel_CastMessage) -> Bool {
@ -515,14 +515,14 @@ extension Extensions_Api_CastChannel_CastMessage: SwiftProtobuf._MessageImplemen
extension Extensions_Api_CastChannel_CastMessage.ProtocolVersion: SwiftProtobuf._ProtoNameProviding { extension Extensions_Api_CastChannel_CastMessage.ProtocolVersion: SwiftProtobuf._ProtoNameProviding {
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
0: .same(proto: "CASTV2_1_0"), 0: .same(proto: "CASTV2_1_0")
] ]
} }
extension Extensions_Api_CastChannel_CastMessage.PayloadType: SwiftProtobuf._ProtoNameProviding { extension Extensions_Api_CastChannel_CastMessage.PayloadType: SwiftProtobuf._ProtoNameProviding {
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
0: .same(proto: "STRING"), 0: .same(proto: "STRING"),
1: .same(proto: "BINARY"), 1: .same(proto: "BINARY")
] ]
} }
@ -539,7 +539,7 @@ extension Extensions_Api_CastChannel_AuthResponse: SwiftProtobuf._MessageImpleme
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "signature"), 1: .same(proto: "signature"),
2: .standard(proto: "client_auth_certificate"), 2: .standard(proto: "client_auth_certificate"),
3: .standard(proto: "client_ca"), 3: .standard(proto: "client_ca")
] ]
func _protobuf_generated_isEqualTo(other: Extensions_Api_CastChannel_AuthResponse) -> Bool { func _protobuf_generated_isEqualTo(other: Extensions_Api_CastChannel_AuthResponse) -> Bool {
@ -553,7 +553,7 @@ extension Extensions_Api_CastChannel_AuthResponse: SwiftProtobuf._MessageImpleme
extension Extensions_Api_CastChannel_AuthError: SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { extension Extensions_Api_CastChannel_AuthError: SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .standard(proto: "error_type"), 1: .standard(proto: "error_type")
] ]
func _protobuf_generated_isEqualTo(other: Extensions_Api_CastChannel_AuthError) -> Bool { func _protobuf_generated_isEqualTo(other: Extensions_Api_CastChannel_AuthError) -> Bool {
@ -566,7 +566,7 @@ extension Extensions_Api_CastChannel_AuthError: SwiftProtobuf._MessageImplementa
extension Extensions_Api_CastChannel_AuthError.ErrorType: SwiftProtobuf._ProtoNameProviding { extension Extensions_Api_CastChannel_AuthError.ErrorType: SwiftProtobuf._ProtoNameProviding {
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
0: .same(proto: "INTERNAL_ERROR"), 0: .same(proto: "INTERNAL_ERROR"),
1: .same(proto: "NO_TLS"), 1: .same(proto: "NO_TLS")
] ]
} }
@ -574,13 +574,13 @@ extension Extensions_Api_CastChannel_DeviceAuthMessage: SwiftProtobuf._MessageIm
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "challenge"), 1: .same(proto: "challenge"),
2: .same(proto: "response"), 2: .same(proto: "response"),
3: .same(proto: "error"), 3: .same(proto: "error")
] ]
fileprivate class _StorageClass { fileprivate class _StorageClass {
var _challenge: Extensions_Api_CastChannel_AuthChallenge? = nil var _challenge: Extensions_Api_CastChannel_AuthChallenge?
var _response: Extensions_Api_CastChannel_AuthResponse? = nil var _response: Extensions_Api_CastChannel_AuthResponse?
var _error: Extensions_Api_CastChannel_AuthError? = nil var _error: Extensions_Api_CastChannel_AuthError?
static let defaultInstance = _StorageClass() static let defaultInstance = _StorageClass()

View File

@ -16,7 +16,7 @@ public class AppAvailability: NSObject {
extension AppAvailability { extension AppAvailability {
convenience init(json: JSON) { convenience init(json: JSON) {
self.init() self.init()
if let availability = json[CastJSONPayloadKeys.availability].dictionaryObject as? [String: String] { if let availability = json[CastJSONPayloadKeys.availability].dictionaryObject as? [String: String] {
self.availability = availability.mapValues { $0 == "APP_AVAILABLE" } self.availability = availability.mapValues { $0 == "APP_AVAILABLE" }
} }

View File

@ -23,39 +23,39 @@ public final class CastApp: NSObject {
public var statusText: String = "" public var statusText: String = ""
public var transportId: String = "" public var transportId: String = ""
public var namespaces = [String]() public var namespaces = [String]()
convenience init(json: JSON) { convenience init(json: JSON) {
self.init() self.init()
if let id = json[CastJSONPayloadKeys.appId].string { if let id = json[CastJSONPayloadKeys.appId].string {
self.id = id self.id = id
} }
if let displayName = json[CastJSONPayloadKeys.displayName].string { if let displayName = json[CastJSONPayloadKeys.displayName].string {
self.displayName = displayName self.displayName = displayName
} }
if let isIdleScreen = json[CastJSONPayloadKeys.isIdleScreen].bool { if let isIdleScreen = json[CastJSONPayloadKeys.isIdleScreen].bool {
self.isIdleScreen = isIdleScreen self.isIdleScreen = isIdleScreen
} }
if let sessionId = json[CastJSONPayloadKeys.sessionId].string { if let sessionId = json[CastJSONPayloadKeys.sessionId].string {
self.sessionId = sessionId self.sessionId = sessionId
} }
if let statusText = json[CastJSONPayloadKeys.statusText].string { if let statusText = json[CastJSONPayloadKeys.statusText].string {
self.statusText = statusText self.statusText = statusText
} }
if let transportId = json[CastJSONPayloadKeys.transportId].string { if let transportId = json[CastJSONPayloadKeys.transportId].string {
self.transportId = transportId self.transportId = transportId
} }
if let namespaces = json[CastJSONPayloadKeys.namespaces].array { if let namespaces = json[CastJSONPayloadKeys.namespaces].array {
self.namespaces = namespaces.compactMap { $0[CastJSONPayloadKeys.name].string } self.namespaces = namespaces.compactMap { $0[CastJSONPayloadKeys.name].string }
} }
} }
public override var description: String { public override var description: String {
return "CastApp(id: \(id), displayName: \(displayName), isIdleScreen: \(isIdleScreen), sessionId: \(sessionId), statusText: \(statusText), transportId: \(transportId), namespaces: \(namespaces)" return "CastApp(id: \(id), displayName: \(displayName), isIdleScreen: \(isIdleScreen), sessionId: \(sessionId), statusText: \(statusText), transportId: \(transportId), namespaces: \(namespaces)"
} }

View File

@ -8,12 +8,10 @@
import Foundation import Foundation
public struct DeviceCapabilities: OptionSet {
public struct DeviceCapabilities : OptionSet
{
public let rawValue: Int public let rawValue: Int
public init(rawValue: Int) { self.rawValue = rawValue } public init(rawValue: Int) { self.rawValue = rawValue }
public static let none = DeviceCapabilities([]) public static let none = DeviceCapabilities([])
public static let videoOut = DeviceCapabilities(rawValue: 1 << 0) public static let videoOut = DeviceCapabilities(rawValue: 1 << 0)
public static let videoIn = DeviceCapabilities(rawValue: 1 << 1) public static let videoIn = DeviceCapabilities(rawValue: 1 << 1)
@ -25,7 +23,7 @@ public struct DeviceCapabilities : OptionSet
} }
public final class CastDevice: NSObject, NSCopying { public final class CastDevice: NSObject, NSCopying {
public private(set) var id: String public private(set) var id: String
public private(set) var name: String public private(set) var name: String
public private(set) var modelName: String public private(set) var modelName: String
@ -35,7 +33,7 @@ public final class CastDevice: NSObject, NSCopying {
public private(set) var capabilities: DeviceCapabilities public private(set) var capabilities: DeviceCapabilities
public private(set) var status: String public private(set) var status: String
public private(set) var iconPath: 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) { init(id: String, name: String, modelName: String, hostName: String, ipAddress: String, port: Int, capabilitiesMask: Int, status: String, iconPath: String) {
self.id = id self.id = id
self.name = name self.name = name
@ -49,7 +47,7 @@ public final class CastDevice: NSObject, NSCopying {
super.init() super.init()
} }
public func copy(with zone: NSZone? = nil) -> Any { public func copy(with zone: NSZone? = nil) -> Any {
return CastDevice(id: self.id, return CastDevice(id: self.id,
name: self.name, name: self.name,
@ -61,7 +59,7 @@ public final class CastDevice: NSObject, NSCopying {
status: self.status, status: self.status,
iconPath: iconPath) iconPath: iconPath)
} }
public override var description: String { public override var description: String {
return "CastDevice(id: \(id), name: \(name), hostName:\(hostName), ipAddress:\(ipAddress), port:\(port))" return "CastDevice(id: \(id), name: \(name), hostName:\(hostName), ipAddress:\(ipAddress), port:\(port))"
} }

View File

@ -20,13 +20,13 @@ public final class CastMedia: NSObject {
public let title: String public let title: String
public let url: URL public let url: URL
public let poster: URL? public let poster: URL?
public let autoplay: Bool public let autoplay: Bool
public let currentTime: Double public let currentTime: Double
public let contentType: String public let contentType: String
public let streamType: CastMediaStreamType public let streamType: CastMediaStreamType
public init(title: String, url: URL, poster: URL? = nil, contentType: String, streamType: CastMediaStreamType = .buffered, autoplay: Bool = true, currentTime: Double = 0) { public init(title: String, url: URL, poster: URL? = nil, contentType: String, streamType: CastMediaStreamType = .buffered, autoplay: Bool = true, currentTime: Double = 0) {
self.title = title self.title = title
self.url = url self.url = url
@ -36,7 +36,7 @@ public final class CastMedia: NSObject {
self.autoplay = autoplay self.autoplay = autoplay
self.currentTime = currentTime self.currentTime = currentTime
} }
// public convenience init(title: String, url: URL, poster: URL, contentType: String, streamType: String, autoplay: Bool, currentTime: Double) { // public convenience init(title: String, url: URL, poster: URL, contentType: String, streamType: String, autoplay: Bool, currentTime: Double) {
// guard let type = CastMediaStreamType(rawValue: streamType) else { // guard let type = CastMediaStreamType(rawValue: streamType) else {
// fatalError("Invalid media stream type \(streamType)") // fatalError("Invalid media stream type \(streamType)")
@ -47,7 +47,7 @@ public final class CastMedia: NSObject {
} }
extension CastMedia { extension CastMedia {
var dict: [String: Any] { var dict: [String: Any] {
if let poster = poster { if let poster = poster {
return [ return [
@ -88,5 +88,5 @@ extension CastMedia {
] ]
} }
} }
} }

View File

@ -17,7 +17,7 @@ public enum CastMediaPlayerState: String {
} }
public final class CastMediaStatus: NSObject { public final class CastMediaStatus: NSObject {
public let mediaSessionId: Int public let mediaSessionId: Int
public let playbackRate: Int public let playbackRate: Int
public let playerState: CastMediaPlayerState public let playerState: CastMediaPlayerState
@ -25,36 +25,36 @@ public final class CastMediaStatus: NSObject {
public let metadata: JSON? public let metadata: JSON?
public let contentID: String? public let contentID: String?
private let createdDate = Date() private let createdDate = Date()
public var adjustedCurrentTime: Double { public var adjustedCurrentTime: Double {
return currentTime - Double(playbackRate)*createdDate.timeIntervalSinceNow return currentTime - Double(playbackRate)*createdDate.timeIntervalSinceNow
} }
public var state: String { public var state: String {
return playerState.rawValue return playerState.rawValue
} }
public override var description: String { public override var description: String {
return "MediaStatus(mediaSessionId: \(mediaSessionId), playbackRate: \(playbackRate), playerState: \(playerState.rawValue), currentTime: \(currentTime))" return "MediaStatus(mediaSessionId: \(mediaSessionId), playbackRate: \(playbackRate), playerState: \(playerState.rawValue), currentTime: \(currentTime))"
} }
init(json: JSON) { init(json: JSON) {
mediaSessionId = json[CastJSONPayloadKeys.mediaSessionId].int ?? 0 mediaSessionId = json[CastJSONPayloadKeys.mediaSessionId].int ?? 0
playbackRate = json[CastJSONPayloadKeys.playbackRate].int ?? 1 playbackRate = json[CastJSONPayloadKeys.playbackRate].int ?? 1
playerState = json[CastJSONPayloadKeys.playerState].string.flatMap(CastMediaPlayerState.init) ?? .buffering playerState = json[CastJSONPayloadKeys.playerState].string.flatMap(CastMediaPlayerState.init) ?? .buffering
currentTime = json[CastJSONPayloadKeys.currentTime].double ?? 0 currentTime = json[CastJSONPayloadKeys.currentTime].double ?? 0
metadata = json[CastJSONPayloadKeys.media][CastJSONPayloadKeys.metadata] metadata = json[CastJSONPayloadKeys.media][CastJSONPayloadKeys.metadata]
if let contentID = json[CastJSONPayloadKeys.media][CastJSONPayloadKeys.contentId].string, let data = contentID.data(using: .utf8) { 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 self.contentID = (try? JSON(data: data))?[CastJSONPayloadKeys.contentId].string ?? contentID
} else { } else {
contentID = nil contentID = nil
} }
super.init() super.init()
} }
} }

View File

@ -15,22 +15,22 @@ extension CastMessage {
message.sourceID = sourceId message.sourceID = sourceId
message.destinationID = destinationId message.destinationID = destinationId
message.namespace = namespace message.namespace = namespace
switch payload { switch payload {
case .json(let payload): case .json(let payload):
let json = try JSONSerialization.data(withJSONObject: payload, options: []) let json = try JSONSerialization.data(withJSONObject: payload, options: [])
guard let jsonString = String(data: json, encoding: .utf8) else { guard let jsonString = String(data: json, encoding: .utf8) else {
fatalError("error forming json string") fatalError("error forming json string")
} }
message.payloadType = .string message.payloadType = .string
message.payloadUtf8 = jsonString message.payloadUtf8 = jsonString
case .data(let payload): case .data(let payload):
message.payloadType = .binary message.payloadType = .binary
message.payloadBinary = payload message.payloadBinary = payload
} }
return try message.serializedData() return try message.serializedData()
} }
} }

View File

@ -15,7 +15,7 @@ public class CastMultizoneDevice {
public let isMuted: Bool public let isMuted: Bool
public let capabilities: DeviceCapabilities public let capabilities: DeviceCapabilities
public let id: String public let id: String
public init(name: String, volume: Float, isMuted: Bool, capabilitiesMask: Int, id: String) { public init(name: String, volume: Float, isMuted: Bool, capabilitiesMask: Int, id: String) {
self.name = name self.name = name
self.volume = volume self.volume = volume
@ -28,15 +28,15 @@ public class CastMultizoneDevice {
extension CastMultizoneDevice { extension CastMultizoneDevice {
convenience init(json: JSON) { convenience init(json: JSON) {
let name = json[CastJSONPayloadKeys.name].stringValue let name = json[CastJSONPayloadKeys.name].stringValue
let volumeValues = json[CastJSONPayloadKeys.volume] let volumeValues = json[CastJSONPayloadKeys.volume]
let volume = volumeValues[CastJSONPayloadKeys.level].floatValue let volume = volumeValues[CastJSONPayloadKeys.level].floatValue
let isMuted = volumeValues[CastJSONPayloadKeys.muted].boolValue let isMuted = volumeValues[CastJSONPayloadKeys.muted].boolValue
let capabilitiesMask = json[CastJSONPayloadKeys.capabilities].intValue let capabilitiesMask = json[CastJSONPayloadKeys.capabilities].intValue
let deviceId = json[CastJSONPayloadKeys.deviceId].stringValue let deviceId = json[CastJSONPayloadKeys.deviceId].stringValue
self.init(name: name, volume: volume, isMuted: isMuted, capabilitiesMask: capabilitiesMask, id: deviceId) self.init(name: name, volume: volume, isMuted: isMuted, capabilitiesMask: capabilitiesMask, id: deviceId)
} }
} }

View File

@ -11,19 +11,19 @@ import SwiftyJSON
public class CastMultizoneStatus { public class CastMultizoneStatus {
public let devices: [CastMultizoneDevice] public let devices: [CastMultizoneDevice]
public init(devices: [CastMultizoneDevice]) { public init(devices: [CastMultizoneDevice]) {
self.devices = devices self.devices = devices
} }
} }
extension CastMultizoneStatus { extension CastMultizoneStatus {
convenience init(json: JSON) { convenience init(json: JSON) {
let status = json[CastJSONPayloadKeys.status] let status = json[CastJSONPayloadKeys.status]
let devices = status[CastJSONPayloadKeys.devices].array?.map(CastMultizoneDevice.init) ?? [] let devices = status[CastJSONPayloadKeys.devices].array?.map(CastMultizoneDevice.init) ?? []
self.init(devices: devices) self.init(devices: devices)
} }
} }

View File

@ -10,35 +10,35 @@ import Foundation
import SwiftyJSON import SwiftyJSON
public final class CastStatus: NSObject { public final class CastStatus: NSObject {
public var volume: Double = 1 public var volume: Double = 1
public var muted: Bool = false public var muted: Bool = false
public var apps: [CastApp] = [] public var apps: [CastApp] = []
public override var description: String { public override var description: String {
return "CastStatus(volume: \(volume), muted: \(muted), apps: \(apps))" return "CastStatus(volume: \(volume), muted: \(muted), apps: \(apps))"
} }
} }
extension CastStatus { extension CastStatus {
convenience init(json: JSON) { convenience init(json: JSON) {
self.init() self.init()
// print(json) // print(json)
let status = json[CastJSONPayloadKeys.status] let status = json[CastJSONPayloadKeys.status]
let volume = status[CastJSONPayloadKeys.volume] let volume = status[CastJSONPayloadKeys.volume]
if let volume = volume[CastJSONPayloadKeys.level].double { if let volume = volume[CastJSONPayloadKeys.level].double {
self.volume = volume self.volume = volume
} }
if let muted = volume[CastJSONPayloadKeys.muted].bool { if let muted = volume[CastJSONPayloadKeys.muted].bool {
self.muted = muted self.muted = muted
} }
if let apps = status[CastJSONPayloadKeys.applications].array { if let apps = status[CastJSONPayloadKeys.applications].array {
self.apps = apps.compactMap(CastApp.init) self.apps = apps.compactMap(CastApp.init)
} }
} }
} }

View File

@ -14,11 +14,11 @@ import Result
public enum CastPayload { public enum CastPayload {
case json([String: Any]) case json([String: Any])
case data(Data) case data(Data)
init(_ json: [String: Any]) { init(_ json: [String: Any]) {
self = .json(json) self = .json(json)
} }
init(_ data: Data) { init(_ data: Data) {
self = .data(data) self = .data(data)
} }
@ -41,14 +41,14 @@ public class CastRequest: NSObject {
var namespace: String var namespace: String
var destinationId: String var destinationId: String
var payload: CastPayload var payload: CastPayload
init(id: Int, namespace: String, destinationId: String, payload: [String: Any]) { init(id: Int, namespace: String, destinationId: String, payload: [String: Any]) {
self.id = id self.id = id
self.namespace = namespace self.namespace = namespace
self.destinationId = destinationId self.destinationId = destinationId
self.payload = CastPayload(payload) self.payload = CastPayload(payload)
} }
init(id: Int, namespace: String, destinationId: String, payload: Data) { init(id: Int, namespace: String, destinationId: String, payload: Data) {
self.id = id self.id = id
self.namespace = namespace self.namespace = namespace
@ -58,27 +58,27 @@ public class CastRequest: NSObject {
} }
@objc public protocol CastClientDelegate: AnyObject { @objc public protocol CastClientDelegate: AnyObject {
@objc optional func castClient(_ client: CastClient, willConnectTo device: CastDevice) @objc optional func castClient(_ client: CastClient, willConnectTo device: CastDevice)
@objc optional func castClient(_ client: CastClient, didConnectTo 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, didDisconnectFrom device: CastDevice)
@objc optional func castClient(_ client: CastClient, connectionTo device: CastDevice, didFailWith error: Error?) @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, deviceStatusDidChange status: CastStatus)
@objc optional func castClient(_ client: CastClient, mediaStatusDidChange status: CastMediaStatus) @objc optional func castClient(_ client: CastClient, mediaStatusDidChange status: CastMediaStatus)
} }
public final class CastClient: NSObject, RequestDispatchable, Channelable { public final class CastClient: NSObject, RequestDispatchable, Channelable {
public let device: CastDevice public let device: CastDevice
public weak var delegate: CastClientDelegate? public weak var delegate: CastClientDelegate?
public var connectedApp: CastApp? public var connectedApp: CastApp?
public private(set) var currentStatus: CastStatus? { public private(set) var currentStatus: CastStatus? {
didSet { didSet {
guard let status = currentStatus else { return } guard let status = currentStatus else { return }
if oldValue != status { if oldValue != status {
DispatchQueue.main.async { DispatchQueue.main.async {
self.delegate?.castClient?(self, deviceStatusDidChange: status) self.delegate?.castClient?(self, deviceStatusDidChange: status)
@ -87,11 +87,11 @@ public final class CastClient: NSObject, RequestDispatchable, Channelable {
} }
} }
} }
public private(set) var currentMediaStatus: CastMediaStatus? { public private(set) var currentMediaStatus: CastMediaStatus? {
didSet { didSet {
guard let status = currentMediaStatus else { return } guard let status = currentMediaStatus else { return }
if oldValue != status { if oldValue != status {
DispatchQueue.main.async { DispatchQueue.main.async {
self.delegate?.castClient?(self, mediaStatusDidChange: status) self.delegate?.castClient?(self, mediaStatusDidChange: status)
@ -100,24 +100,24 @@ public final class CastClient: NSObject, RequestDispatchable, Channelable {
} }
} }
} }
public private(set) var currentMultizoneStatus: CastMultizoneStatus? public private(set) var currentMultizoneStatus: CastMultizoneStatus?
public var statusDidChange: ((CastStatus) -> Void)? public var statusDidChange: ((CastStatus) -> Void)?
public var mediaStatusDidChange: ((CastMediaStatus) -> Void)? public var mediaStatusDidChange: ((CastMediaStatus) -> Void)?
public init(device: CastDevice) { public init(device: CastDevice) {
self.device = device self.device = device
super.init() super.init()
} }
deinit { deinit {
disconnect() disconnect()
} }
// MARK: - Socket Setup // MARK: - Socket Setup
public var isConnected = false { public var isConnected = false {
didSet { didSet {
if oldValue != isConnected { if oldValue != isConnected {
@ -129,7 +129,7 @@ public final class CastClient: NSObject, RequestDispatchable, Channelable {
} }
} }
} }
private var inputStream: InputStream! { private var inputStream: InputStream! {
didSet { didSet {
if let inputStream = inputStream { if let inputStream = inputStream {
@ -139,70 +139,70 @@ public final class CastClient: NSObject, RequestDispatchable, Channelable {
} }
} }
} }
private var outputStream: OutputStream! private var outputStream: OutputStream!
fileprivate lazy var socketQueue = DispatchQueue.global(qos: .userInitiated) fileprivate lazy var socketQueue = DispatchQueue.global(qos: .userInitiated)
public func connect() { public func connect() {
socketQueue.async { socketQueue.async {
do { do {
var readStream: Unmanaged<CFReadStream>? var readStream: Unmanaged<CFReadStream>?
var writeStream: Unmanaged<CFWriteStream>? var writeStream: Unmanaged<CFWriteStream>?
let settings: [String: Any] = [ let settings: [String: Any] = [
kCFStreamSSLValidatesCertificateChain as String: false, kCFStreamSSLValidatesCertificateChain as String: false,
kCFStreamSSLLevel as String: kCFStreamSocketSecurityLevelTLSv1, kCFStreamSSLLevel as String: kCFStreamSocketSecurityLevelTLSv1,
kCFStreamPropertyShouldCloseNativeSocket as String: true kCFStreamPropertyShouldCloseNativeSocket as String: true
] ]
CFStreamCreatePairWithSocketToHost(nil, self.device.hostName as CFString, UInt32(self.device.port), &readStream, &writeStream) CFStreamCreatePairWithSocketToHost(nil, self.device.hostName as CFString, UInt32(self.device.port), &readStream, &writeStream)
guard let readStreamRetained = readStream?.takeRetainedValue() else { guard let readStreamRetained = readStream?.takeRetainedValue() else {
throw CastError.connection("Unable to create input stream") throw CastError.connection("Unable to create input stream")
} }
guard let writeStreamRetained = writeStream?.takeRetainedValue() else { guard let writeStreamRetained = writeStream?.takeRetainedValue() else {
throw CastError.connection("Unable to create output stream") throw CastError.connection("Unable to create output stream")
} }
DispatchQueue.main.async { self.delegate?.castClient?(self, willConnectTo: self.device) } DispatchQueue.main.async { self.delegate?.castClient?(self, willConnectTo: self.device) }
CFReadStreamSetProperty(readStreamRetained, CFStreamPropertyKey(kCFStreamPropertySSLSettings), settings as CFTypeRef?) CFReadStreamSetProperty(readStreamRetained, CFStreamPropertyKey(kCFStreamPropertySSLSettings), settings as CFTypeRef?)
CFWriteStreamSetProperty(writeStreamRetained, CFStreamPropertyKey(kCFStreamPropertySSLSettings), settings as CFTypeRef?) CFWriteStreamSetProperty(writeStreamRetained, CFStreamPropertyKey(kCFStreamPropertySSLSettings), settings as CFTypeRef?)
self.inputStream = readStreamRetained self.inputStream = readStreamRetained
self.outputStream = writeStreamRetained self.outputStream = writeStreamRetained
self.inputStream.delegate = self self.inputStream.delegate = self
self.inputStream.schedule(in: .current, forMode: RunLoop.Mode.default) self.inputStream.schedule(in: .current, forMode: RunLoop.Mode.default)
self.outputStream.schedule(in: .current, forMode: RunLoop.Mode.default) self.outputStream.schedule(in: .current, forMode: RunLoop.Mode.default)
self.inputStream.open() self.inputStream.open()
self.outputStream.open() self.outputStream.open()
RunLoop.current.run() RunLoop.current.run()
} catch { } catch {
DispatchQueue.main.async { self.delegate?.castClient?(self, connectionTo: self.device, didFailWith: error as NSError) } DispatchQueue.main.async { self.delegate?.castClient?(self, connectionTo: self.device, didFailWith: error as NSError) }
} }
} }
} }
public func disconnect() { public func disconnect() {
if isConnected { if isConnected {
isConnected = false isConnected = false
} }
channels.values.forEach(remove) channels.values.forEach(remove)
socketQueue.async { socketQueue.async {
if self.inputStream != nil { if self.inputStream != nil {
self.inputStream.close() self.inputStream.close()
self.inputStream.remove(from: RunLoop.current, forMode: RunLoop.Mode.default) self.inputStream.remove(from: RunLoop.current, forMode: RunLoop.Mode.default)
self.inputStream = nil self.inputStream = nil
} }
if self.outputStream != nil { if self.outputStream != nil {
self.outputStream.close() self.outputStream.close()
self.outputStream.remove(from: RunLoop.current, forMode: RunLoop.Mode.default) self.outputStream.remove(from: RunLoop.current, forMode: RunLoop.Mode.default)
@ -210,16 +210,16 @@ public final class CastClient: NSObject, RequestDispatchable, Channelable {
} }
} }
} }
// MARK: - Socket Lifecycle // MARK: - Socket Lifecycle
private func write(data: Data) throws { private func write(data: Data) throws {
var payloadSize = UInt32(data.count).bigEndian var payloadSize = UInt32(data.count).bigEndian
let packet = NSMutableData(bytes: &payloadSize, length: MemoryLayout<UInt32>.size) let packet = NSMutableData(bytes: &payloadSize, length: MemoryLayout<UInt32>.size)
packet.append(data) packet.append(data)
let streamBytes = packet.bytes.bindMemory(to: UInt8.self, capacity: data.count) let streamBytes = packet.bytes.bindMemory(to: UInt8.self, capacity: data.count)
if outputStream.write(streamBytes, maxLength: packet.length) < 0 { if outputStream.write(streamBytes, maxLength: packet.length) < 0 {
if let error = outputStream.streamError { if let error = outputStream.streamError {
throw CastError.write("Error writing \(packet.length) byte(s) to stream: \(error)") throw CastError.write("Error writing \(packet.length) byte(s) to stream: \(error)")
@ -228,38 +228,37 @@ public final class CastClient: NSObject, RequestDispatchable, Channelable {
} }
} }
} }
fileprivate func sendConnectMessage() throws { fileprivate func sendConnectMessage() throws {
guard outputStream != nil else { return } guard outputStream != nil else { return }
_ = connectionChannel _ = connectionChannel
DispatchQueue.main.async { DispatchQueue.main.async {
_ = self.receiverControlChannel _ = self.receiverControlChannel
_ = self.mediaControlChannel _ = self.mediaControlChannel
_ = self.heartbeatChannel _ = self.heartbeatChannel
if self.device.capabilities.contains(.multizoneGroup) { if self.device.capabilities.contains(.multizoneGroup) {
_ = self.multizoneControlChannel _ = self.multizoneControlChannel
} }
} }
} }
private var reader: CastV2PlatformReader? private var reader: CastV2PlatformReader?
fileprivate func readStream() { fileprivate func readStream() {
do { do {
reader?.readStream() reader?.readStream()
while let payload = reader?.nextMessage() { while let payload = reader?.nextMessage() {
let message = try CastMessage(serializedData: payload) let message = try CastMessage(serializedData: payload)
guard let channel = channels[message.namespace] else { guard let channel = channels[message.namespace] else {
print("No channel attached for namespace \(message.namespace)") print("No channel attached for namespace \(message.namespace)")
return return
} }
switch message.payloadType { switch message.payloadType {
case .string: case .string:
if let messageData = message.payloadUtf8.data(using: .utf8) { if let messageData = message.payloadUtf8.data(using: .utf8) {
@ -267,7 +266,7 @@ public final class CastClient: NSObject, RequestDispatchable, Channelable {
channel.handleResponse(json, channel.handleResponse(json,
sourceId: message.sourceID) sourceId: message.sourceID)
if let requestId = json[CastJSONPayloadKeys.requestId].int { if let requestId = json[CastJSONPayloadKeys.requestId].int {
callResponseHandler(for: requestId, with: Result(value: json)) callResponseHandler(for: requestId, with: Result(value: json))
} }
@ -283,77 +282,77 @@ public final class CastClient: NSObject, RequestDispatchable, Channelable {
NSLog("Error reading: \(error)") NSLog("Error reading: \(error)")
} }
} }
//MARK: - Channelable // MARK: - Channelable
var channels = [String: CastChannel]() var channels = [String: CastChannel]()
private lazy var heartbeatChannel: HeartbeatChannel = { private lazy var heartbeatChannel: HeartbeatChannel = {
let channel = HeartbeatChannel() let channel = HeartbeatChannel()
self.add(channel: channel) self.add(channel: channel)
return channel return channel
}() }()
private lazy var connectionChannel: DeviceConnectionChannel = { private lazy var connectionChannel: DeviceConnectionChannel = {
let channel = DeviceConnectionChannel() let channel = DeviceConnectionChannel()
self.add(channel: channel) self.add(channel: channel)
return channel return channel
}() }()
private lazy var receiverControlChannel: ReceiverControlChannel = { private lazy var receiverControlChannel: ReceiverControlChannel = {
let channel = ReceiverControlChannel() let channel = ReceiverControlChannel()
self.add(channel: channel) self.add(channel: channel)
return channel return channel
}() }()
private lazy var mediaControlChannel: MediaControlChannel = { private lazy var mediaControlChannel: MediaControlChannel = {
let channel = MediaControlChannel() let channel = MediaControlChannel()
self.add(channel: channel) self.add(channel: channel)
return channel return channel
}() }()
private lazy var multizoneControlChannel: MultizoneControlChannel = { private lazy var multizoneControlChannel: MultizoneControlChannel = {
let channel = MultizoneControlChannel() let channel = MultizoneControlChannel()
self.add(channel: channel) self.add(channel: channel)
return channel return channel
}() }()
// MARK: - Request response // MARK: - Request response
private lazy var currentRequestId = Int(arc4random_uniform(800)) private lazy var currentRequestId = Int(arc4random_uniform(800))
func nextRequestId() -> Int { func nextRequestId() -> Int {
currentRequestId += 1 currentRequestId += 1
return currentRequestId return currentRequestId
} }
private let senderName: String = "sender-\(UUID().uuidString)" private let senderName: String = "sender-\(UUID().uuidString)"
private var responseHandlers = [Int: CastResponseHandler]() private var responseHandlers = [Int: CastResponseHandler]()
func send(_ request: CastRequest, response: CastResponseHandler?) { func send(_ request: CastRequest, response: CastResponseHandler?) {
if let response = response { if let response = response {
responseHandlers[request.id] = response responseHandlers[request.id] = response
} }
do { do {
let messageData = try CastMessage.encodedMessage(payload: request.payload, let messageData = try CastMessage.encodedMessage(payload: request.payload,
namespace: request.namespace, namespace: request.namespace,
sourceId: senderName, sourceId: senderName,
destinationId: request.destinationId) destinationId: request.destinationId)
try write(data: messageData) try write(data: messageData)
} catch { } catch {
callResponseHandler(for: request.id, with: Result(error: .request(error.localizedDescription))) callResponseHandler(for: request.id, with: Result(error: .request(error.localizedDescription)))
} }
} }
private func callResponseHandler(for requestId: Int, with result: Result<JSON, CastError>) { private func callResponseHandler(for requestId: Int, with result: Result<JSON, CastError>) {
DispatchQueue.main.async { DispatchQueue.main.async {
if let handler = self.responseHandlers.removeValue(forKey: requestId) { if let handler = self.responseHandlers.removeValue(forKey: requestId) {
@ -361,22 +360,22 @@ public final class CastClient: NSObject, RequestDispatchable, Channelable {
} }
} }
} }
// MARK: - Public messages // MARK: - Public messages
public func getAppAvailability(apps: [CastApp], completion: @escaping (Result<AppAvailability, CastError>) -> Void) { public func getAppAvailability(apps: [CastApp], completion: @escaping (Result<AppAvailability, CastError>) -> Void) {
guard outputStream != nil else { return } guard outputStream != nil else { return }
receiverControlChannel.getAppAvailability(apps: apps, completion: completion) receiverControlChannel.getAppAvailability(apps: apps, completion: completion)
} }
public func join(app: CastApp? = nil, completion: @escaping (Result<CastApp, CastError>) -> Void) { public func join(app: CastApp? = nil, completion: @escaping (Result<CastApp, CastError>) -> Void) {
guard outputStream != nil, guard outputStream != nil,
let target = app ?? currentStatus?.apps.first else { let target = app ?? currentStatus?.apps.first else {
completion(Result(error: CastError.session("No Apps Running"))) completion(Result(error: CastError.session("No Apps Running")))
return return
} }
if target == connectedApp { if target == connectedApp {
completion(Result(value: target)) completion(Result(value: target))
} else if let existing = currentStatus?.apps.first(where: { $0.id == target.id }) { } else if let existing = currentStatus?.apps.first(where: { $0.id == target.id }) {
@ -390,67 +389,67 @@ public final class CastClient: NSObject, RequestDispatchable, Channelable {
completion(Result(error: CastError.launch("Unable to get launched app instance"))) completion(Result(error: CastError.launch("Unable to get launched app instance")))
return return
} }
self?.connect(to: app) self?.connect(to: app)
completion(Result(value: app)) completion(Result(value: app))
case .failure(let error): case .failure(let error):
completion(Result(error: error)) completion(Result(error: error))
} }
} }
} }
} }
public func launch(appId: String, completion: @escaping (Result<CastApp, CastError>) -> Void) { public func launch(appId: String, completion: @escaping (Result<CastApp, CastError>) -> Void) {
guard outputStream != nil else { return } guard outputStream != nil else { return }
receiverControlChannel.launch(appId: appId) { [weak self] result in receiverControlChannel.launch(appId: appId) { [weak self] result in
switch result { switch result {
case .success(let app): case .success(let app):
self?.connect(to: app) self?.connect(to: app)
fallthrough fallthrough
default: default:
completion(result) completion(result)
} }
} }
} }
public func stopCurrentApp() { public func stopCurrentApp() {
guard outputStream != nil, let app = currentStatus?.apps.first else { return } guard outputStream != nil, let app = currentStatus?.apps.first else { return }
receiverControlChannel.stop(app: app) receiverControlChannel.stop(app: app)
} }
public func leave(_ app: CastApp) { public func leave(_ app: CastApp) {
guard outputStream != nil else { return } guard outputStream != nil else { return }
connectionChannel.leave(app) connectionChannel.leave(app)
connectedApp = nil connectedApp = nil
} }
public func load(media: CastMedia, with app: CastApp, completion: @escaping (Result<CastMediaStatus, CastError>) -> Void) { public func load(media: CastMedia, with app: CastApp, completion: @escaping (Result<CastMediaStatus, CastError>) -> Void) {
guard outputStream != nil else { return } guard outputStream != nil else { return }
mediaControlChannel.load(media: media, with: app, completion: completion) mediaControlChannel.load(media: media, with: app, completion: completion)
} }
public func requestMediaStatus(for app: CastApp, completion: ((Result<CastMediaStatus, CastError>) -> Void)? = nil) { public func requestMediaStatus(for app: CastApp, completion: ((Result<CastMediaStatus, CastError>) -> Void)? = nil) {
guard outputStream != nil else { return } guard outputStream != nil else { return }
mediaControlChannel.requestMediaStatus(for: app) mediaControlChannel.requestMediaStatus(for: app)
} }
private func connect(to app: CastApp) { private func connect(to app: CastApp) {
guard outputStream != nil else { return } guard outputStream != nil else { return }
connectionChannel.connect(to: app) connectionChannel.connect(to: app)
connectedApp = app connectedApp = app
} }
public func pause() { public func pause() {
guard outputStream != nil, let app = connectedApp else { return } guard outputStream != nil, let app = connectedApp else { return }
if let mediaStatus = currentMediaStatus { if let mediaStatus = currentMediaStatus {
mediaControlChannel.sendPause(for: app, mediaSessionId: mediaStatus.mediaSessionId) mediaControlChannel.sendPause(for: app, mediaSessionId: mediaStatus.mediaSessionId)
} else { } else {
@ -458,17 +457,17 @@ public final class CastClient: NSObject, RequestDispatchable, Channelable {
switch result { switch result {
case .success(let mediaStatus): case .success(let mediaStatus):
self.mediaControlChannel.sendPause(for: app, mediaSessionId: mediaStatus.mediaSessionId) self.mediaControlChannel.sendPause(for: app, mediaSessionId: mediaStatus.mediaSessionId)
case .failure(let error): case .failure(let error):
print(error) print(error)
} }
} }
} }
} }
public func play() { public func play() {
guard outputStream != nil, let app = connectedApp else { return } guard outputStream != nil, let app = connectedApp else { return }
if let mediaStatus = currentMediaStatus { if let mediaStatus = currentMediaStatus {
mediaControlChannel.sendPlay(for: app, mediaSessionId: mediaStatus.mediaSessionId) mediaControlChannel.sendPlay(for: app, mediaSessionId: mediaStatus.mediaSessionId)
} else { } else {
@ -476,17 +475,17 @@ public final class CastClient: NSObject, RequestDispatchable, Channelable {
switch result { switch result {
case .success(let mediaStatus): case .success(let mediaStatus):
self.mediaControlChannel.sendPlay(for: app, mediaSessionId: mediaStatus.mediaSessionId) self.mediaControlChannel.sendPlay(for: app, mediaSessionId: mediaStatus.mediaSessionId)
case .failure(let error): case .failure(let error):
print(error) print(error)
} }
} }
} }
} }
public func stop() { public func stop() {
guard outputStream != nil, let app = connectedApp else { return } guard outputStream != nil, let app = connectedApp else { return }
if let mediaStatus = currentMediaStatus { if let mediaStatus = currentMediaStatus {
mediaControlChannel.sendStop(for: app, mediaSessionId: mediaStatus.mediaSessionId) mediaControlChannel.sendStop(for: app, mediaSessionId: mediaStatus.mediaSessionId)
} else { } else {
@ -494,17 +493,17 @@ public final class CastClient: NSObject, RequestDispatchable, Channelable {
switch result { switch result {
case .success(let mediaStatus): case .success(let mediaStatus):
self.mediaControlChannel.sendStop(for: app, mediaSessionId: mediaStatus.mediaSessionId) self.mediaControlChannel.sendStop(for: app, mediaSessionId: mediaStatus.mediaSessionId)
case .failure(let error): case .failure(let error):
print(error) print(error)
} }
} }
} }
} }
public func seek(to currentTime: Float) { public func seek(to currentTime: Float) {
guard outputStream != nil, let app = connectedApp else { return } guard outputStream != nil, let app = connectedApp else { return }
if let mediaStatus = currentMediaStatus { if let mediaStatus = currentMediaStatus {
mediaControlChannel.sendSeek(to: currentTime, for: app, mediaSessionId: mediaStatus.mediaSessionId) mediaControlChannel.sendSeek(to: currentTime, for: app, mediaSessionId: mediaStatus.mediaSessionId)
} else { } else {
@ -512,41 +511,41 @@ public final class CastClient: NSObject, RequestDispatchable, Channelable {
switch result { switch result {
case .success(let mediaStatus): case .success(let mediaStatus):
self.mediaControlChannel.sendSeek(to: currentTime, for: app, mediaSessionId: mediaStatus.mediaSessionId) self.mediaControlChannel.sendSeek(to: currentTime, for: app, mediaSessionId: mediaStatus.mediaSessionId)
case .failure(let error): case .failure(let error):
print(error) print(error)
} }
} }
} }
} }
public func setVolume(_ volume: Float) { public func setVolume(_ volume: Float) {
guard outputStream != nil else { return } guard outputStream != nil else { return }
receiverControlChannel.setVolume(volume) receiverControlChannel.setVolume(volume)
} }
public func setMuted(_ muted: Bool) { public func setMuted(_ muted: Bool) {
guard outputStream != nil else { return } guard outputStream != nil else { return }
receiverControlChannel.setMuted(muted) receiverControlChannel.setMuted(muted)
} }
public func setVolume(_ volume: Float, for device: CastMultizoneDevice) { public func setVolume(_ volume: Float, for device: CastMultizoneDevice) {
guard device.capabilities.contains(.multizoneGroup) else { guard device.capabilities.contains(.multizoneGroup) else {
print("Attempted to set zone volume on non-multizone device") print("Attempted to set zone volume on non-multizone device")
return return
} }
multizoneControlChannel.setVolume(volume, for: device) multizoneControlChannel.setVolume(volume, for: device)
} }
public func setMuted(_ isMuted: Bool, for device: CastMultizoneDevice) { public func setMuted(_ isMuted: Bool, for device: CastMultizoneDevice) {
guard device.capabilities.contains(.multizoneGroup) else { guard device.capabilities.contains(.multizoneGroup) else {
print("Attempted to mute zone on non-multizone device") print("Attempted to mute zone on non-multizone device")
return return
} }
multizoneControlChannel.setMuted(isMuted, for: device) multizoneControlChannel.setMuted(isMuted, for: device)
} }
} }
@ -559,14 +558,14 @@ extension CastClient: StreamDelegate {
socketQueue.async { socketQueue.async {
do { do {
try self.sendConnectMessage() try self.sendConnectMessage()
} catch { } catch {
NSLog("Error sending connect message: \(error)") NSLog("Error sending connect message: \(error)")
} }
} }
case Stream.Event.errorOccurred: case Stream.Event.errorOccurred:
NSLog("Stream error occurred: \(aStream.streamError.debugDescription)") NSLog("Stream error occurred: \(aStream.streamError.debugDescription)")
DispatchQueue.main.async { DispatchQueue.main.async {
self.delegate?.castClient?(self, connectionTo: self.device, didFailWith: aStream.streamError) self.delegate?.castClient?(self, connectionTo: self.device, didFailWith: aStream.streamError)
} }
@ -577,7 +576,7 @@ extension CastClient: StreamDelegate {
case Stream.Event.endEncountered: case Stream.Event.endEncountered:
NSLog("Input stream ended") NSLog("Input stream ended")
disconnect() disconnect()
default: break default: break
} }
} }
@ -601,7 +600,7 @@ extension CastClient: HeartbeatChannelDelegate {
isConnected = true isConnected = true
} }
} }
func channelDidTimeout(_ channel: HeartbeatChannel) { func channelDidTimeout(_ channel: HeartbeatChannel) {
disconnect() disconnect()
currentStatus = nil currentStatus = nil
@ -612,17 +611,17 @@ extension CastClient: HeartbeatChannelDelegate {
extension CastClient: MultizoneControlChannelDelegate { extension CastClient: MultizoneControlChannelDelegate {
func channel(_ channel: MultizoneControlChannel, added device: CastMultizoneDevice) { func channel(_ channel: MultizoneControlChannel, added device: CastMultizoneDevice) {
} }
func channel(_ channel: MultizoneControlChannel, updated device: CastMultizoneDevice) { func channel(_ channel: MultizoneControlChannel, updated device: CastMultizoneDevice) {
} }
func channel(_ channel: MultizoneControlChannel, removed deviceId: String) { func channel(_ channel: MultizoneControlChannel, removed deviceId: String) {
} }
func channel(_ channel: MultizoneControlChannel, didReceive status: CastMultizoneStatus) { func channel(_ channel: MultizoneControlChannel, didReceive status: CastMultizoneStatus) {
currentMultizoneStatus = status currentMultizoneStatus = status
} }

View File

@ -11,15 +11,15 @@ import Foundation
extension CastDevice { extension CastDevice {
convenience init(service: NetService, info: [String: String]) { convenience init(service: NetService, info: [String: String]) {
var ipAddress: String? var ipAddress: String?
if let address = service.addresses?.first { if let address = service.addresses?.first {
ipAddress = address.withUnsafeBytes { (pointer: UnsafePointer<sockaddr>) -> String? in ipAddress = address.withUnsafeBytes { (pointer: UnsafePointer<sockaddr>) -> String? in
var hostName = [CChar](repeating: 0, count: Int(NI_MAXHOST)) 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 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"] ?? "", self.init(id: info["id"] ?? "",
name: info["fn"] ?? service.name, name: info["fn"] ?? service.name,
modelName: info["md"] ?? "Google Cast", modelName: info["md"] ?? "Google Cast",
@ -30,129 +30,129 @@ extension CastDevice {
status: info["rs"] ?? "", status: info["rs"] ?? "",
iconPath: info["ic"] ?? "") iconPath: info["ic"] ?? "")
} }
} }
public final class CastDeviceScanner: NSObject { public final class CastDeviceScanner: NSObject {
public weak var delegate: CastDeviceScannerDelegate? public weak var delegate: CastDeviceScannerDelegate?
public static let deviceListDidChange = Notification.Name(rawValue: "DeviceScannerDeviceListDidChangeNotification") public static let deviceListDidChange = Notification.Name(rawValue: "DeviceScannerDeviceListDidChangeNotification")
private lazy var browser: NetServiceBrowser = configureBrowser() private lazy var browser: NetServiceBrowser = configureBrowser()
public var isScanning = false public var isScanning = false
fileprivate var services = [NetService]() fileprivate var services = [NetService]()
public fileprivate(set) var devices = [CastDevice]() { public fileprivate(set) var devices = [CastDevice]() {
didSet { didSet {
NotificationCenter.default.post(name: CastDeviceScanner.deviceListDidChange, object: self) NotificationCenter.default.post(name: CastDeviceScanner.deviceListDidChange, object: self)
} }
} }
private func configureBrowser() -> NetServiceBrowser { private func configureBrowser() -> NetServiceBrowser {
let b = NetServiceBrowser() let b = NetServiceBrowser()
b.includesPeerToPeer = true b.includesPeerToPeer = true
b.delegate = self b.delegate = self
return b return b
} }
public func startScanning() { public func startScanning() {
guard !isScanning else { return } guard !isScanning else { return }
browser.stop() browser.stop()
browser.searchForServices(ofType: "_googlecast._tcp", inDomain: "local") browser.searchForServices(ofType: "_googlecast._tcp", inDomain: "local")
#if DEBUG #if DEBUG
NSLog("Started scanning") NSLog("Started scanning")
#endif #endif
} }
public func stopScanning() { public func stopScanning() {
guard isScanning else { return } guard isScanning else { return }
browser.stop() browser.stop()
#if DEBUG #if DEBUG
NSLog("Stopped scanning") NSLog("Stopped scanning")
#endif #endif
} }
public func reset() { public func reset() {
stopScanning() stopScanning()
devices.removeAll() devices.removeAll()
} }
deinit { deinit {
stopScanning() stopScanning()
} }
} }
extension CastDeviceScanner: NetServiceBrowserDelegate { extension CastDeviceScanner: NetServiceBrowserDelegate {
public func netServiceBrowserWillSearch(_ browser: NetServiceBrowser) { public func netServiceBrowserWillSearch(_ browser: NetServiceBrowser) {
isScanning = true isScanning = true
} }
public func netServiceBrowserDidStopSearch(_ browser: NetServiceBrowser) { public func netServiceBrowserDidStopSearch(_ browser: NetServiceBrowser) {
isScanning = false isScanning = false
} }
public func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) { public func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) {
removeService(service) removeService(service)
service.delegate = self service.delegate = self
service.resolve(withTimeout: 30.0) service.resolve(withTimeout: 30.0)
services.append(service) services.append(service)
#if DEBUG #if DEBUG
NSLog("Did find service: \(service) more: \(moreComing)") NSLog("Did find service: \(service) more: \(moreComing)")
#endif #endif
} }
public func netServiceBrowser(_ browser: NetServiceBrowser, didRemove service: NetService, moreComing: Bool) { public func netServiceBrowser(_ browser: NetServiceBrowser, didRemove service: NetService, moreComing: Bool) {
guard let service = removeService(service) else { return } guard let service = removeService(service) else { return }
#if DEBUG #if DEBUG
NSLog("Did remove service: \(service)") NSLog("Did remove service: \(service)")
#endif #endif
guard let deviceId = service.id, guard let deviceId = service.id,
let index = devices.firstIndex(where: { $0.id == deviceId }) else { let index = devices.firstIndex(where: { $0.id == deviceId }) else {
#if DEBUG #if DEBUG
NSLog("No device") NSLog("No device")
#endif #endif
return return
} }
#if DEBUG #if DEBUG
NSLog("Removing device: \(devices[index])") NSLog("Removing device: \(devices[index])")
#endif #endif
let device = devices.remove(at: index) let device = devices.remove(at: index)
delegate?.deviceDidGoOffline(device) delegate?.deviceDidGoOffline(device)
} }
@discardableResult func removeService(_ service: NetService) -> NetService? { @discardableResult func removeService(_ service: NetService) -> NetService? {
if let index = services.firstIndex(of: service) { if let index = services.firstIndex(of: service) {
return services.remove(at: index) return services.remove(at: index)
} }
return nil return nil
} }
func addDevice(_ device: CastDevice) { func addDevice(_ device: CastDevice) {
if let index = devices.firstIndex(where: { $0.id == device.id }) { if let index = devices.firstIndex(where: { $0.id == device.id }) {
let existing = devices[index] let existing = devices[index]
guard existing.name != device.name || existing.hostName != device.hostName else { return } guard existing.name != device.name || existing.hostName != device.hostName else { return }
devices.remove(at: index) devices.remove(at: index)
devices.insert(device, at: index) devices.insert(device, at: index)
delegate?.deviceDidChange(device) delegate?.deviceDidChange(device)
} else { } else {
devices.append(device) devices.append(device)
@ -162,7 +162,7 @@ extension CastDeviceScanner: NetServiceBrowserDelegate {
} }
extension CastDeviceScanner: NetServiceDelegate { extension CastDeviceScanner: NetServiceDelegate {
public func netServiceDidResolveAddress(_ sender: NetService) { public func netServiceDidResolveAddress(_ sender: NetService) {
guard let infoDict = sender.infoDict else { guard let infoDict = sender.infoDict else {
#if DEBUG #if DEBUG
@ -170,25 +170,25 @@ extension CastDeviceScanner: NetServiceDelegate {
#endif #endif
return return
} }
#if DEBUG #if DEBUG
NSLog("Did resolve service: \(sender)") NSLog("Did resolve service: \(sender)")
NSLog("\(infoDict)") NSLog("\(infoDict)")
#endif #endif
guard infoDict["id"] != nil else { guard infoDict["id"] != nil else {
#if DEBUG #if DEBUG
NSLog("No id for device \(sender), skipping") NSLog("No id for device \(sender), skipping")
#endif #endif
return return
} }
addDevice(CastDevice(service: sender, info: infoDict)) addDevice(CastDevice(service: sender, info: infoDict))
} }
public func netService(_ sender: NetService, didNotResolve errorDict: [String : NSNumber]) { public func netService(_ sender: NetService, didNotResolve errorDict: [String: NSNumber]) {
removeService(sender) removeService(sender)
#if DEBUG #if DEBUG
NSLog("!! Failed to resolve service: \(sender) - \(errorDict) !!") NSLog("!! Failed to resolve service: \(sender) - \(errorDict) !!")
#endif #endif
@ -200,13 +200,13 @@ extension NetService {
guard let data = txtRecordData() else { guard let data = txtRecordData() else {
return nil return nil
} }
var dict = [String: String]() var dict = [String: String]()
NetService.dictionary(fromTXTRecord: data).forEach({ dict[$0.key] = String(data: $0.value, encoding: .utf8)! }) NetService.dictionary(fromTXTRecord: data).forEach({ dict[$0.key] = String(data: $0.value, encoding: .utf8)! })
return dict return dict
} }
var id: String? { var id: String? {
return infoDict?["id"] return infoDict?["id"]
} }

View File

@ -12,19 +12,19 @@ import SwiftyJSON
open class CastChannel { open class CastChannel {
let namespace: String let namespace: String
weak var requestDispatcher: RequestDispatchable! weak var requestDispatcher: RequestDispatchable!
init(namespace: String) { init(namespace: String) {
self.namespace = namespace self.namespace = namespace
} }
open func handleResponse(_ json: JSON, sourceId: String) { open func handleResponse(_ json: JSON, sourceId: String) {
// print(json) // print(json)
} }
open func handleResponse(_ data: Data, sourceId: String) { open func handleResponse(_ data: Data, sourceId: String) {
print("\n--Binary response--\n") print("\n--Binary response--\n")
} }
public func send(_ request: CastRequest, response: CastResponseHandler? = nil) { public func send(_ request: CastRequest, response: CastResponseHandler? = nil) {
requestDispatcher.send(request, response: response) requestDispatcher.send(request, response: response)
} }

View File

@ -10,7 +10,7 @@ import Foundation
protocol Channelable: RequestDispatchable { protocol Channelable: RequestDispatchable {
var channels: [String: CastChannel] { get set } var channels: [String: CastChannel] { get set }
func add(channel: CastChannel) func add(channel: CastChannel)
func remove(channel: CastChannel) func remove(channel: CastChannel)
} }
@ -22,18 +22,18 @@ extension Channelable {
print("Channel already attached for \(namespace)") print("Channel already attached for \(namespace)")
return return
} }
channels[namespace] = channel channels[namespace] = channel
channel.requestDispatcher = self channel.requestDispatcher = self
} }
public func remove(channel: CastChannel) { public func remove(channel: CastChannel) {
let namespace = channel.namespace let namespace = channel.namespace
guard let channel = channels.removeValue(forKey: namespace) else { guard let channel = channels.removeValue(forKey: namespace) else {
print("No channel attached for \(namespace)") print("No channel attached for \(namespace)")
return return
} }
channel.requestDispatcher = nil channel.requestDispatcher = nil
} }
} }

View File

@ -11,16 +11,16 @@ import Foundation
class DeviceAuthChannel: CastChannel { class DeviceAuthChannel: CastChannel {
typealias CastAuthChallenge = Extensions_Api_CastChannel_AuthChallenge typealias CastAuthChallenge = Extensions_Api_CastChannel_AuthChallenge
typealias CastAuthMessage = Extensions_Api_CastChannel_DeviceAuthMessage typealias CastAuthMessage = Extensions_Api_CastChannel_DeviceAuthMessage
init() { init() {
super.init(namespace: CastNamespace.auth) super.init(namespace: CastNamespace.auth)
} }
public func sendAuthChallenge() throws { public func sendAuthChallenge() throws {
let message = CastAuthMessage.with { let message = CastAuthMessage.with {
$0.challenge = CastAuthChallenge() $0.challenge = CastAuthChallenge()
} }
let request = requestDispatcher.request(withNamespace: namespace, let request = requestDispatcher.request(withNamespace: namespace,
destinationId: CastConstants.receiver, destinationId: CastConstants.receiver,
payload: try message.serializedData()) payload: try message.serializedData())

View File

@ -16,32 +16,32 @@ class DeviceConnectionChannel: CastChannel {
} }
} }
} }
init() { init() {
super.init(namespace: CastNamespace.connection) super.init(namespace: CastNamespace.connection)
} }
func connect() { func connect() {
let request = requestDispatcher.request(withNamespace: namespace, let request = requestDispatcher.request(withNamespace: namespace,
destinationId: CastConstants.receiver, destinationId: CastConstants.receiver,
payload: [CastJSONPayloadKeys.type: CastMessageType.connect.rawValue]) payload: [CastJSONPayloadKeys.type: CastMessageType.connect.rawValue])
send(request) send(request)
} }
func connect(to app: CastApp) { func connect(to app: CastApp) {
let request = requestDispatcher.request(withNamespace: namespace, let request = requestDispatcher.request(withNamespace: namespace,
destinationId: app.transportId, destinationId: app.transportId,
payload: [CastJSONPayloadKeys.type: CastMessageType.connect.rawValue]) payload: [CastJSONPayloadKeys.type: CastMessageType.connect.rawValue])
send(request) send(request)
} }
public func leave(_ app: CastApp) { public func leave(_ app: CastApp) {
let request = requestDispatcher.request(withNamespace: namespace, let request = requestDispatcher.request(withNamespace: namespace,
destinationId: app.transportId, destinationId: app.transportId,
payload: [CastJSONPayloadKeys.type: CastMessageType.close.rawValue]) payload: [CastJSONPayloadKeys.type: CastMessageType.close.rawValue])
send(request) send(request)
} }
} }

View File

@ -12,17 +12,17 @@ class DeviceDiscoveryChannel: CastChannel {
init() { init() {
super.init(namespace: CastNamespace.discovery) super.init(namespace: CastNamespace.discovery)
} }
func requestDeviceInfo() { func requestDeviceInfo() {
let request = requestDispatcher.request(withNamespace: namespace, let request = requestDispatcher.request(withNamespace: namespace,
destinationId: CastConstants.receiver, destinationId: CastConstants.receiver,
payload: [CastJSONPayloadKeys.type: CastMessageType.getDeviceInfo.rawValue]) payload: [CastJSONPayloadKeys.type: CastMessageType.getDeviceInfo.rawValue])
send(request) { result in send(request) { result in
switch result { switch result {
case .success(let json): case .success(let json):
print(json) print(json)
case .failure(let error): case .failure(let error):
print(error) print(error)
} }

View File

@ -12,7 +12,7 @@ class DeviceSetupChannel: CastChannel {
init() { init() {
super.init(namespace: CastNamespace.setup) super.init(namespace: CastNamespace.setup)
} }
public func requestDeviceConfig() { public func requestDeviceConfig() {
let params = [ let params = [
"version", "version",
@ -24,28 +24,28 @@ class DeviceSetupChannel: CastChannel {
"wifi.signal_level", "wifi.signal_level",
"wifi.noise_level" "wifi.noise_level"
] ]
let payload: [String: Any] = [ let payload: [String: Any] = [
CastJSONPayloadKeys.type: CastMessageType.getDeviceConfig.rawValue, CastJSONPayloadKeys.type: CastMessageType.getDeviceConfig.rawValue,
"params": params, "params": params,
"data": [:] "data": [:]
] ]
let request = requestDispatcher.request(withNamespace: namespace, let request = requestDispatcher.request(withNamespace: namespace,
destinationId: CastConstants.receiver, destinationId: CastConstants.receiver,
payload: payload) payload: payload)
send(request) { result in send(request) { result in
switch result { switch result {
case .success(let json): case .success(let json):
print(json) print(json)
case .failure(let error): case .failure(let error):
print(error) print(error)
} }
} }
} }
public func requestSetDeviceConfig() { public func requestSetDeviceConfig() {
// let data: [String: Any] = [ // let data: [String: Any] = [
// "name": "JUNK", // "name": "JUNK",
@ -53,42 +53,42 @@ class DeviceSetupChannel: CastChannel {
// //
// ] // ]
// ] // ]
let payload: [String: Any] = [ let payload: [String: Any] = [
CastJSONPayloadKeys.type: CastMessageType.getDeviceConfig.rawValue, CastJSONPayloadKeys.type: CastMessageType.getDeviceConfig.rawValue,
"data": [:] "data": [:]
] ]
let request = requestDispatcher.request(withNamespace: namespace, let request = requestDispatcher.request(withNamespace: namespace,
destinationId: CastConstants.receiver, destinationId: CastConstants.receiver,
payload: payload) payload: payload)
send(request) { result in send(request) { result in
switch result { switch result {
case .success(let json): case .success(let json):
print(json) print(json)
case .failure(let error): case .failure(let error):
print(error) print(error)
} }
} }
} }
public func requestAppDeviceId(app: CastApp) { public func requestAppDeviceId(app: CastApp) {
let payload: [String: Any] = [ let payload: [String: Any] = [
CastJSONPayloadKeys.type: CastMessageType.getAppDeviceId.rawValue, CastJSONPayloadKeys.type: CastMessageType.getAppDeviceId.rawValue,
"data": ["app_id": app.id] "data": ["app_id": app.id]
] ]
let request = requestDispatcher.request(withNamespace: namespace, let request = requestDispatcher.request(withNamespace: namespace,
destinationId: CastConstants.receiver, destinationId: CastConstants.receiver,
payload: payload) payload: payload)
send(request) { result in send(request) { result in
switch result { switch result {
case .success(let json): case .success(let json):
print(json) print(json)
case .failure(let error): case .failure(let error):
print(error) print(error)
} }

View File

@ -11,7 +11,7 @@ import SwiftyJSON
class HeartbeatChannel: CastChannel { class HeartbeatChannel: CastChannel {
private lazy var timer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(sendPing), userInfo: nil, repeats: true) private lazy var timer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(sendPing), userInfo: nil, repeats: true)
private let disconnectTimeout: TimeInterval = 10 private let disconnectTimeout: TimeInterval = 10
private var disconnectTimer: Timer? { private var disconnectTimer: Timer? {
willSet { willSet {
@ -19,11 +19,11 @@ class HeartbeatChannel: CastChannel {
} }
didSet { didSet {
guard let timer = disconnectTimer else { return } guard let timer = disconnectTimer else { return }
RunLoop.main.add(timer, forMode: RunLoop.Mode.common) RunLoop.main.add(timer, forMode: RunLoop.Mode.common)
} }
} }
override weak var requestDispatcher: RequestDispatchable! { override weak var requestDispatcher: RequestDispatchable! {
didSet { didSet {
if let _ = requestDispatcher { if let _ = requestDispatcher {
@ -33,26 +33,26 @@ class HeartbeatChannel: CastChannel {
} }
} }
} }
private var delegate: HeartbeatChannelDelegate? { private var delegate: HeartbeatChannelDelegate? {
return requestDispatcher as? HeartbeatChannelDelegate return requestDispatcher as? HeartbeatChannelDelegate
} }
init() { init() {
super.init(namespace: CastNamespace.heartbeat) super.init(namespace: CastNamespace.heartbeat)
} }
override func handleResponse(_ json: JSON, sourceId: String) { override func handleResponse(_ json: JSON, sourceId: String) {
delegate?.channelDidConnect(self) delegate?.channelDidConnect(self)
guard let rawType = json["type"].string else { return } guard let rawType = json["type"].string else { return }
guard let type = CastMessageType(rawValue: rawType) else { guard let type = CastMessageType(rawValue: rawType) else {
print("Unknown type: \(rawType)") print("Unknown type: \(rawType)")
print(json) print(json)
return return
} }
if type == .ping { if type == .ping {
sendPong(to: sourceId) sendPong(to: sourceId)
print("PING from \(sourceId)") print("PING from \(sourceId)")
@ -69,23 +69,23 @@ class HeartbeatChannel: CastChannel {
_ = timer _ = timer
sendPing() sendPing()
} }
@objc private func sendPing() { @objc private func sendPing() {
let request = requestDispatcher.request(withNamespace: namespace, let request = requestDispatcher.request(withNamespace: namespace,
destinationId: CastConstants.transport, destinationId: CastConstants.transport,
payload: [CastJSONPayloadKeys.type: CastMessageType.ping.rawValue]) payload: [CastJSONPayloadKeys.type: CastMessageType.ping.rawValue])
send(request) send(request)
} }
private func sendPong(to destinationId: String) { private func sendPong(to destinationId: String) {
let request = requestDispatcher.request(withNamespace: namespace, let request = requestDispatcher.request(withNamespace: namespace,
destinationId: destinationId, destinationId: destinationId,
payload: [CastJSONPayloadKeys.type: CastMessageType.pong.rawValue]) payload: [CastJSONPayloadKeys.type: CastMessageType.pong.rawValue])
send(request) send(request)
} }
@objc private func handleTimeout() { @objc private func handleTimeout() {
delegate?.channelDidTimeout(self) delegate?.channelDidTimeout(self)
} }

View File

@ -14,47 +14,47 @@ class MediaControlChannel: CastChannel {
private var delegate: MediaControlChannelDelegate? { private var delegate: MediaControlChannelDelegate? {
return requestDispatcher as? MediaControlChannelDelegate return requestDispatcher as? MediaControlChannelDelegate
} }
init() { init() {
super.init(namespace: CastNamespace.media) super.init(namespace: CastNamespace.media)
} }
override func handleResponse(_ json: JSON, sourceId: String) { override func handleResponse(_ json: JSON, sourceId: String) {
guard let rawType = json["type"].string else { return } guard let rawType = json["type"].string else { return }
guard let type = CastMessageType(rawValue: rawType) else { guard let type = CastMessageType(rawValue: rawType) else {
print("Unknown type: \(rawType)") print("Unknown type: \(rawType)")
print(json) print(json)
return return
} }
switch type { switch type {
case .mediaStatus: case .mediaStatus:
guard let status = json["status"].array?.first else { return } guard let status = json["status"].array?.first else { return }
delegate?.channel(self, didReceive: CastMediaStatus(json: status)) delegate?.channel(self, didReceive: CastMediaStatus(json: status))
default: default:
print(rawType) print(rawType)
} }
} }
public func requestMediaStatus(for app: CastApp, completion: ((Result<CastMediaStatus, CastError>) -> Void)? = nil) { public func requestMediaStatus(for app: CastApp, completion: ((Result<CastMediaStatus, CastError>) -> Void)? = nil) {
let payload: [String: Any] = [ let payload: [String: Any] = [
CastJSONPayloadKeys.type: CastMessageType.statusRequest.rawValue, CastJSONPayloadKeys.type: CastMessageType.statusRequest.rawValue,
CastJSONPayloadKeys.sessionId: app.sessionId CastJSONPayloadKeys.sessionId: app.sessionId
] ]
let request = requestDispatcher.request(withNamespace: namespace, let request = requestDispatcher.request(withNamespace: namespace,
destinationId: app.transportId, destinationId: app.transportId,
payload: payload) payload: payload)
if let completion = completion { if let completion = completion {
send(request) { result in send(request) { result in
switch result { switch result {
case .success(let json): case .success(let json):
completion(Result(value: CastMediaStatus(json: json))) completion(Result(value: CastMediaStatus(json: json)))
case .failure(let error): case .failure(let error):
completion(Result(error: error)) completion(Result(error: error))
} }
@ -63,19 +63,19 @@ class MediaControlChannel: CastChannel {
send(request) send(request)
} }
} }
public func sendPause(for app: CastApp, mediaSessionId: Int) { public func sendPause(for app: CastApp, mediaSessionId: Int) {
send(.pause, for: app, mediaSessionId: mediaSessionId) send(.pause, for: app, mediaSessionId: mediaSessionId)
} }
public func sendPlay(for app: CastApp, mediaSessionId: Int) { public func sendPlay(for app: CastApp, mediaSessionId: Int) {
send(.play, for: app, mediaSessionId: mediaSessionId) send(.play, for: app, mediaSessionId: mediaSessionId)
} }
public func sendStop(for app: CastApp, mediaSessionId: Int) { public func sendStop(for app: CastApp, mediaSessionId: Int) {
send(.stop, for: app, mediaSessionId: mediaSessionId) send(.stop, for: app, mediaSessionId: mediaSessionId)
} }
public func sendSeek(to currentTime: Float, for app: CastApp, mediaSessionId: Int) { public func sendSeek(to currentTime: Float, for app: CastApp, mediaSessionId: Int) {
let payload: [String: Any] = [ let payload: [String: Any] = [
CastJSONPayloadKeys.type: CastMessageType.seek.rawValue, CastJSONPayloadKeys.type: CastMessageType.seek.rawValue,
@ -83,32 +83,32 @@ class MediaControlChannel: CastChannel {
CastJSONPayloadKeys.currentTime: currentTime, CastJSONPayloadKeys.currentTime: currentTime,
CastJSONPayloadKeys.mediaSessionId: mediaSessionId CastJSONPayloadKeys.mediaSessionId: mediaSessionId
] ]
let request = requestDispatcher.request(withNamespace: namespace, let request = requestDispatcher.request(withNamespace: namespace,
destinationId: app.transportId, destinationId: app.transportId,
payload: payload) payload: payload)
send(request) send(request)
} }
private func send(_ message: CastMessageType, for app: CastApp, mediaSessionId: Int) { private func send(_ message: CastMessageType, for app: CastApp, mediaSessionId: Int) {
let payload: [String: Any] = [ let payload: [String: Any] = [
CastJSONPayloadKeys.type: message.rawValue, CastJSONPayloadKeys.type: message.rawValue,
CastJSONPayloadKeys.mediaSessionId: mediaSessionId CastJSONPayloadKeys.mediaSessionId: mediaSessionId
] ]
let request = requestDispatcher.request(withNamespace: namespace, let request = requestDispatcher.request(withNamespace: namespace,
destinationId: app.transportId, destinationId: app.transportId,
payload: payload) payload: payload)
send(request) send(request)
} }
public func load(media: CastMedia, with app: CastApp, completion: @escaping (Result<CastMediaStatus, CastError>) -> Void) { public func load(media: CastMedia, with app: CastApp, completion: @escaping (Result<CastMediaStatus, CastError>) -> Void) {
var payload = media.dict var payload = media.dict
payload[CastJSONPayloadKeys.type] = CastMessageType.load.rawValue payload[CastJSONPayloadKeys.type] = CastMessageType.load.rawValue
payload[CastJSONPayloadKeys.sessionId] = app.sessionId payload[CastJSONPayloadKeys.sessionId] = app.sessionId
let request = requestDispatcher.request(withNamespace: namespace, let request = requestDispatcher.request(withNamespace: namespace,
destinationId: app.transportId, destinationId: app.transportId,
payload: payload) payload: payload)
@ -117,9 +117,9 @@ class MediaControlChannel: CastChannel {
switch result { switch result {
case .success(let json): case .success(let json):
guard let status = json["status"].array?.first else { return } guard let status = json["status"].array?.first else { return }
completion(Result(value: CastMediaStatus(json: status))) completion(Result(value: CastMediaStatus(json: status)))
case .failure(let error): case .failure(let error):
completion(Result(error: CastError.load(error.localizedDescription))) completion(Result(error: CastError.load(error.localizedDescription)))
} }

View File

@ -18,15 +18,15 @@ class MultizoneControlChannel: CastChannel {
} }
} }
} }
private var delegate: MultizoneControlChannelDelegate? { private var delegate: MultizoneControlChannelDelegate? {
return requestDispatcher as? MultizoneControlChannelDelegate return requestDispatcher as? MultizoneControlChannelDelegate
} }
init() { init() {
super.init(namespace: CastNamespace.multizone) super.init(namespace: CastNamespace.multizone)
} }
override func handleResponse(_ json: JSON, sourceId: String) { override func handleResponse(_ json: JSON, sourceId: String) {
guard let rawType = json["type"].string else { return } guard let rawType = json["type"].string else { return }
@ -35,40 +35,40 @@ class MultizoneControlChannel: CastChannel {
print(json) print(json)
return return
} }
switch type { switch type {
case .multizoneStatus: case .multizoneStatus:
delegate?.channel(self, didReceive: CastMultizoneStatus(json: json)) delegate?.channel(self, didReceive: CastMultizoneStatus(json: json))
case .deviceAdded: case .deviceAdded:
let device = CastMultizoneDevice(json: json[CastJSONPayloadKeys.device]) let device = CastMultizoneDevice(json: json[CastJSONPayloadKeys.device])
delegate?.channel(self, added: device) delegate?.channel(self, added: device)
case .deviceUpdated: case .deviceUpdated:
let device = CastMultizoneDevice(json: json[CastJSONPayloadKeys.device]) let device = CastMultizoneDevice(json: json[CastJSONPayloadKeys.device])
delegate?.channel(self, updated: device) delegate?.channel(self, updated: device)
case .deviceRemoved: case .deviceRemoved:
guard let deviceId = json[CastJSONPayloadKeys.deviceId].string else { return } guard let deviceId = json[CastJSONPayloadKeys.deviceId].string else { return }
delegate?.channel(self, removed: deviceId) delegate?.channel(self, removed: deviceId)
default: default:
print(rawType) print(rawType)
print(json) print(json)
} }
} }
public func requestStatus(completion: ((Result<CastStatus, CastError>) -> Void)? = nil) { public func requestStatus(completion: ((Result<CastStatus, CastError>) -> Void)? = nil) {
let request = requestDispatcher.request(withNamespace: namespace, let request = requestDispatcher.request(withNamespace: namespace,
destinationId: CastConstants.receiver, destinationId: CastConstants.receiver,
payload: [CastJSONPayloadKeys.type: CastMessageType.statusRequest.rawValue]) payload: [CastJSONPayloadKeys.type: CastMessageType.statusRequest.rawValue])
if let completion = completion { if let completion = completion {
send(request) { result in send(request) { result in
switch result { switch result {
case .success(let json): case .success(let json):
completion(Result(value: CastStatus(json: json))) completion(Result(value: CastStatus(json: json)))
case .failure(let error): case .failure(let error):
completion(Result(error: error)) completion(Result(error: error))
} }
@ -77,32 +77,32 @@ class MultizoneControlChannel: CastChannel {
send(request) send(request)
} }
} }
public func setVolume(_ volume: Float, for device: CastMultizoneDevice) { public func setVolume(_ volume: Float, for device: CastMultizoneDevice) {
let payload: [String: Any] = [ let payload: [String: Any] = [
CastJSONPayloadKeys.type: CastMessageType.setDeviceVolume.rawValue, CastJSONPayloadKeys.type: CastMessageType.setDeviceVolume.rawValue,
CastJSONPayloadKeys.volume: [CastJSONPayloadKeys.level: volume], CastJSONPayloadKeys.volume: [CastJSONPayloadKeys.level: volume],
CastJSONPayloadKeys.deviceId: device.id CastJSONPayloadKeys.deviceId: device.id
] ]
let request = requestDispatcher.request(withNamespace: namespace, let request = requestDispatcher.request(withNamespace: namespace,
destinationId: CastConstants.receiver, destinationId: CastConstants.receiver,
payload: payload) payload: payload)
send(request) send(request)
} }
public func setMuted(_ isMuted: Bool, for device: CastMultizoneDevice) { public func setMuted(_ isMuted: Bool, for device: CastMultizoneDevice) {
let payload: [String: Any] = [ let payload: [String: Any] = [
CastJSONPayloadKeys.type: CastMessageType.setVolume.rawValue, CastJSONPayloadKeys.type: CastMessageType.setVolume.rawValue,
CastJSONPayloadKeys.volume: [CastJSONPayloadKeys.muted: isMuted], CastJSONPayloadKeys.volume: [CastJSONPayloadKeys.muted: isMuted],
CastJSONPayloadKeys.deviceId: device.id CastJSONPayloadKeys.deviceId: device.id
] ]
let request = requestDispatcher.request(withNamespace: namespace, let request = requestDispatcher.request(withNamespace: namespace,
destinationId: CastConstants.receiver, destinationId: CastConstants.receiver,
payload: payload) payload: payload)
send(request) send(request)
} }
} }

View File

@ -18,43 +18,43 @@ class ReceiverControlChannel: CastChannel {
} }
} }
} }
private var delegate: ReceiverControlChannelDelegate? { private var delegate: ReceiverControlChannelDelegate? {
return requestDispatcher as? ReceiverControlChannelDelegate return requestDispatcher as? ReceiverControlChannelDelegate
} }
init() { init() {
super.init(namespace: CastNamespace.receiver) super.init(namespace: CastNamespace.receiver)
} }
override func handleResponse(_ json: JSON, sourceId: String) { override func handleResponse(_ json: JSON, sourceId: String) {
guard let rawType = json["type"].string else { return } guard let rawType = json["type"].string else { return }
guard let type = CastMessageType(rawValue: rawType) else { guard let type = CastMessageType(rawValue: rawType) else {
print("Unknown type: \(rawType)") print("Unknown type: \(rawType)")
print(json) print(json)
return return
} }
switch type { switch type {
case .status: case .status:
delegate?.channel(self, didReceive: CastStatus(json: json)) delegate?.channel(self, didReceive: CastStatus(json: json))
default: default:
print(rawType) print(rawType)
} }
} }
public func getAppAvailability(apps: [CastApp], completion: @escaping (Result<AppAvailability, CastError>) -> Void) { public func getAppAvailability(apps: [CastApp], completion: @escaping (Result<AppAvailability, CastError>) -> Void) {
let payload: [String: Any] = [ let payload: [String: Any] = [
CastJSONPayloadKeys.type: CastMessageType.availableApps.rawValue, CastJSONPayloadKeys.type: CastMessageType.availableApps.rawValue,
CastJSONPayloadKeys.appId: apps.map { $0.id } CastJSONPayloadKeys.appId: apps.map { $0.id }
] ]
let request = requestDispatcher.request(withNamespace: namespace, let request = requestDispatcher.request(withNamespace: namespace,
destinationId: CastConstants.receiver, destinationId: CastConstants.receiver,
payload: payload) payload: payload)
send(request) { result in send(request) { result in
switch result { switch result {
case .success(let json): case .success(let json):
@ -64,18 +64,18 @@ class ReceiverControlChannel: CastChannel {
} }
} }
} }
public func requestStatus(completion: ((Result<CastStatus, CastError>) -> Void)? = nil) { public func requestStatus(completion: ((Result<CastStatus, CastError>) -> Void)? = nil) {
let request = requestDispatcher.request(withNamespace: namespace, let request = requestDispatcher.request(withNamespace: namespace,
destinationId: CastConstants.receiver, destinationId: CastConstants.receiver,
payload: [CastJSONPayloadKeys.type: CastMessageType.statusRequest.rawValue]) payload: [CastJSONPayloadKeys.type: CastMessageType.statusRequest.rawValue])
if let completion = completion { if let completion = completion {
send(request) { result in send(request) { result in
switch result { switch result {
case .success(let json): case .success(let json):
completion(Result(value: CastStatus(json: json))) completion(Result(value: CastStatus(json: json)))
case .failure(let error): case .failure(let error):
completion(Result(error: error)) completion(Result(error: error))
} }
@ -84,17 +84,17 @@ class ReceiverControlChannel: CastChannel {
send(request) send(request)
} }
} }
func launch(appId: String, completion: @escaping (Result<CastApp, CastError>) -> Void) { func launch(appId: String, completion: @escaping (Result<CastApp, CastError>) -> Void) {
let payload: [String: Any] = [ let payload: [String: Any] = [
CastJSONPayloadKeys.type: CastMessageType.launch.rawValue, CastJSONPayloadKeys.type: CastMessageType.launch.rawValue,
CastJSONPayloadKeys.appId: appId CastJSONPayloadKeys.appId: appId
] ]
let request = requestDispatcher.request(withNamespace: namespace, let request = requestDispatcher.request(withNamespace: namespace,
destinationId: CastConstants.receiver, destinationId: CastConstants.receiver,
payload: payload) payload: payload)
send(request) { result in send(request) { result in
switch result { switch result {
case .success(let json): case .success(let json):
@ -102,52 +102,52 @@ class ReceiverControlChannel: CastChannel {
completion(Result(error: CastError.launch("Unable to get launched app instance"))) completion(Result(error: CastError.launch("Unable to get launched app instance")))
return return
} }
completion(Result(value: app)) completion(Result(value: app))
case .failure(let error): case .failure(let error):
completion(Result(error: error)) completion(Result(error: error))
} }
} }
} }
public func stop(app: CastApp) { public func stop(app: CastApp) {
let payload: [String: Any] = [ let payload: [String: Any] = [
CastJSONPayloadKeys.type: CastMessageType.stop.rawValue, CastJSONPayloadKeys.type: CastMessageType.stop.rawValue,
CastJSONPayloadKeys.sessionId: app.sessionId CastJSONPayloadKeys.sessionId: app.sessionId
] ]
let request = requestDispatcher.request(withNamespace: namespace, let request = requestDispatcher.request(withNamespace: namespace,
destinationId: CastConstants.receiver, destinationId: CastConstants.receiver,
payload: payload) payload: payload)
send(request) send(request)
} }
public func setVolume(_ volume: Float) { public func setVolume(_ volume: Float) {
let payload: [String: Any] = [ let payload: [String: Any] = [
CastJSONPayloadKeys.type: CastMessageType.setVolume.rawValue, CastJSONPayloadKeys.type: CastMessageType.setVolume.rawValue,
CastJSONPayloadKeys.volume: [CastJSONPayloadKeys.level: volume] CastJSONPayloadKeys.volume: [CastJSONPayloadKeys.level: volume]
] ]
let request = requestDispatcher.request(withNamespace: namespace, let request = requestDispatcher.request(withNamespace: namespace,
destinationId: CastConstants.receiver, destinationId: CastConstants.receiver,
payload: payload) payload: payload)
send(request) send(request)
} }
public func setMuted(_ isMuted: Bool) { public func setMuted(_ isMuted: Bool) {
let payload: [String: Any] = [ let payload: [String: Any] = [
CastJSONPayloadKeys.type: CastMessageType.setVolume.rawValue, CastJSONPayloadKeys.type: CastMessageType.setVolume.rawValue,
CastJSONPayloadKeys.volume: [CastJSONPayloadKeys.muted: isMuted] CastJSONPayloadKeys.volume: [CastJSONPayloadKeys.muted: isMuted]
] ]
let request = requestDispatcher.request(withNamespace: namespace, let request = requestDispatcher.request(withNamespace: namespace,
destinationId: CastConstants.receiver, destinationId: CastConstants.receiver,
payload: payload) payload: payload)
send(request) send(request)
} }
} }

View File

@ -10,10 +10,10 @@ import Foundation
protocol RequestDispatchable: AnyObject { protocol RequestDispatchable: AnyObject {
func nextRequestId() -> Int func nextRequestId() -> Int
func request(withNamespace namespace: String, destinationId: String, payload: [String: Any]) -> CastRequest func request(withNamespace namespace: String, destinationId: String, payload: [String: Any]) -> CastRequest
func request(withNamespace namespace: String, destinationId: String, payload: Data) -> CastRequest func request(withNamespace namespace: String, destinationId: String, payload: Data) -> CastRequest
func send(_ request: CastRequest, response: CastResponseHandler?) func send(_ request: CastRequest, response: CastResponseHandler?)
} }
@ -22,13 +22,13 @@ extension RequestDispatchable {
var payload = payload var payload = payload
let requestId = nextRequestId() let requestId = nextRequestId()
payload[CastJSONPayloadKeys.requestId] = requestId payload[CastJSONPayloadKeys.requestId] = requestId
return CastRequest(id: requestId, return CastRequest(id: requestId,
namespace: namespace, namespace: namespace,
destinationId: destinationId, destinationId: destinationId,
payload: payload) payload: payload)
} }
func request(withNamespace namespace: String, destinationId: String, payload: Data) -> CastRequest { func request(withNamespace namespace: String, destinationId: String, payload: Data) -> CastRequest {
return CastRequest(id: nextRequestId(), return CastRequest(id: nextRequestId(),
namespace: namespace, namespace: namespace,

View File

@ -263,8 +263,6 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
override func remoteControlReceived(with event: UIEvent?) { override func remoteControlReceived(with event: UIEvent?) {
dump(event) dump(event)
} }
// MARK: viewDidLoad // MARK: viewDidLoad
override func viewDidLoad() { override func viewDidLoad() {
@ -273,9 +271,9 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
} else { } else {
titleLabel.text = "S\(String(manifest.parentIndexNumber ?? 0)):E\(String(manifest.indexNumber ?? 0))\(manifest.name ?? "")" titleLabel.text = "S\(String(manifest.parentIndexNumber ?? 0)):E\(String(manifest.indexNumber ?? 0))\(manifest.name ?? "")"
} }
super.viewDidLoad() super.viewDidLoad()
if(!UIDevice.current.orientation.isLandscape) { if !UIDevice.current.orientation.isLandscape {
let value = UIInterfaceOrientation.landscapeLeft.rawValue let value = UIInterfaceOrientation.landscapeLeft.rawValue
UIDevice.current.setValue(value, forKey: "orientation") UIDevice.current.setValue(value, forKey: "orientation")
} }
@ -415,9 +413,9 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
selectedAudioTrack = audioTrackArray[0].id selectedAudioTrack = audioTrackArray[0].id
} }
} }
print("gotToEnd") print("gotToEnd")
self.sendPlayReport() self.sendPlayReport()
playbackItem = item playbackItem = item
} }
@ -436,8 +434,8 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
// Pause and load captions into memory. // Pause and load captions into memory.
mediaPlayer.pause() mediaPlayer.pause()
var shouldHaveSubtitleTracks = 0; var shouldHaveSubtitleTracks = 0
subtitleTrackArray.forEach { sub in subtitleTrackArray.forEach { sub in
if sub.id != -1 && sub.delivery == .external && sub.codec != "subrip" { if sub.id != -1 && sub.delivery == .external && sub.codec != "subrip" {
shouldHaveSubtitleTracks = shouldHaveSubtitleTracks + 1 shouldHaveSubtitleTracks = shouldHaveSubtitleTracks + 1
@ -579,9 +577,9 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
func sendPlayReport() { func sendPlayReport() {
startTime = Int(Date().timeIntervalSince1970) * 10000000 startTime = Int(Date().timeIntervalSince1970) * 10000000
print("sending play report!") 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") 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) PlaystateAPI.reportPlaybackStart(playbackStartInfo: startInfo)