Add microphone mute (#158)

* Add code

* add code
This commit is contained in:
Freddy Boulton
2025-03-10 14:53:08 -04:00
committed by GitHub
parent ed59834378
commit 51f1fafa3a
14 changed files with 1579 additions and 1520 deletions

11
frontend/.prettierrc Normal file
View File

@@ -0,0 +1,11 @@
{
"plugins": ["prettier-plugin-svelte"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}

View File

@@ -1,73 +1,73 @@
<script lang="ts"> <script lang="ts">
import { playable } from "./shared/utils"; import { playable } from "./shared/utils";
import { type FileData } from "@gradio/client"; import { type FileData } from "@gradio/client";
export let type: "gallery" | "table"; export let type: "gallery" | "table";
export let selected = false; export let selected = false;
export let value: { video: FileData; subtitles: FileData | null } | null; export let value: { video: FileData; subtitles: FileData | null } | null;
export let loop: boolean; export let loop: boolean;
let video: HTMLVideoElement; let video: HTMLVideoElement;
async function init(): Promise<void> { async function init(): Promise<void> {
video.muted = true; video.muted = true;
video.playsInline = true; video.playsInline = true;
video.controls = false; video.controls = false;
video.setAttribute("muted", ""); video.setAttribute("muted", "");
await video.play(); await video.play();
video.pause(); video.pause();
} }
</script> </script>
{#if value} {#if value}
{#if playable()} {#if playable()}
<div <div
class="container" class="container"
class:table={type === "table"} class:table={type === "table"}
class:gallery={type === "gallery"} class:gallery={type === "gallery"}
class:selected class:selected
> >
<video <video
bind:this={video} bind:this={video}
on:loadeddata={init} on:loadeddata={init}
on:mouseover={video.play.bind(video)} on:mouseover={video.play.bind(video)}
on:mouseout={video.pause.bind(video)} on:mouseout={video.pause.bind(video)}
src={value?.video.url} src={value?.video.url}
/> />
</div> </div>
{:else} {:else}
<div>{value}</div> <div>{value}</div>
{/if} {/if}
{/if} {/if}
<style> <style>
.container { .container {
flex: none; flex: none;
max-width: none; max-width: none;
} }
.container :global(video) { .container :global(video) {
width: var(--size-full); width: var(--size-full);
height: var(--size-full); height: var(--size-full);
object-fit: cover; object-fit: cover;
} }
.container:hover, .container:hover,
.container.selected { .container.selected {
border-color: var(--border-color-accent); border-color: var(--border-color-accent);
} }
.container.table { .container.table {
margin: 0 auto; margin: 0 auto;
border: 2px solid var(--border-color-primary); border: 2px solid var(--border-color-primary);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
overflow: hidden; overflow: hidden;
width: var(--size-20); width: var(--size-20);
height: var(--size-20); height: var(--size-20);
object-fit: cover; object-fit: cover;
} }
.container.gallery { .container.gallery {
height: var(--size-20); height: var(--size-20);
max-height: var(--size-20); max-height: var(--size-20);
object-fit: cover; object-fit: cover;
} }
</style> </style>

View File

@@ -1,192 +1,189 @@
<svelte:options accessors={true} /> <svelte:options accessors={true} />
<script lang="ts"> <script lang="ts">
import { Block, UploadText } from "@gradio/atoms"; import { Block, UploadText } from "@gradio/atoms";
import Video from "./shared/InteractiveVideo.svelte"; import Video from "./shared/InteractiveVideo.svelte";
import { StatusTracker } from "@gradio/statustracker"; import { StatusTracker } from "@gradio/statustracker";
import type { LoadingStatus } from "@gradio/statustracker"; import type { LoadingStatus } from "@gradio/statustracker";
import StaticVideo from "./shared/StaticVideo.svelte"; import StaticVideo from "./shared/StaticVideo.svelte";
import StaticAudio from "./shared/StaticAudio.svelte"; import StaticAudio from "./shared/StaticAudio.svelte";
import InteractiveAudio from "./shared/InteractiveAudio.svelte"; import InteractiveAudio from "./shared/InteractiveAudio.svelte";
export let elem_id = ""; export let elem_id = "";
export let elem_classes: string[] = []; export let elem_classes: string[] = [];
export let visible = true; export let visible = true;
export let value: string = "__webrtc_value__"; export let value: string = "__webrtc_value__";
export let button_labels: { start: string; stop: string; waiting: string }; export let button_labels: { start: string; stop: string; waiting: string };
export let label: string; export let label: string;
export let root: string; export let root: string;
export let show_label: boolean; export let show_label: boolean;
export let loading_status: LoadingStatus; export let loading_status: LoadingStatus;
export let height: number | undefined; export let height: number | undefined;
export let width: number | undefined; export let width: number | undefined;
export let server: { export let server: {
offer: (body: any) => Promise<any>; offer: (body: any) => Promise<any>;
}; };
export let container = false; export let container = false;
export let scale: number | null = null; export let scale: number | null = null;
export let min_width: number | undefined = undefined; export let min_width: number | undefined = undefined;
export let gradio; export let gradio;
export let rtc_configuration: Object; export let rtc_configuration: Object;
export let time_limit: number | null = null; export let time_limit: number | null = null;
export let modality: "video" | "audio" | "audio-video" = "video"; export let modality: "video" | "audio" | "audio-video" = "video";
export let mode: "send-receive" | "receive" | "send" = "send-receive"; export let mode: "send-receive" | "receive" | "send" = "send-receive";
export let rtp_params: RTCRtpParameters = {} as RTCRtpParameters; export let rtp_params: RTCRtpParameters = {} as RTCRtpParameters;
export let track_constraints: MediaTrackConstraints = {}; export let track_constraints: MediaTrackConstraints = {};
export let icon: string | undefined = undefined; export let icon: string | undefined = undefined;
export let icon_button_color: string = "var(--color-accent)"; export let icon_button_color: string = "var(--color-accent)";
export let pulse_color: string = "var(--color-accent)"; export let pulse_color: string = "var(--color-accent)";
export let icon_radius: number = 50; export let icon_radius: number = 50;
const on_change_cb = (msg: "change" | "tick" | any) => { const on_change_cb = (msg: "change" | "tick" | any) => {
if ( if (
msg?.type === "info" || msg?.type === "info" ||
msg?.type === "warning" || msg?.type === "warning" ||
msg?.type === "error" msg?.type === "error"
) { ) {
gradio.dispatch( gradio.dispatch(msg?.type === "error" ? "error" : "warning", msg.message);
msg?.type === "error" ? "error" : "warning", } else if (msg?.type === "fetch_output") {
msg.message, gradio.dispatch("state_change");
); } else if (msg?.type === "send_input") {
} else if (msg?.type === "fetch_output") { gradio.dispatch("tick");
gradio.dispatch("state_change"); } else if (msg?.type === "connection_timeout") {
} else if (msg?.type === "send_input") { gradio.dispatch(
gradio.dispatch("tick"); "warning",
} else if (msg?.type === "connection_timeout") { "Taking a while to connect. Are you on a VPN?",
gradio.dispatch( );
"warning", }
"Taking a while to connect. Are you on a VPN?", if (msg.type === "state_change") {
); gradio.dispatch(msg === "change" ? "state_change" : "tick");
} }
if (msg.type === "state_change") { };
gradio.dispatch(msg === "change" ? "state_change" : "tick");
}
};
const reject_cb = (msg: object) => { const reject_cb = (msg: object) => {
if ( if (
msg.status === "failed" && msg.status === "failed" &&
msg.meta?.error === "concurrency_limit_reached" msg.meta?.error === "concurrency_limit_reached"
) { ) {
gradio.dispatch( gradio.dispatch(
"error", "error",
`Too many concurrent connections. Please try again later!`, `Too many concurrent connections. Please try again later!`,
); );
} else { } else {
gradio.dispatch("error", "Unexpected server error"); gradio.dispatch("error", "Unexpected server error");
} }
}; };
let dragging = false; let dragging = false;
</script> </script>
<Block <Block
{visible} {visible}
variant={"solid"} variant={"solid"}
border_mode={dragging ? "focus" : "base"} border_mode={dragging ? "focus" : "base"}
padding={false} padding={false}
{elem_id} {elem_id}
{elem_classes} {elem_classes}
{height} {height}
{width} {width}
{container} {container}
{scale} {scale}
{min_width} {min_width}
allow_overflow={false} allow_overflow={false}
> >
<StatusTracker <StatusTracker
autoscroll={gradio.autoscroll} autoscroll={gradio.autoscroll}
i18n={gradio.i18n} i18n={gradio.i18n}
{...loading_status} {...loading_status}
on:clear_status={() => gradio.dispatch("clear_status", loading_status)} on:clear_status={() => gradio.dispatch("clear_status", loading_status)}
/> />
{#if mode == "receive" && modality === "video"} {#if mode == "receive" && modality === "video"}
<StaticVideo <StaticVideo
bind:value bind:value
{on_change_cb} {on_change_cb}
{label} {label}
{show_label} {show_label}
{server} {server}
{rtc_configuration} {rtc_configuration}
on:tick={() => gradio.dispatch("tick")} on:tick={() => gradio.dispatch("tick")}
on:error={({ detail }) => gradio.dispatch("error", detail)} on:error={({ detail }) => gradio.dispatch("error", detail)}
/> />
{:else if mode == "receive" && modality === "audio"} {:else if mode == "receive" && modality === "audio"}
<StaticAudio <StaticAudio
bind:value bind:value
{on_change_cb} {on_change_cb}
{label} {label}
{show_label} {show_label}
{server} {server}
{rtc_configuration} {rtc_configuration}
{icon} {icon}
{icon_button_color} {icon_button_color}
{pulse_color} {pulse_color}
{icon_radius} {icon_radius}
i18n={gradio.i18n} i18n={gradio.i18n}
on:tick={() => gradio.dispatch("tick")} on:tick={() => gradio.dispatch("tick")}
on:error={({ detail }) => gradio.dispatch("error", detail)} on:error={({ detail }) => gradio.dispatch("error", detail)}
/> />
{:else if (mode === "send-receive" || mode == "send") && (modality === "video" || modality == "audio-video")} {:else if (mode === "send-receive" || mode == "send") && (modality === "video" || modality == "audio-video")}
<Video <Video
bind:value bind:value
{label} {label}
{show_label} {show_label}
active_source={"webcam"} active_source={"webcam"}
include_audio={modality === "audio-video"} include_audio={modality === "audio-video"}
{server} {server}
{rtc_configuration} {rtc_configuration}
{time_limit} {time_limit}
{mode} {mode}
{track_constraints} {track_constraints}
{rtp_params} {rtp_params}
{on_change_cb} {on_change_cb}
{reject_cb} {reject_cb}
{icon} {icon}
{icon_button_color} {icon_button_color}
{pulse_color} {pulse_color}
{icon_radius} {icon_radius}
{button_labels} {button_labels}
on:clear={() => gradio.dispatch("clear")} on:clear={() => gradio.dispatch("clear")}
on:play={() => gradio.dispatch("play")} on:play={() => gradio.dispatch("play")}
on:pause={() => gradio.dispatch("pause")} on:pause={() => gradio.dispatch("pause")}
on:upload={() => gradio.dispatch("upload")} on:upload={() => gradio.dispatch("upload")}
on:stop={() => gradio.dispatch("stop")} on:stop={() => gradio.dispatch("stop")}
on:end={() => gradio.dispatch("end")} on:end={() => gradio.dispatch("end")}
on:start_recording={() => gradio.dispatch("start_recording")} on:start_recording={() => gradio.dispatch("start_recording")}
on:stop_recording={() => gradio.dispatch("stop_recording")} on:stop_recording={() => gradio.dispatch("stop_recording")}
on:tick={() => gradio.dispatch("tick")} on:tick={() => gradio.dispatch("tick")}
on:error={({ detail }) => gradio.dispatch("error", detail)} on:error={({ detail }) => gradio.dispatch("error", detail)}
i18n={gradio.i18n} i18n={gradio.i18n}
stream_handler={(...args) => gradio.client.stream(...args)} stream_handler={(...args) => gradio.client.stream(...args)}
> >
<UploadText i18n={gradio.i18n} type="video" /> <UploadText i18n={gradio.i18n} type="video" />
</Video> </Video>
{:else if (mode === "send-receive" || mode === "send") && modality === "audio"} {:else if (mode === "send-receive" || mode === "send") && modality === "audio"}
<InteractiveAudio <InteractiveAudio
bind:value bind:value
{on_change_cb} {on_change_cb}
{label} {label}
{show_label} {show_label}
{server} {server}
{rtc_configuration} {rtc_configuration}
{time_limit} {time_limit}
{track_constraints} {track_constraints}
{mode} {mode}
{rtp_params} {rtp_params}
i18n={gradio.i18n} i18n={gradio.i18n}
{icon} {icon}
{reject_cb} {reject_cb}
{icon_button_color} {icon_button_color}
{icon_radius} {icon_radius}
{pulse_color} {pulse_color}
{button_labels} {button_labels}
on:tick={() => gradio.dispatch("tick")} on:tick={() => gradio.dispatch("tick")}
on:error={({ detail }) => gradio.dispatch("error", detail)} on:error={({ detail }) => gradio.dispatch("error", detail)}
on:warning={({ detail }) => gradio.dispatch("warning", detail)} on:warning={({ detail }) => gradio.dispatch("warning", detail)}
/> />
{/if} {/if}
</Block> </Block>

View File

@@ -25,7 +25,8 @@
}, },
"devDependencies": { "devDependencies": {
"@gradio/preview": "0.12.0", "@gradio/preview": "0.12.0",
"prettier": "3.3.3" "prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.3.3"
}, },
"peerDependencies": { "peerDependencies": {
"svelte": "^4.0.0" "svelte": "^4.0.0"
@@ -4118,6 +4119,7 @@
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
"dev": true, "dev": true,
"license": "MIT",
"bin": { "bin": {
"prettier": "bin/prettier.cjs" "prettier": "bin/prettier.cjs"
}, },
@@ -4128,6 +4130,17 @@
"url": "https://github.com/prettier/prettier?sponsor=1" "url": "https://github.com/prettier/prettier?sponsor=1"
} }
}, },
"node_modules/prettier-plugin-svelte": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.3.3.tgz",
"integrity": "sha512-yViK9zqQ+H2qZD1w/bH7W8i+bVfKrD8GIFjkFe4Thl6kCT9SlAsXVNmt3jCvQOCsnOhcvYgsoVlRV/Eu6x5nNw==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"prettier": "^3.0.0",
"svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0"
}
},
"node_modules/prismjs": { "node_modules/prismjs": {
"version": "1.29.0", "version": "1.29.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",

View File

@@ -23,7 +23,8 @@
}, },
"devDependencies": { "devDependencies": {
"@gradio/preview": "0.12.0", "@gradio/preview": "0.12.0",
"prettier": "3.3.3" "prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.3.3"
}, },
"exports": { "exports": {
"./package.json": "./package.json", "./package.json": "./package.json",

File diff suppressed because it is too large Load Diff

View File

@@ -1,90 +1,90 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import type { ComponentType } from "svelte"; import type { ComponentType } from "svelte";
import type { FileData, Client } from "@gradio/client"; import type { FileData, Client } from "@gradio/client";
import { BlockLabel } from "@gradio/atoms"; import { BlockLabel } from "@gradio/atoms";
import Webcam from "./Webcam.svelte"; import Webcam from "./Webcam.svelte";
import { Video } from "@gradio/icons"; import { Video } from "@gradio/icons";
import type { I18nFormatter } from "@gradio/utils"; import type { I18nFormatter } from "@gradio/utils";
export let value: string = null; export let value: string = null;
export let label: string | undefined = undefined; export let label: string | undefined = undefined;
export let show_label = true; export let show_label = true;
export let include_audio: boolean; export let include_audio: boolean;
export let i18n: I18nFormatter; export let i18n: I18nFormatter;
export let active_source: "webcam" | "upload" = "webcam"; export let active_source: "webcam" | "upload" = "webcam";
export let handle_reset_value: () => void = () => {}; export let handle_reset_value: () => void = () => {};
export let stream_handler: Client["stream"]; export let stream_handler: Client["stream"];
export let time_limit: number | null = null; export let time_limit: number | null = null;
export let button_labels: { start: string; stop: string; waiting: string }; export let button_labels: { start: string; stop: string; waiting: string };
export let server: { export let server: {
offer: (body: any) => Promise<any>; offer: (body: any) => Promise<any>;
}; };
export let rtc_configuration: Object; export let rtc_configuration: Object;
export let track_constraints: MediaTrackConstraints = {}; export let track_constraints: MediaTrackConstraints = {};
export let mode: "send" | "send-receive"; export let mode: "send" | "send-receive";
export let on_change_cb: (msg: "change" | "tick") => void; export let on_change_cb: (msg: "change" | "tick") => void;
export let reject_cb: (msg: object) => void; export let reject_cb: (msg: object) => void;
export let rtp_params: RTCRtpParameters = {} as RTCRtpParameters; export let rtp_params: RTCRtpParameters = {} as RTCRtpParameters;
export let icon: string | undefined | ComponentType = undefined; export let icon: string | undefined | ComponentType = undefined;
export let icon_button_color: string = "var(--color-accent)"; export let icon_button_color: string = "var(--color-accent)";
export let pulse_color: string = "var(--color-accent)"; export let pulse_color: string = "var(--color-accent)";
export let icon_radius: number = 50; export let icon_radius: number = 50;
const dispatch = createEventDispatcher<{ const dispatch = createEventDispatcher<{
change: FileData | null; change: FileData | null;
clear?: never; clear?: never;
play?: never; play?: never;
pause?: never; pause?: never;
end?: never; end?: never;
drag: boolean; drag: boolean;
error: string; error: string;
upload: FileData; upload: FileData;
start_recording?: never; start_recording?: never;
stop_recording?: never; stop_recording?: never;
tick: never; tick: never;
}>(); }>();
let dragging = false; let dragging = false;
$: dispatch("drag", dragging); $: dispatch("drag", dragging);
</script> </script>
<BlockLabel {show_label} Icon={Video} label={label || "Video"} /> <BlockLabel {show_label} Icon={Video} label={label || "Video"} />
<div data-testid="video" class="video-container"> <div data-testid="video" class="video-container">
<Webcam <Webcam
{rtc_configuration} {rtc_configuration}
{include_audio} {include_audio}
{time_limit} {time_limit}
{track_constraints} {track_constraints}
{mode} {mode}
{rtp_params} {rtp_params}
{on_change_cb} {on_change_cb}
{icon} {icon}
{icon_button_color} {icon_button_color}
{pulse_color} {pulse_color}
{icon_radius} {icon_radius}
{button_labels} {button_labels}
on:error on:error
on:start_recording on:start_recording
on:stop_recording on:stop_recording
on:tick on:tick
{i18n} {i18n}
stream_every={0.5} stream_every={0.5}
{server} {server}
bind:webrtc_id={value} bind:webrtc_id={value}
{reject_cb} {reject_cb}
/> />
<!-- <SelectSource {sources} bind:active_source /> --> <!-- <SelectSource {sources} bind:active_source /> -->
</div> </div>
<style> <style>
.video-container { .video-container {
display: flex; display: flex;
height: 100%; height: 100%;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
</style> </style>

View File

@@ -0,0 +1,20 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="feather feather-mic"
><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" /><path
d="M19 10v2a7 7 0 0 1-14 0v-2"
/><line x1="12" y1="19" x2="12" y2="23" /><line
x1="8"
y1="23"
x2="16"
y2="23"
/><line x1="1" y1="1" x2="23" y2="23" /></svg
>

After

Width:  |  Height:  |  Size: 489 B

View File

@@ -1,149 +1,146 @@
<script lang="ts"> <script lang="ts">
import { Empty } from "@gradio/atoms"; import { Empty } from "@gradio/atoms";
import { BlockLabel } from "@gradio/atoms"; import { BlockLabel } from "@gradio/atoms";
import { Music } from "@gradio/icons"; import { Music } from "@gradio/icons";
import type { I18nFormatter } from "@gradio/utils"; import type { I18nFormatter } from "@gradio/utils";
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { start, stop } from "./webrtc_utils"; import { start, stop } from "./webrtc_utils";
import AudioWave from "./AudioWave.svelte"; import AudioWave from "./AudioWave.svelte";
export let value: string | null = null; export let value: string | null = null;
export let label: string | undefined = undefined; export let label: string | undefined = undefined;
export let show_label = true; export let show_label = true;
export let rtc_configuration: Object | null = null; export let rtc_configuration: Object | null = null;
export let i18n: I18nFormatter; export let i18n: I18nFormatter;
export let on_change_cb: (msg: "change" | "tick") => void; export let on_change_cb: (msg: "change" | "tick") => void;
export let icon: string | undefined = undefined; export let icon: string | undefined = undefined;
export let icon_button_color: string = "var(--color-accent)"; export let icon_button_color: string = "var(--color-accent)";
export let pulse_color: string = "var(--color-accent)"; export let pulse_color: string = "var(--color-accent)";
export let icon_radius: number = 50; export let icon_radius: number = 50;
export let server: { export let server: {
offer: (body: any) => Promise<any>; offer: (body: any) => Promise<any>;
}; };
let stream_state: "open" | "closed" | "waiting" = "closed"; let stream_state: "open" | "closed" | "waiting" = "closed";
let audio_player: HTMLAudioElement; let audio_player: HTMLAudioElement;
let pc: RTCPeerConnection; let pc: RTCPeerConnection;
let _webrtc_id = Math.random().toString(36).substring(2); let _webrtc_id = Math.random().toString(36).substring(2);
const dispatch = createEventDispatcher<{ const dispatch = createEventDispatcher<{
tick: undefined; tick: undefined;
error: string; error: string;
play: undefined; play: undefined;
stop: undefined; stop: undefined;
}>(); }>();
async function start_stream(value: string): Promise<string> { async function start_stream(value: string): Promise<string> {
if (value === "start_webrtc_stream") { if (value === "start_webrtc_stream") {
stream_state = "waiting"; stream_state = "waiting";
_webrtc_id = Math.random().toString(36).substring(2); _webrtc_id = Math.random().toString(36).substring(2);
value = _webrtc_id; value = _webrtc_id;
pc = new RTCPeerConnection(rtc_configuration); pc = new RTCPeerConnection(rtc_configuration);
pc.addEventListener("connectionstatechange", async (event) => { pc.addEventListener("connectionstatechange", async (event) => {
switch (pc.connectionState) { switch (pc.connectionState) {
case "connected": case "connected":
console.info("connected"); console.info("connected");
stream_state = "open"; stream_state = "open";
dispatch("tick"); dispatch("tick");
break; break;
case "disconnected": case "disconnected":
console.info("closed"); console.info("closed");
stop(pc); stop(pc);
break; break;
case "failed": case "failed":
stream_state = "closed"; stream_state = "closed";
dispatch("error", "Connection failed!"); dispatch("error", "Connection failed!");
stop(pc); stop(pc);
break; break;
default: default:
break; break;
}
});
let stream = null;
const timeoutId = setTimeout(() => {
// @ts-ignore
on_change_cb({ type: "connection_timeout" });
}, 5000);
start(
stream,
pc,
audio_player,
server.offer,
_webrtc_id,
"audio",
on_change_cb,
)
.then((connection) => {
clearTimeout(timeoutId);
pc = connection;
})
.catch(() => {
clearTimeout(timeoutId);
console.info("catching");
dispatch(
"error",
"Too many concurrent users. Come back later!",
);
});
} }
return value; });
} let stream = null;
const timeoutId = setTimeout(() => {
// @ts-ignore
on_change_cb({ type: "connection_timeout" });
}, 5000);
$: start_stream(value).then((val) => { start(
value = val; stream,
}); pc,
audio_player,
server.offer,
_webrtc_id,
"audio",
on_change_cb,
)
.then((connection) => {
clearTimeout(timeoutId);
pc = connection;
})
.catch(() => {
clearTimeout(timeoutId);
console.info("catching");
dispatch("error", "Too many concurrent users. Come back later!");
});
}
return value;
}
$: start_stream(value).then((val) => {
value = val;
});
</script> </script>
<BlockLabel <BlockLabel
{show_label} {show_label}
Icon={Music} Icon={Music}
float={false} float={false}
label={label || i18n("audio.audio")} label={label || i18n("audio.audio")}
/> />
<audio <audio
class="standard-player" class="standard-player"
class:hidden={true} class:hidden={true}
on:load on:load
bind:this={audio_player} bind:this={audio_player}
on:ended={() => dispatch("stop")} on:ended={() => dispatch("stop")}
on:play={() => dispatch("play")} on:play={() => dispatch("play")}
/> />
{#if value !== "__webrtc_value__"} {#if value !== "__webrtc_value__"}
<div class="audio-container"> <div class="audio-container">
<AudioWave <AudioWave
audio_source_callback={() => audio_player.srcObject} audio_source_callback={() => audio_player.srcObject}
{stream_state} {stream_state}
{icon} {icon}
{icon_button_color} {icon_button_color}
{pulse_color} {pulse_color}
{icon_radius} {icon_radius}
/> />
</div> </div>
{/if} {/if}
{#if value === "__webrtc_value__"} {#if value === "__webrtc_value__"}
<Empty size="small"> <Empty size="small">
<Music /> <Music />
</Empty> </Empty>
{/if} {/if}
<style> <style>
.audio-container { .audio-container {
display: flex; display: flex;
height: 100%; height: 100%;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.standard-player { .standard-player {
width: 100%; width: 100%;
} }
.hidden { .hidden {
display: none; display: none;
} }
</style> </style>

View File

@@ -1,124 +1,121 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher, onMount } from "svelte"; import { createEventDispatcher, onMount } from "svelte";
import { BlockLabel, Empty } from "@gradio/atoms"; import { BlockLabel, Empty } from "@gradio/atoms";
import { Video } from "@gradio/icons"; import { Video } from "@gradio/icons";
import { start, stop } from "./webrtc_utils"; import { start, stop } from "./webrtc_utils";
export let value: string | null = null; export let value: string | null = null;
export let label: string | undefined = undefined; export let label: string | undefined = undefined;
export let show_label = true; export let show_label = true;
export let rtc_configuration: Object | null = null; export let rtc_configuration: Object | null = null;
export let on_change_cb: (msg: "change" | "tick") => void; export let on_change_cb: (msg: "change" | "tick") => void;
export let server: { export let server: {
offer: (body: any) => Promise<any>; offer: (body: any) => Promise<any>;
}; };
let video_element: HTMLVideoElement; let video_element: HTMLVideoElement;
let _webrtc_id = Math.random().toString(36).substring(2); let _webrtc_id = Math.random().toString(36).substring(2);
let pc: RTCPeerConnection; let pc: RTCPeerConnection;
const dispatch = createEventDispatcher<{ const dispatch = createEventDispatcher<{
error: string; error: string;
tick: undefined; tick: undefined;
}>(); }>();
let stream_state = "closed"; let stream_state = "closed";
$: if (value === "start_webrtc_stream") { $: if (value === "start_webrtc_stream") {
_webrtc_id = Math.random().toString(36).substring(2); _webrtc_id = Math.random().toString(36).substring(2);
value = _webrtc_id; value = _webrtc_id;
pc = new RTCPeerConnection(rtc_configuration); pc = new RTCPeerConnection(rtc_configuration);
pc.addEventListener("connectionstatechange", async (event) => { pc.addEventListener("connectionstatechange", async (event) => {
switch (pc.connectionState) { switch (pc.connectionState) {
case "connected": case "connected":
stream_state = "open"; stream_state = "open";
dispatch("tick"); dispatch("tick");
break; break;
case "disconnected": case "disconnected":
stop(pc); stop(pc);
break; break;
case "failed": case "failed":
stream_state = "closed"; stream_state = "closed";
dispatch("error", "Connection failed!"); dispatch("error", "Connection failed!");
stop(pc); stop(pc);
break; break;
default: default:
break; break;
} }
}); });
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
// @ts-ignore // @ts-ignore
on_change_cb({ type: "connection_timeout" }); on_change_cb({ type: "connection_timeout" });
}, 5000); }, 5000);
start( start(
null, null,
pc, pc,
video_element, video_element,
server.offer, server.offer,
_webrtc_id, _webrtc_id,
"video", "video",
on_change_cb, on_change_cb,
) )
.then((connection) => { .then((connection) => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
pc = connection; pc = connection;
}) })
.catch(() => { .catch(() => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
dispatch( dispatch("error", "Too many concurrent users. Come back later!");
"error", });
"Too many concurrent users. Come back later!", }
);
});
}
</script> </script>
<BlockLabel {show_label} Icon={Video} label={label || "Video"} /> <BlockLabel {show_label} Icon={Video} label={label || "Video"} />
{#if value === "__webrtc_value__"} {#if value === "__webrtc_value__"}
<Empty unpadded_box={true} size="large"><Video /></Empty> <Empty unpadded_box={true} size="large"><Video /></Empty>
{/if} {/if}
<div class="wrap"> <div class="wrap">
<video <video
class:hidden={value === "__webrtc_value__"} class:hidden={value === "__webrtc_value__"}
bind:this={video_element} bind:this={video_element}
autoplay={true} autoplay={true}
on:loadeddata={dispatch.bind(null, "loadeddata")} on:loadeddata={dispatch.bind(null, "loadeddata")}
on:click={dispatch.bind(null, "click")} on:click={dispatch.bind(null, "click")}
on:play={dispatch.bind(null, "play")} on:play={dispatch.bind(null, "play")}
on:pause={dispatch.bind(null, "pause")} on:pause={dispatch.bind(null, "pause")}
on:ended={dispatch.bind(null, "ended")} on:ended={dispatch.bind(null, "ended")}
on:mouseover={dispatch.bind(null, "mouseover")} on:mouseover={dispatch.bind(null, "mouseover")}
on:mouseout={dispatch.bind(null, "mouseout")} on:mouseout={dispatch.bind(null, "mouseout")}
on:focus={dispatch.bind(null, "focus")} on:focus={dispatch.bind(null, "focus")}
on:blur={dispatch.bind(null, "blur")} on:blur={dispatch.bind(null, "blur")}
on:load on:load
data-testid={$$props["data-testid"]} data-testid={$$props["data-testid"]}
crossorigin="anonymous" crossorigin="anonymous"
> >
<track kind="captions" /> <track kind="captions" />
</video> </video>
</div> </div>
<style> <style>
.hidden { .hidden {
display: none; display: none;
} }
.wrap { .wrap {
position: relative; position: relative;
background-color: var(--background-fill-secondary); background-color: var(--background-fill-secondary);
height: var(--size-full); height: var(--size-full);
width: var(--size-full); width: var(--size-full);
border-radius: var(--radius-xl); border-radius: var(--radius-xl);
} }
.wrap :global(video) { .wrap :global(video) {
height: var(--size-full); height: var(--size-full);
width: var(--size-full); width: var(--size-full);
} }
</style> </style>

View File

@@ -1,472 +1,462 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher, onMount } from "svelte"; import { createEventDispatcher, onMount } from "svelte";
import type { ComponentType } from "svelte"; import type { ComponentType } from "svelte";
import { import {
Circle, Circle,
Square, Square,
DropdownArrow, DropdownArrow,
Spinner, Spinner,
Microphone as Mic, Microphone as Mic,
} from "@gradio/icons"; } from "@gradio/icons";
import type { I18nFormatter } from "@gradio/utils"; import type { I18nFormatter } from "@gradio/utils";
import { StreamingBar } from "@gradio/statustracker"; import { StreamingBar } from "@gradio/statustracker";
import WebcamPermissions from "./WebcamPermissions.svelte"; import WebcamPermissions from "./WebcamPermissions.svelte";
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
import { import {
get_devices, get_devices,
get_video_stream, get_video_stream,
set_available_devices, set_available_devices,
} from "./stream_utils"; } from "./stream_utils";
import { start, stop } from "./webrtc_utils"; import { start, stop } from "./webrtc_utils";
import PulsingIcon from "./PulsingIcon.svelte"; import PulsingIcon from "./PulsingIcon.svelte";
let video_source: HTMLVideoElement; let video_source: HTMLVideoElement;
let available_video_devices: MediaDeviceInfo[] = []; let available_video_devices: MediaDeviceInfo[] = [];
let selected_device: MediaDeviceInfo | null = null; let selected_device: MediaDeviceInfo | null = null;
let _time_limit: number | null = null; let _time_limit: number | null = null;
export let time_limit: number | null = null; export let time_limit: number | null = null;
let stream_state: "open" | "waiting" | "closed" = "closed"; let stream_state: "open" | "waiting" | "closed" = "closed";
export let on_change_cb: (msg: "tick" | "change") => void; export let on_change_cb: (msg: "tick" | "change") => void;
export let reject_cb: (msg: object) => void; export let reject_cb: (msg: object) => void;
export let mode: "send-receive" | "send"; export let mode: "send-receive" | "send";
const _webrtc_id = Math.random().toString(36).substring(2); const _webrtc_id = Math.random().toString(36).substring(2);
export let rtp_params: RTCRtpParameters = {} as RTCRtpParameters; export let rtp_params: RTCRtpParameters = {} as RTCRtpParameters;
export let icon: string | undefined | ComponentType = undefined; export let icon: string | undefined | ComponentType = undefined;
export let icon_button_color: string = "var(--color-accent)"; export let icon_button_color: string = "var(--color-accent)";
export let icon_radius: number = 50; export let icon_radius: number = 50;
export let pulse_color: string = "var(--color-accent)"; export let pulse_color: string = "var(--color-accent)";
export let button_labels: { start: string; stop: string; waiting: string }; export let button_labels: { start: string; stop: string; waiting: string };
export const modify_stream: ( export const modify_stream: (state: "open" | "closed" | "waiting") => void = (
state: "open" | "closed" | "waiting", state: "open" | "closed" | "waiting",
) => void = (state: "open" | "closed" | "waiting") => { ) => {
if (state === "closed") { if (state === "closed") {
_time_limit = null; _time_limit = null;
stream_state = "closed"; stream_state = "closed";
} else if (state === "waiting") { } else if (state === "waiting") {
stream_state = "waiting"; stream_state = "waiting";
} else { } else {
stream_state = "open"; stream_state = "open";
} }
}; };
let canvas: HTMLCanvasElement; let canvas: HTMLCanvasElement;
export let track_constraints: MediaTrackConstraints | null = null; export let track_constraints: MediaTrackConstraints | null = null;
export let rtc_configuration: Object; export let rtc_configuration: Object;
export let stream_every = 1; export let stream_every = 1;
export let server: { export let server: {
offer: (body: any) => Promise<any>; offer: (body: any) => Promise<any>;
}; };
export let include_audio: boolean; export let include_audio: boolean;
export let i18n: I18nFormatter; export let i18n: I18nFormatter;
const dispatch = createEventDispatcher<{ const dispatch = createEventDispatcher<{
tick: undefined; tick: undefined;
error: string; error: string;
start_recording: undefined; start_recording: undefined;
stop_recording: undefined; stop_recording: undefined;
close_stream: undefined; close_stream: undefined;
}>(); }>();
onMount(() => (canvas = document.createElement("canvas"))); onMount(() => (canvas = document.createElement("canvas")));
const handle_device_change = async (event: InputEvent): Promise<void> => { const handle_device_change = async (event: InputEvent): Promise<void> => {
const target = event.target as HTMLInputElement; const target = event.target as HTMLInputElement;
const device_id = target.value; const device_id = target.value;
await get_video_stream( await get_video_stream(
include_audio, include_audio,
video_source, video_source,
device_id, device_id,
track_constraints, track_constraints,
).then(async (local_stream) => { ).then(async (local_stream) => {
stream = local_stream; stream = local_stream;
selected_device = selected_device =
available_video_devices.find( available_video_devices.find(
(device) => device.deviceId === device_id, (device) => device.deviceId === device_id,
) || null; ) || null;
options_open = false; options_open = false;
}); });
}; };
async function access_webcam(): Promise<void> { async function access_webcam(): Promise<void> {
try { try {
get_video_stream( get_video_stream(include_audio, video_source, null, track_constraints)
include_audio, .then(async (local_stream) => {
video_source, webcam_accessed = true;
null, available_video_devices = await get_devices();
track_constraints, stream = local_stream;
) })
.then(async (local_stream) => { .then(() => set_available_devices(available_video_devices))
webcam_accessed = true; .then((devices) => {
available_video_devices = await get_devices(); available_video_devices = devices;
stream = local_stream;
})
.then(() => set_available_devices(available_video_devices))
.then((devices) => {
available_video_devices = devices;
const used_devices = stream const used_devices = stream
.getTracks() .getTracks()
.map((track) => track.getSettings()?.deviceId)[0]; .map((track) => track.getSettings()?.deviceId)[0];
selected_device = used_devices selected_device = used_devices
? devices.find( ? devices.find((device) => device.deviceId === used_devices) ||
(device) => device.deviceId === used_devices, available_video_devices[0]
) || available_video_devices[0] : available_video_devices[0];
: available_video_devices[0]; });
});
if ( if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
!navigator.mediaDevices || dispatch("error", i18n("image.no_webcam_support"));
!navigator.mediaDevices.getUserMedia }
) { } catch (err) {
dispatch("error", i18n("image.no_webcam_support")); if (err instanceof DOMException && err.name == "NotAllowedError") {
} dispatch("error", i18n("image.allow_webcam_access"));
} catch (err) { } else {
if (err instanceof DOMException && err.name == "NotAllowedError") { throw err;
dispatch("error", i18n("image.allow_webcam_access")); }
} else { }
throw err; }
}
}
}
let recording = false; let recording = false;
let stream: MediaStream; let stream: MediaStream;
let webcam_accessed = false; let webcam_accessed = false;
let pc: RTCPeerConnection; let pc: RTCPeerConnection;
export let webrtc_id; export let webrtc_id;
async function start_webrtc(): Promise<void> { async function start_webrtc(): Promise<void> {
if (stream_state === "closed") { if (stream_state === "closed") {
pc = new RTCPeerConnection(rtc_configuration); pc = new RTCPeerConnection(rtc_configuration);
pc.addEventListener("connectionstatechange", async (event) => { pc.addEventListener("connectionstatechange", async (event) => {
switch (pc.connectionState) { switch (pc.connectionState) {
case "connected": case "connected":
stream_state = "open"; stream_state = "open";
_time_limit = time_limit; _time_limit = time_limit;
dispatch("tick"); dispatch("tick");
break; break;
case "disconnected": case "disconnected":
stream_state = "closed"; stream_state = "closed";
_time_limit = null; _time_limit = null;
stop(pc); stop(pc);
await access_webcam(); await access_webcam();
break; break;
case "failed": case "failed":
stream_state = "closed"; stream_state = "closed";
_time_limit = null; _time_limit = null;
dispatch("error", "Connection failed!"); dispatch("error", "Connection failed!");
stop(pc); stop(pc);
break; break;
default: default:
break; break;
} }
}); });
stream_state = "waiting"; stream_state = "waiting";
webrtc_id = Math.random().toString(36).substring(2); webrtc_id = Math.random().toString(36).substring(2);
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
// @ts-ignore // @ts-ignore
on_change_cb({ type: "connection_timeout" }); on_change_cb({ type: "connection_timeout" });
}, 5000); }, 5000);
start( start(
stream, stream,
pc, pc,
mode === "send" ? null : video_source, mode === "send" ? null : video_source,
server.offer, server.offer,
webrtc_id, webrtc_id,
"video", "video",
on_change_cb, on_change_cb,
rtp_params, rtp_params,
undefined, undefined,
reject_cb, reject_cb,
) )
.then((connection) => { .then((connection) => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
pc = connection; pc = connection;
}) })
.catch(() => { .catch(() => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
console.info("catching"); console.info("catching");
stream_state = "closed"; stream_state = "closed";
}); });
} else { } else {
stop(pc); stop(pc);
stream_state = "closed"; stream_state = "closed";
_time_limit = null; _time_limit = null;
await access_webcam(); await access_webcam();
} }
} }
let options_open = false; let options_open = false;
export function click_outside(node: Node, cb: any): any { export function click_outside(node: Node, cb: any): any {
const handle_click = (event: MouseEvent): void => { const handle_click = (event: MouseEvent): void => {
if ( if (
node && node &&
!node.contains(event.target as Node) && !node.contains(event.target as Node) &&
!event.defaultPrevented !event.defaultPrevented
) { ) {
cb(event); cb(event);
} }
}; };
document.addEventListener("click", handle_click, true); document.addEventListener("click", handle_click, true);
return { return {
destroy() { destroy() {
document.removeEventListener("click", handle_click, true); document.removeEventListener("click", handle_click, true);
}, },
}; };
} }
function handle_click_outside(event: MouseEvent): void { function handle_click_outside(event: MouseEvent): void {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
options_open = false; options_open = false;
} }
const audio_source_callback = () => video_source.srcObject as MediaStream; const audio_source_callback = () => video_source.srcObject as MediaStream;
</script> </script>
<div class="wrap"> <div class="wrap">
<StreamingBar time_limit={_time_limit} /> <StreamingBar time_limit={_time_limit} />
{#if stream_state === "open" && include_audio} {#if stream_state === "open" && include_audio}
<div class="audio-indicator"> <div class="audio-indicator">
<PulsingIcon <PulsingIcon
{stream_state} {stream_state}
{audio_source_callback} {audio_source_callback}
icon={icon || Mic} icon={icon || Mic}
{icon_button_color} {icon_button_color}
{pulse_color} {pulse_color}
{icon_radius} {icon_radius}
/> />
</div> </div>
{/if} {/if}
<!-- svelte-ignore a11y-media-has-caption --> <!-- svelte-ignore a11y-media-has-caption -->
<!-- need to suppress for video streaming https://github.com/sveltejs/svelte/issues/5967 --> <!-- need to suppress for video streaming https://github.com/sveltejs/svelte/issues/5967 -->
<video <video
bind:this={video_source} bind:this={video_source}
class:hide={!webcam_accessed} class:hide={!webcam_accessed}
class:flip={stream_state != "open" || class:flip={stream_state != "open" ||
(stream_state === "open" && include_audio)} (stream_state === "open" && include_audio)}
autoplay={true} autoplay={true}
playsinline={true} playsinline={true}
/> />
<!-- svelte-ignore a11y-missing-attribute --> <!-- svelte-ignore a11y-missing-attribute -->
{#if !webcam_accessed} {#if !webcam_accessed}
<div <div
in:fade={{ delay: 100, duration: 200 }} in:fade={{ delay: 100, duration: 200 }}
title="grant webcam access" title="grant webcam access"
style="height: 100%" style="height: 100%"
> >
<WebcamPermissions on:click={async () => access_webcam()} /> <WebcamPermissions on:click={async () => access_webcam()} />
</div> </div>
{:else} {:else}
<div class="button-wrap"> <div class="button-wrap">
<button on:click={start_webrtc} aria-label={"start stream"}> <button on:click={start_webrtc} aria-label={"start stream"}>
{#if stream_state === "waiting"} {#if stream_state === "waiting"}
<div class="icon-with-text"> <div class="icon-with-text">
<div class="icon color-primary" title="spinner"> <div class="icon color-primary" title="spinner">
<Spinner /> <Spinner />
</div> </div>
{button_labels.waiting || i18n("audio.waiting")} {button_labels.waiting || i18n("audio.waiting")}
</div> </div>
{:else if stream_state === "open"} {:else if stream_state === "open"}
<div class="icon-with-text"> <div class="icon-with-text">
<div class="icon color-primary" title="stop recording"> <div class="icon color-primary" title="stop recording">
<Square /> <Square />
</div> </div>
{button_labels.stop || i18n("audio.stop")} {button_labels.stop || i18n("audio.stop")}
</div> </div>
{:else} {:else}
<div class="icon-with-text"> <div class="icon-with-text">
<div class="icon color-primary" title="start recording"> <div class="icon color-primary" title="start recording">
<Circle /> <Circle />
</div> </div>
{button_labels.start || i18n("audio.record")} {button_labels.start || i18n("audio.record")}
</div> </div>
{/if} {/if}
</button> </button>
{#if !recording} {#if !recording}
<button <button
class="icon" class="icon"
on:click={() => (options_open = true)} on:click={() => (options_open = true)}
aria-label="select input source" aria-label="select input source"
> >
<DropdownArrow /> <DropdownArrow />
</button> </button>
{/if} {/if}
</div> </div>
{#if options_open && selected_device} {#if options_open && selected_device}
<select <select
class="select-wrap" class="select-wrap"
aria-label="select source" aria-label="select source"
use:click_outside={handle_click_outside} use:click_outside={handle_click_outside}
on:change={handle_device_change} on:change={handle_device_change}
> >
<button <button
class="inset-icon" class="inset-icon"
on:click|stopPropagation={() => (options_open = false)} on:click|stopPropagation={() => (options_open = false)}
> >
<DropdownArrow /> <DropdownArrow />
</button> </button>
{#if available_video_devices.length === 0} {#if available_video_devices.length === 0}
<option value="">{i18n("common.no_devices")}</option> <option value="">{i18n("common.no_devices")}</option>
{:else} {:else}
{#each available_video_devices as device} {#each available_video_devices as device}
<option <option
value={device.deviceId} value={device.deviceId}
selected={selected_device.deviceId === selected={selected_device.deviceId === device.deviceId}
device.deviceId} >
> {device.label}
{device.label} </option>
</option> {/each}
{/each} {/if}
{/if} </select>
</select> {/if}
{/if} {/if}
{/if}
</div> </div>
<style> <style>
.wrap { .wrap {
position: relative; position: relative;
width: var(--size-full); width: var(--size-full);
height: var(--size-full); height: var(--size-full);
} }
.hide { .hide {
display: none; display: none;
} }
video { video {
width: var(--size-full); width: var(--size-full);
height: var(--size-full); height: var(--size-full);
object-fit: cover; object-fit: cover;
} }
.button-wrap { .button-wrap {
position: absolute; position: absolute;
background-color: var(--block-background-fill); background-color: var(--block-background-fill);
border: 1px solid var(--border-color-primary); border: 1px solid var(--border-color-primary);
border-radius: var(--radius-xl); border-radius: var(--radius-xl);
padding: var(--size-1-5); padding: var(--size-1-5);
display: flex; display: flex;
bottom: var(--size-2); bottom: var(--size-2);
left: 50%; left: 50%;
transform: translate(-50%, 0); transform: translate(-50%, 0);
box-shadow: var(--shadow-drop-lg); box-shadow: var(--shadow-drop-lg);
border-radius: var(--radius-xl); border-radius: var(--radius-xl);
line-height: var(--size-3); line-height: var(--size-3);
color: var(--button-secondary-text-color); color: var(--button-secondary-text-color);
} }
.icon-with-text { .icon-with-text {
min-width: var(--size-16); min-width: var(--size-16);
align-items: center; align-items: center;
margin: 0 var(--spacing-xl); margin: 0 var(--spacing-xl);
display: flex; display: flex;
justify-content: space-evenly; justify-content: space-evenly;
/* Add gap between icon and text */ /* Add gap between icon and text */
gap: var(--size-2); gap: var(--size-2);
} }
.audio-indicator { .audio-indicator {
position: absolute; position: absolute;
top: var(--size-2); top: var(--size-2);
right: var(--size-2); right: var(--size-2);
z-index: var(--layer-2); z-index: var(--layer-2);
height: var(--size-5); height: var(--size-5);
width: var(--size-5); width: var(--size-5);
} }
@media (--screen-md) { @media (--screen-md) {
button { button {
bottom: var(--size-4); bottom: var(--size-4);
} }
} }
@media (--screen-xl) { @media (--screen-xl) {
button { button {
bottom: var(--size-8); bottom: var(--size-8);
} }
} }
.icon { .icon {
width: 18px; width: 18px;
height: 18px; height: 18px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
} }
.color-primary { .color-primary {
fill: var(--primary-600); fill: var(--primary-600);
stroke: var(--primary-600); stroke: var(--primary-600);
color: var(--primary-600); color: var(--primary-600);
} }
.flip { .flip {
transform: scaleX(-1); transform: scaleX(-1);
} }
.select-wrap { .select-wrap {
-webkit-appearance: none; -webkit-appearance: none;
-moz-appearance: none; -moz-appearance: none;
appearance: none; appearance: none;
color: var(--button-secondary-text-color); color: var(--button-secondary-text-color);
background-color: transparent; background-color: transparent;
width: 95%; width: 95%;
font-size: var(--text-md); font-size: var(--text-md);
position: absolute; position: absolute;
bottom: var(--size-2); bottom: var(--size-2);
background-color: var(--block-background-fill); background-color: var(--block-background-fill);
box-shadow: var(--shadow-drop-lg); box-shadow: var(--shadow-drop-lg);
border-radius: var(--radius-xl); border-radius: var(--radius-xl);
z-index: var(--layer-top); z-index: var(--layer-top);
border: 1px solid var(--border-color-primary); border: 1px solid var(--border-color-primary);
text-align: left; text-align: left;
line-height: var(--size-4); line-height: var(--size-4);
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
left: 50%; left: 50%;
transform: translate(-50%, 0); transform: translate(-50%, 0);
max-width: var(--size-52); max-width: var(--size-52);
} }
.select-wrap > option { .select-wrap > option {
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
border-bottom: 1px solid var(--border-color-accent); border-bottom: 1px solid var(--border-color-accent);
padding-right: var(--size-8); padding-right: var(--size-8);
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
} }
.select-wrap > option:hover { .select-wrap > option:hover {
background-color: var(--color-accent); background-color: var(--color-accent);
} }
.select-wrap > option:last-child { .select-wrap > option:last-child {
border: none; border: none;
} }
.inset-icon { .inset-icon {
position: absolute; position: absolute;
top: 5px; top: 5px;
right: -6.5px; right: -6.5px;
width: var(--size-10); width: var(--size-10);
height: var(--size-5); height: var(--size-5);
opacity: 0.8; opacity: 0.8;
} }
@media (--screen-md) { @media (--screen-md) {
.wrap { .wrap {
font-size: var(--text-lg); font-size: var(--text-lg);
} }
} }
</style> </style>

View File

@@ -1,49 +1,50 @@
<script lang="ts"> <script lang="ts">
import { Webcam } from "@gradio/icons"; import { Webcam } from "@gradio/icons";
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
export let icon = Webcam; export let icon = Webcam;
$: text = icon === Webcam ? "Click to Access Webcam" : "Click to Access Microphone"; $: text =
icon === Webcam ? "Click to Access Webcam" : "Click to Access Microphone";
const dispatch = createEventDispatcher<{ const dispatch = createEventDispatcher<{
click: undefined; click: undefined;
}>(); }>();
</script> </script>
<button style:height="100%" on:click={() => dispatch("click")}> <button style:height="100%" on:click={() => dispatch("click")}>
<div class="wrap"> <div class="wrap">
<span class="icon-wrap"> <span class="icon-wrap">
<svelte:component this={icon} /> <svelte:component this={icon} />
</span> </span>
{text} {text}
</div> </div>
</button> </button>
<style> <style>
button { button {
cursor: pointer; cursor: pointer;
width: var(--size-full); width: var(--size-full);
} }
.wrap { .wrap {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
min-height: var(--size-60); min-height: var(--size-60);
color: var(--block-label-text-color); color: var(--block-label-text-color);
height: 100%; height: 100%;
padding-top: var(--size-3); padding-top: var(--size-3);
} }
.icon-wrap { .icon-wrap {
width: 30px; width: 30px;
margin-bottom: var(--spacing-lg); margin-bottom: var(--spacing-lg);
} }
@media (--screen-md) { @media (--screen-md) {
.wrap { .wrap {
font-size: var(--text-lg); font-size: var(--text-lg);
} }
} }
</style> </style>

View File

@@ -51,10 +51,10 @@ export async function start(
server_fn, server_fn,
webrtc_id, webrtc_id,
modality: "video" | "audio" = "video", modality: "video" | "audio" = "video",
on_change_cb: (msg: "change" | "tick") => void = () => { }, on_change_cb: (msg: "change" | "tick") => void = () => {},
rtp_params = {}, rtp_params = {},
additional_message_cb: (msg: object) => void = () => { }, additional_message_cb: (msg: object) => void = () => {},
reject_cb: (msg: object) => void = () => { }, reject_cb: (msg: object) => void = () => {},
) { ) {
pc = createPeerConnection(pc, node); pc = createPeerConnection(pc, node);
const data_channel = pc.createDataChannel("text"); const data_channel = pc.createDataChannel("text");
@@ -105,7 +105,11 @@ export async function start(
return pc; return pc;
} }
function make_offer(server_fn: any, body, reject_cb: (msg: object) => void = () => { }): Promise<object> { function make_offer(
server_fn: any,
body,
reject_cb: (msg: object) => void = () => {},
): Promise<object> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
server_fn(body).then((data) => { server_fn(body).then((data) => {
console.debug("data", data); console.debug("data", data);
@@ -123,7 +127,7 @@ async function negotiate(
pc: RTCPeerConnection, pc: RTCPeerConnection,
server_fn: any, server_fn: any,
webrtc_id: string, webrtc_id: string,
reject_cb: (msg: object) => void = () => { }, reject_cb: (msg: object) => void = () => {},
): Promise<void> { ): Promise<void> {
return pc return pc
.createOffer() .createOffer()
@@ -150,11 +154,15 @@ async function negotiate(
}) })
.then(() => { .then(() => {
var offer = pc.localDescription; var offer = pc.localDescription;
return make_offer(server_fn, { return make_offer(
sdp: offer.sdp, server_fn,
type: offer.type, {
webrtc_id: webrtc_id, sdp: offer.sdp,
}, reject_cb); type: offer.type,
webrtc_id: webrtc_id,
},
reject_cb,
);
}) })
.then((response) => { .then((response) => {
return response; return response;

View File

@@ -52,6 +52,7 @@ format:
ruff format . ruff format .
ruff check --fix . ruff check --fix .
ruff check --select I --fix . ruff check --select I --fix .
cd frontend && npx prettier --write . && cd ..
docs: docs:
mkdocs serve -a localhost:8081 mkdocs serve -a localhost:8081