diff options
| author | Michael Foiani <sotech117@michaels-mbp-5.devices.brown.edu> | 2022-06-02 15:33:26 -0400 |
|---|---|---|
| committer | Michael Foiani <sotech117@michaels-mbp-5.devices.brown.edu> | 2022-06-02 15:33:26 -0400 |
| commit | 43361401c1e963408bc502bad8f53da82498ceec (patch) | |
| tree | 9983d2bb633dfed8abe34e02a8d6e310fde81c97 /src/client/views/nodes/RecordingBox | |
| parent | e6d508b4e1cfe2e7a948bc0399dc5e0e85be8f59 (diff) | |
| parent | 560d7090702c3559724420f3571b11d86c930177 (diff) | |
merge with presentmode
Diffstat (limited to 'src/client/views/nodes/RecordingBox')
| -rw-r--r-- | src/client/views/nodes/RecordingBox/ProgressBar.scss | 26 | ||||
| -rw-r--r-- | src/client/views/nodes/RecordingBox/ProgressBar.tsx | 45 | ||||
| -rw-r--r-- | src/client/views/nodes/RecordingBox/RecordingBox.tsx | 62 | ||||
| -rw-r--r-- | src/client/views/nodes/RecordingBox/RecordingView.scss | 207 | ||||
| -rw-r--r-- | src/client/views/nodes/RecordingBox/RecordingView.tsx | 277 | ||||
| -rw-r--r-- | src/client/views/nodes/RecordingBox/index.ts | 2 |
6 files changed, 619 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..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..82d5e1f04 --- /dev/null +++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { useEffect } from "react" +import "./ProgressBar.scss" + +interface ProgressBarProps { + progress: number, + marks: number[], +} + +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..159271223 --- /dev/null +++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx @@ -0,0 +1,62 @@ +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 { RecordingApi } from "../../../util/RecordingApi"; +import { Doc, FieldsSym } 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() { + console.log("set native width and height") + Doc.SetNativeWidth(this.dataDoc, 1280); + Doc.SetNativeHeight(this.dataDoc, 720); + } + + @observable result: Upload.FileInformation | undefined = undefined + @observable videoDuration: number | undefined = undefined + + @action + setVideoDuration = (duration: number) => { + this.videoDuration = duration + } + + @action + setResult = (info: Upload.FileInformation, trackScreen: boolean) => { + 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.agnostic.client); + this.dataDoc[this.fieldKey + "-recorded"] = true; + // stringify the presenation and store it + if (trackScreen) { + this.dataDoc[this.fieldKey + "-presentation"] = JSON.stringify(RecordingApi.Instance.clear()); + } + } + + render() { + // console.log("Proto[Is]: ", this.rootDoc.proto?.[Id]) + 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..9b2f6d070 --- /dev/null +++ b/src/client/views/nodes/RecordingBox/RecordingView.scss @@ -0,0 +1,207 @@ +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: grey; +} + +.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: 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; + // 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: 30px; + // 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; + 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; + 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-items: center; + position: absolute; + 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; + } + + } +}
\ 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..87716e9cc --- /dev/null +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -0,0 +1,277 @@ +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"; +import { Networking } from '../../../Network'; +import { Upload } from '../../../../server/SharedMediaTypes'; + +import { RecordingApi } from '../../../util/RecordingApi'; + +interface MediaSegment { + videoChunks: any[], + endTime: number +} + +interface IRecordingViewProps { + setResult: (info: Upload.FileInformation, trackScreen: boolean) => 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); + + const [videos, setVideos] = useState<MediaSegment[]>([]); + const videoRecorder = useRef<MediaRecorder | null>(null); + const videoElementRef = useRef<HTMLVideoElement | null>(null); + + const [finished, setFinished] = useState<boolean>(false) + const [trackScreen, setTrackScreen] = useState<boolean>(true) + + + + const DEFAULT_MEDIA_CONSTRAINTS = { + video: { + width: 1280, + height: 720, + }, + audio: { + echoCancellation: true, + noiseSuppression: true, + sampleRate: 44100 + } + } + + useEffect(() => { + + if (finished) { + props.setDuration(recordingTimer * 100) + let allVideoChunks: any = [] + videos.forEach((vid) => { + console.log(vid.videoChunks) + allVideoChunks = allVideoChunks.concat(vid.videoChunks) + }) + + const videoFile = new File(allVideoChunks, "video.mkv", { type: allVideoChunks[0].type, lastModified: Date.now() }); + + Networking.UploadFilesToServer(videoFile) + .then((data) => { + const result = data[0].result + if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox + props.setResult(result, trackScreen) + } else { + alert("video conversion failed"); + } + }) + + } + + + }, [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 + videoElementRef.current = document.getElementById(`video-${props.id}`) as HTMLVideoElement; + }) + + 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(); + videoRecorder.current = new MediaRecorder(stream) + + // 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); + trackScreen && RecordingApi.Instance.start(); + } + + videoRecorder.current.onstop = () => { + // if we have a last portion + if (videoChunks.length > 1) { + // append the current portion to the video pieces + setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current }]) + } + + // reset the temporary chunks + videoChunks = [] + setRecording(false); + setFinished(true); + trackScreen && RecordingApi.Instance.pause(); + } + + // recording paused + videoRecorder.current.onpause = (event: any) => { + // append the current portion to the video pieces + setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current }]) + + // reset the temporary chunks + videoChunks = [] + setRecording(false); + trackScreen && RecordingApi.Instance.pause(); + } + + videoRecorder.current.onresume = async (event: any) => { + await startShowingStream(); + setRecording(true); + trackScreen && RecordingApi.Instance.resume(); + } + + videoRecorder.current.start(200) + } + + + const stop = () => { + if (videoRecorder.current) { + if (videoRecorder.current.state !== "inactive") { + videoRecorder.current.stop(); + // recorder.current.stream.getTracks().forEach((track: any) => track.stop()) + } + } + } + + const pause = () => { + if (videoRecorder.current) { + if (videoRecorder.current.state === "recording") { + videoRecorder.current.pause(); + } + } + } + + const startOrResume = () => { + if (!videoRecorder.current || videoRecorder.current.state === "inactive") { + record(); + } else if (videoRecorder.current.state === "paused") { + videoRecorder.current.resume(); + } + } + + 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 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); + } + + return ( + <div className="recording-container"> + <div className="video-wrapper"> + <video id={`video-${props.id}`} + 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="options-wrapper 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 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> + + <ProgressBar + progress={progress} + marks={videos.map((elt) => elt.endTime / MAXTIME * 100)} + // playSegment={playSegment} + /> + </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 |
