feat: segment playback slider with resolved YouTube chapters

This commit is contained in:
2026-10-25 02:22:37 +01:00
parent 2391dadc75
commit 119fe2e737
5 changed files with 272 additions and 20 deletions
+47 -1
View File
@@ -7,6 +7,12 @@ struct YouTubeStream: Sendable {
let headers: String
}
struct YouTubeChapter: Sendable {
let start: Double
let end: Double
let title: String?
}
struct YouTubeVideo: Sendable {
let title: String
let video: YouTubeStream
@@ -16,6 +22,19 @@ struct YouTubeVideo: Sendable {
let webpageURL: URL
let height: Int?
let availableHeights: [Int]
let chapters: [YouTubeChapter]
func chapterBoundaries(duration: Double) -> [Double] {
guard duration.isFinite, duration > 0 else { return [] }
var boundaries = Set<Double>()
for chapter in chapters {
guard chapter.start.isFinite, chapter.end.isFinite,
chapter.start >= 0, chapter.end > chapter.start, chapter.end <= duration else { continue }
if chapter.start > 0 { boundaries.insert(chapter.start) }
if chapter.end < duration { boundaries.insert(chapter.end) }
}
return boundaries.sorted()
}
}
private struct YouTubeResolutionError: LocalizedError {
@@ -135,7 +154,8 @@ enum YouTubeResolver {
video: video, audio: audio, duration: duration,
isLive: metadata.is_live == true || metadata.live_status == "is_live",
webpageURL: try validateURL(webpageURL.absoluteString),
height: videoFormat.height, availableHeights: availableHeights
height: videoFormat.height, availableHeights: availableHeights,
chapters: metadata.chapters?.values ?? []
)
}
@@ -386,12 +406,38 @@ private struct YouTubeMetadata: Decodable {
let formats: [YouTubeMetadata]?
let http_headers: [String: String]?
let requested_formats: [YouTubeMetadata]?
let chapters: YouTubeChapters?
// Generic extractors may omit codec hints; FFmpeg probes the actual streams.
var hasVideo: Bool { vcodec.map { !$0.isEmpty && $0 != "none" } ?? true }
var hasAudio: Bool { acodec.map { !$0.isEmpty && $0 != "none" } ?? true }
}
private struct YouTubeChapters: Decodable {
let values: [YouTubeChapter]
private enum CodingKeys: String, CodingKey {
case start_time, end_time, title
}
init(from decoder: Decoder) {
guard var rows = try? decoder.unkeyedContainer() else {
values = []
return
}
var chapters: [YouTubeChapter] = []
while !rows.isAtEnd {
guard let rowDecoder = try? rows.superDecoder() else { break }
guard let row = try? rowDecoder.container(keyedBy: CodingKeys.self),
let start = try? row.decode(Double.self, forKey: .start_time),
let end = try? row.decode(Double.self, forKey: .end_time) else { continue }
chapters.append(YouTubeChapter(start: start, end: end,
title: try? row.decode(String.self, forKey: .title)))
}
values = chapters
}
}
private struct YouTubeExtractionOutput: Sendable {
let stdout: Data
let stderr: Data