Skip to content

Commit

Permalink
Refactor rename and update Recorder component and related models
Browse files Browse the repository at this point in the history
  • Loading branch information
zboyles committed Mar 16, 2024
1 parent e98284a commit 0dedbbc
Show file tree
Hide file tree
Showing 5 changed files with 344 additions and 116 deletions.
18 changes: 18 additions & 0 deletions src/npm-fastui-bootstrap/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,24 @@ export const classNameGenerator: ClassNameGenerator = ({
'btn-secondary': props.namedStyle === 'secondary',
'btn-warning': props.namedStyle === 'warning',
}
case 'Recorder':
switch (subElement) {
case 'left-image':
return { 'me-1': !props.hideText, 'ms-0': true, 'align-middle': true }
case 'right-image':
return { 'ms-1': !props.hideText, 'me-0': true, 'align-middle': true }
case 'container':
return { 'd-flex': true, 'gap-1': props.displayStyle !== 'toggle' }
}
return {
btn: true,
'btn-primary': !props.namedStyle || props.namedStyle === 'primary',
'btn-secondary': props.namedStyle === 'secondary',
'btn-warning': props.namedStyle === 'warning',
'd-flex': true,
'flex-row': props.imagePosition === 'left' && props.displayStyle !== 'toggle',
'flex-row-reverse': props.imagePosition === 'right' && props.displayStyle !== 'toggle',
}
case 'Table':
switch (subElement) {
case 'no-data-message':
Expand Down
302 changes: 186 additions & 116 deletions src/npm-fastui/src/components/MediaRecorder.tsx
Original file line number Diff line number Diff line change
@@ -1,141 +1,211 @@
import { useEffect, useState, type FC } from 'react';

export interface MediaRecorderCompProps {
audioConstraints?: MediaStreamConstraints['audio'];
videoConstraints?: MediaStreamConstraints['video'];
peerIdentity?: string;
preferCurrentTab?: boolean;
options?: MediaRecorderOptions;
onRecordingStart?: () => void;
onRecordingComplete: (blob: Blob) => void;
hideText?: boolean;
startText?: string;
stopText?: string;
hideImage?: boolean;
startImageUrl?: string;
stopImageUrl?: string;
imagePosition?: 'left' | 'right';
displayStyle?: 'standard' | 'toggle';
}
import { useEffect, useState, useMemo, type FC, type MouseEventHandler, useRef } from 'react'

import type { Recorder } from '../models'

import { useClassName } from '../hooks/className'

export const RecorderComp: FC<Recorder> = (props) => {
const {
audioConstraints = true,
videoConstraints = false,
peerIdentity,
preferCurrentTab,
options,
submitUrl,
saveRecording,
hideText = false,
text = 'Start Recording',
stopText = 'Stop Recording',
hideImage = false,
imageUrl,
stopImageUrl,
imagePosition = 'left',
imageWidth = '24px',
imageHeight = '24px',
displayStyle = 'standard',
overrideFieldName = 'recording',
} = props
const [recordingSubmitUrl, setRecordingSubmitUrl] = useState(submitUrl)
const [saveRecordings, setSaveRecordings] = useState(saveRecording)
const [buttonStartText, setButtonStartText] = useState(text)
const [buttonStopText, setButtonStopText] = useState(stopText)
const [buttonTextVisible, setButtonTextVisible] = useState(!hideText)
const [buttonStartImageUrl, setButtonStartImageUrl] = useState(imageUrl)
const [buttonStopImageUrl, setButtonStopImageUrl] = useState(stopImageUrl ?? imageUrl)
const [buttonImageVisible, setButtonImageVisible] = useState(!hideImage)
const [buttonImageWidth, setButtonImageWidth] = useState(imageWidth)
const [buttonImageHeight, setButtonImageHeight] = useState(imageHeight)
const [buttonDisplayStyle, setButtonDisplayStyle] = useState(displayStyle)
const [mediaRecorder, setMediaRecorder] = useState<MediaRecorder | null>(null)
const mediaRecorderRef = useRef<MediaRecorder | null>(null)
const [isRecording, setIsRecording] = useState(false)
const [recordingFieldName, setRecordingFieldName] = useState(overrideFieldName)

useEffect(() => {
setRecordingSubmitUrl(submitUrl)
setSaveRecordings(saveRecording)
setButtonTextVisible(!hideText)
setButtonStartText(hideText ? '' : text)
setButtonStopText(hideText ? '' : stopText)
setButtonImageVisible(!hideImage)
setButtonStartImageUrl(hideImage ? '' : imageUrl)
setButtonStopImageUrl(hideImage ? '' : stopImageUrl ?? imageUrl)
setButtonImageWidth(imageWidth)
setButtonImageHeight(imageHeight)
setButtonDisplayStyle(displayStyle)
setRecordingFieldName(overrideFieldName)
}, [
submitUrl,
saveRecording,
hideText,
text,
stopText,
hideImage,
imageUrl,
stopImageUrl,
imageWidth,
imageHeight,
displayStyle,
overrideFieldName,
])

const handleDownloadRecording = (blob: Blob): void => {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.style.display = 'none'
a.href = url
a.download = 'recording.webm'
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
}

export const MediaRecorderComp: FC<MediaRecorderCompProps> = ({
audioConstraints = true,
videoConstraints = false,
peerIdentity,
preferCurrentTab,
options,
onRecordingStart,
onRecordingComplete,
hideText = false,
startText = 'Start Recording',
stopText = 'Stop Recording',
hideImage = false,
startImageUrl,
stopImageUrl,
imagePosition = 'left',
displayStyle = 'standard',
}) => {
const [buttonStartText, setButtonStartText] = useState(startText);
const [buttonStopText, setButtonStopText] = useState(stopText);
const [buttonTextVisible, setButtonTextVisible] = useState(!hideText);
const [buttonStartImageUrl, setButtonStartImageUrl] = useState(startImageUrl);
const [buttonStopImageUrl, setButtonStopImageUrl] = useState(stopImageUrl);
const [buttonImageVisible, setButtonImageVisible] = useState(!hideImage);
const [buttonDisplayStyle, setButtonDisplayStyle] = useState(displayStyle);
const [mediaRecorder, setMediaRecorder] = useState<MediaRecorder | null>(null);
const [isRecording, setIsRecording] = useState(false);
const mediaStreamConstraints: MediaStreamConstraints = useMemo(
() => ({
audio: audioConstraints ?? true,
video: videoConstraints ?? false,
peerIdentity,
preferCurrentTab,
}),
[audioConstraints, videoConstraints, peerIdentity, preferCurrentTab],
)

useEffect(() => {
setButtonTextVisible(!hideText);
setButtonStartText(hideText ? '' : startText);
setButtonStopText(hideText ? '' : stopText);
setButtonStartImageUrl(hideImage ? '' : startImageUrl);
setButtonStopImageUrl(hideImage ? '' : stopImageUrl);
setButtonImageVisible(!hideImage);
setButtonDisplayStyle(displayStyle);
}, [startText, stopText, hideText, startImageUrl, stopImageUrl, hideImage, displayStyle]);
mediaRecorderRef.current = mediaRecorder
}, [mediaRecorder])

useEffect(() => {
// initialize media recording
const initMediaRecorder = async () => {
const initMediaRecorder = async (): Promise<void> => {
try {
const constraints: MediaStreamConstraints = {
audio: audioConstraints ?? true,
video: videoConstraints ?? false,
peerIdentity,
preferCurrentTab,
const stream = await navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
const recorder = new MediaRecorder(stream, options)

// get recorded data
recorder.ondataavailable = async (event) => {
if (event.data.size > 0) {
const blobRecording = new Blob([event.data], { type: event.data.type })

if (saveRecordings) {
console.log('Saving recording')
handleDownloadRecording(blobRecording)
}
if (recordingSubmitUrl) {
const formData = new FormData()
formData.append(recordingFieldName, blobRecording)
const response = await fetch(recordingSubmitUrl, {
method: 'POST',
body: formData,
})
return response
}
}
}
const stream = await navigator.mediaDevices.getUserMedia(constraints);
const recorder = new MediaRecorder(stream, options);
setMediaRecorder(recorder);
setMediaRecorder(recorder)
} catch (error) {
console.error('Error initializing media recorder', error);
console.error('Error initializing media recorder', error)
}
};
}

initMediaRecorder();
initMediaRecorder()

return () => mediaRecorder?.stream?.getTracks?.()?.forEach(track => track.stop());
}, [audioConstraints, videoConstraints, peerIdentity, preferCurrentTab, options]);
return () => mediaRecorderRef.current?.stream?.getTracks?.()?.forEach((track) => track.stop())
}, [options, saveRecordings, recordingFieldName, recordingSubmitUrl, mediaStreamConstraints])

const handleStartRecording = () => {
const handleStartRecording = (): void => {
if (!mediaRecorder) {
console.error('Media recorder not initialized');
return;
console.error('Media recorder not initialized')
return
}
onRecordingStart?.();
mediaRecorder.start();
setIsRecording(true);
console.log('Recording started');
setIsRecording(true)
mediaRecorder.start()
console.log('Recording started')
}

const handleStopRecording = () => {
const handleStopRecording = (): void => {
if (!mediaRecorder) {
console.error('Media recorder not initialized');
return;
}
mediaRecorder.stop();
setIsRecording(false);
console.log('Recording stopped');

// get recorded data
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
onRecordingComplete(event.data);
}
console.error('Media recorder not initialized')
return
}
};

const handleOnClick = () => isRecording ? handleStopRecording() : handleStartRecording();
const displayText = () => isRecording ? buttonStopText : buttonStartText;
const displayImageUrl = () => isRecording ? buttonStopImageUrl : buttonStartImageUrl;

const renderButton = (text?: string, imageUrl?: string, disabled = false) => (
<button onClick={handleOnClick} disabled={disabled}>
{buttonImageVisible && imageUrl && (
<img src={imageUrl} alt="" style={{
marginRight: imagePosition === 'right' ? '0.5rem' : '0',
marginLeft: imagePosition === 'left' ? '0.5rem' : '0',
verticalAlign: 'middle',
}} />
)}
{buttonTextVisible && text}
</button>
);

const renderStandardButtons = () => (
mediaRecorder.stop()
setIsRecording(false)
console.log('Recording stopped')
}

const handleOnClick: MouseEventHandler<HTMLButtonElement> = (e) => {
e.preventDefault()
isRecording ? handleStopRecording() : handleStartRecording()
}

const ImageButton: FC<{
text?: string
imageUrl?: string
disabled?: boolean
}> = ({ text, imageUrl, disabled = false }) => {
const leftImgClassName = useClassName(props, { el: 'left-image' })
const rightImgClassName = useClassName(props, { el: 'right-image' })
const imgClassName = imagePosition === 'left' ? leftImgClassName : rightImgClassName

return (
<button
className={useClassName(props)}
onClick={handleOnClick}
disabled={disabled}
aria-label={text}
aria-disabled={disabled}
>
{buttonImageVisible && imageUrl && (
<img
className={imgClassName}
src={imageUrl}
alt={`${text}${disabled ? ' disabled' : ''} button image`}
style={{
width: buttonImageWidth,
height: buttonImageHeight,
}}
/>
)}
{buttonTextVisible && text}
</button>
)
}

const StandardButtons: FC = (): JSX.Element => (
<>
{renderButton(buttonStartText, buttonStartImageUrl, isRecording)}
{renderButton(buttonStopText, buttonStopImageUrl, !isRecording)}
<ImageButton text={buttonStartText} imageUrl={buttonStartImageUrl} disabled={isRecording} />
<ImageButton text={buttonStopText} imageUrl={buttonStopImageUrl} disabled={!isRecording} />
</>
);
)

