Lanbo
Lanbo

Reputation: 15712

Lazy Bytestring Streaming using Warp

I have been trying to convert this to Haskell. Ultimately I want to use Conduits, but for now, a lazy ByteString will do, I hope.

So I wrote this:

{-# LANGUAGE OverloadedStrings, RecordWildCards #-}
module Main where

--import Data.Conduit.Binary
import Network.Wai
import Network.Wai.Handler.Warp
import Network.HTTP.Types
import qualified Data.ByteString.Lazy as L
import qualified Data.ByteString.Char8 as B
import Control.Monad.Trans
import System.IO
import Control.Exception
import Data.List.Split (splitOn)

video = "videos/big_buck_bunny.mp4"

main = bracket (openFile video ReadMode) (hClose) $ \h ->
    run 3000 $ \Request{..} -> do
        liftIO $ putStrLn "Connection"
        total <- liftIO $ hFileSize h
        case lookup "Range" requestHeaders of
            Nothing -> do
                v <- liftIO $ L.hGetContents h
                return $ responseLBS ok200 [("Content-Type", "video/mp4"), ("Connection","keep-alive"), ("Content-Length", B.pack $ show total)] v
            Just r -> do
                let (starts:ends:_) = splitOn "-" $ drop 6 $ B.unpack r
                    start = read starts
                    end = if not (null ends) then read ends else total - 1
                liftIO $ putStrLn $ "Range request " ++ show start ++ " " ++ show end
                liftIO $ hSeek h AbsoluteSeek start
                v <- liftIO $ L.hGetContents h
                return $ responseLBS partialContent206 [("Content-Type", "video/mp4")
                                                       , ("Accept-Ranges", "bytes")
                                                       , ("Content-Length", B.pack . show $ (end - start) + 1)
                                                       , ("Content-Range", B.pack $ concat ["bytes ", show start, "-", show end, "/", show total])
                                                       ] v

However, when I start it and open up localhost:3000, I get these errors:

Connection
send: resource vanished (Connection reset by peer)
Connection
Range request 0 159240553
send: resource vanished (Connection reset by peer)
Connection
Range request 158852274 159240553
Connection
videos/big_buck_bunny.mp4: hFileSize: illegal operation (handle is closed)
Connection
videos/big_buck_bunny.mp4: hFileSize: illegal operation (handle is closed)

I don't really understand why these errors happen, most of all why the resource vanished. And also why the handle is being closed without me doing hClose on it.

Can someone hint me where I am going wrong with this?

Upvotes: 1

Views: 374

Answers (2)

Tener
Tener

Reputation: 5279

I'm not sure about the first errors, but there is a clear bug in your code:

            v <- liftIO $ L.hGetContents h

You use hGetContents which is known for closing the file once it has completed with it. See: http://hackage.haskell.org/packages/archive/bytestring/0.10.2.0/doc/html/Data-ByteString-Lazy.html#v:hGetContents

After serving the file first time the handle is closed and subsequently it raises errors on read attempts.

If you want to use lazy IO, despite the fact it is unsafe and unsound, you can try something like:

            v <- liftIO $ L.readFile video

But note that it is unsafe because you have a limit on the number of open handles you can have and you have just used one of them and you can't be sure when it is released.

To address these problems you should just use Conduits (or a library built on top of it). In WAI there is ResponseFile, but I'm unsure how exactly you should do this: http://hackage.haskell.org/packages/archive/wai/1.4.0.1/doc/html/Network-Wai.html#v:ResponseFile

Update:

Here is working code using ResponseFile. https://gist.github.com/Tener/5803280

Upvotes: 1

Ankur
Ankur

Reputation: 33657

The documentation for hGetContents says that one EOF is reached the handle is closed. What you can do is take the hFileSize and hGetContents outside of the run command and due to lazy evaluation only the required data from the file will be in memory. For range requests you need to fetch the data from the bytestring that you got from hGetContents using take and drop functions.

Upvotes: 0

Related Questions