packages feed

ebml-0.1.0.0: src/Codec/EBML/WebM.hs

{- | This module contains the logic to convert a raw EBMLDocument into a WebMDocument

See: https://www.matroska.org/technical/diagram.html
-}
module Codec.EBML.WebM where

import Data.Text (Text)
import Data.Text qualified as Text
import Data.Word (Word64)

import Codec.EBML.Element
import Data.Foldable (find)
import Data.Maybe (catMaybes)

-- | A WebM document.
data WebMDocument = WebMDocument
    { timestampScale :: Word64
    -- ^ Base unit for Segment Ticks and Track Ticks, in nanoseconds. A TimestampScale of 1_000_000 means segments' timestamps are expressed in milliseconds;
    , clusters :: [WebMCluster]
    -- ^ The list of clusters.
    }

-- | A WebM cluster, e.g. a media segment.
data WebMCluster = WebMCluster
    { timestamp :: Word64
    -- ^ Absolute timestamp of the cluster.
    , content :: [EBMLElement]
    -- ^ The cluster elements.
    }

decodeWebMDocument :: EBMLDocument -> Either Text WebMDocument
decodeWebMDocument = \case
    (EBMLDocument [header, segment]) -> do
        headerElements <- getChilds header
        segmentElements <- getChilds segment
        docType <- getText =<< getElt headerElements 0x4282
        docVersion <- getUInt =<< getElt headerElements 0x4287
        if docType /= "webm" || docVersion /= 2
            then Left ("Invalid doctype: " <> Text.pack (show (docType, docVersion)))
            else decodeSegment segmentElements
    _ -> Left "Invalid EBML file structure"

decodeSegment :: [EBMLElement] -> Either Text WebMDocument
decodeSegment = go 0
  where
    go scale xs@(x : rest)
        | x.header.eid == 0x1F43B675 = WebMDocument scale . catMaybes <$> traverse decodeWebMCluster xs
        | x.header.eid == 0x1549A966 = do
            info <- getChilds x
            scaleValue <- getUInt =<< getElt info 0x2AD7B1
            go scaleValue rest
        | otherwise = go scale rest
    go scale [] = Right (WebMDocument scale [])

decodeWebMCluster :: EBMLElement -> Either Text (Maybe WebMCluster)
decodeWebMCluster elt
    | elt.header.eid == 0x1F43B675 =
        Just <$> do
            childs <- getChilds elt
            case childs of
                (tsElt : xs)
                    | tsElt.header.eid == 0xE7 -> do
                        timestamp <- getUInt tsElt
                        Right $ WebMCluster timestamp xs
                _ -> Left "Cluster first element is not a timestamp"
    | otherwise = Right Nothing

-- | Extract the document type, version and the segment elements.
documentSegment :: EBMLDocument -> Either Text (Text, Word64, [EBMLElement])
documentSegment (EBMLDocument [header, segment]) = do
    headerElements <- getChilds header
    segmentElements <- getChilds segment
    docType <- getText =<< getElt headerElements 0x4282
    docVersion <- getUInt =<< getElt headerElements 0x4287
    pure (docType, docVersion, segmentElements)
documentSegment _ = Left "Invalid EBML file structure"

getElt :: [EBMLElement] -> EBMLID -> Either Text EBMLElement
getElt xs eid = case find (\elt -> elt.header.eid == eid) xs of
    Just elt -> Right elt
    Nothing -> Left ("Element " <> Text.pack (show eid) <> " not found")

getText :: EBMLElement -> Either Text Text
getText elt = case elt.value of
    EBMLText txt -> Right txt
    _ -> Left "Invalid text value"

getUInt :: EBMLElement -> Either Text Word64
getUInt elt = case elt.value of
    EBMLUnsignedInteger x -> Right x
    _ -> Left ("Invalid uint value " <> Text.pack (show elt.value))

getChilds :: EBMLElement -> Either Text [EBMLElement]
getChilds elt = case elt.value of
    EBMLRoot xs -> Right xs
    _ -> Left "Element is not a root"