import Flutter import UIKit import GoogleCast public class SwiftFlutterCastFrameworkPlugin: NSObject, FlutterPlugin, GCKSessionManagerListener, CastHostApi, GCKRemoteMediaClientListener, GCKMediaQueueDelegate { public static func register(with registrar: FlutterPluginRegistrar) { let messenger : FlutterBinaryMessenger = registrar.messenger() let flutterApi = CastFlutterApi.init(binaryMessenger: messenger) let instance = SwiftFlutterCastFrameworkPlugin(flutterApi: flutterApi) let channel = FlutterMethodChannel(name: "flutter_cast_framework_dummy_channel", binaryMessenger: messenger) registrar.addMethodCallDelegate(instance, channel: channel) let api : CastHostApi & NSObjectProtocol = instance CastHostApiSetup(messenger, api) } private let castContext: GCKCastContext private var castStateObserver: NSKeyValueObservation? private let flutterApi : CastFlutterApi private var progressTimer: Timer? private let sessionManager: GCKSessionManager private var castingChannels: Dictionary = [:] var _castSession: GCKCastSession? var castSession: GCKCastSession? { get { return _castSession } set { debugPrint("Updating castSession - castSession changed: \(_castSession != newValue)") let oldSession = _castSession let newSession = newValue _castSession = newValue remoteMediaClient = newValue?.remoteMediaClient flutterApi.getSessionMessageNamespaces { (namespaces, err) in debugPrint("Updating castSession - getSessionMessageNamespaces success - param: \(namespaces.joined(separator: ", "))") if (oldSession == nil && newSession == nil) { return // nothing to do here } if (namespaces.count == 0) { return // nothing to do here } // removing castingChannels from old session if (oldSession != nil && self.castingChannels.count != 0) { self.castingChannels.values.forEach { (castingChannel) in oldSession?.remove(castingChannel) } } namespaces.forEach({ (namespace) in let castingChannel = MessageCastingChannel.init(namespace: namespace, flutterApi: self.flutterApi) self.castingChannels[namespace] = castingChannel newSession?.add(castingChannel) }) } } } var _remoteMediaClient: GCKRemoteMediaClient? var remoteMediaClient: GCKRemoteMediaClient? { get { return _remoteMediaClient } set { debugPrint("Updating remoteMediaClient - remoteMediaClient changed: \(_remoteMediaClient != newValue)") _remoteMediaClient?.remove(self) newValue?.add(self) _remoteMediaClient = newValue mediaQueue = newValue?.mediaQueue } } var _mediaQueue: GCKMediaQueue? var mediaQueue: GCKMediaQueue? { get { return _mediaQueue } set { debugPrint("Updating mediaQueue - mediaQueue changed: \(_mediaQueue != newValue)") _mediaQueue?.remove(self) newValue?.add(self) _mediaQueue = newValue } } init(flutterApi : CastFlutterApi) { self.castContext = GCKCastContext.sharedInstance() self.sessionManager = GCKCastContext.sharedInstance().sessionManager self.flutterApi = flutterApi super.init() self.castSession = GCKCastContext.sharedInstance().sessionManager.currentCastSession self.castStateObserver = GCKCastContext.sharedInstance().observe(\.castState, changeHandler: onCastStateChanged) let notificationCenter = NotificationCenter.default let app = UIApplication.shared notificationCenter.addObserver(self, selector: #selector(appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: app) notificationCenter.addObserver(self, selector: #selector(appWillResignActive), name: UIApplication.willResignActiveNotification, object: app) //notificationCenter.addObserver(self, selector: #selector(appDidEnterBackground), name: NSNotification.Name.UIApplicationDidEnterBackground, object: app) //notificationCenter.addObserver(self, selector: #selector(appWillEnterForeground), name: NSNotification.Name.UIApplicationWillEnterForeground, object: app) //notificationCenter.addObserver(self, selector: #selector(appWillTerminate), name: NSNotification.Name.UIApplicationWillTerminate, object: app) } @objc func appDidBecomeActive() { debugPrint("AppLife: appDidBecomeActive - App moved to foreground!") self.sessionManager.add(self) self.castSession = self.sessionManager.currentCastSession notifyCastState(castState: GCKCastContext.sharedInstance().castState) } @objc func appWillResignActive() { debugPrint("AppLife: appWillResignActive - App moved to background!") self.sessionManager.remove(self) self.castSession = nil } private func onCastStateChanged(state: GCKCastContext, change: NSKeyValueObservedChange) { let castState = GCKCastContext.sharedInstance().castState debugPrint("cast state change to: \(castState.rawValue)") notifyCastState(castState: castState) } private func notifyCastState(castState: GCKCastState) { let castStateRaw = castState.rawValue // Android CastStates are 1-to-4, while iOS CastStates are 0-to-3. I align iOS to Android by adding 1 let castStateRawAdjusted = castStateRaw + 1 self.flutterApi.onCastStateChangedCastState(NSNumber(value: castStateRawAdjusted)) { (_: Error?) in } } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { print("Method [\(call.method)] is not implemented.") } deinit { castStateObserver?.invalidate() castStateObserver = nil stopProgressTimer() } func startProgressTimer() { if progressTimer != nil { return } if #available(iOS 10.0, *) { debugPrint("ProgressTimer: creating progress timer") let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: onProgressTimerFired) RunLoop.current.add(timer, forMode: .common) progressTimer = timer } else { debugPrint("ProgressTimer: can't create progress timer") } } private func onProgressTimerFired(t: Timer) { let adBreakStatus = remoteMediaClient?.mediaStatus?.adBreakStatus let adBreakId = adBreakStatus?.adBreakID let adBreakClipId = adBreakStatus?.adBreakClipID if (adBreakId?.isEmpty == false || adBreakClipId?.isEmpty == false) { // There is an ad ongoing fireAdBreakProgressUpdate() } else { currentAdBreakClipProgress = -1 currentAdBreakClipId = "" fireMediaProgressUpdate() } } private var currentAdBreakClipProgress = -1 // in seconds private var currentAdBreakClipId = "" private func calculateCurrentAdBreakClipProgress(adBreakClipId: String) -> Int { if currentAdBreakClipProgress < 0 || currentAdBreakClipId != adBreakClipId { // In this case, the ad break clip has just started currentAdBreakClipId = adBreakClipId } currentAdBreakClipProgress += 1 return currentAdBreakClipProgress } private func fireAdBreakProgressUpdate() { let mediaStatus = remoteMediaClient?.mediaStatus let adBreakStatus = mediaStatus?.adBreakStatus if (adBreakStatus == nil) { return } let adBreakId = adBreakStatus?.adBreakID ?? "" let adBreakClipId = adBreakStatus?.adBreakClipID ?? "" let adBreakClipProgressSecs = calculateCurrentAdBreakClipProgress(adBreakClipId: adBreakClipId) let whenSkippableSecs = adBreakStatus?.whenSkippable ?? 0 let adBreakClip = mediaStatus?.mediaInformation?.adBreakClips?.first(where: { (ad:GCKAdBreakClipInfo) -> Bool in ad.adBreakClipID == adBreakClipId }) if (adBreakClip == nil) { return } let adBreakClipDurationSecs = adBreakClip?.duration ?? 0 let adBreakClipProgressMs = adBreakClipProgressSecs * 1000 let whenSkippableMs = Int(whenSkippableSecs * 1000) let adBreakClipDurationMs = Int(adBreakClipDurationSecs * 1000) let nsAdBreakClipProgress = NSNumber(value: adBreakClipProgressMs) let nsWhenSkippable = NSNumber(value: whenSkippableMs) let nsAdBreakClipDuration = NSNumber(value: adBreakClipDurationMs) DispatchQueue.main.async { self.flutterApi.onAdBreakClipProgressUpdatedAdBreakId(adBreakId, adBreakClipId: adBreakClipId, progressMs: nsAdBreakClipProgress, durationMs: nsAdBreakClipDuration, whenSkippableMs: nsWhenSkippable) { (_:Error?) in } } } private func fireMediaProgressUpdate() { let durationSecs = remoteMediaClient?.mediaStatus?.mediaInformation?.streamDuration ?? 0 let progressInterval = remoteMediaClient?.approximateStreamPosition() ?? 0 let durationMs = Int(durationSecs * 1000) let progressMs = Int(progressInterval * 1000) let nsDuration = NSNumber(value: durationMs) let nsProgress = NSNumber(value: progressMs) DispatchQueue.main.async { self.flutterApi.onProgressUpdatedProgressMs(nsProgress, durationMs: nsDuration) { (_:Error?) in } } } func stopProgressTimer() { debugPrint("ProgressTimer: stopping progress timer") progressTimer?.invalidate() progressTimer = nil } public func sendMessageMessage(_ message: CastMessage, error: AutoreleasingUnsafeMutablePointer) { MessageCastingChannel.sendMessage(allCastingChannels: self.castingChannels, castMessage: message) } public func showCastDialogWithError(_ error: AutoreleasingUnsafeMutablePointer) { castContext.presentCastDialog() } public func loadMediaLoadRequestDataRequest(_ request: MediaLoadRequestData, error: AutoreleasingUnsafeMutablePointer) { let remoteMediaClient = castSession?.remoteMediaClient if remoteMediaClient == nil { return } let mediaLoadRequest = getMediaLoadRequest(request: request) remoteMediaClient?.loadMedia(with: mediaLoadRequest) } public func getMediaInfoWithError(_ error: AutoreleasingUnsafeMutablePointer) -> MediaInfo? { let hostMediaInfo = remoteMediaClient?.mediaStatus?.mediaInformation if (hostMediaInfo == nil) { return MediaInfo() } return getFlutterMediaInfo(mediaInfo: hostMediaInfo) } public func playWithError(_ error: AutoreleasingUnsafeMutablePointer) { remoteMediaClient?.play() } public func pauseWithError(_ error: AutoreleasingUnsafeMutablePointer) { remoteMediaClient?.pause() } public func stopWithError(_ error: AutoreleasingUnsafeMutablePointer) { remoteMediaClient?.stop() } public func setMuteMuted(_ muted: NSNumber, error: AutoreleasingUnsafeMutablePointer) { if (castSession == nil) { return } let isMuted = muted == 1 castSession?.setDeviceMuted(isMuted) } public func getCastDeviceWithError(_ error: AutoreleasingUnsafeMutablePointer) -> CastDevice? { let castDevice = castSession?.device if (castDevice == nil) { return CastDevice() } let result = CastDevice() result.deviceId = castDevice?.deviceID result.friendlyName = castDevice?.friendlyName result.modelName = castDevice?.modelName return result } public func showTracksChooserDialogWithError(_ error: AutoreleasingUnsafeMutablePointer) { // let dialog = GCKUIMediaTrackSelectionViewController.init() // let rootViewController = UIApplication.shared.keyWindow?.rootViewController // // if (rootViewController is UINavigationController) { // debugPrint("showTracksChooserDialog: rootViewController is UINavigationController") // (rootViewController as! UINavigationController).pushViewController(dialog,animated:true) // } else if rootViewController != nil { // debugPrint("showTracksChooserDialog: rootViewController is UINavigationController") // let navigationController = UINavigationController(rootViewController:dialog) // rootViewController?.present(navigationController, animated:true, completion:nil) // } // else { // debugPrint("showTracksChooserDialog: missing rootViewController") // } // TODO: implement this feature print("showTracksChooserDialog: unsupported feature") } public func skipAdWithError(_ error: AutoreleasingUnsafeMutablePointer) { remoteMediaClient?.skipAd() } public func queueAppendItemItem(_ item: MediaQueueItem, error: AutoreleasingUnsafeMutablePointer) { let remoteMediaClient = castSession?.remoteMediaClient if remoteMediaClient == nil { return } let mediaQueueItem = getMediaQueueItem(item: item) // as per documentation: If beforeItemWithID is kGCKMediaQueueInvalidItemID, the inserted item will be appended to the end of the queue remoteMediaClient?.queueInsert(mediaQueueItem, beforeItemWithID: kGCKMediaQueueInvalidItemID) } public func queueNextItemWithError(_ error: AutoreleasingUnsafeMutablePointer) { let remoteMediaClient = castSession?.remoteMediaClient remoteMediaClient?.queueNextItem() } public func queuePrevItemWithError(_ error: AutoreleasingUnsafeMutablePointer) { let remoteMediaClient = castSession?.remoteMediaClient remoteMediaClient?.queuePreviousItem() } public func getQueueItemCountWithError(_ error: AutoreleasingUnsafeMutablePointer) -> NSNumber? { if (mediaQueue == nil) { return -1 } let itemCount = mediaQueue?.itemCount ?? 0 return NSNumber(value: itemCount) } public func getQueueItemAtIndexIndex(_ index: NSNumber, error: AutoreleasingUnsafeMutablePointer) -> MediaQueueItem? { if (index.intValue < 0) { return getFlutterMediaQueueItem(item: nil) } let mediaQueueItem = mediaQueue?.item(at: index.uintValue, fetchIfNeeded: true) return getFlutterMediaQueueItem(item: mediaQueueItem) } // MARK: - GCKSessionManagerListener // onSessionSuspended public func sessionManager(_ sessionManager: GCKSessionManager, didSuspend session: GCKCastSession, with reason: GCKConnectionSuspendReason) { debugPrint("SessionListener: didSuspend") flutterApi.onSessionSuspended { (_:Error?) in } } // onSessionStarting public func sessionManager(_ sessionManager: GCKSessionManager, willStart session: GCKCastSession) { debugPrint("SessionListener: willStart") flutterApi.onSessionStarting { (_:Error?) in } castSession = session } // onSessionResuming public func sessionManager(_ sessionManager: GCKSessionManager, willResumeCastSession session: GCKCastSession) { debugPrint("SessionListener: willResumeCastSession") flutterApi.onSessionResuming { (_:Error?) in } castSession = session } // onSessionEnding public func sessionManager(_ sessionManager: GCKSessionManager, willEnd session: GCKCastSession) { debugPrint("SessionListener: willEnd") stopProgressTimer() flutterApi.onSessionEnding { (_:Error?) in } } // onSessionStartFailed public func sessionManager(_ sessionManager: GCKSessionManager, didFailToStart session: GCKCastSession, withError error: Error) { debugPrint("SessionListener: didFailToStart") flutterApi.onSessionStartFailed { (_:Error?) in } } // onSessionResumeFailed - Can't find this on iOS // onSessionStarted public func sessionManager(_ sessionManager: GCKSessionManager, didStart session: GCKCastSession) { debugPrint("SessionListener: didStart") flutterApi.onSessionStarted { (_:Error?) in } castSession = session } // onSessionResumed public func sessionManager(_ sessionManager: GCKSessionManager, didResumeCastSession session: GCKCastSession) { debugPrint("SessionListener: didResumeCastSession") flutterApi.onSessionResumed { (_:Error?) in } castSession = session } // onSessionEnded public func sessionManager(_ sessionManager: GCKSessionManager, didEnd session: GCKCastSession, withError error: Error?) { debugPrint("SessionListener: didEnd") flutterApi.onSessionEnded { (_:Error?) in } } // onQueueStatusUpdated public func remoteMediaClientDidUpdateQueue(_ client: GCKRemoteMediaClient) { debugPrint("RemoteMediaClientListener: didUpdateQueue") flutterApi.onQueueStatusUpdated { (_:Error?) in } } // onPreloadStatusUpdated public func remoteMediaClientDidUpdatePreloadStatus(_ client: GCKRemoteMediaClient) { debugPrint("RemoteMediaClientListener: didUpdatePreloadStatus") flutterApi.onPreloadStatusUpdated { (_:Error?) in } } // onStatusUpdated public func remoteMediaClient(_ client: GCKRemoteMediaClient, didUpdate mediaStatus: GCKMediaStatus?) { var playerStateLabel = "" switch mediaStatus?.playerState { case .unknown: playerStateLabel = "PlayerStateUnknown" case .idle: playerStateLabel = "PlayerStateIdle" case .playing: playerStateLabel = "PlayerStatePlaying" startProgressTimer() case .paused: playerStateLabel = "PlayerStatePaused" stopProgressTimer() case .buffering: playerStateLabel = "PlayerStateBuffering" stopProgressTimer() case .loading: playerStateLabel = "PlayerStateLoading" startProgressTimer() default: break } debugPrint("RemoteMediaClientListener: didUpdate mediaStatus - playerState: \(playerStateLabel)") let flutterMediaStatus = getFlutterMediaStatus(mediaStatus: mediaStatus) flutterApi.onStatusUpdatedMediaStatus(flutterMediaStatus) { (_:Error?) in } flutterApi.onAdBreakStatusUpdatedMediaStatus(flutterMediaStatus) { (_:Error?) in } } // onAdBreakStatusUpdated - Can't find this on iOS // onMediaError - Can't find this on iOS // onMetadataUpdated public func remoteMediaClient(_ client: GCKRemoteMediaClient, didUpdate mediaMetadata: GCKMediaMetadata?) { debugPrint("RemoteMediaClientListener: didUpdate mediaMetadata") flutterApi.onMetadataUpdated { (_:Error?) in } } // onQueueStatusUpdated public func remoteMediaClient(_ client: GCKRemoteMediaClient, didReceive queueItems: [GCKMediaQueueItem]) { debugPrint("RemoteMediaClientListener: didReceive queueItems") flutterApi.onQueueStatusUpdated { (_:Error?) in } } // onSendingRemoteMediaRequest public func remoteMediaClient(_ client: GCKRemoteMediaClient, didStartMediaSessionWithID sessionID: Int) { debugPrint("RemoteMediaClientListener: didStartMediaSessionWithID") flutterApi.onSendingRemoteMediaRequest { (_:Error?) in } } // onQueueStatusUpdated public func remoteMediaClient(_ client: GCKRemoteMediaClient, didReceiveQueueItemIDs queueItemIDs: [NSNumber]) { debugPrint("RemoteMediaClientListener: didReceiveQueueItemIDs") flutterApi.onQueueStatusUpdated { (_:Error?) in } } // onQueueStatusUpdated public func remoteMediaClient(_ client: GCKRemoteMediaClient, didUpdateQueueItemsWithIDs queueItemIDs: [NSNumber]) { debugPrint("RemoteMediaClientListener: didUpdateQueueItemsWithIDs") flutterApi.onQueueStatusUpdated { (_:Error?) in } } // onQueueStatusUpdated public func remoteMediaClient(_ client: GCKRemoteMediaClient, didRemoveQueueItemsWithIDs queueItemIDs: [NSNumber]) { debugPrint("RemoteMediaClientListener: didRemoveQueueItemsWithIDs") flutterApi.onQueueStatusUpdated { (_:Error?) in } } // onQueueStatusUpdated public func remoteMediaClient(_ client: GCKRemoteMediaClient, didInsertQueueItemsWithIDs queueItemIDs: [NSNumber], beforeItemWithID beforeItemID: UInt) { debugPrint("RemoteMediaClientListener: didInsertQueueItemsWithIDs") flutterApi.onQueueStatusUpdated { (_:Error?) in } } // mediaQueueWillChange public func mediaQueueWillChange(_ queue: GCKMediaQueue) { debugPrint("MediaQueueListener: mediaQueueWillChange") flutterApi.mediaQueueWillChange { (_:Error?) in } } // mediaQueueChanged public func mediaQueueDidChange(_ queue: GCKMediaQueue) { debugPrint("MediaQueueListener: mediaQueueDidChange") flutterApi.mediaQueueChanged { (_:Error?) in } } // itemsReloaded public func mediaQueueDidReloadItems(_ queue: GCKMediaQueue) { debugPrint("MediaQueueListener: mediaQueueDidReloadItems") flutterApi.itemsReloaded { (_:Error?) in } } // itemsInsertedInRange public func mediaQueue(_ queue: GCKMediaQueue, didInsertItemsIn range: NSRange) { debugPrint("MediaQueueListener: mediaQueueDidInsertItemsIn") let count = range.upperBound - range.lowerBound let start = range.lowerBound flutterApi.itemsInserted(inRangeInsertIndex: NSNumber(value: start), insertCount: NSNumber(value: count)) { (_:Error?) in } } // itemsUpdatedAtIndexes public func mediaQueue(_ queue: GCKMediaQueue, didUpdateItemsAtIndexes indexes: [NSNumber]) { debugPrint("MediaQueueListener: mediaQueueDidUpdateItemsAtIndexes") flutterApi.itemsUpdated(atIndexesIndexes: indexes) { (_:Error?) in } } // itemsRemovedAtIndexes public func mediaQueue(_ queue: GCKMediaQueue, didRemoveItemsAtIndexes indexes: [NSNumber]) { debugPrint("MediaQueueListener: mediaQueueDidRemoveItemsAtIndexes") flutterApi.itemsRemoved(atIndexesIndexes: indexes) { (_:Error?) in } } // itemsReorderedAtIndexes - Can't find on iOS }