From f040a5fd07b6b1bbc0204a178c1220efac4fe7ca Mon Sep 17 00:00:00 2001 From: "ildar.timerbaev" Date: Fri, 18 Aug 2023 23:21:37 +0300 Subject: [PATCH 1/2] Video recorder: added recorder for 3Speak in video uploading --- package.json | 1 + .../video-upload-threespeak/index.scss | 65 ++++++- .../video-upload-threespeak/index.tsx | 47 +++++- .../video-upload-recorder-actions.tsx | 39 +++++ .../video-upload-recorder-no-permission.tsx | 10 ++ .../video-upload-recorder.tsx | 159 ++++++++++++++++++ src/common/i18n/locales/en-US.json | 8 +- src/common/img/svg.tsx | 34 ++++ yarn.lock | 5 + 9 files changed, 352 insertions(+), 16 deletions(-) create mode 100644 src/common/components/video-upload-threespeak/video-upload-recorder-actions.tsx create mode 100644 src/common/components/video-upload-threespeak/video-upload-recorder-no-permission.tsx create mode 100644 src/common/components/video-upload-threespeak/video-upload-recorder.tsx diff --git a/package.json b/package.json index ec8ba7ad00f..ac6824712ed 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "@types/bytebuffer": "^5.0.41", "@types/cookie-parser": "^1.4.2", "@types/diff-match-patch": "^1.0.32", + "@types/dom-mediacapture-record": "^1.0.16", "@types/express": "^4.17.0", "@types/jest": "^26.0.24", "@types/js-cookie": "^2.2.6", diff --git a/src/common/components/video-upload-threespeak/index.scss b/src/common/components/video-upload-threespeak/index.scss index dc1def00a9c..20cf8fa3bd9 100644 --- a/src/common/components/video-upload-threespeak/index.scss +++ b/src/common/components/video-upload-threespeak/index.scss @@ -16,7 +16,7 @@ @include themify(night) { background-color: lighten($dark, 6); } - + } } @@ -88,11 +88,64 @@ label { .three-speak-video-uploading { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 1rem; - @include media-breakpoint-down(xs) { - grid-template-columns: 1fr; + .video-source { + display: flex; + gap: 1rem; + + > div { + width: 100%; + } + } +} + +.video-upload-recorder { + position: relative; + + .reset-btn { + position: absolute; + top: -2.65rem; + right: 0; + } + + .actions { + position: absolute; + bottom: 1rem; + left: 0; + right: 0; + z-index: 9; + display: flex; + justify-content: center; + + .record-btn { + color: $danger; + border: 0.25rem solid $white; + border-radius: 50%; + cursor: pointer; + + &:hover { + border-color: var(--border-color); + } + } + } + + video { + width: 100%; + border-radius: 1rem; + } + + .no-permission { + width: 100%; + height: 300px; + border-radius: 1rem; + background-color: var(--border-color); + display: flex; + align-items: center; + justify-content: center; + + p { + font-size: 1.125rem; + font-weight: bold; + } } } \ No newline at end of file diff --git a/src/common/components/video-upload-threespeak/index.tsx b/src/common/components/video-upload-threespeak/index.tsx index bb9e95e0e4e..8be1d84a4fa 100644 --- a/src/common/components/video-upload-threespeak/index.tsx +++ b/src/common/components/video-upload-threespeak/index.tsx @@ -6,6 +6,8 @@ import "./index.scss"; import { VideoUploadItem } from "./video-upload-item"; import { createFile } from "../../util/create-file"; import { useMappedStore } from "../../store/use-mapped-store"; +import { recordVideoSvg } from "../../img/svg"; +import { VideoUploadRecorder } from "./video-upload-recorder"; const DEFAULT_THUMBNAIL = require("./assets/thumbnail-play.jpg"); @@ -42,8 +44,9 @@ export const VideoUpload = (props: Props & React.HTMLAttributes) const [videoUrl, setVideoUrl] = useState(""); const [thumbUrl, setThumbUrl] = useState(""); const [duration, setDuration] = useState(""); + const [showRecorder, setShowRecorder] = useState(false); - const canUpload = videoUrl && videoPercentage === 100; + const canUpload = videoUrl; // Reset on dialog hide useEffect(() => { @@ -60,6 +63,7 @@ export const VideoUpload = (props: Props & React.HTMLAttributes) setStep("upload"); setVideoPercentage(0); setThumbnailPercentage(0); + setShowRecorder(false); } }, [props.show]); @@ -108,14 +112,39 @@ export const VideoUpload = (props: Props & React.HTMLAttributes) const uploadVideoModal = (
-
- +
+

Video source

+ {showRecorder ? ( + setShowRecorder(false)} + /> + ) : ( +
+ {selectedFile ? ( + <> + ) : ( +
setShowRecorder(true)} + > + {recordVideoSvg} + {_t("video-upload.record-video")} +
+ )} + +
+ )} +

