Skip to main content

Microphone Permissions

Comprehensive solution for managing microphone permissions in voice-enabled web applications
The useMicrophonePermission hook provides a robust foundation for handling microphone permissions in voice-enabled applications. This guide covers implementation patterns, best practices, and complete examples for seamless voice interaction.

Core Concepts

Permission States

The hook manages six distinct permission states that cover all possible scenarios:

Permission Flow Visualization

The permission flow follows a predictable pattern that ensures smooth user experience:

Hook API Reference

Basic Usage

Import and use the hook to manage microphone permissions in your components:
Basic Hook Usage
import { useMicrophonePermission } from '@sammy-labs/sammy-three';

const MyComponent = () => {
  const {
    state,                      // Current permission state
    error,                      // Error message if any
    stream,                     // MediaStream when granted
    isPermissionGranted,        // Boolean: permission granted
    isPermissionDenied,         // Boolean: permission denied
    needsPermission,            // Boolean: needs user action
    checkPermission,            // Check current permission
    checkPermissionWithPolling, // Check with auto-retry
    request,                    // Request permission
    refresh,                    // Refresh permission state
    stop                        // Stop stream and cleanup
  } = useMicrophonePermission({
    onMicrophonePermissionRequired: () => {
      // Callback when permission is needed
      console.log('Microphone permission required');
    }
  });
};

Return Values

state
PermissionStateType
required
Current permission state. One of: idle, prompt, granted, denied, error, unsupported
error
string | null
Error message when state is error. Null otherwise.
stream
MediaStream | null
Active media stream when permission is granted. Null otherwise.
isPermissionGranted
boolean
Convenience boolean that’s true when state is granted.
isPermissionDenied
boolean
Convenience boolean that’s true when state is denied.
needsPermission
boolean
Indicates whether user action is needed to grant permission.
checkPermission
() => Promise<PermissionCheckResult>
Checks current permission status without requesting access.
checkPermissionWithPolling
(callback?) => Promise<boolean>
Checks permission with automatic retry logic. Useful for waiting for user action.
request
() => Promise<void>
Requests microphone permission from the browser.
refresh
() => Promise<void>
Refreshes the current permission state.
stop
() => void
Stops the media stream and performs cleanup.

Implementation Patterns

UI/UX Best Practices

Clear Permission Context

Explain the Why

Always explain WHY you need microphone access before requesting. Users are more likely to grant permissions when they understand the value.
Permission Context Example
<div className="permission-context">
  <h3>Voice-Guided Experience</h3>
  <ul>
    <li>✓ Real-time voice guidance through the platform</li>
    <li>✓ Answer your questions as you navigate</li>
    <li>✓ Hands-free interaction</li>
  </ul>
  <p>We only access your microphone during active sessions.</p>
</div>

Progressive Disclosure

Wait for User Intent

Don’t request permissions immediately on page load. Wait for explicit user action to avoid feeling intrusive.
Progressive Disclosure Example
export const GuidedTour = () => {
  const [showPermissionModal, setShowPermissionModal] = useState(false);
  
  return (
    <>
      <button onClick={() => setShowPermissionModal(true)}>
        Start Guided Tour
      </button>
      
      {showPermissionModal && (
        <MicrophonePermissionModal 
          open={showPermissionModal}
          onOpenChange={setShowPermissionModal}
        />
      )}
    </>
  );
};

State-Specific Messaging

Provide clear, actionable messages for each permission state:
State Message Handler
const getStateMessage = (state: PermissionStateType, error?: string) => {
  switch (state) {
    case 'prompt':
      return {
        title: 'Enable Microphone Access',
        message: 'Click below to enable your microphone for voice guidance.',
        action: 'Enable Microphone'
      };
    
    case 'denied':
      return {
        title: 'Microphone Access Blocked',
        message: 'You\'ll need to update your browser settings to continue.',
        action: 'View Instructions'
      };
    
    case 'error':
      return {
        title: 'Microphone Error',
        message: error || 'Unable to access microphone',
        action: 'Try Again'
      };
    
    case 'granted':
      return {
        title: 'Ready to Start',
        message: 'Microphone access granted!',
        action: null
      };
    
    default:
      return {
        title: 'Checking Permissions',
        message: 'Please wait...',
        action: null
      };
  }
};

Error Handling

Common Error Scenarios

