diff --git a/MusicShared/Sources/MusicShared/HLSManifestGenerator.swift b/MusicShared/Sources/MusicShared/HLSManifestGenerator.swift new file mode 100644 index 0000000..b4f78fc --- /dev/null +++ b/MusicShared/Sources/MusicShared/HLSManifestGenerator.swift @@ -0,0 +1,42 @@ +import Foundation + +public enum HLSManifestGenerator: Sendable { + public struct TimeRange: Equatable, Sendable { + public var start: Double + public var duration: Double + } + + public static func manifest(trackId: Int64, duration: Double, segmentDuration: Double) -> String { + let count = segmentCount(duration: duration, segmentDuration: segmentDuration) + let targetDuration = Int(segmentDuration.rounded(.up)) + + var lines: [String] = [ + "#EXTM3U", + "#EXT-X-VERSION:3", + "#EXT-X-TARGETDURATION:\(targetDuration)", + "#EXT-X-MEDIA-SEQUENCE:0", + ] + + for i in 0.. Int { + guard duration > 0, segmentDuration > 0 else { return 0 } + return Int((duration / segmentDuration).rounded(.up)) + } + + public static func segmentTimeRange(index: Int, trackDuration: Double, segmentDuration: Double) -> TimeRange { + let start = Double(index) * segmentDuration + let remaining = trackDuration - start + let duration = min(segmentDuration, remaining) + return TimeRange(start: start, duration: duration) + } +} diff --git a/MusicShared/Tests/MusicSharedTests/HLSManifestGeneratorTests.swift b/MusicShared/Tests/MusicSharedTests/HLSManifestGeneratorTests.swift new file mode 100644 index 0000000..c56a5a9 --- /dev/null +++ b/MusicShared/Tests/MusicSharedTests/HLSManifestGeneratorTests.swift @@ -0,0 +1,78 @@ +import Testing +@testable import MusicShared + +struct HLSManifestGeneratorTests { + // Generates a manifest for a 16-second track with 6s segments. + // Expects 3 segments: 6s, 6s, 4s (remainder). + @Test func generatesCorrectManifestForTypicalTrack() { + let manifest = HLSManifestGenerator.manifest( + trackId: 42, + duration: 16.0, + segmentDuration: 6.0 + ) + + #expect(manifest.contains("#EXTM3U")) + #expect(manifest.contains("#EXT-X-VERSION:3")) + #expect(manifest.contains("#EXT-X-TARGETDURATION:6")) + #expect(manifest.contains("#EXT-X-MEDIA-SEQUENCE:0")) + #expect(manifest.contains("#EXTINF:6.000,")) + #expect(manifest.contains("#EXTINF:4.000,")) + #expect(manifest.contains("segments/0.mp3")) + #expect(manifest.contains("segments/1.mp3")) + #expect(manifest.contains("segments/2.mp3")) + #expect(!manifest.contains("segments/3.mp3")) + #expect(manifest.contains("#EXT-X-ENDLIST")) + } + + // A track whose duration is an exact multiple of the segment duration. + // Expects no short final segment. + @Test func exactMultipleOfSegmentDuration() { + let manifest = HLSManifestGenerator.manifest( + trackId: 1, + duration: 12.0, + segmentDuration: 6.0 + ) + + let segmentCount = manifest.components(separatedBy: "#EXTINF:6.000,").count - 1 + #expect(segmentCount == 2) + #expect(!manifest.contains("segments/2.mp3")) + } + + // A very short track (shorter than one segment). + // Expects a single segment with the track's full duration. + @Test func veryShortTrack() { + let manifest = HLSManifestGenerator.manifest( + trackId: 7, + duration: 2.5, + segmentDuration: 6.0 + ) + + #expect(manifest.contains("#EXTINF:2.500,")) + #expect(manifest.contains("segments/0.mp3")) + #expect(!manifest.contains("segments/1.mp3")) + } + + // Segment count helper returns the correct number of segments. + @Test func segmentCountCalculation() { + #expect(HLSManifestGenerator.segmentCount(duration: 16.0, segmentDuration: 6.0) == 3) + #expect(HLSManifestGenerator.segmentCount(duration: 12.0, segmentDuration: 6.0) == 2) + #expect(HLSManifestGenerator.segmentCount(duration: 2.5, segmentDuration: 6.0) == 1) + #expect(HLSManifestGenerator.segmentCount(duration: 6.0, segmentDuration: 6.0) == 1) + } + + // Time range for a given segment index returns correct start and duration. + @Test func segmentTimeRange() { + // Track: 16s, segment: 6s → segments at 0-6, 6-12, 12-16 + let range0 = HLSManifestGenerator.segmentTimeRange( + index: 0, trackDuration: 16.0, segmentDuration: 6.0 + ) + #expect(range0.start == 0.0) + #expect(range0.duration == 6.0) + + let range2 = HLSManifestGenerator.segmentTimeRange( + index: 2, trackDuration: 16.0, segmentDuration: 6.0 + ) + #expect(range2.start == 12.0) + #expect(range2.duration == 4.0) + } +}