Skip to main content

Camera

Display a live camera preview, capture photos and videos, and stream camera frames directly in your Flet apps.

Powered by the camera Flutter package.

Platform Support

PlatformiOSAndroidWebWindowsmacOSLinux
Supported

Usage

Add the flet-camera package to your project dependencies:

uv add flet-camera
Permissions

Request camera (and microphone if recording video with audio) permissions on mobile platforms before initializing the control. You can use PermissionHandler to prompt the user. You can also use predefined cross-platform permission bundles for camera and microphone permissions.

Requirements

The below sections show the required configurations for each platform.

Android

Configuration to be made to access the camera and optionally the microphone:

flet build apk \
--android-permissions android.permission.CAMERA=true \
--android-permissions android.permission.RECORD_AUDIO=true

See also:

iOS

Configuration to be made to access the camera and optionally the microphone:

flet build ipa \
--info-plist NSCameraUsageDescription="Some message to describe why you need this permission..." \
--info-plist NSMicrophoneUsageDescription="Some message to describe why you need this permission..."

See also:

Cross-platform

Additionally/alternatively, you can make use of our predefined cross-platform camera (and optionally microphone) permission bundles:

flet build <target_platform> --permissions camera microphone

Example

import base64
import logging
from dataclasses import dataclass, field
from datetime import datetime
from math import cos, pi, sin

import flet as ft
import flet_camera as fc


@dataclass
class State:
cameras: list[fc.CameraDescription] = field(default_factory=list)
selected_camera: fc.CameraDescription | None = None
camera_labels: dict[str, str] = field(default_factory=dict)
is_streaming: bool = False
is_streaming_supported: bool = False
is_initialized: bool = False
is_preview_paused: bool = False
is_recording: bool = False
is_recording_paused: bool = False
device_orientation: ft.DeviceOrientation | None = None
last_frame_width: int | None = None
last_frame_height: int | None = None


async def main(page: ft.Page):
image_frame_width = 280
image_frame_height = 200
page.title = "Camera control"
page.padding = 16
page.scroll = ft.ScrollMode.AUTO
page.horizontal_alignment = ft.CrossAxisAlignment.STRETCH

state = State()

preview = fc.Camera(
expand=True,
preview_enabled=True,
content=ft.Container(
alignment=ft.Alignment.CENTER,
content=ft.Icon(
ft.Icons.CENTER_FOCUS_STRONG,
color=ft.Colors.WHITE_70,
size=48,
),
),
)

status = ft.Text(value="Select a camera", size=12)
last_photo_label = ft.Text("Last photo", visible=False)
last_image = ft.Image(
src=base64.b64decode(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/wIAAgMBAp0YVwAAAABJRU5ErkJggg=="
),
width=image_frame_width,
height=image_frame_height,
fit=ft.BoxFit.CONTAIN,
gapless_playback=True,
)
last_image_frame = ft.Container(
width=image_frame_width,
height=image_frame_height,
alignment=ft.Alignment.CENTER,
content=last_image,
visible=False,
)
selector = ft.Dropdown(label="Camera", options=[])
recorded_video_path = ft.Text(value="Recorded video: not saved yet", size=12)
take_photo_btn = ft.FilledIconButton(
icon=ft.Icons.PHOTO_CAMERA,
tooltip="Take photo",
disabled=True,
)
record_btn = ft.FilledTonalIconButton(
icon=ft.Icons.VIDEOCAM,
selected_icon=ft.Icons.STOP,
selected=False,
tooltip="Start / stop recording",
disabled=True,
)
pause_recording_btn = ft.OutlinedIconButton(
icon=ft.Icons.PAUSE,
selected_icon=ft.Icons.PLAY_ARROW,
selected=False,
tooltip="Pause / resume recording",
disabled=True,
)
stream_btn = ft.OutlinedIconButton(
icon=ft.Icons.PLAY_ARROW,
selected_icon=ft.Icons.STOP,
selected=False,
tooltip="Start / stop image stream",
disabled=True,
)
preview_btn = ft.OutlinedIconButton(
icon=ft.Icons.VISIBILITY_OFF,
selected_icon=ft.Icons.VISIBILITY,
selected=True,
tooltip="Pause / resume preview",
disabled=True,
)

def has_human_readable_name(camera: fc.CameraDescription) -> bool:
name = camera.name.strip()
if not name:
return False
if name.startswith("com.apple.avfoundation."):
return False
return not (":" in name and "." in name)

def camera_label(camera: fc.CameraDescription) -> str:
if has_human_readable_name(camera):
return camera.name

direction = camera.lens_direction.value.capitalize()
lens_map = {
"wide": "Wide",
"telephoto": "Telephoto",
"ultraWide": "Ultra Wide",
"unknown": "Unknown",
}
lens_type = lens_map.get(camera.lens_type.value, camera.lens_type.value)
return f"{direction} ({lens_type})"

