Skip to 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#

Platform iOS Android Web Windows macOS Linux
Supported

Usage#

Add the flet-camera package to your project dependencies:

uv add flet-camera
pip install flet-camera  # (1)!
  1. After this, you will have to manually add this package to your requirements.txt or pyproject.toml.

Permissions

Request camera (and microphone if recording video with audio) permissions on mobile and desktop platforms before initializing the control. You can use flet-permission-handler to prompt the user.

iOS required Info.plist keys#

Add these entries when building for iOS:

[tool.flet.ios.info]
NSCameraUsageDescription = "This app uses the camera to capture photos and video."
NSMicrophoneUsageDescription = "This app uses the microphone when recording video with audio."
  • NSCameraUsageDescription is required for camera preview/capture.
  • NSMicrophoneUsageDescription is required when enable_audio=True for video recording.

See also: iOS permissions.

Android required permissions#

For Android, enable camera permission, and microphone permission if recording video with audio:

[tool.flet.android.permission]
"android.permission.CAMERA" = true
"android.permission.RECORD_AUDIO" = true
  • android.permission.CAMERA is required for camera usage.
  • android.permission.RECORD_AUDIO is required only for video recording with audio.

See also: Android permissions.

Example#

import logging
from dataclasses import dataclass, field
from datetime import datetime

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_preview_paused: bool = False
    is_recording: bool = False
    is_recording_paused: bool = False


async def main(page: ft.Page):
    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_image = ft.Image(
        src="iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/wIAAgMBAp0YVwAAAABJRU5ErkJggg==",
        height=200,
        fit=ft.BoxFit.CONTAIN,
        gapless_playback=True,
    )
    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, on_click=lambda _: None, tooltip="Take photo"
    )
    record_btn = ft.FilledTonalIconButton(
        icon=ft.Icons.VIDEOCAM,
        selected_icon=ft.Icons.STOP,
        selected=False,
        on_click=lambda _: None,
        tooltip="Start / stop recording",
    )
    pause_recording_btn = ft.OutlinedIconButton(
        icon=ft.Icons.PAUSE,
        selected_icon=ft.Icons.PLAY_ARROW,
        selected=False,
        on_click=lambda _: None,
        tooltip="Pause / resume recording",
        disabled=True,
    )
    stream_btn = ft.OutlinedIconButton(
        icon=ft.Icons.PLAY_ARROW,
        selected_icon=ft.Icons.STOP,
        selected=False,
        on_click=lambda _: None,
        tooltip="Start / stop image stream",
        visible=False,
    )
    preview_btn = ft.OutlinedIconButton(
        icon=ft.Icons.VISIBILITY_OFF,
        selected_icon=ft.Icons.VISIBILITY,
        selected=True,
        on_click=lambda _: None,
        tooltip="Pause / resume preview",
    )

    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 detect_video_extension(data: bytes) -> str:
        # Matroska/WebM EBML header.
        if data.startswith(b"\x1a\x45\xdf\xa3"):
            return "webm"

        # ISO BMFF (mp4/mov) starts with a box size + "ftyp".
        if len(data) >= 12 and data[4:8] == b"ftyp":
            brand = data[8:12]
            if brand == b"qt  ":
                return "mov"
            return "mp4"

        return "bin"

    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
        status.value = "Select a camera"
        page.update()

    def sync_action_buttons():
        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.visible = state.is_streaming_supported
        preview_btn.selected = not state.is_preview_paused

    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,
        )
        state.is_streaming = False
        state.is_streaming_supported = await preview.supports_image_streaming()
        sync_action_buttons()
        page.update()

    async def take_photo():
        data = await preview.take_picture()
        last_image.src = data
        last_image.update()

    async def start_recording():
        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():
        await preview.pause_video_recording()
        state.is_recording_paused = True
        sync_action_buttons()
        status.value = "Recording paused"
        page.update()

    async def resume_recording():
        await preview.resume_video_recording()
        state.is_recording_paused = False
        sync_action_buttons()
        status.value = "Recording resumed"
        page.update()

    async def stop_recording():
        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 = 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: ft.Event[fc.CameraState]):
        if e.description == state.selected_camera:
            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_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():
        await preview.stop_image_stream()
        state.is_streaming = False
        sync_action_buttons()
        page.update()

    def on_stream_image(e: ft.Event[fc.CameraImage]):
        try:
            last_image.src = e.bytes
            last_image.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():
        await preview.pause_preview()
        state.is_preview_paused = True
        sync_action_buttons()
        page.update()

    async def resume_preview():
        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,
                    ft.Text("Last photo"),
                    last_image,
                ]
            )
        )
    )

    await get_cameras()


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

Description#

Inherits: LayoutControl

