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.
- package/AGENTS.md +1 -1
- package/README.md +15 -5
- package/README.zh-CN.md +11 -4
- package/dist/src/cli/hints.js +11 -0
- package/dist/src/cli/hints.js.map +1 -1
- package/dist/src/cli/main.js +1 -1
- package/dist/src/driver/compiler.js +1 -0
- package/dist/src/driver/compiler.js.map +1 -1
- package/dist/src/driver/version.d.ts +1 -1
- package/dist/src/driver/version.js +1 -1
- package/dist/src/extensions/catalog.js +2 -1
- package/dist/src/extensions/catalog.js.map +1 -1
- package/dist/src/extensions/media/index.d.ts +27 -0
- package/dist/src/extensions/media/index.js +60 -0
- package/dist/src/extensions/media/index.js.map +1 -0
- package/dist/src/extensions/registry.d.ts +12 -0
- package/dist/src/extensions/registry.js +16 -0
- package/dist/src/extensions/registry.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/tests/cli/hints.test.js +15 -0
- package/dist/tests/cli/hints.test.js.map +1 -1
- package/dist/tests/cli/process.test.js +12 -0
- package/dist/tests/cli/process.test.js.map +1 -1
- package/dist/tests/e2e/gui-example-canvas.test.js +3 -1
- package/dist/tests/e2e/gui-example-canvas.test.js.map +1 -1
- package/dist/tests/e2e/gui-helpers.d.ts +3 -0
- package/dist/tests/e2e/gui-helpers.js +2 -0
- package/dist/tests/e2e/gui-helpers.js.map +1 -1
- package/dist/tests/e2e/gui-media-disabled.test.d.ts +9 -0
- package/dist/tests/e2e/gui-media-disabled.test.js +42 -0
- package/dist/tests/e2e/gui-media-disabled.test.js.map +1 -0
- package/dist/tests/e2e/gui-media.test.d.ts +6 -4
- package/dist/tests/e2e/gui-media.test.js +15 -12
- package/dist/tests/e2e/gui-media.test.js.map +1 -1
- package/dist/tests/e2e/unicode-output.test.d.ts +16 -0
- package/dist/tests/e2e/unicode-output.test.js +65 -0
- package/dist/tests/e2e/unicode-output.test.js.map +1 -0
- package/dist/tests/extensions/media.test.d.ts +1 -0
- package/dist/tests/extensions/media.test.js +58 -0
- package/dist/tests/extensions/media.test.js.map +1 -0
- package/dist/tests/extensions/registry.test.js +8 -0
- package/dist/tests/extensions/registry.test.js.map +1 -1
- package/dist/tests/lexer/identifiers.test.js +12 -0
- package/dist/tests/lexer/identifiers.test.js.map +1 -1
- package/dist/tests/lexer/scanner.test.js +8 -0
- package/dist/tests/lexer/scanner.test.js.map +1 -1
- package/dist/tests/lexer/strings.test.js +30 -0
- package/dist/tests/lexer/strings.test.js.map +1 -1
- package/doc/ai/cli.md +1 -1
- package/doc/ai/extensions.md +35 -23
- package/doc/ai/troubleshooting.md +9 -0
- package/doc/ai/zh-CN/cli.md +1 -1
- package/doc/ai/zh-CN/extensions.md +20 -8
- package/doc/ai/zh-CN/troubleshooting.md +8 -0
- package/doc/gui/build.md +37 -21
- package/doc/gui/known-issues.md +7 -6
- package/doc/gui/media.md +26 -11
- package/doc/gui.md +43 -33
- package/doc/zh-CN/gui/build.md +28 -16
- package/doc/zh-CN/gui/known-issues.md +6 -5
- package/doc/zh-CN/gui/media.md +21 -10
- package/doc/zh-CN/gui.md +37 -32
- package/package.json +2 -1
- package/runtime/ext_gui/gui_media.cpp +69 -2
- package/runtime/ext_gui/media.cpp +34 -488
- package/runtime/ext_gui/media.h +54 -19
- package/runtime/ext_gui/media_api.h +65 -0
- package/runtime/ext_media/media.cpp +507 -0
- package/runtime/ext_media/media_bridge.c +35 -0
- package/scripts/build-gui.ts +6 -72
- package/scripts/build-media.ts +245 -0
- package/src/cli/hints.ts +11 -0
- package/src/cli/main.ts +1 -1
- package/src/driver/compiler.ts +1 -0
- package/src/driver/version.ts +1 -1
- package/src/extensions/catalog.ts +2 -1
- package/src/extensions/media/index.ts +67 -0
- package/src/extensions/registry.ts +24 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#ifndef XT_GUI_MEDIA_API_H
|
|
2
|
+
#define XT_GUI_MEDIA_API_H
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* The C ABI between the GUI engine and an optional media decoder backend.
|
|
6
|
+
*
|
|
7
|
+
* The engine knows how to model `<video>`/`<audio>` elements and how to draw
|
|
8
|
+
* their frames, but it deliberately contains no decoder: FFmpeg lives in the
|
|
9
|
+
* separate `media` compile extension (`runtime/ext_media/`, linked as
|
|
10
|
+
* `media.a`). Keeping the boundary C-only means the backend can be a prebuilt
|
|
11
|
+
* C++ archive that is compiled independently of the engine.
|
|
12
|
+
*
|
|
13
|
+
* Registration is push-based: the backend's bridge translation unit is linked
|
|
14
|
+
* directly (it is a `runtimeSources()` entry of the media extension), and a
|
|
15
|
+
* constructor calls `xt_media_install` with its function table. The GUI's
|
|
16
|
+
* dispatch layer (`media.cpp`) starts with a null table, so a program built
|
|
17
|
+
* with `--ext gui` alone still links and simply reports that media support was
|
|
18
|
+
* not enabled.
|
|
19
|
+
*
|
|
20
|
+
* `audio_sink` is an opaque `xtgui::AudioSink*` owned by the caller until the
|
|
21
|
+
* open call; the backend takes ownership whether the open succeeds or not.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
#include <stddef.h>
|
|
25
|
+
#include <stdint.h>
|
|
26
|
+
|
|
27
|
+
#ifdef __cplusplus
|
|
28
|
+
extern "C" {
|
|
29
|
+
#endif
|
|
30
|
+
|
|
31
|
+
/** An opaque decoder handle (`xtgui::MediaPlayer*`). */
|
|
32
|
+
typedef void *XtMediaPlayerHandle;
|
|
33
|
+
|
|
34
|
+
/** Open `path`, returning null on failure and copying a message into `error`
|
|
35
|
+
* (which is null-terminated and may be an empty string). */
|
|
36
|
+
typedef XtMediaPlayerHandle (*XtMediaOpenFn)(const char *path, char *error,
|
|
37
|
+
size_t error_size, void *audio_sink);
|
|
38
|
+
/** The live decoder for a process-wide id, or null. */
|
|
39
|
+
typedef XtMediaPlayerHandle (*XtMediaByIdFn)(uint64_t id);
|
|
40
|
+
/** Tear down every decoder and process-wide media state. */
|
|
41
|
+
typedef void (*XtMediaShutdownFn)(void);
|
|
42
|
+
|
|
43
|
+
typedef struct XtMediaApi {
|
|
44
|
+
XtMediaOpenFn open;
|
|
45
|
+
XtMediaByIdFn by_id;
|
|
46
|
+
XtMediaShutdownFn shutdown;
|
|
47
|
+
} XtMediaApi;
|
|
48
|
+
|
|
49
|
+
/** Install a decoder table. Implemented by the GUI engine; called once by the
|
|
50
|
+
* media backend's bridge object before `main`. */
|
|
51
|
+
void xt_media_install(const XtMediaApi *api);
|
|
52
|
+
|
|
53
|
+
/** Decoder backend entry points, implemented by `runtime/ext_media/media.cpp`
|
|
54
|
+
* (inside `media.a`). The bridge references them, which is what makes the
|
|
55
|
+
* linker extract the archive member when the media extension is enabled. */
|
|
56
|
+
XtMediaPlayerHandle xt_media_backend_open(const char *path, char *error,
|
|
57
|
+
size_t error_size, void *audio_sink);
|
|
58
|
+
XtMediaPlayerHandle xt_media_backend_by_id(uint64_t id);
|
|
59
|
+
void xt_media_backend_shutdown(void);
|
|
60
|
+
|
|
61
|
+
#ifdef __cplusplus
|
|
62
|
+
}
|
|
63
|
+
#endif
|
|
64
|
+
|
|
65
|
+
#endif /* XT_GUI_MEDIA_API_H */
|
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* xbintsc media extension — `<video>`/`<audio>` decoding.
|
|
3
|
+
*
|
|
4
|
+
* This translation unit is the only place that knows about FFmpeg. It is
|
|
5
|
+
* compiled into the prebuilt `media.a` by `scripts/build-media.ts` and linked
|
|
6
|
+
* only when the `media` extension is enabled; the GUI engine itself is
|
|
7
|
+
* decoder-free (see `../ext_gui/media_api.h`).
|
|
8
|
+
*
|
|
9
|
+
* Threading: the engine is single-threaded and its values live on the main
|
|
10
|
+
* thread, so nothing here may run on an audio callback. Instead the decoder is
|
|
11
|
+
* pushed from `tick()` on the main thread: it writes PCM into the
|
|
12
|
+
* `xtgui::AudioSink` supplied by the GUI (an SDL audio stream opened in push
|
|
13
|
+
* mode), and SDL pulls from its queued buffer on its own thread.
|
|
14
|
+
* `queued_seconds()` keeps the buffer shallow so there is no unbounded latency.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
#include "../ext_gui/media.h"
|
|
18
|
+
|
|
19
|
+
#include <algorithm>
|
|
20
|
+
#include <chrono>
|
|
21
|
+
#include <cmath>
|
|
22
|
+
#include <cstdio>
|
|
23
|
+
#include <cstring>
|
|
24
|
+
#include <memory>
|
|
25
|
+
#include <unordered_map>
|
|
26
|
+
#include <vector>
|
|
27
|
+
|
|
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
|
+
|
|
37
|
+
namespace xtgui {
|
|
38
|
+
|
|
39
|
+
namespace {
|
|
40
|
+
|
|
41
|
+
std::unordered_map<uint64_t, MediaPlayer*>& PlayerTable() {
|
|
42
|
+
static std::unordered_map<uint64_t, MediaPlayer*> table;
|
|
43
|
+
return table;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Audio format everything is resampled to before it reaches the sink. */
|
|
47
|
+
constexpr int kAudioRate = 48000;
|
|
48
|
+
constexpr int kAudioChannels = 2;
|
|
49
|
+
/** How much audio to keep buffered ahead of the play head. */
|
|
50
|
+
constexpr double kAudioAhead = 0.25;
|
|
51
|
+
/** Minimum gap between `timeupdate` events. */
|
|
52
|
+
constexpr double kTimeUpdateIntervalMs = 250.0;
|
|
53
|
+
|
|
54
|
+
/** Monotonic wall clock, the reference for playback timing. */
|
|
55
|
+
double NowMs() {
|
|
56
|
+
static const auto start = std::chrono::steady_clock::now();
|
|
57
|
+
return std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
uint64_t NextId() {
|
|
61
|
+
static uint64_t next = 1;
|
|
62
|
+
return next++;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
struct QueuedFrame {
|
|
66
|
+
Image image;
|
|
67
|
+
double pts = 0;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
class FfmpegPlayer final : public MediaPlayer {
|
|
71
|
+
public:
|
|
72
|
+
explicit FfmpegPlayer(std::unique_ptr<AudioSink> audio) : audio_(std::move(audio)) {}
|
|
73
|
+
|
|
74
|
+
~FfmpegPlayer() override {
|
|
75
|
+
PlayerTable().erase(id_);
|
|
76
|
+
if (sws_) sws_freeContext(sws_);
|
|
77
|
+
if (swr_) swr_free(&swr_);
|
|
78
|
+
if (vframe_) av_frame_free(&vframe_);
|
|
79
|
+
if (aframe_) av_frame_free(&aframe_);
|
|
80
|
+
if (pkt_) av_packet_free(&pkt_);
|
|
81
|
+
if (vctx_) avcodec_free_context(&vctx_);
|
|
82
|
+
if (actx_) avcodec_free_context(&actx_);
|
|
83
|
+
if (fmt_) avformat_close_input(&fmt_);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
bool Open(const std::string& path, std::string* error) {
|
|
87
|
+
source_ = path;
|
|
88
|
+
if (avformat_open_input(&fmt_, path.c_str(), nullptr, nullptr) < 0) {
|
|
89
|
+
if (error) *error = "cannot open media: " + path;
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
if (avformat_find_stream_info(fmt_, nullptr) < 0) {
|
|
93
|
+
if (error) *error = "cannot read media metadata: " + path;
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
duration_ = fmt_->duration > 0 ? static_cast<double>(fmt_->duration) / AV_TIME_BASE : 0.0;
|
|
97
|
+
|
|
98
|
+
vstream_ = av_find_best_stream(fmt_, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
|
|
99
|
+
astream_ = av_find_best_stream(fmt_, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);
|
|
100
|
+
if (vstream_ < 0 && astream_ < 0) {
|
|
101
|
+
if (error) *error = "no audio or video stream: " + path;
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
if (vstream_ >= 0 && !OpenVideo(fmt_->streams[vstream_])) {
|
|
105
|
+
vstream_ = -1;
|
|
106
|
+
}
|
|
107
|
+
if (astream_ >= 0 && !OpenAudio(fmt_->streams[astream_])) {
|
|
108
|
+
astream_ = -1;
|
|
109
|
+
}
|
|
110
|
+
if (vstream_ < 0 && astream_ < 0) {
|
|
111
|
+
if (error) *error = "no supported stream: " + path;
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
pkt_ = av_packet_alloc();
|
|
115
|
+
vframe_ = av_frame_alloc();
|
|
116
|
+
aframe_ = av_frame_alloc();
|
|
117
|
+
|
|
118
|
+
/* A file whose default duration is unknown falls back to the last
|
|
119
|
+
* timestamp of the video stream, which is enough for the `duration`
|
|
120
|
+
* property and for end-of-stream detection. */
|
|
121
|
+
if (duration_ <= 0 && vstream_ >= 0) {
|
|
122
|
+
AVStream* vs = fmt_->streams[vstream_];
|
|
123
|
+
if (vs->duration > 0) duration_ = vs->duration * av_q2d(vs->time_base);
|
|
124
|
+
}
|
|
125
|
+
id_ = NextId();
|
|
126
|
+
PlayerTable()[id_] = this;
|
|
127
|
+
pending_events_ |= kMediaEventLoadedMetadata;
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
uint64_t id() const override { return id_; }
|
|
132
|
+
const std::string& source() const override { return source_; }
|
|
133
|
+
bool has_video() const override { return vstream_ >= 0; }
|
|
134
|
+
bool has_audio() const override { return astream_ >= 0; }
|
|
135
|
+
double duration() const override { return duration_; }
|
|
136
|
+
|
|
137
|
+
bool paused() const override { return paused_; }
|
|
138
|
+
bool ended() const override { return ended_; }
|
|
139
|
+
double current_time() const override {
|
|
140
|
+
if (paused_) return clock_base_pts_;
|
|
141
|
+
return clock_base_pts_ + (NowMs() - clock_base_ms_) / 1000.0;
|
|
142
|
+
}
|
|
143
|
+
double volume() const override { return volume_; }
|
|
144
|
+
bool muted() const override { return muted_; }
|
|
145
|
+
bool loop() const override { return loop_; }
|
|
146
|
+
bool autoplay() const override { return autoplay_; }
|
|
147
|
+
|
|
148
|
+
void play() override {
|
|
149
|
+
if (ended_) Seek(0);
|
|
150
|
+
if (!paused_) return;
|
|
151
|
+
paused_ = false;
|
|
152
|
+
clock_base_ms_ = NowMs();
|
|
153
|
+
if (audio_ready_) audio_->resume();
|
|
154
|
+
pending_events_ |= kMediaEventPlay;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
void pause() override {
|
|
158
|
+
if (paused_) return;
|
|
159
|
+
clock_base_pts_ = current_time();
|
|
160
|
+
paused_ = true;
|
|
161
|
+
if (audio_ready_) audio_->pause();
|
|
162
|
+
pending_events_ |= kMediaEventPause;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
void seek(double seconds) override { Seek(seconds); }
|
|
166
|
+
|
|
167
|
+
void set_volume(double value) override {
|
|
168
|
+
volume_ = std::min(1.0, std::max(0.0, value));
|
|
169
|
+
ApplyGain();
|
|
170
|
+
}
|
|
171
|
+
void set_muted(bool value) override {
|
|
172
|
+
muted_ = value;
|
|
173
|
+
ApplyGain();
|
|
174
|
+
}
|
|
175
|
+
void set_loop(bool value) override { loop_ = value; }
|
|
176
|
+
void set_autoplay(bool value) override { autoplay_ = value; }
|
|
177
|
+
|
|
178
|
+
int frame_width() const override { return vctx_ ? vctx_->width : 0; }
|
|
179
|
+
int frame_height() const override { return vctx_ ? vctx_->height : 0; }
|
|
180
|
+
const Image* frame() const override { return current_valid_ ? ¤t_ : nullptr; }
|
|
181
|
+
uint64_t frame_generation() const override { return generation_; }
|
|
182
|
+
|
|
183
|
+
unsigned tick(double now_ms) override {
|
|
184
|
+
unsigned events = pending_events_;
|
|
185
|
+
pending_events_ = 0;
|
|
186
|
+
if (vstream_ < 0 && astream_ < 0) return events;
|
|
187
|
+
if (!started_ && autoplay_) {
|
|
188
|
+
started_ = true;
|
|
189
|
+
play();
|
|
190
|
+
events |= pending_events_;
|
|
191
|
+
pending_events_ = 0;
|
|
192
|
+
}
|
|
193
|
+
if (paused_ || ended_) return events;
|
|
194
|
+
if (now_ms - last_timeupdate_ms_ >= kTimeUpdateIntervalMs) {
|
|
195
|
+
events |= kMediaEventTimeUpdate;
|
|
196
|
+
last_timeupdate_ms_ = now_ms;
|
|
197
|
+
}
|
|
198
|
+
const double t = current_time();
|
|
199
|
+
PumpVideo(t);
|
|
200
|
+
PumpAudio();
|
|
201
|
+
events |= CheckEnded(now_ms);
|
|
202
|
+
return events;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
void invalidate_texture() override {}
|
|
206
|
+
|
|
207
|
+
private:
|
|
208
|
+
bool OpenVideo(AVStream* stream) {
|
|
209
|
+
const AVCodec* codec = avcodec_find_decoder(stream->codecpar->codec_id);
|
|
210
|
+
if (!codec) return false;
|
|
211
|
+
vctx_ = avcodec_alloc_context3(codec);
|
|
212
|
+
if (!vctx_) return false;
|
|
213
|
+
if (avcodec_parameters_to_context(vctx_, stream->codecpar) < 0) return false;
|
|
214
|
+
vctx_->pkt_timebase = stream->time_base;
|
|
215
|
+
if (avcodec_open2(vctx_, codec, nullptr) < 0) return false;
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
bool OpenAudio(AVStream* stream) {
|
|
220
|
+
const AVCodec* codec = avcodec_find_decoder(stream->codecpar->codec_id);
|
|
221
|
+
if (!codec) return false;
|
|
222
|
+
actx_ = avcodec_alloc_context3(codec);
|
|
223
|
+
if (!actx_) return false;
|
|
224
|
+
if (avcodec_parameters_to_context(actx_, stream->codecpar) < 0) return false;
|
|
225
|
+
actx_->pkt_timebase = stream->time_base;
|
|
226
|
+
if (avcodec_open2(actx_, codec, nullptr) < 0) return false;
|
|
227
|
+
|
|
228
|
+
AVChannelLayout out_layout;
|
|
229
|
+
av_channel_layout_default(&out_layout, kAudioChannels);
|
|
230
|
+
if (swr_alloc_set_opts2(&swr_, &out_layout, AV_SAMPLE_FMT_FLT, kAudioRate,
|
|
231
|
+
&actx_->ch_layout, actx_->sample_fmt, actx_->sample_rate,
|
|
232
|
+
0, nullptr) < 0 ||
|
|
233
|
+
!swr_ || swr_init(swr_) < 0) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
av_channel_layout_uninit(&out_layout);
|
|
237
|
+
|
|
238
|
+
/* Audio is best effort: a headless CI box has no device. Video still
|
|
239
|
+
* plays, and audio is silently dropped. */
|
|
240
|
+
audio_ready_ = audio_ != nullptr && audio_->open();
|
|
241
|
+
if (audio_ready_) ApplyGain();
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
void ApplyGain() {
|
|
246
|
+
if (audio_ready_) audio_->set_gain(muted_ ? 0.0f : static_cast<float>(volume_));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
double queued_seconds() const {
|
|
250
|
+
if (!audio_ready_) return 0.0;
|
|
251
|
+
return audio_->queued_seconds();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
double stream_pts(const AVStream* stream, int64_t ts) const {
|
|
255
|
+
return static_cast<double>(ts) * av_q2d(stream->time_base);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
void Seek(double seconds) {
|
|
259
|
+
double target = seconds;
|
|
260
|
+
if (duration_ > 0) target = std::min(duration_, std::max(0.0, target));
|
|
261
|
+
if (target < 0) target = 0;
|
|
262
|
+
clock_base_pts_ = target;
|
|
263
|
+
clock_base_ms_ = NowMs();
|
|
264
|
+
ended_ = false;
|
|
265
|
+
started_ = true;
|
|
266
|
+
|
|
267
|
+
if (fmt_) {
|
|
268
|
+
const int seek_stream = vstream_ >= 0 ? vstream_ : astream_;
|
|
269
|
+
const int64_t ts = static_cast<int64_t>(target / av_q2d(fmt_->streams[seek_stream]->time_base));
|
|
270
|
+
if (av_seek_frame(fmt_, seek_stream, ts, AVSEEK_FLAG_BACKWARD) < 0) {
|
|
271
|
+
avformat_flush(fmt_);
|
|
272
|
+
}
|
|
273
|
+
if (vctx_) avcodec_flush_buffers(vctx_);
|
|
274
|
+
if (actx_) avcodec_flush_buffers(actx_);
|
|
275
|
+
}
|
|
276
|
+
if (audio_ready_) audio_->clear();
|
|
277
|
+
video_queue_.clear();
|
|
278
|
+
current_valid_ = false;
|
|
279
|
+
drop_before_ = vstream_ >= 0 ? target : -1;
|
|
280
|
+
reader_eof_ = false;
|
|
281
|
+
video_flushed_ = false;
|
|
282
|
+
audio_flushed_ = false;
|
|
283
|
+
video_drained_ = false;
|
|
284
|
+
audio_drained_ = false;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
bool ReadPacket() {
|
|
288
|
+
const int ret = av_read_frame(fmt_, pkt_);
|
|
289
|
+
if (ret < 0) {
|
|
290
|
+
reader_eof_ = true;
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
if (pkt_->stream_index == vstream_) {
|
|
294
|
+
avcodec_send_packet(vctx_, pkt_);
|
|
295
|
+
DrainVideo();
|
|
296
|
+
} else if (pkt_->stream_index == astream_) {
|
|
297
|
+
avcodec_send_packet(actx_, pkt_);
|
|
298
|
+
DrainAudio();
|
|
299
|
+
}
|
|
300
|
+
av_packet_unref(pkt_);
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
void DrainVideo() {
|
|
305
|
+
while (avcodec_receive_frame(vctx_, vframe_) == 0) {
|
|
306
|
+
const double pts = stream_pts(fmt_->streams[vstream_], vframe_->best_effort_timestamp);
|
|
307
|
+
Image image;
|
|
308
|
+
ConvertVideo(vframe_, &image);
|
|
309
|
+
video_queue_.push_back({std::move(image), pts});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
void ConvertVideo(const AVFrame* frame, Image* out) {
|
|
314
|
+
const int w = frame->width;
|
|
315
|
+
const int h = frame->height;
|
|
316
|
+
if (!sws_ || sws_width_ != w || sws_height_ != h || sws_pix_fmt_ != frame->format) {
|
|
317
|
+
if (sws_) sws_freeContext(sws_);
|
|
318
|
+
sws_ = sws_getContext(w, h, static_cast<AVPixelFormat>(frame->format), w, h, AV_PIX_FMT_RGBA,
|
|
319
|
+
SWS_BILINEAR, nullptr, nullptr, nullptr);
|
|
320
|
+
sws_width_ = w;
|
|
321
|
+
sws_height_ = h;
|
|
322
|
+
sws_pix_fmt_ = frame->format;
|
|
323
|
+
}
|
|
324
|
+
if (!sws_) return;
|
|
325
|
+
out->width = w;
|
|
326
|
+
out->height = h;
|
|
327
|
+
out->pixels.resize(static_cast<size_t>(w) * h * 4);
|
|
328
|
+
uint8_t* dst[4] = {out->pixels.data(), nullptr, nullptr, nullptr};
|
|
329
|
+
int dst_linesize[4] = {w * 4, 0, 0, 0};
|
|
330
|
+
sws_scale(sws_, frame->data, frame->linesize, 0, h, dst, dst_linesize);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
void DrainAudio() {
|
|
334
|
+
while (avcodec_receive_frame(actx_, aframe_) == 0) {
|
|
335
|
+
if (!audio_ready_) continue;
|
|
336
|
+
const int capacity = swr_get_out_samples(swr_, aframe_->nb_samples);
|
|
337
|
+
if (capacity <= 0) continue;
|
|
338
|
+
audio_buf_.resize(static_cast<size_t>(capacity) * kAudioChannels * sizeof(float));
|
|
339
|
+
uint8_t* out = audio_buf_.data();
|
|
340
|
+
const int got = swr_convert(swr_, &out, capacity,
|
|
341
|
+
reinterpret_cast<const uint8_t* const*>(aframe_->extended_data),
|
|
342
|
+
aframe_->nb_samples);
|
|
343
|
+
if (got > 0) {
|
|
344
|
+
const int bytes = got * kAudioChannels * static_cast<int>(sizeof(float));
|
|
345
|
+
audio_->write(audio_buf_.data(), static_cast<size_t>(bytes));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
void PumpVideo(double t) {
|
|
351
|
+
if (vstream_ < 0 || video_drained_) return;
|
|
352
|
+
while (true) {
|
|
353
|
+
if (video_queue_.empty()) {
|
|
354
|
+
if (reader_eof_) {
|
|
355
|
+
if (!video_flushed_) {
|
|
356
|
+
avcodec_send_packet(vctx_, nullptr);
|
|
357
|
+
video_flushed_ = true;
|
|
358
|
+
DrainVideo();
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
video_drained_ = true;
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (!ReadPacket() && video_queue_.empty() && reader_eof_) continue;
|
|
365
|
+
if (video_queue_.empty()) continue;
|
|
366
|
+
}
|
|
367
|
+
QueuedFrame& front = video_queue_.front();
|
|
368
|
+
if (drop_before_ >= 0 && front.pts < drop_before_ - 0.001) {
|
|
369
|
+
video_queue_.erase(video_queue_.begin());
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (front.pts <= t + 0.01) {
|
|
373
|
+
current_ = std::move(front.image);
|
|
374
|
+
current_valid_ = true;
|
|
375
|
+
++generation_;
|
|
376
|
+
video_queue_.erase(video_queue_.begin());
|
|
377
|
+
drop_before_ = -1;
|
|
378
|
+
} else {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
void PumpAudio() {
|
|
385
|
+
if (astream_ < 0 || audio_drained_) return;
|
|
386
|
+
while (queued_seconds() < kAudioAhead) {
|
|
387
|
+
if (reader_eof_) {
|
|
388
|
+
if (!audio_flushed_) {
|
|
389
|
+
avcodec_send_packet(actx_, nullptr);
|
|
390
|
+
audio_flushed_ = true;
|
|
391
|
+
DrainAudio();
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
audio_drained_ = true;
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (!ReadPacket()) return;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
unsigned CheckEnded(double now_ms) {
|
|
402
|
+
if (ended_) return 0;
|
|
403
|
+
if (!reader_eof_) return 0;
|
|
404
|
+
if (vstream_ >= 0 && !video_drained_) return 0;
|
|
405
|
+
if (astream_ >= 0 && !audio_drained_) return 0;
|
|
406
|
+
if (astream_ >= 0 && queued_seconds() > 0.05) return 0;
|
|
407
|
+
if (vstream_ >= 0 && duration_ > 0 && current_time() < duration_ - 0.05) return 0;
|
|
408
|
+
if (loop_) {
|
|
409
|
+
Seek(0);
|
|
410
|
+
play();
|
|
411
|
+
return kMediaEventPlay;
|
|
412
|
+
}
|
|
413
|
+
ended_ = true;
|
|
414
|
+
paused_ = true;
|
|
415
|
+
clock_base_pts_ = duration_ > 0 ? duration_ : current_time();
|
|
416
|
+
last_timeupdate_ms_ = now_ms;
|
|
417
|
+
return kMediaEventEnded;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
uint64_t id_ = 0;
|
|
421
|
+
std::string source_;
|
|
422
|
+
AVFormatContext* fmt_ = nullptr;
|
|
423
|
+
AVCodecContext* vctx_ = nullptr;
|
|
424
|
+
AVCodecContext* actx_ = nullptr;
|
|
425
|
+
int vstream_ = -1;
|
|
426
|
+
int astream_ = -1;
|
|
427
|
+
AVPacket* pkt_ = nullptr;
|
|
428
|
+
AVFrame* vframe_ = nullptr;
|
|
429
|
+
AVFrame* aframe_ = nullptr;
|
|
430
|
+
SwsContext* sws_ = nullptr;
|
|
431
|
+
SwrContext* swr_ = nullptr;
|
|
432
|
+
int sws_width_ = 0;
|
|
433
|
+
int sws_height_ = 0;
|
|
434
|
+
int sws_pix_fmt_ = -1;
|
|
435
|
+
|
|
436
|
+
std::vector<QueuedFrame> video_queue_;
|
|
437
|
+
Image current_;
|
|
438
|
+
bool current_valid_ = false;
|
|
439
|
+
uint64_t generation_ = 0;
|
|
440
|
+
double drop_before_ = -1;
|
|
441
|
+
|
|
442
|
+
std::unique_ptr<AudioSink> audio_;
|
|
443
|
+
bool audio_ready_ = false;
|
|
444
|
+
std::vector<uint8_t> audio_buf_;
|
|
445
|
+
|
|
446
|
+
bool reader_eof_ = false;
|
|
447
|
+
bool video_flushed_ = false;
|
|
448
|
+
bool audio_flushed_ = false;
|
|
449
|
+
bool video_drained_ = false;
|
|
450
|
+
bool audio_drained_ = false;
|
|
451
|
+
|
|
452
|
+
double duration_ = 0;
|
|
453
|
+
double volume_ = 1.0;
|
|
454
|
+
bool muted_ = false;
|
|
455
|
+
bool loop_ = false;
|
|
456
|
+
bool autoplay_ = false;
|
|
457
|
+
bool paused_ = true;
|
|
458
|
+
bool ended_ = false;
|
|
459
|
+
bool started_ = false;
|
|
460
|
+
double clock_base_pts_ = 0;
|
|
461
|
+
double clock_base_ms_ = 0;
|
|
462
|
+
double last_timeupdate_ms_ = -1e9;
|
|
463
|
+
unsigned pending_events_ = 0;
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
} // namespace
|
|
467
|
+
|
|
468
|
+
MediaPlayer* OpenMediaPlayer(const char* path, std::string* error,
|
|
469
|
+
std::unique_ptr<AudioSink> audio) {
|
|
470
|
+
std::unique_ptr<FfmpegPlayer> player(new FfmpegPlayer(std::move(audio)));
|
|
471
|
+
if (!player->Open(path, error)) return nullptr;
|
|
472
|
+
return player.release();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
MediaPlayer* MediaPlayerById(uint64_t id) {
|
|
476
|
+
auto& table = PlayerTable();
|
|
477
|
+
const auto it = table.find(id);
|
|
478
|
+
return it == table.end() ? nullptr : it->second;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
void ShutdownMediaPlayers() {
|
|
482
|
+
/* Move the table out before destroying it: `~FfmpegPlayer` erases its own
|
|
483
|
+
* entry, and a destructor mutating the container it is being cleared from
|
|
484
|
+
* would invalidate the clearing loop. With an empty table the erases are
|
|
485
|
+
* no-ops. */
|
|
486
|
+
auto pending = std::move(PlayerTable());
|
|
487
|
+
PlayerTable().clear();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
} // namespace xtgui
|
|
491
|
+
|
|
492
|
+
extern "C" XtMediaPlayerHandle xt_media_backend_open(const char* path, char* error,
|
|
493
|
+
size_t error_size, void* audio_sink) {
|
|
494
|
+
std::unique_ptr<xtgui::AudioSink> audio(static_cast<xtgui::AudioSink*>(audio_sink));
|
|
495
|
+
std::string message;
|
|
496
|
+
xtgui::MediaPlayer* player = xtgui::OpenMediaPlayer(path != nullptr ? path : "", &message, std::move(audio));
|
|
497
|
+
if (player == nullptr && error != nullptr && error_size > 0) {
|
|
498
|
+
std::snprintf(error, error_size, "%s", message.c_str());
|
|
499
|
+
}
|
|
500
|
+
return player;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
extern "C" XtMediaPlayerHandle xt_media_backend_by_id(uint64_t id) {
|
|
504
|
+
return xtgui::MediaPlayerById(id);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
extern "C" void xt_media_backend_shutdown(void) { xtgui::ShutdownMediaPlayers(); }
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* xbintsc media extension — registration bridge.
|
|
3
|
+
*
|
|
4
|
+
* The decoders live in `media.a`, a prebuilt C++ archive. A static archive is
|
|
5
|
+
* only searched for members that satisfy an undefined symbol, so a backend that
|
|
6
|
+
* merely had a constructor would never be extracted. This tiny C translation
|
|
7
|
+
* unit is compiled and linked *directly* (it is the media extension's
|
|
8
|
+
* `runtimeSources()` entry): its constructor references every backend entry
|
|
9
|
+
* point, which pulls the archive member in, and installs the table into the GUI
|
|
10
|
+
* engine before `main` runs.
|
|
11
|
+
*
|
|
12
|
+
* The bridge is intentionally C-only and has no FFmpeg (or SDL) dependency. If
|
|
13
|
+
* the `gui` extension is not enabled, `xt_media_install` is unresolved; the
|
|
14
|
+
* registry rejects that combination first with a "media requires gui"
|
|
15
|
+
* diagnostic (see `src/extensions/registry.ts`).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
#include "../ext_gui/media_api.h"
|
|
19
|
+
|
|
20
|
+
static const XtMediaApi kMediaApi = {
|
|
21
|
+
xt_media_backend_open,
|
|
22
|
+
xt_media_backend_by_id,
|
|
23
|
+
xt_media_backend_shutdown,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
#if defined(__GNUC__) || defined(__clang__)
|
|
27
|
+
__attribute__((constructor)) static void xt_media_register(void) {
|
|
28
|
+
xt_media_install(&kMediaApi);
|
|
29
|
+
}
|
|
30
|
+
#elif defined(_MSC_VER)
|
|
31
|
+
/* COFF has no `constructor` attribute. `media.lib` is not produced on Windows
|
|
32
|
+
* yet; keep the table referenced so the translation unit is not empty. */
|
|
33
|
+
void xt_media_register(void);
|
|
34
|
+
void xt_media_register(void) { xt_media_install(&kMediaApi); }
|
|
35
|
+
#endif
|