xbintsc 0.3.69 → 0.3.79

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 (99) hide show
  1. package/README.md +59 -4
  2. package/README.zh-CN.md +52 -2
  3. package/dist/tests/e2e/gui-advanced.test.d.ts +6 -0
  4. package/dist/tests/e2e/gui-advanced.test.js +261 -0
  5. package/dist/tests/e2e/gui-advanced.test.js.map +1 -0
  6. package/dist/tests/e2e/gui-canvas.test.d.ts +9 -0
  7. package/dist/tests/e2e/gui-canvas.test.js +233 -0
  8. package/dist/tests/e2e/gui-canvas.test.js.map +1 -0
  9. package/dist/tests/e2e/gui-example.test.d.ts +1 -1
  10. package/dist/tests/e2e/gui-example.test.js +2 -2
  11. package/dist/tests/e2e/gui-helpers.d.ts +3 -0
  12. package/dist/tests/e2e/gui-helpers.js +5 -0
  13. package/dist/tests/e2e/gui-helpers.js.map +1 -1
  14. package/dist/tests/e2e/gui-layout.test.js +73 -0
  15. package/dist/tests/e2e/gui-layout.test.js.map +1 -1
  16. package/dist/tests/e2e/gui-media.test.d.ts +8 -0
  17. package/dist/tests/e2e/gui-media.test.js +119 -0
  18. package/dist/tests/e2e/gui-media.test.js.map +1 -0
  19. package/dist/tests/e2e/gui-paint.test.js +40 -0
  20. package/dist/tests/e2e/gui-paint.test.js.map +1 -1
  21. package/doc/ai/extensions.md +16 -3
  22. package/doc/ai/zh-CN/extensions.md +10 -3
  23. package/doc/gui/animation.md +46 -0
  24. package/doc/gui/api.md +92 -0
  25. package/doc/gui/build.md +91 -0
  26. package/doc/gui/canvas.md +98 -0
  27. package/doc/gui/dom.md +123 -0
  28. package/doc/gui/input.md +22 -0
  29. package/doc/gui/known-issues.md +66 -0
  30. package/doc/gui/layout.md +72 -0
  31. package/doc/gui/media.md +87 -0
  32. package/doc/gui/rendering.md +98 -0
  33. package/doc/gui/runtime.md +88 -0
  34. package/doc/gui-scripts.md +7 -3
  35. package/doc/gui.md +80 -612
  36. package/doc/implemented.md +1 -0
  37. package/doc/zh-CN/gui/animation.md +46 -0
  38. package/doc/zh-CN/gui/api.md +88 -0
  39. package/doc/zh-CN/gui/build.md +84 -0
  40. package/doc/zh-CN/gui/canvas.md +80 -0
  41. package/doc/zh-CN/gui/dom.md +114 -0
  42. package/doc/zh-CN/gui/input.md +21 -0
  43. package/doc/zh-CN/gui/known-issues.md +50 -0
  44. package/doc/zh-CN/gui/layout.md +60 -0
  45. package/doc/zh-CN/gui/media.md +71 -0
  46. package/doc/zh-CN/gui/rendering.md +81 -0
  47. package/doc/zh-CN/gui/runtime.md +76 -0
  48. package/doc/zh-CN/gui-scripts.md +6 -2
  49. package/doc/zh-CN/gui.md +64 -532
  50. package/doc/zh-CN/implemented.md +1 -0
  51. package/llms.txt +15 -2
  52. package/package.json +1 -1
  53. package/runtime/ext_gui/canvas.cpp +580 -0
  54. package/runtime/ext_gui/canvas.h +170 -0
  55. package/runtime/ext_gui/canvas_internal.h +59 -0
  56. package/runtime/ext_gui/canvas_raster.cpp +254 -0
  57. package/runtime/ext_gui/document.cpp +24 -1
  58. package/runtime/ext_gui/document.h +11 -0
  59. package/runtime/ext_gui/dom_api.cpp +3 -0
  60. package/runtime/ext_gui/dom_api_canvas.cpp +548 -0
  61. package/runtime/ext_gui/dom_api_internal.h +73 -0
  62. package/runtime/ext_gui/dom_api_media.cpp +274 -0
  63. package/runtime/ext_gui/dom_api_proto.cpp +80 -0
  64. package/runtime/ext_gui/gui.cpp +6 -0
  65. package/runtime/ext_gui/gui_canvas.cpp +95 -0
  66. package/runtime/ext_gui/gui_engine.h +49 -0
  67. package/runtime/ext_gui/gui_events.cpp +21 -5
  68. package/runtime/ext_gui/gui_media.cpp +193 -0
  69. package/runtime/ext_gui/image.cpp +31 -12
  70. package/runtime/ext_gui/image.h +20 -7
  71. package/runtime/ext_gui/layout.cpp +52 -5
  72. package/runtime/ext_gui/layout.h +33 -1
  73. package/runtime/ext_gui/layout_flex.cpp +263 -99
  74. package/runtime/ext_gui/layout_flow.cpp +85 -9
  75. package/runtime/ext_gui/layout_flow_internal.h +84 -5
  76. package/runtime/ext_gui/layout_inline.cpp +54 -37
  77. package/runtime/ext_gui/media.cpp +514 -0
  78. package/runtime/ext_gui/media.h +104 -0
  79. package/runtime/ext_gui/nanosvg.h +3278 -0
  80. package/runtime/ext_gui/nanosvgrast.h +1472 -0
  81. package/runtime/ext_gui/paint.cpp +123 -38
  82. package/runtime/ext_gui/paint.h +36 -6
  83. package/runtime/ext_gui/renderer.cpp +113 -10
  84. package/runtime/ext_gui/renderer.h +49 -9
  85. package/runtime/ext_gui/renderer_image.cpp +203 -77
  86. package/runtime/ext_gui/renderer_internal.h +6 -8
  87. package/runtime/ext_gui/renderer_text.cpp +11 -26
  88. package/runtime/ext_gui/style.h +11 -0
  89. package/runtime/ext_gui/style_print.cpp +11 -0
  90. package/runtime/ext_gui/style_ua.cpp +7 -0
  91. package/runtime/ext_gui/style_values.cpp +40 -1
  92. package/runtime/ext_gui/svg.cpp +182 -0
  93. package/runtime/ext_gui/svg.h +46 -0
  94. package/runtime/ext_gui/text.cpp +156 -25
  95. package/runtime/ext_gui/text.h +11 -2
  96. package/runtime/ext_gui/window.cpp +4 -1
  97. package/scripts/build-gui.ts +148 -6
  98. package/scripts/check-file-length.ts +5 -1
  99. package/scripts/install-gui-deps.sh +28 -0
