diff --git a/AudioStreaming/Streaming/Audio Entry/AudioEntry.swift b/AudioStreaming/Streaming/Audio Entry/AudioEntry.swift index 1ddb08a..c948801 100644 --- a/AudioStreaming/Streaming/Audio Entry/AudioEntry.swift +++ b/AudioStreaming/Streaming/Audio Entry/AudioEntry.swift @@ -155,8 +155,12 @@ class AudioEntry { if let byteCount = audioStreamState.dataByteCount { return UInt(byteCount) } - guard source.length > 0 else { return 0 } - return UInt(source.length) - UInt(audioStreamState.dataOffset) + let length = UInt64(max(source.length, 0)) + let dataOffset = audioStreamState.dataOffset + // A live stream can report a length smaller than the parsed header offset; + // an unsigned subtraction would trap there. + guard length > dataOffset else { return 0 } + return UInt(length - dataOffset) } } diff --git a/AudioStreamingTests/Core/DispatchTimerSourceTests.swift b/AudioStreamingTests/Core/DispatchTimerSourceTests.swift index 5d272e7..81553b2 100644 --- a/AudioStreamingTests/Core/DispatchTimerSourceTests.swift +++ b/AudioStreamingTests/Core/DispatchTimerSourceTests.swift @@ -96,6 +96,9 @@ class DispatchTimerSourceTests: XCTestCase { func test_HandlerIsExecuted_On_The_Specified_Queue() { let expectaction = expectation(description: "fired") + // The source repeats every 100 ms; on a slow runner it can fire again before + // `wait` returns, and a second `fulfill()` raises an API violation. + expectaction.assertForOverFulfill = false timerSource?.add { XCTAssertEqual(DispatchQueue.getSpecific(key: self.dispatchKey), 1) diff --git a/AudioStreamingTests/Streaming/Audio Entry/AudioEntryTests.swift b/AudioStreamingTests/Streaming/Audio Entry/AudioEntryTests.swift new file mode 100644 index 0000000..126b94e --- /dev/null +++ b/AudioStreamingTests/Streaming/Audio Entry/AudioEntryTests.swift @@ -0,0 +1,70 @@ +// +// AudioEntryTests.swift +// AudioStreamingTests +// + +import AVFoundation +import XCTest +@testable import AudioStreaming + +class AudioEntryTests: XCTestCase { + func test_AudioDataLengthBytes_Is_Zero_When_DataOffset_Exceeds_Length() { + // a live stream can report a length smaller than the parsed header offset + let entry = audioEntry(length: 100) + entry.audioStreamState.dataOffset = 200 + + XCTAssertEqual(entry.audioDataLengthBytes(), 0) + } + + func test_AudioDataLengthBytes_Is_Zero_When_DataOffset_Equals_Length() { + let entry = audioEntry(length: 100) + entry.audioStreamState.dataOffset = 100 + + XCTAssertEqual(entry.audioDataLengthBytes(), 0) + } + + func test_AudioDataLengthBytes_Subtracts_DataOffset_From_Length() { + let entry = audioEntry(length: 1000) + entry.audioStreamState.dataOffset = 200 + + XCTAssertEqual(entry.audioDataLengthBytes(), 800) + } + + func test_AudioDataLengthBytes_Prefers_Explicit_ByteCount() { + let entry = audioEntry(length: 100) + entry.audioStreamState.dataOffset = 200 + entry.audioStreamState.dataByteCount = 5000 + + XCTAssertEqual(entry.audioDataLengthBytes(), 5000) + } + + func test_Duration_Is_Zero_When_DataOffset_Exceeds_Length() { + let entry = audioEntry(length: 100) + entry.sampleRate = 44100 + entry.audioStreamState.bitRate = 128_000 + entry.audioStreamState.dataOffset = 200 + + XCTAssertEqual(entry.duration(), 0) + } +} + +private final class FixedLengthSource: CoreAudioStreamSource { + let position = 0 + let length: Int + weak var delegate: AudioStreamSourceDelegate? + let audioFileHint: AudioFileTypeID = kAudioFileMP3Type + let underlyingQueue = DispatchQueue(label: "audio-entry-tests") + + init(length: Int) { self.length = length } + + func close() {} + func suspend() {} + func resume() {} + func seek(at _: Int) {} +} + +private func audioEntry(length: Int) -> AudioEntry { + AudioEntry(source: FixedLengthSource(length: length), + entryId: AudioEntryId(id: "entry"), + outputAudioFormat: AVAudioFormat()) +}