diff --git a/RWFramework/RWFramework/Playlist/Asset.swift b/RWFramework/RWFramework/Playlist/Asset.swift index 1c10143..7c8607c 100644 --- a/RWFramework/RWFramework/Playlist/Asset.swift +++ b/RWFramework/RWFramework/Playlist/Asset.swift @@ -9,12 +9,12 @@ import GEOSwift */ public class Asset: Codable { public let id: Int + public let tags: [Int] /// URL pointing to the associated media file, relative to the project server let file: String /// Duration of the asset in seconds let length: Double? let createdDate: Date - public let tags: [Int] let weight: Double let description: String let submitted: Bool? @@ -66,7 +66,7 @@ extension Asset { } } -struct AssetPool: Codable { +public struct AssetPool: Codable { let assets: [Asset] let date: Date } diff --git a/RWFramework/RWFramework/Playlist/AssetFilter.swift b/RWFramework/RWFramework/Playlist/AssetFilter.swift index 4e2080e..ea93a46 100644 --- a/RWFramework/RWFramework/Playlist/AssetFilter.swift +++ b/RWFramework/RWFramework/Playlist/AssetFilter.swift @@ -8,7 +8,7 @@ import GEOSwift Multiple assets with the same priority will be sorted by project-level ordering preferences. */ -enum AssetPriority: Int, CaseIterable { +public enum AssetPriority: Int, CaseIterable { /// Discard the asset always case discard = -1 @@ -21,7 +21,7 @@ enum AssetPriority: Int, CaseIterable { } /// Filter applied to assets as candidates for a specific track -protocol AssetFilter { +public protocol AssetFilter { /// Determines whether the given asset should be played on a particular track. /// - returns: .discard to skip the asset, otherwise rank it func keep(_ asset: Asset, playlist: Playlist, track: AudioTrack) -> AssetPriority diff --git a/RWFramework/RWFramework/Playlist/AudioTrack.swift b/RWFramework/RWFramework/Playlist/AudioTrack.swift index d08bc83..612c5fb 100644 --- a/RWFramework/RWFramework/Playlist/AudioTrack.swift +++ b/RWFramework/RWFramework/Playlist/AudioTrack.swift @@ -29,7 +29,7 @@ public class AudioTrack: Codable { var previousAsset: Asset? = nil var currentAsset: Asset? = nil var state: TrackState? = nil - fileprivate var isPlaying: Bool = false + private(set) var isPlaying: Bool = false let player = AVAudioPlayerNode() @@ -578,4 +578,4 @@ private class WaitingForAsset: TimedTrackState { self.track.fadeInNextAsset() } } -} \ No newline at end of file +} diff --git a/RWFramework/RWFramework/Playlist/Geometry.swift b/RWFramework/RWFramework/Playlist/Geometry.swift index 24bf94b..25522d9 100644 --- a/RWFramework/RWFramework/Playlist/Geometry.swift +++ b/RWFramework/RWFramework/Playlist/Geometry.swift @@ -1,5 +1,6 @@ import CoreLocation import GEOSwift +import AVFoundation struct ShapeData: Codable { let coordinates: [[Double]] @@ -13,6 +14,53 @@ extension CLLocation { func toWaypoint() -> Waypoint { return Waypoint(latitude: coordinate.latitude, longitude: coordinate.longitude)! } + + func bearingToLocationRadian(_ destinationLocation: CLLocation) -> Double { + let lat1 = self.coordinate.latitude.degreesToRadians + let lon1 = self.coordinate.longitude.degreesToRadians + + let lat2 = destinationLocation.coordinate.latitude.degreesToRadians + let lon2 = destinationLocation.coordinate.longitude.degreesToRadians + + let dLon = lon2 - lon1 + + let y = sin(dLon) * cos(lat2) + let x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon) + let radiansBearing = atan2(y, x) + + return radiansBearing + } + + func bearingToLocationDegrees(_ destinationLocation: CLLocation) -> Double { + return bearingToLocationRadian(destinationLocation).radiansToDegrees + } + + func toAudioPoint() -> AVAudio3DPoint { + let coord = self.coordinate + let mult = 1.0 + return AVAudio3DPoint( + x: Float(coord.longitude * mult), + y: 0.0, + z: -Float(coord.latitude * mult) + ) + } + + func toAudioPoint(relativeTo other: CLLocation) -> AVAudio3DPoint { + let latCoord = CLLocation(latitude: self.coordinate.latitude, longitude: other.coordinate.longitude) + let lngCoord = CLLocation(latitude: other.coordinate.latitude, longitude: self.coordinate.longitude) + let latDist = latCoord.distance(from: other) + let lngDist = lngCoord.distance(from: other) + let latDir = (self.coordinate.latitude - other.coordinate.latitude).sign + let latMult = latDir == .plus ? -1.0 : 1.0 + let lngDir = (self.coordinate.longitude - other.coordinate.longitude).sign + let lngMult = lngDir == .plus ? 1.0 : -1.0 + let mult = 0.1 + return AVAudio3DPoint( + x: Float(lngDist * lngMult * mult), + y: 0.0, + z: Float(latDist * latMult * mult) + ) + } } extension Double { diff --git a/RWFramework/RWFramework/Playlist/Playlist.swift b/RWFramework/RWFramework/Playlist/Playlist.swift index dd19943..4eec0d6 100644 --- a/RWFramework/RWFramework/Playlist/Playlist.swift +++ b/RWFramework/RWFramework/Playlist/Playlist.swift @@ -28,17 +28,18 @@ public class Playlist { // assets and filters private var filters: AllAssetFilters private var sortMethods: [SortMethod] + + /// Map asset ID to data like last listen time. + private(set) var userAssetData = [Int: UserAssetData]() + /** Mapping of project id to asset pool, allowing for one app to support loading multiple projects at once. */ private var assetPool: AssetPool? = nil - - /// Map asset ID to data like last listen time. - private(set) var userAssetData = [Int: UserAssetData]() // audio tracks, background and foreground - private(set) var speakers: [Speaker] = [] + public private(set) var speakers: [Speaker] = [] public private(set) var tracks: [AudioTrack] = [] private var demoStream: AVPlayer? = nil @@ -117,7 +118,17 @@ public class Playlist { } } +// Start-up and public API extension Playlist { + // Public API + public var isPlaying: Bool { + return self.tracks.contains { $0.isPlaying } + } + + func lastListenDate(for asset: Asset) -> Date? { + return self.userAssetData[asset.id]?.lastListen + } + /** All assets available in the current active project. */ @@ -128,7 +139,8 @@ extension Playlist { public var currentlyPlayingAssets: [Asset] { return tracks.compactMap { $0.currentAsset } } - + + // Internal private var assetPoolFile: URL? { do { let docsDir = try FileManager.default.url( @@ -144,18 +156,112 @@ extension Playlist { return nil } } - - func apply(filter: AssetFilter) { - self.filters.filters.append(filter) + func start() { + // Starts a session and retrieves project-wide config. + RWFramework.sharedInstance.apiStartForClientMixing().then { project in + self.project = project + print("project settings: \(project)") + self.useProjectDefaults() + self.afterSessionInit() + } + } + + private func useProjectDefaults() { + switch project.ordering { + case "random": + self.sortMethods = [SortRandomly()] + case "by_weight": + self.sortMethods = [SortByWeight()] + case "by_like": + self.sortMethods = [SortByLikes()] + default: break + } + } + + /** + * Retrieve tags to filter by for the current project. + * Setup the speakers for background audio. + * Retrieve the list of all assets and check for new assets every few minutes. + **/ + private func afterSessionInit() { + // Mark start of the session + startTime = Date() + + // Load cached assets first + loadAssetPool() + + // Start playing background music from speakers. + let speakerUpdate = initSpeakers() + + // Retrieve the list of tracks + let trackUpdate = initTracks() + + // Initial grab of assets and speakers. + let assetsUpdate = refreshAssetPool() + + all(speakerUpdate, trackUpdate, assetsUpdate).then { _ in + RWFramework.sharedInstance.rwStartedSuccessfully() + } + + updateTimer = .every(.seconds(project.asset_refresh_interval)) { _ in + self.refreshAssetPool() + } + } + + func pause() { + if isPlaying { + for s in speakers { s.pause() } + for t in tracks { t.pause() } + if demoLooper != nil { + demoStream?.pause() + } + } } - func lastListenDate(for asset: Asset) -> Date? { - return self.userAssetData[asset.id]?.lastListen + func resume() { + if !isPlaying { + for s in speakers { s.resume() } + for t in tracks { t.resume() } + if demoLooper != nil { + demoStream?.play() + } + } } + func skip() { + // Fade out the currently playing assets on all tracks. + for t in tracks { + t.playNext() + } + } + + func replay() { + for t in tracks { + t.replay() + } + } +} + +// Filters functionality +extension Playlist { + public func apply(filter: AssetFilter) { + self.filters.filters.append(filter) + } + + func updateFilterData() -> Promise { + return self.filters.onUpdateAssets(playlist: self) + .recover { err in print(err) } + } + + func passesFilters(_ asset: Asset, forTrack track: AudioTrack) -> Bool { + return self.filters.keep(asset, playlist: self, track: track) != .discard + } +} + +// Speaker-associated functionality +extension Playlist { /// Prepares all the speakers for this project. - @discardableResult private func initSpeakers() -> Promise<[Speaker]> { return RWFramework.sharedInstance.apiGetSpeakers([ "project_id": String(project.id), @@ -223,7 +329,49 @@ extension Playlist { demoStream.pause() } } - +} + +// Track-associated functionality +extension Playlist { + /// Grab the list of `AudioTrack`s for the current project. + private func initTracks() -> Promise { + let rw = RWFramework.sharedInstance + + return rw.apiGetAudioTracks([ + "project_id": String(project.id) + ]).then { data -> () in + print("assets: using " + data.count.description + " tracks") + for it in data { + // TODO: Try to remove playlist dependency. Maybe pass into method? + it.playlist = self + self.audioEngine.attach(it.player) + self.audioEngine.connect( + it.player, + to: self.audioMixer, + format: AVAudioFormat(standardFormatWithSampleRate: 96000, channels: 1) + ) + + } + if !self.audioEngine.isRunning { + try self.audioEngine.start() + } + self.tracks = data + }.catch { err in + print(err) + } + } + + private func updateTrackParams() { + if let params = self.currentParams { + // update all tracks in parallel, in case they need to load a new track + for t in tracks { + Promise(on: .global()) { + t.updateParams(params) + } + } + } + } + /// Picks the next-up asset to play on the given track. /// Applies all the playlist-level and track-level filters to make the decision. func next(forTrack track: AudioTrack) -> Asset? { @@ -258,62 +406,21 @@ extension Playlist { } return next } +} - func recordFinishedPlaying(asset: Asset) { - var playCount = 1 - if let prevEntry = userAssetData[asset.id] { - playCount += prevEntry.playCount - } - - userAssetData.updateValue( - UserAssetData(lastListen: Date(), playCount: playCount), - forKey: asset.id - ) - } +// Asset pool functionality +extension Playlist { + /// Periodically check for newly published assets + func refreshAssetPool() -> Promise { + return self.updateAssets().then { + // Update filtered assets given any newly uploaded assets + self.updateParams() - func passesFilters(_ asset: Asset, forTrack track: AudioTrack) -> Bool { - return self.filters.keep(asset, playlist: self, track: track) != .discard - } - - private func updateTrackParams() { - if let params = self.currentParams { - // update all tracks in parallel, in case they need to load a new track - for t in tracks { - Promise(on: .global()) { - t.updateParams(params) - } - } - } - } - - /// Grab the list of `AudioTrack`s for the current project. - private func initTracks() -> Promise { - let rw = RWFramework.sharedInstance - - return rw.apiGetAudioTracks([ - "project_id": String(project.id) - ]).then { data -> () in - print("assets: using " + data.count.description + " tracks") - for it in data { - // TODO: Try to remove playlist dependency. Maybe pass into method? - it.playlist = self - self.audioEngine.attach(it.player) - self.audioEngine.connect( - it.player, - to: self.audioMixer, - format: AVAudioFormat(standardFormatWithSampleRate: 96000, channels: 1) - ) - - } - if !self.audioEngine.isRunning { - try self.audioEngine.start() - } - self.tracks = data - }.catch { err in - print(err) + let locRequested = RWFramework.sharedInstance.requestWhenInUseAuthorizationForLocation() + print("location requested? \(locRequested)") } } - + /// Retrieve audio assets stored on the server. /// At the start of a session, gets all the assets. /// After that, only adds the assets uploaded since the last call of this function. @@ -376,12 +483,7 @@ extension Playlist { self.saveAssetPool() } } - - func updateFilterData() -> Promise { - return self.filters.onUpdateAssets(playlist: self) - .recover { err in print(err) } - } - + /// Framework should call this when stream parameters are updated. func updateParams(_ opts: StreamParams) { self.currentParams = opts @@ -402,44 +504,20 @@ extension Playlist { private func updateParams() { updateSpeakerVolumes() // TODO: Use a filter to clear data for assets we've moved away from. - // Tell our tracks to play any new assets. self.updateTrackParams() } - - /// Periodically check for newly published assets - internal func refreshAssetPool() -> Promise { - return self.updateAssets().then { - // Update filtered assets given any newly uploaded assets - self.updateParams() - let locRequested = RWFramework.sharedInstance.requestWhenInUseAuthorizationForLocation() - print("location requested? \(locRequested)") + func recordFinishedPlaying(asset: Asset) { + var playCount = 1 + if let prevEntry = userAssetData[asset.id] { + playCount += prevEntry.playCount } - } - - func start() { - RWFramework.sharedInstance.isPlaying = false - // Starts a session and retrieves project-wide config. - RWFramework.sharedInstance.apiStartForClientMixing().then { project in - self.project = project - print("project settings: \(project)") - self.useProjectDefaults() - self.afterSessionInit() - } - } - - private func useProjectDefaults() { - switch project.ordering { - case "random": - self.sortMethods = [SortRandomly()] - case "by_weight": - self.sortMethods = [SortByWeight()] - case "by_like": - self.sortMethods = [SortByLikes()] - default: break - } + userAssetData.updateValue( + UserAssetData(lastListen: Date(), playCount: playCount), + forKey: asset.id + ) } private func saveAssetPool() { @@ -466,120 +544,4 @@ extension Playlist { print(error) } } - - /** - * Retrieve tags to filter by for the current project. - * Setup the speakers for background audio. - * Retrieve the list of all assets and check for new assets every few minutes. - **/ - private func afterSessionInit() { - // Mark start of the session - startTime = Date() - - // Load cached assets first - loadAssetPool() - - // Start playing background music from speakers. - let speakerUpdate = initSpeakers() - - // Retrieve the list of tracks - let trackUpdate = initTracks() - - // Initial grab of assets and speakers. - let assetsUpdate = refreshAssetPool() - - all(speakerUpdate, trackUpdate, assetsUpdate).then { _ in - RWFramework.sharedInstance.rwStartedSuccessfully() - } - - updateTimer = .every(.seconds(project.asset_refresh_interval)) { _ in - self.refreshAssetPool() - } - } - - func pause() { - if RWFramework.sharedInstance.isPlaying { - RWFramework.sharedInstance.isPlaying = false - for s in speakers { s.pause() } - for t in tracks { t.pause() } - if demoLooper != nil { - demoStream?.pause() - } - } - } - - func resume() { - if !RWFramework.sharedInstance.isPlaying { - RWFramework.sharedInstance.isPlaying = true - for s in speakers { s.resume() } - for t in tracks { t.resume() } - if demoLooper != nil { - demoStream?.play() - } - } - } - - func skip() { - // Fade out the currently playing assets on all tracks. - for t in tracks { - t.playNext() - } - } - - func replay() { - for t in tracks { - t.replay() - } - } -} - - -extension CLLocation { - func bearingToLocationRadian(_ destinationLocation: CLLocation) -> Double { - - let lat1 = self.coordinate.latitude.degreesToRadians - let lon1 = self.coordinate.longitude.degreesToRadians - - let lat2 = destinationLocation.coordinate.latitude.degreesToRadians - let lon2 = destinationLocation.coordinate.longitude.degreesToRadians - - let dLon = lon2 - lon1 - - let y = sin(dLon) * cos(lat2) - let x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon) - let radiansBearing = atan2(y, x) - - return radiansBearing - } - - func bearingToLocationDegrees(_ destinationLocation: CLLocation) -> Double { - return bearingToLocationRadian(destinationLocation).radiansToDegrees - } - - func toAudioPoint() -> AVAudio3DPoint { - let coord = self.coordinate - let mult = 1.0 - return AVAudio3DPoint( - x: Float(coord.longitude * mult), - y: 0.0, - z: -Float(coord.latitude * mult) - ) - } - - func toAudioPoint(relativeTo other: CLLocation) -> AVAudio3DPoint { - let latCoord = CLLocation(latitude: self.coordinate.latitude, longitude: other.coordinate.longitude) - let lngCoord = CLLocation(latitude: other.coordinate.latitude, longitude: self.coordinate.longitude) - let latDist = latCoord.distance(from: other) - let lngDist = lngCoord.distance(from: other) - let latDir = (self.coordinate.latitude - other.coordinate.latitude).sign - let latMult = latDir == .plus ? -1.0 : 1.0 - let lngDir = (self.coordinate.longitude - other.coordinate.longitude).sign - let lngMult = lngDir == .plus ? 1.0 : -1.0 - let mult = 0.1 - return AVAudio3DPoint( - x: Float(lngDist * lngMult * mult), - y: 0.0, - z: Float(latDist * latMult * mult) - ) - } } diff --git a/RWFramework/RWFramework/Playlist/TimedAsset.swift b/RWFramework/RWFramework/Playlist/TimedAsset.swift index ca0182b..06b61b6 100644 --- a/RWFramework/RWFramework/Playlist/TimedAsset.swift +++ b/RWFramework/RWFramework/Playlist/TimedAsset.swift @@ -11,7 +11,7 @@ public struct TimedAsset: Codable { public class TimedAssetFilter: AssetFilter { private var timedAssets: [TimedAsset]? = nil - func keep(_ asset: Asset, playlist: Playlist, track: AudioTrack) -> AssetPriority { + public func keep(_ asset: Asset, playlist: Playlist, track: AudioTrack) -> AssetPriority { if track.timedAssetPriority == .discard { return .discard } @@ -34,7 +34,7 @@ public class TimedAssetFilter: AssetFilter { return .discard } - func onUpdateAssets(playlist: Playlist) -> Promise { + public func onUpdateAssets(playlist: Playlist) -> Promise { if timedAssets == nil { return RWFramework.sharedInstance.apiGetTimedAssets([ "project_id": String(playlist.project.id) diff --git a/RWFramework/RWFramework/RWFramework.swift b/RWFramework/RWFramework/RWFramework.swift index cb34954..6f09ab0 100644 --- a/RWFramework/RWFramework/RWFramework.swift +++ b/RWFramework/RWFramework/RWFramework.swift @@ -34,7 +34,13 @@ private lazy var __once: () = { () -> Void in // Location (see RWFrameworkCoreLocation.swift) let locationManager: CLLocationManager = CLLocationManager() var lastRecordedLocation: CLLocation = CLLocation() - var streamOptions = [String: Any]() + var streamOptions = StreamParams( + location: CLLocation(), + minDist: nil, + maxDist: nil, + heading: nil, + angularWidth: nil + ) var letFrameworkRequestWhenInUseAuthorizationForLocation = true public let playlist = Playlist(filters: [ @@ -75,20 +81,6 @@ private lazy var __once: () = { () -> Void in return enc }() - // Audio - Stream (see RWFrameworkAudioPlayer.swift) - var streamURL: URL? = nil - var streamID = 0 - var player: AVPlayer? = nil { - willSet { - self.player?.currentItem?.removeObserver(self, forKeyPath: "timedMetadata") - } - didSet { - self.player?.currentItem?.addObserver(self, forKeyPath: "timedMetadata", options: NSKeyValueObservingOptions.new, context: nil) - } - } - /// True if the player (streamer) is currently playing (streaming) - open var isPlaying = false - // Audio - Record (see RWFrameworkAudioRecorder.swift) /// RWFrameworkAudioRecorder.swift calls code in RWFrameworkAudioRecorder.m to perform recording when true let useComplexRecordingMechanism = false @@ -107,12 +99,10 @@ private lazy var __once: () = { () -> Void in } // Flags - // var postUsersSucceeded = false var postSessionsSucceeded = false - // var getProjectsIdSucceeded = false var getProjectsIdTagsSucceeded = false { didSet { - if getProjectsIdTagsSucceeded && requestStreamSucceeded { + if getProjectsIdTagsSucceeded { timeToSendTheListenTags = true } } @@ -120,15 +110,6 @@ private lazy var __once: () = { () -> Void in var getProjectsIdUIGroupsSucceeded = false var getTagCategoriesSucceeded = false var getUIConfigSucceeded = false - public var requestStreamInProgress = false - public var requestStreamSucceeded = false { - didSet { - if getProjectsIdTagsSucceeded && requestStreamSucceeded { - timeToSendTheListenTags = true - } - } - } - // var timeToSendTheListenTagsOnceToken: Int = 0 var timeToSendTheListenTags = false { didSet { if timeToSendTheListenTags { @@ -138,7 +119,6 @@ private lazy var __once: () = { () -> Void in } // Timers (see RWFrameworkTimers.swift) - var heartbeatTimer: Timer? = nil var audioTimer: Timer? = nil var uploadTimer: Timer? = nil @@ -176,10 +156,6 @@ private lazy var __once: () = { () -> Void in addAudioInterruptionNotification() } - deinit { - self.player?.currentItem?.removeObserver(self, forKeyPath: "timedMetadata") - } - /// Start kicks everything else off - call this to start the framework running. /// - Parameter letFrameworkRequestWhenInUseAuthorizationForLocation: false if the caller would rather call requestWhenInUseAuthorizationForLocation() any time after rwGetProjectsIdSuccess is called. open func start(_ letFrameworkRequestWhenInUseAuthorizationForLocation: Bool = true) { diff --git a/RWFramework/RWFramework/RWFrameworkAPI.swift b/RWFramework/RWFramework/RWFrameworkAPI.swift index 30c7fa0..f7e3f97 100644 --- a/RWFramework/RWFramework/RWFrameworkAPI.swift +++ b/RWFramework/RWFramework/RWFrameworkAPI.swift @@ -11,7 +11,6 @@ import CoreLocation import Promises extension RWFramework { - func apiStartForClientMixing() -> Promise { let device_id = UIDevice().identifierForVendor!.uuidString let client_type = UIDevice().model @@ -21,18 +20,9 @@ extension RWFramework { .then { _ in self.apiPostSessions() } .then { data in try self.setupClientSession(data) } } - - func apiStartUp(_ device_id: String, client_type: String, client_system: String) { - // Create new user for the session and save its info - apiPostUsers(device_id, client_type: client_type, client_system: client_system) - // start a session - .then { _ in self.apiPostSessions() } - // initialize all info associated with a session (UI config, tags, etc.) - .then { data in try self.setupSession(data) } - } /// MARK: POST users - func apiPostUsers(_ device_id: String, client_type: String, client_system: String) -> Promise { + private func apiPostUsers(_ device_id: String, client_type: String, client_system: String) -> Promise { let token = RWFrameworkConfig.getConfigValueAsString("token", group: RWFrameworkConfig.ConfigGroup.client) if (token.lengthOfBytes(using: String.Encoding.utf8) > 0) { @@ -76,7 +66,7 @@ extension RWFramework { /// MARK: POST sessions - func apiPostSessions() -> Promise { + private func apiPostSessions() -> Promise { let project_id = RWFrameworkConfig.getConfigValueAsNumber("project_id") let client_system = clientSystem() let language = preferredLanguage() @@ -135,28 +125,8 @@ extension RWFramework { } } - private func setupSession(_ data: Data) throws { - var session_id : NSNumber = 0 - if let dict = try? RWFramework.decoder.decode(Session.self, from: data) { - if let _session_id = dict.id { - session_id = NSNumber(value: _session_id) - RWFrameworkConfig.setConfigValue("session_id", value: session_id, group: RWFrameworkConfig.ConfigGroup.client) - } // TODO: Handle missing value - } - - let project_id = RWFrameworkConfig.getConfigValueAsNumber("project_id") - - apiGetProjectsId(project_id, session_id: session_id) - .then { data in try self.setupStream(data, project_id: project_id, session_id: session_id) } - .then { self.apiGetProjectsIdTags(project_id, session_id: session_id) } - .then { _ in self.apiGetUIConfig(project_id, session_id: session_id) } - .then { _ in self.apiGetProjectsIdUIGroups(project_id, session_id: session_id) } - .then { _ in self.apiGetTagCategories() } - } - - /// MARK: GET projects id - func apiGetProjectsId(_ project_id: NSNumber, session_id: NSNumber) -> Promise { + private func apiGetProjectsId(_ project_id: NSNumber, session_id: NSNumber) -> Promise { return httpGetProjectsId(project_id, session_id: session_id).then { data -> Data in self.rwGetProjectsIdSuccess(data) return data @@ -166,43 +136,7 @@ extension RWFramework { } } - private func setupStream(_ data: Data, project_id: NSNumber, session_id: NSNumber) throws { - RWFrameworkConfig.setConfigDataAsDictionary(data, key: "project") - - // TODO: where is this going to come from? - func configDisplayStartupMessage() { - let startupMessage = RWFrameworkConfig.getConfigValueAsString("startup_message", group: RWFrameworkConfig.ConfigGroup.notifications) - if (startupMessage.lengthOfBytes(using: String.Encoding.utf8) > 0) { - self.rwUpdateStatus(startupMessage) - } - } - configDisplayStartupMessage() - - if letFrameworkRequestWhenInUseAuthorizationForLocation { - _ = requestWhenInUseAuthorizationForLocation() - } - - let listen_enabled = RWFrameworkConfig.getConfigValueAsBool("listen_enabled") - if (listen_enabled) { - let geo_listen_enabled = RWFrameworkConfig.getConfigValueAsBool("geo_listen_enabled") - if (!geo_listen_enabled) { - apiPostStreams() - } - startHeartbeatTimer() - } - - setupRecording() - - // getProjectsIdSucceeded = true - -// apiGetProjectsIdTags(project_id, session_id: session_id) - - // a simpler alternative to apiGetProjectsIdTags and it's subsequent calls but needs - // to be properly vetted before turning off the more complex calls -// apiGetUIConfig(project_id, session_id: session_id) - } - - func setupRecording() { + private func setupRecording() { let speak_enabled = RWFrameworkConfig.getConfigValueAsBool("speak_enabled") if (speak_enabled) { startAudioTimer() @@ -212,7 +146,7 @@ extension RWFramework { } /// MARK: GET ui config - func apiGetUIConfig(_ project_id: NSNumber, session_id: NSNumber) { + private func apiGetUIConfig(_ project_id: NSNumber, session_id: NSNumber) { httpGetUIConfig(project_id, session_id: session_id).then { data in // Save data to UserDefaults for later access UserDefaults.standard.set(data, forKey: "uiconfig") @@ -227,7 +161,7 @@ extension RWFramework { /// MARK: GET projects id tags - func apiGetProjectsIdTags(_ project_id: NSNumber, session_id: NSNumber) -> Promise { + private func apiGetProjectsIdTags(_ project_id: NSNumber, session_id: NSNumber) -> Promise { return httpGetProjectsIdTags(project_id, session_id: session_id).then { data -> Data in // Save data to UserDefaults for later access UserDefaults.standard.set(data, forKey: "tags") @@ -242,7 +176,7 @@ extension RWFramework { } /// MARK: GET projects id uigroups - func apiGetProjectsIdUIGroups(_ project_id: NSNumber, session_id: NSNumber) -> Promise { + private func apiGetProjectsIdUIGroups(_ project_id: NSNumber, session_id: NSNumber) -> Promise { return httpGetProjectsIdUIGroups(project_id, session_id: session_id).then { data -> Data in // Save data to UserDefaults for later access UserDefaults.standard.set(data, forKey: "ui_groups") @@ -261,7 +195,7 @@ extension RWFramework { } /// MARK: GET tagcategories - func apiGetTagCategories() -> Promise { + private func apiGetTagCategories() -> Promise { return httpGetTagCategories().then { data -> Data in UserDefaults.standard.set(data, forKey: "tagcategories") self.getTagCategoriesSucceeded = true @@ -273,176 +207,6 @@ extension RWFramework { } } - /// MARK: POST streams - public func apiPostStreams(at location: CLLocation? = nil) { - if requestStreamInProgress - || requestStreamSucceeded - || !postSessionsSucceeded { return } - - requestStreamInProgress = true - lastRecordedLocation = locationManager.location! - - let session_id = RWFrameworkConfig.getConfigValueAsNumber("session_id", group: RWFrameworkConfig.ConfigGroup.client) - - var lat: String = "0.1", lng: String = "0.1" - if let loc = location?.coordinate { - lat = doubleToStringWithZeroAsEmptyString(loc.latitude) - lng = doubleToStringWithZeroAsEmptyString(loc.longitude) - } - - httpPostStreams(session_id, latitude: lat, longitude: lng).always { - self.requestStreamInProgress = false - }.then { data -> Data in - try self.postStreamsSuccess(data, session_id: session_id) - self.rwPostStreamsSuccess(data) - return data - }.catch { error in - self.rwPostStreamsFailure(error) - self.apiProcessError(nil, error: error, caller: "apiPostStreams") - } - } - - private func postStreamsSuccess(_ data: Data, session_id: NSNumber) throws { - if let dict = try? RWFramework.decoder.decode(Stream.self, from: data) { - if let stream_url = dict.url { - self.streamURL = URL(string: stream_url)! - if let stream_id = dict.id { - self.streamID = stream_id -// self.createPlayer() - self.requestStreamSucceeded = true - // pause stream on server so that assets aren't added until user is actually listening - apiPostStreamsIdPause() - } - } - - // TODO: can we still expect this here? - func requestStreamDisplayUserMessage(_ userMessage: String?) { - if (userMessage != nil && userMessage!.lengthOfBytes(using: String.Encoding.utf8) > 0) { - self.rwUpdateStatus(userMessage!, title: "Out of Range!") - } - } - requestStreamDisplayUserMessage(dict.userMessage) - } - } - - /// MARK: PATCH streams id - public func apiPatchStreamsIdWithLocation( - _ newLocation: CLLocation, - tagIds: String? = nil, - streamPatchOptions: [String: Any] = [:] - ) { - if (requestStreamSucceeded == false || self.streamID == 0) { return } - - let latitude = doubleToStringWithZeroAsEmptyString(newLocation.coordinate.latitude) - let longitude = doubleToStringWithZeroAsEmptyString(newLocation.coordinate.longitude) - httpPatchStreamsId( - self.streamID.description, - tagIds: tagIds, - latitude: latitude, - longitude: longitude, - streamPatchOptions: streamPatchOptions - ).then { data in - self.rwPatchStreamsIdSuccess(data) - }.catch { error in - self.rwPatchStreamsIdFailure(error) - self.apiProcessError(nil, error: error, caller: "apiPatchStreamsIdWithLocation") - } - } - - func apiPatchStreamsIdWithTags(_ tag_ids: String, streamPatchOptions: [String: Any] = [:]) { - if (requestStreamSucceeded == false) { return } - if (self.streamID == 0) { return } - - httpPatchStreamsId(self.streamID.description, tagIds: tag_ids, streamPatchOptions: streamPatchOptions).then { data in - self.rwPatchStreamsIdSuccess(data) - }.catch { error in - self.rwPatchStreamsIdFailure(error) - self.apiProcessError(nil, error: error, caller: "apiPatchStreamsIdWithTags") - } - } - - /// MARK: POST streams id heartbeat - func apiPostStreamsIdHeartbeat() { - if (requestStreamSucceeded == false) { return } - if (self.streamID == 0) { return } - - httpPostStreamsIdHeartbeat(self.streamID.description).then { data in - self.rwPostStreamsIdHeartbeatSuccess(data) - }.catch { error in - self.rwPostStreamsIdHeartbeatFailure(error) - self.apiProcessError(nil, error: error, caller: "apiPostStreamsIdHeartbeat") - } - } - - /// MARK: POST streams id replay - func apiPostStreamsIdReplay() { - if (requestStreamSucceeded == false) { return } - if (self.streamID == 0) { return } - - httpPostStreamsIdReplay(self.streamID.description).then { data in - self.rwPostStreamsIdReplaySuccess(data) - }.catch { error in - self.rwPostStreamsIdReplayFailure(error) - self.apiProcessError(nil, error: error, caller: "apiPostStreamsIdReplay") - } - } - - /// MARK: POST streams id skip - func apiPostStreamsIdSkip() { - if (requestStreamSucceeded == false) { return } - if (self.streamID == 0) { return } - - httpPostStreamsIdSkip(self.streamID.description).then { data in - self.rwPostStreamsIdSkipSuccess(data) - }.catch { error in - self.rwPostStreamsIdSkipFailure(error) - self.apiProcessError(nil, error: error, caller: "apiPostStreamsIdSkip") - } - } - - - /// MARK: POST streams id pause - func apiPostStreamsIdPause() { - if (requestStreamSucceeded == false) { return } - if (self.streamID == 0) { return } - - httpPostStreamsIdPause(self.streamID.description).then { data in - self.rwPostStreamsIdPauseSuccess(data) - }.catch { error in - self.rwPostStreamsIdPauseFailure(error) - self.apiProcessError(nil, error: error, caller: "apiPostStreamsIdPause") - } - } - - - /// MARK: POST streams id resume - func apiPostStreamsIdResume() { - if (requestStreamSucceeded == false) { return } - if (self.streamID == 0) { return } - - httpPostStreamsIdResume(self.streamID.description).then { data in - self.rwPostStreamsIdResumeSuccess(data) - }.catch { error in - self.rwPostStreamsIdResumeFailure(error) - self.apiProcessError(nil, error: error, caller: "apiPostStreamsIdResume") - } - } - - - /// MARK: GET streams id isactive - func apiGetStreamsIdIsActive() { - if (requestStreamSucceeded == false) { return } - if (self.streamID == 0) { return } - - httpGetStreamsIdIsActive(self.streamID.description).then { data in - self.rwGetStreamsIdIsActiveSuccess(data) - }.catch { error in - self.rwGetStreamsIdIsActiveFailure(error) - self.apiProcessError(nil, error: error, caller: "apiGetStreamsIdIsActive") - } - } - - /// MARK: POST envelopes func apiPostEnvelopes() -> Promise { let session_id = RWFrameworkConfig.getConfigValueAsNumber("session_id", group: RWFrameworkConfig.ConfigGroup.client) @@ -472,7 +236,7 @@ extension RWFramework { } } - func patchEnvelopesSuccess(_ data: Data) { + private func patchEnvelopesSuccess(_ data: Data) { do { let json = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) @@ -495,8 +259,7 @@ extension RWFramework { // Not needed on client - not implementing for now - - public func apiGetAudioTracks(_ dict: [String:String]) -> Promise<[AudioTrack]> { + func apiGetAudioTracks(_ dict: [String:String]) -> Promise<[AudioTrack]> { return httpGetAudioTracks(dict).then { data in try RWFramework.decoder.decode([AudioTrack].self, from: data) }.catch { error in @@ -505,7 +268,7 @@ extension RWFramework { } /// MARK: GET assets PUBLIC - public func apiGetAssets(_ dict: [String:String]) -> Promise<[Asset]> { + func apiGetAssets(_ dict: [String:String]) -> Promise<[Asset]> { return httpGetAssets(dict).then { data -> [Asset] in self.rwGetAssetsSuccess(data) return try RWFramework.decoder.decode([Asset].self, from: data) @@ -514,8 +277,8 @@ extension RWFramework { self.apiProcessError(nil, error: error, caller: "apiGetAssets") } } - - public func apiGetTimedAssets(_ dict: [String:String]) -> Promise<[TimedAsset]> { + + func apiGetTimedAssets(_ dict: [String:String]) -> Promise<[TimedAsset]> { return httpGetTimedAssets(dict).then { data -> [TimedAsset] in return try RWFramework.decoder.decode([TimedAsset].self, from: data) }.catch { error in @@ -582,7 +345,7 @@ extension RWFramework { return httpGetVotesSummary(type: type, projectId: projectId, assetId: assetId) } - public func apiGetSpeakers(_ dict: [String:String]) -> Promise<[Speaker]> { + func apiGetSpeakers(_ dict: [String:String]) -> Promise<[Speaker]> { return httpGetSpeakers(dict).then { data -> [Speaker] in self.rwGetSpeakersSuccess(data) return try RWFramework.decoder.decode([Speaker].self, from: data) diff --git a/RWFramework/RWFramework/RWFrameworkAudioPlayer.swift b/RWFramework/RWFramework/RWFrameworkAudioPlayer.swift index 75b8650..7d0923f 100644 --- a/RWFramework/RWFramework/RWFrameworkAudioPlayer.swift +++ b/RWFramework/RWFramework/RWFrameworkAudioPlayer.swift @@ -10,6 +10,9 @@ import Foundation import AVFoundation extension RWFramework { + public var isPlaying: Bool { + return self.playlist.isPlaying + } /// This is set in the self.player's willSet/didSet open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { @@ -22,42 +25,14 @@ extension RWFramework { return listen_enabled //&& streamURL != nil } - /// Create an AVPlayer to play the stream - func createPlayer() { - if (streamURL == nil) { return } - player = AVPlayer(url: streamURL! as URL) - } - - /// Destroy the AVPlayer - public func destroyPlayer() { - if (player == nil) { return } - player = nil - } - /// Begin playing audio public func play() { -// if (canPlay() == false) { return } self.playlist.resume() -// if (player == nil) { -// createPlayer() -// } -// player?.play() -// isPlaying = (player?.rate == 1.0) -// logToServer("start_listen") -// -// // Tell server to resume asset playback -// apiPostStreamsIdResume() } /// Pause audio public func pause() { -// if (canPlay() == false) { return } self.playlist.pause() -// player?.pause() -// isPlaying = (player?.rate == 1.0) -// -// // Tell server to stop adding assets to stream -// apiPostStreamsIdPause() } /// Stop audio @@ -78,5 +53,4 @@ extension RWFramework { /// Check if stream active public func isActive() { } - } diff --git a/RWFramework/RWFramework/RWFrameworkCoreLocation.swift b/RWFramework/RWFramework/RWFrameworkCoreLocation.swift index 3c3c0ec..e67b186 100644 --- a/RWFramework/RWFramework/RWFrameworkCoreLocation.swift +++ b/RWFramework/RWFramework/RWFrameworkCoreLocation.swift @@ -39,27 +39,17 @@ extension RWFramework: CLLocationManagerDelegate { headingAngle: Double? = nil, angularWidth: Double? = nil ) { - if let r = range { - streamOptions["listener_range_min"] = r.lowerBound - streamOptions["listener_range_max"] = r.upperBound - } - if let a = headingAngle { - streamOptions["listener_heading"] = a - } - if let w = angularWidth { - streamOptions["listener_width"] = w - } if let loc = location { lastRecordedLocation = loc } - - playlist.updateParams(StreamParams( - location: lastRecordedLocation, - minDist: streamOptions["listener_range_min"] as? Double, - maxDist: streamOptions["listener_range_max"] as? Double, - heading: streamOptions["listener_heading"] as? Double, - angularWidth: streamOptions["listener_width"] as? Double - )) + streamOptions = StreamParams( + location: location ?? streamOptions.location, + minDist: range?.lowerBound ?? streamOptions.minDist, + maxDist: range?.upperBound ?? streamOptions.maxDist, + heading: headingAngle ?? streamOptions.heading, + angularWidth: angularWidth ?? streamOptions.angularWidth + ) + playlist.updateParams(streamOptions) } /// Called by the CLLocationManager when location has been updated diff --git a/RWFramework/RWFramework/RWFrameworkHTTP.swift b/RWFramework/RWFramework/RWFrameworkHTTP.swift index 0d9d2c9..d490237 100644 --- a/RWFramework/RWFramework/RWFrameworkHTTP.swift +++ b/RWFramework/RWFramework/RWFrameworkHTTP.swift @@ -61,83 +61,7 @@ extension RWFramework: URLSessionDelegate, URLSessionTaskDelegate, URLSessionDat func httpGetTagCategories() -> Promise { return getData(from: RWFrameworkURLFactory.getTagCategoriesURL()) } - - func httpPostStreams( - _ session_id: NSNumber, - latitude: String = "0.1", - longitude: String = "0.1" - ) -> Promise { - return postData( - to: RWFrameworkURLFactory.postStreamsURL(), - postData: [ - "session_id": session_id, - "latitude": latitude, - "longitude": longitude - ] - ) - } - - func httpPatchStreamsId( - _ stream_id: String, - tagIds: String? = nil, - latitude: String? = nil, - longitude: String? = nil, - streamPatchOptions: [String: Any] = [:] - ) -> Promise { - var postData = [String: Any]() - if let lat = latitude { postData["latitude"] = lat } - if let lng = longitude { postData["longitude"] = lng } - if let ids = tagIds { postData["tag_ids"] = ids } - // append postData with any key/value pairs that exist in optionalParams dictionary; if empty dictionary, append nothing - postData.merge(streamPatchOptions) { (first, _) in first } - - return patchData( - to: RWFrameworkURLFactory.patchStreamsIdURL(stream_id), - postData: postData - ) - } - - func httpPatchStreamsId(_ stream_id: String, tag_ids: String) -> Promise { - return patchData( - to: RWFrameworkURLFactory.patchStreamsIdURL(stream_id), - postData: ["tag_ids": tag_ids] - ) - } - - func httpPostStreamsIdHeartbeat(_ stream_id: String) -> Promise { - return postData( - to: RWFrameworkURLFactory.postStreamsIdHeartbeatURL(stream_id) - ) - } - - func httpPostStreamsIdReplay(_ stream_id: String) -> Promise { - return postData( - to: RWFrameworkURLFactory.postStreamsIdReplayURL(stream_id) - ) - } - func httpPostStreamsIdSkip(_ stream_id: String) -> Promise { - return postData( - to: RWFrameworkURLFactory.postStreamsIdSkipURL(stream_id) - ) - } - - func httpPostStreamsIdPause(_ stream_id: String) -> Promise { - return postData( - to: RWFrameworkURLFactory.postStreamsIdPauseURL(stream_id) - ) - } - - func httpPostStreamsIdResume(_ stream_id: String) -> Promise { - return postData( - to: RWFrameworkURLFactory.postStreamsIdResumeURL(stream_id) - ) - } - - func httpGetStreamsIdIsActive(_ stream_id: String) -> Promise { - return getData(from: RWFrameworkURLFactory.getStreamsIdIsActiveURL(stream_id)) - } - func httpPostEnvelopes(_ session_id: NSNumber) -> Promise { return postData( to: RWFrameworkURLFactory.postEnvelopesURL(), diff --git a/RWFramework/RWFramework/RWFrameworkTimers.swift b/RWFramework/RWFramework/RWFrameworkTimers.swift index 1144850..ea8d1ec 100644 --- a/RWFramework/RWFramework/RWFrameworkTimers.swift +++ b/RWFramework/RWFramework/RWFrameworkTimers.swift @@ -9,26 +9,6 @@ import Foundation extension RWFramework { - -// MARK: - Heartbeat - - @objc func heartbeatTimer(_ timer: Timer) { - if (requestStreamSucceeded == false) { return } - - let geo_listen_enabled = RWFrameworkConfig.getConfigValueAsBool("geo_listen_enabled") - if (!geo_listen_enabled) || - (geo_listen_enabled && lastRecordedLocation.timestamp.timeIntervalSinceNow < -RWFrameworkConfig.getConfigValueAsNumber("gps_idle_interval_in_seconds").doubleValue) { - apiPostStreamsIdHeartbeat() - } - } - - func startHeartbeatTimer() { - DispatchQueue.main.async(execute: { () -> Void in - let gps_idle_interval_in_seconds = RWFrameworkConfig.getConfigValueAsNumber("gps_idle_interval_in_seconds").doubleValue - self.heartbeatTimer = Timer.scheduledTimer(timeInterval: gps_idle_interval_in_seconds, target:self, selector:#selector(RWFramework.heartbeatTimer(_:)), userInfo:nil, repeats:true) - }) - } - // MARK: - Audio @objc func audioTimer(_ timer: Timer) { diff --git a/RWFramework/RWFramework/RWFrameworkURLFactory.swift b/RWFramework/RWFramework/RWFrameworkURLFactory.swift index 2553e81..05475fd 100644 --- a/RWFramework/RWFramework/RWFrameworkURLFactory.swift +++ b/RWFramework/RWFramework/RWFrameworkURLFactory.swift @@ -42,38 +42,6 @@ open class RWFrameworkURLFactory { return "\(api())/projects/\(project_id.stringValue)/uiconfig/?session_id=\(session_id.stringValue)" } - class func postStreamsURL() -> String { - return "\(api())/streams/" - } - - class func patchStreamsIdURL(_ stream_id: String) -> String { - return "\(api())/streams/\(stream_id)/" - } - - class func postStreamsIdHeartbeatURL(_ stream_id: String) -> String { - return "\(api())/streams/\(stream_id)/heartbeat/" - } - - class func postStreamsIdReplayURL(_ stream_id: String) -> String { - return "\(api())/streams/\(stream_id)/replayasset/" - } - - class func postStreamsIdSkipURL(_ stream_id: String) -> String { - return "\(api())/streams/\(stream_id)/skipasset/" - } - - class func postStreamsIdPauseURL(_ stream_id: String) -> String { - return "\(api())/streams/\(stream_id)/pause/" - } - - class func postStreamsIdResumeURL(_ stream_id: String) -> String { - return "\(api())/streams/\(stream_id)/resume/" - } - - class func getStreamsIdIsActiveURL(_ stream_id: String) -> String { - return "\(api())/streams/\(stream_id)/isactive/" - } - class func postEnvelopesURL() -> String { return "\(api())/envelopes/" } diff --git a/Roundware.podspec b/Roundware.podspec index 81a1689..35ebf20 100644 --- a/Roundware.podspec +++ b/Roundware.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'Roundware' - s.version = '0.2.3' + s.version = '0.3.0' s.summary = 'Audio framework' # This description is used to generate tags and improve search results.