Handle different error types with specific solutions:
Error Handler
const handleMicrophoneError = (error: string) => {
  const errorHandlers = {
    'No microphone device found.': {
      icon: '🎤',
      solution: 'Please connect a microphone to your device.',
      canRetry: true
    },
    'Microphone is already in use by another application.': {
      icon: '⚠️',
      solution: 'Close other apps using your microphone (Zoom, Teams, etc.)',
      canRetry: true
    },
    'Microphone permission was denied.': {
      icon: '🚫',
      solution: 'Update browser settings to allow microphone access.',
      canRetry: false,
      showInstructions: true
    }
  };

  const handler = errorHandlers[error] || {
    icon: '❌',
    solution: 'Please try again or contact support.',
    canRetry: true
  };

  return handler;
};

Graceful Degradation

Always provide fallback options when voice isn’t available:
Adaptive Guide Component
export const AdaptiveGuide = () => {
  const { state } = useMicrophonePermission();
  const isVoiceAvailable = state === 'granted';

  return (
    <div className="guide-container">
      {isVoiceAvailable ? (
        <VoiceGuidedExperience />
      ) : (
        <TextBasedGuide 
          showVoicePrompt={state === 'prompt'}
          onEnableVoice={() => /* handle permission request */}
        />
      )}
    </div>
  );
};

Complete Integration Example

Here’s a comprehensive implementation that combines all the best practices:
/**
 * Sammy Start Walkthrough Modal
 * Modal that automatically appears when a guide is available
 * Handles microphone permission flow seamlessly
 */
import React, { useEffect, useState, useCallback } from 'react';
import { 
  useMicrophonePermission, 
  useSammyAgentContext,
  AgentMode 
} from '@sammy-labs/sammy-three';

