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.
Files changed (67) hide show
  1. package/.github/workflows/check-formatting.yml +32 -0
  2. package/README.md +31 -5
  3. package/package.json +1 -1
  4. package/src/cli.js +104 -9
  5. package/tests/liq/audio.liq +460 -0
  6. package/tests/liq/autocue.liq +1081 -0
  7. package/tests/liq/clock.liq +14 -0
  8. package/tests/liq/cron.liq +74 -0
  9. package/tests/liq/error.liq +48 -0
  10. package/tests/liq/extra/audio.liq +677 -0
  11. package/tests/liq/extra/audioscrobbler.liq +482 -0
  12. package/tests/liq/extra/deprecations.liq +976 -0
  13. package/tests/liq/extra/externals.liq +196 -0
  14. package/tests/liq/extra/fades.liq +260 -0
  15. package/tests/liq/extra/file.liq +66 -0
  16. package/tests/liq/extra/http.liq +160 -0
  17. package/tests/liq/extra/interactive.liq +917 -0
  18. package/tests/liq/extra/metadata.liq +75 -0
  19. package/tests/liq/extra/native.liq +201 -0
  20. package/tests/liq/extra/openai.liq +150 -0
  21. package/tests/liq/extra/server.liq +177 -0
  22. package/tests/liq/extra/source.liq +476 -0
  23. package/tests/liq/extra/spinitron.liq +272 -0
  24. package/tests/liq/extra/telnet.liq +266 -0
  25. package/tests/liq/extra/video.liq +59 -0
  26. package/tests/liq/extra/visualization.liq +68 -0
  27. package/tests/liq/fades.liq +941 -0
  28. package/tests/liq/ffmpeg.liq +605 -0
  29. package/tests/liq/file.liq +387 -0
  30. package/tests/liq/getter.liq +74 -0
  31. package/tests/liq/hls.liq +329 -0
  32. package/tests/liq/http.liq +1048 -0
  33. package/tests/liq/http_codes.liq +447 -0
  34. package/tests/liq/icecast.liq +58 -0
  35. package/tests/liq/io.liq +106 -0
  36. package/tests/liq/liquidsoap.liq +31 -0
  37. package/tests/liq/list.liq +440 -0
  38. package/tests/liq/log.liq +47 -0
  39. package/tests/liq/lufs.liq +295 -0
  40. package/tests/liq/math.liq +23 -0
  41. package/tests/liq/medialib.liq +752 -0
  42. package/tests/liq/metadata.liq +253 -0
  43. package/tests/liq/nfo.liq +258 -0
  44. package/tests/liq/null.liq +71 -0
  45. package/tests/liq/playlist.liq +1347 -0
  46. package/tests/liq/predicate.liq +106 -0
  47. package/tests/liq/process.liq +93 -0
  48. package/tests/liq/profiler.liq +5 -0
  49. package/tests/liq/protocols.liq +1139 -0
  50. package/tests/liq/ref.liq +28 -0
  51. package/tests/liq/replaygain.liq +135 -0
  52. package/tests/liq/request.liq +467 -0
  53. package/tests/liq/resolvers.liq +33 -0
  54. package/tests/liq/runtime.liq +70 -0
  55. package/tests/liq/server.liq +99 -0
  56. package/tests/liq/settings.liq +41 -0
  57. package/tests/liq/socket.liq +33 -0
  58. package/tests/liq/source.liq +362 -0
  59. package/tests/liq/sqlite.liq +161 -0
  60. package/tests/liq/stdlib.liq +172 -0
  61. package/tests/liq/string.liq +476 -0
  62. package/tests/liq/switches.liq +197 -0
  63. package/tests/liq/testing.liq +37 -0
  64. package/tests/liq/thread.liq +161 -0
  65. package/tests/liq/tracks.liq +100 -0
  66. package/tests/liq/utils.liq +81 -0
  67. 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}&copy=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