radames commited on
Commit
cb60b56
1 Parent(s): 9d30058

add fullscreen button and aspect ratio selector

Browse files
frontend/src/lib/components/AspectRatioSelect.svelte ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { createEventDispatcher } from 'svelte';
3
+
4
+ let options: string[] = ['1:1', '16:9', '4:3', '3:2', '3:4', '9:16'];
5
+ export let aspectRatio: number = 1;
6
+ const dispatchEvent = createEventDispatcher();
7
+
8
+ function onChange(e: Event) {
9
+ const target = e.target as HTMLSelectElement;
10
+ const value = target.value;
11
+ const [width, height] = value.split(':').map((v) => parseInt(v));
12
+ aspectRatio = width / height;
13
+ dispatchEvent('change', aspectRatio);
14
+ }
15
+ </script>
16
+
17
+ <div class="relative">
18
+ <select
19
+ on:change={onChange}
20
+ title="Aspect Ratio"
21
+ class="border-1 block cursor-pointer rounded-md border-gray-800 border-opacity-50 bg-slate-100 bg-opacity-30 p-1 font-medium text-white"
22
+ >
23
+ {#each options as option, i}
24
+ <option value={option}>{option}</option>
25
+ {/each}
26
+ </select>
27
+ </div>
frontend/src/lib/components/ImagePlayer.svelte CHANGED
@@ -4,11 +4,14 @@
4
 
5
  import Button from '$lib/components/Button.svelte';
6
  import Floppy from '$lib/icons/floppy.svelte';
7
- import { snapImage } from '$lib/utils';
 
8
 
9
  $: isLCMRunning = $lcmLiveStatus !== LCMLiveStatus.DISCONNECTED;
10
  $: console.log('isLCMRunning', isLCMRunning);
11
  let imageEl: HTMLImageElement;
 
 
12
  async function takeSnapshot() {
13
  if (isLCMRunning) {
14
  await snapImage(imageEl, {
@@ -19,6 +22,18 @@
19
  });
20
  }
21
  }
 
 
 
 
 
 
 
 
 
 
 
 
22
  </script>
23
 
24
  <div
@@ -26,12 +41,21 @@
26
  >
27
  <!-- svelte-ignore a11y-missing-attribute -->
28
  {#if isLCMRunning}
29
- <img
30
- bind:this={imageEl}
31
- class="aspect-square w-full rounded-lg"
32
- src={'/api/stream/' + $streamId}
33
- />
 
 
34
  <div class="absolute bottom-1 right-1">
 
 
 
 
 
 
 
35
  <Button
36
  on:click={takeSnapshot}
37
  disabled={!isLCMRunning}
 
4
 
5
  import Button from '$lib/components/Button.svelte';
6
  import Floppy from '$lib/icons/floppy.svelte';
7
+ import Expand from '$lib/icons/expand.svelte';
8
+ import { snapImage, expandWindow } from '$lib/utils';
9
 
10
  $: isLCMRunning = $lcmLiveStatus !== LCMLiveStatus.DISCONNECTED;
11
  $: console.log('isLCMRunning', isLCMRunning);
12
  let imageEl: HTMLImageElement;
13
+ let expandedWindow: Window;
14
+ let isExpanded = false;
15
  async function takeSnapshot() {
16
  if (isLCMRunning) {
17
  await snapImage(imageEl, {
 
22
  });
23
  }
24
  }
25
+ async function toggleFullscreen() {
26
+ if (isLCMRunning && !isExpanded) {
27
+ expandedWindow = expandWindow('/api/stream/' + $streamId);
28
+ expandedWindow.addEventListener('beforeunload', () => {
29
+ isExpanded = false;
30
+ });
31
+ isExpanded = true;
32
+ } else {
33
+ expandedWindow?.close();
34
+ isExpanded = false;
35
+ }
36
+ }
37
  </script>
38
 
39
  <div
 
41
  >
42
  <!-- svelte-ignore a11y-missing-attribute -->
43
  {#if isLCMRunning}
44
+ {#if !isExpanded}
45
+ <img
46
+ bind:this={imageEl}
47
+ class="aspect-square w-full rounded-lg"
48
+ src={'/api/stream/' + $streamId}
49
+ />
50
+ {/if}
51
  <div class="absolute bottom-1 right-1">
52
+ <Button
53
+ on:click={toggleFullscreen}
54
+ title={'Expand Fullscreen'}
55
+ classList={'text-sm ml-auto text-white p-1 shadow-lg rounded-lg opacity-50'}
56
+ >
57
+ <Expand classList={''} />
58
+ </Button>
59
  <Button
60
  on:click={takeSnapshot}
61
  disabled={!isLCMRunning}
frontend/src/lib/components/MediaListSwitcher.svelte CHANGED
@@ -1,21 +1,28 @@
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 = '';
 
 
 
 
 
7
  $: {
8
- console.log($mediaDevices);
9
  }
10
  $: {
11
- console.log(deviceId);
12
  }
13
- onMount(() => {
14
- deviceId = $mediaDevices[0].deviceId;
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 flex cursor-pointer gap-1 rounded-md border-gray-500 border-opacity-50 bg-slate-100 bg-opacity-30 p-1 font-medium text-white"
@@ -28,7 +35,7 @@
28
  {#if $mediaDevices}
29
  <select
30
  bind:value={deviceId}
31
- on:change={() => mediaStreamActions.switchCamera(deviceId)}
32
  id="devices-list"
33
  class="border-1 block cursor-pointer rounded-md border-gray-800 border-opacity-50 bg-slate-100 bg-opacity-30 p-1 font-medium text-white"
34
  >
 
1
  <script lang="ts">
2
  import { mediaDevices, mediaStreamActions } from '$lib/mediaStream';
3
  import Screen from '$lib/icons/screen.svelte';
4
+ import AspectRatioSelect from './AspectRatioSelect.svelte';
5
  import { onMount } from 'svelte';
6
 
7
  let deviceId: string = '';
8
+ let aspectRatio: number = 1;
9
+
10
+ onMount(() => {
11
+ deviceId = $mediaDevices[0].deviceId;
12
+ });
13
  $: {
14
+ console.log(deviceId);
15
  }
16
  $: {
17
+ console.log(aspectRatio);
18
  }
 
 
 
19
  </script>
20
 
21
+ <div class="flex items-center justify-center text-xs backdrop-blur-sm backdrop-grayscale">
22
+ <AspectRatioSelect
23
+ bind:aspectRatio
24
+ on:change={() => mediaStreamActions.switchCamera(deviceId, aspectRatio)}
25
+ />
26
  <button
27
  title="Share your screen"
28
  class="border-1 my-1 flex cursor-pointer gap-1 rounded-md border-gray-500 border-opacity-50 bg-slate-100 bg-opacity-30 p-1 font-medium text-white"
 
35
  {#if $mediaDevices}
36
  <select
37
  bind:value={deviceId}
38
+ on:change={() => mediaStreamActions.switchCamera(deviceId, aspectRatio)}
39
  id="devices-list"
40
  class="border-1 block cursor-pointer rounded-md border-gray-800 border-opacity-50 bg-slate-100 bg-opacity-30 p-1 font-medium text-white"
41
  >
frontend/src/lib/components/VideoInput.svelte CHANGED
@@ -10,6 +10,7 @@
10
  mediaDevices
11
  } from '$lib/mediaStream';
12
  import MediaListSwitcher from './MediaListSwitcher.svelte';
 
13
  export let width = 512;
14
  export let height = 512;
15
  const size = { width, height };
@@ -32,6 +33,7 @@
32
  $: {
33
  console.log(selectedDevice);
34
  }
 
35
  onDestroy(() => {
36
  if (videoFrameCallbackId) videoEl.cancelVideoFrameCallback(videoFrameCallbackId);
37
  });
@@ -47,18 +49,15 @@
47
  }
48
  const videoWidth = videoEl.videoWidth;
49
  const videoHeight = videoEl.videoHeight;
50
- let height0 = videoHeight;
51
- let width0 = videoWidth;
52
- let x0 = 0;
53
- let y0 = 0;
54
- if (videoWidth > videoHeight) {
55
- width0 = videoHeight;
56
- x0 = (videoWidth - videoHeight) / 2;
57
- } else {
58
- height0 = videoWidth;
59
- y0 = (videoHeight - videoWidth) / 2;
60
- }
61
- ctx.drawImage(videoEl, x0, y0, width0, height0, 0, 0, size.width, size.height);
62
  const blob = await new Promise<Blob>((resolve) => {
63
  canvasEl.toBlob(
64
  (blob) => {
@@ -78,14 +77,14 @@
78
  </script>
79
 
80
  <div class="relative mx-auto max-w-lg overflow-hidden rounded-lg border border-slate-300">
81
- <div class="relative z-10 aspect-square w-full object-cover">
82
  {#if $mediaDevices.length > 0}
83
- <div class="absolute bottom-0 right-0 z-10">
84
  <MediaListSwitcher />
85
  </div>
86
  {/if}
87
  <video
88
- class="pointer-events-none aspect-square w-full object-cover"
89
  bind:this={videoEl}
90
  on:loadeddata={() => {
91
  videoIsReady = true;
 
10
  mediaDevices
11
  } from '$lib/mediaStream';
12
  import MediaListSwitcher from './MediaListSwitcher.svelte';
13
+
14
  export let width = 512;
15
  export let height = 512;
16
  const size = { width, height };
 
33
  $: {
34
  console.log(selectedDevice);
35
  }
36
+
37
  onDestroy(() => {
38
  if (videoFrameCallbackId) videoEl.cancelVideoFrameCallback(videoFrameCallbackId);
39
  });
 
49
  }
50
  const videoWidth = videoEl.videoWidth;
51
  const videoHeight = videoEl.videoHeight;
52
+ // scale down video to fit canvas, size.width, size.height
53
+ const scale = Math.min(size.width / videoWidth, size.height / videoHeight);
54
+ const width0 = videoWidth * scale;
55
+ const height0 = videoHeight * scale;
56
+ const x0 = (size.width - width0) / 2;
57
+ const y0 = (size.height - height0) / 2;
58
+ ctx.clearRect(0, 0, size.width, size.height);
59
+ ctx.drawImage(videoEl, x0, y0, width0, height0);
60
+
 
 
 
61
  const blob = await new Promise<Blob>((resolve) => {
62
  canvasEl.toBlob(
63
  (blob) => {
 
77
  </script>
78
 
79
  <div class="relative mx-auto max-w-lg overflow-hidden rounded-lg border border-slate-300">
80
+ <div class="relative z-10 flex aspect-square w-full items-center justify-center object-cover">
81
  {#if $mediaDevices.length > 0}
82
+ <div class="absolute bottom-0 right-0 z-10 w-full bg-slate-400 bg-opacity-40">
83
  <MediaListSwitcher />
84
  </div>
85
  {/if}
86
  <video
87
+ class="pointer-events-none aspect-square w-full justify-center object-contain"
88
  bind:this={videoEl}
89
  on:loadeddata={() => {
90
  videoIsReady = true;
frontend/src/lib/icons/aspect.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 0 448 512" height="16px" class={classList}>
6
+ <path
7
+ fill="currentColor"
8
+ d="M32 32C14.3 32 0 46.3 0 64v96c0 17.7 14.3 32 32 32s32-14.3 32-32V96h64c17.7 0 32-14.3 32-32s-14.3-32-32-32H32zM64 352c0-17.7-14.3-32-32-32s-32 14.3-32 32v96c0 17.7 14.3 32 32 32h96c17.7 0 32-14.3 32-32s-14.3-32-32-32H64V352zM320 32c-17.7 0-32 14.3-32 32s14.3 32 32 32h64v64c0 17.7 14.3 32 32 32s32-14.3 32-32V64c0-17.7-14.3-32-32-32H320zM448 352c0-17.7-14.3-32-32-32s-32 14.3-32 32v64H320c-17.7 0-32 14.3-32 32s14.3 32 32 32h96c17.7 0 32-14.3 32-32V352z"
9
+ />
10
+ </svg>
frontend/src/lib/icons/expand.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 0 512 512" height="1em" class={classList}>
6
+ <path
7
+ fill="currentColor"
8
+ d="M.3 89.5C.1 91.6 0 93.8 0 96V224 416c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64V224 96c0-35.3-28.7-64-64-64H64c-2.2 0-4.4 .1-6.5 .3c-9.2 .9-17.8 3.8-25.5 8.2C21.8 46.5 13.4 55.1 7.7 65.5c-3.9 7.3-6.5 15.4-7.4 24zM48 224H464l0 192c0 8.8-7.2 16-16 16L64 432c-8.8 0-16-7.2-16-16l0-192z"
9
+ />
10
+ </svg>
frontend/src/lib/mediaStream.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { writable, type Writable, get } from 'svelte/store';
2
 
 
3
  export enum MediaStreamStatusEnum {
4
  INIT = "init",
5
  CONNECTED = "connected",
@@ -23,11 +24,17 @@ export const mediaStreamActions = {
23
  console.error(err);
24
  });
25
  },
26
- async start(mediaDevicedID?: string) {
27
  const constraints = {
28
  audio: false,
29
  video: {
30
- width: 1024, height: 1024, deviceId: mediaDevicedID
 
 
 
 
 
 
31
  }
32
  };
33
 
@@ -36,6 +43,7 @@ export const mediaStreamActions = {
36
  .then((stream) => {
37
  mediaStreamStatus.set(MediaStreamStatusEnum.CONNECTED);
38
  mediaStream.set(stream);
 
39
  })
40
  .catch((err) => {
41
  console.error(`${err.name}: ${err.message}`);
@@ -65,19 +73,33 @@ export const mediaStreamActions = {
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;
76
  }
77
  const constraints = {
78
  audio: false,
79
- video: { width: 1024, height: 1024, deviceId: mediaDevicedID }
 
 
 
 
 
 
 
 
80
  };
 
81
  await navigator.mediaDevices
82
  .getUserMedia(constraints)
83
  .then((stream) => {
 
1
+ import { writable, type Writable, type Readable, get, derived } from 'svelte/store';
2
 
3
+ const BASE_HEIGHT = 720;
4
  export enum MediaStreamStatusEnum {
5
  INIT = "init",
6
  CONNECTED = "connected",
 
24
  console.error(err);
25
  });
26
  },
27
+ async start(mediaDevicedID?: string, aspectRatio: number = 1) {
28
  const constraints = {
29
  audio: false,
30
  video: {
31
+ width: {
32
+ ideal: BASE_HEIGHT * aspectRatio,
33
+ },
34
+ height: {
35
+ ideal: BASE_HEIGHT,
36
+ },
37
+ deviceId: mediaDevicedID
38
  }
39
  };
40
 
 
43
  .then((stream) => {
44
  mediaStreamStatus.set(MediaStreamStatusEnum.CONNECTED);
45
  mediaStream.set(stream);
46
+
47
  })
48
  .catch((err) => {
49
  console.error(`${err.name}: ${err.message}`);
 
73
  console.log(JSON.stringify(videoTrack.getConstraints(), null, 2));
74
  mediaStreamStatus.set(MediaStreamStatusEnum.CONNECTED);
75
  mediaStream.set(captureStream)
76
+
77
+ const capabilities = videoTrack.getCapabilities();
78
+ const aspectRatio = capabilities.aspectRatio;
79
+ console.log('Aspect Ratio Constraints:', aspectRatio);
80
  } catch (err) {
81
  console.error(err);
82
  }
83
 
84
  },
85
+ async switchCamera(mediaDevicedID: string, aspectRatio: number) {
86
+ console.log("Switching camera");
87
  if (get(mediaStreamStatus) !== MediaStreamStatusEnum.CONNECTED) {
88
  return;
89
  }
90
  const constraints = {
91
  audio: false,
92
+ video: {
93
+ width: {
94
+ ideal: BASE_HEIGHT * aspectRatio,
95
+ },
96
+ height: {
97
+ ideal: BASE_HEIGHT,
98
+ },
99
+ deviceId: mediaDevicedID
100
+ }
101
  };
102
+ console.log("Switching camera", constraints);
103
  await navigator.mediaDevices
104
  .getUserMedia(constraints)
105
  .then((stream) => {
frontend/src/lib/utils.ts CHANGED
@@ -36,3 +36,46 @@ export function snapImage(imageEl: HTMLImageElement, info: IImageInfo) {
36
  console.log(err);
37
  }
38
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  console.log(err);
37
  }
38
  }
39
+
40
+ export function expandWindow(steramURL: string): Window {
41
+ const html = `
42
+ <html>
43
+ <head>
44
+ <title>Real-Time Latent Consistency Model</title>
45
+ <style>
46
+ body {
47
+ margin: 0;
48
+ padding: 0;
49
+ background-color: black;
50
+ }
51
+ </style>
52
+ </head>
53
+ <body>
54
+ <script>
55
+ let isFullscreen = false;
56
+ window.onkeydown = function(event) {
57
+ switch (event.code) {
58
+ case "Escape":
59
+ window.close();
60
+ break;
61
+ case "Enter":
62
+ if (isFullscreen) {
63
+ document.exitFullscreen();
64
+ isFullscreen = false;
65
+ } else {
66
+ document.documentElement.requestFullscreen();
67
+ isFullscreen = true;
68
+ }
69
+ break;
70
+ }
71
+ }
72
+ </script>
73
+
74
+ <img src="${steramURL}" style="width: 100%; height: 100%; object-fit: contain;" />
75
+ </body>
76
+ </html>
77
+ `;
78
+ const newWindow = window.open("", "_blank", "width=1024,height=1024,scrollbars=0,resizable=1,toolbar=0,menubar=0,location=0,directories=0,status=0") as Window;
79
+ newWindow.document.write(html);
80
+ return newWindow;
81
+ }