Vidtreo

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

On this page