mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-06 10:11:31 +00:00
new video player, simlified version of trim player
This commit is contained in:
1 parent
dcafbfb537
commit
f9edfa523f
23 files changed
+824
-125
No files matched your search
@@ -10,8 +10,12 @@ const Line = ({ yah }:{ yah: any }) => <polyline {...{
|
||||
transform: `rotate(${ 30 * yah })`,
|
||||
}}/>;
|
||||
|
||||
export default function Spinner() {
|
||||
return <svg {...{ className: "fy-spinner", height: 32, viewBox: "0 0 32 32", width: 32 }}>
|
||||
export default function Spinner({
|
||||
size= 32
|
||||
}:{
|
||||
size?:number
|
||||
}) {
|
||||
return <svg {...{ className: "fy-spinner", height: size, viewBox: "0 0 32 32", width: size }}>
|
||||
{ [...Array(12)].map((l,key) => <Line {...{ key, yah: key }}/>) }
|
||||
</svg>;
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
import React, {
|
||||
useRef,
|
||||
useCallback,
|
||||
useReducer,
|
||||
} from "react";
|
||||
|
||||
import { VideoFakeyou } from "components/common";
|
||||
import { VideoFakeyouProps } from "../VideoFakeyou/VideoFakeyou";
|
||||
import { QuickTrimData } from "./utilities";
|
||||
|
||||
import {
|
||||
reducer,
|
||||
initialState,
|
||||
ACTION_TYPES
|
||||
} from "./reducer";
|
||||
import {
|
||||
VideoElementContext
|
||||
} from "./contexts";
|
||||
|
||||
import { VideoTakeover } from "./components/VideoTakeover";
|
||||
import { ScrubberBar } from "./components/ScrubberBar";
|
||||
import { ControlBar } from "./components/ControlBar";
|
||||
|
||||
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 [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}>
|
||||
<div className="fy-video-quicktrim2">
|
||||
<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 />
|
||||
<ControlBar debug={debug} status={compState.status}/>
|
||||
</div>
|
||||
</VideoElementContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
+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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import React, {
|
||||
} from 'react';
|
||||
|
||||
// import { SelectionBubbles } from "components/common";
|
||||
import { STATE_STATUSES } from "../../reducer";
|
||||
import { ButtonPlaypause } from './ButtonPlaypause';
|
||||
import { LabelTimeDuration } from "./LabelTimeDuration"
|
||||
|
||||
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 />
|
||||
{/* <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})}
|
||||
/> */}
|
||||
<LabelTimeDuration />
|
||||
</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;
|
||||
}
|
||||
};
|
||||
|
||||
+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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ControlBar } from "./ControlBar";
|
||||
+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">
|
||||
<span className="loaded" style={{width: (buffered / vidEl.duration* 100) + "%"}} />
|
||||
<span className="played" style={{width: portionPlayed + "%"}} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else return null;
|
||||
}
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
|
||||
import { ProgressBar } from "./ProgressBar";
|
||||
|
||||
export const ScrubberBar = ({
|
||||
debug: propsDebug = false,
|
||||
status,
|
||||
}:{
|
||||
debug?: boolean;
|
||||
status: string;
|
||||
})=>{
|
||||
const debug = false || propsDebug;
|
||||
if (debug) console.log("reRENDERING ------ ScrubberBar");
|
||||
|
||||
return(
|
||||
<div className="scrubber-bar">
|
||||
<ProgressBar />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { ScrubberBar } from "./ScrubberBar";
|
||||
@@ -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,3 @@
|
||||
import {createContext} from 'react';
|
||||
|
||||
export const VideoElementContext = createContext<HTMLVideoElement | null>(null);
|
||||
@@ -0,0 +1,2 @@
|
||||
export { VideoPlayer } from './VideoPlayer'
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
@import "scss/colours.scss";
|
||||
|
||||
$scrubber-bar-height: 2rem;
|
||||
|
||||
.fy-video-quicktrim2{
|
||||
.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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
.scrubber-bar {
|
||||
position: relative;
|
||||
height: $scrubber-bar-height;
|
||||
width: 100%;
|
||||
|
||||
.progress-bar {
|
||||
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;
|
||||
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);
|
||||
}
|
||||
.trim-scrubber{
|
||||
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;
|
||||
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.fa-grip-dots {
|
||||
margin-top: -1rem;
|
||||
color: $panel-inner-border;
|
||||
}
|
||||
}
|
||||
.playcursor {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 0.5rem; //=8px
|
||||
transform: translate(-3px, 0px);
|
||||
height: $scrubber-bar-height;
|
||||
background-color: $white;
|
||||
border-radius: 1px;
|
||||
box-shadow: 1px 1px 4px 0px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.playpause-external {
|
||||
.button-playpause {
|
||||
width: 55px;
|
||||
height: 40px;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
.button-mute, .button-repeat{
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
.playtime{
|
||||
font-size: smaller;
|
||||
span{
|
||||
width: 50px;
|
||||
}
|
||||
div {
|
||||
width: 10px;
|
||||
margin-left: 3px;
|
||||
margin-right: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
//For integration into other components
|
||||
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;
|
||||
}
|
||||
|
||||
export function formatSecondsToHHMMSSCS(seconds:number){
|
||||
//example of the ISO String: 1970-01-01T00:01:40.774Z
|
||||
const isoString = new Date(seconds * 1000).toISOString();
|
||||
if(seconds > 3600)
|
||||
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,5 +1,5 @@
|
||||
import React, {
|
||||
memo,
|
||||
// memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useReducer,
|
||||
@@ -22,7 +22,10 @@ import {
|
||||
PLAYPUASE_STATES,
|
||||
STATE_STATUSES,
|
||||
} from "./reducer";
|
||||
import { VideoElementContext } from "./contexts";
|
||||
|
||||
import {
|
||||
VideoElementContext,
|
||||
} from "./contexts";
|
||||
|
||||
import { QuickTrimData } from "./utilities";
|
||||
|
||||
@@ -37,7 +40,7 @@ interface VideoQuickTrimProps extends VideoFakeyouProps {
|
||||
trimEndSeconds: number;
|
||||
}
|
||||
|
||||
export const VideoQuickTrim = memo(({
|
||||
export const VideoQuickTrim = ({
|
||||
debug: propsDebug = false,
|
||||
onSelect,
|
||||
trimStartSeconds: propsTrimStartSeconds,
|
||||
@@ -46,13 +49,14 @@ export const VideoQuickTrim = memo(({
|
||||
}: VideoQuickTrimProps) => {
|
||||
const debug = true || propsDebug;
|
||||
|
||||
const { t } = useLocalize("VideoPlayerQuickTrim");
|
||||
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){
|
||||
@@ -61,30 +65,29 @@ export const VideoQuickTrim = memo(({
|
||||
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.onloadedmetadata = ()=>{
|
||||
dispatchCompState({
|
||||
type: ACTION_TYPES.ON_LOADED_METADATA,
|
||||
payload:{ videoDuration: node.duration,}
|
||||
});
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
node.onprogress = ()=>{
|
||||
console.log(node.buffered);
|
||||
if(node.buffered.length > 0){
|
||||
dispatchCompState({
|
||||
type: ACTION_TYPES.SET_VIDEO_LOAD_PROGRESS,
|
||||
payload: {videoLoadProgress: node.buffered}
|
||||
});
|
||||
}
|
||||
}
|
||||
} // else{} DOM node referenced by ref has been unmounted
|
||||
}, [
|
||||
// No Dependency !
|
||||
]); //END videoRefCallback\
|
||||
@@ -172,47 +175,48 @@ export const VideoQuickTrim = memo(({
|
||||
</div>
|
||||
}
|
||||
</div>{/* END of Video Wrapper */}
|
||||
<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}
|
||||
videoBuffered={compState.videoLoadProgress || undefined}
|
||||
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 &&
|
||||
<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;
|
||||
}
|
||||
videoRef.current.currentTime = newTime;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</VideoElementContext.Provider>
|
||||
<ControlBar
|
||||
debug={debug}
|
||||
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={togglePlaypause}
|
||||
dispatchCompState={dispatchCompState}
|
||||
/>
|
||||
}}
|
||||
/>
|
||||
|
||||
<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>
|
||||
);
|
||||
});
|
||||
};
|
||||
+48
-24
@@ -1,4 +1,9 @@
|
||||
import React, { memo } from 'react'
|
||||
import React, {
|
||||
useState,
|
||||
useContext,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
faArrowsRepeat,
|
||||
faPlay,
|
||||
@@ -15,12 +20,11 @@ import {
|
||||
PLAYPUASE_STATES
|
||||
} from '../reducer';
|
||||
import { TRIM_OPTIONS, formatSecondsToHHMMSSCS } from "../utilities";
|
||||
import { VideoElementContext } from '../contexts';
|
||||
|
||||
export const ControlBar = memo(({
|
||||
export const ControlBar = ({
|
||||
debug: propsDebug = false,
|
||||
readyToMount,
|
||||
videoCurrentTime,
|
||||
videoDuration,
|
||||
isRepeatOn,
|
||||
isMuted,
|
||||
playpause,
|
||||
@@ -29,8 +33,6 @@ export const ControlBar = memo(({
|
||||
}:{
|
||||
debug?:boolean;
|
||||
readyToMount: boolean;
|
||||
videoCurrentTime: number | undefined;
|
||||
videoDuration: number | undefined;
|
||||
isMuted: boolean,
|
||||
isRepeatOn: boolean;
|
||||
playpause: string;
|
||||
@@ -38,8 +40,8 @@ export const ControlBar = memo(({
|
||||
dispatchCompState: (action: Action) => void;
|
||||
})=>{
|
||||
const debug = false;// || propsDebug;
|
||||
if (debug) console.log("ControlBar reRENDERED!!");
|
||||
|
||||
if (debug) console.log("reRENDERING ------ ControlBar");
|
||||
|
||||
function handleSetTrimDuration(selected: string){
|
||||
dispatchCompState({
|
||||
type: ACTION_TYPES.SET_TRIM_DURATION,
|
||||
@@ -72,21 +74,7 @@ export const ControlBar = memo(({
|
||||
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>
|
||||
<TimeLabel />
|
||||
</div>
|
||||
<SelectionBubbles
|
||||
options={Object.keys(TRIM_OPTIONS)}
|
||||
@@ -100,4 +88,40 @@ export const ControlBar = memo(({
|
||||
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>
|
||||
);
|
||||
}
|
||||
+15
-11
@@ -30,17 +30,21 @@ export const PlayCursor = ({
|
||||
);
|
||||
});
|
||||
useEffect(()=>{
|
||||
if(videoElement!==null){
|
||||
videoElement.ontimeupdate = ()=>{
|
||||
if(isRepeatOn && (
|
||||
videoElement.currentTime < playBoundStart ||
|
||||
videoElement.currentTime > playBoundEnd
|
||||
)){
|
||||
videoElement.currentTime = playBoundStart + ONE_MS
|
||||
}
|
||||
const newOffset = (videoElement.currentTime / videoElement.duration) * (rest.boundingWidth);
|
||||
setTimeCursorOffset(newOffset);
|
||||
};
|
||||
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]);
|
||||
|
||||
|
||||
+30
-12
@@ -1,5 +1,6 @@
|
||||
import React, {
|
||||
memo,
|
||||
useState,
|
||||
useEffect,
|
||||
useContext,
|
||||
useCallback,
|
||||
useLayoutEffect,
|
||||
@@ -11,8 +12,9 @@ import {
|
||||
} from '../reducer';
|
||||
import { TrimScrubber } from './TrimScrubber';
|
||||
import { PlayCursor } from './PlayCursor';
|
||||
import { VideoElementContext } from '../contexts';
|
||||
|
||||
export const ProgressBar = memo(({
|
||||
export const ProgressBar = ({
|
||||
debug: propsDebug = false,
|
||||
readyToMount,
|
||||
isRepeatOn,
|
||||
@@ -22,7 +24,6 @@ export const ProgressBar = memo(({
|
||||
playbarWidth,
|
||||
scrubberWidth,
|
||||
videoDuration,
|
||||
videoBuffered,
|
||||
onPlayCursorChanged,
|
||||
handlePlaypause,
|
||||
dispatchCompState
|
||||
@@ -36,14 +37,13 @@ export const ProgressBar = memo(({
|
||||
playbarWidth: number;
|
||||
scrubberWidth: number;
|
||||
videoDuration: number;
|
||||
videoBuffered: TimeRanges | undefined;
|
||||
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 => {
|
||||
@@ -81,12 +81,7 @@ export const ProgressBar = memo(({
|
||||
// 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>
|
||||
<Progress />
|
||||
<TrimScrubber
|
||||
debug={debug}
|
||||
boundingWidth={playbarWidth}
|
||||
@@ -121,4 +116,27 @@ export const ProgressBar = memo(({
|
||||
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;
|
||||
}
|
||||
+10
-9
@@ -34,7 +34,7 @@ export const withScrubbing = <P extends withScrubbingPropsI>(Component: React.Co
|
||||
onScrubEnd,
|
||||
...rest
|
||||
}: withScrubbingPropsI) => {
|
||||
const debug = false; //|| propsDebug;
|
||||
const debug = true; //|| propsDebug;
|
||||
|
||||
|
||||
const refEl = useRef<HTMLDivElement| null>(null);
|
||||
@@ -63,8 +63,8 @@ export const withScrubbing = <P extends withScrubbingPropsI>(Component: React.Co
|
||||
...curr,
|
||||
pointerStartPos: e.clientX
|
||||
}));
|
||||
window.addEventListener("mouseup", handleScrubEnd);
|
||||
window.addEventListener("mousemove", handleScrubMove);
|
||||
|
||||
window.addEventListener("pointermove", handleScrubMove);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -73,8 +73,8 @@ export const withScrubbing = <P extends withScrubbingPropsI>(Component: React.Co
|
||||
if (debug) console.log(`${refListener.current} withSCRUBBING SCRUB_END!! `);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
window.removeEventListener("mouseup", handleScrubEnd);
|
||||
window.removeEventListener("mousemove", handleScrubMove);
|
||||
|
||||
window.removeEventListener("pointermove", handleScrubMove);
|
||||
setStates((curr)=>({
|
||||
...curr,
|
||||
pointerStartPos: -1,
|
||||
@@ -108,12 +108,13 @@ 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("mousedown", handleScrubStart);
|
||||
window.addEventListener("pointerdown", handleScrubStart);
|
||||
window.addEventListener("pointerup", handleScrubEnd);
|
||||
return () => {
|
||||
(window as any)[`listener-id-${refListener.current}`] = false;
|
||||
window.removeEventListener("mousedown", handleScrubStart);
|
||||
window.removeEventListener("mouseup", handleScrubEnd);
|
||||
window.removeEventListener("mousemove", handleScrubMove);
|
||||
window.removeEventListener("pointerdown", handleScrubStart);
|
||||
window.removeEventListener("pointerup", handleScrubEnd);
|
||||
window.removeEventListener("pointermove", handleScrubMove);
|
||||
};
|
||||
}
|
||||
}, [scrubberWidth, boundingWidth]);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
MIN_VID_DURATION,
|
||||
TIME_CURSOR_WIDTH,
|
||||
TRIM_OPTIONS,
|
||||
roundToMilliseconds
|
||||
} from "./utilities";
|
||||
|
||||
@@ -71,6 +71,7 @@ $playbar-height: 2rem;
|
||||
position: relative;
|
||||
height: $playbar-height;
|
||||
width: 100%;
|
||||
margin-top: 7px;
|
||||
}
|
||||
.playbar-bg {
|
||||
position: absolute;
|
||||
@@ -94,6 +95,7 @@ $playbar-height: 2rem;
|
||||
}
|
||||
.loaded{
|
||||
background-color: $gray-600;
|
||||
transition: width 0.5s;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import VideoQuickTrim from 'components/common/VideoQuickTrim';
|
||||
import VideoPlayer from 'components/common/VideoPlayer';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import {Container, Panel} from 'components/common';
|
||||
@@ -21,10 +21,10 @@ export default function ComponentSandbox(
|
||||
<Panel>
|
||||
|
||||
<div className="m-4">
|
||||
<VideoQuickTrim
|
||||
trimStartSeconds={Number(state1 ||0)}
|
||||
trimEndSeconds={Number(state2 || 0)}
|
||||
onSelect={handleCallback}
|
||||
<VideoPlayer
|
||||
// trimStartSeconds={Number(state1 ||0)}
|
||||
// trimEndSeconds={Number(state2 || 0)}
|
||||
// onSelectTrim={handleCallback}
|
||||
mediaToken='m_wdptbe5rfzpb1gzhva78gpw8qrkgs9'
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user