@edifice.io/react 2.3.1 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hooks/useDate/useDate.d.ts +16 -3
- package/dist/hooks/useDate/useDate.js +1 -1
- package/dist/modules/multimedia/VideoRecorder/VideoRecorder.js +34 -143
- package/dist/modules/multimedia/VideoRecorder/VideoRecorderToolbar.d.ts +18 -0
- package/dist/modules/multimedia/VideoRecorder/VideoRecorderToolbar.js +96 -0
- package/dist/modules/multimedia/VideoRecorder/useCameras.d.ts +10 -0
- package/dist/modules/multimedia/VideoRecorder/useCameras.js +86 -0
- package/package.json +6 -6
|
@@ -6,10 +6,23 @@ export type NumberDate = number;
|
|
|
6
6
|
/** Date formats we are going to deal with. */
|
|
7
7
|
export type CoreDate = IsoDate | MongoDate | NumberDate;
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* Custom React hook for date parsing, formatting, and localization.
|
|
10
|
+
*
|
|
11
|
+
* Provides utility functions to:
|
|
12
|
+
* - Parse various date formats and timestamps into Dayjs objects, respecting the current language.
|
|
13
|
+
* - Format dates in user-friendly ways, including "time ago", "yesterday", and localized date strings.
|
|
14
|
+
* - Compute elapsed durations from a given date to now.
|
|
15
|
+
*
|
|
16
|
+
* @returns An object containing:
|
|
17
|
+
* - `fromNow(date: CoreDate | NumberDate): string` — Returns a human-readable elapsed duration from the given date to now.
|
|
18
|
+
* - `formatDate(date: CoreDate, format?: 'short' | 'long' | 'abbr' | string): string` — Formats a date according to the specified format and current language.
|
|
19
|
+
* - `formatTimeAgo(date: CoreDate | NumberDate): string` — Returns a localized string representing how long ago the date was, with special handling for today, yesterday, and recent dates.
|
|
20
|
+
*
|
|
21
|
+
* @remarks
|
|
22
|
+
* - Uses the current language from the Edifice client context for localization.
|
|
10
23
|
*/
|
|
11
24
|
export default function useDate(): {
|
|
12
|
-
fromNow: (date: CoreDate
|
|
25
|
+
fromNow: (date: CoreDate) => string;
|
|
13
26
|
formatDate: (date: CoreDate, format?: string) => string;
|
|
14
|
-
formatTimeAgo: (date: CoreDate
|
|
27
|
+
formatTimeAgo: (date: CoreDate) => string;
|
|
15
28
|
};
|
|
@@ -8,8 +8,8 @@ import "dayjs/locale/es.js";
|
|
|
8
8
|
import "dayjs/locale/fr.js";
|
|
9
9
|
import "dayjs/locale/it.js";
|
|
10
10
|
import "dayjs/locale/pt.js";
|
|
11
|
-
import { useEdificeClient } from "../../providers/EdificeClientProvider/EdificeClientProvider.hook.js";
|
|
12
11
|
import { useTranslation } from "react-i18next";
|
|
12
|
+
import { useEdificeClient } from "../../providers/EdificeClientProvider/EdificeClientProvider.hook.js";
|
|
13
13
|
dayjs.extend(relativeTime);
|
|
14
14
|
dayjs.extend(customParseFormat);
|
|
15
15
|
dayjs.extend(localizedFormat);
|
|
@@ -1,23 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef, useState, useRef, useImperativeHandle, useEffect, useCallback } from "react";
|
|
3
3
|
import { odeServices } from "@edifice.io/client";
|
|
4
4
|
import { getBestSupportedMimeType, convertMsToMS } from "@edifice.io/utilities";
|
|
5
5
|
import { useTranslation } from "react-i18next";
|
|
6
|
-
import SvgIconPause from "../../icons/components/IconPause.js";
|
|
7
|
-
import SvgIconPlayFilled from "../../icons/components/IconPlayFilled.js";
|
|
8
|
-
import SvgIconRecordStop from "../../icons/components/IconRecordStop.js";
|
|
9
6
|
import SvgIconRecordVideo from "../../icons/components/IconRecordVideo.js";
|
|
10
7
|
import SvgIconRecord from "../../icons/components/IconRecord.js";
|
|
11
|
-
import
|
|
12
|
-
import
|
|
8
|
+
import { useCameras } from "./useCameras.js";
|
|
9
|
+
import { VideoRecorderToolbar } from "./VideoRecorderToolbar.js";
|
|
13
10
|
import useUpload from "../../../hooks/useUpload/useUpload.js";
|
|
14
|
-
import useBrowserInfo from "../../../hooks/useBrowserInfo/useBrowserInfo.js";
|
|
15
11
|
import FormControl from "../../../components/Form/FormControl.js";
|
|
16
12
|
import Label from "../../../components/Label/Label.js";
|
|
17
13
|
import Select from "../../../components/Select/Select.js";
|
|
18
14
|
import LoadingScreen from "../../../components/LoadingScreen/LoadingScreen.js";
|
|
19
|
-
|
|
20
|
-
const VIDEO_HEIGHT = 9, VIDEO_WIDTH = 16, VideoRecorder = /* @__PURE__ */ forwardRef(({
|
|
15
|
+
const VideoRecorder = /* @__PURE__ */ forwardRef(({
|
|
21
16
|
appCode,
|
|
22
17
|
caption,
|
|
23
18
|
onSuccess,
|
|
@@ -25,17 +20,14 @@ const VIDEO_HEIGHT = 9, VIDEO_WIDTH = 16, VideoRecorder = /* @__PURE__ */ forwar
|
|
|
25
20
|
onRecordUpdated,
|
|
26
21
|
hideSaveAction = !1
|
|
27
22
|
}, ref) => {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}), [stream, setStream] = useState(), [mimeType, setMimeType] = useState(""), [recordedChunks, setRecordedChunks] = useState([]), [recordedVideo, setRecordedVideo] = useState(), [recordedTime, setRecordedTime] = useState(0), [playedTime, setPlayedTime] = useState(0), videoRef = useRef(null), recorderRef = useRef(null), {
|
|
23
|
+
const {
|
|
24
|
+
inputDevices,
|
|
25
|
+
setPreferedDevice,
|
|
26
|
+
restartStream,
|
|
27
|
+
stream
|
|
28
|
+
} = useCameras(), [maxDuration, setMaxDuration] = useState(18e4), [recording, setRecording] = useState(!1), [recorded, setRecorded] = useState(!1), [playing, setPlaying] = useState(!1), [saving, setSaving] = useState(!1), [saved, setSaved] = useState(!1), [mimeType, setMimeType] = useState(""), [recordedChunks, setRecordedChunks] = useState([]), [recordedVideo, setRecordedVideo] = useState(), [recordedTime, setRecordedTime] = useState(0), [playedTime, setPlayedTime] = useState(0), videoRef = useRef(null), recorderRef = useRef(null), {
|
|
35
29
|
uploadBlob
|
|
36
|
-
} = useUpload(void 0, appCode)
|
|
37
|
-
device
|
|
38
|
-
} = useBrowserInfo(navigator.userAgent);
|
|
30
|
+
} = useUpload(void 0, appCode);
|
|
39
31
|
useImperativeHandle(ref, () => ({
|
|
40
32
|
save: handleSave
|
|
41
33
|
}));
|
|
@@ -43,10 +35,23 @@ const VIDEO_HEIGHT = 9, VIDEO_WIDTH = 16, VideoRecorder = /* @__PURE__ */ forwar
|
|
|
43
35
|
t
|
|
44
36
|
} = useTranslation();
|
|
45
37
|
useEffect(() => {
|
|
46
|
-
initMaxDuration()
|
|
47
|
-
}, [])
|
|
48
|
-
|
|
49
|
-
|
|
38
|
+
initMaxDuration();
|
|
39
|
+
}, []);
|
|
40
|
+
async function initMaxDuration() {
|
|
41
|
+
try {
|
|
42
|
+
const videoConfResponse = await odeServices.video().getVideoConf();
|
|
43
|
+
setMaxDuration(videoConfResponse.maxDuration * 60 * 1e3);
|
|
44
|
+
} catch {
|
|
45
|
+
setMaxDuration(3 * 60 * 1e3);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
try {
|
|
50
|
+
videoRef.current && (videoRef.current.src && (window.URL.revokeObjectURL(videoRef.current.src), videoRef.current.src = ""), videoRef.current.srcObject instanceof MediaStream && (videoRef.current.srcObject = null), stream && (videoRef.current.srcObject = stream, videoRef.current.autoplay = !0, videoRef.current.volume = 1, videoRef.current.muted = !0, videoRef.current.load()));
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.error(err);
|
|
53
|
+
}
|
|
54
|
+
}, [stream]), useEffect(() => {
|
|
50
55
|
if (recordedChunks.length && !recording && videoRef.current) {
|
|
51
56
|
const finalVideo = new Blob(recordedChunks, {
|
|
52
57
|
type: mimeType
|
|
@@ -76,40 +81,7 @@ const VIDEO_HEIGHT = 9, VIDEO_WIDTH = 16, VideoRecorder = /* @__PURE__ */ forwar
|
|
|
76
81
|
};
|
|
77
82
|
}
|
|
78
83
|
}, [playing]);
|
|
79
|
-
const
|
|
80
|
-
const videoConfResponse = await odeServices.video().getVideoConf();
|
|
81
|
-
setMaxDuration((videoConfResponse.maxDuration ?? 3) * 60 * 1e3);
|
|
82
|
-
}, initInputDevices = async () => {
|
|
83
|
-
const videoDevices = (await navigator.mediaDevices.enumerateDevices()).filter((device2) => device2.kind === "videoinput");
|
|
84
|
-
switch (device.type) {
|
|
85
|
-
case "mobile":
|
|
86
|
-
case "tablet": {
|
|
87
|
-
const backCamera = {
|
|
88
|
-
deviceId: "environment",
|
|
89
|
-
label: t("video.back.camera"),
|
|
90
|
-
groupId: "",
|
|
91
|
-
kind: "videoinput"
|
|
92
|
-
}, frontCamera = {
|
|
93
|
-
deviceId: "user",
|
|
94
|
-
label: t("video.front.camera"),
|
|
95
|
-
groupId: "",
|
|
96
|
-
kind: "videoinput"
|
|
97
|
-
};
|
|
98
|
-
(videoDevices == null ? void 0 : videoDevices.length) > 1 ? setInputDevices([backCamera, frontCamera]) : setInputDevices([backCamera]);
|
|
99
|
-
break;
|
|
100
|
-
}
|
|
101
|
-
default:
|
|
102
|
-
setInputDevices(videoDevices);
|
|
103
|
-
break;
|
|
104
|
-
}
|
|
105
|
-
}, enableStream = async (mediaStreamConstraints2) => {
|
|
106
|
-
try {
|
|
107
|
-
const mediaStream = await navigator.mediaDevices.getUserMedia(mediaStreamConstraints2);
|
|
108
|
-
setStream(mediaStream), videoRef.current && (videoRef.current.src && (window.URL.revokeObjectURL(videoRef.current.src), videoRef.current.src = ""), videoRef.current.srcObject = mediaStream, videoRef.current.autoplay = !0, videoRef.current.volume = 1, videoRef.current.muted = !0);
|
|
109
|
-
} catch (err) {
|
|
110
|
-
console.error(err);
|
|
111
|
-
}
|
|
112
|
-
}, handleRecord = useCallback(() => {
|
|
84
|
+
const handleRecord = useCallback(() => {
|
|
113
85
|
setRecording(!0), videoRef && videoRef.current && (videoRef.current.muted = !0);
|
|
114
86
|
const mimeType2 = getBestSupportedMimeType();
|
|
115
87
|
setMimeType(mimeType2), stream && (recorderRef.current = new MediaRecorder(stream, {
|
|
@@ -126,7 +98,7 @@ const VIDEO_HEIGHT = 9, VIDEO_WIDTH = 16, VideoRecorder = /* @__PURE__ */ forwar
|
|
|
126
98
|
var _a, _b;
|
|
127
99
|
videoRef && videoRef.current && (videoRef.current.muted = !1), playing ? ((_b = videoRef == null ? void 0 : videoRef.current) == null || _b.pause(), setPlaying(!1)) : ((_a = videoRef == null ? void 0 : videoRef.current) == null || _a.play(), setPlaying(!0));
|
|
128
100
|
}, [playing]), handleReset = () => {
|
|
129
|
-
setRecorded(!1), setRecording(!1), setPlaying(!1), setSaved(!1), setRecordedTime(0), setRecordedChunks([]), setRecordedVideo(void 0),
|
|
101
|
+
setRecorded(!1), setRecording(!1), setPlaying(!1), setSaved(!1), setRecordedTime(0), setRecordedChunks([]), setRecordedVideo(void 0), restartStream(), onRecordUpdated && onRecordUpdated();
|
|
130
102
|
}, handleSave = async () => {
|
|
131
103
|
var _a;
|
|
132
104
|
if ((_a = videoRef == null ? void 0 : videoRef.current) == null || _a.pause(), setSaving(!0), !recordedVideo) {
|
|
@@ -144,92 +116,11 @@ const VIDEO_HEIGHT = 9, VIDEO_WIDTH = 16, VideoRecorder = /* @__PURE__ */ forwar
|
|
|
144
116
|
}, handleInputDeviceChange = (option) => {
|
|
145
117
|
var _a;
|
|
146
118
|
const selectedDevice = inputDevices.find((inputDevice) => inputDevice.label === option);
|
|
147
|
-
|
|
148
|
-
selectedDevice != null && selectedDevice.deviceId ? ((selectedDevice == null ? void 0 : selectedDevice.deviceId) === "environment" || (selectedDevice == null ? void 0 : selectedDevice.deviceId) === "user" ? mediaStreamConstraints2 = {
|
|
149
|
-
audio: !0,
|
|
150
|
-
video: {
|
|
151
|
-
aspectRatio: VIDEO_WIDTH / VIDEO_HEIGHT,
|
|
152
|
-
facingMode: selectedDevice == null ? void 0 : selectedDevice.deviceId
|
|
153
|
-
}
|
|
154
|
-
} : mediaStreamConstraints2 = {
|
|
155
|
-
audio: !0,
|
|
156
|
-
video: {
|
|
157
|
-
aspectRatio: VIDEO_WIDTH / VIDEO_HEIGHT,
|
|
158
|
-
deviceId: selectedDevice.deviceId
|
|
159
|
-
}
|
|
160
|
-
}, setMediaStreamConstraints(mediaStreamConstraints2), stream && (((_a = recorderRef.current) == null ? void 0 : _a.state) === "recording" && (recorderRef.current.requestData(), recorderRef.current.stop()), stream.getTracks().forEach((track) => track.stop()), setStream(void 0)), enableStream(mediaStreamConstraints2)) : console.error("Selected input device id is null");
|
|
119
|
+
((_a = recorderRef.current) == null ? void 0 : _a.state) === "recording" && (recorderRef.current.requestData(), recorderRef.current.stop()), setPreferedDevice(selectedDevice);
|
|
161
120
|
};
|
|
162
|
-
useEffect(() => {
|
|
121
|
+
return useEffect(() => {
|
|
163
122
|
recordedTime >= maxDuration && handleStop();
|
|
164
|
-
}, [recordedTime, handleStop])
|
|
165
|
-
const toolbarItems = [{
|
|
166
|
-
type: "icon",
|
|
167
|
-
name: "record",
|
|
168
|
-
props: {
|
|
169
|
-
icon: /* @__PURE__ */ jsx(SvgIconRecord, { color: recording || recorded ? "" : "red" }),
|
|
170
|
-
color: "danger",
|
|
171
|
-
disabled: recording || recorded || saving,
|
|
172
|
-
onClick: handleRecord,
|
|
173
|
-
"aria-label": t("bbm.video.record.start")
|
|
174
|
-
},
|
|
175
|
-
tooltip: t("bbm.video.record.start")
|
|
176
|
-
}, {
|
|
177
|
-
type: "icon",
|
|
178
|
-
name: "stop",
|
|
179
|
-
props: {
|
|
180
|
-
icon: /* @__PURE__ */ jsx(SvgIconRecordStop, {}),
|
|
181
|
-
disabled: !recording || recorded || saving,
|
|
182
|
-
onClick: handleStop,
|
|
183
|
-
"aria-label": t("bbm.video.record.stop")
|
|
184
|
-
},
|
|
185
|
-
tooltip: t("bbm.video.record.stop")
|
|
186
|
-
}, {
|
|
187
|
-
type: "icon",
|
|
188
|
-
name: "play",
|
|
189
|
-
visibility: playing ? "hide" : "show",
|
|
190
|
-
props: {
|
|
191
|
-
icon: /* @__PURE__ */ jsx(SvgIconPlayFilled, {}),
|
|
192
|
-
disabled: !recorded || saving,
|
|
193
|
-
onClick: handlePlayPause,
|
|
194
|
-
"aria-label": t("bbm.video.play.start")
|
|
195
|
-
},
|
|
196
|
-
tooltip: t("bbm.video.play.start")
|
|
197
|
-
}, {
|
|
198
|
-
type: "icon",
|
|
199
|
-
name: "pause",
|
|
200
|
-
visibility: playing ? "show" : "hide",
|
|
201
|
-
props: {
|
|
202
|
-
icon: /* @__PURE__ */ jsx(SvgIconPause, {}),
|
|
203
|
-
disabled: !recorded || saving,
|
|
204
|
-
onClick: handlePlayPause,
|
|
205
|
-
"aria-label": t("bbm.video.play.pause")
|
|
206
|
-
},
|
|
207
|
-
tooltip: t("bbm.video.play.pause")
|
|
208
|
-
}, {
|
|
209
|
-
type: "divider"
|
|
210
|
-
}, {
|
|
211
|
-
type: "icon",
|
|
212
|
-
name: "reset",
|
|
213
|
-
props: {
|
|
214
|
-
icon: /* @__PURE__ */ jsx(SvgIconRefresh, {}),
|
|
215
|
-
disabled: !recorded || saving,
|
|
216
|
-
onClick: handleReset,
|
|
217
|
-
"aria-label": t("bbm.video.record.reset")
|
|
218
|
-
},
|
|
219
|
-
tooltip: t("bbm.video.record.reset")
|
|
220
|
-
}, {
|
|
221
|
-
type: "icon",
|
|
222
|
-
name: "save",
|
|
223
|
-
visibility: hideSaveAction ? "hide" : "show",
|
|
224
|
-
props: {
|
|
225
|
-
icon: /* @__PURE__ */ jsx(SvgIconSave, {}),
|
|
226
|
-
disabled: !recorded || saving || saved,
|
|
227
|
-
onClick: handleSave,
|
|
228
|
-
"aria-label": t("bbm.video.record.save")
|
|
229
|
-
},
|
|
230
|
-
tooltip: t("bbm.video.record.save")
|
|
231
|
-
}];
|
|
232
|
-
return /* @__PURE__ */ jsxs("div", { className: "video-recorder d-flex flex-fill flex-column align-items-center pb-8", children: [
|
|
123
|
+
}, [recordedTime, handleStop]), /* @__PURE__ */ jsxs("div", { className: "video-recorder d-flex flex-fill flex-column align-items-center pb-8", children: [
|
|
233
124
|
/* @__PURE__ */ jsx("div", { className: "video-recorder-caption d-none d-md-block", children: caption }),
|
|
234
125
|
inputDevices.length > 1 && /* @__PURE__ */ jsx("div", { className: "video-recorder-devices mb-12", children: /* @__PURE__ */ jsxs(FormControl, { id: "selectInputDevice", children: [
|
|
235
126
|
/* @__PURE__ */ jsx(Label, { className: "d-none d-md-block", children: t("bbm.video.record.select.devices.label") }),
|
|
@@ -255,7 +146,7 @@ const VIDEO_HEIGHT = 9, VIDEO_WIDTH = 16, VideoRecorder = /* @__PURE__ */ forwar
|
|
|
255
146
|
] })
|
|
256
147
|
] })
|
|
257
148
|
] }),
|
|
258
|
-
stream && /* @__PURE__ */ jsx(
|
|
149
|
+
stream && /* @__PURE__ */ jsx(VideoRecorderToolbar, { playing, recording, recorded, saving, saved, hideSaveAction, handleRecord, handleStop, handlePlayPause, handleReset, handleSave })
|
|
259
150
|
] }),
|
|
260
151
|
saving && /* @__PURE__ */ jsx(LoadingScreen, { position: !1, caption: t("bbm.video.save.loader.caption") })
|
|
261
152
|
] });
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { WorkspaceElement } from '@edifice.io/client';
|
|
2
|
+
export interface VideoRecorderToolbarProps {
|
|
3
|
+
playing: boolean;
|
|
4
|
+
recording: boolean;
|
|
5
|
+
recorded: boolean;
|
|
6
|
+
saving: boolean;
|
|
7
|
+
saved: boolean;
|
|
8
|
+
hideSaveAction: boolean;
|
|
9
|
+
handleRecord: () => void;
|
|
10
|
+
handleStop: () => void;
|
|
11
|
+
handlePlayPause: () => void;
|
|
12
|
+
handleReset: () => void;
|
|
13
|
+
handleSave: () => void;
|
|
14
|
+
}
|
|
15
|
+
export interface VideoRecorderRef {
|
|
16
|
+
save: () => Promise<WorkspaceElement | undefined>;
|
|
17
|
+
}
|
|
18
|
+
export declare const VideoRecorderToolbar: ({ playing, recording, recorded, saving, saved, hideSaveAction, handleRecord, handleStop, handlePlayPause, handleReset, handleSave, }: VideoRecorderToolbarProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useTranslation } from "react-i18next";
|
|
3
|
+
import SvgIconPause from "../../icons/components/IconPause.js";
|
|
4
|
+
import SvgIconPlayFilled from "../../icons/components/IconPlayFilled.js";
|
|
5
|
+
import SvgIconRecordStop from "../../icons/components/IconRecordStop.js";
|
|
6
|
+
import SvgIconRecord from "../../icons/components/IconRecord.js";
|
|
7
|
+
import SvgIconRefresh from "../../icons/components/IconRefresh.js";
|
|
8
|
+
import SvgIconSave from "../../icons/components/IconSave.js";
|
|
9
|
+
import { Toolbar } from "../../../components/Toolbar/Toolbar.js";
|
|
10
|
+
const VideoRecorderToolbar = ({
|
|
11
|
+
playing,
|
|
12
|
+
recording,
|
|
13
|
+
recorded,
|
|
14
|
+
saving,
|
|
15
|
+
saved,
|
|
16
|
+
hideSaveAction,
|
|
17
|
+
handleRecord,
|
|
18
|
+
handleStop,
|
|
19
|
+
handlePlayPause,
|
|
20
|
+
handleReset,
|
|
21
|
+
handleSave
|
|
22
|
+
}) => {
|
|
23
|
+
const {
|
|
24
|
+
t
|
|
25
|
+
} = useTranslation(), toolbarItems = [{
|
|
26
|
+
type: "icon",
|
|
27
|
+
name: "record",
|
|
28
|
+
props: {
|
|
29
|
+
icon: /* @__PURE__ */ jsx(SvgIconRecord, { color: recording || recorded ? "" : "red" }),
|
|
30
|
+
color: "danger",
|
|
31
|
+
disabled: recording || recorded || saving,
|
|
32
|
+
onClick: handleRecord,
|
|
33
|
+
"aria-label": t("bbm.video.record.start")
|
|
34
|
+
},
|
|
35
|
+
tooltip: t("bbm.video.record.start")
|
|
36
|
+
}, {
|
|
37
|
+
type: "icon",
|
|
38
|
+
name: "stop",
|
|
39
|
+
props: {
|
|
40
|
+
icon: /* @__PURE__ */ jsx(SvgIconRecordStop, {}),
|
|
41
|
+
disabled: !recording || recorded || saving,
|
|
42
|
+
onClick: handleStop,
|
|
43
|
+
"aria-label": t("bbm.video.record.stop")
|
|
44
|
+
},
|
|
45
|
+
tooltip: t("bbm.video.record.stop")
|
|
46
|
+
}, {
|
|
47
|
+
type: "icon",
|
|
48
|
+
name: "play",
|
|
49
|
+
visibility: playing ? "hide" : "show",
|
|
50
|
+
props: {
|
|
51
|
+
icon: /* @__PURE__ */ jsx(SvgIconPlayFilled, {}),
|
|
52
|
+
disabled: !recorded || saving,
|
|
53
|
+
onClick: handlePlayPause,
|
|
54
|
+
"aria-label": t("bbm.video.play.start")
|
|
55
|
+
},
|
|
56
|
+
tooltip: t("bbm.video.play.start")
|
|
57
|
+
}, {
|
|
58
|
+
type: "icon",
|
|
59
|
+
name: "pause",
|
|
60
|
+
visibility: playing ? "show" : "hide",
|
|
61
|
+
props: {
|
|
62
|
+
icon: /* @__PURE__ */ jsx(SvgIconPause, {}),
|
|
63
|
+
disabled: !recorded || saving,
|
|
64
|
+
onClick: handlePlayPause,
|
|
65
|
+
"aria-label": t("bbm.video.play.pause")
|
|
66
|
+
},
|
|
67
|
+
tooltip: t("bbm.video.play.pause")
|
|
68
|
+
}, {
|
|
69
|
+
type: "divider"
|
|
70
|
+
}, {
|
|
71
|
+
type: "icon",
|
|
72
|
+
name: "reset",
|
|
73
|
+
props: {
|
|
74
|
+
icon: /* @__PURE__ */ jsx(SvgIconRefresh, {}),
|
|
75
|
+
disabled: !recorded || saving,
|
|
76
|
+
onClick: handleReset,
|
|
77
|
+
"aria-label": t("bbm.video.record.reset")
|
|
78
|
+
},
|
|
79
|
+
tooltip: t("bbm.video.record.reset")
|
|
80
|
+
}, {
|
|
81
|
+
type: "icon",
|
|
82
|
+
name: "save",
|
|
83
|
+
visibility: hideSaveAction ? "hide" : "show",
|
|
84
|
+
props: {
|
|
85
|
+
icon: /* @__PURE__ */ jsx(SvgIconSave, {}),
|
|
86
|
+
disabled: !recorded || saving || saved,
|
|
87
|
+
onClick: handleSave,
|
|
88
|
+
"aria-label": t("bbm.video.record.save")
|
|
89
|
+
},
|
|
90
|
+
tooltip: t("bbm.video.record.save")
|
|
91
|
+
}];
|
|
92
|
+
return /* @__PURE__ */ jsx(Toolbar, { items: toolbarItems, className: "position-absolute bottom-0 start-50 bg-white" });
|
|
93
|
+
};
|
|
94
|
+
export {
|
|
95
|
+
VideoRecorderToolbar
|
|
96
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function useCameras(): {
|
|
2
|
+
/** Readonly list (array) of available video input devices. */
|
|
3
|
+
inputDevices: MediaDeviceInfo[];
|
|
4
|
+
/** Select which input video device to use. */
|
|
5
|
+
setPreferedDevice: (device?: MediaDeviceInfo) => void;
|
|
6
|
+
/** The current video stream. */
|
|
7
|
+
stream: MediaStream | undefined;
|
|
8
|
+
/** Start a video stream from the default or prefered device. */
|
|
9
|
+
restartStream: () => void;
|
|
10
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { useTranslation } from "react-i18next";
|
|
3
|
+
import useBrowserInfo from "../../../hooks/useBrowserInfo/useBrowserInfo.js";
|
|
4
|
+
const VIDEO_HEIGHT = 9, VIDEO_WIDTH = 16;
|
|
5
|
+
function useCameras() {
|
|
6
|
+
const {
|
|
7
|
+
device
|
|
8
|
+
} = useBrowserInfo(navigator.userAgent), {
|
|
9
|
+
t
|
|
10
|
+
} = useTranslation(), [inputDevices, setInputDevices] = useState([]), [mediaStreamConstraints, setMediaStreamConstraints] = useState({
|
|
11
|
+
audio: !0,
|
|
12
|
+
video: !0
|
|
13
|
+
}), [stream, setStream] = useState();
|
|
14
|
+
async function getVideoInputDevices() {
|
|
15
|
+
return (await navigator.mediaDevices.enumerateDevices()).filter((device2) => device2.kind === "videoinput");
|
|
16
|
+
}
|
|
17
|
+
const enableStream = async (constraints) => {
|
|
18
|
+
try {
|
|
19
|
+
const mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
20
|
+
setStream(mediaStream);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
console.error(err);
|
|
23
|
+
}
|
|
24
|
+
}, restartStream = () => {
|
|
25
|
+
stream == null || stream.getTracks().forEach((track) => track.stop()), enableStream(mediaStreamConstraints);
|
|
26
|
+
}, setPreferedDevice = (device2) => {
|
|
27
|
+
let constraints = {};
|
|
28
|
+
device2 != null && device2.deviceId ? (device2.deviceId === "environment" || device2.deviceId === "user" ? constraints = {
|
|
29
|
+
audio: !0,
|
|
30
|
+
video: {
|
|
31
|
+
aspectRatio: VIDEO_WIDTH / VIDEO_HEIGHT,
|
|
32
|
+
facingMode: device2 == null ? void 0 : device2.deviceId
|
|
33
|
+
}
|
|
34
|
+
} : constraints = {
|
|
35
|
+
audio: !0,
|
|
36
|
+
video: {
|
|
37
|
+
aspectRatio: VIDEO_WIDTH / VIDEO_HEIGHT,
|
|
38
|
+
deviceId: {
|
|
39
|
+
exact: device2.deviceId
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}, setMediaStreamConstraints(constraints)) : console.error("Selected input device id is null");
|
|
43
|
+
};
|
|
44
|
+
return useEffect(() => {
|
|
45
|
+
restartStream();
|
|
46
|
+
}, [mediaStreamConstraints]), useEffect(() => {
|
|
47
|
+
async function initInputDevices() {
|
|
48
|
+
await enableStream(mediaStreamConstraints);
|
|
49
|
+
const videoDevices = await getVideoInputDevices();
|
|
50
|
+
switch (device.type) {
|
|
51
|
+
case "mobile":
|
|
52
|
+
case "tablet": {
|
|
53
|
+
const backCamera = {
|
|
54
|
+
deviceId: "environment",
|
|
55
|
+
label: t("video.back.camera"),
|
|
56
|
+
groupId: "",
|
|
57
|
+
kind: "videoinput"
|
|
58
|
+
}, frontCamera = {
|
|
59
|
+
deviceId: "user",
|
|
60
|
+
label: t("video.front.camera"),
|
|
61
|
+
groupId: "",
|
|
62
|
+
kind: "videoinput"
|
|
63
|
+
};
|
|
64
|
+
(videoDevices == null ? void 0 : videoDevices.length) > 1 ? setInputDevices([backCamera, frontCamera]) : setInputDevices([backCamera]);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
default:
|
|
68
|
+
setInputDevices(videoDevices);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
initInputDevices();
|
|
73
|
+
}, []), {
|
|
74
|
+
/** Readonly list (array) of available video input devices. */
|
|
75
|
+
inputDevices,
|
|
76
|
+
/** Select which input video device to use. */
|
|
77
|
+
setPreferedDevice,
|
|
78
|
+
/** The current video stream. */
|
|
79
|
+
stream,
|
|
80
|
+
/** Start a video stream from the default or prefered device. */
|
|
81
|
+
restartStream
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export {
|
|
85
|
+
useCameras
|
|
86
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edifice.io/react",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.2",
|
|
4
4
|
"description": "Edifice React Library",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -130,9 +130,9 @@
|
|
|
130
130
|
"react-slugify": "^3.0.3",
|
|
131
131
|
"swiper": "^10.1.0",
|
|
132
132
|
"ua-parser-js": "^1.0.36",
|
|
133
|
-
"@edifice.io/bootstrap": "2.3.
|
|
134
|
-
"@edifice.io/tiptap-extensions": "2.3.
|
|
135
|
-
"@edifice.io/utilities": "2.3.
|
|
133
|
+
"@edifice.io/bootstrap": "2.3.2",
|
|
134
|
+
"@edifice.io/tiptap-extensions": "2.3.2",
|
|
135
|
+
"@edifice.io/utilities": "2.3.2"
|
|
136
136
|
},
|
|
137
137
|
"devDependencies": {
|
|
138
138
|
"@babel/plugin-transform-react-pure-annotations": "^7.23.3",
|
|
@@ -163,8 +163,8 @@
|
|
|
163
163
|
"vite": "^5.4.11",
|
|
164
164
|
"vite-plugin-dts": "^4.1.0",
|
|
165
165
|
"vite-tsconfig-paths": "^5.0.1",
|
|
166
|
-
"@edifice.io/client": "2.3.
|
|
167
|
-
"@edifice.io/config": "2.3.
|
|
166
|
+
"@edifice.io/client": "2.3.2",
|
|
167
|
+
"@edifice.io/config": "2.3.2"
|
|
168
168
|
},
|
|
169
169
|
"peerDependencies": {
|
|
170
170
|
"@react-spring/web": "^9.7.5",
|