Custom UI
Build your own video recording interface with VIDTREO hooks.
Custom UI
Use the useVidtreoRecorder hook to build completely custom recording interfaces.
Complete Custom Recorder
import { useVidtreoRecorder } from '@vidtreo/recorder-react';
import { useRef, useEffect } from 'react';
import './custom-recorder.css';
function CustomRecorder() {
const videoRef = useRef<HTMLVideoElement>(null);
const { state, actions, audioLevel } = useVidtreoRecorder({
apiKey: import.meta.env.VITE_VIDTREO_API_KEY,
countdownDuration: 3000,
maxRecordingTime: 120000,
onUploadComplete: (result) => {
console.log('Video ready:', result.uploadUrl);
},
});
// Attach stream to video
useEffect(() => {
if (videoRef.current && state.stream) {
videoRef.current.srcObject = state.stream;
}
}, [state.stream]);
// Start preview on mount
useEffect(() => {
actions.startPreview();
return () => actions.cleanup();
}, []);
return (
<div className="custom-recorder">
{/* Video Preview */}
<div className="video-container">
<video ref={videoRef} autoPlay muted playsInline />
{/* Recording indicator */}
{state.recordingState === 'recording' && (
<div className="recording-indicator">
<span className="rec-dot" />
<span className="timer">{state.timer}</span>
</div>
)}
{/* Countdown overlay */}
{state.recordingState === 'countdown' && (
<div className="countdown-overlay">
<span className="countdown-number">{state.countdown}</span>
</div>
)}
</div>
{/* Audio Level Meter */}
<div className="audio-meter">
<div
className="audio-level"
style={{
width: `${audioLevel * 100}%`,
backgroundColor: audioLevel > 0.8 ? '#ef4444' : '#22c55e'
}}
/>
</div>
{/* Controls */}
<div className="controls">
{state.recordingState === 'idle' && (
<>
<button
className="btn btn-record"
onClick={() => actions.startRecording('camera')}
>
<span className="icon">⏺</span>
Record Camera
</button>
<button
className="btn btn-screen"
onClick={() => actions.startRecording('screen')}
>
<span className="icon">🖥</span>
Record Screen
</button>
</>
)}
{state.recordingState === 'recording' && (
<>
<button
className="btn btn-mute"
onClick={() => actions.toggleMute()}
>
{state.isMuted ? '🔇' : '🔊'}
</button>
<button
className="btn btn-pause"
onClick={() =>
state.isPaused
? actions.resumeRecording()
: actions.pauseRecording()
}
>
{state.isPaused ? '▶️' : '⏸'}
</button>
<button
className="btn btn-stop"
onClick={() => actions.stopRecording()}
>
<span className="icon">⏹</span>
Stop
</button>
</>
)}
</div>
{/* Device Selection */}
{state.recordingState === 'idle' && (
<div className="device-selection">
<select
value={state.devices.selectedCamera || ''}
onChange={(e) => actions.changeCamera(e.target.value)}
>
{state.devices.cameras.map((camera) => (
<option key={camera.deviceId} value={camera.deviceId}>
{camera.label || `Camera ${camera.deviceId.slice(0, 8)}`}
</option>
))}
</select>
<select
value={state.devices.selectedMic || ''}
onChange={(e) => actions.changeMic(e.target.value)}
>
{state.devices.microphones.map((mic) => (
<option key={mic.deviceId} value={mic.deviceId}>
{mic.label || `Microphone ${mic.deviceId.slice(0, 8)}`}
</option>
))}
</select>
</div>
)}
{/* Upload Progress */}
{state.uploadProgress !== null && (
<div className="upload-progress">
<div
className="progress-bar"
style={{ width: `${state.uploadProgress * 100}%` }}
/>
<span>{Math.round(state.uploadProgress * 100)}%</span>
</div>
)}
{/* Error Display */}
{state.error && (
<div className="error-message">
{state.error}
</div>
)}
</div>
);
}CSS Styles
.custom-recorder {
max-width: 640px;
margin: 0 auto;
font-family: system-ui, sans-serif;
}
.video-container {
position: relative;
background: #000;
border-radius: 12px;
overflow: hidden;
}
.video-container video {
width: 100%;
display: block;
}
.recording-indicator {
position: absolute;
top: 16px;
left: 16px;
display: flex;
align-items: center;
gap: 8px;
background: rgba(0, 0, 0, 0.6);
padding: 8px 12px;
border-radius: 20px;
color: white;
}
.rec-dot {
width: 12px;
height: 12px;
background: #ef4444;
border-radius: 50%;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.countdown-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.7);
}
.countdown-number {
font-size: 120px;
font-weight: bold;
color: white;
}
.audio-meter {
height: 4px;
background: #e5e7eb;
margin-top: 8px;
border-radius: 2px;
overflow: hidden;
}
.audio-level {
height: 100%;
transition: width 0.1s ease;
}
.controls {
display: flex;
gap: 8px;
margin-top: 16px;
justify-content: center;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
}
.btn-record { background: #ef4444; color: white; }
.btn-screen { background: #3b82f6; color: white; }
.btn-stop { background: #6b7280; color: white; }
.btn-mute, .btn-pause { background: #f3f4f6; }
.device-selection {
display: flex;
gap: 8px;
margin-top: 16px;
}
.device-selection select {
flex: 1;
padding: 8px;
border: 1px solid #e5e7eb;
border-radius: 6px;
}
.upload-progress {
margin-top: 16px;
background: #e5e7eb;
border-radius: 4px;
overflow: hidden;
position: relative;
}
.progress-bar {
height: 24px;
background: #22c55e;
transition: width 0.3s ease;
}
.error-message {
margin-top: 16px;
padding: 12px;
background: #fef2f2;
color: #991b1b;
border-radius: 8px;
}Minimal Example
For a simpler custom UI:
function MinimalRecorder() {
const { state, actions } = useVidtreoRecorder({
apiKey: import.meta.env.VITE_VIDTREO_API_KEY,
});
return (
<div>
<video
ref={(el) => el && state.stream && (el.srcObject = state.stream)}
autoPlay
muted
/>
<button
onClick={() =>
state.recordingState === 'idle'
? actions.startRecording()
: actions.stopRecording()
}
>
{state.recordingState === 'idle' ? 'Record' : 'Stop'}
</button>
</div>
);
}Next Steps
- Advanced Usage — Error handling, offline support
- Hooks API — Complete hook reference