-- | A high-level (no raw pointer types) interface to PipeWire.
module PipeWire
( SpaDictMap (..),
RegistryObject (..),
PipeWireCallbacks (..),
PipeWireClient,
run,
quit,
waitForQuit,
)
where
import Bindings.PipeWire (DictPtr, MainLoop, Properties (..), RegistryEvents (..))
import Bindings.PipeWire qualified as B
import Bindings.Util qualified as B
import Control.Concurrent (MVar, modifyMVar_, newEmptyMVar, newMVar, putMVar, readMVar, tryTakeMVar)
import Control.Concurrent.Async (Async, asyncBound, asyncThreadId, wait)
import Control.Exception (bracket)
import Control.Monad (forM, void, when)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Foreign (allocaBytes, freeHaskellFunPtr, nullPtr, peek, peekArray)
import System.Posix.Types (Fd (..))
import Unsafe.Coerce (unsafeCoerce)
data SpaDictMap = SpaDictMap
{ flags :: Word,
items :: Map Text Text
}
deriving (Eq, Show)
data RegistryObject = RegistryObject
{ id :: Word,
permissions :: Word,
typ :: Text,
version :: Word,
props :: SpaDictMap
}
deriving (Eq, Show)
data PipeWireCallbacks = PipeWireCallbacks
{ cameraAdded :: Map Word RegistryObject -> RegistryObject -> IO (),
cameraRemoved :: Map Word RegistryObject -> Word -> IO ()
}
data PipeWireClient = PipeWireClient
{ async :: Async (),
mainLoop :: MVar MainLoop
}
instance Eq PipeWireClient where
p1 == p2 = p1.async == p2.async
instance Show PipeWireClient where
show p = "PipeWireClient<" <> show (asyncThreadId p.async) <> ">"
run :: Fd -> PipeWireCallbacks -> IO PipeWireClient
run (Fd fd) cbs = do
loopVar <- newEmptyMVar
camerasVar <- newMVar Map.empty
async <- asyncBound $ do
bracket (B.registryEventsGlobalCallback (addCb camerasVar)) freeHaskellFunPtr $ \globalCallback -> do
bracket (B.registryEventsGlobalRemoveCallback (removeCb camerasVar)) freeHaskellFunPtr $ \globalRemoveCallback -> do
let events =
RegistryEvents
{ version = B.versionRegistryEvents,
globalCallback,
globalRemoveCallback
}
B.init nullPtr nullPtr
mainLoop <- B.mainLoopNew nullPtr
putMVar loopVar mainLoop
loop <- B.mainLoopGetLoop mainLoop
context <- B.contextNew loop (Properties nullPtr) 0
core <- B.contextConnectFd context fd (Properties nullPtr) 0
allocaBytes B.spaHookSize $ \listenerHandle -> do
registry <- B.coreGetRegistry core B.versionRegistry 0
B.registryAddListener registry listenerHandle events
void (B.mainLoopRun mainLoop)
B.proxyDestroy (unsafeCoerce registry)
void (B.coreDisconnect core)
B.contextDestroy context
B.mainLoopDestroy mainLoop
B.deinit
void (readMVar loopVar) -- don't return until we can call quit safely...
pure PipeWireClient {mainLoop = loopVar, async}
where
addCb camerasVar _data camId permissions typPtr version propsPtr = do
typ <- B.cStringToText typPtr
props <- decodeDict propsPtr
let registryObj =
RegistryObject
{ id = fromIntegral camId,
permissions = fromIntegral permissions,
typ,
version = fromIntegral version,
props
}
when (isCamera registryObj) $ do
modifyMVar_ camerasVar $ \cameras -> do
let newCameras = Map.insert registryObj.id registryObj cameras
cbs.cameraAdded newCameras registryObj
pure newCameras
removeCb camerasVar _data camId = do
let wordId = fromIntegral camId
modifyMVar_ camerasVar $ \cameras -> do
case Map.lookup wordId cameras of
Just registryObj | isCamera registryObj -> do
let newCameras = Map.delete wordId cameras
cbs.cameraRemoved newCameras wordId
pure newCameras
_ -> do
pure cameras
quit :: PipeWireClient -> IO ()
quit client = do
tryTakeMVar client.mainLoop >>= \case
Nothing -> pure () -- we already quit
Just ml -> void (B.mainLoopQuit ml)
waitForQuit :: PipeWireClient -> IO ()
waitForQuit client = do
wait client.async
isCamera :: RegistryObject -> Bool
isCamera obj =
Map.lookup "media.class" obj.props.items == Just "Video/Source"
&& Map.lookup "media.role" obj.props.items == Just "Camera"
decodeDict :: DictPtr -> IO SpaDictMap
decodeDict dictPtr = do
dict <- peek dictPtr
items <- peekArray (fromIntegral dict.nItems) dict.items
itemPairs <- forM items $ \item -> do
(,)
<$> B.cStringToText item.key
<*> B.cStringToText item.value
pure
SpaDictMap
{ flags = fromIntegral dict.flags,
items = Map.fromList itemPairs
}