@npo/player 1.19.0 → 1.20.1
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 +1 -1
- package/lib/js/drm/handlers/decideprofile.js +17 -17
- package/lib/js/drm/handlers/decideprofile.test.d.ts +1 -0
- package/lib/js/drm/handlers/decideprofile.test.js +33 -0
- package/lib/js/fragments/removefragments.test.d.ts +1 -0
- package/lib/js/fragments/removefragments.test.js +26 -0
- package/lib/js/fragments/setfragments.test.d.ts +1 -0
- package/lib/js/fragments/setfragments.test.js +72 -0
- package/lib/js/playeractions/handlers/processsourceconfig.js +13 -7
- package/lib/js/playeractions/handlers/removereplayclass.test.d.ts +1 -0
- package/lib/js/playeractions/handlers/removereplayclass.test.js +28 -0
- package/lib/js/playeractions/handlers/resolvekeypress.test.d.ts +1 -0
- package/lib/js/playeractions/handlers/resolvekeypress.test.js +80 -0
- package/lib/js/tracking/handlers/eventlogging.test.d.ts +1 -0
- package/lib/js/tracking/handlers/eventlogging.test.js +46 -0
- package/lib/js/ui/handlers/listboxhandlers.js +0 -1
- package/lib/js/ui/handlers/nicamhandler.d.ts +3 -0
- package/lib/js/ui/handlers/nicamhandler.js +16 -0
- package/lib/js/ui/nativemobileuicontainer.js +2 -2
- package/lib/js/ui/nativemobileuifactory.js +12 -12
- package/lib/npoplayer.d.ts +4 -2
- package/lib/npoplayer.js +36 -11
- package/lib/package.json +5 -5
- package/lib/src/js/drm/handlers/decideprofile.test.d.ts +1 -0
- package/lib/src/js/fragments/removefragments.test.d.ts +1 -0
- package/lib/src/js/fragments/setfragments.test.d.ts +1 -0
- package/lib/src/js/playeractions/handlers/removereplayclass.test.d.ts +1 -0
- package/lib/src/js/playeractions/handlers/resolvekeypress.test.d.ts +1 -0
- package/lib/src/js/tracking/handlers/eventlogging.test.d.ts +1 -0
- package/lib/src/js/ui/handlers/nicamhandler.d.ts +3 -0
- package/lib/src/npoplayer.d.ts +4 -2
- package/lib/src/types/interfaces.d.ts +9 -0
- package/lib/tests/mocks/mockNpoplayer.d.ts +2 -0
- package/lib/tests/mocks/mockNpoplayer.js +115 -0
- package/lib/types/interfaces.d.ts +9 -0
- package/package.json +5 -5
- package/src/scss/components/_advert.scss +5 -5
- package/src/scss/components/_hugeplaybacktogglebutton.scss +1 -0
- package/src/scss/components/_metadata.scss +17 -0
- package/src/scss/components/_nicam.scss +20 -0
- package/src/scss/components/_replay.scss +0 -1
- package/src/scss/components/_seekbarthumbnail.scss +0 -1
- package/src/scss/components/_subtitles.scss +1 -1
- package/src/scss/npoplayer.css +35 -30
- package/src/scss/variants/_player-base.scss +6 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Extensive and up-to-date documentation is available at https://docs.npoplayer.nl
|
|
|
11
11
|
Code quality is analysed by SonarCloud. The project can be found at https://sonarcloud.io/project/overview?id=NPOstart_npo-player
|
|
12
12
|
|
|
13
13
|
# Changelog
|
|
14
|
-
Current version: v1.
|
|
14
|
+
Current version: v1.20.0
|
|
15
15
|
|
|
16
16
|
The changelog is available at https://docs.npoplayer.nl/implementation/web/changelog/
|
|
17
17
|
|
|
@@ -12,39 +12,39 @@ export async function decideProfile(player, preferredDRM = '') {
|
|
|
12
12
|
supportedTech = player.getSupportedTech();
|
|
13
13
|
supportedDRM = await player.getSupportedDRM();
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
for (const tech of supportedTech) {
|
|
16
16
|
if (bestWithoutDRM === '')
|
|
17
17
|
bestWithoutDRM = tech.streaming;
|
|
18
18
|
if (bestWithDRM !== '')
|
|
19
|
-
|
|
20
|
-
if (tech.streaming === 'dash') {
|
|
21
|
-
|
|
19
|
+
continue;
|
|
20
|
+
if (tech.streaming === 'dash' && preferredDRM !== 'fairplay') {
|
|
21
|
+
for (const drm of supportedDRM) {
|
|
22
22
|
if (bestDRM !== '' && (preferredDRM === '' || preferredDRM.length > 0)) {
|
|
23
|
-
|
|
23
|
+
continue;
|
|
24
24
|
}
|
|
25
|
-
if (['com.widevine.alpha'].includes(drm)) {
|
|
25
|
+
if (['com.widevine.alpha', 'com.microsoft.playready'].includes(drm) && preferredDRM === '') {
|
|
26
26
|
bestDRM = 'widevine';
|
|
27
27
|
}
|
|
28
|
-
if (['com.
|
|
29
|
-
bestDRM = '
|
|
28
|
+
if (['com.apple.fps.1_0', 'com.apple.fps.2_0'].includes(drm) && preferredDRM === 'fairplay') {
|
|
29
|
+
bestDRM = 'fairplay';
|
|
30
30
|
}
|
|
31
|
-
}
|
|
31
|
+
}
|
|
32
32
|
if (bestDRM !== '')
|
|
33
33
|
bestWithDRM = 'dash';
|
|
34
34
|
}
|
|
35
|
-
if (tech.streaming === 'hls') {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (['com.apple.fps.1_0', 'com.apple.fps.2_0'].includes(drm)) {
|
|
35
|
+
if (tech.streaming === 'hls' || preferredDRM === 'fairplay') {
|
|
36
|
+
for (const drm of supportedDRM) {
|
|
37
|
+
if (bestDRM !== '' && (preferredDRM === '' || bestDRM === preferredDRM)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (['com.apple.fps.1_0', 'com.apple.fps.2_0'].includes(drm) && preferredDRM === 'fairplay') {
|
|
41
41
|
bestDRM = 'fairplay';
|
|
42
42
|
}
|
|
43
|
-
}
|
|
43
|
+
}
|
|
44
44
|
if (bestDRM !== '')
|
|
45
45
|
bestWithDRM = 'hls';
|
|
46
46
|
}
|
|
47
|
-
}
|
|
47
|
+
}
|
|
48
48
|
if (bestWithDRM !== '')
|
|
49
49
|
return { profileName: bestWithDRM, drm: bestDRM };
|
|
50
50
|
return { profileName: bestWithoutDRM, drm: bestDRM };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { decideProfile } from './decideprofile';
|
|
2
|
+
jest.mock("bitmovin-player");
|
|
3
|
+
describe("decideProfile", () => {
|
|
4
|
+
let mockPlayerAPI;
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
mockPlayerAPI = {
|
|
7
|
+
getSupportedTech: jest.fn().mockReturnValue([
|
|
8
|
+
{ streaming: 'dash' },
|
|
9
|
+
{ streaming: 'hls' },
|
|
10
|
+
]),
|
|
11
|
+
getSupportedDRM: jest.fn().mockResolvedValue([
|
|
12
|
+
'com.widevine.alpha',
|
|
13
|
+
'com.microsoft.playready',
|
|
14
|
+
'com.apple.fps.1_0',
|
|
15
|
+
]),
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
jest.clearAllMocks();
|
|
20
|
+
});
|
|
21
|
+
it("should return empty profile when player is null", async () => {
|
|
22
|
+
const result = await decideProfile(null);
|
|
23
|
+
expect(result).toEqual({ profileName: '', drm: '' });
|
|
24
|
+
});
|
|
25
|
+
it("should return profile with dash streaming and widevine DRM", async () => {
|
|
26
|
+
const result = await decideProfile(mockPlayerAPI);
|
|
27
|
+
expect(result).toEqual({ profileName: 'dash', drm: 'widevine' });
|
|
28
|
+
});
|
|
29
|
+
it("should return profile with hls streaming and fairplay DRM", async () => {
|
|
30
|
+
const result = await decideProfile(mockPlayerAPI, 'fairplay');
|
|
31
|
+
expect(result).toEqual({ profileName: 'hls', drm: 'fairplay' });
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { removeFragments } from './removefragments';
|
|
2
|
+
describe("removeFragments", () => {
|
|
3
|
+
it("should remove all timeline markers", () => {
|
|
4
|
+
const mockUIManager = {
|
|
5
|
+
getTimelineMarkers: jest.fn().mockReturnValue([
|
|
6
|
+
{ id: 1, time: 10 },
|
|
7
|
+
{ id: 2, time: 20 },
|
|
8
|
+
{ id: 3, time: 30 },
|
|
9
|
+
]),
|
|
10
|
+
removeTimelineMarker: jest.fn(),
|
|
11
|
+
};
|
|
12
|
+
removeFragments(mockUIManager);
|
|
13
|
+
expect(mockUIManager.removeTimelineMarker).toHaveBeenCalledTimes(3);
|
|
14
|
+
expect(mockUIManager.removeTimelineMarker).toHaveBeenCalledWith({ id: 1, time: 10 });
|
|
15
|
+
expect(mockUIManager.removeTimelineMarker).toHaveBeenCalledWith({ id: 2, time: 20 });
|
|
16
|
+
expect(mockUIManager.removeTimelineMarker).toHaveBeenCalledWith({ id: 3, time: 30 });
|
|
17
|
+
});
|
|
18
|
+
it("should not remove any timeline markers if there are none", () => {
|
|
19
|
+
const mockUIManager = {
|
|
20
|
+
getTimelineMarkers: jest.fn().mockReturnValue([]),
|
|
21
|
+
removeTimelineMarker: jest.fn(),
|
|
22
|
+
};
|
|
23
|
+
removeFragments(mockUIManager);
|
|
24
|
+
expect(mockUIManager.removeTimelineMarker).not.toHaveBeenCalled();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { setFragments } from './setfragments';
|
|
2
|
+
import { PlayerEvent } from 'bitmovin-player';
|
|
3
|
+
const SEEKBAR_MARKER_UNPLAYABLE = 'seekbar-marker-unplayable';
|
|
4
|
+
describe("setFragments", () => {
|
|
5
|
+
let mockPlayer;
|
|
6
|
+
let mockUIManager;
|
|
7
|
+
let fragments;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
jest.clearAllMocks();
|
|
10
|
+
mockPlayer = {
|
|
11
|
+
seek: jest.fn(),
|
|
12
|
+
isPaused: jest.fn().mockReturnValue(true),
|
|
13
|
+
play: jest.fn(),
|
|
14
|
+
pause: jest.fn(),
|
|
15
|
+
off: jest.fn(),
|
|
16
|
+
on: jest.fn().mockImplementation((event, handler) => {
|
|
17
|
+
if (event === PlayerEvent.Ready) {
|
|
18
|
+
handler();
|
|
19
|
+
}
|
|
20
|
+
}),
|
|
21
|
+
getContainer: jest.fn().mockReturnValue({
|
|
22
|
+
querySelector: jest.fn().mockReturnValue({
|
|
23
|
+
textContent: '',
|
|
24
|
+
classList: {
|
|
25
|
+
add: jest.fn()
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
}),
|
|
29
|
+
getConfig: jest.fn().mockReturnValue({
|
|
30
|
+
playback: {
|
|
31
|
+
autoplay: true
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
};
|
|
35
|
+
mockUIManager = {
|
|
36
|
+
addTimelineMarker: jest.fn(),
|
|
37
|
+
getTimelineMarkers: jest.fn().mockReturnValue([]),
|
|
38
|
+
removeTimelineMarker: jest.fn()
|
|
39
|
+
};
|
|
40
|
+
fragments = {
|
|
41
|
+
sections: [
|
|
42
|
+
{
|
|
43
|
+
start: 10,
|
|
44
|
+
end: 20,
|
|
45
|
+
title: 'Test Fragment',
|
|
46
|
+
cssClasses: [SEEKBAR_MARKER_UNPLAYABLE]
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
it("should set fragments correctly and handle autoplay", async () => {
|
|
52
|
+
setFragments(mockPlayer, mockUIManager, fragments);
|
|
53
|
+
expect(mockPlayer.seek).toHaveBeenCalledWith(fragments.sections[0].start);
|
|
54
|
+
expect(mockPlayer.play).toHaveBeenCalled();
|
|
55
|
+
expect(mockUIManager.addTimelineMarker).toHaveBeenCalledTimes(2);
|
|
56
|
+
});
|
|
57
|
+
it("should correctly set the title of the fragment", async () => {
|
|
58
|
+
setFragments(mockPlayer, mockUIManager, fragments);
|
|
59
|
+
expect(mockUIManager.addTimelineMarker).toHaveBeenCalledWith({
|
|
60
|
+
time: 0,
|
|
61
|
+
title: "Niet beschikbaar",
|
|
62
|
+
cssClasses: [SEEKBAR_MARKER_UNPLAYABLE],
|
|
63
|
+
duration: 10
|
|
64
|
+
});
|
|
65
|
+
expect(mockUIManager.addTimelineMarker).toHaveBeenCalledWith({
|
|
66
|
+
time: 20,
|
|
67
|
+
duration: 10000000,
|
|
68
|
+
title: "Niet beschikbaar",
|
|
69
|
+
cssClasses: [SEEKBAR_MARKER_UNPLAYABLE],
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -36,13 +36,19 @@ function setDrm(sourceConfig, drm, _streamObject) {
|
|
|
36
36
|
};
|
|
37
37
|
break;
|
|
38
38
|
case 'widevine':
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
fetch('https://npo-drm-gateway.samgcloud.nepworldwide.nl/widevine_player_cert.bin')
|
|
40
|
+
.then(response => response.arrayBuffer())
|
|
41
|
+
.then(certificate => {
|
|
42
|
+
sourceConfig.drm = {
|
|
43
|
+
widevine: {
|
|
44
|
+
serverCertificate: certificate,
|
|
45
|
+
LA_URL: npoDrmGateway + _streamObject.stream.drmToken,
|
|
46
|
+
audioRobustness: 'SW_SECURE_CRYPTO',
|
|
47
|
+
videoRobustness: 'SW_SECURE_CRYPTO'
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
})
|
|
51
|
+
.catch(error => console.error('Error:', error));
|
|
46
52
|
break;
|
|
47
53
|
case 'playready':
|
|
48
54
|
sourceConfig.drm = {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { removeReplayClass } from './removereplayclass';
|
|
2
|
+
import { PlayerEvent } from 'bitmovin-player';
|
|
3
|
+
describe('removeReplayClass', () => {
|
|
4
|
+
let mockPlayer;
|
|
5
|
+
let mockPlayerContainer;
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
jest.clearAllMocks();
|
|
8
|
+
mockPlayerContainer = document.createElement('div');
|
|
9
|
+
mockPlayerContainer.classList.add('bmpui-player-state-finished', 'bmpui-player-state-replay');
|
|
10
|
+
mockPlayer = {
|
|
11
|
+
off: jest.fn(),
|
|
12
|
+
getContainer: jest.fn().mockReturnValue(mockPlayerContainer),
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
it('should remove "bmpui-player-state-finished" and "bmpui-player-state-replay" classes from the player container', () => {
|
|
16
|
+
removeReplayClass(mockPlayer);
|
|
17
|
+
expect(mockPlayerContainer.classList.contains('bmpui-player-state-finished')).toBe(false);
|
|
18
|
+
expect(mockPlayerContainer.classList.contains('bmpui-player-state-replay')).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
it('should unregister the Seek event listener', () => {
|
|
21
|
+
removeReplayClass(mockPlayer);
|
|
22
|
+
expect(mockPlayer.off).toHaveBeenCalledWith(PlayerEvent.Seek, expect.any(Function));
|
|
23
|
+
});
|
|
24
|
+
it('should not unregister the Seek event listener if the player is null', () => {
|
|
25
|
+
removeReplayClass(null);
|
|
26
|
+
expect(mockPlayer.off).not.toHaveBeenCalled();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { resolveKeyPress } from './resolvekeypress';
|
|
2
|
+
import { ViewMode } from 'bitmovin-player';
|
|
3
|
+
import { mockNpoPlayer } from '../../../../tests/mocks/mockNpoplayer';
|
|
4
|
+
describe('resolveKeyPress', () => {
|
|
5
|
+
let mockPlayer;
|
|
6
|
+
let mockNpoplayer = mockNpoPlayer;
|
|
7
|
+
let e;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
jest.clearAllMocks();
|
|
10
|
+
mockPlayer = {
|
|
11
|
+
seek: jest.fn(),
|
|
12
|
+
isPaused: jest.fn().mockReturnValue(true),
|
|
13
|
+
play: jest.fn(),
|
|
14
|
+
pause: jest.fn(),
|
|
15
|
+
isMuted: jest.fn(),
|
|
16
|
+
unmute: jest.fn(),
|
|
17
|
+
getViewMode: jest.fn(),
|
|
18
|
+
setViewMode: jest.fn(),
|
|
19
|
+
getContainer: jest.fn().mockReturnValue({
|
|
20
|
+
querySelector: jest.fn().mockReturnValue({
|
|
21
|
+
textContent: '',
|
|
22
|
+
classList: {
|
|
23
|
+
add: jest.fn()
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
}),
|
|
27
|
+
getConfig: jest.fn().mockReturnValue({
|
|
28
|
+
playback: {
|
|
29
|
+
autoplay: true
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
};
|
|
33
|
+
e = new KeyboardEvent('keydown');
|
|
34
|
+
});
|
|
35
|
+
it('should mute/unmute the player when "KeyM" is pressed', () => {
|
|
36
|
+
mockPlayer.isMuted.mockReturnValue(true);
|
|
37
|
+
resolveKeyPress(mockPlayer, mockNpoplayer, { ...e, code: 'KeyM' });
|
|
38
|
+
expect(mockPlayer.unmute).toHaveBeenCalled();
|
|
39
|
+
});
|
|
40
|
+
it('should enter/exit fullscreen mode when "KeyF" is pressed', () => {
|
|
41
|
+
mockPlayer.getViewMode.mockReturnValue(ViewMode.Inline);
|
|
42
|
+
resolveKeyPress(mockPlayer, mockNpoplayer, { ...e, code: 'KeyF' });
|
|
43
|
+
expect(mockPlayer.setViewMode).toHaveBeenCalledWith(ViewMode.Fullscreen);
|
|
44
|
+
});
|
|
45
|
+
it('should play/pause the player when "Space" is pressed', () => {
|
|
46
|
+
mockPlayer.isPaused.mockReturnValue(true);
|
|
47
|
+
resolveKeyPress(mockPlayer, mockNpoplayer, { ...e, code: 'Space' });
|
|
48
|
+
expect(mockPlayer.play).toHaveBeenCalled();
|
|
49
|
+
});
|
|
50
|
+
it('should increase the volume when "ArrowUp" is pressed', () => {
|
|
51
|
+
mockPlayer.isMuted.mockReturnValue(true);
|
|
52
|
+
mockNpoplayer.increaseVolume = jest.fn();
|
|
53
|
+
resolveKeyPress(mockPlayer, mockNpoplayer, { ...e, code: 'ArrowUp' });
|
|
54
|
+
expect(mockPlayer.unmute).toHaveBeenCalled();
|
|
55
|
+
expect(mockNpoplayer.increaseVolume).toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
it('should decrease the volume when "ArrowDown" is pressed', () => {
|
|
58
|
+
mockPlayer.isMuted.mockReturnValue(true);
|
|
59
|
+
mockNpoplayer.decreaseVolume = jest.fn();
|
|
60
|
+
resolveKeyPress(mockPlayer, mockNpoplayer, { ...e, code: 'ArrowDown' });
|
|
61
|
+
expect(mockPlayer.unmute).toHaveBeenCalled();
|
|
62
|
+
expect(mockNpoplayer.decreaseVolume).toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
it('should go backwards when "ArrowLeft" is pressed', () => {
|
|
65
|
+
mockNpoplayer.goBackwards = jest.fn();
|
|
66
|
+
resolveKeyPress(mockPlayer, mockNpoplayer, { ...e, code: 'ArrowLeft' });
|
|
67
|
+
expect(mockNpoplayer.goBackwards).toHaveBeenCalledWith(10);
|
|
68
|
+
});
|
|
69
|
+
it('should go forward when "ArrowRight" is pressed', () => {
|
|
70
|
+
mockNpoplayer.goForward = jest.fn();
|
|
71
|
+
resolveKeyPress(mockPlayer, mockNpoplayer, { ...e, code: 'ArrowRight' });
|
|
72
|
+
expect(mockNpoplayer.goForward).toHaveBeenCalledWith(10);
|
|
73
|
+
});
|
|
74
|
+
it('should hide the settings panel when "Escape" is pressed', () => {
|
|
75
|
+
if (mockNpoplayer.uiComponents.settingsPanel) {
|
|
76
|
+
resolveKeyPress(mockPlayer, mockNpoplayer, { ...e, code: 'Escape' });
|
|
77
|
+
expect(mockNpoplayer.uiComponents.settingsPanel.hide).toHaveBeenCalled();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { logEvent } from './eventlogging';
|
|
2
|
+
import { mockNpoPlayer } from '../../../../tests/mocks/mockNpoplayer';
|
|
3
|
+
describe('logEvent', () => {
|
|
4
|
+
let npoplayer;
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
npoplayer = mockNpoPlayer;
|
|
7
|
+
});
|
|
8
|
+
it('should not log event if player is not available', () => {
|
|
9
|
+
npoplayer.player = null;
|
|
10
|
+
if (npoplayer.streamTracker && npoplayer.player) {
|
|
11
|
+
logEvent(npoplayer, 'start');
|
|
12
|
+
expect(npoplayer.streamTracker.start).not.toHaveBeenCalled();
|
|
13
|
+
expect(npoplayer.logEmitter.emit).not.toHaveBeenCalled();
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
it('should not log event if streamTracker is not available', () => {
|
|
17
|
+
npoplayer.streamTracker = null;
|
|
18
|
+
if (npoplayer.streamTracker) {
|
|
19
|
+
logEvent(npoplayer, 'start');
|
|
20
|
+
expect(npoplayer.logEmitter.emit).not.toHaveBeenCalled();
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
it('should not log event if adBreakActive is true', () => {
|
|
24
|
+
npoplayer.adBreakActive = true;
|
|
25
|
+
if (npoplayer.streamTracker) {
|
|
26
|
+
logEvent(npoplayer, 'start');
|
|
27
|
+
expect(npoplayer.streamTracker.start).not.toHaveBeenCalled();
|
|
28
|
+
expect(npoplayer.logEmitter.emit).not.toHaveBeenCalled();
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
it('should log event and call the corresponding streamTracker handler', () => {
|
|
32
|
+
if (npoplayer.streamTracker) {
|
|
33
|
+
logEvent(npoplayer, 'start');
|
|
34
|
+
expect(npoplayer.streamTracker.start).toHaveBeenCalledWith({ stream_position: 10 });
|
|
35
|
+
expect(npoplayer.logEmitter.emit).toHaveBeenCalledWith('logEvent', 'start');
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
it('should log event and call the seek streamTracker handler with data', () => {
|
|
39
|
+
const data = { position: 20 };
|
|
40
|
+
if (npoplayer.streamTracker) {
|
|
41
|
+
logEvent(npoplayer, 'seek', data);
|
|
42
|
+
expect(npoplayer.streamTracker.seek).toHaveBeenCalledWith(data);
|
|
43
|
+
expect(npoplayer.logEmitter.emit).toHaveBeenCalledWith('logEvent', 'seek');
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -5,7 +5,6 @@ function closeSettingsPanel(settingsPanel, mainSettingsPage) {
|
|
|
5
5
|
}, 100);
|
|
6
6
|
}
|
|
7
7
|
export function handleSubtitleListBoxItemSelected(subtitleSettingsOpenButton, settingsPanel, mainSettingsPage) {
|
|
8
|
-
console.log('handleSubtitleListBoxItemSelected');
|
|
9
8
|
return (e) => {
|
|
10
9
|
e.getItems().forEach((item) => {
|
|
11
10
|
if (item.key === e.getSelectedItem()) {
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { PlayerAPI } from 'bitmovin-player';
|
|
2
|
+
import { UIManager } from 'bitmovin-player-ui';
|
|
1
3
|
import { StreamObject, StreamOptions } from 'types/interfaces';
|
|
2
4
|
export declare function processNicam(streamObject: StreamObject, nicamElement: HTMLElement | null, streamOptions: StreamOptions | null): void;
|
|
3
5
|
export declare function addNicamIcon(character: string, nicamElement: Element): void;
|
|
6
|
+
export declare function showNicamAfterUiDelay(player: PlayerAPI, uiManager: UIManager | null): void;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PlayerEvent } from 'bitmovin-player';
|
|
1
2
|
export function processNicam(streamObject, nicamElement, streamOptions) {
|
|
2
3
|
const effectiveMetadata = streamOptions ? { ...streamObject.metadata, ...streamOptions } : streamObject.metadata;
|
|
3
4
|
if (nicamElement) {
|
|
@@ -24,3 +25,18 @@ export function addNicamIcon(character, nicamElement) {
|
|
|
24
25
|
span.classList.add('nicam-icon', iconClass);
|
|
25
26
|
nicamElement.appendChild(span);
|
|
26
27
|
}
|
|
28
|
+
export function showNicamAfterUiDelay(player, uiManager) {
|
|
29
|
+
player.off(PlayerEvent.Play, () => { showNicamAfterUiDelay(player, uiManager); });
|
|
30
|
+
if (uiManager === null)
|
|
31
|
+
return;
|
|
32
|
+
const playerContainer = player.getContainer().querySelector('.bmpui-npo-player.bmpui-layout-max-width-400') || player.getContainer().querySelector('.bmpui-npo-player.bmpui-layout-max-width-600');
|
|
33
|
+
if (!playerContainer)
|
|
34
|
+
return;
|
|
35
|
+
uiManager.activeUi.onControlsHide.subscribeOnce(() => {
|
|
36
|
+
const className = 'show-nicam';
|
|
37
|
+
playerContainer.classList.add(className);
|
|
38
|
+
setTimeout(() => {
|
|
39
|
+
playerContainer.classList.remove(className);
|
|
40
|
+
}, 3000);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -10,6 +10,7 @@ const defaultData = {
|
|
|
10
10
|
defaultActionRequired: true,
|
|
11
11
|
};
|
|
12
12
|
export function sendCustomMessage(message, data) {
|
|
13
|
+
console.log('sendCustomMessage', message, data);
|
|
13
14
|
try {
|
|
14
15
|
if (data !== undefined) {
|
|
15
16
|
return JSON.parse(window.bitmovin.customMessageHandler.sendSynchronous(message, JSON.stringify(data)));
|
|
@@ -18,8 +19,7 @@ export function sendCustomMessage(message, data) {
|
|
|
18
19
|
return JSON.parse(window.bitmovin.customMessageHandler.sendSynchronous(message));
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
|
-
catch
|
|
22
|
-
console.warn(`Custom message ${message} may not be send, as it returned no valid data (JSON as a string)`, e);
|
|
22
|
+
catch {
|
|
23
23
|
return defaultData;
|
|
24
24
|
}
|
|
25
25
|
}
|
|
@@ -18,18 +18,18 @@ export function nativeMobileUiFactory(player, config = {}) {
|
|
|
18
18
|
let _streamObject = {};
|
|
19
19
|
const removeFinishedClass = () => {
|
|
20
20
|
player.off(PlayerEvent.Seeked, removeFinishedClass);
|
|
21
|
-
const playerFinishedElement =
|
|
21
|
+
const playerFinishedElement = player.getContainer().querySelector('.bmpui-npo-player.bmpui-player-state-finished');
|
|
22
22
|
if (playerFinishedElement) {
|
|
23
23
|
playerFinishedElement.classList.remove('bmpui-player-state-finished');
|
|
24
24
|
}
|
|
25
25
|
};
|
|
26
26
|
const clearUI = () => {
|
|
27
|
-
const nicamElement =
|
|
27
|
+
const nicamElement = player.getContainer().querySelector('.bmpui-nicam');
|
|
28
28
|
if (nicamElement) {
|
|
29
29
|
nicamElement.innerHTML = '';
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
const element =
|
|
31
|
+
player.getContainer().querySelector('.bmpui-npo-player')?.classList.remove('livestream-dvr');
|
|
32
|
+
const element = player.getContainer().querySelector('.bmpui-seekbar-label-title');
|
|
33
33
|
if (element) {
|
|
34
34
|
element.textContent = '';
|
|
35
35
|
}
|
|
@@ -37,11 +37,11 @@ export function nativeMobileUiFactory(player, config = {}) {
|
|
|
37
37
|
};
|
|
38
38
|
const updateUI = () => {
|
|
39
39
|
if (_streamObject.metadata?.ageRating) {
|
|
40
|
-
processNicam(_streamObject,
|
|
40
|
+
processNicam(_streamObject, player.getContainer().querySelector('.bmpui-nicam'), null);
|
|
41
41
|
}
|
|
42
42
|
const isLiveStream = _streamObject.stream?.isLiveStream && _streamObject.stream?.hasDvrWindow;
|
|
43
43
|
if (isLiveStream) {
|
|
44
|
-
|
|
44
|
+
player.getContainer().querySelector('.bmpui-npo-player')?.classList.add('livestream-dvr');
|
|
45
45
|
}
|
|
46
46
|
if (_streamObject.segment) {
|
|
47
47
|
const seg = _streamObject.segment;
|
|
@@ -55,7 +55,7 @@ export function nativeMobileUiFactory(player, config = {}) {
|
|
|
55
55
|
else {
|
|
56
56
|
addFragments(player, mobileUIManager, null);
|
|
57
57
|
}
|
|
58
|
-
const element =
|
|
58
|
+
const element = player.getContainer().querySelector('.bmpui-seekbar-label-title');
|
|
59
59
|
if (element) {
|
|
60
60
|
element.textContent = _streamObject.title || '';
|
|
61
61
|
}
|
|
@@ -72,12 +72,12 @@ export function nativeMobileUiFactory(player, config = {}) {
|
|
|
72
72
|
const addEventListeners = () => {
|
|
73
73
|
player.off(PlayerEvent.Ready, addEventListeners);
|
|
74
74
|
sendCustomMessage(CustomMessages.JAVASCRIPT_READY);
|
|
75
|
-
const playbackToggleElement =
|
|
75
|
+
const playbackToggleElement = player.getContainer().querySelector('#native-mobile-playbacktoggleoverlay');
|
|
76
76
|
if (playbackToggleElement) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
sendCustomMessage(CustomMessages.TOGGLE_PLAY_PAUSE, { isChecked:
|
|
77
|
+
playbackToggleElement.addEventListener('touchend', (event) => {
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
player?.play();
|
|
80
|
+
sendCustomMessage(CustomMessages.TOGGLE_PLAY_PAUSE, { isChecked: true });
|
|
81
81
|
});
|
|
82
82
|
}
|
|
83
83
|
};
|
package/lib/npoplayer.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ export default class NpoPlayer {
|
|
|
32
32
|
createUIManager(player: PlayerAPI, variant: NpoPlayerUIVariants): Promise<void>;
|
|
33
33
|
doError(input: any, status?: number): void;
|
|
34
34
|
keyPress(e: KeyboardEvent): void;
|
|
35
|
+
play(): void;
|
|
36
|
+
pause(): void;
|
|
35
37
|
setVolume(volume: number): void;
|
|
36
38
|
increaseVolume(): void;
|
|
37
39
|
decreaseVolume(): void;
|
|
@@ -42,7 +44,7 @@ export default class NpoPlayer {
|
|
|
42
44
|
cancelPlayNextScreen(): void;
|
|
43
45
|
hidePlayNextScreen(): void;
|
|
44
46
|
doPlayNext(): void;
|
|
45
|
-
destroy(asyncMode?: boolean): Promise<boolean
|
|
46
|
-
unload(asyncMode?: boolean): Promise<boolean
|
|
47
|
+
destroy(asyncMode?: boolean): Promise<boolean> | boolean;
|
|
48
|
+
unload(asyncMode?: boolean): Promise<boolean> | boolean;
|
|
47
49
|
printVersion(): void;
|
|
48
50
|
}
|
package/lib/npoplayer.js
CHANGED
|
@@ -19,6 +19,7 @@ import { nativeMobileUiFactory } from './js/ui/nativemobileuifactory';
|
|
|
19
19
|
import { setupMediaSessionActionHandlers } from './js/playeractions/handlers/mediasessionactions';
|
|
20
20
|
import { processStream } from './js/ui/handlers/streamhandler';
|
|
21
21
|
import { removeReplayClass } from './js/playeractions/handlers/removereplayclass';
|
|
22
|
+
import { showNicamAfterUiDelay } from './js/ui/handlers/nicamhandler';
|
|
22
23
|
export { NpoPlayerUIVariants, };
|
|
23
24
|
export default class NpoPlayer {
|
|
24
25
|
constructor(_container, _playerConfig, _npotag, _npotaginstance, _variant) {
|
|
@@ -56,6 +57,13 @@ export default class NpoPlayer {
|
|
|
56
57
|
this.container.addEventListener('keydown', e => {
|
|
57
58
|
this.keyPress(e);
|
|
58
59
|
}, true);
|
|
60
|
+
const playbackToggleElement = this.container.querySelector('.bmpui-ui-playbacktoggle-overlay');
|
|
61
|
+
if (playbackToggleElement) {
|
|
62
|
+
playbackToggleElement.addEventListener('touchend', (event) => {
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
this.player?.play();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
59
67
|
}
|
|
60
68
|
async loadStream(source, options = {}) {
|
|
61
69
|
this.streamOptions = options;
|
|
@@ -95,7 +103,16 @@ export default class NpoPlayer {
|
|
|
95
103
|
const payload = {
|
|
96
104
|
baseURL: endpoint,
|
|
97
105
|
jwt: source,
|
|
98
|
-
data: {
|
|
106
|
+
data: {
|
|
107
|
+
profileName: profile.profileName,
|
|
108
|
+
drmType: profile.drm,
|
|
109
|
+
referrerUrl: window.location.href,
|
|
110
|
+
ster: {
|
|
111
|
+
identifier: options?.ster?.identifier ?? 'npo-app-desktop',
|
|
112
|
+
deviceType: options?.ster?.deviceType ?? 4,
|
|
113
|
+
player: 'web'
|
|
114
|
+
}
|
|
115
|
+
}
|
|
99
116
|
};
|
|
100
117
|
try {
|
|
101
118
|
_streamObject = await getStreamObject(this, payload);
|
|
@@ -156,6 +173,7 @@ export default class NpoPlayer {
|
|
|
156
173
|
this.hidePlayNextScreen();
|
|
157
174
|
});
|
|
158
175
|
}
|
|
176
|
+
this.player.on(PlayerEvent.Play, () => { showNicamAfterUiDelay(this.player, this.uiManager); });
|
|
159
177
|
if (options?.startOffset != null)
|
|
160
178
|
playerAction.handleStartOffset(this.player, options.startOffset);
|
|
161
179
|
const setLiveOffsetListener = function () {
|
|
@@ -194,6 +212,16 @@ export default class NpoPlayer {
|
|
|
194
212
|
return;
|
|
195
213
|
playerAction.resolveKeyPress(this.player, this, e);
|
|
196
214
|
}
|
|
215
|
+
play() {
|
|
216
|
+
if (this.player == null)
|
|
217
|
+
return;
|
|
218
|
+
this.player.play();
|
|
219
|
+
}
|
|
220
|
+
pause() {
|
|
221
|
+
if (this.player == null)
|
|
222
|
+
return;
|
|
223
|
+
this.player.pause();
|
|
224
|
+
}
|
|
197
225
|
setVolume(volume) {
|
|
198
226
|
if (this.player == null)
|
|
199
227
|
return;
|
|
@@ -290,13 +318,7 @@ export default class NpoPlayer {
|
|
|
290
318
|
}
|
|
291
319
|
};
|
|
292
320
|
if (asyncMode) {
|
|
293
|
-
|
|
294
|
-
destroyLogic();
|
|
295
|
-
return Promise.resolve(true);
|
|
296
|
-
}
|
|
297
|
-
catch (error) {
|
|
298
|
-
return Promise.resolve(false);
|
|
299
|
-
}
|
|
321
|
+
return destroyLogic().then(() => true).catch(() => false);
|
|
300
322
|
}
|
|
301
323
|
else {
|
|
302
324
|
return destroyLogic();
|
|
@@ -324,9 +346,12 @@ export default class NpoPlayer {
|
|
|
324
346
|
return unloadLogic();
|
|
325
347
|
}
|
|
326
348
|
else {
|
|
327
|
-
|
|
328
|
-
unloadLogic()
|
|
329
|
-
}
|
|
349
|
+
try {
|
|
350
|
+
return unloadLogic();
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
330
355
|
}
|
|
331
356
|
}
|
|
332
357
|
printVersion() {
|