mbr/elm-camera/1.1.0

NOTE: the latest version is 1.3.0

Camera

A friendly interface for built-in video sources, such as cameras.

Camera lets you open a camera, change its settings, take snapshots, and show a live preview in an HTML element.

The current API covers the common case of opening and using a single camera. It does not yet expose the full range of browser media features.

Commands and information pass between Elm and the browser through ports. Large data items are not passed to Elm, but are addressed through opaque handles instead.

Camera workflow

Using a camera follows a small request-and-result loop:

  1. Call open, or use openWithOptions to choose a device and send initial Configuration.Constraints. The regular open function starts with Configuration.defaultConstraints, where every field is Nothing.
  2. Handle OpenCompleted. A successful result contains an OpenedCamera with its ID, capabilities, and active settings.
  3. Keep your constraints in your application model. To change the camera, update that value and send the complete new constraints with configure.
  4. Handle ConfigureCompleted. A successful result contains the active settings the browser actually applied.

Constraints express what your application wants, while settings describe what the browser is using. The browser may choose compatible values that differ from the request, so use the returned settings as the current camera state. Both completion events return Err CameraError when their operation fails.

Installation

Install the Elm package:

elm install mbr/elm-camera

Elm packages cannot declare ports. Make Main.elm a port module, or add an application-owned port module containing these declarations:

import Camera exposing (CommandPort, EventPort)


port cameraCmd : CommandPort msg


port cameraMsg : EventPort msg

The JavaScript runtime expects the exact port names cameraCmd and cameraMsg.

Copy js/elm-camera.js into the browser assets. Load the runtime and compiled Elm application, initialize Elm, and then initialize the camera runtime:

<script src="elm-camera.js"></script>
<script src="app.js"></script>
<script>
var app = Elm.Main.init({
  node: document.getElementById("elm")
});
var cameraRuntime = ElmCamera.initApp(app);
</script>

initApp returns a small runtime object. Most applications can leave it running for the lifetime of the page. If your Elm application is removed or replaced, call shutdown() to release its cameras, snapshots, and any URLs created for those snapshots.

If the cameraCmd port is not present, initApp simply returns null. Application-owned browser integrations can resolve a retained snapshot handle with cameraRuntime.snapshotBlob(handle). Use Snapshot.encodeHandle and Snapshot.handleDecoder at their port boundaries.

Camera access requires a secure browser context, so use HTTPS in production. Browsers normally treat http://localhost, http://127.0.0.1, and http://[::1] as trustworthy for local development. A phone visiting a plain HTTP address on the local network is not using localhost and generally needs HTTPS.

Basic usage

Call openRear from a user action, subscribe to camera events, and track the pending operation in your application's model until OpenCompleted arrives:

import Camera
import CameraPorts as Ports


type Model
    = Opening
    | Ready Camera.OpenedCamera
    | Failed Camera.CameraError


type Msg
    = CameraEvent Camera.Event


init : () -> ( Model, Cmd Msg )
init _ =
    ( Opening, Camera.openRear Ports.cameraCmd )


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        CameraEvent (Camera.OpenCompleted result) ->
            case result of
                Ok camera ->
                    ( Ready camera, Cmd.none )


                Err error ->
                    ( Failed error, Cmd.none )


        CameraEvent _ ->
            ( model, Cmd.none )


subscriptions : Model -> Sub Msg
subscriptions _ =
    Sub.map CameraEvent (Camera.subscribe Ports.cameraMsg)

A successful open returns the camera ID together with its capabilities and active settings. Camera acquisition and permission failures arrive as Err CameraError.

Ports

CommandPort and EventPort match the two application-owned ports shown above. Pass those ports to camera commands and subscriptions.

type alias CommandPort msg =
( String, Value ) -> Cmd msg

A port provided by the application for sending camera commands to the browser.

type alias EventPort msg =
(( String, Value ) -> msg) -> Sub msg

A port provided by the application for receiving camera events from the browser.

Commands and events

Only one camera can be opening at a time. Wait for a command's completion event before sending another command that changes the same camera.

The named functions below cover the usual commands. send is available when it is more convenient to work with Command values directly. subscribe receives both replies to commands and events generated by an open camera.

type Command
= EnumerateDevices
| Open OpenOptions
| SetEnabled CameraId Bool
| Close CameraId
| GetSupportedConstraints
| GetCapabilities CameraId
| GetSettings CameraId
| Configure CameraId Constraints
| CapturePng CameraId Options
| CreateSnapshotObjectUrl Handle
| DiscardSnapshot Handle

A request sent to the browser runtime.

Most applications can use the named command functions. send is useful when keeping commands as values is more convenient.

