diff --git a/docs/es-modules/index.html b/docs/es-modules/index.html
index eae76ca39..5bb92eb3a 100644
--- a/docs/es-modules/index.html
+++ b/docs/es-modules/index.html
@@ -73,6 +73,7 @@
Code examples:
Subtitles & Captions
Video Transformations
VAST & VPAID Support
+ Visual Search
VR/360 Videos
/all build
diff --git a/docs/es-modules/visual-search.html b/docs/es-modules/visual-search.html
new file mode 100644
index 000000000..73971698a
--- /dev/null
+++ b/docs/es-modules/visual-search.html
@@ -0,0 +1,69 @@
+
+
+
+
+ Cloudinary Video Player
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/index.html b/docs/index.html
index bd4e16c96..f7d3e190f 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -79,6 +79,7 @@ Some code examples:
Subtitles & Captions
Video Transformations
VAST & VPAID Support
+ Visual Search
VR/360 Videos
Embedded (iframe) player
diff --git a/docs/visual-search.html b/docs/visual-search.html
new file mode 100644
index 000000000..175f04195
--- /dev/null
+++ b/docs/visual-search.html
@@ -0,0 +1,147 @@
+
+
+
+
+ Cloudinary Video Player - Visual Search
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Cloudinary Video Player
+
Visual Search Plugin
+
+
+
+
+
+
Playlist with mixed content
+
+
+
+
+ Full documentation
+
+
+
Example Code:
+
+
+ <video
+ id="player"
+ playsinline
+ controls
+ class="cld-video-player cld-fluid"
+ width="500"
+ ></video>
+
+
+ // Initialize player with visual search plugin
+ const player = cloudinary.videoPlayer('player', {
+ cloudName: 'demo',
+ publicId: 'elephants',
+ visualSearch: true
+ });
+
+
+
+
+
+
diff --git a/src/assets/icon-font/README.md b/src/assets/icon-font/README.md
index caae8d76f..7e67054a1 100644
--- a/src/assets/icon-font/README.md
+++ b/src/assets/icon-font/README.md
@@ -3,3 +3,7 @@
## How to generate an updated icon-font
Use the utility from https://github.com/videojs/font with the custom icons in the `cld` folder and the configuration in the `icons.json` provided here.
+
+MUI3 icons can be found [here](https://github.com/google/material-design-icons/blob/3.0.2/sprites/css-sprite/sprite-action-black.png).
+
+Copy the generated `videojs-icons.scss` file to the `styles` folder.
diff --git a/src/assets/icon-font/VideoJS.svg b/src/assets/icon-font/VideoJS.svg
index 613c6bacc..1b27e37df 100755
--- a/src/assets/icon-font/VideoJS.svg
+++ b/src/assets/icon-font/VideoJS.svg
@@ -2,158 +2,146 @@
diff --git a/src/assets/icon-font/VideoJS.ttf b/src/assets/icon-font/VideoJS.ttf
index 1969881c3..53b10bc14 100755
Binary files a/src/assets/icon-font/VideoJS.ttf and b/src/assets/icon-font/VideoJS.ttf differ
diff --git a/src/assets/icon-font/VideoJS.woff b/src/assets/icon-font/VideoJS.woff
index ffb4e69df..1b07ea611 100755
Binary files a/src/assets/icon-font/VideoJS.woff and b/src/assets/icon-font/VideoJS.woff differ
diff --git a/src/assets/icon-font/icons.json b/src/assets/icon-font/icons.json
index 50ab51cfd..04f887e06 100644
--- a/src/assets/icon-font/icons.json
+++ b/src/assets/icon-font/icons.json
@@ -148,25 +148,8 @@
"name": "close",
"svg": "navigation/svg/production/ic_close_48px.svg"
}, {
- "name": "facebook",
- "svg": "facebook.svg",
- "root-dir": "./custom-icons/fontawesome/"
- }, {
- "name": "linkedin",
- "svg": "linkedin.svg",
- "root-dir": "./custom-icons/fontawesome/"
- }, {
- "name": "twitter",
- "svg": "twitter.svg",
- "root-dir": "./custom-icons/fontawesome/"
- }, {
- "name": "tumblr",
- "svg": "tumblr.svg",
- "root-dir": "./custom-icons/fontawesome/"
- }, {
- "name": "pinterest",
- "svg": "pinterest.svg",
- "root-dir": "./custom-icons/fontawesome/"
+ "name": "search",
+ "svg": "action/svg/production/ic_search_48px.svg"
}, {
"name": "audio-description",
"svg": "audio-description.svg",
diff --git a/src/assets/styles/_icons.scss b/src/assets/styles/_icons.scss
index 327f9dfd5..488792cb6 100644
--- a/src/assets/styles/_icons.scss
+++ b/src/assets/styles/_icons.scss
@@ -13,7 +13,7 @@ $icon-font-family: VideoJS;
@font-face {
font-family: $icon-font-family;
- src: url(data:application/font-woff;charset=utf-8;base64,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) format('woff');
+ src: url(data:application/font-woff;charset=utf-8;base64,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) format('woff');
font-weight: normal;
font-style: normal;
}
@@ -61,14 +61,10 @@ $icons: (
picture-in-picture-enter: 'f127',
picture-in-picture-exit: 'f128',
close: 'f129',
- facebook: 'f12a',
- linkedin: 'f12b',
- twitter: 'f12c',
- tumblr: 'f12d',
- pinterest: 'f12e',
- audio-description: 'f12f',
- cart: 'f130',
- check: 'f131',
+ search: 'f12a',
+ audio-description: 'f12b',
+ cart: 'f12c',
+ check: 'f12d',
);
// NOTE: This is as complex as we want to get with SCSS functionality.
diff --git a/src/config/defaults.js b/src/config/defaults.js
index 70f664f9e..32b868bb7 100644
--- a/src/config/defaults.js
+++ b/src/config/defaults.js
@@ -13,6 +13,7 @@ export default {
pictureInPictureToggle: false,
seekThumbnails: true,
aiHighlightsGraph: false,
+ visualSearch: false,
preload: PRELOAD.AUTO,
textTrackSettings: false,
loop: false,
diff --git a/src/plugins/chapters/chapters.js b/src/plugins/chapters/chapters.js
index 4b4c102a4..418d53772 100644
--- a/src/plugins/chapters/chapters.js
+++ b/src/plugins/chapters/chapters.js
@@ -193,7 +193,7 @@ const ChaptersPlugin = (function () {
const getChapterFromPoint = point => {
const total = this.player.duration();
const seekBarTime = point * total;
- const chapter = Array.from(this.chaptersTrack.cues).find(marker => {
+ const chapter = Array.from(this.chaptersTrack?.cues || []).find(marker => {
return seekBarTime >= marker.startTime && seekBarTime <= marker.endTime;
});
return chapter ? chapter.text : '';
diff --git a/src/plugins/cloudinary/models/video-source/video-source.js b/src/plugins/cloudinary/models/video-source/video-source.js
index 1abcdbedb..d4dab2c94 100644
--- a/src/plugins/cloudinary/models/video-source/video-source.js
+++ b/src/plugins/cloudinary/models/video-source/video-source.js
@@ -46,7 +46,8 @@ class VideoSource extends BaseSource {
textTracks,
withCredentials,
interactionAreas,
- chapters
+ chapters,
+ visualSearch
} = sliceAndUnsetProperties(
options,
'poster',
@@ -57,7 +58,8 @@ class VideoSource extends BaseSource {
'textTracks',
'withCredentials',
'interactionAreas',
- 'chapters'
+ 'chapters',
+ 'visualSearch'
);
super(publicId, options);
@@ -70,6 +72,7 @@ class VideoSource extends BaseSource {
this._sourceTransformation = null;
this._interactionAreas = null;
this._chapters = null;
+ this._visualSearch = null;
this._type = SOURCE_TYPE.VIDEO;
this.isRawUrl = _isRawUrl;
this.isLiveStream = options.type === 'live';
@@ -83,6 +86,7 @@ class VideoSource extends BaseSource {
this.info(info);
this.interactionAreas(interactionAreas);
this.chapters(chapters);
+ this.visualSearch(visualSearch);
this.recommendations(recommendations);
this.textTracks(textTracks);
this.objectId = generateId();
@@ -147,6 +151,16 @@ class VideoSource extends BaseSource {
return this;
}
+ visualSearch(visualSearch) {
+ if (!visualSearch) {
+ return this._visualSearch;
+ }
+
+ this._visualSearch = visualSearch;
+
+ return this;
+ }
+
sourceTransformation(trans) {
if (!trans) {
return this._sourceTransformation;
diff --git a/src/plugins/index.js b/src/plugins/index.js
index 2a47de3df..058360da5 100644
--- a/src/plugins/index.js
+++ b/src/plugins/index.js
@@ -19,6 +19,7 @@ import imaPlugin from './ima';
import interactionAreas from './interaction-areas';
import playlist from './playlist';
import shoppable from './shoppable-plugin';
+import visualSearch from './visual-search';
const plugins = {
aiHighlightsGraph,
@@ -39,7 +40,8 @@ const plugins = {
shoppable,
srtTextTracks,
styledTextTracks,
- interactionAreas
+ interactionAreas,
+ visualSearch
};
export default plugins;
diff --git a/src/plugins/visual-search/components/SearchButton.js b/src/plugins/visual-search/components/SearchButton.js
new file mode 100644
index 000000000..3ddaf2790
--- /dev/null
+++ b/src/plugins/visual-search/components/SearchButton.js
@@ -0,0 +1,22 @@
+import videojs from 'video.js';
+
+export const SearchButton = (onClick) => {
+ const button = videojs.dom.createEl('button', {
+ className: 'vjs-visual-search-button',
+ title: 'Search video content'
+ });
+
+ const searchIcon = videojs.dom.createEl('span', {
+ className: 'vjs-icon-search'
+ });
+ button.appendChild(searchIcon);
+
+ const spinnerIcon = videojs.dom.createEl('span', {
+ className: 'vjs-loading-spinner'
+ });
+ button.appendChild(spinnerIcon);
+
+ button.addEventListener('click', onClick);
+
+ return button;
+};
diff --git a/src/plugins/visual-search/components/SearchInput.js b/src/plugins/visual-search/components/SearchInput.js
new file mode 100644
index 000000000..4f969bb56
--- /dev/null
+++ b/src/plugins/visual-search/components/SearchInput.js
@@ -0,0 +1,49 @@
+import videojs from 'video.js';
+
+export const SearchInput = (onSearch, onClose) => {
+ const form = videojs.dom.createEl('form', {
+ className: 'vjs-visual-search-form'
+ });
+
+ const input = videojs.dom.createEl('input', {
+ className: 'vjs-visual-search-input',
+ type: 'text'
+ });
+
+ const closeButton = videojs.dom.createEl('button', {
+ className: 'vjs-visual-search-close',
+ type: 'button',
+ title: 'Close search'
+ });
+
+ // Add close icon
+ const closeIcon = videojs.dom.createEl('span', {
+ className: 'vjs-icon-close'
+ });
+ closeButton.appendChild(closeIcon);
+
+ form.appendChild(input);
+ form.appendChild(closeButton);
+
+ // Handle search submission
+ form.addEventListener('submit', (e) => {
+ e.preventDefault();
+ const query = input.value.trim();
+ if (query) {
+ onSearch(query);
+ }
+ });
+
+ // Handle close button
+ closeButton.addEventListener('click', (e) => {
+ e.preventDefault();
+ if (onClose) {
+ onClose();
+ }
+ });
+
+ return {
+ element: form,
+ input
+ };
+};
diff --git a/src/plugins/visual-search/components/SearchResults.js b/src/plugins/visual-search/components/SearchResults.js
new file mode 100644
index 000000000..6e9396cd1
--- /dev/null
+++ b/src/plugins/visual-search/components/SearchResults.js
@@ -0,0 +1,53 @@
+import videojs from 'video.js';
+
+export const SearchResults = (player) => {
+ const clearMarkers = () => {
+ player.$$('.vjs-visual-search-marker').forEach(el => el.remove());
+ player.$$('.vjs-visual-search-results-wrapper').forEach(el => el.remove());
+
+ // Remove the class that indicates search results are displayed
+ player.removeClass('vjs-visual-search-results-active');
+ };
+
+ const displayResults = (results) => {
+ // Clear existing markers
+ clearMarkers();
+
+ const total = player.duration();
+ const seekBar = player.controlBar.progressControl.seekBar;
+
+ // Create wrapper for search results
+ const wrapperEl = videojs.dom.createEl('div', {
+ className: 'vjs-visual-search-results-wrapper'
+ });
+
+ // Add markers for each result
+ results.forEach(result => {
+ const { start_time, end_time } = result;
+ const markerEl = videojs.dom.createEl('div', {
+ className: 'vjs-visual-search-marker',
+ style: `left: ${(start_time / total) * 100}%; width: ${((end_time - start_time) / total) * 100}%`
+ });
+
+ wrapperEl.appendChild(markerEl);
+
+ // Add click handler to jump to this time
+ markerEl.addEventListener('click', () => {
+ player.currentTime(start_time);
+ });
+ });
+
+ // Add wrapper to seek bar
+ seekBar.el().appendChild(wrapperEl);
+
+ // Add a class to indicate search results are displayed
+ if (results.length > 0) {
+ player.addClass('vjs-visual-search-results-active');
+ }
+ };
+
+ return {
+ displayResults,
+ clearMarkers
+ };
+};
diff --git a/src/plugins/visual-search/index.js b/src/plugins/visual-search/index.js
new file mode 100644
index 000000000..bab095131
--- /dev/null
+++ b/src/plugins/visual-search/index.js
@@ -0,0 +1,9 @@
+export default async function lazyVisualSearchPlugin(options) {
+ const player = this;
+ try {
+ const { default: initPlugin } = await import(/* webpackChunkName: "visual-search" */ './visual-search');
+ player.ready(() => initPlugin(options, player));
+ } catch (error) {
+ console.error('Failed to load plugin:', error);
+ }
+}
diff --git a/src/plugins/visual-search/visual-search.js b/src/plugins/visual-search/visual-search.js
new file mode 100644
index 000000000..b2d310992
--- /dev/null
+++ b/src/plugins/visual-search/visual-search.js
@@ -0,0 +1,119 @@
+import videojs from 'video.js';
+import { SearchButton } from './components/SearchButton';
+import { SearchInput } from './components/SearchInput';
+import { SearchResults } from './components/SearchResults';
+
+import './visual-search.scss';
+
+const visualSearch = (options, player) => {
+ player.addClass('vjs-visual-search');
+
+ let isSearchActive = false;
+
+ const searchResults = SearchResults(player);
+
+ const performSearch = async query => {
+ const searchButton = player.$('.vjs-visual-search-button');
+ searchButton.classList.add('vjs-waiting');
+
+ try {
+ const source = player.cloudinary.source();
+ const publicId = source.publicId();
+ const transformation = Object.assign({}, source.transformation());
+
+ transformation.flags = transformation.flags || [];
+ transformation.flags.push(`getinfo:search_b64_${btoa(query)}`);
+
+ const visualSearchSrc = source.config().url(`${publicId}`, { transformation });
+
+ const response = await fetch(visualSearchSrc, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`Search request failed with status: ${response.status}`);
+ }
+
+ const results = await response.json();
+ searchResults.displayResults(results.timestamps);
+
+ if (results && !player.hasStarted()) {
+ // Make sure the progress bar is visible
+ player.play().then(() => player.pause());
+ }
+ } catch (error) {
+ console.error('Error performing visual search:', error);
+ } finally {
+ searchButton.classList.remove('vjs-waiting');
+ }
+ };
+
+ const clearUI = () => {
+ isSearchActive = false;
+ searchResults.clearMarkers();
+ player.$('.vjs-visual-search-wrapper')?.remove();
+ };
+
+ const createSearchUI = () => {
+ clearUI();
+
+ const titleBar = player.$('.vjs-title-bar');
+ if (titleBar) {
+ titleBar.classList.remove('vjs-hidden');
+ }
+
+ const searchContainer = videojs.dom.createEl('div', {
+ className: 'vjs-visual-search-wrapper'
+ });
+
+ // Handle the search icon click (expand or submit)
+ const handleSearchButtonClick = () => {
+ if (!isSearchActive) {
+ isSearchActive = true;
+ searchContainer.classList.add('vjs-visual-search-active');
+ searchInput.input.focus();
+ } else {
+ const query = searchInput.input.value.trim();
+ if (query) {
+ performSearch(query);
+ }
+ }
+ };
+
+ const closeSearch = () => {
+ if (isSearchActive) {
+ isSearchActive = false;
+ searchContainer.classList.remove('vjs-visual-search-active');
+ searchInput.input.value = '';
+
+ searchResults.clearMarkers();
+ }
+ };
+
+ const searchButton = SearchButton(handleSearchButtonClick);
+ const searchInput = SearchInput(performSearch, closeSearch);
+ searchContainer.appendChild(searchButton);
+ searchContainer.appendChild(searchInput.element);
+
+ titleBar.prepend(searchContainer);
+
+ player.on('keydown', e => {
+ if (e.key === 'Escape' && isSearchActive) {
+ closeSearch();
+ }
+ });
+ };
+
+ createSearchUI();
+
+ // Public methods
+ player.visualSearch = {
+ createSearchUI,
+ clearUI
+ };
+};
+
+export default visualSearch;
diff --git a/src/plugins/visual-search/visual-search.scss b/src/plugins/visual-search/visual-search.scss
new file mode 100644
index 000000000..78b0cab05
--- /dev/null
+++ b/src/plugins/visual-search/visual-search.scss
@@ -0,0 +1,179 @@
+.vjs-visual-search {
+ .vjs-visual-search-wrapper {
+ position: absolute;
+ top: 1.5em;
+ right: 1.5em;
+ margin-left: auto;
+ display: flex;
+ align-items: center;
+ transition: all 0.3s ease;
+ border-radius: 1.5em;
+ overflow: hidden;
+ background-color: transparent;
+ pointer-events: auto;
+
+ &.vjs-visual-search-active {
+ background-color: color-mix(in srgb, var(--color-base) 60%, transparent);
+ backdrop-filter: blur(10px);
+
+ .vjs-visual-search-form {
+ width: 13.75em;
+ margin-right: 0.25em;
+ opacity: 1;
+ }
+
+ .vjs-visual-search-button {
+ background-color: transparent;
+ text-shadow: none;
+
+ &:hover {
+ text-shadow: 0 0 0.5em var(--color-accent);
+ }
+ }
+ }
+
+ &:hover:not(.vjs-visual-search-active) {
+ .vjs-visual-search-button {
+ background-color: color-mix(in srgb, var(--color-base) 25%, transparent);
+ }
+ }
+
+ + .vjs-title-bar-title {
+ padding-right: 2em;
+ }
+ }
+
+ .vjs-visual-search-button {
+ background: transparent;
+ border: none;
+ color: var(--color-text);
+ cursor: pointer;
+ width: 2.75em;
+ height: 2.75em;
+ padding: 0.1em;
+ opacity: 0.9;
+ border-radius: 50%;
+ transition: all 0.25s ease;
+ z-index: 2;
+ flex-shrink: 0;
+ text-shadow: 0 0 1em var(--color-base);
+
+ &:hover {
+ opacity: 1;
+ }
+
+ > span:before {
+ font-size: 1.8em;
+ }
+
+ .vjs-loading-spinner {
+ display: none;
+ width: 2.15em;
+ height: 2.15em;
+ position: absolute;
+ top: 0.3em;
+ left: 0.3em;
+ border-width: 0.4em;
+ transform: none;
+ }
+
+ &.vjs-waiting {
+ > .vjs-icon-search {
+ display: none;
+ }
+ > .vjs-loading-spinner {
+ display: flex;
+ }
+ }
+ }
+
+ .vjs-visual-search-form {
+ display: flex;
+ align-items: center;
+ width: 0;
+ opacity: 0;
+ transition: all 0.3s ease;
+ overflow: hidden;
+ }
+
+ .vjs-visual-search-input {
+ background: transparent;
+ border: none;
+ color: var(--color-text);
+ font-size: 0.938em;
+ padding: 0;
+ width: 100%;
+ height: 2.25em;
+ outline: none;
+
+ &::placeholder {
+ color: color-mix(in srgb, white 70%, transparent);
+ }
+ }
+
+ .vjs-visual-search-close {
+ background: transparent;
+ border: none;
+ color: var(--color-text);
+ cursor: pointer;
+ width: 2em;
+ height: 2em;
+ padding: 0.25em;
+ opacity: 0.7;
+ transition: opacity 0.2s ease;
+ flex-shrink: 0;
+ text-shadow: 0 0 0.25em var(--color-base);
+
+ &:hover {
+ opacity: 1;
+ }
+
+ .vjs-icon-close:before {
+ font-size: 1em;
+ }
+ }
+
+ .vjs-visual-search-results-wrapper {
+ position: absolute;
+ bottom: 100%;
+ left: 0;
+ width: 100%;
+ height: 0.75em;
+ z-index: 1;
+ $bg1: color-mix(in srgb, var(--color-accent) 15%, transparent);
+ $bg2: color-mix(in srgb, var(--color-base) 20%, transparent);
+ background:
+ linear-gradient(to right, $bg1, $bg1),
+ linear-gradient(to right, $bg2, $bg2);
+ }
+
+ .vjs-visual-search-marker {
+ position: absolute;
+ height: 100%;
+ background-color: var(--color-accent);
+ opacity: 0.7;
+ pointer-events: auto;
+ cursor: pointer;
+ transition: opacity 0.2s ease;
+
+ &:hover {
+ opacity: 0.8;
+ }
+ }
+}
+
+// Push seek-thumbs/chapters/time up when search results are active
+.vjs-visual-search-results-active {
+ .vjs-mouse-display {
+ translate: 0 -0.75em;
+ }
+}
+
+@keyframes vjs-visual-search-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
diff --git a/src/utils/get-analytics-player-options.js b/src/utils/get-analytics-player-options.js
index 9a0b9bdab..830bc676e 100644
--- a/src/utils/get-analytics-player-options.js
+++ b/src/utils/get-analytics-player-options.js
@@ -37,6 +37,7 @@ const getTranscriptOptions = (textTracks = {}) => {
const getSourceOptions = (sourceOptions = {}) => ({
chapters: sourceOptions.chapters && (sourceOptions.chapters.url ? 'url' : 'inline-chapters'),
+ visualSearch: hasConfig(sourceOptions.visualSearch),
recommendations: sourceOptions.recommendations && sourceOptions.recommendations.length,
shoppable: hasConfig(sourceOptions.shoppable),
shoppableProductsLength: sourceOptions.shoppable && sourceOptions.shoppable.products && sourceOptions.shoppable.products.length,
diff --git a/src/validators/validators.js b/src/validators/validators.js
index d0d096b56..764bbe320 100644
--- a/src/validators/validators.js
+++ b/src/validators/validators.js
@@ -86,6 +86,7 @@ export const sourceValidators = {
raw_transformation: validator.isString,
shoppable: validator.isPlainObject,
chapters: validator.or(validator.isBoolean, validator.isPlainObject),
+ visualSearch: validator.or(validator.isBoolean),
interactionAreas: {
enable: validator.isBoolean,
template: validator.or(validator.isString(INTERACTION_AREAS_TEMPLATE), validator.isArray),
diff --git a/src/video-player.const.js b/src/video-player.const.js
index 31905a627..2198dde92 100644
--- a/src/video-player.const.js
+++ b/src/video-player.const.js
@@ -38,7 +38,8 @@ export const PLAYER_PARAMS = CLOUDINARY_PARAMS.concat([
'aiHighlightsGraph',
'chapters',
'queryParams',
- 'type'
+ 'type',
+ 'visualSearch'
]);
export const CLOUDINARY_CONFIG_PARAM = [
diff --git a/src/video-player.js b/src/video-player.js
index f49f7214c..3386efbee 100644
--- a/src/video-player.js
+++ b/src/video-player.js
@@ -198,6 +198,7 @@ class VideoPlayer extends Utils.mixin(Eventable) {
this._initAnalytics();
this._initCloudinaryAnalytics();
this._initFloatingPlayer();
+ this._initVisualSearch();
this._initColors();
this._initTextTracks();
this._initHighlightsGraph();
@@ -331,6 +332,8 @@ class VideoPlayer extends Utils.mixin(Eventable) {
isFunction(this.videojs.chapters)
? this.videojs.chapters(source._chapters)
: this.videojs.chapters.src(source._chapters);
+ } else if (this.videojs.chapters?.resetPlugin) {
+ this.videojs.chapters.resetPlugin();
}
});
}
@@ -343,6 +346,19 @@ class VideoPlayer extends Utils.mixin(Eventable) {
});
}
+ _initVisualSearch() {
+ // Listen for source changes to apply visual search based on source config
+ this.videojs.on(PLAYER_EVENT.CLD_SOURCE_CHANGED, (e, { source }) => {
+ if (source._visualSearch && this.videojs.visualSearch) {
+ isFunction(this.videojs.visualSearch)
+ ? this.videojs.visualSearch(source._visualSearch)
+ : this.videojs.visualSearch.createSearchUI(source._visualSearch);
+ } else if (!source._visualSearch && this.videojs.visualSearch?.clearUI) {
+ this.videojs.visualSearch.clearUI();
+ }
+ });
+ }
+
_initColors () {
this.videojs.colors(this.playerOptions.colors ? { colors: this.playerOptions.colors } : {});
}
diff --git a/test/e2e/specs/ESM/linksConsoleErrorsEsmPage.spec.ts b/test/e2e/specs/ESM/linksConsoleErrorsEsmPage.spec.ts
index c6bb512f8..70b823f49 100644
--- a/test/e2e/specs/ESM/linksConsoleErrorsEsmPage.spec.ts
+++ b/test/e2e/specs/ESM/linksConsoleErrorsEsmPage.spec.ts
@@ -25,7 +25,7 @@ for (const link of ESM_LINKS) {
*/
vpTest('ESM page Link count test', async ({ page }) => {
await page.goto(ESM_URL);
- const expectedNumberOfLinks = 33;
+ const expectedNumberOfLinks = 34;
const numberOfLinks = await page.getByRole('link').count();
expect(numberOfLinks).toBe(expectedNumberOfLinks);
});
diff --git a/test/e2e/specs/NonESM/linksConsolErros.spec.ts b/test/e2e/specs/NonESM/linksConsolErros.spec.ts
index 98791381c..0d550969c 100644
--- a/test/e2e/specs/NonESM/linksConsolErros.spec.ts
+++ b/test/e2e/specs/NonESM/linksConsolErros.spec.ts
@@ -22,7 +22,7 @@ for (const link of LINKS) {
* Testing number of links in page.
*/
vpTest('Link count test', async ({ page }) => {
- const expectedNumberOfLinks = 36;
+ const expectedNumberOfLinks = 37;
const numberOfLinks = await page.getByRole('link').count();
expect(numberOfLinks).toBe(expectedNumberOfLinks);
});
diff --git a/test/e2e/specs/NonESM/visualSearchPage.spec.ts b/test/e2e/specs/NonESM/visualSearchPage.spec.ts
new file mode 100644
index 000000000..6f3b2f337
--- /dev/null
+++ b/test/e2e/specs/NonESM/visualSearchPage.spec.ts
@@ -0,0 +1,36 @@
+import { vpTest } from '../../fixtures/vpTest';
+import { test } from '@playwright/test';
+import { waitForPageToLoadWithTimeout } from '../../src/helpers/waitForPageToLoadWithTimeout';
+import { getLinkByName } from '../../testData/pageLinksData';
+import { ExampleLinkName } from '../../testData/ExampleLinkNames';
+
+const link = getLinkByName(ExampleLinkName.VisualSearch);
+
+vpTest(`Test if video on visual search page is playing as expected`, async ({ page, pomPages }) => {
+ await test.step('Navigate to visual search page by clicking on link', async () => {
+ await pomPages.mainPage.clickLinkByName(link.name);
+ await waitForPageToLoadWithTimeout(page, 10000);
+ });
+
+ // Wait for both video elements to be ready
+ await test.step('Wait for video elements to be ready', async () => {
+ await pomPages.visualSearchPage.visualSearchVideoComponent.locator.scrollIntoViewIfNeeded();
+ await pomPages.visualSearchPage.visualSearchPlaylistVideoComponent.locator.scrollIntoViewIfNeeded();
+ });
+
+ await test.step('Click play button on visual search video', async () => {
+ await pomPages.visualSearchPage.visualSearchVideoComponent.clickPlay();
+ });
+
+ await test.step('Validating that visual search video is playing', async () => {
+ await pomPages.visualSearchPage.visualSearchVideoComponent.validateVideoIsPlaying(true);
+ });
+
+ await test.step('Click play button on playlist video', async () => {
+ await pomPages.visualSearchPage.visualSearchPlaylistVideoComponent.clickPlay();
+ });
+
+ await test.step('Validating that visual search playlist video is playing', async () => {
+ await pomPages.visualSearchPage.visualSearchPlaylistVideoComponent.validateVideoIsPlaying(true);
+ });
+});
diff --git a/test/e2e/src/pom/PageManager.ts b/test/e2e/src/pom/PageManager.ts
index 48afd7052..112e9fdd2 100644
--- a/test/e2e/src/pom/PageManager.ts
+++ b/test/e2e/src/pom/PageManager.ts
@@ -28,6 +28,7 @@ import { SubtitlesAndCaptionsPage } from './subtitlesAndCaptionsPage';
import { VideoTransformationsPage } from './videoTransformationsPage';
import { VastAndVpaidPage } from './vastAndVpaidPage';
import { Vr360VideosPage } from './vr360VideosPage';
+import { VisualSearchPage } from './visualSearchPage';
/**
* Page manager,
@@ -191,5 +192,12 @@ export class PageManager {
public get vr360VideosPage(): Vr360VideosPage {
return this.getPage(Vr360VideosPage);
}
+
+ /**
+ * Returns visual search page object
+ */
+ public get visualSearchPage(): VisualSearchPage {
+ return this.getPage(VisualSearchPage);
+ }
}
export default PageManager;
diff --git a/test/e2e/src/pom/visualSearchPage.ts b/test/e2e/src/pom/visualSearchPage.ts
new file mode 100644
index 000000000..2bb506434
--- /dev/null
+++ b/test/e2e/src/pom/visualSearchPage.ts
@@ -0,0 +1,20 @@
+import { Page } from '@playwright/test';
+import { VideoComponent } from '../../components/videoComponent';
+import { BasePage } from './BasePage';
+
+const VISUAL_SEARCH_PAGE_VIDEO_SELECTOR = '//*[@id="player_html5_api"]';
+const VISUAL_SEARCH_PLAYLIST_VIDEO_SELECTOR = '//*[@id="player-playlist_html5_api"]';
+
+/**
+ * Video player examples visual search page object
+ */
+export class VisualSearchPage extends BasePage {
+ public visualSearchVideoComponent: VideoComponent;
+ public visualSearchPlaylistVideoComponent: VideoComponent;
+
+ constructor(page: Page) {
+ super(page);
+ this.visualSearchVideoComponent = new VideoComponent(page, VISUAL_SEARCH_PAGE_VIDEO_SELECTOR);
+ this.visualSearchPlaylistVideoComponent = new VideoComponent(page, VISUAL_SEARCH_PLAYLIST_VIDEO_SELECTOR);
+ }
+}
diff --git a/test/e2e/testData/ExampleLinkNames.ts b/test/e2e/testData/ExampleLinkNames.ts
index 9e3d0439a..742777e77 100644
--- a/test/e2e/testData/ExampleLinkNames.ts
+++ b/test/e2e/testData/ExampleLinkNames.ts
@@ -38,4 +38,5 @@ export enum ExampleLinkName {
ESMImports = 'ESM Imports',
AllBuild = '/all build',
LightBuild = '/light build',
+ VisualSearch = 'Visual Search',
}
diff --git a/test/e2e/testData/pageLinksData.ts b/test/e2e/testData/pageLinksData.ts
index c88bc0046..d54e43e8e 100644
--- a/test/e2e/testData/pageLinksData.ts
+++ b/test/e2e/testData/pageLinksData.ts
@@ -39,6 +39,7 @@ export const LINKS: ExampleLinkType[] = [
{ name: ExampleLinkName.VR360Videos, endpoint: '360.html' },
{ name: ExampleLinkName.EmbeddedIframePlayer, endpoint: 'embedded-iframe.html' },
{ name: ExampleLinkName.ESMImports, endpoint: 'cld-vp-esm-pages.netlify.app' },
+ { name: ExampleLinkName.VisualSearch, endpoint: 'visual-search.html' },
];
/**