mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-01-20 07:12:58 -05:00
feat: add right-click context menu with share/embed options on video player
- Add VideoContextMenu component with copy URL, URL with timestamp, and embed code options - Integrate context menu into VideoJSPlayer with proper event handling - Add visual feedback via SeekIndicator when copying URLs/embed code - Support embed mode with showTitle URL parameter handling - Handle cross-origin iframe scenarios for embed URL generation - Add helper functions for media ID, origin, and embed URL resolution
This commit is contained in:
21
__test-iframe/index.html
Normal file
21
__test-iframe/index.html
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h2>Embed Video with showTitle=0</h2>
|
||||||
|
<iframe
|
||||||
|
width="560"
|
||||||
|
height="315"
|
||||||
|
src="http://localhost/embed?m=6WShYNxZx&showTitle=0"
|
||||||
|
frameborder="0"
|
||||||
|
allowfullscreen
|
||||||
|
></iframe>
|
||||||
|
|
||||||
|
<h2>Embed Video with showTitle=1</h2>
|
||||||
|
<iframe
|
||||||
|
width="560"
|
||||||
|
height="315"
|
||||||
|
src="http://localhost/embed?m=SOgLTsrAH&showTitle=1"
|
||||||
|
frameborder="0"
|
||||||
|
allowfullscreen
|
||||||
|
></iframe>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
.video-context-menu {
|
||||||
|
position: fixed;
|
||||||
|
background-color: #282828;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 0;
|
||||||
|
min-width: 240px;
|
||||||
|
z-index: 10000;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-context-menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 16px;
|
||||||
|
color: #ffffff;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
font-size: 14px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-context-menu-item:hover {
|
||||||
|
background-color: #3d3d3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-context-menu-item:active {
|
||||||
|
background-color: #4a4a4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-context-menu-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
margin-right: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
stroke: currentColor;
|
||||||
|
fill: none;
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-context-menu-item span {
|
||||||
|
flex: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import './VideoContextMenu.css';
|
||||||
|
|
||||||
|
function VideoContextMenu({ visible, position, onClose, onCopyVideoUrl, onCopyVideoUrlAtTime, onCopyEmbedCode }) {
|
||||||
|
const menuRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible && menuRef.current) {
|
||||||
|
// Position the menu
|
||||||
|
menuRef.current.style.left = `${position.x}px`;
|
||||||
|
menuRef.current.style.top = `${position.y}px`;
|
||||||
|
|
||||||
|
// Adjust if menu goes off screen
|
||||||
|
const rect = menuRef.current.getBoundingClientRect();
|
||||||
|
const windowWidth = window.innerWidth;
|
||||||
|
const windowHeight = window.innerHeight;
|
||||||
|
|
||||||
|
if (rect.right > windowWidth) {
|
||||||
|
menuRef.current.style.left = `${position.x - rect.width}px`;
|
||||||
|
}
|
||||||
|
if (rect.bottom > windowHeight) {
|
||||||
|
menuRef.current.style.top = `${position.y - rect.height}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [visible, position]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (e) => {
|
||||||
|
if (visible && menuRef.current && !menuRef.current.contains(e.target)) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEscape = (e) => {
|
||||||
|
if (e.key === 'Escape' && visible) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
// Use capture phase to catch events earlier, before they can be stopped
|
||||||
|
// Listen to both mousedown and click to ensure we catch all clicks
|
||||||
|
document.addEventListener('mousedown', handleClickOutside, true);
|
||||||
|
document.addEventListener('click', handleClickOutside, true);
|
||||||
|
document.addEventListener('keydown', handleEscape);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside, true);
|
||||||
|
document.removeEventListener('click', handleClickOutside, true);
|
||||||
|
document.removeEventListener('keydown', handleEscape);
|
||||||
|
};
|
||||||
|
}, [visible, onClose]);
|
||||||
|
|
||||||
|
if (!visible) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={menuRef} className="video-context-menu" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div className="video-context-menu-item" onClick={onCopyVideoUrl}>
|
||||||
|
<svg className="video-context-menu-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
<span>Copy video URL</span>
|
||||||
|
</div>
|
||||||
|
<div className="video-context-menu-item" onClick={onCopyVideoUrlAtTime}>
|
||||||
|
<svg className="video-context-menu-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
<span>Copy video URL at current time</span>
|
||||||
|
</div>
|
||||||
|
<div className="video-context-menu-item" onClick={onCopyEmbedCode}>
|
||||||
|
<svg className="video-context-menu-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M16 18l6-6-6-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
<path d="M8 6l-6 6 6 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
<span>Copy embed code</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VideoContextMenu;
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useRef, useMemo } from 'react';
|
import React, { useEffect, useRef, useMemo, useState, useCallback } from 'react';
|
||||||
import videojs from 'video.js';
|
import videojs from 'video.js';
|
||||||
import 'video.js/dist/video-js.css';
|
import 'video.js/dist/video-js.css';
|
||||||
import '../../styles/embed.css';
|
import '../../styles/embed.css';
|
||||||
@@ -17,6 +17,7 @@ import CustomRemainingTime from '../controls/CustomRemainingTime';
|
|||||||
import CustomChaptersOverlay from '../controls/CustomChaptersOverlay';
|
import CustomChaptersOverlay from '../controls/CustomChaptersOverlay';
|
||||||
import CustomSettingsMenu from '../controls/CustomSettingsMenu';
|
import CustomSettingsMenu from '../controls/CustomSettingsMenu';
|
||||||
import SeekIndicator from '../controls/SeekIndicator';
|
import SeekIndicator from '../controls/SeekIndicator';
|
||||||
|
import VideoContextMenu from '../overlays/VideoContextMenu';
|
||||||
import UserPreferences from '../../utils/UserPreferences';
|
import UserPreferences from '../../utils/UserPreferences';
|
||||||
import PlayerConfig from '../../config/playerConfig';
|
import PlayerConfig from '../../config/playerConfig';
|
||||||
import { AutoplayHandler } from '../../utils/AutoplayHandler';
|
import { AutoplayHandler } from '../../utils/AutoplayHandler';
|
||||||
@@ -169,7 +170,7 @@ const enableStandardButtonTooltips = (player) => {
|
|||||||
}, 500); // Delay to ensure all components are ready
|
}, 500); // Delay to ensure all components are ready
|
||||||
};
|
};
|
||||||
|
|
||||||
function VideoJSPlayer({ videoId = 'default-video' }) {
|
function VideoJSPlayer({ videoId = 'default-video', showTitle = true }) {
|
||||||
const videoRef = useRef(null);
|
const videoRef = useRef(null);
|
||||||
const playerRef = useRef(null); // Track the player instance
|
const playerRef = useRef(null); // Track the player instance
|
||||||
const userPreferences = useRef(new UserPreferences()); // User preferences instance
|
const userPreferences = useRef(new UserPreferences()); // User preferences instance
|
||||||
@@ -177,9 +178,28 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
|
|||||||
const keyboardHandler = useRef(null); // Keyboard handler instance
|
const keyboardHandler = useRef(null); // Keyboard handler instance
|
||||||
const playbackEventHandler = useRef(null); // Playback event handler instance
|
const playbackEventHandler = useRef(null); // Playback event handler instance
|
||||||
|
|
||||||
|
// Context menu state
|
||||||
|
const [contextMenuVisible, setContextMenuVisible] = useState(false);
|
||||||
|
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 });
|
||||||
|
|
||||||
// Check if this is an embed player (disable next video and autoplay features)
|
// Check if this is an embed player (disable next video and autoplay features)
|
||||||
const isEmbedPlayer = videoId === 'video-embed';
|
const isEmbedPlayer = videoId === 'video-embed';
|
||||||
|
|
||||||
|
// Read showTitle from URL parameter if available (for embed players)
|
||||||
|
const getShowTitleFromURL = useMemo(() => {
|
||||||
|
if (isEmbedPlayer && typeof window !== 'undefined') {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const urlShowTitle = urlParams.get('showTitle');
|
||||||
|
if (urlShowTitle !== null) {
|
||||||
|
return urlShowTitle === '1' || urlShowTitle === 'true';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return showTitle;
|
||||||
|
}, [isEmbedPlayer, showTitle]);
|
||||||
|
|
||||||
|
// Use URL parameter value if available, otherwise use prop value
|
||||||
|
const finalShowTitle = isEmbedPlayer ? getShowTitleFromURL : showTitle;
|
||||||
|
|
||||||
// Utility function to detect touch devices
|
// Utility function to detect touch devices
|
||||||
const isTouchDevice = useMemo(() => {
|
const isTouchDevice = useMemo(() => {
|
||||||
return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
|
return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
|
||||||
@@ -738,6 +758,212 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Context menu handlers
|
||||||
|
const handleContextMenu = useCallback((e) => {
|
||||||
|
// Only handle if clicking on video player area
|
||||||
|
const target = e.target;
|
||||||
|
const isVideoPlayerArea =
|
||||||
|
target.closest('.video-js') ||
|
||||||
|
target.classList.contains('vjs-tech') ||
|
||||||
|
target.tagName === 'VIDEO' ||
|
||||||
|
target.closest('video');
|
||||||
|
|
||||||
|
if (isVideoPlayerArea) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
setContextMenuPosition({ x: e.clientX, y: e.clientY });
|
||||||
|
setContextMenuVisible(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeContextMenu = () => {
|
||||||
|
setContextMenuVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to get media ID
|
||||||
|
const getMediaId = () => {
|
||||||
|
if (typeof window !== 'undefined' && window.MEDIA_DATA?.data?.friendly_token) {
|
||||||
|
return window.MEDIA_DATA.data.friendly_token;
|
||||||
|
}
|
||||||
|
if (mediaData?.data?.friendly_token) {
|
||||||
|
return mediaData.data.friendly_token;
|
||||||
|
}
|
||||||
|
// Try to get from URL (works for both main page and embed page)
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const mediaIdFromUrl = urlParams.get('m');
|
||||||
|
if (mediaIdFromUrl) {
|
||||||
|
return mediaIdFromUrl;
|
||||||
|
}
|
||||||
|
// Also check if we're on an embed page with media ID in path
|
||||||
|
const pathMatch = window.location.pathname.match(/\/embed\/([^/?]+)/);
|
||||||
|
if (pathMatch) {
|
||||||
|
return pathMatch[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return currentVideo.id || 'default-video';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to get base origin URL (handles embed mode)
|
||||||
|
const getBaseOrigin = () => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
// In embed mode, try to get origin from parent window if possible
|
||||||
|
// Otherwise use current window origin
|
||||||
|
try {
|
||||||
|
// Check if we're in an iframe and can access parent
|
||||||
|
if (window.parent !== window && window.parent.location.origin) {
|
||||||
|
return window.parent.location.origin;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Cross-origin iframe, use current origin
|
||||||
|
}
|
||||||
|
return window.location.origin;
|
||||||
|
}
|
||||||
|
return mediaData.siteUrl || 'https://deic.mediacms.io';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to get embed URL
|
||||||
|
const getEmbedUrl = () => {
|
||||||
|
const mediaId = getMediaId();
|
||||||
|
const origin = getBaseOrigin();
|
||||||
|
|
||||||
|
// Try to get embed URL from config or construct it
|
||||||
|
if (typeof window !== 'undefined' && window.MediaCMS?.config?.url?.embed) {
|
||||||
|
return window.MediaCMS.config.url.embed + mediaId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: construct embed URL (check if current URL is embed format)
|
||||||
|
if (typeof window !== 'undefined' && window.location.pathname.includes('/embed')) {
|
||||||
|
// If we're already on an embed page, use current URL format
|
||||||
|
const currentUrl = new URL(window.location.href);
|
||||||
|
currentUrl.searchParams.set('m', mediaId);
|
||||||
|
return currentUrl.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default embed URL format
|
||||||
|
return `${origin}/embed?m=${mediaId}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Copy video URL to clipboard
|
||||||
|
const handleCopyVideoUrl = async () => {
|
||||||
|
const mediaId = getMediaId();
|
||||||
|
const origin = getBaseOrigin();
|
||||||
|
const videoUrl = `${origin}/view?m=${mediaId}`;
|
||||||
|
|
||||||
|
// Show copy icon
|
||||||
|
if (customComponents.current?.seekIndicator) {
|
||||||
|
customComponents.current.seekIndicator.show('copy-url');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(videoUrl);
|
||||||
|
closeContextMenu();
|
||||||
|
// You can add a notification here if needed
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy video URL:', err);
|
||||||
|
// Fallback for older browsers
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
textArea.value = videoUrl;
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
closeContextMenu();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Copy video URL at current time to clipboard
|
||||||
|
const handleCopyVideoUrlAtTime = async () => {
|
||||||
|
if (!playerRef.current) {
|
||||||
|
closeContextMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTime = Math.floor(playerRef.current.currentTime() || 0);
|
||||||
|
const mediaId = getMediaId();
|
||||||
|
const origin = getBaseOrigin();
|
||||||
|
const videoUrl = `${origin}/view?m=${mediaId}&t=${currentTime}`;
|
||||||
|
|
||||||
|
// Show copy icon
|
||||||
|
if (customComponents.current?.seekIndicator) {
|
||||||
|
customComponents.current.seekIndicator.show('copy-url');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(videoUrl);
|
||||||
|
closeContextMenu();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy video URL at time:', err);
|
||||||
|
// Fallback for older browsers
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
textArea.value = videoUrl;
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
closeContextMenu();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Copy embed code to clipboard
|
||||||
|
const handleCopyEmbedCode = async () => {
|
||||||
|
const embedUrl = getEmbedUrl();
|
||||||
|
const embedCode = `<iframe width="560" height="315" src="${embedUrl}" frameborder="0" allowfullscreen></iframe>`;
|
||||||
|
|
||||||
|
// Show copy embed icon
|
||||||
|
if (customComponents.current?.seekIndicator) {
|
||||||
|
customComponents.current.seekIndicator.show('copy-embed');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(embedCode);
|
||||||
|
closeContextMenu();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy embed code:', err);
|
||||||
|
// Fallback for older browsers
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
textArea.value = embedCode;
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
closeContextMenu();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add context menu handler directly to video element and document (works before and after Video.js initialization)
|
||||||
|
useEffect(() => {
|
||||||
|
const videoElement = videoRef.current;
|
||||||
|
|
||||||
|
// Attach to document with capture to catch all contextmenu events, then filter
|
||||||
|
const documentHandler = (e) => {
|
||||||
|
// Check if the event originated from within the video player
|
||||||
|
const target = e.target;
|
||||||
|
const playerWrapper =
|
||||||
|
videoElement?.closest('.video-js') || document.querySelector(`#${videoId}`)?.closest('.video-js');
|
||||||
|
|
||||||
|
if (playerWrapper && (playerWrapper.contains(target) || target === playerWrapper)) {
|
||||||
|
handleContextMenu(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use capture phase on document to catch before anything else
|
||||||
|
document.addEventListener('contextmenu', documentHandler, true);
|
||||||
|
|
||||||
|
// Also attach directly to video element
|
||||||
|
if (videoElement) {
|
||||||
|
videoElement.addEventListener('contextmenu', handleContextMenu, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('contextmenu', documentHandler, true);
|
||||||
|
if (videoElement) {
|
||||||
|
videoElement.removeEventListener('contextmenu', handleContextMenu, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [handleContextMenu, videoId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only initialize if we don't already have a player and element exists
|
// Only initialize if we don't already have a player and element exists
|
||||||
if (videoRef.current && !playerRef.current) {
|
if (videoRef.current && !playerRef.current) {
|
||||||
@@ -1997,6 +2223,7 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
|
|||||||
authorThumbnail: currentVideo.author_thumbnail,
|
authorThumbnail: currentVideo.author_thumbnail,
|
||||||
videoTitle: currentVideo.title,
|
videoTitle: currentVideo.title,
|
||||||
videoUrl: currentVideo.url,
|
videoUrl: currentVideo.url,
|
||||||
|
showTitle: finalShowTitle,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// END: Add Embed Info Overlay Component
|
// END: Add Embed Info Overlay Component
|
||||||
@@ -2084,51 +2311,109 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
|
|||||||
const videoElement = playerRef.current.el();
|
const videoElement = playerRef.current.el();
|
||||||
videoElement.setAttribute('tabindex', '0');
|
videoElement.setAttribute('tabindex', '0');
|
||||||
videoElement.focus();
|
videoElement.focus();
|
||||||
|
|
||||||
|
// Add context menu (right-click) handler to the player wrapper and video element
|
||||||
|
// Attach to player wrapper (this catches all clicks on the player)
|
||||||
|
videoElement.addEventListener('contextmenu', handleContextMenu, true);
|
||||||
|
|
||||||
|
// Also try to attach to the actual video tech element
|
||||||
|
const attachContextMenu = () => {
|
||||||
|
const techElement =
|
||||||
|
playerRef.current.el().querySelector('.vjs-tech') ||
|
||||||
|
playerRef.current.el().querySelector('video') ||
|
||||||
|
(playerRef.current.tech() && playerRef.current.tech().el());
|
||||||
|
|
||||||
|
if (techElement && techElement !== videoRef.current && techElement !== videoElement) {
|
||||||
|
// Use capture phase to catch before Video.js might prevent it
|
||||||
|
techElement.addEventListener('contextmenu', handleContextMenu, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try to attach immediately
|
||||||
|
attachContextMenu();
|
||||||
|
|
||||||
|
// Also try after a short delay in case elements aren't ready yet
|
||||||
|
setTimeout(() => {
|
||||||
|
attachContextMenu();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Also try when video is loaded
|
||||||
|
playerRef.current.one('loadedmetadata', () => {
|
||||||
|
attachContextMenu();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
//}, 0);
|
//}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cleanup: Remove context menu event listener
|
||||||
|
return () => {
|
||||||
|
if (playerRef.current && playerRef.current.el()) {
|
||||||
|
const playerEl = playerRef.current.el();
|
||||||
|
playerEl.removeEventListener('contextmenu', handleContextMenu, true);
|
||||||
|
|
||||||
|
const techElement =
|
||||||
|
playerEl.querySelector('.vjs-tech') ||
|
||||||
|
playerEl.querySelector('video') ||
|
||||||
|
(playerRef.current.tech() && playerRef.current.tech().el());
|
||||||
|
if (techElement) {
|
||||||
|
techElement.removeEventListener('contextmenu', handleContextMenu, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<video
|
<>
|
||||||
ref={videoRef}
|
<video
|
||||||
id={videoId}
|
ref={videoRef}
|
||||||
controls={true}
|
id={videoId}
|
||||||
className={`video-js vjs-fluid vjs-default-skin${currentVideo.useRoundedCorners ? ' video-js-rounded-corners' : ''}`}
|
controls={true}
|
||||||
preload="auto"
|
className={`video-js vjs-fluid vjs-default-skin${currentVideo.useRoundedCorners ? ' video-js-rounded-corners' : ''}`}
|
||||||
poster={currentVideo.poster}
|
preload="auto"
|
||||||
tabIndex="0"
|
poster={currentVideo.poster}
|
||||||
>
|
tabIndex="0"
|
||||||
{/* <source src="/videos/sample-video.mp4" type="video/mp4" />
|
>
|
||||||
<source src="/videos/sample-video.webm" type="video/webm" /> */}
|
{/* <source src="/videos/sample-video.mp4" type="video/mp4" />
|
||||||
<p className="vjs-no-js">
|
<source src="/videos/sample-video.webm" type="video/webm" /> */}
|
||||||
To view this video please enable JavaScript, and consider upgrading to a web browser that
|
<p className="vjs-no-js">
|
||||||
<a href="https://videojs.com/html5-video-support/" target="_blank">
|
To view this video please enable JavaScript, and consider upgrading to a web browser that
|
||||||
supports HTML5 video
|
<a href="https://videojs.com/html5-video-support/" target="_blank">
|
||||||
</a>
|
supports HTML5 video
|
||||||
</p>
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
{/* Add subtitle tracks */}
|
{/* Add subtitle tracks */}
|
||||||
{/* {subtitleTracks &&
|
{/* {subtitleTracks &&
|
||||||
subtitleTracks.map((track, index) => (
|
subtitleTracks.map((track, index) => (
|
||||||
<track
|
<track
|
||||||
key={index}
|
key={index}
|
||||||
kind={track.kind}
|
kind={track.kind}
|
||||||
src={track.src}
|
src={track.src}
|
||||||
srcLang={track.srclang}
|
srcLang={track.srclang}
|
||||||
label={track.label}
|
label={track.label}
|
||||||
default={track.default}
|
default={track.default}
|
||||||
/>
|
/>
|
||||||
))} */}
|
))} */}
|
||||||
{/*
|
{/*
|
||||||
<track kind="chapters" src="/sample-chapters.vtt" /> */}
|
<track kind="chapters" src="/sample-chapters.vtt" /> */}
|
||||||
{/* Add chapters track */}
|
{/* Add chapters track */}
|
||||||
{/* {chaptersData &&
|
{/* {chaptersData &&
|
||||||
chaptersData.length > 0 &&
|
chaptersData.length > 0 &&
|
||||||
(console.log('chaptersData', chaptersData), (<track kind="chapters" src="/sample-chapters.vtt" />))} */}
|
(console.log('chaptersData', chaptersData), (<track kind="chapters" src="/sample-chapters.vtt" />))} */}
|
||||||
</video>
|
</video>
|
||||||
|
<VideoContextMenu
|
||||||
|
visible={contextMenuVisible}
|
||||||
|
position={contextMenuPosition}
|
||||||
|
onClose={closeContextMenu}
|
||||||
|
onCopyVideoUrl={handleCopyVideoUrl}
|
||||||
|
onCopyVideoUrlAtTime={handleCopyVideoUrlAtTime}
|
||||||
|
onCopyEmbedCode={handleCopyEmbedCode}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user