fixes and cleanup
This commit is contained in:
13
package-lock.json
generated
13
package-lock.json
generated
@@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/chromecast-caf-receiver": "^6.0.16",
|
"@types/chromecast-caf-receiver": "^6.0.16",
|
||||||
"@types/chromecast-caf-sender": "^1.0.10",
|
"@types/chromecast-caf-sender": "^1.0.10",
|
||||||
|
"uuid": "^11.0.3",
|
||||||
"vite-plugin-list-directory-contents": "^1.4.5",
|
"vite-plugin-list-directory-contents": "^1.4.5",
|
||||||
"vue": "^3.4.29"
|
"vue": "^3.4.29"
|
||||||
},
|
},
|
||||||
@@ -2526,6 +2527,18 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "11.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz",
|
||||||
|
"integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/esm/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/validator": {
|
"node_modules/validator": {
|
||||||
"version": "13.12.0",
|
"version": "13.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/chromecast-caf-receiver": "^6.0.16",
|
"@types/chromecast-caf-receiver": "^6.0.16",
|
||||||
"@types/chromecast-caf-sender": "^1.0.10",
|
"@types/chromecast-caf-sender": "^1.0.10",
|
||||||
|
"uuid": "^11.0.3",
|
||||||
"vite-plugin-list-directory-contents": "^1.4.5",
|
"vite-plugin-list-directory-contents": "^1.4.5",
|
||||||
"vue": "^3.4.29"
|
"vue": "^3.4.29"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,13 +8,9 @@ const chromecast = new ChromecastChannelCaster(searchParams.get('ws') ?? undefin
|
|||||||
const postMessage = new PostMessageChannel(window);
|
const postMessage = new PostMessageChannel(window);
|
||||||
chromecast.initialize();
|
chromecast.initialize();
|
||||||
chromecast.on((msg) => {
|
chromecast.on((msg) => {
|
||||||
console.log('CHROMECAST MESSAGE', msg);
|
|
||||||
console.log(msg);
|
|
||||||
postMessage.send(msg);
|
postMessage.send(msg);
|
||||||
})
|
})
|
||||||
postMessage.on((msg) => {
|
postMessage.on((msg) => {
|
||||||
console.log('POST MESSAGE', msg);
|
|
||||||
console.log(msg);
|
|
||||||
chromecast.send(msg);
|
chromecast.send(msg);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
@dragover.prevent="onDragover"
|
@dragover.prevent="onDragover"
|
||||||
>
|
>
|
||||||
<div class="a-card__label-wrapper">
|
<div class="a-card__label-wrapper">
|
||||||
<div class="a-card__label">{{ label }}</div>
|
<input class="a-card__label" v-model="editableLabel" />
|
||||||
<div class="a-card__delete" @click="emits('delete')">×</div>
|
<div class="a-card__delete" @click="emits('delete')">×</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="a-card__volume">
|
<div class="a-card__volume">
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, UnwrapNestedRefs} from 'vue';
|
import {computed, ref, UnwrapNestedRefs} from 'vue';
|
||||||
import {SoundGroupNode} from '../../model/Audio/SoundGroupNode';
|
import {SoundGroupNode} from '../../model/Audio/SoundGroupNode';
|
||||||
|
|
||||||
import Play from '../Icons/Play.vue';
|
import Play from '../Icons/Play.vue';
|
||||||
@@ -82,6 +82,11 @@ const props = withDefaults(defineProps<{
|
|||||||
noDetail: false
|
noDetail: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const editableLabel = computed({
|
||||||
|
get: () => props.node.label,
|
||||||
|
set: (value: string) => props.node.label = value
|
||||||
|
});
|
||||||
|
|
||||||
const popover = ref<HTMLElement>();
|
const popover = ref<HTMLElement>();
|
||||||
function openPopover() {
|
function openPopover() {
|
||||||
if (props.noDetail) {
|
if (props.noDetail) {
|
||||||
@@ -92,17 +97,14 @@ function openPopover() {
|
|||||||
|
|
||||||
function onDragover(event: DragEvent) {
|
function onDragover(event: DragEvent) {
|
||||||
const data = event.dataTransfer?.getData('application/json');
|
const data = event.dataTransfer?.getData('application/json');
|
||||||
console.log('dragover', event, '>', data, '<', typeof data);
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDrop(event: DragEvent) {
|
function onDrop(event: DragEvent) {
|
||||||
console.log('drop', event, event.dataTransfer?.getData('application/json'));
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (event.dataTransfer?.getData('application/json')) {
|
if (event.dataTransfer?.getData('application/json')) {
|
||||||
const meta = JSON.parse(event.dataTransfer?.getData('application/json'));
|
const meta = JSON.parse(event.dataTransfer?.getData('application/json'));
|
||||||
if (meta.type === 'file') {
|
if (meta.type === 'file') {
|
||||||
console.log('got file', meta);
|
|
||||||
props.node.addSound(meta);
|
props.node.addSound(meta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
149
src/components/Audio/Parallel.vue
Normal file
149
src/components/Audio/Parallel.vue
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
<template>
|
||||||
|
<AudioCard
|
||||||
|
:node="node"
|
||||||
|
:supports="[ 'togglePlay' ]"
|
||||||
|
@delete="$emit('delete')"
|
||||||
|
label="Parallel"
|
||||||
|
style="--box-color: var(--color-emerald)"
|
||||||
|
>
|
||||||
|
<template #preview>
|
||||||
|
<!-- <div>{{ node.currentTrack?.name ?? 'No track selected' }}</div> -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #popover>
|
||||||
|
<!--<div>
|
||||||
|
{{ node.currentTrack?.name ?? 'No track selected' }}
|
||||||
|
<div>
|
||||||
|
<input type="range" min="0" max="100" style="width: 100%" />
|
||||||
|
</div>
|
||||||
|
</div>-->
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="a-parallel__song"
|
||||||
|
:class="{
|
||||||
|
'a-parallel__song--playing': song.isPlaying
|
||||||
|
}"
|
||||||
|
v-for="(song, idx) in node.sounds()"
|
||||||
|
:key="song.options.vFile.name"
|
||||||
|
@click="playTrack(song)"
|
||||||
|
>
|
||||||
|
<span class="a-parallel__song-play-indicator">
|
||||||
|
<Play style="height: 1rem" />
|
||||||
|
</span>
|
||||||
|
<span class="a-parallel__song-name">
|
||||||
|
{{ song.options.vFile.name }}
|
||||||
|
</span>
|
||||||
|
<span @click.prevent="removeSong(song)">
|
||||||
|
×
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</AudioCard>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import AudioCard from './AudioCard.vue';
|
||||||
|
import {ParallelGroupNode} from '../../model/Audio/ParallelGroupNode';
|
||||||
|
import Play from '../Icons/Play.vue';
|
||||||
|
import {MediaElementSoundNode} from '../../model/Audio/SoundNode';
|
||||||
|
import {UnwrapNestedRefs} from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
node: UnwrapNestedRefs<ParallelGroupNode>
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'delete': [],
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// playlist.connect(audioContext.destination);
|
||||||
|
|
||||||
|
function playTrack(song: MediaElementSoundNode) {
|
||||||
|
props.node.toggleTrack(song);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSong(song: MediaElementSoundNode) {
|
||||||
|
props.node.removeSoundNode(song);
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
|
||||||
|
@layer component {
|
||||||
|
.a-parallel {
|
||||||
|
--box-color: var(--audio-box-color-parallel, #10b981);
|
||||||
|
--box-color-900: color-mix(in lab, var(--box-color), #000 80%);
|
||||||
|
--box-color-100: color-mix(in lab, var(--box-color), #fff 80%);
|
||||||
|
--box-color-300: color-mix(in lab, var(--box-color), #fff 40%);
|
||||||
|
width: 160px;
|
||||||
|
aspect-ratio: 4/5;
|
||||||
|
border: 2px solid rgba(from var(--box-color) r g b / 0.2);
|
||||||
|
padding: 4px;
|
||||||
|
background: var(--box-color-900);
|
||||||
|
color: var(--box-color-100);
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-parallel input {
|
||||||
|
accent-color: var(--box-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-parallel__top, .a-parallel__bottom {
|
||||||
|
height: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-parallel__content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;;
|
||||||
|
border-radius: 4px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-parallel__popover {
|
||||||
|
height: calc(100% - 10rem);
|
||||||
|
width: calc(100% - 10rem);
|
||||||
|
background: var(--box-color-900);
|
||||||
|
color: var(--box-color-100);
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 2px solid var(--box-color-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-parallel__song {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-bottom: 1px solid var(--box-color-800);
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-parallel__song-name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-parallel__song--playing {
|
||||||
|
background-color: var(--box-color-800);
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-parallel__song:first-child {
|
||||||
|
border-top: 1px solid var(--box-color-800);
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-parallel__song-play-indicator {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-parallel__song:hover .a-parallel__song-play-indicator,
|
||||||
|
.a-parallel__song--playing .a-parallel__song-play-indicator {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -20,6 +20,11 @@
|
|||||||
:node="node"
|
:node="node"
|
||||||
@delete="deleteNode(node)"
|
@delete="deleteNode(node)"
|
||||||
/>
|
/>
|
||||||
|
<Parallel
|
||||||
|
v-else-if="node.type === 'parallel'"
|
||||||
|
:node="node"
|
||||||
|
@delete="deleteNode(node)"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="a-card add-card" style="--box-color: var(--color-zinc)">
|
<div class="a-card add-card" style="--box-color: var(--color-zinc)">
|
||||||
@@ -32,6 +37,9 @@
|
|||||||
<div @click="addNode('sfx')" class="soundboard-add-card__btn">
|
<div @click="addNode('sfx')" class="soundboard-add-card__btn">
|
||||||
Sfx
|
Sfx
|
||||||
</div>
|
</div>
|
||||||
|
<div @click="addNode('parallel')" class="soundboard-add-card__btn">
|
||||||
|
Parallel
|
||||||
|
</div>
|
||||||
<div @click="addNode('tab')" class="soundboard-add-card__btn">
|
<div @click="addNode('tab')" class="soundboard-add-card__btn">
|
||||||
Tab Source
|
Tab Source
|
||||||
</div>
|
</div>
|
||||||
@@ -50,6 +58,8 @@ import TabSource from './TabSource.vue';
|
|||||||
import DeviceVolume from './DeviceVolume.vue';
|
import DeviceVolume from './DeviceVolume.vue';
|
||||||
import {AudioNodeManager} from '../../model/Audio/AudioNodesManager';
|
import {AudioNodeManager} from '../../model/Audio/AudioNodesManager';
|
||||||
import {debounce} from '../../utils/debounce';
|
import {debounce} from '../../utils/debounce';
|
||||||
|
import {ParallelGroupNode} from '../../model/Audio/ParallelGroupNode';
|
||||||
|
import Parallel from './Parallel.vue';
|
||||||
|
|
||||||
const fs = inject(MountedFsSymbol)!;
|
const fs = inject(MountedFsSymbol)!;
|
||||||
const audioContext = inject(AudioContextSymbol) as AudioContext;
|
const audioContext = inject(AudioContextSymbol) as AudioContext;
|
||||||
@@ -61,29 +71,43 @@ const nodes = reactive(audioNodesManager.restore({fs, audioContext}));
|
|||||||
function addNode(name: string) {
|
function addNode(name: string) {
|
||||||
if (name === 'playlist') {
|
if (name === 'playlist') {
|
||||||
nodes.push(reactive(new PlaylistGroupNode({
|
nodes.push(reactive(new PlaylistGroupNode({
|
||||||
currentTrack: 0,
|
label: 'Playlist',
|
||||||
currentTrackTime: 0,
|
currentTrack: 0,
|
||||||
volume: 100,
|
currentTrackTime: 0,
|
||||||
isMuted: false,
|
volume: 100,
|
||||||
sounds: [],
|
isMuted: false,
|
||||||
|
sounds: [],
|
||||||
}, {
|
}, {
|
||||||
fs,
|
fs,
|
||||||
audioContext,
|
audioContext,
|
||||||
})))
|
})))
|
||||||
} else if (name === 'sfx') {
|
} else if (name === 'sfx') {
|
||||||
nodes.push(reactive(new SfxGroupNode({
|
nodes.push(reactive(new SfxGroupNode({
|
||||||
isMuted: false,
|
label: 'SFX',
|
||||||
volume: 100,
|
isMuted: false,
|
||||||
sounds: [],
|
volume: 100,
|
||||||
|
sounds: [],
|
||||||
}, {
|
}, {
|
||||||
fs,
|
fs,
|
||||||
audioContext,
|
audioContext,
|
||||||
})))
|
})))
|
||||||
} else if (name === 'tab') {
|
} else if (name === 'tab') {
|
||||||
nodes.push(reactive(new TabSourceNode({
|
nodes.push(reactive(new TabSourceNode({
|
||||||
isMuted: false,
|
label: 'Tab Source',
|
||||||
volume: 100,
|
isMuted: false,
|
||||||
sounds: [],
|
volume: 100,
|
||||||
|
sounds: [],
|
||||||
|
}, {
|
||||||
|
fs,
|
||||||
|
audioContext,
|
||||||
|
})))
|
||||||
|
} else if (name === 'parallel') {
|
||||||
|
nodes.push(reactive(new ParallelGroupNode({
|
||||||
|
label: 'Parallel',
|
||||||
|
isMuted: false,
|
||||||
|
volume: 100,
|
||||||
|
enabledTracks: [],
|
||||||
|
sounds: [],
|
||||||
}, {
|
}, {
|
||||||
fs,
|
fs,
|
||||||
audioContext,
|
audioContext,
|
||||||
@@ -115,10 +139,8 @@ const dest = audioContext.createGain();
|
|||||||
|
|
||||||
|
|
||||||
watch(() => deviceManager.audioDevices.length, () => {
|
watch(() => deviceManager.audioDevices.length, () => {
|
||||||
console.log('DEVICE_MANAGER', deviceManager)
|
|
||||||
for(const device of deviceManager.audioDevices) {
|
for(const device of deviceManager.audioDevices) {
|
||||||
dest.connect(device.audioSink);
|
dest.connect(device.audioSink);
|
||||||
console.log('DEVICES', device, device.audioSink);
|
|
||||||
}
|
}
|
||||||
}, {immediate: true})
|
}, {immediate: true})
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ function onClose() {
|
|||||||
|
|
||||||
watch(() => props.open, (open) => {
|
watch(() => props.open, (open) => {
|
||||||
if (open) {
|
if (open) {
|
||||||
console.log(dialog.value);
|
|
||||||
dialog.value?.showModal();
|
dialog.value?.showModal();
|
||||||
popover.value?.showPopover();
|
popover.value?.showPopover();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ function onFilePreview(file: VirtualFile) {
|
|||||||
|
|
||||||
watch(search, async (value) => {
|
watch(search, async (value) => {
|
||||||
searchResults.value = [];
|
searchResults.value = [];
|
||||||
console.log('search', value, value.length);
|
|
||||||
if (value.length < 3) {
|
if (value.length < 3) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -117,16 +116,6 @@ async function buildIndex() {
|
|||||||
grid-gap: 4px;
|
grid-gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-manager__preview {
|
|
||||||
max-height: 25vh;
|
|
||||||
transition: max-height 200ms;
|
|
||||||
transition-behavior: allow-discrete;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-manager__preview:hover {
|
|
||||||
max-height: initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-manager-dir {
|
.file-manager-dir {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
|
|||||||
@@ -59,8 +59,12 @@ async function toggleCollapse() {
|
|||||||
async function fetchDirectoryContent() {
|
async function fetchDirectoryContent() {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
// preload from cache
|
try {
|
||||||
content.value = await fs.listDir(props.dir, {cache: 'only-cache'});
|
// preload from cache
|
||||||
|
content.value = await fs.listDir(props.dir, {cache: 'only-cache'});
|
||||||
|
} catch (err) {
|
||||||
|
// ignore missing cache
|
||||||
|
}
|
||||||
// replace with network
|
// replace with network
|
||||||
content.value = await fs.listDir(props.dir, {cache: 'use-network'});
|
content.value = await fs.listDir(props.dir, {cache: 'use-network'});
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,27 +1,37 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="currentPreviewUrl" class="file-preview">
|
<div class="file-preview" ref="root">
|
||||||
<img
|
<template v-if="currentPreviewUrl && visible">
|
||||||
v-if="currentPrevewType?.startsWith('image/')"
|
<img
|
||||||
|
v-if="currentPrevewType?.startsWith('image/')"
|
||||||
|
:src="currentPreviewUrl"
|
||||||
|
alt="preview"
|
||||||
|
class="file-preview__image" />
|
||||||
|
<video
|
||||||
|
v-if="currentPrevewType?.startsWith('video/')"
|
||||||
:src="currentPreviewUrl"
|
:src="currentPreviewUrl"
|
||||||
alt="preview"
|
autoplay
|
||||||
class="file-preview__image" />
|
muted
|
||||||
<video
|
loop
|
||||||
v-if="currentPrevewType?.startsWith('video/')"
|
playsinline
|
||||||
:src="currentPreviewUrl"
|
class="file-preview__video"
|
||||||
autoplay
|
></video>
|
||||||
muted
|
<audio
|
||||||
loop
|
v-if="currentPrevewType?.startsWith('audio/')"
|
||||||
playsinline
|
autoplay
|
||||||
class="file-preview__video"
|
:src="currentPreviewUrl"
|
||||||
></video>
|
class="file-preview__audio"
|
||||||
|
controls
|
||||||
|
:muted="muted"
|
||||||
|
@volumechange="muted = ($event.target as any).muted"
|
||||||
|
></audio>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {inject, ref, watch} from 'vue';
|
import {inject, onMounted, ref, watch} from 'vue';
|
||||||
import {VirtualFile} from '../../model/FileSystem/types';
|
import {VirtualFile} from '../../model/FileSystem/types';
|
||||||
import {MountedFsSymbol} from '../../model/injectionSymbols';
|
import {MountedFsSymbol} from '../../model/injectionSymbols';
|
||||||
|
|
||||||
|
|
||||||
const props=defineProps<{
|
const props=defineProps<{
|
||||||
file?:VirtualFile
|
file?:VirtualFile
|
||||||
}>()
|
}>()
|
||||||
@@ -30,10 +40,12 @@ const fs = inject(MountedFsSymbol)!;
|
|||||||
const currentPreviewUrl = ref<string | null>(null);
|
const currentPreviewUrl = ref<string | null>(null);
|
||||||
const currentPrevewType = ref<string | null>(null);
|
const currentPrevewType = ref<string | null>(null);
|
||||||
let abort: AbortController | undefined = undefined;
|
let abort: AbortController | undefined = undefined;
|
||||||
|
const root = ref<HTMLElement>();
|
||||||
|
const visible = ref(false);
|
||||||
|
const muted = ref(false);
|
||||||
|
|
||||||
|
|
||||||
watch(()=> props.file, async (file)=>{
|
watch(()=> props.file, async (file)=>{
|
||||||
console.log('file', file);
|
|
||||||
if (abort) {
|
if (abort) {
|
||||||
abort.abort('User canceled');
|
abort.abort('User canceled');
|
||||||
}
|
}
|
||||||
@@ -43,29 +55,45 @@ watch(()=> props.file, async (file)=>{
|
|||||||
}
|
}
|
||||||
currentPreviewUrl.value = null;
|
currentPreviewUrl.value = null;
|
||||||
try {
|
try {
|
||||||
console.log('downloading thumbnail');
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const blob = await fs.downloadThumbnail(file, {abortSignal: abort.signal});
|
const blob = await fs.downloadThumbnail(file, {abortSignal: abort.signal});
|
||||||
currentPreviewUrl.value = URL.createObjectURL(blob);
|
currentPreviewUrl.value = URL.createObjectURL(blob);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
currentPrevewType.value = file.mimeType;
|
currentPrevewType.value = file.mimeType;
|
||||||
console.log('currentPreviewUrl', currentPreviewUrl.value);
|
|
||||||
abort = undefined;
|
abort = undefined;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e !== 'User canceled') {
|
if (e !== 'User canceled') {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
} else {
|
|
||||||
console.log('User canceled');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
new IntersectionObserver((entries) => {
|
||||||
|
if (entries[0].isIntersecting) {
|
||||||
|
visible.value = true;
|
||||||
|
} else {
|
||||||
|
visible.value = false;
|
||||||
|
}
|
||||||
|
}).observe(root.value!);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
|
.file-preview {
|
||||||
|
width: 100%;
|
||||||
|
height: 30vh;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-preview__audio,
|
||||||
.file-preview__video,
|
.file-preview__video,
|
||||||
.file-preview__image {
|
.file-preview__image {
|
||||||
max-width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -58,7 +58,6 @@ let canvasDisplay: undefined | CanvasDisplay;
|
|||||||
function selectScreen (screen: any) {
|
function selectScreen (screen: any) {
|
||||||
if (screen.imageSink?.display) {
|
if (screen.imageSink?.display) {
|
||||||
selectedScreen.value = screen;
|
selectedScreen.value = screen;
|
||||||
console.log('selectScreen!!!', screen.imageSink?.display);
|
|
||||||
canvasDisplay?.disconnectTarget(canvas.value!);
|
canvasDisplay?.disconnectTarget(canvas.value!);
|
||||||
canvasDisplay = screen.imageSink?.display;
|
canvasDisplay = screen.imageSink?.display;
|
||||||
canvasDisplay?.connectTarget(canvas.value!);
|
canvasDisplay?.connectTarget(canvas.value!);
|
||||||
@@ -66,7 +65,6 @@ function selectScreen (screen: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onDrop(file: VirtualFile) {
|
async function onDrop(file: VirtualFile) {
|
||||||
console.log('onDrop!!', file);
|
|
||||||
onLoad(file);
|
onLoad(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +75,6 @@ const dragging = ref(false);
|
|||||||
|
|
||||||
function onScroll(event: WheelEvent) {
|
function onScroll(event: WheelEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
// console.log('scroll', event.deltaY, event.offsetX, event.offsetY);
|
|
||||||
const delta = -1 * event.deltaY / 2000;
|
const delta = -1 * event.deltaY / 2000;
|
||||||
if (!canvasDisplay) {
|
if (!canvasDisplay) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ function redraw() {
|
|||||||
ctx: map.value!.getContext('2d')!,
|
ctx: map.value!.getContext('2d')!,
|
||||||
...mapContext.value,
|
...mapContext.value,
|
||||||
};
|
};
|
||||||
console.log('redraw', ctx.scale);
|
|
||||||
ctx.ctx.fillStyle = 'black';
|
ctx.ctx.fillStyle = 'black';
|
||||||
ctx.ctx.fillRect(0, 0, ctx.virtualWidth, ctx.virtualHeight);
|
ctx.ctx.fillRect(0, 0, ctx.virtualWidth, ctx.virtualHeight);
|
||||||
for (const layer of props.layers) {
|
for (const layer of props.layers) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export async function loadImage(src: string | File): Promise<HTMLImageElement> {
|
export async function loadImage(src: string | File): Promise<HTMLImageElement> {
|
||||||
if (src instanceof File) {
|
if (src instanceof File) {
|
||||||
console.log('Creating object URL');
|
|
||||||
src = URL.createObjectURL(src);
|
src = URL.createObjectURL(src);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ export class DistanceMeasurement implements CanvasDisplayDrawable {
|
|||||||
ctx.scale(1/display.scale, 1/display.scale);
|
ctx.scale(1/display.scale, 1/display.scale);
|
||||||
ctx.translate(display.offsetX - display.width/2, display.offsetY - display.height / 2 );
|
ctx.translate(display.offsetX - display.width/2, display.offsetY - display.height / 2 );
|
||||||
if (this.pointer) {
|
if (this.pointer) {
|
||||||
console.log('Drawing pointer', );
|
|
||||||
|
|
||||||
ctx.fillStyle = 'rgba(255,0,0,0.5)';
|
ctx.fillStyle = 'rgba(255,0,0,0.5)';
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
@@ -65,7 +64,9 @@ export class DistanceMeasurement implements CanvasDisplayDrawable {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const lengthPx = Math.sqrt(Math.pow(this.start!.x - this.end!.x, 2) + Math.pow(this.start!.y - this.end!.y, 2));
|
const lengthPx = Math.sqrt(Math.pow(this.start!.x - this.end!.x, 2) + Math.pow(this.start!.y - this.end!.y, 2));
|
||||||
|
const chebychevPx = Math.max(Math.abs(this.start!.x - this.end!.x), Math.abs(this.start!.y - this.end!.y));
|
||||||
const lengthFeet = ((lengthPx / display.dpi) * 5) / display.scale;
|
const lengthFeet = ((lengthPx / display.dpi) * 5) / display.scale;
|
||||||
|
const chebychevFeet = Math.round(((chebychevPx / display.dpi) / display.scale)) * 5;
|
||||||
const roundedFeets = Math.floor(lengthFeet/5) * 5;
|
const roundedFeets = Math.floor(lengthFeet/5) * 5;
|
||||||
const roundedPx = ((roundedFeets) / 5) * display.dpi * display.scale;
|
const roundedPx = ((roundedFeets) / 5) * display.dpi * display.scale;
|
||||||
const halfInchInPx = 0.5 * display.dpi * display.scale;
|
const halfInchInPx = 0.5 * display.dpi * display.scale;
|
||||||
@@ -93,7 +94,7 @@ export class DistanceMeasurement implements CanvasDisplayDrawable {
|
|||||||
ctx.fillRect(this.end!.x, this.end!.y - 30, 200, 40);
|
ctx.fillRect(this.end!.x, this.end!.y - 30, 200, 40);
|
||||||
ctx.font = "30px Arial";
|
ctx.font = "30px Arial";
|
||||||
ctx.fillStyle = 'red';
|
ctx.fillStyle = 'red';
|
||||||
ctx.fillText(`${roundedFeets} / ${lengthFeet.toFixed(2)} ft`, this.end!.x + 5, this.end!.y);
|
ctx.fillText(`${chebychevFeet} / ${lengthFeet.toFixed(2)} ft`, this.end!.x + 5, this.end!.y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +132,9 @@ export class MapLayerDrawable implements CanvasDisplayDrawable {
|
|||||||
}
|
}
|
||||||
if (this.file.type.startsWith('image/')) {
|
if (this.file.type.startsWith('image/')) {
|
||||||
this.img = await loadImage(this.file);
|
this.img = await loadImage(this.file);
|
||||||
this.mapWidthInInches = this.mapWidth ?? 10;
|
if (this.mapWidth) {
|
||||||
|
this.mapWidthInInches = this.mapWidth;
|
||||||
|
}
|
||||||
if (this.img.naturalWidth < this.img.naturalHeight) {
|
if (this.img.naturalWidth < this.img.naturalHeight) {
|
||||||
this.rotation = 1;
|
this.rotation = 1;
|
||||||
}
|
}
|
||||||
@@ -146,6 +149,9 @@ export class MapLayerDrawable implements CanvasDisplayDrawable {
|
|||||||
video.loop = true;
|
video.loop = true;
|
||||||
video.play();
|
video.play();
|
||||||
await videoMeta;
|
await videoMeta;
|
||||||
|
if (this.mapWidth) {
|
||||||
|
this.mapWidthInInches = this.mapWidth;
|
||||||
|
}
|
||||||
if (video.videoWidth < video.videoHeight) {
|
if (video.videoWidth < video.videoHeight) {
|
||||||
this.rotation = 1;
|
this.rotation = 1;
|
||||||
}
|
}
|
||||||
@@ -169,6 +175,7 @@ export class MapLayerDrawable implements CanvasDisplayDrawable {
|
|||||||
private setDefaultDPI(display: CanvasDisplay, width: number, height: number) {
|
private setDefaultDPI(display: CanvasDisplay, width: number, height: number) {
|
||||||
if (this.dpi === undefined) {
|
if (this.dpi === undefined) {
|
||||||
this.dpi = Math.min((width / display.width), height / display.height) * display.dpi;
|
this.dpi = Math.min((width / display.width), height / display.height) * display.dpi;
|
||||||
|
this.mapWidth = Math.floor(width / this.dpi);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,7 +287,6 @@ export class CanvasDisplay {
|
|||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
if (diameter) {
|
if (diameter) {
|
||||||
console.log('Setting diameter', diameter);
|
|
||||||
this.setDiameter(diameter);
|
this.setDiameter(diameter);
|
||||||
}
|
}
|
||||||
this.offsetX = 0;
|
this.offsetX = 0;
|
||||||
@@ -364,7 +370,6 @@ export class CanvasDisplay {
|
|||||||
this.targets.add(target);
|
this.targets.add(target);
|
||||||
if (this.targets.size > 0) {
|
if (this.targets.size > 0) {
|
||||||
this.start();
|
this.start();
|
||||||
console.log('Canvas', this.canvas);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,7 +379,6 @@ export class CanvasDisplay {
|
|||||||
|
|
||||||
async getStream() {
|
async getStream() {
|
||||||
this.redrawBuffer();
|
this.redrawBuffer();
|
||||||
console.log('Streamed Canvas', this.canvas.width, this.canvas.height);
|
|
||||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||||
return this.canvas.captureStream(25);
|
return this.canvas.captureStream(25);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ import {DeviceConfig} from '../../model/Remote/RemoteDeviceManager';
|
|||||||
|
|
||||||
const name = ref('');
|
const name = ref('');
|
||||||
const manager = inject(RemoteDeviceManagerSymbol)!;
|
const manager = inject(RemoteDeviceManagerSymbol)!;
|
||||||
console.log(manager);
|
|
||||||
const localWindowDialog = ref<HTMLDialogElement>();
|
const localWindowDialog = ref<HTMLDialogElement>();
|
||||||
|
|
||||||
const availableDevices = ref<DeviceConfig[]>([]);
|
const availableDevices = ref<DeviceConfig[]>([]);
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ const {onMouseDown} = useMouseDragging()
|
|||||||
|
|
||||||
function onWheel(event: WheelEvent) {
|
function onWheel(event: WheelEvent) {
|
||||||
scale.value = Math.max(10, Math.min(1000, scale.value + event.deltaY / 100));
|
scale.value = Math.max(10, Math.min(1000, scale.value + event.deltaY / 100));
|
||||||
console.log('scroll', event.deltaY, scale.value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -71,7 +70,6 @@ async function loadFile(meta: VirtualFile, file: File) {
|
|||||||
const map = new MapLayerDrawable(file, meta?.map?.width);
|
const map = new MapLayerDrawable(file, meta?.map?.width);
|
||||||
await map.preloadFile();
|
await map.preloadFile();
|
||||||
imageSink.value.display.mapLayer = map
|
imageSink.value.display.mapLayer = map
|
||||||
console.log(imageSink.value?.display);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,8 +93,6 @@ async function onDrop(event: DragEvent) {
|
|||||||
await loadFile(meta, file);
|
await loadFile(meta, file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('files', event.dataTransfer?.files);
|
|
||||||
console.log('drop', event.dataTransfer?.getData('application/json'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -8,6 +8,5 @@ import {reactive} from 'vue';
|
|||||||
import {SoundPlayer} from '../model/SoundPlayer';
|
import {SoundPlayer} from '../model/SoundPlayer';
|
||||||
|
|
||||||
const soundPlayer = reactive(new SoundPlayer());
|
const soundPlayer = reactive(new SoundPlayer());
|
||||||
console.log(soundPlayer);
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -26,8 +26,6 @@ const slots = defineSlots<{
|
|||||||
Id: string,
|
Id: string,
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
console.log(slots)
|
|
||||||
|
|
||||||
const selectedTab = ref(props.tabs?.[0]?.key);
|
const selectedTab = ref(props.tabs?.[0]?.key);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ const app = createApp(ControlApp);
|
|||||||
|
|
||||||
const chromecastChannelCaster = new ChromecastChannelCaster();
|
const chromecastChannelCaster = new ChromecastChannelCaster();
|
||||||
chromecastChannelCaster.initialize();
|
chromecastChannelCaster.initialize();
|
||||||
console.log('chromecastChannelCaster', chromecastChannelCaster);
|
|
||||||
|
|
||||||
const audioContext = new AudioContext();
|
const audioContext = new AudioContext();
|
||||||
|
|
||||||
@@ -44,6 +43,13 @@ app
|
|||||||
key: '426pBJgSyfmgJAaUmNjk',
|
key: '426pBJgSyfmgJAaUmNjk',
|
||||||
share: 'nCKcVyl',
|
share: 'nCKcVyl',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Scenes',
|
||||||
|
type: 'filestash',
|
||||||
|
domain: 'https://files.fisoft.eu',
|
||||||
|
key: '426pBJgSyfmgJAaUmNjk',
|
||||||
|
share: 'ONiRCBi',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Audio',
|
name: 'Audio',
|
||||||
type: 'filestash',
|
type: 'filestash',
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ParallelGroupNode } from "./ParallelGroupNode";
|
||||||
import { PlaylistGroupNode } from "./PlaylistGroupNode";
|
import { PlaylistGroupNode } from "./PlaylistGroupNode";
|
||||||
import { SfxGroupNode } from "./SfxGroupNode";
|
import { SfxGroupNode } from "./SfxGroupNode";
|
||||||
import { SoundGroupNode } from "./SoundGroupNode";
|
import { SoundGroupNode } from "./SoundGroupNode";
|
||||||
@@ -6,7 +7,7 @@ import { SoundboardContext } from "./types";
|
|||||||
|
|
||||||
|
|
||||||
type SerializedNode<T extends SoundGroupNode<any>> = ReturnType<T['serializeOptions']>
|
type SerializedNode<T extends SoundGroupNode<any>> = ReturnType<T['serializeOptions']>
|
||||||
export type SupportedDefinitions = SerializedNode<PlaylistGroupNode> | SerializedNode<SfxGroupNode> | SerializedNode<TabSourceNode>
|
export type SupportedDefinitions = SerializedNode<PlaylistGroupNode> | SerializedNode<SfxGroupNode> | SerializedNode<TabSourceNode> | SerializedNode<ParallelGroupNode>;
|
||||||
|
|
||||||
|
|
||||||
export class AudioNodeManager {
|
export class AudioNodeManager {
|
||||||
@@ -23,6 +24,8 @@ export class AudioNodeManager {
|
|||||||
return new SfxGroupNode(definition, ctx);
|
return new SfxGroupNode(definition, ctx);
|
||||||
} else if (definition.type === 'tabSource') {
|
} else if (definition.type === 'tabSource') {
|
||||||
return new TabSourceNode(definition, ctx);
|
return new TabSourceNode(definition, ctx);
|
||||||
|
} else if (definition.type === 'parallel') {
|
||||||
|
return new ParallelGroupNode(definition, ctx);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Invalid node type');
|
throw new Error('Invalid node type');
|
||||||
}
|
}
|
||||||
|
|||||||
75
src/model/Audio/ParallelGroupNode.ts
Normal file
75
src/model/Audio/ParallelGroupNode.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { VirtualFile } from "../FileSystem/types";
|
||||||
|
import { SoundGroupNode, SoundGroupNodeOptions } from "./SoundGroupNode";
|
||||||
|
import { MediaElementSoundNode } from "./SoundNode";
|
||||||
|
|
||||||
|
export interface ParallelGroupNodeOptions extends SoundGroupNodeOptions {
|
||||||
|
enabledTracks: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ParallelGroupNode extends SoundGroupNode<ParallelGroupNodeOptions, MediaElementSoundNode> {
|
||||||
|
|
||||||
|
public readonly type = 'parallel';
|
||||||
|
private enabledTracks: Set<string> = new Set();
|
||||||
|
|
||||||
|
get isPlaying() {
|
||||||
|
return this.soundNodes.some(sound => sound.isPlaying);
|
||||||
|
}
|
||||||
|
|
||||||
|
play() {
|
||||||
|
this.soundNodes.forEach((sound, i) => {
|
||||||
|
if (this.enabledTracks.has(sound.id)) {
|
||||||
|
sound.play();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeSoundNode(node: MediaElementSoundNode) {
|
||||||
|
this.enabledTracks.delete(node.id);
|
||||||
|
super.removeSoundNode(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.soundNodes.forEach(sound => sound.stop());
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePlay() {
|
||||||
|
if (this.isPlaying) {
|
||||||
|
this.stop();
|
||||||
|
} else {
|
||||||
|
this.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserializeOptions(options: ParallelGroupNodeOptions) {
|
||||||
|
this.enabledTracks = new Set(options.enabledTracks);
|
||||||
|
super.deserializeOptions(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleTrack(track: MediaElementSoundNode) {
|
||||||
|
if (this.enabledTracks.has(track.id)) {
|
||||||
|
track.pause();
|
||||||
|
this.enabledTracks.delete(track.id);
|
||||||
|
} else {
|
||||||
|
this.enabledTracks.add(track.id);
|
||||||
|
track.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serializeOptions() {
|
||||||
|
return {
|
||||||
|
...super.serializeOptions(),
|
||||||
|
volume: this.volume,
|
||||||
|
isMuted: this.options.isMuted,
|
||||||
|
enabledTracks: [...this.options.enabledTracks.values()] as string[],
|
||||||
|
type: this.type,
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
addSound(vFile: VirtualFile) {
|
||||||
|
this.addSoundNode({
|
||||||
|
type: 'mediaElement',
|
||||||
|
vFile,
|
||||||
|
volume: 100,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { BufferedSoundNode, MediaElementSoundNode, PartiaSoundNodeOptions, Sound
|
|||||||
import { SoundboardContext } from "./types";
|
import { SoundboardContext } from "./types";
|
||||||
|
|
||||||
export interface SoundGroupNodeOptions {
|
export interface SoundGroupNodeOptions {
|
||||||
|
label: string,
|
||||||
volume: number,
|
volume: number,
|
||||||
isMuted: boolean,
|
isMuted: boolean,
|
||||||
sounds: PartiaSoundNodeOptions[]
|
sounds: PartiaSoundNodeOptions[]
|
||||||
@@ -29,25 +30,35 @@ export class DeviceSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class SoundGroupNode<T extends SoundGroupNodeOptions> {
|
export abstract class SoundGroupNode<T extends SoundGroupNodeOptions, TNode extends SoundNode=SoundNode> {
|
||||||
|
|
||||||
public abstract readonly type: string;
|
public abstract readonly type: string;
|
||||||
public deviceSources: Map<RemoteDevice, DeviceSource> = new Map();
|
public deviceSources: Map<RemoteDevice, DeviceSource> = new Map();
|
||||||
|
// @ts-ignore it's in deserializeOptions
|
||||||
protected options: Omit<T, 'sounds'>
|
protected options: Omit<T, 'sounds'>
|
||||||
protected gainNode: GainNode;
|
protected gainNode: GainNode;
|
||||||
protected soundNodes: SoundNode[] = [];
|
protected soundNodes: TNode[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
options: T,
|
options: T,
|
||||||
protected ctx: SoundboardContext
|
protected ctx: SoundboardContext
|
||||||
) {
|
) {
|
||||||
this.options = options;
|
this.deserializeOptions(options);
|
||||||
this.gainNode = this.ctx.audioContext.createGain();
|
this.gainNode = this.ctx.audioContext.createGain();
|
||||||
this.gainNode.gain.value = (options.volume / 100);
|
this.gainNode.gain.value = (options.volume / 100);
|
||||||
for (const sound of options.sounds) {
|
for (const sound of options.sounds) {
|
||||||
this.addSoundNode(sound);
|
this.addSoundNode(sound);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get label() {
|
||||||
|
return this.options.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
set label(val: string) {
|
||||||
|
this.options.label = val;
|
||||||
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
addSound(vFile: VirtualFile) {
|
addSound(vFile: VirtualFile) {
|
||||||
// implement in subclass
|
// implement in subclass
|
||||||
@@ -56,16 +67,18 @@ export abstract class SoundGroupNode<T extends SoundGroupNodeOptions> {
|
|||||||
addSoundNode(options: PartiaSoundNodeOptions) {
|
addSoundNode(options: PartiaSoundNodeOptions) {
|
||||||
if (options.type === 'buffered') {
|
if (options.type === 'buffered') {
|
||||||
const sound = new BufferedSoundNode(options, this.ctx);
|
const sound = new BufferedSoundNode(options, this.ctx);
|
||||||
|
// @ts-ignore
|
||||||
this.soundNodes.push(sound);
|
this.soundNodes.push(sound);
|
||||||
sound.connect(this.gainNode);
|
sound.connect(this.gainNode);
|
||||||
} else if (options.type === 'mediaElement') {
|
} else if (options.type === 'mediaElement') {
|
||||||
const sound = new MediaElementSoundNode(options, this.ctx);
|
const sound = new MediaElementSoundNode(options, this.ctx);
|
||||||
|
// @ts-ignore
|
||||||
this.soundNodes.push(sound);
|
this.soundNodes.push(sound);
|
||||||
sound.connect(this.gainNode);
|
sound.connect(this.gainNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
removeSoundNode(node: SoundNode) {
|
removeSoundNode(node: TNode) {
|
||||||
node?.stop();
|
node?.stop();
|
||||||
const index = this.soundNodes.indexOf(node);
|
const index = this.soundNodes.indexOf(node);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
@@ -86,7 +99,6 @@ export abstract class SoundGroupNode<T extends SoundGroupNodeOptions> {
|
|||||||
|
|
||||||
|
|
||||||
connect(destination: AudioNode) {
|
connect(destination: AudioNode) {
|
||||||
console.log('connecting', this, destination);
|
|
||||||
this.gainNode.connect(destination);
|
this.gainNode.connect(destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,6 +152,10 @@ export abstract class SoundGroupNode<T extends SoundGroupNodeOptions> {
|
|||||||
return this.deviceSources.get(remoteDevice)!;
|
return this.deviceSources.get(remoteDevice)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deserializeOptions(options: T) {
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
|
||||||
serializeOptions() {
|
serializeOptions() {
|
||||||
return {
|
return {
|
||||||
...this.options,
|
...this.options,
|
||||||
|
|||||||
@@ -1,23 +1,27 @@
|
|||||||
import { MountedFs } from "../FileSystem/MountedFs";
|
import { MountedFs } from "../FileSystem/MountedFs";
|
||||||
import { VirtualFile } from "../FileSystem/types";
|
import { VirtualFile } from "../FileSystem/types";
|
||||||
import { SoundboardContext } from "./types";
|
import { SoundboardContext } from "./types";
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
|
||||||
export interface PartiaSoundNodeOptions {
|
export interface PartiaSoundNodeOptions {
|
||||||
|
id?: string;
|
||||||
vFile: VirtualFile;
|
vFile: VirtualFile;
|
||||||
volume?: number;
|
volume?: number;
|
||||||
type: 'buffered' | 'mediaElement';
|
type: 'buffered' | 'mediaElement';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SoundNodeOptions extends PartiaSoundNodeOptions {
|
export interface SoundNodeOptions extends PartiaSoundNodeOptions {
|
||||||
|
id: string;
|
||||||
volume: number;
|
volume: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class SoundNode {
|
export abstract class SoundNode<T extends SoundNodeOptions = SoundNodeOptions> {
|
||||||
|
|
||||||
|
public readonly id: string;
|
||||||
protected gainNode: GainNode;
|
protected gainNode: GainNode;
|
||||||
protected file?: File;
|
protected file?: File;
|
||||||
public options: SoundNodeOptions;
|
public options: T;
|
||||||
public readonly numberOfInputs = 0;
|
public readonly numberOfInputs = 0;
|
||||||
public readonly numberOfOutputs = 1;
|
public readonly numberOfOutputs = 1;
|
||||||
protected activeSources: AudioBufferSourceNode[] = [];
|
protected activeSources: AudioBufferSourceNode[] = [];
|
||||||
@@ -29,10 +33,14 @@ export abstract class SoundNode {
|
|||||||
options: PartiaSoundNodeOptions,
|
options: PartiaSoundNodeOptions,
|
||||||
ctx: SoundboardContext,
|
ctx: SoundboardContext,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
this.options = {
|
this.options = {
|
||||||
|
id: uuidv4(),
|
||||||
...options,
|
...options,
|
||||||
volume: options.volume ?? 100
|
volume: options.volume ?? 100
|
||||||
}
|
}
|
||||||
|
this.id = options.id!;
|
||||||
this.audioContext = ctx.audioContext;
|
this.audioContext = ctx.audioContext;
|
||||||
this.fs = ctx.fs;
|
this.fs = ctx.fs;
|
||||||
this.gainNode = this.audioContext.createGain();
|
this.gainNode = this.audioContext.createGain();
|
||||||
@@ -58,7 +66,6 @@ export abstract class SoundNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
connect(destination: AudioNode) {
|
connect(destination: AudioNode) {
|
||||||
console.log('connecting', this, destination);
|
|
||||||
this.gainNode.connect(destination);
|
this.gainNode.connect(destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +79,7 @@ export abstract class SoundNode {
|
|||||||
|
|
||||||
setVolume(volume: number) {
|
setVolume(volume: number) {
|
||||||
this.gainNode.gain.value = volume / 100;
|
this.gainNode.gain.value = volume / 100;
|
||||||
|
this.options.volume = volume;
|
||||||
}
|
}
|
||||||
|
|
||||||
getVolume() {
|
getVolume() {
|
||||||
@@ -115,7 +123,11 @@ export class BufferedSoundNode extends SoundNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MediaElementSoundNode extends SoundNode {
|
export interface MediaElementSoundNodeOptions extends SoundNodeOptions {
|
||||||
|
loop: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MediaElementSoundNode extends SoundNode<MediaElementSoundNodeOptions> {
|
||||||
private mediaElement?: HTMLMediaElement;
|
private mediaElement?: HTMLMediaElement;
|
||||||
private source?: MediaElementAudioSourceNode;
|
private source?: MediaElementAudioSourceNode;
|
||||||
private _isPlaying = false;
|
private _isPlaying = false;
|
||||||
@@ -124,6 +136,9 @@ export class MediaElementSoundNode extends SoundNode {
|
|||||||
await super.preload();
|
await super.preload();
|
||||||
if (!this.mediaElement) {
|
if (!this.mediaElement) {
|
||||||
this.mediaElement = document.createElement('audio');
|
this.mediaElement = document.createElement('audio');
|
||||||
|
if (this.options.loop) {
|
||||||
|
this.mediaElement.loop = true;
|
||||||
|
}
|
||||||
this.mediaElement.src = URL.createObjectURL(this.file!);
|
this.mediaElement.src = URL.createObjectURL(this.file!);
|
||||||
this.source = this.audioContext.createMediaElementSource(this.mediaElement);
|
this.source = this.audioContext.createMediaElementSource(this.mediaElement);
|
||||||
this.source.connect(this.gainNode);
|
this.source.connect(this.gainNode);
|
||||||
@@ -132,7 +147,6 @@ export class MediaElementSoundNode extends SoundNode {
|
|||||||
this._isPlaying = false;
|
this._isPlaying = false;
|
||||||
});
|
});
|
||||||
this.mediaElement.addEventListener('playing', (event) => {
|
this.mediaElement.addEventListener('playing', (event) => {
|
||||||
console.log(event);
|
|
||||||
this._isPlaying = true;
|
this._isPlaying = true;
|
||||||
});
|
});
|
||||||
this.mediaElement.addEventListener('pause', () => {
|
this.mediaElement.addEventListener('pause', () => {
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ export class TabSourceNode extends SoundGroupNode<TabSourceNodeOptions> {
|
|||||||
surfaceSwitching: "include",
|
surfaceSwitching: "include",
|
||||||
monitorTypeSurfaces: "include",
|
monitorTypeSurfaces: "include",
|
||||||
});
|
});
|
||||||
console.log(this.stream);
|
|
||||||
this.source = this.ctx.audioContext.createMediaStreamSource(this.stream);
|
this.source = this.ctx.audioContext.createMediaStreamSource(this.stream);
|
||||||
this.source.connect(this.gainNode);
|
this.source.connect(this.gainNode);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -128,12 +128,10 @@ export class WebRtcChannelSummon extends MessageEmitter implements JsonChannelSu
|
|||||||
event.streams.forEach((stream) => {
|
event.streams.forEach((stream) => {
|
||||||
stream.getTracks().forEach((track) => {
|
stream.getTracks().forEach((track) => {
|
||||||
if (event.track.kind === 'audio') {
|
if (event.track.kind === 'audio') {
|
||||||
console.log('audio track', track);
|
|
||||||
const audioElement = document.createElement('audio');
|
const audioElement = document.createElement('audio');
|
||||||
audioElement.srcObject = stream; // Attach the received stream
|
audioElement.srcObject = stream; // Attach the received stream
|
||||||
audioElement.play();
|
audioElement.play();
|
||||||
} else if (event.track.kind === 'video') {
|
} else if (event.track.kind === 'video') {
|
||||||
console.log('video track', track);
|
|
||||||
const videoEl = document.getElementById('mainVideo') as HTMLVideoElement;
|
const videoEl = document.getElementById('mainVideo') as HTMLVideoElement;
|
||||||
if (videoEl) {
|
if (videoEl) {
|
||||||
videoEl.srcObject = stream;
|
videoEl.srcObject = stream;
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ export class RemoteDeviceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async add(device: DeviceConfig, connect: boolean = false): Promise<void> {
|
async add(device: DeviceConfig, connect: boolean = false): Promise<void> {
|
||||||
console.log('add', device);
|
|
||||||
const instance = await this.createInstance(device);
|
const instance = await this.createInstance(device);
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -16,9 +16,6 @@ export class SoundPlayer {
|
|||||||
const randomOption = options[Math.floor(Math.random() * options.length)];
|
const randomOption = options[Math.floor(Math.random() * options.length)];
|
||||||
const soundBuffer = await this.fetchSound(sound, randomOption);
|
const soundBuffer = await this.fetchSound(sound, randomOption);
|
||||||
this.playBuffer(soundBuffer);
|
this.playBuffer(soundBuffer);
|
||||||
console.log(`Playing sound: ${sound}`);
|
|
||||||
} else {
|
|
||||||
console.log(`Sound not found: ${sound}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +37,6 @@ export class SoundPlayer {
|
|||||||
if (this.cache.has(url)) {
|
if (this.cache.has(url)) {
|
||||||
return this.cache.get(url)!;
|
return this.cache.get(url)!;
|
||||||
}
|
}
|
||||||
console.log(`Fetching sound: ${sound}/${options}`);
|
|
||||||
const arrayBuffer = await (await fetch(url)).arrayBuffer();
|
const arrayBuffer = await (await fetch(url)).arrayBuffer();
|
||||||
const audioBuffer = await this.ctx.decodeAudioData(arrayBuffer);
|
const audioBuffer = await this.ctx.decodeAudioData(arrayBuffer);
|
||||||
this.cache.set(url, audioBuffer);
|
this.cache.set(url, audioBuffer);
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ const dynamicStyle = computed(() => {
|
|||||||
|
|
||||||
|
|
||||||
function onVideoLoaded(event: Event) {
|
function onVideoLoaded(event: Event) {
|
||||||
console.log('video loaded', event.target);
|
|
||||||
mediaDimensions.value = {
|
mediaDimensions.value = {
|
||||||
width: video.value!.videoWidth,
|
width: video.value!.videoWidth,
|
||||||
height: video.value!.videoHeight,
|
height: video.value!.videoHeight,
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { JsonChannelSymbol, WebRtcChannelSymbol } from '../../model/injectionSym
|
|||||||
|
|
||||||
const jsonMessages = ref<{source: 'json' | 'webrtc', message: Message}[]>([]);
|
const jsonMessages = ref<{source: 'json' | 'webrtc', message: Message}[]>([]);
|
||||||
const lastMessages = computed(() => [...jsonMessages.value].reverse().slice(0, 10));
|
const lastMessages = computed(() => [...jsonMessages.value].reverse().slice(0, 10));
|
||||||
console.log(jsonMessages, lastMessages);
|
|
||||||
const channel = inject(JsonChannelSymbol)!;
|
const channel = inject(JsonChannelSymbol)!;
|
||||||
const webRTC = inject(WebRtcChannelSymbol)!;
|
const webRTC = inject(WebRtcChannelSymbol)!;
|
||||||
channel.on((message) => {
|
channel.on((message) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user