mirror of
https://github.com/SrIzan10/helium.git
synced 2026-06-16 07:47:35 +00:00
Compare commits
1 Commits
v0.2.3
...
feat/multi
| Author | SHA1 | Date | |
|---|---|---|---|
| 045e363dc1 |
@@ -55,6 +55,94 @@
|
||||
|
||||
<Separator />
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-1">
|
||||
<Label
|
||||
class="text-xs text-muted-foreground uppercase tracking-wider font-semibold"
|
||||
>
|
||||
{{ $t("streamQuality") }}
|
||||
</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ $t("streamQualityDescription") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
v-for="preset in qualityPresets"
|
||||
:key="preset.id"
|
||||
type="button"
|
||||
class="rounded-lg border px-2.5 py-2 text-left transition hover:border-primary/60 hover:bg-primary/5"
|
||||
:class="[
|
||||
isSelectedQuickPreset(preset)
|
||||
? 'border-primary bg-primary/10 shadow-sm'
|
||||
: 'border-border bg-background',
|
||||
]"
|
||||
@click="applyQuickPreset(preset)"
|
||||
>
|
||||
<span class="flex items-center justify-between gap-2">
|
||||
<span class="text-xs font-semibold">
|
||||
{{ $t(preset.labelKey) }}
|
||||
</span>
|
||||
<Badge
|
||||
v-if="isSelectedQuickPreset(preset)"
|
||||
variant="secondary"
|
||||
class="text-[9px] uppercase tracking-wide"
|
||||
>
|
||||
{{ $t("active") }}
|
||||
</Badge>
|
||||
</span>
|
||||
<span class="mt-0.5 block text-[11px] text-muted-foreground">
|
||||
{{ $t(preset.summaryKey) }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs text-muted-foreground">
|
||||
{{ $t("quality") }}
|
||||
</Label>
|
||||
<Select v-model="selectedVideoQuality">
|
||||
<SelectTrigger class="w-full h-9">
|
||||
<SelectValue :placeholder="$t('quality')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="quality in videoQualityOptions"
|
||||
:key="quality.id"
|
||||
:value="quality.id"
|
||||
>
|
||||
{{ $t(quality.labelKey) }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs text-muted-foreground">
|
||||
{{ $t("fps") }}
|
||||
</Label>
|
||||
<Select v-model="selectedFrameRate">
|
||||
<SelectTrigger class="w-full h-9">
|
||||
<SelectValue :placeholder="$t('fps')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="option in frameRateOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label
|
||||
class="text-xs text-muted-foreground uppercase tracking-wider font-semibold"
|
||||
@@ -111,6 +199,22 @@
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-3 rounded-lg border px-3 py-2">
|
||||
<div class="space-y-0.5">
|
||||
<Label for="high-quality-audio" class="text-sm font-medium">
|
||||
{{ $t("highQualityAudio") }}
|
||||
</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ $t("highQualityAudioDescription") }}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="high-quality-audio"
|
||||
v-model="highQualityAudio"
|
||||
:disabled="!includeAudio"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="platformInfo?.isLinux && platformInfo?.supportsVenmic && includeAudio"
|
||||
class="space-y-3 pt-2 border-t border-border"
|
||||
@@ -230,9 +334,137 @@ const isCleaningUp = ref(false);
|
||||
const wsUrl = useWebSocketUrl();
|
||||
|
||||
const includeAudio = ref(true);
|
||||
const highQualityAudio = ref(true);
|
||||
const selectedAudioSource = ref("all");
|
||||
const selectedPresetId = ref("");
|
||||
|
||||
const streamSettingsStorageKey = "helium-stream-settings";
|
||||
|
||||
type VideoQualityId = "dataSaver" | "standard" | "sharp" | "ultra";
|
||||
type FrameRateValue = "24" | "30" | "60";
|
||||
|
||||
interface QualityPreset {
|
||||
id: string;
|
||||
labelKey: string;
|
||||
summaryKey: string;
|
||||
quality: VideoQualityId;
|
||||
frameRate: FrameRateValue;
|
||||
}
|
||||
|
||||
interface VideoQualityOption {
|
||||
id: VideoQualityId;
|
||||
labelKey: string;
|
||||
width: number;
|
||||
height: number;
|
||||
maxBitrate: number;
|
||||
contentHint: "motion" | "detail";
|
||||
}
|
||||
|
||||
interface FrameRateOption {
|
||||
value: FrameRateValue;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface StoredStreamSettings {
|
||||
videoQuality?: VideoQualityId;
|
||||
frameRate?: FrameRateValue;
|
||||
includeAudio?: boolean;
|
||||
highQualityAudio?: boolean;
|
||||
audioSource?: string;
|
||||
}
|
||||
|
||||
const qualityPresets: QualityPreset[] = [
|
||||
{
|
||||
id: "speed",
|
||||
labelKey: "preferSpeed",
|
||||
summaryKey: "preferSpeedSummary",
|
||||
quality: "dataSaver",
|
||||
frameRate: "24",
|
||||
},
|
||||
{
|
||||
id: "balanced",
|
||||
labelKey: "balanced",
|
||||
summaryKey: "balancedSummary",
|
||||
quality: "standard",
|
||||
frameRate: "30",
|
||||
},
|
||||
{
|
||||
id: "quality",
|
||||
labelKey: "preferQuality",
|
||||
summaryKey: "preferQualitySummary",
|
||||
quality: "sharp",
|
||||
frameRate: "30",
|
||||
},
|
||||
{
|
||||
id: "cinematic",
|
||||
labelKey: "highMotion",
|
||||
summaryKey: "highMotionSummary",
|
||||
quality: "standard",
|
||||
frameRate: "60",
|
||||
},
|
||||
];
|
||||
|
||||
const videoQualityOptions: VideoQualityOption[] = [
|
||||
{
|
||||
id: "dataSaver",
|
||||
labelKey: "dataSaver",
|
||||
width: 1280,
|
||||
height: 720,
|
||||
maxBitrate: 1_200_000,
|
||||
contentHint: "detail",
|
||||
},
|
||||
{
|
||||
id: "standard",
|
||||
labelKey: "standard",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
maxBitrate: 2_800_000,
|
||||
contentHint: "detail",
|
||||
},
|
||||
{
|
||||
id: "sharp",
|
||||
labelKey: "sharp",
|
||||
width: 2560,
|
||||
height: 1440,
|
||||
maxBitrate: 5_000_000,
|
||||
contentHint: "detail",
|
||||
},
|
||||
{
|
||||
id: "ultra",
|
||||
labelKey: "ultra",
|
||||
width: 3840,
|
||||
height: 2160,
|
||||
maxBitrate: 8_000_000,
|
||||
contentHint: "detail",
|
||||
},
|
||||
];
|
||||
|
||||
const frameRateOptions: FrameRateOption[] = [
|
||||
{ value: "24", label: "24 FPS" },
|
||||
{ value: "30", label: "30 FPS" },
|
||||
{ value: "60", label: "60 FPS" },
|
||||
];
|
||||
|
||||
const selectedVideoQuality = ref<VideoQualityId>("standard");
|
||||
const selectedFrameRate = ref<FrameRateValue>("30");
|
||||
|
||||
const activeQualityPreset = computed(
|
||||
() =>
|
||||
videoQualityOptions.find(
|
||||
(quality) => quality.id === selectedVideoQuality.value,
|
||||
) ?? videoQualityOptions[1]!,
|
||||
);
|
||||
|
||||
const activeFrameRate = computed(() => Number(selectedFrameRate.value));
|
||||
|
||||
const activeContentHint = computed(() =>
|
||||
selectedFrameRate.value === "60" ? "motion" : activeQualityPreset.value.contentHint,
|
||||
);
|
||||
|
||||
const audioMaxBitrate = computed(() =>
|
||||
highQualityAudio.value ? 128_000 : 64_000,
|
||||
);
|
||||
|
||||
const {
|
||||
isElectron,
|
||||
platformInfo,
|
||||
@@ -249,6 +481,7 @@ const {
|
||||
} = useElectron();
|
||||
|
||||
onMounted(async () => {
|
||||
restoreStreamSettings();
|
||||
await getPlatformInfo();
|
||||
|
||||
if (platformInfo.value?.isLinux && platformInfo.value?.supportsVenmic) {
|
||||
@@ -280,6 +513,95 @@ watch([localStream, locale], async ([stream]) => {
|
||||
);
|
||||
});
|
||||
|
||||
watch(
|
||||
[selectedVideoQuality, selectedFrameRate, highQualityAudio],
|
||||
async () => {
|
||||
persistStreamSettings();
|
||||
|
||||
if (!localStream.value) return;
|
||||
|
||||
await applyCurrentStreamSettings();
|
||||
toast.success(t("streamQualityUpdated"));
|
||||
},
|
||||
);
|
||||
|
||||
watch([includeAudio, selectedAudioSource], () => {
|
||||
persistStreamSettings();
|
||||
});
|
||||
|
||||
function isVideoQualityId(value: unknown): value is VideoQualityId {
|
||||
return (
|
||||
typeof value === "string" &&
|
||||
videoQualityOptions.some((quality) => quality.id === value)
|
||||
);
|
||||
}
|
||||
|
||||
function isFrameRateValue(value: unknown): value is FrameRateValue {
|
||||
return (
|
||||
typeof value === "string" &&
|
||||
frameRateOptions.some((option) => option.value === value)
|
||||
);
|
||||
}
|
||||
|
||||
function restoreStreamSettings(): void {
|
||||
const rawSettings = localStorage.getItem(streamSettingsStorageKey);
|
||||
|
||||
if (!rawSettings) return;
|
||||
|
||||
try {
|
||||
const settings = JSON.parse(rawSettings) as StoredStreamSettings;
|
||||
|
||||
if (isVideoQualityId(settings.videoQuality)) {
|
||||
selectedVideoQuality.value = settings.videoQuality;
|
||||
}
|
||||
if (isFrameRateValue(settings.frameRate)) {
|
||||
selectedFrameRate.value = settings.frameRate;
|
||||
}
|
||||
if (typeof settings.includeAudio === "boolean") {
|
||||
includeAudio.value = settings.includeAudio;
|
||||
}
|
||||
if (typeof settings.highQualityAudio === "boolean") {
|
||||
highQualityAudio.value = settings.highQualityAudio;
|
||||
}
|
||||
if (typeof settings.audioSource === "string") {
|
||||
selectedAudioSource.value = settings.audioSource;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to restore stream settings:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function persistStreamSettings(): void {
|
||||
const settings: StoredStreamSettings = {
|
||||
videoQuality: selectedVideoQuality.value,
|
||||
frameRate: selectedFrameRate.value,
|
||||
includeAudio: includeAudio.value,
|
||||
highQualityAudio: highQualityAudio.value,
|
||||
audioSource: selectedAudioSource.value,
|
||||
};
|
||||
|
||||
localStorage.setItem(streamSettingsStorageKey, JSON.stringify(settings));
|
||||
}
|
||||
|
||||
function applyQuickPreset(preset: QualityPreset): void {
|
||||
selectedVideoQuality.value = preset.quality;
|
||||
selectedFrameRate.value = preset.frameRate;
|
||||
}
|
||||
|
||||
function isSelectedQuickPreset(preset: QualityPreset): boolean {
|
||||
return (
|
||||
selectedVideoQuality.value === preset.quality &&
|
||||
selectedFrameRate.value === preset.frameRate
|
||||
);
|
||||
}
|
||||
|
||||
async function applyCurrentStreamSettings(): Promise<void> {
|
||||
if (!localStream.value) return;
|
||||
|
||||
await applyQualityPresetToStream(localStream.value);
|
||||
await applyQualityPresetToPeerConnections();
|
||||
}
|
||||
|
||||
async function refreshAudioSources() {
|
||||
await getVenmicSources();
|
||||
}
|
||||
@@ -289,6 +611,177 @@ async function copyCode() {
|
||||
toast.success(t("codeCopied"));
|
||||
}
|
||||
|
||||
function getDisplayMediaConstraints(): DisplayMediaStreamOptions {
|
||||
const preset = activeQualityPreset.value;
|
||||
const frameRate = activeFrameRate.value;
|
||||
const shouldRequestAudio =
|
||||
isElectron.value && includeAudio.value && supportsAudioScreenShare.value;
|
||||
|
||||
return {
|
||||
video: {
|
||||
width: { ideal: preset.width },
|
||||
height: { ideal: preset.height },
|
||||
frameRate: { ideal: frameRate, max: frameRate },
|
||||
},
|
||||
audio: shouldRequestAudio
|
||||
? {
|
||||
echoCancellation: false,
|
||||
noiseSuppression: false,
|
||||
autoGainControl: false,
|
||||
}
|
||||
: false,
|
||||
};
|
||||
}
|
||||
|
||||
async function applyQualityPresetToSender(
|
||||
sender: RTCRtpSender,
|
||||
): Promise<void> {
|
||||
const preset = activeQualityPreset.value;
|
||||
const frameRate = activeFrameRate.value;
|
||||
const parameters = sender.getParameters();
|
||||
|
||||
if (!parameters.encodings || parameters.encodings.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [encoding] = parameters.encodings;
|
||||
encoding.maxBitrate = preset.maxBitrate;
|
||||
encoding.maxFramerate = frameRate;
|
||||
|
||||
await sender.setParameters(parameters);
|
||||
}
|
||||
|
||||
async function applyAudioQualityToSender(sender: RTCRtpSender): Promise<void> {
|
||||
const parameters = sender.getParameters();
|
||||
|
||||
if (!parameters.encodings || parameters.encodings.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [encoding] = parameters.encodings;
|
||||
encoding.maxBitrate = audioMaxBitrate.value;
|
||||
|
||||
await sender.setParameters(parameters);
|
||||
}
|
||||
|
||||
async function safelyApplySenderSettings(
|
||||
sender: RTCRtpSender,
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (sender.track?.kind === "video") {
|
||||
await applyQualityPresetToSender(sender);
|
||||
}
|
||||
if (sender.track?.kind === "audio") {
|
||||
await applyAudioQualityToSender(sender);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to apply stream sender settings:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function withVideoBitrateHints(sdp: string): string {
|
||||
const lines = sdp.split("\r\n");
|
||||
const videoLineIndex = lines.findIndex((line) => line.startsWith("m=video"));
|
||||
|
||||
if (videoLineIndex === -1) return sdp;
|
||||
|
||||
const nextMediaLineIndex = lines.findIndex(
|
||||
(line, index) => index > videoLineIndex && line.startsWith("m="),
|
||||
);
|
||||
const videoSectionEnd =
|
||||
nextMediaLineIndex === -1 ? lines.length : nextMediaLineIndex;
|
||||
const maxBitrateKbps = Math.round(activeQualityPreset.value.maxBitrate / 1000);
|
||||
const startBitrateKbps = Math.min(
|
||||
maxBitrateKbps,
|
||||
Math.max(1_000, Math.round(maxBitrateKbps * 0.75)),
|
||||
);
|
||||
const payloadTypes = lines[videoLineIndex]!.split(" ").slice(3);
|
||||
const tunedPayloadTypes = payloadTypes.filter((payloadType) => {
|
||||
const rtmap = lines
|
||||
.slice(videoLineIndex, videoSectionEnd)
|
||||
.find((line) => line.startsWith(`a=rtmap:${payloadType} `));
|
||||
|
||||
return /\b(VP8|VP9|H264|AV1)\//i.test(rtmap ?? "");
|
||||
});
|
||||
|
||||
const hasBandwidthHint = lines
|
||||
.slice(videoLineIndex, videoSectionEnd)
|
||||
.some((line) => line.startsWith("b=AS:") || line.startsWith("b=TIAS:"));
|
||||
|
||||
if (!hasBandwidthHint) {
|
||||
const connectionLineIndex = lines.findIndex(
|
||||
(line, index) =>
|
||||
index > videoLineIndex &&
|
||||
index < videoSectionEnd &&
|
||||
line.startsWith("c="),
|
||||
);
|
||||
|
||||
if (connectionLineIndex !== -1) {
|
||||
lines.splice(connectionLineIndex + 1, 0, `b=AS:${maxBitrateKbps}`);
|
||||
}
|
||||
}
|
||||
|
||||
tunedPayloadTypes.forEach((payloadType) => {
|
||||
const fmtpIndex = lines.findIndex(
|
||||
(line, index) =>
|
||||
index > videoLineIndex &&
|
||||
index < videoSectionEnd &&
|
||||
line.startsWith(`a=fmtp:${payloadType} `),
|
||||
);
|
||||
const bitrateHint = `x-google-start-bitrate=${startBitrateKbps};x-google-max-bitrate=${maxBitrateKbps}`;
|
||||
|
||||
if (fmtpIndex !== -1) {
|
||||
if (!lines[fmtpIndex]!.includes("x-google-start-bitrate")) {
|
||||
lines[fmtpIndex] = `${lines[fmtpIndex]};${bitrateHint}`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const rtmapIndex = lines.findIndex(
|
||||
(line, index) =>
|
||||
index > videoLineIndex &&
|
||||
index < videoSectionEnd &&
|
||||
line.startsWith(`a=rtmap:${payloadType} `),
|
||||
);
|
||||
|
||||
if (rtmapIndex !== -1) {
|
||||
lines.splice(rtmapIndex + 1, 0, `a=fmtp:${payloadType} ${bitrateHint}`);
|
||||
}
|
||||
});
|
||||
|
||||
return lines.join("\r\n");
|
||||
}
|
||||
|
||||
async function applyQualityPresetToPeerConnections(): Promise<void> {
|
||||
const senders = Object.values(streamerStore.peerConnections).flatMap((pc) =>
|
||||
pc.getSenders(),
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
senders.map((sender) => safelyApplySenderSettings(sender)),
|
||||
);
|
||||
}
|
||||
|
||||
async function applyQualityPresetToStream(stream: MediaStream): Promise<void> {
|
||||
const preset = activeQualityPreset.value;
|
||||
const frameRate = activeFrameRate.value;
|
||||
const [videoTrack] = stream.getVideoTracks();
|
||||
|
||||
if (!videoTrack) return;
|
||||
|
||||
videoTrack.contentHint = activeContentHint.value;
|
||||
|
||||
try {
|
||||
await videoTrack.applyConstraints({
|
||||
width: { ideal: preset.width },
|
||||
height: { ideal: preset.height },
|
||||
frameRate: { ideal: frameRate, max: frameRate },
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("Failed to apply capture quality constraints:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function notifyRoomClosed() {
|
||||
if (!streamerStore.code) return;
|
||||
|
||||
@@ -351,13 +844,23 @@ const { send, close: closeWebSocket } = useWebSocket(wsUrl, {
|
||||
};
|
||||
|
||||
const offer = await peerConnection.createOffer();
|
||||
await peerConnection.setLocalDescription(offer);
|
||||
const tunedOffer: RTCSessionDescriptionInit = {
|
||||
type: offer.type,
|
||||
sdp: offer.sdp ? withVideoBitrateHints(offer.sdp) : undefined,
|
||||
};
|
||||
await peerConnection.setLocalDescription(tunedOffer);
|
||||
|
||||
void Promise.all(
|
||||
peerConnection
|
||||
.getSenders()
|
||||
.map((sender) => safelyApplySenderSettings(sender)),
|
||||
);
|
||||
|
||||
send(
|
||||
JSON.stringify({
|
||||
event: "offer",
|
||||
targetId: message.viewerId,
|
||||
sdp: offer,
|
||||
sdp: peerConnection.localDescription,
|
||||
iceServers: streamerStore.iceServers,
|
||||
}),
|
||||
);
|
||||
@@ -415,19 +918,11 @@ async function startScreenShare() {
|
||||
}
|
||||
}
|
||||
|
||||
const shouldRequestAudio =
|
||||
isElectron.value && includeAudio.value && supportsAudioScreenShare.value;
|
||||
const stream = await navigator.mediaDevices.getDisplayMedia(
|
||||
getDisplayMediaConstraints(),
|
||||
);
|
||||
|
||||
const stream = await navigator.mediaDevices.getDisplayMedia({
|
||||
video: true,
|
||||
audio: shouldRequestAudio
|
||||
? {
|
||||
echoCancellation: false,
|
||||
noiseSuppression: false,
|
||||
autoGainControl: false,
|
||||
}
|
||||
: false,
|
||||
});
|
||||
await applyQualityPresetToStream(stream);
|
||||
|
||||
localStream.value = stream;
|
||||
|
||||
@@ -471,45 +966,50 @@ async function changeScreenShareSource() {
|
||||
}
|
||||
}
|
||||
|
||||
const shouldRequestAudio =
|
||||
isElectron.value && includeAudio.value && supportsAudioScreenShare.value;
|
||||
|
||||
const newStream = await navigator.mediaDevices.getDisplayMedia({
|
||||
video: true,
|
||||
audio: shouldRequestAudio,
|
||||
});
|
||||
const newStream = await navigator.mediaDevices.getDisplayMedia(
|
||||
getDisplayMediaConstraints(),
|
||||
);
|
||||
|
||||
if (!localStream.value) return;
|
||||
|
||||
const newVideoTrack = newStream.getVideoTracks()[0];
|
||||
const newAudioTrack = newStream.getAudioTracks()[0];
|
||||
|
||||
await applyQualityPresetToStream(newStream);
|
||||
|
||||
newVideoTrack!.onended = () => {
|
||||
console.log("Screen sharing stopped by user");
|
||||
stopStreaming();
|
||||
};
|
||||
|
||||
Object.values(streamerStore.peerConnections).forEach((pc) => {
|
||||
const senders = pc.getSenders();
|
||||
const peerUpdates = Object.values(streamerStore.peerConnections).map(
|
||||
async (pc) => {
|
||||
const senders = pc.getSenders();
|
||||
|
||||
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 videoSender = senders.find(
|
||||
(sender) => sender.track?.kind === "video",
|
||||
);
|
||||
if (audioSender) {
|
||||
audioSender.replaceTrack(newAudioTrack);
|
||||
} else {
|
||||
pc.addTrack(newAudioTrack, newStream);
|
||||
if (videoSender) {
|
||||
await videoSender.replaceTrack(newVideoTrack!);
|
||||
await safelyApplySenderSettings(videoSender);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (newAudioTrack) {
|
||||
const audioSender = senders.find(
|
||||
(sender) => sender.track?.kind === "audio",
|
||||
);
|
||||
if (audioSender) {
|
||||
await audioSender.replaceTrack(newAudioTrack);
|
||||
await safelyApplySenderSettings(audioSender);
|
||||
} else {
|
||||
const sender = pc.addTrack(newAudioTrack, newStream);
|
||||
await safelyApplySenderSettings(sender);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
await Promise.all(peerUpdates);
|
||||
|
||||
localStream.value.getTracks().forEach((track) => {
|
||||
track.stop();
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
type IpcMainInvokeEvent,
|
||||
type DesktopCapturerSource,
|
||||
} from 'electron';
|
||||
import { autoUpdater, type AppUpdater, type ProgressInfo, type UpdateInfo } from 'electron-updater';
|
||||
import electronUpdater, { type AppUpdater, type ProgressInfo, type UpdateInfo } from 'electron-updater';
|
||||
import path from 'path';
|
||||
import { VenmicManager, type VenmicLinkOptions } from './venmic';
|
||||
|
||||
@@ -79,6 +79,7 @@ interface UpdateStatusPayload {
|
||||
}
|
||||
|
||||
function getAutoUpdater(): AppUpdater {
|
||||
const { autoUpdater } = electronUpdater;
|
||||
return autoUpdater;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,30 @@
|
||||
"live": "Live",
|
||||
"previewWaiting": "Preview waiting",
|
||||
"previewWaitingDescription": "Your selected screen will appear here once sharing starts.",
|
||||
"streamQuality": "Stream quality",
|
||||
"streamQualityDescription": "Switch quality live without restarting the room.",
|
||||
"streamQualityUpdated": "Stream quality updated.",
|
||||
"active": "Active",
|
||||
"preferSpeed": "Prefer speed",
|
||||
"preferSpeedDescription": "720p at 24 FPS with lower bitrate for weaker networks.",
|
||||
"preferSpeedSummary": "720p · 24 FPS",
|
||||
"balanced": "Balanced",
|
||||
"balancedDescription": "1080p at 30 FPS for everyday sharing.",
|
||||
"balancedSummary": "1080p · 30 FPS",
|
||||
"preferQuality": "Prefer quality",
|
||||
"preferQualityDescription": "Up to 1440p with higher detail for text and design work.",
|
||||
"preferQualitySummary": "1440p · 30 FPS",
|
||||
"highMotion": "High motion",
|
||||
"highMotionDescription": "1080p at 60 FPS for smoother demos and video.",
|
||||
"highMotionSummary": "1080p · 60 FPS",
|
||||
"quality": "Quality",
|
||||
"fps": "FPS",
|
||||
"dataSaver": "Data saver",
|
||||
"standard": "Standard",
|
||||
"sharp": "Sharp",
|
||||
"ultra": "Ultra",
|
||||
"highQualityAudio": "High quality audio",
|
||||
"highQualityAudioDescription": "Stereo 48 kHz audio with less processing.",
|
||||
"presets": "Presets",
|
||||
"effortlessScreensharing": "effortless screensharing powered by webrtc",
|
||||
"hostInstead": "stream instead?",
|
||||
|
||||
@@ -11,6 +11,30 @@
|
||||
"live": "En vivo",
|
||||
"previewWaiting": "Vista previa en espera",
|
||||
"previewWaitingDescription": "La pantalla seleccionada aparecerá aquí cuando empieces a compartir.",
|
||||
"streamQuality": "Calidad de transmisión",
|
||||
"streamQualityDescription": "Cambia la calidad en vivo sin reiniciar la sala.",
|
||||
"streamQualityUpdated": "Calidad de transmisión actualizada.",
|
||||
"active": "Activo",
|
||||
"preferSpeed": "Priorizar velocidad",
|
||||
"preferSpeedDescription": "720p a 24 FPS con menor bitrate para redes débiles.",
|
||||
"preferSpeedSummary": "720p · 24 FPS",
|
||||
"balanced": "Equilibrado",
|
||||
"balancedDescription": "1080p a 30 FPS para compartir en el día a día.",
|
||||
"balancedSummary": "1080p · 30 FPS",
|
||||
"preferQuality": "Priorizar calidad",
|
||||
"preferQualityDescription": "Hasta 1440p con más detalle para texto y diseño.",
|
||||
"preferQualitySummary": "1440p · 30 FPS",
|
||||
"highMotion": "Mucho movimiento",
|
||||
"highMotionDescription": "1080p a 60 FPS para demos y video más fluidos.",
|
||||
"highMotionSummary": "1080p · 60 FPS",
|
||||
"quality": "Calidad",
|
||||
"fps": "FPS",
|
||||
"dataSaver": "Ahorro de datos",
|
||||
"standard": "Estándar",
|
||||
"sharp": "Nítido",
|
||||
"ultra": "Ultra",
|
||||
"highQualityAudio": "Audio de alta calidad",
|
||||
"highQualityAudioDescription": "Audio estéreo a 48 kHz con menos procesamiento.",
|
||||
"presets": "Ajustes predefinidos",
|
||||
"effortlessScreensharing": "comparte pantalla sin complicaciones",
|
||||
"hostInstead": "¿prefieres transmitir pantalla?",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "helium",
|
||||
"version": "0.2.3",
|
||||
"version": "0.2.2",
|
||||
"author": {
|
||||
"email": "helium@srizan.dev",
|
||||
"name": "eth0 software",
|
||||
|
||||
Reference in New Issue
Block a user