xbintsc 0.5.22 → 0.5.23

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 (81) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +15 -5
  3. package/README.zh-CN.md +11 -4
  4. package/dist/src/cli/hints.js +11 -0
  5. package/dist/src/cli/hints.js.map +1 -1
  6. package/dist/src/cli/main.js +1 -1
  7. package/dist/src/driver/compiler.js +1 -0
  8. package/dist/src/driver/compiler.js.map +1 -1
  9. package/dist/src/driver/version.d.ts +1 -1
  10. package/dist/src/driver/version.js +1 -1
  11. package/dist/src/extensions/catalog.js +2 -1
  12. package/dist/src/extensions/catalog.js.map +1 -1
  13. package/dist/src/extensions/media/index.d.ts +27 -0
  14. package/dist/src/extensions/media/index.js +60 -0
  15. package/dist/src/extensions/media/index.js.map +1 -0
  16. package/dist/src/extensions/registry.d.ts +12 -0
  17. package/dist/src/extensions/registry.js +16 -0
  18. package/dist/src/extensions/registry.js.map +1 -1
  19. package/dist/src/index.d.ts +1 -0
  20. package/dist/src/index.js +1 -0
  21. package/dist/src/index.js.map +1 -1
  22. package/dist/tests/cli/hints.test.js +15 -0
  23. package/dist/tests/cli/hints.test.js.map +1 -1
  24. package/dist/tests/cli/process.test.js +12 -0
  25. package/dist/tests/cli/process.test.js.map +1 -1
  26. package/dist/tests/e2e/gui-example-canvas.test.js +3 -1
  27. package/dist/tests/e2e/gui-example-canvas.test.js.map +1 -1
  28. package/dist/tests/e2e/gui-helpers.d.ts +3 -0
  29. package/dist/tests/e2e/gui-helpers.js +2 -0
  30. package/dist/tests/e2e/gui-helpers.js.map +1 -1
  31. package/dist/tests/e2e/gui-media-disabled.test.d.ts +9 -0
  32. package/dist/tests/e2e/gui-media-disabled.test.js +42 -0
  33. package/dist/tests/e2e/gui-media-disabled.test.js.map +1 -0
  34. package/dist/tests/e2e/gui-media.test.d.ts +6 -4
  35. package/dist/tests/e2e/gui-media.test.js +15 -12
  36. package/dist/tests/e2e/gui-media.test.js.map +1 -1
  37. package/dist/tests/e2e/unicode-output.test.d.ts +16 -0
  38. package/dist/tests/e2e/unicode-output.test.js +65 -0
  39. package/dist/tests/e2e/unicode-output.test.js.map +1 -0
  40. package/dist/tests/extensions/media.test.d.ts +1 -0
  41. package/dist/tests/extensions/media.test.js +58 -0
  42. package/dist/tests/extensions/media.test.js.map +1 -0
  43. package/dist/tests/extensions/registry.test.js +8 -0
  44. package/dist/tests/extensions/registry.test.js.map +1 -1
  45. package/dist/tests/lexer/identifiers.test.js +12 -0
  46. package/dist/tests/lexer/identifiers.test.js.map +1 -1
  47. package/dist/tests/lexer/scanner.test.js +8 -0
  48. package/dist/tests/lexer/scanner.test.js.map +1 -1
  49. package/dist/tests/lexer/strings.test.js +30 -0
  50. package/dist/tests/lexer/strings.test.js.map +1 -1
  51. package/doc/ai/cli.md +1 -1
  52. package/doc/ai/extensions.md +35 -23
  53. package/doc/ai/troubleshooting.md +9 -0
  54. package/doc/ai/zh-CN/cli.md +1 -1
  55. package/doc/ai/zh-CN/extensions.md +20 -8
  56. package/doc/ai/zh-CN/troubleshooting.md +8 -0
  57. package/doc/gui/build.md +37 -21
  58. package/doc/gui/known-issues.md +7 -6
  59. package/doc/gui/media.md +26 -11
  60. package/doc/gui.md +43 -33
  61. package/doc/zh-CN/gui/build.md +28 -16
  62. package/doc/zh-CN/gui/known-issues.md +6 -5
  63. package/doc/zh-CN/gui/media.md +21 -10
  64. package/doc/zh-CN/gui.md +37 -32
  65. package/package.json +2 -1
  66. package/runtime/ext_gui/gui_media.cpp +69 -2
  67. package/runtime/ext_gui/media.cpp +34 -488
  68. package/runtime/ext_gui/media.h +54 -19
  69. package/runtime/ext_gui/media_api.h +65 -0
  70. package/runtime/ext_media/media.cpp +507 -0
  71. package/runtime/ext_media/media_bridge.c +35 -0
  72. package/scripts/build-gui.ts +6 -72
  73. package/scripts/build-media.ts +245 -0
  74. package/src/cli/hints.ts +11 -0
  75. package/src/cli/main.ts +1 -1
  76. package/src/driver/compiler.ts +1 -0
  77. package/src/driver/version.ts +1 -1
  78. package/src/extensions/catalog.ts +2 -1
  79. package/src/extensions/media/index.ts +67 -0
  80. package/src/extensions/registry.ts +24 -0
  81. package/src/index.ts +1 -0
