Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 21f7df22d5 | |||
| 0744922d19 | |||
| a2414aee2f | |||
| a90d7c8f8c |
13
.dockerignore
Normal file
@@ -0,0 +1,13 @@
|
||||
.git
|
||||
.nuxt
|
||||
.output
|
||||
.data
|
||||
node_modules
|
||||
dist
|
||||
dist-electron
|
||||
native-app
|
||||
coverage
|
||||
*.log
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
@@ -3,13 +3,13 @@
|
||||
FROM node:22-alpine AS build
|
||||
WORKDIR /app
|
||||
|
||||
RUN corepack enable
|
||||
RUN corepack enable && corepack prepare pnpm@10.33.0 --activate
|
||||
|
||||
# Copy package.json and your lockfile, here we add pnpm-lock.yaml for illustration
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
# Install dependencies
|
||||
RUN pnpm i
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Copy the entire project
|
||||
COPY . ./
|
||||
|
||||
@@ -1 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" aria-label="Logo" role="img" viewBox="0 0 176 174"><path fill-rule="evenodd" d="M47.0,154.5 L8.5,153.0 L8.5,105.0 L28.0,90.5 L29.0,95.5 L68.0,95.5 L107.5,66.0 L107.5,31.0 L110.0,28.5 L128.0,15.5 L166.0,15.5 L167.5,65.0 L148.0,79.5 L147.0,74.5 L105.0,76.5 L68.5,104.0 L68.5,139.0 Z M29.0,89.5 L28.5,46.0 L32.0,42.5 L69.0,15.5 L107.5,16.0 L107.5,30.0 L87.5,46.0 L87.0,74.5 L49.0,74.5 Z M107.0,154.5 L68.5,154.0 L68.5,140.0 L88.5,124.0 L88.5,96.0 L127.0,95.5 L147.0,80.5 L147.5,124.0 Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 176 174"
|
||||
fill="currentColor"
|
||||
role="img"
|
||||
aria-label="Logo">
|
||||
|
||||
<path fill-rule="evenodd" d="
|
||||
M47.0,154.5 L8.5,153.0 L8.5,105.0 L28.0,90.5 L29.0,95.5 L68.0,95.5 L107.5,66.0
|
||||
L107.5,31.0 L110.0,28.5 L128.0,15.5 L166.0,15.5 L167.5,65.0 L148.0,79.5
|
||||
L147.0,74.5 L105.0,76.5 L68.5,104.0 L68.5,139.0 Z
|
||||
|
||||
M29.0,89.5 L28.5,46.0 L32.0,42.5 L69.0,15.5 L107.5,16.0 L107.5,30.0
|
||||
L87.5,46.0 L87.0,74.5 L49.0,74.5 Z
|
||||
|
||||
M107.0,154.5 L68.5,154.0 L68.5,140.0 L88.5,124.0 L88.5,96.0 L127.0,95.5
|
||||
L147.0,80.5 L147.5,124.0 Z
|
||||
"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 551 B After Width: | Height: | Size: 611 B |
@@ -14,6 +14,7 @@ import { useStreamerStore } from "~/state/streamer";
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const selectedValue = ref("");
|
||||
const modelValue = defineModel<string>({ default: "" });
|
||||
const presets = ref<PresetUser[]>([]);
|
||||
const loading = ref(true);
|
||||
const streamerStore = useStreamerStore();
|
||||
@@ -27,6 +28,7 @@ onMounted(async () => {
|
||||
const defaultPreset = presets.value.find((p) => p.isDefault);
|
||||
if (defaultPreset) {
|
||||
selectedValue.value = defaultPreset.presetId;
|
||||
modelValue.value = defaultPreset.presetId;
|
||||
// Load the default preset's ice servers
|
||||
loadPresetIceServers(defaultPreset.presetId);
|
||||
}
|
||||
@@ -60,9 +62,19 @@ watch(selectedValue, (newValue) => {
|
||||
if (newValue === "create-new") {
|
||||
router.push("/presets/new");
|
||||
selectedValue.value = "";
|
||||
modelValue.value = "";
|
||||
} else if (newValue) {
|
||||
modelValue.value = newValue;
|
||||
// Load ice servers for the selected preset
|
||||
loadPresetIceServers(newValue);
|
||||
} else {
|
||||
modelValue.value = "";
|
||||
}
|
||||
});
|
||||
|
||||
watch(modelValue, (newValue) => {
|
||||
if (newValue !== selectedValue.value) {
|
||||
selectedValue.value = newValue;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -24,6 +24,13 @@ export interface VenmicLinkOptions {
|
||||
only_default_speakers?: boolean;
|
||||
}
|
||||
|
||||
export interface StreamingClosePromptOptions {
|
||||
title: string;
|
||||
message: string;
|
||||
confirmLabel: string;
|
||||
cancelLabel: string;
|
||||
}
|
||||
|
||||
interface HeliumElectronAPI {
|
||||
isElectron: boolean;
|
||||
getPlatform: () => Promise<PlatformInfo>;
|
||||
@@ -37,6 +44,10 @@ interface HeliumElectronAPI {
|
||||
venmicUnlink: () => Promise<boolean>;
|
||||
checkScreenPermission: () => Promise<string>;
|
||||
openScreenPermissionSettings: () => Promise<boolean>;
|
||||
setStreamingActive: (
|
||||
active: boolean,
|
||||
promptOptions?: StreamingClosePromptOptions,
|
||||
) => Promise<boolean>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
@@ -235,6 +246,23 @@ export function useElectron() {
|
||||
}
|
||||
};
|
||||
|
||||
const setStreamingActive = async (
|
||||
active: boolean,
|
||||
promptOptions?: StreamingClosePromptOptions,
|
||||
): Promise<boolean> => {
|
||||
if (!checkElectron()) return false;
|
||||
|
||||
try {
|
||||
return await window.heliumElectron!.setStreamingActive(
|
||||
active,
|
||||
promptOptions,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("[useElectron] Failed to set streaming status:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
checkElectron();
|
||||
if (isElectron.value) {
|
||||
@@ -271,6 +299,7 @@ export function useElectron() {
|
||||
unlinkVenmicAudio,
|
||||
getScreenPermissionStatus,
|
||||
openScreenPermissionSettings,
|
||||
setStreamingActive,
|
||||
|
||||
startScreenShareWithAudio,
|
||||
stopScreenShare,
|
||||
|
||||
@@ -32,10 +32,14 @@ onMounted(async () => {
|
||||
const navLinks = [
|
||||
{ to: "/", label: "home" },
|
||||
{ to: "/stream", label: "stream" },
|
||||
{ to: "/about", label: "about" },
|
||||
{ to: "/downloads", label: "downloads" },
|
||||
{ to: "/about", label: "about", hideInElectron: true },
|
||||
{ to: "/downloads", label: "downloads", hideInElectron: true },
|
||||
{ to: "/presets", label: "presets", requiresAuth: true },
|
||||
];
|
||||
|
||||
const visibleNavLinks = computed(() => {
|
||||
return navLinks.filter((link) => !isElectron.value || !link.hideInElectron);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -56,7 +60,7 @@ const navLinks = [
|
||||
<span class="leading-none">helium</span>
|
||||
</NuxtLink>
|
||||
<nav class="hidden md:flex space-x-4">
|
||||
<template v-for="link in navLinks" :key="link.to">
|
||||
<template v-for="link in visibleNavLinks" :key="link.to">
|
||||
<ClientOnly v-if="link.requiresAuth">
|
||||
<SignedIn>
|
||||
<NuxtLink
|
||||
@@ -114,7 +118,7 @@ const navLinks = [
|
||||
<SheetTitle>{{ t("menu") || "Menu" }}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<nav class="flex flex-col space-y-4 mt-6">
|
||||
<template v-for="link in navLinks" :key="link.to">
|
||||
<template v-for="link in visibleNavLinks" :key="link.to">
|
||||
<ClientOnly v-if="link.requiresAuth">
|
||||
<SignedIn>
|
||||
<NuxtLink
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
const { locale } = useI18n();
|
||||
const clerkLocaleVersion = useState("clerk-locale-version", () => 0);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full w-full items-center justify-center">
|
||||
<SignIn routing="path" path="/sign-in" />
|
||||
<SignIn :key="`${locale}-${clerkLocaleVersion}`" routing="path" path="/sign-in" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
const { locale } = useI18n();
|
||||
const clerkLocaleVersion = useState("clerk-locale-version", () => 0);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full w-full items-center justify-center">
|
||||
<SignUp routing="path" path="/sign-up" />
|
||||
<SignUp :key="`${locale}-${clerkLocaleVersion}`" routing="path" path="/sign-up" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,56 +1,189 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center justify-center gap-6 mt-10 px-4">
|
||||
<div class="flex flex-wrap gap-4 items-center justify-center">
|
||||
<Button v-if="!localStream" @click="startScreenShare">
|
||||
{{ $t("screenshare") }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="localStream"
|
||||
@click="changeScreenShareSource"
|
||||
variant="outline"
|
||||
>
|
||||
{{ $t("changeSource") }}
|
||||
</Button>
|
||||
<PresetSelect />
|
||||
<div
|
||||
class="min-h-[80vh] flex flex-col items-center justify-start gap-8 mt-10 px-4 pb-16"
|
||||
>
|
||||
<div class="text-center space-y-2">
|
||||
<h1 class="text-4xl font-bold tracking-tight">{{ $t("stream") }}</h1>
|
||||
</div>
|
||||
|
||||
<div v-if="isElectron && supportsAudioScreenShare" class="flex flex-col items-center gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch id="include-audio" v-model:checked="includeAudio" />
|
||||
<Label for="include-audio" class="text-sm">{{ $t("includeAudio") }}</Label>
|
||||
</div>
|
||||
|
||||
<div v-if="platformInfo?.isLinux && platformInfo?.supportsVenmic && includeAudio" class="flex flex-col items-center gap-2">
|
||||
<Select v-model="selectedAudioSource">
|
||||
<SelectTrigger class="w-[200px]">
|
||||
<SelectValue :placeholder="$t('audioSource')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">{{ $t("allSystemAudio") }}</SelectItem>
|
||||
<SelectItem
|
||||
v-for="source in audioSources"
|
||||
:key="source['node.name']"
|
||||
:value="source['application.name']! || source['node.name']!"
|
||||
<div
|
||||
class="w-full max-w-5xl grid grid-cols-1 lg:grid-cols-[380px_1fr] gap-6 items-start"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-base flex items-center gap-2">
|
||||
<Monitor class="size-4 text-primary" />
|
||||
{{ $t("screenshare") }}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{{ $t("streamControlDescription") }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<Button
|
||||
v-if="!localStream"
|
||||
@click="startScreenShare"
|
||||
size="lg"
|
||||
class="w-full gap-2"
|
||||
>
|
||||
{{ source['application.name'] || source['node.name'] }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="ghost" size="sm" @click="refreshAudioSources">
|
||||
{{ $t("refreshSources") }}
|
||||
</Button>
|
||||
<Monitor class="size-4" />
|
||||
{{ $t("screenshare") }}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
v-if="localStream"
|
||||
@click="changeScreenShareSource"
|
||||
variant="outline"
|
||||
size="lg"
|
||||
class="w-full gap-2"
|
||||
>
|
||||
<RefreshCw class="size-4" />
|
||||
{{ $t("changeSource") }}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
v-if="localStream"
|
||||
@click="stopStreaming"
|
||||
variant="destructive"
|
||||
size="lg"
|
||||
class="w-full gap-2"
|
||||
>
|
||||
<Square class="size-4" />
|
||||
{{ $t("stopStream") }}
|
||||
</Button>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label
|
||||
class="text-xs text-muted-foreground uppercase tracking-wider font-semibold"
|
||||
>
|
||||
{{ $t("selectAPreset") }}
|
||||
</Label>
|
||||
<PresetSelect v-model="selectedPresetId" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card v-if="streamerStore.code" class="border-primary/30">
|
||||
<CardHeader class="pb-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<CardTitle class="text-base flex items-center gap-2">
|
||||
<Share2 class="size-4 text-primary" />
|
||||
{{ $t("shareCode") }}
|
||||
</CardTitle>
|
||||
<Badge class="text-xs bg-green-500/15 text-green-600 dark:text-green-400 border-0">
|
||||
● {{ $t("live") }}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-3">
|
||||
<p
|
||||
class="font-mono text-5xl font-bold tracking-[0.3em] text-primary text-center py-2"
|
||||
>
|
||||
{{ streamerStore.code }}
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="w-full gap-2"
|
||||
@click="copyCode"
|
||||
>
|
||||
<Copy class="size-3" />
|
||||
{{ $t("copyCode") }}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card v-if="isElectron && supportsAudioScreenShare">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-base flex items-center gap-2">
|
||||
<Radio class="size-4 text-primary" />
|
||||
{{ $t("includeAudio") }}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch id="include-audio" v-model="includeAudio" />
|
||||
<Label for="include-audio" class="text-sm font-normal">
|
||||
{{ $t("includeAudio") }}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="platformInfo?.isLinux && platformInfo?.supportsVenmic && includeAudio"
|
||||
class="space-y-3 pt-2 border-t border-border"
|
||||
>
|
||||
<Select v-model="selectedAudioSource">
|
||||
<SelectTrigger class="w-full">
|
||||
<SelectValue :placeholder="$t('audioSource')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">
|
||||
{{ $t("allSystemAudio") }}
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
v-for="source in audioSources"
|
||||
:key="source['node.name']"
|
||||
:value="
|
||||
source['application.name']! || source['node.name']!
|
||||
"
|
||||
>
|
||||
{{ source['application.name'] || source['node.name'] }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="refreshAudioSources"
|
||||
class="gap-2 w-full"
|
||||
>
|
||||
<RefreshCw class="size-3" />
|
||||
{{ $t("refreshSources") }}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="relative rounded-xl overflow-hidden border shadow-sm"
|
||||
:class="[
|
||||
localStream
|
||||
? 'aspect-video bg-black'
|
||||
: 'aspect-video bg-muted/50 flex items-center justify-center',
|
||||
]"
|
||||
>
|
||||
<video
|
||||
ref="videofeedRef"
|
||||
autoplay
|
||||
playsinline
|
||||
muted
|
||||
class="w-full h-full object-contain"
|
||||
:class="{ hidden: !localStream }"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="!localStream"
|
||||
class="flex flex-col items-center gap-3 text-muted-foreground"
|
||||
>
|
||||
<div
|
||||
class="size-16 rounded-full bg-muted flex items-center justify-center"
|
||||
>
|
||||
<Monitor class="size-8 opacity-40" />
|
||||
</div>
|
||||
<div class="text-center space-y-1">
|
||||
<p class="text-sm font-medium">{{ $t("previewWaiting") }}</p>
|
||||
<p class="text-xs opacity-60">{{ $t("previewWaitingDescription") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isElectron" class="text-xs text-muted-foreground">
|
||||
<span v-if="platformInfo?.isWindows">Windows</span>
|
||||
<span v-else-if="platformInfo?.isMac">macOS</span>
|
||||
<span v-else-if="platformInfo?.isLinux">Linux</span>
|
||||
<span v-if="supportsAudioScreenShare" class="ml-2">• {{ $t("audioSupported") }}</span>
|
||||
</div>
|
||||
|
||||
<p v-if="streamerStore.code" class="font-mono text-lg">{{ streamerStore.code }}</p>
|
||||
<video ref="videofeedRef" autoplay playsinline muted class="max-w-full rounded-lg"></video>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -60,20 +193,45 @@ import { toast } from "vue-sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from "@/components/ui/card";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
Copy,
|
||||
Monitor,
|
||||
Radio,
|
||||
RefreshCw,
|
||||
Share2,
|
||||
Square,
|
||||
} from "lucide-vue-next";
|
||||
import { useStreamerStore } from "~/state/streamer";
|
||||
import { useWebSocketUrl } from "~/composables/useWebSocketUrl";
|
||||
import { useElectron } from "~/composables/useElectron";
|
||||
import PresetSelect from "~/components/app/PresetSelect.vue";
|
||||
|
||||
const streamerStore = useStreamerStore();
|
||||
const { t } = useI18n();
|
||||
const { t, locale } = useI18n();
|
||||
const videofeedRef = ref<HTMLVideoElement | null>(null);
|
||||
const localStream = ref<MediaStream | null>(null);
|
||||
const isCleaningUp = ref(false);
|
||||
const wsUrl = useWebSocketUrl();
|
||||
|
||||
const includeAudio = ref(true);
|
||||
const selectedAudioSource = ref("all");
|
||||
const selectedPresetId = ref("");
|
||||
|
||||
const {
|
||||
isElectron,
|
||||
@@ -87,19 +245,68 @@ const {
|
||||
unlinkVenmicAudio,
|
||||
getScreenPermissionStatus,
|
||||
openScreenPermissionSettings,
|
||||
setStreamingActive,
|
||||
} = useElectron();
|
||||
|
||||
onMounted(async () => {
|
||||
await getPlatformInfo();
|
||||
|
||||
if (platformInfo.value?.isLinux && platformInfo.value?.supportsVenmic) {
|
||||
await refreshAudioSources();
|
||||
}
|
||||
});
|
||||
|
||||
watch(localStream, async (stream) => {
|
||||
await nextTick();
|
||||
|
||||
if (videofeedRef.value) {
|
||||
videofeedRef.value.srcObject = stream;
|
||||
}
|
||||
});
|
||||
|
||||
watch([localStream, locale], async ([stream]) => {
|
||||
if (!isElectron.value) return;
|
||||
|
||||
await setStreamingActive(
|
||||
!!stream,
|
||||
stream
|
||||
? {
|
||||
title: t("activeStreamCloseTitle"),
|
||||
message: t("activeStreamCloseMessage"),
|
||||
confirmLabel: t("stopStreamAndClose"),
|
||||
cancelLabel: t("keepStreaming"),
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
});
|
||||
|
||||
async function refreshAudioSources() {
|
||||
await getVenmicSources();
|
||||
}
|
||||
|
||||
async function copyCode() {
|
||||
await navigator.clipboard.writeText(streamerStore.code);
|
||||
toast.success(t("codeCopied"));
|
||||
}
|
||||
|
||||
function notifyRoomClosed() {
|
||||
if (!streamerStore.code) return;
|
||||
|
||||
send(
|
||||
JSON.stringify({
|
||||
event: "close-room",
|
||||
roomId: streamerStore.code,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function stopStreaming() {
|
||||
if (isCleaningUp.value) return;
|
||||
|
||||
notifyRoomClosed();
|
||||
await cleanupStreaming();
|
||||
}
|
||||
|
||||
const { send, close: closeWebSocket } = useWebSocket(wsUrl, {
|
||||
autoReconnect: true,
|
||||
heartbeat: {
|
||||
@@ -189,9 +396,17 @@ const { send, close: closeWebSocket } = useWebSocket(wsUrl, {
|
||||
});
|
||||
|
||||
async function startScreenShare() {
|
||||
if (!selectedPresetId.value) {
|
||||
toast.error(t("selectPresetBeforeStreaming"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const isLinuxWithVenmic = isElectron.value && platformInfo.value?.isLinux && platformInfo.value?.supportsVenmic;
|
||||
|
||||
const isLinuxWithVenmic =
|
||||
isElectron.value &&
|
||||
platformInfo.value?.isLinux &&
|
||||
platformInfo.value?.supportsVenmic;
|
||||
|
||||
if (isLinuxWithVenmic && includeAudio.value) {
|
||||
if (selectedAudioSource.value === "all") {
|
||||
await linkAllAudio();
|
||||
@@ -200,33 +415,34 @@ async function startScreenShare() {
|
||||
}
|
||||
}
|
||||
|
||||
const shouldRequestAudio = isElectron.value && includeAudio.value && supportsAudioScreenShare.value;
|
||||
const shouldRequestAudio =
|
||||
isElectron.value && includeAudio.value && supportsAudioScreenShare.value;
|
||||
|
||||
const stream = await navigator.mediaDevices.getDisplayMedia({
|
||||
video: true,
|
||||
audio: shouldRequestAudio ? {
|
||||
echoCancellation: false,
|
||||
noiseSuppression: false,
|
||||
autoGainControl: false,
|
||||
} : false,
|
||||
audio: shouldRequestAudio
|
||||
? {
|
||||
echoCancellation: false,
|
||||
noiseSuppression: false,
|
||||
autoGainControl: false,
|
||||
}
|
||||
: false,
|
||||
});
|
||||
|
||||
localStream.value = stream;
|
||||
|
||||
if (videofeedRef.value) {
|
||||
videofeedRef.value.srcObject = stream;
|
||||
}
|
||||
|
||||
stream.getTracks().forEach((track) => {
|
||||
track.onended = () => {
|
||||
console.log("Screen sharing stopped by user");
|
||||
cleanupStreaming();
|
||||
stopStreaming();
|
||||
};
|
||||
});
|
||||
|
||||
const videoTracks = stream.getVideoTracks();
|
||||
const audioTracks = stream.getAudioTracks();
|
||||
console.log(`[Helium] Stream started - Video: ${videoTracks.length}, Audio: ${audioTracks.length}`);
|
||||
console.log(
|
||||
`[Helium] Stream started - Video: ${videoTracks.length}, Audio: ${audioTracks.length}`,
|
||||
);
|
||||
|
||||
send(
|
||||
JSON.stringify({
|
||||
@@ -242,8 +458,11 @@ async function startScreenShare() {
|
||||
|
||||
async function changeScreenShareSource() {
|
||||
try {
|
||||
const isLinuxWithVenmic = isElectron.value && platformInfo.value?.isLinux && platformInfo.value?.supportsVenmic;
|
||||
|
||||
const isLinuxWithVenmic =
|
||||
isElectron.value &&
|
||||
platformInfo.value?.isLinux &&
|
||||
platformInfo.value?.supportsVenmic;
|
||||
|
||||
if (isLinuxWithVenmic && includeAudio.value) {
|
||||
if (selectedAudioSource.value === "all") {
|
||||
await linkAllAudio();
|
||||
@@ -252,7 +471,8 @@ async function changeScreenShareSource() {
|
||||
}
|
||||
}
|
||||
|
||||
const shouldRequestAudio = isElectron.value && includeAudio.value && supportsAudioScreenShare.value;
|
||||
const shouldRequestAudio =
|
||||
isElectron.value && includeAudio.value && supportsAudioScreenShare.value;
|
||||
|
||||
const newStream = await navigator.mediaDevices.getDisplayMedia({
|
||||
video: true,
|
||||
@@ -266,19 +486,23 @@ async function changeScreenShareSource() {
|
||||
|
||||
newVideoTrack!.onended = () => {
|
||||
console.log("Screen sharing stopped by user");
|
||||
cleanupStreaming();
|
||||
stopStreaming();
|
||||
};
|
||||
|
||||
Object.values(streamerStore.peerConnections).forEach((pc) => {
|
||||
const senders = pc.getSenders();
|
||||
|
||||
const videoSender = senders.find((sender) => sender.track?.kind === "video");
|
||||
|
||||
const videoSender = senders.find(
|
||||
(sender) => sender.track?.kind === "video",
|
||||
);
|
||||
if (videoSender) {
|
||||
videoSender.replaceTrack(newVideoTrack!);
|
||||
}
|
||||
|
||||
|
||||
if (newAudioTrack) {
|
||||
const audioSender = senders.find((sender) => sender.track?.kind === "audio");
|
||||
const audioSender = senders.find(
|
||||
(sender) => sender.track?.kind === "audio",
|
||||
);
|
||||
if (audioSender) {
|
||||
audioSender.replaceTrack(newAudioTrack);
|
||||
} else {
|
||||
@@ -292,10 +516,6 @@ async function changeScreenShareSource() {
|
||||
});
|
||||
|
||||
localStream.value = newStream;
|
||||
|
||||
if (videofeedRef.value) {
|
||||
videofeedRef.value.srcObject = newStream;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to change screen share source:", error);
|
||||
await handleScreenShareError(error);
|
||||
@@ -306,7 +526,11 @@ async function handleScreenShareError(error: unknown): Promise<void> {
|
||||
const isPermissionDeniedError =
|
||||
error instanceof DOMException && error.name === "NotAllowedError";
|
||||
|
||||
if (!isPermissionDeniedError || !isElectron.value || !platformInfo.value?.isMac) {
|
||||
if (
|
||||
!isPermissionDeniedError ||
|
||||
!isElectron.value ||
|
||||
!platformInfo.value?.isMac
|
||||
) {
|
||||
toast.error(t("failedToStartScreenShare"));
|
||||
return;
|
||||
}
|
||||
@@ -329,28 +553,36 @@ async function handleScreenShareError(error: unknown): Promise<void> {
|
||||
}
|
||||
|
||||
async function cleanupStreaming() {
|
||||
if (localStream.value) {
|
||||
localStream.value.getTracks().forEach((track) => {
|
||||
track.stop();
|
||||
if (isCleaningUp.value) return;
|
||||
|
||||
isCleaningUp.value = true;
|
||||
|
||||
try {
|
||||
if (localStream.value) {
|
||||
localStream.value.getTracks().forEach((track) => {
|
||||
track.stop();
|
||||
});
|
||||
localStream.value = null;
|
||||
}
|
||||
|
||||
if (isElectron.value && platformInfo.value?.isLinux) {
|
||||
await unlinkVenmicAudio();
|
||||
}
|
||||
|
||||
Object.values(streamerStore.peerConnections).forEach((pc) => {
|
||||
pc.close();
|
||||
});
|
||||
localStream.value = null;
|
||||
|
||||
streamerStore.clearPeerConnections();
|
||||
|
||||
if (videofeedRef.value) {
|
||||
videofeedRef.value.srcObject = null;
|
||||
}
|
||||
|
||||
streamerStore.setCode("");
|
||||
} finally {
|
||||
isCleaningUp.value = false;
|
||||
}
|
||||
|
||||
if (isElectron.value && platformInfo.value?.isLinux) {
|
||||
await unlinkVenmicAudio();
|
||||
}
|
||||
|
||||
Object.values(streamerStore.peerConnections).forEach((pc) => {
|
||||
pc.close();
|
||||
});
|
||||
|
||||
streamerStore.clearPeerConnections();
|
||||
|
||||
if (videofeedRef.value) {
|
||||
videofeedRef.value.srcObject = null;
|
||||
}
|
||||
|
||||
streamerStore.setCode("");
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
|
||||
@@ -1,44 +1,106 @@
|
||||
import { updateClerkOptions } from "#imports";
|
||||
import { esES } from "@clerk/localizations";
|
||||
import { shadcn } from "@clerk/themes";
|
||||
import type { Ref } from "vue";
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
const i18n = nuxtApp.$i18n as any;
|
||||
interface ClerkLocaleOptions {
|
||||
localization: typeof esES | undefined;
|
||||
appearance: {
|
||||
theme: typeof shadcn;
|
||||
};
|
||||
}
|
||||
|
||||
if (!i18n) return;
|
||||
interface I18nRuntime {
|
||||
locale: Ref<string>;
|
||||
}
|
||||
|
||||
interface MutablePublicConfig {
|
||||
clerk?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
function getClerkLocaleOptions(locale: string): ClerkLocaleOptions {
|
||||
return {
|
||||
localization: locale === "es" ? esES : undefined,
|
||||
appearance: {
|
||||
theme: shadcn,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getCookieValue(name: string): string | undefined {
|
||||
const cookie = document.cookie
|
||||
.split("; ")
|
||||
.find((entry) => entry.startsWith(`${name}=`));
|
||||
|
||||
return cookie ? decodeURIComponent(cookie.split("=")[1] ?? "") : undefined;
|
||||
}
|
||||
|
||||
function getInitialLocale(): string {
|
||||
const cookieLocale = getCookieValue("i18n_locale");
|
||||
if (cookieLocale) return cookieLocale;
|
||||
|
||||
if (navigator.language.toLowerCase().startsWith("es")) {
|
||||
return "es";
|
||||
}
|
||||
|
||||
return "en";
|
||||
}
|
||||
|
||||
export default defineNuxtPlugin({
|
||||
name: "clerk-locale",
|
||||
enforce: "pre",
|
||||
setup(nuxtApp) {
|
||||
const runtimeConfig = useRuntimeConfig();
|
||||
const clerkLocaleVersion = useState("clerk-locale-version", () => 0);
|
||||
const publicConfig = runtimeConfig.public as MutablePublicConfig;
|
||||
const initialLocale = getInitialLocale();
|
||||
|
||||
function updateRuntimeConfig(options: ClerkLocaleOptions): void {
|
||||
publicConfig.clerk = {
|
||||
...(publicConfig.clerk ?? {}),
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
updateRuntimeConfig(getClerkLocaleOptions(initialLocale));
|
||||
|
||||
function updateLocale(locale: string, attempts = 0): void {
|
||||
const options = getClerkLocaleOptions(locale);
|
||||
|
||||
updateRuntimeConfig(options);
|
||||
|
||||
nuxtApp.hook("app:mounted", () => {
|
||||
const checkClerk = () => {
|
||||
try {
|
||||
const testUpdate = () => {
|
||||
updateClerkOptions({
|
||||
localization: i18n.locale.value === "es" ? esES : undefined,
|
||||
appearance: {
|
||||
theme: shadcn,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
testUpdate();
|
||||
|
||||
watch(
|
||||
() => i18n.locale.value,
|
||||
(newLocale) => {
|
||||
const clerkLocale = newLocale === "es" ? esES : undefined;
|
||||
|
||||
updateClerkOptions({
|
||||
localization: clerkLocale,
|
||||
appearance: {
|
||||
theme: shadcn,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
setTimeout(checkClerk, 100);
|
||||
updateClerkOptions(options);
|
||||
clerkLocaleVersion.value += 1;
|
||||
} catch {
|
||||
if (attempts < 20) {
|
||||
setTimeout(() => updateLocale(locale, attempts + 1), 100);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
checkClerk();
|
||||
});
|
||||
nuxtApp.hook("i18n:beforeLocaleSwitch", (options) => {
|
||||
updateLocale(options.newLocale);
|
||||
});
|
||||
|
||||
nuxtApp.hook("i18n:localeSwitched", (options) => {
|
||||
updateLocale(options.newLocale);
|
||||
});
|
||||
|
||||
nuxtApp.hook("app:mounted", () => {
|
||||
const i18n = nuxtApp.$i18n as I18nRuntime | undefined;
|
||||
if (!i18n) {
|
||||
updateLocale(initialLocale);
|
||||
return;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => i18n.locale.value,
|
||||
(locale) => {
|
||||
updateLocale(locale);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
BIN
build/icon.png
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 139 KiB |
@@ -3,6 +3,7 @@ import {
|
||||
BrowserWindow,
|
||||
ipcMain,
|
||||
desktopCapturer,
|
||||
dialog,
|
||||
session,
|
||||
shell,
|
||||
systemPreferences,
|
||||
@@ -48,6 +49,13 @@ const NUXT_DEV_URL = process.env.NUXT_DEV_URL || 'http://localhost:3000';
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let venmicManager: VenmicManager | null = null;
|
||||
let isStreamingActive = false;
|
||||
let streamingClosePrompt = {
|
||||
title: 'Active stream',
|
||||
message: 'A stream is still active. Closing Helium will stop it for all viewers.',
|
||||
confirmLabel: 'Stop stream and close',
|
||||
cancelLabel: 'Keep streaming',
|
||||
};
|
||||
|
||||
console.log('[Helium] Platform:', process.platform);
|
||||
console.log('[Helium] Wayland:', isWayland);
|
||||
@@ -89,6 +97,27 @@ function createWindow(): void {
|
||||
mainWindow.loadURL(prodUrl);
|
||||
}
|
||||
|
||||
mainWindow.on('close', (event) => {
|
||||
if (!isStreamingActive || !mainWindow) return;
|
||||
|
||||
const choice = dialog.showMessageBoxSync(mainWindow, {
|
||||
type: 'warning',
|
||||
title: streamingClosePrompt.title,
|
||||
message: streamingClosePrompt.message,
|
||||
buttons: [streamingClosePrompt.cancelLabel, streamingClosePrompt.confirmLabel],
|
||||
defaultId: 0,
|
||||
cancelId: 0,
|
||||
noLink: true,
|
||||
});
|
||||
|
||||
if (choice === 0) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
isStreamingActive = false;
|
||||
});
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
@@ -152,7 +181,7 @@ ipcMain.handle('helium:get-platform', () => {
|
||||
isWindows,
|
||||
isWayland,
|
||||
isElectron: true,
|
||||
supportsLoopbackAudio: isWindows || isMac,
|
||||
supportsLoopbackAudio: isWindows || isMac || isLinux,
|
||||
supportsVenmic: isLinux && (venmicManager?.isAvailable() ?? false),
|
||||
};
|
||||
});
|
||||
@@ -223,6 +252,19 @@ ipcMain.handle('helium:open-screen-permission-settings', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle(
|
||||
'helium:set-streaming-active',
|
||||
(_event: IpcMainInvokeEvent, active: boolean, promptOptions?: typeof streamingClosePrompt) => {
|
||||
isStreamingActive = active;
|
||||
|
||||
if (promptOptions) {
|
||||
streamingClosePrompt = promptOptions;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
|
||||
if (!gotTheLock) {
|
||||
|
||||
@@ -35,6 +35,13 @@ export interface VenmicLinkOptions {
|
||||
only_default_speakers?: boolean;
|
||||
}
|
||||
|
||||
export interface StreamingClosePromptOptions {
|
||||
title: string;
|
||||
message: string;
|
||||
confirmLabel: string;
|
||||
cancelLabel: string;
|
||||
}
|
||||
|
||||
const heliumElectronAPI = {
|
||||
isElectron: true as const,
|
||||
getPlatform: (): Promise<PlatformInfo> => ipcRenderer.invoke('helium:get-platform'),
|
||||
@@ -59,6 +66,10 @@ const heliumElectronAPI = {
|
||||
checkScreenPermission: (): Promise<string> => ipcRenderer.invoke('helium:check-screen-permission'),
|
||||
openScreenPermissionSettings: (): Promise<boolean> =>
|
||||
ipcRenderer.invoke('helium:open-screen-permission-settings'),
|
||||
setStreamingActive: (
|
||||
active: boolean,
|
||||
promptOptions?: StreamingClosePromptOptions,
|
||||
): Promise<boolean> => ipcRenderer.invoke('helium:set-streaming-active', active, promptOptions),
|
||||
};
|
||||
|
||||
contextBridge.exposeInMainWorld('heliumElectron', heliumElectronAPI);
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
{
|
||||
"welcome": "Welcome",
|
||||
"language": "Language",
|
||||
"selectLanguage": "Select language",
|
||||
"home": "Home",
|
||||
"about": "About",
|
||||
"contact": "Contact",
|
||||
"downloads": "Downloads",
|
||||
"stream": "Stream",
|
||||
"streamControlDescription": "Choose a source, tune your connection preset, then share the room code.",
|
||||
"shareCode": "Stream code",
|
||||
"copyCode": "Copy code",
|
||||
"codeCopied": "Code copied to clipboard",
|
||||
"live": "Live",
|
||||
"previewWaiting": "Preview waiting",
|
||||
"previewWaitingDescription": "Your selected screen will appear here once sharing starts.",
|
||||
"presets": "Presets",
|
||||
"effortlessScreensharing": "effortless screensharing powered by webrtc",
|
||||
"hostInstead": "stream instead?",
|
||||
@@ -18,6 +22,7 @@
|
||||
"screenshare": "Screenshare",
|
||||
"loadingPresets": "Loading presets...",
|
||||
"selectAPreset": "Select a preset",
|
||||
"selectPresetBeforeStreaming": "Select a preset before starting a stream.",
|
||||
"noPresetsAvailable": "No presets available",
|
||||
"default": "default",
|
||||
"createNewPreset": "Create New Preset",
|
||||
@@ -56,11 +61,15 @@
|
||||
"presetSharingDisabled": "The preset owner has not enabled sharing for this preset.",
|
||||
"goBack": "Go Back",
|
||||
"changeSource": "Change Source",
|
||||
"stopStream": "Stop Stream",
|
||||
"activeStreamCloseTitle": "Active stream",
|
||||
"activeStreamCloseMessage": "A stream is still active. Closing Helium will stop it for all viewers.",
|
||||
"stopStreamAndClose": "Stop stream and close",
|
||||
"keepStreaming": "Keep streaming",
|
||||
"includeAudio": "Include Audio",
|
||||
"audioSource": "Audio Source",
|
||||
"allSystemAudio": "All System Audio",
|
||||
"refreshSources": "Refresh Sources",
|
||||
"audioSupported": "Audio Supported",
|
||||
"failedToStartScreenShare": "Failed to start screen share.",
|
||||
"screenRecordingPermissionRequired": "macOS blocked screen capture. Allow Helium in System Settings > Privacy & Security > Screen Recording, then restart Helium.",
|
||||
"screenRecordingPermissionRequiredNoShortcut": "macOS blocked screen capture. Open System Settings > Privacy & Security > Screen Recording, allow Helium, then restart Helium.",
|
||||
@@ -70,11 +79,8 @@
|
||||
"desktopAppDescription": "The full-featured Electron app with system audio support.",
|
||||
"androidApp": "Android App",
|
||||
"androidAppDescription": "Stream directly from your Android device.",
|
||||
"desktopAppNote": "Includes advanced features like system audio capture, venmic support on Linux, and native screen recording permissions.",
|
||||
"androidAppNote": "Install the APK directly on your Android device. Make sure to allow installation from unknown sources if prompted.",
|
||||
"desktopAppNote": "Includes advanced features like system audio capture and native screen recording permissions.",
|
||||
"androidAppNote": "Install the APK directly on your Android device.",
|
||||
"downloadFromGitHub": "Download from GitHub",
|
||||
"viewSourceCode": "View Source Code",
|
||||
"preferTheBrowser": "Prefer the browser?",
|
||||
"browserVersionDescription": "The web version works great too. No installation required — just open the page and start streaming or watching.",
|
||||
"useWebVersion": "Use Web Version"
|
||||
"viewSourceCode": "View Source Code"
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
{
|
||||
"welcome": "Bienvenido",
|
||||
"language": "Idioma",
|
||||
"selectLanguage": "Seleccionar idioma",
|
||||
"home": "Inicio",
|
||||
"about": "Acerca de",
|
||||
"contact": "Contacto",
|
||||
"downloads": "Descargas",
|
||||
"stream": "Transmisión",
|
||||
"streamControlDescription": "Elige una fuente, ajusta el preset de conexión y comparte el código de la sala.",
|
||||
"shareCode": "Código de la emisión",
|
||||
"copyCode": "Copiar código",
|
||||
"codeCopied": "Código copiado al portapapeles",
|
||||
"live": "En vivo",
|
||||
"previewWaiting": "Vista previa en espera",
|
||||
"previewWaitingDescription": "La pantalla seleccionada aparecerá aquí cuando empieces a compartir.",
|
||||
"presets": "Ajustes predefinidos",
|
||||
"effortlessScreensharing": "comparte pantalla sin complicaciones",
|
||||
"hostInstead": "¿prefieres transmitir pantalla?",
|
||||
@@ -18,6 +22,7 @@
|
||||
"screenshare": "Compartir pantalla",
|
||||
"loadingPresets": "Cargando ajustes...",
|
||||
"selectAPreset": "Seleccionar un ajuste",
|
||||
"selectPresetBeforeStreaming": "Selecciona un ajuste antes de iniciar una transmisión.",
|
||||
"noPresetsAvailable": "No hay ajustes disponibles",
|
||||
"default": "predeterminado",
|
||||
"createNewPreset": "Crear nuevo ajuste",
|
||||
@@ -56,11 +61,15 @@
|
||||
"presetSharingDisabled": "El propietario del ajuste no ha habilitado el uso compartido para este ajuste.",
|
||||
"goBack": "Volver",
|
||||
"changeSource": "Cambiar fuente",
|
||||
"stopStream": "Detener transmisión",
|
||||
"activeStreamCloseTitle": "Transmisión activa",
|
||||
"activeStreamCloseMessage": "Hay una transmisión activa. Cerrar Helium la detendrá para todos los espectadores.",
|
||||
"stopStreamAndClose": "Detener transmisión y cerrar",
|
||||
"keepStreaming": "Seguir transmitiendo",
|
||||
"includeAudio": "Incluir audio",
|
||||
"audioSource": "Fuente de audio",
|
||||
"allSystemAudio": "Todo el audio del sistema",
|
||||
"refreshSources": "Actualizar fuentes",
|
||||
"audioSupported": "Audio soportado",
|
||||
"failedToStartScreenShare": "No se pudo iniciar el uso compartido de pantalla.",
|
||||
"screenRecordingPermissionRequired": "macOS bloqueó la captura de pantalla. Permite Helium en Configuración del Sistema > Privacidad y seguridad > Grabación de pantalla y luego reinicia Helium.",
|
||||
"screenRecordingPermissionRequiredNoShortcut": "macOS bloqueó la captura de pantalla. Abre Configuración del Sistema > Privacidad y seguridad > Grabación de pantalla, permite Helium y luego reinicia Helium.",
|
||||
@@ -70,11 +79,8 @@
|
||||
"desktopAppDescription": "La aplicación Electron completa con soporte de audio del sistema.",
|
||||
"androidApp": "Aplicación Android",
|
||||
"androidAppDescription": "Transmite directamente desde tu dispositivo Android.",
|
||||
"desktopAppNote": "Incluye funciones avanzadas como captura de audio del sistema, soporte de venmic en Linux y permisos de grabación de pantalla nativos.",
|
||||
"androidAppNote": "Instala el APK directamente en tu dispositivo Android. Asegúrate de permitir la instalación desde fuentes desconocidas si se te solicita.",
|
||||
"desktopAppNote": "Incluye funciones avanzadas como captura de audio del sistema y permisos de grabación de pantalla nativos.",
|
||||
"androidAppNote": "Instala el APK directamente en tu dispositivo Android.",
|
||||
"downloadFromGitHub": "Descargar desde GitHub",
|
||||
"viewSourceCode": "Ver Código Fuente",
|
||||
"preferTheBrowser": "¿Prefieres el navegador?",
|
||||
"browserVersionDescription": "La versión web también funciona muy bien. No requiere instalación: simplemente abre la página y empieza a transmitir o ver.",
|
||||
"useWebVersion": "Usar Versión Web"
|
||||
"viewSourceCode": "Ver Código Fuente"
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -1 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="#c026d3" aria-label="Logo" role="img" viewBox="0 0 176 174"><path fill-rule="evenodd" d="M47.0,154.5 L8.5,153.0 L8.5,105.0 L28.0,90.5 L29.0,95.5 L68.0,95.5 L107.5,66.0 L107.5,31.0 L110.0,28.5 L128.0,15.5 L166.0,15.5 L167.5,65.0 L148.0,79.5 L147.0,74.5 L105.0,76.5 L68.5,104.0 L68.5,139.0 Z M29.0,89.5 L28.5,46.0 L32.0,42.5 L69.0,15.5 L107.5,16.0 L107.5,30.0 L87.5,46.0 L87.0,74.5 L49.0,74.5 Z M107.0,154.5 L68.5,154.0 L68.5,140.0 L88.5,124.0 L88.5,96.0 L127.0,95.5 L147.0,80.5 L147.5,124.0 Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 176 174"
|
||||
fill="#c026d3"
|
||||
role="img"
|
||||
aria-label="Logo">
|
||||
|
||||
<path fill-rule="evenodd" d="
|
||||
M47.0,154.5 L8.5,153.0 L8.5,105.0 L28.0,90.5 L29.0,95.5 L68.0,95.5 L107.5,66.0
|
||||
L107.5,31.0 L110.0,28.5 L128.0,15.5 L166.0,15.5 L167.5,65.0 L148.0,79.5
|
||||
L147.0,74.5 L105.0,76.5 L68.5,104.0 L68.5,139.0 Z
|
||||
|
||||
M29.0,89.5 L28.5,46.0 L32.0,42.5 L69.0,15.5 L107.5,16.0 L107.5,30.0
|
||||
L87.5,46.0 L87.0,74.5 L49.0,74.5 Z
|
||||
|
||||
M107.0,154.5 L68.5,154.0 L68.5,140.0 L88.5,124.0 L88.5,96.0 L127.0,95.5
|
||||
L147.0,80.5 L147.5,124.0 Z
|
||||
"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 546 B After Width: | Height: | Size: 606 B |
@@ -1 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="#fff" aria-label="Logo" role="img" viewBox="0 0 176 174"><path fill-rule="evenodd" d="M47.0,154.5 L8.5,153.0 L8.5,105.0 L28.0,90.5 L29.0,95.5 L68.0,95.5 L107.5,66.0 L107.5,31.0 L110.0,28.5 L128.0,15.5 L166.0,15.5 L167.5,65.0 L148.0,79.5 L147.0,74.5 L105.0,76.5 L68.5,104.0 L68.5,139.0 Z M29.0,89.5 L28.5,46.0 L32.0,42.5 L69.0,15.5 L107.5,16.0 L107.5,30.0 L87.5,46.0 L87.0,74.5 L49.0,74.5 Z M107.0,154.5 L68.5,154.0 L68.5,140.0 L88.5,124.0 L88.5,96.0 L127.0,95.5 L147.0,80.5 L147.5,124.0 Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 176 174"
|
||||
fill="#ffffff"
|
||||
role="img"
|
||||
aria-label="Logo">
|
||||
|
||||
<path fill-rule="evenodd" d="
|
||||
M47.0,154.5 L8.5,153.0 L8.5,105.0 L28.0,90.5 L29.0,95.5 L68.0,95.5 L107.5,66.0
|
||||
L107.5,31.0 L110.0,28.5 L128.0,15.5 L166.0,15.5 L167.5,65.0 L148.0,79.5
|
||||
L147.0,74.5 L105.0,76.5 L68.5,104.0 L68.5,139.0 Z
|
||||
|
||||
M29.0,89.5 L28.5,46.0 L32.0,42.5 L69.0,15.5 L107.5,16.0 L107.5,30.0
|
||||
L87.5,46.0 L87.0,74.5 L49.0,74.5 Z
|
||||
|
||||
M107.0,154.5 L68.5,154.0 L68.5,140.0 L88.5,124.0 L88.5,96.0 L127.0,95.5
|
||||
L147.0,80.5 L147.5,124.0 Z
|
||||
"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 543 B After Width: | Height: | Size: 606 B |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 141 KiB |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "helium",
|
||||
"version": "0.1.1",
|
||||
"version": "0.2.0",
|
||||
"author": {
|
||||
"email": "helium@srizan.dev",
|
||||
"name": "eth0 software",
|
||||
@@ -8,6 +8,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.33.0",
|
||||
"main": "./electron/dist/main/index.js",
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
|
||||
@@ -54,6 +54,20 @@ async function deleteRoom(roomId: string) {
|
||||
await db.delete(schema.rooms).where(eq(schema.rooms.id, roomId));
|
||||
}
|
||||
|
||||
async function closeRoom(roomId: string, broadcasterId: string) {
|
||||
const room = await getRoom(roomId);
|
||||
if (!room || room.broadcaster !== broadcasterId) return;
|
||||
|
||||
room.viewers.forEach((viewerId: string) => {
|
||||
const viewer = activePeers.get(viewerId);
|
||||
if (viewer) {
|
||||
viewer.send(JSON.stringify({ event: 'room-closed' }));
|
||||
}
|
||||
});
|
||||
|
||||
await deleteRoom(roomId);
|
||||
}
|
||||
|
||||
async function addViewerToRoom(roomId: string, viewerId: string) {
|
||||
await db.insert(schema.roomViewers).values({
|
||||
roomId,
|
||||
@@ -104,6 +118,9 @@ export default defineWebSocketHandler({
|
||||
await createRoom(roomId, peer.id);
|
||||
peer.send(JSON.stringify({ event: 'room-created', roomId }));
|
||||
}
|
||||
if (msg.event === 'close-room') {
|
||||
await closeRoom(msg.roomId, peer.id);
|
||||
}
|
||||
if (msg.event === 'join-room') {
|
||||
const room = await getRoom(msg.roomId);
|
||||
if (room) {
|
||||
|
||||