feat: add HLSManifestGenerator with TDD tests

feat/music-streaming
Laurent 1 month ago
parent 981c9123a1
commit 0829dba09a
  1. 42
      MusicShared/Sources/MusicShared/HLSManifestGenerator.swift
  2. 78
      MusicShared/Tests/MusicSharedTests/HLSManifestGeneratorTests.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..<count {
let range = segmentTimeRange(index: i, trackDuration: duration, segmentDuration: segmentDuration)
lines.append(String(format: "#EXTINF:%.3f,", range.duration))
lines.append("segments/\(i).mp3")
}
lines.append("#EXT-X-ENDLIST")
lines.append("")
return lines.joined(separator: "\n")
}
public static func segmentCount(duration: Double, segmentDuration: Double) -> 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)
}
}

@ -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)
}
}
Loading…
Cancel
Save