@@ -1,514 +1,60 @@
1
1
  /*
2
- * xbintsc GUI engine — `<video>`/`<audio>` decoding.
2
+ * xbintsc GUI engine — media decoder dispatch.
3
3
  *
4
- * This translation unit is the only place that knows about FFmpeg. It is built
5
- * with `-DXT_GUI_WITH_FFMPEG=1` when `scripts/build-gui.ts` managed to build a
6
- * minimal static FFmpeg; without it `MediaPlayer::open` always fails and the
7
- * rest of the engine still compiles and runs (the element simply reports an
8
- * `error` event).
4
+ * The engine models `<video>`/`<audio>` elements, but the decoder is an
5
+ * optional backend supplied by the separate `media` extension. That backend
6
+ * installs its table through `xt_media_install` from a bridge object that is
7
+ * linked directly when the extension is enabled (see
8
+ * `runtime/ext_media/media_bridge.c`), so this translation unit stays tiny and
9
+ * FFmpeg-free.
9
10
  *
10
- * Threading: the engine is single-threaded and its values live on the main
11
- * thread, so nothing here may run on an SDL audio callback. Instead the SDL
12
- * audio device is opened with a *null* callback (push mode): `tick()` decodes
13
- * PCM on the main thread and `SDL_PutAudioStreamData`s it, and SDL pulls from
14
- * the queued buffer on its own thread. `queued_seconds()` keeps the buffer
15
- * shallow so there is no unbounded latency.
11
+ * A program built with `--ext gui` alone leaves `g_api` null. `MediaPlayer::open`
12
+ * then fails with an actionable message and the element fires `error`, exactly
13
+ * as it does for an unreadable file. Nothing here touches GUI state, so it can
14
+ * be compiled independently of SDL and the renderer.
16
15
  */
17
16
 
18
17
  #include "media.h"
19
18
 
20
- #include <algorithm>
21
- #include <cmath>
19
+ #include <cstdio>
22
20
  #include <cstring>
