parent
981c9123a1
commit
0829dba09a
@ -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…
Reference in new issue