Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
chenpangpang
open-webui
Commits
d17cdc80
Commit
d17cdc80
authored
Jun 07, 2024
by
Timothy J. Baek
Browse files
feat: video devices support
parent
e5ad7661
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
169 additions
and
73 deletions
+169
-73
src/lib/components/chat/MessageInput/CallOverlay.svelte
src/lib/components/chat/MessageInput/CallOverlay.svelte
+118
-73
src/lib/components/chat/MessageInput/CallOverlay/VideoInputMenu.svelte
...nents/chat/MessageInput/CallOverlay/VideoInputMenu.svelte
+51
-0
No files found.
src/lib/components/chat/MessageInput/CallOverlay.svelte
View file @
d17cdc80
...
@@ -5,7 +5,9 @@
...
@@ -5,7 +5,9 @@
import { blobToFile, calculateSHA256, extractSentences, findWordIndices } from '$lib/utils';
import { blobToFile, calculateSHA256, extractSentences, findWordIndices } from '$lib/utils';
import { synthesizeOpenAISpeech, transcribeAudio } from '$lib/apis/audio';
import { synthesizeOpenAISpeech, transcribeAudio } from '$lib/apis/audio';
import { toast } from 'svelte-sonner';
import { toast } from 'svelte-sonner';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import VideoInputMenu from './CallOverlay/VideoInputMenu.svelte';
const i18n = getContext('i18n');
const i18n = getContext('i18n');
...
@@ -25,13 +27,6 @@
...
@@ -25,13 +27,6 @@
let rmsLevel = 0;
let rmsLevel = 0;
let hasStartedSpeaking = false;
let hasStartedSpeaking = false;
let audioContext;
let analyser;
let dataArray;
let audioElement;
let animationFrameId;
let speechRecognition;
let currentUtterance = null;
let currentUtterance = null;
let mediaRecorder;
let mediaRecorder;
...
@@ -40,28 +35,6 @@
...
@@ -40,28 +35,6 @@
const MIN_DECIBELS = -45;
const MIN_DECIBELS = -45;
const VISUALIZER_BUFFER_LENGTH = 300;
const VISUALIZER_BUFFER_LENGTH = 300;
let visualizerData = Array(VISUALIZER_BUFFER_LENGTH).fill(0);
const startAudio = () => {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
analyser = audioContext.createAnalyser();
const source = audioContext.createMediaElementSource(audioElement);
source.connect(analyser);
analyser.connect(audioContext.destination);
analyser.fftSize = 32; // Adjust the fftSize
dataArray = new Uint8Array(analyser.frequencyBinCount);
visualize();
};
const visualize = () => {
analyser.getByteFrequencyData(dataArray);
div1Height = dataArray[1] / 2;
div2Height = dataArray[3] / 2;
div3Height = dataArray[5] / 2;
div4Height = dataArray[7] / 2;
animationFrameId = requestAnimationFrame(visualize);
};
// Function to calculate the RMS level from time domain data
// Function to calculate the RMS level from time domain data
const calculateRMS = (data: Uint8Array) => {
const calculateRMS = (data: Uint8Array) => {
let sumSquares = 0;
let sumSquares = 0;
...
@@ -333,23 +306,74 @@
...
@@ -333,23 +306,74 @@
mediaRecorder.start();
mediaRecorder.start();
};
};
let videoInputDevices = [];
let selectedVideoInputDeviceId = null;
const getVideoInputDevices = async () => {
const devices = await navigator.mediaDevices.enumerateDevices();
videoInputDevices = devices.filter((device) => device.kind === 'videoinput');
videoInputDevices = [
...videoInputDevices,
{
deviceId: 'screen',
label: 'Screen Share'
}
];
console.log(videoInputDevices);
if (selectedVideoInputDeviceId === null && videoInputDevices.length > 0) {
selectedVideoInputDeviceId = videoInputDevices[0].deviceId;
}
};
const startCamera = async () => {
const startCamera = async () => {
await getVideoInputDevices();
if (cameraStream === null) {
if (cameraStream === null) {
camera = true;
camera = true;
await tick();
await tick();
try {
try {
const video = document.getElementById('camera-feed');
await startVideoStream();
if (video) {
cameraStream = await navigator.mediaDevices.getUserMedia({ video: true });
video.srcObject = cameraStream;
await video.play();
}
} catch (err) {
} catch (err) {
console.error('Error accessing webcam: ', err);
console.error('Error accessing webcam: ', err);
}
}
}
}
};
};
const startVideoStream = async () => {
const video = document.getElementById('camera-feed');
if (video) {
if (selectedVideoInputDeviceId === 'screen') {
cameraStream = await navigator.mediaDevices.getDisplayMedia({
video: {
cursor: 'always'
},
audio: false
});
} else {
cameraStream = await navigator.mediaDevices.getUserMedia({
video: {
deviceId: selectedVideoInputDeviceId ? { exact: selectedVideoInputDeviceId } : undefined
}
});
}
video.srcObject = cameraStream;
await video.play();
}
};
const stopVideoStream = async () => {
if (cameraStream) {
const tracks = cameraStream.getTracks();
tracks.forEach((track) => track.stop());
}
cameraStream = null;
};
const takeScreenshot = () => {
const takeScreenshot = () => {
const video = document.getElementById('camera-feed');
const video = document.getElementById('camera-feed');
const canvas = document.getElementById('camera-canvas');
const canvas = document.getElementById('camera-canvas');
...
@@ -359,14 +383,13 @@
...
@@ -359,14 +383,13 @@
}
}
const context = canvas.getContext('2d');
const context = canvas.getContext('2d');
// Make the canvas match the video dimensions
// Make the canvas match the video dimensions
canvas.width = video.videoWidth;
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.height = video.videoHeight;
// Draw the flipped image from the video onto the canvas
context.save();
// Draw the image from the video onto the canvas
context.scale(-1, 1); // Flip horizontally
context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
context.drawImage(video, 0, 0, video.videoWidth * -1, video.videoHeight);
context.restore();
// Convert the canvas to a data base64 URL and console log it
// Convert the canvas to a data base64 URL and console log it
const dataURL = canvas.toDataURL('image/png');
const dataURL = canvas.toDataURL('image/png');
...
@@ -375,13 +398,8 @@
...
@@ -375,13 +398,8 @@
return dataURL;
return dataURL;
};
};
const stopCamera = () => {
const stopCamera = async () => {
if (cameraStream) {
await stopVideoStream();
const tracks = cameraStream.getTracks();
tracks.forEach((track) => track.stop());
}
cameraStream = null;
camera = false;
camera = false;
};
};
...
@@ -539,35 +557,62 @@
...
@@ -539,35 +557,62 @@
<div class="flex justify-between items-center pb-2 w-full">
<div class="flex justify-between items-center pb-2 w-full">
<div>
<div>
<Tooltip content="Camera">
{#if camera}
<button
<VideoInputMenu
class=" p-3 rounded-full bg-gray-50 dark:bg-gray-900"
devices={videoInputDevices}
type="button"
on:change={async (e) => {
on:click={() => {
console.log(e.detail);
startCamera();
selectedVideoInputDeviceId = e.detail;
await stopVideoStream();
await startVideoStream();
}}
}}
>
>
<svg
<button class=" p-3 rounded-full bg-gray-50 dark:bg-gray-900" type="button">
xmlns="http://www.w3.org/2000/svg"
<svg
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
viewBox="0 0 20 20"
stroke-width="1.5"
fill="currentColor"
stroke="currentColor"
class="size-5"
class="size-5"
>
<path
fill-rule="evenodd"
d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.433a.75.75 0 0 0 0-1.5H3.989a.75.75 0 0 0-.75.75v4.242a.75.75 0 0 0 1.5 0v-2.43l.31.31a7 7 0 0 0 11.712-3.138.75.75 0 0 0-1.449-.39Zm1.23-3.723a.75.75 0 0 0 .219-.53V2.929a.75.75 0 0 0-1.5 0V5.36l-.31-.31A7 7 0 0 0 3.239 8.188a.75.75 0 1 0 1.448.389A5.5 5.5 0 0 1 13.89 6.11l.311.31h-2.432a.75.75 0 0 0 0 1.5h4.243a.75.75 0 0 0 .53-.219Z"
clip-rule="evenodd"
/>
</svg>
</button>
</VideoInputMenu>
{:else}
<Tooltip content="Camera">
<button
class=" p-3 rounded-full bg-gray-50 dark:bg-gray-900"
type="button"
on:click={() => {
startCamera();
}}
>
>
<path
<svg
stroke-linecap="round"
xmlns="http://www.w3.org/2000/svg"
stroke-linejoin="round"
fill="none"
d="M6.827 6.175A2.31 2.31 0 0 1 5.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 0 0-1.134-.175 2.31 2.31 0 0 1-1.64-1.055l-.822-1.316a2.192 2.192 0 0 0-1.736-1.039 48.774 48.774 0 0 0-5.232 0 2.192 2.192 0 0 0-1.736 1.039l-.821 1.316Z"
viewBox="0 0 24 24"
/>
stroke-width="1.5"
<path
stroke="currentColor"
stroke-linecap="round"
class="size-5"
stroke-linejoin="round"
>
d="M16.5 12.75a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0ZM18.75 10.5h.008v.008h-.008V10.5Z"
<path
/>
stroke-linecap="round"
</svg>
stroke-linejoin="round"
</button>
d="M6.827 6.175A2.31 2.31 0 0 1 5.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 0 0-1.134-.175 2.31 2.31 0 0 1-1.64-1.055l-.822-1.316a2.192 2.192 0 0 0-1.736-1.039 48.774 48.774 0 0 0-5.232 0 2.192 2.192 0 0 0-1.736 1.039l-.821 1.316Z"
</Tooltip>
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.5 12.75a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0ZM18.75 10.5h.008v.008h-.008V10.5Z"
/>
</svg>
</button>
</Tooltip>
{/if}
</div>
</div>
<div>
<div>
...
...
src/lib/components/chat/MessageInput/CallOverlay/VideoInputMenu.svelte
0 → 100644
View file @
d17cdc80
<script lang="ts">
import { DropdownMenu } from 'bits-ui';
import { flyAndScale } from '$lib/utils/transitions';
import { getContext, createEventDispatcher } from 'svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
import Dropdown from '$lib/components/common/Dropdown.svelte';
export let onClose: Function = () => {};
export let devices: any;
let show = false;
</script>
<Dropdown
bind:show
on:change={(e) => {
if (e.detail === false) {
onClose();
}
}}
>
<slot />
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[180px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-[9999] bg-white dark:bg-gray-900 dark:text-white shadow"
sideOffset={6}
side="top"
align="start"
transition={flyAndScale}
>
{#each devices as device}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
dispatch('change', device.deviceId);
}}
>
<div class="flex items-center">
<div class=" line-clamp-1">
{device?.label ?? 'Camera'}
</div>
</div>
</DropdownMenu.Item>
{/each}
</DropdownMenu.Content>
</div>
</Dropdown>
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment