feat: segment playback slider with resolved YouTube chapters
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user