+
+
[[ _getParagraphText(indicator.text, localize) ]]
-
+
@@ -159,6 +159,10 @@ class IndicatorBlock extends BindingHelpersMixin(IocRequesterMixin(LocalizationM
];
}
+ _stopPropagation(e) {
+ e.stopPropagation();
+ }
+
_indicatorChanged(indicator) {
if(indicator.initialFocus) {
// Show tooltip, wait for browser to apply, then focus, and hide again. Tooltip will stay open since we have it focussed.
diff --git a/src/components/progress-container/video-progress.js b/src/components/progress-container/video-progress.js
index 8771ee37..0dc53ab3 100644
--- a/src/components/progress-container/video-progress.js
+++ b/src/components/progress-container/video-progress.js
@@ -12,7 +12,9 @@ class VideoProgress extends BindingHelpersMixin(IocRequesterMixin(PolymerElement
-
+
`;
@@ -32,9 +34,14 @@ class VideoProgress extends BindingHelpersMixin(IocRequesterMixin(PolymerElement
type: Object,
inject: 'AnalyticsManager',
},
+ _customProgressPosition: {
+ type: Number,
+ value: 0,
+ },
_position: {
type: Number,
computed: '_getPosition(state.position, state.trimStart, state.trimEnd, state.live, state.liveSync, state.liveStartPosition, state.livePosition)',
+ observer: '_positionChanged',
},
_bufferPosition: {
type: Number,
@@ -44,9 +51,22 @@ class VideoProgress extends BindingHelpersMixin(IocRequesterMixin(PolymerElement
type: Number,
computed: '_getDuration(state.duration, state.trimStart, state.trimEnd, state.live, state.liveStartPosition, state.livePosition)',
},
+ _insideDragOperation: {
+ type: Boolean,
+ value: false,
+ },
};
}
+ _positionChanged(newValue) {
+ if (this._insideDragOperation) {
+ // Do not update the position of the custom progress bar if the user is dragging it.
+ return;
+ }
+
+ this.set('_customProgressPosition', newValue);
+ }
+
_getPosition(position, trimStart, trimEnd, live, liveSync, liveStartPosition, livePosition) {
if(live) {
if(liveSync) {
@@ -66,7 +86,24 @@ class VideoProgress extends BindingHelpersMixin(IocRequesterMixin(PolymerElement
return trimEnd - trimStart;
}
+ _handleDrag() {
+ this._insideDragOperation = true;
+ }
+
+ _handleDrop(e) {
+ this._insideDragOperation = false;
+ this._analyticsManager.changeState('setPosition', [e.detail.position], {verb: ANALYTICS_TOPICS.VIDEO_SEEK});
+ }
+
_handleChange(e) {
+ if (this._insideDragOperation) {
+ // If the user is dragging, we do not want to update the video position
+ // on every frame. Otherwise, dragging the progress slides from start to
+ // end will go through all positions in the video and the browser will
+ // start caching all positions.
+ return;
+ }
+
let position;
if(this.state.live)
position = this.state.liveStartPosition + e.target.value;
diff --git a/src/components/video-stream.js b/src/components/video-stream.js
index c23a2644..d8501a12 100644
--- a/src/components/video-stream.js
+++ b/src/components/video-stream.js
@@ -183,7 +183,15 @@ class VideoStream extends BindingHelpersMixin(IocRequesterMixin(PolymerElement))
return;
}
- if(Math.abs(this.$.video.currentTime - position) > SEEK_DIFF_THRESHOLD) {
+ const positionChanged = this.$.video.currentTime !== position;
+ if(!positionChanged) {
+ return false;
+ }
+
+ const seekDiffThesholdMet = Math.abs(this.$.video.currentTime - position) > SEEK_DIFF_THRESHOLD;
+ const paused = this.state.playState === PLAY_STATES.PAUSED;
+
+ if(paused || seekDiffThesholdMet) {
this.$.video.currentTime = position;
return true;
}
diff --git a/src/configuration-schema.js b/src/configuration-schema.js
index ecc607af..dcd2f218 100644
--- a/src/configuration-schema.js
+++ b/src/configuration-schema.js
@@ -35,7 +35,6 @@ const stateSchema = {
},
playbackRate: {
type: 'number',
- options: [0.7, 1.0, 1.3, 1.5, 1.8, 2.0],
default: 1,
},
quality: {
@@ -96,6 +95,11 @@ export const configurationSchema = {
description: 'Contains a fallback stream that the user can switch to, i.e. a single stream source.',
schema: streamSchema,
},
+ framesPerSecond: {
+ type: 'number',
+ default: 25,
+ description: 'Fps of the video. Used for skipping frame wise.',
+ },
language: {
type: 'string',
default: 'en',
@@ -106,6 +110,11 @@ export const configurationSchema = {
description: 'The initial state the player has when loaded.',
schema: stateSchema,
},
+ playbackRates: {
+ type: 'array',
+ default: [0.7, 1.0, 1.3, 1.5, 1.8, 2.0],
+ description: 'The playback rates the user can choose from in the selection menu',
+ },
userPreferences: {
type: 'object',
schema: stateSchema,
diff --git a/src/constants.js b/src/constants.js
index ab2fb3e1..9cad6ca7 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -16,12 +16,6 @@ export const PLAY_STATES = {
FINISHED: 'FINISHED',
};
-/**
- * Contains the supported playback rates sorted ascending.
- * @type {Number[]}
- */
-export const PLAYBACK_RATES = [0.7, 1.0, 1.3, 1.5, 1.8, 2.0];
-
/**
* Contains the supported quality modes.
* @type {Object.
}
@@ -120,6 +114,7 @@ export const DEFAULT_STATE = {
*/
export const DEFAULT_CONFIGURATION = {
streams: [],
+ framesPerSecond: 25,
chapters: [],
captions: [],
slides: [],
@@ -129,6 +124,7 @@ export const DEFAULT_CONFIGURATION = {
questions: [],
},
initialState: {},
+ playbackRates: [0.7, 1.0, 1.3, 1.5, 1.8, 2.0],
videoPreload: true,
liveDvr: false,
theme: 'dark-orange',
@@ -204,6 +200,7 @@ export const USER_PREFERENCES_KEYS = [
'showCaptions',
'showInteractiveTranscript',
'resizerRatios',
+ 'isQuizOverlayEnabled',
];
/**
diff --git a/src/services/ioc-kernel.js b/src/services/ioc-kernel.js
index 7ae62ddc..7429de65 100644
--- a/src/services/ioc-kernel.js
+++ b/src/services/ioc-kernel.js
@@ -34,7 +34,7 @@ export class IocKernel {
* @return {Boolean} A value determining whether the kernel can resolve the service request or not.
*/
has(keyExpression) {
- return this._bindings.hasOwnProperty(this._getKey(keyExpression));
+ return Object.prototype.hasOwnProperty.call(this._bindings, this._getKey(keyExpression));
}
/**
diff --git a/src/services/state-manager.js b/src/services/state-manager.js
index 0207ac1d..89630237 100644
--- a/src/services/state-manager.js
+++ b/src/services/state-manager.js
@@ -1,4 +1,4 @@
-import {PLAY_STATES, PLAYBACK_RATES, CAPTION_TYPES} from '../constants.js';
+import {PLAY_STATES, CAPTION_TYPES} from '../constants.js';
export class StateManager {
/**
@@ -155,6 +155,16 @@ export class StateManager {
this.setPosition(position);
}
+ /**
+ * Skips a number of frames from the current position, assuming the fps provided in the configuration.
+ * @param {number} frames The number of frames to skip.
+ * @returns {void}
+ */
+ skipFrames(frames) {
+ const seconds = frames / this.configuration.framesPerSecond;
+ this.skipSeconds(seconds);
+ }
+
/**
* Sets the current buffer position of the videos.
* @param {number} seconds The current buffer position in seconds.
@@ -238,15 +248,47 @@ export class StateManager {
* @returns {void}
*/
setPlaybackRate(playbackRate) {
- if(!PLAYBACK_RATES.includes(playbackRate)) {
- throw new RangeError(`Value must be in [${PLAYBACK_RATES.toString()}].`);
- }
-
if(!this.state.live) {
this.setState('playbackRate', playbackRate);
}
}
+ /**
+ * Increases the playback rate of the video to the next entry in configuration.playbackRates.
+ * @returns {void}
+ */
+ increasePlaybackRate() {
+ this.switchToNextPlaybackRate(1);
+ }
+
+ /**
+ * Decreases the playback rate of the video to the next entry in configuration.playbackRates.
+ * @returns {void}
+ */
+ decreasePlaybackRate() {
+ this.switchToNextPlaybackRate(-1);
+ }
+
+ /**
+ * Sets the playback rate relatively to the currently selected playback rate.
+ * @param {number} offset The offset from the currently selected playback rate to be applied.
+ * @returns {void}
+ */
+ switchToNextPlaybackRate(offset) {
+ const playbackRates = Array.from(this.configuration.playbackRates).sort();
+
+ const distancesToCurrentRate = playbackRates.map(
+ el => Math.abs(el - this.state.playbackRate)
+ );
+
+ const currentIndex = distancesToCurrentRate.indexOf(Math.min(...distancesToCurrentRate));
+ const newIndex = currentIndex + offset;
+ const clampedNewIndex = Math.max(0, Math.min(playbackRates.length - 1, newIndex));
+ const newPlaybackRate = playbackRates[clampedNewIndex];
+
+ this.setPlaybackRate(newPlaybackRate);
+ }
+
/**
* Toggles the muting.
* @returns {void}
diff --git a/src/styling/global--style-module.js b/src/styling/global--style-module.js
index a225155b..f754dce4 100644
--- a/src/styling/global--style-module.js
+++ b/src/styling/global--style-module.js
@@ -11,6 +11,19 @@ documentContainer.innerHTML = `
.-hidden {
display: none !important;
}
+
+ button {
+ outline: none;
+ border: none;
+ background: none;
+ color: inherit;
+
+ font-size: inherit;
+ font-family: inherit;
+ line-height: inherit;
+ margin: inherit;
+ padding: 0;
+ }
`;
diff --git a/src/video-player.js b/src/video-player.js
index 6da2d18c..a084ffbd 100644
--- a/src/video-player.js
+++ b/src/video-player.js
@@ -152,7 +152,7 @@ class VideoPlayer extends BindingHelpersMixin(IocRequesterMixin(IocProviderMixin
-
+
@@ -329,6 +329,13 @@ class VideoPlayer extends BindingHelpersMixin(IocRequesterMixin(IocProviderMixin
if(startPosition) {
this.configuration.initialState.position = startPosition;
}
+
+ window.addEventListener('hashchange', () => {
+ let position = parseInt(UrlFragmentHelper.getParameter('t'));
+ if (position) {
+ this.seek(position);
+ }
+ }, false);
}
// Build state from provided intial state configuration and default values
diff --git a/test/video-player_test.html b/test/video-player_test.html
index 4e72edd4..9a98a307 100644
--- a/test/video-player_test.html
+++ b/test/video-player_test.html
@@ -40,6 +40,7 @@
"sd" : "test-files/BigBuckBunny30s_fallback.mp4",
"poster" : "test-files/poster_1.jpg"
},
+ "playbackRates": [1.0, 1.1, 1.2, 1.3],
"captions" : [
{
"language" : "de",
@@ -196,13 +197,11 @@
test('testing speed control', () => {
// This one is weird. We would want to do a CSS selector at the end with
- // value "#dropdown__select [name=1.5x]", just like above, but apparently
- // this is "not a valid selector". Instead we just hope those values
- // never change and expect 1.5 to be the 3rd item.
- // We test for the value '1.5x' beforehand.
- assert.include(videoPlayer.shadowRoot.querySelectorAll('control-bar')[0].shadowRoot.querySelectorAll('speed-control')[0].shadowRoot.querySelectorAll('select-control')[0].shadowRoot.querySelectorAll('#dropdown__select [name]')[2].innerText, '1.5x', 'workaround for small testing hack: \'1.5x\' should be the third item in the list');
+ // value "#dropdown__select [name=1.1x]", just like above, but apparently
+ // this is "not a valid selector".
+ assert.include(videoPlayer.shadowRoot.querySelectorAll('control-bar')[0].shadowRoot.querySelectorAll('speed-control')[0].shadowRoot.querySelectorAll('select-control')[0].shadowRoot.querySelectorAll('#dropdown__select [name]')[2].innerText, '1.1x', '\'1.1x\' should be the third item in the list');
videoPlayer.shadowRoot.querySelectorAll('control-bar')[0].shadowRoot.querySelectorAll('speed-control')[0].shadowRoot.querySelectorAll('select-control')[0].shadowRoot.querySelectorAll('#dropdown__select [name]')[2].click();
- assert.equal(videoPlayer.state.playbackRate, 1.5);
+ assert.equal(videoPlayer.state.playbackRate, 1.1);
});
test('testing quality control', () => {
@@ -219,7 +218,7 @@
assert.include(Array.from(videoPlayer.shadowRoot.querySelectorAll('playlist-chapter-list')[0].classList), '-hidden', 'chapter list is hidden by default');
videoPlayer.shadowRoot.querySelectorAll('control-bar')[0].shadowRoot.querySelectorAll('playlist-chapter-list-switch')[0].shadowRoot.querySelectorAll('#button__chapter_list')[0].click();
assert.notInclude(Array.from(videoPlayer.shadowRoot.querySelectorAll('playlist-chapter-list')[0].classList), '-hidden', 'chapter list is not hidden anymore after activating it');
- videoPlayer.shadowRoot.querySelectorAll('playlist-chapter-list')[0].shadowRoot.querySelectorAll('#container__playlist_chapter_list .list_item.sublist_item a')[1].click();
+ videoPlayer.shadowRoot.querySelectorAll('playlist-chapter-list')[0].shadowRoot.querySelectorAll('#container__playlist_chapter_list .list_item.sublist_item button')[1].click();
assert.equal(videoPlayer.state.position, 15, 'position value is at value of second chapter');
});
diff --git a/utils/build-hls-js.sh b/utils/build-hls-js.sh
index 10fed2cb..284f3b6f 100755
--- a/utils/build-hls-js.sh
+++ b/utils/build-hls-js.sh
@@ -1,14 +1,20 @@
# Hls.js is shipped as UMD modules that may cause compatibility issues if the surrounding website uses a modules loader, such as require.js
# Therefore, hls.js is rebuilt with output library target window to ensure it is globally available.
+# See https://github.com/video-dev/hls.js/issues/2910 on status of an ES6 module build of hls.js
echo 'Rebuilding hls.js...'
cd node_modules/hls.js
-npm install --silent
-node_modules/.bin/webpack --config-name debug --output-library-target window --display errors-only
-#node_modules/.bin/webpack --config-name dist --output-library-target window --display errors-only
-node_modules/.bin/webpack --config-name light --output-library-target window --display errors-only
-#node_modules/.bin/webpack --config-name light-dist --output-library-target window --display errors-only
+npm install --legacy-peer-deps --silent
+
+# For some reason, they decided to not include their build config in their npm packages anymore
+# see https://github.com/video-dev/hls.js/commit/083322b49e3c09ae35a33e553fe79343cf9e68dd
+wget -nc https://raw.githubusercontent.com/video-dev/hls.js/v1.0.11/webpack.config.js
+sed -i "s/'umd'/'window'/g" webpack.config.js
+
+npx webpack --stats errors-only --env debug light
+
tail -n +2 dist/hls.js > temp
cat temp > dist/hls.js
tail -n +2 dist/hls.light.js > temp
cat temp > dist/hls.light.js
-rm temp
\ No newline at end of file
+
+rm temp