Spaces:
Paused
Paused
enable screen sharing
Browse files
frontend/src/lib/components/MediaListSwitcher.svelte
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
<script lang="ts">
|
| 2 |
import { mediaDevices, mediaStreamActions } from '$lib/mediaStream';
|
|
|
|
| 3 |
import { onMount } from 'svelte';
|
| 4 |
|
| 5 |
let deviceId: string = '';
|
|
@@ -14,13 +15,20 @@
|
|
| 14 |
});
|
| 15 |
</script>
|
| 16 |
|
| 17 |
-
<div class="text-xs">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
{#if $mediaDevices}
|
| 19 |
<select
|
| 20 |
bind:value={deviceId}
|
| 21 |
on:change={() => mediaStreamActions.switchCamera(deviceId)}
|
| 22 |
id="devices-list"
|
| 23 |
-
class="border-1 cursor-pointer rounded-md border-gray-
|
| 24 |
>
|
| 25 |
{#each $mediaDevices as device, i}
|
| 26 |
<option value={device.deviceId}>{device.label}</option>
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
import { mediaDevices, mediaStreamActions } from '$lib/mediaStream';
|
| 3 |
+
import Screen from '$lib/icons/screen.svelte';
|
| 4 |
import { onMount } from 'svelte';
|
| 5 |
|
| 6 |
let deviceId: string = '';
|
|
|
|
| 15 |
});
|
| 16 |
</script>
|
| 17 |
|
| 18 |
+
<div class="flex items-center justify-center text-xs">
|
| 19 |
+
<button
|
| 20 |
+
title="Share your screen"
|
| 21 |
+
class="border-1 my-1 block cursor-pointer rounded-md border-gray-500 border-opacity-50 bg-slate-100 bg-opacity-30 p-[2px] font-medium text-white"
|
| 22 |
+
on:click={() => mediaStreamActions.startScreenCapture()}
|
| 23 |
+
>
|
| 24 |
+
<Screen classList={'w-100'} />
|
| 25 |
+
</button>
|
| 26 |
{#if $mediaDevices}
|
| 27 |
<select
|
| 28 |
bind:value={deviceId}
|
| 29 |
on:change={() => mediaStreamActions.switchCamera(deviceId)}
|
| 30 |
id="devices-list"
|
| 31 |
+
class="border-1 block cursor-pointer rounded-md border-gray-800 border-opacity-50 bg-slate-100 bg-opacity-30 p-[2px] font-medium text-white"
|
| 32 |
>
|
| 33 |
{#each $mediaDevices as device, i}
|
| 34 |
<option value={device.deviceId}>{device.label}</option>
|
frontend/src/lib/components/VideoInput.svelte
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
<script lang="ts">
|
| 2 |
import 'rvfc-polyfill';
|
| 3 |
-
|
|
|
|
| 4 |
import {
|
| 5 |
mediaStreamStatus,
|
| 6 |
MediaStreamStatusEnum,
|
|
@@ -11,10 +12,20 @@
|
|
| 11 |
import MediaListSwitcher from './MediaListSwitcher.svelte';
|
| 12 |
|
| 13 |
let videoEl: HTMLVideoElement;
|
|
|
|
|
|
|
| 14 |
let videoFrameCallbackId: number;
|
| 15 |
-
const WIDTH =
|
| 16 |
-
const HEIGHT =
|
|
|
|
|
|
|
| 17 |
let selectedDevice: string = '';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
$: {
|
| 19 |
console.log(selectedDevice);
|
| 20 |
}
|
|
@@ -25,8 +36,22 @@
|
|
| 25 |
$: if (videoEl) {
|
| 26 |
videoEl.srcObject = $mediaStream;
|
| 27 |
}
|
|
|
|
| 28 |
async function onFrameChange(now: DOMHighResTimeStamp, metadata: VideoFrameCallbackMetadata) {
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
onFrameChangeStore.set({ blob });
|
| 31 |
videoFrameCallbackId = videoEl.requestVideoFrameCallback(onFrameChange);
|
| 32 |
}
|
|
@@ -34,24 +59,17 @@
|
|
| 34 |
$: if ($mediaStreamStatus == MediaStreamStatusEnum.CONNECTED) {
|
| 35 |
videoFrameCallbackId = videoEl.requestVideoFrameCallback(onFrameChange);
|
| 36 |
}
|
| 37 |
-
async function
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
const ctx = canvas.getContext('2d') as OffscreenCanvasRenderingContext2D;
|
| 44 |
-
ctx.drawImage(
|
| 45 |
-
videoEl,
|
| 46 |
-
videoW / 2 - (videoH * aspectRatio) / 2,
|
| 47 |
-
0,
|
| 48 |
-
videoH * aspectRatio,
|
| 49 |
-
videoH,
|
| 50 |
-
0,
|
| 51 |
-
0,
|
| 52 |
-
WIDTH,
|
| 53 |
-
HEIGHT
|
| 54 |
-
);
|
| 55 |
const blob = await canvas.convertToBlob({ type: 'image/jpeg', quality: 1 });
|
| 56 |
return blob;
|
| 57 |
}
|
|
@@ -60,7 +78,7 @@
|
|
| 60 |
<div class="relative mx-auto max-w-lg overflow-hidden rounded-lg border border-slate-300">
|
| 61 |
<div class="relative z-10 aspect-square w-full object-cover">
|
| 62 |
{#if $mediaDevices.length > 0}
|
| 63 |
-
<div class="absolute bottom-0 right-0">
|
| 64 |
<MediaListSwitcher />
|
| 65 |
</div>
|
| 66 |
{/if}
|
|
@@ -72,6 +90,8 @@
|
|
| 72 |
muted
|
| 73 |
loop
|
| 74 |
></video>
|
|
|
|
|
|
|
| 75 |
</div>
|
| 76 |
<div class="absolute left-0 top-0 flex aspect-square w-full items-center justify-center">
|
| 77 |
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 448" class="w-40 p-5 opacity-20">
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
import 'rvfc-polyfill';
|
| 3 |
+
|
| 4 |
+
import { onDestroy, onMount } from 'svelte';
|
| 5 |
import {
|
| 6 |
mediaStreamStatus,
|
| 7 |
MediaStreamStatusEnum,
|
|
|
|
| 12 |
import MediaListSwitcher from './MediaListSwitcher.svelte';
|
| 13 |
|
| 14 |
let videoEl: HTMLVideoElement;
|
| 15 |
+
let canvasEl: HTMLCanvasElement;
|
| 16 |
+
let ctx: CanvasRenderingContext2D;
|
| 17 |
let videoFrameCallbackId: number;
|
| 18 |
+
const WIDTH = 768;
|
| 19 |
+
const HEIGHT = 768;
|
| 20 |
+
// ajust the throttle time to your needs
|
| 21 |
+
const THROTTLE_TIME = 1000 / 15;
|
| 22 |
let selectedDevice: string = '';
|
| 23 |
+
|
| 24 |
+
onMount(() => {
|
| 25 |
+
ctx = canvasEl.getContext('2d') as CanvasRenderingContext2D;
|
| 26 |
+
canvasEl.width = WIDTH;
|
| 27 |
+
canvasEl.height = HEIGHT;
|
| 28 |
+
});
|
| 29 |
$: {
|
| 30 |
console.log(selectedDevice);
|
| 31 |
}
|
|
|
|
| 36 |
$: if (videoEl) {
|
| 37 |
videoEl.srcObject = $mediaStream;
|
| 38 |
}
|
| 39 |
+
let lastMillis = 0;
|
| 40 |
async function onFrameChange(now: DOMHighResTimeStamp, metadata: VideoFrameCallbackMetadata) {
|
| 41 |
+
if (now - lastMillis < THROTTLE_TIME) {
|
| 42 |
+
videoFrameCallbackId = videoEl.requestVideoFrameCallback(onFrameChange);
|
| 43 |
+
return;
|
| 44 |
+
}
|
| 45 |
+
const videoWidth = videoEl.videoWidth;
|
| 46 |
+
const videoHeight = videoEl.videoHeight;
|
| 47 |
+
const blob = await grapCropBlobImg(
|
| 48 |
+
videoEl,
|
| 49 |
+
videoWidth / 2 - WIDTH / 2,
|
| 50 |
+
videoHeight / 2 - HEIGHT / 2,
|
| 51 |
+
WIDTH,
|
| 52 |
+
HEIGHT
|
| 53 |
+
);
|
| 54 |
+
|
| 55 |
onFrameChangeStore.set({ blob });
|
| 56 |
videoFrameCallbackId = videoEl.requestVideoFrameCallback(onFrameChange);
|
| 57 |
}
|
|
|
|
| 59 |
$: if ($mediaStreamStatus == MediaStreamStatusEnum.CONNECTED) {
|
| 60 |
videoFrameCallbackId = videoEl.requestVideoFrameCallback(onFrameChange);
|
| 61 |
}
|
| 62 |
+
async function grapCropBlobImg(
|
| 63 |
+
video: HTMLVideoElement,
|
| 64 |
+
x: number,
|
| 65 |
+
y: number,
|
| 66 |
+
width: number,
|
| 67 |
+
height: number
|
| 68 |
+
) {
|
| 69 |
+
const canvas = new OffscreenCanvas(width, height);
|
| 70 |
|
| 71 |
const ctx = canvas.getContext('2d') as OffscreenCanvasRenderingContext2D;
|
| 72 |
+
ctx.drawImage(video, x, y, width, height, 0, 0, width, height);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
const blob = await canvas.convertToBlob({ type: 'image/jpeg', quality: 1 });
|
| 74 |
return blob;
|
| 75 |
}
|
|
|
|
| 78 |
<div class="relative mx-auto max-w-lg overflow-hidden rounded-lg border border-slate-300">
|
| 79 |
<div class="relative z-10 aspect-square w-full object-cover">
|
| 80 |
{#if $mediaDevices.length > 0}
|
| 81 |
+
<div class="absolute bottom-0 right-0 z-10">
|
| 82 |
<MediaListSwitcher />
|
| 83 |
</div>
|
| 84 |
{/if}
|
|
|
|
| 90 |
muted
|
| 91 |
loop
|
| 92 |
></video>
|
| 93 |
+
<canvas bind:this={canvasEl} class="absolute left-0 top-0 aspect-square w-full object-cover"
|
| 94 |
+
></canvas>
|
| 95 |
</div>
|
| 96 |
<div class="absolute left-0 top-0 flex aspect-square w-full items-center justify-center">
|
| 97 |
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 448" class="w-40 p-5 opacity-20">
|
frontend/src/lib/icons/screen.svelte
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
export let classList: string = '';
|
| 3 |
+
</script>
|
| 4 |
+
|
| 5 |
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -32 576 576" height="16px" class={classList}>
|
| 6 |
+
<path
|
| 7 |
+
fill="currentColor"
|
| 8 |
+
d="M64 0A64 64 0 0 0 0 64v288a64 64 0 0 0 64 64h176l-10.7 32H160a32 32 0 1 0 0 64h256a32 32 0 1 0 0-64h-69.3L336 416h176a64 64 0 0 0 64-64V64a64 64 0 0 0-64-64H64zm448 64v288H64V64h448z"
|
| 9 |
+
/>
|
| 10 |
+
</svg>
|
frontend/src/lib/mediaStream.ts
CHANGED
|
@@ -43,6 +43,33 @@ export const mediaStreamActions = {
|
|
| 43 |
mediaStream.set(null);
|
| 44 |
});
|
| 45 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
async switchCamera(mediaDevicedID: string) {
|
| 47 |
if (get(mediaStreamStatus) !== MediaStreamStatusEnum.CONNECTED) {
|
| 48 |
return;
|
|
|
|
| 43 |
mediaStream.set(null);
|
| 44 |
});
|
| 45 |
},
|
| 46 |
+
async startScreenCapture() {
|
| 47 |
+
const displayMediaOptions = {
|
| 48 |
+
video: {
|
| 49 |
+
displaySurface: "window",
|
| 50 |
+
},
|
| 51 |
+
audio: false,
|
| 52 |
+
surfaceSwitching: "include"
|
| 53 |
+
};
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
let captureStream = null;
|
| 57 |
+
|
| 58 |
+
try {
|
| 59 |
+
captureStream = await navigator.mediaDevices.getDisplayMedia(displayMediaOptions);
|
| 60 |
+
const videoTrack = captureStream.getVideoTracks()[0];
|
| 61 |
+
|
| 62 |
+
console.log("Track settings:");
|
| 63 |
+
console.log(JSON.stringify(videoTrack.getSettings(), null, 2));
|
| 64 |
+
console.log("Track constraints:");
|
| 65 |
+
console.log(JSON.stringify(videoTrack.getConstraints(), null, 2));
|
| 66 |
+
mediaStreamStatus.set(MediaStreamStatusEnum.CONNECTED);
|
| 67 |
+
mediaStream.set(captureStream)
|
| 68 |
+
} catch (err) {
|
| 69 |
+
console.error(err);
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
},
|
| 73 |
async switchCamera(mediaDevicedID: string) {
|
| 74 |
if (get(mediaStreamStatus) !== MediaStreamStatusEnum.CONNECTED) {
|
| 75 |
return;
|