From 08489ed67b6fe567d2aded9b72452a18d56e1b23 Mon Sep 17 00:00:00 2001 From: Stanislav Fifik Date: Sun, 18 Aug 2024 19:11:41 +0000 Subject: [PATCH] sounds, better files --- .env.development | 2 + .env.production | 1 + .gitignore | 1 + Dockerfile | 21 ++ README.md | 17 +- chromecast/index.html | 16 ++ docker-compose.yml | 1 + index.html | 2 +- map/index.html | 2 +- nginx/default.conf | 21 +- ...ns_-_Metal_Weapon_Swing-_Whoosh_normal.aac | Bin 0 -> 33384 bytes ...al_Weapons_-_Metal_Weapon_Swing_normal.aac | Bin 0 -> 19405 bytes ...l_2_-_Sword_Slash_01_-_MASTERED_-_2496.aac | Bin 0 -> 31921 bytes ...7_719601_Throwing_Weapon_Hits_Body (1).aac | Bin 0 -> 24448 bytes ...99157_719601_Throwing_Weapon_Hits_Body.aac | Bin 0 -> 24448 bytes src/assets/main.css | 35 +++ src/assets/theme.css | 174 +++++++++++++ src/chromecast.ts | 23 ++ src/components/Audio/AudioCard.vue | 237 ++++++++++++++++++ src/components/Audio/DeviceVolume.vue | 78 ++++++ src/components/Audio/Playlist.vue | 95 +++++++ src/components/Audio/Sfx.vue | 58 +++++ src/components/Audio/SfxSound.vue | 129 ++++++++++ src/components/Audio/Soundboard.vue | 120 +++++++++ src/components/Audio/TabSource.vue | 21 ++ src/components/Audio/modal.vue | 69 +++++ src/components/FileManager/FileManager.vue | 74 +++++- src/components/FileManager/FileManagerDir.vue | 29 ++- .../FileManager/FileManagerFile.vue | 73 +++++- src/components/Files/FilesAside.vue | 100 ++++++++ src/components/Icons/Backward.vue | 5 + src/components/Icons/Cog.vue | 5 + src/components/Icons/Forward.vue | 5 + src/components/Icons/Mute.vue | 5 + src/components/Icons/Pause.vue | 5 + src/components/Icons/Play.vue | 5 + src/components/Icons/Speaker.vue | 6 + src/components/Icons/Stop.vue | 5 + .../RemoteDevices/RemoteDevices.vue | 64 +++++ src/components/ScreenManager/RemoteScreen.vue | 192 ++++++++++++++ .../ScreenManager/ScreenManager.vue | 28 +++ src/components/Soundboard.vue | 13 + src/components/Tabs.vue | 56 +++++ src/helpers/immediate.ts | 4 + src/helpers/loadScript.ts | 9 + src/helpers/mouseDragging.ts | 34 +++ src/helpers/withLoading.ts | 16 ++ src/main.ts | 57 ++++- src/model/Audio/AudioNodesManager.ts | 44 ++++ src/model/Audio/PlaylistGroupNode.ts | 68 +++++ src/model/Audio/README.md | 4 + src/model/Audio/SfxGroupNode.ts | 41 +++ src/model/Audio/SoundGroupNode.ts | 146 +++++++++++ src/model/Audio/SoundNode.ts | 177 +++++++++++++ src/model/Audio/TabSourceNode.ts | 76 ++++++ src/model/Audio/types.ts | 7 + src/model/ChromecastListener.ts | 11 +- src/model/FileMeta/ExtensionMimeType.ts | 21 ++ src/model/FileMeta/MapMeta.ts | 50 ++++ src/model/FileSystem/FileStashFs.ts | 100 ++++++++ src/model/FileSystem/FsCache.ts | 52 ++++ src/model/FileSystem/MountedFs.ts | 129 ++++++++++ src/model/FileSystem/NginxFS.ts | 102 ++++++++ src/model/FileSystem/types.ts | 27 ++ src/model/Remote/Channels/ChromcastCaster.ts | 111 ++++++++ src/model/Remote/Channels/ChromcastSummon.ts | 63 +++++ src/model/Remote/Channels/PostMessage.ts | 25 ++ src/model/Remote/Channels/WebRTCChannel.ts | 172 +++++++++++++ src/model/Remote/Channels/WsSignaling.ts | 92 +++++++ src/model/Remote/Channels/type.ts | 131 ++++++++++ src/model/Remote/ChromecastDevice.ts | 97 +++++++ src/model/Remote/FullscreenWindow.ts | 135 ++++++++++ src/model/Remote/LocalAudioDevice.ts | 9 + src/model/Remote/RemoteDeviceManager.ts | 105 ++++++++ src/model/Remote/types.ts | 32 +++ src/model/SoundPlayer.ts | 60 +++++ src/model/injectionSymbols.ts | 13 +- src/modules/controls/ControlApp.vue | 73 ++++-- src/modules/summon/Debug.vue | 50 ++++ src/modules/summon/SummonApp.vue | 68 +++++ src/summon.ts | 58 +++++ src/utils/debounce.ts | 16 ++ src/utils/groupBy.ts | 11 + src/utils/index.ts | 0 summon/index.html | 13 + vite.config.ts | 10 +- 86 files changed, 4226 insertions(+), 86 deletions(-) create mode 100644 .env.development create mode 100644 .env.production create mode 100644 Dockerfile create mode 100644 chromecast/index.html create mode 100644 public/soundfx/sword slash/52752_438046_438045_Medieval_Weapons_-_Metal_Weapon_Swing-_Whoosh_normal.aac create mode 100644 public/soundfx/sword slash/52753_438048_438047_Medieval_Weapons_-_Metal_Weapon_Swing_normal.aac create mode 100644 public/soundfx/sword slash/762456_762424_Shinobi_Vol_2_-_Sword_Slash_01_-_MASTERED_-_2496.aac create mode 100644 public/soundfx/sword slash/99157_719601_Throwing_Weapon_Hits_Body (1).aac create mode 100644 public/soundfx/sword slash/99157_719601_Throwing_Weapon_Hits_Body.aac create mode 100644 src/assets/main.css create mode 100644 src/assets/theme.css create mode 100644 src/chromecast.ts create mode 100644 src/components/Audio/AudioCard.vue create mode 100644 src/components/Audio/DeviceVolume.vue create mode 100644 src/components/Audio/Playlist.vue create mode 100644 src/components/Audio/Sfx.vue create mode 100644 src/components/Audio/SfxSound.vue create mode 100644 src/components/Audio/Soundboard.vue create mode 100644 src/components/Audio/TabSource.vue create mode 100644 src/components/Audio/modal.vue create mode 100644 src/components/Files/FilesAside.vue create mode 100644 src/components/Icons/Backward.vue create mode 100644 src/components/Icons/Cog.vue create mode 100644 src/components/Icons/Forward.vue create mode 100644 src/components/Icons/Mute.vue create mode 100644 src/components/Icons/Pause.vue create mode 100644 src/components/Icons/Play.vue create mode 100644 src/components/Icons/Speaker.vue create mode 100644 src/components/Icons/Stop.vue create mode 100644 src/components/RemoteDevices/RemoteDevices.vue create mode 100644 src/components/ScreenManager/RemoteScreen.vue create mode 100644 src/components/ScreenManager/ScreenManager.vue create mode 100644 src/components/Soundboard.vue create mode 100644 src/components/Tabs.vue create mode 100644 src/helpers/immediate.ts create mode 100644 src/helpers/loadScript.ts create mode 100644 src/helpers/mouseDragging.ts create mode 100644 src/helpers/withLoading.ts create mode 100644 src/model/Audio/AudioNodesManager.ts create mode 100644 src/model/Audio/PlaylistGroupNode.ts create mode 100644 src/model/Audio/README.md create mode 100644 src/model/Audio/SfxGroupNode.ts create mode 100644 src/model/Audio/SoundGroupNode.ts create mode 100644 src/model/Audio/SoundNode.ts create mode 100644 src/model/Audio/TabSourceNode.ts create mode 100644 src/model/Audio/types.ts create mode 100644 src/model/FileMeta/ExtensionMimeType.ts create mode 100644 src/model/FileMeta/MapMeta.ts create mode 100644 src/model/FileSystem/FileStashFs.ts create mode 100644 src/model/FileSystem/FsCache.ts create mode 100644 src/model/FileSystem/MountedFs.ts create mode 100644 src/model/FileSystem/NginxFS.ts create mode 100644 src/model/FileSystem/types.ts create mode 100644 src/model/Remote/Channels/ChromcastCaster.ts create mode 100644 src/model/Remote/Channels/ChromcastSummon.ts create mode 100644 src/model/Remote/Channels/PostMessage.ts create mode 100644 src/model/Remote/Channels/WebRTCChannel.ts create mode 100644 src/model/Remote/Channels/WsSignaling.ts create mode 100644 src/model/Remote/Channels/type.ts create mode 100644 src/model/Remote/ChromecastDevice.ts create mode 100644 src/model/Remote/FullscreenWindow.ts create mode 100644 src/model/Remote/LocalAudioDevice.ts create mode 100644 src/model/Remote/RemoteDeviceManager.ts create mode 100644 src/model/Remote/types.ts create mode 100644 src/model/SoundPlayer.ts create mode 100644 src/modules/summon/Debug.vue create mode 100644 src/modules/summon/SummonApp.vue create mode 100644 src/summon.ts create mode 100644 src/utils/debounce.ts create mode 100644 src/utils/groupBy.ts create mode 100644 src/utils/index.ts create mode 100644 summon/index.html diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..d997789 --- /dev/null +++ b/.env.development @@ -0,0 +1,2 @@ +#VITE_CHROMECAST_APP_ID=0D9CC4B6 +VITE_CHROMECAST_APP_ID=AB5F6AA2 \ No newline at end of file diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..8153404 --- /dev/null +++ b/.env.production @@ -0,0 +1 @@ +VITE_CHROMECAST_APP_ID=0D9CC4B6 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5a16850..95acbec 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ node_modules dist dist-ssr *.local +maps public/maps/* # Editor directories and files diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..860aeff --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM node:18 AS build + +WORKDIR /srv + +COPY ./package.json ./package-lock.json /srv/ +RUN npm install +COPY tsconfig.json tsconfig.app.json tsconfig.node.json vite.config.ts /srv/ + +COPY src /srv/src +COPY public /srv/public +COPY map /srv/map +COPY ./index.html /srv/ + +RUN npm run build + +FROM nginx:latest + +COPY --from=build /srv/dist /usr/share/nginx/html +COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 \ No newline at end of file diff --git a/README.md b/README.md index 33895ab..714fc1c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,14 @@ -# Vue 3 + TypeScript + Vite +# Battle caster -This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` + + diff --git a/docker-compose.yml b/docker-compose.yml index 084f69e..dd785e4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,4 +9,5 @@ services: - "8080:80" volumes: - ./public/maps:/usr/share/nginx/html/maps + - ./public/soundfx:/usr/share/nginx/html/soundfx - ./nginx/default.conf:/etc/nginx/conf.d/default.conf diff --git a/index.html b/index.html index 7145fd6..b356270 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - Vite + Vue + TS + BattleCaster
diff --git a/map/index.html b/map/index.html index 9b142c1..0687c77 100644 --- a/map/index.html +++ b/map/index.html @@ -4,7 +4,7 @@ - Vite + Vue + TS + MAP | BattleCaster
diff --git a/nginx/default.conf b/nginx/default.conf index 047dfe6..266d0b4 100644 --- a/nginx/default.conf +++ b/nginx/default.conf @@ -5,10 +5,23 @@ server { root /usr/share/nginx/html; location / { - index index.html index.htm; - } - - location /maps { + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Credentials' 'true'; + add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS'; + add_header 'Access-Control-Allow-Private-Network' 'true'; + add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; + add_header 'Access-Control-Max-Age' 1728000; + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Credentials' 'true'; + add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS'; + add_header 'Access-Control-Allow-Private-Network' 'true'; + add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; + add_header 'Access-Control-Max-Age' 1728000; + add_header 'Content-Type' 'text/plain charset=UTF-8'; + add_header 'Content-Length' 0; + return 204; + } autoindex on; autoindex_format json; } diff --git a/public/soundfx/sword slash/52752_438046_438045_Medieval_Weapons_-_Metal_Weapon_Swing-_Whoosh_normal.aac b/public/soundfx/sword slash/52752_438046_438045_Medieval_Weapons_-_Metal_Weapon_Swing-_Whoosh_normal.aac new file mode 100644 index 0000000000000000000000000000000000000000..9e10240f9d359c7fb066ae774d33a203ddc7420c GIT binary patch literal 33384 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 z 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

