diff --git a/MapboxNavigation/PollyVoiceController.swift b/MapboxNavigation/PollyVoiceController.swift index 37c8e67c7c4..1ca57777f26 100644 --- a/MapboxNavigation/PollyVoiceController.swift +++ b/MapboxNavigation/PollyVoiceController.swift @@ -31,11 +31,21 @@ public class PollyVoiceController: RouteVoiceController { */ public var timeoutIntervalForRequest:TimeInterval = 2 + /** + Number of steps ahead of the current step to cache spoken instructions. + */ + public var stepsAheadToCache: Int = 3 + var pollyTask: URLSessionDataTask? let sessionConfiguration = URLSessionConfiguration.default var urlSession: URLSession + var cacheURLSession: URLSession + var cachePollyTask: URLSessionDataTask? + + var spokenInstructionsForRoute: [String: Data] = [:] + public init(identityPoolId: String) { self.identityPoolId = identityPoolId @@ -45,6 +55,7 @@ public class PollyVoiceController: RouteVoiceController { sessionConfiguration.timeoutIntervalForRequest = timeoutIntervalForRequest; urlSession = URLSession(configuration: sessionConfiguration) + cacheURLSession = URLSession(configuration: URLSessionConfiguration.default) super.init() } @@ -57,14 +68,31 @@ public class PollyVoiceController: RouteVoiceController { pollyTask?.cancel() audioPlayer?.stop() + startAnnouncementTimer() + + for (stepIndex, step) in routeProgresss.currentLegProgress.leg.steps.suffix(from: routeProgresss.currentLegProgress.stepIndex).enumerated() { + let adjustedStepIndex = stepIndex + routeProgresss.currentLegProgress.stepIndex + + guard adjustedStepIndex < routeProgresss.currentLegProgress.stepIndex + stepsAheadToCache else { continue } + guard let instructions = step.instructionsSpokenAlongStep else { continue } + + for instruction in instructions { + guard spokenInstructionsForRoute[instruction.ssmlText] == nil else { continue } + + cacheSpokenInstruction(instruction: instruction.ssmlText) + } + } + + + guard spokenInstructionsForRoute[instruction] == nil else { + play(spokenInstructionsForRoute[instruction]!) + return + } speak(instruction, error: nil) - startAnnouncementTimer() } - override func speak(_ text: String, error: String?) { - assert(!text.isEmpty) - + func pollyURL(for instruction: String) -> AWSPollySynthesizeSpeechURLBuilderRequest { let input = AWSPollySynthesizeSpeechURLBuilderRequest() input.textType = .ssml input.outputFormat = .mp3 @@ -108,15 +136,22 @@ public class PollyVoiceController: RouteVoiceController { case ("tr", _): input.voiceId = .filiz default: - callSuperSpeak(fallbackText, error: "Voice \(langCode)-\(countryCode) not found") - return + input.voiceId = .joanna } if let voiceId = globalVoiceId { input.voiceId = voiceId } - input.text = text + input.text = instruction + + return input + } + + override func speak(_ text: String, error: String?) { + assert(!text.isEmpty) + + let input = pollyURL(for: text) let builder = AWSPollySynthesizeSpeechURLBuilder.default().getPreSignedURL(input) builder.continueWith { [weak self] (awsTask: AWSTask) -> Any? in @@ -172,29 +207,63 @@ public class PollyVoiceController: RouteVoiceController { return } + strongSelf.play(data) + } + + pollyTask?.resume() + } + + func cacheSpokenInstruction(instruction: String) { + + let pollyRequestURL = pollyURL(for: instruction) + + let builder = AWSPollySynthesizeSpeechURLBuilder.default().getPreSignedURL(pollyRequestURL) + builder.continueWith { [weak self] (awsTask: AWSTask) -> Any? in + guard let strongSelf = self else { + return nil + } + + guard let url = awsTask.result else { return nil } + + strongSelf.cachePollyTask = strongSelf.cacheURLSession.dataTask(with: url as URL) { (data, response, error) in + + if let error = error { + print(error.localizedDescription) + } + + if let data = data { + strongSelf.spokenInstructionsForRoute[instruction] = data + } + } + + strongSelf.cachePollyTask?.resume() + + return nil + } + } + + func play(_ data: Data) { + DispatchQueue.global(qos: .userInitiated).async { do { - strongSelf.audioPlayer = try AVAudioPlayer(data: data) - let prepared = strongSelf.audioPlayer?.prepareToPlay() ?? false + self.audioPlayer = try AVAudioPlayer(data: data) + let prepared = self.audioPlayer?.prepareToPlay() ?? false guard prepared else { - strongSelf.callSuperSpeak(strongSelf.fallbackText, error: "Audio player failed to prepare") + self.callSuperSpeak(self.fallbackText, error: "Audio player failed to prepare") return } - DispatchQueue.main.async { - strongSelf.audioPlayer?.delegate = self - let played = strongSelf.audioPlayer?.play() ?? false - - guard played else { - strongSelf.callSuperSpeak(strongSelf.fallbackText, error: "Audio player failed to play") - return - } + self.audioPlayer?.delegate = self + let played = self.audioPlayer?.play() ?? false + + guard played else { + self.callSuperSpeak(self.fallbackText, error: "Audio player failed to play") + return } + } catch let error as NSError { - strongSelf.callSuperSpeak(strongSelf.fallbackText, error: error.localizedDescription) + self.callSuperSpeak(self.fallbackText, error: error.localizedDescription) } } - - pollyTask?.resume() } }