[{"data":1,"prerenderedAt":514},["ShallowReactive",2],{"project-echo":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"name":8,"year":10,"topics":11,"branch":13,"languages":14,"tools":21,"archived":6,"githubUrl":31,"heroImage":32,"homepage":35,"body":36,"_type":508,"_id":509,"_source":510,"_file":511,"_stem":512,"_extension":513},"/projects/echo","projects",false,"","echo","DIY video conferencing ⚡",2021,[12],"video streaming","master",[15,16,17,18,19,20],"TypeScript","C#","HTML","HCL","Dockerfile","CSS",[22,23,24,25,26,27,28,29,30],"docker","dotnet","react","redux","signalr","tensorflowjs","terraform","webrtc","websockets","https://github.com/ryanachten/echo",{"remote":33,"local":34},"https://raw.githubusercontent.com/ryanachten/echo/master/docs/echo_loading.gif","hero-images/echo.webp","https://echo-video.fly.dev",{"type":37,"children":38,"toc":493},"root",[39,46,56,66,71,96,104,109,127,132,140,147,155,160,167,172,180,186,200,213,219,227,232,336,342,350,355,360,368,374,380,393,399,426,432,485],{"type":40,"tag":41,"props":42,"children":43},"element","h1",{"id":8},[44],{"type":45,"value":8},"text",{"type":40,"tag":47,"props":48,"children":49},"p",{},[50],{"type":40,"tag":51,"props":52,"children":55},"img",{"alt":53,"src":54},"Echo loading","https://github.com/ryanachten/echo/raw/master/docs/echo_loading.gif",[],{"type":40,"tag":47,"props":57,"children":58},{},[59,64],{"type":40,"tag":60,"props":61,"children":62},"strong",{},[63],{"type":45,"value":8},{"type":45,"value":65}," is a DIY video conferencing software hacked together while coping with endless online meetings during COVID lockdowns.",{"type":40,"tag":47,"props":67,"children":68},{},[69],{"type":45,"value":70},"echo currently provides the following features:",{"type":40,"tag":72,"props":73,"children":74},"ul",{},[75,81,86,91],{"type":40,"tag":76,"props":77,"children":78},"li",{},[79],{"type":45,"value":80},"Video calling with multiple peers",{"type":40,"tag":76,"props":82,"children":83},{},[84],{"type":45,"value":85},"Calls are restricted to groups requiring valid pass code",{"type":40,"tag":76,"props":87,"children":88},{},[89],{"type":45,"value":90},"Limited video effects, such as background blurring and replacement",{"type":40,"tag":76,"props":92,"children":93},{},[94],{"type":45,"value":95},"Muting of video and audio",{"type":40,"tag":47,"props":97,"children":98},{},[99],{"type":40,"tag":51,"props":100,"children":103},{"alt":101,"src":102},"Echo call","https://github.com/ryanachten/echo/raw/master/docs/echo_call.jpg",[],{"type":40,"tag":47,"props":105,"children":106},{},[107],{"type":45,"value":108},"In the future, the following features might be considered:",{"type":40,"tag":72,"props":110,"children":111},{},[112,117,122],{"type":40,"tag":76,"props":113,"children":114},{},[115],{"type":45,"value":116},"Inserting images into background replacement",{"type":40,"tag":76,"props":118,"children":119},{},[120],{"type":45,"value":121},"Other cool WebGL background / foreground effects",{"type":40,"tag":76,"props":123,"children":124},{},[125],{"type":45,"value":126},"Better mobile support",{"type":40,"tag":47,"props":128,"children":129},{},[130],{"type":45,"value":131},"However, I've moved onto other projects now, so will have to see if this ever happens 😉",{"type":40,"tag":47,"props":133,"children":134},{},[135],{"type":40,"tag":51,"props":136,"children":139},{"alt":137,"src":138},"Echo group call","https://github.com/ryanachten/echo/raw/master/docs/echo_group.jpg",[],{"type":40,"tag":141,"props":142,"children":144},"h2",{"id":143},"architecture",[145],{"type":45,"value":146},"Architecture",{"type":40,"tag":47,"props":148,"children":149},{},[150],{"type":40,"tag":51,"props":151,"children":154},{"alt":152,"src":153},"Echo architecture","https://github.com/ryanachten/echo/raw/master/docs/echo_architecture.png",[],{"type":40,"tag":47,"props":156,"children":157},{},[158],{"type":45,"value":159},"Echo is comprised of the following components:",{"type":40,"tag":161,"props":162,"children":164},"h3",{"id":163},"backend",[165],{"type":45,"value":166},"Backend",{"type":40,"tag":47,"props":168,"children":169},{},[170],{"type":45,"value":171},".NET and SignalR are used for websockets communication. Websockets makes it easier and more efficient to fulfill the necessary peer-2-peer handshakes required as part of the WebRTC signaling process.",{"type":40,"tag":47,"props":173,"children":174},{},[175],{"type":40,"tag":51,"props":176,"children":179},{"alt":177,"src":178},"Echo network components","https://github.com/ryanachten/echo/raw/master/docs/echo_network1.png",[],{"type":40,"tag":161,"props":181,"children":183},{"id":182},"frontend",[184],{"type":45,"value":185},"Frontend",{"type":40,"tag":47,"props":187,"children":188},{},[189,191,198],{"type":45,"value":190},"React is used as the front-end library in this project, alongside SignalR to receive and send Websockets signals to the SignalR hub on the backend. ",{"type":40,"tag":192,"props":193,"children":195},"code",{"className":194},[],[196],{"type":45,"value":197},"simple-peer",{"type":45,"value":199}," is used to help handle the peer-2-peer negotiations when setting up WebRTC connections.",{"type":40,"tag":47,"props":201,"children":202},{},[203,205,211],{"type":45,"value":204},"Grommet is used as the UI component and theming library, allowing us to make use of ",{"type":40,"tag":192,"props":206,"children":208},{"className":207},[],[209],{"type":45,"value":210},"styled-components",{"type":45,"value":212}," alongside pre-made components for efficient development.",{"type":40,"tag":141,"props":214,"children":216},{"id":215},"connection-lifecycle",[217],{"type":45,"value":218},"Connection lifecycle",{"type":40,"tag":47,"props":220,"children":221},{},[222],{"type":40,"tag":51,"props":223,"children":226},{"alt":224,"src":225},"Echo network flow","https://github.com/ryanachten/echo/raw/master/docs/echo_network2.png",[],{"type":40,"tag":47,"props":228,"children":229},{},[230],{"type":45,"value":231},"The process for a new peer joining a call can broken down into the following lifecycle phases:",{"type":40,"tag":233,"props":234,"children":235},"ol",{},[236,254,282,300,318],{"type":40,"tag":76,"props":237,"children":238},{},[239,244,246],{"type":40,"tag":60,"props":240,"children":241},{},[242],{"type":45,"value":243},"Initialisation",{"type":45,"value":245}," (blue)",{"type":40,"tag":72,"props":247,"children":248},{},[249],{"type":40,"tag":76,"props":250,"children":251},{},[252],{"type":45,"value":253},"A new users opens the echo web page, initialising a new SignalR Websocket connection between client and server",{"type":40,"tag":76,"props":255,"children":256},{},[257,262,264],{"type":40,"tag":60,"props":258,"children":259},{},[260],{"type":45,"value":261},"Group setup",{"type":45,"value":263}," (green)",{"type":40,"tag":72,"props":265,"children":266},{},[267,272,277],{"type":40,"tag":76,"props":268,"children":269},{},[270],{"type":45,"value":271},"User chooses between creating a new group call or existing and existing one",{"type":40,"tag":76,"props":273,"children":274},{},[275],{"type":45,"value":276},"In the case they choose an existing group call, a signal will be sent to other peers which exist as part of that group to add the user to their group",{"type":40,"tag":76,"props":278,"children":279},{},[280],{"type":45,"value":281},"If no group exists, a group does not exist signal will be returned",{"type":40,"tag":76,"props":283,"children":284},{},[285,290,292],{"type":40,"tag":60,"props":286,"children":287},{},[288],{"type":45,"value":289},"Connection setup",{"type":45,"value":291}," (yellow)",{"type":40,"tag":72,"props":293,"children":294},{},[295],{"type":40,"tag":76,"props":296,"children":297},{},[298],{"type":45,"value":299},"If the user provides echo with permission to access their video and audio, and connection request will be made to other group members to establish a peer-to-peer connection",{"type":40,"tag":76,"props":301,"children":302},{},[303,308,310],{"type":40,"tag":60,"props":304,"children":305},{},[306],{"type":45,"value":307},"Streaming",{"type":45,"value":309}," (orange)",{"type":40,"tag":72,"props":311,"children":312},{},[313],{"type":40,"tag":76,"props":314,"children":315},{},[316],{"type":45,"value":317},"Once the user is successfully connected with another peer, their are able to send and receive WebRTC signals which can be used to stream video and audio",{"type":40,"tag":76,"props":319,"children":320},{},[321,326,328],{"type":40,"tag":60,"props":322,"children":323},{},[324],{"type":45,"value":325},"Disconnection",{"type":45,"value":327}," (red)",{"type":40,"tag":72,"props":329,"children":330},{},[331],{"type":40,"tag":76,"props":332,"children":333},{},[334],{"type":45,"value":335},"When a user closes the webpage, or leaves the call - a disconnection signal is sent to all other peers to remove the user from the call",{"type":40,"tag":141,"props":337,"children":339},{"id":338},"effects",[340],{"type":45,"value":341},"Effects",{"type":40,"tag":47,"props":343,"children":344},{},[345],{"type":40,"tag":51,"props":346,"children":349},{"alt":347,"src":348},"Echo video pipeline","https://github.com/ryanachten/echo/raw/master/docs/echo_video-pipeline.png",[],{"type":40,"tag":47,"props":351,"children":352},{},[353],{"type":45,"value":354},"To achieve modern video chat features such as background removal or blurring, we used Tensorflow.js and the Tensorflow BodyPix model to track figures in the video feed and use this outline to blur or change the colour of the background.",{"type":40,"tag":47,"props":356,"children":357},{},[358],{"type":45,"value":359},"The updated video feed is then rendered using the HTML canvas API and streamed via WebRTC.",{"type":40,"tag":47,"props":361,"children":362},{},[363],{"type":40,"tag":51,"props":364,"children":367},{"alt":365,"src":366},"Echo effects","https://github.com/ryanachten/echo/raw/master/docs/echo_effects.jpg",[],{"type":40,"tag":141,"props":369,"children":371},{"id":370},"developing",[372],{"type":45,"value":373},"Developing",{"type":40,"tag":161,"props":375,"children":377},{"id":376},"requirements",[378],{"type":45,"value":379},"Requirements",{"type":40,"tag":72,"props":381,"children":382},{},[383,388],{"type":40,"tag":76,"props":384,"children":385},{},[386],{"type":45,"value":387},".NET CLI v5.0",{"type":40,"tag":76,"props":389,"children":390},{},[391],{"type":45,"value":392},"Node v16",{"type":40,"tag":161,"props":394,"children":396},{"id":395},"running-net-and-react-locally",[397],{"type":45,"value":398},"Running .NET and React locally",{"type":40,"tag":72,"props":400,"children":401},{},[402,413],{"type":40,"tag":76,"props":403,"children":404},{},[405,407],{"type":45,"value":406},"Run while watching for client and API changes: ",{"type":40,"tag":192,"props":408,"children":410},{"className":409},[],[411],{"type":45,"value":412},"dotnet run watch",{"type":40,"tag":76,"props":414,"children":415},{},[416,418],{"type":45,"value":417},"This needs to be run over HTTPS, so go to ",{"type":40,"tag":419,"props":420,"children":424},"a",{"href":421,"rel":422},"https://localhost:5001/",[423],"nofollow",[425],{"type":45,"value":421},{"type":40,"tag":161,"props":427,"children":429},{"id":428},"running-docker-container-locally",[430],{"type":45,"value":431},"Running Docker container locally",{"type":40,"tag":72,"props":433,"children":434},{},[435,446,457,468],{"type":40,"tag":76,"props":436,"children":437},{},[438,440],{"type":45,"value":439},"Build Docker container: ",{"type":40,"tag":192,"props":441,"children":443},{"className":442},[],[444],{"type":45,"value":445},"docker build -t ryanachten/echo .",{"type":40,"tag":76,"props":447,"children":448},{},[449,451],{"type":45,"value":450},"Run Docker container: ",{"type":40,"tag":192,"props":452,"children":454},{"className":453},[],[455],{"type":45,"value":456},"docker run -p 8080:80 ryanachten/echo",{"type":40,"tag":76,"props":458,"children":459},{},[460,462],{"type":45,"value":461},"App should then be accessible on ",{"type":40,"tag":419,"props":463,"children":466},{"href":464,"rel":465},"http://localhost:8080",[423],[467],{"type":45,"value":464},{"type":40,"tag":76,"props":469,"children":470},{},[471,473],{"type":45,"value":472},"To update the Docker image in Docker Hub with the latest image tag:",{"type":40,"tag":72,"props":474,"children":475},{},[476],{"type":40,"tag":76,"props":477,"children":478},{},[479],{"type":40,"tag":192,"props":480,"children":482},{"className":481},[],[483],{"type":45,"value":484},"docker push ryanachten/echo",{"type":40,"tag":47,"props":486,"children":487},{},[488],{"type":40,"tag":51,"props":489,"children":492},{"alt":490,"src":491},"Echo login","https://github.com/ryanachten/echo/raw/master/docs/echo_login.jpg",[],{"title":7,"searchDepth":494,"depth":494,"links":495},2,[496,501,502,503],{"id":143,"depth":494,"text":146,"children":497},[498,500],{"id":163,"depth":499,"text":166},3,{"id":182,"depth":499,"text":185},{"id":215,"depth":494,"text":218},{"id":338,"depth":494,"text":341},{"id":370,"depth":494,"text":373,"children":504},[505,506,507],{"id":376,"depth":499,"text":379},{"id":395,"depth":499,"text":398},{"id":428,"depth":499,"text":431},"markdown","content:projects:echo.md","content","projects/echo.md","projects/echo","md",1776573205290]