type Event
= DevicesCompleted (Result CameraError (List Device))
| DevicesChanged (Result CameraError (List Device))
| OpenCompleted (Result CameraError OpenedCamera)
| EnabledCompleted CameraId Bool (Result CameraError ())
| CloseCompleted CameraId (Result CameraError ())
| SupportedConstraintsCompleted
| CapabilitiesCompleted
CameraId
(Result CameraError Capabilities)
| SettingsCompleted CameraId (Result CameraError Settings)
| ConfigureCompleted CameraId (Result CameraError Settings)
| SnapshotCaptured CameraId (Result CameraError Metadata)
| SnapshotObjectUrlCreated Handle (Result CameraError Url)
| SnapshotDiscarded Handle (Result CameraError ())
| CameraMuted CameraId
| CameraUnmuted CameraId
| CameraEnded CameraId
| RuntimeError CameraError

A reply or camera event received from the browser runtime.

Each command produces a matching completion event containing a Result. Device and camera changes can arrive on their own. RuntimeError contains a failure that could not be tied to a completion event. An ended camera is already closed and does not need to be passed to close.

send : CommandPort msg -> Command -> Cmd msg

Send a Command through the application's command port.

The named command functions are usually more convenient.

subscribe : EventPort Event -> Sub Event

Listen for camera events through the application's event port.

Use Sub.map to turn these events into application messages. Keep the subscription active while a command is pending or a camera is open.

Opening and devices

open uses the browser's default camera, which is usually the right choice for desktop computers and USB webcams. openFront and openRear ask for a camera facing in a particular direction, but still allow the browser to choose another camera when needed.

Use openWithOptions to choose a device or ask for particular initial settings. devices can help with device pickers, but opening a camera is the only way to know whether it is available. If the browser lists a camera without an ID, devices returns Err DeviceInformationUnavailable. Opening a camera and then calling devices again will usually reveal IDs and labels.

Camera settings and facing modes are defined in Camera.Configuration.

type alias Device =
{ id : DeviceId
, label : Maybe String
}

A camera listed by the browser.

The label is Nothing when the browser does not provide one.

type alias DeviceId =
DeviceId

A camera device known to the browser.

type alias OpenOptions =
{ deviceId : Maybe DeviceId
, constraints : Constraints
}

The device and initial settings to ask for when opening a camera.

defaultOpenOptions : OpenOptions

Options that let the browser choose the camera and its settings.

type alias OpenedCamera =
{ id : CameraId
, capabilities : Capabilities
, settings : Settings
}

The handle and initial information returned for an opened camera.

devices : CommandPort msg -> Cmd msg

Ask the browser for its current list of cameras.

Browsers often hide device IDs until the page has opened a camera. In that case, the result is Err DeviceInformationUnavailable. Call devices again after a successful OpenCompleted to get the selectable cameras.

open : CommandPort msg -> Cmd msg

Open the browser's default camera.

This is usually the right choice on desktop computers. The result arrives in OpenCompleted.

openFront : CommandPort msg -> Cmd msg

Open a camera while preferring one that faces the user.

The browser may choose another camera when needed. The result arrives in OpenCompleted.

openRear : CommandPort msg -> Cmd msg

Open a camera while preferring one that faces away from the user.

The browser may choose another camera when needed. The result arrives in OpenCompleted.

openWithOptions : CommandPort msg -> OpenOptions -> Cmd msg

Open a camera with a chosen device or initial settings.

The result arrives in OpenCompleted.

Camera information and configuration

Camera configuration has four parts:

  • supportedConstraints tells you which setting names the browser knows.
  • capabilities tells you what an open camera can do.
  • Configuration.Constraints contains the values your application asks for.
  • settings tells you what the browser is actually using.

Keep your constraints in your application model when making several changes. A property the browser does not report is Nothing. Even when a property is supported, a particular value or combination may still fail. After calling configure, use the Configuration.Settings from ConfigureCompleted as the camera's current settings.

For example, request continuous autofocus only when the camera advertises it:

import Camera.Configuration as Configuration


enableContinuousFocus :
    CommandPort msg
    -> OpenedCamera
    -> Configuration.Constraints
    -> Cmd msg
enableContinuousFocus commandPort camera constraints =
    if
        camera.capabilities.focusMode
            |> Maybe.withDefault []
            |> List.member Configuration.Continuous
    then
        configure commandPort
            camera.id
            { constraints
                | focusMode = Just Configuration.Continuous
            }


    else
        Cmd.none

See Camera.Configuration for the control types and a detailed explanation of capabilities, constraints, and settings.

supportedConstraints : CommandPort msg -> Cmd msg

Ask which camera setting names the browser recognizes.

capabilities : CommandPort msg -> CameraId -> Cmd msg

Ask what an open camera can do.

settings : CommandPort msg -> CameraId -> Cmd msg

