diff --git a/packages/cxl-ui/package.json b/packages/cxl-ui/package.json index 493fbe6a8..9e9eb7855 100644 --- a/packages/cxl-ui/package.json +++ b/packages/cxl-ui/package.json @@ -32,8 +32,10 @@ "@vaadin/tooltip": "^23.3.7", "@vaadin/vaadin-themable-mixin": "^23.3.7", "cross-env": "~7.0.2", + "crypto-js": "^4.1.1", "headroom.js": "^0.12.0", "imports-loader": "^2.0.0", + "jose": "^4.13.1", "laravel-mix": "^6.0.39", "lit": "^2.2.5", "lodash-es": "^4.17.21", diff --git a/packages/cxl-ui/scss/cxl-jw-player/cxl-jw-player-shadow.scss b/packages/cxl-ui/scss/cxl-jw-player/cxl-jw-player-shadow.scss index 54f4f6c4e..22d930f3e 100644 --- a/packages/cxl-ui/scss/cxl-jw-player/cxl-jw-player-shadow.scss +++ b/packages/cxl-ui/scss/cxl-jw-player/cxl-jw-player-shadow.scss @@ -1,10 +1,12 @@ -@use "~@conversionxl/cxl-lumo-styles/scss/mixins"; +@use '~@conversionxl/cxl-lumo-styles/scss/mixins'; :host { box-sizing: border-box; +} - [active] { - background-color: var(--lumo-shade-10pct); +:host([has-captions]) { + .cxl-jw-player-container { + display: grid; } } @@ -35,6 +37,10 @@ justify-content: center; } +.cxl-jw-player-container { + height: 100%; +} + .flex { display: flex; height: 100%; diff --git a/packages/cxl-ui/scss/cxl-jw-player/cxl-jw-player.scss b/packages/cxl-ui/scss/cxl-jw-player/cxl-jw-player.scss deleted file mode 100644 index 3a387c1c1..000000000 --- a/packages/cxl-ui/scss/cxl-jw-player/cxl-jw-player.scss +++ /dev/null @@ -1,8 +0,0 @@ -.jw-player-button { - width: 32px; - fill: rgba(255, 255, 255, 0.8); - - &:hover { - fill: rgba(255, 255, 255, 1); - } -} diff --git a/packages/cxl-ui/scss/cxl-jw-player/cxl-jw-player-chapter-navigation.scss b/packages/cxl-ui/scss/global/cxl-jw-player/cxl-jw-player-chapter-navigation.scss similarity index 97% rename from packages/cxl-ui/scss/cxl-jw-player/cxl-jw-player-chapter-navigation.scss rename to packages/cxl-ui/scss/global/cxl-jw-player/cxl-jw-player-chapter-navigation.scss index 83f43539d..54de81403 100644 --- a/packages/cxl-ui/scss/cxl-jw-player/cxl-jw-player-chapter-navigation.scss +++ b/packages/cxl-ui/scss/global/cxl-jw-player/cxl-jw-player-chapter-navigation.scss @@ -14,6 +14,10 @@ background: var(--lumo-shade); gap: var(--lumo-space-s); + &[hidden] { + display: none; + } + .close { font-size: var(--lumo-font-size-xs); cursor: pointer; diff --git a/packages/cxl-ui/scss/global/cxl-jw-player/cxl-jw-player-nextup.scss b/packages/cxl-ui/scss/global/cxl-jw-player/cxl-jw-player-nextup.scss new file mode 100644 index 000000000..1587b92c9 --- /dev/null +++ b/packages/cxl-ui/scss/global/cxl-jw-player/cxl-jw-player-nextup.scss @@ -0,0 +1,20 @@ +cxl-jw-player { + .jw-nextup-container { + display: flex; + flex-direction: column; + align-items: flex-end; + + .jw-nextup-cta { + max-width: 400px; + width: 64%; + + a { + pointer-events: all; + + vaadin-button { + width: 100%; + } + } + } + } +} diff --git a/packages/cxl-ui/scss/cxl-jw-player/cxl-jw-player-transcript.scss b/packages/cxl-ui/scss/global/cxl-jw-player/cxl-jw-player-transcript.scss similarity index 100% rename from packages/cxl-ui/scss/cxl-jw-player/cxl-jw-player-transcript.scss rename to packages/cxl-ui/scss/global/cxl-jw-player/cxl-jw-player-transcript.scss diff --git a/packages/cxl-ui/scss/global/cxl-jw-player/cxl-jw-player.scss b/packages/cxl-ui/scss/global/cxl-jw-player/cxl-jw-player.scss new file mode 100644 index 000000000..29e15a17d --- /dev/null +++ b/packages/cxl-ui/scss/global/cxl-jw-player/cxl-jw-player.scss @@ -0,0 +1,25 @@ +cxl-jw-player { + .jw-player-button { + width: 32px; + fill: rgba(255, 255, 255, 0.8); + + &:hover { + fill: rgba(255, 255, 255, 1); + } + } + + .jw-related-item { + height: 100% !important; /* stylelint-disable-line declaration-no-important */ + } + + .jwplayer:not(.jw-flag-small-player) .jw-related-item-next-up { + .jw-related-item-poster { + height: 100%; + } + + .jw-related-item-title { + position: absolute; + bottom: 0; + } + } +} diff --git a/packages/cxl-ui/src/components/cxl-jw-player/README.md b/packages/cxl-ui/src/components/cxl-jw-player/README.md index 96d3b7815..0baa00e76 100644 --- a/packages/cxl-ui/src/components/cxl-jw-player/README.md +++ b/packages/cxl-ui/src/components/cxl-jw-player/README.md @@ -62,13 +62,13 @@ Order is important as each mixin extends the previous one. In this case, `MixinT There are currently two methods which are important to the lifecycle of the component: -`__setup()` +`_setup()` -This method is async and called when the component is first created. You must call `await super.__setup()` at the beginning of this method to make sure each parent class's setup method is called. +This method is async and called when the component is first created. You must call `await super._setup()` at the beginning of this method to make sure each parent class's setup method is called. -`__onTimeListener()` +`_onTimeListener()` -This method is async and called when the player's time changes. As with `__setup()`, you must call `await super.__onTimeListener()` at the beginning of this method. +This method is async and called when the player's time changes. As with `_setup()`, you must call `await super._onTimeListener()` at the beginning of this method. Current mixins available for use: diff --git a/packages/cxl-ui/src/components/cxl-jw-player/cxl-jw-player-transcript/index.js b/packages/cxl-ui/src/components/cxl-jw-player/cxl-jw-player-transcript/index.js index 5bbd17f44..35a3a47c2 100644 --- a/packages/cxl-ui/src/components/cxl-jw-player/cxl-jw-player-transcript/index.js +++ b/packages/cxl-ui/src/components/cxl-jw-player/cxl-jw-player-transcript/index.js @@ -1,6 +1,6 @@ import { html, LitElement } from 'lit'; import { customElement } from 'lit/decorators.js'; -import style from '../../../styles/cxl-jw-player/cxl-jw-player-transcript-css'; +import style from '../../../styles/global/cxl-jw-player/cxl-jw-player-transcript-css'; import shadowStyle from '../../../styles/cxl-jw-player/cxl-jw-player-transcript-shadow-css'; @customElement('cxl-jw-player-transcript') @@ -13,9 +13,9 @@ export class CXLJWPlayerTranscriptElement extends LitElement { return html``; } - async __setup() { - await super.__setup(); + async _setup() { + await super._setup(); - this.__addStyle(style); + this._addStyle(style); } } diff --git a/packages/cxl-ui/src/components/cxl-jw-player/index.html.js b/packages/cxl-ui/src/components/cxl-jw-player/index.html.js index 0199e1eb3..57f840bfa 100644 --- a/packages/cxl-ui/src/components/cxl-jw-player/index.html.js +++ b/packages/cxl-ui/src/components/cxl-jw-player/index.html.js @@ -11,7 +11,7 @@ export const template = function () { ? html`
- ${this.__tracks.map( + ${this._tracks.map( (track, index) => html`${track.isChapter - ? html`

+ ? html`

${track.data.text}

` : html` ${track.data.text} diff --git a/packages/cxl-ui/src/components/cxl-jw-player/index.js b/packages/cxl-ui/src/components/cxl-jw-player/index.js index fd42ca9d0..903c10535 100644 --- a/packages/cxl-ui/src/components/cxl-jw-player/index.js +++ b/packages/cxl-ui/src/components/cxl-jw-player/index.js @@ -1,20 +1,28 @@ import { LitElement } from 'lit'; import { customElement } from 'lit/decorators.js'; -import { BaseMixin, TranscriptMixin, ChapterNavigationMixin, SavePositionMixin } from './mixins'; -import style from '../../styles/cxl-jw-player/cxl-jw-player-css'; +import style from '../../styles/global/cxl-jw-player/cxl-jw-player-css'; import shadowStyle from '../../styles/cxl-jw-player/cxl-jw-player-shadow-css'; -import { mixin } from './utility'; import { template } from './index.html'; +import { + BaseMixin, + ChapterNavigationMixin, + NextUpMixin, + StateMixin, + TranscriptMixin, +} from './mixins'; +import { mixin } from './utility'; @customElement('cxl-jw-player') export class CXLJWPlayerElement extends mixin(LitElement, [ BaseMixin, TranscriptMixin, ChapterNavigationMixin, - SavePositionMixin, + NextUpMixin, + StateMixin, ]) { config = { width: '100%', + nextupoffset: '-60', playbackRateControls: [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2], plugins: { // 'http://192.168.0.101:8080/telemetry-8.20.0.js': {}, @@ -33,9 +41,9 @@ export class CXLJWPlayerElement extends mixin(LitElement, [ return template.bind(this)(); } - async __setup() { - await super.__setup(); + async _setup() { + await super._setup(); - this.__addStyle(style); + this._addStyle(style); } } diff --git a/packages/cxl-ui/src/components/cxl-jw-player/mixins/BaseMixin.js b/packages/cxl-ui/src/components/cxl-jw-player/mixins/BaseMixin.js index 531233f97..db0ac098d 100644 --- a/packages/cxl-ui/src/components/cxl-jw-player/mixins/BaseMixin.js +++ b/packages/cxl-ui/src/components/cxl-jw-player/mixins/BaseMixin.js @@ -1,30 +1,32 @@ +import * as jose from 'jose'; import { render } from 'lit'; import { property } from 'lit/decorators.js'; import { throttle } from 'lodash-es'; import { parseSync } from 'subtitle'; +import { MD5 } from 'crypto-js'; export function BaseMixin(BaseClass) { class Mixin extends BaseClass { - __boundOnTimeListener; + _boundOnTimeListener; - __chapters; + _chapters; - __jwPlayer; + _jwPlayer; - __jwPlayerContainer; + _jwPlayerContainer; - __position; + @property({ attribute: 'api-secret', type: String }) apiSecret = ''; - @property({ attribute: 'media-id', type: String }) mediaId; - - @property({ attribute: 'media-source', type: String }) mediaSource; - - @property({ attribute: 'is-public', type: String }) isPublic; + @property({ attribute: 'is-public', type: Boolean }) isPublic; @property({ attribute: 'library-id', type: String }) libraryId; @property({ attribute: 'library-source', type: String }) librarySource; + @property({ attribute: 'media-id', type: String }) mediaId; + + @property({ attribute: 'media-source', type: String }) mediaSource; + @property({ attribute: 'playlist-id', type: String }) playlistId; @property({ attribute: 'playlist-source', type: String }) playlistSource; @@ -32,77 +34,100 @@ export function BaseMixin(BaseClass) { firstUpdated(_changedProperties) { super.firstUpdated(_changedProperties); - this.__setup(); + this._setup(); } updated(_changedProperties) { super.updated(_changedProperties); if (_changedProperties.has('captions') || _changedProperties.has('mediaId')) { - // this.__setup(); + // this._setup(); } } - get __scriptUrl() { + get _scriptUrl() { + if (!this.libraryId && !this.librarySource) return false; + let scriptUrl; - if (this.libraryId && this.isPublic) { - scriptUrl = `https://content.jwplatform.com/libraries/${this.libraryId}.js`; - } else if (this.librarySource) { + if (this.libraryId) { + if (this.isPublic) { + scriptUrl = `https://content.jwplatform.com/libraries/${this.libraryId}.js`; + } else { + scriptUrl = this.__signedURL(`libraries/${this.libraryId}.js`); + } + } + + if (this.librarySource) { scriptUrl = this.librarySource; - } else { - return false; } return scriptUrl; } - __addStyle(style) { + _addStyle(style) { const el = document.createElement('style'); render(style, el); this.appendChild(el); } - async __getChapters() { - const playlistItem = this.__jwPlayer.getPlaylistItem(); + async _getChapters() { + const playlistItem = this._jwPlayer.getPlaylistItem(); const chapters = playlistItem.tracks.filter((track) => track.kind === 'chapters'); + + if (chapters.length === 0) { + return []; + } + const { file } = chapters.length > 0 ? chapters[0] : ''; const response = await (await fetch(file)).text(); return parseSync(response); } - async __getMedia() { + async _getMedia() { + if (!this.mediaId && !this.mediaSource) return false; + let response; - if (this.mediaId && this.isPublic) { - response = await fetch(`https://cdn.jwplayer.com/v2/media/${this.mediaId}`); - } else if (this.mediaSource) { + if (this.mediaId) { + if (this.isPublic) { + response = await fetch(`https://cdn.jwplayer.com/v2/media/${this.mediaId}`); + } else { + response = await fetch(await this._signedJWTURL(`/v2/media/${this.mediaId}`)); + } + } + + if (this.mediaSource) { response = await fetch(this.mediaSource); - } else { - return false; } return response.json(); } - async __getPlaylist() { + async _getPlaylist() { + if (!this.playlistId && !this.playlistSource) return false; + let response; if (this.playlistId) { - response = await fetch(`https://cdn.jwplayer.com/v2/playlists/${this.playlistId}`); - } else if (this.playlistSource) { - response = await fetch(`https://cdn.jwplayer.com/v2/playlists/${this.playlistId}`); - } else { - return false; + if (this.isPublic) { + response = await fetch(`https://cdn.jwplayer.com/v2/playlists/${this.playlistId}`); + } else { + response = await fetch(await this._signedJWTURL(`/v2/playlists/${this.playlistId}`)); + } + } + + if (this.playlistSource) { + response = await fetch(this.playlistSource); } return response.json(); } - async __loadScript() { - return new Promise((resolve) => { + async _loadScript() { + return new Promise(async (resolve) => { const el = document.createElement('script'); - el.src = this.__scriptUrl; + el.src = this._scriptUrl; el.onload = () => { resolve(self.jwplayer); }; @@ -115,18 +140,17 @@ export function BaseMixin(BaseClass) { */ // eslint-disable-next-line class-methods-use-this, no-unused-vars, no-empty-function - async __onTimeListener(event) {} + async _onTimeListener(event) {} - __registerListeners() { - this.__boundOnTimeListener = throttle(this.__onTimeListener.bind(this), 1000); - this.__jwPlayer.on('time', this.__boundOnTimeListener); + _registerListeners() { + this._boundOnTimeListener = throttle(this._onTimeListener.bind(this), 1000); + this._jwPlayer.on('time', this._boundOnTimeListener); } /** * Each mixin has the ability to hook onto this method. */ - async __setup() { - + async _setup() { // Merge configs from `cxlJWPlayerData`. if (typeof window.cxlJWPlayerData !== 'undefined') { // eslint-disable-next-line camelcase @@ -135,26 +159,46 @@ export function BaseMixin(BaseClass) { this.config = { ...this.config, ...media_config }; } - const jwPlayer = await this.__loadScript(); + const jwPlayer = await this._loadScript(); const el = document.createElement('div'); this.appendChild(el); - this.__jwPlayer = jwPlayer(el).setup({ + this._jwPlayer = jwPlayer(el).setup({ ...this.config, - ...(await this.__getMedia()), - ...(await this.__getPlaylist()), + ...(await this._getMedia()), + ...(await this._getPlaylist()), }); await new Promise((resolve) => { - this.__jwPlayer.on('ready', resolve); + this._jwPlayer.on('ready', resolve); }); - this.__jwPlayerContainer = this.__jwPlayer.getContainer(); + this._jwPlayerContainer = this._jwPlayer.getContainer(); + + this._registerListeners(); + + this._chapters = await this._getChapters(); + } + + async _signedJWTURL(path) { + const secret = new TextEncoder().encode(this.apiSecret); + const alg = 'HS256'; + const typ = 'JWT'; + + const token = await new jose.SignJWT({ resource: path }) + .setProtectedHeader({ alg, typ }) + .setExpirationTime('2h') + .sign(secret); + + return `https://cdn.jwplayer.com${path}?token=${token}`; + } - this.__registerListeners(); + __signedURL(path) { + const expires = Math.ceil((new Date().getTime() + 3600) / 300) * 300; + const signature = MD5(`${path}:${expires}:${this.apiSecret}`); - this.__chapters = await this.__getChapters(); + return `https://cdn.jwplayer.com/${path}?exp=${expires}&sig=${signature}`; } } diff --git a/packages/cxl-ui/src/components/cxl-jw-player/mixins/NextUpMixin.js b/packages/cxl-ui/src/components/cxl-jw-player/mixins/NextUpMixin.js new file mode 100644 index 000000000..3b8797d51 --- /dev/null +++ b/packages/cxl-ui/src/components/cxl-jw-player/mixins/NextUpMixin.js @@ -0,0 +1,41 @@ +import { html, render } from 'lit'; +import style from '../../../styles/global/cxl-jw-player/cxl-jw-player-nextup-css'; +export function NextUpMixin(BaseClass) { + class Mixin extends BaseClass { + _nextUpCTA; + + async _setup() { + await super._setup(); + + this._addStyle(style); + + this._nextUpCTA = document.createElement('div'); + this._nextUpCTA.classList.add('jw-nextup-cta'); + + const container = this.querySelector('.jw-nextup-container'); + container.insertBefore(this._nextUpCTA, container.firstChild); + + this._updateNextUp(); + this._jwPlayer.on('playlistItem', this._updateNextUp.bind(this)); + } + + _updateNextUp() { + const playlistItem = this._jwPlayer.getPlaylistItem(); + + if (playlistItem && playlistItem.coursePage) { + render(this._getTemplate(playlistItem), this._nextUpCTA); + } + } + + // eslint-disable-next-line class-methods-use-this + _getTemplate(playlistItem) { + return html` + + Click here to continue this course + + `; + } + } + + return Mixin; +} diff --git a/packages/cxl-ui/src/components/cxl-jw-player/mixins/SavePositionMixin.js b/packages/cxl-ui/src/components/cxl-jw-player/mixins/SavePositionMixin.js deleted file mode 100644 index 9ea091ba6..000000000 --- a/packages/cxl-ui/src/components/cxl-jw-player/mixins/SavePositionMixin.js +++ /dev/null @@ -1,50 +0,0 @@ -export function SavePositionMixin(BaseClass) { - class Mixin extends BaseClass { - __endpoint; - - __nonce; - - __userId; - - async __setup() { - await super.__setup(); - - this.__endpoint = `${window.ajaxurl}?action=jwplayer_save_position`; - - if ( typeof window.cxl_pum_vars !== 'undefined' ) { - this.__nonce = window.cxl_pum_vars.nonce; - } - - this.__loadPosition(); - } - - async __loadPosition() { - this.__jwPlayer.seek(Number(localStorage.getItem(`jw-player-${this.mediaId}-position`))); - this.__jwPlayer.pause(); - } - - /** - * The listener is registered in the base class (../index.js). - */ - __onTimeListener(event) { - super.__onTimeListener(event); - - this.__savePosition(event); - } - - __savePosition({ position }) { - localStorage.setItem(`jw-player-${this.mediaId}-position`, position); - - fetch(this.__endpoint, { - _ajax_nonce: this.__nonce, - body: JSON.stringify({ mediaId: this.mediaId, position }), - headers: { - 'Content-Type': 'application/json', - }, - method: 'POST', - }); - } - } - - return Mixin; -} diff --git a/packages/cxl-ui/src/components/cxl-jw-player/mixins/StateMixin.js b/packages/cxl-ui/src/components/cxl-jw-player/mixins/StateMixin.js new file mode 100644 index 000000000..cb28364ed --- /dev/null +++ b/packages/cxl-ui/src/components/cxl-jw-player/mixins/StateMixin.js @@ -0,0 +1,63 @@ +export function StateMixin(BaseClass) { + class Mixin extends BaseClass { + _endpoint; + + _nonce; + + _userId; + + async _setup() { + await super._setup(); + + this._endpoint = `${window.ajaxurl}?action=jwplayer_save_position`; + + if (typeof window.cxl_pum_vars !== 'undefined') { + this._nonce = window.cxl_pum_vars.nonce; + } + + this._playbackRate(); + this._position(); + } + + _playbackRate() { + const playbackRate = localStorage.getItem(`jw-player-playback-rate`); + + if (playbackRate) { + this._jwPlayer.setPlaybackRate(Number(playbackRate)); + } + + this._jwPlayer.on('playbackRateChanged', ({ playbackRate }) => { + localStorage.setItem(`jw-player-playback-rate`, playbackRate); + }); + } + + _position() { + const mediaId = this.mediaId || this._jwPlayer.getPlaylistItem().mediaId; + + const position = localStorage.getItem(`jw-player-${mediaId}-position`); + + if (position) { + if (this.mediaId) { + this._setPosition(position); + } else { + this._jwPlayer.on('playlistItem', () => { + this._setPosition(position); + }); + + this._jwPlayer.playlistItem(this._jwPlayer.getPlaylistIndex()); + } + } + + this._jwPlayer.on('seek time', ({ position }) => { + localStorage.setItem(`jw-player-${mediaId}-position`, position); + }); + } + + _setPosition(position) { + this._jwPlayer.seek(Number(position)); + this._jwPlayer.pause(); + } + } + + return Mixin; +} diff --git a/packages/cxl-ui/src/components/cxl-jw-player/mixins/TranscriptMixin.js b/packages/cxl-ui/src/components/cxl-jw-player/mixins/TranscriptMixin.js index 9ee8d35f2..49b03c624 100644 --- a/packages/cxl-ui/src/components/cxl-jw-player/mixins/TranscriptMixin.js +++ b/packages/cxl-ui/src/components/cxl-jw-player/mixins/TranscriptMixin.js @@ -5,46 +5,52 @@ import { parseSync } from 'subtitle'; export function TranscriptMixin(BaseClass) { class Mixin extends BaseClass { - __debouncedSearch; + _debouncedSearch; - __mark; + _mark; @property({ reflect: true, type: Boolean }) captions = false; + @property({ attribute: 'has-captions', reflect: true, type: Boolean }) hasCaptions = false; - @state() __currentCue = 0; + @state() _currentCue = 0; - @state() __currentTrack = 0; + @state() _currentTrack = 0; - @state() __isSearchMinimumLength = false; + @state() _isSearchMinimumLength = false; - @state() __matches = 0; + @state() _matches = 0; @property({ attribute: 'minimum-search-length', type: Number }) minimumSearchLength = 3; @property({ type: Boolean }) shouldScroll = true; - @query('#search') __searchElement; + @query('#search') _searchElement; - @state() __searchValue; + @state() _searchValue; - @state() __tracks = []; + @state() _tracks = []; constructor() { super(); - this.__debouncedSearch = debounce(this.__search, 300); + this._debouncedSearch = debounce(this._search, 300); } - async __getCaptions() { - const playlistItem = this.__jwPlayer.getPlaylistItem(); - const { file } = playlistItem.tracks.filter((track) => track.kind === 'captions')[0]; - const response = await (await fetch(file)).text(); + async _getCaptions() { + const playlistItem = this._jwPlayer.getPlaylistItem(); + const track = playlistItem.tracks.filter((track) => track.kind === 'captions')[0]; + + if (!track) { + return []; + } + + const response = await (await fetch(track.file)).text(); return parseSync(response); } /* eslint-disable array-callback-return, class-methods-use-this, consistent-return, no-return-assign */ - __getCaptionsInChapter(chapters, captions, index) { + _getCaptionsInChapter(chapters, captions, index) { return captions.filter((caption) => { if (caption.data.start >= chapters[index].data.start) { if (chapters[index + 1]) { @@ -59,31 +65,34 @@ export function TranscriptMixin(BaseClass) { } /* eslint-enable array-callback-return, class-methods-use-this, consistent-return, no-return-assign */ - async __getTracks() { + async _getTracks() { const tracks = []; - const captions = await this.__getCaptions(); - const chapters = [...[{ data: { start: 0, text: '' } }], ...(await this.__getChapters())]; + const captions = await this._getCaptions(); - chapters.forEach((chapter, index) => { - tracks.push({ ...chapter, ...{ isChapter: true } }); - tracks.push(...this.__getCaptionsInChapter(chapters, captions, index)); - }); + if (captions.length) { + const chapters = [...[{ data: { start: 0, text: '' } }], ...(await this._getChapters())]; + + chapters.forEach((chapter, index) => { + tracks.push({ ...chapter, ...{ isChapter: true } }); + tracks.push(...this._getCaptionsInChapter(chapters, captions, index)); + }); + } return tracks; } - __onCaptionClick(e) { + _onCaptionClick(e) { const index = Number(e.currentTarget.dataset.index); - this.__jwPlayer.seek(this.__tracks[index].data.start / 1000); + this._jwPlayer.seek(this._tracks[index].data.start / 1000); } - __onTimeListener(event) { - super.__onTimeListener(event); + _onTimeListener(event) { + super._onTimeListener(event); const position = event.position * 1000; // Convert to milliseconds - this.__tracks.forEach(({ data: { end, start } }, index) => { + this._tracks.forEach(({ data: { end, start } }, index) => { if (start <= position && end >= position) { if (this.shouldScroll) { const el = this.renderRoot.querySelector(`[data-index="${index}"]`); @@ -92,51 +101,62 @@ export function TranscriptMixin(BaseClass) { } } - this.__currentTrack = index; + this._currentTrack = index; } }); } - __search() { - this.__mark.unmark(); + _search() { + this._mark.unmark(); - if (this.__searchElement.value.length >= this.minimumSearchLength) { - this.__isSearchMinimumLength = true; + if (this._searchElement.value.length >= this.minimumSearchLength) { + this._isSearchMinimumLength = true; - this.__mark.mark(this.__searchElement.value, { + this._mark.mark(this._searchElement.value, { done: (total) => { - this.__matches = total; + this._matches = total; }, separateWordSearch: false, }); } else { - this.__isSearchMinimumLength = false; + this._isSearchMinimumLength = false; } } - async __setup() { - await super.__setup(); + async _setup() { + await super._setup(); + + this._setupTranscript(); - this.__setupTranscript(); + this._jwPlayer.on('playlistItem', this._setupTranscript.bind(this)); } - async __setupTranscript() { - if (!this.__jwPlayer) return; + async _setupTranscript() { + if (!this._jwPlayer) return; - this.__jwPlayer.addButton( - ``, - 'Transcript', - this.__toggleTranscript.bind(this), - 'toggle-transcript' - ); + this._tracks = []; if (this.captions) { - this.__tracks = await this.__getTracks(); + this._tracks = await this._getTracks(); + } + + if (this._tracks.length) { + this.hasCaptions = true; // Make sure the DOM is up to date await this.updateComplete; - this.__mark = new Mark(this.renderRoot.querySelectorAll('.captions h2, .captions span')); + this._mark = new Mark(this.renderRoot.querySelectorAll('.captions h2, .captions span')); + + this._jwPlayer.addButton( + ``, + 'Transcript', + this._toggleTranscript.bind(this), + 'toggle-transcript' + ); + } else { + this.hasCaptions = false; + this._jwPlayer.removeButton('toggle-transcript'); } } @@ -145,22 +165,22 @@ export function TranscriptMixin(BaseClass) { if (changedProperties.has('captions')) { if (this.captions) { - this.__setupTranscript(); + this._setupTranscript(); } else if (this.mark) { - this.__mark.unmark(); + this._mark.unmark(); } } } - __attachListeners() { - super.__attachListeners(); + _attachListeners() { + super._attachListeners(); } - __toggleShouldScroll() { + _toggleShouldScroll() { this.shouldScroll = !this.shouldScroll; } - __toggleTranscript() { + _toggleTranscript() { // this.dispatchEvent(new CustomEvent('toggle-transcript')); this.captions = !this.captions; diff --git a/packages/cxl-ui/src/components/cxl-jw-player/mixins/chapter-navigation/index.html.js b/packages/cxl-ui/src/components/cxl-jw-player/mixins/chapter-navigation/index.html.js index 6c1cb1f4d..cb89afd14 100644 --- a/packages/cxl-ui/src/components/cxl-jw-player/mixins/chapter-navigation/index.html.js +++ b/packages/cxl-ui/src/components/cxl-jw-player/mixins/chapter-navigation/index.html.js @@ -8,7 +8,7 @@ export const chapterNavigationTemplate = function (chapters) {
Chapters html` -
  • +
  • ${chapter.data.text}
  • ` diff --git a/packages/cxl-ui/src/components/cxl-jw-player/mixins/chapter-navigation/index.js b/packages/cxl-ui/src/components/cxl-jw-player/mixins/chapter-navigation/index.js index 2ab998723..4e9df2514 100644 --- a/packages/cxl-ui/src/components/cxl-jw-player/mixins/chapter-navigation/index.js +++ b/packages/cxl-ui/src/components/cxl-jw-player/mixins/chapter-navigation/index.js @@ -1,45 +1,51 @@ import { render } from 'lit'; import { property } from 'lit/decorators.js'; -import style from '../../../../styles/cxl-jw-player/cxl-jw-player-chapter-navigation-css'; +import style from '../../../../styles/global/cxl-jw-player/cxl-jw-player-chapter-navigation-css'; import { chapterNavigationTemplate } from './index.html'; export function ChapterNavigationMixin(BaseClass) { class Mixin extends BaseClass { @property({ attribute: 'plugin-path', type: String }) pluginPath; - async __setupChapterNavigation() { - const chapters = await this.__getChapters(); + async _setupChapterNavigation() { + const chapters = await this._getChapters(); - this.__chapterNavigation = document.createElement('div'); - this.__chapterNavigation.classList.add('chapter-navigation'); - this.__chapterNavigation.hidden = true; + if (!chapters.length) { + this._jwPlayer.removeButton('chapter-navigation'); - render(chapterNavigationTemplate.bind(this)(chapters), this.__chapterNavigation); + return; + } - this.__jwPlayerContainer.appendChild(this.__chapterNavigation); + this._chapterNavigation = document.createElement('div'); + this._chapterNavigation.classList.add('chapter-navigation'); + this._chapterNavigation.hidden = true; - this.__jwPlayer.addButton( + render(chapterNavigationTemplate.bind(this)(chapters), this._chapterNavigation); + + this._jwPlayerContainer.appendChild(this._chapterNavigation); + + this._jwPlayer.addButton( ``, 'Show Chapters', - this.__toggleChapterNavigation.bind(this), + this._toggleChapterNavigation.bind(this), 'toggle-chapters' ); } - __chapterSeek(e) { + _chapterSeek(e) { const index = Number(e.currentTarget.dataset.index); - this.__jwPlayer.seek(this.__chapters[index].data.start / 1000); + this._jwPlayer.seek(this._chapters[index].data.start / 1000); } - async __setup() { - await super.__setup(); + async _setup() { + await super._setup(); - this.__addStyle(style); - this.__setupChapterNavigation(); + this._addStyle(style); + this._setupChapterNavigation(); } - __toggleChapterNavigation() { - this.__chapterNavigation.hidden = !this.__chapterNavigation.hidden; + _toggleChapterNavigation() { + this._chapterNavigation.hidden = !this._chapterNavigation.hidden; } } diff --git a/packages/cxl-ui/src/components/cxl-jw-player/mixins/index.js b/packages/cxl-ui/src/components/cxl-jw-player/mixins/index.js index 280a40e71..ba2613c5a 100644 --- a/packages/cxl-ui/src/components/cxl-jw-player/mixins/index.js +++ b/packages/cxl-ui/src/components/cxl-jw-player/mixins/index.js @@ -1,4 +1,5 @@ export { BaseMixin } from './BaseMixin'; +export { ChapterNavigationMixin } from './chapter-navigation/index.js'; +export { NextUpMixin } from './NextUpMixin'; +export { StateMixin } from './StateMixin'; export { TranscriptMixin } from './TranscriptMixin'; -export { ChapterNavigationMixin } from './chapter-navigation'; -export { SavePositionMixin } from './SavePositionMixin'; diff --git a/packages/cxl-ui/src/styles/global/cxl-jw-player/.gitignore b/packages/cxl-ui/src/styles/global/cxl-jw-player/.gitignore new file mode 100644 index 000000000..f935021a8 --- /dev/null +++ b/packages/cxl-ui/src/styles/global/cxl-jw-player/.gitignore @@ -0,0 +1 @@ +!.gitignore diff --git a/packages/storybook/cxl-ui/cxl-jw-player/index.stories.js b/packages/storybook/cxl-ui/cxl-jw-player/index.stories.js index c9adc7c12..60b129bc8 100644 --- a/packages/storybook/cxl-ui/cxl-jw-player/index.stories.js +++ b/packages/storybook/cxl-ui/cxl-jw-player/index.stories.js @@ -5,7 +5,19 @@ export default { title: 'CXL UI/cxl-jw-player', }; -const Template = ({ captions, mediaId, mediaSource, minimumSearchLength, libraryId, librarySource, playlistId, playlistSource, pluginPath }) => +const Template = ({ + apiSecret, + captions, + isPublic, + libraryId, + librarySource, + mediaId, + mediaSource, + minimumSearchLength, + playlistId, + playlistSource, + pluginPath, +}) => html` + +const Template = ({ + apiSecret, + captions, + isPublic, + libraryId, + librarySource, + mediaId, + mediaSource, + minimumSearchLength, + playlistId, + playlistSource, + pluginPath, +}) => html` + `; @@ -36,10 +53,16 @@ export const Playlist = Template.bind({}); Object.assign(Playlist, { args: { - captions: false, + apiSecret: '', + captions: true, + isPublic: true, + libraryId: '5CFJNXKb', + librarySource: '', mediaId: '', - playerId: '5CFJNXKb', + mediaSource: '', + minimumSearchLength: 3, playlistId: 'tAxwbNsA', + playlistSource: '', pluginPath: 'https://cxl.com/institute/wp-content/plugins/cxl-jwplayer/', }, }); diff --git a/yarn.lock b/yarn.lock index b3ac10475..08b0f8282 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7127,6 +7127,11 @@ crypto-browserify@^3.11.0: randombytes "^2.0.0" randomfill "^1.0.3" +crypto-js@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf" + integrity sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw== + crypto-random-string@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" @@ -11213,6 +11218,11 @@ jest-worker@^27.4.5: merge-stream "^2.0.0" supports-color "^8.0.0" +jose@^4.13.1: + version "4.13.1" + resolved "https://registry.yarnpkg.com/jose/-/jose-4.13.1.tgz#449111bb5ab171db85c03f1bd2cb1647ca06db1c" + integrity sha512-MSJQC5vXco5Br38mzaQKiq9mwt7lwj2eXpgpRyQYNHYt2lq1PjkWa7DLXX0WVcQLE9HhMh3jPiufS7fhJf+CLQ== + js-string-escape@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef"