A webcam server in 35 lines of Haskell
See also : vidéo peertube - vidéo youtube - dépôt git - article linuxfr.org
This post shows how to implement a webcam server in Haskell. The implemented server is a classic web server that stores the current image in memory and sends it to any HTTP client that requests it. The current image is updated in parallel, from the video stream of the webcam.
Video capture
First, we need to open and capture the video stream of the webcam. This can be done easily using OpenCV, a classic computer vision library. OpenCV is implemented in C++ but it has many wrappers, including a Haskell wrapper: haskell-opencv.
In the following code, the openCam
function opens the first video device (id
0) and sets its frame rate at 5 FPS. Then, the captureCam
function reads an
OpenCV image (type Mat ('S ['D, 'D]) 'D 'D
) from a video device (type
VideoCapture
). Finally, the imgToPng
function converts an OpenCV image to a
PNG image that can be displayed by a web browser.
{-# language DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
import Control.Concurrent (forkIO)
import Control.Monad (forever, unless, liftM)
import Data.ByteString (ByteString)
import Data.ByteString.Lazy (fromStrict)
import Data.IORef (atomicWriteIORef, IORef, newIORef, readIORef)
import qualified Web.Scotty as SC
import OpenCV
import OpenCV.VideoIO.Types
openCam :: IO (Maybe VideoCapture)
= do
openCam <- newVideoCapture
cap $ videoCaptureOpen cap $ VideoDeviceSource 0 Nothing
exceptErrorIO <- videoCaptureIsOpened cap
isOpened case isOpened of
False -> return Nothing
True -> videoCaptureSetD cap VideoCapPropFps 5 >> (return $ Just cap)
captureCam :: VideoCapture -> IO (Maybe (Mat ('S ['D, 'D]) 'D 'D))
= videoCaptureGrab cap >> videoCaptureRetrieve cap
captureCam cap
imgToPng :: Mat ('S ['D, 'D]) 'D 'D -> ByteString
= exceptError . imencode (OutputPng defaultPngParams) imgToPng
We can test these functions locally, with the following code. The loopCam
function reads an image (using captureCam
), displays this image in a window
then loops recursively unless the “esc” key is pressed. The main function
simply opens a video device (with openCam
), creates a window and launches
the loopCam
recursion.
main :: IO ()
= do
main <- openCam
capMaybe case capMaybe of
Nothing -> putStrLn "couldn't open device"
Just cap -> withWindow "webcamer" (loopCam cap)
loopCam :: VideoCapture -> Window -> IO ()
= do
loopCam cap window <- captureCam cap
imgMaybe case imgMaybe of
Nothing -> return ()
Just img -> do
imshow window img<- waitKey 20
key == 27) $ loopCam cap window unless (key
If you run this code, you should see a window showing the video stream of your webcam, at 5 FPS.
Web server
Now let’s serve our video stream through a web server, using the scotty web
framework. Instead of the
previous main
and loopCam
functions, the main
function now opens the
video device then runs runServer
. The runServer
function runs a scotty
server that serves two routes. For the route “/”, it serves the home page (i.e.,
the file index.html
). For the route “/out.png”, it reads an image from the
webcam, converts this image to PNG format then sends it to the HTTP client.
main :: IO ()
= do
main <- openCam
capMaybe case capMaybe of
Nothing -> putStrLn "couldn't open device"
Just cap -> runServer 3042 cap
runServer :: Int -> VideoCapture -> IO ()
= SC.scotty port $ do
runServer port cap "/" $ SC.file "index.html"
SC.get "/out.png" $ do
SC.get "Content-Type" "image/png"
SC.setHeader <- SC.liftAndCatchIO $ liftM imgToPng <$> captureCam cap
imgMaybe case imgMaybe of
Nothing -> return ()
Just img -> SC.raw $ fromStrict img
This web server sends an image when a client requests it. To really
display the video stream, the client has to request an image regularly. This is
done in the following index.html
page. The updateImg
function requests the
“out.png” route to the server and updates the HTML page when the
image is received from the server. This function is called every 200 ms (i.e.,
at 5 FPS), thanks to setInterval
.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
<h1>webcamer</h1>
<img id="my_img"> </img>
<script>
function updateImg() {
fetch("out.png")
.then(response => response.blob())
.then(function(myBlob){
.revokeObjectURL(my_img.src);
URL.src = URL.createObjectURL(myBlob);
my_img;
})
}const my_interval = setInterval(updateImg, 200);
</script>
</body>
</html>
Handling multiple connections
The previous web server reads an image when a client requests the “out.png” route. However, this doesn’t work for multiple clients because the video stream can’t provide enough images. To solve this problem, we can read the video stream and handle the HTTP requests independently.
The following code uses a mutable reference
IORef to store
the current image. This image is read in the runServer
function when a HTTP
client requests it, and it is updated in the runCam
function when a new image
is available from the video stream. Finally, the main
function initializes
the mutable reference and runs runServer
and runCam
in parallel, using
forkIO
(lightweight threads).
main :: IO ()
= do
main <- openCam
capMaybe case capMaybe of
Nothing -> putStrLn "couldn't open device"
Just cap -> do
Just png0 <- liftM imgToPng <$> captureCam cap
<- newIORef png0
pngRef <- forkIO $ runCam cap pngRef
_ 3042 pngRef
runServer
runServer :: Int -> IORef ByteString -> IO ()
= SC.scotty port $ do
runServer port pngRef "/" $ SC.file "index.html"
SC.get "/out.png" $ do
SC.get "Content-Type" "image/png"
SC.setHeader <- SC.liftAndCatchIO (readIORef pngRef)
img $ fromStrict img
SC.raw
runCam :: VideoCapture -> IORef ByteString -> IO ()
= forever $ do
runCam cap pngRef <- liftM imgToPng <$> captureCam cap
imgMaybe mapM_ (atomicWriteIORef pngRef) imgMaybe
Thus, if several HTTP clients request an image but only one image is available in the video stream, the server sends the same image to all clients.
Putting everything together
The final code of the server is given below. This code handles webcam capture, web service and multiple clients. And it’s only 35 lines of Haskell.
{-# LANGUAGE OverloadedStrings #-}
import Control.Concurrent (forkIO)
import Control.Monad (forever, liftM)
import Data.ByteString.Lazy (fromStrict)
import Data.IORef (atomicWriteIORef, newIORef, readIORef)
import Web.Scotty (get, file, raw, scotty, liftAndCatchIO, setHeader)
import OpenCV
import OpenCV.VideoIO.Types
= do
main <- openCam
capMaybe case capMaybe of
Nothing -> putStrLn "couldn't open device"
Just cap -> do
Just png0 <- liftM imgToPng <$> captureCam cap
<- newIORef png0
pngRef <- forkIO $ runCam cap pngRef
_ 3042 pngRef
runServer
= scotty port $ do
runServer port pngRef "/" $ file "index.html"
get "/out.png" $ do
get "Content-Type" "image/png"
setHeader <- liftAndCatchIO (readIORef pngRef)
img $ fromStrict img
raw
= forever $ do
runCam cap pngRef <- liftM imgToPng <$> captureCam cap
imgMaybe mapM_ (atomicWriteIORef pngRef) imgMaybe
= do
openCam <- newVideoCapture
cap $ videoCaptureOpen cap $ VideoDeviceSource 0 Nothing
exceptErrorIO <- videoCaptureIsOpened cap
isOpened case isOpened of
False -> return Nothing
True -> videoCaptureSetD cap VideoCapPropFps 5 >> (return $ Just cap)
= videoCaptureGrab cap >> videoCaptureRetrieve cap
captureCam cap
= exceptError . imencode (OutputPng defaultPngParams) imgToPng