@npo/player 1.21.1 → 1.22.0

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.
Files changed (43) hide show
  1. package/README.md +1 -2
  2. package/lib/js/drm/handlers/verifydrm.test.js +1 -1
  3. package/lib/js/markers/updateLiveMarkers.d.ts +1 -1
  4. package/lib/js/markers/updateLiveMarkers.js +42 -13
  5. package/lib/js/markers/updateLiveMarkers.test.js +3 -2
  6. package/lib/js/playeractions/handlers/error.test.js +6 -6
  7. package/lib/js/playeractions/handlers/handleoffsets.test.js +7 -7
  8. package/lib/js/playeractions/handlers/processsourceconfig.d.ts +2 -2
  9. package/lib/js/playeractions/handlers/processsourceconfig.js +20 -6
  10. package/lib/js/playeractions/handlers/removereplayclass.js +4 -3
  11. package/lib/js/playeractions/handlers/removereplayclass.test.js +7 -3
  12. package/lib/js/tracking/handlers/eventbinding.js +41 -31
  13. package/lib/js/tracking/handlers/eventlogging.d.ts +1 -1
  14. package/lib/js/tracking/handlers/eventlogging.js +1 -1
  15. package/lib/js/ui/components/buttons.js +2 -2
  16. package/lib/js/ui/components/nativemobile/buttons.d.ts +0 -12
  17. package/lib/js/ui/components/nativemobile/buttons.js +0 -41
  18. package/lib/js/ui/handlers/domhandlers.test.js +2 -1
  19. package/lib/{src/js/ui/components/shared → js/ui/handlers}/playnextscreen.d.ts +2 -2
  20. package/lib/js/ui/handlers/playnextscreen.js +26 -0
  21. package/lib/js/ui/{components/shared → handlers}/playnextstreen.test.js +6 -16
  22. package/lib/js/ui/nativemobileuicontainer.js +1 -4
  23. package/lib/js/ui/nativemobileuifactory.js +4 -20
  24. package/lib/npoplayer.js +32 -18
  25. package/lib/package.json +2 -2
  26. package/lib/src/js/markers/updateLiveMarkers.d.ts +1 -1
  27. package/lib/src/js/playeractions/handlers/processsourceconfig.d.ts +2 -2
  28. package/lib/src/js/tracking/handlers/eventlogging.d.ts +1 -1
  29. package/lib/src/js/ui/components/nativemobile/buttons.d.ts +0 -12
  30. package/lib/{js/ui/components/shared → src/js/ui/handlers}/playnextscreen.d.ts +2 -2
  31. package/lib/src/types/interfaces.d.ts +8 -0
  32. package/lib/types/interfaces.d.ts +8 -0
  33. package/package.json +2 -2
  34. package/src/scss/components/_playnext.scss +29 -1
  35. package/src/scss/components/_textbuttons.scss +17 -7
  36. package/src/scss/npoplayer.css +9 -5
  37. package/src/scss/vars/_colors.scss +1 -0
  38. package/lib/js/ui/components/nativemobile/playnext.d.ts +0 -11
  39. package/lib/js/ui/components/nativemobile/playnext.js +0 -10
  40. package/lib/js/ui/components/shared/playnextscreen.js +0 -46
  41. package/lib/src/js/ui/components/nativemobile/playnext.d.ts +0 -11
  42. /package/lib/js/ui/{components/shared → handlers}/playnextstreen.test.d.ts +0 -0
  43. /package/lib/src/js/ui/{components/shared → handlers}/playnextstreen.test.d.ts +0 -0
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.21.0
14
+ Current version: v1.22.0
15
15
 
16
16
  The changelog is available at https://docs.npoplayer.nl/implementation/web/changelog/
17
17
 
@@ -25,4 +25,3 @@ MIT
25
25
  ## Native mobile development
26
26
 
27
27
  For detailed information on the JavaScript player integration for ios and Android, including custom message handling, UI customization, and interaction with native mobile SDKs, see the [Bitmovin Player Integration for Native Mobile SDKs](src/js/ui/README.md) located in the `src/js/ui` directory.
