diff options
Diffstat (limited to 'src/client/views')
| -rw-r--r-- | src/client/views/nodes/RecordingBox/RecordingView.tsx | 443 |
1 files changed, 221 insertions, 222 deletions
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx index 15883632a..ec9838bdd 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -11,262 +11,261 @@ import { returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils'; import { Presentation, RecordingApi } from '../../../util/RecordingApi'; export interface MediaSegment { - videoChunks: any[], - endTime: number, - startTime: number, - presentation?: Presentation, + videoChunks: any[], + endTime: number, + startTime: number, + presentation?: Presentation, } interface IRecordingViewProps { - setResult: (info: Upload.AccessPathInfo, presentation?: Presentation) => void - setDuration: (seconds: number) => void - id: string + 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>(true) + 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>(true) + + + + 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 && RecordingApi.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; - const DEFAULT_MEDIA_CONSTRAINTS = { - video: { - width: 1280, - height: 720, - }, - audio: { - echoCancellation: true, - noiseSuppression: true, - sampleRate: 44100 + return stream; } - } - // useEffect(() => console.debug('progress', progress), [progress]) + 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()); - useEffect(() => { - if (finished) { - // make the total presentation that'll match the concatted video - let concatPres = trackScreen && RecordingApi.Instance.concatPresentations(videos.map(v => v.presentation as Presentation)); + // temporary chunks of video + let videoChunks: any = []; - // 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() })); + videoRecorder.current.ondataavailable = (event: any) => { + if (event.data.size > 0) videoChunks.push(event.data); + }; - // 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) + videoRecorder.current.onstart = (event: any) => { + setRecording(true); + // start the recording api when the video recorder starts + trackScreen && RecordingApi.Instance.start(); + }; - // 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"); - })(); + 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 = RecordingApi.Instance.yieldPresentation(); + setVideos(videos => [...videos, (presentation != null && trackScreen) ? { ...nextVideo, presentation } : nextVideo]); + } + + // reset the temporary chunks + videoChunks = []; + setRecording(false); + } + + videoRecorder.current.start(200); } - }, [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 + // if this is called, then we're done recording all the segments + const finish = (e: React.PointerEvent) => { + e.stopPropagation(); - return stream - } + // call stop on the video recorder if active + videoRecorder.current?.state !== "inactive" && videoRecorder.current?.stop(); - 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()) + // end the streams (audio/video) to remove recording icon + const stream = videoElementRef.current!.srcObject; + stream instanceof MediaStream && stream.getTracks().forEach(track => track.stop()); - // temporary chunks of video - let videoChunks: any = [] + // finish/clear the recoringApi + RecordingApi.Instance.finish(); - videoRecorder.current.ondataavailable = (event: any) => { - if (event.data.size > 0) videoChunks.push(event.data) + // this will call upon progessbar to update videos to be in the correct order + setFinished(true); } - videoRecorder.current.onstart = (event: any) => { - setRecording(true); - // start the recording api when the video recorder starts - trackScreen && RecordingApi.Instance.start(); + const pause = (e: React.PointerEvent) => { + e.stopPropagation(); + // if recording, then this is just a new segment + videoRecorder.current?.state === "recording" && videoRecorder.current.stop(); } - 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 = RecordingApi.Instance.yieldPresentation(); - setVideos(videos => [...videos, (presentation != null && trackScreen) ? { ...nextVideo, presentation } : nextVideo]); - } + const start = (e: React.PointerEvent) => { + setupMoveUpEvents({}, e, returnTrue, returnFalse, e => { + // start recording if not already recording + if (!videoRecorder.current || videoRecorder.current.state === "inactive") record(); - // reset the temporary chunks - videoChunks = [] - setRecording(false); + return true; // cancels propagation to documentView to avoid selecting it. + }, false, 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 - RecordingApi.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 undoPrevious = (e: React.PointerEvent) => { + e.stopPropagation(); + setDoUndo(prev => !prev); + } - const handleOnTimeUpdate = () => { playing && setVideoProgressHelper(videoElementRef.current!.currentTime); }; + const handleOnTimeUpdate = () => { playing && setVideoProgressHelper(videoElementRef.current!.currentTime); }; - const millisecondToMinuteSecond = (milliseconds: number) => { - const toTwoDigit = (digit: number) => { - return String(digit).length == 1 ? "0" + digit : digit + 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); } - 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>) + 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 |