A control that provides camera preview and capture capabilities.

Properties

Events

Methods

Properties#

content class-attribute instance-attribute #

content: Control | None = None

Optional child to overlay on top of the camera preview.

preview_enabled class-attribute instance-attribute #

preview_enabled: bool = True

Whether the preview surface is shown.

Events#

on_state_change class-attribute instance-attribute #

on_state_change: EventHandler[CameraState] | None = None

Fires when the camera controller state changes.

on_stream_image class-attribute instance-attribute #

on_stream_image: EventHandler[CameraImage] | None = None

Fires when an image frame is available while streaming.

Methods#

get_available_cameras async #

get_available_cameras() -> list[CameraDescription]

Lists the available camera devices on the current platform.

Returns:

get_exposure_offset_step_size async #

get_exposure_offset_step_size() -> float

Returns the smallest increment supported for exposure offset changes.

get_max_exposure_offset async #

get_max_exposure_offset() -> float

Maximum exposure offset supported by the current camera.

get_max_zoom_level async #

get_max_zoom_level() -> float

Maximum zoom level supported by the current camera.

get_min_exposure_offset async #

get_min_exposure_offset() -> float

Minimum exposure offset supported by the current camera.

get_min_zoom_level async #

get_min_zoom_level() -> float

Minimum zoom level supported by the current camera.

initialize async #

initialize(
    description: CameraDescription,
    resolution_preset: ResolutionPreset,
    enable_audio: bool = True,
    fps: int | None = None,
    video_bitrate: int | None = None,
    audio_bitrate: int | None = None,
    image_format_group: ImageFormatGroup | None = 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 (int | None, default: None ) –

    Optional target frames per second.

  • video_bitrate (int | None, default: None ) –

    Optional video bitrate.

  • audio_bitrate (int | None, default: None ) –

    Optional audio bitrate.

  • image_format_group (ImageFormatGroup | None, default: None ) –

    Optional image format group override.

lock_capture_orientation async #

lock_capture_orientation(
    orientation: DeviceOrientation | None = None,
)

Locks capture orientation to the specified device orientation.

Parameters:

  • orientation (DeviceOrientation | None, default: None ) –

    Specific orientation to lock, or current

pause_preview async #

pause_preview()

Pauses the camera preview.

pause_video_recording async #

pause_video_recording()

Pauses an active video recording.

prepare_for_video_recording async #

prepare_for_video_recording()

Prepares the capture session for video recording.

resume_preview async #

resume_preview()

Resumes the camera preview.

resume_video_recording async #

resume_video_recording()

Resumes a paused video recording.

set_description async #

set_description(description: CameraDescription)

Switches to another camera description.

Parameters:

set_exposure_mode async #

set_exposure_mode(mode: ExposureMode)

Changes the exposure mode.

Parameters:

  • mode (ExposureMode) –

    Exposure mode to apply.

set_exposure_offset async #

set_exposure_offset(offset: float) -> float

Sets exposure offset in EV units.

Parameters:

  • offset (float) –

    Exposure offset to apply.

Returns:

  • float

    The offset value that was set.

set_exposure_point async #

set_exposure_point(point: OffsetValue | None)

Sets the exposure metering point.

Parameters:

  • point (OffsetValue | None) –

    Normalized offset (0..1) or None to reset.

set_flash_mode async #

set_flash_mode(mode: FlashMode)

Changes the flash mode.

Parameters:

  • mode (FlashMode) –

    Flash mode to apply.

set_focus_mode async #

set_focus_mode(mode: FocusMode)

Changes the focus mode.

Parameters:

  • mode (FocusMode) –

    Focus mode to apply.

set_focus_point async #

set_focus_point(point: OffsetValue | None)

Sets the focus metering point.

Parameters:

  • point (OffsetValue | None) –

    Normalized offset (0..1) or None to reset.

set_zoom_level async #

set_zoom_level(zoom: float)

Applies the provided zoom level.

Parameters:

  • zoom (float) –

    Zoom level to set.

start_image_stream async #

start_image_stream()

Begins streaming camera image frames.

start_video_recording async #

start_video_recording()

Starts capturing video.

stop_image_stream async #

stop_image_stream()

Stops streaming camera image frames.

stop_video_recording async #

stop_video_recording() -> bytes

Stops video recording and returns the recorded bytes.

Returns:

  • bytes

    Encoded video file bytes.

supports_image_streaming async #

supports_image_streaming() -> bool

Indicates whether image streaming is supported on the current platform.

take_picture async #

take_picture() -> bytes

Captures a still image and returns the encoded bytes.

Returns:

  • bytes

    Encoded image bytes.

unlock_capture_orientation async #

unlock_capture_orientation()

Unlocks the capture orientation.