28
-
@@ -12,6 +12,6 @@ const payload = {
12
12
  describe("Test DRM verification", () => {
13
13
  it("should call player.load when the drmToken is null", async () => {
14
14
  await verifyDRM(npoplayer, player, payload);
15
- expect(load).toBeCalled();
15
+ expect(load).toHaveBeenCalled();
16
16
  });
17
17
  });
@@ -1,4 +1,4 @@
1
1
  import { PlayerAPI } from "bitmovin-player";
2
2
  import { UIManager } from "bitmovin-player-ui";
3
3
  import { TimeLineMarker } from "types/interfaces";
4
- export declare function updateLiveMarkers(timeLineMarkers: TimeLineMarker[], player: PlayerAPI, uiManager: UIManager): void;
4
+ export declare function updateLiveMarkers(timeLineMarkers: TimeLineMarker[], player: PlayerAPI, uiManager: UIManager, autoFillTimeLineMarkerDuration: boolean): void;
@@ -1,21 +1,50 @@
1
- export function updateLiveMarkers(timeLineMarkers, player, uiManager) {
1
+ export function updateLiveMarkers(timeLineMarkers, player, uiManager, autoFillTimeLineMarkerDuration) {
2
2
  if (!player || !uiManager) {
3
3
  console.error('Player or UIManager is null');
4
4
  return;
5
5
  }
6
+ removeAllMarkers(uiManager);
7
+ let currentTimeEpoch = Math.floor(Date.now() / 1000);
6
8
  timeLineMarkers.sort((a, b) => a.time - b.time);
7
- let markesWithClass = timeLineMarkers.map((marker) => {
8
- return {
9
- time: marker.time,
10
- title: marker.title,
11
- cssClasses: ['npo-livestream-marker'],
12
- };
13
- });
14
- const existingTimelineMarkers = uiManager.getTimelineMarkers();
15
- existingTimelineMarkers.forEach((marker) => {
16
- uiManager?.removeTimelineMarker(marker);
17
- });
18
- markesWithClass.forEach(marker => {
9
+ let relativeTimeMarkers = timeLineMarkers.map((marker, index) => transformMarker(marker, index, timeLineMarkers, currentTimeEpoch, player, autoFillTimeLineMarkerDuration));
10
+ relativeTimeMarkers.forEach(marker => {
19
11
  uiManager?.addTimelineMarker(marker);
20
12
  });
21
13
  }
14
+ function transformMarker(marker, index, timeLineMarkers, currentTimeEpoch, player, autoFillTimeLineMarkerDuration) {
15
+ const DVR_WINDOW_LENGTH = Math.abs(player.getMaxTimeShift());
16
+ let duration = marker.duration !== undefined ? marker.duration : 0;
17
+ let startTimeRelativeToCurrent = marker.time - currentTimeEpoch;
18
+ if (autoFillTimeLineMarkerDuration) {
19
+ if (index === timeLineMarkers.length - 1) {
20
+ duration = Math.min(currentTimeEpoch, marker.time + DVR_WINDOW_LENGTH) - marker.time;
21
+ }
22
+ else {
23
+ duration = timeLineMarkers[index + 1].time - marker.time;
24
+ }
25
+ if (startTimeRelativeToCurrent < -DVR_WINDOW_LENGTH) {
26
+ duration = index === timeLineMarkers.length - 1 ?
27
+ currentTimeEpoch - (currentTimeEpoch - DVR_WINDOW_LENGTH) :
28
+ timeLineMarkers[index + 1].time - (currentTimeEpoch - DVR_WINDOW_LENGTH);
29
+ startTimeRelativeToCurrent = -DVR_WINDOW_LENGTH;
30
+ }
31
+ }
32
+ return {
33
+ time: marker.time,
34
+ duration: duration,
35
+ title: marker.title,
36
+ cssClasses: ['npo-livestream-marker'],
37
+ };
38
+ }
39
+ function removeAllMarkers(uiManager) {
40
+ let existingTimelineMarkers = uiManager.getTimelineMarkers();
41
+ while (existingTimelineMarkers.length > 0) {
42
+ const marker = existingTimelineMarkers[0];
43
+ const removed = uiManager?.removeTimelineMarker(marker);
44
+ if (removed) {
45
+ existingTimelineMarkers = uiManager.getTimelineMarkers();
46
+ continue;
47
+ }
48
+ break;
49
+ }
50
+ }
@@ -15,6 +15,7 @@ jest.mock("bitmovin-player", () => ({
15
15
  describe('updateLiveMarkers', () => {
16
16
  const player = {
17
17
  getCurrentTime: jest.fn().mockReturnValue(10),
18
+ getMaxTimeShift: jest.fn().mockReturnValue(-3600)
18
19
  };
19
20
  let uiManager;
20
21
  let ui;
@@ -26,7 +27,7 @@ describe('updateLiveMarkers', () => {
26
27
  { time: 3000, title: 'Marker 2' },
27
28
  { time: 1000, title: 'Marker 1' },
28
29
  ];
29
- updateLiveMarkers(timeLineMarkers, player, uiManager);
30
+ updateLiveMarkers(timeLineMarkers, player, uiManager, true);
30
31
  expect(uiManager.getTimelineMarkers).toHaveBeenCalled();
31
32
  expect(uiManager.removeTimelineMarker).toHaveBeenCalledTimes(0);
32
33
  expect(uiManager.addTimelineMarker).toHaveBeenCalledTimes(2);
@@ -46,7 +47,7 @@ describe('updateLiveMarkers', () => {
46
47
  const timeLineMarkers = [
47
48
  { time: 1000, title: 'New Marker' },
48
49
  ];
49
- updateLiveMarkers(timeLineMarkers, player, uiManager);
50
+ updateLiveMarkers(timeLineMarkers, player, uiManager, true);
50
51
  expect(uiManager.removeTimelineMarker).toHaveBeenCalledTimes(1);
51
52
  expect(uiManager.addTimelineMarker).toHaveBeenCalledTimes(1);
52
53
  });
@@ -18,11 +18,11 @@ describe("Test DRM verification", () => {
18
18
  });
19
19
  it("should call player.unload", () => {
20
20
  handlePlayerError(player, uiComponents, input);
21
- expect(unload).toBeCalled();
21
+ expect(unload).toHaveBeenCalled();
22
22
  });
23
23
  it("should hide the controlbar", () => {
24
24
  handlePlayerError(player, uiComponents, input);
25
- expect(hide).toBeCalled();
25
+ expect(hide).toHaveBeenCalled();
26
26
  });
27
27
  it("should not hide the controlbar", () => {
28
28
  const components = {
@@ -30,15 +30,15 @@ describe("Test DRM verification", () => {
30
30
  controlbar: undefined,
31
31
  };
32
32
  handlePlayerError(player, components, input);
33
- expect(hide).not.toBeCalled();
33
+ expect(hide).not.toHaveBeenCalled();
34
34
  });
35
35
  it("should show the message overlay", () => {
36
36
  handlePlayerError(player, uiComponents, input);
37
- expect(show).toBeCalled();
38
- expect(setText).toBeCalled();
37
+ expect(show).toHaveBeenCalled();
38
+ expect(setText).toHaveBeenCalled();
39
39
  });
40
40
  it("should not show the message overlay", () => {
41
41
  handlePlayerError(player, { ...uiComponents, errorMessageOverlay: undefined }, input);
42
- expect(show).not.toBeCalled();
42
+ expect(show).not.toHaveBeenCalled();
43
43
  });
44
44
  });
@@ -15,15 +15,15 @@ describe("Test offset handling", () => {
15
15
  });
16
16
  it("should not seek when player is live", () => {
17
17
  handleStartOffset(player, offset);
18
- expect(onReady).not.toBeCalled();
18
+ expect(onReady).not.toHaveBeenCalled();
19
19
  });
20
20
  it("should not seek when offset is undefined", () => {
21
21
  handleStartOffset({ ...player, isLive: () => false });
22
- expect(onReady).not.toBeCalled();
22
+ expect(onReady).not.toHaveBeenCalled();
23
23
  });
24
24
  it("should seek when player is not live and offset is defined", () => {
25
25
  handleStartOffset({ ...player, isLive: () => false }, offset);
26
- expect(onReady).toBeCalled();
26
+ expect(onReady).toHaveBeenCalled();
27
27
  });
28
28
  });
29
29
  describe("Test programStart shift", () => {
@@ -32,18 +32,18 @@ describe("Test programStart shift", () => {
32
32
  });
33
33
  it("should not call timeshift when player is null", () => {
34
34
  shiftToProgramStart(null, timestamp);
35
- expect(timeShift).not.toBeCalled();
35
+ expect(timeShift).not.toHaveBeenCalled();
36
36
  });
37
37
  it("should not call timeshift when timestamp is undefined", () => {
38
38
  shiftToProgramStart(player);
39
- expect(timeShift).not.toBeCalled();
39
+ expect(timeShift).not.toHaveBeenCalled();
40
40
  });
41
41
  it("should not call timeshift when player is not live", () => {
42
42
  shiftToProgramStart({ ...player, isLive: () => false }, timestamp);
43
- expect(timeShift).not.toBeCalled();
43
+ expect(timeShift).not.toHaveBeenCalled();
44
44
  });
45
45
  it("should not call timeshift when player is live and timestamp is set", () => {
46
46
  shiftToProgramStart(player, timestamp);
47
- expect(timeShift).toBeCalled();
47
+ expect(timeShift).toHaveBeenCalled();
48
48
  });
49
49
  });
@@ -1,3 +1,3 @@
1
- import { StreamObject } from 'types/interfaces';
1
+ import { StreamObject, StreamOptions } from 'types/interfaces';
2
2
  import { SourceConfig } from 'bitmovin-player';
3
- export declare function processSourceConfig(_sourceConfig: SourceConfig | undefined, _streamObject: StreamObject, drm: string | null | undefined, useWidevineServerCertificate: boolean): Promise<SourceConfig>;
3
+ export declare function processSourceConfig(_sourceConfig: SourceConfig | undefined, _streamObject: StreamObject, drm: string | null | undefined, streamOptions: StreamOptions, version: string, npoTagPartyId: string | undefined): Promise<SourceConfig>;
@@ -91,7 +91,7 @@ function setStreamProfile(sourceConfig, _sourceConfig, _streamObject) {
91
91
  }
92
92
  return sourceConfig;
93
93
  }
94
- export async function processSourceConfig(_sourceConfig = {}, _streamObject, drm = null, useWidevineServerCertificate) {
94
+ export async function processSourceConfig(_sourceConfig = {}, _streamObject, drm = null, streamOptions, version, npoTagPartyId) {
95
95
  let sourceConfig = {};
96
96
  sourceConfig.title =
97
97
  _sourceConfig.title ?? _streamObject.metadata.title ?? '';
@@ -99,9 +99,7 @@ export async function processSourceConfig(_sourceConfig = {}, _streamObject, drm
99
99
  _sourceConfig.description ??
100
100
  _streamObject.metadata.description ??
101
101
  '';
102
- sourceConfig.poster =
103
- _sourceConfig.poster ?? _streamObject.metadata.poster ?? '';
104
- sourceConfig.poster = replaceSpecialChars(sourceConfig.poster);
102
+ sourceConfig.poster = decidePoster(_streamObject, _sourceConfig, streamOptions);
105
103
  sourceConfig.metadata = {
106
104
  title: sourceConfig.title,
107
105
  description: sourceConfig.description,
@@ -125,7 +123,7 @@ export async function processSourceConfig(_sourceConfig = {}, _streamObject, drm
125
123
  : undefined;
126
124
  sourceConfig.options = _sourceConfig.options;
127
125
  sourceConfig = setStreamProfile(sourceConfig, _sourceConfig, _streamObject);
128
- sourceConfig = await setDrm(sourceConfig, drm, _streamObject, useWidevineServerCertificate);
126
+ sourceConfig = await setDrm(sourceConfig, drm, _streamObject, streamOptions.useWidevineServerCertificate ?? true);
129
127
  const streamSource = _streamObject.stream.streamURL;
130
128
  let cdnString = streamSource;
131
129
  if (streamSource.startsWith('file:///')) {
@@ -143,9 +141,13 @@ export async function processSourceConfig(_sourceConfig = {}, _streamObject, drm
143
141
  sourceConfig.analytics.cdnProvider = cdnString;
144
142
  sourceConfig.analytics.videoId = _streamObject.metadata.prid;
145
143
  sourceConfig.analytics.title = _streamObject.metadata.title;
144
+ sourceConfig.analytics.customData2 = _streamObject.user.type;
145
+ sourceConfig.analytics.customData3 = _streamObject.stream.avType;
146
+ sourceConfig.analytics.customData4 = npoTagPartyId;
147
+ sourceConfig.analytics.customData5 = version;
146
148
  sourceConfig.analytics = {
147
149
  ...sourceConfig.analytics,
148
- ..._sourceConfig.analytics
150
+ ..._sourceConfig.analytics,
149
151
  };
150
152
  const getSubtitleLabels = function (data) {
151
153
  if (data.label === 'nl') {
@@ -170,3 +172,15 @@ export async function processSourceConfig(_sourceConfig = {}, _streamObject, drm
170
172
  }
171
173
  return sourceConfig;
172
174
  }
175
+ function decidePoster(streamObject, sourceConfig, streamOptions) {
176
+ if (sourceConfig.poster) {
177
+ return replaceSpecialChars(sourceConfig.poster);
178
+ }
179
+ if (streamObject.metadata.poster && !streamObject.metadata.posterIsDefault) {
180
+ return streamObject.metadata.poster;
181
+ }
182
+ if (streamObject.metadata.posterIsDefault && streamOptions.customFallbackPoster) {
183
+ return replaceSpecialChars(streamOptions.customFallbackPoster);
184
+ }
185
+ return streamObject.metadata.poster ?? '';
186
+ }
@@ -5,7 +5,8 @@ export function removeReplayClass(player) {
5
5
  player.off(PlayerEvent.Seek, () => {
6
6
  removeReplayClass(player);
7
7
  });
8
- const playerContainer = player.getContainer();
9
- playerContainer.classList.remove('bmpui-player-state-finished');
10
- playerContainer.classList.remove('bmpui-player-state-replay');
8
+ const playerContainer = player.getContainer().querySelector('.bmpui-npo-player');
9
+ playerContainer?.classList.remove('bmpui-player-state-finished');
10
+ playerContainer?.classList.remove('bmpui-player-state-replay');
11
+ playerContainer?.classList.add('bmpui-player-state-paused');
11
12
  }
@@ -3,10 +3,13 @@ import { PlayerEvent } from 'bitmovin-player';
3
3
  describe('removeReplayClass', () => {
4
4
  let mockPlayer;
5
5
  let mockPlayerContainer;
6
+ let mockPlayerWrapper;
6
7
  beforeEach(() => {
7
8
  jest.clearAllMocks();
8
9
  mockPlayerContainer = document.createElement('div');
9
- mockPlayerContainer.classList.add('bmpui-player-state-finished', 'bmpui-player-state-replay');
10
+ mockPlayerWrapper = document.createElement('div');
11
+ mockPlayerWrapper.classList.add('bmpui-npo-player', 'bmpui-player-state-finished', 'bmpui-player-state-replay');
12
+ mockPlayerContainer.appendChild(mockPlayerWrapper);
10
13
  mockPlayer = {
11
14
  off: jest.fn(),
12
15
  getContainer: jest.fn().mockReturnValue(mockPlayerContainer),
@@ -14,8 +17,9 @@ describe('removeReplayClass', () => {
14
17
  });
15
18
  it('should remove "bmpui-player-state-finished" and "bmpui-player-state-replay" classes from the player container', () => {
16
19
  removeReplayClass(mockPlayer);
17
- expect(mockPlayerContainer.classList.contains('bmpui-player-state-finished')).toBe(false);
18
- expect(mockPlayerContainer.classList.contains('bmpui-player-state-replay')).toBe(false);
20
+ expect(mockPlayerWrapper.classList.contains('bmpui-player-state-finished')).toBe(false);
21
+ expect(mockPlayerWrapper.classList.contains('bmpui-player-state-replay')).toBe(false);
22
+ expect(mockPlayerWrapper.classList.contains('bmpui-player-state-paused')).toBe(true);
19
23
  });
20
24
  it('should unregister the Seek event listener', () => {
21
25
  removeReplayClass(mockPlayer);
@@ -4,14 +4,20 @@ const logEventHandlers = new Map();
4
4
  export function bindPlayerEvents(npoplayer, player) {
5
5
  if (player == null)
6
6
  return;
7
- let isNewSource = false;
8
7
  let isSeeking = false;
8
+ let isLiveStream = npoplayer.streamObject.stream.isLiveStream;
9
+ let currentTime = 0;
10
+ let isNewSource = true;
11
+ const timeDifference = (time) => {
12
+ if (time === undefined || time === null)
13
+ return;
14
+ currentTime = isLiveStream ? +(time - Date.now() / 1000).toFixed(3) : +time.toFixed(3);
15
+ return currentTime;
16
+ };
9
17
  const logEventHandler = (eventName) => {
10
- return (event) => {
11
- const timeDifference = npoplayer.streamObject.stream.isLiveStream
12
- ? event.position - Date.now() / 1000
13
- : event.position;
14
- logEvent(npoplayer, eventName, timeDifference);
18
+ return (e) => {
19
+ const eventTime = e.time !== undefined ? timeDifference(e.time) : undefined;
20
+ logEvent(npoplayer, eventName, eventTime);
15
21
  };
16
22
  };
17
23
  const seekHandler = (e) => {
@@ -33,33 +39,38 @@ export function bindPlayerEvents(npoplayer, player) {
33
39
  }
34
40
  logEvent(npoplayer, e.type, data);
35
41
  };
36
- const handleSourceLoaded = (e) => {
42
+ const handleSourceLoaded = () => {
37
43
  isNewSource = true;
38
- if (npoplayer.streamObject.stream.isLiveStream) {
39
- logEvent(npoplayer, 'stop', e.position);
44
+ if (isLiveStream) {
45
+ logEvent(npoplayer, 'stop');
40
46
  }
41
47
  };
42
48
  const handlePlay = (e) => {
43
49
  if (!isNewSource && !isSeeking) {
44
- logEvent(npoplayer, 'resume', e.position);
50
+ logEvent(npoplayer, 'resume', timeDifference(e.time));
45
51
  }
46
52
  };
47
- const handlePlaying = (e) => {
53
+ const handlePlaying = () => {
48
54
  if (isSeeking) {
49
55
  isSeeking = false;
50
56
  }
51
- if (isNewSource) {
52
- logEvent(npoplayer, 'start', e.position);
57
+ };
58
+ const handleTime = (e) => {
59
+ if (isNewSource && !npoplayer.adBreakActive) {
60
+ logEvent(npoplayer, 'start', timeDifference(e.time));
53
61
  isNewSource = false;
54
62
  }
63
+ else {
64
+ logEvent(npoplayer, 'time', timeDifference(e.time));
65
+ }
55
66
  };
56
- const handleViewModeChange = (e) => {
67
+ const handleViewModeChange = () => {
57
68
  const newViewMode = player.getViewMode();
58
69
  if (newViewMode === ViewMode.Fullscreen) {
59
- logEvent(npoplayer, 'fullscreen', e.position);
70
+ logEvent(npoplayer, 'fullscreen', currentTime);
60
71
  }
61
72
  else {
62
- logEvent(npoplayer, 'windowed', e.position);
73
+ logEvent(npoplayer, 'windowed', currentTime);
63
74
  }
64
75
  };
65
76
  const stopBeforeUnload = () => {
@@ -71,30 +82,29 @@ export function bindPlayerEvents(npoplayer, player) {
71
82
  window.removeEventListener('beforeunload', logEventHandlers.get('stopBeforeUnload'));
72
83
  logEventHandlers.clear();
73
84
  const playerEvents = [
85
+ { event: PlayerEvent.Ready, name: 'load_complete' },
74
86
  { event: PlayerEvent.Paused, name: 'pause' },
87
+ { event: PlayerEvent.Play, handler: handlePlay },
75
88
  { event: PlayerEvent.SourceLoaded, handler: handleSourceLoaded },
76
- { event: PlayerEvent.Ready, name: 'load_complete' },
77
89
  { event: PlayerEvent.StallStarted, name: 'buffering' },
78
90
  { event: PlayerEvent.StallEnded, name: 'buffering_complete' },
79
91
  { event: PlayerEvent.PlaybackFinished, name: 'complete' },
80
- { event: PlayerEvent.TimeChanged, name: 'time' },
81
92
  { event: PlayerEvent.SourceUnloaded, name: 'stop' },
93
+ { event: PlayerEvent.Seek, handler: seekHandler },
94
+ { event: PlayerEvent.TimeShift, handler: seekHandler },
95
+ { event: PlayerEvent.TimeChanged, handler: handleTime },
96
+ { event: PlayerEvent.ViewModeChanged, handler: handleViewModeChange },
82
97
  ];
83
98
  playerEvents.forEach(({ event, name, handler }) => {
84
- handler = handler || logEventHandler(name);
85
- logEventHandlers.set(event, handler);
86
- player.on(event, handler);
99
+ const effectiveName = name || 'unnamed_event';
100
+ const eventHandler = handler || logEventHandler(effectiveName);
101
+ logEventHandlers.set(event, eventHandler);
102
+ player.on(event, eventHandler);
87
103
  });
88
- logEventHandlers.set(PlayerEvent.Seek, seekHandler);
89
- player.on(PlayerEvent.Seek, seekHandler);
90
- logEventHandlers.set(PlayerEvent.TimeShift, seekHandler);
91
- player.on(PlayerEvent.TimeShift, seekHandler);
92
- logEventHandlers.set(PlayerEvent.Play, handlePlay);
93
- player.on(PlayerEvent.Play, handlePlay);
94
- logEventHandlers.set(PlayerEvent.Playing, handlePlaying);
95
- player.on(PlayerEvent.Playing, handlePlaying);
96
- logEventHandlers.set(PlayerEvent.ViewModeChanged, handleViewModeChange);
97
- player.on(PlayerEvent.ViewModeChanged, handleViewModeChange);
104
+ if (npoplayer.streamObject.assets.preroll === null) {
105
+ logEventHandlers.set(PlayerEvent.Playing, handlePlaying);
106
+ player.on(PlayerEvent.Playing, handlePlaying);
107
+ }
98
108
  logEventHandlers.set('beforeunload', stopBeforeUnload);
99
109
  window.addEventListener('beforeunload', stopBeforeUnload);
100
110
  }
@@ -1,2 +1,2 @@
1
- import NpoPlayer from "../../../npoplayer";
1
+ import NpoPlayer from '../../../npoplayer';
2
2
  export declare function logEvent(npoplayer: NpoPlayer, event: string, data?: any): void;
@@ -26,6 +26,6 @@ export function logEvent(npoplayer, event, data) {
26
26
  }
27
27
  };
28
28
  const streamTrackerHandler = eventHandlers[event];
29
- npoplayer.logEmitter.emit('logEvent', event);
29
+ npoplayer.logEmitter.emit('logEvent', event, data);
30
30
  streamTrackerHandler(streamOptions);
31
31
  }
@@ -38,8 +38,8 @@ export function createForwardButton(player) {
38
38
  export function createPlayNextButton(player, npoplayer) {
39
39
  const playNextButton = new Button({
40
40
  cssClass: 'ui-textbutton ui-playNextButton bmpui-ui-button',
41
- text: 'Volgende aflevering over <span class="countdown"></span>…',
42
- ariaLabel: 'Speel volgende aflevering af',
41
+ text: '<span class="bmpui-ui-textbutton-icon bmpui-ui-textbutton-icon-play"></span> Volgende aflevering',
42
+ ariaLabel: 'Volgende aflevering',
43
43
  hidden: false
44
44
  });
45
45
  playNextButton.onClick.subscribe(() => {
@@ -1,18 +1,6 @@
1
1
  import { AirPlayToggleButton, Button, CastToggleButton, ControlBar, FullscreenToggleButton, PictureInPictureToggleButton, PlaybackToggleButton, SettingsPanel, SettingsToggleButton, VolumeToggleButton } from 'bitmovin-player-ui';
2
2
  import { PlayerAPI } from 'bitmovin-player';
3
3
  export declare function createMiddleButtons(player: PlayerAPI): ControlBar;
4
- export declare function createPlayNextButton(player: PlayerAPI): Button<{
5
- cssClass: string;
6
- text: string;
7
- ariaLabel: string;
8
- hidden: false;
9
- }>;
10
- export declare function createCancelPlayNextButton(player: PlayerAPI): Button<{
11
- cssClass: string;
12
- text: string;
13
- ariaLabel: string;
14
- hidden: false;
15
- }>;
16
4
  export declare function createWatchFromStartButton(): Button<{
17
5
  cssClass: string;
18
6
  text: string;
@@ -3,7 +3,6 @@ import { sendCustomMessage } from '../../nativemobileuicontainer';
3
3
  import { CustomMessages } from '../../../../types/interfaces';
4
4
  import { ViewMode } from 'bitmovin-player';
5
5
  import { createForwardButton, createRewindButton } from '../../components/buttons';
6
- import { hidePlayNextScreen } from '../shared/playnextscreen';
7
6
  function createBigPlayToggleButton() {
8
7
  const bigPlayToggleButton = new PlaybackToggleButton({
9
8
  cssClass: 'ui-playbacktogglebutton'
@@ -30,46 +29,6 @@ export function createMiddleButtons(player) {
30
29
  cssClasses: ['controlbar-middle']
31
30
  });
32
31
  }
33
- export function createPlayNextButton(player) {
34
- const playNextButton = new Button({
35
- cssClass: 'ui-textbutton ui-playNextButton bmpui-ui-button',
36
- text: 'Volgende aflevering over <span class="countdown"></span>…',
37
- ariaLabel: 'Speel volgende aflevering af',
38
- hidden: false
39
- });
40
- const container = player.getContainer();
41
- const playNextButtonClickHandler = () => {
42
- sendCustomMessage(CustomMessages.PLAY_NEXT_PROCEED_CLICK);
43
- hidePlayNextScreen(container, true);
44
- };
45
- playNextButton.onClick.subscribe(() => {
46
- playNextButtonClickHandler();
47
- });
48
- playNextButton.onClick.unsubscribe(() => {
49
- playNextButtonClickHandler();
50
- });
51
- return playNextButton;
52
- }
53
- export function createCancelPlayNextButton(player) {
54
- const cancelPlayNextButton = new Button({
55
- cssClass: 'ui-textbutton ui-cancelPlayNextButton bmpui-ui-button',
56
- text: 'Annuleer',
57
- ariaLabel: 'Annuleer',
58
- hidden: false
59
- });
60
- const container = player.getContainer();
61
- const cancelPlayNextButtonClickHandler = () => {
62
- hidePlayNextScreen(container, true);
63
- sendCustomMessage(CustomMessages.PLAY_NEXT_CANCEL_CLICK);
64
- cancelPlayNextButton.onClick.unsubscribe(() => {
65
- cancelPlayNextButtonClickHandler();
66
- });
67
- };
68
- cancelPlayNextButton.onClick.subscribe(() => {
69
- cancelPlayNextButtonClickHandler();
70
- });
71
- return cancelPlayNextButton;
72
- }
73
32
  export function createWatchFromStartButton() {
74
33
  const watchFromStartButton = new Button({
75
34
  cssClass: 'ui-textbutton ui-watchfromstartbutton bmpui-ui-button',
@@ -28,7 +28,8 @@ describe('handleLiveStreamNoDvr', () => {
28
28
  scrubbingThumbnail: "",
29
29
  subtitles: null,
30
30
  preroll: ""
31
- }
31
+ },
32
+ user: { type: 'anonymous' }
32
33
  };
33
34
  mockContainer = document.createElement('div');
34
35
  mockPlayerElement = document.createElement('div');
@@ -1,2 +1,2 @@
1
- export declare function showPlayNextScreenIfNeeded(overlayDuration: number, containerEl: HTMLElement, proceedCallBack?: () => void, nativeUI?: boolean): void;
2
- export declare function hidePlayNextScreen(containerEl: HTMLElement, nativeUI?: boolean): void;
1
+ export declare function showPlayNextScreenIfNeeded(overlayDuration: number, containerEl: HTMLElement, proceedCallBack?: () => void): void;
2
+ export declare function hidePlayNextScreen(containerEl: HTMLElement): void;
@@ -0,0 +1,26 @@
1
+ let countdownInterval = null;
2
+ export function showPlayNextScreenIfNeeded(overlayDuration, containerEl, proceedCallBack) {
3
+ const playnextOverlay = containerEl.querySelector('.bmpui-overlay-playnext');
4
+ if (playnextOverlay?.classList.contains('show'))
5
+ return;
6
+ playnextOverlay?.classList.add('show');
7
+ const playNextButton = playnextOverlay?.querySelector('.ui-playNextButton');
8
+ if (playNextButton) {
9
+ playNextButton.style.setProperty('--animation-duration', `${overlayDuration}s`);
10
+ playNextButton.focus();
11
+ }
12
+ let countdown = overlayDuration - 1;
13
+ countdownInterval = window.setInterval(() => {
14
+ if (playnextOverlay?.classList.contains('show')) {
15
+ countdown--;
16
+ if (countdown < 0) {
17
+ clearInterval(countdownInterval);
18
+ proceedCallBack?.();
19
+ playnextOverlay?.classList.remove('show');
20
+ }
21
+ }
22
+ }, 1000);
23
+ }
24
+ export function hidePlayNextScreen(containerEl) {
25
+ containerEl.querySelector('.bmpui-overlay-playnext')?.classList.remove('show');
26
+ }
@@ -1,7 +1,6 @@
1
1
  import { showPlayNextScreenIfNeeded } from './playnextscreen';
2
- import { sendCustomMessage } from '../../nativemobileuicontainer';
3
- import { CustomMessages } from '../../../../types/interfaces';
4
- jest.mock('../../nativemobileuicontainer');
2
+ import { sendCustomMessage } from '../nativemobileuicontainer';
3
+ jest.mock('../nativemobileuicontainer');
5
4
  describe('showPlayNextScreenIfNeeded', () => {
6
5
  let containerEl;
7
6
  let playnextOverlay;
@@ -27,30 +26,21 @@ describe('showPlayNextScreenIfNeeded', () => {
27
26
  });
28
27
  it('should not show overlay if it is already shown', () => {
29
28
  playnextOverlay.classList.add('show');
30
- showPlayNextScreenIfNeeded(10, containerEl, proceedCallBack, false);
29
+ showPlayNextScreenIfNeeded(10, containerEl, proceedCallBack);
31
30
  expect(playnextOverlay.classList.contains('show')).toBe(true);
32
31
  expect(proceedCallBack).not.toHaveBeenCalled();
33
32
  });
34
33
  it('should show overlay and start countdown', () => {
35
34
  jest.useFakeTimers();
36
- showPlayNextScreenIfNeeded(10, containerEl, proceedCallBack, false);
35
+ showPlayNextScreenIfNeeded(10, containerEl, proceedCallBack);
37
36
  expect(playnextOverlay.classList.contains('show')).toBe(true);
38
- expect(countdownLabel.innerHTML).toBe('10');
39
- jest.advanceTimersByTime(1000);
40
- expect(countdownLabel.innerHTML).toBe('9');
37
+ expect(playNextButton.style.getPropertyValue('--animation-duration')).toBe('10s');
41
38
  });
42
39
  it('should call proceedCallBack and hide overlay when countdown finishes', () => {
43
40
  jest.useFakeTimers();
44
- showPlayNextScreenIfNeeded(1, containerEl, proceedCallBack, false);
41
+ showPlayNextScreenIfNeeded(1, containerEl, proceedCallBack);
45
42
  jest.advanceTimersByTime(2000);
46
43
  expect(proceedCallBack).toHaveBeenCalled();
47
44
  expect(playnextOverlay.classList.contains('show')).toBe(false);
48
45
  });
49
- it('should send custom messages when nativeUI is true', () => {
50
- jest.useFakeTimers();
51
- showPlayNextScreenIfNeeded(5, containerEl, proceedCallBack, true);
52
- jest.advanceTimersByTime(1000);
53
- expect(sendCustomMessage).toHaveBeenCalledWith(CustomMessages.PLAY_NEXT_OVERLAY_SHOWN);
54
- expect(sendCustomMessage).toHaveBeenCalledWith(CustomMessages.PLAY_NEXT_COUNTDOWN_PROGRESS, { playNext: { remainingCountDownDuration: 3 } });
55
- });
56
46
  });
@@ -1,5 +1,4 @@
1
1
  import { BufferingOverlay, CastStatusOverlay, ErrorMessageOverlay, PlaybackToggleOverlay, PlayerUtils, SubtitleOverlay, UIContainer, } from 'bitmovin-player-ui';
2
- import { createPlayNextScreen } from './components/nativemobile/playnext';
3
2
  import { createCTABar } from './components/nativemobile/ctabar';
4
3
  import { createControlBar } from './components/nativemobile/controlbar';
5
4
  import { createTopBar } from './components/nativemobile/topbar';
@@ -24,7 +23,6 @@ export function sendCustomMessage(message, data) {
24
23
  }
25
24
  export function nativeMobileUIContainer(player) {
26
25
  const middleButtons = createMiddleButtons(player);
27
- const playNextScreen = createPlayNextScreen(player);
28
26
  const settingsPanel = createSettingsPanel(player);
29
27
  const ctaBar = createCTABar(player);
30
28
  const topBar = createTopBar(player, settingsPanel);
@@ -38,13 +36,12 @@ export function nativeMobileUIContainer(player) {
38
36
  new CastStatusOverlay(),
39
37
  middleButtons,
40
38
  controlBar,
41
- playNextScreen,
42
39
  ctaBar,
43
40
  topBar,
44
41
  createTitleBar(),
45
42
  errorMessageOverlay
46
43
  ],
47
44
  cssClasses: ['npo-player', 'native-mobile'],
48
- hidePlayerStateExceptions: [PlayerUtils.PlayerState.Paused],
45
+ hidePlayerStateExceptions: [PlayerUtils.PlayerState.Paused, PlayerUtils.PlayerState.Finished],
49
46
  });
50
47
  }