diff options
-rw-r--r-- | package-lock.json | 13 | ||||
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | src/client/documents/DocumentTypes.ts | 1 | ||||
-rw-r--r-- | src/client/documents/Documents.ts | 16 | ||||
-rw-r--r-- | src/client/util/CurrentUserUtils.ts | 4 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentContentsView.tsx | 3 | ||||
-rw-r--r-- | src/client/views/nodes/FieldView.tsx | 1 | ||||
-rw-r--r-- | src/client/views/nodes/RecordingBox/ProgressBar.scss | 26 | ||||
-rw-r--r-- | src/client/views/nodes/RecordingBox/ProgressBar.tsx | 46 | ||||
-rw-r--r-- | src/client/views/nodes/RecordingBox/RecordingBox.tsx | 24 | ||||
-rw-r--r-- | src/client/views/nodes/RecordingBox/RecordingView.scss | 207 | ||||
-rw-r--r-- | src/client/views/nodes/RecordingBox/RecordingView.tsx | 326 | ||||
-rw-r--r-- | src/client/views/nodes/RecordingBox/index.ts | 2 | ||||
-rw-r--r-- | src/fields/URLField.ts | 1 |
14 files changed, 667 insertions, 5 deletions
diff --git a/package-lock.json b/package-lock.json index b1431e55b..d84e8a572 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2001,6 +2001,14 @@ "@types/react": "*" } }, + "@types/react-icons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/react-icons/-/react-icons-3.0.0.tgz", + "integrity": "sha512-Vefs6LkLqF61vfV7AiAqls+vpR94q67gunhMueDznG+msAkrYgRxl7gYjNem/kZ+as2l2mNChmF1jRZzzQQtMg==", + "requires": { + "react-icons": "*" + } + }, "@types/react-measure": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/@types/react-measure/-/react-measure-2.0.8.tgz", @@ -15764,6 +15772,11 @@ "react-resizable": "^1.9.0" } }, + "react-icons": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.3.1.tgz", + "integrity": "sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ==" + }, "react-image-lightbox-with-rotate": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/react-image-lightbox-with-rotate/-/react-image-lightbox-with-rotate-5.1.1.tgz", diff --git a/package.json b/package.json index 2c3e641dd..35aa2ef92 100644 --- a/package.json +++ b/package.json @@ -132,6 +132,7 @@ "@types/dom-speech-recognition": "0.0.1", "@types/formidable": "1.0.31", "@types/google-maps": "^3.2.3", + "@types/react-icons": "^3.0.0", "@types/react-reconciler": "^0.26.4", "@types/reveal": "^3.3.33", "@types/supercluster": "^7.1.0", @@ -253,6 +254,7 @@ "react-datepicker": "^3.8.0", "react-dom": "^16.14.0", "react-grid-layout": "^0.18.3", + "react-icons": "^4.3.1", "react-image-lightbox-with-rotate": "^5.1.1", "react-jsx-parser": "^1.29.0", "react-loading": "^2.0.3", 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 { } |