Skip to content

Commit ab0300e

Browse files
committed
Surface HTTP and audio system errors
1 parent cc972c0 commit ab0300e

7 files changed

Lines changed: 132 additions & 26 deletions

File tree

AudioStreaming/Core/Extensions/AVAudioUnit+Convenience.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ extension AVAudioUnit {
2121
completion(.failure(error))
2222
return
2323
}
24-
completion(.failure(AudioPlayerError.audioSystemError(.playerNotFound)))
24+
completion(.failure(AudioPlayerError.audioSystemError(.playerNotFound(nil))))
2525
return
2626
}
2727
completion(.success(audioUnit))

AudioStreaming/Core/Network/NetworkingClient.swift

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@ enum DataStreamError: Error {
1212

1313
public enum NetworkError: Error, Equatable {
1414
case failure(Error)
15-
case serverError
15+
case serverError(statusCode: Int)
1616
case missingData
17+
1718
public static func == (lhs: NetworkError, rhs: NetworkError) -> Bool {
1819
switch (lhs, rhs) {
19-
case (.failure, failure):
20-
return true
21-
case (.serverError, .serverError):
22-
return true
20+
case let (.failure(lhsError), .failure(rhsError)):
21+
return compareErrors(lhsError, rhsError)
22+
case let (.serverError(lhsStatusCode), .serverError(rhsStatusCode)):
23+
return lhsStatusCode == rhsStatusCode
2324
case (.missingData, .missingData):
2425
return true
2526
default:
@@ -28,6 +29,34 @@ public enum NetworkError: Error, Equatable {
2829
}
2930
}
3031

32+
extension NetworkError: LocalizedError {
33+
public var errorDescription: String? {
34+
switch self {
35+
case let .failure(error):
36+
let nsError = error as NSError
37+
return "\(error.localizedDescription) [\(nsError.domain):\(nsError.code)]"
38+
case let .serverError(statusCode):
39+
return "HTTP server error \(statusCode)"
40+
case .missingData:
41+
return "Missing audio data from network stream"
42+
}
43+
}
44+
}
45+
46+
func compareErrors(_ lhs: Error?, _ rhs: Error?) -> Bool {
47+
switch (lhs, rhs) {
48+
case (nil, nil):
49+
return true
50+
case let (lhs?, rhs?):
51+
let lhsNSError = lhs as NSError
52+
let rhsNSError = rhs as NSError
53+
return lhsNSError.domain == rhsNSError.domain &&
54+
lhsNSError.code == rhsNSError.code
55+
default:
56+
return false
57+
}
58+
}
59+
3160
protocol StreamTaskProvider: AnyObject {
3261
func dataStream(for request: URLSessionTask) -> NetworkDataStream?
3362
}

AudioStreaming/Streaming/Audio Source/FileAudioSource.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -152,15 +152,15 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
152152
func performMp4Restructure(inputStream: InputStream, moovOffset: Int) throws {
153153
let offsetAccepted = inputStream.setProperty(moovOffset, forKey: .fileCurrentOffsetKey)
154154
if !offsetAccepted {
155-
delegate?.errorOccurred(source: self, error: inputStream.streamError ?? AudioSystemError.playerStartError)
155+
delegate?.errorOccurred(source: self, error: inputStream.streamError ?? AudioSystemError.playerStartError(nil))
156156
return
157157
}
158158

159159
// Read moov header (8 bytes)
160160
var header = [UInt8](repeating: 0, count: 8)
161161
let headerRead = inputStream.read(&header, maxLength: 8)
162162
guard headerRead == 8 else {
163-
delegate?.errorOccurred(source: self, error: AudioSystemError.playerStartError)
163+
delegate?.errorOccurred(source: self, error: AudioSystemError.playerStartError(nil))
164164
return
165165
}
166166

@@ -180,7 +180,7 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
180180
var ext = [UInt8](repeating: 0, count: 8)
181181
let extRead = inputStream.read(&ext, maxLength: 8)
182182
guard extRead == 8 else {
183-
delegate?.errorOccurred(source: self, error: AudioSystemError.playerStartError)
183+
delegate?.errorOccurred(source: self, error: AudioSystemError.playerStartError(nil))
184184
return
185185
}
186186
let ext64 = Data(ext).withUnsafeBytes { $0.load(as: UInt64.self) }.bigEndian
@@ -190,7 +190,7 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
190190

191191
let remaining = moovSize - moovData.count
192192
if remaining < 0 {
193-
delegate?.errorOccurred(source: self, error: AudioSystemError.playerStartError)
193+
delegate?.errorOccurred(source: self, error: AudioSystemError.playerStartError(nil))
194194
return
195195
}
196196
if remaining > 0 {
@@ -202,7 +202,7 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
202202
return inputStream.read(base, maxLength: remaining - total)
203203
}
204204
guard readBytes > 0 else {
205-
delegate?.errorOccurred(source: self, error: AudioSystemError.playerStartError)
205+
delegate?.errorOccurred(source: self, error: AudioSystemError.playerStartError(nil))
206206
return
207207
}
208208
total += readBytes
@@ -213,13 +213,13 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
213213
let moovResult = try mp4Restructure.restructureMoov(data: moovData)
214214
delegate?.dataAvailable(source: self, data: moovResult.initialData)
215215
if !inputStream.setProperty(moovResult.mdatOffset, forKey: .fileCurrentOffsetKey) {
216-
delegate?.errorOccurred(source: self, error: AudioSystemError.playerStartError)
216+
delegate?.errorOccurred(source: self, error: AudioSystemError.playerStartError(nil))
217217
}
218218
}
219219

220220
private func open() throws {
221221
guard let inputStream = InputStream(url: url) else {
222-
throw AudioSystemError.playerStartError
222+
throw AudioSystemError.playerStartError(nil)
223223
}
224224
self.inputStream = inputStream
225225
CFReadStreamSetDispatchQueue(inputStream, underlyingQueue)

AudioStreaming/Streaming/Audio Source/RemoteAudioSource.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ public class RemoteAudioSource: AudioStreamSource {
360360
} else if statusCode >= 300 {
361361
delegate?.errorOccurred(
362362
source: self,
363-
error: NetworkError.serverError
363+
error: NetworkError.serverError(statusCode: statusCode)
364364
)
365365
}
366366
}

AudioStreaming/Streaming/AudioPlayer/AudioPlayer.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ open class AudioPlayer {
282282
do {
283283
try self.startEngineIfNeeded()
284284
} catch {
285-
self.raiseUnexpected(error: .audioSystemError(.engineFailure))
285+
self.raiseUnexpected(error: .audioSystemError(.engineFailure(.init(error: error))))
286286
}
287287
}
288288

@@ -301,7 +301,7 @@ open class AudioPlayer {
301301
do {
302302
try self.startEngineIfNeeded()
303303
} catch {
304-
self.raiseUnexpected(error: .audioSystemError(.engineFailure))
304+
self.raiseUnexpected(error: .audioSystemError(.engineFailure(nil)))
305305
}
306306
}
307307

@@ -578,7 +578,7 @@ open class AudioPlayer {
578578
self.playerRenderProcessor.attachCallback(on: unit, audioFormat: self.outputAudioFormat)
579579
case let .failure(error):
580580
assertionFailure("couldn't create player unit: \(error)")
581-
self.raiseUnexpected(error: .audioSystemError(.playerNotFound))
581+
self.raiseUnexpected(error: .audioSystemError(.playerNotFound(.init(error: error))))
582582
}
583583
}
584584
}
@@ -705,7 +705,7 @@ open class AudioPlayer {
705705
try player.auAudioUnit.startHardware()
706706
} catch {
707707
stopEngine(reason: .error)
708-
raiseUnexpected(error: .audioSystemError(.playerStartError))
708+
raiseUnexpected(error: .audioSystemError(.playerStartError(.init(error: error))))
709709
}
710710
}
711711

