From cbe1aa9387efa0dfdf57e612b29dd346dfeea765 Mon Sep 17 00:00:00 2001 From: Yoriiis Date: Wed, 3 Aug 2022 01:03:01 +0200 Subject: [PATCH 01/18] Chromecast integration --- config/webpack.config.js | 5 + examples/html5/config.js | 28 ++-- examples/html5/index.html | 2 + src/plugins/chromecast/config.js | 4 + src/plugins/chromecast/js/chromecast.js | 201 ++++++++++++++++++++++++ 5 files changed, 227 insertions(+), 13 deletions(-) create mode 100644 src/plugins/chromecast/config.js create mode 100644 src/plugins/chromecast/js/chromecast.js diff --git a/config/webpack.config.js b/config/webpack.config.js index ea1a780d..fcd74b36 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -46,6 +46,11 @@ const plugins = [ entrykey: 'plugins/pip', library: `${libraryName}Pip`, path: './src/plugins/pip/config' + }, + { + entrykey: 'plugins/chromecast', + library: `${libraryName}Chromecast`, + path: './src/plugins/chromecast/config' } ] diff --git a/examples/html5/config.js b/examples/html5/config.js index fc08ff5a..261c6873 100644 --- a/examples/html5/config.js +++ b/examples/html5/config.js @@ -3,9 +3,11 @@ import '../../dist/plugins/subtitle.css' import Vlitejs from '../../dist/vlite.js' import VlitejsSubtitle from '../../dist/plugins/subtitle.js' import VlitejsPip from '../../dist/plugins/pip.js' +import VlitejsChromecast from '../../dist/plugins/chromecast.js' Vlitejs.registerPlugin('subtitle', VlitejsSubtitle) Vlitejs.registerPlugin('pip', VlitejsPip) +Vlitejs.registerPlugin('chromecast', VlitejsChromecast) /* eslint-disable no-unused-vars */ const vlite = new Vlitejs('#player', { @@ -24,22 +26,22 @@ const vlite = new Vlitejs('#player', { muted: false, autoHide: true }, - plugins: ['subtitle', 'pip'], + plugins: ['subtitle', 'pip', 'chromecast'], onReady: function (player) { console.log(player) - player.on('play', () => console.log('play')) - player.on('pause', () => console.log('pause')) - player.on('progress', () => console.log('progress')) - player.on('timeupdate', () => console.log('timeupdate')) - player.on('volumechange', () => console.log('volumechange')) - player.on('enterfullscreen', () => console.log('enterfullscreen')) - player.on('exitfullscreen', () => console.log('exitfullscreen')) - player.on('enterpip', () => console.log('enterpip')) - player.on('leavepip', () => console.log('leavepip')) - player.on('trackenabled', () => console.log('trackenabled')) - player.on('trackdisabled', () => console.log('trackdisabled')) - player.on('ended', () => console.log('ended')) + // player.on('play', () => console.log('play')) + // player.on('pause', () => console.log('pause')) + // player.on('progress', () => console.log('progress')) + // player.on('timeupdate', () => console.log('timeupdate')) + // player.on('volumechange', () => console.log('volumechange')) + // player.on('enterfullscreen', () => console.log('enterfullscreen')) + // player.on('exitfullscreen', () => console.log('exitfullscreen')) + // player.on('enterpip', () => console.log('enterpip')) + // player.on('leavepip', () => console.log('leavepip')) + // player.on('trackenabled', () => console.log('trackenabled')) + // player.on('trackdisabled', () => console.log('trackdisabled')) + // player.on('ended', () => console.log('ended')) } }) /* eslint-enable no-unused-vars */ diff --git a/examples/html5/index.html b/examples/html5/index.html index 41bde5b6..54a8066f 100644 --- a/examples/html5/index.html +++ b/examples/html5/index.html @@ -9,5 +9,7 @@ + diff --git a/src/plugins/chromecast/config.js b/src/plugins/chromecast/config.js new file mode 100644 index 00000000..d6da9f30 --- /dev/null +++ b/src/plugins/chromecast/config.js @@ -0,0 +1,4 @@ +// import JS +import chromecast from './js/chromecast' + +export default chromecast diff --git a/src/plugins/chromecast/js/chromecast.js b/src/plugins/chromecast/js/chromecast.js new file mode 100644 index 00000000..49d8bf1d --- /dev/null +++ b/src/plugins/chromecast/js/chromecast.js @@ -0,0 +1,201 @@ +/** + * Vlitejs Chromecast plugin + * @module Vlitejs/plugins/chromecast + */ +export default class ChromecastPlugin { + providers = ['html5'] + types = ['video'] + + /** + * @constructor + * @param {Object} options + * @param {Class} options.player Player instance + */ + constructor({ player }) { + this.player = player + + this.sessionListener = this.sessionListener.bind(this) + this.receiverListener = this.receiverListener.bind(this) + this.onInitializeSuccess = this.onInitializeSuccess.bind(this) + this.onInitializeError = this.onInitializeError.bind(this) + this.onStopCastSuccess = this.onStopCastSuccess.bind(this) + this.onStopCastError = this.onStopCastError.bind(this) + this.onRequestSessionInitializeSuccess = this.onRequestSessionInitializeSuccess.bind(this) + this.onRequestSessionError = this.onRequestSessionError.bind(this) + this.onLoadMediaSuccess = this.onLoadMediaSuccess.bind(this) + this.onLoadMediaError = this.onLoadMediaError.bind(this) + } + + /** + * Initialize the plugin + */ + init() { + document.createElement('google-cast-launcher') + window.__onGCastApiAvailable = (isAvailable) => isAvailable && this.initCastApi() + + this.loadWebSenderApi() + } + + loadWebSenderApi() { + const script = document.createElement('script') + script.defer = true + script.type = 'text/javascript' + script.src = '//www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1' + document.getElementsByTagName('body')[0].appendChild(script) + } + + initCastApi() { + const applicationId = chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID + // const sessionRequest = new chrome.cast.SessionRequest(applicationId) + // const apiConfig = new chrome.cast.ApiConfig( + // sessionRequest, + // this.sessionListener, + // this.receiverListener + // ) + // chrome.cast.initialize(apiConfig, this.onInitializeSuccess, this.onInitializeError) + + this.render() + + const context = cast.framework.CastContext.getInstance() + // console.log(context) + context.addEventListener(cast.framework.CastContextEventType.SESSION_STATE_CHANGED, (e) => { + console.log(e) + + switch (e.sessionState) { + case cast.framework.SessionState.SESSION_STARTED: + case cast.framework.SessionState.SESSION_RESUMED: + this.loadMedia() + break + } + }) + + context.setOptions({ + receiverApplicationId: applicationId, + autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED + }) + + // console.log(context.getCurrentSession()) + + // const mediaSrc = this.player.media.src + // const contentType = this.player.type === 'video' ? 'video/mp4' : '' + // const mediaInfo = new chrome.cast.media.MediaInfo(mediaSrc, contentType) + // const request = new chrome.cast.media.LoadRequest(mediaInfo) + // this.getSession() + // .loadMedia(request) + // .then( + // function () { + // console.log('Load succeed') + // }, + // function (errorCode) { + // console.log('Error code: ' + errorCode) + // } + // ) + } + + render() { + const controlBar = this.player.elements.container.querySelector('.v-controlBar') + const fullscreenButton = this.player.elements.container.querySelector('.v-fullscreenButton') + const castButton = document.createElement('google-cast-launcher') + castButton.classList.add('v-controlButton') + if (controlBar) { + if (fullscreenButton) { + fullscreenButton.insertAdjacentElement('beforebegin', castButton) + } else { + controlBar.insertAdjacentElement('beforeend', castButton) + } + } + } + + getSession() { + return cast.framework.CastContext.getInstance().getCurrentSession() + } + + // sessionListener(e) { + // this.session = e + // } + + // receiverListener(e) {} + + // onInitializeSuccess(e) { + // console.log('onInitializeSuccess', e) + // this.addEvents() + // } + + // onInitializeError() {} + + // addEvents() { + // document.querySelector('.cast-start').addEventListener('click', () => { + // this.startCast() + // }) + + // document.querySelector('.cast-stop').addEventListener('click', () => { + // this.stopCast() + // }) + // } + + // startCast() { + // chrome.cast.requestSession( + // this.onRequestSessionInitializeSuccess, + // this.onRequestSessionError + // ) + // } + + // onRequestSessionInitializeSuccess(e) { + // console.log('onRequestSessionInitializeSuccess', e) + // this.session = e + // this.session && this.loadMedia() + // } + + // onRequestSessionError(e) { + // console.log('onRequestSessionError', e) + // } + + // stopCast() { + // this.session.stop(this.onStopCastSuccess, this.onStopCastError) + // } + + // onStopCastSuccess(e) { + // console.log('onStopCastSuccess', e) + // } + + // onStopCastError(e) { + // console.log('onStopCastError', e) + // } + + loadMedia() { + const session = this.getSession() + if (!session) { + return + } + + const mediaSrc = this.player.media.src + const contentType = this.player.type === 'video' ? 'video/mp4' : '' + const mediaInfo = new chrome.cast.media.MediaInfo(mediaSrc, contentType) + mediaInfo.contentType = this.player.type === 'video' ? 'video/mp4' : '' + const request = new chrome.cast.media.LoadRequest(mediaInfo) + session.loadMedia(request).then(this.onLoadMediaSuccess, this.onLoadMediaError) + + this.player.plugins.subtitle && this.addTracks() + } + + addTracks() { + this.player.plugins.subtitle.tracks.forEach((track) => { + const castTrack = new chrome.cast.media.Track(1, chrome.cast.media.TrackType.TEXT) + castTrack.trackContentId = + 'https://yoriiis.github.io/cdn/static/vlitejs/demo-video-html5-subtitle-en.vtt' + castTrack.trackContentType = 'text/vtt' + castTrack.subtype = chrome.cast.media.TextTrackType.SUBTITLES + castTrack.name = track.label + castTrack.language = track.language + console.log(castTrack) + }) + } + + onLoadMediaSuccess(e) { + console.log('onLoadMediaSuccess', e) + } + + onLoadMediaError(e) { + console.log('onLoadMediaError', e) + } +} From aa4bcd14c562843d90e4e58001913e548ff05922 Mon Sep 17 00:00:00 2001 From: Yoriiis Date: Fri, 5 Aug 2022 21:20:02 +0200 Subject: [PATCH 02/18] Trigger volumechange event on toggle volume --- src/vlite/components/control-bar/assets/scripts/control-bar.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vlite/components/control-bar/assets/scripts/control-bar.ts b/src/vlite/components/control-bar/assets/scripts/control-bar.ts index 1530b831..ff785831 100644 --- a/src/vlite/components/control-bar/assets/scripts/control-bar.ts +++ b/src/vlite/components/control-bar/assets/scripts/control-bar.ts @@ -191,6 +191,8 @@ export default class ControlBar { this.player.elements.volume!.classList.contains('v-controlPressed') ? this.player.unMute() : this.player.mute() + + this.player.dispatchEvent('volumechange') } /** From 98325460b18933c5184389c941c74c942d68f79c Mon Sep 17 00:00:00 2001 From: Yoriiis Date: Fri, 5 Aug 2022 21:20:38 +0200 Subject: [PATCH 03/18] Update svg to remove white space and set sizes --- README.md | 2 -- src/shared/assets/svgs/big-play.svg | 2 +- src/shared/assets/svgs/check.svg | 2 +- src/shared/assets/svgs/fullscreen-exit.svg | 2 +- src/shared/assets/svgs/fullscreen.svg | 2 +- src/shared/assets/svgs/pause.svg | 2 +- src/shared/assets/svgs/pip.svg | 2 +- src/shared/assets/svgs/play.svg | 2 +- src/shared/assets/svgs/subtitle-off.svg | 2 +- src/shared/assets/svgs/subtitle-on.svg | 2 +- src/shared/assets/svgs/volume-high.svg | 2 +- src/shared/assets/svgs/volume-mute.svg | 2 +- src/vlite/assets/styles/vlite.css | 36 +++++++++++++++++++--- 13 files changed, 43 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index f15b8860..c6818123 100644 --- a/README.md +++ b/README.md @@ -319,8 +319,6 @@ The player exposes some custom CSS properties, locally scopped under the `.v-vli | `--vlite-controlBarBackground` | `linear-gradient(to top, #000 -50%, transparent)` | Control bar background | | `--vlite-controlsColor` | `#fff\|#000` | Controls color (video\|audio) | | `--vlite-controlsOpacity` | `0.9` | Controls opacity | -| `--vlite-controlsIconWidth` | `28px` | Controls icon width | -| `--vlite-controlsIconHeight` | `28px` | Controls icon height | | `--vlite-progressBarHeight` | `5px` | Progress bar height | | `--vlite-progressBarBackground` | `rgba(0 0 0 / 25%)` | Progress bar background | diff --git a/src/shared/assets/svgs/big-play.svg b/src/shared/assets/svgs/big-play.svg index 35451236..26f50554 100755 --- a/src/shared/assets/svgs/big-play.svg +++ b/src/shared/assets/svgs/big-play.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/shared/assets/svgs/check.svg b/src/shared/assets/svgs/check.svg index 2023b78a..11c3827a 100644 --- a/src/shared/assets/svgs/check.svg +++ b/src/shared/assets/svgs/check.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/shared/assets/svgs/fullscreen-exit.svg b/src/shared/assets/svgs/fullscreen-exit.svg index a7a2e32c..fb3d217d 100755 --- a/src/shared/assets/svgs/fullscreen-exit.svg +++ b/src/shared/assets/svgs/fullscreen-exit.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/shared/assets/svgs/fullscreen.svg b/src/shared/assets/svgs/fullscreen.svg index 569cd6bd..57cbb70b 100755 --- a/src/shared/assets/svgs/fullscreen.svg +++ b/src/shared/assets/svgs/fullscreen.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/shared/assets/svgs/pause.svg b/src/shared/assets/svgs/pause.svg index 1921c90c..29fa25cc 100755 --- a/src/shared/assets/svgs/pause.svg +++ b/src/shared/assets/svgs/pause.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/shared/assets/svgs/pip.svg b/src/shared/assets/svgs/pip.svg index 5dac068a..8d9a1801 100644 --- a/src/shared/assets/svgs/pip.svg +++ b/src/shared/assets/svgs/pip.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/shared/assets/svgs/play.svg b/src/shared/assets/svgs/play.svg index 818addf1..2dd62974 100755 --- a/src/shared/assets/svgs/play.svg +++ b/src/shared/assets/svgs/play.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/shared/assets/svgs/subtitle-off.svg b/src/shared/assets/svgs/subtitle-off.svg index deb588c3..24b2e457 100644 --- a/src/shared/assets/svgs/subtitle-off.svg +++ b/src/shared/assets/svgs/subtitle-off.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/shared/assets/svgs/subtitle-on.svg b/src/shared/assets/svgs/subtitle-on.svg index 3524ea95..a3be516f 100644 --- a/src/shared/assets/svgs/subtitle-on.svg +++ b/src/shared/assets/svgs/subtitle-on.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/shared/assets/svgs/volume-high.svg b/src/shared/assets/svgs/volume-high.svg index 17eef5d8..dcbbe89f 100755 --- a/src/shared/assets/svgs/volume-high.svg +++ b/src/shared/assets/svgs/volume-high.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/shared/assets/svgs/volume-mute.svg b/src/shared/assets/svgs/volume-mute.svg index 72d92b63..f7df5056 100755 --- a/src/shared/assets/svgs/volume-mute.svg +++ b/src/shared/assets/svgs/volume-mute.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/vlite/assets/styles/vlite.css b/src/vlite/assets/styles/vlite.css index d02e6d84..7bed9911 100644 --- a/src/vlite/assets/styles/vlite.css +++ b/src/vlite/assets/styles/vlite.css @@ -10,8 +10,6 @@ /* Controls */ --vlite-controlsColor: #fff; --vlite-controlsOpacity: 0.9; - --vlite-controlsIconWidth: 28px; - --vlite-controlsIconHeight: 28px; /* Progress bar */ --vlite-progressBarHeight: 5px; @@ -143,8 +141,38 @@ svg { fill: var(--vlite-controlsColor); - width: var(--vlite-controlsIconWidth); - height: var(--vlite-controlsIconHeight); + width: 28px; + height: 28px; + } + + &.v-playPauseButton svg { + width: 15px; + height: 17px; + } + + &.v-subtitleButton svg { + width: 26px; + height: 22px; + } + + &.v-volumeButton svg { + width: 24px; + height: 20px; + } + + &.v-pipButton svg { + width: 26px; + height: 22px; + } + + &.v-castButton svg { + width: 26px; + height: 22px; + } + + &.v-fullscreenButton svg { + width: 20px; + height: 20px; } } } From d3c7fcdc243c8f16fc9efa374f09e815cf7021ba Mon Sep 17 00:00:00 2001 From: Yoriiis Date: Fri, 5 Aug 2022 21:21:27 +0200 Subject: [PATCH 04/18] Update webpack config with remote access during development --- examples/webpack.config.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/webpack.config.js b/examples/webpack.config.js index 9d63dc2b..6d627d14 100644 --- a/examples/webpack.config.js +++ b/examples/webpack.config.js @@ -69,7 +69,9 @@ module.exports = (env, argv) => { historyApiFallback: true, port: 3000, compress: true, - hot: true + hot: true, + host: '0.0.0.0', + https: true }, context: appDirectory, plugins: [ From 2734283fb69b92d24d12eac2ba4b395c32bd353b Mon Sep 17 00:00:00 2001 From: Yoriiis Date: Fri, 5 Aug 2022 21:21:54 +0200 Subject: [PATCH 05/18] Update chromecast --- config/.eslintrc.js | 6 +- examples/html5/config.js | 2 + examples/html5/index.html | 2 - src/plugins/chromecast/config.js | 3 + src/plugins/chromecast/css/chromecast.css | 14 ++ src/plugins/chromecast/js/chromecast.js | 222 +++++++++--------- src/providers/html5/html5.ts | 10 +- src/shared/assets/svgs/cast.svg | 1 + src/vlite/assets/scripts/player.ts | 17 +- src/vlite/assets/scripts/vlite.ts | 2 +- .../big-play/assets/styles/big-play.css | 4 +- 11 files changed, 160 insertions(+), 123 deletions(-) create mode 100644 src/plugins/chromecast/css/chromecast.css create mode 100644 src/shared/assets/svgs/cast.svg diff --git a/config/.eslintrc.js b/config/.eslintrc.js index 3a2d09a8..cffab7e9 100644 --- a/config/.eslintrc.js +++ b/config/.eslintrc.js @@ -20,7 +20,7 @@ module.exports = { plugins: ['prettier'], rules: { - indent: ['error', 'tab', { ignoredNodes: ['TemplateLiteral *'] }], + indent: ['error', 'tab', { ignoredNodes: ['TemplateLiteral *'], SwitchCase: 1 }], 'no-tabs': 0, 'space-before-function-paren': [ 'error', @@ -31,6 +31,8 @@ module.exports = { globals: { document: false, navigator: false, - window: false + window: false, + chrome: false, + cast: false } } diff --git a/examples/html5/config.js b/examples/html5/config.js index 261c6873..076c71c7 100644 --- a/examples/html5/config.js +++ b/examples/html5/config.js @@ -1,5 +1,6 @@ import '../../dist/vlite.css' import '../../dist/plugins/subtitle.css' +import '../../dist/plugins/chromecast.css' import Vlitejs from '../../dist/vlite.js' import VlitejsSubtitle from '../../dist/plugins/subtitle.js' import VlitejsPip from '../../dist/plugins/pip.js' @@ -44,4 +45,5 @@ const vlite = new Vlitejs('#player', { // player.on('ended', () => console.log('ended')) } }) +window.player = vlite /* eslint-enable no-unused-vars */ diff --git a/examples/html5/index.html b/examples/html5/index.html index 54a8066f..41bde5b6 100644 --- a/examples/html5/index.html +++ b/examples/html5/index.html @@ -9,7 +9,5 @@ - diff --git a/src/plugins/chromecast/config.js b/src/plugins/chromecast/config.js index d6da9f30..627c7264 100644 --- a/src/plugins/chromecast/config.js +++ b/src/plugins/chromecast/config.js @@ -1,3 +1,6 @@ +// import CSS +import './css/chromecast.css' + // import JS import chromecast from './js/chromecast' diff --git a/src/plugins/chromecast/css/chromecast.css b/src/plugins/chromecast/css/chromecast.css new file mode 100644 index 00000000..a8f4a329 --- /dev/null +++ b/src/plugins/chromecast/css/chromecast.css @@ -0,0 +1,14 @@ +.v { + &-castButton { + &.active svg { + fill: #3ea6ff; + } + } + + &-remote { + .v-pipButton, + .v-fullscreenButton { + display: none; + } + } +} diff --git a/src/plugins/chromecast/js/chromecast.js b/src/plugins/chromecast/js/chromecast.js index 49d8bf1d..880011e2 100644 --- a/src/plugins/chromecast/js/chromecast.js +++ b/src/plugins/chromecast/js/chromecast.js @@ -1,3 +1,5 @@ +import svgCast from 'shared/assets/svgs/cast.svg' + /** * Vlitejs Chromecast plugin * @module Vlitejs/plugins/chromecast @@ -14,25 +16,17 @@ export default class ChromecastPlugin { constructor({ player }) { this.player = player - this.sessionListener = this.sessionListener.bind(this) - this.receiverListener = this.receiverListener.bind(this) - this.onInitializeSuccess = this.onInitializeSuccess.bind(this) - this.onInitializeError = this.onInitializeError.bind(this) - this.onStopCastSuccess = this.onStopCastSuccess.bind(this) - this.onStopCastError = this.onStopCastError.bind(this) - this.onRequestSessionInitializeSuccess = this.onRequestSessionInitializeSuccess.bind(this) - this.onRequestSessionError = this.onRequestSessionError.bind(this) this.onLoadMediaSuccess = this.onLoadMediaSuccess.bind(this) this.onLoadMediaError = this.onLoadMediaError.bind(this) + this.onCastStateChange = this.onCastStateChange.bind(this) + this.onClickOnCastButton = this.onClickOnCastButton.bind(this) } /** * Initialize the plugin */ init() { - document.createElement('google-cast-launcher') window.__onGCastApiAvailable = (isAvailable) => isAvailable && this.initCastApi() - this.loadWebSenderApi() } @@ -45,137 +39,115 @@ export default class ChromecastPlugin { } initCastApi() { - const applicationId = chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID - // const sessionRequest = new chrome.cast.SessionRequest(applicationId) - // const apiConfig = new chrome.cast.ApiConfig( - // sessionRequest, - // this.sessionListener, - // this.receiverListener - // ) - // chrome.cast.initialize(apiConfig, this.onInitializeSuccess, this.onInitializeError) - this.render() + this.castButton = this.player.elements.container.querySelector('.v-castButton') - const context = cast.framework.CastContext.getInstance() - // console.log(context) - context.addEventListener(cast.framework.CastContextEventType.SESSION_STATE_CHANGED, (e) => { - console.log(e) - - switch (e.sessionState) { - case cast.framework.SessionState.SESSION_STARTED: - case cast.framework.SessionState.SESSION_RESUMED: - this.loadMedia() - break - } - }) - - context.setOptions({ - receiverApplicationId: applicationId, + this.getCastInstance().setOptions({ + receiverApplicationId: chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID, autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED }) - - // console.log(context.getCurrentSession()) - - // const mediaSrc = this.player.media.src - // const contentType = this.player.type === 'video' ? 'video/mp4' : '' - // const mediaInfo = new chrome.cast.media.MediaInfo(mediaSrc, contentType) - // const request = new chrome.cast.media.LoadRequest(mediaInfo) - // this.getSession() - // .loadMedia(request) - // .then( - // function () { - // console.log('Load succeed') - // }, - // function (errorCode) { - // console.log('Error code: ' + errorCode) - // } - // ) + this.addEvents() } render() { const controlBar = this.player.elements.container.querySelector('.v-controlBar') const fullscreenButton = this.player.elements.container.querySelector('.v-fullscreenButton') - const castButton = document.createElement('google-cast-launcher') - castButton.classList.add('v-controlButton') + const template = `` if (controlBar) { if (fullscreenButton) { - fullscreenButton.insertAdjacentElement('beforebegin', castButton) + fullscreenButton.insertAdjacentHTML('beforebegin', template) } else { - controlBar.insertAdjacentElement('beforeend', castButton) + controlBar.insertAdjacentHTML('beforeend', template) } } } getSession() { - return cast.framework.CastContext.getInstance().getCurrentSession() + return this.getCastInstance().getCurrentSession() } - // sessionListener(e) { - // this.session = e - // } - - // receiverListener(e) {} - - // onInitializeSuccess(e) { - // console.log('onInitializeSuccess', e) - // this.addEvents() - // } - - // onInitializeError() {} + addEvents() { + this.castButton.addEventListener('click', this.onClickOnCastButton) + this.getCastInstance().addEventListener( + cast.framework.CastContextEventType.SESSION_STATE_CHANGED, + this.onCastStateChange + ) + } - // addEvents() { - // document.querySelector('.cast-start').addEventListener('click', () => { - // this.startCast() - // }) + onCastStateChange(e) { + // console.log('onCastStateChange', e) + + switch (e.sessionState) { + case cast.framework.SessionState.SESSION_STARTED: + this.remotePlayerStart() + break + case cast.framework.SessionState.SESSION_RESUMED: + this.remotePlayerResume() + break + case cast.framework.SessionState.SESSION_ENDED: + this.remotePlayerStop() + break + } + } - // document.querySelector('.cast-stop').addEventListener('click', () => { - // this.stopCast() - // }) - // } + remotePlayerStart() { + this.backupAutoHide = this.player.Vlitejs.autoHideGranted + this.player.Vlitejs.autoHideGranted = false + this.player.Vlitejs.stopAutoHideTimer() + this.player.elements.container.classList.add('v-remote') + this.loadMedia() + } - // startCast() { - // chrome.cast.requestSession( - // this.onRequestSessionInitializeSuccess, - // this.onRequestSessionError - // ) - // } + remotePlayerResume() { + this.player.play() + this.remotePlayerStart() + } - // onRequestSessionInitializeSuccess(e) { - // console.log('onRequestSessionInitializeSuccess', e) - // this.session = e - // this.session && this.loadMedia() - // } + remotePlayerStop() { + this.player.Vlitejs.autoHideGranted = this.backupAutoHide + if (this.backupAutoHide) { + this.player.Vlitejs.startAutoHideTimer() + } + this.castButton.classList.remove('active') + this.player.elements.container.classList.remove('v-remote') + this.player.isChromecast = false - // onRequestSessionError(e) { - // console.log('onRequestSessionError', e) - // } + if (!this.player.isPaused) { + this.player.methodPlay() + this.player.Vlitejs.startAutoHideTimer() + } + } - // stopCast() { - // this.session.stop(this.onStopCastSuccess, this.onStopCastError) - // } + onClickOnCastButton(e) { + e.preventDefault() - // onStopCastSuccess(e) { - // console.log('onStopCastSuccess', e) - // } + const session = this.getSession() + if (session) { + this.getCastInstance().endCurrentSession() + } else { + this.getCastInstance() + .requestSession() + .then(this.onRequestSessionInitializeSuccess, this.onRequestSessionError) + } + } - // onStopCastError(e) { - // console.log('onStopCastError', e) - // } + getCastInstance() { + return cast.framework.CastContext.getInstance() + } loadMedia() { - const session = this.getSession() - if (!session) { - return - } + const session = this.session || this.getSession() + if (!session) return const mediaSrc = this.player.media.src const contentType = this.player.type === 'video' ? 'video/mp4' : '' const mediaInfo = new chrome.cast.media.MediaInfo(mediaSrc, contentType) - mediaInfo.contentType = this.player.type === 'video' ? 'video/mp4' : '' const request = new chrome.cast.media.LoadRequest(mediaInfo) + request.autoplay = !this.player.isPaused + request.currentTime = this.player.media.currentTime session.loadMedia(request).then(this.onLoadMediaSuccess, this.onLoadMediaError) - this.player.plugins.subtitle && this.addTracks() + // this.player.plugins.subtitle && this.addTracks() } addTracks() { @@ -192,10 +164,46 @@ export default class ChromecastPlugin { } onLoadMediaSuccess(e) { - console.log('onLoadMediaSuccess', e) + // console.log('onLoadMediaSuccess', e) + + if (!this.player.isPaused) { + this.player.methodPause() + } + + this.remotePlayer = new cast.framework.RemotePlayer() + this.remotePlayerController = new cast.framework.RemotePlayerController(this.remotePlayer) + + this.player.on('play', () => { + this.remotePlayerController.playOrPause() + }) + this.player.on('pause', () => { + this.remotePlayerController.playOrPause() + }) + this.player.on('volumechange', () => { + this.player.getVolume().then((volume) => { + this.remotePlayer.volumeLevel = this.player.isMuted ? 0 : volume + this.remotePlayerController.setVolumeLevel() + }) + }) + + this.player.on('timeupdate', () => { + this.player.getCurrentTime().then((currentTime) => { + // this.remotePlayer.currentTime = currentTime + // this.remotePlayerController.seek() + }) + }) + + this.castButton.classList.add('active') + this.player.isChromecast = true + + this.player.getRemoteCurrentTime = () => this.remotePlayer.currentTime + setInterval(() => { + !this.player.isPaused && this.player.onTimeUpdate(true) + // this.player.methodSeekTo(this.player.getRemoteCurrentTime()) + }, 1000) } onLoadMediaError(e) { - console.log('onLoadMediaError', e) + // console.log('onLoadMediaError', e) } } diff --git a/src/providers/html5/html5.ts b/src/providers/html5/html5.ts index 9eda1f20..a34114aa 100644 --- a/src/providers/html5/html5.ts +++ b/src/providers/html5/html5.ts @@ -76,8 +76,14 @@ export default function (Player: any) { * Get the player current time * @returns {Promise} Current time of the video */ - getCurrentTime(): Promise { - return new window.Promise((resolve) => resolve(this.media.currentTime)) + getCurrentTime(isRemote: Boolean = false): Promise { + return new window.Promise((resolve) => { + if (isRemote) { + resolve(this.getRemoteCurrentTime()) + } else { + resolve(this.media.currentTime) + } + }) } /** diff --git a/src/shared/assets/svgs/cast.svg b/src/shared/assets/svgs/cast.svg new file mode 100644 index 00000000..8e8e487e --- /dev/null +++ b/src/shared/assets/svgs/cast.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vlite/assets/scripts/player.ts b/src/vlite/assets/scripts/player.ts index 4d42a62a..6bdf8fc2 100644 --- a/src/vlite/assets/scripts/player.ts +++ b/src/vlite/assets/scripts/player.ts @@ -12,6 +12,7 @@ export default class Player { type: string media: HTMLAudioElement | HTMLVideoElement options: Options + isChromecast: Boolean isFullScreen: Boolean isMuted: Boolean isPaused: null | Boolean @@ -21,6 +22,7 @@ export default class Player { plugins: { [key: string]: any } + getRemoteCurrentTime!: Function elements: { container: HTMLElement bigPlay: HTMLElement | null @@ -46,6 +48,7 @@ export default class Player { this.plugins = {} this.media = Vlitejs.media this.options = Vlitejs.options + this.isChromecast = false this.elements = { container: Vlitejs.container, @@ -108,7 +111,7 @@ export default class Player { * getCurrentTime * Extends by the provider */ - getCurrentTime(): Promise { + getCurrentTime(isRemote: Boolean): Promise { throw new Error('You have to implement the function "getCurrentTime".') } @@ -237,12 +240,12 @@ export default class Player { * Update current time displaying in the control bar * Udpdate the progress bar */ - onTimeUpdate() { + onTimeUpdate(isRemote: Boolean = false) { if (this.options.time) { - Promise.all([this.getCurrentTime(), this.getDuration()]).then( + Promise.all([this.getCurrentTime(isRemote), this.getDuration()]).then( ([seconds, duration]: [number, number]) => { const currentTime = Math.round(seconds) - + console.log(currentTime) if (this.elements.progressBar) { const width = (currentTime * 100) / duration this.elements.progressBar.value = `${width}` @@ -257,7 +260,7 @@ export default class Player { this.elements.currentTime.innerHTML = formatVideoTime(currentTime) } - this.dispatchEvent('timeupdate') + !isRemote && this.dispatchEvent('timeupdate') } ) } @@ -303,7 +306,7 @@ export default class Player { } } - this.methodPlay() + !this.isChromecast && this.methodPlay() this.isPaused = false this.elements.container.classList.replace('v-paused', 'v-playing') @@ -324,7 +327,7 @@ export default class Player { * Pause the media element */ pause() { - this.methodPause() + !this.isChromecast && this.methodPause() this.isPaused = true this.elements.container.classList.replace('v-playing', 'v-paused') diff --git a/src/vlite/assets/scripts/vlite.ts b/src/vlite/assets/scripts/vlite.ts index 2a06abfc..80c06ef1 100644 --- a/src/vlite/assets/scripts/vlite.ts +++ b/src/vlite/assets/scripts/vlite.ts @@ -284,7 +284,7 @@ class Vlitejs { * On mousemove on the player */ onMousemove() { - if (!this.player.isPaused) { + if (!this.player.isPaused && this.autoHideGranted) { this.stopAutoHideTimer() this.startAutoHideTimer() } diff --git a/src/vlite/components/big-play/assets/styles/big-play.css b/src/vlite/components/big-play/assets/styles/big-play.css index 3b4121ed..c3d89550 100644 --- a/src/vlite/components/big-play/assets/styles/big-play.css +++ b/src/vlite/components/big-play/assets/styles/big-play.css @@ -3,8 +3,8 @@ position: absolute; top: 50%; left: 50%; - width: 70px; - height: 70px; + width: 58px; + height: 58px; transform: translateX(-50%) translateY(-50%); z-index: 2; transition: opacity var(--vlite-transition); From e4c10a04d8e15ea6319b5a28c981119e3107f554 Mon Sep 17 00:00:00 2001 From: Yoriiis Date: Mon, 8 Aug 2022 01:43:33 +0200 Subject: [PATCH 06/18] Add plugin options Add track and style Add metadata --- examples/html5/config.js | 10 ++- src/plugins/chromecast/js/chromecast.js | 86 ++++++++++++------------- src/plugins/plugin.ts | 14 ++-- src/providers/html5/html5.ts | 10 +-- src/vlite/assets/scripts/player.ts | 2 +- src/vlite/assets/scripts/vlite.ts | 2 +- 6 files changed, 69 insertions(+), 55 deletions(-) diff --git a/examples/html5/config.js b/examples/html5/config.js index 076c71c7..f8c2ffdc 100644 --- a/examples/html5/config.js +++ b/examples/html5/config.js @@ -8,7 +8,15 @@ import VlitejsChromecast from '../../dist/plugins/chromecast.js' Vlitejs.registerPlugin('subtitle', VlitejsSubtitle) Vlitejs.registerPlugin('pip', VlitejsPip) -Vlitejs.registerPlugin('chromecast', VlitejsChromecast) +Vlitejs.registerPlugin('chromecast', VlitejsChromecast, { + textTrackStyle: { + backgroundColor: '#21212190' + }, + metadata: { + title: 'The Jungle Book', + subtitle: 'Walt Disney Animation Studios' + } +}) /* eslint-disable no-unused-vars */ const vlite = new Vlitejs('#player', { diff --git a/src/plugins/chromecast/js/chromecast.js b/src/plugins/chromecast/js/chromecast.js index 880011e2..844da7bf 100644 --- a/src/plugins/chromecast/js/chromecast.js +++ b/src/plugins/chromecast/js/chromecast.js @@ -13,8 +13,9 @@ export default class ChromecastPlugin { * @param {Object} options * @param {Class} options.player Player instance */ - constructor({ player }) { + constructor({ player, options }) { this.player = player + this.options = options this.onLoadMediaSuccess = this.onLoadMediaSuccess.bind(this) this.onLoadMediaError = this.onLoadMediaError.bind(this) @@ -39,13 +40,14 @@ export default class ChromecastPlugin { } initCastApi() { - this.render() - this.castButton = this.player.elements.container.querySelector('.v-castButton') - - this.getCastInstance().setOptions({ + this.castContext = cast.framework.CastContext.getInstance() + this.castContext.setOptions({ receiverApplicationId: chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID, autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED }) + + this.render() + this.castButton = this.player.elements.container.querySelector('.v-castButton') this.addEvents() } @@ -62,21 +64,15 @@ export default class ChromecastPlugin { } } - getSession() { - return this.getCastInstance().getCurrentSession() - } - addEvents() { - this.castButton.addEventListener('click', this.onClickOnCastButton) - this.getCastInstance().addEventListener( + this.castContext.addEventListener( cast.framework.CastContextEventType.SESSION_STATE_CHANGED, this.onCastStateChange ) + this.castButton.addEventListener('click', this.onClickOnCastButton) } onCastStateChange(e) { - // console.log('onCastStateChange', e) - switch (e.sessionState) { case cast.framework.SessionState.SESSION_STARTED: this.remotePlayerStart() @@ -99,7 +95,6 @@ export default class ChromecastPlugin { } remotePlayerResume() { - this.player.play() this.remotePlayerStart() } @@ -120,58 +115,65 @@ export default class ChromecastPlugin { onClickOnCastButton(e) { e.preventDefault() - - const session = this.getSession() - if (session) { - this.getCastInstance().endCurrentSession() - } else { - this.getCastInstance() - .requestSession() - .then(this.onRequestSessionInitializeSuccess, this.onRequestSessionError) - } + this.castContext.requestSession() } - getCastInstance() { - return cast.framework.CastContext.getInstance() + getSession() { + return this.castContext.getCurrentSession() } loadMedia() { const session = this.session || this.getSession() if (!session) return - const mediaSrc = this.player.media.src - const contentType = this.player.type === 'video' ? 'video/mp4' : '' - const mediaInfo = new chrome.cast.media.MediaInfo(mediaSrc, contentType) - const request = new chrome.cast.media.LoadRequest(mediaInfo) - request.autoplay = !this.player.isPaused - request.currentTime = this.player.media.currentTime - session.loadMedia(request).then(this.onLoadMediaSuccess, this.onLoadMediaError) + const mediaInfo = new window.chrome.cast.media.MediaInfo(this.player.media.src) + mediaInfo.contentType = this.player.type === 'video' ? 'video/mp4' : '' + + const textTrackStyle = new window.chrome.cast.media.TextTrackStyle() + mediaInfo.textTrackStyle = { backgroundColor: '#21212100', ...this.options.textTrackStyle } + + var metadata = new window.chrome.cast.media.MovieMediaMetadata() + mediaInfo.metadata = { + images: [new window.chrome.cast.Image(this.player.options.poster)], + ...this.options.metadata + } - // this.player.plugins.subtitle && this.addTracks() + if (this.player.plugins.subtitle) { + mediaInfo.tracks = this.addTracks() + } + + const loadRequest = new window.chrome.cast.media.LoadRequest(mediaInfo) + loadRequest.autoplay = this.player.isPaused === false + loadRequest.currentTime = this.player.media.currentTime + loadRequest.activeTrackIds = [1] + session.loadMedia(loadRequest).then(this.onLoadMediaSuccess, this.onLoadMediaError) } addTracks() { - this.player.plugins.subtitle.tracks.forEach((track) => { - const castTrack = new chrome.cast.media.Track(1, chrome.cast.media.TrackType.TEXT) + return this.player.plugins.subtitle.tracks.map((track) => { + const castTrack = new window.chrome.cast.media.Track( + 1, + chrome.cast.media.TrackType.TEXT + ) castTrack.trackContentId = 'https://yoriiis.github.io/cdn/static/vlitejs/demo-video-html5-subtitle-en.vtt' castTrack.trackContentType = 'text/vtt' castTrack.subtype = chrome.cast.media.TextTrackType.SUBTITLES castTrack.name = track.label castTrack.language = track.language - console.log(castTrack) + return castTrack }) } onLoadMediaSuccess(e) { - // console.log('onLoadMediaSuccess', e) - if (!this.player.isPaused) { this.player.methodPause() } - this.remotePlayer = new cast.framework.RemotePlayer() - this.remotePlayerController = new cast.framework.RemotePlayerController(this.remotePlayer) + this.remotePlayer = new window.cast.framework.RemotePlayer() + this.remotePlayerController = new window.cast.framework.RemotePlayerController( + this.remotePlayer + ) this.player.on('play', () => { this.remotePlayerController.playOrPause() @@ -203,7 +205,5 @@ export default class ChromecastPlugin { }, 1000) } - onLoadMediaError(e) { - // console.log('onLoadMediaError', e) - } + onLoadMediaError(e) {} } diff --git a/src/plugins/plugin.ts b/src/plugins/plugin.ts index 4dac6a34..4c6b55a0 100644 --- a/src/plugins/plugin.ts +++ b/src/plugins/plugin.ts @@ -8,6 +8,7 @@ export interface interfacePluginsInstance { } const vlitePlugins: interfaceVlitePlugins = {} +const pluginsOptions = {} /** * Get plugins instances from the registered list @@ -22,7 +23,8 @@ export function getPluginInstance(plugins: Array): Array): Array { - const plugin = new Plugin({ player }) + getPluginInstance(plugins).forEach(({ id, Plugin, options }: { id: string; Plugin: any }) => { + const plugin = new Plugin({ player, options }) // Store the plugin instance on the player player.plugins[id] = plugin diff --git a/src/providers/html5/html5.ts b/src/providers/html5/html5.ts index a34114aa..f88dac96 100644 --- a/src/providers/html5/html5.ts +++ b/src/providers/html5/html5.ts @@ -78,11 +78,11 @@ export default function (Player: any) { */ getCurrentTime(isRemote: Boolean = false): Promise { return new window.Promise((resolve) => { - if (isRemote) { - resolve(this.getRemoteCurrentTime()) - } else { - resolve(this.media.currentTime) - } + // if (isRemote) { + // resolve(this.getRemoteCurrentTime()) + // } else { + resolve(this.media.currentTime) + // } }) } diff --git a/src/vlite/assets/scripts/player.ts b/src/vlite/assets/scripts/player.ts index 6bdf8fc2..cdec5a18 100644 --- a/src/vlite/assets/scripts/player.ts +++ b/src/vlite/assets/scripts/player.ts @@ -245,7 +245,7 @@ export default class Player { Promise.all([this.getCurrentTime(isRemote), this.getDuration()]).then( ([seconds, duration]: [number, number]) => { const currentTime = Math.round(seconds) - console.log(currentTime) + // console.log(currentTime) if (this.elements.progressBar) { const width = (currentTime * 100) / duration this.elements.progressBar.value = `${width}` diff --git a/src/vlite/assets/scripts/vlite.ts b/src/vlite/assets/scripts/vlite.ts index 80c06ef1..6a644d5a 100644 --- a/src/vlite/assets/scripts/vlite.ts +++ b/src/vlite/assets/scripts/vlite.ts @@ -7,7 +7,7 @@ import OverlayTemplate from '../../components/overlay/assets/scripts/overlay' import PosterTemplate from '../../components/poster/assets/scripts/poster' import { Options, FullScreenSupport } from 'shared/assets/interfaces/interfaces' import { registerProvider, getProviderInstance } from '../../../providers/provider' -import { getPluginInstance, registerPlugin, initializePlugins } from '../../../plugins/plugin' +import { registerPlugin, initializePlugins } from '../../../plugins/plugin' type TimerHandle = number From 322e2720c53d705a88ff98603d62a0a25cda90db Mon Sep 17 00:00:00 2001 From: Yoriiis Date: Mon, 8 Aug 2022 20:59:57 +0200 Subject: [PATCH 07/18] Transform to TS Update track style Add Chromecast name and hide video --- examples/webpack.config.js | 3 +- src/plugins/chromecast/css/chromecast.css | 22 ++ .../js/{chromecast.js => chromecast.ts} | 205 +++++++++++++----- src/plugins/plugin.ts | 29 +-- src/plugins/subtitle/js/subtitle.ts | 5 +- src/shared/assets/interfaces/interfaces.ts | 1 + 6 files changed, 198 insertions(+), 67 deletions(-) rename src/plugins/chromecast/js/{chromecast.js => chromecast.ts} (51%) diff --git a/examples/webpack.config.js b/examples/webpack.config.js index 6d627d14..94634bf4 100644 --- a/examples/webpack.config.js +++ b/examples/webpack.config.js @@ -71,7 +71,8 @@ module.exports = (env, argv) => { compress: true, hot: true, host: '0.0.0.0', - https: true + https: true, + open: ['/html5'] }, context: appDirectory, plugins: [ diff --git a/src/plugins/chromecast/css/chromecast.css b/src/plugins/chromecast/css/chromecast.css index a8f4a329..364173e2 100644 --- a/src/plugins/chromecast/css/chromecast.css +++ b/src/plugins/chromecast/css/chromecast.css @@ -5,10 +5,32 @@ } } + &-chromecastName { + display: none; + position: absolute; + top: 0; + right: 0; + color: #fff; + width: auto; + padding: 10px; + font-size: 12px; + line-height: 12px; + pointer-events: none; + } + &-remote { .v-pipButton, .v-fullscreenButton { display: none; } + + .vlite-js, + .v-captions { + display: none; + } + + .v-chromecastName { + display: block; + } } } diff --git a/src/plugins/chromecast/js/chromecast.js b/src/plugins/chromecast/js/chromecast.ts similarity index 51% rename from src/plugins/chromecast/js/chromecast.js rename to src/plugins/chromecast/js/chromecast.ts index 844da7bf..64e8cbbb 100644 --- a/src/plugins/chromecast/js/chromecast.js +++ b/src/plugins/chromecast/js/chromecast.ts @@ -1,10 +1,79 @@ import svgCast from 'shared/assets/svgs/cast.svg' +import { pluginParameter } from 'shared/assets/interfaces/interfaces' + +declare global { + interface Window { + chrome: { + cast: { + media: { + TextTrackType: { + SUBTITLES: string + } + TrackType: { + TEXT: string + } + DEFAULT_MEDIA_RECEIVER_APP_ID: string + MediaInfo: Constructable + TextTrackStyle: Constructable + GenericMediaMetadata: Constructable + LoadRequest: Constructable + Track: Constructable + } + AutoJoinPolicy: { + ORIGIN_SCOPED: string + } + Image: Constructable + } + } + cast: { + framework: { + CastContext: { + getInstance: Function + } + RemotePlayer: Constructable + RemotePlayerController: Constructable + CastContextEventType: { + SESSION_STATE_CHANGED: string + } + SessionState: { + SESSION_STARTED: string + SESSION_RESUMED: string + SESSION_ENDED: string + } + } + } + __onGCastApiAvailable: Function + } +} + +export interface Constructable { + new (...args: any): T +} + +interface Subtitle { + url: string + label: string + language: string +} + +interface CastEvent { + sessionState: string +} /** * Vlitejs Chromecast plugin * @module Vlitejs/plugins/chromecast */ export default class ChromecastPlugin { + player: any + options: any + castContext: any + remotePlayer: any + remotePlayerController: any + subtitles: Array + castButton!: HTMLElement + backupAutoHide: Boolean | null + providers = ['html5'] types = ['video'] @@ -13,12 +82,13 @@ export default class ChromecastPlugin { * @param {Object} options * @param {Class} options.player Player instance */ - constructor({ player, options }) { + constructor({ player, options }: pluginParameter) { this.player = player this.options = options + this.subtitles = [] + this.backupAutoHide = null this.onLoadMediaSuccess = this.onLoadMediaSuccess.bind(this) - this.onLoadMediaError = this.onLoadMediaError.bind(this) this.onCastStateChange = this.onCastStateChange.bind(this) this.onClickOnCastButton = this.onClickOnCastButton.bind(this) } @@ -27,7 +97,7 @@ export default class ChromecastPlugin { * Initialize the plugin */ init() { - window.__onGCastApiAvailable = (isAvailable) => isAvailable && this.initCastApi() + window.__onGCastApiAvailable = (isAvailable: Boolean) => isAvailable && this.initCastApi() this.loadWebSenderApi() } @@ -40,17 +110,32 @@ export default class ChromecastPlugin { } initCastApi() { - this.castContext = cast.framework.CastContext.getInstance() + this.castContext = window.cast.framework.CastContext.getInstance() this.castContext.setOptions({ - receiverApplicationId: chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID, - autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED + receiverApplicationId: window.chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID, + autoJoinPolicy: window.chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED }) + this.remotePlayer = new window.cast.framework.RemotePlayer() + this.remotePlayerController = new window.cast.framework.RemotePlayerController( + this.remotePlayer + ) + this.subtitles = this.getSubtitles() this.render() this.castButton = this.player.elements.container.querySelector('.v-castButton') this.addEvents() } + onReady() {} + + getSubtitles(): Array { + return [...this.player.media.querySelectorAll('track')].map((track) => ({ + url: track.getAttribute('src'), + label: track.getAttribute('label'), + language: track.getAttribute('srclang') + })) + } + render() { const controlBar = this.player.elements.container.querySelector('.v-controlBar') const fullscreenButton = this.player.elements.container.querySelector('.v-fullscreenButton') @@ -66,43 +151,57 @@ export default class ChromecastPlugin { addEvents() { this.castContext.addEventListener( - cast.framework.CastContextEventType.SESSION_STATE_CHANGED, + window.cast.framework.CastContextEventType.SESSION_STATE_CHANGED, this.onCastStateChange ) + this.castButton.addEventListener('click', this.onClickOnCastButton) + this.player.on('trackdisabled', (e: Event) => { + console.log('trackdisabled') + }) + this.player.on('trackenabled', (e: Event) => { + console.log('trackenabled') + }) } - onCastStateChange(e) { + onCastStateChange(e: CastEvent) { + console.log('onCastStateChange', e) switch (e.sessionState) { - case cast.framework.SessionState.SESSION_STARTED: - this.remotePlayerStart() + case window.cast.framework.SessionState.SESSION_STARTED: + this.onSessionStart() break - case cast.framework.SessionState.SESSION_RESUMED: - this.remotePlayerResume() + case window.cast.framework.SessionState.SESSION_RESUMED: + this.castContext.endCurrentSession(true) break - case cast.framework.SessionState.SESSION_ENDED: - this.remotePlayerStop() + case window.cast.framework.SessionState.SESSION_ENDED: + this.onSessionStop() break } } - remotePlayerStart() { + onSessionStart() { this.backupAutoHide = this.player.Vlitejs.autoHideGranted this.player.Vlitejs.autoHideGranted = false this.player.Vlitejs.stopAutoHideTimer() this.player.elements.container.classList.add('v-remote') - this.loadMedia() - } - remotePlayerResume() { - this.remotePlayerStart() + this.castButton.classList.add('active') + this.player.isChromecast = true + + const friendlyName = + this.castContext.getCurrentSession().getCastDevice().friendlyName || 'Chromecast' + this.player.media.insertAdjacentHTML( + 'afterend', + `Cast on ${friendlyName}` + ) + + this.loadMedia() } - remotePlayerStop() { + onSessionStop() { this.player.Vlitejs.autoHideGranted = this.backupAutoHide - if (this.backupAutoHide) { - this.player.Vlitejs.startAutoHideTimer() - } + this.backupAutoHide && this.player.Vlitejs.startAutoHideTimer() + this.castButton.classList.remove('active') this.player.elements.container.classList.remove('v-remote') this.player.isChromecast = false @@ -113,7 +212,7 @@ export default class ChromecastPlugin { } } - onClickOnCastButton(e) { + onClickOnCastButton(e: Event) { e.preventDefault() this.castContext.requestSession() } @@ -123,58 +222,65 @@ export default class ChromecastPlugin { } loadMedia() { - const session = this.session || this.getSession() + const session = this.getSession() if (!session) return const mediaInfo = new window.chrome.cast.media.MediaInfo(this.player.media.src) mediaInfo.contentType = this.player.type === 'video' ? 'video/mp4' : '' const textTrackStyle = new window.chrome.cast.media.TextTrackStyle() - mediaInfo.textTrackStyle = { backgroundColor: '#21212100', ...this.options.textTrackStyle } + mediaInfo.textTrackStyle = { + backgroundColor: '#ffffff00', + edgeColor: '#00000016', + edgeType: 'DROP_SHADOW', + fontFamily: 'CASUAL', + fontScale: 1.0, + foregroundColor: '#ffffffff', + ...this.options.textTrackStyle + } - var metadata = new window.chrome.cast.media.MovieMediaMetadata() + var metadata = new window.chrome.cast.media.GenericMediaMetadata() mediaInfo.metadata = { images: [new window.chrome.cast.Image(this.player.options.poster)], ...this.options.metadata } - if (this.player.plugins.subtitle) { - mediaInfo.tracks = this.addTracks() + if (this.subtitles.length) { + mediaInfo.tracks = this.getCastTracks() } const loadRequest = new window.chrome.cast.media.LoadRequest(mediaInfo) loadRequest.autoplay = this.player.isPaused === false loadRequest.currentTime = this.player.media.currentTime + loadRequest.playbackRate = 3 + + // console.log(this.player.plugins.subtitle.trackIsEnabled) + // if (this.player.plugins.subtitle.trackIsEnabled) { loadRequest.activeTrackIds = [1] - session.loadMedia(loadRequest).then(this.onLoadMediaSuccess, this.onLoadMediaError) + // } + session.loadMedia(loadRequest).then(this.onLoadMediaSuccess) } - addTracks() { - return this.player.plugins.subtitle.tracks.map((track) => { + getCastTracks() { + return this.subtitles.map(({ url, label, language }, index) => { const castTrack = new window.chrome.cast.media.Track( - 1, - chrome.cast.media.TrackType.TEXT + index, + window.chrome.cast.media.TrackType.TEXT ) - castTrack.trackContentId = - 'https://yoriiis.github.io/cdn/static/vlitejs/demo-video-html5-subtitle-en.vtt' + castTrack.trackContentId = url castTrack.trackContentType = 'text/vtt' - castTrack.subtype = chrome.cast.media.TextTrackType.SUBTITLES - castTrack.name = track.label - castTrack.language = track.language + castTrack.subtype = window.chrome.cast.media.TextTrackType.SUBTITLES + castTrack.name = label + castTrack.language = language return castTrack }) } - onLoadMediaSuccess(e) { + onLoadMediaSuccess() { if (!this.player.isPaused) { this.player.methodPause() } - this.remotePlayer = new window.cast.framework.RemotePlayer() - this.remotePlayerController = new window.cast.framework.RemotePlayerController( - this.remotePlayer - ) - this.player.on('play', () => { this.remotePlayerController.playOrPause() }) @@ -182,28 +288,23 @@ export default class ChromecastPlugin { this.remotePlayerController.playOrPause() }) this.player.on('volumechange', () => { - this.player.getVolume().then((volume) => { + this.player.getVolume().then((volume: number) => { this.remotePlayer.volumeLevel = this.player.isMuted ? 0 : volume this.remotePlayerController.setVolumeLevel() }) }) this.player.on('timeupdate', () => { - this.player.getCurrentTime().then((currentTime) => { + this.player.getCurrentTime().then((currentTime: number) => { // this.remotePlayer.currentTime = currentTime // this.remotePlayerController.seek() }) }) - this.castButton.classList.add('active') - this.player.isChromecast = true - this.player.getRemoteCurrentTime = () => this.remotePlayer.currentTime setInterval(() => { !this.player.isPaused && this.player.onTimeUpdate(true) // this.player.methodSeekTo(this.player.getRemoteCurrentTime()) }, 1000) } - - onLoadMediaError(e) {} } diff --git a/src/plugins/plugin.ts b/src/plugins/plugin.ts index 4c6b55a0..d7cab176 100644 --- a/src/plugins/plugin.ts +++ b/src/plugins/plugin.ts @@ -5,10 +5,11 @@ export interface interfaceVlitePlugins { export interface interfacePluginsInstance { id: string Plugin: any + options: any } const vlitePlugins: interfaceVlitePlugins = {} -const pluginsOptions = {} +const pluginsOptions: any = {} /** * Get plugins instances from the registered list @@ -40,7 +41,7 @@ export function getPluginInstance(plugins: Array): Array { - const plugin = new Plugin({ player, options }) + getPluginInstance(plugins).forEach( + ({ id, Plugin, options }: { id: string; Plugin: any; options: any }) => { + const plugin = new Plugin({ player, options }) - // Store the plugin instance on the player - player.plugins[id] = plugin + // Store the plugin instance on the player + player.plugins[id] = plugin - if (plugin.providers.includes(provider) && plugin.types.includes(type)) { - plugin.init() - } else { - throw new Error( - `vlitejs :: The "${id}" plugin is only compatible with providers:"${plugin.providers}" and types:"${plugin.types}"` - ) + if (plugin.providers.includes(provider) && plugin.types.includes(type)) { + plugin.init() + } else { + throw new Error( + `vlitejs :: The "${id}" plugin is only compatible with providers:"${plugin.providers}" and types:"${plugin.types}"` + ) + } } - }) + ) } diff --git a/src/plugins/subtitle/js/subtitle.ts b/src/plugins/subtitle/js/subtitle.ts index 2f4daf5d..730b7185 100644 --- a/src/plugins/subtitle/js/subtitle.ts +++ b/src/plugins/subtitle/js/subtitle.ts @@ -2,7 +2,7 @@ import validateTarget from 'validate-target' import svgSubtitleOn from 'shared/assets/svgs/subtitle-on.svg' import svgSubtitleOff from 'shared/assets/svgs/subtitle-off.svg' import svgCheck from 'shared/assets/svgs/check.svg' -import { Options, pluginParameter } from 'shared/assets/interfaces/interfaces' +import { pluginParameter } from 'shared/assets/interfaces/interfaces' export interface InsertPosition { selector: string @@ -15,6 +15,7 @@ export interface InsertPosition { */ export default class Subtitle { player: any + trackIsEnabled: Boolean tracks: Array activeTrack!: TextTrack | null captions!: HTMLElement @@ -32,6 +33,7 @@ export default class Subtitle { */ constructor({ player }: pluginParameter) { this.player = player + this.trackIsEnabled = false this.tracks = Array.from(this.player.media.textTracks) this.subtitlesListCssTransitionDuration = 0 @@ -264,6 +266,7 @@ export default class Subtitle { */ updateCues({ isDisabled = false }: { isDisabled?: Boolean } = {}) { if (this.activeTrack && this.activeTrack.cues && this.activeTrack.cues.length) { + this.trackIsEnabled = !isDisabled const cues = Array.from(this.activeTrack.cues) const activeCues = this.activeTrack.activeCues diff --git a/src/shared/assets/interfaces/interfaces.ts b/src/shared/assets/interfaces/interfaces.ts index 1d10a2d9..787341d2 100644 --- a/src/shared/assets/interfaces/interfaces.ts +++ b/src/shared/assets/interfaces/interfaces.ts @@ -15,6 +15,7 @@ export interface playerParameters { export interface pluginParameter { player: playerParameters + options: any } export interface Options { From 241148bab203ce0f1ccd5cd5cc95b9d1829cd660 Mon Sep 17 00:00:00 2001 From: Yoriiis Date: Tue, 9 Aug 2022 18:49:28 +0200 Subject: [PATCH 08/18] Add subtitle changes Fix subtitles --- examples/html5/index.html | 3 +- src/plugins/chromecast/js/chromecast.ts | 87 +++++++++++++++---------- 2 files changed, 56 insertions(+), 34 deletions(-) diff --git a/examples/html5/index.html b/examples/html5/index.html index 41bde5b6..5d37320b 100644 --- a/examples/html5/index.html +++ b/examples/html5/index.html @@ -7,7 +7,8 @@ diff --git a/src/plugins/chromecast/js/chromecast.ts b/src/plugins/chromecast/js/chromecast.ts index 64e8cbbb..fef491be 100644 --- a/src/plugins/chromecast/js/chromecast.ts +++ b/src/plugins/chromecast/js/chromecast.ts @@ -91,6 +91,7 @@ export default class ChromecastPlugin { this.onLoadMediaSuccess = this.onLoadMediaSuccess.bind(this) this.onCastStateChange = this.onCastStateChange.bind(this) this.onClickOnCastButton = this.onClickOnCastButton.bind(this) + this.updateSubtitle = this.updateSubtitle.bind(this) } /** @@ -129,10 +130,12 @@ export default class ChromecastPlugin { onReady() {} getSubtitles(): Array { - return [...this.player.media.querySelectorAll('track')].map((track) => ({ + return [...this.player.media.querySelectorAll('track')].map((track, index) => ({ + index, url: track.getAttribute('src'), label: track.getAttribute('label'), - language: track.getAttribute('srclang') + language: track.getAttribute('srclang'), + isDefault: track.hasAttribute('default') })) } @@ -156,12 +159,8 @@ export default class ChromecastPlugin { ) this.castButton.addEventListener('click', this.onClickOnCastButton) - this.player.on('trackdisabled', (e: Event) => { - console.log('trackdisabled') - }) - this.player.on('trackenabled', (e: Event) => { - console.log('trackenabled') - }) + this.player.on('trackdisabled', this.updateSubtitle) + this.player.on('trackenabled', this.updateSubtitle) } onCastStateChange(e: CastEvent) { @@ -180,6 +179,8 @@ export default class ChromecastPlugin { } onSessionStart() { + this.isPaused = this.player.isPaused + this.player.methodPause() this.backupAutoHide = this.player.Vlitejs.autoHideGranted this.player.Vlitejs.autoHideGranted = false this.player.Vlitejs.stopAutoHideTimer() @@ -188,8 +189,7 @@ export default class ChromecastPlugin { this.castButton.classList.add('active') this.player.isChromecast = true - const friendlyName = - this.castContext.getCurrentSession().getCastDevice().friendlyName || 'Chromecast' + const friendlyName = this.getSession().getCastDevice().friendlyName || 'Chromecast' this.player.media.insertAdjacentHTML( 'afterend', `Cast on ${friendlyName}` @@ -217,6 +217,25 @@ export default class ChromecastPlugin { this.castContext.requestSession() } + updateSubtitle() { + const newLanguage = this.player.plugins.subtitle.subtitlesList + .querySelector('.v-trackButton.v-active') + .getAttribute('data-language') + + let activeTrackIds + if (newLanguage === 'off') { + activeTrackIds = [] + } else { + const newTrackIndex = this.subtitles.find(({ language }) => language === newLanguage) + if (newTrackIndex && !isNaN(newTrackIndex.index)) { + activeTrackIds = [parseInt(newTrackIndex.index)] + } + } + + const tracksInfoRequest = new chrome.cast.media.EditTracksInfoRequest(activeTrackIds) + this.getSession().getMediaSession().editTracksInfo(tracksInfoRequest) + } + getSession() { return this.castContext.getCurrentSession() } @@ -229,38 +248,44 @@ export default class ChromecastPlugin { mediaInfo.contentType = this.player.type === 'video' ? 'video/mp4' : '' const textTrackStyle = new window.chrome.cast.media.TextTrackStyle() + textTrackStyle.backgroundColor = '#ffffff00' + textTrackStyle.edgeColor = '#00000016' + textTrackStyle.edgeType = 'DROP_SHADOW' + textTrackStyle.fontFamily = 'CASUAL' + textTrackStyle.fontScale = 1.0 + textTrackStyle.foregroundColor = '#ffffffff' mediaInfo.textTrackStyle = { - backgroundColor: '#ffffff00', - edgeColor: '#00000016', - edgeType: 'DROP_SHADOW', - fontFamily: 'CASUAL', - fontScale: 1.0, - foregroundColor: '#ffffffff', + ...textTrackStyle, ...this.options.textTrackStyle } - var metadata = new window.chrome.cast.media.GenericMediaMetadata() - mediaInfo.metadata = { - images: [new window.chrome.cast.Image(this.player.options.poster)], - ...this.options.metadata - } - if (this.subtitles.length) { mediaInfo.tracks = this.getCastTracks() } + const metadata = new window.chrome.cast.media.GenericMediaMetadata() + if (this.player.options.poster) { + metadata.images = [new window.chrome.cast.Image(this.player.options.poster)] + } + mediaInfo.metadata = { + ...metadata, + ...this.options.metadata + } + const loadRequest = new window.chrome.cast.media.LoadRequest(mediaInfo) - loadRequest.autoplay = this.player.isPaused === false + loadRequest.autoplay = this.isPaused === false loadRequest.currentTime = this.player.media.currentTime - loadRequest.playbackRate = 3 - // console.log(this.player.plugins.subtitle.trackIsEnabled) - // if (this.player.plugins.subtitle.trackIsEnabled) { - loadRequest.activeTrackIds = [1] - // } + if (this.subtitles.length) { + loadRequest.activeTrackIds = [this.getActiveTrack().index] + } session.loadMedia(loadRequest).then(this.onLoadMediaSuccess) } + getActiveTrack() { + return this.subtitles.find((item) => item.isDefault) || this.subtitles[0] + } + getCastTracks() { return this.subtitles.map(({ url, label, language }, index) => { const castTrack = new window.chrome.cast.media.Track( @@ -277,10 +302,6 @@ export default class ChromecastPlugin { } onLoadMediaSuccess() { - if (!this.player.isPaused) { - this.player.methodPause() - } - this.player.on('play', () => { this.remotePlayerController.playOrPause() }) @@ -303,7 +324,7 @@ export default class ChromecastPlugin { this.player.getRemoteCurrentTime = () => this.remotePlayer.currentTime setInterval(() => { - !this.player.isPaused && this.player.onTimeUpdate(true) + // !this.player.isPaused && this.player.onTimeUpdate(true) // this.player.methodSeekTo(this.player.getRemoteCurrentTime()) }, 1000) } From 0ad7ed36b9bc05435a3ddf4a9a64313f5046eaae Mon Sep 17 00:00:00 2001 From: Yoriiis Date: Tue, 9 Aug 2022 19:10:57 +0200 Subject: [PATCH 09/18] Update the progress bar on chromecast current time changed --- src/plugins/chromecast/js/chromecast.ts | 41 ++++++++++++--------- src/providers/html5/html5.ts | 10 +---- src/vlite/assets/scripts/player.ts | 49 ++++++++++++++----------- 3 files changed, 52 insertions(+), 48 deletions(-) diff --git a/src/plugins/chromecast/js/chromecast.ts b/src/plugins/chromecast/js/chromecast.ts index fef491be..cd282e06 100644 --- a/src/plugins/chromecast/js/chromecast.ts +++ b/src/plugins/chromecast/js/chromecast.ts @@ -18,6 +18,7 @@ declare global { GenericMediaMetadata: Constructable LoadRequest: Constructable Track: Constructable + EditTracksInfoRequest: Constructable } AutoJoinPolicy: { ORIGIN_SCOPED: string @@ -54,10 +55,13 @@ interface Subtitle { url: string label: string language: string + index: number + isDefault: boolean } interface CastEvent { sessionState: string + value: number } /** @@ -72,7 +76,8 @@ export default class ChromecastPlugin { remotePlayerController: any subtitles: Array castButton!: HTMLElement - backupAutoHide: Boolean | null + backupAutoHide: boolean | null + isPaused: boolean | null providers = ['html5'] types = ['video'] @@ -87,9 +92,11 @@ export default class ChromecastPlugin { this.options = options this.subtitles = [] this.backupAutoHide = null + this.isPaused = null this.onLoadMediaSuccess = this.onLoadMediaSuccess.bind(this) this.onCastStateChange = this.onCastStateChange.bind(this) + this.onCurrentTimeChanged = this.onCurrentTimeChanged.bind(this) this.onClickOnCastButton = this.onClickOnCastButton.bind(this) this.updateSubtitle = this.updateSubtitle.bind(this) } @@ -98,7 +105,7 @@ export default class ChromecastPlugin { * Initialize the plugin */ init() { - window.__onGCastApiAvailable = (isAvailable: Boolean) => isAvailable && this.initCastApi() + window.__onGCastApiAvailable = (isAvailable: boolean) => isAvailable && this.initCastApi() this.loadWebSenderApi() } @@ -157,12 +164,23 @@ export default class ChromecastPlugin { window.cast.framework.CastContextEventType.SESSION_STATE_CHANGED, this.onCastStateChange ) + this.remotePlayerController.addEventListener( + 'currentTimeChanged', + this.onCurrentTimeChanged.bind(this) + ) this.castButton.addEventListener('click', this.onClickOnCastButton) this.player.on('trackdisabled', this.updateSubtitle) this.player.on('trackenabled', this.updateSubtitle) } + onCurrentTimeChanged() { + this.player.updateProgressBar({ + seconds: this.remotePlayer.currentTime, + duration: this.remotePlayer.duration + }) + } + onCastStateChange(e: CastEvent) { console.log('onCastStateChange', e) switch (e.sessionState) { @@ -228,11 +246,11 @@ export default class ChromecastPlugin { } else { const newTrackIndex = this.subtitles.find(({ language }) => language === newLanguage) if (newTrackIndex && !isNaN(newTrackIndex.index)) { - activeTrackIds = [parseInt(newTrackIndex.index)] + activeTrackIds = [newTrackIndex.index] } } - const tracksInfoRequest = new chrome.cast.media.EditTracksInfoRequest(activeTrackIds) + const tracksInfoRequest = new window.chrome.cast.media.EditTracksInfoRequest(activeTrackIds) this.getSession().getMediaSession().editTracksInfo(tracksInfoRequest) } @@ -282,7 +300,7 @@ export default class ChromecastPlugin { session.loadMedia(loadRequest).then(this.onLoadMediaSuccess) } - getActiveTrack() { + getActiveTrack(): Subtitle { return this.subtitles.find((item) => item.isDefault) || this.subtitles[0] } @@ -314,18 +332,5 @@ export default class ChromecastPlugin { this.remotePlayerController.setVolumeLevel() }) }) - - this.player.on('timeupdate', () => { - this.player.getCurrentTime().then((currentTime: number) => { - // this.remotePlayer.currentTime = currentTime - // this.remotePlayerController.seek() - }) - }) - - this.player.getRemoteCurrentTime = () => this.remotePlayer.currentTime - setInterval(() => { - // !this.player.isPaused && this.player.onTimeUpdate(true) - // this.player.methodSeekTo(this.player.getRemoteCurrentTime()) - }, 1000) } } diff --git a/src/providers/html5/html5.ts b/src/providers/html5/html5.ts index f88dac96..9eda1f20 100644 --- a/src/providers/html5/html5.ts +++ b/src/providers/html5/html5.ts @@ -76,14 +76,8 @@ export default function (Player: any) { * Get the player current time * @returns {Promise} Current time of the video */ - getCurrentTime(isRemote: Boolean = false): Promise { - return new window.Promise((resolve) => { - // if (isRemote) { - // resolve(this.getRemoteCurrentTime()) - // } else { - resolve(this.media.currentTime) - // } - }) + getCurrentTime(): Promise { + return new window.Promise((resolve) => resolve(this.media.currentTime)) } /** diff --git a/src/vlite/assets/scripts/player.ts b/src/vlite/assets/scripts/player.ts index cdec5a18..ff6b6d3d 100644 --- a/src/vlite/assets/scripts/player.ts +++ b/src/vlite/assets/scripts/player.ts @@ -111,7 +111,7 @@ export default class Player { * getCurrentTime * Extends by the provider */ - getCurrentTime(isRemote: Boolean): Promise { + getCurrentTime(): Promise { throw new Error('You have to implement the function "getCurrentTime".') } @@ -240,32 +240,37 @@ export default class Player { * Update current time displaying in the control bar * Udpdate the progress bar */ - onTimeUpdate(isRemote: Boolean = false) { + onTimeUpdate() { if (this.options.time) { - Promise.all([this.getCurrentTime(isRemote), this.getDuration()]).then( - ([seconds, duration]: [number, number]) => { - const currentTime = Math.round(seconds) - // console.log(currentTime) - if (this.elements.progressBar) { - const width = (currentTime * 100) / duration - this.elements.progressBar.value = `${width}` - this.elements.progressBar.style.setProperty('--value', `${width}%`) - this.elements.progressBar.setAttribute( - 'aria-valuenow', - `${Math.round(seconds)}` - ) - } - - if (this.elements.currentTime) { - this.elements.currentTime.innerHTML = formatVideoTime(currentTime) - } - - !isRemote && this.dispatchEvent('timeupdate') - } + Promise.all([this.getCurrentTime(), this.getDuration()]).then( + ([seconds, duration]: [number, number]) => + this.updateProgressBar({ seconds, duration }) ) } } + /** + * Update the progress bar + * @param {Object} options + * @param {Object} options.seconds Current time in seconds + * @param {Object} options.duration Duration in seconds + */ + updateProgressBar({ seconds, duration }: { seconds: number; duration: number }) { + const currentTime = Math.round(seconds) + if (this.elements.progressBar) { + const width = (currentTime * 100) / duration + this.elements.progressBar.value = `${width}` + this.elements.progressBar.style.setProperty('--value', `${width}%`) + this.elements.progressBar.setAttribute('aria-valuenow', `${Math.round(seconds)}`) + } + + if (this.elements.currentTime) { + this.elements.currentTime.innerHTML = formatVideoTime(currentTime) + } + + this.dispatchEvent('timeupdate') + } + /** * On media ended */ From 48ce674e0f48d287bd6ea766926e7552785d3593 Mon Sep 17 00:00:00 2001 From: Yoriiis Date: Tue, 9 Aug 2022 19:18:29 +0200 Subject: [PATCH 10/18] Update the current time of the remote player on time update --- src/plugins/chromecast/js/chromecast.ts | 9 ++++++++- src/vlite/assets/scripts/player.ts | 12 ++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/plugins/chromecast/js/chromecast.ts b/src/plugins/chromecast/js/chromecast.ts index cd282e06..639a7dba 100644 --- a/src/plugins/chromecast/js/chromecast.ts +++ b/src/plugins/chromecast/js/chromecast.ts @@ -177,7 +177,8 @@ export default class ChromecastPlugin { onCurrentTimeChanged() { this.player.updateProgressBar({ seconds: this.remotePlayer.currentTime, - duration: this.remotePlayer.duration + duration: this.remotePlayer.duration, + isRemote: true }) } @@ -332,5 +333,11 @@ export default class ChromecastPlugin { this.remotePlayerController.setVolumeLevel() }) }) + this.player.on('timeupdate', () => { + this.player.getCurrentTime().then((currentTime: number) => { + this.remotePlayer.currentTime = currentTime + this.remotePlayerController.seek() + }) + }) } } diff --git a/src/vlite/assets/scripts/player.ts b/src/vlite/assets/scripts/player.ts index ff6b6d3d..70642e37 100644 --- a/src/vlite/assets/scripts/player.ts +++ b/src/vlite/assets/scripts/player.ts @@ -255,7 +255,15 @@ export default class Player { * @param {Object} options.seconds Current time in seconds * @param {Object} options.duration Duration in seconds */ - updateProgressBar({ seconds, duration }: { seconds: number; duration: number }) { + updateProgressBar({ + seconds, + duration, + isRemote = false + }: { + seconds: number + duration: number + isRemote: boolean + }) { const currentTime = Math.round(seconds) if (this.elements.progressBar) { const width = (currentTime * 100) / duration @@ -268,7 +276,7 @@ export default class Player { this.elements.currentTime.innerHTML = formatVideoTime(currentTime) } - this.dispatchEvent('timeupdate') + !isRemote && this.dispatchEvent('timeupdate') } /** From 943636a0ca14d1c182d4b19d7011ac58a8e63b57 Mon Sep 17 00:00:00 2001 From: Yoriiis Date: Tue, 9 Aug 2022 19:54:33 +0200 Subject: [PATCH 11/18] Add JSDoc Re-order functions Replace loadMedia promise callback by remote player controller event --- src/plugins/chromecast/js/chromecast.ts | 181 ++++++++++++++------- src/shared/assets/interfaces/interfaces.ts | 4 + src/vlite/assets/scripts/player.ts | 2 +- 3 files changed, 131 insertions(+), 56 deletions(-) diff --git a/src/plugins/chromecast/js/chromecast.ts b/src/plugins/chromecast/js/chromecast.ts index 639a7dba..f905d3ec 100644 --- a/src/plugins/chromecast/js/chromecast.ts +++ b/src/plugins/chromecast/js/chromecast.ts @@ -1,5 +1,5 @@ import svgCast from 'shared/assets/svgs/cast.svg' -import { pluginParameter } from 'shared/assets/interfaces/interfaces' +import { pluginParameter, Constructable } from 'shared/assets/interfaces/interfaces' declare global { interface Window { @@ -47,10 +47,6 @@ declare global { } } -export interface Constructable { - new (...args: any): T -} - interface Subtitle { url: string label: string @@ -71,11 +67,11 @@ interface CastEvent { export default class ChromecastPlugin { player: any options: any + castButton!: HTMLElement castContext: any remotePlayer: any remotePlayerController: any subtitles: Array - castButton!: HTMLElement backupAutoHide: boolean | null isPaused: boolean | null @@ -86,6 +82,7 @@ export default class ChromecastPlugin { * @constructor * @param {Object} options * @param {Class} options.player Player instance + * @param {Object} options.options Plugins options */ constructor({ player, options }: pluginParameter) { this.player = player @@ -94,9 +91,9 @@ export default class ChromecastPlugin { this.backupAutoHide = null this.isPaused = null - this.onLoadMediaSuccess = this.onLoadMediaSuccess.bind(this) this.onCastStateChange = this.onCastStateChange.bind(this) this.onCurrentTimeChanged = this.onCurrentTimeChanged.bind(this) + this.isMediaLoadedChanged = this.isMediaLoadedChanged.bind(this) this.onClickOnCastButton = this.onClickOnCastButton.bind(this) this.updateSubtitle = this.updateSubtitle.bind(this) } @@ -109,6 +106,9 @@ export default class ChromecastPlugin { this.loadWebSenderApi() } + /** + * Load web sender API + */ loadWebSenderApi() { const script = document.createElement('script') script.defer = true @@ -117,6 +117,10 @@ export default class ChromecastPlugin { document.getElementsByTagName('body')[0].appendChild(script) } + /** + * Chromecast API is available + * Initialize the Chromecast API + */ initCastApi() { this.castContext = window.cast.framework.CastContext.getInstance() this.castContext.setOptions({ @@ -128,28 +132,24 @@ export default class ChromecastPlugin { this.remotePlayer ) - this.subtitles = this.getSubtitles() this.render() this.castButton = this.player.elements.container.querySelector('.v-castButton') this.addEvents() } + /** + * On player ready + */ onReady() {} - getSubtitles(): Array { - return [...this.player.media.querySelectorAll('track')].map((track, index) => ({ - index, - url: track.getAttribute('src'), - label: track.getAttribute('label'), - language: track.getAttribute('srclang'), - isDefault: track.hasAttribute('default') - })) - } - + /** + * Render the plugin HTML + */ render() { const controlBar = this.player.elements.container.querySelector('.v-controlBar') const fullscreenButton = this.player.elements.container.querySelector('.v-fullscreenButton') const template = `` + if (controlBar) { if (fullscreenButton) { fullscreenButton.insertAdjacentHTML('beforebegin', template) @@ -159,6 +159,9 @@ export default class ChromecastPlugin { } } + /** + * Add event listeners + */ addEvents() { this.castContext.addEventListener( window.cast.framework.CastContextEventType.SESSION_STATE_CHANGED, @@ -166,7 +169,11 @@ export default class ChromecastPlugin { ) this.remotePlayerController.addEventListener( 'currentTimeChanged', - this.onCurrentTimeChanged.bind(this) + this.onCurrentTimeChanged + ) + this.remotePlayerController.addEventListener( + 'isMediaLoadedChanged', + this.isMediaLoadedChanged ) this.castButton.addEventListener('click', this.onClickOnCastButton) @@ -174,17 +181,14 @@ export default class ChromecastPlugin { this.player.on('trackenabled', this.updateSubtitle) } - onCurrentTimeChanged() { - this.player.updateProgressBar({ - seconds: this.remotePlayer.currentTime, - duration: this.remotePlayer.duration, - isRemote: true - }) - } - + /** + * On cast state change + * Chromecast event + */ onCastStateChange(e: CastEvent) { - console.log('onCastStateChange', e) - switch (e.sessionState) { + const sessionState = e.sessionState + + switch (sessionState) { case window.cast.framework.SessionState.SESSION_STARTED: this.onSessionStart() break @@ -197,14 +201,65 @@ export default class ChromecastPlugin { } } + /** + * On current time changed + * Chromecast event + */ + onCurrentTimeChanged() { + this.player.updateProgressBar({ + seconds: this.remotePlayer.currentTime, + duration: this.remotePlayer.duration, + isRemote: true + }) + } + + /** + * On click on cast button, open the cast selection UI + * @param {Event} e Event data + */ + onClickOnCastButton(e: Event) { + e.preventDefault() + this.castContext.requestSession() + } + + /** + * Update the cast subtitle + */ + updateSubtitle() { + if (!this.remotePlayer.isMediaLoaded) return + + const newLanguage = this.player.plugins.subtitle.subtitlesList + .querySelector('.v-trackButton.v-active') + .getAttribute('data-language') + + let activeTrackIds + if (newLanguage === 'off') { + activeTrackIds = [] + } else { + const newTrackIndex = this.subtitles.find(({ language }) => language === newLanguage) + if (newTrackIndex && !isNaN(newTrackIndex.index)) { + activeTrackIds = [newTrackIndex.index] + } + } + + const tracksInfoRequest = new window.chrome.cast.media.EditTracksInfoRequest(activeTrackIds) + this.getSession().getMediaSession().editTracksInfo(tracksInfoRequest) + } + + /** + * On cast session start + */ onSessionStart() { + this.subtitles = this.getSubtitles() + this.isPaused = this.player.isPaused this.player.methodPause() + this.backupAutoHide = this.player.Vlitejs.autoHideGranted this.player.Vlitejs.autoHideGranted = false this.player.Vlitejs.stopAutoHideTimer() - this.player.elements.container.classList.add('v-remote') + this.player.elements.container.classList.add('v-remote') this.castButton.classList.add('active') this.player.isChromecast = true @@ -217,6 +272,9 @@ export default class ChromecastPlugin { this.loadMedia() } + /** + * On cast session stop + */ onSessionStop() { this.player.Vlitejs.autoHideGranted = this.backupAutoHide this.backupAutoHide && this.player.Vlitejs.startAutoHideTimer() @@ -231,34 +289,31 @@ export default class ChromecastPlugin { } } - onClickOnCastButton(e: Event) { - e.preventDefault() - this.castContext.requestSession() - } - - updateSubtitle() { - const newLanguage = this.player.plugins.subtitle.subtitlesList - .querySelector('.v-trackButton.v-active') - .getAttribute('data-language') - - let activeTrackIds - if (newLanguage === 'off') { - activeTrackIds = [] - } else { - const newTrackIndex = this.subtitles.find(({ language }) => language === newLanguage) - if (newTrackIndex && !isNaN(newTrackIndex.index)) { - activeTrackIds = [newTrackIndex.index] - } - } - - const tracksInfoRequest = new window.chrome.cast.media.EditTracksInfoRequest(activeTrackIds) - this.getSession().getMediaSession().editTracksInfo(tracksInfoRequest) + /** + * Get subtitles data from the media + * @returns {Array} List of subtitles with somes fields + */ + getSubtitles(): Array { + return [...this.player.media.querySelectorAll('track')].map((track, index) => ({ + index, + url: track.getAttribute('src'), + label: track.getAttribute('label'), + language: track.getAttribute('srclang'), + isDefault: track.hasAttribute('default') + })) } - getSession() { + /** + * Get the cast session + * @returns {Object} Current cast session + */ + getSession(): any { return this.castContext.getCurrentSession() } + /** + * Load the media to the Chromecast + */ loadMedia() { const session = this.getSession() if (!session) return @@ -298,13 +353,21 @@ export default class ChromecastPlugin { if (this.subtitles.length) { loadRequest.activeTrackIds = [this.getActiveTrack().index] } - session.loadMedia(loadRequest).then(this.onLoadMediaSuccess) + session.loadMedia(loadRequest) } + /** + * Get the default track or the first one if no match + * @returns {Object} Active track + */ getActiveTrack(): Subtitle { return this.subtitles.find((item) => item.isDefault) || this.subtitles[0] } + /** + * Get the cast Track + * @returns {Array} List of cast Track + */ getCastTracks() { return this.subtitles.map(({ url, label, language }, index) => { const castTrack = new window.chrome.cast.media.Track( @@ -320,20 +383,28 @@ export default class ChromecastPlugin { }) } - onLoadMediaSuccess() { + /** + * On cast media loaded changed + * Chromecast event + */ + isMediaLoadedChanged() { this.player.on('play', () => { + if (!this.remotePlayer.isMediaLoaded) return this.remotePlayerController.playOrPause() }) this.player.on('pause', () => { + if (!this.remotePlayer.isMediaLoaded) return this.remotePlayerController.playOrPause() }) this.player.on('volumechange', () => { + if (!this.remotePlayer.isMediaLoaded) return this.player.getVolume().then((volume: number) => { this.remotePlayer.volumeLevel = this.player.isMuted ? 0 : volume this.remotePlayerController.setVolumeLevel() }) }) this.player.on('timeupdate', () => { + if (!this.remotePlayer.isMediaLoaded) return this.player.getCurrentTime().then((currentTime: number) => { this.remotePlayer.currentTime = currentTime this.remotePlayerController.seek() diff --git a/src/shared/assets/interfaces/interfaces.ts b/src/shared/assets/interfaces/interfaces.ts index 787341d2..d50fe534 100644 --- a/src/shared/assets/interfaces/interfaces.ts +++ b/src/shared/assets/interfaces/interfaces.ts @@ -1,3 +1,7 @@ +export interface Constructable { + new (...args: any): T +} + export interface FullScreenSupport { requestFn: string cancelFn: string diff --git a/src/vlite/assets/scripts/player.ts b/src/vlite/assets/scripts/player.ts index 70642e37..9859060f 100644 --- a/src/vlite/assets/scripts/player.ts +++ b/src/vlite/assets/scripts/player.ts @@ -262,7 +262,7 @@ export default class Player { }: { seconds: number duration: number - isRemote: boolean + isRemote?: boolean }) { const currentTime = Math.round(seconds) if (this.elements.progressBar) { From f17ed332c061c5dc6644157e2099c459f7cd8734 Mon Sep 17 00:00:00 2001 From: Yoriiis Date: Tue, 9 Aug 2022 20:01:38 +0200 Subject: [PATCH 12/18] Use cast event from event type object Add focus to the player on session start Remove unused code --- src/plugins/chromecast/js/chromecast.ts | 9 +++++++-- src/plugins/subtitle/js/subtitle.ts | 3 --- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/plugins/chromecast/js/chromecast.ts b/src/plugins/chromecast/js/chromecast.ts index f905d3ec..3eb54bfe 100644 --- a/src/plugins/chromecast/js/chromecast.ts +++ b/src/plugins/chromecast/js/chromecast.ts @@ -36,6 +36,10 @@ declare global { CastContextEventType: { SESSION_STATE_CHANGED: string } + RemotePlayerEventType: { + CURRENT_TIME_CHANGED: string + IS_MEDIA_LOADED_CHANGED: string + } SessionState: { SESSION_STARTED: string SESSION_RESUMED: string @@ -168,11 +172,11 @@ export default class ChromecastPlugin { this.onCastStateChange ) this.remotePlayerController.addEventListener( - 'currentTimeChanged', + window.cast.framework.RemotePlayerEventType.CURRENT_TIME_CHANGED, this.onCurrentTimeChanged ) this.remotePlayerController.addEventListener( - 'isMediaLoadedChanged', + window.cast.framework.RemotePlayerEventType.IS_MEDIA_LOADED_CHANGED, this.isMediaLoadedChanged ) @@ -250,6 +254,7 @@ export default class ChromecastPlugin { * On cast session start */ onSessionStart() { + this.player.elements.container.focus() this.subtitles = this.getSubtitles() this.isPaused = this.player.isPaused diff --git a/src/plugins/subtitle/js/subtitle.ts b/src/plugins/subtitle/js/subtitle.ts index 730b7185..06267953 100644 --- a/src/plugins/subtitle/js/subtitle.ts +++ b/src/plugins/subtitle/js/subtitle.ts @@ -15,7 +15,6 @@ export interface InsertPosition { */ export default class Subtitle { player: any - trackIsEnabled: Boolean tracks: Array activeTrack!: TextTrack | null captions!: HTMLElement @@ -33,7 +32,6 @@ export default class Subtitle { */ constructor({ player }: pluginParameter) { this.player = player - this.trackIsEnabled = false this.tracks = Array.from(this.player.media.textTracks) this.subtitlesListCssTransitionDuration = 0 @@ -266,7 +264,6 @@ export default class Subtitle { */ updateCues({ isDisabled = false }: { isDisabled?: Boolean } = {}) { if (this.activeTrack && this.activeTrack.cues && this.activeTrack.cues.length) { - this.trackIsEnabled = !isDisabled const cues = Array.from(this.activeTrack.cues) const activeCues = this.activeTrack.activeCues From bd46d10a8c236a0fc9749a0af0d4064fa038311a Mon Sep 17 00:00:00 2001 From: Yoriiis Date: Thu, 11 Aug 2022 21:32:41 +0200 Subject: [PATCH 13/18] Rename chromecast to cast --- config/webpack.config.js | 6 +- examples/html5/config.js | 8 +- src/plugins/cast/README.md | 77 +++++++++++++++++++ src/plugins/cast/config.js | 7 ++ .../{chromecast => cast}/css/chromecast.css | 4 +- .../{chromecast => cast}/js/chromecast.ts | 26 +++---- src/plugins/chromecast/config.js | 7 -- src/plugins/pip/README.md | 4 +- src/plugins/subtitle/README.md | 7 +- src/vlite/assets/scripts/player.ts | 8 +- 10 files changed, 115 insertions(+), 39 deletions(-) create mode 100644 src/plugins/cast/README.md create mode 100644 src/plugins/cast/config.js rename src/plugins/{chromecast => cast}/css/chromecast.css (90%) rename src/plugins/{chromecast => cast}/js/chromecast.ts (95%) delete mode 100644 src/plugins/chromecast/config.js diff --git a/config/webpack.config.js b/config/webpack.config.js index fcd74b36..5f920fe2 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -48,9 +48,9 @@ const plugins = [ path: './src/plugins/pip/config' }, { - entrykey: 'plugins/chromecast', - library: `${libraryName}Chromecast`, - path: './src/plugins/chromecast/config' + entrykey: 'plugins/cast', + library: `${libraryName}Cast`, + path: './src/plugins/cast/config' } ] diff --git a/examples/html5/config.js b/examples/html5/config.js index f8c2ffdc..a3f817cf 100644 --- a/examples/html5/config.js +++ b/examples/html5/config.js @@ -1,14 +1,14 @@ import '../../dist/vlite.css' import '../../dist/plugins/subtitle.css' -import '../../dist/plugins/chromecast.css' +import '../../dist/plugins/cast.css' import Vlitejs from '../../dist/vlite.js' import VlitejsSubtitle from '../../dist/plugins/subtitle.js' import VlitejsPip from '../../dist/plugins/pip.js' -import VlitejsChromecast from '../../dist/plugins/chromecast.js' +import VlitejsCast from '../../dist/plugins/cast.js' Vlitejs.registerPlugin('subtitle', VlitejsSubtitle) Vlitejs.registerPlugin('pip', VlitejsPip) -Vlitejs.registerPlugin('chromecast', VlitejsChromecast, { +Vlitejs.registerPlugin('cast', VlitejsCast, { textTrackStyle: { backgroundColor: '#21212190' }, @@ -35,7 +35,7 @@ const vlite = new Vlitejs('#player', { muted: false, autoHide: true }, - plugins: ['subtitle', 'pip', 'chromecast'], + plugins: ['subtitle', 'pip', 'cast'], onReady: function (player) { console.log(player) diff --git a/src/plugins/cast/README.md b/src/plugins/cast/README.md new file mode 100644 index 00000000..9570a42c --- /dev/null +++ b/src/plugins/cast/README.md @@ -0,0 +1,77 @@ +# Plugin: Cast + +Supports for Google Cast API. + +## Overview + +| | | +| ----------------- | ----------------------------------- | +| Name | `cast` | +| Global name¹ | `window.VlitejsCast` | +| Path | `vlitejs/dist/plugins/cast` | +| Entry point | `vlitejs/dist/plugins/cast/cast.js` | +| Stylesheet | - | +| Provider² | `'html5'` | +| Media type³ | `'video'` | + +- _¹ Useful only if `vLitejs` is included with a `