export const SammyStartWalkthroughModal = () => {
  const [isModalOpen, setIsModalOpen] = useState(false);
  const [hasStartedWalkthrough, setHasStartedWalkthrough] = useState(false);
  const [isStarting, setIsStarting] = useState(false);
  
  const { activeSession, startAgent, guides } = useSammyAgentContext();
  const {
    state,
    error,
    isPermissionGranted,
    isPermissionDenied,
    needsPermission,
    checkPermission,
    request,
    refresh
  } = useMicrophonePermission();

  const startWalkthroughSession = useCallback(async () => {
    try {
      setHasStartedWalkthrough(true);
      await startAgent({
        agentMode: AgentMode.USER,
        guideId: guides?.currentGuide?.guideId,
      });
      setIsModalOpen(false);
      setIsStarting(false);
    } catch (error) {
      console.error('Error starting walkthrough:', error);
      setIsStarting(false);
      // Handle error appropriately
    }
  }, [startAgent, guides?.currentGuide?.guideId]);

  // Open modal immediately when guide is available but session isn't active
  useEffect(() => {
    if (guides?.currentGuide && !activeSession && !hasStartedWalkthrough) {
      setIsModalOpen(true);
      checkPermission(); // Pre-check permission
    }
  }, [guides?.currentGuide, activeSession, hasStartedWalkthrough, checkPermission]);

  // Auto-start when permission is granted while modal is open and starting
  useEffect(() => {
    if (isPermissionGranted && isModalOpen && isStarting) {
      startWalkthroughSession();
    }
  }, [isPermissionGranted, isModalOpen, isStarting, startWalkthroughSession]);

  const handleStartWalkthrough = async () => {
    setIsStarting(true);
    
    try {
      // Step 1: Check current permission state
      const permissionResult = await checkPermission();
      
      // Step 2: If permission already granted, start immediately
      if (permissionResult.state === 'granted' || isPermissionGranted) {
        await startWalkthroughSession();
        return;
      }
      
      // Step 3: Request permission if needed
      if (permissionResult.needsPermission) {
        await request();
        // The useEffect above will handle starting when permission is granted
      }
    } catch (error) {
      console.error('Error in handleStartWalkthrough:', error);
      setIsStarting(false);
    }
  };

  const handleRefresh = async () => {
    await refresh();
    // Re-check after refresh
    const result = await checkPermission();
    if (result.state === 'granted') {
      await startWalkthroughSession();
    }
  };

  const handleClose = () => {
    setIsModalOpen(false);
    setIsStarting(false);
  };

  if (!isModalOpen || !guides?.currentGuide) return null;

  const renderModalContent = () => {
    // Starting the walkthrough (permission already granted or being processed)
    if (isStarting && (isPermissionGranted || state === 'granted')) {
      return (
        <div className="sammy-modal-auto-starting">
          <div className="sammy-modal-spinner" />
          <h3>Starting Voice Assistant...</h3>
          <p>Preparing your guided experience</p>
        </div>
      );
    }

    // Permission denied - show instructions
    if (isPermissionDenied) {
      return (
        <div className="sammy-modal-permission-denied">
          <div className="sammy-modal-icon">🚫</div>
          <h3>Microphone Access Blocked</h3>
          <p>To use the voice-guided walkthrough, you need to grant microphone access:</p>
          <ol className="sammy-modal-instructions">
            <li>Click the lock/info icon in your browser's address bar</li>
            <li>Find "Microphone" in the site settings</li>
            <li>Change from "Block" to "Allow"</li>
            <li>Click "Check Again" below</li>
          </ol>
          <div className="sammy-modal-button-group">
            <button 
              className="sammy-modal-btn sammy-modal-btn-secondary"
              onClick={handleClose}
            >
              Cancel
            </button>
            <button 
              className="sammy-modal-btn sammy-modal-btn-primary"
              onClick={handleRefresh}
            >
              🔄 Check Again
            </button>
          </div>
        </div>
      );
    }

    // Error state
    if (state === 'error' && error) {
      return (
        <div className="sammy-modal-error">
          <div className="sammy-modal-icon">⚠️</div>
          <h3>Microphone Error</h3>
          <p className="sammy-modal-error-message">{error}</p>
          <div className="sammy-modal-button-group">
            <button 
              className="sammy-modal-btn sammy-modal-btn-secondary"
              onClick={handleClose}
            >
              Cancel
            </button>
            <button 
              className="sammy-modal-btn sammy-modal-btn-primary"
              onClick={handleRefresh}
            >
              Try Again
            </button>
          </div>
        </div>
      );
    }

    // Browser not supported
    if (state === 'unsupported') {
      return (
        <div className="sammy-modal-unsupported">
          <div className="sammy-modal-icon">⚠️</div>
          <h3>Browser Not Supported</h3>
          <p>Your browser doesn't support microphone access. Please use Chrome, Firefox, or Safari.</p>
          <div className="sammy-modal-button-group">
            <button 
              className="sammy-modal-btn sammy-modal-btn-primary"
              onClick={handleClose}
            >
              OK
            </button>
          </div>
        </div>
      );
    }

    // Default: Initial prompt to start walkthrough
    return (
      <div className="sammy-modal-initial">
        <div className="sammy-modal-icon">
          <div className="sammy-modal-speech-icon">💬</div>
        </div>
        
        <h2>Start AI Walkthrough Now?</h2>
        
        <p className="sammy-modal-description">
          Our AI assistant will walk you through the platform in real time, 
          answering your questions and guiding you step by step based on what's 
          on your screen.
        </p>

        <div className="sammy-modal-features">
          <div className="sammy-modal-feature">
            <span className="sammy-modal-feature-icon">🎯</span>
            <span>Real-time guidance through the platform</span>
          </div>
          <div className="sammy-modal-feature">
            <span className="sammy-modal-feature-icon">💬</span>
            <span>Interactive Q&A as you navigate</span>
          </div>
          <div className="sammy-modal-feature">
            <span className="sammy-modal-feature-icon">🎤</span>
            <span>Natural voice interaction</span>
          </div>
        </div>

        {needsPermission && (
          <p className="sammy-modal-permission-note">
            <span className="sammy-modal-info-icon"></span>
            Clicking "Start Now" will request microphone access
          </p>
        )}

        <div className="sammy-modal-button-group">
          <button 
            className="sammy-modal-btn sammy-modal-btn-secondary"
            onClick={handleClose}
          >
            Not Now
          </button>
          <button 
            className="sammy-modal-btn sammy-modal-btn-primary"
            onClick={handleStartWalkthrough}
            disabled={isStarting}
          >
            {isStarting ? 'Starting...' : 'Start Now'}
          </button>
        </div>
      </div>
    );
  };

  return (
    <div className="sammy-modal-backdrop" onClick={handleClose}>
      <div className="sammy-modal" onClick={(e) => e.stopPropagation()}>
        <button 
          className="sammy-modal-close"
          onClick={handleClose}
          aria-label="Close modal"
        >
          ×
        </button>
        
        <div className="sammy-modal-content">
          {renderModalContent()}
        </div>
      </div>
    </div>
  );
};

Common Pitfalls and Mistakes

These are critical mistakes that will cause your microphone permission flow to fail. Review this section carefully to avoid common implementation errors.

❌ Incorrect Implementation (Will Fail)

Broken Implementation - DO NOT USE
const handleStartWalkthrough = async () => {
  try {
    // ❌ WRONG: Directly starting agent without permission check
    handleCloseModal();
    setHasStartedWalkthrough(true);
    await startAgent({
      agentMode: AgentMode.USER,
      guideId: guides?.currentGuide?.guideId,
    });
  } catch (error) {
    console.error('Error starting walkthrough:', error);
  }
};

