aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorgeireann <geireann.lindfield@gmail.com>2022-04-07 18:06:40 -0400
committergeireann <geireann.lindfield@gmail.com>2022-04-07 18:06:40 -0400
commitb3d6eaa3a0b126712eae25c1b91925d030a2d900 (patch)
tree6d721a49711e71e5311c1720e0532f9b1bfb7a38 /src
parente8938d5d7b889551c1d32bcf5385e369ed67cea5 (diff)
added RecordingView
Diffstat (limited to 'src')
-rw-r--r--src/client/documents/DocumentTypes.ts1
-rw-r--r--src/client/documents/Documents.ts16
-rw-r--r--src/client/util/CurrentUserUtils.ts4
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx3
-rw-r--r--src/client/views/nodes/FieldView.tsx1
-rw-r--r--src/client/views/nodes/RecordingBox/ProgressBar.scss26
-rw-r--r--src/client/views/nodes/RecordingBox/ProgressBar.tsx46
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingBox.tsx24
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingView.scss207
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingView.tsx326
-rw-r--r--src/client/views/nodes/RecordingBox/index.ts2
-rw-r--r--src/fields/URLField.ts1
12 files changed, 652 insertions, 5 deletions
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 161dff6e0..ca942a38a 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -9,6 +9,7 @@ export enum DocumentType {
KVP = "kvp",
VID = "video",
AUDIO = "audio",
+ REC = "recording",
PDF = "pdf",
INK = "inks",
SCREENSHOT = "screenshot",
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index bb60586eb..e50172af5 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -11,7 +11,7 @@ import { RichTextField } from "../../fields/RichTextField";
import { SchemaHeaderField } from "../../fields/SchemaHeaderField";
import { ComputedField, ScriptField } from "../../fields/ScriptField";
import { Cast, NumCast, StrCast } from "../../fields/Types";
-import { AudioField, ImageField, MapField, PdfField, VideoField, WebField, YoutubeField } from "../../fields/URLField";
+import { AudioField, ImageField, MapField, PdfField, RecordingField, VideoField, WebField, YoutubeField } from "../../fields/URLField";
import { SharingPermissions } from "../../fields/util";
import { Upload } from "../../server/SharedMediaTypes";
import { OmitKeys, Utils } from "../../Utils";
@@ -61,6 +61,7 @@ import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo";
import { DocumentType } from "./DocumentTypes";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { MapBox } from "../views/nodes/MapBox/MapBox";
+import { RecordingBox } from "../views/nodes/RecordingBox/RecordingBox";
const defaultNativeImageDim = Number(DFLT_IMAGE_NATIVE_DIM.replace("px", ""));
class EmptyBox {
@@ -401,6 +402,10 @@ export namespace Docs {
layout: { view: AudioBox, dataField: defaultDataKey },
options: { _height: 100, backgroundColor: "lightGray", links: "@links(self)" }
}],
+ [DocumentType.REC, {
+ layout: { view: VideoBox, dataField: defaultDataKey },
+ options: { _height: 100, backgroundColor: "pink", links: "@links(self)" }
+ }],
[DocumentType.PDF, {
layout: { view: PDFBox, dataField: defaultDataKey },
options: { _curPage: 1, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, links: "@links(self)" }
@@ -468,7 +473,7 @@ export namespace Docs {
options: { hideLinkButton: true, _width: 40, _height: 40, borderRounding: "100%", links: "@links(self)" },
}],
[DocumentType.WEBCAM, {
- layout: { view: DashWebRTCVideo, dataField: defaultDataKey },
+ layout: { view: RecordingBox, dataField: defaultDataKey },
options: { links: "@links(self)" }
}],
[DocumentType.PRESELEMENT, {
@@ -699,6 +704,10 @@ export namespace Docs {
{ ...options, backgroundColor: ComputedField.MakeFunction("this._mediaState === 'playing' ? 'green':'gray'") as any });
}
+ export function RecordingDocument(url: string, options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.REC), "", options);
+ }
+
export function SearchDocument(options: DocumentOptions = {}) {
return InstanceFromProto(Prototypes.get(DocumentType.SEARCH), new List<Doc>([]), options);
}
@@ -1160,6 +1169,9 @@ export namespace DocUtils {
} else if (field instanceof AudioField) {
created = Docs.Create.AudioDocument((field).url.href, resolved);
layout = AudioBox.LayoutString;
+ } else if (field instanceof RecordingField) {
+ created = Docs.Create.RecordingDocument((field).url.href, resolved);
+ layout = RecordingBox.LayoutString;
} else if (field instanceof InkField) {
created = Docs.Create.InkDocument(ActiveInkColor(), CurrentUserUtils.SelectedTool, ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), (field).inkData, resolved);
layout = InkingStroke.LayoutString;
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index af731ce9f..edb362a37 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -424,8 +424,8 @@ export class CurrentUserUtils {
doc.emptyScreenshot = Docs.Create.ScreenshotDocument("empty screenshot", { _fitWidth: true, title: "empty screenshot", _width: 400, _height: 200, system: true, cloneFieldFilter: new List<string>(["system"]) });
}
if (doc.emptyWall === undefined) {
- doc.emptyWall = Docs.Create.ScreenshotDocument("", { _fitWidth: true, _width: 400, _height: 200, title: "screen snapshot", system: true, cloneFieldFilter: new List<string>(["system"]) });
- (doc.emptyWall as Doc).videoWall = true;
+ doc.emptyWall = Docs.Create.WebCamDocument("", { _width: 400, _height: 200, title: "recording", system: true, cloneFieldFilter: new List<string>(["system"]) });
+ (doc.emptyWall as Doc).recording = true;
}
if (doc.emptyAudio === undefined) {
doc.emptyAudio = Docs.Create.AudioDocument(nullAudio, { _width: 200, _height: 100, title: "audio recording", system: true, cloneFieldFilter: new List<string>(["system"]) });
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 005133eb0..da9b448ec 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -41,6 +41,7 @@ import { WebBox } from "./WebBox";
import React = require("react");
import XRegExp = require("xregexp");
import { MapBox } from "./MapBox/MapBox";
+import { RecordingBox } from "./RecordingBox";
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
@@ -225,7 +226,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & Fo
components={{
FormattedTextBox, ImageBox, DirectoryImportBox, FontIconBox, LabelBox, EquationBox, SliderBox, FieldView,
CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox,
- PDFBox, VideoBox, AudioBox, PresBox, YoutubeBox, PresElementBox, SearchBox, FilterBox, FunctionPlotBox,
+ PDFBox, VideoBox, AudioBox, RecordingBox, PresBox, YoutubeBox, PresElementBox, SearchBox, FilterBox, FunctionPlotBox,
ColorBox, DashWebRTCVideo, LinkAnchorBox, InkingStroke, LinkBox, ScriptingBox, MapBox,
ScreenshotBox,
HTMLtag, ComparisonBox
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 943b9f153..1fd665bad 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -6,6 +6,7 @@ import { Doc, Field, FieldResult } from "../../../fields/Doc";
import { List } from "../../../fields/List";
import { WebField } from "../../../fields/URLField";
import { DocumentViewSharedProps } from "./DocumentView";
+import { RecordingBox } from "./RecordingBox";
//
// these properties get assigned through the render() method of the DocumentView when it creates this node.
diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.scss b/src/client/views/nodes/RecordingBox/ProgressBar.scss
new file mode 100644
index 000000000..a493b0b89
--- /dev/null
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.scss
@@ -0,0 +1,26 @@
+
+.progressbar {
+ position: absolute;
+ display: flex;
+ justify-content: flex-start;
+ bottom: 10px;
+ width: 80%;
+ height: 5px;
+ 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;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
new file mode 100644
index 000000000..da5fa2b00
--- /dev/null
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
@@ -0,0 +1,46 @@
+import * as React from 'react';
+import { useEffect } from "react"
+import "./ProgressBar.scss"
+
+interface ProgressBarProps {
+ progress: number,
+ marks: number[],
+ playSegment: (idx: number) => void
+}
+
+export function ProgressBar(props: ProgressBarProps) {
+
+ const handleClick = (e: React.MouseEvent) => {
+ let progressbar = document.getElementById('progressbar')!
+ let bounds = progressbar!.getBoundingClientRect();
+ let x = e.clientX - bounds.left;
+ let percent = x / progressbar.clientWidth * 100
+
+ for (let i = 0; i < props.marks.length; i++) {
+ let start = i == 0 ? 0 : props.marks[i-1];
+ if (percent > start && percent < props.marks[i]) {
+ props.playSegment(i)
+ // console.log(i)
+ // console.log(percent)
+ // console.log(props.marks[i])
+ break
+ }
+ }
+ }
+
+ return(
+ <div className="progressbar" id="progressbar">
+ <div
+ className="progressbar done"
+ style={{ width: `${props.progress}%` }}
+ onClick={handleClick}
+ ></div>
+ {props.marks.map((mark) => {
+ return <div
+ className="progressbar mark"
+ style={{ width: `${mark}%` }}
+ ></div>
+ })}
+ </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..6d444d324
--- /dev/null
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -0,0 +1,24 @@
+import { observer } from "mobx-react";
+import * as React from "react";
+import { ViewBoxBaseComponent } from "../../DocComponent";
+import { FieldView } from "../FieldView";
+import { RecordingView } from './RecordingView';
+
+
+@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);
+ }
+
+ render() {
+ return <div className="recordingBox" ref={this._ref}>
+ <RecordingView/>
+ </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..2eaf5468d
--- /dev/null
+++ b/src/client/views/nodes/RecordingBox/RecordingView.scss
@@ -0,0 +1,207 @@
+video {
+ flex: 100%;
+ width: 100%;
+ min-height: 400px;
+ height: auto;
+ display: block;
+ background-color: black;
+}
+
+button { margin: 0 .5rem }
+
+.recording-container {
+ height: 100%;
+ width: 100%;
+ display: flex;
+}
+
+.video-wrapper {
+ max-width: 600px;
+ max-width: 700px;
+ position: relative;
+ display: flex;
+ justify-content: center;
+ overflow: hidden;
+ border-radius: 10px;
+}
+
+.video-wrapper:hover .controls {
+ bottom: 30px;
+ transform: translateY(0%);
+ opacity: 100%;
+}
+
+.controls {
+ display: flex;
+ align-items: center;
+ justify-content: space-evenly;
+ position: absolute;
+ padding: 14px;
+ width: 100%;
+ max-width: 500px;
+ 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: 30px;
+ // bottom: -150px;
+}
+
+.actions button {
+ background: none;
+ border: none;
+ outline: none;
+ cursor: pointer;
+}
+
+.actions button i {
+ background-color: none;
+ color: white;
+ font-size: 30px;
+}
+
+// input[type="range"] {
+// -webkit-appearance: none !important;
+// background: rgba(255, 255, 255, 0.2);
+// border-radius: 20px;
+// height: 4px;
+// width: 350px;
+// }
+
+// input[type="range"]::-webkit-slider-thumb {
+// -webkit-appearance: none !important;
+// cursor: pointer;
+// height: 6px;
+// }
+
+// input[type="range"]::-moz-range-progress {
+// background: white;
+// }
+
+.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;
+ justify-content: center;
+ width: 100%;
+
+}
+
+.record-button-wrapper {
+ width: 35px;
+ height: 35px;
+ font-size: 0;
+ background-color: grey;
+ border: 0px;
+ border-radius: 35px;
+ margin: 10px;
+
+ .record-button {
+ position: relative;
+ background-color: red;
+ border: 0px;
+ border-radius: 50%;
+ height: 28px;
+ width: 28px;
+ top: 50%;
+ left: 50%;
+ margin: -14px 0px 0px -14px;
+
+ &:hover {
+ width: 30px;
+ height: 30px;
+ margin: -15px 0px 0px -15px;
+ }
+ }
+
+ .stop-button{
+ position: relative;
+ background-color: red;
+ border: 0px;
+ border-radius: 10%;
+ height: 18px;
+ width: 18px;
+ top: 50%;
+ left: 50%;
+ margin: -9px 0px 0px -9px;
+
+ // &:hover {
+ // width: 40px;
+ // height: 40px
+ // }
+ }
+
+}
+
+.video-edit-wrapper {
+ height: 100%;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ right: 50% - 15;
+
+ .video-edit-buttons {
+ margin: 0 5px;
+ }
+
+}
+
+
+
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
new file mode 100644
index 000000000..15f8c8626
--- /dev/null
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -0,0 +1,326 @@
+import * as React from 'react';
+import "./RecordingView.scss";
+import { ReactElement, useCallback, 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";
+
+
+enum RecordingStatus {
+ Recording,
+ Stopped,
+ Paused
+}
+
+interface VideoSegment {
+ chunks: any[],
+ endTime: number
+}
+
+const MAXTIME = 1000;
+
+export function RecordingView() {
+
+ 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);
+ const [speed, setSpeed] = useState(1);
+ const [muted, setMuted] = useState(false);
+
+ const [videos, setVideos] = useState<VideoSegment[]>([]);
+ // const [videos, setVideos] = useState<string[]>([]);
+ const [currentVid, setCurrentVid] = useState<number>(0);
+ const recorder = useRef<MediaRecorder | null>(null);
+ const videoElementRef = useRef<HTMLVideoElement | null>(null);
+
+ const [finished, setFinished] = useState<Boolean>(false)
+
+
+
+ const DEFAULT_MEDIA_CONSTRAINTS = {
+ video: {
+ width: 1280,
+ height: 720,
+ },
+ audio: {
+ echoCancellation: true,
+ noiseSuppression: true,
+ sampleRate: 44100
+ }
+ }
+
+ useEffect(() => {
+
+ if (finished) {
+ let allVideoChunks : any = []
+ console.log(videos)
+ videos.forEach((vid) => {
+ console.log(vid.chunks)
+ allVideoChunks = allVideoChunks.concat(vid.chunks)
+ })
+
+ console.log(allVideoChunks)
+
+ const blob = new Blob(allVideoChunks, {
+ type: 'video/webm'
+ })
+ const blobUrl = URL.createObjectURL(blob)
+
+ videoElementRef.current!.srcObject = null
+ videoElementRef.current!.src = blobUrl
+ videoElementRef.current!.muted = false
+ }
+
+
+ }, [finished])
+
+ useEffect(() => {
+ // check if the browser supports media devices on first load
+ if (!navigator.mediaDevices) {
+ console.log('This browser does not support getUserMedia.')
+ }
+ console.log('This device has the correct media devices.')
+ }, [])
+
+ useEffect(() => {
+ // get access to the video element on every render
+ // videoElement = document.getElementById('video') as HTMLVideoElement;
+ videoElementRef.current = document.getElementById('video') as HTMLVideoElement;
+ })
+
+ // useEffect(() => {
+ // if (playing) {
+ // videoElement!.srcObject = null
+ // // videoElement!.src = videos[currentVid].url
+ // videoElement!.muted = false
+ // videoElement!.play()
+ // } else {
+ // videoElement!.pause();
+ // }
+ // }, [playing, videoElement]);
+
+ 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 () => {
+ const stream = await startShowingStream();
+ recorder.current = new MediaRecorder(stream)
+
+ // temporary chunks of video
+ let chunks: any = []
+ recorder.current.ondataavailable = (event: any) => {
+ // store the video chunks as it is recording
+ console.log("data available")
+ if (event.data.size > 0) {
+ chunks.push(event.data)
+ }
+ }
+
+ recorder.current.onstart = (event: any) => {
+ console.log("on start")
+ setRecording(true);
+ }
+
+ recorder.current.onstop = () => {
+ // if we have a last portion
+ if (chunks.length > 1) {
+ // append the current portion to the video pieces
+ setVideos(videos => [...videos, {chunks: chunks, endTime: recordingTimerRef.current}])
+ }
+
+ // reset the temporary chunks
+ chunks = []
+ setRecording(false);
+ setFinished(true);
+ }
+
+ // recording paused
+ recorder.current.onpause = (event: any) => {
+ // append the current portion to the video pieces
+ console.log(chunks)
+ setVideos(videos => [...videos, {chunks: chunks, endTime: recordingTimerRef.current}])
+
+ // reset the temporary chunks
+ chunks = []
+ setRecording(false);
+ }
+
+ recorder.current.onresume = async (event: any) => {
+ console.log(event)
+ await startShowingStream();
+ setRecording(true);
+ }
+
+ recorder.current.start(200)
+ }
+
+
+ const stop = () => {
+ if (recorder.current) {
+ if (recorder.current.state !== "inactive") {
+ recorder.current.stop();
+ // recorder.current.stream.getTracks().forEach((track: any) => track.stop())
+ }
+ }
+ }
+
+ const pause = () => {
+ if (recorder.current) {
+ if (recorder.current.state === "recording") {
+ recorder.current.pause();
+ }
+ }
+ }
+
+ const startOrResume = () => {
+ console.log('[RecordingView.tsx] startOrResume')
+ if (!recorder.current || recorder.current.state === "inactive") {
+ record();
+ } else if (recorder.current.state === "paused") {
+ recorder.current.resume();
+ }
+ }
+
+ const playSegment = (idx: number) => {
+ console.log(idx)
+ let currentChunks = videos[idx].chunks
+ console.log(currentChunks)
+
+ const blob = new Blob(currentChunks, {
+ type: 'video/webm'
+ })
+ const blobUrl = URL.createObjectURL(blob)
+ console.log(blobUrl)
+
+ videoElementRef.current!.srcObject = null
+ videoElementRef.current!.src = blobUrl
+ videoElementRef.current!.muted = false
+ }
+
+ const clearPrevious = () => {
+ const numVideos = videos.length
+ setRecordingTimer(numVideos == 1 ? 0 : videos[numVideos - 2].endTime)
+ setVideoProgressHelper(numVideos == 1 ? 0 : videos[numVideos - 2].endTime)
+ setVideos(videos.filter((_, idx) => idx !== numVideos - 1));
+ }
+
+ // const handleVideoProgress = (event: any) => {
+ // const manualChange = Number(event.target.value);
+ // videoElement!.currentTime = (videoElement!.duration / 100) * manualChange;
+ // setProgress(manualChange)
+ // };
+
+ // const handleVideoSpeed = (event: any) => {
+ // const newSpeed = Number(event.target.value);
+ // videoElement!.playbackRate = speed;
+ // setSpeed(newSpeed)
+ // };
+
+ const handleOnTimeUpdate = () => {
+ if (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);
+ }
+
+
+
+
+ useEffect(() => {
+ console.log(videos.map((elt) => elt.endTime / MAXTIME * 100))
+ console.log(videos)
+ }, [videos])
+
+ return (
+ <div className="recording-container">
+ <div className="video-wrapper">
+ <video id="video"
+ autoPlay
+ muted
+ onTimeUpdate={handleOnTimeUpdate}
+ />
+ <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" onClick={pause}/> :
+ <button className="record-button" onClick={startOrResume}/>
+ }
+ </div>
+ {!recording && videos.length > 0 ?
+
+ <div className="video-edit-wrapper">
+ <IconContext.Provider value={{ color: "grey", className: "video-edit-buttons" }}>
+ <MdBackspace onClick={clearPrevious}/>
+ </IconContext.Provider>
+ <IconContext.Provider value={{ color: "#cc1c08", className: "video-edit-buttons" }}>
+ <FaCheckCircle onClick={stop}/>
+ </IconContext.Provider>
+ </div>
+
+ : <></>}
+
+ </div>
+
+ <ProgressBar
+ progress={progress}
+ marks={videos.map((elt) => elt.endTime / MAXTIME * 100)}
+ playSegment={playSegment}
+ />
+
+ {/* <button className="mute-btn" onClick={() => setMuted(!muted)}>
+ {!muted ? (
+ <i className="bx bxs-volume-full"></i>
+ ) : (
+ <i className="bx bxs-volume-mute"></i>
+ )}
+ </button> */}
+ </div>
+
+ </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
diff --git a/src/fields/URLField.ts b/src/fields/URLField.ts
index 1d4bbaed0..3e7542e46 100644
--- a/src/fields/URLField.ts
+++ b/src/fields/URLField.ts
@@ -53,6 +53,7 @@ export abstract class URLField extends ObjectField {
export const nullAudio = "https://actions.google.com/sounds/v1/alarms/beep_short.ogg";
@scriptingGlobal @Deserializable("audio") export class AudioField extends URLField { }
+@scriptingGlobal @Deserializable("recording") export class RecordingField extends URLField { }
@scriptingGlobal @Deserializable("image") export class ImageField extends URLField { }
@scriptingGlobal @Deserializable("video") export class VideoField extends URLField { }
@scriptingGlobal @Deserializable("pdf") export class PdfField extends URLField { }