@eva/plugin-renderer-video 2.1.0-beta.3 → 2.1.0-beta.5

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.
@@ -0,0 +1,495 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var eva_js = require('@eva/eva.js');
6
+ var inspectorDecorator = require('@eva/inspector-decorator');
7
+ var pluginRenderer = require('@eva/plugin-renderer');
8
+ var pixi_js = require('pixi.js');
9
+
10
+ /******************************************************************************
11
+ Copyright (c) Microsoft Corporation.
12
+
13
+ Permission to use, copy, modify, and/or distribute this software for any
14
+ purpose with or without fee is hereby granted.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
17
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
18
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
19
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
20
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
21
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
22
+ PERFORMANCE OF THIS SOFTWARE.
23
+ ***************************************************************************** */
24
+
25
+ function __decorate(decorators, target, key, desc) {
26
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
27
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
28
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
29
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
30
+ }
31
+
32
+ function __metadata(metadataKey, metadataValue) {
33
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(metadataKey, metadataValue);
34
+ }
35
+
36
+ function __awaiter(thisArg, _arguments, P, generator) {
37
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
38
+ return new (P || (P = Promise))(function (resolve, reject) {
39
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
40
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
41
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
42
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
43
+ });
44
+ }
45
+
46
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
47
+ var e = new Error(message);
48
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
49
+ };
50
+
51
+ /**
52
+ * Phaser 风格的 Video 渲染组件(Eva.js MVP 实现)。
53
+ *
54
+ * 行为:
55
+ * 1. ADD 时 VideoSystem 会创建一个 detached 的 `<video>` DOM 元素 + PixiJS Texture + Sprite,
56
+ * Sprite 加入到 GameObject 的 PixiJS Container;
57
+ * 2. video 的源 (`<video>.src`) 由 `src` 决定:
58
+ * - 若注册过同名 VIDEO/IMAGE 资源(且 instance.src 可用),取注册的 url;
59
+ * - 否则直接当作 URL 使用。
60
+ * 3. 视频进入 `loadeddata` 后即可绘制;
61
+ * 4. CHANGE prop 时按属性增量更新(loop/muted/volume/playbackRate/src);
62
+ * 5. REMOVE/destroy 时停止播放、销毁 video/texture/sprite。
63
+ *
64
+ * 不支持(明确 SKIP):
65
+ * - HLS/DASH/MSE 高级流;
66
+ * - getUserMedia / 透明视频(alpha video / chroma key);
67
+ * - 作为 shader 纹理(Eva.js 无 GLSL 注入);
68
+ * - Phaser saveTexture 把视频转 sprite atlas 的工作流。
69
+ */
70
+ class Video extends eva_js.Component {
71
+ constructor() {
72
+ super(...arguments);
73
+ this.src = '';
74
+ this.loop = false;
75
+ this.autoplay = true;
76
+ this.muted = true;
77
+ this.volume = 1;
78
+ this.playbackRate = 1;
79
+ this.anchorX = 0.5;
80
+ this.anchorY = 0.5;
81
+ this.crossOrigin = 'anonymous';
82
+ this.playsInline = true;
83
+ /** Runtime 标记:System 设置,表示视频已经触发过 `ended`。 */
84
+ this.completed = false;
85
+ }
86
+ init(obj) {
87
+ if (obj)
88
+ Object.assign(this, obj);
89
+ }
90
+ /** 立即播放(若 video 已就绪)。 */
91
+ play() {
92
+ if (!this.videoElement)
93
+ return;
94
+ try {
95
+ const ret = this.videoElement.play();
96
+ if (ret && typeof ret.catch === 'function') {
97
+ return ret.catch((e) => {
98
+ // eslint-disable-next-line no-console
99
+ console.warn('[Video] play() rejected:', e);
100
+ });
101
+ }
102
+ }
103
+ catch (e) {
104
+ // eslint-disable-next-line no-console
105
+ console.warn('[Video] play() threw:', e);
106
+ }
107
+ }
108
+ /** 暂停。 */
109
+ pause() {
110
+ if (!this.videoElement)
111
+ return;
112
+ try {
113
+ this.videoElement.pause();
114
+ }
115
+ catch (_a) {
116
+ /* ignore */
117
+ }
118
+ }
119
+ /** 跳转到指定时间(秒)。 */
120
+ setCurrentTime(seconds) {
121
+ if (!this.videoElement)
122
+ return;
123
+ try {
124
+ this.videoElement.currentTime = Math.max(0, seconds);
125
+ }
126
+ catch (_a) {
127
+ /* ignore */
128
+ }
129
+ }
130
+ /** 把当前帧画到一个独立 canvas 上并返回,用于 snapshot(失败时返回 null)。 */
131
+ snapshot(area) {
132
+ var _a, _b, _c, _d;
133
+ const v = this.videoElement;
134
+ if (!v || v.readyState < 2)
135
+ return null;
136
+ const w = (_a = area === null || area === void 0 ? void 0 : area.width) !== null && _a !== void 0 ? _a : v.videoWidth;
137
+ const h = (_b = area === null || area === void 0 ? void 0 : area.height) !== null && _b !== void 0 ? _b : v.videoHeight;
138
+ if (w <= 0 || h <= 0)
139
+ return null;
140
+ try {
141
+ const canvas = document.createElement('canvas');
142
+ canvas.width = w;
143
+ canvas.height = h;
144
+ const ctx = canvas.getContext('2d');
145
+ if (!ctx)
146
+ return null;
147
+ ctx.drawImage(v, (_c = area === null || area === void 0 ? void 0 : area.x) !== null && _c !== void 0 ? _c : 0, (_d = area === null || area === void 0 ? void 0 : area.y) !== null && _d !== void 0 ? _d : 0, w, h, 0, 0, w, h);
148
+ return canvas;
149
+ }
150
+ catch (_e) {
151
+ return null;
152
+ }
153
+ }
154
+ }
155
+ Video.componentName = 'Video';
156
+ __decorate([
157
+ inspectorDecorator.type('string'),
158
+ __metadata("design:type", String)
159
+ ], Video.prototype, "src", void 0);
160
+ __decorate([
161
+ inspectorDecorator.type('boolean'),
162
+ __metadata("design:type", Boolean)
163
+ ], Video.prototype, "loop", void 0);
164
+ __decorate([
165
+ inspectorDecorator.type('boolean'),
166
+ __metadata("design:type", Boolean)
167
+ ], Video.prototype, "autoplay", void 0);
168
+ __decorate([
169
+ inspectorDecorator.type('boolean'),
170
+ __metadata("design:type", Boolean)
171
+ ], Video.prototype, "muted", void 0);
172
+ __decorate([
173
+ inspectorDecorator.type('number'),
174
+ __metadata("design:type", Number)
175
+ ], Video.prototype, "volume", void 0);
176
+ __decorate([
177
+ inspectorDecorator.type('number'),
178
+ __metadata("design:type", Number)
179
+ ], Video.prototype, "playbackRate", void 0);
180
+ __decorate([
181
+ inspectorDecorator.type('number'),
182
+ __metadata("design:type", Number)
183
+ ], Video.prototype, "anchorX", void 0);
184
+ __decorate([
185
+ inspectorDecorator.type('number'),
186
+ __metadata("design:type", Number)
187
+ ], Video.prototype, "anchorY", void 0);
188
+ __decorate([
189
+ inspectorDecorator.type('boolean'),
190
+ __metadata("design:type", Boolean)
191
+ ], Video.prototype, "playsInline", void 0);
192
+
193
+ /**
194
+ * 把 `src` 解析为可播放 URL。
195
+ * 1. 如果 `src` 是完整 URL(以 http(s):// 或 / 开头,或包含 .mp4/.webm/.ogv 后缀),直接使用。
196
+ * 2. 否则尝试从 resource manager 拿对应资源 base 元数据,优先取 `src.video.url` 或 `src.url`。
197
+ */
198
+ function resolveSrc(src) {
199
+ if (!src)
200
+ return '';
201
+ if (src.startsWith('http://') || src.startsWith('https://') || src.startsWith('//') || src.startsWith('/')) {
202
+ return src;
203
+ }
204
+ if (/\.(mp4|webm|ogv|m4v|mov)(\?|#|$)/i.test(src)) {
205
+ return src;
206
+ }
207
+ // try resource registry
208
+ try {
209
+ const all = eva_js.resource.resourcesMap || eva_js.resource.resources;
210
+ if (all && all[src]) {
211
+ const meta = all[src];
212
+ const sources = meta.src || meta.data || {};
213
+ const candidate = sources.video || sources.audio || sources.image || sources;
214
+ if (candidate === null || candidate === void 0 ? void 0 : candidate.url)
215
+ return candidate.url;
216
+ if (candidate === null || candidate === void 0 ? void 0 : candidate.src)
217
+ return candidate.src;
218
+ }
219
+ }
220
+ catch (_a) {
221
+ /* ignore */
222
+ }
223
+ return src;
224
+ }
225
+ /**
226
+ * 透明 1x1 像素的 PNG,用作未就绪时的占位 texture 资源。
227
+ * 不直接用 Texture.EMPTY 是因为 EMPTY 的 source 在 v8 里某些路径上会触发 isValid 校验。
228
+ */
229
+ const TRANSPARENT_PIXEL_DATA_URL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
230
+ let VideoSystem = class VideoSystem extends pluginRenderer.Renderer {
231
+ constructor() {
232
+ super(...arguments);
233
+ this.name = 'Video';
234
+ this.records = {};
235
+ }
236
+ init() {
237
+ this.renderSystem = this.game.getSystem(pluginRenderer.RendererSystem);
238
+ this.renderSystem.rendererManager.register(this);
239
+ }
240
+ rendererUpdate(gameObject) {
241
+ var _a, _b, _c, _d, _e;
242
+ const record = this.records[gameObject.id];
243
+ if (!record)
244
+ return;
245
+ const c = record.component;
246
+ const v = record.video;
247
+ // 视频可绘制时把帧拷贝到 canvas,再通过 texture.source.update() 推到 GPU。
248
+ if (record.ctx && v.readyState >= 2 && v.videoWidth > 0 && v.videoHeight > 0) {
249
+ // 首次拿到真实尺寸时按 video 自然尺寸调整 canvas 分辨率。
250
+ if (!record.resizedToVideo) {
251
+ record.canvas.width = v.videoWidth;
252
+ record.canvas.height = v.videoHeight;
253
+ record.resizedToVideo = true;
254
+ // 重建 texture 以让 PixiJS 感知新的源尺寸。
255
+ const oldTex = record.texture;
256
+ const newTex = pixi_js.Texture.from(record.canvas);
257
+ record.texture = newTex;
258
+ record.sprite.texture = newTex;
259
+ if (oldTex && oldTex !== newTex) {
260
+ try {
261
+ oldTex.destroy(false);
262
+ }
263
+ catch ( /* ignore */_f) { /* ignore */ }
264
+ }
265
+ }
266
+ try {
267
+ record.ctx.drawImage(v, 0, 0, record.canvas.width, record.canvas.height);
268
+ // PixiJS v8: 通过 texture.source.update() 通知 GPU 上传新帧。
269
+ const src = (_a = record.texture) === null || _a === void 0 ? void 0 : _a.source;
270
+ if (src) {
271
+ if (typeof src.update === 'function')
272
+ src.update();
273
+ else
274
+ src.dirty = true;
275
+ }
276
+ }
277
+ catch (_g) {
278
+ /* drawImage 在某些跨域场景会抛 SecurityError,忽略让下一帧继续尝试 */
279
+ }
280
+ }
281
+ const w = (_b = c.width) !== null && _b !== void 0 ? _b : (v.videoWidth || 256);
282
+ const h = (_c = c.height) !== null && _c !== void 0 ? _c : (v.videoHeight || 256);
283
+ record.sprite.width = w;
284
+ record.sprite.height = h;
285
+ record.sprite.anchor.set((_d = c.anchorX) !== null && _d !== void 0 ? _d : 0.5, (_e = c.anchorY) !== null && _e !== void 0 ? _e : 0.5);
286
+ }
287
+ componentChanged(changed) {
288
+ var _a, _b, _c, _d, _e;
289
+ return __awaiter(this, void 0, void 0, function* () {
290
+ if (changed.componentName !== 'Video')
291
+ return;
292
+ const component = changed.component;
293
+ const gameObjectId = changed.gameObject.id;
294
+ if (changed.type === eva_js.OBSERVER_TYPE.ADD) {
295
+ const record = this.createRecord(component);
296
+ this.records[gameObjectId] = record;
297
+ const container = this.containerManager.getContainer(gameObjectId);
298
+ if (container)
299
+ container.addChildAt(record.sprite, 0);
300
+ this.attachEndedHandler(record, changed.gameObject);
301
+ this.startLoad(record);
302
+ }
303
+ else if (changed.type === eva_js.OBSERVER_TYPE.CHANGE) {
304
+ const record = this.records[gameObjectId];
305
+ if (!record)
306
+ return;
307
+ const propKey = (_b = (_a = changed.prop) === null || _a === void 0 ? void 0 : _a.prop) === null || _b === void 0 ? void 0 : _b[0];
308
+ if (propKey === 'src') {
309
+ try {
310
+ record.video.pause();
311
+ }
312
+ catch ( /* ignore */_f) { /* ignore */ }
313
+ const url = resolveSrc(component.src);
314
+ record.video.src = url;
315
+ record.component = component;
316
+ component.videoElement = record.video;
317
+ component.completed = false;
318
+ record.resizedToVideo = false;
319
+ try {
320
+ record.video.load();
321
+ }
322
+ catch ( /* ignore */_g) { /* ignore */ }
323
+ if (component.autoplay !== false)
324
+ this.tryPlay(record.video);
325
+ }
326
+ else if (propKey === 'loop') {
327
+ record.video.loop = !!component.loop;
328
+ }
329
+ else if (propKey === 'muted') {
330
+ record.video.muted = !!component.muted;
331
+ }
332
+ else if (propKey === 'volume') {
333
+ record.video.volume = Math.max(0, Math.min(1, (_c = component.volume) !== null && _c !== void 0 ? _c : 1));
334
+ }
335
+ else if (propKey === 'playbackRate') {
336
+ record.video.playbackRate = (_d = component.playbackRate) !== null && _d !== void 0 ? _d : 1;
337
+ }
338
+ }
339
+ else if (changed.type === eva_js.OBSERVER_TYPE.REMOVE) {
340
+ const record = this.records[gameObjectId];
341
+ if (!record)
342
+ return;
343
+ this.tearDown(record);
344
+ const container = (_e = this.containerManager) === null || _e === void 0 ? void 0 : _e.getContainer(gameObjectId);
345
+ if (container)
346
+ container.removeChild(record.sprite);
347
+ try {
348
+ record.sprite.destroy({ children: true });
349
+ }
350
+ catch ( /* ignore */_h) { /* ignore */ }
351
+ delete this.records[gameObjectId];
352
+ }
353
+ });
354
+ }
355
+ createRecord(component) {
356
+ var _a, _b, _c, _d, _e, _f, _g;
357
+ const video = document.createElement('video');
358
+ video.crossOrigin = (_a = component.crossOrigin) !== null && _a !== void 0 ? _a : 'anonymous';
359
+ video.loop = !!component.loop;
360
+ video.muted = component.muted !== false;
361
+ video.volume = Math.max(0, Math.min(1, (_b = component.volume) !== null && _b !== void 0 ? _b : 1));
362
+ video.playbackRate = (_c = component.playbackRate) !== null && _c !== void 0 ? _c : 1;
363
+ video.playsInline = component.playsInline !== false;
364
+ video['webkit-playsinline'] = '';
365
+ video.preload = 'auto';
366
+ component.videoElement = video;
367
+ // 用一个 canvas 作为帧中转:PixiJS 直接把这个 canvas 当 ImageSource,
368
+ // 完全绕过 PixiJS v8 的 VideoSource(避免 _onPlayStart/_mediaReady 无限递归)。
369
+ const canvas = document.createElement('canvas');
370
+ canvas.width = (_d = component.width) !== null && _d !== void 0 ? _d : 256;
371
+ canvas.height = (_e = component.height) !== null && _e !== void 0 ? _e : 256;
372
+ const ctx = canvas.getContext('2d');
373
+ // 占位 texture:用透明 PNG 兜底,绝不传 EMPTY(v8 在某些 isValid 路径上不友好)。
374
+ const placeholderTex = pixi_js.Texture.from(TRANSPARENT_PIXEL_DATA_URL);
375
+ const sprite = new pixi_js.Sprite(placeholderTex);
376
+ sprite.anchor.set((_f = component.anchorX) !== null && _f !== void 0 ? _f : 0.5, (_g = component.anchorY) !== null && _g !== void 0 ? _g : 0.5);
377
+ return {
378
+ video,
379
+ canvas,
380
+ ctx,
381
+ texture: placeholderTex,
382
+ sprite,
383
+ component,
384
+ endedHandler: null,
385
+ resizedToVideo: false,
386
+ };
387
+ }
388
+ attachEndedHandler(record, _go) {
389
+ const handler = () => {
390
+ record.component.completed = true;
391
+ try {
392
+ record.video.dispatchEvent(new Event('eva-video-complete'));
393
+ }
394
+ catch (_a) {
395
+ /* ignore */
396
+ }
397
+ };
398
+ record.video.addEventListener('ended', handler);
399
+ record.endedHandler = handler;
400
+ }
401
+ startLoad(record) {
402
+ const url = resolveSrc(record.component.src);
403
+ record.video.src = url;
404
+ try {
405
+ record.video.load();
406
+ }
407
+ catch ( /* ignore */_a) { /* ignore */ }
408
+ if (record.component.autoplay !== false) {
409
+ // 元数据就绪后再 play,这样 videoWidth/videoHeight 已经稳定。
410
+ const onCanPlay = () => {
411
+ record.video.removeEventListener('canplay', onCanPlay);
412
+ record.video.removeEventListener('loadeddata', onCanPlay);
413
+ this.tryPlay(record.video);
414
+ };
415
+ if (record.video.readyState >= 2) {
416
+ this.tryPlay(record.video);
417
+ }
418
+ else {
419
+ record.video.addEventListener('canplay', onCanPlay);
420
+ record.video.addEventListener('loadeddata', onCanPlay);
421
+ }
422
+ }
423
+ }
424
+ tryPlay(video) {
425
+ try {
426
+ const p = video.play();
427
+ if (p && typeof p.catch === 'function') {
428
+ p.catch((err) => {
429
+ // eslint-disable-next-line no-console
430
+ console.warn('[Video] autoplay rejected:', (err === null || err === void 0 ? void 0 : err.message) || err);
431
+ });
432
+ }
433
+ }
434
+ catch (e) {
435
+ // eslint-disable-next-line no-console
436
+ console.warn('[Video] autoplay threw:', e);
437
+ }
438
+ }
439
+ tearDown(record) {
440
+ try {
441
+ record.video.pause();
442
+ }
443
+ catch ( /* ignore */_a) { /* ignore */ }
444
+ if (record.endedHandler) {
445
+ try {
446
+ record.video.removeEventListener('ended', record.endedHandler);
447
+ }
448
+ catch ( /* ignore */_b) { /* ignore */ }
449
+ }
450
+ try {
451
+ record.video.removeAttribute('src');
452
+ record.video.load();
453
+ }
454
+ catch ( /* ignore */_c) { /* ignore */ }
455
+ if (record.texture) {
456
+ try {
457
+ record.texture.destroy(false);
458
+ }
459
+ catch ( /* ignore */_d) { /* ignore */ }
460
+ record.texture = null;
461
+ }
462
+ }
463
+ destroy() {
464
+ var _a;
465
+ for (const key in this.records) {
466
+ const id = parseInt(key);
467
+ const record = this.records[id];
468
+ this.tearDown(record);
469
+ const container = (_a = this.containerManager) === null || _a === void 0 ? void 0 : _a.getContainer(id);
470
+ if (container)
471
+ container.removeChild(record.sprite);
472
+ try {
473
+ record.sprite.destroy({ children: true });
474
+ }
475
+ catch ( /* ignore */_b) { /* ignore */ }
476
+ delete this.records[id];
477
+ }
478
+ }
479
+ };
480
+ VideoSystem.systemName = 'Video';
481
+ VideoSystem = __decorate([
482
+ eva_js.decorators.componentObserver({
483
+ Video: [
484
+ { prop: ['src'], deep: false },
485
+ { prop: ['loop'], deep: false },
486
+ { prop: ['muted'], deep: false },
487
+ { prop: ['volume'], deep: false },
488
+ { prop: ['playbackRate'], deep: false },
489
+ ],
490
+ })
491
+ ], VideoSystem);
492
+ var VideoSystem$1 = VideoSystem;
493
+
494
+ exports.Video = Video;
495
+ exports.VideoSystem = VideoSystem$1;
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("@eva/eva.js"),t=require("@eva/inspector-decorator"),o=require("@eva/plugin-renderer"),r=require("pixi.js");function n(e,t,o,r){var n,i=arguments.length,d=i<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,o):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)d=Reflect.decorate(e,t,o,r);else for(var a=e.length-1;a>=0;a--)(n=e[a])&&(d=(i<3?n(d):i>3?n(t,o,d):n(t,o))||d);return i>3&&d&&Object.defineProperty(t,o,d),d}function i(e,t){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(e,t)}function d(e,t,o,r){return new(o||(o=Promise))(function(n,i){function d(e){try{s(r.next(e))}catch(e){i(e)}}function a(e){try{s(r.throw(e))}catch(e){i(e)}}function s(e){var t;e.done?n(e.value):(t=e.value,t instanceof o?t:new o(function(e){e(t)})).then(d,a)}s((r=r.apply(e,t||[])).next())})}"function"==typeof SuppressedError&&SuppressedError;class a extends e.Component{constructor(){super(...arguments),this.src="",this.loop=!1,this.autoplay=!0,this.muted=!0,this.volume=1,this.playbackRate=1,this.anchorX=.5,this.anchorY=.5,this.crossOrigin="anonymous",this.playsInline=!0,this.completed=!1}init(e){e&&Object.assign(this,e)}play(){if(this.videoElement)try{const e=this.videoElement.play();if(e&&"function"==typeof e.catch)return e.catch(e=>{console.warn("[Video] play() rejected:",e)})}catch(e){console.warn("[Video] play() threw:",e)}}pause(){if(this.videoElement)try{this.videoElement.pause()}catch(e){}}setCurrentTime(e){if(this.videoElement)try{this.videoElement.currentTime=Math.max(0,e)}catch(e){}}snapshot(e){var t,o,r,n;const i=this.videoElement;if(!i||i.readyState<2)return null;const d=null!==(t=null==e?void 0:e.width)&&void 0!==t?t:i.videoWidth,a=null!==(o=null==e?void 0:e.height)&&void 0!==o?o:i.videoHeight;if(d<=0||a<=0)return null;try{const t=document.createElement("canvas");t.width=d,t.height=a;const o=t.getContext("2d");return o?(o.drawImage(i,null!==(r=null==e?void 0:e.x)&&void 0!==r?r:0,null!==(n=null==e?void 0:e.y)&&void 0!==n?n:0,d,a,0,0,d,a),t):null}catch(e){return null}}}function s(t){if(!t)return"";if(t.startsWith("http://")||t.startsWith("https://")||t.startsWith("//")||t.startsWith("/"))return t;if(/\.(mp4|webm|ogv|m4v|mov)(\?|#|$)/i.test(t))return t;try{const o=e.resource.resourcesMap||e.resource.resources;if(o&&o[t]){const e=o[t],r=e.src||e.data||{},n=r.video||r.audio||r.image||r;if(null==n?void 0:n.url)return n.url;if(null==n?void 0:n.src)return n.src}}catch(e){}return t}a.componentName="Video",n([t.type("string"),i("design:type",String)],a.prototype,"src",void 0),n([t.type("boolean"),i("design:type",Boolean)],a.prototype,"loop",void 0),n([t.type("boolean"),i("design:type",Boolean)],a.prototype,"autoplay",void 0),n([t.type("boolean"),i("design:type",Boolean)],a.prototype,"muted",void 0),n([t.type("number"),i("design:type",Number)],a.prototype,"volume",void 0),n([t.type("number"),i("design:type",Number)],a.prototype,"playbackRate",void 0),n([t.type("number"),i("design:type",Number)],a.prototype,"anchorX",void 0),n([t.type("number"),i("design:type",Number)],a.prototype,"anchorY",void 0),n([t.type("boolean"),i("design:type",Boolean)],a.prototype,"playsInline",void 0);let c=class extends o.Renderer{constructor(){super(...arguments),this.name="Video",this.records={}}init(){this.renderSystem=this.game.getSystem(o.RendererSystem),this.renderSystem.rendererManager.register(this)}rendererUpdate(e){var t,o,n,i,d;const a=this.records[e.id];if(!a)return;const s=a.component,c=a.video;if(a.ctx&&c.readyState>=2&&c.videoWidth>0&&c.videoHeight>0){if(!a.resizedToVideo){a.canvas.width=c.videoWidth,a.canvas.height=c.videoHeight,a.resizedToVideo=!0;const e=a.texture,t=r.Texture.from(a.canvas);if(a.texture=t,a.sprite.texture=t,e&&e!==t)try{e.destroy(!1)}catch(e){}}try{a.ctx.drawImage(c,0,0,a.canvas.width,a.canvas.height);const e=null===(t=a.texture)||void 0===t?void 0:t.source;e&&("function"==typeof e.update?e.update():e.dirty=!0)}catch(e){}}const l=null!==(o=s.width)&&void 0!==o?o:c.videoWidth||256,p=null!==(n=s.height)&&void 0!==n?n:c.videoHeight||256;a.sprite.width=l,a.sprite.height=p,a.sprite.anchor.set(null!==(i=s.anchorX)&&void 0!==i?i:.5,null!==(d=s.anchorY)&&void 0!==d?d:.5)}componentChanged(t){var o,r,n,i,a;return d(this,void 0,void 0,function*(){if("Video"!==t.componentName)return;const d=t.component,c=t.gameObject.id;if(t.type===e.OBSERVER_TYPE.ADD){const e=this.createRecord(d);this.records[c]=e;const o=this.containerManager.getContainer(c);o&&o.addChildAt(e.sprite,0),this.attachEndedHandler(e,t.gameObject),this.startLoad(e)}else if(t.type===e.OBSERVER_TYPE.CHANGE){const e=this.records[c];if(!e)return;const a=null===(r=null===(o=t.prop)||void 0===o?void 0:o.prop)||void 0===r?void 0:r[0];if("src"===a){try{e.video.pause()}catch(e){}const t=s(d.src);e.video.src=t,e.component=d,d.videoElement=e.video,d.completed=!1,e.resizedToVideo=!1;try{e.video.load()}catch(e){}!1!==d.autoplay&&this.tryPlay(e.video)}else"loop"===a?e.video.loop=!!d.loop:"muted"===a?e.video.muted=!!d.muted:"volume"===a?e.video.volume=Math.max(0,Math.min(1,null!==(n=d.volume)&&void 0!==n?n:1)):"playbackRate"===a&&(e.video.playbackRate=null!==(i=d.playbackRate)&&void 0!==i?i:1)}else if(t.type===e.OBSERVER_TYPE.REMOVE){const e=this.records[c];if(!e)return;this.tearDown(e);const t=null===(a=this.containerManager)||void 0===a?void 0:a.getContainer(c);t&&t.removeChild(e.sprite);try{e.sprite.destroy({children:!0})}catch(e){}delete this.records[c]}})}createRecord(e){var t,o,n,i,d,a,s;const c=document.createElement("video");c.crossOrigin=null!==(t=e.crossOrigin)&&void 0!==t?t:"anonymous",c.loop=!!e.loop,c.muted=!1!==e.muted,c.volume=Math.max(0,Math.min(1,null!==(o=e.volume)&&void 0!==o?o:1)),c.playbackRate=null!==(n=e.playbackRate)&&void 0!==n?n:1,c.playsInline=!1!==e.playsInline,c["webkit-playsinline"]="",c.preload="auto",e.videoElement=c;const l=document.createElement("canvas");l.width=null!==(i=e.width)&&void 0!==i?i:256,l.height=null!==(d=e.height)&&void 0!==d?d:256;const p=l.getContext("2d"),u=r.Texture.from("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="),v=new r.Sprite(u);return v.anchor.set(null!==(a=e.anchorX)&&void 0!==a?a:.5,null!==(s=e.anchorY)&&void 0!==s?s:.5),{video:c,canvas:l,ctx:p,texture:u,sprite:v,component:e,endedHandler:null,resizedToVideo:!1}}attachEndedHandler(e,t){const o=()=>{e.component.completed=!0;try{e.video.dispatchEvent(new Event("eva-video-complete"))}catch(e){}};e.video.addEventListener("ended",o),e.endedHandler=o}startLoad(e){const t=s(e.component.src);e.video.src=t;try{e.video.load()}catch(e){}if(!1!==e.component.autoplay){const t=()=>{e.video.removeEventListener("canplay",t),e.video.removeEventListener("loadeddata",t),this.tryPlay(e.video)};e.video.readyState>=2?this.tryPlay(e.video):(e.video.addEventListener("canplay",t),e.video.addEventListener("loadeddata",t))}}tryPlay(e){try{const t=e.play();t&&"function"==typeof t.catch&&t.catch(e=>{console.warn("[Video] autoplay rejected:",(null==e?void 0:e.message)||e)})}catch(e){console.warn("[Video] autoplay threw:",e)}}tearDown(e){try{e.video.pause()}catch(e){}if(e.endedHandler)try{e.video.removeEventListener("ended",e.endedHandler)}catch(e){}try{e.video.removeAttribute("src"),e.video.load()}catch(e){}if(e.texture){try{e.texture.destroy(!1)}catch(e){}e.texture=null}}destroy(){var e;for(const t in this.records){const o=parseInt(t),r=this.records[o];this.tearDown(r);const n=null===(e=this.containerManager)||void 0===e?void 0:e.getContainer(o);n&&n.removeChild(r.sprite);try{r.sprite.destroy({children:!0})}catch(e){}delete this.records[o]}}};c.systemName="Video",c=n([e.decorators.componentObserver({Video:[{prop:["src"],deep:!1},{prop:["loop"],deep:!1},{prop:["muted"],deep:!1},{prop:["volume"],deep:!1},{prop:["playbackRate"],deep:!1}]})],c);var l=c;exports.Video=a,exports.VideoSystem=l;
@@ -0,0 +1,118 @@
1
+ import { Component } from '@eva/eva.js';
2
+ import { ComponentChanged } from '@eva/eva.js';
3
+ import { ContainerManager } from '@eva/plugin-renderer';
4
+ import { GameObject } from '@eva/eva.js';
5
+ import { Renderer } from '@eva/plugin-renderer';
6
+ import { RendererManager } from '@eva/plugin-renderer';
7
+ import { RendererSystem } from '@eva/plugin-renderer';
8
+
9
+ /**
10
+ * Phaser 风格的 Video 渲染组件(Eva.js MVP 实现)。
11
+ *
12
+ * 行为:
13
+ * 1. ADD 时 VideoSystem 会创建一个 detached 的 `<video>` DOM 元素 + PixiJS Texture + Sprite,
14
+ * Sprite 加入到 GameObject 的 PixiJS Container;
15
+ * 2. video 的源 (`<video>.src`) 由 `src` 决定:
16
+ * - 若注册过同名 VIDEO/IMAGE 资源(且 instance.src 可用),取注册的 url;
17
+ * - 否则直接当作 URL 使用。
18
+ * 3. 视频进入 `loadeddata` 后即可绘制;
19
+ * 4. CHANGE prop 时按属性增量更新(loop/muted/volume/playbackRate/src);
20
+ * 5. REMOVE/destroy 时停止播放、销毁 video/texture/sprite。
21
+ *
22
+ * 不支持(明确 SKIP):
23
+ * - HLS/DASH/MSE 高级流;
24
+ * - getUserMedia / 透明视频(alpha video / chroma key);
25
+ * - 作为 shader 纹理(Eva.js 无 GLSL 注入);
26
+ * - Phaser saveTexture 把视频转 sprite atlas 的工作流。
27
+ */
28
+ export declare class Video extends Component<VideoParams> {
29
+ static componentName: string;
30
+ src: string;
31
+ loop: boolean;
32
+ autoplay: boolean;
33
+ muted: boolean;
34
+ volume: number;
35
+ playbackRate: number;
36
+ width?: number;
37
+ height?: number;
38
+ anchorX: number;
39
+ anchorY: number;
40
+ onComplete?: string;
41
+ crossOrigin?: 'anonymous' | 'use-credentials' | '';
42
+ playsInline: boolean;
43
+ /** Runtime 标记:System 设置,表示视频已经触发过 `ended`。 */
44
+ completed: boolean;
45
+ /** 由 System 在 ADD 后写回,便于业务侧 component.play()/pause() 直接操控底层 video。 */
46
+ videoElement?: HTMLVideoElement;
47
+ init(obj?: VideoParams): void;
48
+ /** 立即播放(若 video 已就绪)。 */
49
+ play(): Promise<void> | void;
50
+ /** 暂停。 */
51
+ pause(): void;
52
+ /** 跳转到指定时间(秒)。 */
53
+ setCurrentTime(seconds: number): void;
54
+ /** 把当前帧画到一个独立 canvas 上并返回,用于 snapshot(失败时返回 null)。 */
55
+ snapshot(area?: {
56
+ x: number;
57
+ y: number;
58
+ width: number;
59
+ height: number;
60
+ }): HTMLCanvasElement | null;
61
+ }
62
+
63
+ /**
64
+ * Phaser-style Video 组件入参(MVP)。
65
+ *
66
+ * 注意:
67
+ * - `src` 既可以是已注册到资源加载器的资源 key,也可以直接是 .mp4/.webm 等可播放 URL。
68
+ * - 浏览器的 autoplay 限制要求视频必须 muted=true 才能在没有用户交互时播放。
69
+ * - 视频通过隐藏的 `<video>` 元素 + PixiJS `Texture.from(video)` 接入 ECS 渲染管线。
70
+ */
71
+ export declare interface VideoParams {
72
+ /** 视频 URL 或 DSL assets 中的资源 key。 */
73
+ src: string;
74
+ /** 是否循环播放,默认 false。 */
75
+ loop?: boolean;
76
+ /** 是否在加载完成后自动开始播放,默认 true。 */
77
+ autoplay?: boolean;
78
+ /** 是否静音,默认 true(浏览器自动播放限制下必须为 true)。 */
79
+ muted?: boolean;
80
+ /** 音量 0~1,默认 1。 */
81
+ volume?: number;
82
+ /** 播放速率,默认 1。 */
83
+ playbackRate?: number;
84
+ /** 显示宽度;不填则使用 video 的 videoWidth(回落到 256)。 */
85
+ width?: number;
86
+ /** 显示高度;不填则使用 video 的 videoHeight(回落到 256)。 */
87
+ height?: number;
88
+ /** Sprite anchor X,默认 0.5。 */
89
+ anchorX?: number;
90
+ /** Sprite anchor Y,默认 0.5。 */
91
+ anchorY?: number;
92
+ /** 命名钩子:视频播放结束 (ended 事件)时触发,DSL 中保存事件 key 供宿主消费。 */
93
+ onComplete?: string;
94
+ /** 跨域设置,默认 'anonymous'(允许 canvas 取像素 / snapshot)。 */
95
+ crossOrigin?: 'anonymous' | 'use-credentials' | '';
96
+ /** 是否 inline 播放(iOS 必需),默认 true。 */
97
+ playsInline?: boolean;
98
+ }
99
+
100
+ export declare class VideoSystem extends Renderer {
101
+ static systemName: string;
102
+ name: string;
103
+ private records;
104
+ renderSystem: RendererSystem;
105
+ rendererManager: RendererManager;
106
+ containerManager: ContainerManager;
107
+ init(): void;
108
+ rendererUpdate(gameObject: GameObject): void;
109
+ componentChanged(changed: ComponentChanged): Promise<void>;
110
+ private createRecord;
111
+ private attachEndedHandler;
112
+ private startLoad;
113
+ private tryPlay;
114
+ private tearDown;
115
+ destroy(): void;
116
+ }
117
+
118
+ export { }