aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/RecordingBox
diff options
context:
space:
mode:
authorljungster <parkerljung@gmail.com>2022-08-09 11:52:07 -0500
committerljungster <parkerljung@gmail.com>2022-08-09 11:52:07 -0500
commitda3cb00f809a482a9fdf732f6a656fbc467cce27 (patch)
tree9eb1fd278bc71d080d71bbfb7e3aec482d35f439 /src/client/views/nodes/RecordingBox
parent1638527259a072dfc2ab286bd27bbb1751e8434e (diff)
parent26670c8b9eb6e2fd981c3a0997bff5556b60504b (diff)
Merge branch 'parker' of https://github.com/brown-dash/Dash-Web into parker
Diffstat (limited to 'src/client/views/nodes/RecordingBox')
-rw-r--r--src/client/views/nodes/RecordingBox/ProgressBar.scss123
-rw-r--r--src/client/views/nodes/RecordingBox/ProgressBar.tsx301
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingBox.tsx58
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingView.scss214
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingView.tsx271
-rw-r--r--src/client/views/nodes/RecordingBox/index.ts2
6 files changed, 969 insertions, 0 deletions
diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.scss b/src/client/views/nodes/RecordingBox/ProgressBar.scss
new file mode 100644
index 000000000..28ad25ffa
--- /dev/null
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.scss
@@ -0,0 +1,123 @@
+
+.progressbar {
+ touch-action: none;
+ vertical-align: middle;
+ text-align: center;
+
+ align-items: center;
+ cursor: default;
+
+
+ position: absolute;
+ display: flex;
+ justify-content: flex-start;
+ bottom: 2px;
+ width: 99%;
+ height: 30px;
+ background-color: gray;
+
+ &.done {
+ top: 0;
+ width: 0px;
+ height: 5px;
+ background-color: red;
+ z-index: 2;
+ }
+
+ &.mark {
+ top: 0;
+ background-color: transparent;
+ border-right: 2px solid white;
+ z-index: 3;
+ pointer-events: none;
+ }
+}
+
+.progressbar-disabled {
+ cursor: not-allowed;
+}
+
+.progressbar-dragging {
+ cursor: grabbing;
+}
+
+// citation: https://codepen.io/_Master_/pen/PRdjmQ
+@keyframes blinker {
+ from {opacity: 1.0;}
+ to {opacity: 0.0;}
+}
+.blink {
+ text-decoration: blink;
+ animation-name: blinker;
+ animation-duration: 0.6s;
+ animation-iteration-count:infinite;
+ animation-timing-function:ease-in-out;
+ animation-direction: alternate;
+}
+
+.segment {
+ border: 3px solid black;
+ background-color: red;
+ margin: 1px;
+ padding: 0;
+ cursor: pointer;
+ transition-duration: .5s;
+ user-select: none;
+
+ vertical-align: middle;
+ text-align: center;
+}
+
+.segment-expanding {
+border-color: red;
+ background-color: white;
+ transition-duration: 0s;
+ opacity: .75;
+ pointer-events: none;
+}
+
+.segment-expanding:hover {
+ background-color: inherit;
+ cursor: not-allowed;
+}
+
+.segment-disabled {
+ pointer-events: none;
+ opacity: 0.5;
+ transition-duration: 0s;
+ /* Hide the text. */
+ text-indent: 100%;
+ white-space: nowrap;
+ overflow: hidden;
+}
+
+.segment-hide {
+ background-color: inherit;
+ text-align: center;
+ vertical-align: middle;
+ user-select: none;
+}
+
+.segment:first-child {
+ margin-left: 2px;
+}
+.segment:last-child {
+ margin-right: 2px;
+}
+
+.segment:hover {
+ background-color: white;
+}
+
+.segment:hover, .segment-selected {
+ margin: 0px;
+ border: 4px solid red;
+ border-radius: 2px;
+}
+
+.segment-selected {
+ border: 4px solid #202020;
+ background-color: red;
+ opacity: .75;
+ cursor: grabbing;
+}
diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
new file mode 100644
index 000000000..1bb2b7c84
--- /dev/null
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
@@ -0,0 +1,301 @@
+import * as React from 'react';
+import { useEffect, useState, useCallback, useRef } from "react"
+import "./ProgressBar.scss"
+import { MediaSegment } from './RecordingView';
+
+interface ProgressBarProps {
+ videos: MediaSegment[],
+ setVideos: React.Dispatch<React.SetStateAction<MediaSegment[]>>,
+ orderVideos: boolean,
+ progress: number,
+ recording: boolean,
+ doUndo: boolean,
+ setCanUndo?: React.Dispatch<React.SetStateAction<boolean>>,
+}
+
+interface SegmentBox {
+ endTime: number,
+ startTime: number,
+ order: number,
+}
+interface CurrentHover {
+ index: number,
+ minX: number,
+ maxX: number
+}
+
+export function ProgressBar(props: ProgressBarProps) {
+ const progressBarRef = useRef<HTMLDivElement | null>(null)
+
+ // the actual list of JSX elements rendered as segments
+ const [segments, setSegments] = useState<JSX.Element[]>([]);
+ // array for the order of video segments
+ const [ordered, setOrdered] = useState<SegmentBox[]>([]);
+
+ const [undoStack, setUndoStack] = useState<SegmentBox[]>([]);
+
+ // -1 if no segment is currently being dragged around; else, it is the id of that segment over
+ // NOTE: the id of a segment is its index in the ordered array
+ const [dragged, setDragged] = useState<number>(-1);
+
+ // length of the time removed from the video, in seconds*100
+ const [totalRemovedTime, setTotalRemovedTime] = useState<number>(0);
+
+ // this holds the index of the videoc segment to be removed
+ const [removed, setRemoved] = useState<number>(-1);
+
+ // update the canUndo props based on undo stack
+ useEffect(() => props.setCanUndo?.(undoStack.length > 0), [undoStack.length]);
+
+ // useEffect for undo - brings back the most recently deleted segment
+ useEffect(() => handleUndo(), [props.doUndo])
+ const handleUndo = () => {
+ // get the last element from the undo if it exists
+ if (undoStack.length === 0) return;
+ // get and remove the last element from the undo stack
+ const last = undoStack.lastElement();
+ setUndoStack(prevUndo => prevUndo.slice(0, -1));
+
+ // update the removed time and place element back into ordered
+ setTotalRemovedTime(prevRemoved => prevRemoved - (last.endTime - last.startTime));
+ setOrdered(prevOrdered => [...prevOrdered, last]);
+ }
+
+ // useEffect for recording changes - changes style to disabled and adds the "expanding-segment"
+ useEffect(() => {
+ // get segments segment's html using it's id -> make them appeared disabled (or enabled)
+ segments.forEach((seg) => document.getElementById(seg.props.id)?.classList.toggle('segment-disabled', props.recording));
+ progressBarRef.current?.classList.toggle('progressbar-disabled', props.recording);
+
+ if (props.recording)
+ setSegments(prevSegments => [...prevSegments, <div key='segment-expanding' id='segment-expanding' className='segment segment-expanding blink' style={{ width: 'fit-content' }}>{props.videos.length + 1}</div>]);
+ }, [props.recording])
+
+
+ // useEffect that updates the segmentsJSX, which is rendered
+ // only updated when ordered is updated or if the user is dragging around a segment
+ useEffect(() => {
+ const totalTime = props.progress * 1000 - totalRemovedTime;
+ const segmentsJSX = ordered.map((seg, i) =>
+ <div key={`segment-${i}`} id={`segment-${i}`} className={dragged === i ? 'segment-hide' : 'segment'} style={{ width: `${((seg.endTime - seg.startTime) / totalTime) * 100}%` }}>{seg.order + 1}</div>);
+
+ setSegments(segmentsJSX)
+ }, [dragged, ordered]);
+
+ // useEffect for dragged - update the cursor to be grabbing while grabbing
+ useEffect(() => {
+ progressBarRef.current?.classList.toggle('progressbar-dragging', dragged !== -1);
+ }, [dragged]);
+
+ // to imporve performance, only want to update the CSS width, not re-render the whole JSXList
+ useEffect(() => {
+ if (!props.recording) return
+ const totalTime = props.progress * 1000 - totalRemovedTime;
+ let remainingTime = totalTime;
+ segments.forEach((seg, i) => {
+ // for the last segment, we need to set that directly
+ if (i === segments.length - 1) return;
+ // update remaining time
+ remainingTime -= (ordered[i].endTime - ordered[i].startTime);
+
+ // update the width for this segment
+ const htmlId = seg.props.id;
+ const segmentHtml = document.getElementById(htmlId);
+ if (segmentHtml) segmentHtml.style.width = `${((ordered[i].endTime - ordered[i].startTime) / totalTime) * 100}%`;
+ });
+
+ // update the width of the expanding segment using the remaining time
+ const segExapandHtml = document.getElementById('segment-expanding');
+ if (segExapandHtml)
+ segExapandHtml.style.width = ordered.length === 0 ? '100%' : `${(remainingTime / totalTime) * 100}%`;
+ }, [props.progress]);
+
+ // useEffect for props.videos - update the ordered array when a new video is added
+ useEffect(() => {
+ // this useEffect fired when the videos are being rearragned to the order
+ // in this case, do nothing.
+ if (props.orderVideos) return;
+
+ const order = props.videos.length - 1;
+ // in this case, a new video is added -> push it onto ordered
+ if (order >= ordered.length) {
+ const { endTime, startTime } = props.videos.lastElement();
+ setOrdered(prevOrdered => {
+ return [...prevOrdered, { endTime, startTime, order }];
+ });
+ }
+
+ // in this case, a video is removed
+ else if (order < ordered.length) {
+ console.warn('warning: video removed from parent');
+ }
+ }, [props.videos]);
+
+ // useEffect for props.orderVideos - matched the order array with the videos array before the export
+ useEffect(() => props.setVideos(vids => ordered.map((seg) => vids[seg.order])), [props.orderVideos]);
+
+ // useEffect for removed - handles logic for removing a segment
+ useEffect(() => {
+ if (removed === -1) return;
+ // update total removed time
+ setTotalRemovedTime(prevRemoved => prevRemoved + (ordered[removed].endTime - ordered[removed].startTime));
+
+ // put the element on the undo stack
+ setUndoStack(prevUndo => [...prevUndo, ordered[removed]]);
+ // remove the segment from the array
+ setOrdered(prevOrdered => prevOrdered.filter((seg, i) => i !== removed));
+ // reset to default/nullish state
+ setRemoved(-1);
+ }, [removed]);
+
+ // returns the new currentHover based on the new index
+ const updateCurrentHover = (segId: number): CurrentHover | null => {
+ // get the segId of the segment that will become the new bounding area
+ const rect = progressBarRef.current?.children[segId].getBoundingClientRect()
+ if (rect == null) return null
+ return {
+ index: segId,
+ minX: rect.x,
+ maxX: rect.x + rect.width,
+ }
+ }
+
+ // pointerdown event for the progress bar
+ const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
+ // don't move the videobox element
+ e.stopPropagation();
+
+ // if recording, do nothing
+ if (props.recording) return;
+
+ // get the segment the user clicked on to be dragged
+ const clickedSegment = e.target as HTMLDivElement & EventTarget
+
+ // get the profess bar ro add event listeners
+ // don't do anything if null
+ const progressBar = progressBarRef.current
+ if (progressBar == null || clickedSegment.id === progressBar.id) return
+
+ // if holding shift key, let's remove that segment
+ if (e.shiftKey) {
+ const segId = parseInt(clickedSegment.id.split('-')[1]);
+ setRemoved(segId);
+ return
+ }
+
+ // if holding ctrl key and click, let's undo that segment #hiddenfeature lol
+ if (e.ctrlKey) {
+ handleUndo();
+ return;
+ }
+
+ // if we're here, the user is dragging a segment around
+ // let the progress bar capture all the pointer events until the user releases (pointerUp)
+ const ptrId = e.pointerId;
+ progressBar.setPointerCapture(ptrId)
+
+ const rect = clickedSegment.getBoundingClientRect()
+ // id for segment is like 'segment-1' or 'segment-10',
+ // so this works to get the id
+ const segId = parseInt(clickedSegment.id.split('-')[1])
+ // set the selected segment to be the one dragged
+ setDragged(segId)
+
+ // this is the logic for storing the lower X bound and upper X bound to know
+ // whether a swap is needed between two segments
+ let currentHover: CurrentHover = {
+ index: segId,
+ minX: rect.x,
+ maxX: rect.x + rect.width,
+ }
+
+ // create the floating segment that tracks the cursor
+ const detchedSegment = document.createElement("div")
+ initDeatchSegment(detchedSegment, rect);
+
+ const updateSegmentOrder = (event: PointerEvent): void => {
+ event.stopPropagation();
+ event.preventDefault();
+
+ // this fixes a bug where pointerup doesn't fire while cursor is upped while being dragged
+ if (!progressBar.hasPointerCapture(ptrId)) {
+ placeSegmentandCleanup();
+ return;
+ }
+
+ followCursor(event, detchedSegment);
+
+ const curX = event.clientX;
+ // handle the left bound
+ if (curX < currentHover.minX && currentHover.index > 0) {
+ swapSegments(currentHover.index, currentHover.index - 1)
+ currentHover = updateCurrentHover(currentHover.index - 1) ?? currentHover
+ }
+ // handle the right bound
+ else if (curX > currentHover.maxX && currentHover.index < segments.length - 1) {
+ swapSegments(currentHover.index, currentHover.index + 1)
+ currentHover = updateCurrentHover(currentHover.index + 1) ?? currentHover
+ }
+ }
+
+ // handles when the user is done dragging the segment (pointerUp)
+ const placeSegmentandCleanup = (event?: PointerEvent): void => {
+ event?.stopPropagation();
+ event?.preventDefault();
+ // if they put the segment outside of the bounds, remove it
+ if (event && (event.clientX < 0 || event.clientX > document.body.clientWidth || event.clientY < 0 || event.clientY > document.body.clientHeight))
+ setRemoved(currentHover.index);
+
+ // remove the update event listener for pointermove
+ progressBar.removeEventListener('pointermove', updateSegmentOrder);
+ // remove the floating segment from the DOM
+ detchedSegment.remove();
+ // dragged is -1 is equiv to nothing being dragged, so the normal state
+ // so this will place the segment in it's location and update the segment bar
+ setDragged(-1);
+ }
+
+ // event listeners that allow the user to drag and release the floating segment
+ progressBar.addEventListener('pointermove', updateSegmentOrder);
+ progressBar.addEventListener('pointerup', placeSegmentandCleanup, { once: true });
+ }
+
+ const swapSegments = (oldIndex: number, newIndex: number) => {
+ if (newIndex == null) return;
+ setOrdered(prevOrdered => {
+ const temp = { ...prevOrdered[oldIndex] }
+ prevOrdered[oldIndex] = prevOrdered[newIndex]
+ prevOrdered[newIndex] = temp
+ return prevOrdered
+ });
+ // update visually where the segment is hovering over
+ setDragged(newIndex);
+ }
+
+ // functions for the floating segment that tracks the cursor while grabbing it
+ const initDeatchSegment = (dot: HTMLDivElement, rect: DOMRect) => {
+ dot.classList.add("segment-selected");
+ dot.style.transitionDuration = '0s';
+ dot.style.position = 'absolute';
+ dot.style.zIndex = '999';
+ dot.style.width = `${rect.width}px`;
+ dot.style.height = `${rect.height}px`;
+ dot.style.left = `${rect.x}px`;
+ dot.style.top = `${rect.y}px`;
+ dot.draggable = false;
+ document.body.append(dot);
+ }
+ const followCursor = (event: PointerEvent, dot: HTMLDivElement): void => {
+ // event.stopPropagation()
+ const { width, height } = dot.getBoundingClientRect();
+ dot.style.left = `${event.clientX - width / 2}px`;
+ dot.style.top = `${event.clientY - height / 2}px`;
+ }
+
+
+ return (
+ <div className="progressbar" id="progressbar" onPointerDown={onPointerDown} ref={progressBarRef}>
+ {segments}
+ </div>
+ )
+} \ No newline at end of file
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
new file mode 100644
index 000000000..0ff7c4292
--- /dev/null
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -0,0 +1,58 @@
+import { action, observable } from "mobx";
+import { observer } from "mobx-react";
+import * as React from "react";
+import { VideoField } from "../../../../fields/URLField";
+import { Upload } from "../../../../server/SharedMediaTypes";
+import { ViewBoxBaseComponent } from "../../DocComponent";
+import { FieldView } from "../FieldView";
+import { VideoBox } from "../VideoBox";
+import { RecordingView } from './RecordingView';
+import { DocumentType } from "../../../documents/DocumentTypes";
+import { Presentation } from "../../../util/TrackMovements";
+import { Doc } from "../../../../fields/Doc";
+import { Id } from "../../../../fields/FieldSymbols";
+
+
+@observer
+export class RecordingBox extends ViewBoxBaseComponent() {
+
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(RecordingBox, fieldKey); }
+
+ private _ref: React.RefObject<HTMLDivElement> = React.createRef();
+
+ constructor(props: any) {
+ super(props);
+ }
+
+ componentDidMount() {
+ Doc.SetNativeWidth(this.dataDoc, 1280);
+ Doc.SetNativeHeight(this.dataDoc, 720);
+ }
+
+ @observable result: Upload.AccessPathInfo | undefined = undefined
+ @observable videoDuration: number | undefined = undefined
+
+ @action
+ setVideoDuration = (duration: number) => {
+ this.videoDuration = duration
+ }
+
+ @action
+ setResult = (info: Upload.AccessPathInfo, presentation?: Presentation) => {
+ this.result = info
+ this.dataDoc.type = DocumentType.VID;
+ this.dataDoc[this.fieldKey + "-duration"] = this.videoDuration;
+
+ this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey);
+ this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.client);
+ this.dataDoc[this.fieldKey + "-recorded"] = true;
+ // stringify the presentation and store it
+ presentation?.movements && (this.dataDoc[this.fieldKey + "-presentation"] = JSON.stringify(presentation));
+ }
+
+ render() {
+ return <div className="recordingBox" ref={this._ref}>
+ {!this.result && <RecordingView setResult={this.setResult} setDuration={this.setVideoDuration} id={this.rootDoc.proto?.[Id] || ''} />}
+ </div>;
+ }
+}
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.scss b/src/client/views/nodes/RecordingBox/RecordingView.scss
new file mode 100644
index 000000000..2e6f6bc26
--- /dev/null
+++ b/src/client/views/nodes/RecordingBox/RecordingView.scss
@@ -0,0 +1,214 @@
+video {
+ // flex: 100%;
+ width: 100%;
+ // min-height: 400px;
+ //height: auto;
+ height: 100%;
+ //display: block;
+ object-fit: cover;
+ background-color: black;
+}
+
+button {
+ margin: 0 .5rem
+}
+
+.recording-container {
+ height: 100%;
+ width: 100%;
+ // display: flex;
+ pointer-events: all;
+ background-color: black;
+}
+
+.video-wrapper {
+ // max-width: 600px;
+ // max-width: 700px;
+ // position: relative;
+ display: flex;
+ justify-content: center;
+ // overflow: hidden;
+ border-radius: 10px;
+ margin: 0;
+}
+
+.video-wrapper:hover .controls {
+ bottom: 34.5px;
+ transform: translateY(0%);
+ opacity: 100%;
+}
+
+.controls {
+ display: flex;
+ align-items: center;
+ justify-content: space-evenly;
+ position: absolute;
+ // padding: 14px;
+ //width: 100%;
+ max-width: 500px;
+ // max-height: 20%;
+ flex-wrap: wrap;
+ background: rgba(255, 255, 255, 0.25);
+ box-shadow: 0 8px 32px 0 rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(4px);
+ border-radius: 10px;
+ border: 1px solid rgba(255, 255, 255, 0.18);
+ // transform: translateY(150%);
+ transition: all 0.3s ease-in-out;
+ // opacity: 0%;
+ bottom: 34.5px;
+ height: 60px;
+ right: 2px;
+ // bottom: -150px;
+}
+
+.controls:active {
+ bottom: 40px;
+ // bottom: -150px;
+}
+
+.actions button {
+ background: none;
+ border: none;
+ outline: none;
+ cursor: pointer;
+}
+
+.actions button i {
+ background-color: none;
+ color: white;
+ font-size: 30px;
+}
+
+
+.velocity {
+ appearance: none;
+ background: none;
+ color: white;
+ outline: none;
+ border: none;
+ text-align: center;
+ font-size: 16px;
+}
+
+.mute-btn {
+ background: none;
+ border: none;
+ outline: none;
+ cursor: pointer;
+}
+
+.mute-btn i {
+ background-color: none;
+ color: white;
+ font-size: 20px;
+}
+
+.recording-sign {
+ height: 20px;
+ width: auto;
+ display: flex;
+ flex-direction: row;
+ position: absolute;
+ top: 10px;
+ right: 15px;
+ align-items: center;
+ justify-content: center;
+
+ .timer {
+ font-size: 15px;
+ color: white;
+ margin: 0;
+ }
+
+ .dot {
+ height: 15px;
+ width: 15px;
+ margin: 5px;
+ background-color: red;
+ border-radius: 50%;
+ display: inline-block;
+ }
+}
+
+.controls-inner-container {
+ display: flex;
+ flex-direction: row;
+ align-content: center;
+ position: relative;
+}
+
+.record-button-wrapper {
+ width: 35px;
+ height: 35px;
+ font-size: 0;
+ background-color: grey;
+ border: 0px;
+ border-radius: 35px;
+ margin: 10px;
+ display: flex;
+ justify-content: center;
+
+ .record-button {
+ background-color: red;
+ border: 0px;
+ border-radius: 50%;
+ height: 80%;
+ width: 80%;
+ align-self: center;
+ margin: 0;
+
+ &:hover {
+ height: 85%;
+ width: 85%;
+ }
+ }
+
+ .stop-button {
+ background-color: red;
+ border: 0px;
+ border-radius: 10%;
+ height: 70%;
+ width: 70%;
+ align-self: center;
+ margin: 0;
+
+
+ // &:hover {
+ // width: 40px;
+ // height: 40px
+ // }
+ }
+
+}
+
+.options-wrapper {
+ height: 100%;
+ display: flex;
+ flex-direction: row;
+ align-content: center;
+ position: relative;
+ top: 0;
+ bottom: 0;
+
+ &.video-edit-wrapper {
+
+ // right: 50% - 15;
+
+ .track-screen {
+ font-weight: 200;
+ }
+
+ }
+
+ &.track-screen-wrapper {
+
+ // right: 50% - 30;
+
+ .track-screen {
+ font-weight: 200;
+ color: aqua;
+ }
+
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
new file mode 100644
index 000000000..ec5917b9e
--- /dev/null
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -0,0 +1,271 @@
+import * as React from 'react';
+import "./RecordingView.scss";
+import { useEffect, useRef, useState } from "react";
+import { ProgressBar } from "./ProgressBar"
+import { MdBackspace } from 'react-icons/md';
+import { FaCheckCircle } from 'react-icons/fa';
+import { IconContext } from "react-icons";
+import { Networking } from '../../../Network';
+import { Upload } from '../../../../server/SharedMediaTypes';
+import { returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils';
+import { Presentation, TrackMovements } from '../../../util/TrackMovements';
+
+export interface MediaSegment {
+ videoChunks: any[],
+ endTime: number,
+ startTime: number,
+ presentation?: Presentation,
+}
+
+interface IRecordingViewProps {
+ setResult: (info: Upload.AccessPathInfo, presentation?: Presentation) => void
+ setDuration: (seconds: number) => void
+ id: string
+}
+
+const MAXTIME = 100000;
+
+export function RecordingView(props: IRecordingViewProps) {
+
+ const [recording, setRecording] = useState(false);
+ const recordingTimerRef = useRef<number>(0);
+ const [recordingTimer, setRecordingTimer] = useState(0); // unit is 0.01 second
+ const [playing, setPlaying] = useState(false);
+ const [progress, setProgress] = useState(0);
+
+ // acts as a "refresh state" to tell progressBar when to undo
+ const [doUndo, setDoUndo] = useState(false);
+ // whether an undo can occur or not
+ const [canUndo, setCanUndo] = useState(false);
+
+ const [videos, setVideos] = useState<MediaSegment[]>([]);
+ const [orderVideos, setOrderVideos] = useState<boolean>(false);
+ const videoRecorder = useRef<MediaRecorder | null>(null);
+ const videoElementRef = useRef<HTMLVideoElement | null>(null);
+
+ const [finished, setFinished] = useState<boolean>(false);
+ const [trackScreen, setTrackScreen] = useState<boolean>(false);
+
+
+
+ const DEFAULT_MEDIA_CONSTRAINTS = {
+ video: {
+ width: 1280,
+ height: 720,
+
+ },
+ audio: {
+ echoCancellation: true,
+ noiseSuppression: true,
+ sampleRate: 44100
+ }
+ };
+
+ useEffect(() => {
+ if (finished) {
+ // make the total presentation that'll match the concatted video
+ let concatPres = trackScreen && TrackMovements.Instance.concatPresentations(videos.map(v => v.presentation as Presentation));
+
+ // this async function uses the server to create the concatted video and then sets the result to it's accessPaths
+ (async () => {
+ const videoFiles = videos.map((vid, i) => new File(vid.videoChunks, `segvideo${i}.mkv`, { type: vid.videoChunks[0].type, lastModified: Date.now() }));
+
+ // upload the segments to the server and get their server access paths
+ const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles))
+ .map(res => (res.result instanceof Error) ? '' : res.result.accessPaths.agnostic.server)
+
+ // concat the segments together using post call
+ const result: Upload.AccessPathInfo | Error = await Networking.PostToServer('/concatVideos', serverPaths);
+ !(result instanceof Error) ? props.setResult(result, concatPres || undefined) : console.error("video conversion failed");
+ })();
+ }
+ }, [videos]);
+
+ // this will call upon the progress bar to edit videos to be in the correct order
+ useEffect(() => {
+ finished && setOrderVideos(true);
+ }, [finished]);
+
+ // check if the browser supports media devices on first load
+ useEffect(() => { if (!navigator.mediaDevices) alert('This browser does not support getUserMedia.'); }, []);
+
+ useEffect(() => {
+ let interval: any = null;
+ if (recording) {
+ interval = setInterval(() => {
+ setRecordingTimer(unit => unit + 1);
+ }, 10);
+ } else if (!recording && recordingTimer !== 0) {
+ clearInterval(interval);
+ }
+ return () => clearInterval(interval);
+ }, [recording]);
+
+ useEffect(() => {
+ setVideoProgressHelper(recordingTimer)
+ recordingTimerRef.current = recordingTimer;
+ }, [recordingTimer]);
+
+ const setVideoProgressHelper = (progress: number) => {
+ const newProgress = (progress / MAXTIME) * 100;
+ setProgress(newProgress);
+ }
+
+ const startShowingStream = async (mediaConstraints = DEFAULT_MEDIA_CONSTRAINTS) => {
+ const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
+
+ videoElementRef.current!.src = "";
+ videoElementRef.current!.srcObject = stream;
+ videoElementRef.current!.muted = true;
+
+ return stream;
+ }
+
+ const record = async () => {
+ // don't need to start a new stream every time we start recording a new segment
+ if (!videoRecorder.current) videoRecorder.current = new MediaRecorder(await startShowingStream());
+
+ // temporary chunks of video
+ let videoChunks: any = [];
+
+ videoRecorder.current.ondataavailable = (event: any) => {
+ if (event.data.size > 0) videoChunks.push(event.data);
+ };
+
+ videoRecorder.current.onstart = (event: any) => {
+ setRecording(true);
+ // start the recording api when the video recorder starts
+ trackScreen && TrackMovements.Instance.start();
+ };
+
+ videoRecorder.current.onstop = () => {
+ // if we have a last portion
+ if (videoChunks.length > 1) {
+ // append the current portion to the video pieces
+ const nextVideo = {
+ videoChunks,
+ endTime: recordingTimerRef.current,
+ startTime: videos?.lastElement()?.endTime || 0
+ };
+
+ // depending on if a presenation exists, add it to the video
+ const presentation = TrackMovements.Instance.yieldPresentation();
+ setVideos(videos => [...videos, (presentation != null && trackScreen) ? { ...nextVideo, presentation } : nextVideo]);
+ }
+
+ // reset the temporary chunks
+ videoChunks = [];
+ setRecording(false);
+ }
+
+ videoRecorder.current.start(200);
+ }
+
+
+ // if this is called, then we're done recording all the segments
+ const finish = (e: React.PointerEvent) => {
+ e.stopPropagation();
+
+ // call stop on the video recorder if active
+ videoRecorder.current?.state !== "inactive" && videoRecorder.current?.stop();
+
+ // end the streams (audio/video) to remove recording icon
+ const stream = videoElementRef.current!.srcObject;
+ stream instanceof MediaStream && stream.getTracks().forEach(track => track.stop());
+
+ // finish/clear the recoringApi
+ TrackMovements.Instance.finish();
+
+ // this will call upon progessbar to update videos to be in the correct order
+ setFinished(true);
+ }
+
+ const pause = (e: React.PointerEvent) => {
+ e.stopPropagation();
+ // if recording, then this is just a new segment
+ videoRecorder.current?.state === "recording" && videoRecorder.current.stop();
+ }
+
+ const start = (e: React.PointerEvent) => {
+ setupMoveUpEvents({}, e, returnTrue, returnFalse, e => {
+ // start recording if not already recording
+ if (!videoRecorder.current || videoRecorder.current.state === "inactive") record();
+
+ return true; // cancels propagation to documentView to avoid selecting it.
+ }, false, false);
+ }
+
+ const undoPrevious = (e: React.PointerEvent) => {
+ e.stopPropagation();
+ setDoUndo(prev => !prev);
+ }
+
+ const handleOnTimeUpdate = () => { playing && setVideoProgressHelper(videoElementRef.current!.currentTime); };
+
+ const millisecondToMinuteSecond = (milliseconds: number) => {
+ const toTwoDigit = (digit: number) => {
+ return String(digit).length == 1 ? "0" + digit : digit
+ }
+ const minutes = Math.floor((milliseconds % (1000 * 60 * 60)) / (1000 * 60));
+ const seconds = Math.floor((milliseconds % (1000 * 60)) / 1000);
+ return toTwoDigit(minutes) + " : " + toTwoDigit(seconds);
+ }
+
+ return (
+ <div className="recording-container">
+ <div className="video-wrapper">
+ <video id={`video-${props.id}`}
+ autoPlay
+ muted
+ onTimeUpdate={() => handleOnTimeUpdate()}
+ ref={videoElementRef}
+ />
+ <div className="recording-sign">
+ <span className="dot" />
+ <p className="timer">{millisecondToMinuteSecond(recordingTimer * 10)}</p>
+ </div>
+ <div className="controls">
+
+ <div className="controls-inner-container">
+ <div className="record-button-wrapper">
+ {recording ?
+ <button className="stop-button" onPointerDown={pause} /> :
+ <button className="record-button" onPointerDown={start} />
+ }
+ </div>
+
+ {!recording && (videos.length > 0 ?
+
+ <div className="options-wrapper video-edit-wrapper">
+ <IconContext.Provider value={{ color: "grey", className: "video-edit-buttons", style: { display: canUndo ? 'inherit' : 'none' } }}>
+ <MdBackspace onPointerDown={undoPrevious} />
+ </IconContext.Provider>
+ <IconContext.Provider value={{ color: "#cc1c08", className: "video-edit-buttons" }}>
+ <FaCheckCircle onPointerDown={finish} />
+ </IconContext.Provider>
+ </div>
+
+ : <div className="options-wrapper track-screen-wrapper">
+ <label className="track-screen">
+ <input type="checkbox" checked={trackScreen} onChange={(e) => { setTrackScreen(e.target.checked) }} />
+ <span className="checkmark"></span>
+ Track Screen
+ </label>
+ </div>)}
+
+ </div>
+
+ </div>
+
+ <ProgressBar
+ videos={videos}
+ setVideos={setVideos}
+ orderVideos={orderVideos}
+ progress={progress}
+ recording={recording}
+ doUndo={doUndo}
+ setCanUndo={setCanUndo}
+ />
+ </div>
+ </div>)
+} \ No newline at end of file
diff --git a/src/client/views/nodes/RecordingBox/index.ts b/src/client/views/nodes/RecordingBox/index.ts
new file mode 100644
index 000000000..ff21eaed6
--- /dev/null
+++ b/src/client/views/nodes/RecordingBox/index.ts
@@ -0,0 +1,2 @@
+export * from './RecordingView'
+export * from './RecordingBox' \ No newline at end of file