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