rill-node 0.0.0-stage → 2026.11.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +64 -2
- package/binding.gyp +31 -0
- package/index.d.ts +45 -0
- package/index.js +112 -0
- package/native/darwin-arm64/librill.dylib +0 -0
- package/native/darwin-arm64/rill-media +0 -0
- package/native/darwin-arm64/rill_node.node +0 -0
- package/native/darwin-x64/librill.dylib +0 -0
- package/native/darwin-x64/rill-media +0 -0
- package/native/darwin-x64/rill_node.node +0 -0
- package/native/linux-arm64-gnu/librill.so +0 -0
- package/native/linux-arm64-gnu/rill-media +0 -0
- package/native/linux-arm64-gnu/rill_node.node +0 -0
- package/native/linux-x64-gnu/librill.so +0 -0
- package/native/linux-x64-gnu/rill-media +0 -0
- package/native/linux-x64-gnu/rill_node.node +0 -0
- package/native-src/include/rill.h +90 -0
- package/native-src/include/rill_media.h +22 -0
- package/native-src/shard.yml +9 -0
- package/native-src/src/c_api.cr +194 -0
- package/native-src/src/cli.cr +637 -0
- package/native-src/src/rill/c_api/job.cr +336 -0
- package/native-src/src/rill/c_api/memory.cr +32 -0
- package/native-src/src/rill/c_api/status.cr +25 -0
- package/native-src/src/rill/c_api/sync.cr +125 -0
- package/native-src/src/rill/c_api/worker_pool.cr +403 -0
- package/native-src/src/rill/cancellation.cr +29 -0
- package/native-src/src/rill/client.cr +52 -0
- package/native-src/src/rill/cookie_jar.cr +130 -0
- package/native-src/src/rill/dash/manifest.cr +279 -0
- package/native-src/src/rill/dash/parser.cr +227 -0
- package/native-src/src/rill/download_options.cr +96 -0
- package/native-src/src/rill/download_plan.cr +27 -0
- package/native-src/src/rill/download_progress.cr +16 -0
- package/native-src/src/rill/downloader.cr +662 -0
- package/native-src/src/rill/errors.cr +155 -0
- package/native-src/src/rill/extractor.cr +18 -0
- package/native-src/src/rill/extractor_registry.cr +98 -0
- package/native-src/src/rill/extractors/bandcamp.cr +265 -0
- package/native-src/src/rill/extractors/dash.cr +77 -0
- package/native-src/src/rill/extractors/direct_media.cr +73 -0
- package/native-src/src/rill/extractors/generic.cr +441 -0
- package/native-src/src/rill/extractors/hls.cr +202 -0
- package/native-src/src/rill/extractors/internet_archive.cr +180 -0
- package/native-src/src/rill/extractors/m3u.cr +81 -0
- package/native-src/src/rill/extractors/media_ccc_de.cr +219 -0
- package/native-src/src/rill/extractors/mixcloud.cr +495 -0
- package/native-src/src/rill/extractors/peer_tube.cr +226 -0
- package/native-src/src/rill/extractors/rumble.cr +325 -0
- package/native-src/src/rill/extractors/soundcloud.cr +553 -0
- package/native-src/src/rill/extractors/spotify.cr +184 -0
- package/native-src/src/rill/extractors/twitch_clip.cr +273 -0
- package/native-src/src/rill/extractors/youtube.cr +520 -0
- package/native-src/src/rill/format.cr +82 -0
- package/native-src/src/rill/format_selector.cr +143 -0
- package/native-src/src/rill/hls/parser.cr +257 -0
- package/native-src/src/rill/hls/playlist.cr +100 -0
- package/native-src/src/rill/http_client.cr +503 -0
- package/native-src/src/rill/http_url.cr +28 -0
- package/native-src/src/rill/json_value.cr +66 -0
- package/native-src/src/rill/media.cr +151 -0
- package/native-src/src/rill/media_muxer.cr +333 -0
- package/native-src/src/rill/mime_type.cr +58 -0
- package/native-src/src/rill/output_file_name.cr +91 -0
- package/native-src/src/rill/playlist.cr +109 -0
- package/native-src/src/rill/request_options.cr +179 -0
- package/native-src/src/rill/segment_assembly.cr +70 -0
- package/native-src/src/rill/timestamp.cr +34 -0
- package/native-src/src/rill/version.cr +3 -0
- package/native-src/src/rill.cr +47 -0
- package/package.json +48 -4
- package/scripts/build-native.js +46 -0
- package/scripts/install-gyp.js +10 -0
- package/scripts/install.js +19 -0
- package/scripts/lib.js +1 -0
- package/scripts/libdir.js +23 -0
- package/scripts/package-native.js +35 -0
- package/scripts/platform.js +41 -0
- package/scripts/prepack.js +47 -0
- package/src/addon.c +498 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
require "./download_plan"
|
|
2
|
+
require "./format"
|
|
3
|
+
|
|
4
|
+
module Rill
|
|
5
|
+
enum FormatPreference
|
|
6
|
+
AudioAndVideo
|
|
7
|
+
AudioOnly
|
|
8
|
+
VideoOnly
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
module Rill::FormatSelector
|
|
13
|
+
extend self
|
|
14
|
+
|
|
15
|
+
CONTAINER_RANK = {
|
|
16
|
+
"mp4" => 5,
|
|
17
|
+
"webm" => 4,
|
|
18
|
+
"mkv" => 4,
|
|
19
|
+
"m4v" => 3,
|
|
20
|
+
"mov" => 3,
|
|
21
|
+
"ogv" => 2,
|
|
22
|
+
"avi" => 1,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
def best(formats : Array(Format)) : Format?
|
|
26
|
+
return nil if formats.empty?
|
|
27
|
+
|
|
28
|
+
combined = formats.select { |format| format.kind == FormatKind::Combined }
|
|
29
|
+
return best_video(combined) unless combined.empty?
|
|
30
|
+
|
|
31
|
+
video = formats.select { |format| format.kind == FormatKind::Video }
|
|
32
|
+
return best_video(video) unless video.empty?
|
|
33
|
+
|
|
34
|
+
best_audio(formats)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def plan(
|
|
38
|
+
formats : Array(Format),
|
|
39
|
+
format_id : String? = nil,
|
|
40
|
+
preference : FormatPreference = FormatPreference::AudioAndVideo,
|
|
41
|
+
) : DownloadPlan?
|
|
42
|
+
return nil if formats.empty?
|
|
43
|
+
|
|
44
|
+
if format_id
|
|
45
|
+
selected = by_id(formats, format_id)
|
|
46
|
+
return nil unless selected
|
|
47
|
+
|
|
48
|
+
return plan_for(selected, formats)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
case preference
|
|
52
|
+
when .audio_only?
|
|
53
|
+
audio = best_audio_only(formats)
|
|
54
|
+
audio ? DownloadPlan.new(audio) : nil
|
|
55
|
+
when .video_only?
|
|
56
|
+
video = best_video_only(formats)
|
|
57
|
+
video ? DownloadPlan.new(video) : nil
|
|
58
|
+
else
|
|
59
|
+
audio_and_video_plan(formats)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private def audio_and_video_plan(formats : Array(Format)) : DownloadPlan?
|
|
64
|
+
combined = formats.select { |format| format.kind == FormatKind::Combined }
|
|
65
|
+
unless combined.empty?
|
|
66
|
+
return DownloadPlan.new(best_video(combined))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
video = best_video_only(formats)
|
|
70
|
+
unless video.nil?
|
|
71
|
+
return DownloadPlan.new(video, best_audio_for(video, formats))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
audio = best_audio_only(formats)
|
|
75
|
+
audio ? DownloadPlan.new(audio) : nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private def best_video_only(formats : Array(Format)) : Format?
|
|
79
|
+
video = formats.select { |format| format.kind == FormatKind::Video }
|
|
80
|
+
return nil if video.empty?
|
|
81
|
+
|
|
82
|
+
best_video(video)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private def best_audio_only(formats : Array(Format)) : Format?
|
|
86
|
+
audio = formats.select { |format| format.kind == FormatKind::Audio }
|
|
87
|
+
return nil if audio.empty?
|
|
88
|
+
|
|
89
|
+
audio.max_by? do |format|
|
|
90
|
+
{
|
|
91
|
+
(format.bitrate || 0).to_i64,
|
|
92
|
+
(format.audio_bitrate || 0).to_i64,
|
|
93
|
+
format.filesize || 0_i64,
|
|
94
|
+
}
|
|
95
|
+
end || audio.first
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def by_id(formats : Array(Format), format_id : String) : Format?
|
|
99
|
+
formats.find { |format| format.format_id == format_id }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private def plan_for(selected : Format, formats : Array(Format)) : DownloadPlan
|
|
103
|
+
return DownloadPlan.new(selected) unless selected.kind == FormatKind::Video
|
|
104
|
+
|
|
105
|
+
DownloadPlan.new(selected, best_audio_for(selected, formats))
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private def best_audio_for(video : Format, formats : Array(Format)) : Format?
|
|
109
|
+
audio = formats.select { |format| format.kind == FormatKind::Audio }
|
|
110
|
+
return nil if audio.empty?
|
|
111
|
+
|
|
112
|
+
audio.max_by? do |format|
|
|
113
|
+
{
|
|
114
|
+
video.container && format.container == video.container ? 1 : 0,
|
|
115
|
+
(format.bitrate || 0).to_i64,
|
|
116
|
+
(format.audio_bitrate || 0).to_i64,
|
|
117
|
+
format.filesize || 0_i64,
|
|
118
|
+
}
|
|
119
|
+
end || audio.first
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private def best_video(formats : Array(Format)) : Format
|
|
123
|
+
formats.max_by? do |format|
|
|
124
|
+
{
|
|
125
|
+
format.height || 0,
|
|
126
|
+
CONTAINER_RANK[format.container || ""]? || 0,
|
|
127
|
+
(format.video_bitrate || 0).to_i64,
|
|
128
|
+
format.bitrate || 0_i64,
|
|
129
|
+
format.filesize || 0_i64,
|
|
130
|
+
}
|
|
131
|
+
end || formats.first
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private def best_audio(formats : Array(Format)) : Format
|
|
135
|
+
formats.max_by? do |format|
|
|
136
|
+
{
|
|
137
|
+
(format.bitrate || 0).to_i64,
|
|
138
|
+
(format.audio_bitrate || 0).to_i64,
|
|
139
|
+
format.filesize || 0_i64,
|
|
140
|
+
}
|
|
141
|
+
end || formats.first
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
require "uri"
|
|
2
|
+
|
|
3
|
+
require "../errors"
|
|
4
|
+
require "./playlist"
|
|
5
|
+
|
|
6
|
+
module Rill::Hls::Parser
|
|
7
|
+
extend self
|
|
8
|
+
|
|
9
|
+
STREAM_INF = "#EXT-X-STREAM-INF:"
|
|
10
|
+
MEDIA = "#EXT-X-MEDIA:"
|
|
11
|
+
EXTINF = "#EXTINF:"
|
|
12
|
+
KEY = "#EXT-X-KEY:"
|
|
13
|
+
MAP = "#EXT-X-MAP:"
|
|
14
|
+
BYTERANGE = "#EXT-X-BYTERANGE:"
|
|
15
|
+
ENDLIST = "#EXT-X-ENDLIST"
|
|
16
|
+
|
|
17
|
+
def parse(source : String, base_url : URI) : MultivariantPlaylist | MediaPlaylist
|
|
18
|
+
text = source.lchop('\uFEFF')
|
|
19
|
+
lines = text.lines.map(&.strip).reject(&.empty?)
|
|
20
|
+
|
|
21
|
+
unless lines.first? == "#EXTM3U"
|
|
22
|
+
raise ManifestError.new("HLS playlist is missing #EXTM3U")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
if lines.any?(&.starts_with?(STREAM_INF))
|
|
26
|
+
parse_master(lines, base_url)
|
|
27
|
+
else
|
|
28
|
+
parse_media(lines, base_url)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def parse_attributes(text : String) : Hash(String, String)
|
|
33
|
+
attributes = {} of String => String
|
|
34
|
+
index = 0
|
|
35
|
+
|
|
36
|
+
while index < text.size
|
|
37
|
+
while index < text.size && (text[index] == ' ' || text[index] == ',')
|
|
38
|
+
index += 1
|
|
39
|
+
end
|
|
40
|
+
break if index >= text.size
|
|
41
|
+
|
|
42
|
+
key_start = index
|
|
43
|
+
while index < text.size && text[index] != '='
|
|
44
|
+
index += 1
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
if index >= text.size
|
|
48
|
+
raise ManifestError.new("invalid HLS attribute: #{text}")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
key = text[key_start, index - key_start].strip
|
|
52
|
+
index += 1
|
|
53
|
+
|
|
54
|
+
value = if index < text.size && text[index] == '"'
|
|
55
|
+
index += 1
|
|
56
|
+
value_start = index
|
|
57
|
+
while index < text.size && text[index] != '"'
|
|
58
|
+
index += 1
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if index >= text.size
|
|
62
|
+
raise ManifestError.new("unterminated HLS attribute: #{text}")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
result = text[value_start, index - value_start]
|
|
66
|
+
index += 1
|
|
67
|
+
result
|
|
68
|
+
else
|
|
69
|
+
value_start = index
|
|
70
|
+
while index < text.size && text[index] != ','
|
|
71
|
+
index += 1
|
|
72
|
+
end
|
|
73
|
+
text[value_start, index - value_start].strip
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
attributes[key.upcase] = value unless key.empty?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
attributes
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private def parse_master(lines : Array(String), base_url : URI) : MultivariantPlaylist
|
|
83
|
+
variants = [] of Variant
|
|
84
|
+
renditions = [] of Rendition
|
|
85
|
+
pending : Hash(String, String)? = nil
|
|
86
|
+
|
|
87
|
+
lines.each do |line|
|
|
88
|
+
if line.starts_with?(STREAM_INF)
|
|
89
|
+
pending = parse_attributes(line[STREAM_INF.size..])
|
|
90
|
+
next
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
if line.starts_with?(MEDIA)
|
|
94
|
+
renditions << build_rendition(parse_attributes(line[MEDIA.size..]), base_url)
|
|
95
|
+
next
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
next if line.starts_with?('#')
|
|
99
|
+
|
|
100
|
+
if attributes = pending
|
|
101
|
+
variants << build_variant(attributes, line, base_url)
|
|
102
|
+
pending = nil
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
raise ManifestError.new("HLS master playlist has a dangling stream") if pending
|
|
107
|
+
raise ManifestError.new("HLS master playlist has no variants") if variants.empty?
|
|
108
|
+
|
|
109
|
+
MultivariantPlaylist.new(variants, renditions)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private def build_rendition(
|
|
113
|
+
attributes : Hash(String, String),
|
|
114
|
+
base_url : URI,
|
|
115
|
+
) : Rendition
|
|
116
|
+
type = attributes["TYPE"]?
|
|
117
|
+
raise ManifestError.new("HLS media rendition is missing a type") unless type
|
|
118
|
+
|
|
119
|
+
uri = attributes["URI"]?
|
|
120
|
+
|
|
121
|
+
Rendition.new(
|
|
122
|
+
type: type.upcase,
|
|
123
|
+
group_id: attributes["GROUP-ID"]?,
|
|
124
|
+
name: attributes["NAME"]?,
|
|
125
|
+
language: attributes["LANGUAGE"]?,
|
|
126
|
+
url: uri ? resolve(uri, base_url) : nil,
|
|
127
|
+
default: attributes["DEFAULT"]? == "YES",
|
|
128
|
+
autoselect: attributes["AUTOSELECT"]? == "YES",
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private def build_variant(
|
|
133
|
+
attributes : Hash(String, String),
|
|
134
|
+
uri_text : String,
|
|
135
|
+
base_url : URI,
|
|
136
|
+
) : Variant
|
|
137
|
+
width = nil
|
|
138
|
+
height = nil
|
|
139
|
+
|
|
140
|
+
if resolution = attributes["RESOLUTION"]?
|
|
141
|
+
if match = /\A(\d+)x(\d+)\z/.match(resolution)
|
|
142
|
+
width = match[1].to_i?
|
|
143
|
+
height = match[2].to_i?
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
Variant.new(
|
|
148
|
+
url: resolve(uri_text, base_url),
|
|
149
|
+
bandwidth: parse_i64(attributes["BANDWIDTH"]?),
|
|
150
|
+
average_bandwidth: parse_i64(attributes["AVERAGE-BANDWIDTH"]?),
|
|
151
|
+
width: width,
|
|
152
|
+
height: height,
|
|
153
|
+
codecs: attributes["CODECS"]?,
|
|
154
|
+
audio_group: attributes["AUDIO"]?,
|
|
155
|
+
subtitle_group: attributes["SUBTITLES"]?,
|
|
156
|
+
)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
private def parse_media(lines : Array(String), base_url : URI) : MediaPlaylist
|
|
160
|
+
segments = [] of Segment
|
|
161
|
+
map_url : URI? = nil
|
|
162
|
+
map_byte_range : ByteRange? = nil
|
|
163
|
+
encrypted = false
|
|
164
|
+
ended = false
|
|
165
|
+
duration = 0.0
|
|
166
|
+
pending_duration = 0.0
|
|
167
|
+
pending_title : String? = nil
|
|
168
|
+
pending_range : ByteRange? = nil
|
|
169
|
+
|
|
170
|
+
lines.each do |line|
|
|
171
|
+
case
|
|
172
|
+
when line.starts_with?(EXTINF)
|
|
173
|
+
pending_duration, pending_title = parse_extinf(line[EXTINF.size..])
|
|
174
|
+
when line.starts_with?(KEY)
|
|
175
|
+
attributes = parse_attributes(line[KEY.size..])
|
|
176
|
+
method = attributes["METHOD"]?
|
|
177
|
+
encrypted = true unless method.nil? || method == "NONE"
|
|
178
|
+
when line.starts_with?(MAP)
|
|
179
|
+
attributes = parse_attributes(line[MAP.size..])
|
|
180
|
+
uri = attributes["URI"]?
|
|
181
|
+
raise ManifestError.new("HLS map section is missing a URI") unless uri
|
|
182
|
+
|
|
183
|
+
map_url = resolve(uri, base_url)
|
|
184
|
+
if range = attributes["BYTERANGE"]?
|
|
185
|
+
map_byte_range = parse_byte_range(range)
|
|
186
|
+
end
|
|
187
|
+
when line.starts_with?(BYTERANGE)
|
|
188
|
+
pending_range = parse_byte_range(line[BYTERANGE.size..])
|
|
189
|
+
when line.starts_with?(ENDLIST)
|
|
190
|
+
ended = true
|
|
191
|
+
when line.starts_with?('#')
|
|
192
|
+
next
|
|
193
|
+
else
|
|
194
|
+
segments << Segment.new(
|
|
195
|
+
url: resolve(line, base_url),
|
|
196
|
+
duration: pending_duration,
|
|
197
|
+
title: pending_title,
|
|
198
|
+
byte_range: pending_range,
|
|
199
|
+
)
|
|
200
|
+
duration += pending_duration
|
|
201
|
+
pending_duration = 0.0
|
|
202
|
+
pending_title = nil
|
|
203
|
+
pending_range = nil
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
raise ManifestError.new("HLS media playlist has no segments") if segments.empty?
|
|
208
|
+
|
|
209
|
+
MediaPlaylist.new(
|
|
210
|
+
segments: segments,
|
|
211
|
+
duration_seconds: duration,
|
|
212
|
+
ended: ended,
|
|
213
|
+
encrypted: encrypted,
|
|
214
|
+
map_url: map_url,
|
|
215
|
+
map_byte_range: map_byte_range,
|
|
216
|
+
)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
private def parse_extinf(text : String) : Tuple(Float64, String?)
|
|
220
|
+
duration_text, comma, title = text.partition(',')
|
|
221
|
+
duration = duration_text.to_f?
|
|
222
|
+
unless duration && duration.finite? && duration >= 0
|
|
223
|
+
raise ManifestError.new("invalid HLS segment duration: #{duration_text}")
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
parsed_title = comma.empty? || title.empty? ? nil : title
|
|
227
|
+
{duration, parsed_title}
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
private def parse_byte_range(value : String) : ByteRange
|
|
231
|
+
length_text, separator, offset_text = value.strip.partition('@')
|
|
232
|
+
length = length_text.to_i64?
|
|
233
|
+
raise ManifestError.new("invalid HLS byte range: #{value}") unless length
|
|
234
|
+
|
|
235
|
+
offset = nil
|
|
236
|
+
unless separator.empty?
|
|
237
|
+
offset = offset_text.to_i64?
|
|
238
|
+
raise ManifestError.new("invalid HLS byte range offset: #{value}") unless offset
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
ByteRange.new(length, offset)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
private def parse_i64(value : String?) : Int64?
|
|
245
|
+
value.try(&.to_i64?)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
private def resolve(value : String, base_url : URI) : URI
|
|
249
|
+
uri = base_url.resolve(value)
|
|
250
|
+
unless uri.scheme == "http" || uri.scheme == "https"
|
|
251
|
+
raise ManifestError.new("unsupported HLS URI: #{value}")
|
|
252
|
+
end
|
|
253
|
+
uri
|
|
254
|
+
rescue URI::Error
|
|
255
|
+
raise ManifestError.new("invalid HLS URI: #{value}")
|
|
256
|
+
end
|
|
257
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
require "uri"
|
|
2
|
+
|
|
3
|
+
module Rill::Hls
|
|
4
|
+
struct ByteRange
|
|
5
|
+
getter length : Int64
|
|
6
|
+
getter offset : Int64?
|
|
7
|
+
|
|
8
|
+
def initialize(@length : Int64, @offset : Int64? = nil)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
struct Variant
|
|
13
|
+
getter url : URI
|
|
14
|
+
getter bandwidth : Int64?
|
|
15
|
+
getter average_bandwidth : Int64?
|
|
16
|
+
getter width : Int32?
|
|
17
|
+
getter height : Int32?
|
|
18
|
+
getter codecs : String?
|
|
19
|
+
getter audio_group : String?
|
|
20
|
+
getter subtitle_group : String?
|
|
21
|
+
|
|
22
|
+
def initialize(
|
|
23
|
+
@url : URI,
|
|
24
|
+
@bandwidth : Int64? = nil,
|
|
25
|
+
@average_bandwidth : Int64? = nil,
|
|
26
|
+
@width : Int32? = nil,
|
|
27
|
+
@height : Int32? = nil,
|
|
28
|
+
@codecs : String? = nil,
|
|
29
|
+
@audio_group : String? = nil,
|
|
30
|
+
@subtitle_group : String? = nil,
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
struct Segment
|
|
36
|
+
getter url : URI
|
|
37
|
+
getter duration : Float64
|
|
38
|
+
getter title : String?
|
|
39
|
+
getter byte_range : ByteRange?
|
|
40
|
+
|
|
41
|
+
def initialize(
|
|
42
|
+
@url : URI,
|
|
43
|
+
@duration : Float64 = 0.0,
|
|
44
|
+
@title : String? = nil,
|
|
45
|
+
@byte_range : ByteRange? = nil,
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
struct Rendition
|
|
51
|
+
getter type : String
|
|
52
|
+
getter group_id : String?
|
|
53
|
+
getter name : String?
|
|
54
|
+
getter language : String?
|
|
55
|
+
getter url : URI?
|
|
56
|
+
getter? default : Bool
|
|
57
|
+
getter? autoselect : Bool
|
|
58
|
+
|
|
59
|
+
def initialize(
|
|
60
|
+
@type : String,
|
|
61
|
+
@group_id : String? = nil,
|
|
62
|
+
@name : String? = nil,
|
|
63
|
+
@language : String? = nil,
|
|
64
|
+
@url : URI? = nil,
|
|
65
|
+
@default : Bool = false,
|
|
66
|
+
@autoselect : Bool = false,
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
struct MediaPlaylist
|
|
72
|
+
getter segments : Array(Segment)
|
|
73
|
+
getter map_url : URI?
|
|
74
|
+
getter map_byte_range : ByteRange?
|
|
75
|
+
getter duration_seconds : Float64
|
|
76
|
+
getter ended : Bool
|
|
77
|
+
getter encrypted : Bool
|
|
78
|
+
|
|
79
|
+
def initialize(
|
|
80
|
+
@segments : Array(Segment),
|
|
81
|
+
@duration_seconds : Float64,
|
|
82
|
+
@ended : Bool,
|
|
83
|
+
@encrypted : Bool,
|
|
84
|
+
@map_url : URI? = nil,
|
|
85
|
+
@map_byte_range : ByteRange? = nil,
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
struct MultivariantPlaylist
|
|
91
|
+
getter variants : Array(Variant)
|
|
92
|
+
getter renditions : Array(Rendition)
|
|
93
|
+
|
|
94
|
+
def initialize(
|
|
95
|
+
@variants : Array(Variant),
|
|
96
|
+
@renditions : Array(Rendition) = [] of Rendition,
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|