diff --git a/__test-iframe/index.html b/__test-iframe/index.html
new file mode 100644
index 00000000..0a2f1db0
--- /dev/null
+++ b/__test-iframe/index.html
@@ -0,0 +1,21 @@
+
+
+ Embed Video with showTitle=0
+
+
+ Embed Video with showTitle=1
+
+
+
diff --git a/frontend-tools/video-js/src/components/overlays/VideoContextMenu.css b/frontend-tools/video-js/src/components/overlays/VideoContextMenu.css
new file mode 100644
index 00000000..76e0d9a7
--- /dev/null
+++ b/frontend-tools/video-js/src/components/overlays/VideoContextMenu.css
@@ -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;
+}
+
diff --git a/frontend-tools/video-js/src/components/overlays/VideoContextMenu.jsx b/frontend-tools/video-js/src/components/overlays/VideoContextMenu.jsx
new file mode 100644
index 00000000..0bbb2869
--- /dev/null
+++ b/frontend-tools/video-js/src/components/overlays/VideoContextMenu.jsx
@@ -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 (
+ e.stopPropagation()}>
+
+
+
Copy video URL
+
+
+
+
Copy video URL at current time
+
+
+
+
Copy embed code
+
+
+ );
+}
+
+export default VideoContextMenu;
+
diff --git a/frontend-tools/video-js/src/components/video-player/VideoJSPlayer.jsx b/frontend-tools/video-js/src/components/video-player/VideoJSPlayer.jsx
index ff758b6c..c85a73d8 100644
--- a/frontend-tools/video-js/src/components/video-player/VideoJSPlayer.jsx
+++ b/frontend-tools/video-js/src/components/video-player/VideoJSPlayer.jsx
@@ -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 'video.js/dist/video-js.css';
import '../../styles/embed.css';
@@ -17,6 +17,7 @@ import CustomRemainingTime from '../controls/CustomRemainingTime';
import CustomChaptersOverlay from '../controls/CustomChaptersOverlay';
import CustomSettingsMenu from '../controls/CustomSettingsMenu';
import SeekIndicator from '../controls/SeekIndicator';
+import VideoContextMenu from '../overlays/VideoContextMenu';
import UserPreferences from '../../utils/UserPreferences';
import PlayerConfig from '../../config/playerConfig';
import { AutoplayHandler } from '../../utils/AutoplayHandler';
@@ -169,7 +170,7 @@ const enableStandardButtonTooltips = (player) => {
}, 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 playerRef = useRef(null); // Track the player 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 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)
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
const isTouchDevice = useMemo(() => {
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 = ``;
+
+ // 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(() => {
// Only initialize if we don't already have a player and element exists
if (videoRef.current && !playerRef.current) {
@@ -1997,6 +2223,7 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
authorThumbnail: currentVideo.author_thumbnail,
videoTitle: currentVideo.title,
videoUrl: currentVideo.url,
+ showTitle: finalShowTitle,
});
}
// END: Add Embed Info Overlay Component
@@ -2084,51 +2311,109 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
const videoElement = playerRef.current.el();
videoElement.setAttribute('tabindex', '0');
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);
}
+
+ // 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 (
-
+
+ >
);
}