ebml-0.1.1.0: src/Codec/EBML/Stream.hs
{- | This module contains the incremental decoder logic to process a continuous stream.
Here is an example stream layout:
> | EBML | SIZE | ELT | ELTA | ELT | ... | SEGMENT | USIZE | ELT | ELTB | ... |
> | CLUSTER | USIZE | ELT | ELTC | ... | CLUSTER | USIZE | ELT | ELT | ... |
There are two difficulties:
- The element are not aligned, a segment id can start at position 15.
- Unknown sized element, such as segment and cluster, need to use a look-ahead to ensure it is completed.
Here are the main scenarios:
- The initial buffers does not contains the begining of a media segment.
In that case we need to accumulate the data to provide the complete initialization segments.
- The buffer contains multiple segments. In that case we need to find the last one, e.g. the most recent.
- The buffer ends on the middle of the cluster id, e.g. "...\x1f\x43".
In that case we need to wait for the next buffer to confirm a new media segment exists.
We also need to returns the end of the previous buffer, so that the media segment does start with "\x1f\x43...".
This is somehow already managed by the 'Data.Binary.Get.runGetIncremental'.
Checkout the 'testIncrementalLookahead' case in the test/Spec.hs module that validates these scenarios.
-}
module Codec.EBML.Stream (StreamReader, newStreamReader, StreamFrame (..), feedReader) where
import Control.Monad (when)
import Data.Binary.Get qualified as Get
import Data.ByteString qualified as BS
import Data.Text (Text)
import Data.Text qualified as Text
import Codec.EBML.Decoder
import Codec.EBML.Element
import Codec.EBML.Matroska
import Codec.EBML.Schema
import Codec.EBML.WebM qualified as WebM
-- | A valid frame that can be served.
data StreamFrame = StreamFrame
{ initialization :: BS.ByteString
-- ^ The initialization segments, to be provided before the first media segment.
, media :: BS.ByteString
-- ^ The begining of the last media segment found in the input buffer.
}
-- | Create a stream reader with 'newStreamReader', and decode media segments with 'feedReader'.
data StreamReader = StreamReader
{ header :: Either (Int, [BS.ByteString]) BS.ByteString
-- ^ The stream initialization segments, either an accumulator (read bytes, list of buffer), either the full segments.
, decoder :: Get.Decoder ()
-- ^ The current decoder.
}
streamSchema :: EBMLSchemas
streamSchema = compileSchemas schemaHeader
-- | Read elements until the first cluster eid.
getUntilNextCluster :: Get.Get [EBMLElement]
getUntilNextCluster =
Get.lookAheadM getNonCluster >>= \case
Just elt -> do
elts <- getUntilNextCluster
pure (elt : elts)
Nothing -> pure []
where
getNonCluster = do
eid <- getElementID
if eid == 0x1F43B675
then pure Nothing
else do
elth <- EBMLElementHeader eid <$> getMaybeDataSize
Just <$> getElementValue streamSchema elth
-- | Read the initialization frame.
getInitialization :: Get.Get ()
getInitialization = do
-- Read the EBML header element
elt <- getElement streamSchema
when (elt.header.eid /= 0x1A45DFA3) do
fail $ "Invalid magic: " <> show elt.header
-- Read the begining of the first segment, until the first cluster
segmentHead <- getElementHeader
when (segmentHead.eid /= 0x18538067) do
fail $ "Invalid segment: " <> show segmentHead
elts <- getUntilNextCluster
case WebM.decodeSegment elts of
Right _webmDocument -> pure ()
Left err -> fail (Text.unpack err)
-- | Read a cluster frame.
getCluster :: Get.Get ()
getCluster = do
clusterHead <- getElementHeader
when (clusterHead.eid /= 0x1F43B675) do
fail $ "Invalid cluster: " <> show clusterHead
elts <- getUntilNextCluster
case elts of
(elt : _) | elt.header.eid == 0xE7 -> pure ()
_ -> fail "Cluster first element is not a timestamp"
-- | Initialize a stream reader.
newStreamReader :: StreamReader
newStreamReader = StreamReader (Left (0, [])) (Get.runGetIncremental getInitialization)
-- | Feed data into a stream reader. Returns either an error, or maybe a new 'StreamFrame' and an updated StreamReader.
feedReader :: BS.ByteString -> StreamReader -> Either Text (Maybe StreamFrame, StreamReader)
feedReader = go Nothing
where
-- This is the end
go Nothing "" _ = Left "empty buffer"
-- Feed the decoder
go mFrame bs sr =
case Get.pushChunk sr.decoder bs of
Get.Fail _ _ s -> Left (Text.pack s)
-- More data is needed.
newDecoder@(Get.Partial _) -> Right (mFrame, newSR)
where
-- Accumulate the buffer for the initialization segments if needed.
newHeader = case sr.header of
Left (consumed, acc) -> Left (consumed + BS.length bs, bs : acc)
Right _ -> sr.header
newSR = StreamReader newHeader newDecoder
Get.Done leftover consumed _ -> go newFrame leftover newSR
where
-- The header is either the one already parsed, or the current complete decoded buffer.
newHeader = case sr.header of
Left (prevConsumed, acc) ->
let currentPos = fromIntegral consumed - prevConsumed
in mconcat $ reverse (BS.take currentPos bs : acc)
Right header -> header
-- The new frame starts after what was decoded.
newFrame = Just (StreamFrame newHeader leftover)
newSR = StreamReader (Right newHeader) (Get.runGetIncremental getCluster)