xbintsc 0.5.21 → 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-canvas.test.js +159 -0
- package/dist/tests/e2e/gui-canvas.test.js.map +1 -1
- package/dist/tests/e2e/gui-example-canvas.test.d.ts +11 -0
- package/dist/tests/e2e/gui-example-canvas.test.js +72 -0
- package/dist/tests/e2e/gui-example-canvas.test.js.map +1 -0
- 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 -22
- package/doc/ai/troubleshooting.md +9 -0
- package/doc/ai/zh-CN/cli.md +1 -1
- package/doc/ai/zh-CN/extensions.md +22 -9
- package/doc/ai/zh-CN/troubleshooting.md +8 -0
- package/doc/gui/build.md +37 -21
- package/doc/gui/canvas.md +47 -21
- package/doc/gui/known-issues.md +7 -6
- package/doc/gui/media.md +26 -11
- package/doc/gui.md +50 -34
- package/doc/zh-CN/gui/build.md +28 -16
- package/doc/zh-CN/gui/canvas.md +36 -17
- 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 +43 -34
- package/package.json +2 -1
- package/runtime/ext_gui/canvas.cpp +114 -199
- package/runtime/ext_gui/canvas.h +101 -2
- package/runtime/ext_gui/canvas_gradient.cpp +112 -0
- package/runtime/ext_gui/canvas_internal.h +6 -6
- package/runtime/ext_gui/canvas_raster.cpp +27 -5
- package/runtime/ext_gui/canvas_stroke.cpp +252 -0
- package/runtime/ext_gui/canvas_style.cpp +240 -0
- package/runtime/ext_gui/dom_api.cpp +1 -0
- package/runtime/ext_gui/dom_api_canvas.cpp +38 -4
- package/runtime/ext_gui/dom_api_canvas_extra.cpp +232 -0
- package/runtime/ext_gui/dom_api_internal.h +31 -0
- package/runtime/ext_gui/dom_api_proto.cpp +24 -0
- 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
package/runtime/ext_gui/media.h
CHANGED
|
@@ -4,29 +4,39 @@
|
|
|
4
4
|
/*
|
|
5
5
|
* Media decoding for `<video>` and `<audio>`.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
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
|
|
15
|
-
* `tick()` advances the wall clock, decodes whatever
|
|
16
|
-
* bitmask of DOM events the caller should dispatch. The
|
|
17
|
-
* frame loop, so there is no separate media thread and no
|
|
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
|
|
21
|
-
* types so the rest of the engine (and `gui_engine.h`) can
|
|
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
|
|
48
|
-
* (`
|
|
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
|
|
55
|
-
|
|
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&
|
|
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*
|
|
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*
|
|
132
|
+
MediaPlayer *xt_media_by_id(uint64_t id);
|
|
98
133
|
|
|
99
|
-
/** Tear down process-wide media state (
|
|
134
|
+
/** Tear down process-wide media state (id table, decoder state). */
|
|
100
135
|
void xt_media_shutdown();
|
|
101
136
|
|
|
102
137
|
} // namespace xtgui
|
|
@@ -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(); }
|