jellyflood/JellyfinPlayer/OpenCastSwift/Networking/CastClient.swift

627 lines
18 KiB
Swift

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