feat: Timestamp click functionality, URL timestamps, embed functionality

This commit is contained in:
Yiannis Christodoulou 2025-09-19 09:32:41 +03:00
parent e3291a5d75
commit b96134bd9f
2 changed files with 120 additions and 7 deletions

View File

@ -32,6 +32,7 @@ const VideoJSEmbed = ({
inEmbed, inEmbed,
hasTheaterMode, hasTheaterMode,
hasNextLink, hasNextLink,
nextLink,
hasPreviousLink, hasPreviousLink,
errorMessage, errorMessage,
onClickNextCallback, onClickNextCallback,
@ -41,14 +42,30 @@ const VideoJSEmbed = ({
}) => { }) => {
const containerRef = useRef(null); const containerRef = useRef(null);
const assetsLoadedRef = useRef(false); const assetsLoadedRef = useRef(false);
const playerInstanceRef = useRef(null);
const inEmbedRef = useRef(inEmbed);
// Helper function to get URL parameters
const getUrlParameter = (name) => {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(name);
};
useEffect(() => { useEffect(() => {
// Update the ref whenever inEmbed changes
inEmbedRef.current = inEmbed;
// Set the global MEDIA_DATA that the video js expects // Set the global MEDIA_DATA that the video js expects
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
// Get URL parameters for autoplay, muted, and timestamp
const urlTimestamp = getUrlParameter('t');
const urlAutoplay = getUrlParameter('autoplay');
const urlMuted = getUrlParameter('muted');
window.MEDIA_DATA = { window.MEDIA_DATA = {
data: data || {}, // TODO: Check if this is needed data: data || {}, // TODO: Check if this is needed
playerVolume: playerVolume || 0.5, playerVolume: playerVolume || 0.5,
playerSoundMuted: playerSoundMuted || false, playerSoundMuted: playerSoundMuted || (urlMuted === '1'),
videoQuality: videoQuality || 'auto', videoQuality: videoQuality || 'auto',
videoPlaybackSpeed: videoPlaybackSpeed || 1, videoPlaybackSpeed: videoPlaybackSpeed || 1,
inTheaterMode: inTheaterMode || false, inTheaterMode: inTheaterMode || false,
@ -60,16 +77,28 @@ const VideoJSEmbed = ({
poster: poster || '', poster: poster || '',
previewSprite: previewSprite || null, previewSprite: previewSprite || null,
subtitlesInfo: subtitlesInfo || [], subtitlesInfo: subtitlesInfo || [],
enableAutoplay: enableAutoplay || false, enableAutoplay: enableAutoplay || (urlAutoplay === '1'),
inEmbed: inEmbed || false, inEmbed: inEmbed || false,
hasTheaterMode: hasTheaterMode || false, hasTheaterMode: hasTheaterMode || false,
hasNextLink: hasNextLink || false, hasNextLink: hasNextLink || false,
nextLink: nextLink || null,
hasPreviousLink: hasPreviousLink || false, hasPreviousLink: hasPreviousLink || false,
errorMessage: errorMessage || '', errorMessage: errorMessage || '',
// URL parameters
urlTimestamp: urlTimestamp ? parseInt(urlTimestamp, 10) : null,
urlAutoplay: urlAutoplay === '1',
urlMuted: urlMuted === '1',
onClickNextCallback: onClickNextCallback || null, onClickNextCallback: onClickNextCallback || null,
onClickPreviousCallback: onClickPreviousCallback || null, onClickPreviousCallback: onClickPreviousCallback || null,
onStateUpdateCallback: onStateUpdateCallback || null, onStateUpdateCallback: onStateUpdateCallback || null,
onPlayerInitCallback: onPlayerInitCallback || null, onPlayerInitCallback: (instance, elem) => {
// Store the player instance for timestamp functionality
playerInstanceRef.current = instance;
// Call the original callback if provided
if (onPlayerInitCallback) {
onPlayerInitCallback(instance, elem);
}
},
}; };
} }
@ -78,7 +107,89 @@ const VideoJSEmbed = ({
loadVideoJSAssets(); loadVideoJSAssets();
assetsLoadedRef.current = true; assetsLoadedRef.current = true;
} }
}, [data, siteUrl]); }, [data, siteUrl, inEmbed]);
// New effect to manually trigger VideoJS mounting for embed players
useEffect(() => {
if (inEmbed && containerRef.current) {
// Small delay to ensure DOM is fully ready, then trigger VideoJS mounting
const timer = setTimeout(() => {
// Try to trigger the VideoJS mount by dispatching a custom event
const event = new CustomEvent('triggerVideoJSMount', {
detail: { targetId: 'video-js-root-embed' }
});
document.dispatchEvent(event);
// Also try to trigger by calling the global function if it exists
if (typeof window !== 'undefined' && window.triggerVideoJSMount) {
window.triggerVideoJSMount();
}
}, 100);
return () => clearTimeout(timer);
}
}, [inEmbed, containerRef.current]);
// Set up timestamp click functionality
useEffect(() => {
const handleTimestampClick = (e) => {
if (e.target.classList.contains('video-timestamp')) {
e.preventDefault();
const timestamp = parseInt(e.target.dataset.timestamp, 10);
// Try to get the player from multiple sources
let player = null;
// First try: from our stored instance
if (playerInstanceRef.current && playerInstanceRef.current.player) {
player = playerInstanceRef.current.player;
}
// Second try: from global window.videojsPlayers
if (!player && typeof window !== 'undefined' && window.videojsPlayers) {
const videoId = inEmbedRef.current ? 'video-embed' : 'video-main';
player = window.videojsPlayers[videoId];
}
// Third try: from the global videojs function looking for existing players
if (!player && typeof window !== 'undefined' && window.videojs) {
const videoElement = document.querySelector(inEmbedRef.current ? '#video-embed' : '#video-main');
if (videoElement && videoElement.player) {
player = videoElement.player;
}
}
// If we found a player, seek to the timestamp
if (player) {
if (timestamp >= 0 && timestamp < player.duration()) {
player.currentTime(timestamp);
} else if (timestamp >= 0) {
player.play();
}
// Scroll to the video player with smooth behavior
const videoElement = document.querySelector(inEmbedRef.current ? '#video-embed' : '#video-main');
if (videoElement) {
videoElement.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest'
});
}
} else {
console.warn('VideoJS player not found for timestamp navigation');
}
}
};
// Add the event listener to the document for timestamp clicks
document.addEventListener('click', handleTimestampClick);
// Cleanup function
return () => {
document.removeEventListener('click', handleTimestampClick);
};
}, []); // Empty dependency array so this effect only runs once
const loadVideoJSAssets = () => { const loadVideoJSAssets = () => {
// Check if assets are already loaded // Check if assets are already loaded
@ -103,7 +214,7 @@ const VideoJSEmbed = ({
return ( return (
<div className="video-js-wrapper" ref={containerRef}> <div className="video-js-wrapper" ref={containerRef}>
<div id="video-js-root" /> {inEmbed ? <div id="video-js-root-embed" className="video-js-root-embed" /> : <div id="video-js-root-main" className="video-js-root-main" />}
</div> </div>
); );
}; };

View File

@ -1,6 +1,6 @@
import React, { useRef, useState, useEffect, useContext } from 'react'; import React, { useRef, useState, useEffect, useContext } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { LinksContext } from '../../utils/contexts/'; import { LinksContext, SiteConsumer } from '../../utils/contexts/';
import { PageStore, MediaPageStore } from '../../utils/stores/'; import { PageStore, MediaPageStore } from '../../utils/stores/';
import { PageActions, MediaPageActions } from '../../utils/actions/'; import { PageActions, MediaPageActions } from '../../utils/actions/';
import { CircleIconButton, MaterialIcon, NumericInputWithUnit } from '../_shared/'; import { CircleIconButton, MaterialIcon, NumericInputWithUnit } from '../_shared/';
@ -135,7 +135,9 @@ export function MediaShareEmbed(props) {
<div className="share-embed-inner"> <div className="share-embed-inner">
<div className="on-left"> <div className="on-left">
<div className="media-embed-wrap"> <div className="media-embed-wrap">
<VideoViewer data={MediaPageStore.get('media-data')} inEmbed={true} /> <SiteConsumer>
{(site) => <VideoViewer data={MediaPageStore.get('media-data')} siteUrl={site.url} inEmbed={true} />}
</SiteConsumer>
</div> </div>
</div> </div>