`V`-Bc;+sAyf+QR(6(*2bEnfD|rCt4P9C4hm%&m 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 Uv4Raq2gcbc+AK0ICR8_e_&a7F@S`zq)eX z(|0nh;`4F8ZPV%H%a_H==f{Bf^80x4aQl7%$Da?2nE7$|pFU5Y$LGv*a^cH|%bz#E zV))^%FiK(oIh-8Bm^e6`&(Y7(^j5%G!2j_~fHi;oAsb0d@0-I^9Dpr26ER35(f|Me z02NBZ@~#ZCVg|@i?kpy8DW8lqx^|%aQ5o=<T&E{)e~&Qy$V0Nmc2<|M5(KGGF{58%tLIZ~u55 zT5uLpt4h!S00041RQbsus@_?S?iW22z(QO#suh;;885QA-iwH!jbH#xM8s7_aSw3W z1}$16OduktCdgMpOG5WY;VN>3cx5Cw8zG6QrQ}CLVgfWw#eq%;D-i_-Kc$TWP$>!w zieRKlg5YU;MUF>6$kMx6rmA)7#TjO6!#lj%^v3i|YO7bE#XfvFIh;V{^5esi&!5QU z#Bk*Ja^&U3%jw{Jz;YijapVqMx$`&$MT$ajO%g&29Dpt23ZqH@0001Fs;Bgl3MyEs z8tri`Ciq66hr1JL8rk5-&lSc3kdGpk081>XZ17VJC?4OVU6Gp?vZ%Uvic7(~8}&~{ zy{?ni_uj5%tV+ZzaRV&|KEu6U`BvyBDM3Wl9fTpI#g;Lc5E56q$VZV3)dun^V)kgyBpoY& z0000Ks>j7i1@_7Ip`~w^BzR?*$&3an@^})f)epj z819@dtYf`(;o3!{nIgS{!9)Urz#dz~Q6`zIhyVZrsY+E--WLIZy+>GAQ05_-+j$I3 z0Zs2x0Wnsf;dJ6Pnak^lR>>j(g~c*y&QUwu=R56riqfy)C>=%u2yz0AM#CiM09MMG zhgE^qFMPQTvKp|$L@8y8AY_?lNy{zf0Wi-GTNJD{On@X|3pf~)bmx%V7gw!FOcAcr zUte>!<5WP1zS#Gm^3>VgP@*d2EU;C)021K;@l1dofBYdE2ptc;fB-oGS7ae3n{b2x z1v03mRX^lFE=PcV3 zo0wm27^ZTfqM^rt2R=u`pCQNK00Hm;%ZJPyj$m+b4}bvXasV99%59qM$==#v9W}!? zI&dRA+F947bxJLAPv1Vyfz9FBasj{Ey%t9Due zHLB+g6s*-tS5h9cF=)Whi`t%6Z8VpNqoV2m6`SU&Us literal 0 HcmV?d00001 diff --git a/public/soundfx/sword slash/762456_762424_Shinobi_Vol_2_-_Sword_Slash_01_-_MASTERED_-_2496.aac b/public/soundfx/sword slash/762456_762424_Shinobi_Vol_2_-_Sword_Slash_01_-_MASTERED_-_2496.aac new file mode 100644 index 0000000000000000000000000000000000000000..ec238cbc04ed91a9c2c3faa74df71a7aa01bca62 GIT binary patch literal 31921 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 J`2SH1{ttR~Y-<1j literal 0 HcmV?d00001 diff --git a/public/soundfx/sword slash/99157_719601_Throwing_Weapon_Hits_Body.aac b/public/soundfx/sword slash/99157_719601_Throwing_Weapon_Hits_Body.aac new file mode 100644 index 0000000000000000000000000000000000000000..e074141863aabfe3802273aeaa201785efb7911c GIT binary patch literal 24448 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 J`2SH1{ttR~Y-<1j literal 0 HcmV?d00001 diff --git a/src/assets/main.css b/src/assets/main.css new file mode 100644 index 0000000..6c0055c --- /dev/null +++ b/src/assets/main.css @@ -0,0 +1,35 @@ +@import './theme.css'; + +@layer base { + + :root { + --border-radius-sm: 4px; + --border-radius: 6px; + --border-radius-lg: 8px; + --border-radius-xl: 12px; + + --s-0_5: 0.125rem; + --s-1: 0.25rem; + --s-2: 0.5rem; + --s-3: 0.75rem; + --s-4: 1rem; + --s-5: 1.25rem; + --s-6: 1.5rem; + --s-7: 1.75rem; + --s-8: 2rem; + --s-9: 2.25rem; + --s-10: 2.5rem; + --s-11: 2.75rem; + --s-12: 3rem; + } + + .frosted { + --color-bg: var(--color-theme-700); + --frosted-box-shadow: inset 10px -10px 30px 5px rgba(from var(--color-theme-400) r g b / 0.1); + --frosted-gradient: linear-gradient(15deg, var(--color-theme-700), var(--color-theme-800)); + border: 1px solid var(--color-contrast-50); + background: var(--frosted-gradient) var(--color-bg); + box-shadow: var(--frosted-box-shadow); + color: var(--contrast-100); + } +} diff --git a/src/assets/theme.css b/src/assets/theme.css new file mode 100644 index 0000000..60efc15 --- /dev/null +++ b/src/assets/theme.css @@ -0,0 +1,174 @@ +@layer base { + + + + :root { + --color-slate: #64748b; + --color-gray: #6b7280; + --color-zinc: #71717a; + --color-neutral: #737373; + --color-stone: #78716c; + --color-red: #ef4444; + --color-orange: #f97316; + --color-amber: #f59e0b; + --color-yellow: #eab308; + --color-lime: #84cc16; + --color-green: #22c55e; + --color-emerald: #10b981; + --color-teal: #14b8a6; + --color-cyan: #06b6d4; + --color-sky: #0ea5e9; + --color-blue: #3b82f6; + --color-indigo: #6366f1; + --color-violet: #8b5cf6; + --color-purple: #a855f7; + --color-fuchsia: #d946ef; + --color-pink: #ec4899; + --color-rose: #f43f5e; + } + + .theme { + --color-theme-50: color-mix(in oklab, var(--color-theme), #fff 90%); + --color-theme-100: color-mix(in oklab, var(--color-theme), #fff 80%); + --color-theme-200: color-mix(in oklab, var(--color-theme), #fff 60%); + --color-theme-300: color-mix(in oklab, var(--color-theme), #fff 40%); + --color-theme-400: color-mix(in oklab, var(--color-theme), #fff 20%); + --color-theme-500: var(--color-theme); + --color-theme-600: color-mix(in oklab, var(--color-theme), #000 20%); + --color-theme-700: color-mix(in oklab, var(--color-theme), #000 40%); + --color-theme-800: color-mix(in oklab, var(--color-theme), #000 60%); + --color-theme-900: color-mix(in oklab, var(--color-theme), #000 80%); + --color-theme-950: color-mix(in oklab, var(--color-theme), #000 90%); + } + + .contrast { + --color-contrast-base: rgb( + from var(--color-bg) + clamp(0, calc((r/255*0.2126 + g/255*0.7152 + b/255*0.0722 - 0.5) * -64000), 255) + clamp(0, calc((r/255*0.2126 + g/255*0.7152 + b/255*0.0722 - 0.5) * -64000), 255) + clamp(0, calc((r/255*0.2126 + g/255*0.7152 + b/255*0.0722 - 0.5) * -64000), 255) + ); + --color-contrast-50: hsl(from color-mix(in oklab, var(--color-bg), var(--color-contrast-base) 5%) h calc(s * 1) l); + --color-contrast-100: hsl(from color-mix(in oklab, var(--color-bg), var(--color-contrast-base) 10%) h calc(s * 1.5) l); + --color-contrast-200: hsl(from color-mix(in oklab, var(--color-bg), var(--color-contrast-base) 20%) h calc(s * 2) l); + --color-contrast-300: hsl(from color-mix(in oklab, var(--color-bg), var(--color-contrast-base) 30%) h calc(s * 3) l); + --color-contrast-400: hsl(from color-mix(in oklab, var(--color-bg), var(--color-contrast-base) 40%) h calc(s * 4) l); + --color-contrast-500: hsl(from color-mix(in oklab, var(--color-bg), var(--color-contrast-base) 50%) h calc(s * 5) l); + --color-contrast-600: hsl(from color-mix(in oklab, var(--color-bg), var(--color-contrast-base) 60%) h calc(s*6) l); + --color-contrast-700: hsl(from color-mix(in oklab, var(--color-bg), var(--color-contrast-base) 70%) h calc(s*7) l); + --color-contrast-800: hsl(from color-mix(in oklab, var(--color-bg), var(--color-contrast-base) 80%) h calc(s*8) l); + --color-contrast-900: hsl(from color-mix(in oklab, var(--color-bg), var(--color-contrast-base) 90%) h calc(s*9) l); + --color-contrast-950: hsl(from color-mix(in oklab, var(--color-bg), var(--color-contrast-base) 95%) h calc(s*9.5) l); + } + + .text-constrast-50 { + color: var(--color-contrast-50); + --color-text: var(--color-contrast-50); + } + + .text-constrast-100 { + color: var(--color-contrast-100); + --color-text: var(--color-contrast-100); + } + + .text-constrast-200 { + color: var(--color-contrast-200); + --color-text: var(--color-contrast-200); + } + + .text-constrast-300 { + color: var(--color-contrast-300); + --color-text: var(--color-contrast-300); + } + + .text-constrast-400 { + color: var(--color-contrast-400); + --color-text: var(--color-contrast-400); + } + + .text-constrast-500 { + color: var(--color-contrast-500); + --color-text: var(--color-contrast-500); + } + + .text-constrast-600 { + color: var(--color-contrast-600); + --color-text: var(--color-contrast-600); + } + + .text-constrast-700 { + color: var(--color-contrast-700); + --color-text: var(--color-contrast-700); + } + + .text-constrast-800 { + color: var(--color-contrast-800); + --color-text: var(--color-contrast-800); + } + + .text-constrast-900 { + color: var(--color-contrast-900); + --color-text: var(--color-contrast-900); + } + + .text-constrast-950 { + color: var(--color-contrast-950); + --color-text: var(--color-contrast-950); + } + + .bg-theme-50 { + background-color: var(--color-theme-50); + --color-bg: var(--color-theme-50); + } + + .bg-theme-100 { + background-color: var(--color-theme-100); + --color-bg: var(--color-theme-100); + } + + .bg-theme-200 { + background-color: var(--color-theme-200); + --color-bg: var(--color-theme-200); + } + + .bg-theme-300 { + background-color: var(--color-theme-300); + --color-bg: var(--color-theme-300); + } + + .bg-theme-400 { + background-color: var(--color-theme-400); + --color-bg: var(--color-theme-400); + } + + .bg-theme-500 { + background-color: var(--color-theme-500); + --color-bg: var(--color-theme-500); + } + + .bg-theme-600 { + background-color: var(--color-theme-600); + --color-bg: var(--color-theme-600); + } + + .bg-theme-700 { + background-color: var(--color-theme-700); + --color-bg: var(--color-theme-700); + } + + .bg-theme-800 { + background-color: var(--color-theme-800); + --color-bg: var(--color-theme-800); + } + + .bg-theme-900 { + background-color: var(--color-theme-900); + --color-bg: var(--color-theme-900); + } + + .bg-theme-950 { + background-color: var(--color-theme-950); + --color-bg: var(--color-theme-950); + } + +} \ No newline at end of file diff --git a/src/chromecast.ts b/src/chromecast.ts new file mode 100644 index 0000000..1bf7683 --- /dev/null +++ b/src/chromecast.ts @@ -0,0 +1,23 @@ +import { ChromecastChannelCaster } from './model/Remote/Channels/ChromcastCaster' +import { PostMessageChannel } from './model/Remote/Channels/PostMessage'; +import './style.css' + +const searchParams = new URLSearchParams(window.location.search); + +const chromecast = new ChromecastChannelCaster(searchParams.get('ws') ?? undefined); +const postMessage = new PostMessageChannel(window); +chromecast.initialize(); +chromecast.on((msg) => { + console.log('CHROMECAST MESSAGE', msg); + console.log(msg); + postMessage.send(msg); +}) +postMessage.on((msg) => { + console.log('POST MESSAGE', msg); + console.log(msg); + chromecast.send(msg); +}); + +document.getElementById('hail')?.addEventListener('click', () => { + chromecast.hail(); +}) \ No newline at end of file diff --git a/src/components/Audio/AudioCard.vue b/src/components/Audio/AudioCard.vue new file mode 100644 index 0000000..69d3926 --- /dev/null +++ b/src/components/Audio/AudioCard.vue @@ -0,0 +1,237 @@ + + + \ No newline at end of file diff --git a/src/components/Audio/DeviceVolume.vue b/src/components/Audio/DeviceVolume.vue new file mode 100644 index 0000000..742700a --- /dev/null +++ b/src/components/Audio/DeviceVolume.vue @@ -0,0 +1,78 @@ + + + \ No newline at end of file diff --git a/src/components/Audio/Playlist.vue b/src/components/Audio/Playlist.vue new file mode 100644 index 0000000..e2bd8f2 --- /dev/null +++ b/src/components/Audio/Playlist.vue @@ -0,0 +1,95 @@ + + + \ No newline at end of file diff --git a/src/components/Audio/Sfx.vue b/src/components/Audio/Sfx.vue new file mode 100644 index 0000000..29d19e5 --- /dev/null +++ b/src/components/Audio/Sfx.vue @@ -0,0 +1,58 @@ + + + \ No newline at end of file diff --git a/src/components/Audio/SfxSound.vue b/src/components/Audio/SfxSound.vue new file mode 100644 index 0000000..f17bb3f --- /dev/null +++ b/src/components/Audio/SfxSound.vue @@ -0,0 +1,129 @@ + + + \ No newline at end of file diff --git a/src/components/Audio/Soundboard.vue b/src/components/Audio/Soundboard.vue new file mode 100644 index 0000000..6fa9de5 --- /dev/null +++ b/src/components/Audio/Soundboard.vue @@ -0,0 +1,120 @@ + + + + diff --git a/src/components/Audio/TabSource.vue b/src/components/Audio/TabSource.vue new file mode 100644 index 0000000..6e0d9f9 --- /dev/null +++ b/src/components/Audio/TabSource.vue @@ -0,0 +1,21 @@ + + + + diff --git a/src/components/Audio/modal.vue b/src/components/Audio/modal.vue new file mode 100644 index 0000000..4445741 --- /dev/null +++ b/src/components/Audio/modal.vue @@ -0,0 +1,69 @@ + + + \ No newline at end of file diff --git a/src/components/FileManager/FileManager.vue b/src/components/FileManager/FileManager.vue index 286b773..51467cc 100644 --- a/src/components/FileManager/FileManager.vue +++ b/src/components/FileManager/FileManager.vue @@ -1,19 +1,77 @@ \ No newline at end of file diff --git a/src/components/Files/FilesAside.vue b/src/components/Files/FilesAside.vue new file mode 100644 index 0000000..97c128f --- /dev/null +++ b/src/components/Files/FilesAside.vue @@ -0,0 +1,100 @@ + + + + + \ No newline at end of file diff --git a/src/components/Icons/Backward.vue b/src/components/Icons/Backward.vue new file mode 100644 index 0000000..ed5e8cb --- /dev/null +++ b/src/components/Icons/Backward.vue @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/src/components/Icons/Cog.vue b/src/components/Icons/Cog.vue new file mode 100644 index 0000000..0ed1978 --- /dev/null +++ b/src/components/Icons/Cog.vue @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/src/components/Icons/Forward.vue b/src/components/Icons/Forward.vue new file mode 100644 index 0000000..1bdafb4 --- /dev/null +++ b/src/components/Icons/Forward.vue @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/src/components/Icons/Mute.vue b/src/components/Icons/Mute.vue new file mode 100644 index 0000000..ef314e8 --- /dev/null +++ b/src/components/Icons/Mute.vue @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/src/components/Icons/Pause.vue b/src/components/Icons/Pause.vue new file mode 100644 index 0000000..652de01 --- /dev/null +++ b/src/components/Icons/Pause.vue @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/src/components/Icons/Play.vue b/src/components/Icons/Play.vue new file mode 100644 index 0000000..05ca625 --- /dev/null +++ b/src/components/Icons/Play.vue @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/src/components/Icons/Speaker.vue b/src/components/Icons/Speaker.vue new file mode 100644 index 0000000..cccf736 --- /dev/null +++ b/src/components/Icons/Speaker.vue @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/src/components/Icons/Stop.vue b/src/components/Icons/Stop.vue new file mode 100644 index 0000000..187e9c3 --- /dev/null +++ b/src/components/Icons/Stop.vue @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/src/components/RemoteDevices/RemoteDevices.vue b/src/components/RemoteDevices/RemoteDevices.vue new file mode 100644 index 0000000..a1711f2 --- /dev/null +++ b/src/components/RemoteDevices/RemoteDevices.vue @@ -0,0 +1,64 @@ + + + \ No newline at end of file diff --git a/src/components/ScreenManager/RemoteScreen.vue b/src/components/ScreenManager/RemoteScreen.vue new file mode 100644 index 0000000..d09306b --- /dev/null +++ b/src/components/ScreenManager/RemoteScreen.vue @@ -0,0 +1,192 @@ + + + diff --git a/src/components/ScreenManager/ScreenManager.vue b/src/components/ScreenManager/ScreenManager.vue new file mode 100644 index 0000000..8f12f8c --- /dev/null +++ b/src/components/ScreenManager/ScreenManager.vue @@ -0,0 +1,28 @@ + + + \ No newline at end of file diff --git a/src/components/Soundboard.vue b/src/components/Soundboard.vue new file mode 100644 index 0000000..2ec3c43 --- /dev/null +++ b/src/components/Soundboard.vue @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/src/components/Tabs.vue b/src/components/Tabs.vue new file mode 100644 index 0000000..0c148d7 --- /dev/null +++ b/src/components/Tabs.vue @@ -0,0 +1,56 @@ + + + \ No newline at end of file diff --git a/src/helpers/immediate.ts b/src/helpers/immediate.ts new file mode 100644 index 0000000..345e63c --- /dev/null +++ b/src/helpers/immediate.ts @@ -0,0 +1,4 @@ +export function immediate any>(fn: T):T { + fn(); + return fn; +} \ No newline at end of file diff --git a/src/helpers/loadScript.ts b/src/helpers/loadScript.ts new file mode 100644 index 0000000..2a3334a --- /dev/null +++ b/src/helpers/loadScript.ts @@ -0,0 +1,9 @@ +export function loadScript(src: string) { + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = src; + script.onload = () => resolve(); + script.onerror = () => reject(new Error('Failed to load script')); + document.head.appendChild(script); + }); +} \ No newline at end of file diff --git a/src/helpers/mouseDragging.ts b/src/helpers/mouseDragging.ts new file mode 100644 index 0000000..e54d371 --- /dev/null +++ b/src/helpers/mouseDragging.ts @@ -0,0 +1,34 @@ +import { onBeforeUnmount, ref } from "vue"; + +export const useMouseDragging = () => { + const dragging = ref(false); + const x = ref(0); + const y = ref(0); + const initialPosition = { x: 0, y: 0 }; + + function onMouseDown(event) { + dragging.value = true; + initialPosition.x = event.clientX - x.value; + initialPosition.y = event.clientY - y.value; + + window.addEventListener('mouseup', () => { + dragging.value = false; + }, { once: true }); + } + + function onMouseMove(event) { + if (dragging.value) { + x.value = event.clientX - initialPosition.x; + y.value = event.clientY - initialPosition.y; + } + } + + + window.addEventListener('mousemove', onMouseMove); + + onBeforeUnmount(() => { + window.removeEventListener('mousemove', onMouseMove); + }); + + return {onMouseDown, x, y}; +} \ No newline at end of file diff --git a/src/helpers/withLoading.ts b/src/helpers/withLoading.ts new file mode 100644 index 0000000..ad1348b --- /dev/null +++ b/src/helpers/withLoading.ts @@ -0,0 +1,16 @@ +import { computed, ref } from "vue"; + + +export const useLoading = () => { + const loadingCount = ref(0); + const isLoading = computed(() => loadingCount.value > 0); + const withLoading = async (fn: () => Promise) => { + loadingCount.value++; + try { + return await fn(); + } finally { + loadingCount.value--; + } + } + return { isLoading, withLoading }; +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 4fcbf17..3ea13c5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,21 +1,58 @@ -import { createApp } from 'vue' +import './assets/main.css' +import { createApp, reactive } from 'vue' import './style.css' import ControlApp from './modules/controls/ControlApp.vue' -import { FileManager } from './model/FileManager' -import { FileManagerSymbol, SenderSymbol } from './model/injectionSymbols' -import { BroadcastSender } from './model/BroadcastListener'; -import { MultiSender } from './model/MultiListener' -import { ChromecastSender } from './model/ChromecastListener' +import { AudioContextSymbol, MountedFsSymbol, RemoteDeviceManagerSymbol } from './model/injectionSymbols' +import { MountedFs } from './model/FileSystem/MountedFs' +import { RemoteDeviceManager } from './model/Remote/RemoteDeviceManager' +import { ChromecastChannelCaster } from './model/Remote/Channels/ChromcastCaster' +import { WsSignalingCaster } from './model/Remote/Channels/WsSignaling' const app = createApp(ControlApp); +const chromecastChannelCaster = new ChromecastChannelCaster(); +chromecastChannelCaster.initialize(); +console.log('chromecastChannelCaster', chromecastChannelCaster); + +const audioContext = new AudioContext(); + app - .provide(SenderSymbol, new MultiSender([ - new BroadcastSender(), - new ChromecastSender(), + // .provide(SenderSymbol, new MultiSender([ + // new BroadcastSender(), + // new ChromecastSender(), + // ])) + // .provide(FileManagerSymbol, new FileManager()) + .provide(RemoteDeviceManagerSymbol, reactive(RemoteDeviceManager.restore({audioContext}))) + .provide(AudioContextSymbol, audioContext) + .provide(MountedFsSymbol, new MountedFs([ + /*{ + name: 'Maps', + type: 'nginx', + rootUrl: 'http://localhost:8080/maps/', + auth: undefined, + }, + { + name: 'SFX', + type: 'nginx', + rootUrl: 'http://localhost:8080/soundfx/', + auth: undefined, + },*/ + { + name: 'Maps', + type: 'filestash', + domain: 'https://files.fisoft.eu', + key: '426pBJgSyfmgJAaUmNjk', + share: 'nCKcVyl', + }, + { + name: 'Audio', + type: 'filestash', + domain: 'https://files.fisoft.eu', + key: '426pBJgSyfmgJAaUmNjk', + share: '74uqzNZ', + } ])) - .provide(FileManagerSymbol, new FileManager()) .mount('#app') diff --git a/src/model/Audio/AudioNodesManager.ts b/src/model/Audio/AudioNodesManager.ts new file mode 100644 index 0000000..f445eec --- /dev/null +++ b/src/model/Audio/AudioNodesManager.ts @@ -0,0 +1,44 @@ +import { PlaylistGroupNode, PlaylistGroupNodeOptions } from "./PlaylistGroupNode"; +import { SfxGroupNode, SfxGroupNodeOptions } from "./SfxGroupNode"; +import { SoundGroupNode } from "./SoundGroupNode"; +import { TabSourceNode, TabSourceNodeOptions } from "./TabSourceNode"; +import { SoundboardContext } from "./types"; + + +type SerializedNode> = ReturnType +export type SupportedDefinitions = SerializedNode | SerializedNode | SerializedNode + + +export class AudioNodeManager { + + deserializeNodes(definition: string, ctx: SoundboardContext) { + const definitions = JSON.parse(definition) as SupportedDefinitions[]; + return definitions.map((definition) => this.deserializeNode(definition, ctx)); + } + + deserializeNode(definition: SupportedDefinitions, ctx: SoundboardContext) { + if (definition.type === 'playlist') { + return new PlaylistGroupNode(definition, ctx); + } else if (definition.type === 'sfx') { + return new SfxGroupNode(definition, ctx); + } else if (definition.type === 'tabSource') { + return new TabSourceNode(definition, ctx); + } else { + throw new Error('Invalid node type'); + } + } + + restore(ctx: SoundboardContext) { + const nodes = localStorage.getItem('audioNodes'); + if (nodes) { + return this.deserializeNodes(nodes, ctx); + } + return []; + } + + store(nodes: SoundGroupNode[]) { + const definitions = nodes.map(node => node.serializeOptions()); + localStorage.setItem('audioNodes', JSON.stringify(definitions)); + } + +} \ No newline at end of file diff --git a/src/model/Audio/PlaylistGroupNode.ts b/src/model/Audio/PlaylistGroupNode.ts new file mode 100644 index 0000000..d7f7831 --- /dev/null +++ b/src/model/Audio/PlaylistGroupNode.ts @@ -0,0 +1,68 @@ +import { VirtualFile } from "../FileSystem/types"; +import { RemoteDevice } from "../Remote/types"; +import { SoundGroupNode, SoundGroupNodeOptions } from "./SoundGroupNode"; +import { MediaElementSoundNode } from "./SoundNode"; + +export interface PlaylistGroupNodeOptions extends SoundGroupNodeOptions { + currentTrack: number; + currentTrackTime: number; +} + +export class PlaylistGroupNode extends SoundGroupNode { + + public readonly type = 'playlist'; + + changeTrack(track: number) { + this.currentTrack.stop(); + (this.currentTrack as MediaElementSoundNode)?.seek(0); + this.soundNodes[this.options.currentTrack]?.stop(); + this.options.currentTrack = track; + this.options.currentTrackTime = 0; + this.play(); + } + + get currentTrack() { + return this.soundNodes[this.options.currentTrack]; + } + + get isPlaying() { + return this.soundNodes.some(sound => sound.isPlaying); + } + + play() { + this.currentTrack?.play(); + } + + togglePlay() { + if (this.isPlaying) { + this.currentTrack?.stop(); + } else { + this.play(); + } + } + + prev() { + this.changeTrack(((this.options.currentTrack ?? 0) + this.soundNodes.length - 1) % this.soundNodes.length); + } + + next() { + this.changeTrack(((this.options.currentTrack ?? 0) + 1) % this.soundNodes.length); + } + + serializeOptions() { + return { + ...super.serializeOptions(), + currentTrack: this.options.currentTrack ?? 0, + currentTrackTime: this.options.currentTrackTime ?? 0, + type: this.type, + } as const; + } + + addSound(vFile: VirtualFile) { + this.addSoundNode({ + type: 'mediaElement', + vFile, + volume: 100, + }); + } +} \ No newline at end of file diff --git a/src/model/Audio/README.md b/src/model/Audio/README.md new file mode 100644 index 0000000..a4a4ada --- /dev/null +++ b/src/model/Audio/README.md @@ -0,0 +1,4 @@ +# AudioTrack + +represents one audio file + diff --git a/src/model/Audio/SfxGroupNode.ts b/src/model/Audio/SfxGroupNode.ts new file mode 100644 index 0000000..e6f5b8f --- /dev/null +++ b/src/model/Audio/SfxGroupNode.ts @@ -0,0 +1,41 @@ +import { VirtualFile } from "../FileSystem/types"; +import { SoundGroupNode, SoundGroupNodeOptions } from "./SoundGroupNode"; +import { BufferedSoundNode } from "./SoundNode"; + +export interface SfxGroupNodeOptions extends SoundGroupNodeOptions { + +} + +export class SfxGroupNode extends SoundGroupNode { + public readonly type = 'sfx'; + + get activeSounds() { + return this.soundNodes.filter(sound => sound.isPlaying); + } + + play() { + return; + } + + togglePlay() { + if (this.isPlaying) { + this.soundNodes.forEach(sound => sound.stop()); + } + } + + addSound(vFile: VirtualFile) { + this.addSoundNode({ + type: 'buffered', + vFile, + volume: 100, + }); + } + + serializeOptions() { + return { + ...super.serializeOptions(), + type: this.type, + } as const; + } + +} \ No newline at end of file diff --git a/src/model/Audio/SoundGroupNode.ts b/src/model/Audio/SoundGroupNode.ts new file mode 100644 index 0000000..8477cf3 --- /dev/null +++ b/src/model/Audio/SoundGroupNode.ts @@ -0,0 +1,146 @@ +import { VirtualFile } from "../FileSystem/types"; +import { RemoteDevice } from "../Remote/types"; +import { BufferedSoundNode, MediaElementSoundNode, PartiaSoundNodeOptions, SoundNode } from "./SoundNode"; +import { SoundboardContext } from "./types"; + +export interface SoundGroupNodeOptions { + volume: number, + isMuted: boolean, + sounds: PartiaSoundNodeOptions[] +} + +export class DeviceSource { + + public isConnected = false; + + constructor(public device: RemoteDevice, public gainNode: GainNode) { + this.device = device; + this.gainNode = gainNode; + } + + connect() { + this.gainNode.connect(this.device.audioSink); + this.isConnected = true; + } + + diconnect() { + this.gainNode.disconnect(this.device.audioSink); + this.isConnected = false; + } +} + +export abstract class SoundGroupNode { + + public abstract readonly type: string; + public deviceSources: Map = new Map(); + protected options: Omit + protected gainNode: GainNode; + protected soundNodes: SoundNode[] = []; + + constructor( + options: T, + protected ctx: SoundboardContext + ) { + this.options = options; + this.gainNode = this.ctx.audioContext.createGain(); + this.gainNode.gain.value = (options.volume / 100); + for (const sound of options.sounds) { + this.addSoundNode(sound); + } + } + + addSound(vFile: VirtualFile) { + // implement in subclass + } + + addSoundNode(options: PartiaSoundNodeOptions) { + if (options.type === 'buffered') { + const sound = new BufferedSoundNode(options, this.ctx); + this.soundNodes.push(sound); + sound.connect(this.gainNode); + } else if (options.type === 'mediaElement') { + const sound = new MediaElementSoundNode(options, this.ctx); + this.soundNodes.push(sound); + sound.connect(this.gainNode); + } + } + + sounds() { + return this.soundNodes; + } + + get isPlaying() { + return this.soundNodes.some(sound => sound.isPlaying); + } + + abstract play(): void + abstract togglePlay(): void + + + connect(destination: AudioNode) { + console.log('connecting', this, destination); + this.gainNode.connect(destination); + } + + disconnect(destinationNode?: AudioNode) { + if (destinationNode) { + this.gainNode.disconnect(destinationNode); + } else { + this.gainNode.disconnect(); + } + } + + get volume() { + + return this.options.volume; + } + + set volume(val) { + this.options.volume = val; + this.gainNode.gain.value = val / 100; + } + + get isMuted() { + return this.options.isMuted; + } + + mute() { + this.options.isMuted = true; + this.gainNode.gain.value = 0; + } + + unmute() { + this.options.isMuted = false; + this.gainNode.gain.value = this.options.volume / 100; + } + + toggleMute() { + if (this.isMuted) { + this.unmute(); + } else { + this.mute(); + } + } + + getDeviceSource(remoteDevice: RemoteDevice) { + if (!this.deviceSources.has(remoteDevice)) { + const gain = this.ctx.audioContext.createGain(); + this.gainNode.connect(gain); + const deviceSource = new DeviceSource(remoteDevice, gain); + this.deviceSources.set(remoteDevice, deviceSource); + deviceSource.connect(); + } + return this.deviceSources.get(remoteDevice)!; + } + + serializeOptions() { + return { + ...this.options, + volume: this.volume, + isMuted: this.options.isMuted, + sounds: this.soundNodes.map(soundNode => soundNode.options), + type: this.type, + } + } + +} \ No newline at end of file diff --git a/src/model/Audio/SoundNode.ts b/src/model/Audio/SoundNode.ts new file mode 100644 index 0000000..930cbc1 --- /dev/null +++ b/src/model/Audio/SoundNode.ts @@ -0,0 +1,177 @@ +import { MountedFs } from "../FileSystem/MountedFs"; +import { FileSystemSource, VirtualFile } from "../FileSystem/types"; +import { SoundboardContext } from "./types"; + + +export interface PartiaSoundNodeOptions { + vFile: VirtualFile; + volume?: number; + type: 'buffered' | 'mediaElement'; +} + +export interface SoundNodeOptions extends PartiaSoundNodeOptions { + volume: number; +} + +export abstract class SoundNode { + + protected gainNode: GainNode; + protected file?: File; + public options: SoundNodeOptions; + public readonly numberOfInputs = 0; + public readonly numberOfOutputs = 1; + protected activeSources: AudioBufferSourceNode[] = []; + protected audioContext: AudioContext; + protected readonly fs: MountedFs; + + constructor( + options: PartiaSoundNodeOptions, + ctx: SoundboardContext, +) { + this.options = { + ...options, + volume: options.volume ?? 100 + } + this.audioContext = ctx.audioContext; + this.fs = ctx.fs; + this.gainNode = this.audioContext.createGain(); + this.gainNode.gain.value = (this.options.volume / 100); + } + + get isPlaying() { + return this.activeSources.length > 0; + } + + get name() { + return this.options.vFile.name.replace(/\.[^/.]+$/, ""); + } + + get key() { + return this.options.vFile.path.join('/'); + } + + protected async preload() { + if (!this.file) { + this.file = await this.fs.download(this.options.vFile); + } + } + + connect(destination: AudioNode) { + console.log('connecting', this, destination); + this.gainNode.connect(destination); + } + + disconnect(destinationNode?: AudioNode) { + if (destinationNode) { + this.gainNode.disconnect(destinationNode); + } else { + this.gainNode.disconnect(); + } + } + + setVolume(volume: number) { + this.gainNode.gain.value = volume / 100; + } + + getVolume() { + return this.gainNode.gain.value * 100; + } + + abstract play(): Promise; + abstract stop(): Promise; +} + +export class BufferedSoundNode extends SoundNode { + private buffer?: AudioBuffer; + + async preload() { + await super.preload(); + if (!this.buffer) { + const arrayBuffer = await this.file!.arrayBuffer(); + this.buffer = await this.audioContext.decodeAudioData(arrayBuffer); + } + } + + async play() { + await this.preload(); + const source = this.audioContext.createBufferSource(); + source.buffer = this.buffer!; + source.connect(this.gainNode); + source.start(); + this.audioContext.resume(); + this.activeSources.push(source); + source.addEventListener('ended', () => { + const index = this.activeSources.indexOf(source); + if (index !== -1) { + this.activeSources.splice(index, 1); + } + }); + } + + async stop() { + this.activeSources.forEach((source) => source.stop()); + this.activeSources = []; + } +} + +export class MediaElementSoundNode extends SoundNode { + private mediaElement?: HTMLMediaElement; + private source?: MediaElementAudioSourceNode; + private _isPlaying = false; + + async preload() { + await super.preload(); + if (!this.mediaElement) { + this.mediaElement = document.createElement('audio'); + this.mediaElement.src = URL.createObjectURL(this.file!); + this.source = this.audioContext.createMediaElementSource(this.mediaElement); + this.source.connect(this.gainNode); + this.audioContext.resume(); + this.mediaElement.addEventListener('ended', ()=> { + this._isPlaying = false; + }); + this.mediaElement.addEventListener('playing', (event) => { + console.log(event); + this._isPlaying = true; + }); + this.mediaElement.addEventListener('pause', () => { + this._isPlaying = false; + }); + } + } + + get isPlaying() { + return this._isPlaying; + } + + get currentTime() { + return this.mediaElement!.currentTime; + } + + get duration() { + return this.mediaElement!.duration; + } + + async play() { + await this.preload(); + await this.mediaElement?.play(); + } + + async stop() { + await this.mediaElement?.pause(); + } + + pause() { + this.mediaElement?.pause(); + } + + resume() { + this.mediaElement?.play(); + } + + seek(time: number) { + if (this.mediaElement) { + this.mediaElement.currentTime = time; + } + } +} \ No newline at end of file diff --git a/src/model/Audio/TabSourceNode.ts b/src/model/Audio/TabSourceNode.ts new file mode 100644 index 0000000..c5567bd --- /dev/null +++ b/src/model/Audio/TabSourceNode.ts @@ -0,0 +1,76 @@ +import { SoundGroupNode, SoundGroupNodeOptions } from "./SoundGroupNode"; + +export interface TabSourceNodeOptions extends SoundGroupNodeOptions { +} + +export class TabSourceNode extends SoundGroupNode { + + public readonly type = 'tabSource'; + + private stream?: MediaStream; + private source?: MediaStreamAudioSourceNode; + + + play() { + if (!this.isPlaying) { + this.setup() + this.ctx.audioContext.resume(); + } + } + + private async setup() { + try { + this.stream = await navigator.mediaDevices.getDisplayMedia({ + video: { + displaySurface: "browser", + frameRate: {ideal: 1} + }, + audio: { + suppressLocalAudioPlayback: false, + echoCancellation: false, + noiseSuppression: false, + sampleRate: 44100, + }, + preferCurrentTab: false, + selfBrowserSurface: "exclude", + systemAudio: "exclude", + surfaceSwitching: "include", + monitorTypeSurfaces: "include", + }); + console.log(this.stream); + this.source = this.ctx.audioContext.createMediaStreamSource(this.stream); + this.source.connect(this.gainNode); + } catch (err) { + console.error(err); + } + } + + stop() { + this.stream?.getTracks().forEach(track => { + track.stop(); + }); + this.source?.disconnect(); + this.source = undefined; + this.stream = undefined; + } + + get isPlaying(): boolean { + return !!(this.stream && this.source); + } + + togglePlay(): void { + if (this.isPlaying) { + this.stop(); + } else { + this.play(); + } + } + + serializeOptions() { + return { + ...super.serializeOptions(), + type: this.type, + } as const; + } + +} \ No newline at end of file diff --git a/src/model/Audio/types.ts b/src/model/Audio/types.ts new file mode 100644 index 0000000..d8df8fa --- /dev/null +++ b/src/model/Audio/types.ts @@ -0,0 +1,7 @@ +import { MountedFs } from "../FileSystem/MountedFs"; +import { FileSystemSource } from "../FileSystem/types"; + +export type SoundboardContext = { + audioContext: AudioContext; + fs: MountedFs, +} \ No newline at end of file diff --git a/src/model/ChromecastListener.ts b/src/model/ChromecastListener.ts index 8b7acba..686be3e 100644 --- a/src/model/ChromecastListener.ts +++ b/src/model/ChromecastListener.ts @@ -1,3 +1,4 @@ +import { loadScript } from "../helpers/loadScript"; import { Message, Sender, Receiver, SendMesssageOptions } from "./CommunicationChannel"; let logCounter = 0; @@ -24,15 +25,7 @@ function fixResolution() { document.body.style.height = `${height}px`; } -function loadScript(src: string) { - return new Promise((resolve, reject) => { - const script = document.createElement('script'); - script.src = src; - script.onload = () => resolve(); - script.onerror = () => reject(new Error('Failed to load script')); - document.head.appendChild(script); - }); -} + diff --git a/src/model/FileMeta/ExtensionMimeType.ts b/src/model/FileMeta/ExtensionMimeType.ts new file mode 100644 index 0000000..bd7a770 --- /dev/null +++ b/src/model/FileMeta/ExtensionMimeType.ts @@ -0,0 +1,21 @@ +import { VirtualFile } from "../FileSystem/types"; + +export function extensionMimeType(fileName: string) { + if (fileName.endsWith('.jpg') || fileName.endsWith('.jpeg') || fileName.endsWith('.jpe')) { + return 'image/jpeg'; + } else if (fileName.endsWith('.png')) { + return 'image/png'; + } else if (fileName.endsWith('.webp')) { + return 'image/webp'; + } else if (fileName.endsWith('.mp4')) { + return 'video/mp4'; + } else if (fileName.endsWith('.webm')) { + return 'video/webm'; + } else if (fileName.endsWith('.ogg')) { + return 'audio/ogg'; + } else if (fileName.endsWith('.mp3')) { + return 'audio/mpeg'; + } else { + return 'application/octet-stream'; + } +} \ No newline at end of file diff --git a/src/model/FileMeta/MapMeta.ts b/src/model/FileMeta/MapMeta.ts new file mode 100644 index 0000000..0f8c375 --- /dev/null +++ b/src/model/FileMeta/MapMeta.ts @@ -0,0 +1,50 @@ +import { VirtualFile } from "../FileSystem/types"; + +export type MapMeta = { + width: number | undefined; + height: number | undefined; + animated?: boolean; + valid: boolean; +} + +function extractSize(file: VirtualFile): MapMeta { + { // czepeku format [${width}x${height}] + const match = [...file.path, file.name].join('/').match(/(\d+)x(\d+)/); + if (match) { + return { + width: parseInt(match[1]), + height: parseInt(match[2]), + } + } + } + + { // width format [${width}w] + const match = [...file.path, file.name].join('/').match(/(\d+)w/); + if (match) { + return { + width: parseInt(match[1]), + height: undefined, + } + } + } + + return { + width: undefined, + height: undefined, + } +} + +export function addMapMeta(file: T): T & { map: MapMeta } { + const { width, height } = extractSize(file); + + + return { + ...file, + map: { + width, + height, + valid: file.mimeType.startsWith('image/') || file.mimeType.startsWith('video/'), + animated: file.mimeType.startsWith('video/'), + }, + } +} \ No newline at end of file diff --git a/src/model/FileSystem/FileStashFs.ts b/src/model/FileSystem/FileStashFs.ts new file mode 100644 index 0000000..2764b17 --- /dev/null +++ b/src/model/FileSystem/FileStashFs.ts @@ -0,0 +1,100 @@ +import { extensionMimeType } from "../FileMeta/ExtensionMimeType"; +import { CachePolicy, FsCache } from "./FsCache"; +import { VirtualDirectory, VirtualFile, FileSystemSource, ListDirOptions } from "./types"; + +export type FileStashFsConfig = { + name: string; + domain: string; + share: string, + key: string; +} + +interface LsResult { + results: Array<{ + name: string, + size: number, + time: number, + type: 'file' | 'directory', + }> + status: "ok" +} + +export class FileStashFs implements FileSystemSource { + + private fsCache: FsCache; + + constructor(private options: FileStashFsConfig) { + this.fsCache = new FsCache(options.name); + } + + get name() { + return this.options.name; + } + + get root(): VirtualDirectory { + return { + type: 'directory', + source: this.options.name, + path: [], + name: this.options.name + } + } + + private createUrl(type: 'cat' | 'ls', path: string[]) { + const url = new URL(this.options.domain); + url.pathname = `/api/files/${type}`; + const params = new URLSearchParams(); + params.set('share', this.options.share); + params.set('key', this.options.key); + params.set('path', `/${path.join('/')}`); + url.search = params.toString(); + return url; + } + + async listDir(dir: VirtualDirectory, {cache = CachePolicy.useNetwork}: ListDirOptions = {}): Promise<(VirtualDirectory|VirtualFile)[]> { + const request = new Request(this.createUrl('ls', dir.path)); + const response = await this.fsCache.get(request, cache); + if (!response.ok) { + throw new Error(`Failed to list directory ${dir.path.join('/')}`); + } + const data = await response.json() as LsResult; + if (data.status !== 'ok') { + throw new Error(`Failed to list directory ${dir.path.join('/')}`); + } + return data.results.map((result) => { + if (result.type === 'file') { + return { + type: 'file', + source: this.options.name, + name: result.name, + mimeType: extensionMimeType(result.name), + path: [...dir.path, result.name], + } + } else if (result.type === 'directory') { + return { + type: 'directory', + source: this.options.name, + name: result.name, + path: [...dir.path, result.name], + } + } else { + throw new Error(`Unknown type ${result.type}`); + } + }); + } + + async download(file: VirtualFile): Promise { + const response = await fetch(this.createUrl('cat', file.path)); + if (!response.ok) { + throw new Error(`Failed to download file ${file.path.join('/')}`); + } + return new File([await response.blob()], file.name); + } + + getConfig() { + return { + ...this.options, + type: 'filestash', + } as const; + } +} \ No newline at end of file diff --git a/src/model/FileSystem/FsCache.ts b/src/model/FileSystem/FsCache.ts new file mode 100644 index 0000000..e390ff2 --- /dev/null +++ b/src/model/FileSystem/FsCache.ts @@ -0,0 +1,52 @@ + +export const CachePolicy = { + useCache: 'use-cache', + useNetwork: 'use-network', + onlyCache: 'only-cache', +} as const; +export type CachePolicy = typeof CachePolicy[keyof typeof CachePolicy]; + +export class CacheError extends Error {} +export class NoCacheEntryError extends CacheError {} + +export class FsCache { + + private _cache?: Cache; + + constructor(public readonly name: string) {} + + public async cache() { + if (!this._cache) { + this._cache = await caches.open(this.name); + } + return this._cache; + } + + public async fetch(request: RequestInfo): Promise { + const response = await fetch(request); + if (response.ok) { + const cache = await this.cache() + cache.put(request, response.clone()); + } + return response; + } + + public async get(request: RequestInfo, policy: CachePolicy): Promise { + if (policy === CachePolicy.useNetwork) { + return this.fetch(request); + } + const cache = await this.cache(); + const response = await cache.match(request); + if (response && policy === CachePolicy.useCache) { + return response; + } else if (policy === CachePolicy.onlyCache) { + if (!response) { + throw new NoCacheEntryError('No cache entry found'); + } + return response; + } else { + return this.fetch(request); + } + } + +} \ No newline at end of file diff --git a/src/model/FileSystem/MountedFs.ts b/src/model/FileSystem/MountedFs.ts new file mode 100644 index 0000000..6154dff --- /dev/null +++ b/src/model/FileSystem/MountedFs.ts @@ -0,0 +1,129 @@ +import { FileSystemSource, ListDirOptions, VirtualDirectory, VirtualFile } from "./types"; +import { NginxFS } from "./NginxFS"; +import { FileStashFs } from "./FileStashFs"; +import { CachePolicy, NoCacheEntryError } from "./FsCache"; + +type Config = T extends FileSystemSource ? ReturnType : never; + +const CACHE = Symbol('Cache'); +export interface Fs { + get root(): VirtualDirectory[]; + download(file: VirtualFile): Promise; + listDir(dir: VirtualDirectory, options?: ListDirOptions): Promise<(VirtualDirectory|VirtualFile)[]>; +} + +export class MountedFs implements Fs { + private filesystems: Map> = new Map(); + public readonly name: string = 'root'; + + constructor(filesystems: (Config | Config)[]) { + for (const fsOptions of filesystems) { + if (fsOptions.type === 'nginx') { + this.filesystems.set(fsOptions.name, new NginxFS(fsOptions)); + } else if (fsOptions.type === 'filestash') { + this.filesystems.set(fsOptions.name, new FileStashFs(fsOptions)); + } + } + } + + get root() { + return [...this.filesystems.values()].map(fs => fs.root); + } + + download(file: VirtualFile) { + const fs = this.filesystems.get(file.source); + if (!fs) { + throw new Error(`Filesystem ${file.source} not found`); + } + return fs.download(file); + } + + async listDir(dir: VirtualDirectory, options?: ListDirOptions) { + + const fs = this.filesystems.get(dir.source); + if (!fs) { + throw new Error(`Filesystem ${dir.source} not found`); + } + const result = await fs.listDir(dir, options); + + return result; + } + + async *search(query: string, cancelToken?: AbortSignal) { + query = query.toLocaleLowerCase(); + for (const fs of this.filesystems.values()) { + const dirQueue = [fs.root]; + while (dirQueue.length > 0) { + const dir = dirQueue.shift()!; + try { + const children = await this.listDir(dir, {cache: CachePolicy.onlyCache}); + for (const child of children) { + if (cancelToken?.aborted) { + return; + } + if (child.type === 'directory') { + dirQueue.push(child); + } else if (child.name.toLocaleLowerCase().includes(query)) { + yield child; + } + } + } catch (err) { + if (!(err instanceof NoCacheEntryError)) { + console.warn(`Failed to list directory ${dir.path.join('/')}`, err); + } + continue; + } + } + } + } + + async *buildIndex() { + let counter = 0; + for (const fs of this.filesystems.values()) { + const dirQueue = [fs.root]; + while (dirQueue.length > 0) { + const dir = dirQueue.shift()!; + for (const child of await this.listDir(dir)) { + if (child.type === 'directory') { + dirQueue.push(child); + } + counter++; + } + yield counter + } + } + } +} + + +// export class SearchableFilesystem implements Fs { +// public allFiles: VirtualFile[] = []; + +// constructor(private fs: MountedFs) {} + +// async precacheDir(dir: VirtualDirectory) { +// for (const child of await this.fs.listDir(dir, {cache: CachePolicy.useCache})) { +// if (child.type === 'file') { +// this.allFiles.push(child); +// } else { +// await this.precacheDir(child as VirtualDirectory); +// } +// } +// } + +// async search(query: string) { +// await Promise.all(this.fs.root.map((root) => this.precacheDir(root))); +// } + +// get root() { +// return this.fs.root; +// } + +// async listDir(dir: VirtualDirectory) { +// return this.fs.listDir(dir); +// } + +// async download(file: VirtualFile) { +// return this.fs.download(file); +// } +// } \ No newline at end of file diff --git a/src/model/FileSystem/NginxFS.ts b/src/model/FileSystem/NginxFS.ts new file mode 100644 index 0000000..79837f9 --- /dev/null +++ b/src/model/FileSystem/NginxFS.ts @@ -0,0 +1,102 @@ +import { extensionMimeType } from "../FileMeta/ExtensionMimeType"; +import { FileSystemSource, VirtualFile, VirtualDirectory } from "./types"; + +export type NginxFsConfig = { + name: string; + rootUrl: string; + auth?: { + username: string; + password: string; + }; +} + +type DateString = string & {}; + +interface NginxFile { + name: string; + type: 'file' + mtime: DateString; + size: number; +} + +interface NginxDirectory { + name: string; + type: 'directory'; + mtime: DateString; +} + +export class NginxFS implements FileSystemSource { + constructor(private options: NginxFsConfig) {} + + get name() { + return this.options.name; + } + + get root():VirtualDirectory { + return { + type: 'directory', + source: this.options.name, + path: [], + name: this.options.name + } + } + + private get sanitizedRootUrl() { + return this.options.rootUrl.endsWith("/") ? this.options.rootUrl : this.options.rootUrl + "/"; + } + + private get authHeaders(): {Authorization: string}|{} { + return this.options.auth ? { + Authorization: `Basic ${btoa(`${this.options.auth.username}:${this.options.auth.password}`)}` + } : {} + } + + async listDir(dir: VirtualDirectory): Promise<(VirtualDirectory|VirtualFile)[]> { + + const response = await fetch(`${this.sanitizedRootUrl}${dir.path.map((x) => `${x}/`).join("")}`, { + headers: { + ...this.authHeaders, + } + }); + if (!response.ok) { + console.error("Failed to list directory", response); + return []; + } + + const data = await response.json() as Array; + return data.map((item) => { + if (item.type === "directory") { + return { + type: "directory", + source: this.options.name, + path: [...dir.path, item.name], + name: item.name + } + } else { + return { + type: "file", + source: this.options.name, + path: [...dir.path, item.name], + name: item.name, + mimeType: extensionMimeType(item.name), + } + } + }); + } + + async download(file: VirtualFile): Promise { + const response = await fetch(`${this.sanitizedRootUrl}${file.path.join("/")}`); + if (!response.ok) { + throw new Error("Failed to download file"); + } + return new File([await response.blob()], file.name, { type: file.mimeType }); + } + + getConfig() { + return { + ...this.options, + type: "nginx", + } as const; + } +} + diff --git a/src/model/FileSystem/types.ts b/src/model/FileSystem/types.ts new file mode 100644 index 0000000..9f97268 --- /dev/null +++ b/src/model/FileSystem/types.ts @@ -0,0 +1,27 @@ +import {CachePolicy} from './FsCache'; +export interface FileSystemSource { + name: string; + root: VirtualDirectory; + listDir(dir: VirtualDirectory, options?: ListDirOptions): Promise<(VirtualDirectory|VirtualFile)[]>; + download(file: VirtualFile): Promise; + getConfig(): TOptions & { type: string }; +} + +export interface ListDirOptions { + cache?: CachePolicy +} + +export interface VirtualDirectory { + type: 'directory'; + source: string; + path: string[]; + name: string; +} + +export interface VirtualFile { + type: 'file'; + source: string; + path: string[]; + name: string; + mimeType: string; +} diff --git a/src/model/Remote/Channels/ChromcastCaster.ts b/src/model/Remote/Channels/ChromcastCaster.ts new file mode 100644 index 0000000..744c7e4 --- /dev/null +++ b/src/model/Remote/Channels/ChromcastCaster.ts @@ -0,0 +1,111 @@ +import { loadScript } from "../../../helpers/loadScript"; +import { JsonChannelCaster, Message, MessageEmitter } from "./type"; + +const CHANNEL = 'urn:x-cast:eu.fisoft.battlecaster.custom'; + +export class ChromecastChannelCaster extends MessageEmitter implements JsonChannelCaster { + + private context?: cast.framework.CastContext; + private sessionStarted = false; + + constructor(private wsUuid?: string) { + super(); + } + + async initialize() { + window['__onGCastApiAvailable'] = (isAvailable) => { + console.log('__onGCastApiAvailable', isAvailable); + if (isAvailable) { + console.log('cast available'); + this.initializeCastApi(); + } + }; + if (typeof window.cast?.framework?.CastContext === 'undefined') { + loadScript('//www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1') + } + } + + initializeCastApi() { + this.context = cast.framework.CastContext.getInstance(); + this.context.setOptions({ + receiverApplicationId: import.meta.env.VITE_CHROMECAST_APP_ID, + androidReceiverCompatible: true, + autoJoinPolicy: chrome.cast.AutoJoinPolicy.CUSTOM_CONTROLLER_SCOPED + }); + this.context.addEventListener( + cast.framework.CastContextEventType.SESSION_STATE_CHANGED, + this.onSessionStateChange.bind(this) + ); + } + + + private onSessionStateChange(event: cast.framework.SessionStateEventData) { + console.log('onSessionStateChange', event); + if (event.sessionState === cast.framework.SessionState.SESSION_STARTED) { + this.sessionStarted = true; + console.log('session started'); + const session = this.context?.getCurrentSession(); + // @ts-ignore + // this.sendMessage({ type: 'INIT', data: 'Hello, receiver!' }); + session?.addMessageListener(CHANNEL, (namespace, message) => { + console.log('Message received', namespace, message); + const msg = JSON.parse(message); + console.log('Message received', session, namespace, msg); + this.emit(msg); + }); + this.send({ + type: 'hello', + name: session?.getCastDevice().friendlyName + }); + if (this.wsUuid) { + this.send({ + type: 'RtcSignalingConnect', + uuid: this.wsUuid + }); + } + + console.log('session', session); + + // const heartbeat = () => { + // const session = this.context?.getCurrentSession(); + // if (session) { + // setTimeout(heartbeat, 10000); + // session?.sendMessage(CHANNEL, { type: 'HEARTBEAT' }) + // .then(() => console.log('Heartbeat sent')) + // .catch(error => console.error('Error sending heartbeat:', error)); + // } + // }; + // heartbeat(); + } + } + + hail() { + const session = this.context?.getCurrentSession(); + this.send({ + type: 'hello', + name: session?.getCastDevice().friendlyName ?? 'Chromecast' + }); + if (this.wsUuid) { + this.send({ + type: 'RtcSignalingConnect', + uuid: this.wsUuid + }); + } + } + + send(data: Message): void { + if (!this.sessionStarted) { + console.error('No session started'); + return; + } + const session = this.context?.getCurrentSession(); + if (!session) { + console.error('No session'); + return; + } + + this.context?.getCurrentSession()?.sendMessage(CHANNEL, data) + .then(() => console.log('Message sent successfully')) + .catch(error => console.error('Error sending message:', error)); + } +} \ No newline at end of file diff --git a/src/model/Remote/Channels/ChromcastSummon.ts b/src/model/Remote/Channels/ChromcastSummon.ts new file mode 100644 index 0000000..633b7f9 --- /dev/null +++ b/src/model/Remote/Channels/ChromcastSummon.ts @@ -0,0 +1,63 @@ +import { loadScript } from "../../../helpers/loadScript"; +import { JsonChannelCaster, Message, MessageEmitter } from "./type"; +import type {CastReceiverContext, CastReceiverOptions} from '@types/chromecast-caf-receiver/cast.framework'; +import { WsSignalingSummon } from "./WsSignaling"; + +const CHANNEL = 'urn:x-cast:eu.fisoft.battlecaster.custom'; + +export class ChromecastChannelSummon extends MessageEmitter implements JsonChannelSummon { + + constructor() { + super(); + } + + log(...data: any[]) { + // @ts-ignore + this.emit({ type: 'log', data: data }); + } + + async initialize() { + this.log('loading cast_receiver_framework.js'); + await loadScript('//www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js') + this.initializeCastApi(); + } + + initializeCastApi() { + this.log('initializeCastApi'); + // @ts-ignore + const context: CastReceiverContext = cast.framework.CastReceiverContext.getInstance(); + // @ts-ignore + const options: CastReceiverOptions = new cast.framework.CastReceiverOptions(); + + context.addCustomMessageListener(CHANNEL, async (event: any) => { + try { + this.log('Message received', event.senderId, event.data); + const msg = event.data; + if (msg.type === 'hello') { + this.log('Chromecast summoned', event.senderId); + this.send({type: 'summoned', name: msg.name ?? 'Chromecast'}); + } + this.emit(msg); + } catch (err) { + this.log('error', err); + } + }); + options.customNamespaces = { + // @ts-ignore + [CHANNEL]: cast.framework.system.MessageType.JSON, + } + options.disableIdleTimeout = true; + options.maxInactivity = 3600; // Development only + context.start(options); + } + + send(data: Message): void { + try { + // @ts-ignore + const context: CastReceiverContext = cast.framework.CastReceiverContext.getInstance() + context.sendCustomMessage(CHANNEL, undefined, data); + } catch (err) { + this.log('send error', err); + } + } +} \ No newline at end of file diff --git a/src/model/Remote/Channels/PostMessage.ts b/src/model/Remote/Channels/PostMessage.ts new file mode 100644 index 0000000..eb2ef44 --- /dev/null +++ b/src/model/Remote/Channels/PostMessage.ts @@ -0,0 +1,25 @@ +import { JsonChannelCaster, JsonChannelSummon, Message, MessageEmitter } from "./type"; + + + +export class PostMessageChannel extends MessageEmitter implements JsonChannelCaster, JsonChannelSummon { + + constructor(private win: WindowProxy) { + super(); + win.addEventListener('message', this.onMessage); + } + + private onMessage = (event: MessageEvent) => { + const data = JSON.parse(event.data) as Message; + this.emit(data); + } + + send(data: Message) { + this.win.postMessage(JSON.stringify(data), '*'); + } + + disconnect() { + super.disconnect(); + this.win.removeEventListener('message', this.onMessage); + } +} \ No newline at end of file diff --git a/src/model/Remote/Channels/WebRTCChannel.ts b/src/model/Remote/Channels/WebRTCChannel.ts new file mode 100644 index 0000000..643352c --- /dev/null +++ b/src/model/Remote/Channels/WebRTCChannel.ts @@ -0,0 +1,172 @@ +import { VirtualFile } from "../../FileSystem/types"; +import { RemoteDeviceManagerCtx } from "../RemoteDeviceManager"; +import { JsonChannelCaster, JsonChannelSummon, Message, MessageEmitter } from "./type"; + + +export class WebRtcChannelCaster extends MessageEmitter implements JsonChannelCaster { + + private peer = new RTCPeerConnection() + private offerSent = false; + private dataChannel = this.peer.createDataChannel('json'); + public readonly sink: MediaStreamAudioDestinationNode; + + constructor( + private channel: JsonChannelCaster, + {onOpen}:Partial<{onOpen: Function}> = {}, + private ctx: RemoteDeviceManagerCtx + ) { + super(); + this.sink = ctx.audioContext.createMediaStreamDestination(); + this.channel.on(this.onJsonMessage); + this.dataChannel.addEventListener('close', () => { + this.emit({ type: 'disconnected' }); + }); + this.dataChannel.addEventListener('open', () => { + onOpen?.(); + this.emit({ type: 'connected' }); + }); + this.dataChannel.addEventListener('message', this.onMessage); + + this.peer.addTrack(this.sink.stream.getAudioTracks()[0], this.sink.stream); + this.peer.onicecandidate = (event) => { + if (event.candidate) { + this.channel.send({ + type: 'RtcIce', + candidate: event.candidate + }); + } + } + } + + private onMessage = (event: MessageEvent) => { + const data = JSON.parse(event.data) as Message; + this.emit(data); + } + + private onJsonMessage = (data: Message) => { + if (data.type === 'summoned') { + if (!this.offerSent) { + this.peer.createOffer().then(offer => { + this.peer.setLocalDescription(offer); + this.channel.send({ + type: 'RtcOffer', + offer + }); + }); + this.offerSent = true; + } + } else if (data.type === 'RtcAnswer') { + this.peer.setRemoteDescription(new RTCSessionDescription(data.answer)); + } else if (data.type === 'RtcIce') { + this.peer.addIceCandidate(new RTCIceCandidate(data.candidate)); + } + } + + async sendBinary(file: VirtualFile, data: Blob) { + const channelName = `file:${Math.random()}`; + this.dataChannel.send(JSON.stringify({ + type: 'fileTransfer', + channel: channelName, + size: data.size, + file, + })); + const channel = this.peer.createDataChannel(channelName); + channel.binaryType = 'arraybuffer'; + const buffer = await data.arrayBuffer(); + const chunkSize = 16384; + for (let i = 0; i < buffer.byteLength; i += chunkSize) { + const chunk = buffer.slice(i, i + chunkSize); + channel.send(chunk); + } + channel.close(); + await new Promise((resolve) => channel.addEventListener('close', resolve, { once: true })); + } + + send(data: Message) { + this.dataChannel.send(JSON.stringify(data)); + } + + disconnect() { + super.disconnect(); + } +} + + +export class WebRtcChannelSummon extends MessageEmitter implements JsonChannelSummon { + private peer = new RTCPeerConnection() + private dataChannel?: RTCDataChannel; + private transferedFilesMeta = new Map(); + + constructor(private channel: JsonChannelCaster) { + super(); + this.channel.on(this.onJsonMessage); + this.peer.onicecandidate = (event) => { + if (event.candidate) { + this.channel.send({ + type: 'RtcIce', + candidate: event.candidate + }); + } + } + this.peer.ontrack = (event) => { + console.log('ontrack', event); + const audioElement = document.createElement('audio'); + audioElement.srcObject = event.streams[0]; // Attach the received stream + audioElement.play(); + } + this.peer.ondatachannel = (event) => { + if (event.channel.label === 'json') { + this.dataChannel = event.channel; + this.dataChannel.addEventListener('message', this.onRtcMessage); + this.emit({ type: 'connected' }); + } else { + const channel = event.channel; + const chunks:ArrayBuffer[] = []; + channel.addEventListener('message', (event: MessageEvent) => { + console.log('>', channel.label, event.data); + chunks.push(event.data); + }); + channel.addEventListener('close', () => { + console.log('>', channel.label, 'closed'); + const meta = this.transferedFilesMeta.get(channel.label)!; + // TODO: handle data before meta? + const blob = new Blob(chunks, { type: meta?.mimeType ?? 'application/octet-stream' }); + console.log(meta, blob); + this.emit({ + type: 'fileTransfered', + meta, + blob, + }) + }); + } + } + } + + private onRtcMessage = (event: MessageEvent) => { + const msg = JSON.parse(event.data) as Message; + if (msg.type === 'fileTransfer') { + this.transferedFilesMeta.set(msg.channel, msg.file); + } else { + this.emit(msg); + } + }; + + private onJsonMessage = (data: Message) => { + if (data.type === 'RtcOffer') { + this.peer.setRemoteDescription(new RTCSessionDescription(data.offer)); + this.peer.createAnswer().then(answer => { + this.peer.setLocalDescription(answer); + this.channel.send({ + type: 'RtcAnswer', + answer + }); + }); + } else if (data.type === 'RtcIce') { + this.peer.addIceCandidate(new RTCIceCandidate(data.candidate)); + } + } + + send(data: Message) { + this.dataChannel?.send(JSON.stringify(data)); + } +} diff --git a/src/model/Remote/Channels/WsSignaling.ts b/src/model/Remote/Channels/WsSignaling.ts new file mode 100644 index 0000000..b2237d8 --- /dev/null +++ b/src/model/Remote/Channels/WsSignaling.ts @@ -0,0 +1,92 @@ +import { JsonChannelCaster, JsonChannelSummon, Message, MessageEmitter } from "./type"; + +const BASE_DOMAIN = 'wss://webrtc.worlddominationproject.dev' + +export class WsSignalingSummon extends MessageEmitter implements JsonChannelCaster, JsonChannelSummon { + + private ws?: WebSocket; + + constructor(private uuid?: string) { + super(); + } + + async connect(uuid?: string) { + this.uuid = uuid ?? this.uuid; + return new Promise((resolve, reject) => { + if (!this.uuid) { + reject(new Error('No uuid provided')); + } + this.ws = new WebSocket(`${BASE_DOMAIN}/${this.uuid}`); + this.ws.onmessage = (event: MessageEvent) => { + const msg = JSON.parse(event.data); + this.emit(msg); + } + + this.ws.onerror = (err) => { + reject(err); + } + + this.ws.onopen = () => { + resolve(); + } + }) + } + + disconnect(): void { + try { + this.ws?.close(); + } catch (err) { + console.log('Error closing ws', err); + } + } + + send(data: Message): void { + this.ws?.send(JSON.stringify(data)); + } +} + + + +export class WsSignalingCaster extends MessageEmitter implements JsonChannelCaster { + + private ws?: WebSocket; + constructor() { + super(); + } + + async initialize() { + return new Promise((resolve, reject) => { + this.ws = new WebSocket(BASE_DOMAIN); + this.ws.onmessage = (event: MessageEvent) => { + try { + const msg = JSON.parse(event.data); + if (msg.type === 'webRtcSignalingId') { + resolve(msg.uuid); + } else { + this.emit(msg); + } + } catch (e) { + console.error('Error parsing message', event.data); + } + }; + + this.ws.onclose = (ev) => { + console.trace('Ws closed', ev); + } + + this.ws.onerror = (err) => { + console.log('Error connecting to ws', err); + reject(err); + } + }) + } + + disconnect(): void { + console.trace('Closing ws'); + this.ws?.close(); + } + + send(data: Message): void { + this.ws?.send(JSON.stringify(data)); + } +} \ No newline at end of file diff --git a/src/model/Remote/Channels/type.ts b/src/model/Remote/Channels/type.ts new file mode 100644 index 0000000..ec4313b --- /dev/null +++ b/src/model/Remote/Channels/type.ts @@ -0,0 +1,131 @@ +import { VirtualFile } from "../../FileSystem/types"; + +export type PingMessage = { + type: 'ping'; + ts: number; +}; +export type PongMessage = { + type: 'pong'; +}; +export type SummonedMeessage = { + type: 'summoned'; + name: string; +}; + +export type ResolutionMessage = { + type: 'resolution'; + width: number; + height: number; +}; + +export type WebRTCSignalingConnect = { + type: 'RtcSignalingConnect'; + uuid: string; +} + +export type WebRTCOfferMessage = { + type: 'RtcOffer'; + offer: RTCSessionDescriptionInit; +}; + +export type WebRTCAnswerMessage = { + type: 'RtcAnswer'; + answer: RTCSessionDescriptionInit; +}; + +export type RtcIceMessage = { + type: 'RtcIce'; + candidate: RTCIceCandidate; +} + +export type FileTransferMessage = { + type: 'fileTransfer'; + channel: string; + size: number; + file: VirtualFile; +} + +export type FileTransferedMessage = { + type: 'fileTransfered'; + meta: VirtualFile; + blob: Blob; +} + +export type FileTransferProgressMessage = { + type: 'transferProgress'; + percent: number; +} + +export type ShowFileMessage = { + type: 'showFile'; + file: VirtualFile; +} + +export type DisconnectedMessage = { + type: 'disconnected'; +} + +export type ConnectedMessage = { + type: 'connected'; +} + +export type HelloMessage = { + type: 'hello'; + name?: string; +} + +export type TransformMessage = { + type: 'transform'; + x: number; + y: number; + scale: number; +} + +export type Message = PingMessage | + PongMessage | + SummonedMeessage | + ResolutionMessage | + WebRTCOfferMessage | + WebRTCAnswerMessage | + RtcIceMessage | + FileTransferMessage | + FileTransferedMessage | + FileTransferProgressMessage | + ShowFileMessage | + DisconnectedMessage | + ConnectedMessage | + HelloMessage | + WebRTCSignalingConnect; + +export type CasterMessage = PingMessage; +export type SummonMessage = PongMessage | SummonedMeessage | ResolutionMessage; + + +export interface JsonChannelCaster { + send(data: Message): void; + on(callback: (data: Message) => void): void; + disconnect(): void; +} + +export interface JsonChannelSummon { + send(data: Message): void; + on(callback: (data: Message) => void): void; +} + +export class MessageEmitter { + private listeners: ((data: Message) => void)[] = []; + + on(callback: (data: Message) => void) { + this.listeners.push(callback); + } + + protected emit(data: Message) { + for (const listener of this.listeners) { + listener(data); + } + } + + disconnect() { + this.listeners = []; + } +} \ No newline at end of file diff --git a/src/model/Remote/ChromecastDevice.ts b/src/model/Remote/ChromecastDevice.ts new file mode 100644 index 0000000..359b3bd --- /dev/null +++ b/src/model/Remote/ChromecastDevice.ts @@ -0,0 +1,97 @@ +import { VirtualFile } from "../FileSystem/types"; +import { PostMessageChannel } from "./Channels/PostMessage"; +import { WebRtcChannelCaster } from "./Channels/WebRTCChannel"; +import { WsSignalingCaster } from "./Channels/WsSignaling"; +import { RemoteDeviceManagerCtx } from "./RemoteDeviceManager"; +import { ImageSink, RemoteDevice } from "./types"; + +export class ChromecastDevice { + + public readonly type = 'chromecast'; + webRtcChannel?: WebRtcChannelCaster; + public name: string = 'Chromecast'; + public readonly hasImageSink = true; + public readonly imageSink? = new ChromecastImageSink(this); + public readonly audioSink: GainNode; + + constructor(private ctx: RemoteDeviceManagerCtx) { + this.audioSink = ctx.audioContext.createGain(); + } + + setup() { + this.imageSink?.setup(); + } + + get isConnected(): boolean { + return this.webRtcChannel !== null; + } + + async connect(): Promise { + return new Promise(async (resolve) => { + const channel = new WsSignalingCaster(); + const uuid = await channel.initialize(); + const win = window.open(`/chromecast/?ws=${uuid}`, '', 'width=200,height=200')!; + // const channel = new PostMessageChannel(win); + channel.on((msg) => { + console.log('CHROMECAST MESSAGE', msg) + // @ts-ignore + if (msg.type === 'webRtcSignalingNewClient') { + this.webRtcChannel = new WebRtcChannelCaster(channel, { + onOpen: () => { + win.close(); + resolve(this); + } + }, this.ctx); + this.audioSink.connect(this.webRtcChannel.sink); + } + if (msg.type == 'summoned') { + this.name = msg.name; + } + }); + + win.onbeforeunload = () => { + console.log('closed'); + channel.disconnect(); + } + + + }); + } +} + +class ChromecastImageSink implements ImageSink { + resolution: { width: number; height: number; } = {width: NaN, height: NaN}; + + constructor(private device: ChromecastDevice) { + + } + + setup() { + this.device.webRtcChannel?.on((msg)=> { + if (msg.type === 'resolution') { + this.resolution = msg; + } + console.log(msg); + }) + } + + get name(): string { + return this.device.name; + } + + async transferFile(meta: VirtualFile, data: Blob) { + await this.device.webRtcChannel?.sendBinary(meta, data); + } + + async show(file: VirtualFile) { + await this.device.webRtcChannel?.send({type: 'showFile', file}); + console.log('show', file); + } + + async transform({x, y, scale}: {x: number, y: number, scale: number}) { + await this.device.webRtcChannel?.send({ + type: 'transform', + x, y, scale + }); + } +} \ No newline at end of file diff --git a/src/model/Remote/FullscreenWindow.ts b/src/model/Remote/FullscreenWindow.ts new file mode 100644 index 0000000..f05bcd8 --- /dev/null +++ b/src/model/Remote/FullscreenWindow.ts @@ -0,0 +1,135 @@ +import { VirtualFile } from "../FileSystem/types"; +import { PostMessageChannel } from "./Channels/PostMessage"; +import { JsonChannelCaster } from "./Channels/type"; +import { WebRtcChannelCaster } from "./Channels/WebRTCChannel"; +import { RemoteDeviceManagerCtx } from "./RemoteDeviceManager"; +import { ImageSink, RemoteDevice, RemoteDeviceOptions } from "./types"; + +export interface FullscreenWindowConfig extends RemoteDeviceOptions { + type: 'fullscreen-window'; + screen: number; +} + +export class FullscreenWindowDevice implements RemoteDevice { + + public readonly type = 'fullscreen-window'; + private target: WindowProxy|null = null; + public jsonChannel?: JsonChannelCaster; + public webRtcChannel?: WebRtcChannelCaster; + public imageSink?: FullscreenWindowImageSink; + public readonly hasImageSink = true; + public readonly hasAudioSink = false; + public readonly audioSink: GainNode; + + constructor(private options: FullscreenWindowConfig, private ctx: RemoteDeviceManagerCtx) { + this.audioSink = ctx.audioContext.createGain(); + window.addEventListener('unload', () => { + this.disconnect(); + }); + } + + async connect() { + // @ts-ignore + const screenDetails = await window.getScreenDetails() + const {availHeight, availWidth, left, top} = screenDetails.screens[this.options.screen]; + this.target = window.open( + `/summon/?channel=postMessage&name=${this.options.name}`, + '', + `left=${left},top=${top},width=${availWidth},height=${availHeight},fullscreen=yes` + ); + this.connectJsonChannel(); + this.imageSink = new FullscreenWindowImageSink(this); + this.imageSink.setup(); + } + + private connectJsonChannel() { + if (this.webRtcChannel) { + this.webRtcChannel.disconnect(); + this.webRtcChannel = undefined; + } + if (this.jsonChannel) { + this.jsonChannel.disconnect(); + this.jsonChannel = undefined; + } + if (this.target) { + this.jsonChannel = new PostMessageChannel(this.target); + this.webRtcChannel = new WebRtcChannelCaster(this.jsonChannel, {}, this.ctx); + this.audioSink.connect(this.webRtcChannel.sink); + this.webRtcChannel.on((message) => { + if (message.type === 'disconnected') { + this.disconnect(); + } + }); + this.jsonChannel.on(console.log); + } + } + + async disconnect(): Promise { + if (this.target) { + this.target.close(); + this.target = null; + this.imageSink = undefined; + this.connectJsonChannel(); + } + } + + static async getDevices(): Promise { + // @ts-ignore + const screenDetails = await window.getScreenDetails() + return screenDetails.screens.map((screen: any, index: number) => ({ + name: `Screen ${index + 1}`, + type: "fullscreen-window", + screen: index + })); + } + + get isConnected(): boolean { + return this.target !== null; + } + + get name(): string { + return this.options.name; + } + + get config(): FullscreenWindowConfig { + return this.options; + } +} + +class FullscreenWindowImageSink implements ImageSink { + + public resolution = {width: NaN, height: NaN}; + + constructor(private device: FullscreenWindowDevice) {} + + setup() { + this.device.webRtcChannel?.on((msg) => { + if(msg.type === 'resolution') { + this.resolution = { + width: msg.width, + height: msg.height + } + } + }); + } + + get name(): string { + return this.device.name; + } + + async transferFile(meta: VirtualFile, data: Blob) { + await this.device.webRtcChannel?.sendBinary(meta, data); + } + + async show(file: VirtualFile) { + await this.device.webRtcChannel?.send({type: 'showFile', file}); + console.log('show', file); + } + + async transform({x, y, scale}: {x: number, y: number, scale: number}) { + await this.device.webRtcChannel?.send({ + type: 'transform', + x, y, scale + }); + } +} diff --git a/src/model/Remote/LocalAudioDevice.ts b/src/model/Remote/LocalAudioDevice.ts new file mode 100644 index 0000000..c83b504 --- /dev/null +++ b/src/model/Remote/LocalAudioDevice.ts @@ -0,0 +1,9 @@ +export class LocalAudioDevice { + audioSink: GainNode; + public readonly name = 'Local Audio'; + public readonly type = 'local'; + constructor(audioContext: AudioContext) { + this.audioSink = audioContext.createGain(); + this.audioSink.connect(audioContext.destination); + } +} \ No newline at end of file diff --git a/src/model/Remote/RemoteDeviceManager.ts b/src/model/Remote/RemoteDeviceManager.ts new file mode 100644 index 0000000..7013e20 --- /dev/null +++ b/src/model/Remote/RemoteDeviceManager.ts @@ -0,0 +1,105 @@ +import { ChromecastDevice } from "./ChromecastDevice"; +import { FullscreenWindowConfig, FullscreenWindowDevice } from "./FullscreenWindow"; +import { LocalAudioDevice } from "./LocalAudioDevice"; + +export type DeviceConfig = FullscreenWindowConfig; +export type Devices = FullscreenWindowDevice | ChromecastDevice; + +export interface RemoteDeviceManagerOptions { + devices: (DeviceConfig)[]; +} + +export interface RemoteDeviceManagerCtx { + audioContext: AudioContext; +} + + +export class RemoteDeviceManager { + + private _devices:Devices[] = []; + private localAudioDevice: LocalAudioDevice; + + constructor(private options: RemoteDeviceManagerOptions, private ctx: RemoteDeviceManagerCtx) { + for (const device of options.devices) { + this.createInstance(device); + } + + this.localAudioDevice = new LocalAudioDevice(ctx.audioContext); + } + + get devices(): Devices[] { + return this._devices; + } + + get audioDevices() { + return [ + ...this._devices.filter(d => d.audioSink), + this.localAudioDevice, + ]; + } + + private async createInstance(device: DeviceConfig): Promise { + if (device.type === 'fullscreen-window') { + const deviceInstance = new FullscreenWindowDevice(device, this.ctx); + this._devices.push(deviceInstance); + return deviceInstance; + } + } + + async add(device: DeviceConfig, connect: boolean = false): Promise { + console.log('add', device); + const instance = await this.createInstance(device); + if (!instance) { + return; + } + this.persist(); + if (connect) { + await instance.connect(); + } + } + + removeDevice(device: Devices) { + this._devices = this._devices.filter(d => d !== device); + this.persist() + } + + async availableDevices(): Promise { + return [ + ...(await FullscreenWindowDevice.getDevices()), + ]; + } + + async connectChromeCast() { + new ChromecastDevice(this.ctx) + .connect() + .then((dev) => { + this._devices.push(dev); + (this._devices.at(-1) as ChromecastDevice).setup(); + }) + } + + get config(): RemoteDeviceManagerOptions { + return { + ...this.options, + devices: this._devices + .map(d => { + // @ts-ignore + return d.config; + }) + .filter(d => d !== undefined) as DeviceConfig[] + }; + } + + persist() { + console.log('persist'); + localStorage.setItem('remote-devices', JSON.stringify(this.config)); + } + + static restore(ctx: RemoteDeviceManagerCtx): RemoteDeviceManager { + const data = localStorage.getItem('remote-devices'); + if (data) { + return new RemoteDeviceManager(JSON.parse(data), ctx); + } + return new RemoteDeviceManager({devices: []}, ctx); + } +} \ No newline at end of file diff --git a/src/model/Remote/types.ts b/src/model/Remote/types.ts new file mode 100644 index 0000000..aa79eb0 --- /dev/null +++ b/src/model/Remote/types.ts @@ -0,0 +1,32 @@ +import { VirtualFile } from "../FileSystem/types"; + +export interface RemoteDeviceOptions { + name: string; + type: string; +} + +export interface RemoteDevice { + name: string; + // audioSink?: AudioSink; + imageSink?: ImageSink; + audioSink: GainNode; + + isConnected: boolean; + connect(): Promise; + disconnect(): Promise; +} + + +export interface RemoteSink { + name: string; + transferFile(meta: VirtualFile, blob: Blob): void; +} + +export interface ImageSink extends RemoteSink { + resolution: { width: number, height: number }; + show(file: VirtualFile): void; +} + +// export interface AudioSink extends RemoteSink { +// play(file: VirtualFile): void; +// } \ No newline at end of file diff --git a/src/model/SoundPlayer.ts b/src/model/SoundPlayer.ts new file mode 100644 index 0000000..040f986 --- /dev/null +++ b/src/model/SoundPlayer.ts @@ -0,0 +1,60 @@ + + +export class SoundPlayer { + public sounds = ['sword slash', 'knock - wood']; + private ctx = new AudioContext(); + private cache = new Map(); + private cacheOptions = new Map(); + + constructor() { + + } + + async play(sound: string) { + if (this.sounds.includes(sound)) { + const options = await this.fetchSoundOptions(sound); + const randomOption = options[Math.floor(Math.random() * options.length)]; + const soundBuffer = await this.fetchSound(sound, randomOption); + this.playBuffer(soundBuffer); + console.log(`Playing sound: ${sound}`); + } else { + console.log(`Sound not found: ${sound}`); + } + } + + async fetchSoundOptions(sound: string) { + const url = `/soundfx/${sound}/`; + const dir = fetch(url) + .then((res) => res.json()) + .then((dir) => { + this.cacheOptions.set(url, dir.map(({name}: any) => name) as string[]); + }); + if (!this.cacheOptions.has(url)) { + await dir; + } + return this.cacheOptions.get(url)!; + } + + async fetchSound(sound: string, options: string) { + const url = `/soundfx/${sound}/${options}`; + if (this.cache.has(url)) { + return this.cache.get(url)!; + } + console.log(`Fetching sound: ${sound}/${options}`); + const arrayBuffer = await (await fetch(url)).arrayBuffer(); + const audioBuffer = await this.ctx.decodeAudioData(arrayBuffer); + this.cache.set(url, audioBuffer); + return audioBuffer; + } + + async playBuffer(buffer: AudioBuffer) { + const source = this.ctx.createBufferSource(); + const panner = this.ctx.createStereoPanner(); + // panner.pan.value = Math.random() > 0.5 ? 1 : -1; + source.buffer = buffer; + source.connect(panner); + panner.connect(this.ctx.destination); + source.start(); + } +} + diff --git a/src/model/injectionSymbols.ts b/src/model/injectionSymbols.ts index f697898..3e37960 100644 --- a/src/model/injectionSymbols.ts +++ b/src/model/injectionSymbols.ts @@ -1,9 +1,18 @@ -import { InjectionKey } from "vue"; +import { InjectionKey, Reactive } from "vue"; import type { FileManager } from "./FileManager"; import { Receiver, Sender } from "./CommunicationChannel"; +import type { MountedFs } from "./FileSystem/MountedFs"; +import type { RemoteDeviceManager } from "./Remote/RemoteDeviceManager"; +import { JsonChannelSummon } from "./Remote/Channels/type"; +import { WebRtcChannelSummon } from "./Remote/Channels/WebRTCChannel"; export const FileManagerSymbol = Symbol("FileManager") as InjectionKey; +export const RemoteDeviceManagerSymbol = Symbol("RemoteDeviceManager") as InjectionKey>; +export const MountedFsSymbol = Symbol("MountedFs") as InjectionKey; +export const AudioContextSymbol = Symbol("AudioContext") as InjectionKey; export const CastTypeSymbol = Symbol("CastType") as InjectionKey<"sender" | "receiver">; export const SenderSymbol = Symbol('Sender') as InjectionKey; -export const ReceiverSymbol = Symbol('Receiver') as InjectionKey; \ No newline at end of file +export const ReceiverSymbol = Symbol('Receiver') as InjectionKey; +export const JsonChannelSymbol = Symbol('JsonChannel') as InjectionKey; +export const WebRtcChannelSymbol = Symbol('WebRtcChannelSymbol') as InjectionKey; \ No newline at end of file diff --git a/src/modules/controls/ControlApp.vue b/src/modules/controls/ControlApp.vue index 8069f6f..789b337 100644 --- a/src/modules/controls/ControlApp.vue +++ b/src/modules/controls/ControlApp.vue @@ -1,26 +1,46 @@ + \ No newline at end of file diff --git a/src/modules/summon/SummonApp.vue b/src/modules/summon/SummonApp.vue new file mode 100644 index 0000000..18e9ed3 --- /dev/null +++ b/src/modules/summon/SummonApp.vue @@ -0,0 +1,68 @@ + + \ No newline at end of file diff --git a/src/summon.ts b/src/summon.ts new file mode 100644 index 0000000..e2bc64c --- /dev/null +++ b/src/summon.ts @@ -0,0 +1,58 @@ +import { createApp } from 'vue'; +import './style.css'; +import { JsonChannelSymbol, WebRtcChannelSymbol } from './model/injectionSymbols'; +import { PostMessageChannel } from './model/Remote/Channels/PostMessage'; +import SummonApp from './modules/summon/SummonApp.vue'; +import { immediate } from './helpers/immediate'; +import { WebRtcChannelSummon } from './model/Remote/Channels/WebRTCChannel'; +import { loadScript } from './helpers/loadScript'; +import { ChromecastChannelSummon } from './model/Remote/Channels/ChromcastSummon'; +import { WsSignalingSummon } from './model/Remote/Channels/WsSignaling'; + +const app = createApp(SummonApp) + +const query = new URLSearchParams(location.search); +let name = query.get('name') ?? 'Unknown'; +app.provide('name', name); + +const channel = (() => { + if (query.get('channel') === 'postMessage') { + const channel = new PostMessageChannel(window) + channel.send({type: 'summoned', name}); + return channel; + } else if (query.get('channel') === 'chromecast') { + const chromeCastChannel = new ChromecastChannelSummon(); + const channel = new WsSignalingSummon(); + + chromeCastChannel.initialize(); + chromeCastChannel.on(async (msg) => { + if (msg.type === 'hello') { + name = msg.name ?? 'Chromecast'; + } + if (msg.type === 'RtcSignalingConnect') { + channel.disconnect(); + await channel.connect(msg.uuid); + channel.send({type: 'summoned', name}); + } + }); + return channel; + } +})(); + +if (channel) { + const webRtcChannel = new WebRtcChannelSummon(channel); + app.provide(WebRtcChannelSymbol, webRtcChannel); + app.provide(JsonChannelSymbol, channel); + + webRtcChannel.on((msg) => { + if (msg.type === 'connected') { + webRtcChannel.send({ + type: 'resolution', + width: window.innerWidth * window.devicePixelRatio, + height: window.innerHeight * window.devicePixelRatio, + }); + } + }) +} + +app.mount('#app') diff --git a/src/utils/debounce.ts b/src/utils/debounce.ts new file mode 100644 index 0000000..1407828 --- /dev/null +++ b/src/utils/debounce.ts @@ -0,0 +1,16 @@ +export function debounce void>(func: T, wait: number, immediate: boolean = false): T { + let timeout: number | NodeJS.Timeout | undefined; + return function (this: any, ...args) { + var context = this; + var later = function () { + timeout = undefined; + if (!immediate) + func.apply(context, args); + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) + func.apply(context, args); + } as T; +} \ No newline at end of file diff --git a/src/utils/groupBy.ts b/src/utils/groupBy.ts new file mode 100644 index 0000000..466ec28 --- /dev/null +++ b/src/utils/groupBy.ts @@ -0,0 +1,11 @@ +export function groupBy(arr: T[], key: (item: T) => TK): Map { + const map = new Map(); + for (const item of arr) { + const group = key(item); + if (!map.has(group)) { + map.set(group, []); + } + map.get(group)!.push(item); + } + return map; +} \ No newline at end of file diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/summon/index.html b/summon/index.html new file mode 100644 index 0000000..297ae55 --- /dev/null +++ b/summon/index.html @@ -0,0 +1,13 @@ + + + + + + + Summon | BattleCaster + + +

+ + + diff --git a/vite.config.ts b/vite.config.ts index 103285b..ebe425a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { resolve } from 'path' + // https://vitejs.dev/config/ export default defineConfig({ build: { @@ -9,9 +10,14 @@ export default defineConfig({ input: { main: resolve(__dirname, 'index.html'), map: resolve(__dirname, 'map/index.html'), + summon: resolve(__dirname, 'summon/index.html'), + chromecast: resolve(__dirname, 'chromecast/index.html'), }, }, }, + define: { + 'import.meta.env.CHROMECAST_APP_ID': JSON.stringify(process.env.VITE_CHROMECAST_APP_ID) + }, plugins: [ vue({ template: { @@ -23,8 +29,10 @@ export default defineConfig({ ], server: { host: '0.0.0.0', + cors: true, proxy: { - '/maps': 'http://localhost:8080' + '/maps': 'http://localhost:8080', + '/soundfx': 'http://localhost:8080' } } })