def device_orientation_degrees(orientation: ft.DeviceOrientation | None) -> int:
if orientation == ft.DeviceOrientation.PORTRAIT_UP:
return 0
if orientation == ft.DeviceOrientation.LANDSCAPE_RIGHT:
return 90
if orientation == ft.DeviceOrientation.PORTRAIT_DOWN:
return 180
if orientation == ft.DeviceOrientation.LANDSCAPE_LEFT:
return 270
return 0

def image_rotation_radians() -> float:
camera = state.selected_camera
if camera is None:
return 0.0
sensor_orientation = camera.sensor_orientation
device_degrees = device_orientation_degrees(state.device_orientation)
if camera.lens_direction == fc.CameraLensDirection.FRONT:
rotation_degrees = (sensor_orientation + device_degrees) % 360
else:
rotation_degrees = (sensor_orientation - device_degrees + 360) % 360
return rotation_degrees * pi / 180

def apply_last_image_transform(src_width: int | None, src_height: int | None):
_ = (src_width, src_height)
last_image.width = image_frame_width
last_image.height = image_frame_height

if page.platform != ft.PagePlatform.ANDROID:
last_image.offset = None
last_image.rotate = None
return

angle = image_rotation_radians()
width = float(image_frame_width)
height = float(image_frame_height)
calc_angle = -angle

# Flet uses clockwise radians for Rotate.angle.
def rotate_point(x: float, y: float) -> tuple[float, float]:
return (
x * cos(calc_angle) + y * sin(calc_angle),
-x * sin(calc_angle) + y * cos(calc_angle),
)

corners = [
rotate_point(0.0, 0.0),
rotate_point(width, 0.0),
rotate_point(0.0, height),
rotate_point(width, height),
]
min_x = min(p[0] for p in corners)
min_y = min(p[1] for p in corners)

# Control.offset is normalized by control width/height.
last_image.offset = ft.Offset(
x=(-min_x / width) if width else 0.0,
y=(-min_y / height) if height else 0.0,
)
last_image.rotate = ft.Rotate(angle=angle, alignment=ft.Alignment.TOP_LEFT)

async def get_cameras():
state.cameras = await preview.get_available_cameras()
state.camera_labels.clear()
seen_labels: dict[str, int] = {}
for camera in state.cameras:
label = camera_label(camera)
seen_labels[label] = seen_labels.get(label, 0) + 1
if seen_labels[label] > 1:
label = f"{label} {seen_labels[label]}"
state.camera_labels[camera.name] = label

selector.options = [
ft.DropdownOption(key=c.name, text=state.camera_labels[c.name])
for c in state.cameras
]
if selector.value and selector.value not in state.camera_labels:
selector.value = None
state.selected_camera = None
state.is_initialized = False
state.is_streaming = False
state.is_recording = False
state.is_recording_paused = False
state.is_preview_paused = False
state.is_streaming_supported = False
state.device_orientation = None
sync_action_buttons()
status.value = "Select a camera"
page.update()

def sync_action_buttons():
take_photo_btn.disabled = not state.is_initialized
record_btn.disabled = not state.is_initialized
record_btn.selected = state.is_recording
pause_recording_btn.selected = state.is_recording_paused
pause_recording_btn.disabled = not state.is_recording
stream_btn.selected = state.is_streaming
stream_btn.disabled = not (
state.is_initialized and state.is_streaming_supported
)
preview_btn.selected = not state.is_preview_paused
preview_btn.disabled = not state.is_initialized

async def init_camera(e=None):
state.selected_camera = next(
(c for c in state.cameras if c.name == selector.value),
None,
)
if not state.selected_camera:
status.value = "No camera selected"
return

selected_label = state.camera_labels.get(
state.selected_camera.name, state.selected_camera.name
)
status.value = f"Initializing {selected_label}"
status.update()
await preview.initialize(
description=state.selected_camera,
resolution_preset=fc.ResolutionPreset.MEDIUM,
enable_audio=True,
image_format_group=fc.ImageFormatGroup.JPEG,
)
if not page.web:
try:
await preview.lock_capture_orientation()
except RuntimeError as ex:
logging.warning("Could not lock capture orientation: %s", ex)
state.is_initialized = True
state.is_streaming = False
state.is_streaming_supported = await preview.supports_image_streaming()
sync_action_buttons()
page.update()

async def take_photo():
if not state.is_initialized:
status.value = "Initialize camera first"
page.update()
return
data = await preview.take_picture()
last_image.src = data
apply_last_image_transform(state.last_frame_width, state.last_frame_height)
last_photo_label.visible = True
last_image_frame.visible = True
page.update()