const ToggleButton: FC = (): JSX.Element => {
const displayText = () => (isRecording ? buttonStopText : buttonStartText)
const displayImageUrl = () => (isRecording ? buttonStopImageUrl : buttonStartImageUrl)
return <ImageButton text={displayText()} imageUrl={displayImageUrl()} />
}

return (
<>
{buttonDisplayStyle === 'standard' && renderStandardButtons()}
{buttonDisplayStyle === 'toggle' && renderButton(displayText(), displayImageUrl())}
</>
);
<div className={useClassName(props, { el: 'container' })}>
{buttonDisplayStyle === 'toggle' ? <ToggleButton /> : <StandardButtons />}
</div>
)
}

7 changes: 7 additions & 0 deletions src/npm-fastui/src/components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { FooterComp } from './footer'
import { ServerLoadComp } from './ServerLoad'
import { ImageComp } from './image'
import { IframeComp } from './Iframe'
import { RecorderComp } from './MediaRecorder'
import { VideoComp } from './video'
import { FireEventComp } from './FireEvent'
import { ErrorComp } from './error'
Expand Down Expand Up @@ -71,6 +72,7 @@ export {
ServerLoadComp,
ImageComp,
IframeComp,
RecorderComp,
VideoComp,
FireEventComp,
ErrorComp,
Expand Down Expand Up @@ -156,6 +158,11 @@ export const AnyComp: FC<FastProps> = (props) => {
return <ImageComp {...props} />
case 'Iframe':
return <IframeComp {...props} />
case 'MediaTrackSettings':
case 'RecorderOptions':
return <></>
case 'Recorder':
return <RecorderComp {...props} />
case 'Video':
return <VideoComp {...props} />
case 'FireEvent':
Expand Down
Loading

0 comments on commit 0dedbbc

Please sign in to comment.