mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-06 02:00:46 +00:00
Merge branch 'feature/mvp' of https://github.com/storytold/storyteller-frontend into feature/mvp
This commit is contained in:
commit
6de4c4e26e
40 files changed
+1213
-983
No files matched your search
@@ -3,11 +3,13 @@ import { UserDetailsLight } from "../_common/UserDetailsLight";
|
||||
import { MediaFileType } from "../_common/enums/MediaFileType";
|
||||
import { WeightCategory } from "../_common/enums/WeightCategory";
|
||||
import { WeightType } from "../_common/enums/WeightType";
|
||||
import { MediaFileClass } from "../enums/MediaFileClass";
|
||||
import { MediaFileSubtype } from "../enums/MediaFileSubtype";
|
||||
|
||||
export interface MediaFile {
|
||||
token: string;
|
||||
media_type: MediaFileType;
|
||||
media_class: MediaFileClass | null;
|
||||
maybe_media_subtype: MediaFileSubtype | null;
|
||||
public_bucket_path: string;
|
||||
maybe_batch_token: string;
|
||||
|
||||
@@ -3,12 +3,14 @@ import { UserDetailsLight } from "../_common/UserDetailsLight";
|
||||
import { MediaFileType } from "../_common/enums/MediaFileType";
|
||||
import { WeightCategory } from "../_common/enums/WeightCategory";
|
||||
import { WeightType } from "../_common/enums/WeightType";
|
||||
import { MediaFileClass } from "../enums/MediaFileClass";
|
||||
import { MediaFileSubtype } from "../enums/MediaFileSubtype";
|
||||
// import { Weight } from "../weights/GetWeight";
|
||||
|
||||
export interface MediaFile {
|
||||
token: string;
|
||||
media_type: MediaFileType;
|
||||
media_class: MediaFileClass | null;
|
||||
maybe_media_subtype: MediaFileSubtype | null;
|
||||
public_bucket_path: string;
|
||||
maybe_creator_user: UserDetailsLight | null;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { WeightCategory } from "@storyteller/components/src/api/_common/enums/We
|
||||
import { WeightType } from "@storyteller/components/src/api/_common/enums/WeightType";
|
||||
import { MediaFileSubtype } from "@storyteller/components/src/api/enums/MediaFileSubtype";
|
||||
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMedia";
|
||||
import { MediaFileClass } from "@storyteller/components/src/api/enums/MediaFileClass";
|
||||
|
||||
// These tests are important - constructing Storyteller Engine URLs is complicated,
|
||||
// and we want to make sure we test the various cases that occur in production.
|
||||
@@ -55,6 +56,7 @@ describe('media files', () => {
|
||||
mediaFile = {
|
||||
token: "MEDIA_FILE_TOKEN",
|
||||
media_type: MediaFileType.GLB,
|
||||
media_class: MediaFileClass.Unknown,
|
||||
maybe_media_subtype: null,
|
||||
maybe_prompt_token: null,
|
||||
public_bucket_path: "path/to/file",
|
||||
@@ -95,8 +97,8 @@ describe('media files', () => {
|
||||
expect(url).toEqual("https://engine.fakeyou.com/?mode=studio&scene=remote://MEDIA_FILE_TOKEN.scn.ron");
|
||||
});
|
||||
|
||||
test('from media subtype', () => {
|
||||
mediaFile.media_type = MediaFileType.Audio; // NB: Not the real time; forcing test to act on subtype.
|
||||
test('from media subtype and media type (.scn.ron)', () => {
|
||||
mediaFile.media_type = MediaFileType.SceneRon;
|
||||
mediaFile.maybe_media_subtype = MediaFileSubtype.StorytellerScene;
|
||||
|
||||
const url = GetEngineUrl({mode: EngineMode.Studio, asset: mediaFile });
|
||||
@@ -107,6 +109,7 @@ describe('media files', () => {
|
||||
// Production examples:
|
||||
// [without subtype] https://feature-mvp--fakeyou.netlify.app/media/m_tcj2zzncmvmn32f0yavmys5jdrc8cc (Majora's Mask GLB)
|
||||
// [scene_import subtype] https://feature-mvp--fakeyou.netlify.app/media/m_a504ma0n7vv3y80bw7bvgx7q2cecmb (Goron GLB)
|
||||
// [scene media_class] https://feature-mvp--fakeyou.netlify.app/media/m_2yw1ytwec9wj8y74k3kc26grn4q341 (Joel's island GLB)
|
||||
describe('generic scene file (not storyteller studio scene)', () => {
|
||||
test('glb without subtype', () => {
|
||||
mediaFile.media_type = MediaFileType.GLB;
|
||||
@@ -125,6 +128,15 @@ describe('media files', () => {
|
||||
const url = GetEngineUrl({mode: EngineMode.Viewer, asset: mediaFile });
|
||||
expect(url).toEqual("https://engine.fakeyou.com/?mode=viewer&sceneImport=https://storage.googleapis.com/dev-vocodes-public/path/to/file.gltf");
|
||||
});
|
||||
|
||||
test('glb with scene media_class and storyteller_scene subtype', () => {
|
||||
mediaFile.media_type = MediaFileType.GLB;
|
||||
mediaFile.media_class = MediaFileClass.Scene;
|
||||
mediaFile.maybe_media_subtype = MediaFileSubtype.StorytellerScene;
|
||||
|
||||
const url = GetEngineUrl({mode: EngineMode.Studio, asset: mediaFile });
|
||||
expect(url).toEqual("https://engine.fakeyou.com/?mode=studio&sceneImport=remote://MEDIA_FILE_TOKEN.glb");
|
||||
});
|
||||
});
|
||||
|
||||
// Production examples:
|
||||
|
||||
@@ -4,6 +4,7 @@ import { EngineMode } from "./EngineMode";
|
||||
import { MediaFileSubtype } from "@storyteller/components/src/api/enums/MediaFileSubtype";
|
||||
import { MediaFileType } from "@storyteller/components/src/api/_common/enums/MediaFileType";
|
||||
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
|
||||
import { MediaFileClass } from "@storyteller/components/src/api/enums/MediaFileClass";
|
||||
|
||||
// Storyteller Engine parameters
|
||||
// These are documented here:
|
||||
@@ -52,6 +53,16 @@ export function GetEngineUrl(args: GetEngineUrlArgs) : string {
|
||||
const bucketAssetUrl = new BucketConfig().getGcsUrl(args.asset.public_bucket_path);
|
||||
|
||||
if (
|
||||
args.asset.media_type === MediaFileType.GLB &&
|
||||
args.asset.media_class === MediaFileClass.Scene &&
|
||||
args.asset.maybe_media_subtype === MediaFileSubtype.StorytellerScene
|
||||
) {
|
||||
// GLB Scenes
|
||||
// NB: Not sure if `maybe_media_subtype` is required to be "storyteller_scene".
|
||||
// We'll require it in the file for now, but may loosen this restriction.
|
||||
const sceneUrlRef = `remote://${args.asset.token}.glb`;
|
||||
engineUrl += `&sceneImport=${sceneUrlRef}`;
|
||||
} else if (
|
||||
args.asset.maybe_media_subtype === MediaFileSubtype.StorytellerScene ||
|
||||
args.asset.media_type === MediaFileType.SceneRon
|
||||
) {
|
||||
|
||||
@@ -6,7 +6,6 @@ import React, {
|
||||
|
||||
import { VideoFakeyou } from "components/common";
|
||||
import { VideoFakeyouProps } from "../VideoFakeyou/VideoFakeyou";
|
||||
// import { QuickTrimData } from "./utilities";
|
||||
|
||||
import {
|
||||
reducer,
|
||||
@@ -27,21 +26,16 @@ import './styles.scss';
|
||||
interface VideoPlayerProps extends VideoFakeyouProps {
|
||||
debug?: boolean
|
||||
height?: number
|
||||
// onSelectTrim: (values: QuickTrimData) => void;
|
||||
// trimStartSeconds: number;
|
||||
// trimEndSeconds: number;
|
||||
}
|
||||
|
||||
export const VideoPlayer = ({
|
||||
debug: propsDebug = false,
|
||||
height=500,
|
||||
// onSelectTrim,
|
||||
// trimStartSeconds: propsTrimStartSeconds,
|
||||
// trimEndSeconds : propsTrimEndSeconds,
|
||||
...rest
|
||||
}: VideoPlayerProps) => {
|
||||
const debug = true || propsDebug;
|
||||
if(debug) console.log(`reRENDER VideoQuickTrim @${String(Date.now()).slice(-5)}`)
|
||||
const debug = false; // || propsDebug;
|
||||
if(debug) console.log(`reRENDER VideoPlayer @${String(Date.now()).slice(-5)}`)
|
||||
|
||||
const [compState, dispatchCompState] = useReducer(reducer, initialState);
|
||||
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
import React, {
|
||||
useRef,
|
||||
useCallback,
|
||||
useReducer,
|
||||
} from "react";
|
||||
|
||||
import { VideoFakeyou } from "components/common";
|
||||
import { VideoFakeyouProps } from "../VideoFakeyou/VideoFakeyou";
|
||||
|
||||
import {
|
||||
reducer,
|
||||
initialState,
|
||||
ACTION_TYPES
|
||||
} from "./reducer";
|
||||
import {
|
||||
VideoElementContext,
|
||||
TrimContext
|
||||
} from "./contexts";
|
||||
|
||||
import { VideoTakeover } from "./components/VideoTakeover";
|
||||
import { ScrubberBar } from "./components/ScrubberBar";
|
||||
import { ControlBar } from "./components/ControlBar";
|
||||
|
||||
import './styles.scss';
|
||||
|
||||
|
||||
interface Props extends VideoFakeyouProps {
|
||||
debug?: boolean;
|
||||
height?: number;
|
||||
trimStartMs?: number;
|
||||
trimEndMs?: number;
|
||||
onSelectTrim: ({
|
||||
trimStartMs,trimEndMs,
|
||||
}:{
|
||||
trimStartMs: number,
|
||||
trimEndMs:number,
|
||||
})=>void
|
||||
}
|
||||
|
||||
export const VideoPlayerQuickTrim = ({
|
||||
debug: propsDebug = false,
|
||||
height=500,
|
||||
trimStartMs,
|
||||
trimEndMs,
|
||||
onSelectTrim,
|
||||
...rest
|
||||
}: Props) => {
|
||||
const debug = true || propsDebug;
|
||||
if(debug) console.log(`reRENDER VP_QuickTrim start@${trimStartMs} end@${trimEndMs}`)
|
||||
|
||||
const [compState, dispatchCompState] = useReducer(reducer, initialState);
|
||||
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const videoRefCallback = useCallback(node => {
|
||||
function handleLoadedmetadata (){
|
||||
dispatchCompState({
|
||||
type: ACTION_TYPES.ON_LOADED_METADATA,
|
||||
payload:{ videoDuration: node.duration,}
|
||||
});
|
||||
};
|
||||
if (node !== null) {
|
||||
// DOM node referenced by ref has changed and exists
|
||||
videoRef.current = node;
|
||||
node.addEventListener("loadedmetadata", handleLoadedmetadata);
|
||||
} // else{} DOM node referenced by ref has been unmounted
|
||||
|
||||
return()=>{
|
||||
node?.removeEventListener("loadedmetadata",handleLoadedmetadata);
|
||||
}
|
||||
|
||||
}, [
|
||||
// No Dependency !
|
||||
]); //END videoRefCallback\
|
||||
|
||||
return(
|
||||
<VideoElementContext.Provider value={videoRef.current}>
|
||||
<TrimContext.Provider value={{
|
||||
trimStartMs: trimStartMs || 0,
|
||||
trimEndMs: trimEndMs || 3000,
|
||||
trimDurationMs: (trimEndMs || 3000) - (trimStartMs || 0),
|
||||
onChange: onSelectTrim
|
||||
}}>
|
||||
<div className="fy-video-player">
|
||||
<div className="video-wrapper">
|
||||
<VideoFakeyou
|
||||
debug={false}
|
||||
height={height}
|
||||
controls={false}
|
||||
// muted={compState.isMuted}
|
||||
ref={videoRefCallback}
|
||||
{...rest}
|
||||
/>
|
||||
|
||||
{/* components that takes over for spinner and warning message */}
|
||||
<VideoTakeover height={height} status={compState.status}/>
|
||||
</div>{/* END of Video Wrapper */}
|
||||
<ScrubberBar status={compState.status}/>
|
||||
<ControlBar debug={debug} status={compState.status}/>
|
||||
</div>
|
||||
</TrimContext.Provider>
|
||||
</VideoElementContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import React, {
|
||||
useContext,
|
||||
useEffect,
|
||||
useState
|
||||
} from "react";
|
||||
|
||||
import {
|
||||
faVolumeSlash,
|
||||
faVolume
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
import { Button } from "components/common";
|
||||
import { VideoElementContext } from '../../contexts';
|
||||
|
||||
export function ButtonMute (){
|
||||
const vidEl = useContext(VideoElementContext);
|
||||
const [isMuted, setIsMuted] = useState<boolean>(vidEl?.muted || false);
|
||||
|
||||
const toggleMute = ()=>{
|
||||
setIsMuted((curr)=>(!curr));
|
||||
}
|
||||
|
||||
useEffect(()=>{
|
||||
if(vidEl !==null){
|
||||
if (isMuted) vidEl.muted = true;
|
||||
else vidEl.muted = false;
|
||||
}
|
||||
},[isMuted, vidEl]);
|
||||
|
||||
return(
|
||||
<Button
|
||||
className="button-mute"
|
||||
icon={ isMuted ? faVolumeSlash : faVolume}
|
||||
variant="secondary"
|
||||
onClick={toggleMute}
|
||||
/>
|
||||
)
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import React, {
|
||||
useContext,
|
||||
useEffect,
|
||||
useState
|
||||
} from "react";
|
||||
|
||||
import {
|
||||
faPlay,
|
||||
faPause,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
import { Button } from "components/common";
|
||||
import {VideoElementContext} from '../../contexts';
|
||||
|
||||
export function ButtonPlaypause (){
|
||||
const vidEl = useContext(VideoElementContext);
|
||||
const [playpause, setPlaypause] = useState<"playing"|"paused">("paused");
|
||||
const togglePlaypause = ()=>{
|
||||
if (playpause === "playing") vidEl?.pause();
|
||||
else vidEl?.play();
|
||||
}
|
||||
|
||||
useEffect(()=>{
|
||||
const setPlaying = ()=>setPlaypause("playing");
|
||||
const setPaused = ()=>setPlaypause("paused");
|
||||
if(vidEl!==null){
|
||||
vidEl.addEventListener("play", setPlaying );
|
||||
vidEl.addEventListener("pause", setPaused);
|
||||
}
|
||||
return()=>{
|
||||
if(vidEl!==null){
|
||||
vidEl.removeEventListener("play", setPlaying);
|
||||
vidEl.removeEventListener("pause", setPaused);
|
||||
}
|
||||
};
|
||||
},[vidEl]);
|
||||
|
||||
return(
|
||||
<Button
|
||||
className="button-playpause"
|
||||
icon={ playpause === "playing" ? faPause : faPlay }
|
||||
variant="secondary"
|
||||
onClick={togglePlaypause}
|
||||
/>
|
||||
)
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import React, {
|
||||
useContext,
|
||||
useEffect,
|
||||
useState
|
||||
} from "react";
|
||||
|
||||
import {
|
||||
faArrowsRepeat
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
import { Button } from "components/common";
|
||||
import { ONE_MS } from "../../utilities";
|
||||
import {
|
||||
VideoElementContext,
|
||||
TrimContext
|
||||
} from '../../contexts';
|
||||
|
||||
|
||||
export function ButtonRepeat (){
|
||||
const vidEl = useContext(VideoElementContext);
|
||||
const trimVals = useContext(TrimContext);
|
||||
const [isRepeatOn, setIsRepeat] = useState<boolean>(true);
|
||||
|
||||
const toggleRepeat = ()=>{
|
||||
setIsRepeat((curr)=>(!curr));
|
||||
}
|
||||
|
||||
useEffect(()=>{
|
||||
function handleRepeat(){
|
||||
if(isRepeatOn && vidEl !==null && trimVals!==null){
|
||||
const currentMs = Math.round(vidEl.currentTime * 1000)
|
||||
if(currentMs > trimVals.trimEndMs
|
||||
|| currentMs < trimVals.trimStartMs
|
||||
){
|
||||
vidEl.currentTime = trimVals.trimStartMs/1000 + ONE_MS
|
||||
}
|
||||
}
|
||||
}
|
||||
vidEl?.addEventListener("timeupdate", handleRepeat);
|
||||
return ()=>{
|
||||
vidEl?.removeEventListener("timeupdate", handleRepeat);
|
||||
};
|
||||
},[isRepeatOn, vidEl, trimVals]);
|
||||
|
||||
return(
|
||||
<Button
|
||||
className="button-repeat"
|
||||
icon={ faArrowsRepeat }
|
||||
variant={ isRepeatOn ? "primary":"secondary"}
|
||||
onClick={toggleRepeat}
|
||||
/>
|
||||
)
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
|
||||
import { STATE_STATUSES } from "../../reducer";
|
||||
import { ButtonPlaypause } from './ButtonPlaypause';
|
||||
import { ButtonRepeat } from './ButtonRepeat';
|
||||
import { ButtonMute } from './ButtonMute';
|
||||
import { LabelTimeDuration } from "./LabelTimeDuration"
|
||||
import { SelectTrim } from './SelectTrims';
|
||||
import { LoadingDots } from '../LoadingDots';
|
||||
export const ControlBar = ({
|
||||
debug: propsDebug = false,
|
||||
status
|
||||
}:{
|
||||
debug?:boolean;
|
||||
status: string;
|
||||
})=>{
|
||||
const debug = false || propsDebug;
|
||||
if (debug) console.log("reRENDERING ------ ControlBar");
|
||||
|
||||
|
||||
if (status === STATE_STATUSES.VIDEO_METADATA_LOADED){
|
||||
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">
|
||||
<ButtonPlaypause />
|
||||
<ButtonRepeat />
|
||||
<ButtonMute />
|
||||
<LabelTimeDuration />
|
||||
</div>
|
||||
<SelectTrim />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return(
|
||||
<div className="d-flex w-100 justify-content-center mt-3 flex-wrap">
|
||||
<LoadingDots />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import React, {
|
||||
useContext,
|
||||
useEffect,
|
||||
useState
|
||||
} from "react";
|
||||
|
||||
import { formatSecondsToHHMMSSCS } from "../../utilities";
|
||||
import { VideoElementContext } from '../../contexts';
|
||||
|
||||
export function LabelTimeDuration(){
|
||||
const vidEl = useContext(VideoElementContext);
|
||||
const [currentTime, setCurrentTime] = useState<number>(0);
|
||||
const [duration, setDuration] = useState<number>(vidEl?.duration || 0);
|
||||
//vidEl context is guarunteed to exist via the load order
|
||||
|
||||
useEffect(()=>{
|
||||
const handleTimeStamp = ()=>setCurrentTime(vidEl?.currentTime ||0);
|
||||
const handleDuration = ()=>setDuration(vidEl?.duration ||0);
|
||||
vidEl?.addEventListener("timeupdate", handleTimeStamp);
|
||||
vidEl?.addEventListener("durationchange", handleDuration);
|
||||
return()=>{
|
||||
vidEl?.removeEventListener("timeupdate",handleTimeStamp);
|
||||
vidEl?.addEventListener("durationchange", handleDuration);
|
||||
}
|
||||
},[vidEl]);
|
||||
|
||||
return(
|
||||
<div className="playtime d-flex">
|
||||
<span>
|
||||
<p>
|
||||
{`${formatSecondsToHHMMSSCS(currentTime)}`}
|
||||
</p>
|
||||
</span>
|
||||
<div>/</div>
|
||||
<span>
|
||||
<p>
|
||||
{`${formatSecondsToHHMMSSCS(duration)}`}
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
} from 'react';
|
||||
|
||||
import { TrimContext } from '../../contexts';
|
||||
import { TRIM_OPTIONS } from '../../utilities';
|
||||
import { SelectionBubbles } from 'components/common';
|
||||
|
||||
export const SelectTrim = ()=>{
|
||||
const trimValues = useContext(TrimContext);
|
||||
|
||||
const handleSetTrimDuration = useCallback((selected: string)=>{
|
||||
// console.log(`trimDurationString: ${selected}`);
|
||||
if (trimValues!==null) {
|
||||
trimValues.onChange({
|
||||
trimStartMs: trimValues.trimStartMs,
|
||||
trimEndMs: trimValues.trimStartMs + TRIM_OPTIONS[selected],
|
||||
// trimDurationMs: TRIM_OPTIONS[selected],
|
||||
});
|
||||
}
|
||||
}, [trimValues]);
|
||||
|
||||
return(
|
||||
<SelectionBubbles
|
||||
options={Object.keys(TRIM_OPTIONS)}
|
||||
onSelect={handleSetTrimDuration}
|
||||
selectedStyle="outline"
|
||||
/>
|
||||
);
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { ControlBar } from "./ControlBar";
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import React from 'react';
|
||||
import './styles.scss';
|
||||
|
||||
export const LoadingDots = () => <div className="dot-flashing"></div>;
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
.dot-flashing {
|
||||
position: relative;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
background-color: #9880ff;
|
||||
color: #9880ff;
|
||||
animation: dot-flashing 1s infinite linear alternate;
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
.dot-flashing::before, .dot-flashing::after {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
.dot-flashing::before {
|
||||
left: -15px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
background-color: #9880ff;
|
||||
color: #9880ff;
|
||||
animation: dot-flashing 1s infinite alternate;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
.dot-flashing::after {
|
||||
left: 15px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
background-color: #9880ff;
|
||||
color: #9880ff;
|
||||
animation: dot-flashing 1s infinite alternate;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
@keyframes dot-flashing {
|
||||
0% {
|
||||
background-color: #9880ff;
|
||||
}
|
||||
50%, 100% {
|
||||
background-color: rgba(152, 128, 255, 0.2);
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import React, {
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { VideoElementContext } from '../../contexts';
|
||||
|
||||
export const ProgressBar = ()=>{
|
||||
// console.log("reRENDERING -------- ScrubberBar");
|
||||
|
||||
const vidEl = useContext(VideoElementContext);
|
||||
const [buffered, setBuffered] = useState<number>(0);
|
||||
const [portionPlayed, setPortionPlayed] = useState<number>(0);
|
||||
|
||||
useEffect(()=>{
|
||||
function handleBuffer(){
|
||||
if(vidEl && vidEl.buffered.length > 0){
|
||||
const bufferEnd = vidEl.buffered.end(vidEl.buffered.length-1);
|
||||
const allLoaded = (bufferEnd === vidEl.duration);
|
||||
// console.log(`end:${bufferEnd} === ${vidEl.duration} = ${allLoaded}`);
|
||||
if(allLoaded) vidEl.removeEventListener("timeupdate", handleBuffer);
|
||||
setBuffered((curr)=>{
|
||||
if(curr!==bufferEnd) return bufferEnd;
|
||||
else return curr
|
||||
});
|
||||
}
|
||||
}
|
||||
function handlePortionPlayed(){
|
||||
if(vidEl){
|
||||
setPortionPlayed(vidEl.currentTime / vidEl.duration * 100);
|
||||
}
|
||||
}
|
||||
|
||||
vidEl?.addEventListener("timeupdate", handleBuffer);
|
||||
vidEl?.addEventListener("timeupdate", handlePortionPlayed);
|
||||
setTimeout(handleBuffer, 500); // delaying for first load;
|
||||
return ()=>{
|
||||
vidEl?.removeEventListener("timeupdate", handleBuffer);
|
||||
vidEl?.removeEventListener("timeupdate", handlePortionPlayed);
|
||||
};
|
||||
},[vidEl]);
|
||||
|
||||
if(vidEl){
|
||||
return(
|
||||
<div className="progress-bar player-border-bottom">
|
||||
<span className="loaded" style={{width: (buffered / vidEl.duration* 100) + "%"}} />
|
||||
<span className="played" style={{width: portionPlayed + "%"}} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else return null;
|
||||
}
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
|
||||
import { ProgressBar } from "./ProgressBar";
|
||||
import { TimeCursor } from './TimeCursor';
|
||||
import { TrimScrubber } from "./TrimScrubber";
|
||||
import { LoadingDots } from "../LoadingDots";
|
||||
|
||||
import { STATE_STATUSES } from "../../reducer";
|
||||
|
||||
export const ScrubberBar = ({
|
||||
debug: propsDebug = false,
|
||||
status,
|
||||
}:{
|
||||
debug?: boolean;
|
||||
status: string;
|
||||
})=>{
|
||||
const debug = false || propsDebug;
|
||||
if (debug) console.log("reRENDERING ------ ScrubberBar");
|
||||
|
||||
if (status === STATE_STATUSES.VIDEO_METADATA_LOADED){
|
||||
return(
|
||||
<div className="scrubber-bar">
|
||||
<ProgressBar />
|
||||
<TrimScrubber debug={debug}/>
|
||||
<TimeCursor />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return(
|
||||
<div className="scrubber-bar player-border-bottom d-flex justify-content-center align-items-center">
|
||||
<LoadingDots />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useState,
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
import { VideoElementContext } from '../../contexts';
|
||||
import { withScrubbing, withScrubbingPropsI } from './withScrubbing';
|
||||
|
||||
|
||||
export const TimeCursor = (props:{})=>{
|
||||
// console.log("reRENDERING ----- Time Cursor");
|
||||
|
||||
const videoElement = useContext(VideoElementContext);
|
||||
const [timeCursorOffset, setTimeCursorOffset] = useState(0);
|
||||
const [boundingWidth, setBoundingWidth] = useState(videoElement?.getBoundingClientRect().width || 0);
|
||||
const isLockedForScrubbing = useRef<boolean>(false);
|
||||
const wasPlaying = useRef<boolean>(false);
|
||||
|
||||
const PlayCursorWithScrubbing = withScrubbing<withScrubbingPropsI>(()=>{
|
||||
return(
|
||||
<div className="playcursor" />
|
||||
);
|
||||
});
|
||||
useEffect(()=>{
|
||||
const handleTimeCursorPosition = ()=>{
|
||||
if(videoElement!==null && !isLockedForScrubbing.current){
|
||||
const newOffset = (videoElement.currentTime / videoElement.duration) * (boundingWidth);
|
||||
|
||||
setTimeCursorOffset(newOffset);
|
||||
}
|
||||
};
|
||||
videoElement?.addEventListener("timeupdate", handleTimeCursorPosition);
|
||||
return()=>{
|
||||
videoElement?.removeEventListener("timeupdate",handleTimeCursorPosition);
|
||||
}
|
||||
},[videoElement, boundingWidth]);
|
||||
|
||||
const handleWindowResize = useCallback(()=> {
|
||||
if (videoElement !== null){
|
||||
console.log("BOUNDING WIDTH IS " + videoElement.getBoundingClientRect().width);
|
||||
setBoundingWidth(videoElement.getBoundingClientRect().width)
|
||||
}
|
||||
},[videoElement]);
|
||||
|
||||
useLayoutEffect(()=>{
|
||||
window.addEventListener("resize", handleWindowResize);
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleWindowResize);
|
||||
};
|
||||
}, [handleWindowResize])
|
||||
|
||||
const handleOnScrubStart = useCallback(() => {
|
||||
isLockedForScrubbing.current = true;
|
||||
if(videoElement!==null) {
|
||||
if (videoElement.paused) wasPlaying.current = false;
|
||||
else {
|
||||
wasPlaying.current = true;
|
||||
videoElement.pause();
|
||||
}
|
||||
}
|
||||
}, [videoElement])
|
||||
|
||||
const handleOnScrubEnd = useCallback( (newPos: number)=>{
|
||||
isLockedForScrubbing.current = false;
|
||||
if(videoElement !== null){
|
||||
const newTime = newPos / boundingWidth * videoElement.duration;
|
||||
videoElement.currentTime = newTime;
|
||||
if(wasPlaying.current === true){
|
||||
videoElement.play();
|
||||
}
|
||||
}
|
||||
},[videoElement, boundingWidth]);
|
||||
|
||||
return(
|
||||
<PlayCursorWithScrubbing
|
||||
boundingWidth={boundingWidth}
|
||||
hitboxPadding={16}
|
||||
scrubberWidth={8}
|
||||
onScrubStart={handleOnScrubStart}
|
||||
onScrubEnd={handleOnScrubEnd}
|
||||
scrubPosition={timeCursorOffset}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useState,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import {
|
||||
faGripDots
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { withScrubbing, withScrubbingPropsI } from 'components/highorder/withScrubbing';
|
||||
import {
|
||||
VideoElementContext,
|
||||
TrimContext,
|
||||
} from '../../contexts';
|
||||
|
||||
|
||||
interface TrimScrubberPropsI {
|
||||
debug?: boolean
|
||||
}
|
||||
export const TrimScrubber = ({
|
||||
debug: propsDebug = false,
|
||||
...rest
|
||||
}:TrimScrubberPropsI)=>{
|
||||
const debug = false || propsDebug;
|
||||
if (debug) console.log("reRENDERING ----- Trim Scrubber");
|
||||
|
||||
const videoElement = useContext(VideoElementContext);
|
||||
const trimValues = useContext(TrimContext);
|
||||
|
||||
const calcScrubberWidth = useCallback(()=>{
|
||||
if (videoElement!==null && trimValues !==null){
|
||||
const bound = videoElement.getBoundingClientRect().width;
|
||||
return Math.round(
|
||||
trimValues.trimDurationMs / (videoElement.duration * 1000) * bound
|
||||
);
|
||||
}
|
||||
return 0
|
||||
}, [videoElement, trimValues]);
|
||||
const [scrubberWidth, setScrubberWidth] = useState<number>(calcScrubberWidth());
|
||||
useEffect(()=>{
|
||||
if (trimValues !== null){
|
||||
const newWidth = calcScrubberWidth();
|
||||
setScrubberWidth((currWidth)=>{
|
||||
if (newWidth !== currWidth) return newWidth;
|
||||
else return currWidth;
|
||||
});
|
||||
}
|
||||
},[trimValues, calcScrubberWidth]);
|
||||
|
||||
|
||||
|
||||
const handleWindowResize = useCallback(()=> {
|
||||
setScrubberWidth(calcScrubberWidth());
|
||||
},[calcScrubberWidth]);
|
||||
|
||||
useLayoutEffect(()=>{
|
||||
window.addEventListener("resize", handleWindowResize);
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleWindowResize);
|
||||
};
|
||||
}, [handleWindowResize])
|
||||
|
||||
const TrimScrubberWithScrubbing = withScrubbing<withScrubbingPropsI>(() => {
|
||||
return(
|
||||
<div className="trim-scrubber">
|
||||
<FontAwesomeIcon icon={faGripDots} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
const calcScrubberPosition = useCallback(()=>{
|
||||
if (videoElement !== null && trimValues !== null){
|
||||
return (trimValues.trimStartMs / (videoElement.duration * 1000) * videoElement.getBoundingClientRect().width);
|
||||
}
|
||||
return 0;
|
||||
}, [videoElement, trimValues]);
|
||||
const scrubberPosition = useRef(calcScrubberPosition());
|
||||
const handleOnScrubEnd = useCallback((newPos: number)=>{
|
||||
scrubberPosition.current = newPos;
|
||||
|
||||
if(videoElement !== null && trimValues !== null){
|
||||
const boundingWidth = videoElement.getBoundingClientRect().width
|
||||
const newStartTime = Math.round(newPos / boundingWidth * videoElement.duration * 1000);
|
||||
if(trimValues.trimStartMs !== newStartTime){
|
||||
trimValues.onChange({
|
||||
trimStartMs: newStartTime,
|
||||
trimEndMs: newStartTime + trimValues.trimDurationMs,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [videoElement, trimValues])
|
||||
|
||||
return (
|
||||
<TrimScrubberWithScrubbing
|
||||
debug={debug}
|
||||
boundingWidth={videoElement?.getBoundingClientRect().width || 0}
|
||||
scrubberWidth={scrubberWidth}
|
||||
styleOverride={{
|
||||
top: '-1rem',
|
||||
}}
|
||||
scrubPosition={scrubberPosition.current}
|
||||
onScrubEnd={handleOnScrubEnd}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { ScrubberBar } from "./ScrubberBar";
|
||||
+26
-22
@@ -10,6 +10,7 @@ export interface withScrubbingPropsI {
|
||||
debug?: boolean
|
||||
boundingWidth: number;
|
||||
scrubberWidth: number;
|
||||
hitboxPadding?: number;
|
||||
scrubPosition?: number; // scrubber location as px
|
||||
styleOverride?: {[key: string]: string|number };
|
||||
onScrubStart?: ()=>void;
|
||||
@@ -28,13 +29,14 @@ export const withScrubbing = <P extends withScrubbingPropsI>(Component: React.Co
|
||||
debug: propsDebug = false,
|
||||
boundingWidth,
|
||||
scrubberWidth,
|
||||
hitboxPadding = 0,
|
||||
scrubPosition: propsLeftOffset = 0,
|
||||
styleOverride = {},
|
||||
onScrubStart,
|
||||
onScrubEnd,
|
||||
...rest
|
||||
}: withScrubbingPropsI) => {
|
||||
// const debug = true; //|| propsDebug;
|
||||
// const debug = true || propsDebug;
|
||||
|
||||
|
||||
const refEl = useRef<HTMLDivElement| null>(null);
|
||||
@@ -53,7 +55,7 @@ export const withScrubbing = <P extends withScrubbingPropsI>(Component: React.Co
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// if (debug) console.log(`withSCRUBBING useLAYOUTeffect!! `);
|
||||
function handleScrubStart (e: MouseEvent) {
|
||||
function handleScrubStart (e: PointerEvent) {
|
||||
// if (debug) console.log(`${refListener.current} withSCRUBBING SCRUB_START!! `);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -63,13 +65,14 @@ export const withScrubbing = <P extends withScrubbingPropsI>(Component: React.Co
|
||||
...curr,
|
||||
pointerStartPos: e.clientX
|
||||
}));
|
||||
|
||||
|
||||
if(onScrubStart) onScrubStart();
|
||||
window.addEventListener("pointermove", handleScrubMove);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
function handleScrubEnd (e: MouseEvent){
|
||||
function handleScrubEnd (e: PointerEvent){
|
||||
// if (debug) console.log(`${refListener.current} withSCRUBBING SCRUB_END!! `);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -82,7 +85,7 @@ export const withScrubbing = <P extends withScrubbingPropsI>(Component: React.Co
|
||||
setBySelf: Date.now(),
|
||||
}));
|
||||
};
|
||||
function handleScrubMove (e: MouseEvent){
|
||||
function handleScrubMove (e: PointerEvent){
|
||||
// if (debug) console.log(`${refListener.current} withSCRUBBING SCRUB_MOVE!! `);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -108,23 +111,23 @@ export const withScrubbing = <P extends withScrubbingPropsI>(Component: React.Co
|
||||
|
||||
// if(!(window as any)[`listener-id-${refListener.current}`]){
|
||||
// (window as any)[`listender-id-${refListener.current}`] = true;
|
||||
window.addEventListener("pointerdown", handleScrubStart);
|
||||
window.addEventListener("pointerup", handleScrubEnd);
|
||||
return () => {
|
||||
// (window as any)[`listener-id-${refListener.current}`] = false;
|
||||
window.removeEventListener("pointerdown", handleScrubStart);
|
||||
window.removeEventListener("pointerup", handleScrubEnd);
|
||||
window.removeEventListener("pointermove", handleScrubMove);
|
||||
};
|
||||
window.addEventListener("pointerdown", handleScrubStart);
|
||||
window.addEventListener("pointerup", handleScrubEnd);
|
||||
return () => {
|
||||
// (window as any)[`listener-id-${refListener.current}`] = false;
|
||||
window.removeEventListener("pointerdown", handleScrubStart);
|
||||
window.removeEventListener("pointerup", handleScrubEnd);
|
||||
window.removeEventListener("pointermove", handleScrubMove);
|
||||
};
|
||||
// }
|
||||
}, [scrubberWidth, boundingWidth]);
|
||||
}, [scrubberWidth, boundingWidth, onScrubStart]);
|
||||
|
||||
useEffect(()=>{
|
||||
// if (debug) console.log(`${refListener.current} withSCRUBBING useEFFECT for scrubStart!! `);
|
||||
if(onScrubStart && pointerStartPos > 0){
|
||||
onScrubStart();
|
||||
}
|
||||
},[pointerStartPos, onScrubStart]);
|
||||
// useEffect(()=>{
|
||||
// // if (debug) console.log(`${refListener.current} withSCRUBBING useEFFECT for scrubStart!! `);
|
||||
// if(onScrubStart && pointerStartPos > 0){
|
||||
// onScrubStart();
|
||||
// }
|
||||
// },[pointerStartPos, onScrubStart]);
|
||||
|
||||
useEffect(()=>{
|
||||
// if (debug) console.log(`${refListener.current} withSCRUBBING useEFFECT for scrubEend!! `);
|
||||
@@ -153,13 +156,14 @@ export const withScrubbing = <P extends withScrubbingPropsI>(Component: React.Co
|
||||
|
||||
return(
|
||||
<div
|
||||
className="scrubber-wrapper"
|
||||
className="with-scrubber-wrapper"
|
||||
ref={refEl}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top:0,
|
||||
width: scrubberWidth + 'px',
|
||||
left: currLeftOffset + 'px',
|
||||
padding: hitboxPadding + 'px',
|
||||
left: (currLeftOffset - hitboxPadding) + 'px',
|
||||
cursor: pointerStartPos >=0 ? 'grabbing': 'grab',
|
||||
...styleOverride
|
||||
}}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import React, {
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
import {
|
||||
faPlay,
|
||||
faPause,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useLocalize } from "hooks";
|
||||
import Spinner from "components/common/Spinner";
|
||||
import { STATE_STATUSES } from "../reducer";
|
||||
import { VideoElementContext } from "../contexts";
|
||||
|
||||
export function VideoTakeover({
|
||||
status,
|
||||
height
|
||||
}:{
|
||||
status: string;
|
||||
height:number;
|
||||
}){
|
||||
const { t } = useLocalize("vidEl");
|
||||
const vidEl = useContext(VideoElementContext);
|
||||
const [playpause, setPlaypause] = useState<"playing"|"paused">("paused");
|
||||
const togglePlaypause = ()=>{
|
||||
if (playpause === "playing") vidEl?.pause();
|
||||
else vidEl?.play();
|
||||
}
|
||||
|
||||
useEffect(()=>{
|
||||
const setPlaying = ()=>setPlaypause("playing");
|
||||
const setPaused = ()=>setPlaypause("paused");
|
||||
if(vidEl!==null){
|
||||
vidEl.addEventListener("play", setPlaying );
|
||||
vidEl.addEventListener("pause", setPaused);
|
||||
}
|
||||
return()=>{
|
||||
if(vidEl!==null){
|
||||
vidEl.removeEventListener("play", setPlaying);
|
||||
vidEl.removeEventListener("pause", setPaused);
|
||||
}
|
||||
};
|
||||
},[vidEl]);
|
||||
|
||||
|
||||
if(status === STATE_STATUSES.INIT){
|
||||
return(
|
||||
<div className="video-takeover">
|
||||
<div className="video-takeover-inner loading">
|
||||
<Spinner size={height/5}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}else if(status === STATE_STATUSES.ERROR_VIDEO_TOO_SHORT){
|
||||
return(
|
||||
<div className="video-takeover">
|
||||
<div className="video-takeover-inner">
|
||||
<h1>{t('error.videoTooShort')}</h1>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}else if(status === STATE_STATUSES.VIDEO_METADATA_LOADED){
|
||||
return(
|
||||
<div className="playpause-overlay" onClick={togglePlaypause}>
|
||||
{playpause === "playing" && (
|
||||
<FontAwesomeIcon
|
||||
className="playpause-icon"
|
||||
icon={faPause}
|
||||
size="8x"
|
||||
/>
|
||||
)}
|
||||
{playpause === "paused" && (
|
||||
<FontAwesomeIcon
|
||||
className="playpause-icon"
|
||||
icon={faPlay}
|
||||
size="8x"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null; //no take overs
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import {createContext} from 'react';
|
||||
|
||||
export const VideoElementContext = createContext<HTMLVideoElement | null>(null);
|
||||
export type TrimContextType = {
|
||||
trimStartMs: number,
|
||||
trimEndMs:number,
|
||||
trimDurationMs:number
|
||||
onChange: ({
|
||||
trimStartMs,trimEndMs,
|
||||
}:{
|
||||
trimStartMs: number,
|
||||
trimEndMs:number,
|
||||
})=>void
|
||||
}
|
||||
export const TrimContext = createContext<TrimContextType| null>(null)
|
||||
@@ -0,0 +1,2 @@
|
||||
export { VideoPlayerQuickTrim } from './VideoPlayerQuickTrim'
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
MIN_VID_DURATION,
|
||||
} from "./utilities";
|
||||
|
||||
export enum STATE_STATUSES {
|
||||
INIT = "init",
|
||||
VIDEO_METADATA_LOADED = "video_metadata_loaded",
|
||||
ERROR_VIDEO_TOO_SHORT = "error_video_too_short",
|
||||
ERROR_LOAD_ORDER = "error_load_order",
|
||||
ERROR_ACTION_TYPE = "error_no_such_action"
|
||||
};
|
||||
|
||||
export type State = {
|
||||
status: string;
|
||||
errorMessage: string[];
|
||||
videoDuration: number | undefined;
|
||||
canNotTrim: boolean | undefined;
|
||||
};
|
||||
export const initialState = {
|
||||
status: STATE_STATUSES.INIT,
|
||||
errorMessage : [],
|
||||
videoDuration: undefined,
|
||||
canNotTrim: undefined,
|
||||
}
|
||||
|
||||
export enum ACTION_TYPES {
|
||||
ON_LOADED_METADATA = "on_loaded_metadata",
|
||||
};
|
||||
|
||||
export type Action =
|
||||
| {type: ACTION_TYPES.ON_LOADED_METADATA, payload: {videoDuration: number}};
|
||||
|
||||
export function reducer(state: State, action: Action): State {
|
||||
switch(action.type){
|
||||
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,
|
||||
}
|
||||
}else{
|
||||
return {
|
||||
...state,
|
||||
status: STATE_STATUSES.ERROR_VIDEO_TOO_SHORT,
|
||||
errorMessage: [...state.errorMessage, `Videos need to be at least ${MIN_VID_DURATION}s long`],
|
||||
videoDuration: action.payload.videoDuration,
|
||||
}
|
||||
}
|
||||
}
|
||||
default:{
|
||||
return {
|
||||
...state,
|
||||
status: STATE_STATUSES.ERROR_ACTION_TYPE,
|
||||
errorMessage: [...state.errorMessage, "Reducer Action switch's default case is reached"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+89
-81
@@ -1,107 +1,114 @@
|
||||
@import "scss/colours.scss";
|
||||
|
||||
$playbar-height: 2rem;
|
||||
$scrubber-bar-height: 2rem;
|
||||
|
||||
.fy-video-quicktrim {
|
||||
.video-wrapper {
|
||||
.fy-video-player{
|
||||
.video-wrapper{
|
||||
position: relative;
|
||||
background-color: $bg;
|
||||
border-radius: 0.5rem 0.5rem 0 0;
|
||||
border-top: $panel-inner-border 1px solid;
|
||||
border-left: $panel-inner-border 1px solid;
|
||||
border-right: $panel-inner-border 1px solid;
|
||||
}
|
||||
.fy-video {
|
||||
max-height: 500px;
|
||||
width: 100%;
|
||||
margin-bottom: -7px;
|
||||
border-radius: 0.5rem 0.5rem 0 0;
|
||||
border: none;
|
||||
|
||||
video {
|
||||
overflow: hidden;
|
||||
|
||||
.fy-video {
|
||||
max-height: 500px;
|
||||
width: 100%;
|
||||
border-radius: 0.5rem 0.5rem 0 0;
|
||||
border: none;
|
||||
|
||||
video {
|
||||
border-radius: 0.5rem 0.5rem 0 0;
|
||||
}
|
||||
}
|
||||
.video-takeover{
|
||||
position: absolute;
|
||||
display: block;
|
||||
top:0;
|
||||
left:0;
|
||||
width:100%;
|
||||
height: 100%;
|
||||
|
||||
.video-takeover-inner{
|
||||
position:relative;
|
||||
width:100%;
|
||||
height: 100%;
|
||||
&.loading{
|
||||
background-color: $bg;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.fy-spinner{
|
||||
position: absolute;
|
||||
top:50%;
|
||||
left:50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.playpause-overlay {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
&:hover svg {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.playpause-overlay {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
.scrubber-bar {
|
||||
position: relative;
|
||||
height: $scrubber-bar-height;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s;
|
||||
&.player-border-bottom,
|
||||
.player-border-bottom {
|
||||
background-color: $panel-inner-border;
|
||||
border-radius: 0 0 0.5rem 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
&:hover svg {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.warning-too-short {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
div {
|
||||
.progress-bar {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: #e66462;
|
||||
opacity: 70%;
|
||||
}
|
||||
h1 {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
.playbar {
|
||||
position: relative;
|
||||
height: $playbar-height;
|
||||
width: 100%;
|
||||
margin-top: 7px;
|
||||
}
|
||||
.playbar-bg {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
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;
|
||||
transition: width 0.5s;
|
||||
|
||||
.played, .loaded{
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.played{
|
||||
background-color: $primary-dark;
|
||||
// transition: width 0.5s;
|
||||
}
|
||||
.loaded{
|
||||
background-color: $gray-600;
|
||||
transition: width 0.5s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scrubber-wrapper{
|
||||
box-shadow: 1px 1px 4px 0px rgba(0,0,0,1);
|
||||
}
|
||||
|
||||
// .with-scrubber-wrapper{
|
||||
|
||||
// // transition: 0.5s;
|
||||
// }
|
||||
.trim-scrubber{
|
||||
height: 3rem;
|
||||
background-color: #e66462;
|
||||
@@ -109,6 +116,7 @@ $playbar-height: 2rem;
|
||||
outline: 0.125rem $white solid;
|
||||
border-top: 1rem $white solid;
|
||||
border-radius: 0.25rem 0.25rem 0.125rem 0.125rem;
|
||||
|
||||
|
||||
|
||||
display: flex;
|
||||
@@ -124,7 +132,7 @@ $playbar-height: 2rem;
|
||||
top: 0;
|
||||
width: 0.5rem; //=8px
|
||||
transform: translate(-3px, 0px);
|
||||
height: $playbar-height;
|
||||
height: $scrubber-bar-height;
|
||||
background-color: $white;
|
||||
border-radius: 1px;
|
||||
box-shadow: 1px 1px 4px 0px rgba(0,0,0,0.2);
|
||||
+4
-9
@@ -3,11 +3,6 @@ export const TIME_CURSOR_WIDTH = 8;
|
||||
export const MIN_VID_DURATION = 3;
|
||||
export const ONE_MS = 0.001;
|
||||
|
||||
export type QuickTrimData = {
|
||||
trimStartSeconds: number;
|
||||
trimEndSeconds: number;
|
||||
}
|
||||
|
||||
export function roundToMilliseconds(floaty:number){
|
||||
return Math.round(floaty*1000)/1000;
|
||||
}
|
||||
@@ -22,8 +17,8 @@ export function formatSecondsToHHMMSSCS(seconds:number){
|
||||
}
|
||||
|
||||
export const TRIM_OPTIONS:{[key: string]: number } = {
|
||||
"3s": 3,
|
||||
"5s": 5,
|
||||
"10s":10,
|
||||
"15s":15
|
||||
"3s": 3000,
|
||||
"5s": 5000,
|
||||
"10s":10000,
|
||||
"15s":15000,
|
||||
};
|
||||
@@ -1,222 +0,0 @@
|
||||
import React, {
|
||||
// memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useReducer,
|
||||
useRef,
|
||||
} from "react";
|
||||
import {
|
||||
faPlay,
|
||||
faPause,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useLocalize } from "hooks";
|
||||
import { VideoFakeyou } from "components/common";
|
||||
import { VideoFakeyouProps } from "../VideoFakeyou/VideoFakeyou";
|
||||
|
||||
import {
|
||||
reducer,
|
||||
initialState,
|
||||
ACTION_TYPES,
|
||||
PLAYPUASE_STATES,
|
||||
STATE_STATUSES,
|
||||
} from "./reducer";
|
||||
|
||||
import {
|
||||
VideoElementContext,
|
||||
} from "./contexts";
|
||||
|
||||
import { QuickTrimData } from "./utilities";
|
||||
|
||||
import { ProgressBar } from "./components/ProgressBar";
|
||||
import { ControlBar } from "./components/ControlBar";
|
||||
import './styles.scss'
|
||||
|
||||
interface VideoQuickTrimProps extends VideoFakeyouProps {
|
||||
debug?: boolean
|
||||
onSelect: (values: QuickTrimData) => void;
|
||||
trimStartSeconds: number;
|
||||
trimEndSeconds: number;
|
||||
}
|
||||
|
||||
export const VideoQuickTrim = ({
|
||||
debug: propsDebug = false,
|
||||
onSelect,
|
||||
trimStartSeconds: propsTrimStartSeconds,
|
||||
trimEndSeconds : propsTrimEndSeconds,
|
||||
...rest
|
||||
}: VideoQuickTrimProps) => {
|
||||
const debug = true || propsDebug;
|
||||
|
||||
const { t } = useLocalize("vidEl");
|
||||
const [compState, dispatchCompState] = useReducer(reducer, initialState);
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
|
||||
if(compState.status === STATE_STATUSES.LOAD_ORDER_ERROR){
|
||||
console.log(`${compState.errorMessage[compState.errorMessage.length -1]}`);
|
||||
}
|
||||
if(debug) console.log("reRENDERING - Video Quicktrim");
|
||||
|
||||
const videoRefCallback = useCallback(node => {
|
||||
function setPlaypause(newState: PLAYPUASE_STATES){
|
||||
dispatchCompState({
|
||||
type: ACTION_TYPES.SET_PLAYPUASE,
|
||||
payload: { playpause: newState}
|
||||
})
|
||||
}
|
||||
function handleLoadedmetadata (){
|
||||
dispatchCompState({
|
||||
type: ACTION_TYPES.ON_LOADED_METADATA,
|
||||
payload:{ videoDuration: node.duration,}
|
||||
});
|
||||
};
|
||||
if (node !== null) {
|
||||
// DOM node referenced by ref has changed and exists
|
||||
videoRef.current = node;
|
||||
|
||||
node.addEventListener("loadedmetadata", handleLoadedmetadata);
|
||||
|
||||
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
|
||||
|
||||
|
||||
return()=>{
|
||||
if(node!==null)
|
||||
node.removeEventListener("loadedmetadata",handleLoadedmetadata);
|
||||
}
|
||||
|
||||
}, [
|
||||
// No Dependency !
|
||||
]); //END videoRefCallback\
|
||||
|
||||
useEffect(()=>{
|
||||
if( compState.trimStartSeconds !== undefined
|
||||
&& compState.trimEndSeconds !== undefined
|
||||
&& (
|
||||
compState.trimStartSeconds !== propsTrimStartSeconds
|
||||
|| compState.trimEndSeconds !== propsTrimEndSeconds
|
||||
)
|
||||
){
|
||||
console.log(`TRIM on useEFFECT: calling onSELECT`);
|
||||
onSelect({
|
||||
trimStartSeconds: compState.trimStartSeconds,
|
||||
trimEndSeconds: compState.trimEndSeconds,
|
||||
});
|
||||
}
|
||||
}, [onSelect, compState.trimStartSeconds, compState.trimEndSeconds, propsTrimStartSeconds, propsTrimEndSeconds])
|
||||
|
||||
const togglePlaypause = useCallback(()=>{
|
||||
if (videoRef.current === null){
|
||||
console.log('Playpause is toggled while it is NOT_READY');
|
||||
}else if(videoRef.current.paused){
|
||||
videoRef.current.play();
|
||||
}else{
|
||||
videoRef.current.pause();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handlePlaypause = useCallback((shouldPlay: boolean)=>{
|
||||
if(videoRef.current){
|
||||
if(shouldPlay){
|
||||
videoRef.current.play();
|
||||
}else {
|
||||
videoRef.current.pause();
|
||||
}
|
||||
}else{
|
||||
console.log('Playpause is handled while it is NOT_READY');
|
||||
}
|
||||
}, []);
|
||||
|
||||
function disableRepeatOn(){
|
||||
dispatchCompState({
|
||||
type: ACTION_TYPES.TOGGLE_REPEAT,
|
||||
payload: {isRepeatOn: false}
|
||||
});
|
||||
}
|
||||
|
||||
// Refs that should be used to re-render childs are put in context
|
||||
|
||||
return (
|
||||
<div className="fy-video-quicktrim">
|
||||
<div className="video-wrapper">
|
||||
<VideoFakeyou
|
||||
debug={false}
|
||||
height={500}
|
||||
controls={false}
|
||||
muted={compState.isMuted}
|
||||
ref={videoRefCallback}
|
||||
{...rest}
|
||||
/>
|
||||
{compState.playpause !== PLAYPUASE_STATES.NOT_READY &&
|
||||
<div className="playpause-overlay" onClick={togglePlaypause}>
|
||||
{videoRef.current !==null && videoRef.current.paused && (
|
||||
<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>
|
||||
<h1>{t('error.videoTooShort')}</h1>
|
||||
</div>
|
||||
}
|
||||
</div>{/* END of Video Wrapper */}
|
||||
{videoRef.current &&
|
||||
<VideoElementContext.Provider value={videoRef.current}>
|
||||
<ProgressBar
|
||||
debug={debug}
|
||||
readyToMount={(compState.status === STATE_STATUSES.VIDEO_METADATA_LOADED)}
|
||||
isRepeatOn={compState.isRepeatOn}
|
||||
trimStartSeconds={compState.trimStartSeconds ||0}
|
||||
trimEndSeconds={compState.trimEndSeconds ||0}
|
||||
trimDuration={compState.trimDuration ||0}
|
||||
playbarWidth={compState.playbarWidth ||0}
|
||||
scrubberWidth={compState.scrubberWidth ||0}
|
||||
videoDuration={compState.videoDuration ||0}
|
||||
handlePlaypause={handlePlaypause}
|
||||
dispatchCompState={dispatchCompState}
|
||||
onPlayCursorChanged={(newPos: number)=>{
|
||||
if(videoRef.current !== null && videoRef.current.currentTime
|
||||
&& compState.playbarWidth && compState.videoDuration){
|
||||
const newTime = newPos / compState.playbarWidth * compState.videoDuration;
|
||||
if(compState.trimStartSeconds
|
||||
&& compState.trimEndSeconds
|
||||
&& (newTime < compState.trimStartSeconds
|
||||
|| newTime > compState.trimEndSeconds
|
||||
)){
|
||||
disableRepeatOn();
|
||||
}
|
||||
videoRef.current.currentTime = newTime;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<ControlBar
|
||||
debug={debug}
|
||||
readyToMount={(compState.status === STATE_STATUSES.VIDEO_METADATA_LOADED)}
|
||||
isMuted={compState.isMuted}
|
||||
isRepeatOn={compState.isRepeatOn}
|
||||
playpause={compState.playpause}
|
||||
handlePlaypause={togglePlaypause}
|
||||
dispatchCompState={dispatchCompState}
|
||||
/>
|
||||
|
||||
</VideoElementContext.Provider>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,127 +0,0 @@
|
||||
import React, {
|
||||
useState,
|
||||
useContext,
|
||||
useEffect,
|
||||
} 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";
|
||||
import { VideoElementContext } from '../contexts';
|
||||
|
||||
export const ControlBar = ({
|
||||
debug: propsDebug = false,
|
||||
readyToMount,
|
||||
isRepeatOn,
|
||||
isMuted,
|
||||
playpause,
|
||||
handlePlaypause,
|
||||
dispatchCompState
|
||||
}:{
|
||||
debug?:boolean;
|
||||
readyToMount: boolean;
|
||||
isMuted: boolean,
|
||||
isRepeatOn: boolean;
|
||||
playpause: string;
|
||||
handlePlaypause: ()=>void;
|
||||
dispatchCompState: (action: Action) => void;
|
||||
})=>{
|
||||
const debug = false;// || propsDebug;
|
||||
if (debug) console.log("reRENDERING ------ ControlBar");
|
||||
|
||||
function handleSetTrimDuration(selected: string){
|
||||
dispatchCompState({
|
||||
type: ACTION_TYPES.SET_TRIM_DURATION,
|
||||
payload: {
|
||||
trimDurationString: selected
|
||||
}
|
||||
});
|
||||
};
|
||||
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})}
|
||||
/>
|
||||
<TimeLabel />
|
||||
</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;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
function TimeLabel(){
|
||||
const vidEl = useContext(VideoElementContext);
|
||||
const [currentTime, setCurrentTime] = useState<number>(0);
|
||||
const [duration, setDuration] = useState<number>(0);
|
||||
|
||||
useEffect(()=>{
|
||||
function handleTimeStamp(){
|
||||
setCurrentTime(vidEl?.currentTime ||0);
|
||||
};
|
||||
function handleDuration(){
|
||||
setDuration(vidEl?.duration ||0)
|
||||
};
|
||||
vidEl?.addEventListener("timeupdate", handleTimeStamp);
|
||||
vidEl?.addEventListener("loadmetadata", handleDuration);
|
||||
return()=>{
|
||||
vidEl?.removeEventListener("timeupdate",handleTimeStamp);
|
||||
vidEl?.addEventListener("loadmetadata", handleDuration);
|
||||
}
|
||||
},[vidEl]);
|
||||
return(
|
||||
<div className="playtime d-flex">
|
||||
<span>
|
||||
<p>
|
||||
{`${formatSecondsToHHMMSSCS(currentTime)}`}
|
||||
</p>
|
||||
</span>
|
||||
<div>/</div>
|
||||
<span>
|
||||
<p>
|
||||
{`${formatSecondsToHHMMSSCS(duration)}`}
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import React, {
|
||||
useContext,
|
||||
useState,
|
||||
useEffect
|
||||
} from 'react';
|
||||
import { withScrubbing, withScrubbingPropsI } from './withScrubbing';
|
||||
|
||||
import { VideoElementContext } from '../contexts';
|
||||
import { ONE_MS } from '../utilities';
|
||||
|
||||
interface PlayCursorPropsI extends withScrubbingPropsI{
|
||||
isRepeatOn: boolean;
|
||||
playBoundStart: number;
|
||||
playBoundEnd: number;
|
||||
onChanged: (posPercent: number)=>void;
|
||||
}
|
||||
|
||||
export const PlayCursor = ({
|
||||
isRepeatOn,
|
||||
playBoundStart,
|
||||
playBoundEnd,
|
||||
onChanged,
|
||||
...rest
|
||||
}:PlayCursorPropsI)=>{
|
||||
const videoElement = useContext(VideoElementContext);
|
||||
const [timeCursorOffset, setTimeCursorOffset] = useState(0);
|
||||
const PlayCursorWithScrubbing = withScrubbing<withScrubbingPropsI>(()=>{
|
||||
return(
|
||||
<div className="playcursor" />
|
||||
);
|
||||
});
|
||||
useEffect(()=>{
|
||||
const handleTimeCursorPosition = ()=>{
|
||||
if(videoElement!==null){
|
||||
if(isRepeatOn && (
|
||||
videoElement.currentTime < playBoundStart ||
|
||||
videoElement.currentTime > playBoundEnd
|
||||
)){
|
||||
videoElement.currentTime = playBoundStart + ONE_MS
|
||||
}
|
||||
const newOffset = (videoElement.currentTime / videoElement.duration) * (rest.boundingWidth);
|
||||
setTimeCursorOffset(newOffset);
|
||||
}
|
||||
};
|
||||
videoElement?.addEventListener("timeupdate", handleTimeCursorPosition);
|
||||
return()=>{
|
||||
videoElement?.removeEventListener("timeupdate",handleTimeCursorPosition);
|
||||
}
|
||||
},[videoElement, isRepeatOn, playBoundStart, playBoundEnd, rest.boundingWidth]);
|
||||
|
||||
return(
|
||||
<PlayCursorWithScrubbing
|
||||
onScrubEnd={onChanged}
|
||||
scrubPosition={timeCursorOffset}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,142 +0,0 @@
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useContext,
|
||||
useCallback,
|
||||
useLayoutEffect,
|
||||
useRef
|
||||
} from 'react';
|
||||
import {
|
||||
Action,
|
||||
ACTION_TYPES,
|
||||
} from '../reducer';
|
||||
import { TrimScrubber } from './TrimScrubber';
|
||||
import { PlayCursor } from './PlayCursor';
|
||||
import { VideoElementContext } from '../contexts';
|
||||
|
||||
export const ProgressBar = ({
|
||||
debug: propsDebug = false,
|
||||
readyToMount,
|
||||
isRepeatOn,
|
||||
trimStartSeconds,
|
||||
trimEndSeconds,
|
||||
trimDuration,
|
||||
playbarWidth,
|
||||
scrubberWidth,
|
||||
videoDuration,
|
||||
onPlayCursorChanged,
|
||||
handlePlaypause,
|
||||
dispatchCompState
|
||||
}:{
|
||||
debug?: boolean;
|
||||
readyToMount: boolean;
|
||||
isRepeatOn: boolean;
|
||||
trimStartSeconds: number;
|
||||
trimEndSeconds: number;
|
||||
trimDuration: number;
|
||||
playbarWidth: number;
|
||||
scrubberWidth: number;
|
||||
videoDuration: number;
|
||||
onPlayCursorChanged: (newPos: number) => void;
|
||||
handlePlaypause: (shouldPlay:boolean)=>void;
|
||||
dispatchCompState: (action: Action) => void;
|
||||
})=>{
|
||||
console.log(`ProgressBAR reRENDERING`)
|
||||
const debug = false || propsDebug;
|
||||
if(debug) console.log("reRENDERING --- Progress Bar");
|
||||
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}>
|
||||
<Progress />
|
||||
<TrimScrubber
|
||||
debug={debug}
|
||||
boundingWidth={playbarWidth}
|
||||
scrubberWidth={scrubberWidth}
|
||||
trimStartSeconds={trimStartSeconds}
|
||||
trimDuration={trimDuration}
|
||||
videoDuration={videoDuration}
|
||||
handlePlaypause={()=>handlePlaypause(false)}
|
||||
onChange={(newPos: number)=>{
|
||||
const newTrimStartSeconds = newPos / (playbarWidth) * videoDuration;
|
||||
dispatchCompState({
|
||||
type: ACTION_TYPES.MOVE_TRIM,
|
||||
payload: {
|
||||
trimStartSeconds: newTrimStartSeconds,
|
||||
trimEndSeconds: newTrimStartSeconds + trimDuration
|
||||
}
|
||||
});
|
||||
// handlePlaypause(true);
|
||||
}}
|
||||
/>
|
||||
<PlayCursor
|
||||
isRepeatOn={isRepeatOn}
|
||||
onChanged={onPlayCursorChanged}
|
||||
playBoundStart={trimStartSeconds}
|
||||
playBoundEnd={trimEndSeconds}
|
||||
boundingWidth={playbarWidth}
|
||||
scrubberWidth={8}
|
||||
/>
|
||||
{/* END of Playbar */}</div>
|
||||
);
|
||||
}else{
|
||||
console.log('TODO: ProgressBar should rendering loading state instead of null');
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
function Progress (){
|
||||
const vidEl = useContext(VideoElementContext);
|
||||
const [buffered, setBuffered] = useState<number>(0);
|
||||
useEffect(()=>{
|
||||
function handleBuffer(){
|
||||
if(vidEl && vidEl.buffered.length > 0){
|
||||
setBuffered(vidEl.buffered.end(0))
|
||||
}
|
||||
}
|
||||
vidEl?.addEventListener("progress", handleBuffer);
|
||||
},[vidEl]);
|
||||
|
||||
if(vidEl){
|
||||
return(
|
||||
<div className="playbar-bg">
|
||||
<span className="loaded" style={{width: (buffered / vidEl.duration* 100) + "%"}} />
|
||||
{/* <span className="played" style={{width: timeCursorOffset+"px"}} /> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else return null;
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
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;
|
||||
handlePlaypause: ()=> void;
|
||||
onChange:(newPos:number)=>void
|
||||
}
|
||||
export const TrimScrubber = memo(({
|
||||
debug: propsDebug = false,
|
||||
trimStartSeconds,
|
||||
trimDuration,
|
||||
videoDuration,
|
||||
handlePlaypause,
|
||||
onChange,
|
||||
...rest
|
||||
}:TrimScrubberPropsI)=>{
|
||||
const debug = false || propsDebug;
|
||||
console.log("trimScrubber reRENDERING");
|
||||
const TrimScrubberWithScrubbing = withScrubbing<TrimScrubberPropsI>(() => {
|
||||
return(
|
||||
<div className="trim-scrubber">
|
||||
<FontAwesomeIcon icon={faGripDots} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<TrimScrubberWithScrubbing
|
||||
debug={debug}
|
||||
styleOverride={{
|
||||
top: '-1rem',
|
||||
}}
|
||||
scrubPosition={trimStartSeconds/videoDuration*(rest.boundingWidth)}
|
||||
onScrubStart={handlePlaypause}
|
||||
onScrubEnd={onChange}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
import {createContext} from 'react';
|
||||
|
||||
export const VideoElementContext = createContext<HTMLVideoElement | null>(null);
|
||||
@@ -1,5 +0,0 @@
|
||||
import {VideoQuickTrim} from './VideoQuickTrim'
|
||||
import { QuickTrimData } from './utilities';
|
||||
|
||||
export type { QuickTrimData };
|
||||
export default VideoQuickTrim;
|
||||
@@ -1,218 +0,0 @@
|
||||
import {
|
||||
MIN_VID_DURATION,
|
||||
TRIM_OPTIONS,
|
||||
roundToMilliseconds
|
||||
} 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;
|
||||
trimDuration: number | undefined;
|
||||
trimStartSeconds: number | undefined;
|
||||
trimEndSeconds: number | undefined;
|
||||
videoLoadProgress: TimeRanges | undefined;
|
||||
videoDuration: number | undefined;
|
||||
|
||||
playbarWidth: number | undefined;
|
||||
scrubberWidth: number | undefined;
|
||||
|
||||
isMuted: boolean;
|
||||
isRepeatOn: boolean;
|
||||
};
|
||||
|
||||
export const initialState = {
|
||||
status: STATE_STATUSES.INIT,
|
||||
errorMessage : [],
|
||||
playpause: PLAYPUASE_STATES.NOT_READY,
|
||||
canNotTrim: undefined,
|
||||
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, payload?:{isRepeatOn: boolean}}
|
||||
| {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: TimeRanges
|
||||
}}
|
||||
| {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:{
|
||||
if(action.payload){
|
||||
return {...state, isRepeatOn: action.payload.isRepeatOn}
|
||||
}
|
||||
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 = roundToMilliseconds(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: roundToMilliseconds(action.payload.trimStartSeconds),
|
||||
trimEndSeconds: roundToMilliseconds(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;
|
||||
*/
|
||||
|
||||
@@ -51,7 +51,6 @@ import TintSpinner from "./TintSpinner";
|
||||
import VideoBasic from "./VideoBasic";
|
||||
import VideoFakeyou from "./VideoFakeyou";
|
||||
import VideoInput from "./VideoInput";
|
||||
// import VideoQuickTrim, {QuickTrimType} from "./VideoQuickTrim";
|
||||
import WeightCoverImage from "./WeightCoverImage";
|
||||
export type { SelectModalData };
|
||||
|
||||
@@ -115,5 +114,4 @@ export {
|
||||
VideoFakeyou,
|
||||
VideoInput,
|
||||
WeightCoverImage,
|
||||
// VideoQuickTrim,
|
||||
};
|
||||
@@ -0,0 +1,176 @@
|
||||
import React,{
|
||||
memo,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useLayoutEffect,
|
||||
} from 'react';
|
||||
|
||||
export interface withScrubbingPropsI {
|
||||
debug?: boolean
|
||||
boundingWidth: number;
|
||||
scrubberWidth: number;
|
||||
hitboxPadding?: number;
|
||||
scrubPosition?: number; // scrubber location as px
|
||||
styleOverride?: {[key: string]: string|number };
|
||||
onScrubStart?: ()=>void;
|
||||
//only deal with event side effecrts
|
||||
onScrubEnd?: (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(({
|
||||
debug: propsDebug = false,
|
||||
boundingWidth,
|
||||
scrubberWidth,
|
||||
hitboxPadding = 0,
|
||||
scrubPosition: propsLeftOffset = 0,
|
||||
styleOverride = {},
|
||||
onScrubStart,
|
||||
onScrubEnd,
|
||||
...rest
|
||||
}: withScrubbingPropsI) => {
|
||||
// const debug = true || propsDebug;
|
||||
|
||||
|
||||
const refEl = useRef<HTMLDivElement| null>(null);
|
||||
// const refListener = useRef<number>(Date.now());
|
||||
|
||||
// if (debug) console.log(`${refListener.current} withSCRUBBING reRENDERING!! `);
|
||||
|
||||
const [{
|
||||
currLeftOffset, pointerStartPos,
|
||||
prevLeftOffset
|
||||
}, setStates] = useState<withSrcubbingStates>({
|
||||
currLeftOffset: propsLeftOffset,
|
||||
prevLeftOffset: propsLeftOffset,
|
||||
pointerStartPos: -1 // negative denotes pointer not engaged
|
||||
});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// if (debug) console.log(`withSCRUBBING useLAYOUTeffect!! `);
|
||||
function handleScrubStart (e: PointerEvent) {
|
||||
// if (debug) console.log(`${refListener.current} withSCRUBBING SCRUB_START!! `);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if(refEl.current){
|
||||
if(refEl.current.contains(e.target as Node)){
|
||||
setStates((curr)=>({
|
||||
...curr,
|
||||
pointerStartPos: e.clientX
|
||||
}));
|
||||
|
||||
if(onScrubStart) onScrubStart();
|
||||
window.addEventListener("pointermove", handleScrubMove);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
function handleScrubEnd (e: PointerEvent){
|
||||
// if (debug) console.log(`${refListener.current} withSCRUBBING SCRUB_END!! `);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
window.removeEventListener("pointermove", handleScrubMove);
|
||||
setStates((curr)=>({
|
||||
...curr,
|
||||
pointerStartPos: -1,
|
||||
prevLeftOffset: curr.currLeftOffset,
|
||||
setBySelf: Date.now(),
|
||||
}));
|
||||
};
|
||||
function handleScrubMove (e: PointerEvent){
|
||||
// if (debug) console.log(`${refListener.current} withSCRUBBING SCRUB_MOVE!! `);
|
||||
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.current}`]){
|
||||
// (window as any)[`listender-id-${refListener.current}`] = true;
|
||||
window.addEventListener("pointerdown", handleScrubStart);
|
||||
window.addEventListener("pointerup", handleScrubEnd);
|
||||
return () => {
|
||||
// (window as any)[`listener-id-${refListener.current}`] = false;
|
||||
window.removeEventListener("pointerdown", handleScrubStart);
|
||||
window.removeEventListener("pointerup", handleScrubEnd);
|
||||
window.removeEventListener("pointermove", handleScrubMove);
|
||||
};
|
||||
// }
|
||||
}, [scrubberWidth, boundingWidth, onScrubStart]);
|
||||
|
||||
// useEffect(()=>{
|
||||
// // if (debug) console.log(`${refListener.current} withSCRUBBING useEFFECT for scrubStart!! `);
|
||||
// if(onScrubStart && pointerStartPos > 0){
|
||||
// onScrubStart();
|
||||
// }
|
||||
// },[pointerStartPos, onScrubStart]);
|
||||
|
||||
useEffect(()=>{
|
||||
// if (debug) console.log(`${refListener.current} withSCRUBBING useEFFECT for scrubEend!! `);
|
||||
if(onScrubEnd && boundingWidth > 0 && prevLeftOffset >= 0 && propsLeftOffset !== prevLeftOffset){
|
||||
onScrubEnd(prevLeftOffset);
|
||||
}
|
||||
},[propsLeftOffset, prevLeftOffset, boundingWidth, onScrubEnd]);
|
||||
|
||||
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="with-scrubber-wrapper"
|
||||
ref={refEl}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top:0,
|
||||
width: scrubberWidth + 'px',
|
||||
padding: hitboxPadding + 'px',
|
||||
left: (currLeftOffset - hitboxPadding) + 'px',
|
||||
cursor: pointerStartPos >=0 ? 'grabbing': 'grab',
|
||||
...styleOverride
|
||||
}}
|
||||
>
|
||||
<Component
|
||||
{...rest as P}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, {
|
||||
// useState
|
||||
useState
|
||||
} from 'react';
|
||||
import { VideoPlayer } from 'components/common/VideoPlayer';
|
||||
import { VideoPlayerQuickTrim } from 'components/common/VideoPlayerQuickTrim';
|
||||
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import {Container, Panel} from 'components/common';
|
||||
@@ -9,37 +11,33 @@ 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);
|
||||
// }
|
||||
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.trimStartMs);
|
||||
setState2(val.trimEndMs);
|
||||
}
|
||||
return (<Container>
|
||||
<NavLink to={`${parentPath}`}>← Back</NavLink>
|
||||
<h1>Sandbox</h1>
|
||||
<Panel>
|
||||
|
||||
<div className="m-4">
|
||||
<VideoPlayer
|
||||
// trimStartSeconds={Number(state1 ||0)}
|
||||
// trimEndSeconds={Number(state2 || 0)}
|
||||
// onSelectTrim={handleCallback}
|
||||
<VideoPlayer
|
||||
mediaToken='m_wdptbe5rfzpb1gzhva78gpw8qrkgs9'
|
||||
/>
|
||||
</div>
|
||||
<br/>
|
||||
{/* <p>{`Start Seconds: ${state1}`}</p>
|
||||
<p>{`Start Seconds: ${state2}`}</p> */}
|
||||
{/* <br/>
|
||||
<div className="m-4">
|
||||
<VideoQuickTrim
|
||||
onSelect={handleCallback}
|
||||
mediaToken='m_h21nykes0j3ph23y5z7w9axtjjn2rr'
|
||||
<VideoPlayerQuickTrim
|
||||
trimStartMs={Number(state1 ||0)}
|
||||
trimEndMs={Number(state2 || 0)}
|
||||
onSelectTrim={handleCallback}
|
||||
mediaToken='m_wdptbe5rfzpb1gzhva78gpw8qrkgs9'
|
||||
/>
|
||||
</div> */}
|
||||
<p>{`Start Seconds: ${state1}`}</p>
|
||||
<p>{`Start Seconds: ${state2}`}</p>
|
||||
</Panel>
|
||||
</Container>);
|
||||
}
|
||||
+3
-5
@@ -2,10 +2,8 @@ import React, { useRef, useState } from "react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { NavLink, useParams, useHistory } from "react-router-dom";
|
||||
import { Button, Container, Panel, TextArea } from "components/common";
|
||||
// import VideoQuickTrim, {
|
||||
// QuickTrimData,
|
||||
// } from "components/common/VideoQuickTrim";
|
||||
import { VideoFakeyou } from "components/common";
|
||||
|
||||
import { VideoPlayer } from "components/common/VideoPlayer";
|
||||
import { useJobStatus, useInferenceJobs } from "hooks";
|
||||
import { EnqueueVST, EnqueueVSTResponse } from "@storyteller/components/src/api/video_styleTransfer/Enqueue_VST";
|
||||
import { initialValues } from "./defaultValues";
|
||||
@@ -145,7 +143,7 @@ export default function PageVSTApp() {
|
||||
<div className="row g-5">
|
||||
<div className="col-12 col-md-6">
|
||||
{job.isSuccessful && job.maybe_result ? (
|
||||
<VideoFakeyou
|
||||
<VideoPlayer
|
||||
mediaToken={job.maybe_result.entity_token}
|
||||
/>
|
||||
) : (
|
||||
|
||||
+10
-9
@@ -3,9 +3,7 @@ import { NavLink } from "react-router-dom";
|
||||
import { useParams, useHistory } from "react-router-dom";
|
||||
|
||||
import { Button, Panel, TextArea } from "components/common";
|
||||
import VideoQuickTrim, {
|
||||
QuickTrimData,
|
||||
} from "components/common/VideoQuickTrim";
|
||||
import {VideoPlayerQuickTrim} from "components/common/VideoPlayerQuickTrim";
|
||||
import EnqueueVideoStyleTransfer from "@storyteller/components/src/api/video_styleTransfer";
|
||||
|
||||
import { Action, State } from "../../reducer";
|
||||
@@ -75,14 +73,17 @@ export default function PageVSTApp({
|
||||
<Panel padding={true}>
|
||||
<div className="row g-5 mb-4">
|
||||
<div className="col-12 col-md-6">
|
||||
<VideoQuickTrim
|
||||
trimStartSeconds={vstValues.trimStart}
|
||||
trimEndSeconds={vstValues.trimEnd}
|
||||
<VideoPlayerQuickTrim
|
||||
trimStartMs={vstValues.trimStart}
|
||||
trimEndMs={vstValues.trimEnd}
|
||||
mediaToken={vstValues.fileToken}
|
||||
onSelect={(val: QuickTrimData) =>{
|
||||
onSelectTrim={(val: {
|
||||
trimStartMs: number;
|
||||
trimEndMs: number;
|
||||
}) =>{
|
||||
handleOnChange({
|
||||
trimStart: val.trimStartSeconds,
|
||||
trimEnd: val.trimEndSeconds,
|
||||
trimStart: val.trimStartMs,
|
||||
trimEnd: val.trimEndMs,
|
||||
})
|
||||
}}
|
||||
onResponse={res => {
|
||||
|
||||
Reference in new issue
Block a user