fixes and cleanup

This commit is contained in:
2024-11-25 23:34:08 +00:00
parent 9601597cf6
commit 944ac08695
29 changed files with 398 additions and 100 deletions

13
package-lock.json generated
View File

@@ -10,6 +10,7 @@
"dependencies": {
"@types/chromecast-caf-receiver": "^6.0.16",
"@types/chromecast-caf-sender": "^1.0.10",
"uuid": "^11.0.3",
"vite-plugin-list-directory-contents": "^1.4.5",
"vue": "^3.4.29"
},
@@ -2526,6 +2527,18 @@
"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": {
"version": "13.12.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz",

View File

@@ -11,6 +11,7 @@
"dependencies": {
"@types/chromecast-caf-receiver": "^6.0.16",
"@types/chromecast-caf-sender": "^1.0.10",
"uuid": "^11.0.3",
"vite-plugin-list-directory-contents": "^1.4.5",
"vue": "^3.4.29"
},

View File

@@ -8,13 +8,9 @@ const chromecast = new ChromecastChannelCaster(searchParams.get('ws') ?? undefin
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);
});

View File

@@ -6,7 +6,7 @@
@dragover.prevent="onDragover"
>
<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')">&times;</div>
</div>
<div class="a-card__volume">
@@ -48,7 +48,7 @@
</div>
</template>
<script setup lang="ts">
import {ref, UnwrapNestedRefs} from 'vue';
import {computed, ref, UnwrapNestedRefs} from 'vue';
import {SoundGroupNode} from '../../model/Audio/SoundGroupNode';
import Play from '../Icons/Play.vue';
@@ -82,6 +82,11 @@ const props = withDefaults(defineProps<{
noDetail: false
});
const editableLabel = computed({
get: () => props.node.label,
set: (value: string) => props.node.label = value
});
const popover = ref<HTMLElement>();
function openPopover() {
if (props.noDetail) {
@@ -92,17 +97,14 @@ function openPopover() {
function onDragover(event: DragEvent) {
const data = event.dataTransfer?.getData('application/json');
console.log('dragover', event, '>', data, '<', typeof data);
event.preventDefault();
}
function onDrop(event: DragEvent) {
console.log('drop', event, event.dataTransfer?.getData('application/json'));
event.preventDefault();
if (event.dataTransfer?.getData('application/json')) {
const meta = JSON.parse(event.dataTransfer?.getData('application/json'));
if (meta.type === 'file') {
console.log('got file', meta);
props.node.addSound(meta);
}
}

View 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)">
&times;
</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>

View File

@@ -20,6 +20,11 @@
:node="node"
@delete="deleteNode(node)"
/>
<Parallel
v-else-if="node.type === 'parallel'"
:node="node"
@delete="deleteNode(node)"
/>
</template>
</div>
<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">
Sfx
</div>
<div @click="addNode('parallel')" class="soundboard-add-card__btn">
Parallel
</div>
<div @click="addNode('tab')" class="soundboard-add-card__btn">
Tab Source
</div>
@@ -50,6 +58,8 @@ import TabSource from './TabSource.vue';
import DeviceVolume from './DeviceVolume.vue';
import {AudioNodeManager} from '../../model/Audio/AudioNodesManager';
import {debounce} from '../../utils/debounce';
import {ParallelGroupNode} from '../../model/Audio/ParallelGroupNode';
import Parallel from './Parallel.vue';
const fs = inject(MountedFsSymbol)!;
const audioContext = inject(AudioContextSymbol) as AudioContext;
@@ -61,29 +71,43 @@ const nodes = reactive(audioNodesManager.restore({fs, audioContext}));
function addNode(name: string) {
if (name === 'playlist') {
nodes.push(reactive(new PlaylistGroupNode({
currentTrack: 0,
currentTrackTime: 0,
volume: 100,
isMuted: false,
sounds: [],
label: 'Playlist',
currentTrack: 0,
currentTrackTime: 0,
volume: 100,
isMuted: false,
sounds: [],
}, {
fs,
audioContext,
})))
} else if (name === 'sfx') {
nodes.push(reactive(new SfxGroupNode({
isMuted: false,
volume: 100,
sounds: [],
label: 'SFX',
isMuted: false,
volume: 100,
sounds: [],
}, {
fs,
audioContext,
})))
} else if (name === 'tab') {
nodes.push(reactive(new TabSourceNode({
isMuted: false,
volume: 100,
sounds: [],
label: 'Tab Source',
isMuted: false,
volume: 100,
sounds: [],
}, {
fs,
audioContext,
})))
} else if (name === 'parallel') {
nodes.push(reactive(new ParallelGroupNode({
label: 'Parallel',
isMuted: false,
volume: 100,
enabledTracks: [],
sounds: [],
}, {
fs,
audioContext,
@@ -115,10 +139,8 @@ const dest = audioContext.createGain();
watch(() => deviceManager.audioDevices.length, () => {
console.log('DEVICE_MANAGER', deviceManager)
for(const device of deviceManager.audioDevices) {
dest.connect(device.audioSink);
console.log('DEVICES', device, device.audioSink);
}
}, {immediate: true})

View File

@@ -35,7 +35,6 @@ function onClose() {
watch(() => props.open, (open) => {
if (open) {
console.log(dialog.value);
dialog.value?.showModal();
popover.value?.showPopover();
} else {

View File

@@ -69,7 +69,6 @@ function onFilePreview(file: VirtualFile) {
watch(search, async (value) => {
searchResults.value = [];
console.log('search', value, value.length);
if (value.length < 3) {
return;
}
@@ -117,16 +116,6 @@ async function buildIndex() {
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 {
list-style-type: none;
padding-left: 1rem;

View File

@@ -59,8 +59,12 @@ async function toggleCollapse() {
async function fetchDirectoryContent() {
try {
isLoading.value = true;
// preload from cache
content.value = await fs.listDir(props.dir, {cache: 'only-cache'});
try {
// preload from cache
content.value = await fs.listDir(props.dir, {cache: 'only-cache'});
} catch (err) {
// ignore missing cache
}
// replace with network
content.value = await fs.listDir(props.dir, {cache: 'use-network'});
} finally {

View File

@@ -1,27 +1,37 @@
<template>
<div v-if="currentPreviewUrl" class="file-preview">
<img
v-if="currentPrevewType?.startsWith('image/')"
<div class="file-preview" ref="root">
<template v-if="currentPreviewUrl && visible">
<img
v-if="currentPrevewType?.startsWith('image/')"
:src="currentPreviewUrl"
alt="preview"
class="file-preview__image" />
<video
v-if="currentPrevewType?.startsWith('video/')"
:src="currentPreviewUrl"
alt="preview"
class="file-preview__image" />
<video
v-if="currentPrevewType?.startsWith('video/')"
:src="currentPreviewUrl"
autoplay
muted
loop
playsinline
class="file-preview__video"
></video>
autoplay
muted
loop
playsinline
class="file-preview__video"
></video>
<audio
v-if="currentPrevewType?.startsWith('audio/')"
autoplay
:src="currentPreviewUrl"
class="file-preview__audio"
controls
:muted="muted"
@volumechange="muted = ($event.target as any).muted"
></audio>
</template>
</div>
</template>
<script setup lang="ts">
import {inject, ref, watch} from 'vue';
import {inject, onMounted, ref, watch} from 'vue';
import {VirtualFile} from '../../model/FileSystem/types';
import {MountedFsSymbol} from '../../model/injectionSymbols';
const props=defineProps<{
file?:VirtualFile
}>()
@@ -30,10 +40,12 @@ const fs = inject(MountedFsSymbol)!;
const currentPreviewUrl = ref<string | null>(null);
const currentPrevewType = ref<string | null>(null);
let abort: AbortController | undefined = undefined;
const root = ref<HTMLElement>();
const visible = ref(false);
const muted = ref(false);
watch(()=> props.file, async (file)=>{
console.log('file', file);
if (abort) {
abort.abort('User canceled');
}
@@ -43,29 +55,45 @@ watch(()=> props.file, async (file)=>{
}
currentPreviewUrl.value = null;
try {
console.log('downloading thumbnail');
// @ts-ignore
const blob = await fs.downloadThumbnail(file, {abortSignal: abort.signal});
currentPreviewUrl.value = URL.createObjectURL(blob);
// @ts-ignore
currentPrevewType.value = file.mimeType;
console.log('currentPreviewUrl', currentPreviewUrl.value);
abort = undefined;
} catch (e) {
if (e !== 'User canceled') {
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>
<style>
.file-preview {
width: 100%;
height: 30vh;
overflow: hidden;
position: relative;
}
.file-preview__audio,
.file-preview__video,
.file-preview__image {
max-width: 100%;
width: 100%;
height: 100%;
object-fit: contain;
}
</style>

View File

@@ -58,7 +58,6 @@ let canvasDisplay: undefined | CanvasDisplay;
function selectScreen (screen: any) {
if (screen.imageSink?.display) {
selectedScreen.value = screen;
console.log('selectScreen!!!', screen.imageSink?.display);
canvasDisplay?.disconnectTarget(canvas.value!);
canvasDisplay = screen.imageSink?.display;
canvasDisplay?.connectTarget(canvas.value!);
@@ -66,7 +65,6 @@ function selectScreen (screen: any) {
}
async function onDrop(file: VirtualFile) {
console.log('onDrop!!', file);
onLoad(file);
}
@@ -77,7 +75,6 @@ const dragging = ref(false);
function onScroll(event: WheelEvent) {
event.preventDefault();
// console.log('scroll', event.deltaY, event.offsetX, event.offsetY);
const delta = -1 * event.deltaY / 2000;
if (!canvasDisplay) {
return;

View File

@@ -41,7 +41,6 @@ function redraw() {
ctx: map.value!.getContext('2d')!,
...mapContext.value,
};
console.log('redraw', ctx.scale);
ctx.ctx.fillStyle = 'black';
ctx.ctx.fillRect(0, 0, ctx.virtualWidth, ctx.virtualHeight);
for (const layer of props.layers) {

View File

@@ -1,6 +1,5 @@
export async function loadImage(src: string | File): Promise<HTMLImageElement> {
if (src instanceof File) {
console.log('Creating object URL');
src = URL.createObjectURL(src);
}
try {

View File

@@ -43,7 +43,6 @@ export class DistanceMeasurement implements CanvasDisplayDrawable {
ctx.scale(1/display.scale, 1/display.scale);
ctx.translate(display.offsetX - display.width/2, display.offsetY - display.height / 2 );
if (this.pointer) {
console.log('Drawing pointer', );
ctx.fillStyle = 'rgba(255,0,0,0.5)';
ctx.beginPath();
@@ -65,7 +64,9 @@ export class DistanceMeasurement implements CanvasDisplayDrawable {
return;
}
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 chebychevFeet = Math.round(((chebychevPx / display.dpi) / display.scale)) * 5;
const roundedFeets = Math.floor(lengthFeet/5) * 5;
const roundedPx = ((roundedFeets) / 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.font = "30px Arial";
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/')) {
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) {
this.rotation = 1;
}
@@ -146,6 +149,9 @@ export class MapLayerDrawable implements CanvasDisplayDrawable {
video.loop = true;
video.play();
await videoMeta;
if (this.mapWidth) {
this.mapWidthInInches = this.mapWidth;
}
if (video.videoWidth < video.videoHeight) {
this.rotation = 1;
}
@@ -169,6 +175,7 @@ export class MapLayerDrawable implements CanvasDisplayDrawable {
private setDefaultDPI(display: CanvasDisplay, width: number, height: number) {
if (this.dpi === undefined) {
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.height = height;
if (diameter) {
console.log('Setting diameter', diameter);
this.setDiameter(diameter);
}
this.offsetX = 0;
@@ -364,7 +370,6 @@ export class CanvasDisplay {
this.targets.add(target);
if (this.targets.size > 0) {
this.start();
console.log('Canvas', this.canvas);
}
}
@@ -374,7 +379,6 @@ export class CanvasDisplay {
async getStream() {
this.redrawBuffer();
console.log('Streamed Canvas', this.canvas.width, this.canvas.height);
await new Promise((resolve) => requestAnimationFrame(resolve));
return this.canvas.captureStream(25);
}

View File

@@ -53,7 +53,6 @@ import {DeviceConfig} from '../../model/Remote/RemoteDeviceManager';
const name = ref('');
const manager = inject(RemoteDeviceManagerSymbol)!;
console.log(manager);
const localWindowDialog = ref<HTMLDialogElement>();
const availableDevices = ref<DeviceConfig[]>([]);

View File

@@ -55,7 +55,6 @@ const {onMouseDown} = useMouseDragging()
function onWheel(event: WheelEvent) {
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);
await map.preloadFile();
imageSink.value.display.mapLayer = map
console.log(imageSink.value?.display);
}
}
@@ -95,8 +93,6 @@ async function onDrop(event: DragEvent) {
await loadFile(meta, file);
}
}
console.log('files', event.dataTransfer?.files);
console.log('drop', event.dataTransfer?.getData('application/json'));
}
</script>

View File

@@ -8,6 +8,5 @@ import {reactive} from 'vue';
import {SoundPlayer} from '../model/SoundPlayer';
const soundPlayer = reactive(new SoundPlayer());
console.log(soundPlayer);
</script>

View File

@@ -26,8 +26,6 @@ const slots = defineSlots<{
Id: string,
}>()
console.log(slots)
const selectedTab = ref(props.tabs?.[0]?.key);
</script>

View File

@@ -12,7 +12,6 @@ const app = createApp(ControlApp);
const chromecastChannelCaster = new ChromecastChannelCaster();
chromecastChannelCaster.initialize();
console.log('chromecastChannelCaster', chromecastChannelCaster);
const audioContext = new AudioContext();
@@ -44,6 +43,13 @@ app
key: '426pBJgSyfmgJAaUmNjk',
share: 'nCKcVyl',
},
{
name: 'Scenes',
type: 'filestash',
domain: 'https://files.fisoft.eu',
key: '426pBJgSyfmgJAaUmNjk',
share: 'ONiRCBi',
},
{
name: 'Audio',
type: 'filestash',

View File

@@ -1,3 +1,4 @@
import { ParallelGroupNode } from "./ParallelGroupNode";
import { PlaylistGroupNode } from "./PlaylistGroupNode";
import { SfxGroupNode } from "./SfxGroupNode";
import { SoundGroupNode } from "./SoundGroupNode";
@@ -6,7 +7,7 @@ import { SoundboardContext } from "./types";
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 {
@@ -23,6 +24,8 @@ export class AudioNodeManager {
return new SfxGroupNode(definition, ctx);
} else if (definition.type === 'tabSource') {
return new TabSourceNode(definition, ctx);
} else if (definition.type === 'parallel') {
return new ParallelGroupNode(definition, ctx);
} else {
throw new Error('Invalid node type');
}

View 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,
});
}
}

View File

@@ -4,6 +4,7 @@ import { BufferedSoundNode, MediaElementSoundNode, PartiaSoundNodeOptions, Sound
import { SoundboardContext } from "./types";
export interface SoundGroupNodeOptions {
label: string,
volume: number,
isMuted: boolean,
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 deviceSources: Map<RemoteDevice, DeviceSource> = new Map();
// @ts-ignore it's in deserializeOptions
protected options: Omit<T, 'sounds'>
protected gainNode: GainNode;
protected soundNodes: SoundNode[] = [];
protected soundNodes: TNode[] = [];
constructor(
options: T,
protected ctx: SoundboardContext
) {
this.options = options;
this.deserializeOptions(options);
this.gainNode = this.ctx.audioContext.createGain();
this.gainNode.gain.value = (options.volume / 100);
for (const sound of options.sounds) {
this.addSoundNode(sound);
}
}
get label() {
return this.options.label;
}
set label(val: string) {
this.options.label = val;
}
// @ts-ignore
addSound(vFile: VirtualFile) {
// implement in subclass
@@ -56,16 +67,18 @@ export abstract class SoundGroupNode<T extends SoundGroupNodeOptions> {
addSoundNode(options: PartiaSoundNodeOptions) {
if (options.type === 'buffered') {
const sound = new BufferedSoundNode(options, this.ctx);
// @ts-ignore
this.soundNodes.push(sound);
sound.connect(this.gainNode);
} else if (options.type === 'mediaElement') {
const sound = new MediaElementSoundNode(options, this.ctx);
// @ts-ignore
this.soundNodes.push(sound);
sound.connect(this.gainNode);
}
}
removeSoundNode(node: SoundNode) {
removeSoundNode(node: TNode) {
node?.stop();
const index = this.soundNodes.indexOf(node);
if (index > -1) {
@@ -86,7 +99,6 @@ export abstract class SoundGroupNode<T extends SoundGroupNodeOptions> {
connect(destination: AudioNode) {
console.log('connecting', this, destination);
this.gainNode.connect(destination);
}
@@ -140,6 +152,10 @@ export abstract class SoundGroupNode<T extends SoundGroupNodeOptions> {
return this.deviceSources.get(remoteDevice)!;
}
deserializeOptions(options: T) {
this.options = options;
}
serializeOptions() {
return {
...this.options,

View File

@@ -1,23 +1,27 @@
import { MountedFs } from "../FileSystem/MountedFs";
import { VirtualFile } from "../FileSystem/types";
import { SoundboardContext } from "./types";
import { v4 as uuidv4 } from 'uuid';
export interface PartiaSoundNodeOptions {
id?: string;
vFile: VirtualFile;
volume?: number;
type: 'buffered' | 'mediaElement';
}
export interface SoundNodeOptions extends PartiaSoundNodeOptions {
id: string;
volume: number;
}
export abstract class SoundNode {
export abstract class SoundNode<T extends SoundNodeOptions = SoundNodeOptions> {
public readonly id: string;
protected gainNode: GainNode;
protected file?: File;
public options: SoundNodeOptions;
public options: T;
public readonly numberOfInputs = 0;
public readonly numberOfOutputs = 1;
protected activeSources: AudioBufferSourceNode[] = [];
@@ -29,10 +33,14 @@ export abstract class SoundNode {
options: PartiaSoundNodeOptions,
ctx: SoundboardContext,
) {
// @ts-ignore
this.options = {
id: uuidv4(),
...options,
volume: options.volume ?? 100
}
this.id = options.id!;
this.audioContext = ctx.audioContext;
this.fs = ctx.fs;
this.gainNode = this.audioContext.createGain();
@@ -58,7 +66,6 @@ export abstract class SoundNode {
}
connect(destination: AudioNode) {
console.log('connecting', this, destination);
this.gainNode.connect(destination);
}
@@ -72,6 +79,7 @@ export abstract class SoundNode {
setVolume(volume: number) {
this.gainNode.gain.value = volume / 100;
this.options.volume = volume;
}
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 source?: MediaElementAudioSourceNode;
private _isPlaying = false;
@@ -124,6 +136,9 @@ export class MediaElementSoundNode extends SoundNode {
await super.preload();
if (!this.mediaElement) {
this.mediaElement = document.createElement('audio');
if (this.options.loop) {
this.mediaElement.loop = true;
}
this.mediaElement.src = URL.createObjectURL(this.file!);
this.source = this.audioContext.createMediaElementSource(this.mediaElement);
this.source.connect(this.gainNode);
@@ -132,7 +147,6 @@ export class MediaElementSoundNode extends SoundNode {
this._isPlaying = false;
});
this.mediaElement.addEventListener('playing', (event) => {
console.log(event);
this._isPlaying = true;
});
this.mediaElement.addEventListener('pause', () => {

View File

@@ -38,7 +38,6 @@ export class TabSourceNode extends SoundGroupNode<TabSourceNodeOptions> {
surfaceSwitching: "include",
monitorTypeSurfaces: "include",
});
console.log(this.stream);
this.source = this.ctx.audioContext.createMediaStreamSource(this.stream);
this.source.connect(this.gainNode);
} catch (err) {

View File

@@ -128,12 +128,10 @@ export class WebRtcChannelSummon extends MessageEmitter implements JsonChannelSu
event.streams.forEach((stream) => {
stream.getTracks().forEach((track) => {
if (event.track.kind === 'audio') {
console.log('audio track', track);
const audioElement = document.createElement('audio');
audioElement.srcObject = stream; // Attach the received stream
audioElement.play();
} else if (event.track.kind === 'video') {
console.log('video track', track);
const videoEl = document.getElementById('mainVideo') as HTMLVideoElement;
if (videoEl) {
videoEl.srcObject = stream;

View File

@@ -47,7 +47,6 @@ export class RemoteDeviceManager {
}
async add(device: DeviceConfig, connect: boolean = false): Promise<void> {
console.log('add', device);
const instance = await this.createInstance(device);
if (!instance) {
return;

View File

@@ -16,10 +16,7 @@ export class SoundPlayer {
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) {
@@ -40,7 +37,6 @@ export class SoundPlayer {
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);

View File

@@ -108,7 +108,6 @@ const dynamicStyle = computed(() => {
function onVideoLoaded(event: Event) {
console.log('video loaded', event.target);
mediaDimensions.value = {
width: video.value!.videoWidth,
height: video.value!.videoHeight,

View File

@@ -13,7 +13,6 @@ import { JsonChannelSymbol, WebRtcChannelSymbol } from '../../model/injectionSym
const jsonMessages = ref<{source: 'json' | 'webrtc', message: Message}[]>([]);
const lastMessages = computed(() => [...jsonMessages.value].reverse().slice(0, 10));
console.log(jsonMessages, lastMessages);
const channel = inject(JsonChannelSymbol)!;
const webRTC = inject(WebRtcChannelSymbol)!;
channel.on((message) => {