File size: 4,417 Bytes
1123781
246efdb
12ffaf3
246efdb
ff9325e
3207814
 
 
3dd0aef
246efdb
 
 
 
 
 
 
 
cb60b56
46bd9ac
ff9325e
246efdb
 
 
 
46bd9ac
12ffaf3
a659304
246efdb
 
12ffaf3
 
246efdb
 
 
 
 
12ffaf3
246efdb
3dd0aef
246efdb
cb60b56
ff9325e
246efdb
 
 
 
 
 
 
 
 
ff9325e
 
12ffaf3
246efdb
 
 
a659304
12ffaf3
 
 
246efdb
 
12ffaf3
 
cb60b56
 
 
 
 
 
 
 
 
547a086
246efdb
547a086
 
 
246efdb
 
547a086
 
1d3190d
ff9325e
 
 
246efdb
 
 
 
 
 
 
 
 
1ea3019
 
246efdb
1ea3019
 
 
 
 
 
 
1123781
 
246efdb
 
 
 
 
 
3dd0aef
246efdb
3dd0aef
1ea3019
246efdb
 
 
1ea3019
246efdb
1ea3019
3dd0aef
 
d1f4c77
cb60b56
d1f4c77
246efdb
547a086
 
d1f4c77
 
 
 
 
246efdb
 
 
12ffaf3
d1f4c77
246efdb
 
 
 
 
 
 
 
d1f4c77
 
 
 
 
 
d6fedfa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
<script lang="ts">
  import "rvfc-polyfill";

  import { onDestroy, onMount } from "svelte";
  import {
    mediaStreamStatus,
    MediaStreamStatusEnum,
    onFrameChangeStore,
    mediaStream,
    mediaDevices,
  } from "$lib/mediaStream";
  import MediaListSwitcher from "./MediaListSwitcher.svelte";
  import Button from "$lib/components/Button.svelte";
  import Expand from "$lib/icons/expand.svelte";

  let { width = 512, height = 512 }: { width: number; height: number } =
    $props();

  const size = { width, height };

  let videoEl: HTMLVideoElement | undefined = $state();
  let canvasEl: HTMLCanvasElement | undefined = $state();
  let ctx: CanvasRenderingContext2D | undefined = $state();
  let videoFrameCallbackId: number | undefined = $state();

  // ajust the throttle time to your needs
  const THROTTLE = 1000 / 120;
  let selectedDevice: string = $state("");
  let videoIsReady = $state(false);

  onMount(() => {
    if (canvasEl) {
      ctx = canvasEl.getContext("2d") as CanvasRenderingContext2D;
      canvasEl.width = size.width;
      canvasEl.height = size.height;
    }
  });
  $effect(() => {
    console.log(selectedDevice);
  });

  onDestroy(() => {
    if (videoFrameCallbackId && videoEl) {
      videoEl.cancelVideoFrameCallback(videoFrameCallbackId);
    }
  });

  $effect(() => {
    if (videoEl && $mediaStream) {
      videoEl.srcObject = $mediaStream;
    }
  });

  let lastMillis = 0;
  async function onFrameChange(now: DOMHighResTimeStamp) {
    if (!videoEl || !ctx || !canvasEl) return;

    if (now - lastMillis < THROTTLE) {
      videoFrameCallbackId = videoEl.requestVideoFrameCallback(onFrameChange);
      return;
    }

    lastMillis = now;
    const videoWidth = videoEl.videoWidth;
    const videoHeight = videoEl.videoHeight;
    // scale down video to fit canvas,  size.width, size.height
    const scale = Math.min(size.width / videoWidth, size.height / videoHeight);
    const width0 = videoWidth * scale;
    const height0 = videoHeight * scale;
    const x0 = (size.width - width0) / 2;
    const y0 = (size.height - height0) / 2;
    ctx.clearRect(0, 0, size.width, size.height);
    ctx.drawImage(videoEl, x0, y0, width0, height0);

    const blob = await new Promise<Blob>((resolve) => {
      canvasEl?.toBlob(
        (blob) => {
          resolve(blob as Blob);
        },
        "image/jpeg",
        1,
      );
    });
    onFrameChangeStore.set({ blob });
    videoFrameCallbackId = videoEl.requestVideoFrameCallback(onFrameChange);
  }

  $effect(() => {
    if (
      $mediaStreamStatus == MediaStreamStatusEnum.CONNECTED &&
      videoIsReady &&
      videoEl
    ) {
      videoFrameCallbackId = videoEl.requestVideoFrameCallback(onFrameChange);
    }
  });

  function toggleFullscreen() {
    if (videoIsReady && videoEl) {
      if (document.fullscreenElement) {
        document.exitFullscreen();
      } else {
        videoEl.requestFullscreen();
      }
    }
  }
</script>

<div
  class="relative mx-auto max-w-lg overflow-hidden rounded-lg border border-slate-300"
>
  <div
    class="relative z-10 flex aspect-square w-full items-center justify-center object-cover"
  >
    {#if $mediaDevices.length > 0}
      <div class="absolute bottom-0 right-0 z-10 flex bg-slate-400/40">
        <MediaListSwitcher />
        <Button
          onclick={toggleFullscreen}
          title="Expand Fullscreen"
          class="ml-auto rounded-lg p-1 text-sm text-white opacity-50 shadow-lg"
        >
          <Expand />
        </Button>
      </div>
    {/if}
    <video
      class="pointer-events-none aspect-square w-full justify-center object-contain"
      bind:this={videoEl}
      onloadeddata={() => {
        videoIsReady = true;
      }}
      playsinline
      autoplay
      muted
      loop
    ></video>
    <canvas
      bind:this={canvasEl}
      class="absolute left-0 top-0 aspect-square w-full object-cover"
    ></canvas>
  </div>
  <div
    class="absolute left-0 top-0 flex aspect-square w-full items-center justify-center"
  >
    <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 448 448"
      class="w-40 p-5 opacity-20"
    >
      <path
        fill="currentColor"
        d="M224 256a128 128 0 1 0 0-256 128 128 0 1 0 0 256zm-45.7 48A178.3 178.3 0 0 0 0 482.3 29.7 29.7 0 0 0 29.7 512h388.6a29.7 29.7 0 0 0 29.7-29.7c0-98.5-79.8-178.3-178.3-178.3h-91.4z"
      />
    </svg>
  </div>
</div>