diff --git a/.gitignore b/.gitignore index 4a7f73a..aac2668 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,10 @@ .cache dist +# Electron compiled output +electron/dist +dist-electron + # Node dependencies node_modules diff --git a/app/composables/useElectron.ts b/app/composables/useElectron.ts new file mode 100644 index 0000000..73eb2cb --- /dev/null +++ b/app/composables/useElectron.ts @@ -0,0 +1,254 @@ +export interface PlatformInfo { + platform: string; + isLinux: boolean; + isMac: boolean; + isWindows: boolean; + isElectron: boolean; + supportsLoopbackAudio: boolean; + supportsVenmic: boolean; +} + +export interface DesktopSource { + id: string; + name: string; + thumbnail: string; + appIcon: string | null; + display_id?: string; +} + +export interface VenmicLinkOptions { + include?: Record[]; + exclude?: Record[]; + ignore_devices?: boolean; + only_speakers?: boolean; + only_default_speakers?: boolean; +} + +interface HeliumElectronAPI { + isElectron: boolean; + getPlatform: () => Promise; + getSources: () => Promise; + onSourcesAvailable: (callback: (sources: DesktopSource[]) => void) => void; + selectSource: (sourceId: string | null) => void; + removeSourcesListener: () => void; + venmicAvailable: () => Promise; + venmicList: () => Promise[]>; + venmicLink: (options: VenmicLinkOptions) => Promise; + venmicUnlink: () => Promise; + checkScreenPermission: () => Promise; +} + +declare global { + interface Window { + heliumElectron?: HeliumElectronAPI; + } +} + +export function useElectron() { + const isElectron = ref(false); + const platformInfo = ref(null); + const audioSources = ref[]>([]); + const isVenmicLinked = ref(false); + + const checkElectron = () => { + if (import.meta.client) { + isElectron.value = !!window.heliumElectron?.isElectron; + } + return isElectron.value; + }; + + const getPlatformInfo = async (): Promise => { + if (!checkElectron()) { + return { + platform: 'browser', + isLinux: false, + isMac: false, + isWindows: false, + isElectron: false, + supportsLoopbackAudio: false, + supportsVenmic: false, + }; + } + + try { + platformInfo.value = await window.heliumElectron!.getPlatform(); + return platformInfo.value; + } catch (error) { + console.error('[useElectron] Failed to get platform info:', error); + return null; + } + }; + + const getDesktopSources = async (): Promise => { + if (!checkElectron()) return []; + + try { + return await window.heliumElectron!.getSources(); + } catch (error) { + console.error('[useElectron] Failed to get sources:', error); + return []; + } + }; + + const onSourcesAvailable = (callback: (sources: DesktopSource[]) => void) => { + if (!checkElectron()) return; + window.heliumElectron!.onSourcesAvailable(callback); + }; + + const selectSource = (sourceId: string | null) => { + if (!checkElectron()) return; + window.heliumElectron!.selectSource(sourceId); + }; + + const removeSourcesListener = () => { + if (!checkElectron()) return; + window.heliumElectron!.removeSourcesListener(); + }; + + const isVenmicAvailable = async (): Promise => { + if (!checkElectron()) return false; + try { + return await window.heliumElectron!.venmicAvailable(); + } catch { + return false; + } + }; + + const getVenmicSources = async (): Promise[]> => { + if (!checkElectron()) return []; + try { + const sources = await window.heliumElectron!.venmicList(); + audioSources.value = sources; + return sources; + } catch (error) { + console.error('[useElectron] Failed to list venmic sources:', error); + return []; + } + }; + + const linkVenmicAudio = async (options: VenmicLinkOptions = {}): Promise => { + if (!checkElectron()) return false; + try { + const success = await window.heliumElectron!.venmicLink(options); + isVenmicLinked.value = success; + return success; + } catch (error) { + console.error('[useElectron] Failed to link venmic:', error); + return false; + } + }; + + const linkAllAudio = async (): Promise => { + return linkVenmicAudio({ + exclude: [], + ignore_devices: true, + only_speakers: true, + only_default_speakers: false, + }); + }; + + const linkAppAudio = async (appName: string): Promise => { + return linkVenmicAudio({ + include: [{ 'application.name': appName }], + }); + }; + + const unlinkVenmicAudio = async (): Promise => { + if (!checkElectron()) return false; + try { + const success = await window.heliumElectron!.venmicUnlink(); + isVenmicLinked.value = !success; + return success; + } catch (error) { + console.error('[useElectron] Failed to unlink venmic:', error); + return false; + } + }; + + const startScreenShareWithAudio = async (options: { + video?: boolean | MediaTrackConstraints; + audioSource?: 'all' | 'none' | string; + } = {}): Promise => { + const { video = true, audioSource = 'all' } = options; + const platform = await getPlatformInfo(); + + try { + if (platform?.isLinux && platform.supportsVenmic && audioSource !== 'none') { + if (audioSource === 'all') { + await linkAllAudio(); + } else { + await linkAppAudio(audioSource); + } + } + + const stream = await navigator.mediaDevices.getDisplayMedia({ + video, + audio: audioSource !== 'none', + }); + + return stream; + } catch (error) { + console.error('[useElectron] Failed to start screen share:', error); + if (platform?.isLinux && isVenmicLinked.value) { + await unlinkVenmicAudio(); + } + return null; + } + }; + + const stopScreenShare = async (stream: MediaStream | null) => { + if (stream) { + stream.getTracks().forEach(track => track.stop()); + } + + const platform = platformInfo.value; + if (platform?.isLinux && isVenmicLinked.value) { + await unlinkVenmicAudio(); + } + }; + + const supportsAudioScreenShare = computed(() => { + if (!platformInfo.value) return false; + return platformInfo.value.supportsLoopbackAudio || platformInfo.value.supportsVenmic; + }); + + onMounted(() => { + checkElectron(); + if (isElectron.value) { + getPlatformInfo(); + } + }); + + onUnmounted(() => { + removeSourcesListener(); + if (isVenmicLinked.value) { + unlinkVenmicAudio(); + } + }); + + return { + isElectron: readonly(isElectron), + platformInfo: readonly(platformInfo), + audioSources: readonly(audioSources), + isVenmicLinked: readonly(isVenmicLinked), + supportsAudioScreenShare, + + checkElectron, + getPlatformInfo, + getDesktopSources, + onSourcesAvailable, + selectSource, + removeSourcesListener, + + isVenmicAvailable, + getVenmicSources, + linkVenmicAudio, + linkAllAudio, + linkAppAudio, + unlinkVenmicAudio, + + startScreenShareWithAudio, + stopScreenShare, + }; +} + diff --git a/app/pages/stream.vue b/app/pages/stream.vue index 56afcd6..45a25a2 100644 --- a/app/pages/stream.vue +++ b/app/pages/stream.vue @@ -1,6 +1,6 @@