async def start_recording():
if not state.is_initialized:
status.value = "Initialize camera first"
page.update()
return
await preview.prepare_for_video_recording()
await preview.start_video_recording()
state.is_recording = True
state.is_recording_paused = False
sync_action_buttons()
status.value = "Recording video..."
page.update()

async def pause_recording():
if not state.is_initialized or not state.is_recording:
return
await preview.pause_video_recording()
state.is_recording_paused = True
sync_action_buttons()
status.value = "Recording paused"
page.update()

async def resume_recording():
if not state.is_initialized or not state.is_recording:
return
await preview.resume_video_recording()
state.is_recording_paused = False
sync_action_buttons()
status.value = "Recording resumed"
page.update()

async def stop_recording():
if not state.is_initialized or not state.is_recording:
return
data = await preview.stop_video_recording()
state.is_recording = False
state.is_recording_paused = False
sync_action_buttons()
if not data:
status.value = "No video data returned"
page.update()
return

ext = fc.detect_video_extension(data)
filename = f"recording_{datetime.now().strftime('%Y%m%d_%H%M%S')}.{ext}"
saved_path = await ft.FilePicker().save_file(
file_name=filename,
src_bytes=data,
)

kb_size = len(data) / 1024
status.value = f"Video recorded: {kb_size:.1f} KB"
if saved_path:
recorded_video_path.value = f"Recorded video: {saved_path}"
else:
recorded_video_path.value = "Recorded video save canceled"
page.update()

async def on_state_change(e: fc.CameraStateEvent):
if e.description == state.selected_camera:
state.device_orientation = e.device_orientation
state.is_recording = e.is_recording_video
state.is_recording_paused = e.is_recording_paused
state.is_streaming = e.is_streaming_images
state.is_preview_paused = e.is_preview_paused
sync_action_buttons()
if e.has_error:
status.value = f"Camera error: {e.error_description}"
elif e.is_taking_picture:
status.value = "Taking picture..."
elif e.is_recording_paused:
status.value = "Recording paused"
elif e.is_recording_video:
status.value = "Recording video..."
elif e.is_streaming_images:
status.value = "Streaming images..."
elif e.is_preview_paused:
status.value = "Preview paused"
else:
status.value = "Camera ready"
page.update()

preview.on_state_change = on_state_change
selector.on_select = init_camera

async def start_streaming():
if not state.is_initialized:
status.value = "Initialize camera first"
page.update()
return
if not state.is_streaming_supported:
status.value = "Image streaming is not supported by this camera"
page.update()
return
await preview.start_image_stream()
state.is_streaming = True
sync_action_buttons()
page.update()

async def stop_streaming():
if not state.is_initialized:
return
await preview.stop_image_stream()
state.is_streaming = False
sync_action_buttons()
page.update()

def on_stream_image(e: fc.CameraImageEvent):
try:
state.last_frame_width = e.width
state.last_frame_height = e.height
last_image.src = e.bytes
apply_last_image_transform(e.width, e.height)
last_photo_label.visible = True
last_image_frame.visible = True
page.update()
except Exception as ex:
logging.exception("Failed to render stream frame: %s", ex)

preview.on_stream_image = on_stream_image

async def pause_preview():
if not state.is_initialized:
return
await preview.pause_preview()
state.is_preview_paused = True
sync_action_buttons()
page.update()

async def resume_preview():
if not state.is_initialized:
return
await preview.resume_preview()
state.is_preview_paused = False
sync_action_buttons()
page.update()

async def toggle_recording():
if state.is_recording:
await stop_recording()
else:
await start_recording()

async def toggle_recording_pause():
if state.is_recording_paused:
await resume_recording()
else:
await pause_recording()

async def toggle_streaming():
if state.is_streaming:
await stop_streaming()
else:
await start_streaming()

async def toggle_preview():
if state.is_preview_paused:
await resume_preview()
else:
await pause_preview()

take_photo_btn.on_click = take_photo
record_btn.on_click = toggle_recording
pause_recording_btn.on_click = toggle_recording_pause
stream_btn.on_click = toggle_streaming
preview_btn.on_click = toggle_preview

page.on_connect = get_cameras

page.add(
ft.SafeArea(
content=ft.Column(
[
ft.Row(
[
selector,
ft.IconButton(ft.Icons.REFRESH, on_click=get_cameras),
],
wrap=True,
),
ft.Container(
height=320,
bgcolor=ft.Colors.BLACK,
border_radius=3,
content=preview,
),
status,
ft.Row(
[
take_photo_btn,
record_btn,
pause_recording_btn,
stream_btn,
preview_btn,
],
wrap=True,
),
recorded_video_path,
last_photo_label,
last_image_frame,
]
)
)
)

await get_cameras()


if __name__ == "__main__":
ft.run(main)

Description

A control that provides camera preview and capture capabilities.

Inherits: LayoutControl

