Before a user can share their camera or mic, the browser or phone asks for permission ("Allow this site to use your camera?"). The OS shows the prompt, not Livqeno, so each platform works a little differently. This page covers Web, React Native, and Flutter.
Why it matters: if a user says "no", or the prompt never appears, your call shows a black tile or silence, often with no obvious error. Handling permissions well is the difference between "it just works" and a support ticket.
Two kinds of permission, don't mix them up:
| Who decides | Where it's set | |
|---|---|---|
| Device permission (this page) | The user, in the OS prompt | Browser / phone settings |
Token permission (publish, subscribe, ...) | Your backend | When it makes the token. See Authentication. |
A user needs both to send video: your token must allow publish, and
the user must allow the camera.
All code on this page runs in your app. The manifest/plist setup at the end goes in your mobile project's config files.
Web
The browser shows the prompt the first time you call enableCamera() or
enableMicrophone(). There's nothing to call ahead of time. After the user
says "yes", the browser remembers it for your site.
try {
await room.enableCamera();
} catch (error) {
if (error.code === 'CAMERA_PERMISSION_DENIED') {
// show your own "please allow camera access" UI
}
}The browser doesn't tell you if the user said "no" once or blocked it for good. Just tell the user: "Check your browser's camera permission for this site."
React Native
Phones give you more control than browsers: you can check the status
without a prompt (on Android), and tell "denied once" apart from "blocked".
React Native has a full permissions module:
import { permissions } from '@livqeno/react-native';
const status = await permissions.check();
// { camera: 'granted'|'denied'|'blocked'|'undetermined'|'unavailable', microphone: ... }
const result = await permissions.request(['camera', 'microphone']); // prompts; never throws
await permissions.require(['camera']); // prompts; throws LivqenoPermissionError on anything but granted| Method | Shows a prompt? | Throws? |
|---|---|---|
check() | No | No |
request() | Yes | Never |
require() | Yes | Yes, unless granted |
How the platforms differ:
- Android:
check()tells you the real status without a prompt. - iOS:
check()always saysundetermined, because iOS doesn't let JavaScript read the status. Userequest()to find out.
When permission is blocked
blocked means the user said "no" for good (Android's "Don't ask again",
or any iOS denial after the first one). Asking again shows nothing.
In this case, LivqenoPermissionError.requiresSettings is true. Send the
user to the Settings app:
import { permissions, isLivqenoPermissionError } from '@livqeno/react-native';
try {
await permissions.require(['camera']);
} catch (error) {
if (isLivqenoPermissionError(error) && error.requiresSettings) {
// open Settings — prompting again shows no dialog on this platform
}
}Automatic prompt on join
livqeno.join() calls permissions.request() for you. It doesn't throw, so
a user who says "no" to the camera can still join and listen.
Have your own permission screen? Pass { requestPermissions: false } to
turn this off.
Flutter
final granted = await LivqenoPermissions.request();
// { LivqenoPermission.camera: true, LivqenoPermission.microphone: false }
await LivqenoPermissions.require(); // throws LivqenoPermissionExceptionrequest()shows the OS prompt.- Livqeno can't check the status without a prompt, and can't tell "denied" from "denied for good".
Need either of those? Add the permission_handler package alongside Livqeno:
final status = await Permission.camera.status;
if (status.isPermanentlyDenied) {
await openAppSettings();
} else {
await LivqenoPermissions.require([LivqenoPermission.camera]);
}Platform manifest/plist setup
You must declare the permissions in your app's config. Livqeno can't do this for you, and nothing above works without it.
iOS (React Native and Flutter), ios/*/Info.plist:
<key>NSCameraUsageDescription</key>
<string>Camera access is used for video calls.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone access is used for calls.</string>Android (React Native and Flutter), android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />Common errors
| Error | Platform | Cause | Fix |
|---|---|---|---|
| The app crashes (no error) | React Native, Flutter | An iOS usage string is missing from Info.plist. | Add both NSCameraUsageDescription and NSMicrophoneUsageDescription. |
CAMERA_PERMISSION_DENIED / MICROPHONE_PERMISSION_DENIED | Web | The user said "no" to the browser prompt. | Show your own explanation. The browser controls asking again. |
LivqenoPermissionError with requiresSettings: true | React Native | Blocked for good. | Send the user to system Settings. Asking again shows nothing. |
LivqenoPermissionException (permanentlyDenied: false) | Flutter | Livqeno can't tell if the "no" is permanent. | Add permission_handler if you need to know (see above). |
Tips for production
- Ask when the user joins a call, and explain why in your own UI first. People say "no" much more often to a prompt with no context.
- Don't block joining if the user says "no". Let them join as a listener.
- Test the "blocked for good" case on a real device. Simulators don't always behave the same.
Next steps
- Audio & Video: where permission errors show up when you turn on the camera.
- React Native SDK and Flutter SDK: full platform setup.
- Troubleshooting: "black video, no error" is almost always a permission issue.