@npo/player 1.20.3 → 1.21.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 (84) hide show
  1. package/README.md +5 -0
  2. package/lib/js/api/getstreamobject.test.js +2 -2
  3. package/lib/js/markers/updateLiveMarkers.d.ts +4 -0
  4. package/lib/js/markers/updateLiveMarkers.js +21 -0
  5. package/lib/js/markers/updateLiveMarkers.test.d.ts +1 -0
  6. package/lib/js/markers/updateLiveMarkers.test.js +53 -0
  7. package/lib/js/playeractions/handlers/error.d.ts +2 -2
  8. package/lib/js/playeractions/handlers/processsourceconfig.d.ts +1 -1
  9. package/lib/js/playeractions/handlers/processsourceconfig.js +12 -10
  10. package/lib/js/ui/components/adbutton.js +1 -1
  11. package/lib/js/ui/components/buttons.js +2 -2
  12. package/lib/js/ui/components/nativemobile/buttons.d.ts +7 -1
  13. package/lib/js/ui/components/nativemobile/buttons.js +27 -4
  14. package/lib/js/ui/components/nativemobile/controlbar.js +1 -2
  15. package/lib/js/ui/components/nativemobile/playnext.d.ts +2 -1
  16. package/lib/js/ui/components/nativemobile/playnext.js +5 -4
  17. package/lib/js/ui/components/settingspanel.js +30 -23
  18. package/lib/js/ui/components/shared/playnextscreen.d.ts +2 -2
  19. package/lib/js/ui/components/shared/playnextscreen.js +14 -2
  20. package/lib/js/ui/components/shared/playnextstreen.test.d.ts +6 -0
  21. package/lib/js/ui/components/shared/playnextstreen.test.js +56 -0
  22. package/lib/js/ui/handlers/accessibilityhandler.js +26 -2
  23. package/lib/js/ui/handlers/accessibilityhandler.test.d.ts +1 -0
  24. package/lib/js/ui/handlers/accessibilityhandler.test.js +63 -0
  25. package/lib/js/ui/handlers/domhandlers.d.ts +3 -0
  26. package/lib/js/ui/handlers/domhandlers.js +7 -0
  27. package/lib/js/ui/handlers/domhandlers.test.d.ts +1 -0
  28. package/lib/js/ui/handlers/domhandlers.test.js +49 -0
  29. package/lib/js/ui/handlers/nicamhandler.js +1 -1
  30. package/lib/js/ui/handlers/streamhandler.js +4 -4
  31. package/lib/js/ui/nativemobileuicontainer.js +5 -5
  32. package/lib/js/ui/nativemobileuifactory.js +82 -42
  33. package/lib/js/ui/nativemobileuifactory.test.d.ts +1 -0
  34. package/lib/js/ui/nativemobileuifactory.test.js +66 -0
  35. package/lib/js/ui/uicontainer.js +4 -3
  36. package/lib/js/utilities/utilities.d.ts +3 -0
  37. package/lib/js/utilities/utilities.js +9 -0
  38. package/lib/lang/nl.json +1 -1
  39. package/lib/npoplayer-bridge.test.d.ts +1 -0
  40. package/lib/npoplayer-bridge.test.js +23 -0
  41. package/lib/npoplayer.d.ts +2 -1
  42. package/lib/npoplayer.js +13 -4
  43. package/lib/npoplayer.test.js +4 -4
  44. package/lib/package.json +3 -2
  45. package/lib/src/js/markers/updateLiveMarkers.d.ts +4 -0
  46. package/lib/src/js/markers/updateLiveMarkers.test.d.ts +1 -0
  47. package/lib/src/js/playeractions/handlers/error.d.ts +2 -2
  48. package/lib/src/js/playeractions/handlers/processsourceconfig.d.ts +1 -1
  49. package/lib/src/js/ui/components/nativemobile/buttons.d.ts +7 -1
  50. package/lib/src/js/ui/components/nativemobile/playnext.d.ts +2 -1
  51. package/lib/src/js/ui/components/shared/playnextscreen.d.ts +2 -2
  52. package/lib/src/js/ui/components/shared/playnextstreen.test.d.ts +6 -0
  53. package/lib/src/js/ui/handlers/accessibilityhandler.test.d.ts +1 -0
  54. package/lib/src/js/ui/handlers/domhandlers.d.ts +3 -0
  55. package/lib/src/js/ui/handlers/domhandlers.test.d.ts +1 -0
  56. package/lib/src/js/ui/nativemobileuifactory.test.d.ts +1 -0
  57. package/lib/src/js/utilities/utilities.d.ts +3 -0
  58. package/lib/src/npoplayer-bridge.test.d.ts +1 -0
  59. package/lib/src/npoplayer.d.ts +2 -1
  60. package/lib/src/types/interfaces.d.ts +26 -2
  61. package/lib/tests/mocks/mockNpoplayer.js +4 -1
  62. package/lib/types/interfaces.d.ts +26 -2
  63. package/lib/types/interfaces.js +12 -2
  64. package/package.json +3 -2
  65. package/src/scss/components/_advert.scss +34 -68
  66. package/src/scss/components/_hugeplaybacktogglebutton.scss +11 -0
  67. package/src/scss/components/_icons.scss +50 -40
  68. package/src/scss/components/_nicam.scss +9 -2
  69. package/src/scss/components/_playnext.scss +5 -1
  70. package/src/scss/components/_replay.scss +1 -1
  71. package/src/scss/components/_seekbar.scss +13 -0
  72. package/src/scss/components/_settingspanel.scss +37 -12
  73. package/src/scss/components/_textbuttons.scss +12 -11
  74. package/src/scss/components/_volumeslider.scss +5 -0
  75. package/src/scss/components/audio/_playbutton.scss +6 -0
  76. package/src/scss/components/audio/_volumeslider.scss +6 -1
  77. package/src/scss/npoplayer.css +80 -56
  78. package/src/scss/npoplayer.scss +3 -0
  79. package/src/scss/variants/_player-base.scss +1 -9
  80. package/src/scss/variants/_player-large.scss +20 -0
  81. package/src/scss/variants/_player-medium.scss +31 -1
  82. package/src/scss/variants/_player-native-mobile.scss +4 -0
  83. package/src/scss/variants/_player-small.scss +11 -8
  84. package/src/scss/vars/_colors.scss +1 -0
