liquidsoap-prettier 1.8.2 → 1.8.3
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/.github/workflows/check-formatting.yml +32 -0
- package/README.md +31 -5
- package/package.json +1 -1
- package/src/cli.js +104 -9
- package/tests/liq/audio.liq +460 -0
- package/tests/liq/autocue.liq +1081 -0
- package/tests/liq/clock.liq +14 -0
- package/tests/liq/cron.liq +74 -0
- package/tests/liq/error.liq +48 -0
- package/tests/liq/extra/audio.liq +677 -0
- package/tests/liq/extra/audioscrobbler.liq +482 -0
- package/tests/liq/extra/deprecations.liq +976 -0
- package/tests/liq/extra/externals.liq +196 -0
- package/tests/liq/extra/fades.liq +260 -0
- package/tests/liq/extra/file.liq +66 -0
- package/tests/liq/extra/http.liq +160 -0
- package/tests/liq/extra/interactive.liq +917 -0
- package/tests/liq/extra/metadata.liq +75 -0
- package/tests/liq/extra/native.liq +201 -0
- package/tests/liq/extra/openai.liq +150 -0
- package/tests/liq/extra/server.liq +177 -0
- package/tests/liq/extra/source.liq +476 -0
- package/tests/liq/extra/spinitron.liq +272 -0
- package/tests/liq/extra/telnet.liq +266 -0
- package/tests/liq/extra/video.liq +59 -0
- package/tests/liq/extra/visualization.liq +68 -0
- package/tests/liq/fades.liq +941 -0
- package/tests/liq/ffmpeg.liq +605 -0
- package/tests/liq/file.liq +387 -0
- package/tests/liq/getter.liq +74 -0
- package/tests/liq/hls.liq +329 -0
- package/tests/liq/http.liq +1048 -0
- package/tests/liq/http_codes.liq +447 -0
- package/tests/liq/icecast.liq +58 -0
- package/tests/liq/io.liq +106 -0
- package/tests/liq/liquidsoap.liq +31 -0
- package/tests/liq/list.liq +440 -0
- package/tests/liq/log.liq +47 -0
- package/tests/liq/lufs.liq +295 -0
- package/tests/liq/math.liq +23 -0
- package/tests/liq/medialib.liq +752 -0
- package/tests/liq/metadata.liq +253 -0
- package/tests/liq/nfo.liq +258 -0
- package/tests/liq/null.liq +71 -0
- package/tests/liq/playlist.liq +1347 -0
- package/tests/liq/predicate.liq +106 -0
- package/tests/liq/process.liq +93 -0
- package/tests/liq/profiler.liq +5 -0
- package/tests/liq/protocols.liq +1139 -0
- package/tests/liq/ref.liq +28 -0
- package/tests/liq/replaygain.liq +135 -0
- package/tests/liq/request.liq +467 -0
- package/tests/liq/resolvers.liq +33 -0
- package/tests/liq/runtime.liq +70 -0
- package/tests/liq/server.liq +99 -0
- package/tests/liq/settings.liq +41 -0
- package/tests/liq/socket.liq +33 -0
- package/tests/liq/source.liq +362 -0
- package/tests/liq/sqlite.liq +161 -0
- package/tests/liq/stdlib.liq +172 -0
- package/tests/liq/string.liq +476 -0
- package/tests/liq/switches.liq +197 -0
- package/tests/liq/testing.liq +37 -0
- package/tests/liq/thread.liq +161 -0
- package/tests/liq/tracks.liq +100 -0
- package/tests/liq/utils.liq +81 -0
- package/tests/liq/video.liq +918 -0
|
@@ -0,0 +1,918 @@
|
|
|
1
|
+
# Width for all video frames.
|
|
2
|
+
# @category Source / Video processing
|
|
3
|
+
def video.frame.width =
|
|
4
|
+
settings.frame.video.width
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
# Height for all video frames.
|
|
8
|
+
# @category Source / Video processing
|
|
9
|
+
def video.frame.height =
|
|
10
|
+
settings.frame.video.height
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Framerate for all video frames.
|
|
14
|
+
# @category Source / Video processing
|
|
15
|
+
def video.frame.rate =
|
|
16
|
+
settings.frame.video.framerate
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Generate a source from an image request.
|
|
20
|
+
# @category Source / Video processing
|
|
21
|
+
# @param ~id Force the value of the source ID.
|
|
22
|
+
# @param ~fallible Whether we are allowed to fail (in case the file is non-existent or invalid).
|
|
23
|
+
# @param ~width Scale to width
|
|
24
|
+
# @param ~height Scale to height
|
|
25
|
+
# @param ~x x position.
|
|
26
|
+
# @param ~y y position.
|
|
27
|
+
# @param req Image request
|
|
28
|
+
def request.image(
|
|
29
|
+
~id=null,
|
|
30
|
+
~fallible=false,
|
|
31
|
+
~width=null,
|
|
32
|
+
~height=null,
|
|
33
|
+
~x=getter(0),
|
|
34
|
+
~y=getter(0),
|
|
35
|
+
req
|
|
36
|
+
) =
|
|
37
|
+
last_req = ref(null)
|
|
38
|
+
|
|
39
|
+
def next() =
|
|
40
|
+
req = (getter.get(req) : request)
|
|
41
|
+
|
|
42
|
+
if
|
|
43
|
+
req != last_req()
|
|
44
|
+
then
|
|
45
|
+
last_req := req
|
|
46
|
+
image = request.single(id=id, fallible=fallible, req)
|
|
47
|
+
image = video.crop(image)
|
|
48
|
+
video.resize(id=id, x=x, y=y, width=width, height=height, image)
|
|
49
|
+
else
|
|
50
|
+
null
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
source.dynamic(id=id, track_sensitive=false, next)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Generate a source from an image file.
|
|
58
|
+
# @category Source / Video processing
|
|
59
|
+
# @param ~id Force the value of the source ID.
|
|
60
|
+
# @param ~fallible Whether we are allowed to fail (in case the file is non-existent or invalid).
|
|
61
|
+
# @param ~width Scale to width
|
|
62
|
+
# @param ~height Scale to height
|
|
63
|
+
# @param ~x x position.
|
|
64
|
+
# @param ~y y position.
|
|
65
|
+
# @param file Path to the image.
|
|
66
|
+
# @method set Change the request.
|
|
67
|
+
def image(%argsof(request.image), file) =
|
|
68
|
+
r = getter.map.memoize(fun (file) -> request.create(file), file)
|
|
69
|
+
|
|
70
|
+
request.image(%argsof(request.image), r)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @flag hidden
|
|
74
|
+
let orig_request = request
|
|
75
|
+
|
|
76
|
+
# Add a static request on the given video track.
|
|
77
|
+
# @category Track / Video processing
|
|
78
|
+
# @param ~id Force the value of the source ID.
|
|
79
|
+
# @param ~fallible Whether we are allowed to fail (in case the file is non-existent or invalid).
|
|
80
|
+
# @param ~width Scale to width
|
|
81
|
+
# @param ~height Scale to height
|
|
82
|
+
# @param ~x x position.
|
|
83
|
+
# @param ~y y position.
|
|
84
|
+
# @param ~request Request to add to the video track
|
|
85
|
+
def track.video.add_request(
|
|
86
|
+
~id=null("track.video.add_request"),
|
|
87
|
+
~fallible=false,
|
|
88
|
+
~width=null,
|
|
89
|
+
~height=null,
|
|
90
|
+
~x=getter(0),
|
|
91
|
+
~y=getter(0),
|
|
92
|
+
~request,
|
|
93
|
+
v
|
|
94
|
+
) =
|
|
95
|
+
image =
|
|
96
|
+
orig_request.image(
|
|
97
|
+
id=id,
|
|
98
|
+
fallible=fallible,
|
|
99
|
+
x=x,
|
|
100
|
+
y=y,
|
|
101
|
+
width=width,
|
|
102
|
+
height=height,
|
|
103
|
+
request
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
let {video = image} = source.tracks(image)
|
|
107
|
+
track.video.add([v, image])
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Add a static image on the given video track.
|
|
111
|
+
# @category Track / Video processing
|
|
112
|
+
# @param ~id Force the value of the source ID.
|
|
113
|
+
# @param ~fallible Whether we are allowed to fail (in case the file is non-existent or invalid).
|
|
114
|
+
# @param ~width Scale to width
|
|
115
|
+
# @param ~height Scale to height
|
|
116
|
+
# @param ~x x position.
|
|
117
|
+
# @param ~y y position.
|
|
118
|
+
# @param ~file Path to the image file.
|
|
119
|
+
def track.video.add_image(
|
|
120
|
+
~id=null("track.video.add_image"),
|
|
121
|
+
~fallible=false,
|
|
122
|
+
~width=null,
|
|
123
|
+
~height=null,
|
|
124
|
+
~x=getter(0),
|
|
125
|
+
~y=getter(0),
|
|
126
|
+
~file,
|
|
127
|
+
v
|
|
128
|
+
) =
|
|
129
|
+
image =
|
|
130
|
+
image(id=id, fallible=fallible, x=x, y=y, width=width, height=height, file)
|
|
131
|
+
|
|
132
|
+
let {video = image} = source.tracks(image)
|
|
133
|
+
track.video.add([v, image])
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Add a static request on the source video channel.
|
|
137
|
+
# @category Source / Video processing
|
|
138
|
+
# @param ~id Force the value of the source ID.
|
|
139
|
+
# @param ~fallible Whether we are allowed to fail (in case the file is non-existent or invalid).
|
|
140
|
+
# @param ~width Scale to width
|
|
141
|
+
# @param ~height Scale to height
|
|
142
|
+
# @param ~x x position.
|
|
143
|
+
# @param ~y y position.
|
|
144
|
+
# @param ~request Request to add to the video channel
|
|
145
|
+
def video.add_request(
|
|
146
|
+
~id=null("video.add_request"),
|
|
147
|
+
~fallible=false,
|
|
148
|
+
~width=null,
|
|
149
|
+
~height=null,
|
|
150
|
+
~x=getter(0),
|
|
151
|
+
~y=getter(0),
|
|
152
|
+
~request,
|
|
153
|
+
(s:source)
|
|
154
|
+
) =
|
|
155
|
+
let {video, ...tracks} = source.tracks(s)
|
|
156
|
+
video =
|
|
157
|
+
track.video.add_request(
|
|
158
|
+
fallible=fallible,
|
|
159
|
+
width=width,
|
|
160
|
+
height=height,
|
|
161
|
+
x=x,
|
|
162
|
+
y=y,
|
|
163
|
+
request=request,
|
|
164
|
+
video
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
source(id=id, tracks.{video = video})
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Add a static image on the source video channel.
|
|
171
|
+
# @category Source / Video processing
|
|
172
|
+
# @param ~id Force the value of the source ID.
|
|
173
|
+
# @param ~fallible Whether we are allowed to fail (in case the file is non-existent or invalid).
|
|
174
|
+
# @param ~width Scale to width
|
|
175
|
+
# @param ~height Scale to height
|
|
176
|
+
# @param ~x x position.
|
|
177
|
+
# @param ~y y position.
|
|
178
|
+
# @param ~file Path to the image file.
|
|
179
|
+
def video.add_image(
|
|
180
|
+
~id=null("video.add_image"),
|
|
181
|
+
~fallible=false,
|
|
182
|
+
~width=null,
|
|
183
|
+
~height=null,
|
|
184
|
+
~x=getter(0),
|
|
185
|
+
~y=getter(0),
|
|
186
|
+
~file,
|
|
187
|
+
(s:source)
|
|
188
|
+
) =
|
|
189
|
+
let {video, ...tracks} = source.tracks(s)
|
|
190
|
+
video =
|
|
191
|
+
track.video.add_image(
|
|
192
|
+
fallible=fallible,
|
|
193
|
+
width=width,
|
|
194
|
+
height=height,
|
|
195
|
+
x=x,
|
|
196
|
+
y=y,
|
|
197
|
+
file=file,
|
|
198
|
+
video
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
source(id=id, tracks.{video = video})
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Generate a video source containing cover-art for current track of input audio
|
|
205
|
+
# source.
|
|
206
|
+
# @category Source / Video processing
|
|
207
|
+
# @param s Audio source whose metadata contain cover-art.
|
|
208
|
+
def video.cover(s) =
|
|
209
|
+
last_filename = ref(null)
|
|
210
|
+
last_metadata = source.methods(s).last_metadata
|
|
211
|
+
b = (blank() : source)
|
|
212
|
+
|
|
213
|
+
def next() =
|
|
214
|
+
m = last_metadata() ?? []
|
|
215
|
+
|
|
216
|
+
filename = m["filename"]
|
|
217
|
+
|
|
218
|
+
if
|
|
219
|
+
filename != last_filename()
|
|
220
|
+
then
|
|
221
|
+
last_filename := filename
|
|
222
|
+
|
|
223
|
+
cover =
|
|
224
|
+
if
|
|
225
|
+
file.exists(filename)
|
|
226
|
+
then
|
|
227
|
+
file.cover(filename)
|
|
228
|
+
else
|
|
229
|
+
"".{mime = ""}
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
if
|
|
233
|
+
null.defined(cover) and null.get(cover) != ""
|
|
234
|
+
then
|
|
235
|
+
cover = null.get(cover)
|
|
236
|
+
extname =
|
|
237
|
+
(
|
|
238
|
+
null.defined(cover.mime)
|
|
239
|
+
? file.mime.extension(null.get(cover.mime))
|
|
240
|
+
: null
|
|
241
|
+
)
|
|
242
|
+
?? ".osb"
|
|
243
|
+
f = file.temp("cover", extname)
|
|
244
|
+
log.debug(
|
|
245
|
+
"Found cover for #{filename}."
|
|
246
|
+
)
|
|
247
|
+
file.write(data=cover, f)
|
|
248
|
+
request.once(request.create(temporary=false, f))
|
|
249
|
+
else
|
|
250
|
+
log.debug(
|
|
251
|
+
"No cover for #{filename}."
|
|
252
|
+
)
|
|
253
|
+
b
|
|
254
|
+
end
|
|
255
|
+
else
|
|
256
|
+
null
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
source.dynamic(track_sensitive=false, next)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
let output.youtube = ()
|
|
264
|
+
let output.youtube.live = ()
|
|
265
|
+
|
|
266
|
+
# Stream to youtube using RTMP.
|
|
267
|
+
# @category Source / Output
|
|
268
|
+
# @param ~id Force the value of the source ID.
|
|
269
|
+
# @param ~fallible Allow the child source to fail, in which case the output will be (temporarily) stopped.
|
|
270
|
+
# @param ~start Automatically start outputting whenever possible. If true, an infallible (normal) output will start outputting as soon as it is created, and a fallible output will (re)start as soon as its source becomes available for streaming.
|
|
271
|
+
# @param ~url RTMP URL to stream to
|
|
272
|
+
# @param ~encoder Encoder to use (most likely a `%ffmpeg` encoder)
|
|
273
|
+
# @param ~key Your secret youtube key
|
|
274
|
+
def output.youtube.live.rtmp(
|
|
275
|
+
~id=null,
|
|
276
|
+
~fallible=false,
|
|
277
|
+
~start=true,
|
|
278
|
+
~url="rtmp://a.rtmp.youtube.com/live2",
|
|
279
|
+
~(key:string),
|
|
280
|
+
~encoder,
|
|
281
|
+
s
|
|
282
|
+
) =
|
|
283
|
+
output.url(
|
|
284
|
+
id=id,
|
|
285
|
+
fallible=fallible,
|
|
286
|
+
start=start,
|
|
287
|
+
url="#{url}/#{key}",
|
|
288
|
+
encoder,
|
|
289
|
+
s
|
|
290
|
+
)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Stream to youtube using HLS.
|
|
294
|
+
# @category Source / Output
|
|
295
|
+
# @param ~id Force the value of the source ID.
|
|
296
|
+
# @param ~fallible Allow the child source to fail, in which case the output will be (temporarily) stopped.
|
|
297
|
+
# @param ~start Automatically start outputting whenever possible. If true, an infallible (normal) output will start outputting as soon as it is created, and a fallible output will (re)start as soon as its source becomes available for streaming.
|
|
298
|
+
# @param ~segment_duration Segment duration (in seconds).
|
|
299
|
+
# @param ~segments Number of segments per playlist.
|
|
300
|
+
# @param ~segments_overhead Number of segments to keep after they have been featured in the live playlist.
|
|
301
|
+
# @param ~url HLS URL to stream to
|
|
302
|
+
# @param ~encoder Encoder to use (most likely a `%ffmpeg` encoder)
|
|
303
|
+
# @param ~key Your secret youtube key
|
|
304
|
+
def output.youtube.live.hls(
|
|
305
|
+
~id=null,
|
|
306
|
+
~fallible=false,
|
|
307
|
+
~segment_duration=2.0,
|
|
308
|
+
~segments=4,
|
|
309
|
+
~segments_overhead=4,
|
|
310
|
+
~start=true,
|
|
311
|
+
~url="https://a.upload.youtube.com/http_upload_hls",
|
|
312
|
+
~(key:string),
|
|
313
|
+
~encoder,
|
|
314
|
+
s
|
|
315
|
+
) =
|
|
316
|
+
id = string.id.default(default="output.youtube.live.hls", id)
|
|
317
|
+
|
|
318
|
+
def file_url(fname) =
|
|
319
|
+
"#{url}?cid=#{key}©=0&file=#{fname}"
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def on_file_change({state, path = fname}) =
|
|
323
|
+
if
|
|
324
|
+
(state == "created" or state == "updated")
|
|
325
|
+
and path.basename(fname) != "main.m3u8"
|
|
326
|
+
then
|
|
327
|
+
try
|
|
328
|
+
ignore(http.post(data=file.read(fname), file_url(path.basename(fname))))
|
|
329
|
+
catch err do
|
|
330
|
+
log(
|
|
331
|
+
label=id,
|
|
332
|
+
level=3,
|
|
333
|
+
"Error while uploading: #{err}"
|
|
334
|
+
)
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
tmpdir = file.temp_dir("hls", "")
|
|
340
|
+
on_shutdown({file.rmdir(tmpdir)})
|
|
341
|
+
o =
|
|
342
|
+
output.file.hls(
|
|
343
|
+
id=id,
|
|
344
|
+
start=start,
|
|
345
|
+
fallible=fallible,
|
|
346
|
+
playlist="main.m3u8",
|
|
347
|
+
segment_duration=segment_duration,
|
|
348
|
+
segments=segments,
|
|
349
|
+
segments_overhead=segments_overhead,
|
|
350
|
+
tmpdir,
|
|
351
|
+
[("live", encoder)],
|
|
352
|
+
s
|
|
353
|
+
)
|
|
354
|
+
o.on_file_change(synchronous=false, on_file_change)
|
|
355
|
+
o
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# @flag hidden
|
|
359
|
+
def add_text_builder(f) =
|
|
360
|
+
def at(
|
|
361
|
+
~id=null,
|
|
362
|
+
~duration=null,
|
|
363
|
+
~color=getter(0xffffff),
|
|
364
|
+
~cycle=true,
|
|
365
|
+
~font=null,
|
|
366
|
+
~metadata=null,
|
|
367
|
+
~size=getter(18),
|
|
368
|
+
~speed=0,
|
|
369
|
+
~x=getter(10),
|
|
370
|
+
~y=getter(10),
|
|
371
|
+
~on_cycle={()},
|
|
372
|
+
text,
|
|
373
|
+
s
|
|
374
|
+
) =
|
|
375
|
+
available = s.is_ready
|
|
376
|
+
|
|
377
|
+
# Handle modifying the text with metadata.
|
|
378
|
+
tref = ref(getter.get(text))
|
|
379
|
+
text = null.defined(metadata) ? tref : text
|
|
380
|
+
|
|
381
|
+
def on_metadata(m) =
|
|
382
|
+
if
|
|
383
|
+
null.defined(metadata)
|
|
384
|
+
then
|
|
385
|
+
m = m[null.get(metadata)]
|
|
386
|
+
if m != "" then tref := m end
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
if
|
|
391
|
+
null.defined(metadata)
|
|
392
|
+
then
|
|
393
|
+
s.on_metadata(synchronous=true, on_metadata)
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# Our text source.
|
|
397
|
+
t = f(id=id, duration=duration, color=color, font=font, size=size, text)
|
|
398
|
+
t = video.info(video.crop(t))
|
|
399
|
+
|
|
400
|
+
# Handle scrolling if necessary.
|
|
401
|
+
x =
|
|
402
|
+
if
|
|
403
|
+
speed == 0
|
|
404
|
+
then
|
|
405
|
+
x
|
|
406
|
+
else
|
|
407
|
+
fps = video.frame.rate()
|
|
408
|
+
x = ref(getter.get(x))
|
|
409
|
+
|
|
410
|
+
def x() =
|
|
411
|
+
if
|
|
412
|
+
cycle and x() < 0 - t.width()
|
|
413
|
+
then
|
|
414
|
+
on_cycle()
|
|
415
|
+
x := video.frame.width()
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
x := x() - getter.get(speed) / fps
|
|
419
|
+
x()
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
x
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
t = video.translate(x=x, y=y, t)
|
|
426
|
+
|
|
427
|
+
# Ensure that we fail when s fails.
|
|
428
|
+
t = source.available(t, available)
|
|
429
|
+
|
|
430
|
+
# Add the text to the original source.
|
|
431
|
+
let {video = v, ...tracks} = source.tracks(s)
|
|
432
|
+
let {video = t} = source.tracks(t)
|
|
433
|
+
let v = track.video.add([v, t])
|
|
434
|
+
source(tracks.{video = v})
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
at
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
let video.add_text = ()
|
|
441
|
+
let video.text.available = ref([])
|
|
442
|
+
|
|
443
|
+
# Add a text to a stream (native implementation).
|
|
444
|
+
# @category Source / Video processing
|
|
445
|
+
# @param ~id Force the value of the source ID.
|
|
446
|
+
# @param ~color Text color (in 0xRRGGBB format).
|
|
447
|
+
# @param ~cycle Cycle text when it reaches left boundary.
|
|
448
|
+
# @param ~font Path to ttf font file.
|
|
449
|
+
# @param ~metadata Change text on a particular metadata (empty string means disabled).
|
|
450
|
+
# @param ~size Font size.
|
|
451
|
+
# @param ~speed Horizontal speed in pixels per second (`0` means no scrolling and update according to `x` and `y` in case they are variable).
|
|
452
|
+
# @param ~x x offset.
|
|
453
|
+
# @param ~y y offset.
|
|
454
|
+
# @params d Text to display.
|
|
455
|
+
def video.add_text.native =
|
|
456
|
+
add_text_builder(video.text.native)
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
video.text.available :=
|
|
460
|
+
[("native", video.text.native), ...video.text.available()]
|
|
461
|
+
|
|
462
|
+
%ifdef video.text.gd
|
|
463
|
+
video.text.available := [("gd", video.text.gd), ...video.text.available()]
|
|
464
|
+
|
|
465
|
+
# Add a text to a stream (GD implementation).
|
|
466
|
+
# @category Source / Video processing
|
|
467
|
+
# @param ~id Force the value of the source ID.
|
|
468
|
+
# @param ~color Text color (in 0xRRGGBB format).
|
|
469
|
+
# @param ~cycle Cycle text when it reaches left boundary.
|
|
470
|
+
# @param ~font Path to ttf font file.
|
|
471
|
+
# @param ~metadata Change text on a particular metadata (empty string means disabled).
|
|
472
|
+
# @param ~size Font size.
|
|
473
|
+
# @param ~speed Horizontal speed in pixels per second (`0` means no scrolling and update according to `x` and `y` in case they are variable).
|
|
474
|
+
# @param ~x x offset.
|
|
475
|
+
# @param ~y y offset.
|
|
476
|
+
# @params d Text to display.
|
|
477
|
+
def video.add_text.gd =
|
|
478
|
+
add_text_builder(video.text.gd)
|
|
479
|
+
end
|
|
480
|
+
%endif
|
|
481
|
+
|
|
482
|
+
%ifdef video.text.sdl
|
|
483
|
+
video.text.available := [("sdl", video.text.sdl), ...video.text.available()]
|
|
484
|
+
|
|
485
|
+
# Add a text to a stream (SDL implementation).
|
|
486
|
+
# @category Source / Video processing
|
|
487
|
+
# @param ~id Force the value of the source ID.
|
|
488
|
+
# @param ~color Text color (in 0xRRGGBB format).
|
|
489
|
+
# @param ~cycle Cycle text when it reaches left boundary.
|
|
490
|
+
# @param ~font Path to ttf font file.
|
|
491
|
+
# @param ~metadata Change text on a particular metadata (empty string means disabled).
|
|
492
|
+
# @param ~size Font size.
|
|
493
|
+
# @param ~speed Horizontal speed in pixels per second (`0` means no scrolling and update according to `x` and `y` in case they are variable).
|
|
494
|
+
# @param ~x x offset.
|
|
495
|
+
# @param ~y y offset.
|
|
496
|
+
# @params d Text to display.
|
|
497
|
+
def video.add_text.sdl =
|
|
498
|
+
add_text_builder(video.text.sdl)
|
|
499
|
+
end
|
|
500
|
+
%endif
|
|
501
|
+
|
|
502
|
+
%ifdef video.text.camlimages
|
|
503
|
+
video.text.available :=
|
|
504
|
+
[("camlimages", video.text.camlimages), ...video.text.available()]
|
|
505
|
+
|
|
506
|
+
# Add a text to a stream (camlimages implementation).
|
|
507
|
+
# @category Source / Video processing
|
|
508
|
+
# @param ~id Force the value of the source ID.
|
|
509
|
+
# @param ~color Text color (in 0xRRGGBB format).
|
|
510
|
+
# @param ~cycle Cycle text when it reaches left boundary.
|
|
511
|
+
# @param ~font Path to ttf font file.
|
|
512
|
+
# @param ~metadata Change text on a particular metadata (empty string means disabled).
|
|
513
|
+
# @param ~size Font size.
|
|
514
|
+
# @param ~speed Horizontal speed in pixels per second (`0` means no scrolling and update according to `x` and `y` in case they are variable).
|
|
515
|
+
# @param ~x x offset.
|
|
516
|
+
# @param ~y y offset.
|
|
517
|
+
# @params d Text to display.
|
|
518
|
+
def video.add_text.camlimages =
|
|
519
|
+
add_text_builder(video.text.camlimages)
|
|
520
|
+
end
|
|
521
|
+
%endif
|
|
522
|
+
|
|
523
|
+
let settings.video.text =
|
|
524
|
+
settings.make(
|
|
525
|
+
description="`video.text` implementation.",
|
|
526
|
+
fst(list.hd(video.text.available()))
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
thread.run(
|
|
530
|
+
(
|
|
531
|
+
fun () ->
|
|
532
|
+
begin
|
|
533
|
+
text = settings.video.text()
|
|
534
|
+
if
|
|
535
|
+
list.assoc.mem(text, video.text.available())
|
|
536
|
+
then
|
|
537
|
+
log.important(
|
|
538
|
+
label="video.text",
|
|
539
|
+
"Using #{text} implementation"
|
|
540
|
+
)
|
|
541
|
+
else
|
|
542
|
+
log.severe(
|
|
543
|
+
label="video.text",
|
|
544
|
+
"Cannot find #{text} implementation for `video.text`, using default #{
|
|
545
|
+
fst(list.hd(video.text.available()))
|
|
546
|
+
}"
|
|
547
|
+
)
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
)
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
# Display a text using the first available operator in: camlimages, SDL, FFmpeg, gd or native.
|
|
554
|
+
# @category Source / Video processing
|
|
555
|
+
# @param ~id Force the value of the source ID.
|
|
556
|
+
# @param ~color Text color (in 0xRRGGBB format).
|
|
557
|
+
# @param ~duration Duration in seconds (`null` means infinite).
|
|
558
|
+
# @param ~font Path to ttf font file.
|
|
559
|
+
# @param ~size Font size.
|
|
560
|
+
# @param text Text to display.
|
|
561
|
+
def replaces video.text(
|
|
562
|
+
~id=null,
|
|
563
|
+
~color=getter(0xffffff),
|
|
564
|
+
~duration=null,
|
|
565
|
+
~font=null,
|
|
566
|
+
~size=getter(18),
|
|
567
|
+
text
|
|
568
|
+
) =
|
|
569
|
+
f =
|
|
570
|
+
list.assoc(
|
|
571
|
+
default=snd(list.hd(video.text.available())),
|
|
572
|
+
settings.video.text(),
|
|
573
|
+
video.text.available()
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
f(id=id, color=color, duration=duration, font=font, size=size, text)
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
# Add a text to a stream. Uses the first available operator in: camlimages, SDL,
|
|
580
|
+
# FFmpeg, gd or native.
|
|
581
|
+
# @category Source / Video processing
|
|
582
|
+
# @param ~id Force the value of the source ID.
|
|
583
|
+
# @param ~color Text color (in 0xRRGGBB format).
|
|
584
|
+
# @param ~cycle Cycle text when it reaches left boundary.
|
|
585
|
+
# @param ~font Path to ttf font file.
|
|
586
|
+
# @param ~metadata Change text on a particular metadata (empty string means disabled).
|
|
587
|
+
# @param ~size Font size.
|
|
588
|
+
# @param ~speed Horizontal speed in pixels per second (`0` means no scrolling and update according to `x` and `y` in case they are variable).
|
|
589
|
+
# @param ~x x offset.
|
|
590
|
+
# @param ~y y offset.
|
|
591
|
+
# @param ~on_cycle Function called when text is cycling.
|
|
592
|
+
# @params d Text to display.
|
|
593
|
+
def replaces video.add_text(
|
|
594
|
+
~id=null,
|
|
595
|
+
~duration=null,
|
|
596
|
+
~color=0xffffff,
|
|
597
|
+
~cycle=true,
|
|
598
|
+
~font=null,
|
|
599
|
+
~metadata=null,
|
|
600
|
+
~size=18,
|
|
601
|
+
~speed=0,
|
|
602
|
+
~x=getter(10),
|
|
603
|
+
~y=getter(10),
|
|
604
|
+
~on_cycle={()},
|
|
605
|
+
d,
|
|
606
|
+
s
|
|
607
|
+
) =
|
|
608
|
+
add_text = add_text_builder(video.text)
|
|
609
|
+
add_text(
|
|
610
|
+
id=id,
|
|
611
|
+
duration=duration,
|
|
612
|
+
cycle=cycle,
|
|
613
|
+
font=font,
|
|
614
|
+
metadata=metadata,
|
|
615
|
+
size=size,
|
|
616
|
+
color=color,
|
|
617
|
+
speed=speed,
|
|
618
|
+
x=x,
|
|
619
|
+
y=y,
|
|
620
|
+
on_cycle=on_cycle,
|
|
621
|
+
d,
|
|
622
|
+
s
|
|
623
|
+
)
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
# Add subtitle from metadata.
|
|
627
|
+
# @category Source / Video processing
|
|
628
|
+
# @param ~override Metadata where subtitle to display are located.
|
|
629
|
+
# @param ~offset Offset in pixels.
|
|
630
|
+
# @param s Source.
|
|
631
|
+
def video.add_subtitle(
|
|
632
|
+
~override="subtitle",
|
|
633
|
+
~size=18,
|
|
634
|
+
~color=0xffffff,
|
|
635
|
+
~offset=20,
|
|
636
|
+
s
|
|
637
|
+
) =
|
|
638
|
+
subtitle = ref("")
|
|
639
|
+
t = video.text(size=size, color=color, subtitle)
|
|
640
|
+
t = video.bounding_box(t)
|
|
641
|
+
x = {(video.frame.width() - t.width()) / 2}
|
|
642
|
+
y = {video.frame.height() - t.height() - offset}
|
|
643
|
+
t = video.translate(x=x, y=y, t)
|
|
644
|
+
|
|
645
|
+
def meta(m) =
|
|
646
|
+
if list.assoc.mem(override, m) then subtitle := list.assoc(override, m) end
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
s.on_metadata(synchronous=true, meta)
|
|
650
|
+
let {video = v, ...tracks} = source.tracks(s)
|
|
651
|
+
let {video = t} = source.tracks(t)
|
|
652
|
+
let v = track.video.add([v, t])
|
|
653
|
+
source(tracks.{video = v})
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
# Display a slideshow (typically of pictures).
|
|
657
|
+
# @category Source / Video processing
|
|
658
|
+
# @param ~cyclic Go to the first picture after the last.
|
|
659
|
+
# @param ~advance Skip to the next file after this amount of time in seconds (negative means never).
|
|
660
|
+
# @param l List of files to display.
|
|
661
|
+
# @method append Append a list of files to the slideshow.
|
|
662
|
+
# @method clear Clear the list of files in the slideshow.
|
|
663
|
+
# @method next Go to next file.
|
|
664
|
+
# @method prev Go to previous file.
|
|
665
|
+
# @method current Currently displayed file.
|
|
666
|
+
def video.slideshow(
|
|
667
|
+
~id=null,
|
|
668
|
+
~cyclic=getter(true),
|
|
669
|
+
~advance=getter(-1.),
|
|
670
|
+
l=[]
|
|
671
|
+
) =
|
|
672
|
+
id = string.id.default(default="video.slideshow", id)
|
|
673
|
+
l = ref(l)
|
|
674
|
+
n = ref(-1)
|
|
675
|
+
|
|
676
|
+
next_source = ref(null)
|
|
677
|
+
|
|
678
|
+
def next() =
|
|
679
|
+
s = next_source()
|
|
680
|
+
next_source := null
|
|
681
|
+
s
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
s = source.dynamic(next)
|
|
685
|
+
|
|
686
|
+
def current() =
|
|
687
|
+
list.nth(l(), n())
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
# Set current file to the nth.
|
|
691
|
+
def set(n') =
|
|
692
|
+
if
|
|
693
|
+
0 <= n' and n' < list.length(l()) and n' != n()
|
|
694
|
+
then
|
|
695
|
+
n := n'
|
|
696
|
+
new_source = request.once(request.create(current()))
|
|
697
|
+
new_source = s.prepare(new_source)
|
|
698
|
+
next_source := new_source
|
|
699
|
+
end
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
def next() =
|
|
703
|
+
log.debug(
|
|
704
|
+
label=id,
|
|
705
|
+
"Going to next file"
|
|
706
|
+
)
|
|
707
|
+
n' = n() + 1
|
|
708
|
+
n' =
|
|
709
|
+
if
|
|
710
|
+
n' >= list.length(l())
|
|
711
|
+
then
|
|
712
|
+
if getter.get(cyclic) then 0 else list.length(l()) - 1 end
|
|
713
|
+
else
|
|
714
|
+
n'
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
set(n')
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
def prev() =
|
|
721
|
+
log.debug(
|
|
722
|
+
label=id,
|
|
723
|
+
"Going to previous file"
|
|
724
|
+
)
|
|
725
|
+
n' = n() - 1
|
|
726
|
+
n' =
|
|
727
|
+
if
|
|
728
|
+
n' < 0
|
|
729
|
+
then
|
|
730
|
+
if getter.get(cyclic) then list.length(l()) - 1 else 0 end
|
|
731
|
+
else
|
|
732
|
+
n'
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
set(n')
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
def clear() =
|
|
739
|
+
l := []
|
|
740
|
+
n := 0
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
def append(l') =
|
|
744
|
+
l := list.append(l(), l')
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
set(0)
|
|
748
|
+
if
|
|
749
|
+
getter.get(advance) >= 0.
|
|
750
|
+
then
|
|
751
|
+
thread.run(delay=getter.get(advance), every=advance, next)
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
s.{
|
|
755
|
+
append = append,
|
|
756
|
+
clear = clear,
|
|
757
|
+
next = next,
|
|
758
|
+
prev = prev,
|
|
759
|
+
current = current
|
|
760
|
+
}
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
# Generate a video filled with given color.
|
|
764
|
+
# @category Source / Video processing
|
|
765
|
+
# @param color Color (in 0xRRGGBB format).
|
|
766
|
+
def video.color(color) =
|
|
767
|
+
video.fill(color=color, blank())
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
# Tile sources
|
|
771
|
+
# @category Source / Video processing
|
|
772
|
+
# @argsof track.video.tile
|
|
773
|
+
# @argsof track.audio.add[!id]
|
|
774
|
+
def video.tile(
|
|
775
|
+
~id=null("video.tile"),
|
|
776
|
+
%argsof(track.audio.add[!id]),
|
|
777
|
+
%argsof(track.video.tile[!id]),
|
|
778
|
+
~weights=[],
|
|
779
|
+
sources
|
|
780
|
+
) =
|
|
781
|
+
tracks = list.map(fun (s) -> source.tracks(s), sources)
|
|
782
|
+
video_tracks = list.map(fun (t) -> t.video, tracks)
|
|
783
|
+
new_tracks =
|
|
784
|
+
{video = track.video.tile(%argsof(track.video.tile[!id]), video_tracks)}
|
|
785
|
+
|
|
786
|
+
new_tracks =
|
|
787
|
+
if
|
|
788
|
+
list.length(tracks) != 0 and null.defined(list.hd(tracks)?.audio)
|
|
789
|
+
then
|
|
790
|
+
def mk_audio_track(pos, track) =
|
|
791
|
+
weight =
|
|
792
|
+
try
|
|
793
|
+
list.nth(weights, pos)
|
|
794
|
+
catch _ do
|
|
795
|
+
getter(1.)
|
|
796
|
+
end
|
|
797
|
+
|
|
798
|
+
null.get(track?.audio).{weight = weight}
|
|
799
|
+
end
|
|
800
|
+
|
|
801
|
+
audio_tracks = list.mapi(mk_audio_track, tracks)
|
|
802
|
+
new_tracks.{
|
|
803
|
+
audio = track.audio.add(%argsof(track.audio.add[!id]), audio_tracks)
|
|
804
|
+
}
|
|
805
|
+
else
|
|
806
|
+
new_tracks
|
|
807
|
+
end
|
|
808
|
+
|
|
809
|
+
source(id=id, new_tracks)
|
|
810
|
+
end
|
|
811
|
+
|
|
812
|
+
# Plot a floating point value.
|
|
813
|
+
# @category Source / Video processing
|
|
814
|
+
# @param ~color Color of the drawn point (in 0xRRGGBB format).
|
|
815
|
+
# @param ~lines Draw lines connecting plotted points.
|
|
816
|
+
# @param ~min Minimal value of the parameter.
|
|
817
|
+
# @param ~max Maximal value of the parameter.
|
|
818
|
+
# @param ~speed Speed in pixels per second.
|
|
819
|
+
# @param y Value to plot.
|
|
820
|
+
def video.plot(~lines=true, ~min=0., ~max=1., ~speed=100., ~color=0xffffff, y) =
|
|
821
|
+
width = video.frame.width()
|
|
822
|
+
height = video.frame.height()
|
|
823
|
+
s = video.board(width=width * 3, height=height)
|
|
824
|
+
height = float(s.height())
|
|
825
|
+
tx = ref(width)
|
|
826
|
+
ty = ref(0)
|
|
827
|
+
s' = video.translate(x=tx, y=ty, s)
|
|
828
|
+
|
|
829
|
+
# offset of the currently drawn point.
|
|
830
|
+
x = ref(0)
|
|
831
|
+
dx = int_of_float(speed * frame.duration())
|
|
832
|
+
|
|
833
|
+
def update() =
|
|
834
|
+
tx := tx() - dx
|
|
835
|
+
y = int_of_float(((max - y()) / (max - min)) * height)
|
|
836
|
+
if
|
|
837
|
+
lines
|
|
838
|
+
then
|
|
839
|
+
s.line_to(color=color, x(), y)
|
|
840
|
+
else
|
|
841
|
+
s.pixel(x(), y) := color
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
x := x() + dx
|
|
845
|
+
if
|
|
846
|
+
x() > width * 2
|
|
847
|
+
then
|
|
848
|
+
s.clear_and_copy(x=0 - width)
|
|
849
|
+
tx := tx() + width
|
|
850
|
+
x := x() - width
|
|
851
|
+
end
|
|
852
|
+
end
|
|
853
|
+
|
|
854
|
+
s'.on_frame(synchronous=true, update)
|
|
855
|
+
s'
|
|
856
|
+
end
|
|
857
|
+
|
|
858
|
+
let video.canvas = ()
|
|
859
|
+
|
|
860
|
+
# Create a virtual canvas that can be used to return video position and sizes
|
|
861
|
+
# that are independent of the frame's dimensions.
|
|
862
|
+
# @category Source / Video processing
|
|
863
|
+
# @param ~virtual_width Virtual height, in pixels, of the canvas
|
|
864
|
+
# @param ~actual_size Actual size, in pixels, of the canvas
|
|
865
|
+
# @param ~font_size Font size, in virtual pixels.
|
|
866
|
+
# @method ~px Map a virtual size in pixel to the actual size.
|
|
867
|
+
# @method ~rem Map a fraction of the virtual font size into an actual font size
|
|
868
|
+
# @method ~vh Return a position in percent (as a value between `0.` and `1.`) \
|
|
869
|
+
# of the canvas height
|
|
870
|
+
# @method ~vw Return a position in percent (as a value between `0.` and `1.`) \
|
|
871
|
+
# of the canvas width
|
|
872
|
+
def video.canvas.make(~virtual_width, ~actual_size, ~font_size) =
|
|
873
|
+
virtual_width = float(virtual_width)
|
|
874
|
+
actual_height = float(actual_size.height)
|
|
875
|
+
actual_width = float(actual_size.width)
|
|
876
|
+
ratio = actual_width / virtual_width
|
|
877
|
+
font_ratio = float(font_size) * ratio
|
|
878
|
+
|
|
879
|
+
def px((v:int)) =
|
|
880
|
+
int_of_float(float(v) * ratio)
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
def vh(v) =
|
|
884
|
+
int_of_float(v * actual_height)
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
def vw(v) =
|
|
888
|
+
int_of_float(v * (float(actual_width)))
|
|
889
|
+
end
|
|
890
|
+
|
|
891
|
+
def rem(v) =
|
|
892
|
+
int_of_float(v * font_ratio)
|
|
893
|
+
end
|
|
894
|
+
|
|
895
|
+
{px = px, rem = rem, vw = vw, vh = vh, ...actual_size}
|
|
896
|
+
end
|
|
897
|
+
|
|
898
|
+
# Standard video canvas based off a `10k` virtual canvas.
|
|
899
|
+
# @category Source / Video processing
|
|
900
|
+
def video.canvas.virtual_10k =
|
|
901
|
+
def make(width, height) =
|
|
902
|
+
video.canvas.make(
|
|
903
|
+
virtual_width=10000,
|
|
904
|
+
actual_size={width = width, height = height},
|
|
905
|
+
font_size=160
|
|
906
|
+
)
|
|
907
|
+
end
|
|
908
|
+
|
|
909
|
+
{
|
|
910
|
+
actual_360p = make(640, 360),
|
|
911
|
+
actual_480p = make(640, 480),
|
|
912
|
+
actual_720p = make(1280, 720),
|
|
913
|
+
actual_1080p = make(1920, 1080),
|
|
914
|
+
actual_1440p = make(2560, 1440),
|
|
915
|
+
actual_4k = make(3840, 2160),
|
|
916
|
+
actual_8k = make(7680, 4320)
|
|
917
|
+
}
|
|
918
|
+
end
|