Recover a Denied Camera Permission
Build a camera page that explains how to restore a denied permission.
Introduction
30 Second Summary
A camera button can look broken after someone chooses Block once. The browser remembers that choice, so pressing the button again gets them nowhere.
In this project, you will build a camera page powered by getUserMedia plus live state from the Permissions API. A recovery panel guides denied users back to a working preview after deployment over HTTPS.
What You'll Build
Picture your deployed page reacting instantly to a blocked camera by revealing a denied status plus the settings path that restores the live preview.
By the end of this project, you'll have:
- A live camera preview that starts after you approve access from the page.
- A real-time permission status that moves from prompt to granted or denied without a page reload.
- A browser-specific recovery guide that appears after access is blocked. The preview restarts when access is restored.
- Secret Mission: Extend the denied-permission recovery flow to browser notifications.
Are there any prerequisites?
You need a text editor plus a device with a camera. You also need access to a static host that serves HTTPS.
Before We Start
Before any hands-on work begins, take a moment to lock in what you are building. This gives every later browser test a clear purpose.
Build the Permission Status Page
Camera features can look broken before the camera ever starts. Your page needs a visible place to report what the browser currently allows.
The Permissions API lets the page read that state without triggering a camera prompt. This first version proves the page can see the browser's starting point.
In this step, get ready to:
- Build the page shell for the permission flow.
- Serve the page from localhost.
- Read the browser's initial camera permission state.
Create the page shell
The HTML shell gives each part of the flow a stable place on the page. The camera stays untouched while you confirm those parts render.
- Use your code editor to create index.html inside a new Desktop folder named permission-recovery.
- Build the page shell by replacing the contents of index.html with this code:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Camera Permission Recovery</title>
</head>
<body>
<!-- Show the controls and live permission state -->
<main>
<h1>Camera Permission Recovery</h1>
<video id="camera-preview" autoplay playsinline></video>
<button id="request-camera" type="button">Request Camera Access</button>
<p>Camera permission: <strong id="permission-status">Checking...</strong></p>
</main>
</body>
</html>
What does this page contain?
- The camera-preview video element gives the future camera stream somewhere to appear.
- The request-camera button is the control that starts the permission flow. This step leaves it inactive so the browser keeps its untouched permission state.
- The permission-status element starts with Checking... until the page reads the browser's state.
- Save index.html.
- Start your available local development server with permission-recovery as the folder it serves.
The server gives you a localhost address for the page.
- Open the server's localhost address in your desktop browser.
Your first visible slice is in place. You'll see the page heading above the empty preview.
You'll see Request Camera Access below the preview. The status line reads Camera permission: Checking....
Page not loading on localhost?
- Confirm your local development server is serving the Desktop permission-recovery folder.
- Confirm you saved the file as index.html.
- Still stuck? Help me troubleshoot why index.html is not loading from my local development server.
Read the initial permission state
The browser returns a PermissionStatus object when the page queries camera permission. Its state value contains the browser's current decision.
- In index.html, add this script immediately before the closing </body> tag:
<script>
// Keep the status element ready for browser updates
const permissionStatus = document.querySelector('#permission-status');
// Read the browser's current camera permission state
async function showCameraPermission() {
const { state } = await navigator.permissions.query({ name: 'camera' });
permissionStatus.textContent = state;
}
showCameraPermission();
</script>
What does this code do?
- The permissionStatus reference connects the script to the status line on the page.
- The navigator.permissions.query({ name: 'camera' }) call reads the existing camera decision without requesting access.
- The state value replaces Checking... with the result reported by the browser.
- Save index.html.
Before you refresh, do you think the browser has already made a camera decision for this new localhost page?
- Refresh the page in your desktop browser.
You'll see the status change to Camera permission: prompt. This means the browser is waiting for the page to request a decision.
The browser shows no camera prompt. The preview stays empty because the page only reads the permission.
Status still showing Checking...?
- Confirm the script sits above the closing </body> tag in index.html.
- Confirm the page address starts with localhost.
- Remove any existing camera decision for this local site in your browser's site-permission settings if you see a different state.
- Still stuck? Help me debug why my camera permission status is not appearing.
- Compare your saved index.html with the full file if you want a final check.
✔️ Awesome, I've got everything!
Your index.html file is saved. Keep the page open on localhost for the next step.
ⓧ I'd like to double check the full code
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Camera Permission Recovery</title>
</head>
<body>
<!-- Show the controls and live permission state -->
<main>
<h1>Camera Permission Recovery</h1>
<video id="camera-preview" autoplay playsinline></video>
<button id="request-camera" type="button">Request Camera Access</button>
<p>Camera permission: <strong id="permission-status">Checking...</strong></p>
</main>
<script>
// Keep the status element ready for browser updates
const permissionStatus = document.querySelector('#permission-status');
// Read the browser's current camera permission state
async function showCameraPermission() {
const { state } = await navigator.permissions.query({ name: 'camera' });
permissionStatus.textContent = state;
}
showCameraPermission();
</script>
</body>
</html>
That first browser-state check is working. Next, you'll connect the button to the camera request so the empty preview can come alive.
Request Camera Access
Your page can already read the pending camera decision through the Permissions API. The preview still needs a camera request before it can display anything.
In this step, you connect the existing button to navigator.mediaDevices.getUserMedia({ video: true }).
After you grant access, the returned MediaStream powers the preview. A fresh permission check updates the status to granted.
In this step, get ready to:
- Connect the existing request button to a camera-only request.
- Display the returned camera stream in the existing video preview.
- Confirm that the permission status changes to granted.
Build the successful request path
The button handler asks the browser for camera access. It stores the returned stream before assigning that stream to the video element.
How does the preview receive video?
- The getUserMedia() method asks for permission to use the requested media input.
- The { video: true } constraint requests camera video without requesting microphone audio.
- The returned MediaStream becomes the video element's source through its srcObject property.
- Return to the source file from earlier that contains navigator.permissions.query({ name: 'camera' }).
- Implement an asynchronous click handler for the existing camera-request button using the three-stage flow above.
- Save the source file.
The first request triggers the browser's camera prompt. Your operating system may show a separate camera prompt for the browser.
- Switch back to the permission-recovery page on localhost.
Before you test it, predict whether the browser will ask for camera access.
- Click the existing camera-request button.
- Grant camera access in the browser prompt.
- Grant camera access in the operating system prompt if it appears.
You should see the live camera image inside the video element. That is the successful permission path working.
No prompt or camera preview?
- Clear the saved camera permission for localhost in your browser's site settings if no prompt appears.
- Reload the permission-recovery page after clearing the saved permission.
- Check that the returned MediaStream is assigned to the existing video element's srcObject property if the preview stays blank.
- Still stuck? Help me troubleshoot my camera request and blank video preview.
Synchronize the granted state
The live preview proves that camera capture succeeded. The status line needs a fresh permission read after that successful request.
- Return to the source file from earlier.
- Extend the successful request path so it runs the existing navigator.permissions.query({ name: 'camera' }) logic after assigning the stream.
- Save the source file.
A clean reload gives you a reliable final check. The browser remembers the permission while the page starts with a fresh preview.
- Refresh the permission-recovery page on localhost.
Before you click, predict whether the browser will prompt again or start the preview immediately.
- Click the existing camera-request button.
You should see the camera preview start without the initial permission prompt. The status line displays granted.
Status still showing prompt?
- Check that the permission query runs after getUserMedia() resolves.
- Check that the status line receives the latest permission state from the repeated query.
- Still stuck? Help me update the status after camera access is granted.
Your page now completes the successful camera-permission path. Next, you'll make it react when that permission changes outside the page.
Watch for Permission Changes
Your camera preview now proves the allowed path works. The page still needs a way to notice when someone changes that choice outside the tab.
The Permissions API returns a PermissionStatus object that reports later changes. Its change event refreshes the status line without reloading the page.
In this step, get ready to:
- Store the browser's camera permission status.
- Subscribe to its change event.
- Prove the live update by blocking camera access.
Store the live permission state
A permission status has a state value such as granted or denied. Keeping the returned object available gives the page something to monitor after the initial query finishes.
- Switch back to the JavaScript from earlier.
- Find the logic that writes the current camera permission into the status line.
- Store the PermissionStatus returned by navigator.permissions.query({ name: 'camera' }) in a persistent reference beside that logic.
- Set the status line from the stored object's state value when the query resolves.
- Save the JavaScript file.
- Refresh the permission-recovery page.
You should still see granted after the refresh. This confirms that the initial status now comes from the browser's permission record.
Why Keep the Permission Status?
Each permission query returns a PermissionStatus object. Retaining that object lets the page receive later events from the same permission record.
The status line can now stay connected to the browser's current camera decision.
Still Seeing the Old Status?
- Check that the permission query finishes before your code reads the state value.
- Confirm that the existing status line receives the stored PermissionStatus state.
- Need another pair of eyes? Help me connect the camera PermissionStatus to my existing status line.
Listen for camera changes
A permission choice can change while the page stays open. The event listener turns that browser-level change into an immediate page update.
- Switch back to the JavaScript from earlier.
- Attach an event listener for change to the stored PermissionStatus.
- Update the existing status line from the latest state value inside the listener.
- Save the JavaScript file.
- Refresh the permission-recovery page.
Before you change the browser setting, predict what the status line will show without a reload.
- Keep the permission-recovery page open in its current browser tab.
- Open the site-permission settings for localhost from the control beside the browser address.
- Set camera access for localhost to blocked.
- Return to the permission-recovery page without refreshing it.
You should see the status line change to denied without a reload. The existing camera preview should stop because the previous stream is no longer usable.
You've now proved that the page reacts to a real browser setting change while it stays open.
Status Not Changing Live?
- Confirm that you saved the event listener before changing the browser setting.
- Check that the listener is attached to the stored PermissionStatus returned by the camera query.
- Still stuck? Help me debug why my camera permission status does not change to denied without a reload.
Your page can now detect a blocked camera permission immediately. Next, you'll turn that signal into clear recovery instructions for the browser.
Add Denied-Permission Recovery
Your Permissions API listener now catches a blocked camera permission without a reload. The page already knows when the preview has lost access.
That denied state still leaves the visitor at a dead end. A recovery panel gives them the browser-specific steps for restoring camera access.
In this step, get ready to:
- Choose the recovery path for your current desktop browser.
- Show a recovery panel only when camera permission is denied.
- Capture the panel beside the browser's real permission settings.
Choose your browser's recovery path
Browsers use different labels for their camera controls. Your recovery panel needs to match the labels that a visitor sees in the current browser.
- Select the tab that matches Chrome or Safari.
Chrome
The Chrome recovery panel needs these steps.
- Select the icon to the left of the web address.
- Select Site settings.
- Set Camera to Allow.
- Keep the current camera setting at Block during this step.
Safari
The Safari recovery panel needs these steps.
- Select Safari from the menu bar.
- Select Settings for This Website.
- Set Camera to Allow.
- Keep the current camera setting at Deny during this step.
Why Does Denied Access Need Guidance?
A denied camera permission stays under browser control. JavaScript can read the permission state but cannot turn camera access back on.
The recovery panel gives the visitor a route into that browser control. Clear steps turn a silent failure into a fixable state.
Add the denied-state panel
The panel needs useful recovery instructions before its visibility becomes conditional. The current denied state gives you an immediate way to check its content.
- Switch back to the code editor from earlier.
- Add a recovery panel to the existing page near the status line. Give it the heading Camera access is blocked plus the recovery path from your browser tab.
- Save the page file.
- Return to the browser page from earlier.
- Refresh the page.
You should see the recovery heading plus the browser-specific steps. That gives the denied state a visible next move.
The panel content is now in place. Its final visibility comes from the permission-state display logic you already use for the status line.
- Update the existing permission-state display logic so the recovery panel is visible only when the state equals denied.
- Save the page file.
- Refresh the open browser page.
The status line should still read denied. The recovery panel should remain visible.
The video preview should remain inactive because camera access is still blocked.
Panel Missing While Access Is Blocked?
- Check that the visibility condition matches the exact state denied.
- Check that the permission-change listener uses the same display logic that controls the recovery panel.
- Still stuck? Help me debug a recovery panel that stays hidden when camera permission is denied.
Capture the denied state
The final check places your page beside the browser's source of truth. This proves that the recovery instructions match the real permission controls.
Before you check, which denied-state signals do you expect to see on the page and in the browser menu?
- Return to the permission-recovery page from earlier.
- Confirm that the status line reads denied.
- Confirm that the recovery panel is visible.
- Return to the camera site-permission menu from earlier.
- Position the open permission menu beside the recovery panel.
- Capture a screenshot of the full denied state.
You should see the denied status plus the recovery panel on the page. Beside it, the browser's real camera control should still show the blocked setting.
You have turned a silent camera failure into a clear recovery path. Next, you'll follow that path and watch the page respond when camera access returns.
Restore the Camera Preview
Your page already catches blocked camera access. The screenshot from the previous step proves its recovery path matches the real browser menu.
Now the recovery flow needs to close the loop. This step proves that the Permissions API listener reacts when camera access is restored.
In this step, get ready to:
- Restore camera access from the browser's site-permission settings.
- Confirm the permission status reacts without a reload.
- Confirm a live camera preview restarts automatically.
Restore camera access in browser settings
A blocked decision stays attached to the site after the permission prompt closes. The browser's site-permission control is the route back to an allowed state.
Before you change the setting, predict whether the open page reacts without a reload.
- Switch back to the browser site-permission settings from earlier.
- Locate the camera permission control for the current localhost page by following the path shown in the recovery panel.
- Change the camera permission from blocked to allowed.
The browser now stores an allowed camera decision for this site.
Confirm automatic recovery
The open page receives the PermissionStatus change event as soon as the setting changes. This event starts the recovery sequence.
- Return to the permission-recovery page from earlier without refreshing it.
- Inspect the status line.
- Inspect the area where the recovery panel appeared.
- Watch the video element.
You should see granted in the status line.
The denied-only recovery panel should be gone.
You should see a live camera preview playing in the video element.
You have closed the full recovery loop. The page now restores its camera feature from a browser-setting change alone.
Status or preview not updating?
- Confirm the camera setting belongs to the same localhost origin shown in the page's address bar.
- Confirm the browser saved the camera permission as allowed.
- Close any other application that is currently using the camera.
- Still stuck? Help me troubleshoot the permission recovery flow.
- Keep the denied-state screenshot from the previous step as your record of the blocked state.
Why does the preview return?
The PermissionStatus object's change event tells the page that camera access moved from denied to granted.
The getUserMedia({ video: true }) call obtains a new MediaStream. Attaching that stream to the video element starts the restored preview.
Your desktop recovery flow now handles the full permission cycle. Next, you'll deploy the page over HTTPS for a phone test.
Deploy and Test on a Phone
Your desktop page already recovers after you restore camera access in browser settings. Now it needs to prove the same flow works beyond localhost.
On a phone, camera access needs a secure context. HTTPS provides that secure context for a public page.
Mobile browsers also place site permissions in different settings menus, so this final test checks the full recovery experience under real conditions.
In this step, get ready to:
- Publish the completed page at a public HTTPS URL.
- Run the denied flow on a phone.
- Restore camera access through mobile settings.
Deploy the completed page
Netlify Drop publishes a local project folder at a public address. The resulting page uses HTTPS, which lets your phone request camera access.
Why does the phone need HTTPS?
Camera access uses a powerful device capability. Browsers limit that capability to secure contexts such as HTTPS or localhost.
Localhost works for development on your computer. The HTTPS deployment creates a secure address that your phone can reach.
- Switch back to your code editor.
- Save the completed page's source files.
- Use the file sidebar to identify the outer folder containing those source files.
- Open the Netlify login page in your desktop browser.
- Create a Netlify account if you do not have one.
- Sign in to Netlify.
- Open Netlify Drop in the same browser.
- Drag the folder containing your page into the Drop zone.
- Wait until Netlify displays a Preview URL.
- Copy the HTTPS Preview URL ending in netlify.app.
- Record it here: your deployed HTTPS URL.
Before you load the new address, predict whether the camera request will work on the deployed page.
- Paste your deployed HTTPS URL into your desktop browser's address bar.
- Load the deployed page.
- Select the page's camera-request button.
- Grant camera access in the browser prompt.
You should see granted in the status line. The recovery panel should be hidden.
The camera preview should be playing on the HTTPS page. That is the deployment complete.
Page missing or camera unavailable?
- Confirm that you dropped the folder containing all of the completed page's source files.
- Confirm that the deployed address begins with https://.
- If the preview stays blank, confirm that your desktop browser can use the camera in your device settings.
- Still stuck? Help me diagnose my HTTPS deployment.
Block camera access on your phone
Your phone stores its own permission decision for the deployed site. The mobile settings path depends on the browser you use.
iPhone with Safari
- Tap Safari on your iPhone.
- Enter your deployed HTTPS URL in Safari's address bar.
- Load the deployed page.
- Select the page's camera-request button.
- Choose Allow when Safari asks for camera access.
You should see the camera preview playing. The status line should read granted.
Before you block the camera, predict whether the status will change while the page stays open.
- Open the current website's settings from Safari's page menu.
- Select Camera.
- Select Deny.
- Return to the deployed page.
You should see denied in the status line without reloading. The recovery panel should be visible.
Android with Chrome
- Tap Chrome on your Android phone.
- Enter your deployed HTTPS URL in Chrome's address bar.
- Load the deployed page.
- Select the page's camera-request button.
- Choose Allow when Chrome asks for camera access.
You should see the camera preview playing. The status line should read granted.
Before you block the camera, predict whether the status will change while the page stays open.
- Tap View site information beside the address bar.
- Tap Permissions.
- Tap Camera.
- Select Block.
- Return to the deployed page.
You should see denied in the status line without reloading. The recovery panel should be visible.
You have now reproduced the denied branch on a phone. The visible recovery panel proves the page responds when the browser stops offering a permission prompt.
Status still reads granted?
- Confirm that you changed the camera permission for the deployed site.
- Check that your phone's device settings allow the browser app to manage camera access.
- Return to the deployed page after changing the permission.
- Still stuck? Help me diagnose the mobile permission state.
Restore camera access and verify recovery
Restoring access tests the permission change path in reverse. The page should recover without a reload or another click on its camera-request button.
Before you restore access, predict how the status line and camera preview will respond.
iPhone with Safari
- Return to the deployed website's settings in Safari.
- Select Camera.
- Select Allow.
- Return to the deployed page.
Android with Chrome
- Tap View site information beside the address bar.
- Tap Permissions.
- Tap Camera.
- Select Allow.
- Return to the deployed page.
You should see granted in the status line. The recovery panel should disappear.
The camera preview should restart on its own without another page load. Your phone has completed the full block-and-recover cycle.
- Return to the deployed page in your desktop browser.
- Confirm that its status line still reads granted.
- Confirm that its recovery panel remains hidden.
- Confirm that its camera preview is playing.
- Keep the denied-state screenshot from earlier with your project evidence.
- Point your phone camera at a blank surface before capturing the final proof.
Both devices now finish in the recovered state. Your page can detect denial and guide someone back to a working camera over HTTPS.
Secret mission
Run a Zero-Reload Recovery Drill
A recovery flow earns trust when it rescues the page in place. Recreate the denied state on your deployed desktop page. Use the on-page guidance to restore the camera without a reload.
Clean Up Your Resources
Clean Up Your Resources
This browser-only project has no ongoing project costs. Decide whether to keep the deployed page ready, pause camera use, or clear the saved permission so you can repeat the flow from scratch.
Resources you used:
- A browser camera permission entry for your public HTTPS page.
- An active camera MediaStream while the deployed page is open.
Keep everything running
No action is needed. Choose this option if you want the permission-recovery page to stay ready for another demonstration.
- Your public HTTPS page stays available on desktop browsers and phones.
- Your browser keeps the camera permission in its current granted state.
- Your recovery panel remains ready to appear after a future block.
- Your denied-state screenshot remains available as evidence of the recovery flow.
Pause - I'll come back to this later
Close the page to release the camera while keeping the deployment ready. Your browser preserves the granted permission for your next visit.
- Close the deployed page in your desktop browser.
- Close the deployed page on your phone.
- Confirm the camera-use indicator is no longer active on each device.
- Reopen the public HTTPS URL when you want to continue testing.
Delete - I don't want to use this again
The project keeps the deployed page as a reusable artifact. Clear the browser state created during testing so the permission flow starts from scratch.
- Close the deployed page in your desktop browser.
- Open your desktop browser's settings for the deployed HTTPS page.
- Clear the saved site permission entry for the deployed HTTPS page.
- Repeat the permission reset in the mobile browser or device settings used during your phone test.
- Reopen the public HTTPS URL to verify the reset.
You should see the status return to prompt. The recovery panel stays hidden until you block the camera again.
Nice Work!
Nice Work!
Great work. Your deployed permission-recovery page now carries users from denied camera access back to a playing preview.
You've learned how to:
- Create a camera request flow that turns granted access into a live preview.
- Use the Permissions API to read the camera state. Subscribe to permission changes so the status updates without a reload.
- Deploy the page over HTTPS. Prove the recovery cycle on desktop and phone by blocking camera access before restoring it in settings.
- Apply the same permission-state thinking in an optional Secret Mission that pushes your skills further.
Ready to quiz yourself?