Properties

  • content - Optional child to overlay on top of the camera preview.
  • preview_enabled - Whether the preview surface is shown.

Events

Methods

Properties

contentclass-attributeinstance-attribute

content: Optional[Control] = None

Optional child to overlay on top of the camera preview.

preview_enabledclass-attributeinstance-attribute

preview_enabled: bool = True

Whether the preview surface is shown.

Events

on_state_changeclass-attributeinstance-attribute

on_state_change: Optional[EventHandler[CameraStateEvent]] = None

Fires when the camera controller state changes.

on_stream_imageclass-attributeinstance-attribute

on_stream_image: Optional[EventHandler[CameraImageEvent]] = None

Fires when an image frame is available while streaming.

Methods

get_available_camerasasync

get_available_cameras()

Lists the available camera devices on the current platform.

Returns:

get_exposure_offset_step_sizeasync

get_exposure_offset_step_size()

Returns the smallest increment supported for exposure offset changes.

get_max_exposure_offsetasync

get_max_exposure_offset()

Maximum exposure offset supported by the current camera.

get_max_zoom_levelasync

get_max_zoom_level()

Maximum zoom level supported by the current camera.

get_min_exposure_offsetasync

get_min_exposure_offset()

Minimum exposure offset supported by the current camera.

get_min_zoom_levelasync

get_min_zoom_level()

Minimum zoom level supported by the current camera.

initializeasync

initialize(description: CameraDescription, resolution_preset: ResolutionPreset, enable_audio: bool = True, fps: Optional[int] = None, video_bitrate: Optional[int] = None, audio_bitrate: Optional[int] = None, image_format_group: Optional[ImageFormatGroup] = None)

Initializes a new camera controller for the given description.

Parameters:

  • description (CameraDescription) - Camera device to bind to.
  • resolution_preset (ResolutionPreset) - Desired resolution preset.
  • enable_audio (bool, default: True) - Whether audio is enabled for recordings.
  • fps (Optional[int], default: None) - Optional target frames per second.
  • video_bitrate (Optional[int], default: None) - Optional video bitrate.
  • audio_bitrate (Optional[int], default: None) - Optional audio bitrate.
  • image_format_group (Optional[ImageFormatGroup], default: None) - Optional image format group override.

lock_capture_orientationasync

lock_capture_orientation(orientation: Optional[DeviceOrientation] = None)

Locks capture orientation to the specified device orientation.

Parameters:

  • orientation (Optional[DeviceOrientation], default: None) - Specific orientation to lock, or current device orientation if None.

pause_previewasync

pause_preview()

Pauses the camera preview.

pause_video_recordingasync

pause_video_recording()

Pauses an active video recording.

prepare_for_video_recordingasync

prepare_for_video_recording()

Prepares the capture session for video recording.

resume_previewasync

resume_preview()

Resumes the camera preview.

resume_video_recordingasync

resume_video_recording()

Resumes a paused video recording.

set_descriptionasync

set_description(description: CameraDescription)

Switches to another camera description.

Parameters:

set_exposure_modeasync

set_exposure_mode(mode: ExposureMode)

Changes the exposure mode.

Parameters:

set_exposure_offsetasync

set_exposure_offset(offset: float)

Sets exposure offset in EV units.

Parameters:

  • offset (float) - Exposure offset to apply.

Returns:

  • float - The offset value that was set.

set_exposure_pointasync

set_exposure_point(point: Optional[OffsetValue])

Sets the exposure metering point.

Parameters:

  • point (Optional[OffsetValue]) - Normalized offset (0..1) or None to reset.

set_flash_modeasync

set_flash_mode(mode: FlashMode)

Changes the flash mode.

Parameters:

set_focus_modeasync

set_focus_mode(mode: FocusMode)

Changes the focus mode.

Parameters:

set_focus_pointasync

set_focus_point(point: Optional[OffsetValue])

Sets the focus metering point.

Parameters:

  • point (Optional[OffsetValue]) - Normalized offset (0..1) or None to reset.

set_zoom_levelasync

set_zoom_level(zoom: float)

Applies the provided zoom level.

Parameters:

  • zoom (float) - Zoom level to set.

start_image_streamasync

start_image_stream()

Begins streaming camera image frames.

start_video_recordingasync

start_video_recording()

Starts capturing video.

stop_image_streamasync

stop_image_stream()

Stops streaming camera image frames.

stop_video_recordingasync

stop_video_recording()

Stops video recording and returns the recorded bytes.

Returns:

  • bytes - Encoded video file bytes.

supports_image_streamingasync

supports_image_streaming()

Indicates whether image streaming is supported on the current platform.

take_pictureasync

take_picture()

Captures a still image and returns the encoded bytes.

Returns:

  • bytes - Encoded image bytes.

unlock_capture_orientationasync

unlock_capture_orientation()

Unlocks the capture orientation.