packages feed

polysemy-video-0.2.0.0: src/Polysemy/Video.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeOperators #-}

-- |
--   Module    : Polysemy.Video
--   License   : MIT
--   Stability : experimental
--
-- Experimental Video processing DSL for Polysemy.
module Polysemy.Video
  ( ClipProcess (..),
    extractAudio,
    extractClips,
    extractFrames,
    runFFMpegCli,
    traceFFMpegArgs,
    ignoreClipProcess,
  )
where

import Control.Monad.IO.Class
import Data.Text (Text)
import Formatting
import Media.Timestamp
import Media.Timestamp.Formatting
import Path
import Path.Formatting
import Polysemy
import Polysemy.Trace
import qualified Turtle as S

-- | Effect for disecting a video file.
--
-- @since 0.1.1.0
data ClipProcess m a where
  ExtractAudio :: Path b File -> [(Range, Path b' File)] -> ClipProcess m ()
  ExtractClips :: Path b File -> [(Range, Path b' File)] -> ClipProcess m ()
  ExtractFrames :: Path b File -> [(Time, Path b' File)] -> ClipProcess m ()

makeSem ''ClipProcess

-- | "-ss <x>" where x is a timestamp.
seekFF :: Time -> [Text]
seekFF t = ["-ss", sformat timef t]

-- | "-ss <x> -to <y> <output>".
rangeFF :: Range -> Path b File -> [Text]
rangeFF (Range f t) x = seekFF f ++ ["-to", sformat timef t, sformat pathf x]

-- | "-ss <x> -vframes 1 <output>"
frameFF :: Time -> Path b File -> [Text]
frameFF t x = seekFF t ++ ["-vframes", "1", sformat pathf x]

-- | "-i <output>"
inputFF :: Path b File -> [Text]
inputFF x = ["-i", sformat pathf x]

-- | "ffmpeg -y" followed by some arguments.
runffmpeg :: MonadIO m => [Text] -> m ()
runffmpeg xs = S.sh $ S.inproc "ffmpeg" ("-y" : "-loglevel" : "warning" : xs) mempty

-- | "mkdir -p" with a `Path b Dir`.
mktreeFP :: MonadIO m => Path b Dir -> m ()
mktreeFP = S.mktree . S.decodeString . toFilePath

-- | Interpret `ClipProcess` by running it against ffmpeg on the command line.
--
-- @since 0.2.0.0
runFFMpegCli :: Member (Embed IO) effs => Sem (ClipProcess ': effs) a -> Sem effs a
runFFMpegCli = interpret $ \case
  ExtractAudio x ts -> mapM_ mktreeFP (parent . snd <$> ts) >> runffmpeg (inputFF x <> (uncurry rangeFF =<< ts))
  ExtractClips x ts -> mapM_ mktreeFP (parent . snd <$> ts) >> runffmpeg (inputFF x <> (uncurry rangeFF =<< ts))
  ExtractFrames x ts -> mapM_ mktreeFP (parent . snd <$> ts) >> runffmpeg (inputFF x <> (uncurry frameFF =<< ts))

-- | Trace `ClipProcess` by printing out the arguments it would pass to ffmpeg.
--
-- @since 0.2.0.0
traceFFMpegArgs :: Members '[ClipProcess, Trace] r => Sem r a -> Sem r a
traceFFMpegArgs = intercept $ \case
  ExtractAudio x ts -> do
    trace $ show $ inputFF x <> (uncurry rangeFF =<< ts)
    extractAudio x ts
  ExtractClips x ts -> do
    trace $ show $ inputFF x <> (uncurry rangeFF =<< ts)
    extractClips x ts
  ExtractFrames x ts -> do
    trace $ show $ inputFF x <> (uncurry frameFF =<< ts)
    extractFrames x ts

-- | Noop the `ClipProcess` effect.
--
-- @since 0.2.0.0
ignoreClipProcess :: Sem (ClipProcess ': r) a -> Sem r a
ignoreClipProcess = interpret $ \case
  ExtractAudio _ _ -> pure ()
  ExtractClips _ _ -> pure ()
  ExtractFrames _ _ -> pure ()