✅ Correct Implementation

Proper Permission Flow
const handleStartWalkthrough = async () => {
  setIsStarting(true);
  
  try {
    // ✅ Step 1: ALWAYS check current permission state first
    const permissionResult = await checkPermission();
    
    // ✅ Step 2: If permission already granted, start immediately
    if (permissionResult.state === 'granted' || isPermissionGranted) {
      await startWalkthroughSession();
      return;
    }
    
    // ✅ Step 3: Request permission if needed
    if (permissionResult.needsPermission) {
      await request();
      // The useEffect will handle starting when permission is granted
    }
  } catch (error) {
    console.error('Error in handleStartWalkthrough:', error);
    setIsStarting(false);
  }
};

Critical Requirements Checklist

  • Missing Hook Usage
  • No Permission Check
  • Missing State Tracking
  • No Auto-Start Logic
  • Missing Error States

Problem

Not using the useMicrophonePermission hook at all
Common Mistake
// ❌ WRONG: No permission handling
const MyComponent = () => {
  const { startAgent } = useSammyAgentContext();
  
  const handleStart = async () => {
    await startAgent({ agentMode: AgentMode.USER }); // Will fail!
  };
};

Solution

Always import and use the permission hook
Correct Approach
// ✅ CORRECT: Using permission hook
const MyComponent = () => {
  const { startAgent } = useSammyAgentContext();
  const {
    state,
    error,
    isPermissionGranted,
    isPermissionDenied,
    needsPermission,
    checkPermission,
    request,
    refresh
  } = useMicrophonePermission(); // Essential!
  
  const handleStart = async () => {
    const result = await checkPermission();
    if (result.needsPermission) {
      await request();
    }
    if (isPermissionGranted) {
      await startAgent({ agentMode: AgentMode.USER });
    }
  };
};

Key Implementation Points

Essential Implementation Pattern

The correct pattern ALWAYS includes these elements:
  1. Import and use useMicrophonePermission hook
  2. Check permission state before starting agent
  3. Handle permission request if needed
  4. Track loading/starting state
  5. Implement auto-start logic with useEffect
  6. Handle all permission states in UI
  7. Provide recovery options for denied/error states

Troubleshooting

Common Issues

1

Permission State Not Updating

Use the refresh() method to force a re-check of the permission state:
const handlePermissionChange = async () => {
  await refresh();
  const newState = await checkPermission();
  console.log('Updated state:', newState);
};
2

Browser Compatibility Issues

Check for browser support before attempting to use microphone features:
const checkBrowserSupport = () => {
  if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
    return {
      supported: false,
      message: 'Your browser doesn\'t support microphone access. Please use Chrome, Firefox, or Safari.'
    };
  }
  
  // Check for HTTPS (required for getUserMedia)
  if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
    return {
      supported: false,
      message: 'Microphone access requires HTTPS. Please use a secure connection.'
    };
  }
  
  return { supported: true };
};
3

Handling Permission Reset

Re-check permissions when the browser tab becomes visible:
useEffect(() => {
  const handleVisibilityChange = () => {
    if (document.visibilityState === 'visible') {
      // Re-check permissions when tab becomes visible
      checkPermission();
    }
  };

  document.addEventListener('visibilitychange', handleVisibilityChange);
  return () => {
    document.removeEventListener('visibilitychange', handleVisibilityChange);
  };
}, [checkPermission]);
4

Stream Cleanup

Always ensure proper cleanup of media streams:
useEffect(() => {
  return () => {
    // Hook handles this automatically, but for custom implementations:
    if (stream) {
      stream.getTracks().forEach(track => track.stop());
    }
  };
}, [stream]);

Best Practices

Best Practices Checklist

Follow these guidelines for optimal microphone permission handling:
  1. Always explain why you need microphone access before requesting
  2. Check permission state before attempting to request
  3. Handle all states explicitly with appropriate UI
  4. Provide clear instructions for permission recovery
  5. Test across browsers and handle compatibility issues
  6. Clean up resources properly when component unmounts
  7. Use polling sparingly - only when expecting user action
  8. Provide fallbacks for when voice isn’t available
  9. Log errors for debugging but show user-friendly messages
  10. Respect user choice - don’t repeatedly prompt if denied
Remember that microphone permission is a sensitive user action - always be transparent about why you need it and what you’ll use it for. The modal-based pattern with clear messaging and progressive disclosure provides the best user experience for most use cases.

Next Steps

I