23
- #include <unordered_map>
24
-
25
- #include <SDL3/SDL.h>
26
-
27
- #ifdef XT_GUI_WITH_FFMPEG
28
- extern "C" {
29
- #include <libavcodec/avcodec.h>
30
- #include <libavformat/avformat.h>
31
- #include <libavutil/channel_layout.h>
32
- #include <libavutil/opt.h>
33
- #include <libswresample/swresample.h>
34
- #include <libswscale/swscale.h>
35
- }
36
- #endif
37
-
38
- namespace xtgui {
39
21
 
40
22
  namespace {
41
23
 
42
- std::unordered_map<uint64_t, MediaPlayer*>& PlayerTable() {
43
- static std::unordered_map<uint64_t, MediaPlayer*> table;
44
- return table;
45
- }
46
-
47
- #ifdef XT_GUI_WITH_FFMPEG
48
-
49
- /** Audio format everything is resampled to before it reaches SDL. */
50
- constexpr int kAudioRate = 48000;
51
- constexpr int kAudioChannels = 2;
52
- /** How much audio to keep buffered ahead of the play head. */
53
- constexpr double kAudioAhead = 0.25;
54
- /** Minimum gap between `timeupdate` events. */
55
- constexpr double kTimeUpdateIntervalMs = 250.0;
56
-
57
- double NowMs() { return static_cast<double>(SDL_GetTicks()); }
58
-
59
- uint64_t NextId() {
60
- static uint64_t next = 1;
61
- return next++;
62
- }
63
-
64
- bool EnsureAudioSubsystem() {
65
- static bool tried = false;
66
- static bool ready = false;
67
- if (!tried) {
68
- tried = true;
69
- /* Audio is best effort: a headless CI box has no device. Video still
70
- * plays, and audio is silently dropped. */
71
- ready = SDL_InitSubSystem(SDL_INIT_AUDIO);
72
- }
73
- return ready;
74
- }
75
-
76
- struct QueuedFrame {
77
- Image image;
78
- double pts = 0;
79
- };
80
-
81
- class FfmpegPlayer final : public MediaPlayer {
82
- public:
83
- ~FfmpegPlayer() override {
84
- PlayerTable().erase(id_);
85
- if (audio_stream_) SDL_DestroyAudioStream(audio_stream_);
86
- if (sws_) sws_freeContext(sws_);
87
- if (swr_) swr_free(&swr_);
88
- if (vframe_) av_frame_free(&vframe_);
89
- if (aframe_) av_frame_free(&aframe_);
90
- if (pkt_) av_packet_free(&pkt_);
91
- if (vctx_) avcodec_free_context(&vctx_);
92
- if (actx_) avcodec_free_context(&actx_);
93
- if (fmt_) avformat_close_input(&fmt_);
94
- }
95
-
96
- bool Open(const std::string& path, std::string* error) {
97
- source_ = path;
98
- if (avformat_open_input(&fmt_, path.c_str(), nullptr, nullptr) < 0) {
99
- if (error) *error = "cannot open media: " + path;
100
- return false;
101
- }
102
- if (avformat_find_stream_info(fmt_, nullptr) < 0) {
103
- if (error) *error = "cannot read media metadata: " + path;
104
- return false;
105
- }
106
- duration_ = fmt_->duration > 0 ? static_cast<double>(fmt_->duration) / AV_TIME_BASE : 0.0;
24
+ const XtMediaApi* g_api = nullptr;
107
25
 
108
- vstream_ = av_find_best_stream(fmt_, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
109
- astream_ = av_find_best_stream(fmt_, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);
110
- if (vstream_ < 0 && astream_ < 0) {
111
- if (error) *error = "no audio or video stream: " + path;
112
- return false;
113
- }
114
- if (vstream_ >= 0 && !OpenVideo(fmt_->streams[vstream_])) {
115
- vstream_ = -1;
116
- }
117
- if (astream_ >= 0 && !OpenAudio(fmt_->streams[astream_])) {
118
- astream_ = -1;
119
- }
120
- if (vstream_ < 0 && astream_ < 0) {
121
- if (error) *error = "no supported stream: " + path;
122
- return false;
123
- }
124
- pkt_ = av_packet_alloc();
125
- vframe_ = av_frame_alloc();
126
- aframe_ = av_frame_alloc();
26
+ /** Message length used for a backend's open failure. */
27
+ constexpr size_t kErrorSize = 256;
127
28
 
128
- /* A file whose default duration is unknown falls back to the last
129
- * timestamp of the video stream, which is enough for the `duration`
130
- * property and for end-of-stream detection. */
131
- if (duration_ <= 0 && vstream_ >= 0) {
132
- AVStream* vs = fmt_->streams[vstream_];
133
- if (vs->duration > 0) duration_ = vs->duration * av_q2d(vs->time_base);
134
- }
135
- id_ = NextId();
136
- PlayerTable()[id_] = this;
137
- pending_events_ |= kMediaEventLoadedMetadata;
138
- return true;
139
- }
140
-
141
- uint64_t id() const override { return id_; }
142
- const std::string& source() const override { return source_; }
143
- bool has_video() const override { return vstream_ >= 0; }
144
- bool has_audio() const override { return astream_ >= 0; }
145
- double duration() const override { return duration_; }
146
-
147
- bool paused() const override { return paused_; }
148
- bool ended() const override { return ended_; }
149
- double current_time() const override {
150
- if (paused_) return clock_base_pts_;
151
- return clock_base_pts_ + (NowMs() - clock_base_ms_) / 1000.0;
152
- }
153
- double volume() const override { return volume_; }
154
- bool muted() const override { return muted_; }
155
- bool loop() const override { return loop_; }
156
- bool autoplay() const override { return autoplay_; }
157
-
158
- void play() override {
159
- if (ended_) Seek(0);
160
- if (!paused_) return;
161
- paused_ = false;
162
- clock_base_ms_ = NowMs();
163
- if (audio_stream_) SDL_ResumeAudioStreamDevice(audio_stream_);
164
- pending_events_ |= kMediaEventPlay;
165
- }
166
-
167
- void pause() override {
168
- if (paused_) return;
169
- clock_base_pts_ = current_time();
170
- paused_ = true;
171
- if (audio_stream_) SDL_PauseAudioStreamDevice(audio_stream_);
172
- pending_events_ |= kMediaEventPause;
173
- }
174
-
175
- void seek(double seconds) override { Seek(seconds); }
176
-
177
- void set_volume(double value) override {
178
- volume_ = std::min(1.0, std::max(0.0, value));
179
- ApplyGain();
180
- }
181
- void set_muted(bool value) override {
182
- muted_ = value;
183
- ApplyGain();
184
- }
185
- void set_loop(bool value) override { loop_ = value; }
186
- void set_autoplay(bool value) override { autoplay_ = value; }
187
-
188
- int frame_width() const override { return vctx_ ? vctx_->width : 0; }
189
- int frame_height() const override { return vctx_ ? vctx_->height : 0; }
190
- const Image* frame() const override { return current_valid_ ? &current_ : nullptr; }
191
- uint64_t frame_generation() const override { return generation_; }
192
-
193
- unsigned tick(double now_ms) override {
194
- unsigned events = pending_events_;
195
- pending_events_ = 0;
196
- if (vstream_ < 0 && astream_ < 0) return events;
197
- if (!started_ && autoplay_) {
198
- started_ = true;
199
- play();
200
- events |= pending_events_;
201
- pending_events_ = 0;
202
- }
203
- if (paused_ || ended_) return events;
204
- if (now_ms - last_timeupdate_ms_ >= kTimeUpdateIntervalMs) {
205
- events |= kMediaEventTimeUpdate;
206
- last_timeupdate_ms_ = now_ms;
207
- }
208
- const double t = current_time();
209
- PumpVideo(t);
210
- PumpAudio();
211
- events |= CheckEnded(now_ms);
212
- return events;
213
- }
214
-
215
- void invalidate_texture() override {}
216
-
217
- private:
218
- bool OpenVideo(AVStream* stream) {
219
- const AVCodec* codec = avcodec_find_decoder(stream->codecpar->codec_id);
220
- if (!codec) return false;
221
- vctx_ = avcodec_alloc_context3(codec);
222
- if (!vctx_) return false;
223
- if (avcodec_parameters_to_context(vctx_, stream->codecpar) < 0) return false;
224
- vctx_->pkt_timebase = stream->time_base;
225
- if (avcodec_open2(vctx_, codec, nullptr) < 0) return false;
226
- return true;
227
- }
228
-
229
- bool OpenAudio(AVStream* stream) {
230
- const AVCodec* codec = avcodec_find_decoder(stream->codecpar->codec_id);
231
- if (!codec) return false;
232
- actx_ = avcodec_alloc_context3(codec);
233
- if (!actx_) return false;
234
- if (avcodec_parameters_to_context(actx_, stream->codecpar) < 0) return false;
235
- actx_->pkt_timebase = stream->time_base;
236
- if (avcodec_open2(actx_, codec, nullptr) < 0) return false;
237
-
238
- AVChannelLayout out_layout;
239
- av_channel_layout_default(&out_layout, kAudioChannels);
240
- if (swr_alloc_set_opts2(&swr_, &out_layout, AV_SAMPLE_FMT_FLT, kAudioRate,
241
- &actx_->ch_layout, actx_->sample_fmt, actx_->sample_rate,
242
- 0, nullptr) < 0 ||
243
- !swr_ || swr_init(swr_) < 0) {
244
- return false;
245
- }
246
- av_channel_layout_uninit(&out_layout);
247
-
248
- if (EnsureAudioSubsystem()) {
249
- SDL_AudioSpec spec;
250
- spec.format = SDL_AUDIO_F32;
251
- spec.channels = kAudioChannels;
252
- spec.freq = kAudioRate;
253
- audio_stream_ = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, nullptr, nullptr);
254
- if (audio_stream_) {
255
- SDL_PauseAudioStreamDevice(audio_stream_);
256
- ApplyGain();
257
- }
258
- }
259
- return true;
260
- }
261
-
262
- void ApplyGain() {
263
- if (audio_stream_) SDL_SetAudioStreamGain(audio_stream_, muted_ ? 0.0f : static_cast<float>(volume_));
264
- }
265
-
266
- double queued_seconds() const {
267
- if (!audio_stream_) return 0.0;
268
- const int bytes = SDL_GetAudioStreamQueued(audio_stream_);
269
- return bytes / static_cast<double>(kAudioRate * kAudioChannels * sizeof(float));
270
- }
271
-
272
- double stream_pts(const AVStream* stream, int64_t ts) const {
273
- return static_cast<double>(ts) * av_q2d(stream->time_base);
274
- }
275
-
276
- void Seek(double seconds) {
277
- double target = seconds;
278
- if (duration_ > 0) target = std::min(duration_, std::max(0.0, target));
279
- if (target < 0) target = 0;
280
- clock_base_pts_ = target;
281
- clock_base_ms_ = NowMs();
282
- ended_ = false;
283
- started_ = true;
284
-
285
- if (fmt_) {
286
- const int seek_stream = vstream_ >= 0 ? vstream_ : astream_;
287
- const int64_t ts = static_cast<int64_t>(target / av_q2d(fmt_->streams[seek_stream]->time_base));
288
- if (av_seek_frame(fmt_, seek_stream, ts, AVSEEK_FLAG_BACKWARD) < 0) {
289
- avformat_flush(fmt_);
290
- }
291
- if (vctx_) avcodec_flush_buffers(vctx_);
292
- if (actx_) avcodec_flush_buffers(actx_);
293
- }
294
- if (audio_stream_) SDL_ClearAudioStream(audio_stream_);
295
- video_queue_.clear();
296
- current_valid_ = false;
297
- drop_before_ = vstream_ >= 0 ? target : -1;
298
- reader_eof_ = false;
299
- video_flushed_ = false;
300
- audio_flushed_ = false;
301
- video_drained_ = false;
302
- audio_drained_ = false;
303
- }
304
-
305
- bool ReadPacket() {
306
- const int ret = av_read_frame(fmt_, pkt_);
307
- if (ret < 0) {
308
- reader_eof_ = true;
309
- return false;
310
- }
311
- if (pkt_->stream_index == vstream_) {
312
- avcodec_send_packet(vctx_, pkt_);
313
- DrainVideo();
314
- } else if (pkt_->stream_index == astream_) {
315
- avcodec_send_packet(actx_, pkt_);
316
- DrainAudio();
317
- }
318
- av_packet_unref(pkt_);
319
- return true;
320
- }
321
-
322
- void DrainVideo() {
323
- while (avcodec_receive_frame(vctx_, vframe_) == 0) {
324
- const double pts = stream_pts(fmt_->streams[vstream_], vframe_->best_effort_timestamp);
325
- Image image;
326
- ConvertVideo(vframe_, &image);
327
- video_queue_.push_back({std::move(image), pts});
328
- }
329
- }
330
-
331
- void ConvertVideo(const AVFrame* frame, Image* out) {
332
- const int w = frame->width;
333
- const int h = frame->height;
334
- if (!sws_ || sws_width_ != w || sws_height_ != h || sws_pix_fmt_ != frame->format) {
335
- if (sws_) sws_freeContext(sws_);
336
- sws_ = sws_getContext(w, h, static_cast<AVPixelFormat>(frame->format), w, h, AV_PIX_FMT_RGBA,
337
- SWS_BILINEAR, nullptr, nullptr, nullptr);
338
- sws_width_ = w;
339
- sws_height_ = h;
340
- sws_pix_fmt_ = frame->format;
341
- }
342
- if (!sws_) return;
343
- out->width = w;
344
- out->height = h;
345
- out->pixels.resize(static_cast<size_t>(w) * h * 4);
346
- uint8_t* dst[4] = {out->pixels.data(), nullptr, nullptr, nullptr};
347
- int dst_linesize[4] = {w * 4, 0, 0, 0};
348
- sws_scale(sws_, frame->data, frame->linesize, 0, h, dst, dst_linesize);
349
- }
350
-
351
- void DrainAudio() {
352
- while (avcodec_receive_frame(actx_, aframe_) == 0) {
353
- if (!audio_stream_) continue;
354
- const int capacity = swr_get_out_samples(swr_, aframe_->nb_samples);
355
- if (capacity <= 0) continue;
356
- audio_buf_.resize(static_cast<size_t>(capacity) * kAudioChannels * sizeof(float));
357
- uint8_t* out = audio_buf_.data();
358
- const int got = swr_convert(swr_, &out, capacity,
359
- reinterpret_cast<const uint8_t* const*>(aframe_->extended_data),
360
- aframe_->nb_samples);
361
- if (got > 0) {
362
- const int bytes = got * kAudioChannels * static_cast<int>(sizeof(float));
363
- SDL_PutAudioStreamData(audio_stream_, audio_buf_.data(), bytes);
364
- }
365
- }
366
- }
29
+ } // namespace
367
30
 
368
- void PumpVideo(double t) {
369
- if (vstream_ < 0 || video_drained_) return;
370
- while (true) {
371
- if (video_queue_.empty()) {
372
- if (reader_eof_) {
373
- if (!video_flushed_) {
374
- avcodec_send_packet(vctx_, nullptr);
375
- video_flushed_ = true;
376
- DrainVideo();
377
- continue;
378
- }
379
- video_drained_ = true;
380
- return;
381
- }
382
- if (!ReadPacket() && video_queue_.empty() && reader_eof_) continue;
383
- if (video_queue_.empty()) continue;
384
- }
385
- QueuedFrame& front = video_queue_.front();
386
- if (drop_before_ >= 0 && front.pts < drop_before_ - 0.001) {
387
- video_queue_.erase(video_queue_.begin());
388
- continue;
389
- }
390
- if (front.pts <= t + 0.01) {
391
- current_ = std::move(front.image);
392
- current_valid_ = true;
393
- ++generation_;
394
- video_queue_.erase(video_queue_.begin());
395
- drop_before_ = -1;
396
- } else {
397
- return;
398
- }
399
- }
400
- }
31
+ namespace xtgui {
401
32
 
402
- void PumpAudio() {
403
- if (astream_ < 0 || audio_drained_) return;
404
- while (queued_seconds() < kAudioAhead) {
405
- if (reader_eof_) {
406
- if (!audio_flushed_) {
407
- avcodec_send_packet(actx_, nullptr);
408
- audio_flushed_ = true;
409
- DrainAudio();
410
- continue;
411
- }
412
- audio_drained_ = true;
413
- return;
414
- }
415
- if (!ReadPacket()) return;
416
- }
33
+ std::unique_ptr<MediaPlayer> MediaPlayer::open(const std::string& path, std::string* error,
34
+ std::unique_ptr<AudioSink> audio) {
35
+ if (g_api == nullptr || g_api->open == nullptr) {
36
+ if (error) *error = "media support requires the 'media' extension (pass --ext media)";
37
+ return nullptr;
417
38
  }
418
-
419
- unsigned CheckEnded(double now_ms) {
420
- if (ended_) return 0;
421
- if (!reader_eof_) return 0;
422
- if (vstream_ >= 0 && !video_drained_) return 0;
423
- if (astream_ >= 0 && !audio_drained_) return 0;
424
- if (astream_ >= 0 && queued_seconds() > 0.05) return 0;
425
- if (vstream_ >= 0 && duration_ > 0 && current_time() < duration_ - 0.05) return 0;
426
- if (loop_) {
427
- Seek(0);
428
- play();
429
- return kMediaEventPlay;
430
- }
431
- ended_ = true;
432
- paused_ = true;
433
- clock_base_pts_ = duration_ > 0 ? duration_ : current_time();
434
- last_timeupdate_ms_ = now_ms;
435
- return kMediaEventEnded;
39
+ char message[kErrorSize] = {0};
40
+ auto* player = static_cast<MediaPlayer*>(
41
+ g_api->open(path.c_str(), message, sizeof(message), audio.release()));
42
+ if (player == nullptr && error != nullptr) {
43
+ *error = message[0] != '\0' ? message : "cannot open media: " + path;
436
44
  }
437
-
438
- uint64_t id_ = 0;
439
- std::string source_;
440
- AVFormatContext* fmt_ = nullptr;
441
- AVCodecContext* vctx_ = nullptr;
442
- AVCodecContext* actx_ = nullptr;
443
- int vstream_ = -1;
444
- int astream_ = -1;
445
- AVPacket* pkt_ = nullptr;
446
- AVFrame* vframe_ = nullptr;
447
- AVFrame* aframe_ = nullptr;
448
- SwsContext* sws_ = nullptr;
449
- SwrContext* swr_ = nullptr;
450
- int sws_width_ = 0;
451
- int sws_height_ = 0;
452
- int sws_pix_fmt_ = -1;
453
-
454
- std::vector<QueuedFrame> video_queue_;
455
- Image current_;
456
- bool current_valid_ = false;
457
- uint64_t generation_ = 0;
458
- double drop_before_ = -1;
459
-
460
- SDL_AudioStream* audio_stream_ = nullptr;
461
- std::vector<uint8_t> audio_buf_;
462
-
463
- bool reader_eof_ = false;
464
- bool video_flushed_ = false;
465
- bool audio_flushed_ = false;
466
- bool video_drained_ = false;
467
- bool audio_drained_ = false;
468
-
469
- double duration_ = 0;
470
- double volume_ = 1.0;
471
- bool muted_ = false;
472
- bool loop_ = false;
473
- bool autoplay_ = false;
474
- bool paused_ = true;
475
- bool ended_ = false;
476
- bool started_ = false;
477
- double clock_base_pts_ = 0;
478
- double clock_base_ms_ = 0;
479
- double last_timeupdate_ms_ = -1e9;
480
- unsigned pending_events_ = 0;
481
- };
482
-
483
- #endif // XT_GUI_WITH_FFMPEG
484
-
485
- } // namespace
486
-
487
- std::unique_ptr<MediaPlayer> MediaPlayer::open(const std::string& path, std::string* error) {
488
- #ifdef XT_GUI_WITH_FFMPEG
489
- std::unique_ptr<FfmpegPlayer> player(new FfmpegPlayer());
490
- if (!player->Open(path, error)) return nullptr;
491
- return player;
492
- #else
493
- (void)path;
494
- if (error) *error = "media support was not built into this gui.a";
495
- return nullptr;
496
- #endif
45
+ return std::unique_ptr<MediaPlayer>(player);
497
46
  }
498
47
 
499
48
  MediaPlayer* xt_media_by_id(uint64_t id) {
500
- auto& table = PlayerTable();
501
- const auto it = table.find(id);
502
- return it == table.end() ? nullptr : it->second;
49
+ return g_api != nullptr && g_api->by_id != nullptr
50
+ ? static_cast<MediaPlayer*>(g_api->by_id(id))
51
+ : nullptr;
503
52
  }
504
53
 
505
54
  void xt_media_shutdown() {
506
- /* Move the table out before destroying it: `~FfmpegPlayer` erases its own
507
- * entry, and a destructor mutating the container it is being cleared from
508
- * would invalidate the clearing loop. With an empty table the erases are
509
- * no-ops. */
510
- auto pending = std::move(PlayerTable());
511
- PlayerTable().clear();
55
+ if (g_api != nullptr && g_api->shutdown != nullptr) g_api->shutdown();
512
56
  }
513
57
 
514
58
  } // namespace xtgui
59
+
60
+ extern "C" void xt_media_install(const XtMediaApi* api) { g_api = api; }
@@ -4,29 +4,39 @@
4
4
  /*
5
5
  * Media decoding for `<video>` and `<audio>`.
6
6
  *
7
- * A `MediaPlayer` owns one FFmpeg demux/decode pipeline for a single source
8
- * file. It turns the file into two streams of interest:
7
+ * The engine owns the DOM integration (one `MediaPlayer` per element, owned by
8
+ * the window) and the renderer uploads decoded frames as GPU textures. The
9
+ * decoder itself is optional: `MediaPlayer::open` dispatches through the table
10
+ * the `media` extension installs at startup (see `media_api.h`). A program built
11
+ * with `--ext gui` alone therefore links without FFmpeg and reports an `error`
12
+ * event when a media element is opened.
13
+ *
14
+ * A `MediaPlayer` owns one demux/decode pipeline for a single source file. It
15
+ * turns the file into two streams of interest:
9
16
  *
10
17
  * - the latest decoded video frame (RGBA8, `Image`), which the renderer
11
18
  * uploads as a GPU texture and stretches over the element's box;
12
- * - 48 kHz stereo float PCM, which is pushed into an SDL audio stream.
19
+ * - 48 kHz stereo float PCM, which is written to the caller-provided
20
+ * `AudioSink`.
13
21
  *
14
- * Decoding happens on the main thread (see the threading note in `media.cpp`):
15
- * `tick()` advances the wall clock, decodes whatever is due and returns a
16
- * bitmask of DOM events the caller should dispatch. The engine already runs a
17
- * frame loop, so there is no separate media thread and no interaction with the
18
- * garbage collector.
22
+ * Decoding happens on the main thread (see the threading note in the video
23
+ * extension's `media.cpp`): `tick()` advances the wall clock, decodes whatever
24
+ * is due and returns a bitmask of DOM events the caller should dispatch. The
25
+ * engine already runs a frame loop, so there is no separate media thread and no
26
+ * interaction with the garbage collector.
19
27
  *
20
- * The FFmpeg headers stay inside `media.cpp`; this header only exposes value
21
- * types so the rest of the engine (and `gui_engine.h`) can hold a player
22
- * without pulling in libav*.
28
+ * The FFmpeg headers stay inside the media extension's `media.cpp`; this header
29
+ * only exposes value types so the rest of the engine (and `gui_engine.h`) can
30
+ * hold a player without pulling in libav*.
23
31
  */
24
32
 
33
+ #include <cstddef>
25
34
  #include <cstdint>
26
35
  #include <memory>
27
36
  #include <string>
28
37
 
29
38
  #include "image.h"
39
+ #include "media_api.h"
30
40
 
31
41
  namespace xtgui {
32
42
 
@@ -40,23 +50,48 @@ enum MediaEvent {
40
50
  kMediaEventError = 1 << 5,
41
51
  };
42
52
 
53
+ /**
54
+ * The audio output the decoder writes PCM to.
55
+ *
56
+ * The engine implements it with an SDL audio stream (`gui_media.cpp`), which
57
+ * keeps SDL out of the optional `media.a`. The fixed format is 48 kHz stereo
58
+ * float: the decoder resamples every source stream to it with swresample.
59
+ */
60
+ class AudioSink {
61
+ public:
62
+ virtual ~AudioSink() {}
63
+
64
+ /** Open the output device. Returns false when unavailable (headless CI);
65
+ * playback then continues silently and `write` becomes a no-op. */
66
+ virtual bool open() = 0;
67
+ virtual void write(const void *data, size_t bytes) = 0;
68
+ virtual void clear() = 0;
69
+ virtual void pause() = 0;
70
+ virtual void resume() = 0;
71
+ virtual void set_gain(float gain) = 0;
72
+ /** Seconds of PCM queued ahead of the play head. */
73
+ virtual double queued_seconds() const = 0;
74
+ };
75
+
43
76
  /**
44
77
  * One decoder for one `<video>`/`<audio>` source.
45
78
  *
46
79
  * Instances are created with `open()`, which returns `nullptr` and fills
47
- * `*error` when the file cannot be read or when the build has no decoder
48
- * (`gui.a` built without FFmpeg). All times are seconds.
80
+ * `*error` when the file cannot be read or when the enabled extensions do not
81
+ * include a decoder (the `media` extension). All times are seconds.
49
82
  */
50
83
  class MediaPlayer {
51
84
  public:
52
85
  virtual ~MediaPlayer() {}
53
86
 
54
- /** Open `path`; returns `nullptr` (with `*error` set) on failure. */
55
- static std::unique_ptr<MediaPlayer> open(const std::string& path, std::string* error);
87
+ /** Open `path`, taking ownership of `audio`. Returns `nullptr` (with `*error`
88
+ * set) on failure; the audio sink is released in that case too. */
89
+ static std::unique_ptr<MediaPlayer> open(const std::string &path, std::string *error,
90
+ std::unique_ptr<AudioSink> audio);
56
91
 
57
92
  /** Process-wide identity used to key GPU textures and the lookup below. */
58
93
  virtual uint64_t id() const = 0;
59
- virtual const std::string& source() const = 0;
94
+ virtual const std::string &source() const = 0;
60
95
  virtual bool has_video() const = 0;
61
96
  virtual bool has_audio() const = 0;
62
97
  virtual double duration() const = 0;
@@ -82,7 +117,7 @@ class MediaPlayer {
82
117
  virtual int frame_width() const = 0;
83
118
  virtual int frame_height() const = 0;
84
119
  /** Latest decoded RGBA frame, or `nullptr` before the first one is ready. */
85
- virtual const Image* frame() const = 0;
120
+ virtual const Image *frame() const = 0;
86
121
  /** Bumped every time `frame()` changes, so the renderer can re-upload. */
87
122
  virtual uint64_t frame_generation() const = 0;
88
123
 
@@ -94,9 +129,9 @@ class MediaPlayer {
94
129
  };
95
130
 
96
131
  /** The live player for `id`, or `nullptr`. Used by the renderer. */
97
- MediaPlayer* xt_media_by_id(uint64_t id);
132
+ MediaPlayer *xt_media_by_id(uint64_t id);
98
133
 
99
- /** Tear down process-wide media state (audio subsystem, id table). */
134
+ /** Tear down process-wide media state (id table, decoder state). */
100
135
  void xt_media_shutdown();
101
136
 
102
137
  } // namespace xtgui