Building a Responsive Camera Component Using React Hooks

  • Publish Date
  • Reading Time
    15 minutes
  • Tags
    • react
    • css
Post

    I was recently tasked with building a frontend camera component that allows users to upload images of their identification cards to a backend service. In this post I'll demonstrate how I created the component by explaining how to configure a live media stream and capture a snapshot with React hooks, and how to style and position the elements using Styled Components. As such, the article assumes a working knowledge of functional components in React 16.x and the Styled Components library. Below you can see a demo of the component in action, and feel free to play around with the complete solution on my Code Sandbox as you read along. Enjoy!

    Camera demo
    Camera demo

    Configuration

    Let’s begin by accessing the browser navigator and invoking the

    method to display a live video feed from the user’s camera. Since the component is designed to take photographs of identity cards, we can pass a configuration object that does not require audio and defaults to the rear-facing camera on mobile devices. By passing an options object to the video property, video is assumed to be
    Loading...
    .

    Loading...

    The

    Loading...
    method requests permission from the user to access the media defined in the configuration. It then returns a promise that will either resolve and return a
    Loading...
    object that can be stored in local state, or reject and return an error. Using one of React's
    Loading...
    hooks, we create and store the requested stream if none exists (i.e. our local state is empty) or return a cleanup function to prevent any potential memory leaks when the component unmounts. The cleanup simply loops through and stops each of the media tracks stored in local state via the
    Loading...
    method.

    With the stream stored in local state it can then be bound to a

    Loading...
    element. Since React does not support the srcObject attribute, we use a ref to target the
    Loading...
    and assign the stream to it's
    Loading...
    property. With a valid source the video will trigger an
    Loading...
    event where we can trigger video playback. This implementation is necessary since the video
    Loading...
    attribute does not work consistently across all platforms. We can abstract all of this logic into a custom hook that takes the configuration object as an argument, creates the cleanup function, and returns the stream to the camera component.

    Loading...
    Loading...

    Positioning

    With the media stream configured we can start to position the video within the component. To enhance the user experience, the camera feed should resemble an identification card. This requires the preview container to maintain a landscape ratio regardless of the native resolution of the camera (desktop cameras typically have a square or landscape ratio, where we assume that mobile devices will capture the images in portrait). This is achieved by calculating a ratio that is >= 1 by always dividing by the largest dimension. Once the video is available for playback (i.e. when the

    Loading...
    event is invoked) we can evaluate the native resolution of the camera and use it to calculate the desired aspect ratio of the parent container.

    In order for the component to be responsive, it will need to be notified whenever the width of the parent container has changed so that the height can be recalculated.

    exports a
    Loading...
    component that provides the
    Loading...
    of a referenced element as an argument in an
    Loading...
    callback. Whenever the container mounts or is resized, the argument's
    Loading...
    property is used to determine the container height by dividing it by the calculated ratio.

    Similar to before, the ratio calculation is abstracted into a custom hook and returns both the calculated ratio and setter function. Since the ratio will remain constant we can utilise React's

    hook to prevent any unnecessary recalculations.

    Loading...
    Loading...

    The current solution works well if the video element is smaller than the parent container, but in the event that the native resolution is larger it will overflow and cause layout issues. Adding

    Loading...
    &
    Loading...
    to the parent and absolutely positioning the video will prevent the break in layout, but the video will appear off-centre to the user. To compensate for this we centre the feed by calculating axis-offsets that subtract the dimensions of the video element from the parent container and half the resulting value.

    Loading...

    We only want to apply the offsets in the event that the video (v) is larger than the parent container (c). We can create another custom hook that uses an effect to evaluate whether an offset is required and returns the updated results whenever any of the values change. At this point we now have a responsive live feed that roughly resembles an identification card and is correctly positioned within the parent container.

    Enjoying the article?

    Support the content
    Loading...
    Loading...

    Capture / Clear

    To emulate a camera snapshot, a

    Loading...
    element is positioned on top of the video with matching dimensions. Whenever the user initiates a capture, the current frame in the feed will be drawn onto the canvas and cause the video to become temporarily hidden. This is achieved by creating a two-dimensional rendering context on the canvas, drawing the current frame of the video as an image and then exporting the resulting
    Loading...
    as an argument in a
    Loading...
    callback.

    Loading...

    The arguments supplied to the

    method are broadly split into three groups: the source image, the source image parameters (s), and the destination canvas parameters (d). We need to consider the potential axis-offsets when drawing the canvas, as we only want to snapshot the section of the video feed that is visible from within the parent container. We'll add our offsets to the source image's starting axis coordinates and use the parent container's width and height for both the source and destination boundaries. Since we want to draw our snapshot onto the entire canvas, no destination offsets are required.

    Loading...

    To discard the image, the canvas is reverted to it's initial state via a

    Loading...
    callback. Calling
    Loading...
    will retrieve the same drawing context instance that was previously returned in the
    Loading...
    function. We can then pass the canvas' width and height to the context
    Loading...
    function to convert the requested pixels to transparent and resume displaying the video feed.

    Loading...
    Loading...

    Styling

    With the ability to capture an image, all that remains is to implement a card-aid overlay, a flash animation on capture, and style the elements using Styled Components. The overlay component is a white rounded border layered on top of the video to encourage the user to fit their identification card within the boundary, with an outer box-shadowed area acting as a safe-zone to prevent clipping. The flash component has a solid white background and is also layered on top of the video, but will initially appear hidden due to a default opacity of 0. The keyframes animation triggers whenever the user captures an image, which briefly sets the opacity to 0.75 before quickly reducing it back to zero to emulate a flash effect. Adding a local state variable,

    Loading...
    , keeps the video and overlay elements hidden until the camera begins streaming. We can pass the resolution of the camera as props to the parent container to determine it's maximum width and height, and finally add
    Loading...
    to
    Loading...
    to hide the video's play symbol on iOS devices. πŸ’₯

    Loading...
    Loading...

    Conclusion

    For the moment the component serves to provide images as proof of authenticity, and is used alongside a form where users manually input field information from the identification cards. I'm hoping to follow this post up with an integration with OCR technology to scrape the fields from the images and remove the requirement for the form altogether. Thanks for reading along, and special thanks to Pete Correia for taking the time to review the component code.