mirror of
https://github.com/Qortal/qapp-core.git
synced 2025-06-18 19:01:21 +00:00
added timeline actions
This commit is contained in:
parent
8295688b8a
commit
945f5b7d83
@ -58,13 +58,15 @@ import {
|
||||
showLoading,
|
||||
showSuccess,
|
||||
} from "../../utils/toast";
|
||||
interface SubtitleManagerProps {
|
||||
|
||||
|
||||
export interface SubtitleManagerProps {
|
||||
qortalMetadata: QortalGetMetadata;
|
||||
close: () => void;
|
||||
open: boolean;
|
||||
onSelect: (subtitle: SubtitlePublishedData) => void;
|
||||
subtitleBtnRef: any;
|
||||
currentSubTrack: null | string;
|
||||
currentSubTrack: null | string
|
||||
}
|
||||
export interface Subtitle {
|
||||
language: string | null;
|
||||
@ -123,7 +125,7 @@ const SubtitleManagerComponent = ({
|
||||
setIsLoading(true);
|
||||
const videoId = `${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`;
|
||||
console.log("videoId", videoId);
|
||||
const postIdSearch = await identifierOperations.buildSearchPrefix(
|
||||
const postIdSearch = await identifierOperations.buildLooseSearchPrefix(
|
||||
ENTITY_SUBTITLE,
|
||||
videoId
|
||||
);
|
||||
@ -179,16 +181,16 @@ const SubtitleManagerComponent = ({
|
||||
try {
|
||||
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 identifier = await identifierOperations.buildLooseIdentifier(
|
||||
ENTITY_SUBTITLE,
|
||||
videoId
|
||||
);
|
||||
const data = {
|
||||
subtitleData: sub.base64,
|
||||
language: sub.language,
|
||||
|
72
src/components/VideoPlayer/TimelineActionsComponent.tsx
Normal file
72
src/components/VideoPlayer/TimelineActionsComponent.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { TimelineAction } from './VideoPlayer'
|
||||
import { alpha, Box, ButtonBase, Popover, Typography } from '@mui/material'
|
||||
|
||||
interface TimelineActionsComponentProps {
|
||||
timelineActions: TimelineAction[]
|
||||
progress: number
|
||||
containerRef: any
|
||||
seekTo: (time: number)=> void
|
||||
|
||||
}
|
||||
|
||||
const placementStyles: Record<NonNullable<TimelineAction['placement']>, React.CSSProperties> = {
|
||||
'TOP-RIGHT': { top: 16, right: 16 },
|
||||
'TOP-LEFT': { top: 16, left: 16 },
|
||||
'BOTTOM-LEFT': { bottom: 60, left: 16 },
|
||||
'BOTTOM-RIGHT': { bottom: 60, right: 16 },
|
||||
};
|
||||
|
||||
export const TimelineActionsComponent = ({timelineActions, progress, containerRef, seekTo}: TimelineActionsComponentProps) => {
|
||||
const [isOpen, setIsOpen] = useState(true)
|
||||
|
||||
const handleClick = useCallback((action: TimelineAction)=> {
|
||||
if(action?.type === 'SEEK'){
|
||||
if(!action?.seekToTime) return
|
||||
seekTo(action.seekToTime)
|
||||
} else if(action?.type === 'CUSTOM'){
|
||||
if(action.onClick){
|
||||
action.onClick()
|
||||
}
|
||||
}
|
||||
},[])
|
||||
|
||||
// Find the current matching action(s)
|
||||
const activeActions = useMemo(() => {
|
||||
return timelineActions.filter(action => {
|
||||
return progress >= action.time && progress <= action.time + action.duration;
|
||||
});
|
||||
}, [timelineActions, progress]);
|
||||
|
||||
const hasActive = activeActions.length > 0;
|
||||
|
||||
if (!hasActive) return null; // Don’t render unless active
|
||||
return (
|
||||
<>
|
||||
{activeActions.map((action, index) => {
|
||||
const placement = (action.placement ?? 'TOP-RIGHT') as keyof typeof placementStyles;
|
||||
|
||||
return (
|
||||
<ButtonBase
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
bgcolor: alpha("#181818", 0.95),
|
||||
p: 1,
|
||||
borderRadius: 1,
|
||||
boxShadow: 3,
|
||||
zIndex: 10,
|
||||
outline: '1px solid white',
|
||||
...placementStyles[placement || 'TOP-RIGHT'],
|
||||
}}
|
||||
>
|
||||
|
||||
<Typography key={index} variant="body2" onClick={()=> handleClick(action)}>
|
||||
{action.label}
|
||||
</Typography>
|
||||
|
||||
</ButtonBase>
|
||||
)
|
||||
} )}
|
||||
</>
|
||||
)
|
||||
}
|
@ -17,11 +17,7 @@ export const VideoContainer = styled(Box)(({ theme }) => ({
|
||||
}));
|
||||
|
||||
export const VideoElement = styled("video")(({ theme }) => ({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
|
||||
background: "rgb(33, 33, 33)",
|
||||
|
||||
"&:focus": {
|
||||
|
@ -22,10 +22,12 @@ import Player from "video.js/dist/types/player";
|
||||
import {
|
||||
Subtitle,
|
||||
SubtitleManager,
|
||||
SubtitleManagerProps,
|
||||
SubtitlePublishedData,
|
||||
} from "./SubtitleManager";
|
||||
import { base64ToBlobUrl } from "../../utils/base64";
|
||||
import convert from "srt-webvtt";
|
||||
import { TimelineActionsComponent } from "./TimelineActionsComponent";
|
||||
|
||||
export async function srtBase64ToVttBlobUrl(
|
||||
base64Srt: string
|
||||
@ -51,6 +53,24 @@ export async function srtBase64ToVttBlobUrl(
|
||||
}
|
||||
type StretchVideoType = "contain" | "fill" | "cover" | "none" | "scale-down";
|
||||
|
||||
export type TimelineAction =
|
||||
| {
|
||||
type: 'SEEK';
|
||||
time: number;
|
||||
duration: number;
|
||||
label: string;
|
||||
onClick?: () => void;
|
||||
seekToTime: number; // ✅ Required for SEEK
|
||||
placement?: 'TOP-RIGHT' | 'TOP-LEFT' | 'BOTTOM-LEFT' | 'BOTTOM-RIGHT';
|
||||
}
|
||||
| {
|
||||
type: 'CUSTOM';
|
||||
time: number;
|
||||
duration: number;
|
||||
label: string;
|
||||
onClick: () => void; // ✅ Required for CUSTOM
|
||||
placement?: 'TOP-RIGHT' | 'TOP-LEFT' | 'BOTTOM-LEFT' | 'BOTTOM-RIGHT';
|
||||
};
|
||||
interface VideoPlayerProps {
|
||||
qortalVideoResource: QortalGetMetadata;
|
||||
videoRef: Ref<HTMLVideoElement>;
|
||||
@ -58,8 +78,11 @@ interface VideoPlayerProps {
|
||||
poster?: string;
|
||||
autoPlay?: boolean;
|
||||
onEnded?: (e: React.SyntheticEvent<HTMLVideoElement, Event>) => void;
|
||||
timelineActions?: TimelineAction[]
|
||||
}
|
||||
|
||||
|
||||
|
||||
const videoStyles = {
|
||||
videoContainer: {},
|
||||
video: {},
|
||||
@ -160,6 +183,7 @@ export const VideoPlayer = ({
|
||||
poster,
|
||||
autoPlay,
|
||||
onEnded,
|
||||
timelineActions
|
||||
}: VideoPlayerProps) => {
|
||||
const containerRef = useRef<RefObject<HTMLDivElement> | null>(null);
|
||||
const [videoObjectFit] = useState<StretchVideoType>("contain");
|
||||
@ -207,6 +231,7 @@ export const VideoPlayer = ({
|
||||
percentLoaded,
|
||||
showControlsFullScreen,
|
||||
onSelectPlaybackRate,
|
||||
seekTo
|
||||
} = useVideoPlayerController({
|
||||
autoPlay,
|
||||
playerRef,
|
||||
@ -324,7 +349,7 @@ export const VideoPlayer = ({
|
||||
const videoStylesContainer = useMemo(() => {
|
||||
return {
|
||||
cursor: showControls ? "auto" : "none",
|
||||
aspectRatio: "16 / 9",
|
||||
// aspectRatio: "16 / 9",
|
||||
...videoStyles?.videoContainer,
|
||||
};
|
||||
}, [showControls]);
|
||||
@ -584,9 +609,9 @@ export const VideoPlayer = ({
|
||||
autoplay: true,
|
||||
controls: false,
|
||||
responsive: true,
|
||||
fluid: true,
|
||||
// fluid: true,
|
||||
poster: startPlay ? "" : poster,
|
||||
aspectRatio: "16:9",
|
||||
// aspectRatio: "16:9",
|
||||
sources: [
|
||||
{
|
||||
src: resourceUrl,
|
||||
@ -758,7 +783,10 @@ export const VideoPlayer = ({
|
||||
toggleMute={toggleMute}
|
||||
/>
|
||||
)}
|
||||
{timelineActions && Array.isArray(timelineActions) && (
|
||||
<TimelineActionsComponent seekTo={seekTo} containerRef={containerRef} progress={localProgress} timelineActions={timelineActions}/>
|
||||
|
||||
)}
|
||||
<SubtitleManager
|
||||
subtitleBtnRef={subtitleBtnRef}
|
||||
close={closeSubtitleManager}
|
||||
|
@ -217,6 +217,17 @@ try {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const seekTo = useCallback((time: number) => {
|
||||
try {
|
||||
const player = playerRef.current;
|
||||
if (!player || typeof player.duration !== 'function' || typeof player.currentTime !== 'function') return;
|
||||
|
||||
player.currentTime(time);
|
||||
} catch (error) {
|
||||
console.error('setProgressAbsolute', error)
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
const toggleObjectFit = useCallback(() => {
|
||||
setVideoObjectFit(videoObjectFit === "contain" ? "fill" : "contain");
|
||||
@ -303,6 +314,6 @@ const togglePlay = useCallback(async () => {
|
||||
isReady,
|
||||
resourceUrl,
|
||||
startPlay,
|
||||
status, percentLoaded, showControlsFullScreen, onSelectPlaybackRate: updatePlaybackRate
|
||||
status, percentLoaded, showControlsFullScreen, onSelectPlaybackRate: updatePlaybackRate, seekTo
|
||||
};
|
||||
};
|
||||
|
@ -1,6 +1,8 @@
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import {
|
||||
buildIdentifier,
|
||||
buildLooseIdentifier,
|
||||
buildLooseSearchPrefix,
|
||||
buildSearchPrefix,
|
||||
EnumCollisionStrength,
|
||||
hashWord,
|
||||
@ -60,6 +62,8 @@ export const useIdentifiers = (publicSalt: string, appName: string) => {
|
||||
createSingleIdentifier,
|
||||
hashQortalName,
|
||||
hashString,
|
||||
buildLooseSearchPrefix,
|
||||
buildLooseIdentifier
|
||||
}),
|
||||
[
|
||||
buildIdentifierFunc,
|
||||
|
@ -4,6 +4,7 @@ export { useResourceStatus } from './hooks/useResourceStatus';
|
||||
export { Spacer } from './common/Spacer';
|
||||
export { useModal } from './hooks/useModal';
|
||||
export { AudioPlayerControls , OnTrackChangeMeta, AudioPlayerProps, AudioPlayerHandle} from './components/AudioPlayer/AudioPlayerControls';
|
||||
export {TimelineAction} from './components/VideoPlayer/VideoPlayer'
|
||||
export { useAudioPlayerHotkeys } from './components/AudioPlayer/useAudioPlayerHotkeys';
|
||||
export { VideoPlayer } from './components/VideoPlayer/VideoPlayer';
|
||||
export { useListReturn } from './hooks/useListData';
|
||||
|
@ -129,6 +129,33 @@ export async function buildSearchPrefix(
|
||||
: `${appHash}-${entityPrefix}-`; // ✅ Global search for entity type
|
||||
}
|
||||
|
||||
export async function buildLooseSearchPrefix(
|
||||
entityType: string,
|
||||
parentId?: string | null
|
||||
): Promise<string> {
|
||||
// Hash entity type (6 chars)
|
||||
const entityPrefix: string = await hashWord(
|
||||
entityType,
|
||||
EnumCollisionStrength.ENTITY_LABEL,
|
||||
""
|
||||
);
|
||||
|
||||
let parentRef = "";
|
||||
if (parentId === null) {
|
||||
parentRef = "00000000000000"; // for true root entities
|
||||
} else if (parentId) {
|
||||
parentRef = await hashWord(
|
||||
parentId,
|
||||
EnumCollisionStrength.PARENT_REF,
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
return parentRef
|
||||
? `${entityPrefix}-${parentRef}-` // for nested entity searches
|
||||
: `${entityPrefix}-`; // global entity type prefix
|
||||
}
|
||||
|
||||
// Function to generate IDs dynamically with `publicSalt`
|
||||
export async function buildIdentifier(
|
||||
appName: string,
|
||||
@ -166,6 +193,33 @@ export async function buildIdentifier(
|
||||
return `${appHash}-${entityPrefix}-${parentRef}-${entityUid}-${IDENTIFIER_BUILDER_VERSION}`;
|
||||
}
|
||||
|
||||
export async function buildLooseIdentifier(
|
||||
entityType: string,
|
||||
parentId?: string | null
|
||||
): Promise<string> {
|
||||
// 4-char hash for entity type
|
||||
const entityPrefix: string = await hashWord(
|
||||
entityType,
|
||||
EnumCollisionStrength.ENTITY_LABEL,
|
||||
""
|
||||
);
|
||||
|
||||
// Generate 8-12 character random uid (depends on uid.rnd() settings)
|
||||
const entityUid = uid.rnd();
|
||||
|
||||
// Optional hashed parent ref
|
||||
let parentRef = '';
|
||||
if (parentId) {
|
||||
parentRef = await hashWord(
|
||||
parentId,
|
||||
EnumCollisionStrength.PARENT_REF,
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
return `${entityPrefix}${parentRef ? `-${parentRef}` : ''}-${entityUid}${IDENTIFIER_BUILDER_VERSION ? `-${IDENTIFIER_BUILDER_VERSION}` : ''}`;
|
||||
}
|
||||
|
||||
export const createSymmetricKeyAndNonce = () => {
|
||||
const messageKey = new Uint8Array(32); // 32 bytes for the symmetric key
|
||||
crypto.getRandomValues(messageKey);
|
||||
|
Loading…
x
Reference in New Issue
Block a user