Merge pull request #538 from storytold/feature/comp/playbar-w-trim

Feature/comp/playbar w trim
This commit is contained in:
wil authored and GitHub committed 2024-03-12 11:19:25 -04:00
commit 2834872fd8
15 files changed
+840 -372

No files matched your search

@@ -1,101 +0,0 @@
import React, {
useRef,
useState,
PointerEvent
} from 'react';
import {
faGripDots
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { QuickTrimData } from './types';
export default function TrimScrubber({
trimStart,
trimDuration,
duration,
width,
boundingWidth,
onChange
}:{
trimStart:number;
trimDuration: number;
duration: number;
boundingWidth: number;
width:number;
onChange:(val:QuickTrimData)=>void
}){
const [{
left,
leftOffset,
pointerStart
}, setState] = useState<{
left: number,
leftOffset:number
pointerStart:number
}>({
left: trimStart/duration*boundingWidth,
leftOffset: trimStart/duration*boundingWidth,
pointerStart: -1
});
const ref = useRef<HTMLDivElement | null>(null);
if(ref.current){
ref.current.style.cursor = pointerStart >= 0
? 'grabbing' : 'grab';
}
function handleScrubStart(e: PointerEvent<HTMLDivElement>){
e.persist();
// console.log(`start: ${pointerStart}` );
setState((curr)=>({
...curr,
pointerStart: e.clientX
}));
}
function handleScrubEnd(e: PointerEvent<HTMLDivElement>){
e.persist();
// console.log(`end` );
setState((curr)=>({
...curr,
pointerStart: -1,
leftOffset: left,
}));
//console.log(`${duration} * ${left} / ${boundingWidth} = ${duration * left / boundingWidth}`)
onChange(({
trimStartSeconds: duration * left / boundingWidth,
trimEndSeconds: duration * left / boundingWidth + trimDuration,
}))
}
function handleScrubMove(e: PointerEvent<HTMLDivElement>){
e.persist();
if(pointerStart >= 0 && pointerStart!==null && ref.current){
// console.log(`${e.clientX} - ${pointerStart} = ${e.clientX-pointerStart}`)
let newLeft = leftOffset + e.clientX - pointerStart;
// console.log(`${newLeft} + ${boundingWidth * widthPercent/100} > ${boundingWidth}`)
if (newLeft + width > boundingWidth) {
newLeft = boundingWidth - width;
}else if(newLeft < 0) {
newLeft = 0;
}
setState((curr)=>({
...curr,
left: newLeft
}));
}
}
return(
<div className="trim-scrubber"
ref={ref}
style={{
width: width + "px",
left: left,
}}
onPointerDown={handleScrubStart}
onPointerUp={handleScrubEnd}
onPointerLeave={handleScrubEnd}
onPointerMove={handleScrubMove}
>
<FontAwesomeIcon icon={faGripDots} />
</div>
);
}
@@ -1,296 +1,198 @@
import React, {
memo,
useCallback,
useLayoutEffect,
useState,
useEffect,
useReducer,
useRef,
} from "react";
import {
faArrowsRepeat,
faPlay,
faPause,
faVolume,
faVolumeSlash,
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button, SelectionBubbles, VideoFakeyou } from "components/common";
import { useLocalize } from "hooks";
import { VideoFakeyou } from "components/common";
import { VideoFakeyouProps } from "../VideoFakeyou/VideoFakeyou";
import { QuickTrimData, TrimStates, PlaybarStates } from "./types";
import { formatSecondsToHHMMSSCS } from "./helpers";
import TrimScrbber from "./TrimScrubber";
import {
reducer,
initialState,
ACTION_TYPES,
PLAYPUASE_STATES,
STATE_STATUSES,
} from "./reducer";
import { QuickTrimData } from "./utilities";
import { ProgressBar } from "./components/ProgressBar";
import { ControlBar } from "./components/ControlBar";
import './styles.scss'
interface VideoQuickTrimProps extends VideoFakeyouProps {
onSelect: (values: QuickTrimData) => void;
trimStartSeconds: number;
trimEndSeconds: number;
}
const trimOptions: { [key: string]: number } = {
"3s": 3,
"5s": 5,
"10s": 10,
"15s": 15,
};
const initialTrimState: TrimStates = {
canNotTrim: true,
trimDuration: 0,
trimReset: new Date(),
trimStart: 0,
trimEnd: 0,
maxDuration: 0,
};
const initialPlaybarState: PlaybarStates = {
playbarWidth: 0,
timeCursorOffset: 0,
};
export default memo(function VideoQuickTrim({
export const VideoQuickTrim = memo(({
onSelect,
trimStartSeconds: propsTrimStartSeconds,
trimEndSeconds : propsTrimEndSeconds,
...rest
}: VideoQuickTrimProps) {
// console.log("VideoQuickTrim Rerender!!");
}: VideoQuickTrimProps) => {
const { t } = useLocalize("VideoPlayerQuickTrim");
const videoRef = useRef<HTMLVideoElement | null>(null);
const playbarRef = useRef<HTMLDivElement | null>(null);
const [{
playbarWidth,
timeCursorOffset
}, setPlaybarState] = useState<PlaybarStates>(initialPlaybarState);
const [playpause, setPlaypause] = useState<'playing'|'paused'|'ended'>('paused');
const [isMuted, setIsMuted] = useState<boolean>(false);
const [isRepeatOn, setIsRepatOn] = useState<boolean>(true);
const [{
canNotTrim,
trimReset,
trimStart,
trimEnd,
trimDuration,
maxDuration
}, setTrimState] = useState<TrimStates>(initialTrimState);
const [compState, dispatchCompState] = useReducer(reducer, initialState);
if(compState.status === STATE_STATUSES.LOAD_ORDER_ERROR){
console.log(`${compState.errorMessage[compState.errorMessage.length -1]}`);
}
const videoRefCallback = useCallback(node => {
//console.log("videoRefCallback");
function setPlaypause(newState: PLAYPUASE_STATES){
dispatchCompState({
type: ACTION_TYPES.SET_PLAYPUASE,
payload: { playpause: newState}
})
}
if (node !== null) {
// DOM node referenced by ref has changed and exists
videoRef.current = node;
node.onloadedmetadata = ()=>{
if (node.duration >= 3){
setTrimState((curr)=>({
...curr,
canNotTrim: false,
trimDuration: 3,
trimEnd: 3,
maxDuration: node.duration
}));
//TODO: this sbould be set in USEEFFECT
//IFF USEEFFECT starts working again
onSelect({
trimStartSeconds: 0,
trimEndSeconds: 3,
})
}
dispatchCompState({
type: ACTION_TYPES.ON_LOADED_METADATA,
payload:{ videoDuration: node.duration,}
});
};
node.ontimeupdate = ()=>{
if(isRepeatOn &&
(node.currentTime >= trimEnd || node.currentTime <= trimStart)
if(compState.trimStartSeconds !== undefined &&
compState.trimEndSeconds !== undefined &&
compState.playbarWidth !== undefined){
// reset current time when on repeat
if(compState.isRepeatOn &&
(node.currentTime >= compState.trimEndSeconds || node.currentTime <= compState.trimStartSeconds)
){
node.currentTime = trimStart;
node.currentTime = compState.trimStartSeconds;
console.log('play looped');
}
dispatchCompState({
type: ACTION_TYPES.MOVE_TIMECURSOR,
payload: {
timeCursorOffset: (node.currentTime / node.duration) * (compState.playbarWidth)
}
});
}else{
console.log('ontimeupdate failed');
}
setPlaybarState((curr)=>({
...curr,
timeCursorOffset: (node.currentTime / node.duration) * (playbarWidth-8)
}));
};
node.onplay = ()=>{ setPlaypause("playing");};
node.onpause = ()=>{setPlaypause("paused");};
node.onended = ()=>{setPlaypause("ended");};
node.onplay = ()=>setPlaypause(PLAYPUASE_STATES.PLAYING);
node.onpause = ()=>setPlaypause(PLAYPUASE_STATES.PAUSED);
node.onended = ()=>setPlaypause(PLAYPUASE_STATES.ENDED);
} // else{} DOM node referenced by ref has been unmounted
}, [playbarWidth, onSelect, isRepeatOn, trimStart, trimEnd]); //END videoRefCallback
}, [
compState.trimStartSeconds,
compState.trimEndSeconds,
compState.playbarWidth,
compState.isRepeatOn
]); //END videoRefCallback\
function handleWindowResize() {
if (playbarRef.current !== null) {
const newWidth = playbarRef.current.getBoundingClientRect().width;
setPlaybarState(curr => ({
playbarWidth: newWidth,
timeCursorOffset:
(curr.timeCursorOffset / (curr.playbarWidth - 8)) * (newWidth - 8),
}));
}
}
const playbarRefCallback = useCallback(node => {
if (node !== null) {
playbarRef.current = node;
setPlaybarState(curr => ({
...curr,
playbarWidth: node.getBoundingClientRect().width,
}));
}
}, []);
useLayoutEffect(() => {
window.addEventListener("resize", handleWindowResize);
return () => {
window.removeEventListener("resize", handleWindowResize);
};
}, []);
const handleChangeTrimDuration = (selected: string) =>{
if(!canNotTrim && maxDuration > 0 && trimOptions[selected] <= maxDuration){
let newTrimStart = trimStart;
let newTrimEnd = trimStart+ trimOptions[selected];
if (newTrimEnd > maxDuration){
newTrimEnd = maxDuration;
newTrimStart = maxDuration - trimOptions[selected];
useEffect(()=>{
if( compState.trimStartSeconds
&& compState.trimEndSeconds
&& (
compState.trimStartSeconds !== propsTrimStartSeconds
|| compState.trimEndSeconds !== propsTrimEndSeconds
)
){
onSelect({
trimStartSeconds: compState.trimStartSeconds,
trimEndSeconds: compState.trimEndSeconds,
});
}
setTrimState((curr)=>({
...curr,
trimReset: new Date(),
trimDuration: trimOptions[selected],
trimStart: newTrimStart,
trimEnd: newTrimEnd,
}))
onSelect({
trimStartSeconds: newTrimStart,
trimEndSeconds: newTrimEnd,
});
if (isRepeatOn
&& videoRef.current
&& videoRef.current.currentTime > newTrimEnd
){
videoRef.current.currentTime = newTrimStart;
}
}
}
}, [onSelect, compState.trimStartSeconds, compState.trimEndSeconds, propsTrimStartSeconds, propsTrimEndSeconds])
const handlePlaypause = ()=>{
if (playpause === 'paused' || playpause === 'ended'){
function videoCanPlay(){
return (compState.playpause === PLAYPUASE_STATES.PAUSED
|| compState.playpause === PLAYPUASE_STATES.ENDED
|| compState.playpause === PLAYPUASE_STATES.READY
);
}
function handlePlaypause(){
if (videoCanPlay()){
videoRef.current?.play();
}else{
}else if (compState.playpause === PLAYPUASE_STATES.PLAYING){
videoRef.current?.pause();
}else {
console.log('Playpause is triggered while it is NOT_READY');
}
};
const trimScrubberWidth = videoRef.current && playbarWidth > 0
? trimDuration > 0 && trimDuration < videoRef.current.duration
? (trimDuration / videoRef.current.duration * playbarWidth)
: playbarWidth
: 0;
return (
<div className="fy-video-quicktrim">
<div className="video-wrapper">
<VideoFakeyou
height={500}
controls={false}
muted={isMuted}
muted={compState.isMuted}
ref={videoRefCallback}
{...rest}
/>
<div className="playpause-overlay" onClick={handlePlaypause}>
{playpause === "paused" && (
<FontAwesomeIcon
className="playpause-icon"
icon={faPlay}
size="8x"
/>
)}
{playpause === "playing" && (
<FontAwesomeIcon
className="playpause-icon"
icon={faPause}
size="8x"
/>
)}
</div>
{videoRef.current && canNotTrim && (
{compState.playpause !== PLAYPUASE_STATES.NOT_READY &&
<div className="playpause-overlay" onClick={handlePlaypause}>
{videoCanPlay() && (
<FontAwesomeIcon
className="playpause-icon"
icon={faPlay}
size="8x"
/>
)}
{compState.playpause === PLAYPUASE_STATES.PLAYING && (
<FontAwesomeIcon
className="playpause-icon"
icon={faPause}
size="8x"
/>
)}
</div>
}
{ compState.canNotTrim === true &&
<div className="warning-too-short">
<div className="background"></div>
<p>Warning: Sorry Your Video is TOO Short</p>
<h1>{t('error.videoTooShort')}</h1>
</div>
)}
</div>
{/* END of Video Wrapper */}
<div className="playbar" ref={playbarRefCallback}>
<div className="playbar-bg" />
{trimScrubberWidth > 0 &&
playbarWidth > 0 &&
maxDuration > 0 &&
<TrimScrbber
key={trimReset.toString()}
boundingWidth={playbarWidth}
width={trimScrubberWidth}
trimStart={trimStart}
trimDuration={trimDuration}
duration={maxDuration}
onChange={(val: QuickTrimData)=>{
//console.log(val);
setTrimState((curr)=>({
...curr,
trimStart: val.trimStartSeconds,
trimEnd: val.trimEndSeconds
}))
onSelect({
trimStartSeconds: val.trimStartSeconds,
trimEndSeconds: val.trimEndSeconds,
});
}}
/>
}
<div className="playcursor" style={{left: timeCursorOffset+"px"}}/>
</div> {/* END of Playbar */}
<div className="d-flex w-100 justify-content-between mt-3 flex-wrap">
<div className="playpause-external d-flex align-items-center flex-wrap mb-2">
<Button
className="button-playpause"
icon={playpause === "playing" ? faPause : faPlay}
variant="secondary"
onClick={handlePlaypause}
/>
<Button
className="button-repeat"
icon={faArrowsRepeat}
variant={isRepeatOn ? "primary":"secondary"}
onClick={()=>setIsRepatOn((curr)=>(!curr))}
/>
<Button
className="button-mute"
icon={isMuted ? faVolumeSlash : faVolume}
variant="secondary"
onClick={() => setIsMuted(curr => !curr)}
/>
<div className="playtime d-flex">
<span >
<p>
{`${formatSecondsToHHMMSSCS(
videoRef.current?.currentTime || 0
)}`}
</p>
</span>
<div>/</div>
<span>
<p>
{`${formatSecondsToHHMMSSCS(videoRef.current?.duration || 0)}`}
</p>
</span>
</div>
</div>
<SelectionBubbles
options={Object.keys(trimOptions)}
onSelect={handleChangeTrimDuration}
selectedStyle="outline"
/>
</div>
</div>{/* END of Video Wrapper */}
<ProgressBar
readyToMount={(compState.status === STATE_STATUSES.VIDEO_METADATA_LOADED)}
timeCursorOffset={compState.timeCursorOffset ||0}
trimStartSeconds={compState.trimStartSeconds ||0}
trimDuration={compState.trimDuration ||0}
playbarWidth={compState.playbarWidth ||0}
scrubberWidth={compState.scrubberWidth ||0}
videoBuffered={videoRef.current?.buffered}
videoDuration={compState.videoDuration ||0}
dispatchCompState={dispatchCompState}
onPlayCursorChanged={(newPos: number)=>{
if(videoRef.current !== null && videoRef.current.currentTime
&& compState.playbarWidth && compState.videoDuration){
videoRef.current.currentTime = newPos / compState.playbarWidth * compState.videoDuration;
}
}}
/>
<ControlBar
readyToMount={(compState.status === STATE_STATUSES.VIDEO_METADATA_LOADED)}
videoCurrentTime={videoRef.current?.currentTime}
videoDuration={videoRef.current?.duration}
isMuted={compState.isMuted}
isRepeatOn={compState.isRepeatOn}
playpause={compState.playpause}
handlePlaypause={handlePlaypause}
dispatchCompState={dispatchCompState}
/>
</div>
);
});
@@ -0,0 +1,102 @@
import React, { memo } from 'react'
import {
faArrowsRepeat,
faPlay,
faPause,
faVolume,
faVolumeSlash,
} from "@fortawesome/pro-solid-svg-icons";
import { Button, SelectionBubbles } from "components/common";
import {
Action,
ACTION_TYPES,
PLAYPUASE_STATES
} from '../reducer';
import { TRIM_OPTIONS, formatSecondsToHHMMSSCS } from "../utilities";
export const ControlBar = memo(({
readyToMount,
videoCurrentTime,
videoDuration,
isRepeatOn,
isMuted,
playpause,
handlePlaypause,
dispatchCompState
}:{
readyToMount: boolean;
videoCurrentTime: number | undefined;
videoDuration: number | undefined;
isMuted: boolean,
isRepeatOn: boolean;
playpause: string;
handlePlaypause: ()=>void;
dispatchCompState: (action: Action) => void;
})=>{
function handleSetTrimDuration(selected: string){
dispatchCompState({
type: ACTION_TYPES.SET_TRIM_DURATION,
payload: {
trimDurationString: selected
}
});
// onSelect({
// trimStartSeconds: newTrimStart,
// trimEndSeconds: newTrimEnd,
// });
};
if(readyToMount){
return(
<div className="d-flex w-100 justify-content-between mt-3 flex-wrap">
<div className="playpause-external d-flex align-items-center flex-wrap mb-2">
<Button
className="button-playpause"
icon={ playpause === PLAYPUASE_STATES.PLAYING
? faPause : faPlay
}
variant="secondary"
onClick={handlePlaypause}
/>
<Button
className="button-repeat"
icon={faArrowsRepeat}
variant={isRepeatOn ? "primary":"secondary"}
onClick={()=>dispatchCompState({type:ACTION_TYPES.TOGGLE_REPEAT})}
/>
<Button
className="button-mute"
icon={isMuted ? faVolumeSlash : faVolume}
variant="secondary"
onClick={()=>dispatchCompState({type:ACTION_TYPES.TOGGLE_MUTE})}
/>
<div className="playtime d-flex">
<span >
<p>
{`${formatSecondsToHHMMSSCS(
videoCurrentTime || 0
)}`}
</p>
</span>
<div>/</div>
<span>
<p>
{`${formatSecondsToHHMMSSCS(videoDuration || 0)}`}
</p>
</span>
</div>
</div>
<SelectionBubbles
options={Object.keys(TRIM_OPTIONS)}
onSelect={handleSetTrimDuration}
selectedStyle="outline"
/>
</div>
);
}else{
console.log('TODO: ControlBar should rendering loading state instead of null');
return null;
}
});
@@ -0,0 +1,24 @@
import React from 'react';
import { withScrubbing, withScrubbingPropsI } from './withScrubbing';
interface PlayCursorPropsI extends withScrubbingPropsI{
onChanged: (posPercent: number)=>void;
}
export const PlayCursor = ({
onChanged,
...rest
}:PlayCursorPropsI)=>{
const PlayCursorWithScrubbing = withScrubbing<withScrubbingPropsI>(()=>{
return(
<div className="playcursor" />
);
});
return(
<PlayCursorWithScrubbing
onScrubChanged={onChanged}
{...rest}
/>
);
};
@@ -0,0 +1,109 @@
import React, {
memo,
useCallback,
useLayoutEffect,
useRef
} from 'react';
import {
Action,
ACTION_TYPES,
} from '../reducer';
import { TrimScrubber } from './TrimScrubber';
import { PlayCursor } from './PlayCursor';
export const ProgressBar = memo(({
readyToMount,
timeCursorOffset,
trimStartSeconds,
trimDuration,
playbarWidth,
scrubberWidth,
videoDuration,
videoBuffered,
onPlayCursorChanged,
dispatchCompState
}:{
readyToMount: boolean;
timeCursorOffset: number;
trimStartSeconds: number;
trimDuration: number;
playbarWidth: number;
scrubberWidth: number;
videoDuration: number;
videoBuffered:TimeRanges | undefined;
onPlayCursorChanged: (newPos: number) => void;
dispatchCompState: (action: Action) => void;
})=>{
const playbarRef = useRef<HTMLDivElement | null>(null);
const playbarRefCallback = useCallback(node => {
if (node !== null) {
playbarRef.current = node;
dispatchCompState({
type: ACTION_TYPES.SET_PLAYBAR_LAYOUT,
payload: {
playbarWidth: node.getBoundingClientRect().width,
}
})
}else{
// console.log(node);
}
}, [dispatchCompState]);
const handleWindowResize = useCallback(()=> {
if (playbarRef.current !== null && readyToMount){
dispatchCompState({
type: ACTION_TYPES.SET_PLAYBAR_LAYOUT,
payload: {
playbarWidth: playbarRef.current.getBoundingClientRect().width,
}
});
}
},[dispatchCompState, readyToMount]);
useLayoutEffect(() => {
window.addEventListener("resize", handleWindowResize);
return () => {
window.removeEventListener("resize", handleWindowResize);
};
}, [handleWindowResize]);
if(readyToMount){
// console.log('progress bar rendering');
return(
<div className="playbar" ref={playbarRefCallback}>
<div className="playbar-bg">
{videoBuffered !== undefined &&
<span className="loaded" style={{width: (videoBuffered.end(0) / videoDuration* 100) + "%"}} />
}
<span className="played" style={{width: timeCursorOffset+"px"}} />
</div>
<TrimScrubber
boundingWidth={playbarWidth}
scrubberWidth={scrubberWidth}
trimStartSeconds={trimStartSeconds}
trimDuration={trimDuration}
videoDuration={videoDuration}
onChange={(newPos: number)=>{
const newTrimStartSeconds = newPos / (playbarWidth) * videoDuration;
dispatchCompState({
type: ACTION_TYPES.MOVE_TRIM,
payload: {
trimStartSeconds: newTrimStartSeconds,
trimEndSeconds: newTrimStartSeconds + trimDuration
}
});
}}
/>
<PlayCursor
scrubPosition={timeCursorOffset}
onChanged={onPlayCursorChanged}
boundingWidth={playbarWidth}
scrubberWidth={8}
/>
{/* END of Playbar */}</div>
);
}else{
console.log('TODO: ProgressBar should rendering loading state instead of null');
return null;
}
});
@@ -0,0 +1,42 @@
import React, {
memo,
} from 'react';
import {
faGripDots
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { withScrubbing, withScrubbingPropsI } from './withScrubbing';
interface TrimScrubberPropsI extends withScrubbingPropsI {
trimStartSeconds:number;
trimDuration: number;
videoDuration: number;
onChange:(newPos:number)=>void
}
export const TrimScrubber = memo(({
trimStartSeconds,
trimDuration,
videoDuration,
onChange,
...rest
}:TrimScrubberPropsI)=>{
const TrimScrubberWithScrubbing = withScrubbing<TrimScrubberPropsI>(() => {
return(
<div className="trim-scrubber">
<FontAwesomeIcon icon={faGripDots} />
</div>
);
});
return (
<TrimScrubberWithScrubbing
styleOverride={{
top: '-1rem',
}}
scrubPosition={trimStartSeconds/videoDuration*(rest.boundingWidth)}
onScrubChanged={onChange}
{...rest}
/>
);
});
@@ -0,0 +1,147 @@
import React,{
memo,
useEffect,
useRef,
useState,
useLayoutEffect,
} from 'react';
export interface withScrubbingPropsI {
boundingWidth: number;
scrubberWidth: number;
scrubPosition?: number; // scrubber location as px
styleOverride?: {[key: string]: string|number };
onScrubChanged?: (newPos: number)=>void;
//return scrubber location as px
}
type withSrcubbingStates = {
currLeftOffset: number,
prevLeftOffset: number;
pointerStartPos: number;
}
export const withScrubbing = <P extends withScrubbingPropsI>(Component: React.ComponentType<P>) => memo(({
boundingWidth,
scrubberWidth,
scrubPosition: propsLeftOffset = 0,
styleOverride = {},
onScrubChanged,
...rest
}: withScrubbingPropsI) => {
console.log(`withSCRUBBING reRENDERING!! `);
const refEl = useRef<HTMLDivElement| null>(null);
const refListener = useRef<number>(Date.now());
const [{
currLeftOffset, pointerStartPos,
prevLeftOffset
}, setStates] = useState<withSrcubbingStates>({
currLeftOffset: propsLeftOffset,
prevLeftOffset: propsLeftOffset,
pointerStartPos: -1 // negative denotes pointer not engaged
});
useLayoutEffect(() => {
console.log(`withSCRUBBING useLAYOUTeffect!! `);
function handleScrubStart (e: MouseEvent) {
if(refEl.current){
if(refEl.current.contains(e.target as Node)){
setStates((curr)=>({
...curr,
pointerStartPos: e.clientX
}));
return true;
}
}
};
function handleScrubEnd (e: MouseEvent){
e.preventDefault();
e.stopPropagation();
setStates((curr)=>({
...curr,
pointerStartPos: -1,
prevLeftOffset: curr.currLeftOffset,
setBySelf: Date.now(),
}));
};
function handleScrubMove (e: MouseEvent){
e.preventDefault();
e.stopPropagation();
setStates((curr)=>{
if(curr.pointerStartPos >= 0 && curr.pointerStartPos!==null){
let newLeftOffset = curr.prevLeftOffset + e.clientX - curr.pointerStartPos;
if (newLeftOffset + scrubberWidth > boundingWidth) {
newLeftOffset = boundingWidth - scrubberWidth;
}else if(newLeftOffset < 0) {
newLeftOffset = 0;
}
if(newLeftOffset !== curr.currLeftOffset){
return{
...curr,
currLeftOffset: newLeftOffset,
}
}
}
return curr;
});
};
if(!(window as any)[`listener-id-${refListener}`]){
(window as any)[`listender-id-${refListener}`] = true;
window.addEventListener("mousedown", handleScrubStart);
window.addEventListener("mouseup", handleScrubEnd);
window.addEventListener("mousemove", handleScrubMove);
return () => {
(window as any)[`listener-id-${refListener}`] = false;
window.removeEventListener("mousedown", handleScrubStart);
window.removeEventListener("mouseup", handleScrubEnd);
window.removeEventListener("mousemove", handleScrubMove);
};
}
}, [scrubberWidth, boundingWidth]);
useEffect(()=>{
console.log(`withSCRUBBING useEFFECT!! `);
if(onScrubChanged && boundingWidth > 0 && prevLeftOffset >= 0 && propsLeftOffset !== prevLeftOffset){
onScrubChanged(prevLeftOffset);
}
},[propsLeftOffset, prevLeftOffset, boundingWidth, onScrubChanged]);
useEffect(()=>{
// this takes a forced reset on leftoffset
setStates((curr)=>{
if(curr.prevLeftOffset === curr.currLeftOffset
&& propsLeftOffset !== curr.prevLeftOffset
&& propsLeftOffset !== curr.prevLeftOffset
){
return {
...curr,
currLeftOffset: propsLeftOffset, // in pixels
prevLeftOffset: propsLeftOffset, //in pixels
}
}else{
return curr
}
});
}, [propsLeftOffset])
return(
<div
className="scrubber-wrapper"
ref={refEl}
style={{
position: 'absolute',
top:0,
width: scrubberWidth + 'px',
left: currLeftOffset + 'px',
cursor: pointerStartPos >=0 ? 'grabbing': 'grab',
...styleOverride
}}
>
<Component
{...rest as P}
/>
</div>
);
});
@@ -1,5 +1,5 @@
import VideoQuickTrim from './VideoQuickTrim'
import { QuickTrimData } from './types';
import {VideoQuickTrim} from './VideoQuickTrim'
import { QuickTrimData } from './utilities';
export type { QuickTrimData };
export default VideoQuickTrim;
@@ -0,0 +1,215 @@
import { MIN_VID_DURATION, TIME_CURSOR_WIDTH, TRIM_OPTIONS } from "./utilities";
export enum STATE_STATUSES {
INIT = "init",
VIDEO_METADATA_LOADED = "video_metadata_loaded",
LOAD_ORDER_ERROR = "load_order_error",
ACTION_TYPE_ERROR = "no_such_action_type"
};
export enum PLAYPUASE_STATES{
NOT_READY = "not_ready",
READY = "ready",
PLAYING = "playing",
PAUSED = "paused",
ENDED = "ended",
}
export type State = {
status: string;
errorMessage: string[];
playpause: string;
canNotTrim: boolean | undefined;
trimReset: number;
trimDuration: number | undefined;
trimStartSeconds: number | undefined;
trimEndSeconds: number | undefined;
videoLoadProgress: number | undefined;
videoDuration: number | undefined;
playbarWidth: number | undefined;
scrubberWidth: number | undefined;
timeCursorOffset: number | undefined;
isMuted: boolean;
isRepeatOn: boolean;
};
export const initialState = {
status: STATE_STATUSES.INIT,
errorMessage : [],
playpause: PLAYPUASE_STATES.NOT_READY,
canNotTrim: undefined,
trimReset: Date.now(),
trimDuration: undefined,
trimStartSeconds: undefined,
trimEndSeconds: undefined,
videoLoadProgress: undefined,
videoDuration: undefined,
playbarWidth: undefined,
scrubberWidth: undefined,
timeCursorOffset: undefined,
isMuted: false,
isRepeatOn: true,
}
export enum ACTION_TYPES {
RESET = "reset",
TOGGLE_REPEAT = "toggle_repeat",
TOGGLE_MUTE = "toggle_mute",
ON_LOADED_METADATA = "on_loaded_metadata",
MOVE_TIMECURSOR = "move_timecursor",
SET_PLAYPUASE = "set_playpause",
SET_PLAYBAR_LAYOUT = "set_playbar_layout",
SET_TRIM_DURATION = "set_trim_duration",
SET_VIDEO_LOAD_PROGRESS = "set_video_load_progress",
MOVE_TRIM = "move_trim"
}
export type Action =
| {type: ACTION_TYPES.RESET}
| {type: ACTION_TYPES.TOGGLE_REPEAT}
| {type: ACTION_TYPES.TOGGLE_MUTE}
| {type: ACTION_TYPES.ON_LOADED_METADATA, payload: {videoDuration: number}}
| {type: ACTION_TYPES.MOVE_TIMECURSOR, payload:{ timeCursorOffset: number}}
| {type: ACTION_TYPES.SET_PLAYPUASE, payload:{ playpause: string}}
| {type: ACTION_TYPES.SET_PLAYBAR_LAYOUT, payload:{ playbarWidth: number}}
| {type: ACTION_TYPES.SET_TRIM_DURATION, payload:{ trimDurationString: string}}
| {type: ACTION_TYPES.SET_VIDEO_LOAD_PROGRESS, payload: {
videoLoadProgress: number
}}
| {type: ACTION_TYPES.MOVE_TRIM, payload:{
trimStartSeconds: number,
trimEndSeconds: number,
}}
;
export function reducer(state: State, action: Action): State {
console.log("VideoQuickTrim State Dispatch");
console.log(action);
switch(action.type){
case ACTION_TYPES.SET_PLAYPUASE:{
return {...state, ...action.payload};
}
case ACTION_TYPES.TOGGLE_REPEAT:{
return {...state, isRepeatOn: !state.isRepeatOn}
}
case ACTION_TYPES.TOGGLE_MUTE:{
return {...state, isMuted: !state.isMuted}
}
case ACTION_TYPES.ON_LOADED_METADATA:{
if(action.payload.videoDuration >= MIN_VID_DURATION){
return{
...state,
status: STATE_STATUSES.VIDEO_METADATA_LOADED,
videoDuration: action.payload.videoDuration,
canNotTrim: false,
trimDuration: 3,
trimStartSeconds: 0,
trimEndSeconds: 3,
}
}else{
return {
...state,
videoDuration: action.payload.videoDuration,
canNotTrim: true
}
}
}
case ACTION_TYPES.SET_VIDEO_LOAD_PROGRESS:{
return{
...state,
videoLoadProgress: action.payload.videoLoadProgress
};
}
case ACTION_TYPES.SET_PLAYBAR_LAYOUT:{
const newWidth = action.payload.playbarWidth;
const prevOffset = state.timeCursorOffset || 0;
if(state.trimDuration && state.videoDuration){
return {
...state,
playpause: PLAYPUASE_STATES.READY,
playbarWidth: newWidth,
scrubberWidth: newWidth * (state.trimDuration / state.videoDuration),
timeCursorOffset: (newWidth-TIME_CURSOR_WIDTH)*(prevOffset/ state.videoDuration)
};
}else{
return{
...state,
status: STATE_STATUSES.LOAD_ORDER_ERROR,
errorMessage: [...state.errorMessage, 'Setting playbar layout before video and trim are loaded']
}
}
}
case ACTION_TYPES.SET_TRIM_DURATION:{
const selected = action.payload.trimDurationString;
if( state.canNotTrim !== undefined
&& state.canNotTrim === false
&& state.playbarWidth !==undefined
&& state.videoDuration !== undefined
&& TRIM_OPTIONS[selected] <= state.videoDuration
&& state.trimStartSeconds !== undefined
){
let newTrimStart = state.trimStartSeconds;
let newTrimEnd = state.trimStartSeconds + TRIM_OPTIONS[selected];
if (newTrimEnd > state.videoDuration){
newTrimEnd = state.videoDuration;
if (state.videoDuration - TRIM_OPTIONS[selected] >=0) {
newTrimStart = state.videoDuration - TRIM_OPTIONS[selected];
}else{
newTrimStart = 0;
}
}
return{
...state,
trimDuration: newTrimEnd-newTrimStart,
trimStartSeconds: newTrimStart,
trimEndSeconds: newTrimEnd,
scrubberWidth: (newTrimEnd-newTrimStart)/state.videoDuration * state.playbarWidth,
}
}else{
return {
...state,
status: STATE_STATUSES.LOAD_ORDER_ERROR,
errorMessage: [...state.errorMessage, 'Setting trim duration before video and trim are loaded']
}
}
}
case ACTION_TYPES.MOVE_TRIM:{
return{
...state,
trimStartSeconds: action.payload.trimStartSeconds,
trimEndSeconds: action.payload.trimEndSeconds,
}
}
case ACTION_TYPES.MOVE_TIMECURSOR:{
return {
...state,
timeCursorOffset: action.payload.timeCursorOffset
}
}
case ACTION_TYPES.RESET:{
return initialState
}
default:{
return {
...state,
status: STATE_STATUSES.ACTION_TYPE_ERROR,
errorMessage: [...state.errorMessage, "Reducer Action switch's default case is reached"]
}
}
}
};
/*
const trimScrubberWidth = videoRef.current && playbarWidth > 0
? trimDuration > 0 && trimDuration < videoRef.current.duration
? (trimDuration / videoRef.current.duration * playbarWidth)
: playbarWidth
: 0;
*/
@@ -60,11 +60,11 @@ $playbar-height: 2rem;
background-color: #e66462;
opacity: 70%;
}
p {
h1 {
position: absolute;
margin-top: 230px;
width: 100%;
top: 0;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
.playbar {
@@ -80,17 +80,34 @@ $playbar-height: 2rem;
left: 0;
background-color: $panel-inner-border;
border-radius: 0 0 0.5rem 0.5rem;
overflow: hidden;
.played, .loaded{
position: absolute;
height: 100%;
top: 0;
left: 0;
opacity: 0.5;
}
.played{
background-color: $primary-dark;
}
.loaded{
background-color: $gray-600;
}
}
.scrubber-wrapper{
box-shadow: 1px 1px 4px 0px rgba(0,0,0,1);
}
.trim-scrubber{
position: absolute;
top: -1rem;
height: 3rem;
background-color: #e66462;
opacity: 0.8;
outline: 0.125rem $white solid;
border-top: 1rem $white solid;
border-radius: 0.25rem 0.25rem 0.125rem 0.125rem;
cursor: grab;
display: flex;
justify-content: center;
@@ -103,9 +120,12 @@ $playbar-height: 2rem;
.playcursor {
position: absolute;
top: 0;
width: 0.5rem;
width: 0.5rem; //=8px
transform: translate(-3px, 0px);
height: $playbar-height;
background-color: $white;
border-radius: 1px;
box-shadow: 1px 1px 4px 0px rgba(0,0,0,0.2);
}
.playpause-external {
@@ -1,20 +0,0 @@
//For integration into other components
export type QuickTrimData = {
trimStartSeconds: number;
trimEndSeconds: number;
}
// INTERNALS
export type TrimStates = {
canNotTrim: boolean;
trimReset: Date;
trimDuration: number;
trimStart: number;
trimEnd: number;
maxDuration: number;
}
export type PlaybarStates = {
playbarWidth: number;
timeCursorOffset: number;
}
@@ -1,3 +1,12 @@
//For integration into other components
export const TIME_CURSOR_WIDTH = 8;
export const MIN_VID_DURATION = 3;
export type QuickTrimData = {
trimStartSeconds: number;
trimEndSeconds: number;
}
export function roundTo2Dec(floaty:number){
return Math.round(floaty*100)/100;
}
@@ -11,4 +20,11 @@ export function formatSecondsToHHMMSSCS(seconds:number){
return isoString.substring(11, 19) + "." + isoString.substring(20, 22)
else
return isoString.substring(14, 19) + "." + isoString.substring(20, 22);
}
}
export const TRIM_OPTIONS:{[key: string]: number } = {
"3s": 3,
"5s": 5,
"10s":10,
"15s":15
};
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import VideoQuickTrim from 'components/common/VideoQuickTrim';
import { NavLink } from 'react-router-dom';
@@ -7,9 +7,13 @@ import {Container, Panel} from 'components/common';
export default function ComponentSandbox(
{parentPath}:{parentPath:string}
){
const [state1, setState1] = useState<number|string|boolean|undefined>(undefined);
const [state2, setState2] = useState<number|string|boolean|undefined>(undefined);
const handleCallback = (val:any)=>{
console.log('onChange is triggered')
console.log(val);
setState1(val.trimStartSeconds);
setState2(val.trimEndSeconds);
}
return (<Container>
<NavLink to={`${parentPath}`}>← Back</NavLink>
@@ -18,18 +22,22 @@ export default function ComponentSandbox(
<div className="m-4">
<VideoQuickTrim
trimStartSeconds={Number(state1 ||0)}
trimEndSeconds={Number(state2 || 0)}
onSelect={handleCallback}
mediaToken='m_wdptbe5rfzpb1gzhva78gpw8qrkgs9'
/>
</div>
<br/>
<br/>
<p>{`Start Seconds: ${state1}`}</p>
<p>{`Start Seconds: ${state2}`}</p>
{/* <br/>
<div className="m-4">
<VideoQuickTrim
onSelect={handleCallback}
mediaToken='m_h21nykes0j3ph23y5z7w9axtjjn2rr'
/>
</div>
</div> */}
</Panel>
</Container>);
}
@@ -126,6 +126,8 @@ export default function PageVSTApp() {
<div className="col-12 col-md-6">
{job.isSuccessful && job.maybe_result ? (
<VideoQuickTrim
trimStartSeconds={0}
trimEndSeconds={0}
mediaToken={job.maybe_result.entity_token}
onSelect={(val: QuickTrimData) =>
handleOnChange({
@@ -70,6 +70,8 @@ export default function PageVSTApp({
<div className="row g-5 mb-4">
<div className="col-12 col-md-6">
<VideoQuickTrim
trimStartSeconds={vstValues.trimStart}
trimEndSeconds={vstValues.trimEnd}
mediaToken={vstValues.fileToken}
onSelect={(val: QuickTrimData) =>
handleOnChange({