package/README.md CHANGED
@@ -21,3 +21,8 @@ MIT
21
21
 
22
22
  [JWT.IO - JSON Web Tokens Introduction]: https://jwt.io/introduction
23
23
  [self generated JWT]: https://www.npmjs.com/package/jsonwebtoken
24
+
25
+ ## Native mobile development
26
+
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
+
@@ -9,9 +9,9 @@ describe('getStreamObject', () => {
9
9
  let npoplayer;
10
10
  let apiPayload;
11
11
  const testPlayerConfig = {
12
- key: "D634EA2C-BFE7-4BB6-9EAA-CDCC7B1EDFFA",
12
+ key: 'dummy key',
13
13
  };
14
- const div = document.createElement("div");
14
+ const div = document.createElement('div');
15
15
  beforeEach(() => {
16
16
  npoplayer = new NpoPlayer(div, testPlayerConfig);
17
17
  apiPayload = {
@@ -0,0 +1,4 @@
1
+ import { PlayerAPI } from "bitmovin-player";
2
+ import { UIManager } from "bitmovin-player-ui";
3
+ import { TimeLineMarker } from "types/interfaces";
4
+ export declare function updateLiveMarkers(timeLineMarkers: TimeLineMarker[], player: PlayerAPI, uiManager: UIManager): void;
@@ -0,0 +1,21 @@
1
+ export function updateLiveMarkers(timeLineMarkers, player, uiManager) {
2
+ if (!player || !uiManager) {
3
+ console.error('Player or UIManager is null');
4
+ return;
5
+ }
6
+ 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 => {
19
+ uiManager?.addTimelineMarker(marker);
20
+ });
21
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,53 @@
1
+ import { updateLiveMarkers } from './updateLiveMarkers';
2
+ import { UIManager } from "bitmovin-player-ui";
3
+ jest.mock("bitmovin-player-ui", () => {
4
+ return {
5
+ UIManager: jest.fn().mockImplementation(() => ({
6
+ getTimelineMarkers: jest.fn().mockReturnValue([]),
7
+ addTimelineMarker: jest.fn(),
8
+ removeTimelineMarker: jest.fn(),
9
+ })),
10
+ };
11
+ });
12
+ jest.mock("bitmovin-player", () => ({
13
+ PlayerAPI: jest.fn(),
14
+ }));
15
+ describe('updateLiveMarkers', () => {
16
+ const player = {
17
+ getCurrentTime: jest.fn().mockReturnValue(10),
18
+ };
19
+ let uiManager;
20
+ let ui;
21
+ beforeEach(() => {
22
+ uiManager = new UIManager(player, ui);
23
+ });
24
+ test('should correctly update UI manager with sorted and transformed markers', () => {
25
+ const timeLineMarkers = [
26
+ { time: 3000, title: 'Marker 2' },
27
+ { time: 1000, title: 'Marker 1' },
28
+ ];
29
+ updateLiveMarkers(timeLineMarkers, player, uiManager);
30
+ expect(uiManager.getTimelineMarkers).toHaveBeenCalled();
31
+ expect(uiManager.removeTimelineMarker).toHaveBeenCalledTimes(0);
32
+ expect(uiManager.addTimelineMarker).toHaveBeenCalledTimes(2);
33
+ expect(uiManager.addTimelineMarker).toHaveBeenCalledWith(expect.objectContaining({
34
+ time: 1000,
35
+ title: 'Marker 1',
36
+ cssClasses: ['npo-livestream-marker'],
37
+ }));
38
+ expect(uiManager.addTimelineMarker).toHaveBeenCalledWith(expect.objectContaining({
39
+ time: 3000,
40
+ title: 'Marker 2',
41
+ cssClasses: ['npo-livestream-marker'],
42
+ }));
43
+ });
44
+ test('should remove existing markers before adding new ones', () => {
45
+ uiManager.getTimelineMarkers.mockReturnValue([{ time: 2000, title: 'Existing Marker' }]);
46
+ const timeLineMarkers = [
47
+ { time: 1000, title: 'New Marker' },
48
+ ];
49
+ updateLiveMarkers(timeLineMarkers, player, uiManager);
50
+ expect(uiManager.removeTimelineMarker).toHaveBeenCalledTimes(1);
51
+ expect(uiManager.addTimelineMarker).toHaveBeenCalledTimes(1);
52
+ });
53
+ });
@@ -1,3 +1,3 @@
1
- import { PlayerAPI } from "bitmovin-player";
2
- import { UIComponents } from "../../../types/interfaces";
1
+ import { PlayerAPI } from 'bitmovin-player';
2
+ import { UIComponents } from '../../../types/interfaces';
3
3
  export declare function handlePlayerError(player: PlayerAPI, uiComponents: UIComponents, input: any): void;
@@ -1,3 +1,3 @@
1
1
  import { StreamObject } from 'types/interfaces';
2
2
  import { SourceConfig } from 'bitmovin-player';
3
- export declare function processSourceConfig(_sourceConfig: SourceConfig | undefined, _streamObject: StreamObject, drm?: string | null): SourceConfig;
3
+ export declare function processSourceConfig(_sourceConfig: SourceConfig | undefined, _streamObject: StreamObject, drm: string | null | undefined, useWidevineServerCertificate: boolean): Promise<SourceConfig>;
@@ -1,7 +1,7 @@
1
1
  import { HttpResponseType } from 'bitmovin-player';
2
2
  import { npoCdnProviders } from '../../cdnproviders';
3
3
  import { replaceSpecialChars } from '../../utilities/utilities';
4
- function setDrm(sourceConfig, drm, _streamObject) {
4
+ async function setDrm(sourceConfig, drm, _streamObject, useWidevineServerCertificate) {
5
5
  const npoDrmGateway = 'https://npo-drm-gateway.samgcloud.nepworldwide.nl/authentication?custom_data=';
6
6
  switch (drm) {
7
7
  case 'fairplay':
@@ -36,19 +36,21 @@ function setDrm(sourceConfig, drm, _streamObject) {
36
36
  };
37
37
  break;
38
38
  case 'widevine':
39
- fetch('https://widevine.npoplayer.nl/widevine_player_cert.bin')
40
- .then(response => response.arrayBuffer())
41
- .then(certificate => {
39
+ try {
42
40
  sourceConfig.drm = {
43
41
  widevine: {
44
- serverCertificate: certificate,
45
42
  LA_URL: npoDrmGateway + _streamObject.stream.drmToken,
46
43
  audioRobustness: 'SW_SECURE_CRYPTO',
47
- videoRobustness: 'SW_SECURE_CRYPTO'
44
+ videoRobustness: 'SW_SECURE_CRYPTO',
45
+ serverCertificate: useWidevineServerCertificate
46
+ ? await (await fetch('https://widevine.npoplayer.nl/widevine_player_cert.bin')).arrayBuffer()
47
+ : undefined
48
48
  }
49
49
  };
50
- })
51
- .catch(error => console.error('Error:', error));
50
+ }
51
+ catch (error) {
52
+ console.error('Error:', error);
53
+ }
52
54
  break;
53
55
  case 'playready':
54
56
  sourceConfig.drm = {
@@ -89,7 +91,7 @@ function setStreamProfile(sourceConfig, _sourceConfig, _streamObject) {
89
91
  }
90
92
  return sourceConfig;
91
93
  }
92
- export function processSourceConfig(_sourceConfig = {}, _streamObject, drm = null) {
94
+ export async function processSourceConfig(_sourceConfig = {}, _streamObject, drm = null, useWidevineServerCertificate) {
93
95
  let sourceConfig = {};
94
96
  sourceConfig.title =
95
97
  _sourceConfig.title ?? _streamObject.metadata.title ?? '';
@@ -123,7 +125,7 @@ export function processSourceConfig(_sourceConfig = {}, _streamObject, drm = nul
123
125
  : undefined;
124
126
  sourceConfig.options = _sourceConfig.options;
125
127
  sourceConfig = setStreamProfile(sourceConfig, _sourceConfig, _streamObject);
126
- sourceConfig = setDrm(sourceConfig, drm, _streamObject);
128
+ sourceConfig = await setDrm(sourceConfig, drm, _streamObject, useWidevineServerCertificate);
127
129
  const streamSource = _streamObject.stream.streamURL;
128
130
  let cdnString = streamSource;
129
131
  if (streamSource.startsWith('file:///')) {
@@ -3,7 +3,7 @@ export function createAdButton(npoplayer) {
3
3
  npoplayer.uiComponents.adbutton = undefined;
4
4
  const adButton = new Button({
5
5
  cssClass: 'ui-textbutton ui-sterbutton bmpui-ui-button',
6
- text: 'Meer info',
6
+ text: 'Ga naar website van adverteerder',
7
7
  hidden: true
8
8
  });
9
9
  npoplayer.uiComponents.adbutton = adButton;
@@ -14,7 +14,7 @@ export function createMiddleButtons(player) {
14
14
  export function createRewindButton(player) {
15
15
  const rewindButton = new Button({
16
16
  cssClass: 'ui-rewindbutton bmpui-ui-button',
17
- ariaLabel: 'Spoel 10 seconde terug',
17
+ ariaLabel: 'Spoel 10 seconden terug',
18
18
  });
19
19
  if (player) {
20
20
  rewindButton.onClick.subscribe(() => {
@@ -26,7 +26,7 @@ export function createRewindButton(player) {
26
26
  export function createForwardButton(player) {
27
27
  const forwardButton = new Button({
28
28
  cssClass: 'ui-forwardbutton bmpui-ui-button',
29
- ariaLabel: 'Spoel 10 seconde vooruit',
29
+ ariaLabel: 'Spoel 10 seconden vooruit',
30
30
  });
31
31
  if (player) {
32
32
  forwardButton.onClick.subscribe(() => {
@@ -1,7 +1,13 @@
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(): Button<{
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<{
5
11
  cssClass: string;
6
12
  text: string;
7
13
  ariaLabel: string;
@@ -3,6 +3,7 @@ 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';
6
7
  function createBigPlayToggleButton() {
7
8
  const bigPlayToggleButton = new PlaybackToggleButton({
8
9
  cssClass: 'ui-playbacktogglebutton'
@@ -29,15 +30,17 @@ export function createMiddleButtons(player) {
29
30
  cssClasses: ['controlbar-middle']
30
31
  });
31
32
  }
32
- export function createPlayNextButton() {
33
+ export function createPlayNextButton(player) {
33
34
  const playNextButton = new Button({
34
- cssClass: 'ui-hugeplaynextbacktogglebutton ui-playNextButton bmpui-ui-button',
35
- text: 'De volgende aflevering wordt zo afgespeeld...',
35
+ cssClass: 'ui-textbutton ui-playNextButton bmpui-ui-button',
36
+ text: 'Volgende aflevering over <span class="countdown"></span>…',
36
37
  ariaLabel: 'Speel volgende aflevering af',
37
38
  hidden: false
38
39
  });
40
+ const container = player.getContainer();
39
41
  const playNextButtonClickHandler = () => {
40
- sendCustomMessage(CustomMessages.CLICK_PLAY_NEXT);
42
+ sendCustomMessage(CustomMessages.PLAY_NEXT_PROCEED_CLICK);
43
+ hidePlayNextScreen(container, true);
41
44
  };
42
45
  playNextButton.onClick.subscribe(() => {
43
46
  playNextButtonClickHandler();
@@ -47,6 +50,26 @@ export function createPlayNextButton() {
47
50
  });
48
51
  return playNextButton;
49
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
+ }
50
73
  export function createWatchFromStartButton() {
51
74
  const watchFromStartButton = new Button({
52
75
  cssClass: 'ui-textbutton ui-watchfromstartbutton bmpui-ui-button',
@@ -1,4 +1,4 @@
1
- import { ControlBar, Container, PlaybackTimeLabel, PlaybackTimeLabelMode, SeekBar, SeekBarLabel, VolumeSlider, Spacer, } from 'bitmovin-player-ui';
1
+ import { ControlBar, Container, PlaybackTimeLabel, PlaybackTimeLabelMode, SeekBar, SeekBarLabel, Spacer, } from 'bitmovin-player-ui';
2
2
  import { createForwardButton, createRewindButton } from '../buttons';
3
3
  import { createAirPlayToggleButton, createCastToggleButton, createFullscreenToggleButton, createPictureInPictureToggleButton, createPlaybackToggleButton, createSettingsButton, createVolumeToggleButton, } from './buttons';
4
4
  export function createControlBar(player, settingsPanel) {
@@ -26,7 +26,6 @@ export function createControlBar(player, settingsPanel) {
26
26
  components: [
27
27
  createPlaybackToggleButton(),
28
28
  createVolumeToggleButton(player),
29
- new VolumeSlider(),
30
29
  rewindButton,
31
30
  forwardButton,
32
31
  new Spacer(),
@@ -1,5 +1,6 @@
1
1
  import { Container } from 'bitmovin-player-ui';
2
- export declare function createPlayNextScreen(): Container<{
2
+ import { PlayerAPI } from 'bitmovin-player';
3
+ export declare function createPlayNextScreen(player: PlayerAPI): Container<{
3
4
  components: import("bitmovin-player-ui").Button<{
4
5
  cssClass: string;
5
6
  text: string;
@@ -1,9 +1,10 @@
1
1
  import { Container } from 'bitmovin-player-ui';
2
- import { createPlayNextButton } from './buttons';
3
- export function createPlayNextScreen() {
4
- const playNextButton = createPlayNextButton();
2
+ import { createPlayNextButton, createCancelPlayNextButton } from './buttons';
3
+ export function createPlayNextScreen(player) {
4
+ const playNextButton = createPlayNextButton(player);
5
+ const cancelButton = createCancelPlayNextButton(player);
5
6
  return new Container({
6
- components: [playNextButton],
7
+ components: [cancelButton, playNextButton],
7
8
  cssClasses: ['overlay-playnext']
8
9
  });
9
10
  }
@@ -48,7 +48,13 @@ export function createSettingsPanel(npoplayer) {
48
48
  cssClasses: [listboxPanelClass]
49
49
  });
50
50
  const settingsTriggerButton = new Button({ cssClass: settingTriggerClass });
51
- const mainSettingsPage = new SettingsPanelPage({
51
+ class CustomSettingsPanelPage extends SettingsPanelPage {
52
+ constructor(config) {
53
+ super(config);
54
+ }
55
+ onActiveEvent() { }
56
+ }
57
+ const mainSettingsPage = new CustomSettingsPanelPage({
52
58
  components: [
53
59
  settingsTriggerButton,
54
60
  new Label({ text: settingsLabel, cssClass: 'setting-header' }),
@@ -57,13 +63,11 @@ export function createSettingsPanel(npoplayer) {
57
63
  ],
58
64
  cssClasses: ['main-panel']
59
65
  });
60
- mainSettingsPage.onActive.subscribeOnce(() => {
61
- qualityListBox.addItem('auto', autoLabel);
62
- player?.getAvailableVideoQualities().forEach(quality => {
63
- qualityListBox.addItem(quality.id, quality.label ?? 'test');
64
- });
65
- qualityListBox.selectItem('auto');
66
- });
66
+ const onSourceLoaded = () => {
67
+ player?.off(PlayerEvent.SourceLoaded, onSourceLoaded);
68
+ checkQualities();
69
+ checkSubtitles();
70
+ };
67
71
  const settingsPanel = new SettingsPanel({
68
72
  components: [
69
73
  mainSettingsPage,
@@ -72,7 +76,8 @@ export function createSettingsPanel(npoplayer) {
72
76
  qualityPanelPage
73
77
  ],
74
78
  hidden: true,
75
- pageTransitionAnimation: false
79
+ pageTransitionAnimation: false,
80
+ hideDelay: -1,
76
81
  });
77
82
  const subtitleSettingsOpenButton = new SettingsPanelPageOpenButton({
78
83
  targetPage: subtitlePanelPage,
@@ -131,9 +136,6 @@ export function createSettingsPanel(npoplayer) {
131
136
  npoplayer.uiComponents.settingsPanel = settingsPanel;
132
137
  }
133
138
  const checkSubtitles = () => {
134
- player?.off(PlayerEvent.SourceLoaded, checkSubtitles);
135
- player?.off(PlayerEvent.AdBreakFinished, checkSubtitles);
136
- player?.off(PlayerEvent.AdError, checkSubtitles);
137
139
  const subtitles = player?.subtitles?.list();
138
140
  if (subtitles != null && subtitles.length > 0) {
139
141
  subtitlesPanelItem.show();
@@ -142,22 +144,27 @@ export function createSettingsPanel(npoplayer) {
142
144
  subtitlesPanelItem.hide();
143
145
  }
144
146
  };
145
- const checkQuality = () => {
146
- player?.off(PlayerEvent.SourceLoaded, checkQuality);
147
- player?.off(PlayerEvent.AdBreakFinished, checkQuality);
148
- player?.off(PlayerEvent.AdError, checkQuality);
149
- if (player?.getAvailableVideoQualities() !== undefined && player?.getAvailableVideoQualities().length > 0) {
147
+ const checkQualities = () => {
148
+ const availableVideoQualities = player?.getAvailableVideoQualities();
149
+ qualityListBox.clearItems();
150
+ if (availableVideoQualities?.length) {
151
+ player?.getAvailableVideoQualities().forEach(quality => {
152
+ qualityListBox.addItem(quality.id, quality.label ?? 'test');
153
+ });
154
+ qualityListBox.addItem('auto', autoLabel);
155
+ qualityListBox.selectItem('auto');
150
156
  qualityPanelItem.show();
151
157
  }
152
158
  else {
153
159
  qualityPanelItem.hide();
154
160
  }
155
161
  };
156
- player?.on(PlayerEvent.SourceLoaded, checkSubtitles);
157
- player?.on(PlayerEvent.SourceLoaded, checkQuality);
158
- player?.on(PlayerEvent.AdBreakFinished, checkSubtitles);
159
- player?.on(PlayerEvent.AdBreakFinished, checkQuality);
160
- player?.on(PlayerEvent.AdError, checkSubtitles);
161
- player?.on(PlayerEvent.AdError, checkQuality);
162
+ player?.on(PlayerEvent.SourceLoaded, onSourceLoaded);
163
+ settingsPanel.onHide.subscribe(() => {
164
+ player?.getContainer().querySelector('.bmpui-npo-player')?.classList.remove('bmpui-settings-panel-shown');
165
+ });
166
+ settingsPanel.onShow.subscribe(() => {
167
+ player?.getContainer().querySelector('.bmpui-npo-player')?.classList.add('bmpui-settings-panel-shown');
168
+ });
162
169
  return settingsPanel;
163
170
  }
@@ -1,2 +1,2 @@
1
- export declare function showPlayNextScreen(overlayDuration: number, containerEl: HTMLElement, proceedCallBack?: () => void): void;
2
- export declare function hidePlayNextScreen(containerEl: HTMLElement): void;
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,11 +1,17 @@
1
+ import { sendCustomMessage } from '../../nativemobileuicontainer';
2
+ import { CustomMessages } from '../../../../types/interfaces';
1
3
  let countdownInterval = null;
2
- export function showPlayNextScreen(overlayDuration, containerEl, proceedCallBack) {
4
+ export function showPlayNextScreenIfNeeded(overlayDuration, containerEl, proceedCallBack, nativeUI = false) {
3
5
  const playnextOverlay = containerEl.querySelector('.bmpui-overlay-playnext');
4
6
  const countdownLabel = playnextOverlay?.querySelector('.countdown');
7
+ if (playnextOverlay?.classList.contains('show'))
8
+ return;
5
9
  if (countdownLabel) {
6
10
  countdownLabel.innerHTML = overlayDuration.toString();
7
11
  }
8
12
  playnextOverlay?.classList.add('show');
13
+ if (nativeUI)
14
+ sendCustomMessage(CustomMessages.PLAY_NEXT_OVERLAY_SHOWN);
9
15
  const playNextButton = playnextOverlay?.querySelector('.ui-playNextButton');
10
16
  if (playNextButton) {
11
17
  playNextButton.focus();
@@ -15,18 +21,24 @@ export function showPlayNextScreen(overlayDuration, containerEl, proceedCallBack
15
21
  if (playnextOverlay?.classList.contains('show') && countdownLabel) {
16
22
  countdownLabel.innerHTML = countdown.toString();
17
23
  countdown -= 1;
24
+ if (nativeUI)
25
+ sendCustomMessage(CustomMessages.PLAY_NEXT_COUNTDOWN_PROGRESS, { playNext: { remainingCountDownDuration: countdown } });
18
26
  }
19
27
  if (countdown < 0) {
20
28
  clearInterval(countdownInterval);
21
29
  if (playnextOverlay?.classList.contains('show')) {
22
30
  proceedCallBack?.();
23
31
  playnextOverlay?.classList.remove('show');
32
+ if (nativeUI)
33
+ sendCustomMessage(CustomMessages.PLAY_NEXT_COUNTDOWN_FINISHED);
24
34
  }
25
35
  }
26
36
  }, 1000);
27
37
  }
28
- export function hidePlayNextScreen(containerEl) {
38
+ export function hidePlayNextScreen(containerEl, nativeUI = false) {
29
39
  containerEl.querySelector('.bmpui-overlay-playnext')?.classList.remove('show');
40
+ if (nativeUI)
41
+ sendCustomMessage(CustomMessages.PLAY_NEXT_OVERLAY_HIDDEN);
30
42
  if (countdownInterval !== null) {
31
43
  clearInterval(countdownInterval);
32
44
  countdownInterval = null;
@@ -0,0 +1,6 @@
1
+ declare global {
2
+ interface Window {
3
+ bitmovin: any;
4
+ }
5
+ }
6
+ export {};
@@ -0,0 +1,56 @@
1
+ import { showPlayNextScreenIfNeeded } from './playnextscreen';
2
+ import { sendCustomMessage } from '../../nativemobileuicontainer';
3
+ import { CustomMessages } from '../../../../types/interfaces';
4
+ jest.mock('../../nativemobileuicontainer');
5
+ describe('showPlayNextScreenIfNeeded', () => {
6
+ let containerEl;
7
+ let playnextOverlay;
8
+ let countdownLabel;
9
+ let playNextButton;
10
+ let proceedCallBack;
11
+ beforeEach(() => {
12
+ sendCustomMessage.mockClear();
13
+ playnextOverlay = document.createElement('div');
14
+ playnextOverlay.classList.add('bmpui-overlay-playnext');
15
+ countdownLabel = document.createElement('div');
16
+ countdownLabel.classList.add('countdown');
17
+ playnextOverlay.appendChild(countdownLabel);
18
+ playNextButton = document.createElement('button');
19
+ playNextButton.classList.add('ui-playNextButton');
20
+ playnextOverlay.appendChild(playNextButton);
21
+ containerEl = document.createElement('div');
22
+ containerEl.appendChild(playnextOverlay);
23
+ proceedCallBack = jest.fn();
24
+ });
25
+ afterEach(() => {
26
+ jest.clearAllTimers();
27
+ });
28
+ it('should not show overlay if it is already shown', () => {
29
+ playnextOverlay.classList.add('show');
30
+ showPlayNextScreenIfNeeded(10, containerEl, proceedCallBack, false);
31
+ expect(playnextOverlay.classList.contains('show')).toBe(true);
32
+ expect(proceedCallBack).not.toHaveBeenCalled();
33
+ });
34
+ it('should show overlay and start countdown', () => {
35
+ jest.useFakeTimers();
36
+ showPlayNextScreenIfNeeded(10, containerEl, proceedCallBack, false);
37
+ expect(playnextOverlay.classList.contains('show')).toBe(true);
38
+ expect(countdownLabel.innerHTML).toBe('10');
39
+ jest.advanceTimersByTime(1000);
40
+ expect(countdownLabel.innerHTML).toBe('9');
41
+ });
42
+ it('should call proceedCallBack and hide overlay when countdown finishes', () => {
43
+ jest.useFakeTimers();
44
+ showPlayNextScreenIfNeeded(1, containerEl, proceedCallBack, false);
45
+ jest.advanceTimersByTime(2000);
46
+ expect(proceedCallBack).toHaveBeenCalled();
47
+ expect(playnextOverlay.classList.contains('show')).toBe(false);
48
+ });
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
+ });
@@ -1,9 +1,33 @@
1
1
  export function addAccessibilityAttributes(container, metadata) {
2
+ const label = 'aria-label';
2
3
  const videoEl = container.querySelector('video');
3
- videoEl?.setAttribute('aria-label', metadata.title);
4
- videoEl?.setAttribute('aria-description', metadata.description);
4
+ const hugePlayBackToggleButton = container.querySelector('.bmpui-ui-hugeplaybacktogglebutton');
5
+ videoEl?.setAttribute(label, metadata.title);
6
+ hugePlayBackToggleButton?.setAttribute(label, metadata.title);
7
+ const description = container.querySelector('.bmpui-label-metadata-description');
8
+ if (description) {
9
+ videoEl?.setAttribute('aria-describedby', description.id);
10
+ hugePlayBackToggleButton?.setAttribute('aria-describedby', description.id);
11
+ }
12
+ const imgRoles = container.querySelectorAll('[role="img"]');
13
+ imgRoles.forEach(imgRole => {
14
+ imgRole.setAttribute(label, metadata.title);
15
+ });
16
+ const popupButtons = container.querySelectorAll('[role="pop-up button"]');
17
+ popupButtons.forEach(popupButton => {
18
+ popupButton.setAttribute('role', 'button');
19
+ popupButton.setAttribute('aria-haspopup', 'menu');
20
+ });
5
21
  const menuItems = container.querySelectorAll('[role="menuitem"][aria-pressed]');
6
22
  menuItems.forEach(menuitem => {
7
23
  menuitem.removeAttribute('aria-pressed');
8
24
  });
25
+ const panelItems = container.querySelectorAll('.bmpui-ui-settings-panel-item[role="menuitem"]');
26
+ panelItems.forEach(panelItem => {
27
+ panelItem.setAttribute('role', 'group');
28
+ });
29
+ const panelPages = container.querySelectorAll('.bmpui-ui-settings-panel-page.bmpui-listbox-panel[role="menu"]');
30
+ panelPages.forEach(panelPage => {
31
+ panelPage.setAttribute('role', 'group');
32
+ });
9
33
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,63 @@
1
+ import { addAccessibilityAttributes } from './accessibilityhandler';
2
+ import { JSDOM } from 'jsdom';
3
+ describe('addAccessibilityAttributes', () => {
4
+ let container;
5
+ const mockMetadata = { title: 'Mocked Video Title', description: 'Mocked Description' };
6
+ const label = 'aria-label';
7
+ beforeEach(() => {
8
+ const { window } = new JSDOM(`
9
+ <div>
10
+ <video></video>
11
+ <div class="bmpui-ui-hugeplaybacktogglebutton"></div>
12
+ <div class="bmpui-label-metadata-description" id="desc">Description</div>
13
+ <div role="img"></div>
14
+ <div role="pop-up button"></div>
15
+ <div role="menuitem" aria-pressed="true"></div>
16
+ <div class="bmpui-ui-settings-panel-item" role="menuitem"></div>
17
+ <div class="bmpui-ui-settings-panel-page bmpui-listbox-panel" role="menu"></div>
18
+ </div>
19
+ `);
20
+ container = window.document.body.firstChild;
21
+ });
22
+ function expectAttribute(selector, attribute, expectedValue) {
23
+ const element = container?.querySelector(selector);
24
+ expect(element?.getAttribute(attribute)).toBe(expectedValue);
25
+ }
26
+ it('adds correct aria-labels', () => {
27
+ if (!container)
28
+ return;
29
+ addAccessibilityAttributes(container, mockMetadata);
30
+ expectAttribute('video', label, mockMetadata.title);
31
+ expectAttribute('.bmpui-ui-hugeplaybacktogglebutton', label, mockMetadata.title);
32
+ expectAttribute('[role="img"]', label, mockMetadata.title);
33
+ });
34
+ it('handles aria-describedby correctly', () => {
35
+ if (!container)
36
+ return;
37
+ addAccessibilityAttributes(container, mockMetadata);
38
+ const descriptionId = container.querySelector('.bmpui-label-metadata-description')?.id;
39
+ expectAttribute('video', 'aria-describedby', descriptionId ? descriptionId : '');
40
+ expectAttribute('.bmpui-ui-hugeplaybacktogglebutton', 'aria-describedby', descriptionId ? descriptionId : '');
41
+ });
42
+ it('correctly adjusts role and aria-haspopup attributes', () => {
43
+ if (!container)
44
+ return;
45
+ addAccessibilityAttributes(container, mockMetadata);
46
+ const popupButton = container.querySelector('[role="button"]');
47
+ expect(popupButton).toBeTruthy();
48
+ expect(popupButton?.getAttribute('aria-haspopup')).toBe('menu');
49
+ });
50
+ it('removes aria-pressed from menuitems', () => {
51
+ if (!container)
52
+ return;
53
+ addAccessibilityAttributes(container, mockMetadata);
54
+ expect(container.querySelector('[role="menuitem"][aria-pressed]')).toBeFalsy();
55
+ });
56
+ it('changes the role of menuitems and menus correctly', () => {
57
+ if (!container)
58
+ return;
59
+ addAccessibilityAttributes(container, mockMetadata);
60
+ expectAttribute('.bmpui-ui-settings-panel-item', 'role', 'group');
61
+ expectAttribute('.bmpui-ui-settings-panel-page', 'role', 'group');
62
+ });
63
+ });
@@ -0,0 +1,3 @@
1
+ import { StreamObject } from "types/interfaces";
2
+ import { PlayerAPI } from "bitmovin-player";
3
+ export declare function handleLiveStreamNoDvr(player: PlayerAPI, streamObject: StreamObject, container: HTMLElement): void;
@@ -0,0 +1,7 @@
1
+ import { getPlayerElement } from "../../utilities/utilities";
2
+ export function handleLiveStreamNoDvr(player, streamObject, container) {
3
+ const isLiveStreamNoDvr = streamObject.stream.isLiveStream && streamObject.stream.hasDvrWindow === false;
4
+ const playerElement = getPlayerElement(player, '.bmpui-npo-player');
5
+ const noDvrClass = 'livestream-no-dvr';
6
+ playerElement?.classList[isLiveStreamNoDvr ? 'add' : 'remove'](noDvrClass);
7
+ }