AudioStreaming/Streaming/AudioPlayer/AudioPlayerState.swift

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -100,25 +100,46 @@ public enum AudioPlayerError: LocalizedError, Equatable, Sendable {
100100
}
101101
}
102102

103+
public struct AudioSystemErrorDetails: Equatable, Sendable {
104+
public let description: String
105+
public let domain: String
106+
public let code: Int
107+
108+
init(error: Error) {
109+
let nsError = error as NSError
110+
description = error.localizedDescription
111+
domain = nsError.domain
112+
code = nsError.code
113+
}
114+
}
115+
103116
public enum AudioSystemError: LocalizedError, Equatable, Sendable {
104-
case engineFailure
105-
case playerNotFound
106-
case playerStartError
117+
case engineFailure(AudioSystemErrorDetails?)
118+
case playerNotFound(AudioSystemErrorDetails?)
119+
case playerStartError(AudioSystemErrorDetails?)
107120
case fileStreamError(AudioFileStreamError)
108121
case converterError(AudioConverterError)
109122
case codecError
110123

111124
public var errorDescription: String? {
112125
switch self {
113-
case .engineFailure: return "Audio engine couldn't start"
114-
case .playerNotFound: return "Player not found"
115-
case .playerStartError: return "Player couldn't start"
126+
case let .engineFailure(error):
127+
return detailedDescription(prefix: "Audio engine couldn't start", error: error)
128+
case let .playerNotFound(error):
129+
return detailedDescription(prefix: "Player not found", error: error)
130+
case let .playerStartError(error):
131+
return detailedDescription(prefix: "Player couldn't start", error: error)
116132
case let .fileStreamError(error):
117-
return "Audio file stream error'd: \(error)"
133+
return "Audio file stream errored: \(error)"
118134
case let .converterError(error):
119-
return "Audio converter error'd: \(error)"
135+
return "Audio converter errored: \(error)"
120136
case .codecError:
121137
return "Audio codec error"
122138
}
123139
}
124140
}
141+
142+
private func detailedDescription(prefix: String, error: AudioSystemErrorDetails?) -> String {
143+
guard let error else { return prefix }
144+
return "\(prefix): \(error.description) [\(error.domain):\(error.code)]"
145+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import Foundation
2+
import XCTest
3+
4+
@testable import AudioStreaming
5+
6+
final class NetworkErrorTests: XCTestCase {
7+
func testFailureEqualityUsesNSErrorIdentity() {
8+
XCTAssertEqual(
9+
NetworkError.failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut)),
10+
NetworkError.failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut))
11+
)
12+
XCTAssertNotEqual(
13+
NetworkError.failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut)),
14+
NetworkError.failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotConnectToHost))
15+
)
16+
}
17+
18+
func testServerErrorDescriptionIncludesStatusCode() {
19+
XCTAssertEqual(NetworkError.serverError(statusCode: 403).localizedDescription, "HTTP server error 403")
20+
XCTAssertEqual(NetworkError.serverError(statusCode: 404).localizedDescription, "HTTP server error 404")
21+
XCTAssertEqual(NetworkError.serverError(statusCode: 500).localizedDescription, "HTTP server error 500")
22+
}
23+
24+
func testMissingDataDescription() {
25+
XCTAssertEqual(NetworkError.missingData.localizedDescription, "Missing audio data from network stream")
26+
}
27+
28+
func testEngineFailureDescriptionIncludesUnderlyingNSErrorDetails() {
29+
let error = NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotConnectToHost)
30+
let description = AudioSystemError.engineFailure(.init(error: error)).localizedDescription
31+
32+
XCTAssertTrue(description.contains("Audio engine couldn't start"))
33+
XCTAssertTrue(description.contains(NSURLErrorDomain))
34+
XCTAssertTrue(description.contains("\(NSURLErrorCannotConnectToHost)"))
35+
}
36+
37+
func testNilUnderlyingErrorUsesPlainPrefix() {
38+
XCTAssertEqual(AudioSystemError.engineFailure(nil).localizedDescription, "Audio engine couldn't start")
39+
XCTAssertEqual(AudioSystemError.playerNotFound(nil).localizedDescription, "Player not found")
40+
XCTAssertEqual(AudioSystemError.playerStartError(nil).localizedDescription, "Player couldn't start")
41+
}
42+
43+
func testUnderlyingAudioSystemErrorsIncludePrefixAndNSErrorDetails() {
44+
let playerNotFoundDescription =
45+
AudioSystemError.playerNotFound(.init(error: NSError(domain: "AudioUnit", code: -50))).localizedDescription
46+
XCTAssertTrue(playerNotFoundDescription.contains("Player not found"))
47+
XCTAssertTrue(playerNotFoundDescription.contains("AudioUnit"))
48+
XCTAssertTrue(playerNotFoundDescription.contains("-50"))
49+
50+
let playerStartDescription =
51+
AudioSystemError.playerStartError(.init(error: NSError(domain: NSOSStatusErrorDomain, code: -10875))).localizedDescription
52+
XCTAssertTrue(playerStartDescription.contains("Player couldn't start"))
53+
XCTAssertTrue(playerStartDescription.contains(NSOSStatusErrorDomain))
54+
XCTAssertTrue(playerStartDescription.contains("-10875"))
55+
}
56+
}

0 commit comments

Comments
 (0)