Ask which settings a camera is currently using.

configure : CommandPort msg -> CameraId -> Constraints -> Cmd msg

Ask a camera to use new settings.

The result arrives in ConfigureCompleted with the settings the browser actually applied.

Preview

preview creates an HTML element showing the camera's live video. You can add any HTML attributes your application needs, for example:

Camera.preview camera.id
    [ Html.Attributes.class "my-viewfinder" ]

Removing the element does not close the camera, and adding it again reconnects the preview. The same camera can appear in more than one preview, and regular HTML or SVG can be placed over it.

type alias CameraId =
CameraId

A handle for an open camera.

The camera itself stays in the browser. IDs are never reused, and using one after its camera has closed or ended produces StaleCamera.

preview : CameraId -> List (Attribute msg) -> Html msg

Show a camera's live video in a camera-preview element.

The given attributes are added to the element. Adding, removing, or changing a preview does not open or close its camera.

Snapshots

capturePng takes a snapshot of the current frame. The PNG stays in the browser rather than passing through Elm, and SnapshotCaptured returns its metadata and an opaque handle.

A successful snapshot is kept until you call discardSnapshot for its handle, or until the camera runtime shuts down. Removing the handle from your Elm model, removing an image from the page, or closing the camera does not release it. Use snapshotObjectUrl to show or download the PNG, then call discardSnapshot when it is no longer needed.

import Camera.Snapshot as Snapshot


capture : CommandPort msg -> OpenedCamera -> Cmd msg
capture commandPort camera =
    capturePng commandPort camera.id Snapshot.defaultOptions

See Camera.Snapshot for snapshot options, metadata, and URL cleanup.

capturePng : CommandPort msg -> CameraId -> Options -> Cmd msg

Take a PNG snapshot of the camera's current frame.

The result arrives in SnapshotCaptured. The PNG stays in the browser until its handle is passed to discardSnapshot or the runtime shuts down.

snapshotObjectUrl : CommandPort msg -> Handle -> Cmd msg

Create a browser URL for showing or downloading a snapshot.

discardSnapshot : CommandPort msg -> Handle -> Cmd msg

Release a snapshot and any browser URLs created for it.

The result arrives in SnapshotDiscarded. A snapshot that has already been released produces StaleSnapshot.

Lifecycle

setEnabled pauses or resumes camera output without releasing the camera. close releases the camera and makes its ID stale. Removing every preview does not close the camera.

A camera can also be muted, unmuted, or ended by the browser or device. These changes arrive through CameraMuted, CameraUnmuted, and CameraEnded. Camera and snapshot IDs are never reused, so an old ID cannot accidentally refer to a new resource.

setEnabled : CommandPort msg -> CameraId -> Bool -> Cmd msg

Pause or resume camera output without releasing the camera.

close : CommandPort msg -> CameraId -> Cmd msg

Close and release a camera.

Errors

Command failures arrive in completion events as Err values. If a message from the runtime cannot be read, subscribe produces RuntimeError containing a ProtocolFailure instead of crashing the Elm application.

= UnsupportedBrowser String
| InsecureContext
| PermissionDenied
| DeviceNotFound
| DeviceInformationUnavailable
| DeviceUnavailable String
| UnsupportedConstraint String
| Overconstrained (Maybe String) String
| OpenAlreadyPending
| OperationCanceled
| StaleCamera CameraId
| StaleSnapshot Handle
| PreviewFailed String
| CaptureFailed String
| EncodingFailed String
| UnknownCameraError String
| ProtocolFailure ProtocolError

Something that went wrong while using Camera.

  • UnsupportedBrowser means the required browser camera API is absent.
  • InsecureContext means the page is not allowed to request a camera from its current origin.
  • PermissionDenied means camera access was refused.
  • DeviceNotFound means no camera matched the request.
  • DeviceInformationUnavailable means the browser listed a camera without enough information to select it. Opening a camera and trying again may help.
  • DeviceUnavailable means a matching camera could not be read, for example because another application owns it or the operating system rejected it.
  • UnsupportedConstraint means the browser does not recognize a requested constraint.
  • Overconstrained contains the reported constraint name, when available, and the browser's message.
  • OpenAlreadyPending means another open operation has not completed.
  • OperationCanceled means the operation was canceled before completion.
  • StaleCamera and StaleSnapshot identify resources that no longer exist.
  • PreviewFailed, CaptureFailed, and EncodingFailed describe failures in their respective operations.
  • UnknownCameraError preserves an otherwise unclassified browser failure.
  • ProtocolFailure contains a message from the runtime that Elm could not read.
type alias ProtocolError =
{ event : String
, message : String
}

Details about a message from the browser runtime that Elm could not read.

Describe a camera error in friendly language.

Browser-provided details are included when they help explain the failure.