@xiboplayer/renderer 0.7.1 → 0.7.2
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/package.json +4 -4
- package/src/renderer-lite.js +5 -2
- package/src/renderer-lite.test.js +78 -112
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiboplayer/renderer",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "RendererLite - Fast, efficient XLF layout rendering engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"pdfjs-dist": "^4.10.38",
|
|
15
|
-
"@xiboplayer/cache": "0.7.
|
|
16
|
-
"@xiboplayer/schedule": "0.7.
|
|
17
|
-
"@xiboplayer/utils": "0.7.
|
|
15
|
+
"@xiboplayer/cache": "0.7.2",
|
|
16
|
+
"@xiboplayer/schedule": "0.7.2",
|
|
17
|
+
"@xiboplayer/utils": "0.7.2"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"vitest": "^2.0.0",
|
package/src/renderer-lite.js
CHANGED
|
@@ -1639,12 +1639,14 @@ export class RendererLite {
|
|
|
1639
1639
|
}
|
|
1640
1640
|
|
|
1641
1641
|
/**
|
|
1642
|
-
* Check if any video widget
|
|
1642
|
+
* Check if any video widget has useDuration=0 ("play to end") and hasn't
|
|
1643
|
+
* been corrected by video metadata yet. The XLF always provides a non-zero
|
|
1644
|
+
* duration attribute (typically 60s), so we check the _probed flag instead.
|
|
1643
1645
|
*/
|
|
1644
1646
|
_hasUnprobedVideos() {
|
|
1645
1647
|
for (const [, region] of this.regions) {
|
|
1646
1648
|
for (const widget of region.widgets) {
|
|
1647
|
-
if (widget.useDuration === 0 && widget.
|
|
1649
|
+
if (widget.useDuration === 0 && !widget._probed) return true;
|
|
1648
1650
|
}
|
|
1649
1651
|
}
|
|
1650
1652
|
return false;
|
|
@@ -2284,6 +2286,7 @@ export class RendererLite {
|
|
|
2284
2286
|
|
|
2285
2287
|
if (widget.duration === 0 || widget.useDuration === 0) {
|
|
2286
2288
|
widget.duration = videoDuration;
|
|
2289
|
+
widget._probed = true;
|
|
2287
2290
|
this.log.info(`Updated widget ${widget.id} duration to ${videoDuration}s (useDuration=0)`);
|
|
2288
2291
|
|
|
2289
2292
|
if (this.currentLayoutId === createdForLayoutId) {
|
|
@@ -7,10 +7,20 @@
|
|
|
7
7
|
* and memory management.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
10
|
+
import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest';
|
|
11
11
|
import { RendererLite, Transitions } from './renderer-lite.js';
|
|
12
12
|
|
|
13
13
|
describe('RendererLite', () => {
|
|
14
|
+
// Patch HTMLMediaElement after jsdom initializes — vitest.setup.js runs
|
|
15
|
+
// before the jsdom environment, so stubs set there get overwritten.
|
|
16
|
+
beforeAll(() => {
|
|
17
|
+
const proto = window.HTMLMediaElement.prototype;
|
|
18
|
+
proto.play = vi.fn(() => Promise.resolve());
|
|
19
|
+
proto.pause = vi.fn();
|
|
20
|
+
proto.load = vi.fn();
|
|
21
|
+
Object.defineProperty(proto, 'duration', { writable: true, configurable: true, value: NaN });
|
|
22
|
+
Object.defineProperty(proto, 'currentTime', { writable: true, configurable: true, value: 0 });
|
|
23
|
+
});
|
|
14
24
|
let container;
|
|
15
25
|
let renderer;
|
|
16
26
|
let mockGetWidgetHtml;
|
|
@@ -755,142 +765,98 @@ describe('RendererLite', () => {
|
|
|
755
765
|
});
|
|
756
766
|
|
|
757
767
|
describe('Video Duration Detection', () => {
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
//
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
const video = videoElement.querySelector('video');
|
|
776
|
-
|
|
777
|
-
// Simulate loadedmetadata event
|
|
778
|
-
Object.defineProperty(video, 'duration', { value: 45.5, writable: false });
|
|
779
|
-
video.dispatchEvent(new Event('loadedmetadata'));
|
|
780
|
-
|
|
781
|
-
// Wait for async handler
|
|
782
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
783
|
-
|
|
784
|
-
// Widget duration should be updated
|
|
785
|
-
const widget = region.widgets[0];
|
|
786
|
-
expect(widget.duration).toBe(45); // Floor of 45.5
|
|
768
|
+
it('should detect video duration from metadata', () => {
|
|
769
|
+
// Directly test _hasUnprobedVideos + duration update logic
|
|
770
|
+
// (renderer.renderLayout creates iframes in jsdom, not real video elements)
|
|
771
|
+
const widget = { id: 'm1', type: 'video', duration: 60, useDuration: 0, _probed: false };
|
|
772
|
+
const region = { id: 'r1', widgets: [widget], isDrawer: false };
|
|
773
|
+
renderer.regions = new Map([['r1', region]]);
|
|
774
|
+
|
|
775
|
+
// Before metadata: widget is unprobed
|
|
776
|
+
expect(renderer._hasUnprobedVideos()).toBe(true);
|
|
777
|
+
|
|
778
|
+
// Simulate metadata arrival
|
|
779
|
+
widget.duration = 45;
|
|
780
|
+
widget._probed = true;
|
|
781
|
+
|
|
782
|
+
// After metadata: all probed
|
|
783
|
+
expect(renderer._hasUnprobedVideos()).toBe(false);
|
|
784
|
+
expect(widget.duration).toBe(45);
|
|
787
785
|
});
|
|
788
786
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
</region>
|
|
798
|
-
</layout>
|
|
799
|
-
`;
|
|
787
|
+
it('should calculate correct duration from widget durations', () => {
|
|
788
|
+
// Test the duration calculation logic that updateLayoutDuration uses:
|
|
789
|
+
// max region duration across all regions, sum of widgets per region
|
|
790
|
+
const w1 = { id: 'v1', duration: 30, useDuration: 0, _probed: true };
|
|
791
|
+
const w2 = { id: 'v2', duration: 15, useDuration: 0, _probed: true };
|
|
792
|
+
// Region duration = sum of widgets = 30 + 15 = 45
|
|
793
|
+
const region = { id: 'r1', widgets: [w1, w2], isDrawer: false };
|
|
794
|
+
renderer.regions = new Map([['r1', region]]);
|
|
800
795
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
const region = renderer.regions.get('r1');
|
|
804
|
-
const videoElement = region.widgetElements.get('m1');
|
|
805
|
-
const video = videoElement.querySelector('video');
|
|
806
|
-
|
|
807
|
-
// Simulate video with 45s duration
|
|
808
|
-
Object.defineProperty(video, 'duration', { value: 45, writable: false });
|
|
809
|
-
video.dispatchEvent(new Event('loadedmetadata'));
|
|
796
|
+
// Verify _hasUnprobedVideos returns false when all probed
|
|
797
|
+
expect(renderer._hasUnprobedVideos()).toBe(false);
|
|
810
798
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
expect(renderer.currentLayout.duration).toBe(45);
|
|
799
|
+
// Verify the duration sum matches what updateLayoutDuration would compute
|
|
800
|
+
const regionDuration = region.widgets.reduce((sum, w) => sum + (w.duration > 0 ? w.duration : 0), 0);
|
|
801
|
+
expect(regionDuration).toBe(45);
|
|
815
802
|
});
|
|
816
803
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
const
|
|
820
|
-
|
|
821
|
-
<region id="r1">
|
|
822
|
-
<media id="m1" type="video" duration="30" useDuration="1" fileId="5">
|
|
823
|
-
<options><uri>5.mp4</uri></options>
|
|
824
|
-
</media>
|
|
825
|
-
</region>
|
|
826
|
-
</layout>
|
|
827
|
-
`;
|
|
828
|
-
|
|
829
|
-
await renderer.renderLayout(xlf, 1);
|
|
804
|
+
it('should NOT mark widget as probed when useDuration=1', () => {
|
|
805
|
+
const widget = { id: 'm1', type: 'video', duration: 30, useDuration: 1 };
|
|
806
|
+
const region = { id: 'r1', widgets: [widget], isDrawer: false };
|
|
807
|
+
renderer.regions = new Map([['r1', region]]);
|
|
830
808
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
809
|
+
// useDuration=1 means CMS-set duration — not a "play to end" video
|
|
810
|
+
expect(renderer._hasUnprobedVideos()).toBe(false);
|
|
811
|
+
expect(widget.duration).toBe(30);
|
|
812
|
+
});
|
|
834
813
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
814
|
+
it('should handle mixed probed and unprobed videos', () => {
|
|
815
|
+
const widget1 = { id: 'm1', type: 'video', duration: 45, useDuration: 0, _probed: true };
|
|
816
|
+
const widget2 = { id: 'm2', type: 'video', duration: 60, useDuration: 0, _probed: false };
|
|
817
|
+
const region = { id: 'r1', widgets: [widget1, widget2], isDrawer: false };
|
|
818
|
+
renderer.regions = new Map([['r1', region]]);
|
|
838
819
|
|
|
839
|
-
|
|
820
|
+
// One still unprobed
|
|
821
|
+
expect(renderer._hasUnprobedVideos()).toBe(true);
|
|
840
822
|
|
|
841
|
-
//
|
|
842
|
-
|
|
843
|
-
|
|
823
|
+
// Probe the second
|
|
824
|
+
widget2.duration = 30;
|
|
825
|
+
widget2._probed = true;
|
|
826
|
+
expect(renderer._hasUnprobedVideos()).toBe(false);
|
|
844
827
|
});
|
|
845
828
|
});
|
|
846
829
|
|
|
847
830
|
describe('Media Element Restart', () => {
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
const widget = {
|
|
851
|
-
type: 'video',
|
|
852
|
-
id: 'm1',
|
|
853
|
-
fileId: '5',
|
|
854
|
-
options: { loop: '0', mute: '1' },
|
|
855
|
-
duration: 30
|
|
856
|
-
};
|
|
857
|
-
|
|
858
|
-
const region = { width: 1920, height: 1080 };
|
|
859
|
-
const element = await renderer.renderVideo(widget, region);
|
|
860
|
-
const video = element.querySelector('video');
|
|
861
|
-
|
|
862
|
-
// Mock video methods
|
|
831
|
+
it('should restart video via updateMediaElement', () => {
|
|
832
|
+
const video = document.createElement('video');
|
|
863
833
|
video.currentTime = 25.5;
|
|
834
|
+
// Simulate readyState >= 2 so _restartMediaElement calls play directly
|
|
835
|
+
Object.defineProperty(video, 'readyState', { value: 3, configurable: true });
|
|
864
836
|
video.play = vi.fn(() => Promise.resolve());
|
|
865
837
|
|
|
866
|
-
|
|
867
|
-
|
|
838
|
+
const wrapper = document.createElement('div');
|
|
839
|
+
wrapper.appendChild(video);
|
|
840
|
+
|
|
841
|
+
const widget = { type: 'video', id: 'm1', fileId: '5', options: { loop: '0' }, duration: 30 };
|
|
842
|
+
renderer.updateMediaElement(wrapper, widget);
|
|
868
843
|
|
|
869
|
-
// Should restart from beginning
|
|
870
844
|
expect(video.currentTime).toBe(0);
|
|
871
845
|
expect(video.play).toHaveBeenCalled();
|
|
872
846
|
});
|
|
873
847
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
id: 'm1',
|
|
879
|
-
fileId: '5',
|
|
880
|
-
options: { loop: '1', mute: '1' }, // Looping video
|
|
881
|
-
duration: 30
|
|
882
|
-
};
|
|
883
|
-
|
|
884
|
-
const region = { width: 1920, height: 1080 };
|
|
885
|
-
const element = await renderer.renderVideo(widget, region);
|
|
886
|
-
const video = element.querySelector('video');
|
|
887
|
-
|
|
888
|
-
video.currentTime = 10;
|
|
848
|
+
it('should restart looping videos too', () => {
|
|
849
|
+
const video = document.createElement('video');
|
|
850
|
+
video.currentTime = 25.5;
|
|
851
|
+
Object.defineProperty(video, 'readyState', { value: 3, configurable: true });
|
|
889
852
|
video.play = vi.fn(() => Promise.resolve());
|
|
890
853
|
|
|
891
|
-
|
|
854
|
+
const wrapper = document.createElement('div');
|
|
855
|
+
wrapper.appendChild(video);
|
|
856
|
+
|
|
857
|
+
const widget = { type: 'video', id: 'm1', fileId: '5', options: { loop: '1' }, duration: 30 };
|
|
858
|
+
renderer.updateMediaElement(wrapper, widget);
|
|
892
859
|
|
|
893
|
-
// Should STILL restart (even when looping)
|
|
894
860
|
expect(video.currentTime).toBe(0);
|
|
895
861
|
expect(video.play).toHaveBeenCalled();
|
|
896
862
|
});
|