Skip to content

Commit

Permalink
Merge pull request #1449 from ecency/feature/video-recording
Browse files Browse the repository at this point in the history
Video recorder
  • Loading branch information
feruzm authored Sep 8, 2023
2 parents ee3f563 + a70e18f commit 978180c
Show file tree
Hide file tree
Showing 9 changed files with 352 additions and 16 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,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",
Expand Down
65 changes: 59 additions & 6 deletions src/common/components/video-upload-threespeak/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
@include themify(night) {
background-color: lighten($dark, 6);
}

}
}

Expand Down Expand Up @@ -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;
}
}
}
47 changes: 38 additions & 9 deletions src/common/components/video-upload-threespeak/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -42,8 +44,9 @@ export const VideoUpload = (props: Props & React.HTMLAttributes<HTMLDivElement>)
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(() => {
Expand All @@ -60,6 +63,7 @@ export const VideoUpload = (props: Props & React.HTMLAttributes<HTMLDivElement>)
setStep("upload");
setVideoPercentage(0);
setThumbnailPercentage(0);
setShowRecorder(false);
}
}, [props.show]);

Expand Down Expand Up @@ -108,14 +112,39 @@ export const VideoUpload = (props: Props & React.HTMLAttributes<HTMLDivElement>)

const uploadVideoModal = (
<div className="dialog-content ">
<div className="three-speak-video-uploading">
<VideoUploadItem
label={_t("video-upload.choose-video")}
onFileChange={handleVideoChange}
type="video"
accept="video/*"
completed={videoPercentage}
/>
<div className="three-speak-video-uploading position-relative">
<p className="font-weight-bold">Video source</p>
{showRecorder ? (
<VideoUploadRecorder
setVideoUrl={setVideoUrl}
setFilevName={setFilevName}
setFilevSize={setFilevSize}
setSelectedFile={setSelectedFile}
onReset={() => setShowRecorder(false)}
/>
) : (
<div className="video-source">
{!selectedFile && "MediaRecorder" in window ? (
<div
className="d-flex align-items-center flex-column border rounded p-3 video-upload-item"
onClick={() => setShowRecorder(true)}
>
{recordVideoSvg}
{_t("video-upload.record-video")}
</div>
) : (
<></>
)}
<VideoUploadItem
label={_t("video-upload.choose-video")}
onFileChange={handleVideoChange}
type="video"
accept="video/*"
completed={videoPercentage}
/>
</div>
)}
<p className="font-weight-bold mt-5">Thumbnail</p>
<VideoUploadItem
label={_t("video-upload.choose-thumbnail")}
onFileChange={handleThumbnailChange}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { circleSvg, rectSvg } from "../../img/svg";
import React, { useState } from "react";

interface Props {
noPermission: boolean;
mediaRecorder?: MediaRecorder;
}

export function VideoUploadRecorderActions({ noPermission, mediaRecorder }: Props) {
const [recordStarted, setRecordStarted] = useState(false);

return (
<div className="actions">
{recordStarted ? (
<div
aria-disabled={noPermission}
className="record-btn"
onClick={() => {
mediaRecorder?.stop();
setRecordStarted(false);
}}
>
{rectSvg}
</div>
) : (
<div
aria-disabled={noPermission}
className="record-btn"
onClick={() => {
mediaRecorder?.start();
setRecordStarted(true);
}}
>
{circleSvg}
</div>
)}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { _t } from "../../i18n";
import React from "react";

export function VideoUploadRecorderNoPermission() {
return (
<div className="no-permission">
<p>{_t("video-upload.no-record-permission")}</p>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -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<MediaStream>();
const [mediaRecorder, setMediaRecorder] = useState<MediaRecorder>();
const [recordedVideoSrc, setRecordedVideoSrc] = useState<string>();
const [recordedBlob, setRecordedBlob] = useState<Blob>();
const [noPermission, setNoPermission] = useState(false);

const ref = useRef<HTMLVideoElement | null>(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 (
<div className="video-upload-recorder">
{recordedBlob ? (
<Button className="reset-btn" variant="link" onClick={() => onReset()}>
{_t("video-upload.reset")}
</Button>
) : (
<></>
)}

{!noPermission && !recordedVideoSrc ? (
<VideoUploadRecorderActions noPermission={noPermission} mediaRecorder={mediaRecorder} />
) : (
<></>
)}

{noPermission ? (
<VideoUploadRecorderNoPermission />
) : (
<>
<video
hidden={!recordedVideoSrc}
controls={true}
src={recordedVideoSrc}
autoPlay={false}
playsInline={true}
id="videoRecorded"
/>
<video
hidden={!!recordedVideoSrc}
ref={ref}
muted={true}
autoPlay={true}
playsInline={true}
id="videoLive"
/>
{recordedVideoSrc ? (
<div className="d-flex align-items-center justify-content-center mt-3">
{recordedBlob && isSuccess ? (
<div className="bg-success text-white p-3 text-sm rounded-pill w-100">
{_t("video-upload.uploaded")}
</div>
) : (
<Button
disabled={isLoading}
onClick={async () => {
if (!recordedBlob) {
return;
}

try {
const file = new File([recordedBlob], `ecency-recorder-${v4()}.webm`, {
type: "video/webm"
});
const result = await uploadVideo({
file
});
if (result) {
setVideoUrl(result.fileUrl);
setFilevName(result.fileName);
setFilevSize(result.fileSize);
setSelectedFile(URL.createObjectURL(file));
}
} catch (e) {
error(e);
}
}}
>
{isLoading
? _t("video-upload.uploading", { n: completed, total: 100 })
: _t("video-upload.confirm-and-upload")}
</Button>
)}
</div>
) : (
<></>
)}
</>
)}
</div>
);
}
8 changes: 7 additions & 1 deletion src/common/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
Loading

0 comments on commit 978180c

Please sign in to comment.