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

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