@@ -0,0 +1,514 @@
1
+ /*
2
+ * xbintsc GUI engine — `<video>`/`<audio>` decoding.
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).
9
+ *
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.
16
+ */
17
+
18
+ #include "media.h"
19
+
20
+ #include <algorithm>
21
+ #include <cmath>
22
+ #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
+
40
+ namespace {
41
+
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;
107
+
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();
127
+
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
+ }
367
+
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
+ }
401
+
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
+ }
417
+ }
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;
436
+ }
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
497
+ }
498
+
499
+ 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;
503
+ }
504
+
505
+ 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();
512
+ }
513
+
514
+ } // namespace xtgui
@@ -0,0 +1,104 @@
1
+ #ifndef XT_GUI_MEDIA_H
2
+ #define XT_GUI_MEDIA_H
3
+
4
+ /*
5
+ * Media decoding for `<video>` and `<audio>`.
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:
9
+ *
10
+ * - the latest decoded video frame (RGBA8, `Image`), which the renderer
11
+ * 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.
13
+ *
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.
19
+ *
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*.
23
+ */
24
+
25
+ #include <cstdint>
26
+ #include <memory>
27
+ #include <string>
28
+
29
+ #include "image.h"
30
+
31
+ namespace xtgui {
32
+
33
+ /** DOM events a media element may fire after `tick()`. */
34
+ enum MediaEvent {
35
+ kMediaEventLoadedMetadata = 1 << 0,
36
+ kMediaEventPlay = 1 << 1,
37
+ kMediaEventPause = 1 << 2,
38
+ kMediaEventEnded = 1 << 3,
39
+ kMediaEventTimeUpdate = 1 << 4,
40
+ kMediaEventError = 1 << 5,
41
+ };
42
+
43
+ /**
44
+ * One decoder for one `<video>`/`<audio>` source.
45
+ *
46
+ * 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.
49
+ */
50
+ class MediaPlayer {
51
+ public:
52
+ virtual ~MediaPlayer() {}
53
+
54
+ /** Open `path`; returns `nullptr` (with `*error` set) on failure. */
55
+ static std::unique_ptr<MediaPlayer> open(const std::string& path, std::string* error);
56
+
57
+ /** Process-wide identity used to key GPU textures and the lookup below. */
58
+ virtual uint64_t id() const = 0;
59
+ virtual const std::string& source() const = 0;
60
+ virtual bool has_video() const = 0;
61
+ virtual bool has_audio() const = 0;
62
+ virtual double duration() const = 0;
63
+
64
+ /* Playback state ------------------------------------------------------- */
65
+ virtual bool paused() const = 0;
66
+ virtual bool ended() const = 0;
67
+ virtual double current_time() const = 0;
68
+ virtual double volume() const = 0;
69
+ virtual bool muted() const = 0;
70
+ virtual bool loop() const = 0;
71
+ virtual bool autoplay() const = 0;
72
+
73
+ virtual void play() = 0;
74
+ virtual void pause() = 0;
75
+ virtual void seek(double seconds) = 0;
76
+ virtual void set_volume(double value) = 0;
77
+ virtual void set_muted(bool value) = 0;
78
+ virtual void set_loop(bool value) = 0;
79
+ virtual void set_autoplay(bool value) = 0;
80
+
81
+ /* Video ---------------------------------------------------------------- */
82
+ virtual int frame_width() const = 0;
83
+ virtual int frame_height() const = 0;
84
+ /** Latest decoded RGBA frame, or `nullptr` before the first one is ready. */
85
+ virtual const Image* frame() const = 0;
86
+ /** Bumped every time `frame()` changes, so the renderer can re-upload. */
87
+ virtual uint64_t frame_generation() const = 0;
88
+
89
+ /** Advance the clock, decode/buffer, and return the events that happened. */
90
+ virtual unsigned tick(double now_ms) = 0;
91
+
92
+ /** Drop the cached GPU texture handle (called when it is released). */
93
+ virtual void invalidate_texture() = 0;
94
+ };
95
+
96
+ /** The live player for `id`, or `nullptr`. Used by the renderer. */
97
+ MediaPlayer* xt_media_by_id(uint64_t id);
98
+
99
+ /** Tear down process-wide media state (audio subsystem, id table). */
100
+ void xt_media_shutdown();
101
+
102
+ } // namespace xtgui
103
+
104
+ #endif // XT_GUI_MEDIA_H