Upload
others
View
0
Download
0
Embed Size (px)
Citation preview
Coder un streamer video en 135 lignes de Haskellet en 1 week-end
Julien Dehos
Lambda Lille 2020 (remote)
1 / 23
Contexte
I je suis enseignant-chercheur à l’Université du Littoral
I Covid-19 ⇒ enseignement à distance
I Département Informatique : ouverture d’un serveur Discord(texte + audio + partage d’écran)
I ce que j’avais déjà : support de cours sur le web, dépôts Gitlab
2 / 23
Problème
I ce que j’avais pas : partage d’une zone de l’écran pour les TP(j’ai qu’un écran et je switche tout le temps entre Terminal,Vim, Firefox et Mypaint)
I connection ADSL ⇒ upload inférieur à 1 Mbits/s
I au moins 20 “spectateurs”
I testés mais rejetés : Discord, Zoom, Obs+Twitch, Gstreamer(écran complet ou 1 fenêtre, latence, débit. . . )
3 / 23
Solution envisagée
I un serveur HTTP qui fournit l’image courante à des clients web
I un programme sur ma machine qui envoie une capture d’écranau server via un websocket
I une image jpeg basse qualité à 2 fps suffit
4 / 23
Disclaimer
C’est du code moche, fait dans l’urgence !
I on est averti le vendredi
I j’ai TP le mardi suivant
I et il faut aussi faire une VM + une page web pour les consignes
5 / 23
Jalon 1 : serveur web
I une route / pour la page index.html
I une route /img pour l’image jpeg
I bibliothèque Haskell scotty
6 / 23
I codevideo19-serve.hs :
main :: IO ()main = do
img <- BS.readFile "static/bob.jpg"imgRef <- newIORef imgport <- read . fromMaybe "3000" <$> lookupEnv "PORT"putStrLn $ "listening port " ++ show port ++ "..."SC.scotty port $ do
httpApp imgRef
httpApp :: IORef BS.ByteString -> SC.ScottyM ()httpApp imgRef = do
SC.get "/" $ SC.file "static/index.html"SC.get "/img" $ do
SC.addHeader "Content-Type" "image/jpeg"SC.raw =<< SC.liftAndCatchIO (readIORef imgRef)
7 / 23
I static/index.html :
<html><head>
<meta charset="utf-8"/></head><body>
<img id="my_img"> </img><script>
function updateImg() {fetch("img")
.then(response => response.blob())
.then(function(myBlob){URL.revokeObjectURL(my_img.src);my_img.src = URL.createObjectURL(myBlob);
});}const my_interval = setInterval(updateImg, 500);
</script></body>
</html>
8 / 23
9 / 23
Jalon 2 : websockets
I le client streamer envoie l’image via un websocket
I le server recupère l’image sur le websocket et la rend disponibleen HTTP
I bibliothèque Haskell websockets
10 / 23
I covideo19-record.hs :
main :: IO ()main = do
img <- BS.readFile "static/gary.jpg"args <- getArgscase args of
[ip, portStr] -> doputStrLn $ "connecting " <> ip <> " on port " <> portStrlet app = clientApp img
port = read portStrWS.runClient ip port "" app
_ -> putStrLn "usage: <ip> <port>"
clientApp :: BS.ByteString -> WS.ClientApp ()clientApp img conn = forever $ do
WS.sendBinaryData conn imgthreadDelay 500000
11 / 23
I covideo19-serve.hs :
main :: IO ()main = do
-- ...SC.scotty port $ do
SC.middleware (wsApp imgRef)httpApp imgRef
wsApp :: IORef BS.ByteString -> Application -> ApplicationwsApp imgRef =
websocketsOr WS.defaultConnectionOptions (wsHandle imgRef)
wsHandle :: IORef BS.ByteString -> WS.PendingConnection -> IO ()wsHandle imgRef pc = do
conn <- WS.acceptRequest pcforever (WS.receiveData conn >>= atomicWriteIORef imgRef)
httpApp :: IORef BS.ByteString -> SC.ScottyM ()-- ...
12 / 23
13 / 23
Jalon 3 : capture d’écran
I l’image envoyée par le client streamer est une capture de l’écran
I bibliothèque Haskell haskell-gi (et ses dérivées)
14 / 23
I covideo19-record.hs :
main :: IO ()main = do
window <- initGtk-- ...
initGtk :: IO Gdk.WindowinitGtk = do
_ <- Gtk.init NothingJust screen <- Gdk.screenGetDefaultGdk.screenGetRootWindow screen
clientApp :: Gdk.Window -> WS.ClientApp ()clientApp window conn = forever $ do
Just pxbuf <- Gdk.pixbufGetFromWindow window 0 0 800 600img <- pixbufSaveToBufferv pxbuf "jpeg" ["quality"] ["50"]WS.sendBinaryData conn imgobjectUnref pxbufthreadDelay 500000
15 / 23
16 / 23
Image Docker
I release.nix :
letpkgs = import <nixpkgs> {};app-src = ./. ;app = pkgs.haskellPackages.callCabal2nix "covideo19" ./. {};
inpkgs.runCommand "covideo19" { inherit app; } ''
mkdir -p $out/{bin,static}cp ${app}/bin/covideo19-serve $out/bin/cp ${app-src}/static/* $out/static/
''
17 / 23
I docker.nix :
{ pkgs ? import <nixpkgs> {} }:let
app = import ./release.nix;entrypoint = pkgs.writeScript "entrypoint.sh" ''
#!${pkgs.stdenv.shell}$@
'';in
pkgs.dockerTools.buildLayeredImage {name = "covideo19";tag = "test";config = {
WorkingDir = "${app}";Entrypoint = [ entrypoint ];Cmd = [ "${app}/bin/covideo19-serve" ];
};}
18 / 23
DéploiementI construire l’image Docker :
nix-build docker.nixdocker load < result
I déployer l’image sur Heroku :
heroku loginheroku container:loginheroku create covideo19testdocker tag covideo19:test registry.heroku.com/covideo19test/webdocker push registry.heroku.com/covideo19test/webheroku container:release web --app covideo19test
I lancer le stream :
covideo19-record covideo19.herokuapp.com 80
19 / 23
Résultat
I ici, 60 lignes de Haskell
I mais dans la vraie appli :I affichage du curseurI paramètres supplémentaires (région à streamer, compression)I clé d’autorisation pour streamerI page de monitoringI gestion d’erreur (un peu)
20 / 23
Retour d’expérience
I conclusion 1 :I l’appli est moche mais bien pratique pour mon cas d’utilisationI testée en 800x600, 2 fps, qualité 25% (jusqu’à 25 clients web)
I conclusion 2 :I Haskell c’est cool : typage, fonctionnel pur, libs. . .I je connaissais déjà scotty/websockets/docker/heroku ⇒ RASI je connaissais presque pas haskell-gi ⇒ j’ai un peu galéré pour
trouver les bonnes docs et exemples
21 / 23
Références
I projet : https://gitlab.com/juliendehos/covideo19
I slides : https://gitlab.com/juliendehos/talk-2020-lambdalille-covideo19
22 / 23
Merci ! Questions ou commentaires ?
23 / 23