UNPKG

cloudinary-video-player

Version:
236 lines (218 loc) 7.04 kB
import { _ as _vjs } from './_videojs-proxy.js'; import './_commonjsHelpers.js'; const SearchButton = onClick => { const button = _vjs.dom.createEl('button', { className: 'vjs-control vjs-button vjs-visual-search-button', title: 'Search video content', ariaLabel: 'Search video content' }); const searchIcon = _vjs.dom.createEl('span', { className: 'vjs-icon-search' }); button.appendChild(searchIcon); const spinnerIcon = _vjs.dom.createEl('span', { className: 'vjs-loading-spinner' }); button.appendChild(spinnerIcon); button.addEventListener('click', onClick); return button; }; const SearchInput = (onSearch, onClose) => { const form = _vjs.dom.createEl('form', { className: 'vjs-visual-search-form' }); const input = _vjs.dom.createEl('input', { className: 'vjs-visual-search-input', type: 'text', ariaLabel: 'Search input', tabIndex: -1 }); const closeButton = _vjs.dom.createEl('button', { className: 'vjs-control vjs-button vjs-visual-search-close', type: 'button', title: 'Close search', ariaLabel: 'Close search', tabIndex: -1 }); // Add close icon const closeIcon = _vjs.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, closeButton }; }; 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 = _vjs.dom.createEl('div', { className: 'vjs-visual-search-results-wrapper', role: 'presentation' }); // Add markers for each result results.forEach(result => { const { start_time, end_time } = result; const position = start_time / total * 100; const width = (end_time - start_time) / total * 100; const time = `${Math.floor(start_time / 60)}:${Math.floor(start_time % 60).toString().padStart(2, '0')}`; const markerEl = _vjs.dom.createEl('div', { className: 'vjs-control vjs-visual-search-marker', style: `left: ${position}%; width: ${width}%`, tabIndex: 0, role: 'button', title: `Search result at ${time}`, ariaLabel: `Search result at ${time}` }); wrapperEl.appendChild(markerEl); // Add click handler to jump to this time markerEl.addEventListener('click', () => { player.currentTime(start_time); }); // Add keyboard support markerEl.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); 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 }; }; 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 = _vjs.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.tabIndex = 0; searchInput.closeButton.tabIndex = 0; 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 = ''; searchInput.input.tabIndex = -1; searchInput.closeButton.tabIndex = -1; 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 { visualSearch as default };