diff --git a/apps/web/src/components/views/rooms/VideoRecordComposerTile.tsx b/apps/web/src/components/views/rooms/VideoRecordComposerTile.tsx index 851c64e..4285c72 100644 --- a/apps/web/src/components/views/rooms/VideoRecordComposerTile.tsx +++ b/apps/web/src/components/views/rooms/VideoRecordComposerTile.tsx @@ -30,6 +30,7 @@ interface IState { } const PREFERRED_VIDEO_MIME_TYPES = ["video/webm;codecs=vp8,opus", "video/webm"]; +const MAX_VIDEO_RECORDING_MS = 120000; function preferredVideoMimeType(): string | undefined { for (const mimeType of PREFERRED_VIDEO_MIME_TYPES) { @@ -39,6 +40,18 @@ function preferredVideoMimeType(): string | undefined { } } +function isPermissionDeniedError(error: unknown): boolean { + if (error instanceof DOMException) { + return error.name === "NotAllowedError" || error.name === "PermissionDeniedError"; + } + + if (error instanceof Error) { + return /permission denied|not allowed|denied permission/i.test(error.message); + } + + return false; +} + export function isVideoMessageRecorderSupported(): boolean { return ( typeof window !== "undefined" && @@ -56,6 +69,7 @@ export default class VideoRecordComposerTile extends React.PureComponent => { @@ -96,6 +111,7 @@ export default class VideoRecordComposerTile extends React.PureComponent { logger.error("Error recording video message:", ev); - this.setState({ error: _t("composer|video_message_error") }); + this.failRecording(_t("composer|video_message_error")); }; mediaRecorder.onstop = this.onRecorderStop; this.mediaRecorder = mediaRecorder; mediaRecorder.start(); + this.durationCapTimer = window.setTimeout(this.onDurationCap, MAX_VIDEO_RECORDING_MS); this.setState({ error: undefined, isRecording: true, isSending: false, previewUrl: undefined }); this.props.onRecordingStateChange(true); @@ -132,7 +150,9 @@ export default class VideoRecordComposerTile extends React.PureComponent { + this.durationCapTimer = undefined; + + if (!this.mediaRecorder || this.mediaRecorder.state === "inactive") return; + + this.stopRecording(); + }; + + private onStreamTrackEnded = (): void => { + if (!this.mediaRecorder || this.mediaRecorder.state === "inactive") return; + + logger.warn("Video message media stream ended before recording stopped"); + this.failRecording(_t("composer|video_message_error")); + }; + private stopRecording = (): void => { if (!this.mediaRecorder) return; @@ -160,6 +195,7 @@ export default class VideoRecordComposerTile extends React.PureComponent 0 && !shouldDiscardRecording; this.mediaRecorder = undefined; + this.clearDurationCapTimer(); this.stopStream(); if (this.isUnmounted) { @@ -200,7 +236,9 @@ export default class VideoRecordComposerTile extends React.PureComponent track.addEventListener("ended", this.onStreamTrackEnded)); + } + private stopStream(): void { - this.stream?.getTracks().forEach((track) => track.stop()); + this.stream?.getTracks().forEach((track) => { + track.removeEventListener("ended", this.onStreamTrackEnded); + track.stop(); + }); this.stream = undefined; } diff --git a/apps/web/src/i18n/strings/en_EN.json b/apps/web/src/i18n/strings/en_EN.json index e11fe90..83b7c21 100644 --- a/apps/web/src/i18n/strings/en_EN.json +++ b/apps/web/src/i18n/strings/en_EN.json @@ -648,6 +648,7 @@ "video_message_button": "Record video message", "video_message_cancel": "Cancel video message", "video_message_error": "Unable to record video message", + "video_message_permission_denied": "Camera permission denied", "video_message_preview": "Video message preview", "voice_message_button": "Voice Message" },