diff --git a/src/components/VideoPlayer/SubtitleManager.tsx b/src/components/VideoPlayer/SubtitleManager.tsx index ca636f2..62f9c12 100644 --- a/src/components/VideoPlayer/SubtitleManager.tsx +++ b/src/components/VideoPlayer/SubtitleManager.tsx @@ -1,7 +1,11 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { QortalGetMetadata, QortalMetadata, Service } from "../../types/interfaces/resources"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import { - alpha, + QortalGetMetadata, + QortalMetadata, + Service, +} from "../../types/interfaces/resources"; +import { + alpha, Box, Button, ButtonBase, @@ -15,8 +19,10 @@ import { Popover, Typography, } from "@mui/material"; -import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos'; -import ModeEditIcon from '@mui/icons-material/ModeEdit'; +import CheckIcon from '@mui/icons-material/Check'; +import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; +import ArrowBackIosIcon from "@mui/icons-material/ArrowBackIos"; +import ModeEditIcon from "@mui/icons-material/ModeEdit"; import CloseIcon from "@mui/icons-material/Close"; import { useListStore } from "../../state/lists"; import { Resource, useResources } from "../../hooks/useResources"; @@ -31,14 +37,15 @@ import { } from "react-dropzone"; import { fileToBase64, objectToBase64 } from "../../utils/base64"; import { ResourceToPublish } from "../../types/qortalRequests/types"; -import { useListReturn } from "../../hooks/useListData"; +import { useListReturn } from "../../hooks/useListData"; import { usePublish } from "../../hooks/usePublish"; interface SubtitleManagerProps { qortalMetadata: QortalGetMetadata; close: () => void; open: boolean; - onSelect: (subtitle: SubtitlePublishedData)=> void; - subtitleBtnRef: any + onSelect: (subtitle: SubtitlePublishedData) => void; + subtitleBtnRef: any; + currentSubTrack: null | string; } export interface Subtitle { language: string | null; @@ -65,32 +72,38 @@ const SubtitleManagerComponent = ({ open, close, onSelect, - subtitleBtnRef + subtitleBtnRef, + currentSubTrack, }: SubtitleManagerProps) => { const [mode, setMode] = useState(1); const { lists, identifierOperations, auth } = useGlobal(); const { fetchResources } = useResources(); // const [subtitles, setSubtitles] = useState([]) - const subtitles = useListReturn(`subs-${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`) - + const subtitles = useListReturn( + `subs-${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}` + ); - console.log('subtitles222', subtitles) + console.log("subtitles222", subtitles); const getPublishedSubtitles = useCallback(async () => { try { const videoId = `${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`; - console.log('videoId', videoId) + console.log("videoId", videoId); const postIdSearch = await identifierOperations.buildSearchPrefix( ENTITY_SUBTITLE, - videoId, + videoId ); const searchParams = { service: SERVICE_SUBTITLE, identifier: postIdSearch, - limit: 0 + limit: 0, }; - const res = await lists.fetchResources(searchParams, `subs-${videoId}`, "BASE64"); - lists.addList(`subs-${videoId}`, res || []); - console.log('resres2', res) + const res = await lists.fetchResources( + searchParams, + `subs-${videoId}`, + "BASE64" + ); + lists.addList(`subs-${videoId}`, res || []); + console.log("resres2", res); } catch (error) { console.error(error); } @@ -104,7 +117,7 @@ const SubtitleManagerComponent = ({ ) return; - getPublishedSubtitles() + getPublishedSubtitles(); }, [ qortalMetadata?.identifier, qortalMetadata?.service, @@ -120,137 +133,153 @@ const SubtitleManagerComponent = ({ // setHasMetadata(false); }; - const publishHandler = async (subtitles: Subtitle[]) => { try { - const videoId = `${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`; + const videoId = `${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`; - const identifier = await identifierOperations.buildIdentifier(ENTITY_SUBTITLE, videoId); - const name = auth?.name -console.log('identifier2', identifier) - if(!name) return - const resources: ResourceToPublish[] = [] - const tempResources: {qortalMetadata: QortalMetadata, data: any}[] = [] - for(const sub of subtitles ){ - const data = { - subtitleData: sub.base64, - language: sub.language, - filename: sub.filename, - type: sub.type - } - - const base64Data = await objectToBase64(data) - const resource = { - name, - identifier, - service: SERVICE_SUBTITLE, - base64: base64Data, - filename: sub.filename, - title: sub.language || undefined - } - resources.push(resource) - tempResources.push({ - qortalMetadata: { - identifier, - service: SERVICE_SUBTITLE, - name, - size: 100, - created: Date.now() - }, - data: data, - }) - } - console.log('resources', resources) + const identifier = await identifierOperations.buildIdentifier( + ENTITY_SUBTITLE, + videoId + ); + const name = auth?.name; + console.log("identifier2", identifier); + if (!name) return; + const resources: ResourceToPublish[] = []; + const tempResources: { qortalMetadata: QortalMetadata; data: any }[] = []; + for (const sub of subtitles) { + const data = { + subtitleData: sub.base64, + language: sub.language, + filename: sub.filename, + type: sub.type, + }; - await qortalRequest({ - action: 'PUBLISH_MULTIPLE_QDN_RESOURCES', - resources - }) + const base64Data = await objectToBase64(data); + const resource = { + name, + identifier, + service: SERVICE_SUBTITLE, + base64: base64Data, + filename: sub.filename, + title: sub.language || undefined, + }; + resources.push(resource); + tempResources.push({ + qortalMetadata: { + identifier, + service: SERVICE_SUBTITLE, + name, + size: 100, + created: Date.now(), + }, + data: data, + }); + } + console.log("resources", resources); - - lists.addNewResources(`subs-${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`, tempResources) - } catch (error) { - - } + await qortalRequest({ + action: "PUBLISH_MULTIPLE_QDN_RESOURCES", + resources, + }); + + lists.addNewResources( + `subs-${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`, + tempResources + ); + } catch (error) {} + }; + const onBack = () => { + if (mode === 1) close(); + }; + + const onSelectHandler = (sub: SubtitlePublishedData) => { + console.log('onSelectHandler') + onSelect(sub); + close(); }; - const onBack = ()=> { - if(mode === 1) close() - } - const onSelectHandler = (sub: SubtitlePublishedData)=> { - onSelect(sub) - close() - } return ( + - - - - - - Subtitles - - - - - - - - {mode === 1 && ( + + + + + + Subtitles + + + + + + + + {mode === 1 && ( )} - {/* + {/* {[ 'Ambient mode', 'Annotations', @@ -274,7 +303,7 @@ console.log('identifier2', identifier) ))} */} - + // void; onSelect: (subtitle: any) => void; - onBack: ()=> void; + onBack: () => void; + currentSubTrack: string | null } const PublisherSubtitles = ({ @@ -352,28 +382,29 @@ const PublisherSubtitles = ({ subtitles, setMode, onSelect, - onBack + onBack, + currentSubTrack }: PublisherSubtitlesProps) => { - - return ( <> - - {subtitles?.map((sub)=> { - return - })} - - + {subtitles?.map((sub) => { + return ( + + ); + })} ); }; interface PublishSubtitlesProps { - publishHandler: (subs: Subtitle[])=> void + publishHandler: (subs: Subtitle[]) => void; } - - const PublishSubtitles = ({ publishHandler }: PublishSubtitlesProps) => { const [language, setLanguage] = useState(null); const [subtitles, setSubtitles] = useState([]); @@ -388,7 +419,7 @@ const PublishSubtitles = ({ publishHandler }: PublishSubtitlesProps) => { filename: file.name, size: file.size, }; - newSubtitles.push(newSubtitle) + newSubtitles.push(newSubtitle); } catch (error) { console.error("Failed to parse audio file:", error); } @@ -412,19 +443,19 @@ const PublishSubtitles = ({ publishHandler }: PublishSubtitlesProps) => { maxSize: 2 * 1024 * 1024, // 2MB }); -const onChangeValue = (field: string, data: any, index: number) => { - const sub = subtitles[index]; - if (!sub) return; + const onChangeValue = (field: string, data: any, index: number) => { + const sub = subtitles[index]; + if (!sub) return; - const copySub = { ...sub, [field]: data }; + const copySub = { ...sub, [field]: data }; - setSubtitles((prev) => { - const copyPrev = [...prev]; - copyPrev[index] = copySub; - return copyPrev; - }); -}; -console.log('subtitles', subtitles) + setSubtitles((prev) => { + const copyPrev = [...prev]; + copyPrev[index] = copySub; + return copyPrev; + }); + }; + console.log("subtitles", subtitles); return ( <> @@ -438,64 +469,78 @@ console.log('subtitles', subtitles) alignItems: "flex-start", }} > - - - + + + {subtitles?.map((sub, i) => { return ( <> onChangeValue('language',val, i)} + onChange={(val: string | null) => + onChangeValue("language", val, i) + } /> ); })} - - - + + + ); }; interface SubProps { - sub: QortalGetMetadata - onSelect: (subtitle: Subtitle)=> void; -} -const Subtitle = ({sub, onSelect}: SubProps)=> { - const {resource, isLoading } = usePublish(2, 'JSON', sub) - console.log('resource', resource) - return onSelect(resource?.data)} - - sx={{ - px: 2, - py: 1, - '&:hover': { - backgroundColor: 'rgba(255, 255, 255, 0.1)', - cursor: 'pointer', - }, - }} - > - {resource?.data?.language} - + sub: QortalGetMetadata; + onSelect: (subtitle: Subtitle) => void; + currentSubtrack: null | string } +const Subtitle = ({ sub, onSelect, currentSubtrack }: SubProps) => { + const { resource, isLoading } = usePublish(2, "JSON", sub); + console.log("resource", resource); + const isSelected = currentSubtrack === resource?.data?.language + return ( + onSelect(isSelected ? null : resource?.data)} sx={{ + px: 2, + py: 1, + "&:hover": { + backgroundColor: "rgba(255, 255, 255, 0.1)", + }, + width: '100%', + justifyContent: 'space-between' + }}> + + {resource?.data?.language} + + {isSelected ? ( + + ) : ( + + )} + + + ); +}; - export const SubtitleManager = React.memo(SubtitleManagerComponent); +export const SubtitleManager = React.memo(SubtitleManagerComponent); diff --git a/src/components/VideoPlayer/VideoControlsBar.tsx b/src/components/VideoPlayer/VideoControlsBar.tsx index 355cbcd..261c19b 100644 --- a/src/components/VideoPlayer/VideoControlsBar.tsx +++ b/src/components/VideoPlayer/VideoControlsBar.tsx @@ -65,6 +65,7 @@ export const VideoControlsBar = ({subtitleBtnRef, showControls, playbackRate, in opacity: showControls ? 1 : 0, pointerEvents: showControls ? 'auto' : 'none', transition: 'opacity 0.4s ease-in-out', + width: '100%' // ...additionalStyles // height: controlsHeight, }} @@ -94,7 +95,7 @@ export const VideoControlsBar = ({subtitleBtnRef, showControls, playbackRate, in - + diff --git a/src/components/VideoPlayer/VideoPlayer.tsx b/src/components/VideoPlayer/VideoPlayer.tsx index 096269f..da7c4ba 100644 --- a/src/components/VideoPlayer/VideoPlayer.tsx +++ b/src/components/VideoPlayer/VideoPlayer.tsx @@ -1,4 +1,13 @@ -import { ReactEventHandler, Ref, RefObject, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + ReactEventHandler, + Ref, + RefObject, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { QortalGetMetadata } from "../../types/interfaces/resources"; import { VideoContainer, VideoElement } from "./VideoPlayer-styles"; import { useVideoPlayerHotKeys } from "./useVideoPlayerHotKeys"; @@ -6,15 +15,21 @@ import { useProgressStore, useVideoStore } from "../../state/video"; import { useVideoPlayerController } from "./useVideoPlayerController"; import { LoadingVideo } from "./LoadingVideo"; import { VideoControlsBar } from "./VideoControlsBar"; -import videojs from 'video.js'; -import 'video.js/dist/video-js.css'; +import videojs from "video.js"; +import "video.js/dist/video-js.css"; import Player from "video.js/dist/types/player"; -import { Subtitle, SubtitleManager, SubtitlePublishedData } from "./SubtitleManager"; +import { + Subtitle, + SubtitleManager, + SubtitlePublishedData, +} from "./SubtitleManager"; import { base64ToBlobUrl } from "../../utils/base64"; -import convert from 'srt-webvtt'; +import convert from "srt-webvtt"; -export async function srtBase64ToVttBlobUrl(base64Srt: string): Promise { +export async function srtBase64ToVttBlobUrl( + base64Srt: string +): Promise { try { // Step 1: Convert base64 string to a Uint8Array const binary = atob(base64Srt); @@ -24,20 +39,19 @@ export async function srtBase64ToVttBlobUrl(base64Srt: string): Promise; retryAttempts?: number; @@ -47,27 +61,30 @@ type StretchVideoType = "contain" | "fill" | "cover" | "none" | "scale-down"; } const videoStyles = { - videoContainer: { }, - video: { }, + videoContainer: {}, + video: {}, }; -async function loadMediaInfo(wasmPath = '/MediaInfoModule.wasm') { - const mediaInfoModule = await import('mediainfo.js'); +async function loadMediaInfo(wasmPath = "/MediaInfoModule.wasm") { + const mediaInfoModule = await import("mediainfo.js"); return await mediaInfoModule.default({ - format: 'JSON', + format: "JSON", full: true, locateFile: () => wasmPath, }); } -async function getVideoMimeTypeFromUrl(qortalVideoResource: any): Promise { - +async function getVideoMimeTypeFromUrl( + qortalVideoResource: any +): Promise { try { - const metadataResponse = await fetch(`/arbitrary/metadata/${qortalVideoResource.service}/${qortalVideoResource.name}/${qortalVideoResource.identifier}`) - const metadataData = await metadataResponse.json() - return metadataData?.mimeType || null + const metadataResponse = await fetch( + `/arbitrary/metadata/${qortalVideoResource.service}/${qortalVideoResource.name}/${qortalVideoResource.identifier}` + ); + const metadataData = await metadataResponse.json(); + return metadataData?.mimeType || null; } catch (error) { - return null + return null; } // const mediaInfo = await loadMediaInfo(); // const chunkCache = new Map(); @@ -147,23 +164,26 @@ export const VideoPlayer = ({ const containerRef = useRef | null>(null); const [videoObjectFit] = useState("contain"); const [isPlaying, setIsPlaying] = useState(false); - const { volume, setVolume, setPlaybackRate, playbackRate } = useVideoStore((state) => ({ - volume: state.playbackSettings.volume, - setVolume: state.setVolume, - setPlaybackRate: state.setPlaybackRate, - playbackRate: state.playbackSettings.playbackRate - })); + const { volume, setVolume, setPlaybackRate, playbackRate } = useVideoStore( + (state) => ({ + volume: state.playbackSettings.volume, + setVolume: state.setVolume, + setPlaybackRate: state.setPlaybackRate, + playbackRate: state.playbackSettings.playbackRate, + }) + ); const playerRef = useRef(null); - const [isPlayerInitialized, setIsPlayerInitialized] = useState(false) - const [videoCodec, setVideoCodec] = useState(null) + const [isPlayerInitialized, setIsPlayerInitialized] = useState(false); + const [videoCodec, setVideoCodec] = useState(null); const [isMuted, setIsMuted] = useState(false); const { setProgress } = useProgressStore(); - const [localProgress, setLocalProgress] = useState(0) - const [duration, setDuration] = useState(0) + const [localProgress, setLocalProgress] = useState(0); + const [duration, setDuration] = useState(0); const [isLoading, setIsLoading] = useState(true); - const [showControls, setShowControls] = useState(false) - const [isOpenSubtitleManage, setIsOpenSubtitleManage] = useState(false) - const subtitleBtnRef = useRef(null) + const [showControls, setShowControls] = useState(false); + const [isOpenSubtitleManage, setIsOpenSubtitleManage] = useState(false); + const subtitleBtnRef = useRef(null); + const [currentSubTrack, setCurrentSubTrack] = useState(null) const { reloadVideo, togglePlay, @@ -183,15 +203,15 @@ export const VideoPlayer = ({ startPlay, setProgressAbsolute, setAlwaysShowControls, - status, percentLoaded, + status, + percentLoaded, showControlsFullScreen, - } = useVideoPlayerController({ autoPlay, playerRef, qortalVideoResource, retryAttempts, - isPlayerInitialized + isPlayerInitialized, }); const hotkeyHandlers = useMemo( @@ -223,16 +243,12 @@ export const VideoPlayer = ({ ] ); - - - - -const closeSubtitleManager = useCallback(()=> { - setIsOpenSubtitleManage(false) -}, []) - const openSubtitleManager = useCallback(()=> { - setIsOpenSubtitleManage(true) -}, []) + const closeSubtitleManager = useCallback(() => { + setIsOpenSubtitleManage(false); + }, []); + const openSubtitleManager = useCallback(() => { + setIsOpenSubtitleManage(true); + }, []); const videoLocation = useMemo(() => { if (!qortalVideoResource) return null; @@ -241,15 +257,15 @@ const closeSubtitleManager = useCallback(()=> { useVideoPlayerHotKeys(hotkeyHandlers); const updateProgress = useCallback(() => { - const player = playerRef?.current; - if (!player || typeof player?.currentTime !== 'function') return; + const player = playerRef?.current; + if (!player || typeof player?.currentTime !== "function") return; - const currentTime = player.currentTime(); - if (typeof currentTime === 'number' && videoLocation && currentTime > 0.1) { - setProgress(videoLocation, currentTime); - setLocalProgress(currentTime); - } -}, [videoLocation]); + const currentTime = player.currentTime(); + if (typeof currentTime === "number" && videoLocation && currentTime > 0.1) { + setProgress(videoLocation, currentTime); + setLocalProgress(currentTime); + } + }, [videoLocation]); // useEffect(() => { // const ref = videoRef as React.RefObject; // if (!ref.current) return; @@ -271,33 +287,30 @@ const closeSubtitleManager = useCallback(()=> { (e: React.SyntheticEvent) => { try { const video = e.currentTarget; - setVolume(video.volume); - setIsMuted(video.muted); + setVolume(video.volume); + setIsMuted(video.muted); } catch (error) { - console.error('onVolumeChangeHandler', onVolumeChangeHandler) + console.error("onVolumeChangeHandler", onVolumeChangeHandler); } }, [setIsMuted, setVolume] ); - - const videoStylesContainer = useMemo(() => { return { - cursor: showControls ? 'auto' : 'none', - aspectRatio: '16 / 9', + cursor: showControls ? "auto" : "none", + aspectRatio: "16 / 9", ...videoStyles?.videoContainer, }; }, [showControls]); - const videoStylesVideo = useMemo(() => { return { ...videoStyles?.video, objectFit: videoObjectFit, backgroundColor: "#000000", - height: isFullscreen ? "calc(100vh - 40px)" : "100%", - width: '100%' + height: isFullscreen ? "calc(100vh - 40px)" : "100%", + width: "100%", }; }, [videoObjectFit, isFullscreen]); @@ -310,29 +323,28 @@ const closeSubtitleManager = useCallback(()=> { [onEnded] ); - const handleCanPlay = useCallback(()=> { + const handleCanPlay = useCallback(() => { setIsLoading(false); - }, [setIsLoading]) + }, [setIsLoading]); - useEffect(() => { - if(!isPlayerInitialized) return - const player = playerRef.current; - if (!player || typeof player.on !== 'function') return; + useEffect(() => { + if (!isPlayerInitialized) return; + const player = playerRef.current; + if (!player || typeof player.on !== "function") return; - const handleLoadedMetadata = () => { - const duration = player.duration?.(); - if (typeof duration === 'number' && !isNaN(duration)) { - setDuration(duration); - } - }; + const handleLoadedMetadata = () => { + const duration = player.duration?.(); + if (typeof duration === "number" && !isNaN(duration)) { + setDuration(duration); + } + }; - player.on('loadedmetadata', handleLoadedMetadata); - - return () => { - player.off('loadedmetadata', handleLoadedMetadata); - }; -}, [isPlayerInitialized]); + player.on("loadedmetadata", handleLoadedMetadata); + return () => { + player.off("loadedmetadata", handleLoadedMetadata); + }; + }, [isPlayerInitialized]); const enterFullscreen = () => { const ref = containerRef?.current as any; @@ -341,8 +353,6 @@ const closeSubtitleManager = useCallback(()=> { if (ref.requestFullscreen && !isFullscreen) { ref.requestFullscreen(); } - - }; const exitFullscreen = () => { @@ -353,303 +363,397 @@ const closeSubtitleManager = useCallback(()=> { isFullscreen ? exitFullscreen() : enterFullscreen(); }; -const canvasRef = useRef(null) -const videoRefForCanvas = useRef(null) -const extractFrames = useCallback( (time: number): void => { - // const video = videoRefForCanvas?.current; - // const canvas: any = canvasRef.current; - - // if (!video || !canvas) return null; - - // // Avoid unnecessary resize if already correct - // if (canvas.width !== video.videoWidth || canvas.height !== video.videoHeight) { - // canvas.width = video.videoWidth; - // canvas.height = video.videoHeight; - // } - - // const context = canvas.getContext("2d"); - // if (!context) return null; - - // // If video is already near the correct time, don't seek again - // const threshold = 0.01; // 10ms threshold - // if (Math.abs(video.currentTime - time) > threshold) { - // await new Promise((resolve) => { - // const onSeeked = () => resolve(); - // video.addEventListener("seeked", onSeeked, { once: true }); - // video.currentTime = time; - // }); - // } - - // context.drawImage(video, 0, 0, canvas.width, canvas.height); - - // // Use a faster method for image export (optional tradeoff) - // const blob = await new Promise((resolve) => { - // canvas.toBlob((blob: any) => resolve(blob), "image/webp", 0.7); - // }); - - // if (!blob) return null; - - // return URL.createObjectURL(blob); -}, []); - + const canvasRef = useRef(null); + const videoRefForCanvas = useRef(null); + const extractFrames = useCallback((time: number): void => { + // const video = videoRefForCanvas?.current; + // const canvas: any = canvasRef.current; + // if (!video || !canvas) return null; + // // Avoid unnecessary resize if already correct + // if (canvas.width !== video.videoWidth || canvas.height !== video.videoHeight) { + // canvas.width = video.videoWidth; + // canvas.height = video.videoHeight; + // } + // const context = canvas.getContext("2d"); + // if (!context) return null; + // // If video is already near the correct time, don't seek again + // const threshold = 0.01; // 10ms threshold + // if (Math.abs(video.currentTime - time) > threshold) { + // await new Promise((resolve) => { + // const onSeeked = () => resolve(); + // video.addEventListener("seeked", onSeeked, { once: true }); + // video.currentTime = time; + // }); + // } + // context.drawImage(video, 0, 0, canvas.width, canvas.height); + // // Use a faster method for image export (optional tradeoff) + // const blob = await new Promise((resolve) => { + // canvas.toBlob((blob: any) => resolve(blob), "image/webp", 0.7); + // }); + // if (!blob) return null; + // return URL.createObjectURL(blob); + }, []); const hideTimeout = useRef(null); - -const resetHideTimer = () => { - setShowControls(true); - if (hideTimeout.current) clearTimeout(hideTimeout.current); - hideTimeout.current = setTimeout(() => { - setShowControls(false); - }, 2500); // 3s of inactivity -}; - -const handleMouseMove = () => { - resetHideTimer(); -}; - -useEffect(() => { - resetHideTimer(); // initial show - return () => { + const resetHideTimer = () => { + setShowControls(true); if (hideTimeout.current) clearTimeout(hideTimeout.current); + hideTimeout.current = setTimeout(() => { + setShowControls(false); + }, 2500); // 3s of inactivity }; -}, []); -const previousSubtitleUrlRef = useRef(null); - -useEffect(() => { - return () => { - // Component unmount cleanup - if (previousSubtitleUrlRef.current) { - URL.revokeObjectURL(previousSubtitleUrlRef.current); - previousSubtitleUrlRef.current = null; - } + const handleMouseMove = () => { + resetHideTimer(); }; -}, []); -const onSelectSubtitle = useCallback(async (subtitle: SubtitlePublishedData)=> { - console.log('onSelectSubtitle', subtitle) - const player = playerRef.current; - if (!player || !subtitle.subtitleData || !subtitle.type) return; + useEffect(() => { + resetHideTimer(); // initial show + return () => { + if (hideTimeout.current) clearTimeout(hideTimeout.current); + }; + }, []); - // Cleanup: revoke previous Blob URL - if (previousSubtitleUrlRef.current) { - URL.revokeObjectURL(previousSubtitleUrlRef.current); - previousSubtitleUrlRef.current = null; - } - let blobUrl - if(subtitle?.type === "application/x-subrip"){ - blobUrl = await srtBase64ToVttBlobUrl(subtitle.subtitleData) - } else { - blobUrl = base64ToBlobUrl(subtitle.subtitleData, subtitle.type) + const previousSubtitleUrlRef = useRef(null); - } - - previousSubtitleUrlRef.current = blobUrl; + useEffect(() => { + return () => { + // Component unmount cleanup + if (previousSubtitleUrlRef.current) { + URL.revokeObjectURL(previousSubtitleUrlRef.current); + previousSubtitleUrlRef.current = null; + } + }; + }, []); -const remoteTracksList = playerRef.current?.remoteTextTracks(); + const onSelectSubtitle = useCallback( + async (subtitle: SubtitlePublishedData) => { + if(subtitle === null){ + setCurrentSubTrack(null) + if (previousSubtitleUrlRef.current) { + URL.revokeObjectURL(previousSubtitleUrlRef.current); + previousSubtitleUrlRef.current = null; + } + const remoteTracksList = playerRef.current?.remoteTextTracks(); -if (remoteTracksList) { - const toRemove: TextTrack[] = []; + if (remoteTracksList) { + const toRemove: TextTrack[] = []; - // Bypass TS restrictions safely - const list = remoteTracksList as unknown as { length: number; [index: number]: TextTrack }; + // Bypass TS restrictions safely + const list = remoteTracksList as unknown as { + length: number; + [index: number]: TextTrack; + }; - for (let i = 0; i < list.length; i++) { - const track = list[i]; - if (track) toRemove.push(track); - } + for (let i = 0; i < list.length; i++) { + const track = list[i]; + if (track) toRemove.push(track); + } - toRemove.forEach((track) => { - playerRef.current?.removeRemoteTextTrack(track); - }); -} - playerRef.current?.addRemoteTextTrack({ - kind: 'subtitles', - src: blobUrl, - srclang: 'en', - label: 'English', - default: true - }, true); + toRemove.forEach((track) => { + playerRef.current?.removeRemoteTextTrack(track); + }); + } - // Remove all existing remote text tracks -// try { -// const remoteTracks = playerRef.current?.remoteTextTracks()?.tracks_ -// if (remoteTracks && remoteTracks?.length) { -// const toRemove: TextTrack[] = []; -// for (let i = 0; i < remoteTracks.length; i++) { -// const track = remoteTracks[i]; -// toRemove.push(track); -// } -// toRemove.forEach((track) => { -// console.log('removing track') -// playerRef.current?.removeRemoteTextTrack(track); -// }); -// } -// } catch (error) { -// console.log('error2', error) -// } + return + } + console.log("onSelectSubtitle", subtitle); + const player = playerRef.current; + if (!player || !subtitle.subtitleData || !subtitle.type) return; -await new Promise((res)=> { - setTimeout(() => { - res(null) - }, 1000); -}) -const tracksInfo = playerRef.current?.textTracks(); -console.log('tracksInfo', tracksInfo) -if (!tracksInfo) return; + // Cleanup: revoke previous Blob URL + if (previousSubtitleUrlRef.current) { + URL.revokeObjectURL(previousSubtitleUrlRef.current); + previousSubtitleUrlRef.current = null; + } + let blobUrl; + if (subtitle?.type === "application/x-subrip") { + blobUrl = await srtBase64ToVttBlobUrl(subtitle.subtitleData); + } else { + blobUrl = base64ToBlobUrl(subtitle.subtitleData, subtitle.type); + } -const tracks = Array.from({ length: (tracksInfo as any).length }, (_, i) => (tracksInfo as any)[i]); -console.log('tracks', tracks) -for (const track of tracks) { - console.log('track', track) + previousSubtitleUrlRef.current = blobUrl; - if (track.kind === 'subtitles') { - track.mode = 'showing'; // force display - } -} + const remoteTracksList = playerRef.current?.remoteTextTracks(); + if (remoteTracksList) { + const toRemove: TextTrack[] = []; -},[]) + // Bypass TS restrictions safely + const list = remoteTracksList as unknown as { + length: number; + [index: number]: TextTrack; + }; + + for (let i = 0; i < list.length; i++) { + const track = list[i]; + if (track) toRemove.push(track); + } + + toRemove.forEach((track) => { + playerRef.current?.removeRemoteTextTrack(track); + }); + } + playerRef.current?.addRemoteTextTrack( + { + kind: "subtitles", + src: blobUrl, + srclang: subtitle.language, + label: subtitle.language, + default: true, + }, + true + ); + + // Remove all existing remote text tracks + // try { + // const remoteTracks = playerRef.current?.remoteTextTracks()?.tracks_ + // if (remoteTracks && remoteTracks?.length) { + // const toRemove: TextTrack[] = []; + // for (let i = 0; i < remoteTracks.length; i++) { + // const track = remoteTracks[i]; + // toRemove.push(track); + // } + // toRemove.forEach((track) => { + // console.log('removing track') + // playerRef.current?.removeRemoteTextTrack(track); + // }); + // } + // } catch (error) { + // console.log('error2', error) + // } + + await new Promise((res) => { + setTimeout(() => { + res(null); + }, 1000); + }); + const tracksInfo = playerRef.current?.textTracks(); + console.log("tracksInfo", tracksInfo); + if (!tracksInfo) return; + + const tracks = Array.from( + { length: (tracksInfo as any).length }, + (_, i) => (tracksInfo as any)[i] + ); + console.log("tracks", tracks); + for (const track of tracks) { + console.log("track", track); + + if (track.kind === "subtitles") { + track.mode = "showing"; // force display + } + } + }, + [] + ); const handleMouseLeave = useCallback(() => { setShowControls(false); if (hideTimeout.current) clearTimeout(hideTimeout.current); }, [setShowControls]); - - const videoLocactionStringified = useMemo(()=> { - return JSON.stringify(qortalVideoResource) - }, [qortalVideoResource]) - -useEffect(() => { - if (!resourceUrl || !isReady || !videoLocactionStringified || !startPlay) return; - - const resource = JSON.parse(videoLocactionStringified) - let canceled = false; - - try { - const setupPlayer = async () => { - const type = await getVideoMimeTypeFromUrl(resource); - if (canceled) return; - - const options = { - autoplay: true, - controls: false, - responsive: true, - fluid: true, - poster: startPlay ? "" : poster, - aspectRatio: '16:9' , - sources: [ - { - src: resourceUrl, - type: type || 'video/mp4', // fallback - }, - ], - }; - const ref = videoRef as any; - if (!ref.current) return; - - if (!playerRef.current && ref.current) { - playerRef.current = videojs(ref.current, options, () => { - setIsPlayerInitialized(true) - playerRef.current?.poster(''); - playerRef.current?.playbackRate(playbackRate) - playerRef.current?.volume(volume); - - - playerRef.current?.play(); - - }); - playerRef.current?.on('error', () => { - const error = playerRef.current?.error(); - console.error('Video.js playback error:', error); - // Optional: display user-friendly message - }); - } - }; - - setupPlayer(); - - } catch (error) { - console.error('useEffect start player', error) - } - return () => { - canceled = true; - const player = playerRef.current; - - if (player && typeof player.dispose === 'function') { - try { - player.dispose(); - } catch (err) { - console.error('Error disposing Video.js player:', err); - } - playerRef.current = null; - } -}; -}, [isReady, resourceUrl, startPlay, poster, videoLocactionStringified]); + const videoLocactionStringified = useMemo(() => { + return JSON.stringify(qortalVideoResource); + }, [qortalVideoResource]); useEffect(() => { - if(!isPlayerInitialized) return - const player = playerRef?.current; - if (!player) return; + if (!resourceUrl || !isReady || !videoLocactionStringified || !startPlay) + return; - const handleRateChange = () => { - const newRate = player?.playbackRate(); - if(newRate){ - setPlaybackRate(newRate); // or any other state/action + const resource = JSON.parse(videoLocactionStringified); + let canceled = false; + + try { + const setupPlayer = async () => { + const type = await getVideoMimeTypeFromUrl(resource); + if (canceled) return; + + const options = { + autoplay: true, + controls: false, + responsive: true, + fluid: true, + poster: startPlay ? "" : poster, + aspectRatio: "16:9", + sources: [ + { + src: resourceUrl, + type: type || "video/mp4", // fallback + }, + ], + }; + const ref = videoRef as any; + if (!ref.current) return; + + if (!playerRef.current && ref.current) { + playerRef.current = videojs(ref.current, options, () => { + setIsPlayerInitialized(true); + playerRef.current?.poster(""); + playerRef.current?.playbackRate(playbackRate); + playerRef.current?.volume(volume); + + playerRef.current?.play(); + + const tracksInfo = playerRef.current?.textTracks(); + + const checkActiveSubtitle = () => { + let activeTrack = null; + + const tracks = Array.from( + { length: (tracksInfo as any).length }, + (_, i) => (tracksInfo as any)[i] + ); + console.log("tracks", tracks); + for (const track of tracks) { + + if (track.kind === 'subtitles' || track.kind === 'captions') { + if (track.mode === 'showing') { + activeTrack = track; + break; + } + } + } + + if (activeTrack) { + console.log("Subtitle active:", { + label: activeTrack.label, + srclang: activeTrack.language || activeTrack.srclang, // srclang for native, language for VTT + }); + setCurrentSubTrack(activeTrack.language || activeTrack.srclang) + } else { + setCurrentSubTrack(null) + console.log("No subtitle is currently showing"); + } + }; + + // Initial check in case one is auto-enabled + checkActiveSubtitle(); + + // Use Video.js event system + tracksInfo?.on("change", checkActiveSubtitle); + }); + playerRef.current?.on("error", () => { + const error = playerRef.current?.error(); + console.error("Video.js playback error:", error); + // Optional: display user-friendly message + }); + } + }; + + setupPlayer(); + } catch (error) { + console.error("useEffect start player", error); } - }; + return () => { + canceled = true; + const player = playerRef.current; - player.on('ratechange', handleRateChange); + if (player && typeof player.dispose === "function") { + try { + player.dispose(); + } catch (err) { + console.error("Error disposing Video.js player:", err); + } + playerRef.current = null; + } + }; + }, [isReady, resourceUrl, startPlay, poster, videoLocactionStringified]); - return () => { - player.off('ratechange', handleRateChange); - }; -}, [isPlayerInitialized]); + useEffect(() => { + if (!isPlayerInitialized) return; + const player = playerRef?.current; + if (!player) return; + + const handleRateChange = () => { + const newRate = player?.playbackRate(); + if (newRate) { + setPlaybackRate(newRate); // or any other state/action + } + }; + + player.on("ratechange", handleRateChange); + + return () => { + player.off("ratechange", handleRateChange); + }; + }, [isPlayerInitialized]); return ( <> - {/* */} - - - - */} + + + + + {/* */} - src={isReady && startPlay ? resourceUrl || undefined : undefined} - poster={startPlay ? "" : poster} - onTimeUpdate={updateProgress} - autoPlay={autoPlay} - onClick={togglePlay} - onEnded={handleEnded} - onCanPlay={handleCanPlay} - preload="metadata" - style={videoStylesVideo} - onPlay={onPlay} - onPause={onPause} - onVolumeChange={onVolumeChangeHandler} - controls={false} - - /> - {/* */} - - {isReady && ( - - )} - - - - + + )} + + + + ); };