Skip to content

Pre-Call Component

  • File: src/pages/private/events/[eventId]/_cmps/precall.svelte

Overview

The Pre-Call component manages the pre-meeting setup interface for video conferencing. It handles device selection, audio/video preview, and participant waiting room functionality.

Core Functions

1. Media Device Management

svelte
// Get media stream from devices
export async function getStream(audioSource?: string, videoSource?: string) {
  try {
    const constraints = {
      video: videoSource ? { exact: videoSource } : true,
      audio: audioSource ? { exact: audioSource } : true,
    };
    const stream = await openMediaDevices(constraints);
    return { stream, success: true };
  } catch (error) {
    return { error, success: false };
  }
}

// Enumerate available devices
export async function getDevices() {
  try {
    const devices = await navigator.mediaDevices.enumerateDevices();
    return {
      audioInput: devices.filter(x => x.kind === "audioinput"),
      audioOutput: devices.filter(x => x.kind === "audiooutput"),
      videoInput: devices.filter(x => x.kind === "videoinput"),
      success: true
    };
  } catch (error) {
    return { error, success: false };
  }
}

2. Audio Output Management

svelte
export async function attachSinkId(
  element: HTMLAudioElement,
  sinkId: string
) {
  try {
    if (typeof element.sinkId !== "undefined") {
      await element.setSinkId(sinkId);
      return { success: true, message: "Done" };
    }
    return {
      success: false,
      message: "Browser does not support output device selection"
    };
  } catch (error) {
    return { success: false, message: error.message || error, error };
  }
}

State Management

1. Device State

svelte
let cameras = [];
let selectedCamera;
let mics = [];
let selectedMic;
let speakers = [];
let selectedSpeaker;
let isSupported;
let error;

2. Stream State

svelte
let localVideoStream: MediaStream;
let localAudioStream: MediaStream;
let cameraOn: boolean;
let micOn: boolean;
let noAudio = true;
let noVideo = true;
let noSpeaker = true;

3. Participant State

svelte
export let event = {
  title: "Event Title",
  startDate: new Date(),
  endDate: new Date(),
  timeText: null,
};
export let precallUsers = [];
export let incallUsers = [];
export let maxUserCount = 4;

Event Handlers

1. Device Control

svelte
async function toggleMic() {
  micOn = !micOn;
  if (localAudioStream && micOn) return;
  if (!localAudioStream && !micOn) return;

  if (micOn) {
    await getAudioPreview();
    if (video) video.muted = false;
  } else {
    localAudioStream.getTracks().forEach(x => {
      x.enabled = false;
      x.stop();
    });
    localAudioStream = null;
    if (video) video.muted = true;
  }
}

function toggleVideo() {
  if (!hasVideo || !localVideoStream) return;
  cameraOn = !cameraOn;
  localVideoStream.getVideoTracks()
    .forEach(track => track.enabled = cameraOn);
}

2. Stream Management

svelte
async function start() {
  if (localVideoStream) {
    localVideoStream.getTracks().forEach(x => x.stop());
  }

  const videoSource = selectedCamera;
  const constraints = {
    audio: true,
    video: { deviceId: videoSource ? { exact: videoSource } : undefined },
  };

  // Handle audio and video streams separately
  let audioStream: MediaStream;
  let videoStream: MediaStream;

  try {
    // Get audio stream
    audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
    noAudio = false;
  } catch (e) {
    handleAudioError(e);
  }

  try {
    // Get video stream
    videoStream = await navigator.mediaDevices.getUserMedia({
      video: { deviceId: videoSource ? { exact: videoSource } : undefined },
    });
    noVideo = false;
  } catch (e) {
    handleVideoError(e);
  }

  // Combine streams
  const outputTracks = [
    ...(audioStream?.getTracks() || []),
    ...(videoStream?.getTracks() || [])
  ];
  const stream = new MediaStream(outputTracks);
  await onStream(stream);
}

UI Components

svelte
<div class="bg-[#2d4e8c] flex-grow grid place-items-center w-screen h-screen">
  <div class="max-w-xl">
    <!-- Video Preview -->
    <div class="flex" class:justify-around={noVideo}>
      <!-- Video Element -->
      <div class="flex-grow bg-black mr-2 relative border-gray-400">
        <video autoplay bind:this={video} muted />
        <!-- Controls -->
        <div class="bg-gray-400 absolute left-0 bottom-0 w-full bg-opacity-50">
          <!-- Mic and Camera Controls -->
        </div>
      </div>

      <!-- Device Selection -->
      <div class="text-gray-400 text-sm">
        <!-- Camera Selection -->
        <!-- Microphone Selection -->
        <!-- Speaker Selection -->
      </div>
    </div>

    <!-- Event Info -->
    <div class="text-white text-center mt-8">
      <div>{event.title}</div>
      <div class="text-sm">{event.timeText}</div>
    </div>

    <!-- Participant List -->
    {#each callUsers as { message, users, countLeft }}
      <!-- Participant Avatars -->
    {/each}

    <!-- Action Buttons -->
    <div class="px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
      <!-- Cancel and Join Buttons -->
    </div>
  </div>
</div>

Lifecycle Management

svelte
onMount(async () => {
  // Load event data
  const ret = await getEvent(eventId);
  if (!ret.success) {
    showError(ret.message);
    return;
  }
  event = { ...ret.data };

  // Initialize media devices
  try {
    await start();
    await getAudioPreview();
    await refreshDevices();
    if (!error) autoRefreshDevices();
  } catch (e) {
    console.log(e);
    return;
  }
});

onDestroy(() => {
  if (!error) {
    navigator.mediaDevices.removeEventListener("devicechange", refreshDevices);
  }
});

Released under the MIT License.