@xiboplayer/renderer 0.4.0 → 0.4.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.
- package/README.md +3 -1
- package/docs/RENDERER_COMPARISON.md +9 -18
- package/package.json +3 -3
- package/src/renderer-lite.js +113 -8
- package/src/renderer-lite.test.js +11 -11
package/README.md
CHANGED
|
@@ -12,6 +12,8 @@ RendererLite parses Xibo Layout Format (XLF) files and builds a live DOM with:
|
|
|
12
12
|
- **Layout preloading** — 2-layout pool pre-builds upcoming layouts at 75% of current duration for zero-gap transitions
|
|
13
13
|
- **Proportional scaling** — ResizeObserver-based scaling to fit any screen resolution
|
|
14
14
|
- **Overlay support** — multiple simultaneous overlay layouts with independent z-index (1000+)
|
|
15
|
+
- **Absolute widget positioning** — widget elements use `position: absolute` within regions to layer correctly in multi-widget regions
|
|
16
|
+
- **Animation cleanup** — `fill: forwards` animations cancelled between widgets to prevent stale visual state (e.g. video hidden after PDF)
|
|
15
17
|
|
|
16
18
|
## Installation
|
|
17
19
|
|
|
@@ -37,7 +39,7 @@ await renderer.renderLayout(xlf, { mediaBaseUrl: '/cache/' });
|
|
|
37
39
|
| Widget | Implementation |
|
|
38
40
|
|--------|---------------|
|
|
39
41
|
| Video | `<video>` with native HLS (Safari) + hls.js fallback, pause-on-last-frame |
|
|
40
|
-
| Image | `<img>` with
|
|
42
|
+
| Image | `<img>` with CMS scaleType mapping (center->contain, stretch->fill, fit->cover), blob URL from cache |
|
|
41
43
|
| PDF | PDF.js canvas rendering (dynamically imported) |
|
|
42
44
|
| Text / Ticker | iframe with CMS-rendered HTML via GetResource |
|
|
43
45
|
| Web page | bare `<iframe src="...">` |
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
| Visibility toggle | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Complete |
|
|
27
27
|
| Avoid DOM recreation | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Complete |
|
|
28
28
|
| Layout reuse detection | ⚠️ Partial | ✅ Yes | ✅ Yes | ✅ Better than XLR! |
|
|
29
|
+
| Widget absolute positioning | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Complete |
|
|
30
|
+
| Image scaleType mapping | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Complete (center->contain, stretch->fill, fit->cover) |
|
|
29
31
|
| **Widget Types** | | | | |
|
|
30
32
|
| Image | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Complete |
|
|
31
33
|
| Video | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Complete |
|
|
@@ -206,20 +208,15 @@ RendererLite events match XLR/Arexibo with additions:
|
|
|
206
208
|
|
|
207
209
|
### 7. Memory Management
|
|
208
210
|
|
|
209
|
-
**Status**:
|
|
211
|
+
**Status**: ✅ **Complete**
|
|
210
212
|
|
|
211
213
|
**What's correct**:
|
|
212
214
|
- ✅ Elements reused (not recreated)
|
|
213
|
-
- ✅ Blob URLs revoked on layout change
|
|
215
|
+
- ✅ Blob URLs revoked on layout change (layout-scoped tracking)
|
|
214
216
|
- ✅ Cache cleared appropriately
|
|
215
217
|
- ✅ Timers cleared before new layout
|
|
216
218
|
- ✅ Event listeners managed properly
|
|
217
|
-
|
|
218
|
-
**Gap identified**:
|
|
219
|
-
- ⚠️ Layout-scoped blob URL tracking missing
|
|
220
|
-
- ⚠️ Could accumulate blob URLs across many layout cycles
|
|
221
|
-
|
|
222
|
-
**Impact**: Low (only affects 24/7 deployments with frequent layout changes)
|
|
219
|
+
- ✅ `fill: forwards` animations cancelled between widgets to prevent stale visual state
|
|
223
220
|
|
|
224
221
|
---
|
|
225
222
|
|
|
@@ -231,12 +228,7 @@ RendererLite events match XLR/Arexibo with additions:
|
|
|
231
228
|
|
|
232
229
|
### Important Features (Should Have)
|
|
233
230
|
|
|
234
|
-
1. **
|
|
235
|
-
- **Priority**: Medium
|
|
236
|
-
- **Impact**: Memory leak in long-running deployments
|
|
237
|
-
- **Effort**: Low (add Map tracking)
|
|
238
|
-
|
|
239
|
-
2. **Widget action events**
|
|
231
|
+
1. **Widget action events**
|
|
240
232
|
- **Priority**: Low
|
|
241
233
|
- **Impact**: Interactive widgets might need action callbacks
|
|
242
234
|
- **Effort**: Medium (event propagation from widget iframes)
|
|
@@ -417,9 +409,8 @@ XLF → Parse → Pre-create Elements → Toggle Visibility → Transitions
|
|
|
417
409
|
|
|
418
410
|
### ⚠️ Features Needing Work
|
|
419
411
|
|
|
420
|
-
1. **
|
|
421
|
-
2. **
|
|
422
|
-
3. **Service Worker**: Currently disabled (HTTP 202 issues)
|
|
412
|
+
1. **Widget Actions**: Event propagation from iframes
|
|
413
|
+
2. **Service Worker**: Currently disabled (HTTP 202 issues)
|
|
423
414
|
|
|
424
415
|
### ❌ Features Not Applicable
|
|
425
416
|
|
|
@@ -472,7 +463,7 @@ XLF → Parse → Pre-create Elements → Toggle Visibility → Transitions
|
|
|
472
463
|
|
|
473
464
|
**RendererLite successfully implements the Arexibo pattern** and adds significant performance improvements through parallelization. The implementation is production-ready with minor improvements needed for blob URL lifecycle management.
|
|
474
465
|
|
|
475
|
-
**Feature Parity**: ~
|
|
466
|
+
**Feature Parity**: ~98% (missing only widget action event propagation)
|
|
476
467
|
**Performance**: Exceeds XLR and Arexibo benchmarks
|
|
477
468
|
**Memory**: Stable with Arexibo pattern correctly implemented
|
|
478
469
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiboplayer/renderer",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "RendererLite - Fast, efficient XLF layout rendering engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"nanoevents": "^9.1.0",
|
|
14
14
|
"pdfjs-dist": "^4.10.38",
|
|
15
|
-
"@xiboplayer/cache": "0.4.
|
|
16
|
-
"@xiboplayer/utils": "0.4.
|
|
15
|
+
"@xiboplayer/cache": "0.4.3",
|
|
16
|
+
"@xiboplayer/utils": "0.4.3"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"vitest": "^2.0.0",
|
package/src/renderer-lite.js
CHANGED
|
@@ -1098,6 +1098,11 @@ export class RendererLite {
|
|
|
1098
1098
|
|
|
1099
1099
|
try {
|
|
1100
1100
|
const element = await this.createWidgetElement(widget, region);
|
|
1101
|
+
element.style.position = 'absolute';
|
|
1102
|
+
element.style.top = '0';
|
|
1103
|
+
element.style.left = '0';
|
|
1104
|
+
element.style.width = '100%';
|
|
1105
|
+
element.style.height = '100%';
|
|
1101
1106
|
element.style.visibility = 'hidden'; // Hidden by default
|
|
1102
1107
|
element.style.opacity = '0';
|
|
1103
1108
|
region.element.appendChild(element);
|
|
@@ -1207,8 +1212,9 @@ export class RendererLite {
|
|
|
1207
1212
|
* @returns {Promise<HTMLElement>} Widget DOM element
|
|
1208
1213
|
*/
|
|
1209
1214
|
async createWidgetElement(widget, region) {
|
|
1210
|
-
// render="html" forces GetResource iframe regardless of native type
|
|
1211
|
-
|
|
1215
|
+
// render="html" forces GetResource iframe regardless of native type,
|
|
1216
|
+
// EXCEPT for types we handle natively (PDF: CMS bundle can't work cross-origin)
|
|
1217
|
+
if (widget.render === 'html' && widget.type !== 'pdf') {
|
|
1212
1218
|
return await this.renderGenericWidget(widget, region);
|
|
1213
1219
|
}
|
|
1214
1220
|
|
|
@@ -1228,6 +1234,8 @@ export class RendererLite {
|
|
|
1228
1234
|
return await this.renderWebpage(widget, region);
|
|
1229
1235
|
case 'localvideo':
|
|
1230
1236
|
return await this.renderVideo(widget, region);
|
|
1237
|
+
case 'videoin':
|
|
1238
|
+
return await this.renderVideoIn(widget, region);
|
|
1231
1239
|
case 'powerpoint':
|
|
1232
1240
|
case 'flash':
|
|
1233
1241
|
// Legacy Windows-only types — show placeholder instead of failing silently
|
|
@@ -1259,6 +1267,18 @@ export class RendererLite {
|
|
|
1259
1267
|
// Restart video or audio on widget show (even if looping)
|
|
1260
1268
|
const mediaEl = this.findMediaElement(element, 'VIDEO') || this.findMediaElement(element, 'AUDIO');
|
|
1261
1269
|
if (mediaEl) {
|
|
1270
|
+
// Re-acquire webcam stream if it was stopped during _hideWidget()
|
|
1271
|
+
if (mediaEl.tagName === 'VIDEO' && mediaEl._mediaConstraints && !mediaEl._mediaStream) {
|
|
1272
|
+
navigator.mediaDevices.getUserMedia(mediaEl._mediaConstraints).then(stream => {
|
|
1273
|
+
mediaEl.srcObject = stream;
|
|
1274
|
+
mediaEl._mediaStream = stream;
|
|
1275
|
+
this.log.info(`Webcam stream re-acquired for widget ${widget.id}`);
|
|
1276
|
+
}).catch(e => {
|
|
1277
|
+
this.log.warn('Failed to re-acquire webcam stream:', e.message);
|
|
1278
|
+
});
|
|
1279
|
+
return; // srcObject auto-plays, no need for _restartMediaElement
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1262
1282
|
this._restartMediaElement(mediaEl);
|
|
1263
1283
|
this.log.info(`${mediaEl.tagName === 'VIDEO' ? 'Video' : 'Audio'} restarted: ${widget.fileId || widget.id}`);
|
|
1264
1284
|
}
|
|
@@ -1427,19 +1447,27 @@ export class RendererLite {
|
|
|
1427
1447
|
if (!element) {
|
|
1428
1448
|
this.log.warn(`Widget ${widget.id} not pre-created, creating now`);
|
|
1429
1449
|
element = await this.createWidgetElement(widget, region);
|
|
1450
|
+
element.style.position = 'absolute';
|
|
1451
|
+
element.style.top = '0';
|
|
1452
|
+
element.style.left = '0';
|
|
1453
|
+
element.style.width = '100%';
|
|
1454
|
+
element.style.height = '100%';
|
|
1430
1455
|
region.widgetElements.set(widget.id, element);
|
|
1431
1456
|
region.element.appendChild(element);
|
|
1432
1457
|
}
|
|
1433
1458
|
|
|
1434
1459
|
// Hide all other widgets in region
|
|
1460
|
+
// Cancel fill:forwards animations first — they override inline styles
|
|
1435
1461
|
for (const [widgetId, widgetEl] of region.widgetElements) {
|
|
1436
1462
|
if (widgetId !== widget.id) {
|
|
1463
|
+
widgetEl.getAnimations?.().forEach(a => a.cancel());
|
|
1437
1464
|
widgetEl.style.visibility = 'hidden';
|
|
1438
1465
|
widgetEl.style.opacity = '0';
|
|
1439
1466
|
}
|
|
1440
1467
|
}
|
|
1441
1468
|
|
|
1442
1469
|
this.updateMediaElement(element, widget);
|
|
1470
|
+
element.getAnimations?.().forEach(a => a.cancel());
|
|
1443
1471
|
element.style.visibility = 'visible';
|
|
1444
1472
|
|
|
1445
1473
|
if (widget.transitions.in) {
|
|
@@ -1553,6 +1581,13 @@ export class RendererLite {
|
|
|
1553
1581
|
const videoEl = widgetElement.querySelector('video');
|
|
1554
1582
|
if (videoEl && widget.options.loop !== '1') videoEl.pause();
|
|
1555
1583
|
|
|
1584
|
+
// Stop MediaStream tracks (webcam/mic) to release the device
|
|
1585
|
+
if (videoEl?._mediaStream) {
|
|
1586
|
+
videoEl._mediaStream.getTracks().forEach(t => t.stop());
|
|
1587
|
+
videoEl._mediaStream = null;
|
|
1588
|
+
videoEl.srcObject = null;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1556
1591
|
const audioEl = widgetElement.querySelector('audio');
|
|
1557
1592
|
if (audioEl && widget.options.loop !== '1') audioEl.pause();
|
|
1558
1593
|
|
|
@@ -1794,16 +1829,20 @@ export class RendererLite {
|
|
|
1794
1829
|
img.className = 'renderer-lite-widget';
|
|
1795
1830
|
img.style.width = '100%';
|
|
1796
1831
|
img.style.height = '100%';
|
|
1797
|
-
// Scale type
|
|
1832
|
+
// Scale type mapping (CMS image.xml):
|
|
1833
|
+
// center (default) → contain: scale proportionally to fit region, centered
|
|
1834
|
+
// stretch → fill: ignore aspect ratio, fill entire region
|
|
1835
|
+
// fit → cover: scale proportionally to fill region, crop excess
|
|
1798
1836
|
const scaleType = widget.options.scaleType;
|
|
1799
|
-
const fitMap = { stretch: 'fill', center: '
|
|
1837
|
+
const fitMap = { stretch: 'fill', center: 'contain', fit: 'cover' };
|
|
1800
1838
|
img.style.objectFit = fitMap[scaleType] || 'contain';
|
|
1801
1839
|
|
|
1802
|
-
// Alignment: map
|
|
1840
|
+
// Alignment: map alignId/valignId to CSS object-position
|
|
1841
|
+
// XLF tags are <alignId> and <valignId> (from CMS image.xml property ids)
|
|
1803
1842
|
const alignMap = { left: 'left', center: 'center', right: 'right' };
|
|
1804
1843
|
const valignMap = { top: 'top', middle: 'center', bottom: 'bottom' };
|
|
1805
|
-
const hPos = alignMap[widget.options.
|
|
1806
|
-
const vPos = valignMap[widget.options.
|
|
1844
|
+
const hPos = alignMap[widget.options.alignId] || 'center';
|
|
1845
|
+
const vPos = valignMap[widget.options.valignId] || 'center';
|
|
1807
1846
|
img.style.objectPosition = `${hPos} ${vPos}`;
|
|
1808
1847
|
|
|
1809
1848
|
img.style.opacity = '0';
|
|
@@ -1943,6 +1982,65 @@ export class RendererLite {
|
|
|
1943
1982
|
return video;
|
|
1944
1983
|
}
|
|
1945
1984
|
|
|
1985
|
+
/**
|
|
1986
|
+
* Render videoin (webcam/microphone) widget.
|
|
1987
|
+
* Uses getUserMedia() to capture live video from camera hardware.
|
|
1988
|
+
* @param {Object} widget - Widget config with options (sourceId, showFullScreen, mirror, mute, captureAudio)
|
|
1989
|
+
* @param {Object} region - Region dimensions (width, height)
|
|
1990
|
+
* @returns {HTMLVideoElement}
|
|
1991
|
+
*/
|
|
1992
|
+
async renderVideoIn(widget, region) {
|
|
1993
|
+
const video = document.createElement('video');
|
|
1994
|
+
video.className = 'renderer-lite-widget';
|
|
1995
|
+
video.style.width = '100%';
|
|
1996
|
+
video.style.height = '100%';
|
|
1997
|
+
video.style.objectFit = widget.options.showFullScreen === '1' ? 'cover' : 'contain';
|
|
1998
|
+
video.autoplay = true;
|
|
1999
|
+
video.playsInline = true;
|
|
2000
|
+
video.controls = false;
|
|
2001
|
+
video.muted = widget.options.mute !== '0'; // Muted by default to prevent audio feedback
|
|
2002
|
+
|
|
2003
|
+
// Mirror mode (front-facing camera)
|
|
2004
|
+
if (widget.options.mirror === '1') {
|
|
2005
|
+
video.style.transform = 'scaleX(-1)';
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
// Build getUserMedia constraints
|
|
2009
|
+
const videoConstraints = {
|
|
2010
|
+
width: { ideal: region.width },
|
|
2011
|
+
height: { ideal: region.height },
|
|
2012
|
+
};
|
|
2013
|
+
const deviceId = widget.options.sourceId || widget.options.deviceId;
|
|
2014
|
+
if (deviceId) {
|
|
2015
|
+
videoConstraints.deviceId = { exact: deviceId };
|
|
2016
|
+
} else {
|
|
2017
|
+
videoConstraints.facingMode = widget.options.facingMode || 'environment';
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
const constraints = {
|
|
2021
|
+
video: videoConstraints,
|
|
2022
|
+
audio: widget.options.captureAudio === '1',
|
|
2023
|
+
};
|
|
2024
|
+
|
|
2025
|
+
// Store constraints for re-acquisition after layout transitions
|
|
2026
|
+
video._mediaConstraints = constraints;
|
|
2027
|
+
|
|
2028
|
+
try {
|
|
2029
|
+
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
2030
|
+
video.srcObject = stream;
|
|
2031
|
+
video._mediaStream = stream;
|
|
2032
|
+
this.log.info(`Webcam stream acquired for widget ${widget.id} (tracks: ${stream.getTracks().length})`);
|
|
2033
|
+
} catch (e) {
|
|
2034
|
+
this.log.warn(`getUserMedia failed for widget ${widget.id}: ${e.message}`);
|
|
2035
|
+
return this._renderUnsupportedPlaceholder(
|
|
2036
|
+
{ ...widget, type: 'Camera unavailable' },
|
|
2037
|
+
region
|
|
2038
|
+
);
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
return video;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
1946
2044
|
/**
|
|
1947
2045
|
* Render audio widget
|
|
1948
2046
|
*/
|
|
@@ -2110,7 +2208,9 @@ export class RendererLite {
|
|
|
2110
2208
|
try {
|
|
2111
2209
|
const pdfjsModule = await import('pdfjs-dist');
|
|
2112
2210
|
window.pdfjsLib = pdfjsModule;
|
|
2113
|
-
|
|
2211
|
+
// Derive worker path from current page location (works for /player/pwa/ and /player/)
|
|
2212
|
+
const basePath = window.location.pathname.replace(/\/[^/]*$/, '/');
|
|
2213
|
+
window.pdfjsLib.GlobalWorkerOptions.workerSrc = `${window.location.origin}${basePath}pdf.worker.min.mjs`;
|
|
2114
2214
|
} catch (error) {
|
|
2115
2215
|
this.log.error('PDF.js not available:', error);
|
|
2116
2216
|
container.innerHTML = '<div style="color:white;padding:20px;text-align:center;">PDF viewer unavailable</div>';
|
|
@@ -2454,6 +2554,11 @@ export class RendererLite {
|
|
|
2454
2554
|
|
|
2455
2555
|
try {
|
|
2456
2556
|
const element = await this.createWidgetElement(widget, region);
|
|
2557
|
+
element.style.position = 'absolute';
|
|
2558
|
+
element.style.top = '0';
|
|
2559
|
+
element.style.left = '0';
|
|
2560
|
+
element.style.width = '100%';
|
|
2561
|
+
element.style.height = '100%';
|
|
2457
2562
|
element.style.visibility = 'hidden';
|
|
2458
2563
|
element.style.opacity = '0';
|
|
2459
2564
|
region.element.appendChild(element);
|
|
@@ -451,7 +451,7 @@ describe('RendererLite', () => {
|
|
|
451
451
|
expect(element.style.objectFit).toBe('fill');
|
|
452
452
|
});
|
|
453
453
|
|
|
454
|
-
it('should apply objectFit
|
|
454
|
+
it('should apply objectFit contain when scaleType is center (proportional fit)', async () => {
|
|
455
455
|
const widget = {
|
|
456
456
|
type: 'image',
|
|
457
457
|
id: 'm1',
|
|
@@ -464,10 +464,10 @@ describe('RendererLite', () => {
|
|
|
464
464
|
const region = { width: 1920, height: 1080 };
|
|
465
465
|
const element = await renderer.renderImage(widget, region);
|
|
466
466
|
|
|
467
|
-
expect(element.style.objectFit).toBe('
|
|
467
|
+
expect(element.style.objectFit).toBe('contain');
|
|
468
468
|
});
|
|
469
469
|
|
|
470
|
-
it('should apply objectFit
|
|
470
|
+
it('should apply objectFit cover when scaleType is fit', async () => {
|
|
471
471
|
const widget = {
|
|
472
472
|
type: 'image',
|
|
473
473
|
id: 'm1',
|
|
@@ -480,15 +480,15 @@ describe('RendererLite', () => {
|
|
|
480
480
|
const region = { width: 1920, height: 1080 };
|
|
481
481
|
const element = await renderer.renderImage(widget, region);
|
|
482
482
|
|
|
483
|
-
expect(element.style.objectFit).toBe('
|
|
483
|
+
expect(element.style.objectFit).toBe('cover');
|
|
484
484
|
});
|
|
485
485
|
|
|
486
|
-
it('should map
|
|
486
|
+
it('should map alignId and valignId to objectPosition', async () => {
|
|
487
487
|
const widget = {
|
|
488
488
|
type: 'image',
|
|
489
489
|
id: 'm1',
|
|
490
490
|
fileId: '1',
|
|
491
|
-
options: { uri: 'test.png',
|
|
491
|
+
options: { uri: 'test.png', alignId: 'left', valignId: 'top' },
|
|
492
492
|
duration: 10,
|
|
493
493
|
transitions: { in: null, out: null }
|
|
494
494
|
};
|
|
@@ -499,12 +499,12 @@ describe('RendererLite', () => {
|
|
|
499
499
|
expect(element.style.objectPosition).toBe('left top');
|
|
500
500
|
});
|
|
501
501
|
|
|
502
|
-
it('should map
|
|
502
|
+
it('should map alignId right and valignId bottom to objectPosition', async () => {
|
|
503
503
|
const widget = {
|
|
504
504
|
type: 'image',
|
|
505
505
|
id: 'm1',
|
|
506
506
|
fileId: '1',
|
|
507
|
-
options: { uri: 'test.png',
|
|
507
|
+
options: { uri: 'test.png', alignId: 'right', valignId: 'bottom' },
|
|
508
508
|
duration: 10,
|
|
509
509
|
transitions: { in: null, out: null }
|
|
510
510
|
};
|
|
@@ -515,12 +515,12 @@ describe('RendererLite', () => {
|
|
|
515
515
|
expect(element.style.objectPosition).toBe('right bottom');
|
|
516
516
|
});
|
|
517
517
|
|
|
518
|
-
it('should map
|
|
518
|
+
it('should map valignId middle to center in objectPosition', async () => {
|
|
519
519
|
const widget = {
|
|
520
520
|
type: 'image',
|
|
521
521
|
id: 'm1',
|
|
522
522
|
fileId: '1',
|
|
523
|
-
options: { uri: 'test.png',
|
|
523
|
+
options: { uri: 'test.png', alignId: 'center', valignId: 'middle' },
|
|
524
524
|
duration: 10,
|
|
525
525
|
transitions: { in: null, out: null }
|
|
526
526
|
};
|
|
@@ -536,7 +536,7 @@ describe('RendererLite', () => {
|
|
|
536
536
|
type: 'image',
|
|
537
537
|
id: 'm1',
|
|
538
538
|
fileId: '1',
|
|
539
|
-
options: { uri: 'test.png', scaleType: 'stretch',
|
|
539
|
+
options: { uri: 'test.png', scaleType: 'stretch', alignId: 'left', valignId: 'bottom' },
|
|
540
540
|
duration: 10,
|
|
541
541
|
transitions: { in: null, out: null }
|
|
542
542
|
};
|