Thumbnail

+ {recordStarted ? ( +
{ + mediaRecorder?.stop(); + setRecordStarted(false); + }} + > + {rectSvg} +
+ ) : ( +
{ + mediaRecorder?.start(); + setRecordStarted(true); + }} + > + {circleSvg} +
+ )} +
+ ); +} diff --git a/src/common/components/video-upload-threespeak/video-upload-recorder-no-permission.tsx b/src/common/components/video-upload-threespeak/video-upload-recorder-no-permission.tsx new file mode 100644 index 00000000000..95b907ae2d9 --- /dev/null +++ b/src/common/components/video-upload-threespeak/video-upload-recorder-no-permission.tsx @@ -0,0 +1,10 @@ +import { _t } from "../../i18n"; +import React from "react"; + +export function VideoUploadRecorderNoPermission() { + return ( +
+

{_t("video-upload.no-record-permission")}

+
+ ); +} diff --git a/src/common/components/video-upload-threespeak/video-upload-recorder.tsx b/src/common/components/video-upload-threespeak/video-upload-recorder.tsx new file mode 100644 index 00000000000..d8cd354f0c8 --- /dev/null +++ b/src/common/components/video-upload-threespeak/video-upload-recorder.tsx @@ -0,0 +1,159 @@ +import React, { useEffect, useRef, useState } from "react"; +import useMount from "react-use/lib/useMount"; +import { VideoUploadRecorderActions } from "./video-upload-recorder-actions"; +import { VideoUploadRecorderNoPermission } from "./video-upload-recorder-no-permission"; +import { Button } from "react-bootstrap"; +import { _t } from "../../i18n"; +import { useThreeSpeakVideoUpload } from "../../api/threespeak"; +import { error } from "../feedback"; +import { v4 } from "uuid"; +import { useUnmount } from "react-use"; + +interface Props { + setVideoUrl: (v: string) => void; + setFilevName: (v: string) => void; + setFilevSize: (v: number) => void; + setSelectedFile: (v: string) => void; + onReset: () => void; +} + +export function VideoUploadRecorder({ + setVideoUrl, + setFilevName, + setFilevSize, + onReset, + setSelectedFile +}: Props) { + const [stream, setStream] = useState(); + const [mediaRecorder, setMediaRecorder] = useState(); + const [recordedVideoSrc, setRecordedVideoSrc] = useState(); + const [recordedBlob, setRecordedBlob] = useState(); + const [noPermission, setNoPermission] = useState(false); + + const ref = useRef(null); + + const { + mutateAsync: uploadVideo, + completed, + isLoading, + isSuccess + } = useThreeSpeakVideoUpload("video"); + + useMount(async () => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ + video: true, + audio: true + }); + const mediaRecorder = new MediaRecorder(stream, { + mimeType: "video/webm" + }); + + setMediaRecorder(mediaRecorder); + setStream(stream); + + mediaRecorder.addEventListener("dataavailable", (event) => { + if (event.data.size > 0) { + setRecordedVideoSrc(URL.createObjectURL(event.data)); + setRecordedBlob(event.data); + stream.getTracks().forEach((track) => track.stop()); + } + }); + } catch (e) { + setNoPermission(true); + } + }); + + useUnmount(() => { + stream?.getTracks().forEach((track) => track.stop()); + }); + + useEffect(() => { + if (stream && ref.current) { + // @ts-ignore + ref.current?.srcObject = stream; + } + }, [stream, ref]); + + return ( +
+ {recordedBlob ? ( + + ) : ( + <> + )} + + {!noPermission && !recordedVideoSrc ? ( + + ) : ( + <> + )} + + {noPermission ? ( + + ) : ( + <> +
+ ); +} diff --git a/src/common/i18n/locales/en-US.json b/src/common/i18n/locales/en-US.json index 7412314c768..36201eb14b5 100644 --- a/src/common/i18n/locales/en-US.json +++ b/src/common/i18n/locales/en-US.json @@ -1439,6 +1439,7 @@ }, "video-upload": { "choose-video": "Select a video", + "record-video": "Record a video", "choose-thumbnail": "Set a thumbnail(optional)", "continue": "Continue", "encode": "Send for encoding", @@ -1449,7 +1450,12 @@ "preview": "Preview", "to-gallery": "Go to gallery", "congrats": "Congratulations", - "publishing": "Don't refresh this page, wait for few seconds for video to process" + "publishing": "Don't refresh this page, wait for few seconds for video to process", + "no-record-permission": "You don't have permission to record video", + "confirm-and-upload": "Confirm and upload to 3Speak", + "uploading": "Uploading..{{n}}/{{total}}", + "uploaded": "Uploaded!", + "reset": "Reset" }, "video-gallery": { "all": "All", diff --git a/src/common/img/svg.tsx b/src/common/img/svg.tsx index 0c908326cd2..27774195867 100644 --- a/src/common/img/svg.tsx +++ b/src/common/img/svg.tsx @@ -2042,3 +2042,37 @@ export const uploadSvgV = ( ); + +export const recordVideoSvg = ( + + + + + + +); + +export const circleSvg = ( + + + +); + +export const rectSvg = ( + + + +); diff --git a/yarn.lock b/yarn.lock index 058c132c026..1f96b82a5dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1949,6 +1949,11 @@ resolved "https://registry.yarnpkg.com/@types/diff-match-patch/-/diff-match-patch-1.0.32.tgz#d9c3b8c914aa8229485351db4865328337a3d09f" integrity sha512-bPYT5ECFiblzsVzyURaNhljBH2Gh1t9LowgUwciMrNAhFewLkHT2H0Mto07Y4/3KCOGZHRQll3CTtQZ0X11D/A== +"@types/dom-mediacapture-record@^1.0.16": + version "1.0.16" + resolved "https://registry.yarnpkg.com/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.16.tgz#8a37eaa32c1810519843c6406325bb78618d1daf" + integrity sha512-GB4f8Es3aTEUjDT3ojQ8IWUhb6f5tlUoxvVw65ppkImG09QRRys6V6t56uLAynZCD3LisMQDuVObOKnnllDtCQ== + "@types/express-serve-static-core@^4.17.18": version "4.17.24" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz#ea41f93bf7e0d59cd5a76665068ed6aab6815c07" From a70e18fce6deae1c7ecb301ae7ec24268d154ff3 Mon Sep 17 00:00:00 2001 From: "ildar.timerbaev" Date: Sun, 20 Aug 2023 23:41:12 +0300 Subject: [PATCH 2/2] Video recorder: restrict media recorder by availability --- src/common/components/video-upload-threespeak/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/common/components/video-upload-threespeak/index.tsx b/src/common/components/video-upload-threespeak/index.tsx index 8be1d84a4fa..41c45d98929 100644 --- a/src/common/components/video-upload-threespeak/index.tsx +++ b/src/common/components/video-upload-threespeak/index.tsx @@ -124,9 +124,7 @@ export const VideoUpload = (props: Props & React.HTMLAttributes) /> ) : (
- {selectedFile ? ( - <> - ) : ( + {!selectedFile && "MediaRecorder" in window ? (
setShowRecorder(true)} @@ -134,6 +132,8 @@ export const VideoUpload = (props: Props & React.HTMLAttributes) {recordVideoSvg} {_t("video-upload.record-video")}
+ ) : ( + <> )}