<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Tasha</title>
    <description>The latest articles on Forem by Tasha (@mindchatter).</description>
    <link>https://forem.com/mindchatter</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F362553%2F37c74277-36d5-4c93-8e03-da51601c4d29.jpeg</url>
      <title>Forem: Tasha</title>
      <link>https://forem.com/mindchatter</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/mindchatter"/>
    <language>en</language>
    <item>
      <title>AI-assisted removal of filler words from video recordings</title>
      <dc:creator>Tasha</dc:creator>
      <pubDate>Wed, 01 Nov 2023 17:37:11 +0000</pubDate>
      <link>https://forem.com/trydaily/ai-assisted-removal-of-filler-words-from-video-recordings-2m4c</link>
      <guid>https://forem.com/trydaily/ai-assisted-removal-of-filler-words-from-video-recordings-2m4c</guid>
      <description>&lt;p&gt;&lt;em&gt;By &lt;a href="https://www.daily.co/blog/author/liza/" rel="noopener noreferrer"&gt;Liza Shulyayeva&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;With the ongoing evolution of LLM-powered workflows, the limits of what AI can do with real-time and recorded video are rapidly expanding. AI can now contribute to post-processing through contextualized parsing of video, audio, and transcription output. Some results are production-worthy while others are exploratory, benefiting from an additional human touch. In the end, it’s human intuition and ingenuity that enables LLM-powered applications to shine.&lt;/p&gt;

&lt;p&gt;In this post, I’ll explore one use case and implementation for AI-assisted post-processing that can make video presenters’ lives a little easier. We’ll go through &lt;a href="https://github.com/daily-demos/filler-word-removal" rel="noopener noreferrer"&gt;a small demo&lt;/a&gt; which lets you remove disfluencies, also known as &lt;em&gt;filler words&lt;/em&gt;, from any MP4 file. These can include words like “um”, “uh”, and similar. I will cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How the demo works from an end-user perspective&lt;/li&gt;
&lt;li&gt;A before and after example video&lt;/li&gt;
&lt;li&gt;The demo’s tech stack and architecture&lt;/li&gt;
&lt;li&gt;Running the demo locally&lt;/li&gt;
&lt;li&gt;What’s happening under the hood as filler words are being removed&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How the demo works
&lt;/h2&gt;

&lt;p&gt;When the user opens the filler removal web application, they’re faced with a page that lets them either upload their own MP4 file or fetch the cloud recordings from their Daily domain:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7z07ijdu2q21q8jcgdvx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7z07ijdu2q21q8jcgdvx.png" alt="A webpage with a video upload form and a Daily recording-fetch button "&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The front-end landing page&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For this demo, I’ve stuck with the server framework's (Quart) default request size limit of 16MB (but feel free to &lt;a href="https://pgjones.gitlab.io/quart/discussion/dos_mitigations.html#large-request-body" rel="noopener noreferrer"&gt;configure this&lt;/a&gt; in your local installation). Once the user uploads an MP4 file, the back-end component of the demo starts processing the file to remove filler words. At this point, the client shows the status of the project in the app:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjr0p34on901kylqla144.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjr0p34on901kylqla144.png" alt="The web app page with a single filler word removal project row in the Upload section"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A disfluency removal project being processed&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If the user clicks the “Fetch Daily recordings” button, all the Daily recordings on the configured Daily domain are displayed:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1gxywh1cxsajn4v6rgdy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1gxywh1cxsajn4v6rgdy.png" alt="A table of Daily cloud recordings fetched for the configured domain"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A list of Daily cloud recordings for the configured domain&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The user can then click “Process” next to any of the recordings to begin removing filler words from that file. The status of the project will be displayed:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzr7y3fq5jw0aqclb0ck2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzr7y3fq5jw0aqclb0ck2.png" alt="A list of Daily recordings, one of which is marked as "&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;One recording being processed&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Once a processing project is complete, a “Download Output” link is shown to the user, where they can retrieve their new, de-filler-ized video:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6916c8qnjj7hn001voih.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6916c8qnjj7hn001voih.png" alt="A project row with a "&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A successfully-processed video with an output download link&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here’s an example of a before and after result:&lt;/p&gt;

&lt;p&gt;Before&lt;br&gt;
&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/3t3gg7uRH7s"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;After&lt;br&gt;
&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/6-IQFpwYA5A"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;As you can see, the output is promising but not perfect–I’ll leave some final impressions of both Deepgram and Whisper results at the end of this post.&lt;/p&gt;

&lt;p&gt;Now that we’re familiar with the user flow, let’s look into the demo tech stack and architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech stack and architecture
&lt;/h2&gt;

&lt;p&gt;This demo is built using the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JavaScript for the client-side.&lt;/li&gt;
&lt;li&gt;Python for the server component.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://quart.palletsprojects.com/en/latest/%29" rel="noopener noreferrer"&gt;Quart&lt;/a&gt; for the processing server (similar to Flask, but designed to play nice with asynchronous programming.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pypi.org/project/moviepy/" rel="noopener noreferrer"&gt;moviepy&lt;/a&gt; to extract audio from, split, and then re-concatenate our original video files.&lt;/li&gt;
&lt;li&gt;Deepgram and Whisper as two LLM transcription and filler word detection options:&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developers.deepgram.com/docs/python-sdk" rel="noopener noreferrer"&gt;Deepgram’s Python SDK&lt;/a&gt; to implement Deepgram transcription with their &lt;em&gt;Nova&lt;/em&gt;-tier model, which lets us get filler words in the transcription output. This transcriber relies on a Deepgram API key.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/linto-ai/whisper-timestamped" rel="noopener noreferrer"&gt;&lt;code&gt;whisper-timestamped&lt;/code&gt;&lt;/a&gt;, which is a layer on top of the &lt;a href="https://openai.com/research/whisper" rel="noopener noreferrer"&gt;Whisper&lt;/a&gt; set of models enabling us to get accurate word timestamps and include filler words in transcription output. This transcriber downloads the selected Whisper model to the machine running the demo and no third-party API keys are required.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.daily.co/reference/rest-api" rel="noopener noreferrer"&gt;Daily’s REST API&lt;/a&gt; to retrieve Daily recordings and recording access links. If a Daily API key is not specified, the demo can still be used by uploading your own MP4 file manually.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the server-side, the key concepts are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Projects&lt;/strong&gt;. The &lt;code&gt;Project&lt;/code&gt; class is defined in ￼&lt;a href="https://github.com/daily-demos/filler-word-removal/blob/v1.0/server/project.py" rel="noopener noreferrer"&gt;&lt;code&gt;server/project.py&lt;/code&gt;&lt;/a&gt;. Each instance of this class represents a single video for which filler words are being removed. When a project is instantiated, it takes an optional &lt;code&gt;transcriber&lt;/code&gt; parameter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transcribers&lt;/strong&gt;. Transcribers are the transcription implementations that power filler word detection. As mentioned before, I’ve implemented &lt;a href="https://github.com/daily-demos/filler-word-removal/blob/v1.0/server/transcription/dg.py" rel="noopener noreferrer"&gt;Deepgram&lt;/a&gt; and &lt;a href="https://github.com/daily-demos/filler-word-removal/blob/v1.0/server/transcription/whisper.py" rel="noopener noreferrer"&gt;Whisper&lt;/a&gt; transcribers for this demo. You can also add your own by placing any transcriber you’d like into a new class within the &lt;code&gt;server/transcription/&lt;/code&gt; directory (I’ll talk a bit more about that later).
The steps an input video file goes through are as follows:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk86ybomboxapwj8a12s2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk86ybomboxapwj8a12s2.png" alt="A diagram showing the source MP4 going through 5 steps: Stripping audio into a separate file, transcribing audio, deducing filler word split times, cutting up the original video, reconstituting the video clips"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the demo locally
&lt;/h2&gt;

&lt;p&gt;To run the demo locally, be sure to have &lt;a href="https://www.python.org/" rel="noopener noreferrer"&gt;Python 3.11&lt;/a&gt; and &lt;a href="https://ffmpeg.org/" rel="noopener noreferrer"&gt;FFmpeg&lt;/a&gt; installed.&lt;/p&gt;

&lt;p&gt;Then, run the following commands (replacing the &lt;code&gt;python3&lt;/code&gt; and &lt;code&gt;pip3&lt;/code&gt; commands with your own aliases to Python and pip as needed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Clone the git repository
git clone https://github.com/daily-demos/filler-word-removal.git
cd filler-word-removal
git checkout v1.0

# Configure and activate a virtual environment
python3 -m venv venv
source venv/bin/activate

# Install dependencies
pip install -r requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Optionally, copy the &lt;code&gt;.env.sample&lt;/code&gt; file and assign your &lt;a href="https://console.deepgram.com/" rel="noopener noreferrer"&gt;Deepgram&lt;/a&gt; and &lt;a href="https://dashboard.daily.co/developers" rel="noopener noreferrer"&gt;Daily&lt;/a&gt; API keys. Both of these are optional, but I think Deepgram results are usually superior to Whisper out of the box and I’d really suggest you try that out.&lt;/p&gt;

&lt;p&gt;Now, run the following commands in two &lt;em&gt;separate&lt;/em&gt; terminals within your virtual environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Start the processing server
quart --app server/index.py --debug run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Serve the front-end
python -m http.server --directory client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open your web browser of choice (I suggest Chrome) to the localhost address shown in the second terminal window above. It will probably be &lt;code&gt;http://localhost:8000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now that we’ve got the app running, let’s see what’s happening under the hood.&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the hood of AI-powered video filler word removal
&lt;/h2&gt;

&lt;p&gt;I’m going to mostly focus on the &lt;em&gt;server&lt;/em&gt; side here, because that’s where all the magic happens. You can check out the &lt;a href="https://github.com/daily-demos/filler-word-removal/blob/v1.0/client" rel="noopener noreferrer"&gt;source code for the client on GitHub&lt;/a&gt; to have a look at how it uses the server components below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Server routes
&lt;/h3&gt;

&lt;p&gt;All of the processing server routes are defined in &lt;a href="https://github.com/daily-demos/filler-word-removal/blob/v1.0/server/index.py" rel="noopener noreferrer"&gt;&lt;code&gt;server/index.py&lt;/code&gt;&lt;/a&gt;. They are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POST /upload&lt;/code&gt;: Handles the manual upload of an MP4 file and begins processing the file to remove filler words.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /process_recording/&amp;lt;recording_id&amp;gt;&lt;/code&gt;: Downloads a Daily cloud recording by the provided ID and begins processing the file to remove disfluencies.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /projects/&amp;lt;project_id&amp;gt;&lt;/code&gt;: Reads the status file of the given filler-word-removal project and returns its contents. Enables the client to poll for status updates while processing is in progress.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /projects/&amp;lt;projct_id&amp;gt;/download&lt;/code&gt;: Downloads the output file for the given filler-word-removal project ID, if one exists.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /recordings&lt;/code&gt;: Retrieves a list of all Daily recordings for the configured Daily domain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s go through the manual upload flow and see how processing happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Processing an MP4 file with the &lt;code&gt;/upload&lt;/code&gt; route
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/daily-demos/filler-word-removal/blob/v1.0/server/index.py#L24" rel="noopener noreferrer"&gt;&lt;code&gt;/upload&lt;/code&gt; route&lt;/a&gt; looks as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@app.route('/upload', methods=['POST'])
async def upload_file():
    """Saves uploaded MP4 file and starts processing.
    Returns project ID"""
    files = await request.files
    file = files["file"]
    project = Project()
    file_name = f'{project.id}.mp4'
    file_path = os.path.join(get_upload_dir_path(), file_name)
    try:
        await file.save(file_path)
        if not os.path.exists(file_path):
            raise Exception("uploaded file not saved", file_path)
    except Exception as e:
        return process_error('failed to save uploaded file', e)

    return process(project, file_path, file_name)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Above, I start by retrieving the file from the request. I then create an instance of &lt;code&gt;Project()&lt;/code&gt;, which will &lt;a href="https://github.com/daily-demos/filler-word-removal/blob/v1.0/server/project.py#L51" rel="noopener noreferrer"&gt;generate a unique ID&lt;/a&gt; for itself when being instantiated as well as decide which transcriber to use. I’ll cover the &lt;code&gt;Project&lt;/code&gt; instant setup shortly.&lt;/p&gt;

&lt;p&gt;Next, I retrieve the path to which I’ll save the uploaded file based on the newly-created project ID. This directory can be configured in the application’s environment variables - check out the &lt;a href="https://github.com/daily-demos/filler-word-removal/blob/v1.0/server/config.py" rel="noopener noreferrer"&gt;&lt;code&gt;/server/config.py&lt;/code&gt; file&lt;/a&gt; for more information.&lt;/p&gt;

&lt;p&gt;Once I have the file and the path to save it to, I save the file. If something goes wrong during this step, I return an error to the client. If the file saved successfully, I begin processing. I’ll dive into the processing step shortly. First, let’s take a quick look at the &lt;code&gt;Project&lt;/code&gt; constructor I mentioned above:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;Project&lt;/code&gt; setup
&lt;/h3&gt;

&lt;p&gt;As mentioned above, the &lt;code&gt;Project&lt;/code&gt; class constructor configures a unique ID for the project. It also decides which transcriber (Deepgram or Whisper) will be used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class Project:
    """Class representing a single filler word removal project."""
    transcriber = None
    id = None

    def __init__(
            self,
            transcriber=None,
    ):
        if not transcriber:
            transcriber = Transcribers.WHISPER
            deepgram_api_key = os.getenv("DEEPGRAM_API_KEY")
            if deepgram_api_key:
                transcriber = Transcribers.DEEPGRAM
        self.transcriber = transcriber.value
        self.id = self.configure()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Above, if a &lt;code&gt;transcriber&lt;/code&gt; argument is not passed in, &lt;code&gt;Project&lt;/code&gt; will look for a &lt;code&gt;DEEPGRAM_API_KEY&lt;/code&gt; environment variable. If a Deepgram API key has been configured, Deepgram will be used as the transcriber. Otherwise, it’ll fall back to a locally-downloaded Whisper model.&lt;/p&gt;

&lt;p&gt;The project ID is a UUID generated in the &lt;code&gt;configure()&lt;/code&gt; method, which checks for conflicts with any existing projects and sets up the temporary directory for this project instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def configure(self):
    """Generates a unique ID for this project and creates its temp dir"""
    proj_id = uuid.uuid4()
    temp_dir = get_project_temp_dir_path(proj_id)
    if os.path.exists(temp_dir):
        # Directory already exists, which indicates a conflict.
        # Pick a new UUID and try again
        return self.configure()
    os.makedirs(temp_dir)
    return proj_id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that we know how a project is configured, let’s dig into processing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beginning processing
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/daily-demos/filler-word-removal/blob/v1.0/server/index.py#L65" rel="noopener noreferrer"&gt;&lt;code&gt;process()&lt;/code&gt; function&lt;/a&gt; in &lt;code&gt;server/index.py&lt;/code&gt; takes the &lt;code&gt;Project&lt;/code&gt; instance I created earlier, the path of the uploaded MP4 file, and the file name. It then processes the project in a Quart &lt;a href="https://quart.palletsprojects.com/en/latest/how_to_guides/background_tasks.html" rel="noopener noreferrer"&gt;background task&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def process(project: Project, file_path: str, file_name: str) -&amp;gt; tuple[quart.Response, int]:
    """Runs filler-word-removal processing on given file."""
    try:
        app.add_background_task(project.process, file_path)

        response = {'project_id': project.id, 'name': file_name}
        return jsonify(response), 200
    except Exception as e:
        return process_error('failed to start processing file', e)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, the client’s request does not need to wait until the whole filler-word-removal process is complete, which can take a couple of minutes. The user will know right away that processing has started and receive a project ID which they can use to poll for status updates.&lt;/p&gt;

&lt;p&gt;We’re now ready to dig into the critical part: What does &lt;code&gt;project.process()&lt;/code&gt; &lt;em&gt;do&lt;/em&gt;?&lt;/p&gt;

&lt;h2&gt;
  
  
  The processing step
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/daily-demos/filler-word-removal/blob/v1.0/server/project.py#L64" rel="noopener noreferrer"&gt;&lt;code&gt;process()&lt;/code&gt; project instance method&lt;/a&gt; is responsible for all of the filler-word-removal operations and status updates on the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def process(self, source_video_path: str):
    """Processes the source video to remove filler words"""
    self.update_status(Status.IN_PROGRESS, '')
    try:
        self.update_status(Status.IN_PROGRESS, 'Extracting audio')
        audio_file_path = self.extract_audio(source_video_path)
    except Exception as e:
        traceback.print_exc()
        print(e, file=sys.stderr)
        self.update_status(Status.FAILED, 'failed to extract audio file')
        return

    try:
        self.update_status(Status.IN_PROGRESS, 'Transcribing audio')
        result = self.transcribe(audio_file_path)
    except Exception as e:
        traceback.print_exc()
        print(e, file=sys.stderr)
        self.update_status(Status.FAILED, 'failed to transcribe audio')
        return

    try:
        self.update_status(Status.IN_PROGRESS, 'Splitting video file')
        split_times = self.get_splits(result)
    except Exception as e:
        traceback.print_exc()
        print(e, file=sys.stderr)
        self.update_status(Status.FAILED, 'failed to get split segments')
        return

    try:
        self.update_status(Status.IN_PROGRESS, 'Reconstituting video file')
        self.resplice(source_video_path, split_times)
    except Exception as e:
        traceback.print_exc()
        print(e, file=sys.stderr)
        self.update_status(Status.FAILED, 'failed to resplice video')
        return

    self.update_status(Status.SUCCEEDED, 'Output file ready for download')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aside from basic error handling and status updates, the primary steps being performed above are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://github.com/daily-demos/filler-word-removal/blob/v1.0/server/project.py#L105" rel="noopener noreferrer"&gt;&lt;code&gt;extract_audio()&lt;/code&gt;&lt;/a&gt;: Extracting the audio from the uploaded video file and saving it to a WAV file.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/daily-demos/filler-word-removal/blob/v1.0/server/project.py#L118" rel="noopener noreferrer"&gt;&lt;code&gt;transcribe()&lt;/code&gt;&lt;/a&gt;: Transcribing the audio using the configured transcriber.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/daily-demos/filler-word-removal/blob/v1.0/server/project.py#L122" rel="noopener noreferrer"&gt;&lt;code&gt;get_splits()&lt;/code&gt;&lt;/a&gt;: Getting the &lt;em&gt;split times&lt;/em&gt; we’ll use to split and reconstitute the video with filler words excluded. This also uses the configured transcriber, since the data format here may be different across different transcription models or services.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/daily-demos/filler-word-removal/blob/v1.0/server/project.py#L126" rel="noopener noreferrer"&gt;&lt;code&gt;resplice()&lt;/code&gt;&lt;/a&gt;: Cuts up and then splices the video based on the transcriber’s specified split times.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I’ve linked to each function in GitHub above. Let’s take a look at a few of them in more detail. Specifically, let’s focus on our &lt;em&gt;transcribers&lt;/em&gt;, because this is where the LLM-powered magic happens.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transcribing audio with filler words included using Deepgram
&lt;/h3&gt;

&lt;p&gt;I’ll use Deepgram as the primary example for this post, but I encourage you to also check out the &lt;a href="https://github.com/daily-demos/filler-word-removal/blob/v1.0/server/transcription/whisper.py#L18" rel="noopener noreferrer"&gt;Whisper implementation&lt;/a&gt; to see how it varies.&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://github.com/daily-demos/filler-word-removal/blob/v1.0/server/transcription/dg.py" rel="noopener noreferrer"&gt;&lt;code&gt;server/transcription/dg.py&lt;/code&gt;&lt;/a&gt; module, I start by configuring some Deepgram transcription options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DEEPGRAM_TRANSCRIPTION_OPTIONS = {
    "model": "general",
    "tier": "nova",
    "filler_words": True,
    "language": "en",
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two most important settings above are &lt;code&gt;"tier"&lt;/code&gt; and &lt;code&gt;"filler_words"&lt;/code&gt;. By default, Deepgram omits filler words from the transcription result. To enable &lt;a href="https://developers.deepgram.com/docs/filler-words" rel="noopener noreferrer"&gt;inclusion of filler words&lt;/a&gt; in the output, a Nova-tier model must be used. Currently, this is only supported with the English Nova model.&lt;/p&gt;

&lt;p&gt;Let’s take a look at the &lt;code&gt;dg&lt;/code&gt; module transcription step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def transcribe(audio_path: str):
    """Transcribes give audio file using Deepgram's Nova model"""
    deepgram_api_key = os.getenv("DEEPGRAM_API_KEY")
    if not deepgram_api_key:
        raise Exception("Deepgram API key is missing")
    if not os.path.exists(audio_path):
        raise Exception("Audio file could not be found", audio_path)
    try:
        deepgram = Deepgram(deepgram_api_key)
        with open(audio_path, 'rb') as audio_file:
            source = {'buffer': audio_file, 'mimetype': "audio/wav"}
            res = deepgram.transcription.sync_prerecorded(
                source, DEEPGRAM_TRANSCRIPTION_OPTIONS
            )
        return res
    except Exception as e:
        raise Exception("failed to transcribe with Deepgram") from e
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Above, I start by retrieving the Deepgram API key and raising an exception if it isn’t configured. I then confirm that the provided audio file actually exists and—you guessed it—raise an exception if not. Once we’re sure the basics are in place, we’re good to go with the transcription.&lt;/p&gt;

&lt;p&gt;I then instantiate Deepgram, open the audio file, transcribe it via Deepgram’s &lt;a href="https://github.com/deepgram/deepgram-python-sdk#local-files" rel="noopener noreferrer"&gt;&lt;code&gt;sync_prerecorded()&lt;/code&gt;&lt;/a&gt; SDK method, and return the result.&lt;/p&gt;

&lt;p&gt;Once the transcription is done, the result is returned back to the &lt;code&gt;Project&lt;/code&gt; instance. With Deepgram, the result will be a JSON object that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
   "metadata":{
      //… Metadata properties here, not relevant for our purposes
   },
   "results":{
      "channels":[
         {
            "alternatives":[
               {
                  "transcript":"hello",
                  "confidence":0.9951172,
                  "words":[
                     {
                        "word":"hello",
                        "start":0.79999995,
                        "end":1.3,
                        "confidence":0.796875
                     }
                  ]
               }
            ]
         }
      ]
   }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The next step is to process this output to find relevant &lt;em&gt;split points&lt;/em&gt; for our video.&lt;/p&gt;

&lt;h3&gt;
  
  
  Finding filler word split points in the transcription
&lt;/h3&gt;

&lt;p&gt;After producing a transcription with filler words included, the same transcriber is also responsible for parsing the output and compiling all the split points we’ll need to remove the disfluencies. So, let’s take a look at &lt;a href="https://github.com/daily-demos/filler-word-removal/blob/v1.0/server/transcription/dg.py#L35" rel="noopener noreferrer"&gt;how I do this&lt;/a&gt; in the &lt;code&gt;dg&lt;/code&gt; module (I’ve left some guiding comments inline):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def get_splits(transcription) -&amp;gt; timestamp.Timestamps:
"""Retrieves split points with detected filler words removed"""
filler_triggers = ["um", "uh", "eh", "mmhm", "mm-mm"]
words = get_words(result)
splits = timestamp.Timestamps()
first_split_start = 0
try:
   for text in words:
       word = text["word"]
       word_start = text["start"]
       word_end = text["end"]
       if word in filler_triggers:
           # If non-filler tail already exists, set the end time to the start of this filler word
           if splits.tail:
               splits.tail.end = word_start


               # If previous non-filler's start time is not the same as the start time of this filler,
               # add a new split.
               if splits.tail.start != word_start:
                   splits.add(word_end, -1)
           else:
               # If this is the very first word, be sure to start
               # the first split _after_ this one ends.
               first_split_start = word_end


       # If this is not a filler word and there are no other words
       # already registered, add the first split.
       elif splits.count == 0:
           splits.add(first_split_start, -1)
   splits.tail.end = words[-1]["end"]
   return splits
    except Exception as e:
        raise Exception("failed to split at filler words") from e
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Above, I retrieve all the words from Deepgram’s transcription output by parsing the transcription JSON (check out the &lt;a href="https://github.com/daily-demos/filler-word-removal/blob/v1.0/server/transcription/dg.py#L70" rel="noopener noreferrer"&gt;&lt;code&gt;get_words()&lt;/code&gt; function&lt;/a&gt; source if you’re curious about that object structure).&lt;/p&gt;

&lt;p&gt;I then iterate over each word and retrieve its &lt;code&gt;”text”&lt;/code&gt;, &lt;code&gt;”start”&lt;/code&gt;, and &lt;code&gt;”end”&lt;/code&gt; properties. If the &lt;code&gt;”text”&lt;/code&gt; indicates a filler word, I end the previous split at the beginning of the filler. I then add a new split at the end of the filler.&lt;/p&gt;

&lt;p&gt;The resulting splits could be visualized as follows:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa2kuoa72dbiafgj9o0m0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa2kuoa72dbiafgj9o0m0.png" alt="An image of the sentence "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The collection of split points is then returned back to the &lt;code&gt;Project&lt;/code&gt; class instance, where the original video gets cut and diced.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cutting and reconstituting the original video
&lt;/h2&gt;

&lt;p&gt;The remainder of the work happens entirely in the &lt;code&gt;Project&lt;/code&gt; class, because none of it is specific to the chosen transcription API. Once we get the split points as a collection of &lt;a href="https://github.com/daily-demos/filler-word-removal/blob/v1.0/server/transcription/timestamp.py#L9" rel="noopener noreferrer"&gt;&lt;code&gt;Timestamp&lt;/code&gt; nodes&lt;/a&gt;, the project knows what to do with them in the &lt;a href="https://github.com/daily-demos/filler-word-removal/blob/v1.0/server/project.py#L126" rel="noopener noreferrer"&gt;&lt;code&gt;resplice()&lt;/code&gt; function&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def resplice(self, source_video_path: str, splits: Timestamps):
    """Splits and then reconstitutes given video file at provided split points"""
    tmp = get_project_temp_dir_path(self.id)

    clips = []
    current_split = splits.head
    idx = 0

   # The rest of the function below...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Above, I start by getting the temp directory path for the project based on its ID. This is where all the individual clips will be stored.&lt;/p&gt;

&lt;p&gt;I then initialize an array of clips and define a &lt;code&gt;current_split&lt;/code&gt; variable pointing to the head node of the timestamp collection.&lt;/p&gt;

&lt;p&gt;Finally, I define a starting index for our upcoming loop. The next step is to split up the video:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def resplice(self, source_video_path: str, splits: Timestamps):
    # ...Previously-covered logic above...

    try:
        while current_split:
            start = current_split.start
            end = current_split.en
            # Overarching safeguard against 0-duration and nonsensical splits
            if start &amp;gt;= end:
                current_split = current_split.next
                continue
            clip_file_path = os.path.join(tmp, f"{str(idx)}.mp4")
            ffmpeg_extract_subclip(source_video_path, start, end,
                                   targetname=clip_file_path)
            clips.append(VideoFileClip(clip_file_path))
            current_split = current_split.next
            idx += 1
    except Exception as e:
        raise Exception('failed to split clips') from e

    # The rest of the function below...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Above, I traverse through every split timestamp we have. For each timestamp, I extract a subclip and save it to the project’s temp directory. I append the clip to the previously-defined &lt;code&gt;clips&lt;/code&gt; collection. I then move on to the next split point and do the same, until we’re at the end of the list of timestamps.&lt;/p&gt;

&lt;p&gt;Now that we’ve got all the relevant subclips extracted, it’s time to put them back together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def resplice(self, source_video_path: str, splits: Timestamps):
    # ...Previously-covered logic above...
    try:
        final_clip = concatenate_videoclips(clips)
        output_file_path = get_project_output_file_path(self.id)
        final_clip.write_videofile(
            output_file_path,
            codec='libx264',
            audio_codec='aac',
            fps=60,
        )
    except Exception as e:
        raise Exception('failed to reconcatenate clips') from e

    # Remove temp directory for this project
    shutil.rmtree(tmp)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Above, I concatenate every clip I stored while splitting them and write them to the final output path. Feel free to play around with the &lt;code&gt;codec&lt;/code&gt;, &lt;code&gt;audio_codec&lt;/code&gt;, and &lt;code&gt;fps&lt;/code&gt; parameters above.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6g8vlqn77zl7aqfcfegm.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6g8vlqn77zl7aqfcfegm.gif" alt="Humpty Dumpty on a wall"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, I remove the temp directory associated with this project to avoid clutter.&lt;/p&gt;

&lt;p&gt;And we’re done! We now have a shiny new video file with all detected filler words removed.&lt;/p&gt;

&lt;p&gt;The client can now use the routes we covered earlier to &lt;a href="https://github.com/daily-demos/filler-word-removal/blob/v1.0/client/index.js#L18" rel="noopener noreferrer"&gt;upload a new file&lt;/a&gt;, &lt;a href="https://github.com/daily-demos/filler-word-removal/blob/v1.0/client/index.js#L53" rel="noopener noreferrer"&gt;fetch Daily recordings&lt;/a&gt; and &lt;a href="https://github.com/daily-demos/filler-word-removal/blob/v1.0/client/index.js#L87" rel="noopener noreferrer"&gt;start processing them&lt;/a&gt;, and &lt;a href="https://github.com/daily-demos/filler-word-removal/blob/v1.0/client/index.js#L113" rel="noopener noreferrer"&gt;fetch the latest project status&lt;/a&gt; from the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Impressions of Deepgram and Whisper
&lt;/h3&gt;

&lt;p&gt;I found that Whisper output seemed more aggressive than Deepgram’s in cutting out parts of &lt;em&gt;valid&lt;/em&gt; words that aren’t disfluencies. I am confident that with some further tweaking and maybe selection of a different Whisper sub-model, the output could be refined.&lt;/p&gt;

&lt;p&gt;Deepgram worked better out of the box in terms of not cutting out valid words, but also seemed to skip more filler words in the process. Both models ended up letting some disfluencies through.&lt;/p&gt;

&lt;p&gt;Used out of the box, I’d suggest going with &lt;em&gt;Deepgram&lt;/em&gt; to start with. If you want more configuration or to try out models from HuggingFace, play around with Whisper instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Plugging in another transcriber
&lt;/h3&gt;

&lt;p&gt;If you want to try another transcription method, you can do so by adding a new module to &lt;code&gt;server/transcription&lt;/code&gt;. Just make sure to implement two functions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;transcribe()&lt;/code&gt;, which takes a path to an audio file.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_splits()&lt;/code&gt;, which takes the output from &lt;code&gt;transcribe()&lt;/code&gt; and returns an instance of &lt;code&gt;timestamp.Timestamps()&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With those two in place, the &lt;code&gt;Project&lt;/code&gt; class will know what to do! You can add your new transcriber to the &lt;code&gt;Transcribers&lt;/code&gt; enum and specify it when instantiating your project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Caveats for production use
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Storage&lt;/strong&gt;&lt;br&gt;
This demo utilizes the file system to store uploads, temporary clip files, and output. No space monitoring or cleanup is implemented here (aside from removing temporary directories once a project is done). To use this in a production environment, be sure to implement appropriate monitoring measures and use a robust storage solution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security&lt;/strong&gt;&lt;br&gt;
This demo contains no authentication features. Processed videos are placed into a public folder that anyone can reach, associated with a UUID. Should a malicious actor guess or brute-force a valid project UUID, they can download processed output associated with that ID. For a production use case, access to output files should be gated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Implementing powerful post-processing effects with AI has never been easier. Coupled with Daily’s comprehensive REST API, developers can easily fetch their recordings for further refinement with the help of an LLM. Disfluency removal is just one example of what’s possible. Keep an eye out for more demos and blog posts featuring video and audio recording enhancements with the help of AI workflows.&lt;/p&gt;

&lt;p&gt;If you have any questions, don’t hesitate to &lt;a href="https://www.daily.co/company/contact/support/" rel="noopener noreferrer"&gt;reach out to our support team&lt;/a&gt;. Alternatively, hop over to &lt;a href="https://community.daily.co/" rel="noopener noreferrer"&gt;our WebRTC community, peerConnection&lt;/a&gt; to chat about this demo.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>webrtc</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Manage participants' media tracks in Angular (Part 3)</title>
      <dc:creator>Tasha</dc:creator>
      <pubDate>Tue, 31 Oct 2023 17:21:09 +0000</pubDate>
      <link>https://forem.com/trydaily/manage-participants-media-tracks-in-angular-part-3-3pdf</link>
      <guid>https://forem.com/trydaily/manage-participants-media-tracks-in-angular-part-3-3pdf</guid>
      <description>&lt;p&gt;&lt;em&gt;By &lt;a href="https://www.daily.co/blog/author/jess/"&gt;Jess Mitchell&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In this series, we’re building a video call app with a fully customized UI using &lt;a href="https://angular.io/"&gt;Angular&lt;/a&gt; and &lt;a href="https://docs.daily.co/reference/daily-js"&gt;Daily’s Client SDK for JavaScript&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the first two posts in this series, we:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.daily.co/blog/build-a-daily-video-call-app-with-angular-and-typescript-part-1/"&gt;Reviewed&lt;/a&gt; the app’s core features, as well as the general code structure and the role of each component. Instructions for setting up the demo app locally are also included.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.daily.co/blog/manage-daily-video-call-state-in-angular-part-2/"&gt;Built the join flow&lt;/a&gt; for users to submit an HTML form to join a Daily room. This included keeping track of the &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts#L42"&gt;&lt;code&gt;participants&lt;/code&gt;&lt;/a&gt; list as people join or leave a call.&lt;br&gt;
In this post, we’ll focus on:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Updating the &lt;code&gt;participants&lt;/code&gt; list when participants toggle their device settings during a call.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How to render a &lt;a href="https://github.com/daily-demos/daily-angular/tree/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/video-tile"&gt;&lt;code&gt;video-tile&lt;/code&gt;&lt;/a&gt; component for each participant present in the call.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;General recommendations for improving performance when rendering multiple &lt;code&gt;video&lt;/code&gt; elements.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re looking for more information on the app’s chat component, keep an eye out for the next post in this series.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reviewing where we left off
&lt;/h2&gt;

&lt;p&gt;So far in this series, we have an &lt;a href="https://github.com/daily-demos/daily-angular/tree/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/daily-container"&gt;&lt;code&gt;app-daily-container&lt;/code&gt;&lt;/a&gt; component, which connects the &lt;a href="https://github.com/daily-demos/daily-angular/tree/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/join-form"&gt;&lt;code&gt;join-form&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/daily-demos/daily-angular/tree/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call"&gt;&lt;code&gt;app-call&lt;/code&gt;&lt;/a&gt; components. It allows the information gathered in the &lt;code&gt;join-form&lt;/code&gt; to be passed along to the &lt;code&gt;app-call&lt;/code&gt; component.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--IGOS72P_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9kjl1ucbp93v9tgl7rl5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--IGOS72P_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9kjl1ucbp93v9tgl7rl5.png" alt="Component structure in the Angular demo app" width="800" height="393"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Component structure in the Angular demo app&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Once the HTML form in &lt;code&gt;join-form&lt;/code&gt; is submitted, the &lt;code&gt;app-call&lt;/code&gt; component is shown instead, which immediately creates an instance of Daily’s &lt;a href="https://docs.daily.co/reference/daily-js/daily-iframe-class"&gt;call object&lt;/a&gt; and attaches all of the event handlers related to managing the call and participants.&lt;/p&gt;

&lt;p&gt;In terms of tracking participants as they join and leave the call, &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts"&gt;&lt;code&gt;app-call&lt;/code&gt;&lt;/a&gt; uses a class variable – the &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts#L42"&gt;&lt;code&gt;participants&lt;/code&gt; object&lt;/a&gt; – which will get updated throughout the call. The key-value pair in &lt;code&gt;participants&lt;/code&gt; is the participant’s session ID and a &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts#L102"&gt;participant object&lt;/a&gt;, which contains the participant information we’ll need to update our app UI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// call.component.ts

export type Participant = {
  videoTrack?: MediaStreamTrack | undefined;
  audioTrack?: MediaStreamTrack | undefined;
  videoReady: boolean;
  audioReady: boolean;
  userName: string;
  local: boolean;
  id: string;
};

type Participants = {

};

export class CallComponent {
  // … See source code
  participants: Participants = {};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s now look at what happens when a participant updates their state after they join a call and are already in the &lt;code&gt;participants&lt;/code&gt; list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updating &lt;code&gt;participants&lt;/code&gt; to reflect track updates
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts#L54"&gt;&lt;code&gt;app-call&lt;/code&gt;&lt;/a&gt;, we previously looked at all the Daily event listeners added to the call object instance after it’s created. For this section, we’ll focus on the &lt;a href="https://docs.daily.co/reference/daily-js/events/participant-events#track-started"&gt;&lt;code&gt;”track-started”&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://docs.daily.co/reference/daily-js/events/participant-events#track-stopped"&gt;&lt;code&gt;”track-stopped”&lt;/code&gt;&lt;/a&gt; events, which are emitted when a video or audio track becomes available or unavailable for a participant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Add event listeners for Daily events
this.callObject
  .on("track-started", this.handleTrackStartedStopped)
  .on("track-stopped", this.handleTrackStartedStopped)
  //...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that both of the events attach &lt;code&gt;this.handleTrackStartedStopped()&lt;/code&gt; as the event handler. When emitted, &lt;code&gt;this.handleTrackStartedStopped()&lt;/code&gt; will then invoke &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts#L107"&gt;&lt;code&gt;this.updateTrack()&lt;/code&gt;&lt;/a&gt; and pass information from the Daily event payload, like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;handleTrackStartedStopped = (e: DailyEventObjectTrack | undefined): void =&amp;gt; {
  console.log("track started or stopped")
  if (!e || !e.participant || !this.joined) return;
  this.updateTrack(e.participant, e.type);
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal of &lt;code&gt;this.updateTrack()&lt;/code&gt; is two-fold:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;To update a specific participant in our &lt;code&gt;participants&lt;/code&gt; object when there’s a change that actually affects the UI. In short, this happens if the device is turned on or off, or if the media track itself has changed.&lt;/li&gt;
&lt;li&gt;To only change the specific value of the key that registered an update, which means &lt;em&gt;not&lt;/em&gt; updating or reassigning the whole participant object.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--0iVwZnJW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8qw3ecwvxegsqalaqiti.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0iVwZnJW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8qw3ecwvxegsqalaqiti.gif" alt="Video call participant muting and unmuting their camera and microphone" width="600" height="359"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Muting video/audio cause track changes that impact the app UI (i.e., the state of the icons)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let’s see how &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts#L107"&gt;&lt;code&gt;this.updateTrack()&lt;/code&gt;&lt;/a&gt; determines which participant values to update:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;updateTrack(participant: DailyParticipant, newTrackType: string): void {
  const existingParticipant = this.participants[participant.session_id];
  const currentParticipantCopy = this.formatParticipantObj(participant);

  if (newTrackType === "video") {
    // If videoReady has changed, the track’s state was toggled on or off
    if (existingParticipant.videoReady !== currentParticipantCopy.videoReady) {
      existingParticipant.videoReady = currentParticipantCopy.videoReady;
    }

    // If the id has changed, a new track is available and should be used
    if (currentParticipantCopy.videoReady &amp;amp;&amp;amp; existingParticipant.videoTrack?.id !== currentParticipantCopy.videoTrack?.id) {
      existingParticipant.videoTrack = currentParticipantCopy.videoTrack;
    }
    return;
  }

  if (newTrackType === "audio") {
    // If audioReady has changed, the track’s state was toggled on or off
    if (existingParticipant.audioReady !== currentParticipantCopy.audioReady) {
      existingParticipant.audioReady = currentParticipantCopy.audioReady;
    }

    // If the id has changed, a new track is available and should be used
    if (currentParticipantCopy.audioReady &amp;amp;&amp;amp; existingParticipant.audioTrack?.id !== currentParticipantCopy.audioTrack?.id) {
      existingParticipant.audioTrack = currentParticipantCopy.audioTrack;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;this.updateTrack()&lt;/code&gt; we compare the old and new values related to audio and video tracks to see if there was a change that affects our app UI – for example, if the track ID is different, we know a new track is &lt;a href="https://www.daily.co/blog/working-with-video-call-participants-media-tracks-for-fun-and-profit/"&gt;available&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;videoReady&lt;/code&gt; and &lt;code&gt;audioReady&lt;/code&gt; values represent whether the video/audio track can be played (i.e., if the participant has the device on). As a reminder, these values are set when the participant object is &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts#L85"&gt;reformatted&lt;/a&gt; before getting added to &lt;code&gt;participants&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const PLAYABLE_STATE = "playable";
const LOADING_STATE = "loading";
//… See source code

formatParticipantObj(p: DailyParticipant): Participant {
    const { video, audio } = p.tracks;
    const vt = video?.persistentTrack;
    const at = audio?.persistentTrack;
    return {
      videoReady:
        !!(vt &amp;amp;&amp;amp; (video.state === PLAYABLE_STATE || video.state === LOADING_STATE)),
      audioReady:
        !!(at &amp;amp;&amp;amp; (audio.state === PLAYABLE_STATE || audio.state === LOADING_STATE)),
      // … See source code for full object
    };
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the &lt;code&gt;videoReady&lt;/code&gt; or &lt;code&gt;audioReady&lt;/code&gt; value has changed, then we know the participant has toggled their device on or off.&lt;/p&gt;

&lt;p&gt;The participant object is then updated as needed. We intentionally avoid reference changes as much as possible by updating the object instead of reassigning the &lt;code&gt;existingParticipant&lt;/code&gt; variable to a copy of the object. This helps to avoid unnecessary rerenders of the &lt;code&gt;video&lt;/code&gt; and &lt;code&gt;audio&lt;/code&gt; elements found in &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/video-tile/video-tile.component.html"&gt;&lt;code&gt;video-tile&lt;/code&gt; component&lt;/a&gt;, which will be a major factor in building a performant video call app.&lt;/p&gt;

&lt;p&gt;Now that we have our &lt;code&gt;participants&lt;/code&gt; list and can update it as needed, let’s focus on &lt;code&gt;video-tile&lt;/code&gt; to see how we turn &lt;code&gt;participants&lt;/code&gt; into actual video and audio elements.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;video-tile&lt;/code&gt;: rendering media tracks and device controls
&lt;/h2&gt;

&lt;p&gt;As the &lt;code&gt;participants&lt;/code&gt; variable is updated, &lt;code&gt;app-call&lt;/code&gt; renders a &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.html#L14"&gt;&lt;code&gt;video-tile&lt;/code&gt;&lt;/a&gt; component for each participant in &lt;code&gt;participants&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// call.component.html
&amp;lt;div *ngIf="!error" class="participants-container"&amp;gt;
  &amp;lt;video-tile
    *ngFor="let participant of Object.values(participants)"
    (leaveCallClick)="leaveCall()"
    (toggleVideoClick)="toggleLocalVideo()"
    (toggleAudioClick)="toggleLocalAudio()"
    [joined]="joined"
    [videoReady]="participant.videoReady"
    [audioReady]="participant.audioReady"
    [userName]="participant.userName"
    [local]="participant.local"
    [videoTrack]="participant.videoTrack"
    [audioTrack]="participant.audioTrack"&amp;gt;&amp;lt;/video-tile&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We pass several &lt;a href="https://www.daily.co/blog/manage-daily-video-call-state-in-angular-part-2/#understanding-input-and-output-props-in-angular"&gt;input and output properties&lt;/a&gt;, including every value in the participant object.&lt;/p&gt;

&lt;p&gt;One extremely important detail to remember with Angular is that components only register reference changes for props, which means we can’t just pass each &lt;code&gt;participant&lt;/code&gt; object as a single prop. This is because the &lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/Object_reference"&gt;object reference&lt;/a&gt; doesn’t change when the object values get updated in &lt;code&gt;this.updateTrack()&lt;/code&gt;. To ensure prop changes are registered, we instead pass each object value as a separate prop (e.g., &lt;code&gt;videoTrack&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Note: This also creates an interesting balance for our &lt;code&gt;video-tile&lt;/code&gt; component because – as mentioned in the last section – we want to avoid reference changes that trigger unnecessary rerenders as much as possible; however, we also need to ensure relevant state changes reliably trigger UI updates.&lt;/p&gt;

&lt;p&gt;There are three main aspects to be aware of with the &lt;code&gt;video-tile&lt;/code&gt; component. It needs to render:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;video&lt;/code&gt; and &lt;code&gt;audio&lt;/code&gt; HTML elements for a specific participant. These elements need to update whenever track changes occur for the participant.&lt;/li&gt;
&lt;li&gt;Participant information, including their name and icons to represent if their video and audio tracks are on or off. If the video is turned off, we’ll show a placeholder UI that covers the whole tile.&lt;/li&gt;
&lt;li&gt;If the participant is local (if it’s you!), we’ll show a control panel with buttons to turn the video/audio on or off, as well as a button to leave the call.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--KK2wLuLR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gc1efmsfp7o55n9pufeh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KK2wLuLR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gc1efmsfp7o55n9pufeh.png" alt="Two video tiles: The local participant with the control panel and a remote participant with their video off" width="800" height="225"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Two video tiles: The local participant with the control panel and a remote participant with their video off&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let’s start by seeing how these features relate to the props declared in the &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts"&gt;&lt;code&gt;VideoTileComponent&lt;/code&gt;&lt;/a&gt; class definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export class VideoTileComponent {
  @Input() joined: boolean;
  @Input() videoReady: boolean;
  @Input() audioReady: boolean;
  @Input() local: boolean;
  @Input() userName: string;
  @Input() videoTrack: MediaStreamTrack | undefined;
  @Input() audioTrack: MediaStreamTrack | undefined;
  videoStream: MediaStream | undefined;
  audioStream: MediaStream | undefined;

  @Output() leaveCallClick: EventEmitter&amp;lt;null&amp;gt; = new EventEmitter();
  @Output() toggleVideoClick: EventEmitter&amp;lt;null&amp;gt; = new EventEmitter();
  @Output() toggleAudioClick: EventEmitter&amp;lt;null&amp;gt; = new EventEmitter();
 // …
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As mentioned, there are several input and output properties passed through the &lt;code&gt;video-tile&lt;/code&gt; component. The input properties are the participant values passed from the &lt;a href="https://github.com/daily-demos/daily-angular/tree/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call"&gt;&lt;code&gt;app-call&lt;/code&gt; parent component&lt;/a&gt;. The output properties are the events that will be emitted back to &lt;code&gt;app-call&lt;/code&gt;. (These are all the events that will be triggered by the local participant’s control panel buttons.)&lt;/p&gt;

&lt;p&gt;You’ll also notice there are two class variables: &lt;code&gt;videoStream&lt;/code&gt; and &lt;code&gt;audioStream&lt;/code&gt;. Each instance of &lt;code&gt;video-tile&lt;/code&gt; receives the video and audio &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack"&gt;tracks&lt;/a&gt; as input props, but that’s not what we’ll use in our video and audio HTML elements. Rather, we’ll create a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaStream"&gt;&lt;code&gt;MediaStream&lt;/code&gt;&lt;/a&gt; for each and swap out the track any time it changes. (More on this &lt;a href="https://www.daily.co/blog/manage-participants-media-tracks-in-angular-part-3/#updating-media-streams-during-the-call"&gt;below&lt;/a&gt;.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating media streams on init
&lt;/h3&gt;

&lt;p&gt;When the &lt;code&gt;VideoTileComponent&lt;/code&gt; class instance is &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/video-tile/video-tile.component.ts#L31"&gt;initialized&lt;/a&gt;, we check if playable video and audio tracks exist for the participant and create media streams for them if so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export class VideoTileComponent {
  // See source code for full class definition

  ngOnInit(): void {
    if (this.videoTrack) {
      this.addVideoStream(this.videoTrack);
    }
    if (this.audioTrack) {
      this.addAudioStream(this.audioTrack);
    }
  }

  // … See source code
  addVideoStream(track: MediaStreamTrack) {
    this.videoStream = new MediaStream([track]);
  }

  addAudioStream(track: MediaStreamTrack) {
    this.audioStream = new MediaStream([track]);
  }

  // … See source code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Updating media streams during the call
&lt;/h3&gt;

&lt;p&gt;When these tracks are updated during the call, we are alerted to the change in the &lt;a href="https://angular.io/api/core/OnChanges"&gt;&lt;code&gt;ngOnChanges&lt;/code&gt; lifecycle method&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ngOnChanges(changes: SimpleChanges): void {
  // Note: Only the props that have changed are included in changes.
  // If it's not included, we need to use existing version of the prop (e.g. this.videoTrack)
  const { videoTrack, audioTrack } = changes;

  // If the video stream hasn't been created and the track can be set, create a new stream.
  if (videoTrack?.currentValue &amp;amp;&amp;amp; !this.videoStream) {
    // Use the new track and create a stream for it.
    this.addVideoStream(videoTrack.currentValue);
  }

  // If the video stream hasn't been created and the track can be set, create a new stream.
  if (audioTrack?.currentValue &amp;amp;&amp;amp; !this.audioStream) {
    // Use the new track and create a stream for it.
    this.addAudioStream(audioTrack.currentValue);
  }

  // If the video stream exists and a track change occurred, replace the track only.
  if (videoTrack?.currentValue &amp;amp;&amp;amp; this.videoStream) {
    this.updateVideoTrack(videoTrack.previousValue, videoTrack.currentValue);
  }

  // If the audio stream exists and a track change occurred, replace the track only.
  if (audioTrack?.currentValue &amp;amp;&amp;amp; this.audioStream) {
    this.updateAudioTrack(audioTrack.previousValue, audioTrack.currentValue);
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ngOnChanges&lt;/code&gt; will only include the props that have triggered a change detection, so if the prop is present we already know there was a video or audio track change. From there, we just need to know if we should create a media stream (shown above in &lt;code&gt;this.addVideoStream()&lt;/code&gt; and &lt;code&gt;this.addAudioStream())&lt;/code&gt; or swap out the track in the existing media stream.&lt;/p&gt;

&lt;p&gt;If we’re &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/video-tile/video-tile.component.ts#L83"&gt;updating an existing stream&lt;/a&gt;, we &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaStream/removeTrack"&gt;remove&lt;/a&gt; the old track and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaStream/addTrack"&gt;add&lt;/a&gt; the new. (We do not create a new media stream.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;updateVideoTrack(oldTrack: MediaStreamTrack, track: MediaStreamTrack) {
  // This should be true since it's a track change, but check just in case.
  if (oldTrack) {
    this.videoStream?.removeTrack(oldTrack);
  }
  this.videoStream?.addTrack(track);
}

updateAudioTrack(oldTrack: MediaStreamTrack, track: MediaStreamTrack) {
  // This should be true since it's a track change, but check just in case.
  if (oldTrack) {
    this.audioStream?.removeTrack(oldTrack);
  }
  this.audioStream?.addTrack(track);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By doing so, &lt;code&gt;videoStream&lt;/code&gt; and &lt;code&gt;audioStream&lt;/code&gt; will stay up-to-date with any changes to the participant’s media tracks.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;video-tile&lt;/code&gt; HTML elements
&lt;/h3&gt;

&lt;p&gt;Now that all the track state management is set up, we can render the HTML for the &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/video-tile/video-tile.component.html"&gt;&lt;code&gt;video-tile&lt;/code&gt; component&lt;/a&gt;, starting with the &lt;code&gt;video&lt;/code&gt; and audio elements and participant information:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;video
  *ngIf="videoStream"
  autoPlay
  muted
  playsInline
  [srcObject]="videoStream"&amp;gt;&amp;lt;/video&amp;gt;

&amp;lt;div class="video-placeholder" *ngIf="!videoReady"&amp;gt;
  &amp;lt;span&amp;gt;
    &amp;lt;img
      *ngIf="!videoReady &amp;amp;&amp;amp; !local"
      src="../../assets/vid_off.svg"
      alt="Camera is off" /&amp;gt;
  &amp;lt;/span&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;audio
  *ngIf="audioStream &amp;amp;&amp;amp; !local"
  autoPlay
  playsInline
  [srcObject]="audioStream"&amp;gt;
  &amp;lt;track kind="captions" /&amp;gt;
&amp;lt;/audio&amp;gt;

&amp;lt;div class="participant-info"&amp;gt;
  &amp;lt;p class="name"&amp;gt;
    {{ userName }}
  &amp;lt;/p&amp;gt;
  &amp;lt;img
    *ngIf="!audioReady &amp;amp;&amp;amp; !local"
    src="../../assets/mic_off.svg"
    alt="Mic is off" /&amp;gt;
  &amp;lt;img
    *ngIf="audioReady &amp;amp;&amp;amp; !local"
    src="../../assets/mic_on.svg"
    alt="Mic is on" /&amp;gt;
&amp;lt;/div&amp;gt;
// …
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If there’s a video stream, we render the &lt;code&gt;video&lt;/code&gt; element and pass &lt;code&gt;videoStream&lt;/code&gt; to the &lt;code&gt;srcObject&lt;/code&gt; attribute. If the video isn’t ready to play (e.g., it’s turned off) we render a placeholder display instead. If there’s an audio stream and it’s not the local participant, we render the audio element using the &lt;code&gt;audioStream&lt;/code&gt; variable as the &lt;code&gt;srcObject&lt;/code&gt;. (Note: The local participant (you) doesn’t have an &lt;code&gt;audio&lt;/code&gt; element because you don’t need to hear the playback of your own voice.) And, finally, we display the user’s name and if they’re muted.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--HX1U1ymh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1r09hwhv6iutvbalcgfq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--HX1U1ymh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1r09hwhv6iutvbalcgfq.png" alt="A remote participant’s tile with their name and an icon for their audio state in the top right corner" width="800" height="462"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A remote participant’s tile with their name and an icon for their audio state in the top right corner&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;With these elements added, we now have a functional video/audio component that responds whenever the participant toggles their media devices.&lt;/p&gt;

&lt;p&gt;Next, we need to add a control panel for the local participant to turn their media devices on and off.&lt;/p&gt;
&lt;h3&gt;
  
  
  Building a control panel to manage local devices
&lt;/h3&gt;

&lt;p&gt;The control panel elements are also defined in the &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/video-tile/video-tile.component.html#L36"&gt;&lt;code&gt;video-tile&lt;/code&gt;&lt;/a&gt; component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//…
&amp;lt;div *ngIf="local &amp;amp;&amp;amp; this.joined" id="controls"&amp;gt;

  &amp;lt;button class="media-control" (click)="toggleVideo()"&amp;gt;
    &amp;lt;img
      *ngIf="!videoReady"
      src="../../assets/vid_off.svg"
      alt="Turn video on" /&amp;gt;
    &amp;lt;img
      *ngIf="videoReady"
      src="../../assets/vid_on.svg"
      alt="Turn video off" /&amp;gt;
  &amp;lt;/button&amp;gt;

  &amp;lt;button class="media-control" (click)="toggleAudio()"&amp;gt;
    &amp;lt;img *ngIf="!audioReady" src="../../assets/mic_off.svg" alt="Turn mic on" /&amp;gt;
    &amp;lt;img *ngIf="audioReady" src="../../assets/mic_on.svg" alt="Turn mic off" /&amp;gt;
  &amp;lt;/button&amp;gt;

&amp;lt;/div&amp;gt;

&amp;lt;button *ngIf="local" id="leaveCallButton" (click)="handleLeaveCallClick()"&amp;gt;
  &amp;lt;img src="../../assets/leave_call.svg" alt="Leave call" /&amp;gt;
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are three buttons in the control panel:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One to toggle the local participant’s video.&lt;/li&gt;
&lt;li&gt;One to toggle the local participant’s audio.&lt;/li&gt;
&lt;li&gt;One to leave the call.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Zc1_jojp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wpzcxiyjkg1vzibzt22j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Zc1_jojp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wpzcxiyjkg1vzibzt22j.png" alt="Control panel buttons to toggle local video/audio or leave the call" width="800" height="146"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Control panel buttons to toggle local video/audio or leave the call&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Each button has its associated click handler attached to it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/video-tile/video-tile.component.ts#L99"&gt;&lt;code&gt;handleToggleVideoClick()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/video-tile/video-tile.component.ts#L103"&gt;&lt;code&gt;handleToggleAudioClick()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/video-tile/video-tile.component.ts#L107"&gt;&lt;code&gt;handleLeaveCallClick()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When invoked, each of these will then emit an event that &lt;code&gt;app-call&lt;/code&gt; is already listening for (the output properties mentioned before):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;toggleVideo(): void {
  this.toggleVideoClick.emit();
}

toggleAudio(): void {
  this.toggleAudioClick.emit();
}

handleLeaveCallClick(): void {
  this.leaveCallClick.emit();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s start with the device-related events (&lt;code&gt;toggleVideoClick()&lt;/code&gt; and &lt;code&gt;toggleAudioClick()&lt;/code&gt;) and see what happens when the &lt;code&gt;app-call&lt;/code&gt; component receives the event.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// in call.component.ts

  toggleLocalVideo() {
    // Event is emitted from VideoTileComponent

    // Confirm they're in the call before updating media
    if (!this.joined) return;
    // Toggle current audio state
    const videoReady = this.callObject.localVideo();
    this.callObject.setLocalVideo(!videoReady);
  }

  toggleLocalAudio() {
    // Event is emitted from VideoTileComponent

    // Confirm they're in the call before updating media
    if (!this.joined) return;
    // Toggle current audio state
    const audioReady = this.callObject.localAudio();
    this.callObject.setLocalAudio(!audioReady);
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each one will use the appropriate Daily call instance method (&lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/set-local-video"&gt;&lt;code&gt;setLocalVideo()&lt;/code&gt;&lt;/a&gt; or &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/set-local-audio"&gt;&lt;code&gt;setLocalAudio()&lt;/code&gt;&lt;/a&gt;) to toggle the device’s state. Invoking these instance methods will then cause the device state to change, which will in turn cause either the &lt;code&gt;”track-started”&lt;/code&gt; or &lt;code&gt;”track-stopped”&lt;/code&gt; event to be emitted, depending on the device’s final state.&lt;/p&gt;

&lt;p&gt;The other event emitter – &lt;code&gt;this.leaveCallClick()&lt;/code&gt; – will invoke &lt;code&gt;app-call&lt;/code&gt;’s &lt;code&gt;this.leaveCall()&lt;/code&gt; method, which will then invoke Daily’s &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/leave"&gt;&lt;code&gt;leave()&lt;/code&gt;&lt;/a&gt; instance method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// in call.component.ts

leaveCall(): void {
  this.error = "";
  if (!this.callObject) return;

  // Leave call
  this.callObject.leave();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Calling Daily’s &lt;code&gt;leave()&lt;/code&gt; method will result in the &lt;a href="https://docs.daily.co/reference/daily-js/events/participant-events#participant-left"&gt;&lt;code&gt;”participant-left”&lt;/code&gt;&lt;/a&gt; event being emitted for remote participants and &lt;a href="https://docs.daily.co/reference/daily-js/events/meeting-events#left-meeting"&gt;&lt;code&gt;”left-meeting”&lt;/code&gt;&lt;/a&gt; being emitted if it’s the local participant leaving a call.&lt;/p&gt;

&lt;p&gt;How these Daily events are handled has already been covered in this or the previous post, so we’ve come full circle!&lt;/p&gt;

&lt;p&gt;With that, we have a functional call panel that allows each participant to update their devices, as well as to leave the call to reset the app’s state.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--LPeDbbVh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tuailxa7u13h5k9mxf8s.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--LPeDbbVh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tuailxa7u13h5k9mxf8s.gif" alt="Joining a call, toggling media devices, and leaving a call" width="600" height="291"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Joining a call, toggling media devices, and leaving a call&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;In today’s post, we learned how to update tracks for call participants in an &lt;a href="https://angular.io/"&gt;Angular&lt;/a&gt; app, as well as render video tiles for them, and toggle the state of their devices.&lt;/p&gt;

&lt;p&gt;In our next post, we’ll look at how to add a chat component to the call so participants can message each other. (Spoiler: the &lt;a href="https://github.com/daily-demos/daily-angular/tree/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/chat"&gt;chat feature&lt;/a&gt; is already in the source code.)&lt;/p&gt;

&lt;p&gt;If you have any questions or thoughts about implementing your Daily-powered video app with Angular, &lt;a href="https://www.daily.co/company/contact/support/"&gt;reach out to our support team&lt;/a&gt; or head over to &lt;a href="https://community.daily.co/"&gt;our WebRTC community, peerConnection&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>tutorial</category>
      <category>webrtc</category>
      <category>programming</category>
    </item>
    <item>
      <title>Tracking connection quality with Daily's new connectivity test methods</title>
      <dc:creator>Tasha</dc:creator>
      <pubDate>Thu, 26 Oct 2023 17:18:20 +0000</pubDate>
      <link>https://forem.com/trydaily/tracking-connection-quality-with-dailys-new-connectivity-test-methods-15hg</link>
      <guid>https://forem.com/trydaily/tracking-connection-quality-with-dailys-new-connectivity-test-methods-15hg</guid>
      <description>&lt;p&gt;&lt;em&gt;By &lt;a href="https://www.daily.co/blog/author/liza/" rel="noopener noreferrer"&gt;Liza Shulyayeva&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We’ve recently released three new &lt;a href="https://docs.daily.co/reference/daily-js/daily-iframe-class" rel="noopener noreferrer"&gt;call object&lt;/a&gt; instance methods to test connection and network quality:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/test-connection-quality" rel="noopener noreferrer"&gt;&lt;code&gt;testConnectionQuality()&lt;/code&gt;&lt;/a&gt; - Assesses the quality of a WebRTC connection using a given video track&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/test-network-connectivity" rel="noopener noreferrer"&gt;&lt;code&gt;testNetworkConnectivity()&lt;/code&gt;&lt;/a&gt; - Checks whether a stable connection can be established with Daily’s TURN server&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/test-websocket-connectivity" rel="noopener noreferrer"&gt;&lt;code&gt;testWebsocketConnectivity()&lt;/code&gt;&lt;/a&gt; - Determines whether your internet connection and network conditions support traffic over WebSockets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one can also be aborted mid-test with the associated method:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/stop-test-connection-quality#main" rel="noopener noreferrer"&gt;stopTestConnectionQuality()&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/abort-test-network-connectivity#main" rel="noopener noreferrer"&gt;abortTestNetworkConnectivity()&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/abort-test-websocket-connectivity#main" rel="noopener noreferrer"&gt;abortTestWebsocketConnectivity()&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this post, I’ll go through a small code sample showing you how to use each of these methods.&lt;/p&gt;

&lt;p&gt;But first, let’s go through some common use cases for these connection test features.&lt;/p&gt;

&lt;h2&gt;
  
  
  Video call connection test use cases
&lt;/h2&gt;

&lt;p&gt;We envision three common use cases for our new test methods:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Facilitating local troubleshooting&lt;/li&gt;
&lt;li&gt;Tracking quality metrics internally&lt;/li&gt;
&lt;li&gt;Informing ideal send setting configuration in the call&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let’s cover each of these in a little more detail.&lt;/p&gt;

&lt;h3&gt;
  
  
  Facilitating local troubleshooting
&lt;/h3&gt;

&lt;p&gt;With the new connection test methods, developers can implement a pre-join UI in which the user’s connection quality and connectivity can be gauged. If shortcomings are detected, the user can be prompted with relevant suggestions to improve their experience.&lt;/p&gt;

&lt;p&gt;For example, if &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/test-network-connectivity#main" rel="noopener noreferrer"&gt;&lt;code&gt;testNetworkConnectivity()&lt;/code&gt;&lt;/a&gt; indicates a problem connecting to Daily’s TURN servers, the user can be prompted to check their firewall setup (especially if they’re on a corporate network).&lt;/p&gt;

&lt;p&gt;Likewise, if users are experiencing degraded connection quality in the call itself, showing an indicator of connection quality with &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/test-connection-quality#main" rel="noopener noreferrer"&gt;&lt;code&gt;testConnectionQuality()&lt;/code&gt;&lt;/a&gt; can help participants narrow down which of them is experiencing connection difficulties at the time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tracking quality metrics internally
&lt;/h3&gt;

&lt;p&gt;Helping users troubleshoot their local setups is great, but tracking performance metrics or flagging spikes in problematic sessions can help detect potential issues in advance. You can forward output from the test methods above to your existing metrics and telemetry pipeline and respond to any suspicious spikes in network problems accordingly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Informing ideal send setting configuration
&lt;/h3&gt;

&lt;p&gt;Daily provides reasonable media input and output configuration by default, but some highly customized implementations may benefit from more granular settings. Daily enables developers to fine-tune their &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/update-send-settings#sendsettings" rel="noopener noreferrer"&gt;send settings&lt;/a&gt; by defining their own &lt;a href="https://docs.daily.co/guides/scaling-calls/best-practices-to-scale-large-experiences#simulcast-layer-control" rel="noopener noreferrer"&gt;simulcast layers&lt;/a&gt; or picking from a range of convenient &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/update-send-settings#presets" rel="noopener noreferrer"&gt;presets&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;By polling connection quality at regular intervals during the call, your application can respond to network fluctuations by updating the simulcast preset being used via our &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/update-send-settings#sendsettings" rel="noopener noreferrer"&gt;&lt;code&gt;updateSendSettings()&lt;/code&gt;&lt;/a&gt; call instance method.&lt;/p&gt;

&lt;p&gt;💡 &lt;em&gt;You can also use our network quality events to inform send settings.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now that we have some primary use cases established, let’s take a look at some code showing the usage of these methods.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we’re building
&lt;/h2&gt;

&lt;p&gt;This code sample shows a pre-call flow in which a local participant can test their connection quality, TURN server connectivity, and WebSockets connectivity. You, the local participant, will see their own camera feed when they start the test, but will not have joined a Daily room yet. When you click 'Run Tests', the three connectivity test methods mentioned above will be invoked and the results will be shown in the app UI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhwhwaxvw5kyugvx9bvfi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhwhwaxvw5kyugvx9bvfi.png" alt="Network and connectivity tests running with the user's local video track being shown"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the sample locally
&lt;/h2&gt;

&lt;p&gt;You can find the sample &lt;a href="https://github.com/daily-demos/daily-samples-js/tree/main/samples/client-sdk/connectivity-tests" rel="noopener noreferrer"&gt;in our JavaScript samples repository&lt;/a&gt;. You do &lt;em&gt;not&lt;/em&gt; need a Daily account or a Daily room to run this locally. Perform the following steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Clone the repository: &lt;code&gt;git clone https://github.com/daily-demos/daily-samples-js.git&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Navigate to the relevant sample directory: &lt;code&gt;cd daily-samples-js/samples/client-sdk/connectivity-tests&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Check out the relevant tag: &lt;code&gt;git checkout v1.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;npm i &amp;amp;&amp;amp; npm run dev&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Navigate to the port shown in your terminal in your preferred web browser. This will likely be &lt;code&gt;localhost:8080&lt;/code&gt;
Once you have the demo app open in your browser, click the “Run Tests” button:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftcxw00q0bfcb8cofmvgb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftcxw00q0bfcb8cofmvgb.png" alt="Run Tests button"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You should now see the in-progress test labels and your local camera feed appear. (Be sure to grant camera permissions in your browser if prompted.)&lt;/p&gt;

&lt;p&gt;Now that you’ve got the code running locally, let’s walk through how I implemented usage of the new connectivity test methods. I’ve isolated all the logic relevant to this in the &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/v1.0/samples/client-sdk/connectivity-tests/src/index.js" rel="noopener noreferrer"&gt;&lt;code&gt;index.js&lt;/code&gt;&lt;/a&gt; file, so we’re going to focus on that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up the test button and Daily call object
&lt;/h2&gt;

&lt;p&gt;When the DOM loads, the first thing I do is &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/v1.0/samples/client-sdk/connectivity-tests/src/index.js#L20" rel="noopener noreferrer"&gt;set up the “Run Tests”&lt;/a&gt; button:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;window.addEventListener('DOMContentLoaded', () =&amp;gt; {
  // Set up the primary test button and provide the handler to run when it is clicked.
  setupTestBtn(() =&amp;gt; {
    const callObject = setupCallObject();
    setupLeaveBtn(() =&amp;gt; {
      leave(callObject);
    });
    startTests(callObject);
  });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the test button is clicked, three things happen:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A new Daily call object is instantiated &lt;code&gt;(setupCallObject())&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;em&gt;Leave&lt;/em&gt; button is set up &lt;code&gt;(setupLeaveBtn())&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The tests are run &lt;code&gt;(runTests())&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let’s take a closer look at the most important part: call object setup. I’ve left some guiding comments in-line, but I’ll also provide an overview of what’s happening right underneath the code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function setupCallObject() {
  // If call instance already exists, return it.
  let callObject = window.DailyIframe.getCallInstance();
  if (callObject) return callObject;

  // If call instance does not yet exist, create it.
  callObject = window.DailyIframe.createCallObject();
  const participantParentEle = document.getElementById('participants');

  // Set up relevant event handlers
  callObject
    .on('track-started', (e) =&amp;gt; {
      enableControls();
      showTestResults();
      const p = e.participant;
      if (!p.local) return;

      const newTrack = e.track;

      addParticipantEle(p, participantParentEle);
      updateMediaTrack(p.session_id, newTrack);
      if (e.type === 'video') {
        runAllTests(callObject, newTrack);
      }
    })
    .on('error', (e) =&amp;gt; {
      // If an unrecoverable error is received,
      // allow user to try to re-join the call.
      console.error('An unrecoverable error occurred: ', e);
      enableTestBtn();
    });

  return callObject;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Below are the highlights:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I check if a &lt;em&gt;call instance&lt;/em&gt; (which can be a call object or a &lt;a href="https://www.daily.co/products/prebuilt-video-call-app/" rel="noopener noreferrer"&gt;Daily Prebuilt&lt;/a&gt; call frame) already exists. If so, I return it as there’s no need to create a new one.&lt;/li&gt;
&lt;li&gt;If a call instance doesn’t already exist, I create one using Daily’s &lt;a href="https://docs.daily.co/reference/daily-js/factory-methods/create-call-object" rel="noopener noreferrer"&gt;&lt;code&gt;createCallObject()&lt;/code&gt;&lt;/a&gt; factory method.&lt;/li&gt;
&lt;li&gt;I then set up handlers for the two Daily events I’ll be handling: &lt;a href="https://docs.daily.co/reference/daily-js/events/participant-events#track-started" rel="noopener noreferrer"&gt;&lt;code&gt;”track-started"&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://docs.daily.co/reference/daily-js/events/meeting-events#error" rel="noopener noreferrer"&gt;&lt;code&gt;”error"&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Finally, I return the new call object instance to the caller.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;"track-started”&lt;/code&gt; event is where all the magic happens. Here, I add a DOM element for the participant whose track has just started, and update it with the available tracks. For now, this will only be the local participant since we’re not actually joining any Daily video call room here.&lt;/p&gt;

&lt;p&gt;Then, I check if the started track is a video track. Daily’s &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/test-connection-quality#main" rel="noopener noreferrer"&gt;connection quality test&lt;/a&gt; and &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/test-network-connectivity#main" rel="noopener noreferrer"&gt;network connectivity test&lt;/a&gt; both take a video track as a parameter. So as soon as we get a video track, we’re ready to run the tests.&lt;/p&gt;

&lt;p&gt;However, the local participant’s video won’t just start without us explicitly asking them to start their camera. Let’s take a look at where that’s done once the call object instance is created and set up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting the camera to run video call connection tests
&lt;/h2&gt;

&lt;p&gt;Now that the call object instance is all set up with all the handlers we’ll need, it’s time to actually start the tests. This is done I the &lt;code&gt;”DOMContentLoaded"&lt;/code&gt; event handler we covered above, by invoking the demo’s &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/v1.0/samples/client-sdk/connectivity-tests/src/index.js#L76" rel="noopener noreferrer"&gt;&lt;code&gt;startTests()&lt;/code&gt;&lt;/a&gt; function and passing the new call object instance to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function startTests(callObject) {
  disableTestBtn();

  // Reset results, in case this is a re-run.
  resetTestResults();

  // If local participant already exists and has a track,
  // just run the tests.
  const localParticipant = callObject.participants().local;
  const videoTrack = localParticipant?.tracks?.video?.persistentTrack;
  if (videoTrack) {
    runAllTests(callObject, videoTrack);
    return;
  }

  // If there is not yet a local participant or a video track,
  // start the camera.
  try {
    callObject.startCamera();
  } catch (e) {
    console.error('Failed to start camera', e);
    enableTestBtn();
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s what’s happening above:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The “Run Tests” button is disabled&lt;/li&gt;
&lt;li&gt;The results displayed in the DOM are reset&lt;/li&gt;
&lt;li&gt;I check if a local participant already exists and has a video track. If so, I go ahead and call &lt;code&gt;runAllTests()&lt;/code&gt; with the participant’s existing video track&lt;/li&gt;
&lt;li&gt;If a local participant does not yet exist, or doesn’t have a video track, I call Daily’s &lt;code&gt;[startCamera()](https://docs.daily.co/reference/daily-js/instance-methods/start-camera#main)&lt;/code&gt; call object instance method. This will prompt the user for permissions to start their webcam. This, in turn, will trigger that &lt;code&gt;”track-started"&lt;/code&gt; event I covered above.
With all the setup done and handlers hooked up, we’re ready to look at the &lt;code&gt;runAllTests()&lt;/code&gt; method:&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Running the video call connection tests
&lt;/h2&gt;

&lt;p&gt;I run all three of Daily’s connection tests in parallel as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function runAllTests(callObject, videoTrack) {
  Promise.all([
    testConnectionQuality(callObject, videoTrack),
    testNetworkConnectivity(callObject, videoTrack),
    testWebSocketConnectivity(callObject),
  ]).then(() =&amp;gt; {
    enableTestBtn();
  });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once all the tests are done, I re-enable the “Run Tests” button to let the local user run the tests again. Let’s take a look at each of the tests above.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;testConnectionQuality()&lt;/code&gt;: Test quality of the WebRTC connection
&lt;/h3&gt;

&lt;p&gt;This Daily connection quality test method returns a string reflecting the participant’s current connection quality. The method takes a video track and a &lt;em&gt;duration&lt;/em&gt; value (how long you want the test to run for, in seconds). For the purposes of this example, I’ve set the duration to 5 seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function testConnectionQuality(callObject, videoTrack) {
  return callObject
    .testConnectionQuality({
      videoTrack,
      duration: 5, // In seconds
    })
    .then((res) =&amp;gt; {
      const testResult = res.result;
      let resultMsg = '';
      switch (testResult) {
        case 'aborted':
          resultMsg = 'Test aborted before any data was gathered.';
          break;
        case 'failed':
          resultMsg = 'Unable to run test.';
          break;
        case 'bad':
          resultMsg =
            'Your internet connection is bad. Try a different network.';
          break;
        case 'warning':
          resultMsg = 'Video and audio might be choppy.';
          break;
        case 'good':
          resultMsg = 'Your internet connection is good.';
          break;
        default:
          resultMsg = `Unexpected connection test result: ${testResult}`;
      }
      updateConnectionTestResult(resultMsg);
    })
    .catch((e) =&amp;gt; {
      console.error('Failed to test connection quality:', e);
    });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Above, I invoke Daily’s &lt;code&gt;testConnectionQuality()&lt;/code&gt; call object instance method. Once a result is returned, I interpret it via a switch statement and update the DOM with a relevant result message. The result object actually contains more data, like packet loss and max round trip time, which I’m not using here in order to keep the sample code simple. Refer to &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/test-connection-quality#main" rel="noopener noreferrer"&gt;our &lt;code&gt;testConnectionQuality()&lt;/code&gt; documentation&lt;/a&gt; to learn more about the data being returned.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;testNetworkConnectivity()&lt;/code&gt;: Check connectivity to Daily’s TURN server
&lt;/h3&gt;

&lt;p&gt;This test method returns a string with an &lt;code&gt;”aborted”&lt;/code&gt;, &lt;code&gt;"passed"&lt;/code&gt;, or &lt;code&gt;”failed"&lt;/code&gt; value. Like the connection quality test above, I interpret the result in a switch statement and update the DOM with a user-friendly message. Since the logic is very similar to the test we looked at above I won’t paste the code here, but you can &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/v1.0/samples/client-sdk/connectivity-tests/src/index.js#L164" rel="noopener noreferrer"&gt;check it out on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;testWebsocketConnectivity()&lt;/code&gt;: Check WebSocket traffic support
&lt;/h3&gt;

&lt;p&gt;This Daily instance method returns an object with a result property of four potential string values: &lt;code&gt;”passed”&lt;/code&gt;, &lt;code&gt;”failed”&lt;/code&gt;, &lt;code&gt;”warning”&lt;/code&gt;, or &lt;code&gt;”aborted”&lt;/code&gt;. The object also contains an array of passed, failed, or aborted regions.&lt;/p&gt;

&lt;p&gt;You can check out my &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/v1.0/samples/client-sdk/connectivity-tests/src/index.js#L197" rel="noopener noreferrer"&gt;implementation of this test on GitHub&lt;/a&gt;, since its handling is once again very similar to the two tests above.&lt;/p&gt;

&lt;p&gt;Now that we’ve looked at running the tests and consuming the results, let’s take a look at the final piece: ending tests early.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aborting Daily’s connection tests
&lt;/h2&gt;

&lt;p&gt;If a user presses “Leave” while tests are running, I want to abort the remaining tests. I do that in my &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/v1.0/samples/client-sdk/connectivity-tests/src/index.js#L235" rel="noopener noreferrer"&gt;handler for the “Leave” button&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function leave(callObject) {
  callObject.setLocalVideo(false);

  // Abort any tests that may currently be running.
  callObject.abortTestNetworkConnectivity();
  callObject.abortTestWebsocketConnectivity();
  callObject.stopTestConnectionQuality();

  // Only enable test button again once call object is destroyed
  callObject.destroy().then(() =&amp;gt; {
    enableTestBtn();
  });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Above, I turn off the user’s local video. I then stop all three tests (if a test isn’t currently running, this will be a no-op) and destroy the call object. Once the call object is destroyed, I re-enable the “Run Tests” button.&lt;/p&gt;

&lt;p&gt;With that, our pre-call connection test implementation is complete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Do you have any questions about using Daily’s new connection and connectivity tests? &lt;a href="https://www.daily.co/company/contact/support/" rel="noopener noreferrer"&gt;Reach out to our support team&lt;/a&gt; or head over to &lt;a href="https://community.daily.co/" rel="noopener noreferrer"&gt;peerConnection&lt;/a&gt;, our WebRTC community.&lt;/p&gt;

</description>
      <category>webrtc</category>
      <category>api</category>
      <category>tutorial</category>
      <category>testing</category>
    </item>
    <item>
      <title>Share admin privileges with participants during a real-time video call</title>
      <dc:creator>Tasha</dc:creator>
      <pubDate>Tue, 24 Oct 2023 18:31:49 +0000</pubDate>
      <link>https://forem.com/trydaily/share-admin-privileges-with-participants-during-a-real-time-video-call-3fob</link>
      <guid>https://forem.com/trydaily/share-admin-privileges-with-participants-during-a-real-time-video-call-3fob</guid>
      <description>&lt;p&gt;&lt;em&gt;By &lt;a href="https://www.daily.co/blog/author/jess/"&gt;Jess Mitchell&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Having someone to oversee a video call meeting is important for numerous reasons, including being able to manage which participants are allowed in the call. With &lt;a href="https://docs.daily.co/reference/daily-js"&gt;Daily’s Client SDK for JavaScript&lt;/a&gt;, participants can optionally join as meeting owners, which gives them special privileges like being able to remove participants from a call.&lt;/p&gt;

&lt;p&gt;In some cases, it can be useful for meeting owners to share their administrator responsibilities with other participants, too.&lt;/p&gt;

&lt;p&gt;In today’s post, we’ll look at sample code that demonstrates how to let meeting owners promote participants to admins, as well as how to let admins or owners remove other participants.&lt;/p&gt;

&lt;h2&gt;
  
  
  Daily meeting owners vs. admins
&lt;/h2&gt;

&lt;p&gt;To start, let’s look at how meeting owners and admins differ.&lt;/p&gt;

&lt;h3&gt;
  
  
  Meeting owner privileges
&lt;/h3&gt;

&lt;p&gt;A &lt;a href="https://docs.daily.co/guides/configurations-and-settings/controlling-who-joins-a-meeting#meeting-owner-privileges"&gt;meeting owner&lt;/a&gt; is a participant with the most privileges related to managing room access.&lt;/p&gt;

&lt;p&gt;Participants can join as a meeting owner by using a meeting owner &lt;a href="https://www.daily.co/blog/comparing-domain-room-and-meeting-token-rest-api-configurations-for-daily-video-calls/"&gt;token&lt;/a&gt;: a token that has the &lt;a href="https://docs.daily.co/reference/rest-api/meeting-tokens/config#is_owner"&gt;&lt;code&gt;is_owner&lt;/code&gt;&lt;/a&gt; property set to &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Being a meeting owner allows the participant to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Share media (audio/video/screen) in &lt;a href="https://www.daily.co/blog/daily-prebuilt-broadcast-call-deep-dive/"&gt;owner-only broadcast mode&lt;/a&gt;, if these features are enabled in the Daily room.&lt;/li&gt;
&lt;li&gt;Start or stop a &lt;a href="https://www.daily.co/blog/live-stream-daily-calls-with-only-3-second-latency/"&gt;live stream&lt;/a&gt;, if live streaming is enabled in Daily the room.&lt;/li&gt;
&lt;li&gt;Start or stop call &lt;a href="https://www.daily.co/blog/add-live-transcription-to-a-daily-call-with-our-newest-api/"&gt;transcription&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Allow participants to join a private room by responding to &lt;a href="https://docs.daily.co/guides/configurations-and-settings/controlling-who-joins-a-meeting#knocking"&gt;knocking&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/update-participant"&gt;Update&lt;/a&gt; other participants, such as adding or revoking admin permissions and muting their devices.&lt;/li&gt;
&lt;li&gt;(See an example of creating a token using in a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch"&gt;fetch request&lt;/a&gt; in today’s &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/0ae06dda730c95691ceb743dd0f2db8b44b974e3/samples/daily-prebuilt/permissions-can-admin/src/daily.js#L24"&gt;sample app&lt;/a&gt;.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Admins
&lt;/h3&gt;

&lt;p&gt;Meeting admins are similar to meeting owners, with two key differences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A meeting &lt;em&gt;owner&lt;/em&gt; has to join the call with an owner meeting token. An admin can join with an admin token or be promoted to an admin mid-call.&lt;/li&gt;
&lt;li&gt;Unlike an owner, an admin can be demoted and lose admin privileges.&lt;/li&gt;
&lt;li&gt;An owner has all relevant privileges within the call, whereas an admin can be given more granular permissions. Depending on the specific &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/update-participant#permissions"&gt;permissions&lt;/a&gt; provided, admins can potentially do any of the actions listed above for meeting owners; however, they cannot remove a meeting owner from a call or remove their meeting owner privileges.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--wgYvHZqW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1f6dj8liqi2tddckx69z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--wgYvHZqW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1f6dj8liqi2tddckx69z.png" alt="Table showing difference between owners and admins" width="800" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, onto the code!&lt;/p&gt;

&lt;h2&gt;
  
  
  Today’s goals
&lt;/h2&gt;

&lt;p&gt;In this tutorial, we’ll look at sample code for how to build an admin panel to let meeting owners upgrade regular participants to admins or remove them from the call. We’ll use a &lt;a href="https://github.com/daily-demos/daily-samples-js/tree/main/samples/daily-prebuilt/permissions-can-admin"&gt;sample app&lt;/a&gt; built with &lt;a href="https://nextjs.org/"&gt;Next.js&lt;/a&gt; and &lt;a href="https://www.daily.co/products/prebuilt-video-call-app/"&gt;Daily Prebuilt&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--nu6xRE8Y--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yv2yf4k7crz6fsev0hjo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--nu6xRE8Y--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yv2yf4k7crz6fsev0hjo.png" alt="The meeting owner’s view of the demo, with buttons to remove a participant or make them an admin." width="800" height="445"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The meeting owner’s view of the demo, with buttons to remove a participant or make them an admin.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This tutorial will focus on two features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A button that converts a participant to an admin.&lt;/li&gt;
&lt;li&gt;A button that removes the participant (or admin) from the call.
We won’t cover some of the more general Daily-related code like rendering Daily Prebuilt in your app, but we’ll include some related blog posts at the end!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To test the demo app yourself, follow the instructions included in its &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/0ae06dda730c95691ceb743dd0f2db8b44b974e3/samples/daily-prebuilt/permissions-can-admin/README.md"&gt;README&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Project structure
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--uvzjuM1r--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qhbnf18qkrkgpte3yj52.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--uvzjuM1r--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qhbnf18qkrkgpte3yj52.png" alt="The  raw `Home` endraw  component’s default view, which renders the  raw `Header` endraw  and  raw `JoinForm` endraw  components." width="800" height="289"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The &lt;code&gt;Home&lt;/code&gt; component’s default view, which renders the &lt;code&gt;Header&lt;/code&gt; and &lt;code&gt;JoinForm&lt;/code&gt; components.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When the main route of our app (e.g., &lt;code&gt;localhost:3000/&lt;/code&gt;) is visited, the component in the &lt;a href="https://nextjs.org/docs/app/api-reference/file-conventions/page"&gt;&lt;code&gt;page.js&lt;/code&gt; file&lt;/a&gt; found at the top-level of the app’s &lt;code&gt;/app&lt;/code&gt; directory is rendered – in this case, the &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/main/samples/daily-prebuilt/permissions-can-admin/src/app/page.js"&gt;&lt;code&gt;Home&lt;/code&gt; component&lt;/a&gt;, which parents all other components in the app.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--KDb6CuBs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wuyxp6hu4yw21w2xpbra.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KDb6CuBs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wuyxp6hu4yw21w2xpbra.png" alt="Illustration depicting  raw `Home` endraw ,  raw `Header` endraw ,  raw `DailyContainer` endraw ,  raw `JoinForm` endraw , Admin Panel, and  raw `containerRef` endraw  components" width="800" height="564"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Component structure for this demo app.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;DailyContainer&lt;/code&gt; component is the home of our video call feature and has a number of conditionally-rendered components/elements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A form (&lt;code&gt;JoinForm&lt;/code&gt;) to join a call.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;AdminPanel&lt;/code&gt; component to upgrade regular participants to admins or remove them from the call.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;containerRef&lt;/code&gt;, a &lt;code&gt;div&lt;/code&gt; which will contain the Daily Prebuilt UI once the &lt;code&gt;JoinForm&lt;/code&gt; is submitted and the call is created.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--R1kE2-yu--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8om8i1xa8secfsxi79tb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--R1kE2-yu--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8om8i1xa8secfsxi79tb.png" alt="The admin panel is highlighted in red and the Daily Prebuilt container is highlighted in blue." width="800" height="409"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The admin panel is highlighted in red and the Daily Prebuilt container is highlighted in blue.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Creating a Daily room and joining the call
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--LHNGLHMf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6zfmqk5jzm3316wmpecq.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--LHNGLHMf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6zfmqk5jzm3316wmpecq.gif" alt="Submitting the  raw `JoinForm` endraw  form to create and join a call." width="600" height="405"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Submitting the &lt;code&gt;JoinForm&lt;/code&gt; form to create and join a call.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let’s start by quickly familiarizing ourselves with what happens after the &lt;code&gt;JoinForm&lt;/code&gt; is &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/0ae06dda730c95691ceb743dd0f2db8b44b974e3/samples/daily-prebuilt/permissions-can-admin/src/components/DailyContainer/DailyContainer.js#L119"&gt;submitted&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;JoinForm&lt;/code&gt; itself is &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/0ae06dda730c95691ceb743dd0f2db8b44b974e3/samples/daily-prebuilt/permissions-can-admin/src/components/DailyContainer/DailyContainer.js#L243"&gt;conditionally rendered&lt;/a&gt; and shown by default. Once it’s submitted, it’s destroyed and the call-related components are displayed instead.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Nv6dHtcw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j5pplvof0lx3sibedju7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Nv6dHtcw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j5pplvof0lx3sibedju7.png" alt="Default view of the  raw `JoinForm` endraw ." width="800" height="289"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Default view of the &lt;code&gt;JoinForm&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There are also two conditions to be aware of when the &lt;code&gt;JoinForm&lt;/code&gt; is rendered:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;(Default) The user is creating a new Daily room to join and will join as a meeting owner with an owner meeting token. They can then share a link to that specific room.&lt;/li&gt;
&lt;li&gt;If the participant is using a shared link with a &lt;code&gt;url&lt;/code&gt; query param (e.g., &lt;code&gt;http://localhost:3000/?url=https://domain.daily.co/[room-name]&lt;/code&gt;), a “Daily room URL” form input will be rendered to indicate which room is being joined and no new room or meeting token will be created for them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--gQUTR_uz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1n7q9japlsit4rd5l18z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gQUTR_uz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1n7q9japlsit4rd5l18z.png" alt="Form showing name and Daily room URL fields" width="800" height="327"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Joining a specific Daily room.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This means there are two types of participants on join: meeting owners and regular participants. (No one joins as an admin!)&lt;/p&gt;

&lt;p&gt;When the &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/0ae06dda730c95691ceb743dd0f2db8b44b974e3/samples/daily-prebuilt/permissions-can-admin/src/components/JoinForm/JoinForm.js#L6"&gt;&lt;code&gt;JoinForm&lt;/code&gt;&lt;/a&gt; is &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/0ae06dda730c95691ceb743dd0f2db8b44b974e3/samples/daily-prebuilt/permissions-can-admin/src/components/DailyContainer/DailyContainer.js#L168"&gt;submitted&lt;/a&gt;, a few things happen.&lt;/p&gt;

&lt;p&gt;If the participant is joining as a meeting owner:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A new room is &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/0ae06dda730c95691ceb743dd0f2db8b44b974e3/samples/daily-prebuilt/permissions-can-admin/src/components/DailyContainer/DailyContainer.js#L184"&gt;created&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;An owner &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/0ae06dda730c95691ceb743dd0f2db8b44b974e3/samples/daily-prebuilt/permissions-can-admin/src/components/DailyContainer/DailyContainer.js#L191"&gt;meeting token is created&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;An instance of a call frame (or &lt;code&gt;DailyIframe&lt;/code&gt; class) is &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/0ae06dda730c95691ceb743dd0f2db8b44b974e3/samples/daily-prebuilt/permissions-can-admin/src/components/DailyContainer/DailyContainer.js#L122"&gt;created&lt;/a&gt;. (Note: This is called &lt;code&gt;callFrame&lt;/code&gt; in the code samples below.)&lt;/li&gt;
&lt;li&gt;The call is &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/0ae06dda730c95691ceb743dd0f2db8b44b974e3/samples/daily-prebuilt/permissions-can-admin/src/components/DailyContainer/DailyContainer.js#L140"&gt;joined&lt;/a&gt; using the meeting token.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the participant is joining an existing call, the call frame is created and joined without a meeting token.&lt;/p&gt;

&lt;p&gt;Now that we know how our participants can join the video call, let’s see how the &lt;code&gt;AdminPanel&lt;/code&gt; component works.&lt;/p&gt;
&lt;h2&gt;
  
  
  Rendering the &lt;code&gt;AdminPanel&lt;/code&gt; component
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/0ae06dda730c95691ceb743dd0f2db8b44b974e3/samples/daily-prebuilt/permissions-can-admin/src/components/AdminPanel/AdminPanel.js"&gt;&lt;code&gt;AdminPanel&lt;/code&gt; component&lt;/a&gt; will render a list of all call participants. If the local participant has owner or admin privileges, each participant in the list will have buttons to remove that participant or promote them to be an admin. (Admins can’t remove owners, though!)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Havmt_d---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e4fjmc71l0p9n63u4863.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Havmt_d---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e4fjmc71l0p9n63u4863.png" alt="Owner view of a call with two participants." width="800" height="445"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Owner view of a call with two participants.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Before getting into how we remove/promote participants, let’s first look at how we keep track of them in app state. In &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/0ae06dda730c95691ceb743dd0f2db8b44b974e3/samples/daily-prebuilt/permissions-can-admin/src/components/DailyContainer/DailyContainer.js"&gt;&lt;code&gt;DailyContainer&lt;/code&gt;&lt;/a&gt;, there’s a &lt;code&gt;participants&lt;/code&gt; object saved in the component’s state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const [participants, setParticipants] = useState({});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any time someone joins the call, we &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/0ae06dda730c95691ceb743dd0f2db8b44b974e3/samples/daily-prebuilt/permissions-can-admin/src/components/DailyContainer/DailyContainer.js#L42"&gt;update&lt;/a&gt; the &lt;code&gt;participants&lt;/code&gt; object by adding the participant’s session ID as the key and the &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/participants#participant-properties"&gt;participant’s object&lt;/a&gt; as the value.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;setParticipants((p) =&amp;gt; ({
   ...p,

 }));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: When someone &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/0ae06dda730c95691ceb743dd0f2db8b44b974e3/samples/daily-prebuilt/permissions-can-admin/src/components/DailyContainer/DailyContainer.js#L15"&gt;joins as an owner&lt;/a&gt; or anyone &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/0ae06dda730c95691ceb743dd0f2db8b44b974e3/samples/daily-prebuilt/permissions-can-admin/src/components/DailyContainer/DailyContainer.js#L66"&gt;is promoted to an admin&lt;/a&gt;, their status is tracked in &lt;code&gt;DailyContainer&lt;/code&gt;’s state. These values are passed as props to &lt;code&gt;AdminPanel&lt;/code&gt;, too.&lt;/p&gt;

&lt;p&gt;If someone leaves the call, we &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/0ae06dda730c95691ceb743dd0f2db8b44b974e3/samples/daily-prebuilt/permissions-can-admin/src/components/DailyContainer/DailyContainer.js#L73"&gt;delete&lt;/a&gt; their item from the object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;setParticipants((p) =&amp;gt; {
    const currentParticipants = { ...p };
    delete currentParticipants[e.participant.session_id];
    return currentParticipants;
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using &lt;code&gt;participants&lt;/code&gt;, we can render all call participants as a list. First we render the &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/0ae06dda730c95691ceb743dd0f2db8b44b974e3/samples/daily-prebuilt/permissions-can-admin/src/components/DailyContainer/DailyContainer.js#L2690"&gt;&lt;code&gt;AdminPanel&lt;/code&gt; component&lt;/a&gt; itself, including number of props:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// DailyContainer.js
{callFrame &amp;amp;&amp;amp; (
        &amp;lt;&amp;gt;
          &amp;lt;AdminPanel
            participants={participants}
            localIsOwner={isOwner}
            localIsAdmin={isAdmin}
            makeAdmin={makeAdmin}
            removeFromCall={removeFromCall}
          /&amp;gt;
          // … See source code
        &amp;lt;/&amp;gt;
      )}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;AdminPanel&lt;/code&gt; component is rendered for all participants, but only owners and admins will see buttons to update other participants:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--gDxGR-dx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mcg696aado7tl9w5r1h0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gDxGR-dx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mcg696aado7tl9w5r1h0.png" alt="An owner’s view of a two-person call where the second person is a regular participant." width="800" height="147"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;An owner’s view of a two-person call where the second person is a regular participant.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A regular participant will instead see only the participant information:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--RwFdOVO0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lgke482f16f90df7gmrj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--RwFdOVO0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lgke482f16f90df7gmrj.png" alt="Participant list where no participant can be promoted." width="800" height="144"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A regular participant’s view of the AdminPanel.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The main functionality of AdminPanel is actually defined in its child component, &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/0ae06dda730c95691ceb743dd0f2db8b44b974e3/samples/daily-prebuilt/permissions-can-admin/src/components/AdminPanel/AdminPanel.js#L3"&gt;&lt;code&gt;ParticipantListItem&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export default function AdminPanel({
  participants,
  makeAdmin,
  removeFromCall,
  localIsOwner,
  localIsAdmin,
}) {
  return (
    &amp;lt;div className='admin-panel'&amp;gt;
      // … See source code

      &amp;lt;ul&amp;gt;
        {Object.values(participants).map((p, i) =&amp;gt; {
          const handleMakeAdmin = () =&amp;gt; makeAdmin(p.session_id);
          const handleRemoveFromCall = () =&amp;gt; removeFromCall(p.session_id);
          return (
            &amp;lt;ParticipantListItem
              count={i + 1} // for numbered list
              key={p.session_id}
              p={p}
              localIsOwner={localIsOwner}
              localIsAdmin={localIsAdmin}
              makeAdmin={handleMakeAdmin}
              removeFromCall={handleRemoveFromCall}
            /&amp;gt;
          );
        })}
      &amp;lt;/ul&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using the &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/0ae06dda730c95691ceb743dd0f2db8b44b974e3/samples/daily-prebuilt/permissions-can-admin/src/components/DailyContainer/DailyContainer.js#L18"&gt;&lt;code&gt;participants&lt;/code&gt;&lt;/a&gt; prop, a &lt;code&gt;ParticipantListItem&lt;/code&gt; component is rendered for each participant in an unordered list (&lt;code&gt;ul&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--w2CgMyAz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g6bmffmpbj5gyvsw00z1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--w2CgMyAz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g6bmffmpbj5gyvsw00z1.png" alt="A list of two  raw `ParticipantListItem` endraw  components with the second one highlighted." width="800" height="148"&gt;&lt;/a&gt;&lt;br&gt;
&lt;code&gt;ParticipantListItem&lt;/code&gt; is passed most of the props &lt;code&gt;AdminPanel&lt;/code&gt; received and then actually uses them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const ParticipantListItem = ({
  p,
  makeAdmin,
  removeFromCall,
  localIsOwner,
  localIsAdmin,
  count,
}) =&amp;gt; (
  &amp;lt;li&amp;gt;
    &amp;lt;span&amp;gt;
      {`${count}. `}
      {p.permissions.canAdmin &amp;amp;&amp;amp; &amp;lt;b&amp;gt;{p.owner ? 'Owner | ' : 'Admin | '}&amp;lt;/b&amp;gt;}
      &amp;lt;b&amp;gt;{p.local &amp;amp;&amp;amp; '(You) '}&amp;lt;/b&amp;gt;
      {p.user_name}: {p.session_id}
    &amp;lt;/span&amp;gt;{' '}
    {!p.local &amp;amp;&amp;amp; !p.owner &amp;amp;&amp;amp; (localIsAdmin || localIsOwner) &amp;amp;&amp;amp; (
      &amp;lt;span className='buttons'&amp;gt;
        {(!p.permissions.canAdmin || localIsOwner) &amp;amp;&amp;amp; (
          &amp;lt;button className='red-button-secondary' onClick={removeFromCall}&amp;gt;
            Remove from call
          &amp;lt;/button&amp;gt;
        )}
        {!p.permissions.canAdmin &amp;amp;&amp;amp; (
          &amp;lt;button onClick={makeAdmin}&amp;gt;Make admin&amp;lt;/button&amp;gt;
        )}
      &amp;lt;/span&amp;gt;
    )}
  &amp;lt;/li&amp;gt;
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add the participant’s number to their line item.&lt;/li&gt;
&lt;li&gt;Indicate if they’re an owner or admin and if it’s the local participant (you!).&lt;/li&gt;
&lt;li&gt;Render their username and session ID to confirm they’re unique participants.&lt;/li&gt;
&lt;li&gt;Conditionally render two buttons to either remove the participant or make them an admin.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next, let’s focus on the button to make a participant an admin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Converting non-admins to admins
&lt;/h2&gt;

&lt;p&gt;As we saw above, the &lt;code&gt;ParticipantListItem&lt;/code&gt; component conditionally renders a button to let owners or admins make a regular participant an admin.&lt;/p&gt;

&lt;p&gt;The “Make admin” button click handler uses the &lt;code&gt;makeAdmin&lt;/code&gt; prop passed down from &lt;code&gt;DailyContainer&lt;/code&gt; and includes the participant’s &lt;code&gt;session_id&lt;/code&gt; so we known which participant is being upgraded to an admin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// AdminPanel.js

return (
        // … See See source code for full code block
        {Object.values(participants).map((p, i) =&amp;gt; {
          const handleMakeAdmin = () =&amp;gt; makeAdmin(p.session_id);
          const handleRemoveFromCall = () =&amp;gt; removeFromCall(p.session_id);

          return (
            &amp;lt;ParticipantListItem
makeAdmin={handleMakeAdmin}
    //… See source code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On click, the button invokes the &lt;code&gt;makeAdmin()&lt;/code&gt; function declared in &lt;code&gt;DailyContainer&lt;/code&gt;, which will upgrade the participant to an admin via Daily’s &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/update-participant"&gt;&lt;code&gt;updateParticipant()&lt;/code&gt; instance method&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const makeAdmin = useCallback(
  (participantId) =&amp;gt; {
    // https://docs.daily.co/reference/daily-js/instance-methods/update-participant#permissions
    callFrame.updateParticipant(participantId, {
      updatePermissions: {
        canAdmin: true,
      },
    });
  },
  [callFrame]
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/update-participant"&gt;&lt;code&gt;updateParticipant()&lt;/code&gt;&lt;/a&gt; can be used for various participant updates and will act differently depending on the options passed in the second parameter. In this case, we want to make the participant an admin so we set the &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/update-participant#permissions"&gt;&lt;code&gt;canAdmin&lt;/code&gt;&lt;/a&gt; property to &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Using &lt;code&gt;updateParticipant()&lt;/code&gt; this way will in fact make the participant an admin, but it won’t update our UI. To do this, we need to listen for the &lt;a href="https://docs.daily.co/reference/daily-js/events/participant-events#participant-updated"&gt;&lt;code&gt;”participant-updated”&lt;/code&gt;&lt;/a&gt; Daily event, which is emitted any time a participant is updated in any way.&lt;/p&gt;

&lt;h3&gt;
  
  
  Updating the participant list UI for new admins
&lt;/h3&gt;

&lt;p&gt;To see how we update the UI for new admins, we need to backtrack for a second.&lt;/p&gt;

&lt;p&gt;When the call frame was first created, a &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/0ae06dda730c95691ceb743dd0f2db8b44b974e3/samples/daily-prebuilt/permissions-can-admin/src/components/DailyContainer/DailyContainer.js#L108C19-L108C19"&gt;series of Daily event listeners&lt;/a&gt; were &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/0ae06dda730c95691ceb743dd0f2db8b44b974e3/samples/daily-prebuilt/permissions-can-admin/src/components/DailyContainer/DailyContainer.js#L129"&gt;attached&lt;/a&gt; to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const addDailyEvents = (dailyCallFrame) =&amp;gt; {
  // https://docs.daily.co/reference/daily-js/instance-methods/on
  dailyCallFrame
    .on('joined-meeting', handleJoinedMeeting)
    .on('participant-joined', handleParticipantJoined)
    .on('participant-updated', handleParticipantUpdate)
    // … See source code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/0ae06dda730c95691ceb743dd0f2db8b44b974e3/samples/daily-prebuilt/permissions-can-admin/src/components/DailyContainer/DailyContainer.js#L48"&gt;&lt;code&gt;handleParticipantUpdate()&lt;/code&gt;&lt;/a&gt; method is attached as the handler for &lt;a href="https://docs.daily.co/reference/daily-js/events/participant-events#participant-updated"&gt;&lt;code&gt;”participant-updated”&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const handleParticipantUpdate = (e) =&amp;gt; {
  const { participant } = e;
  const id = participant.session_id;
  if (!prevParticipants.current[id]) return;
  // Only update the participants list if the permission has changed.

  if (
    prevParticipants.current[id].permissions.canAdmin !==
    participant.permissions.canAdmin
  ) {
    setParticipants((p) =&amp;gt; ({
      ...p,
      [id]: participant,
    }));
    if (participant.local) {
      setIsAdmin(participant.permissions.canAdmin);
    }
  }
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the purposes of this app, we are only looking for changes to the &lt;code&gt;participant.permissions.canAdmin&lt;/code&gt; status. If the previously saved value is different from the value included in the event’s payload for the participant, we update the participants list.&lt;/p&gt;

&lt;p&gt;Note: &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/0ae06dda730c95691ceb743dd0f2db8b44b974e3/samples/daily-prebuilt/permissions-can-admin/src/components/DailyContainer/DailyContainer.js#L19"&gt;&lt;code&gt;prevParticipants&lt;/code&gt;&lt;/a&gt; is a &lt;a href="https://react.dev/learn/referencing-values-with-refs"&gt;ref&lt;/a&gt; that keeps a copy of the participants list so that we can compare the current list with possible updates.&lt;/p&gt;

&lt;p&gt;Once the participant change is updated in state, the &lt;code&gt;AdminPanel&lt;/code&gt; component will automatically receive the updates through the &lt;code&gt;participants&lt;/code&gt; prop and update how the panel is rendered.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--zAXOTznU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0p5k3uvnwq4v038i6lly.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zAXOTznU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0p5k3uvnwq4v038i6lly.gif" alt="A participant being made an admin and the  raw `admin panel` endraw  updating its appearance in response." width="600" height="405"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A participant being made an admin.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Ejecting a participant from the call
&lt;/h2&gt;

&lt;p&gt;Ejecting (or removing) a participant from the call works almost identically from a code perspective.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;AdminPanel&lt;/code&gt; when the &lt;code&gt;ParticipantListItems&lt;/code&gt; are rendered, the &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/0ae06dda730c95691ceb743dd0f2db8b44b974e3/samples/daily-prebuilt/permissions-can-admin/src/components/DailyContainer/DailyContainer.js#L202"&gt;&lt;code&gt;removeFromCall()&lt;/code&gt;&lt;/a&gt; prop is passed down, as well:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// AdminPanel

return (
        // … See source code
        {Object.values(participants).map((p, i) =&amp;gt; {
          const handleMakeAdmin = () =&amp;gt; makeAdmin(p.session_id);
          const handleRemoveFromCall = () =&amp;gt; removeFromCall(p.session_id);

          return (
            &amp;lt;ParticipantListItem
removeFromCall={handleRemoveFromCall}
    //… See source code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is then used by the &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/0ae06dda730c95691ceb743dd0f2db8b44b974e3/samples/daily-prebuilt/permissions-can-admin/src/components/AdminPanel/AdminPanel.js#L21"&gt;&lt;code&gt;button&lt;/code&gt; element in &lt;code&gt;ParticipantListItem&lt;/code&gt;&lt;/a&gt; to remove a participant.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{(!p.permissions.canAdmin || localIsOwner) &amp;amp;&amp;amp; (
  &amp;lt;button className='red-button-secondary' onClick={removeFromCall}&amp;gt;
    Remove from call
  &amp;lt;/button&amp;gt;
)}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual action it triggers is defined in &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/0ae06dda730c95691ceb743dd0f2db8b44b974e3/samples/daily-prebuilt/permissions-can-admin/src/components/DailyContainer/DailyContainer.js#L202"&gt;&lt;code&gt;DailyContainer&lt;/code&gt;&lt;/a&gt;, which will invoke the &lt;code&gt;updateParticipant()&lt;/code&gt; instance method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const removeFromCall = useCallback(
  (participantId) =&amp;gt; {
    // https://docs.daily.co/reference/daily-js/instance-methods/update-participant#setaudio-setvideo-and-eject
    callFrame.updateParticipant(participantId, {
      eject: true,
    });
  },
  [callFrame]
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main difference here is the properties passed to &lt;code&gt;updateParticipant()&lt;/code&gt;; this time we use the &lt;code&gt;eject&lt;/code&gt; property set to &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Once they’ve been ejected, the &lt;a href="https://docs.daily.co/reference/daily-js/events/participant-events#participant-left"&gt;&lt;code&gt;”participant-left”&lt;/code&gt;&lt;/a&gt; event is emitted, another event that we’re already listening for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const addDailyEvents = (dailyCallFrame) =&amp;gt; {
  dailyCallFrame
    .on('participant-left', handleParticipantLeft)
    // … See source code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When this event is received, the &lt;code&gt;participants&lt;/code&gt; object is updated in &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/0ae06dda730c95691ceb743dd0f2db8b44b974e3/samples/daily-prebuilt/permissions-can-admin/src/components/DailyContainer/DailyContainer.js#L71"&gt;&lt;code&gt;handleParticipantLeft()&lt;/code&gt;&lt;/a&gt; and the UI updates to reflect the change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const handleParticipantLeft = (e) =&amp;gt; {
  console.log(e.action);
  setParticipants((p) =&amp;gt; {
    const currentParticipants = { ...p };
    delete currentParticipants[e.participant.session_id];
    return currentParticipants;
  });
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---bOwcrtj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6a810oznzjwhejmkeam9.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---bOwcrtj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6a810oznzjwhejmkeam9.gif" alt="An admin’s view of being ejected from the call by a call owner." width="600" height="405"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;An admin’s view of being ejected from the call by a call owner.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And, like that, we can promote participants to admins or remove them from the call!&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In today’s post we looked at &lt;a href="https://github.com/daily-demos/daily-samples-js/blob/0ae06dda730c95691ceb743dd0f2db8b44b974e3/samples/daily-prebuilt/permissions-can-admin/src/components/DailyContainer/DailyContainer.js"&gt;sample code&lt;/a&gt; that demonstrates how to use the &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/update-participant"&gt;&lt;code&gt;updateParticipant()&lt;/code&gt;&lt;/a&gt; instance method with the &lt;code&gt;canAdmin&lt;/code&gt; property to share admin privileges with other call participants. We also looked at ejecting call participants with the &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/update-participant"&gt;&lt;code&gt;updateParticipant()&lt;/code&gt;&lt;/a&gt; instance method.&lt;/p&gt;

&lt;p&gt;To learn more about admin privileges and some of the topics we couldn’t cover in detail today, check out these related blog posts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.daily.co/blog/manage-call-permissions-with-dailys-knocking-feature/"&gt;Add a knocking feature for private rooms&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.daily.co/blog/use-next-api-routes-to-create-daily-rooms-dynamically/"&gt;Create and join a Daily room&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.daily.co/blog/comparing-domain-room-and-meeting-token-rest-api-configurations-for-daily-video-calls/"&gt;Configuring domains, rooms, and meeting tokens&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webrtc</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to properly destroy a Daily video call instance</title>
      <dc:creator>Tasha</dc:creator>
      <pubDate>Thu, 19 Oct 2023 17:31:31 +0000</pubDate>
      <link>https://forem.com/trydaily/how-to-properly-destroy-a-daily-video-call-instance-54ec</link>
      <guid>https://forem.com/trydaily/how-to-properly-destroy-a-daily-video-call-instance-54ec</guid>
      <description>&lt;p&gt;&lt;em&gt;By &lt;a href="https://www.daily.co/blog/author/liza/"&gt;Liza Shulyayeva&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In this post, I’ll show you how to appropriately &lt;em&gt;destroy&lt;/em&gt; a Daily call object or call frame in your JavaScript application. How and when to destroy the call instance is a common question we get from developers building video apps with Daily, and by the end of this post you’ll know exactly what to do.&lt;/p&gt;

&lt;p&gt;Developers often want to recreate or reconfigure the &lt;a href="https://docs.daily.co/reference/daily-js/factory-methods/create-call-object"&gt;call object&lt;/a&gt; or &lt;a href="https://docs.daily.co/reference/daily-js/factory-methods/create-frame"&gt;call frame&lt;/a&gt; at some point in their application lifecycle. As only one instance of these can be active at a time, this means the previous instance needs to be either reused or destroyed. If you’re curious to learn more, check out my previous post about &lt;a href="https://www.daily.co/blog/why-you-probably-dont-need-multiple-daily-call-objects/"&gt;why you don’t need multiple call objects&lt;/a&gt; in your Daily-powered video application.&lt;/p&gt;

&lt;p&gt;First, let’s cover some basics: What is a Daily call object/call frame?&lt;/p&gt;

&lt;h2&gt;
  
  
  A brief overview of Daily’s call object and call frame
&lt;/h2&gt;

&lt;p&gt;When it comes to JavaScript implementations, we have two primary entry points to work with Daily:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="http://daily.co/prebuilt"&gt;Daily Prebuilt&lt;/a&gt;: A full-featured video call UI that you can embed into your own web app.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.daily.co/reference/daily-js"&gt;Daily’s Client SDK for JavaScript&lt;/a&gt;: An SDK that enables you to build video into your applications with granular control and flexibility over the UI, media handling, etc.
The entry point to both of these approaches is &lt;a href="https://docs.daily.co/reference/daily-js"&gt;&lt;code&gt;daily-js&lt;/code&gt;&lt;/a&gt;, our JavaScript library. For Daily Prebuilt, you would use the &lt;a href="https://docs.daily.co/reference/daily-js/factory-methods/create-frame"&gt;&lt;code&gt;DailyIframe.createFrame()&lt;/code&gt;&lt;/a&gt; factory method to get started. For a custom implementation with the client SDK, you’d use the &lt;a href="https://docs.daily.co/reference/daily-js/factory-methods/create-call-object"&gt;&lt;code&gt;DailyIframe.createCallObject()&lt;/code&gt;&lt;/a&gt; factory method.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In reality, both of these will return an instance of the &lt;em&gt;same type&lt;/em&gt; (&lt;a href="https://github.com/daily-co/daily-js/blob/daily-js-releases/index.d.ts#L1663"&gt;&lt;code&gt;DailyCall&lt;/code&gt;&lt;/a&gt;), just configured appropriately for either Daily Prebuilt or a custom usage. For the remainder of this post, I’ll refer to both of these as the &lt;em&gt;Daily call instance&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The Daily call instance will be your main interface with Daily. You’ll use it to set listeners for relevant &lt;a href="https://docs.daily.co/reference/daily-js/events"&gt;events&lt;/a&gt;, &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/update-participants"&gt;update participants&lt;/a&gt;, &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/update-participant#actions"&gt;toggle video and audio&lt;/a&gt;, and more.&lt;/p&gt;

&lt;p&gt;Because the call object and call frame are actually two different configurations of the &lt;em&gt;same underlying type&lt;/em&gt;, the principles of destroying them are the same.&lt;/p&gt;

&lt;p&gt;Now that we’ve got a handle on what the Daily call instance is, let’s look at some basic video call lifecycle guidelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Daily call instance lifecycle
&lt;/h2&gt;

&lt;p&gt;As I mentioned above, a Daily call instance is created by using the &lt;code&gt;createCallObject()&lt;/code&gt; or &lt;code&gt;createFrame()&lt;/code&gt; factory methods of &lt;code&gt;daily-js&lt;/code&gt;. Only &lt;em&gt;one&lt;/em&gt; call instance can exist per window or iframe context in your web app. If you create more than one instance, you’ll see the following error in your console:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--PUw5N-0M--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8boutouxcdlxi5mr4i3p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PUw5N-0M--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8boutouxcdlxi5mr4i3p.png" alt="Error message referencing duplicate call object instances in a Daily video call app" width="800" height="132"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want to check whether a call instance already exists, use the &lt;a href="https://docs.daily.co/reference/daily-js/static-methods/get-call-instance"&gt;&lt;code&gt;getCallInstance()&lt;/code&gt;&lt;/a&gt; static method. If an instance is returned, you can either &lt;em&gt;reuse&lt;/em&gt; it or &lt;em&gt;destroy&lt;/em&gt; it and create a new one, depending on your needs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reusing a call instance:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let dailyCall = DailyIframe.getCallInstance();
if (!dailyCall) {
    dailyCall = DailyIframe.createCallObject();
}
// Proceed to use `dailyCall`...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Creating a new call instance:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let dailyCall = DailyIframe.getCallInstance();
if (dailyCall) {
  await dailyCall.destroy();
}
dailyCall = DailyIframe.createCallObject();

// Proceed to use `dailyCall`...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An instance that is returned from &lt;code&gt;getCallInstance()&lt;/code&gt; is guaranteed to not have been destroyed. If you already have your own reference to a Daily call instance but aren’t sure if it’s been destroyed, you can use the &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/is-destroyed"&gt;&lt;code&gt;isDestroyed()&lt;/code&gt;&lt;/a&gt; instance method to check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leaving a Daily video call
&lt;/h2&gt;

&lt;p&gt;In most cases, reusing the call instance between calls is the most intuitive flow. In this case, you would call the &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/leave"&gt;&lt;code&gt;leave()&lt;/code&gt;&lt;/a&gt; instance method and then simply call &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/join"&gt;&lt;code&gt;join()&lt;/code&gt;&lt;/a&gt; again to join (or rejoin) a video call room.&lt;/p&gt;

&lt;p&gt;Keep in mind that calling &lt;code&gt;leave()&lt;/code&gt; will keep all existing handlers in place. This means they can be reused (if relevant) without any re-initialization. But if you need to modify the handlers in any way for the next call session, be sure to &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/off"&gt;turn them off when you leave the call&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In some cases, developers want a &lt;em&gt;total reset&lt;/em&gt; of call object state at some point in their application. This is where &lt;em&gt;destruction&lt;/em&gt; of the call object comes in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Destroying a Daily video call instance
&lt;/h2&gt;

&lt;p&gt;You should destroy a Daily call instance when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want a complete reset of the call state.&lt;/li&gt;
&lt;li&gt;You’re done with using Daily and want to free up all related resources.&lt;/li&gt;
&lt;li&gt;You want to create a new call instance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To destroy a call object, you can leave the video call first or just call &lt;code&gt;destroy()&lt;/code&gt; directly (which will leave the call as well). If you opt to leave first, you can await the return of the &lt;code&gt;leave()&lt;/code&gt; method before destroying or listen for the &lt;a href="https://docs.daily.co/reference/daily-js/events/meeting-events#left-meeting"&gt;&lt;code&gt;”left-meeting”&lt;/code&gt;&lt;/a&gt; Daily event to be emitted on the call instance. The latter is slightly more idiomatic and if you already have other event handlers in place, it will result in a more consistent implementation across the board (in that this will be just another event you handle). For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;callObject.on("left-meeting", () =&amp;gt; {
    callObject.destroy();
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the call has been left, invoke the &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/destroy"&gt;&lt;code&gt;destroy()&lt;/code&gt;&lt;/a&gt; instance method. The &lt;code&gt;destroy()&lt;/code&gt; method &lt;em&gt;also&lt;/em&gt; returns a Promise. You must wait for it to resolve before recreating another call instance.&lt;/p&gt;

&lt;p&gt;The above recommendations also go for pre-join UI scenarios (such as when you have used &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/start-camera"&gt;&lt;code&gt;startCamera()&lt;/code&gt;&lt;/a&gt; for a pre-join flow, but have not yet joined a specific video call room).&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this post, we’ve covered how to best reset your video call state and destroy a Daily call instance.&lt;/p&gt;

&lt;p&gt;If you have any questions about Daily call state or the call instance lifecycle, &lt;a href="https://www.daily.co/company/contact/support/"&gt;get in touch with our support team&lt;/a&gt;, or head on over to &lt;a href="https://community.daily.co/"&gt;peerConnection, our WebRTC community&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webrtc</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>programming</category>
    </item>
    <item>
      <title>Manage Daily video call state in Angular (Part 2)</title>
      <dc:creator>Tasha</dc:creator>
      <pubDate>Tue, 17 Oct 2023 22:01:25 +0000</pubDate>
      <link>https://forem.com/trydaily/manage-daily-video-call-state-in-angular-part-2-2pfi</link>
      <guid>https://forem.com/trydaily/manage-daily-video-call-state-in-angular-part-2-2pfi</guid>
      <description>&lt;p&gt;&lt;a href="https://www.daily.co/blog/author/jess/"&gt;&lt;em&gt;By Jess Mitchell&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this series, we’re building a video call app with a fully customized UI using &lt;a href="https://angular.io/"&gt;Angular&lt;/a&gt; and &lt;a href="https://docs.daily.co/reference/daily-js"&gt;Daily’s Client SDK for JavaScript&lt;/a&gt;. In the &lt;a href="https://www.daily.co/blog/p/d0d9bc4f-06b7-4c50-b57b-3d48e983a6ce/"&gt;first post&lt;/a&gt;, we reviewed the app’s core features, as well as the general code structure and the role of each component. (Instructions for setting up the demo app locally are also included there.)&lt;/p&gt;

&lt;p&gt;In today’s post, we’ll start digging into the &lt;a href="https://github.com/daily-demos/daily-angular"&gt;Angular demo app’s source code&lt;/a&gt; available on GitHub. More specifically, we will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build the &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/join-form/join-form.component.ts"&gt;&lt;code&gt;join-form&lt;/code&gt;&lt;/a&gt; component to allow users to join a Daily room.&lt;/li&gt;
&lt;li&gt;Show how the &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/daily-container/daily-container.component.ts"&gt;&lt;code&gt;app-daily-container&lt;/code&gt;&lt;/a&gt; component works, including how it responds to the &lt;code&gt;join-form&lt;/code&gt; being submitted.&lt;/li&gt;
&lt;li&gt;See how the &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts"&gt;&lt;code&gt;app-call&lt;/code&gt;&lt;/a&gt; component sets up an instance of the Daily Client SDK’s &lt;a href="https://docs.daily.co/reference/daily-js/daily-iframe-class"&gt;call object&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Review how joining a Daily call works, and how to handle the events related to &lt;a href="https://docs.daily.co/reference/daily-js/events/meeting-events#joined-meeting"&gt;joining&lt;/a&gt; and &lt;a href="https://docs.daily.co/reference/daily-js/events/meeting-events#left-meeting"&gt;leaving&lt;/a&gt; a call.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The next post in this series will cover rendering the actual video tiles for each participant, so stay tuned for that.&lt;/p&gt;

&lt;p&gt;Now that we have a plan, let’s get started!&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;app-daily-container:&lt;/code&gt; The parent component
&lt;/h2&gt;

&lt;p&gt;As a reminder from our first post, &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/daily-container/daily-container.component.ts"&gt;&lt;code&gt;app-daily-container&lt;/code&gt;&lt;/a&gt; is the parent component for the video call feature:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--b7a0GBbZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ebz1eeutn2x99h0hfmf9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--b7a0GBbZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ebz1eeutn2x99h0hfmf9.png" alt="Component structure in the Angular demo app." width="800" height="393"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Component structure in the Angular demo app.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It includes the &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/join-form/join-form.component.ts"&gt;&lt;code&gt;join-form&lt;/code&gt;&lt;/a&gt; component, which allows users to submit an HTML form to join a Daily call. It also includes the &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts"&gt;&lt;code&gt;app-call&lt;/code&gt;&lt;/a&gt; component, which represents the video call UI shown after the form is submitted.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--EHIuUcWk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w00sdwn78txdkaauyvyu.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--EHIuUcWk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w00sdwn78txdkaauyvyu.gif" alt="Gif of call participant submitting the join form and joining the video call" width="600" height="260"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Submitting the form in &lt;code&gt;join-for&lt;/code&gt;m will cause &lt;code&gt;app-daily-container&lt;/code&gt; to render &lt;code&gt;app-call&lt;/code&gt; instead.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In this sense, &lt;code&gt;app-daily-container&lt;/code&gt; is the bridge between the two views because we need to share the form values obtained via &lt;code&gt;join-form&lt;/code&gt; with the in-call components (&lt;code&gt;app-call&lt;/code&gt; and its children).&lt;/p&gt;

&lt;p&gt;💡 &lt;em&gt;&lt;code&gt;app-chat&lt;/code&gt; is also a child of &lt;code&gt;app-daily-container&lt;/code&gt;, but we’ll cover that in a future post.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Looking at the class definition for &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/daily-container/daily-container.component.ts"&gt;&lt;code&gt;app-daily-container&lt;/code&gt;&lt;/a&gt;, we see there are two class variables (&lt;code&gt;dailyRoomUrl&lt;/code&gt; and &lt;code&gt;userName&lt;/code&gt;), as well as methods to set or reset those variables.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Component } from "@angular/core";

@Component({
  selector: "app-daily-container",
  templateUrl: "./daily-container.component.html",
  styleUrls: ["./daily-container.component.css"],
})

export class DailyContainerComponent {
  // Store callObject in this parent container.
  // Most callObject logic in CallComponent.
  userName: string;
  dailyRoomUrl: string;

  setUserName(name: string): void {
    // Event is emitted from JoinForm
    this.userName = name;
  }

  setUrl(url: string): void {
    // Event is emitted from JoinForm
    this.dailyRoomUrl = url;
  }

  callEnded(): void {
    // Truthy value will show the CallComponent; otherwise, the JoinFormComponent is shown.
    this.dailyRoomUrl = "";
    this.userName = "";
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/daily-container/daily-container.component.html"&gt;HTML&lt;/a&gt; for &lt;code&gt;app-daily-container&lt;/code&gt; shows how these variables and methods get shared with the child components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;join-form
  *ngIf="!dailyRoomUrl"
  (setUserName)="setUserName($event)"
  (setUrl)="setUrl($event)"&amp;gt;&amp;lt;/join-form&amp;gt;
&amp;lt;app-call
  *ngIf="dailyRoomUrl"
  [userName]="userName"
  [dailyRoomUrl]="dailyRoomUrl"
  (callEnded)="callEnded()"&amp;gt;&amp;lt;/app-call&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can see that the &lt;code&gt;join-form&lt;/code&gt; and &lt;code&gt;app-call&lt;/code&gt; components use the &lt;a href="https://angular.io/api/common/NgIf"&gt;&lt;code&gt;*ngIf&lt;/code&gt;&lt;/a&gt; attribute to determine when they should be rendered. They have opposite conditions, meaning they will never both be rendered at the same time. If the &lt;code&gt;dailyRoomUrl&lt;/code&gt; variable is not set yet, it renders the &lt;code&gt;join-form&lt;/code&gt; (the default view); otherwise, it renders the &lt;code&gt;app-call&lt;/code&gt; component. This is because the &lt;code&gt;dailyRoomUrl&lt;/code&gt; is set when the join form has been submitted, so if the value isn’t set, the form hasn’t been used yet.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;join-form&lt;/code&gt; has two output properties: the &lt;code&gt;setUserName()&lt;/code&gt; and &lt;code&gt;setUrl()&lt;/code&gt; event emitters, which are invoked when the join form is submitted.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;app-call&lt;/code&gt; receives the &lt;code&gt;userName&lt;/code&gt; and &lt;code&gt;dailyRoomUrl&lt;/code&gt; variables as input props, which automatically update when Angular detects a change in their values. It also has &lt;code&gt;callEnded()&lt;/code&gt; as an output prop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understanding input and output props in Angular
&lt;/h3&gt;

&lt;p&gt;In Angular, props can either be an &lt;a href="https://angular.io/api/core/Input"&gt;input property&lt;/a&gt; – a value passed down to the child – or an &lt;a href="https://angular.io/api/core/Output"&gt;output property&lt;/a&gt; – a method used to emit a value from the child to the parent component. (Data sharing is bidirectional between the parent and child when both types are used.)&lt;/p&gt;

&lt;p&gt;You can tell which type of property it is based on whether the child component declares the prop as an input or output. For example, in &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts#L38"&gt;&lt;code&gt;app-call&lt;/code&gt;&lt;/a&gt; we see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Input() userName: string;
@Output() callEnded: EventEmitter&amp;lt;null&amp;gt; = new EventEmitter();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;userName&lt;/code&gt; prop is an input property, meaning the value will automatically update in &lt;code&gt;app-call&lt;/code&gt; whenever the parent component updates it.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;callEnded&lt;/code&gt; prop is an output property. Whenever &lt;code&gt;app-call&lt;/code&gt; wants to emit an update to the parent component (&lt;code&gt;app-daily-container&lt;/code&gt;), it can call &lt;code&gt;this.callEnded(value)&lt;/code&gt; and &lt;code&gt;app-daily-container&lt;/code&gt; will receive the event and event payload.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;join-form&lt;/code&gt;: Submitting user data for the call
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--RhaMaoWU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f9dchutzfd6nus2j3zdl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--RhaMaoWU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f9dchutzfd6nus2j3zdl.png" alt="Join form UI" width="800" height="358"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The demo app’s &lt;code&gt;join-form&lt;/code&gt; component.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/join-form/join-form.component.ts"&gt;&lt;code&gt;join-form&lt;/code&gt;&lt;/a&gt; component renders an HTML form element. It includes two inputs for the user to provide the Daily room they want to join, as well as their name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;form [formGroup]="joinForm" (ngSubmit)="onSubmit()" class="join-form"&amp;gt;
  &amp;lt;label for="name"&amp;gt;Your name&amp;lt;/label&amp;gt;
  &amp;lt;input type="text" id="name" formControlName="name" required /&amp;gt;
  &amp;lt;label for="url"&amp;gt;Daily room URL&amp;lt;/label&amp;gt;
  &amp;lt;input type="text" id="url" formControlName="url" required /&amp;gt;
  &amp;lt;input type="submit" value="Join call" [disabled]="!joinForm.valid" /&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;💡&lt;em&gt;You can create a Daily room and get its URL through the &lt;a href="https://dashboard.daily.co/rooms/create"&gt;Daily dashboard&lt;/a&gt; or &lt;a href="https://docs.daily.co/reference/rest-api/rooms/create-room"&gt;REST API&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/join-form/join-form.component.ts#L23"&gt;&lt;code&gt;onSubmit()&lt;/code&gt;&lt;/a&gt; class method is invoked when the form is submitted (&lt;a href="https://angular.io/api/forms/NgForm#properties"&gt;&lt;code&gt;ngSubmited&lt;/code&gt;&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The input values are bound to the component’s &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/join-form/join-form.component.ts#L16"&gt;&lt;code&gt;joinForm&lt;/code&gt;&lt;/a&gt; values, an instance of Angular’s &lt;a href="https://angular.io/guide/reactive-forms"&gt;&lt;code&gt;FormBuilder&lt;/code&gt;&lt;/a&gt; class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// … See the source code for the complete component

export class JoinFormComponent {
  @Output() setUserName: EventEmitter&amp;lt;string&amp;gt; = new EventEmitter();
  @Output() setUrl: EventEmitter&amp;lt;string&amp;gt; = new EventEmitter();

  joinForm = this.formBuilder.group({
    name: "",
    url: "",
  });

  constructor(private formBuilder: FormBuilder) {}

  onSubmit(): void {
    const { name, url } = this.joinForm.value;
    if (!name || !url) return;

    // Clear form inputs
    this.joinForm.reset();
    // Emit event to update userName var in parent component
    this.setUserName.emit(name);
    // Emit event to update URL var in parent component
    this.setUrl.emit(url);
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;onSubmit()&lt;/code&gt; is invoked, the form input values are retrieved through &lt;code&gt;this.joinForm.value&lt;/code&gt;. The &lt;code&gt;this.joinForm&lt;/code&gt; values are then reset, which also resets the inputs in the form UI to visually indicate to the user that the form was successfully submitted.&lt;/p&gt;

&lt;p&gt;Next, the &lt;code&gt;setUserName()&lt;/code&gt; and &lt;code&gt;setURL()&lt;/code&gt; output properties are used to emit form input values to &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/daily-container/daily-container.component.ts"&gt;&lt;code&gt;app-daily-container&lt;/code&gt;&lt;/a&gt;. (Remember: &lt;code&gt;app-daily-container&lt;/code&gt; is listening for events already.)&lt;/p&gt;

&lt;p&gt;Once these events are received by &lt;code&gt;app-daily-container&lt;/code&gt;, the form values are assigned to the &lt;code&gt;userName&lt;/code&gt; and &lt;code&gt;dailyRoomUrl&lt;/code&gt; variables in &lt;code&gt;app-daily-container&lt;/code&gt;, the latter of which causes the &lt;code&gt;app-call&lt;/code&gt; component to be rendered instead of the &lt;code&gt;join-form&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It’s a mouthful, but now we can move on to creating the actual video call!&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;app-call&lt;/code&gt;: The brains of this video call operation
&lt;/h2&gt;

&lt;p&gt;Let’s start with &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.html"&gt;&lt;code&gt;app-call&lt;/code&gt;’s HTML&lt;/a&gt; so we know what’s going to be rendered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;error-message
  *ngIf="error"
  [error]="error"
  (reset)="leaveCall()"&amp;gt;&amp;lt;/error-message&amp;gt;

&amp;lt;p *ngIf="dailyRoomUrl"&amp;gt;Daily room URL: {{ dailyRoomUrl }}&amp;lt;/p&amp;gt;

&amp;lt;p *ngIf="!isPublic"&amp;gt;
  This room is private and you are now in the lobby. Please use a public room to
  join a call.
&amp;lt;/p&amp;gt;

&amp;lt;div *ngIf="!error" class="participants-container"&amp;gt;
  &amp;lt;video-tile
    *ngFor="let participant of Object.values(participants)"
    (leaveCallClick)="leaveCall()"
    (toggleVideoClick)="toggleLocalVideo()"
    (toggleAudioClick)="toggleLocalAudio()"
    [joined]="joined"
    [videoReady]="participant.videoReady"
    [audioReady]="participant.audioReady"
    [userName]="participant.userName"
    [local]="participant.local"
    [videoTrack]="participant.videoTrack"
    [audioTrack]="participant.audioTrack"&amp;gt;&amp;lt;/video-tile&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is an &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.html#L4"&gt;error message&lt;/a&gt; that gets shown only if the &lt;code&gt;error&lt;/code&gt; variable is true. (This gets set when Daily’s Client SDK’s &lt;a href="https://docs.daily.co/reference/daily-js/events/meeting-events#error"&gt;&lt;code&gt;”error”&lt;/code&gt;&lt;/a&gt; event is emitted.)&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;dailyRoomUrl&lt;/code&gt; value is rendered in the UI so the local participant can see which room they joined. We also let the participant know if the room is private since we haven’t added a feature yet to join a private call. (You could, for example, add a &lt;a href="https://www.daily.co/blog/manage-call-permissions-with-dailys-knocking-feature/"&gt;knocking&lt;/a&gt; feature).&lt;/p&gt;

&lt;p&gt;Finally, the most important part: the video tiles. In Angular, you can use &lt;a href="https://angular.io/api/common/NgFor"&gt;&lt;code&gt;*ngFor&lt;/code&gt;&lt;/a&gt; attribute like a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of"&gt;&lt;code&gt;for of&lt;/code&gt;&lt;/a&gt; statement. For each value in the participants object (described in more detail below), a &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/video-tile/video-tile.component.ts"&gt;&lt;code&gt;video-tile&lt;/code&gt;&lt;/a&gt; component will be rendered. We will look at how the &lt;code&gt;video-tile&lt;/code&gt; component works in the next post so, for now, know that there’s one for each participant.&lt;/p&gt;

&lt;p&gt;Next, let’s look at how we build our &lt;code&gt;participants&lt;/code&gt; object in &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts"&gt;&lt;code&gt;app-call&lt;/code&gt;’s class definition&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Defining the &lt;code&gt;CallComponent&lt;/code&gt; class
&lt;/h3&gt;

&lt;p&gt;Most of the logic related to interacting with &lt;a href="https://docs.daily.co/reference/daily-js"&gt;Daily’s Client SDK for JavaScript&lt;/a&gt; is included in the &lt;code&gt;app-call&lt;/code&gt; component. The &lt;a href="https://docs.daily.co/reference/daily-js"&gt;&lt;code&gt;daily-js&lt;/code&gt; library&lt;/a&gt; is imported at the top of the file (&lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts#L11"&gt;&lt;code&gt;import DailyIframe from "@daily-co/daily-js";&lt;/code&gt;&lt;/a&gt;) which allows us to create the call for the local participant.&lt;/p&gt;

&lt;p&gt;To build our call, we will need to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create an instance of the call object (i.e., the &lt;a href="https://docs.daily.co/reference/daily-js/daily-iframe-class"&gt;&lt;code&gt;DailyIframe&lt;/code&gt; class&lt;/a&gt;) using the &lt;a href="https://docs.daily.co/reference/daily-js/factory-methods/create-call-object"&gt;&lt;code&gt;createCallObject()&lt;/code&gt;&lt;/a&gt; factory method. (The &lt;a href="https://docs.daily.co/reference/daily-js/static-methods/get-call-instance#main"&gt;&lt;code&gt;getCallInstance()&lt;/code&gt; static method&lt;/a&gt; can be used to see if a call object already exists, too.)&lt;/li&gt;
&lt;li&gt;Attach &lt;a href="https://docs.daily.co/reference/daily-js/events"&gt;event listeners&lt;/a&gt; to the call object. &lt;code&gt;daily-js&lt;/code&gt; will emit events for any changes in the call so we’ll listen for the ones relevant to our demo use case.&lt;/li&gt;
&lt;li&gt;Join the call using the &lt;code&gt;dailyRoomUrl&lt;/code&gt; and the &lt;code&gt;userName&lt;/code&gt;, which were provided in the &lt;code&gt;join-form&lt;/code&gt;. This is done via the &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/join"&gt;&lt;code&gt;join()&lt;/code&gt;&lt;/a&gt; instance method.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Since the call component is rendered as soon as the join form is submitted, we need to set up the call logic as soon as the component is initialized. In Angular, we use the &lt;a href="https://angular.io/api/core/OnInit"&gt;&lt;code&gt;ngOnInit()&lt;/code&gt; lifecycle method&lt;/a&gt; to capture this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// … See source code for more detail

export class CallComponent {
  Object = Object; // Allows us to use Object.values() in the HTML file
  @Input() dailyRoomUrl: string;
  @Input() userName: string;
  // .. See source code

  ngOnInit(): void {
    // Retrieve or create the call object
    this.callObject = DailyIframe.getCallInstance();
    if (!this.callObject) {
      this.callObject = DailyIframe.createCallObject();
    }

    // Add event listeners for Daily events
    this.callObject
      .on("joined-meeting", this.handleJoinedMeeting)
      .on("participant-joined", this.participantJoined)
      .on("track-started", this.handleTrackStartedStopped)
      .on("track-stopped", this.handleTrackStartedStopped)
      .on("participant-left", this.handleParticipantLeft)
      .on("left-meeting", this.handleLeftMeeting)
      .on("error", this.handleError);

    // Join Daily call
    this.callObject.join({
      userName: this.userName,
      url: this.dailyRoomUrl,
    });
  }

 // … See source code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three of the steps mentioned above happen as soon as the component is initialized. The second step – attaching Daily event listeners – is where most of the heavy lifting happens for managing the call. For each event shown in the code block above, an event handler is attached that will be invoked when the associated event is received.&lt;/p&gt;

&lt;p&gt;As an overview, each event used above represents the following:&lt;br&gt;
&lt;a href="https://docs.daily.co/reference/daily-js/events/meeting-events#joined-meeting"&gt;&lt;code&gt;”joined-meeting”&lt;/code&gt;&lt;/a&gt;: The local participant joined the call.&lt;br&gt;
&lt;a href="https://docs.daily.co/reference/daily-js/events/participant-events#participant-joined"&gt;&lt;code&gt;”participant-joined”&lt;/code&gt;&lt;/a&gt;: A remote participant joined the call.&lt;br&gt;
&lt;a href="https://docs.daily.co/reference/daily-js/events/participant-events#track-started"&gt;&lt;code&gt;”track-started”&lt;/code&gt;&lt;/a&gt;: A participant's audio or video track began.&lt;br&gt;
&lt;a href="https://docs.daily.co/reference/daily-js/events/participant-events#track-stopped"&gt;&lt;code&gt;”track-stopped”&lt;/code&gt;&lt;/a&gt;: A participant's audio or video track ended.&lt;br&gt;
&lt;a href="https://docs.daily.co/reference/daily-js/events/participant-events#participant-left"&gt;&lt;code&gt;”participant-left”&lt;/code&gt;&lt;/a&gt;: A remote participant left the call.&lt;br&gt;
&lt;a href="https://docs.daily.co/reference/daily-js/events/meeting-events#left-meeting"&gt;&lt;code&gt;”left-meeting”&lt;/code&gt;&lt;/a&gt;: The local participant left the call.&lt;br&gt;
&lt;a href="https://docs.daily.co/reference/daily-js/events/meeting-events#error"&gt;&lt;code&gt;”error”&lt;/code&gt;&lt;/a&gt;: Something went wrong.&lt;/p&gt;

&lt;p&gt;💡 &lt;em&gt;The next post in this series will cover the track-related events.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Once the call object is initialized and &lt;code&gt;join()&lt;/code&gt;-ed, we need to manage the call state related to which participants are in the call. Once we know who is in the call, we can render &lt;code&gt;video-tile&lt;/code&gt; components for them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adding a new participant&lt;/strong&gt;&lt;br&gt;
The &lt;code&gt;app-call&lt;/code&gt; component uses the &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts#L42"&gt;&lt;code&gt;participants&lt;/code&gt;&lt;/a&gt; variable (an empty object to start) to track all participants currently in the call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export type Participant = {
  videoTrack?: MediaStreamTrack | undefined;
  audioTrack?: MediaStreamTrack | undefined;
  videoReady: boolean;
  audioReady: boolean;
  userName: string;
  local: boolean;
  id: string;
};

type Participants = {

};

export class CallComponent {
  // … See source code
  participants: Participants = {};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the local participant or a new remote participant joins, we’ll respond the same way: by updating &lt;code&gt;participants&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In both &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts#L133"&gt;&lt;code&gt;this.handleJoinedMeeting()&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts#L147"&gt;&lt;code&gt;this.participantJoined()&lt;/code&gt;&lt;/a&gt; – the callbacks for &lt;a href="https://docs.daily.co/reference/daily-js/events/meeting-events#joined-meeting"&gt;&lt;code&gt;”joined-meeting”&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://docs.daily.co/reference/daily-js/events/participant-events#participant-joined"&gt;&lt;code&gt;”participant-joined”&lt;/code&gt;&lt;/a&gt; – the &lt;code&gt;this.addParticipant()&lt;/code&gt; method gets called to update &lt;code&gt;participants&lt;/code&gt;. Let’s see how this works using &lt;code&gt;this.participantJoined()&lt;/code&gt; as an example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;participantJoined = (e: DailyEventObjectParticipant | undefined) =&amp;gt; {
  if (!e) return;
  // Add remote participants to participants list used to display video tiles
  this.addParticipant(e.participant);
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With any Daily event, the event payload is available in the callback method (&lt;code&gt;e&lt;/code&gt; in this case). The &lt;code&gt;participant&lt;/code&gt; information is available in the event payload, which then gets passed to &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts#L102"&gt;&lt;code&gt;this.addParticipant()&lt;/code&gt;&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;addParticipant(participant: DailyParticipant) {
  const p = this.formatParticipantObj(participant);
  this.participants[participant.session_id] = p;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two steps happen here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A reformatted copy of the participant object is made.&lt;/li&gt;
&lt;li&gt;The reformatted participant object gets added to the &lt;code&gt;participants&lt;/code&gt; object.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The participant object doesn’t need to get reformatted for this demo to work, but we do this to make the object easier to work with. The participant object that gets returned in the event payload contains a lot of nested information – much of which doesn’t affect our current feature set. To extract the information we do care about, we use &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts#L85"&gt;&lt;code&gt;this.formatParticipantObj()&lt;/code&gt;&lt;/a&gt; to format a copy of the participant object, like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;formatParticipantObj(p: DailyParticipant): Participant {
  const { video, audio } = p.tracks;
  const vt = video?.persistentTrack;
  const at = audio?.persistentTrack;
  return {
    videoTrack: vt,
    audioTrack: at,
    videoReady:
      !!(vt &amp;amp;&amp;amp; (video.state === PLAYABLE_STATE || video.state === LOADING_STATE)),
    audioReady:
      !!(at &amp;amp;&amp;amp; (audio.state === PLAYABLE_STATE || audio.state === LOADING_STATE)),
    userName: p.user_name,
    local: p.local,
    id: p.session_id,
  };
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What we’re interested in here is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The video and audio tracks.&lt;/li&gt;
&lt;li&gt;Whether the tracks can be played in the demo’s UI, which is represented by &lt;code&gt;videoReady&lt;/code&gt; and &lt;code&gt;audioReady&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The user name of the participant, which we’ll display in the UI.&lt;/li&gt;
&lt;li&gt;Whether they’re local, since local participants will have device controls in their &lt;code&gt;video-tile&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The participant ID, so we can have a unique way of identifying them.
(Refer to our docs for an example of &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/participants"&gt;all the other participant information&lt;/a&gt; returned in the event.)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once the participant has been added to the &lt;code&gt;participants&lt;/code&gt; object, a &lt;code&gt;video-tile&lt;/code&gt; can be rendered for them, but more on that in our next post!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Removing a remote participant from the call&lt;/strong&gt;&lt;br&gt;
A call participant can leave the call in different ways. For example, they can use the “Leave” button in the &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/video-tile/video-tile.component.ts"&gt;&lt;code&gt;video-tile&lt;/code&gt;&lt;/a&gt; component or they can simply exit their browser tab for the app. In any case, we need to know when a participant has left to properly update the app UI.&lt;/p&gt;

&lt;p&gt;When any remote participant leaves the call, we need to update &lt;code&gt;participants&lt;/code&gt; to remove that participant.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;handleParticipantLeft = (
  e: DailyEventObjectParticipantLeft | undefined
): void =&amp;gt; {
  if (!e) return;
  console.log(e.action);

  delete this.participants[e.participant.session_id];
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts#L160"&gt;&lt;code&gt;handleParticipantLeft()&lt;/code&gt;&lt;/a&gt; (the handler for the &lt;a href="https://docs.daily.co/reference/daily-js/events/participant-events#participant-left"&gt;&lt;code&gt;”participant-left”&lt;/code&gt;&lt;/a&gt; event), we simply delete the entry for that participant in the participants object. This will in turn remove their video tile from the call UI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Removing a local participant from the call&lt;/strong&gt;&lt;br&gt;
When the local participant leaves, we need to reset the UI by unmounting the &lt;code&gt;app-call&lt;/code&gt; component and going back to rendering the &lt;code&gt;join-form&lt;/code&gt; view instead, since the participant is no longer in the call. Instead of updating &lt;code&gt;participants&lt;/code&gt;, we can just &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/destroy"&gt;&lt;code&gt;destroy()&lt;/code&gt;&lt;/a&gt; the call object and emit &lt;code&gt;callEnded()&lt;/code&gt;, the output property passed from &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/daily-container/daily-container.component.ts"&gt;&lt;code&gt;app-daily-container&lt;/code&gt;&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;handleLeftMeeting = (e: DailyEventObjectNoPayload | undefined): void =&amp;gt; {
  if (!e) return;
  console.log(e);
  this.joined = false; // this wasn’t mentioned above but is used in the UI
  this.callObject.destroy();
  this.callEnded.emit();
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;callEnded()&lt;/code&gt; is emitted, &lt;code&gt;app-daily-container&lt;/code&gt; will reset the &lt;code&gt;dailyRoomUrl&lt;/code&gt; and &lt;code&gt;userName&lt;/code&gt; class variables.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// In DailyContainerComponent:

callEnded(): void {
    // Truthy value will show the CallComponent; otherwise, the JoinFormComponent is shown.
    this.dailyRoomUrl = "";
    this.userName = "";
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As a reminder, when &lt;code&gt;dailyRoomUrl&lt;/code&gt; is falsy, the &lt;code&gt;join-form&lt;/code&gt; component gets rendered instead of the &lt;code&gt;app-call&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And with that, we have a Daily call that can be joined and left!&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In today’s post, we covered how participants can submit an HTML form in an Angular app to join a Daily video call, as well as how to manage participant state when participants join or leave the call.&lt;/p&gt;

&lt;p&gt;In the next post, we’ll discuss how to render video tiles for each participant while they’re in the call, including our recommendations for optimizing video performance in an app like this.&lt;/p&gt;

&lt;p&gt;If you have any questions or thoughts about implementing your Daily-powered video app with Angular, &lt;a href="https://www.daily.co/company/contact/support/"&gt;reach out to our support team&lt;/a&gt; or head over to our &lt;a href="https://community.daily.co/"&gt;WebRTC community, peerConnection&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webrtc</category>
      <category>angular</category>
      <category>tutorial</category>
      <category>programming</category>
    </item>
    <item>
      <title>Build a Daily video call app with Angular and TypeScript (Part 1)</title>
      <dc:creator>Tasha</dc:creator>
      <pubDate>Thu, 12 Oct 2023 17:23:49 +0000</pubDate>
      <link>https://forem.com/trydaily/build-a-daily-video-call-app-with-angular-and-typescript-part-1-1lf9</link>
      <guid>https://forem.com/trydaily/build-a-daily-video-call-app-with-angular-and-typescript-part-1-1lf9</guid>
      <description>&lt;p&gt;&lt;em&gt;By &lt;a href="https://www.daily.co/blog/author/jess/"&gt;Jess Mitchell&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you’ve read Daily’s blog before, you’ll know we’re happy to show developers all the different ways you can incorporate Daily’s SDKs into your apps. One of the best aspects of &lt;a href="https://docs.daily.co/reference/daily-js"&gt;Daily's Client SDK for JavaScript&lt;/a&gt; is that it’s front-end framework agnostic. If you love &lt;a href="https://www.daily.co/blog/tag/react/"&gt;React&lt;/a&gt; and &lt;a href="https://www.daily.co/blog/tag/next-js/"&gt;Next.js&lt;/a&gt;, we have several tutorials and demo apps for you! If you prefer &lt;a href="https://www.daily.co/blog/tag/svelte/"&gt;Svelte&lt;/a&gt;, &lt;a href="https://www.daily.co/blog/tag/vue/"&gt;Vue&lt;/a&gt;, or &lt;a href="https://www.daily.co/blog/tag/social-gaming/"&gt;plain JavaScript&lt;/a&gt;, we’ve got you covered there, too.&lt;/p&gt;

&lt;p&gt;In this series, we’ll be expanding our front-end framework resources even further with our newest series on building an &lt;a href="https://angular.io/"&gt;Angular&lt;/a&gt; app with &lt;a href="https://docs.daily.co/reference/daily-js"&gt;Daily’s Client SDK for JavaScript&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This will start as a four-part series with the following topics:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An overview of the Angular demo app’s features and component structure.&lt;/li&gt;
&lt;li&gt;Building the user flow of joining and leaving a Daily call.&lt;/li&gt;
&lt;li&gt;How to render video tiles for each participant and manage any track changes.&lt;/li&gt;
&lt;li&gt;Adding a custom chat component to the video call app so participants can send text-based messages to each other in the call.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--g2dH2U_3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v8dozzcbj8nyo9nj84cd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--g2dH2U_3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v8dozzcbj8nyo9nj84cd.png" alt="Three call participants in a video call, two of them with their cameras on" width="800" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In-call demo app view&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Today’s post will focus on the first topic: an overview of the demo app and its components. To follow along with this series, we recommend having existing familiarity with JavaScript/TypeScript, HTML, and Angular. (We won’t go into the CSS styling much since it’s not a key factor in the functionality of the app.)&lt;/p&gt;

&lt;p&gt;We’ve got a lot to get through, so let’s get started!&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the demo app locally
&lt;/h2&gt;

&lt;p&gt;To follow along with this series, we recommend testing the demo app yourself and reviewing the &lt;a href="https://github.com/daily-demos/daily-angular"&gt;source code&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a Daily account and a Daily room
&lt;/h3&gt;

&lt;p&gt;To use the demo app, you will first need to &lt;a href="https://dashboard.daily.co/signup"&gt;create a free Daily account&lt;/a&gt;. Once you have an account, you can &lt;a href="https://dashboard.daily.co/rooms/create"&gt;create a Daily room&lt;/a&gt;. We recommend using the default settings when creating a room, but if you do update the settings, leave the room &lt;code&gt;privacy&lt;/code&gt; configuration set to &lt;a href="https://docs.daily.co/reference/rest-api/rooms/config#privacy"&gt;&lt;code&gt;public&lt;/code&gt;&lt;/a&gt;. (The app isn’t currently set up to handle access requests, like knocking, but you could add that feature.)&lt;/p&gt;

&lt;p&gt;Make note of the room’s URL, which will be required to use the demo app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running the demo app
&lt;/h3&gt;

&lt;p&gt;To use the demo app locally, start by installing Angular if you haven’t already:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install -g @angular/cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, clone the demo &lt;a href="https://github.com/daily-demos/daily-angular"&gt;repository&lt;/a&gt; and navigate into the demo’s root directory on your local machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/daily-demos/daily-angular.git
cd daily-angular
Bash
Install its dependencies, start the local server, and visit the app in your browser of choice at localhost:4200.

npm install
npm run start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We encourage developers to add to this demo app as needed to try out different features available through &lt;a href="https://docs.daily.co/reference/daily-js"&gt;Daily’s Client SDK for JavaScript&lt;/a&gt;. If you want to add a new feature, you can create new components with &lt;code&gt;ng generate component &amp;lt;component-name&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now that we’re set up, let’s dig into the feature set.&lt;/p&gt;

&lt;h2&gt;
  
  
  App features overview
&lt;/h2&gt;

&lt;p&gt;As mentioned, the demo app will be built with &lt;a href="https://angular.io/"&gt;Angular&lt;/a&gt; and &lt;a href="https://docs.daily.co/reference/daily-js"&gt;Daily’s Client SDK for JavaScript&lt;/a&gt;. It will support multi-participant calls, but will focus on the core functionality and not contain &lt;a href="https://www.daily.co/blog/tips-to-improve-performance/"&gt;performance optimizations&lt;/a&gt; for larger calls, like conferences. (Check out our &lt;a href="https://docs.daily.co/guides/scaling-calls/large-real-time-calls"&gt;large calls guide&lt;/a&gt; for more information about building apps for up to 100,000 real-time participants.)&lt;/p&gt;

&lt;p&gt;The app UI will be fully customized, but you could also embed &lt;a href="https://docs.daily.co/guides/products/prebuilt"&gt;Daily Prebuilt&lt;/a&gt; if you prefer a ready-made solution.&lt;/p&gt;

&lt;p&gt;The actual features we’ll have built by the end of this series include:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Two views: a home page with a form to join the call and an in-call view.&lt;/li&gt;
&lt;li&gt;The join form will accept two pieces of information: your name and the Daily room you’d like to join.&lt;/li&gt;
&lt;li&gt;The in-call view will have a video tile for each participant. Participants can turn their video and audio on/off, and can click a button to leave the call. Video tiles will automatically update when a remote participant toggles their media or the track changes, (e.g., if they change their input device).&lt;/li&gt;
&lt;li&gt;An error screen is shown for errors and a “Not Found” page is shown if you navigate away from the main route.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--9sgOHnTf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3gt9ca50h57wv9dvjnco.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9sgOHnTf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3gt9ca50h57wv9dvjnco.png" alt="Entry form to join a Daily video call room" width="800" height="358"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The demo app's join form in the home page view&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  App component structure
&lt;/h2&gt;

&lt;p&gt;Let’s now see how our Angular components are structured relative to each other:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--lDrqM8ex--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jjbt590hoo2d2h16jiho.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--lDrqM8ex--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jjbt590hoo2d2h16jiho.png" alt="A schematic showing app-root, app-call, and other video call Angular application components" width="800" height="393"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Component structure in the Angular demo app&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Each of the components in the flow chart above is only rendered once except for &lt;a href="https://github.com/daily-demos/daily-angular/tree/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/video-tile"&gt;&lt;code&gt;video-tile&lt;/code&gt;&lt;/a&gt; component. A &lt;code&gt;video-tile&lt;/code&gt; component is rendered for each participant present.&lt;/p&gt;

&lt;p&gt;The names used in the chart refer to each component’s selector, which can be found in the component’s TypeScript file. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Component({
  selector: "app-call",
  templateUrl: "./call.component.html",
  styleUrls: ["./call.component.css"],
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In general, we’ll refer to a component by its selector in this series, but we may also use its class name if we’re specifically referring to the class definition.&lt;/p&gt;

&lt;p&gt;If you’re newer to Angular, another detail to be aware of is that each component has three files associated with it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A TypeScript file that defines the component class.&lt;/li&gt;
&lt;li&gt;An HTML file defining its DOM elements.&lt;/li&gt;
&lt;li&gt;A CSS file to style the DOM elements.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first two are the files we’ll focus on in this series.&lt;/p&gt;

&lt;p&gt;And, lastly, if you’re specific looking for example code for the video-aspects of build a video app in Angular, the components with the most Daily-related code are &lt;a href="https://github.com/daily-demos/daily-angular/tree/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call"&gt;&lt;code&gt;app-call&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/daily-demos/daily-angular/tree/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/chat"&gt;&lt;code&gt;app-chat&lt;/code&gt;&lt;/a&gt;, and &lt;a href="https://github.com/daily-demos/daily-angular/tree/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/video-tile"&gt;&lt;code&gt;video-tile&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Next, let’s see what each component does and when it's rendered.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;app-root&lt;/code&gt; and routing
&lt;/h3&gt;

&lt;p&gt;Every Angular app will have a root component that parents the entire app. It includes any HTML elements that are rendered on every page, regardless of the route (in our case, the &lt;code&gt;header&lt;/code&gt; and &lt;code&gt;main&lt;/code&gt; elements). It also manages app routing and will conditionally render a component based on how the routes are defined.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/app.component.html"&gt;&lt;code&gt;app.component.html&lt;/code&gt;&lt;/a&gt;, we see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;main class="content"&amp;gt;
  &amp;lt;!-- App router --&amp;gt;
  &amp;lt;router-outlet&amp;gt;&amp;lt;/router-outlet&amp;gt;
&amp;lt;/main&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;router-outlet&lt;/code&gt; will render the component that matches the route currently being visited.&lt;/p&gt;

&lt;p&gt;The logic for which component should be rendered is defined in &lt;a href="https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/app-routing.module.ts"&gt;&lt;code&gt;app-routing.module.ts&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { PageNotFoundComponent } from "./page-not-found/page-not-found.component";
import { DailyContainerComponent } from "./daily-container/daily-container.component";

const routes: Routes = [
  { path: "", component: DailyContainerComponent },
  { path: "**", component: PageNotFoundComponent },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the &lt;code&gt;routes&lt;/code&gt; array, each path is mapped to a different component. Since our app routing is quite simple, we only have two routes: the base path and literally anything else. If you are visiting the base path (e.g., &lt;code&gt;localhost:4200/&lt;/code&gt;), the &lt;code&gt;DailyContainerComponent&lt;/code&gt; is rendered (a.k.a. &lt;a href="https://github.com/daily-demos/daily-angular/tree/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/daily-container"&gt;&lt;code&gt;app-daily-container&lt;/code&gt;&lt;/a&gt;). Otherwise, a message is shown that the page isn’t found (the &lt;a href="https://github.com/daily-demos/daily-angular/tree/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/page-not-found"&gt;&lt;code&gt;app-page-not-found&lt;/code&gt;&lt;/a&gt; component).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--j-o25_9_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/64qfnfb9h89epu3lsbbb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--j-o25_9_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/64qfnfb9h89epu3lsbbb.png" alt="&amp;quot;Uh oh. This page doesn't exist.&amp;quot;" width="800" height="172"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;&lt;code&gt;app-page-not-found&lt;/code&gt; view&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To add more &lt;code&gt;routes&lt;/code&gt; to the app, the routes array can be updated, though the wildcard option should be left as the last item to catch any routes that aren’t specifically included.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;app-daily-container&lt;/code&gt;, &lt;code&gt;join-form&lt;/code&gt;, and &lt;code&gt;app-call&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Moving further down the flow chart, we have &lt;a href="https://github.com/daily-demos/daily-angular/tree/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/daily-container"&gt;&lt;code&gt;app-daily-container&lt;/code&gt;&lt;/a&gt; next and two of its child components: &lt;a href="https://github.com/daily-demos/daily-angular/tree/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/join-form"&gt;&lt;code&gt;join-form&lt;/code&gt;&lt;/a&gt;, and &lt;a href="https://github.com/daily-demos/daily-angular/tree/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call"&gt;&lt;code&gt;app-call&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The role of &lt;a href="https://github.com/daily-demos/daily-angular/tree/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/daily-container"&gt;&lt;code&gt;app-daily-container&lt;/code&gt;&lt;/a&gt; is to determine whether &lt;a href="https://github.com/daily-demos/daily-angular/tree/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/join-form"&gt;&lt;code&gt;join-form&lt;/code&gt;&lt;/a&gt; or &lt;a href="https://github.com/daily-demos/daily-angular/tree/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call"&gt;&lt;code&gt;app-call&lt;/code&gt;&lt;/a&gt; should be rendered. They will never both be rendered at the same time. It does this by keeping track of whether the HTML form in &lt;code&gt;join-form&lt;/code&gt; has been submitted and, if so, rendering the &lt;code&gt;app-call&lt;/code&gt; component instead. (In the next post in this series, we’ll look more closely at the code that handles this.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--SDqOK-8j--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mgy3ktmztglhujg7wuzm.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SDqOK-8j--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mgy3ktmztglhujg7wuzm.gif" alt="Animation showing user clicking Join to enter the call" width="600" height="260"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Submitting the form in &lt;code&gt;join-form&lt;/code&gt; will cause &lt;code&gt;app-daily-container&lt;/code&gt; to render &lt;code&gt;app-call&lt;/code&gt; instead.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;join-form&lt;/code&gt; accepts two pieces of information:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your user name, which will be displayed in-call.&lt;/li&gt;
&lt;li&gt;The URL for the Daily room you would like to join. (This is the URL for the room created above.)
&lt;code&gt;app-call&lt;/code&gt; is the actual video call UI where you can see video tiles for any participants who have joined the same Daily room.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;video-tile&lt;/code&gt; and &lt;code&gt;error-message&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;As mentioned, each participant will have a &lt;a href="https://github.com/daily-demos/daily-angular/tree/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/video-tile"&gt;&lt;code&gt;video-tile&lt;/code&gt;&lt;/a&gt; component rendered for them to indicate they are present in the call. The &lt;code&gt;video-tile&lt;/code&gt; component includes the participant’s name and icons to indicate if their audio is currently on. It also has an HTML &lt;code&gt;video&lt;/code&gt; element for all participants and an audio element for remote participants. (An HTML &lt;code&gt;audio&lt;/code&gt; element is not rendered for the local participant – you! – because you can already hear yourself speak, meaning you don’t need to have your audio played back to you.) If the participant’s video is turned off, a placeholder tile is rendered over the video.&lt;/p&gt;

&lt;p&gt;You, the local participant, will also see a control panel over your tile with buttons to toggle your audio and video on/off, as well as a button to leave the call.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--1yEv3LKq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eu3s5zajczvjmu0ahh6l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--1yEv3LKq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eu3s5zajczvjmu0ahh6l.png" alt="Two video tiles: The local participant with the control panel and a remote participant with their video off." width="800" height="225"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Two video tiles: The local participant with the control panel and a remote participant with their video off.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If there is an error related to the call, the &lt;a href="https://github.com/daily-demos/daily-angular/tree/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/error-message"&gt;&lt;code&gt;error-message&lt;/code&gt;&lt;/a&gt; component is shown instead of the list of &lt;code&gt;video-tile&lt;/code&gt; components. It is rendered when the &lt;a href="https://docs.daily.co/reference/daily-js/events/meeting-events#error"&gt;&lt;code&gt;error&lt;/code&gt; event&lt;/a&gt; is emitted by &lt;a href="https://docs.daily.co/reference/daily-js"&gt;Daily’s Client SDK for JavaScript&lt;/a&gt;. It displays the error message included in the error event’s payload and provides a button to leave the call.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;app-chat&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;app-chat&lt;/code&gt; is also a child component of &lt;code&gt;app-daily-container&lt;/code&gt;. It is not related to the video part of the call; rather, it represents the text-based chat feature. This component can actually be used in either a custom UI – like this demo app – or in an app that embeds Daily Prebuilt.&lt;/p&gt;

&lt;p&gt;Like &lt;code&gt;app-call&lt;/code&gt;, the chat component requires that the call has been joined before it will work, so it is rendered once the form in &lt;code&gt;join-form&lt;/code&gt; has been submitted.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--mxaL4l1l--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8q2qrwik4fs66p5cz61g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--mxaL4l1l--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8q2qrwik4fs66p5cz61g.png" alt="Slide-out chat panel on the right-hand side of the Angular video app" width="800" height="513"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The &lt;code&gt;app-chat&lt;/code&gt; component highlighted in the app’s UI.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Today, we reviewed the structure and features of Daily’s new Angular demo app. In the next post, we’ll look at how the &lt;code&gt;app-daily-container&lt;/code&gt; component responds to submitting the form in the &lt;code&gt;join-form&lt;/code&gt; component. We’ll also review how to use &lt;a href="https://docs.daily.co/reference/daily-js"&gt;Daily’s Client SDK for JavaScript&lt;/a&gt; to let the local participant join or leave a call.&lt;/p&gt;

&lt;p&gt;Check out the complete &lt;a href="https://github.com/daily-demos/daily-angular"&gt;source code&lt;/a&gt; for this demo app to get a head start and keep an eye on our &lt;a href="https://www.daily.co/blog/tag/angular"&gt;blog&lt;/a&gt; for future posts in this series!&lt;/p&gt;

</description>
      <category>angular</category>
      <category>typescript</category>
      <category>webrtc</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Measuring performance impact of pagination in video apps</title>
      <dc:creator>Tasha</dc:creator>
      <pubDate>Tue, 10 Oct 2023 16:58:29 +0000</pubDate>
      <link>https://forem.com/trydaily/measuring-performance-impact-of-pagination-in-video-apps-4nci</link>
      <guid>https://forem.com/trydaily/measuring-performance-impact-of-pagination-in-video-apps-4nci</guid>
      <description>&lt;p&gt;&lt;em&gt;By &lt;a href="https://www.daily.co/blog/author/olabisi/"&gt;Olabisi Oduola&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In our previous posts, we discussed how pagination can be used in a &lt;a href="https://www.daily.co/blog/add-pagination-to-a-custom-daily-video-chat-app-to-support-larger-meetings/"&gt;custom Daily video chat app to support larger meetings&lt;/a&gt; and how to &lt;a href="https://www.daily.co/blog/optimize-call-quality-in-larger-calls-by-manually-managing-media-tracks-in-a-paginated-video-call-ui/"&gt;manually manage media tracks in a paginated video call&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now, I will show you how to benchmark your pagination implementation and see for yourself how much impact it has on your app’s performance and user experience.&lt;/p&gt;

&lt;p&gt;When building a custom video app with &lt;a href="https://docs.daily.co/reference/daily-js"&gt;Daily's Client SDK for JavaScript&lt;/a&gt;, developers can tailor their implementation to meet their exact product needs. This level of customization  creates the possibility for developers to impact the performance of their app (both positively and negatively) depending on how they decide to approach certain features, such as video tile pagination. This is an important consideration for any type of video call app, and one we can help you navigate. &lt;/p&gt;

&lt;p&gt;💡&lt;em&gt;If you are looking for a video call solution that is already optimized for various platforms and network conditions, check out &lt;a href="https://www.daily.co/products/prebuilt-video-call-app/"&gt;Daily Prebuilt&lt;/a&gt;–our fully-featured video call embed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One common question with building any custom video app is: How many video tiles should we show per page? This decision can have a significant impact on the application’s performance and user experience. &lt;/p&gt;

&lt;p&gt;If we show too &lt;em&gt;few&lt;/em&gt; videos per page, we may:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frustrate users: It can get annoying to click through many pages to find the participant video you are looking for, or miss out on some relevant or interesting videos.&lt;/li&gt;
&lt;li&gt;Compromise retention: Users may lose patience if they have to wait too long for the next page of videos to load.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the other hand, if we show too many videos per page, we may:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Decrease performance: Loading and rendering many videos at once comes with extra bandwidth and CPU consumption, which can slow down the app and degrade the quality of the videos over time.&lt;/li&gt;
&lt;li&gt;Clutter the visual field: Showing too many videos per page may create a crowded and overwhelming visual layout, making it tricky for users to find and focus on the content they are interested in.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Therefore, finding the optimal number of video tiles per page is a matter of balancing these factors. We need to find the “sweet spot” that maximizes both performance and user satisfaction.&lt;/p&gt;

&lt;p&gt;That sweet spot is different for everyone. Different video apps have their own goals, constraints, and user flows. Therefore, we need to consider some factors that may influence this decision, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The nature of the app: Calls intended for smaller audiences, like small group classrooms or webinars, may be designed to support fewer, high-resolution video streams. Therefore, they may not benefit from pagination. On the other hand, if your app involves having many users interacting with each other, you will likely want to enable pagination. You’ll probably also experiment with bandwidth, output, and receive settings to sustain lively conversation in a larger group.&lt;/li&gt;
&lt;li&gt;The device and network conditions: The optimal number of video tiles per page may vary depending on the device and network capabilities of the users. For example, mobile devices have smaller screens, lower CPU power, and lower bandwidth than desktop devices. This may affect how many videos can be loaded and displayed effectively.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How can you test and iterate on different pagination options?
&lt;/h2&gt;

&lt;p&gt;We will use our existing &lt;a href="https://github.com/daily-demos/track-subscriptions"&gt;pagination demo app&lt;/a&gt; introduced in a &lt;a href="https://www.daily.co/blog/optimize-call-quality-in-larger-calls-by-manually-managing-media-tracks-in-a-paginated-video-call-ui/"&gt;previous post&lt;/a&gt;, which allows you to control the number of video tiles shown in your app. We will utilize Chrome developer tools to compare the performance of different pagination scenarios.  We’ll then provide some tips on how to find the optimal balance between performance and user experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;To follow along with this post, prepare your environment as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install &lt;a href="https://www.google.com/chrome/"&gt;Google Chrome&lt;/a&gt; if you don’t already have it.&lt;/li&gt;
&lt;li&gt;Create a free &lt;a href="https://dashboard.daily.co/signup"&gt;Daily account&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Retrieve your API key from the &lt;a href="https://dashboard.daily.co/developers"&gt;developer dashboard&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Clone the &lt;a href="https://github.com/daily-demos/track-subscriptions"&gt;&lt;code&gt;daily-demos/track-subscriptions&lt;/code&gt;&lt;/a&gt; repo from GitHub.&lt;/li&gt;
&lt;li&gt;Navigate to the project directory and check out my &lt;code&gt;custom-tile-pagination&lt;/code&gt; branch:&lt;/li&gt;
&lt;li&gt;Install the necessary dependencies by running &lt;code&gt;yarn&lt;/code&gt;
Create an &lt;code&gt;.env.local&lt;/code&gt; file in the root directory of the app and add your own &lt;code&gt;DAILY_DOMAIN&lt;/code&gt; and &lt;code&gt;DAILY_API_KEY&lt;/code&gt; from your Daily account dashboard. Remember to never submit this file to source control! Your Daily API key is secret.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Finally, you can start the development server by running &lt;code&gt;yarn dev&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Open the app in Google Chrome by navigating to &lt;strong&gt;&lt;a href="http://localhost:3000/"&gt;http://localhost:3000/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You can now create a new video call by clicking on the “Create room” button&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--nc8iMUkU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m5w5vr13rhhu9a2xllqc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--nc8iMUkU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m5w5vr13rhhu9a2xllqc.png" alt="Entering a Daily video call room" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Entering a Daily video call room&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;(Incognito Mode is preferable if you have a lot of extensions installed, it ensures that Chrome runs in a clean state as those extensions might create noise in your performance measurements.)&lt;/p&gt;

&lt;p&gt;For the purpose of this article, I modified the &lt;a href="https://github.com/daily-demos/track-subscriptions/compare/custom-tile-pagination#diff-d0a1cd77dca25318be3c71a4b2f56c91cdca61b3997872a88339902808338439R17"&gt;&lt;code&gt;hooks/useAspectGrid.js&lt;/code&gt;&lt;/a&gt; file in the cloned app so we can pass in custom values for &lt;code&gt;customMaxTilesPerPage&lt;/code&gt;. This determines the maximum video tiles to be displayed on each page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export const useAspectGrid = (
  gridRef,
  numTiles,
  customMaxTilesPerPage = 20
) =&amp;gt; {

    // ...Existing logic

    const MIN_TILE_WIDTH = useMemo(() =&amp;gt; {
        const { width } = dimensions;
        const maxColumnsForCustomMax = Math.max(1, Math.floor(width / customMaxTilesPerPage));
        return width / maxColumnsForCustomMax;
      }, [dimensions, customMaxTilesPerPage]);

    // The rest of the hook…
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this modification, the tile width will reduce as the &lt;code&gt;customMaxTilesPerPage&lt;/code&gt; value increases.&lt;/p&gt;

&lt;p&gt;When you increase the value of &lt;code&gt;customMaxTilesPerPage&lt;/code&gt; above, you are telling the &lt;code&gt;useAspectGrid()&lt;/code&gt; hook (which creates the grid of video tiles to be displayed on a page) to display more tiles on each page. This means each tile will need to be narrower in order to fit into the available space. In the code above, I set the maximum tiles to display per page to 20. This means each participant will subscribe to the video and audio tracks of up to 20 others video call users.&lt;/p&gt;

&lt;p&gt;Now, we can test the effect this will have on performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring the runtime performance of your pagination implementation using Chrome DevTools
&lt;/h2&gt;

&lt;p&gt;I will be using the Chrome DevTools performance monitor to visualize how our app performs during runtime and identify bottlenecks or other inefficiencies. The performance monitor  provides a real-time view of the runtime performance of the app by displaying graphs of various performance metrics that update in real time.&lt;/p&gt;

&lt;p&gt;There are many metrics that can be used to measure the runtime performance of an app, but not all of them are relevant for our use case. For this Daily video demo app, we are interested in metrics that reflect how well the app handles multiple video renders, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CPU&lt;/strong&gt;: The CPU metric shows how much processing power your app consumes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JS heap memory&lt;/strong&gt;: This metric shows how much memory your app allocates and deallocates. (Check out my earlier post about &lt;a href="https://www.daily.co/blog/introduction-to-memory-management-in-node-js-applications/"&gt;memory management&lt;/a&gt; if you want to learn more about how heap memory works.) &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FPS&lt;/strong&gt;: The frames per second (FPS) metric shows how smoothly your app renders on the screen. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network&lt;/strong&gt;: The network metric shows how much data your app transfers over the network. Daily also &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/get-network-stats"&gt;monitors this&lt;/a&gt;, dynamically adjusting call participants' receive settings to provide them with the &lt;a href="https://docs.daily.co/guides/architecture-and-monitoring/intro-to-video-arch"&gt;best possible experience&lt;/a&gt; for their network conditions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Opening the performance monitor
&lt;/h2&gt;

&lt;p&gt;First, open Chrome DevTools by using the shortcut Ctrl+Shift+I (Windows, Linux) or Command+Option+I (macOS).&lt;/p&gt;

&lt;p&gt;In DevTools, on the main toolbar, select the “Performance monitor” tab. If that tab isn't visible, click the More tabs button, or else the More Tools button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--R8gPWmSO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k6coom4w9fr67vydo50u.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--R8gPWmSO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k6coom4w9fr67vydo50u.gif" alt="Opening the DevTools performance monitor" width="800" height="460"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Opening the DevTools performance monitor&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To get the FPS readings, select the Rendering tool, and turn on Frame Rendering Stats. A new overlay will  appear in the top-left corner of your webpage. The Frame Rendering Stats overlay provides real-time estimates for FPS as your app runs. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--x8tURBPk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2uh5vmcs0za3o62hdgps.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--x8tURBPk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2uh5vmcs0za3o62hdgps.gif" alt="20-tile in-app view with performance stats shown on the top left and right-hand sides" width="800" height="463"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;20-tile in-app view with performance stats shown on the top left and right-hand sides&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Based on the screenshot above, we can see that showing 20 video tiles results in a noticeable performance hit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The CPU usage reaches levels of 80-100%, indicating a considerable load that might result in overheating or battery depletion on the device.&lt;/li&gt;
&lt;li&gt;The memory consumption hovers between 80-100MB, displaying frequent fluctuations within the JavaScript heap graph.&lt;/li&gt;
&lt;li&gt;The frames per second (FPS) drops below 20, a level considered quite low, and can potentially lead to rendering issues such as stuttering or lagging.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--b4IWfys4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h5nlhvisqxg8bnwbwlrx.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--b4IWfys4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h5nlhvisqxg8bnwbwlrx.gif" alt="12-tile in-app view" width="800" height="460"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;12-tile in-app view&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When we switch to showing 12  tiles (which is the demo app’s default), we can see that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The CPU usage is around 10-20%, which is not too high for most devices.&lt;/li&gt;
&lt;li&gt;The application is using a moderate amount of memory (40-50MB). ​​This means the app is not creating and deleting too many objects.&lt;/li&gt;
&lt;li&gt;The FPS is around 50-60, which is ideal for a smooth rendering and smooth user experience. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Therefore, we can conclude that &lt;em&gt;with no other changes to optimize output and receive bandwidth&lt;/em&gt;, showing 12 video tiles is optimal for our app. Showing 20 video tiles concurrently, in this configuration, is suboptimal. Seeing the effect first-hand can help us optimize our pagination settings for the ideal effect. &lt;/p&gt;

&lt;p&gt;Of course, the number of video tracks being played is just one of several factors that have an impact on the app’s performance: a starting point. If your use case calls for playback of more (or fewer!) simultaneous video and audio tracks, you can then delve into Daily’s &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/update-send-settings#main"&gt;send settings&lt;/a&gt; and &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/update-receive-settings#main"&gt;receive settings&lt;/a&gt; options to fine-tune your configuration further.  &lt;/p&gt;

&lt;p&gt;Let’s dig a little deeper into the different factors we’re using to judge the performance impact of our pagination configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  CPU
&lt;/h2&gt;

&lt;p&gt;CPU usage numbers may vary depending on the device, browser, network, and other factors, but we can use some general guidelines to help us get a gauge of how our pagination or other settings affect performance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We want to keep the CPU usage as low as possible to avoid overheating or draining the battery of the device.&lt;/li&gt;
&lt;li&gt;Ideally, you want to keep your CPU usage below 50% for a fast and responsive app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note that mobile devices have much less CPU power than desktops and laptops. Whenever you profile a page, use CPU Throttling to simulate how your page performs on mobile devices. To simulate a mobile CPU, you can use 4x slowdown for mid-tier devices or 6x slowdown for low-end mobile devices. You may find that a mobile app can benefit from subscribing to and displaying fewer video tracks than desktop. This can also be beneficial from a UI perspective, considering on mobile there’s not as much display space to work with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Heap memory
&lt;/h2&gt;

&lt;p&gt;If your app is progressively using more and more memory, without it getting garbage collected, you have a &lt;a href="https://www.daily.co/blog/introduction-to-memory-management-in-node-js-applications/"&gt;leak&lt;/a&gt;. But leaks aside, how much memory usage is "too much"? There are no definitive numbers here, because different devices and browsers have different capabilities. JavaScript engines help us along with their own garbage collection processes. However, garbage collection itself can be an expensive operation.&lt;/p&gt;

&lt;p&gt;In performance recordings, frequent changes (rising and falling) to the JS heap graphs indicate frequent garbage collection. This may be worth investigating, especially if you notice degraded performance on certain devices. &lt;/p&gt;

&lt;h2&gt;
  
  
  Frames per second (FPS)
&lt;/h2&gt;

&lt;p&gt;A high FPS value means that your app is responsive and fluid, while low FPS can result in sluggish and choppy rendering. Ideally, you want to maintain an FPS of 50 or higher for a smooth user experience. The FPS is shown in the left-hand corner of your web app when the performance monitor is open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;To achieve optimal performance and a great user experience in your video call application, focus on finding the right balance between performance and UX:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.daily.co/guides/architecture-and-monitoring/intro-to-video-arch#call-constraints-and-cameras"&gt;Consider device constraints&lt;/a&gt;: Account for CPU and bandwidth limitations on mobile and older devices. Limit active video cameras to maintain call quality.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.daily.co/guides/configurations-and-settings/setting-up-calls#building-calls-with-daily-prebuilt"&gt;Customisation&lt;/a&gt;: Tailor room settings to suit different use cases. Consider &lt;a href="https://docs.daily.co/guides/scaling-calls/interactive-live-streaming-rtmp-output#main"&gt;Interactive Live Streaming&lt;/a&gt;: For large audiences, consider live streaming with minimal delay to enhance performance. Daily’s ILS supports 100,000 real-time participants. &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.daily.co/guides/configurations-and-settings/setting-up-calls#building-a-custom-video-call-ui-using-the-daily-call-object"&gt;Find your sweet spot&lt;/a&gt;: Find your ideal balance of pagination settings and media resolution to suit your specific use case.&lt;/li&gt;
&lt;li&gt;Look at your app holistically: Video is likely just one part of your application. Be aware of other components that may impact the performance of your application in conjunction with the video implementation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By taking these performance considerations into account, you can create a video call app that delivers both top-notch performance and an enjoyable user experience.&lt;/p&gt;

</description>
      <category>webrtc</category>
      <category>ux</category>
      <category>tutorial</category>
      <category>performance</category>
    </item>
    <item>
      <title>Automatic short-form video: Highlight reels at scale with AI and VCSRender</title>
      <dc:creator>Tasha</dc:creator>
      <pubDate>Fri, 06 Oct 2023 16:41:31 +0000</pubDate>
      <link>https://forem.com/trydaily/automatic-short-form-video-highlight-reels-at-scale-with-ai-and-vcsrender-2h71</link>
      <guid>https://forem.com/trydaily/automatic-short-form-video-highlight-reels-at-scale-with-ai-and-vcsrender-2h71</guid>
      <description>&lt;p&gt;&lt;em&gt;By &lt;a href="https://www.daily.co/blog/author/pauli/"&gt;Pauli Olavi Ojala&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Daily’s developer platform powers audio and video experiences for millions of people all over the world. Our customers are developers who use our APIs and client SDKs to build audio and video features into applications and websites.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Today, as part of &lt;a href="https://www.daily.co/blog/ai-week-at-daily-2023-edition/"&gt;AI Week&lt;/a&gt;, Pauli Olavi Ojala discusses automation of video editing. Pauli is a senior engineer and architect of Daily's compositing framework VCS (Video Component System).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Our kickoff post for AI Week goes into more detail about what topics we’re diving into and how we think about the potential of combining WebRTC, video and audio, and AI. Feel free to click over and &lt;a href="https://www.daily.co/blog/ai-week-at-daily-2023-edition/"&gt;read that intro&lt;/a&gt; before (or after) reading this post.&lt;/em&gt;&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;💡 This post introduces the problem of mining short-form “video gold” out of a mountain of raw data, and why current AI models can’t solve this problem directly. We take a look at an experimental approach being built at Daily and how it was applied to a Cloud Poker Night session, to create automatic highlight reels. Finally we note a new open source project that provides some missing infrastructure pieces for rendering short-form videos at the large scale enabled by AI.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These days people love watching bite-sized video clips, typically 15-60 seconds long, on platforms like TikTok, Instagram, and YouTube. For many user groups, this kind of short-form video has displaced more traditional social media content formats such as Twitter-style short text posts, Instagram-style static photos, and the classic extended YouTube video.&lt;/p&gt;

&lt;p&gt;Businesses and content creators could reach their audiences in this popular new format, but they face a massive problem in how to produce the content. Creating simple tweets and picture posts is well understood and often automated by various tools. While YouTube-style long-form video can be laborious and also requires frequent posting, newer formats up the ante.&lt;/p&gt;

&lt;p&gt;Short-form video manages to combine the most difficult aspects of both. Like with tweets, you need a high volume of content because the clips are so short and viewers are flooded. On the other hand, producing a great 45-second video can be as much work as making a great ten-minute video. Often it’s even harder — in the classic rambling YouTube format, a creator can often keep people watching simply by talking well, but a short reel must have tight editing, cool graphics, flashy transitions, and so on.&lt;/p&gt;

&lt;p&gt;Although short-form video seems like a pure social media phenomenon today, it’s worth noting that social modes of expression and habits of content consumption tend to get integrated into other digital products over time. Consider Slack: a very popular business product, but the user experience has much more in common with Discord and Twitter than old-school business software like Outlook. Fifteen years ago it would have been unimaginable to use emojis and meme pictures in a business context. As a generation of users grows up with short-form video, its expressive features may similarly be adopted by great products in all kinds of verticals. Even plain old business meetings could benefit from tightly edited video summaries, especially if the editor could incorporate useful context from outside the meeting recording.&lt;/p&gt;

&lt;p&gt;Meanwhile, we see at Daily that our customers often have a problem of abundance. They have the raw materials for interesting video content, but at massive volume. They can record down to the level of individual camera streams in their Daily-based applications, but nobody has the time or attention span to watch all those raw recordings. In the mining business they would call this a “low-grade ore" — there’s gold somewhere in this mountain of video, but extracting it isn’t trivial!&lt;/p&gt;

&lt;p&gt;Could supply meet demand, and the mass of raw video be refined into striking short-form content? Clearly this problem can’t be solved by throwing more human effort at it. A modern gold mine can’t operate with the manual pans and sluice boxes from the days of the California gold rush. Likewise, the amount of video content that flows through real-time applications on Daily can’t realistically be edited by professionals sitting at Adobe Premiere workstations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mine, machine minds
&lt;/h2&gt;

&lt;p&gt;Enter AI. Could we send a video editor robot down the audiovisual mine shaft to extract the short-form gold?&lt;/p&gt;

&lt;p&gt;As we’ve seen over the past few years, large AI models have reached truly impressive capabilities in language processing as well as image recognition and generation. Creating high-quality textual summaries and relevant illustrations is practically a solved problem, which is why AI already has so many applications for traditional social media content.&lt;/p&gt;

&lt;p&gt;But short-form video is a bit different because it tends to use multiple media formats in a dense time-based arrangement. The vocabulary available to a video editor is quite rich: there are cuts, text overlays, infographics, transitions, sound effects, and so on. Ideally these all work in unison to create a cohesive, compelling experience less than a minute long.&lt;/p&gt;

&lt;p&gt;A large language model (LLM) like &lt;a href="https://openai.com/chatgpt"&gt;ChatGPT&lt;/a&gt; operates on language tokens (fragments of words). An image diffusion model like &lt;a href="https://openai.com/dall-e-3"&gt;Dall-E&lt;/a&gt; or &lt;a href="https://www.midjourney.com/home/?callbackUrl=%2Fapp%2F"&gt;Midjourney&lt;/a&gt; operates on pixels. An audio model like &lt;a href="https://openai.com/research/whisper"&gt;Whisper&lt;/a&gt; operates on voice samples. We have some bridges that map between these distinct worlds so that images turn into words and vice versa, but a true multimedia model that could natively understand time-based audiovisual streams and also the underlying creative vocabulary needed to produce editing structures remains out of reach for now.&lt;/p&gt;

&lt;p&gt;Waiting another decade for more generalized AI might be an option if you’re very patient. But if you’re looking to get a product advantage now, we have to come up with something else.&lt;/p&gt;

&lt;h2&gt;
  
  
  CutBot
&lt;/h2&gt;

&lt;p&gt;The solution we’ve been working on at Daily is to break down the problem of video editing into a conversation of models with distinct and opposing capabilities.&lt;/p&gt;

&lt;p&gt;We’re developing an experimental system called CutBot. It’s similar in design to a traditional kind of expert system, but it commands the vocabulary of video editing. (“Expert system” is an older form of AI modeling where a workflow is manually codified into a program. In many ways it’s the opposite approach to most present-day AI that uses tons of training data to create very large and opaque models.)&lt;/p&gt;

&lt;p&gt;CutBot knows enough about the context of the video that it can apply a sequence of generally mechanical steps to produce a short-form reel whose superficial features tick the right boxes—rapid cuts, graphics overlays with appropriate progressive variation over time, and so on. What this system lacks is any kind of creative agency.&lt;/p&gt;

&lt;p&gt;For that, CutBot has access to a high-powered LLM which can make creative decisions when provided enough knowledge about the source footage in a text format. We can use voice recognition and image recognition to create those textual representations. CutBot can also share further context derived from application-specific data. (In the next section we’ll see a practical example of what this can be.)&lt;/p&gt;

&lt;p&gt;In this partnership of systems, the LLM is like the director of the reel, bringing its experience and commercial taste so it can answer opinion-based questions in the vein of: “What’s relevant here?” and “What’s fun about this?”&lt;/p&gt;

&lt;p&gt;On the other side of the table, CutBot plays the complementary role of an editing technician who knows just enough so it can pose the right questions to the director, then turn those opinions into specific data that will produce the output videos. Note the plural—since we’re doing fully automated editing, it becomes easy to render many variations based on the same core of creative decisions, for example different form factors such as portrait or landscape video, short and extended cuts like 30 or 60 seconds, branded and non-branded versions, and so on.&lt;/p&gt;

&lt;p&gt;A bit further down we’re going to show some early open source tooling we have developed for this purpose. But first, let’s take a look at a real example from a Daily customer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Highlights from Cloud Poker Night
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://cloudpokernight.com/"&gt;Cloud Poker Night&lt;/a&gt; is a fun and striking new way to play poker. The platform is aimed at a business audience, and it uses Daily to provide a rich social experience. You can see and hear all the other players just like at a real poker table, and a professional dealer keeps the game flowing.&lt;/p&gt;

&lt;p&gt;Short highlight reels would be a great way for players to share the experiences they had on Cloud Poker Night. If these could be generated automatically, they could even be made player-specific, so that you can share your own best moments with friends on social media. What makes this quite difficult is that the game is so rich and carries so many simultaneous data streams — it wouldn’t be an easy task even for a human video editor.&lt;/p&gt;

&lt;p&gt;In the image below, on the left-hand side is the Cloud Poker Night game interface as it appears to a player. On the right-hand side, we have a glimpse into the underlying raw data that is used to build up the game’s visuals:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--mO29SmKL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/he8gvtqmhpk4hubioid3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--mO29SmKL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/he8gvtqmhpk4hubioid3.png" alt="Screenshot of Cloud Poker Night player interface on the left and underlying video and JSON data components on the right" width="800" height="290"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a great example of an application that has very interesting raw materials for short-form video, but the effort of producing reels manually is too great to even consider. A game session can last for an hour. There can be a dozen or more players across two tables. That would be a lot of video tracks for a human to watch.&lt;/p&gt;

&lt;p&gt;In addition to these raw AV streams, there are also the game events like winning hands, emoji reactions sent by players, etc. A human editor would probably just watch the video tracks and try to figure out what was interesting in the game, but a computer can use the game event data and players’ emoji reactions to hone its attention onto interesting stuff more directly.&lt;/p&gt;

&lt;p&gt;So we take these inputs and use a first stage of AI processing to create textual representations. Then the “partnership of systems” described in the previous section — CutBot and LLM — makes a series of editing decisions to produce a cut for the highlight reel:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--8xqodgMK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mc81y6eaog6i6j7vwzcb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--8xqodgMK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mc81y6eaog6i6j7vwzcb.png" alt="Raw data components being transcribed on the left. Short-form video output on the right." width="800" height="290"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once we have the cut, it’s not yet set in stone — or rather, fixed in pixels. The output from the AI is an intermediate representation that remains “responsive” in the sense that we can render into multiple configurations. For example, having both portrait and landscape versions of the same reel can be useful for better reach across social media platforms:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--YI8eK0Fp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7exorpkrpfx4hleqs026.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YI8eK0Fp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7exorpkrpfx4hleqs026.png" alt="Side by side view of a short-form video screenshots in portrait and landscape modes." width="800" height="290"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We’ll discuss the specifics that enable this kind of responsive visual design in a minute. Before we jump to the rendering side, let’s pull together the threads of AI processing. What we’ve shown here with reel generation from Cloud Poker Night’s raw data is a prototype. How could this be deployed for a real-world application, and how would users access this functionality?&lt;/p&gt;

&lt;h2&gt;
  
  
  User interface considerations
&lt;/h2&gt;

&lt;p&gt;CutBot isn’t a fully generalized AI. It’s a combination of the “dumb but technically qualified” editing-specific expert system and the “smart but loose” generic LLM. To some extent, the heuristics used by the expert system need to be tuned for each application. We’re still in the early stages of making this process more accessible to developers.&lt;/p&gt;

&lt;p&gt;For the Cloud Poker Night example shown above, the tuning was done manually. The demo was programmed with some details of the poker application and the desired visual vocabulary for the video. Partially this was done using what could be called “low-code” methods. For example, the visual templates for the overlay graphics in the Cloud Poker Night clip were designed in the &lt;a href="https://www.daily.co/tools/vcs-simulator/daily_baseline.html"&gt;VCS Simulator&lt;/a&gt; using its GUI. It has a feature that outputs code blocks ready for use in Daily’s compositing system. Those code blocks can simply be copied over into the CutBot’s repertoire of graphics.&lt;/p&gt;

&lt;p&gt;For some applications, the video generation process can be fully automated. This is a valid approach when you want to deliver the short-form videos as soon as possible, as would be the case for Cloud Poker Night. You’d usually want to enable players to share their highlight reels immediately after the game, and it doesn’t matter if the reels are 100% polished.&lt;/p&gt;

&lt;p&gt;The other possible approach is to have a human in the loop providing input to the process. This means building a user interface that presents options from CutBot and lets the user make choices. For most users, this wouldn’t need to resemble the traditional video editing interface—the kind where you’re manually slipping tracks and trimming clips with sub-second precision. The paradigm here can be inverted because the cut is primarily created in conversation with the LLM-as-director. The end-user UI can reflect that reality and operate on higher-level content blocks and associated guidance.&lt;/p&gt;

&lt;h2&gt;
  
  
  VCSRender
&lt;/h2&gt;

&lt;p&gt;What happens when CutBot has produced a cut — how does it turn into the actual AV artifacts, one or more output videos?&lt;/p&gt;

&lt;p&gt;We mentioned before that producing multiple form factors and durations from the same AI-designed cut is a common requirement. For this reason, it’s not desirable to have the output from CutBot be precisely nailed down so that every visual layer is expressed in absolute display coordinates and animation keyframe timings. Instead, we’d like to have an intermediate format which represents the high-level rendering intent, but leaves the details of producing each frame to a smart compositor that gets executed for each output format.&lt;/p&gt;

&lt;p&gt;As it happens, we already have such a thing at Daily — and it’s open source to boot. Our smart compositor engine is called &lt;a href="https://www.daily.co/blog/new-beta-dailys-video-component-system/"&gt;VCS&lt;/a&gt;, the Video Component System.&lt;/p&gt;

&lt;p&gt;VCS is based on &lt;a href="https://react.dev/"&gt;React&lt;/a&gt;, so you can express dynamic and stateful rendering logic with the same tools that are familiar to front-end developers everywhere. This makes it a great fit for the compositor we need to generate videos from CutBot.&lt;/p&gt;

&lt;p&gt;Until now we’ve provided two renderers for VCS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A server-side engine for live streaming and recording. It runs on Daily’s cloud and operates on live video inputs. You can access it via &lt;a href="https://docs.daily.co/guides/products/live-streaming-recording/vcs"&gt;Daily’s API&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;A client-side engine that runs directly in the web browser. It operates on video inputs from a Daily room, or &lt;code&gt;MediaStream&lt;/code&gt; instances provided by your application. This library is open source. (You can &lt;a href="https://docs.daily.co/reference/vcs/tools/build-a-js-package"&gt;create your own JS bundles directly&lt;/a&gt;, and we’re also working on a renderer package that will make it much easier to use the VCS engine in your Daily-using app.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We’ll be adding a third renderer called VCSRender that’s specifically designed for batch rendering situations like these short-form videos.&lt;/p&gt;

&lt;p&gt;VCSRender is an open-source CLI tool and library that produces videos from inputs like video clips, graphics, and VCS events that drive the composition. It’s a lightweight program with few dependencies, and it’s designed to adhere to the “Unix way” where possible. This means all non-essential tasks like encoding and decoding media files are left to existing programs like &lt;a href="https://ffmpeg.org/"&gt;FFmpeg&lt;/a&gt; which already perform these admirably. This fine-grained division of labor makes it easier to deploy VCSRender in modern “serverless” style cloud architectures where swarms of small workers can be rapidly summoned when scale is needed, but no resources are consumed when usage is low.&lt;/p&gt;

&lt;h2&gt;
  
  
  VCSCut
&lt;/h2&gt;

&lt;p&gt;VCSRender discussed above is fundamentally a low-level tool. To provide a more manageable interface to this system, we’re also releasing a program called VCSCut. It defines the “cut” file format which is produced by our CutBot AI. These cut files are expressed in a simple JSON format so it’s easily possible to create them with other tooling or even by manual editing.&lt;/p&gt;

&lt;p&gt;VCSCut is really a script that orchestrates the following open source components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FFmpeg for decoding and encoding&lt;/li&gt;
&lt;li&gt;VCS batch runner for executing and capturing the VCS React state&lt;/li&gt;
&lt;li&gt;VCSRender for final compositing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Together these provide a rendering environment that’s well suited for short-form video. In a cloud environment, you could even split these tasks onto separate cloud functions orchestrated by the VCSCut script, with something like AWS S3 providing intermediate storage. This kind of design can be cost-effective for variable workloads because you don’t need to keep a pool of workers running so there’s no expense when the system isn’t used, but it still can scale up very rapidly.&lt;/p&gt;

&lt;p&gt;It’s worth emphasizing that the VCSCut+VCSRender combo is in early alpha. It’s tuned for short-form video and is not a solution for every video rendering need. For long videos and real-time streaming sessions, we internally use &lt;a href="https://gstreamer.freedesktop.org/"&gt;GStreamer&lt;/a&gt;, an extremely mature solution with a different set of trade-offs. (There’s an interesting story to tell about how VCS works with GStreamer, but that will have to wait for another day!)&lt;/p&gt;

&lt;p&gt;The alpha release will be part of the &lt;a href="https://github.com/daily-co/daily-vcs"&gt;&lt;code&gt;daily-vcs&lt;/code&gt; repo on Github&lt;/a&gt;. You can watch and star the repo if you want to stay up to date on the code directly! We’ll be talking about it on our blog and social media channels too. (And you can refer to &lt;a href="https://docs.daily.co/"&gt;our reference documentation&lt;/a&gt; for more information about VCS and Daily’s recording features.)&lt;/p&gt;

&lt;p&gt;We are always excited to discuss these topics — you can find us on social media; join us on the &lt;a href="https://community.daily.co/"&gt;peerConnection&lt;/a&gt; WebRTC forum; or find us online or IRL at one of the &lt;a href="https://www.daily.co/resources/"&gt;events&lt;/a&gt; we host.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>react</category>
      <category>socialmedia</category>
    </item>
    <item>
      <title>How to talk to an LLM (with your voice)</title>
      <dc:creator>Tasha</dc:creator>
      <pubDate>Thu, 05 Oct 2023 18:46:59 +0000</pubDate>
      <link>https://forem.com/trydaily/how-to-talk-to-an-llm-with-your-voice-533l</link>
      <guid>https://forem.com/trydaily/how-to-talk-to-an-llm-with-your-voice-533l</guid>
      <description>&lt;p&gt;&lt;em&gt;By &lt;a href="https://www.daily.co/blog/author/kwindla-hultman-kramer/" rel="noopener noreferrer"&gt;Kwindla Hultman Kramer&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Daily’s developer platform powers audio and video experiences for millions of people all over the world. Our customers are developers who use our APIs and client SDKs to build audio and video features into applications and websites.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Today, as part of &lt;a href="https://www.daily.co/blog/ai-week-at-daily-2023-edition/" rel="noopener noreferrer"&gt;AI Week&lt;/a&gt;, we’re talking about building voice-driven AI applications.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Our kickoff post for AI Week goes into more detail about what topics we’re diving into and how we think about the potential of combining WebRTC, video and audio, and AI. Feel free to click over and &lt;a href="https://www.daily.co/blog/ai-week-at-daily-2023-edition/" rel="noopener noreferrer"&gt;read that intro&lt;/a&gt; before (or after) reading this post.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Lately, we’ve been doing a lot of experimenting and development with the newest AI large language models (LLMs). We’ve shipped &lt;a href="https://www.daily.co/blog/the-technology-behind-ai-powered-clinical-notes/" rel="noopener noreferrer"&gt;products that use GPT-4&lt;/a&gt;. We’ve helped customers build features that leverage several kinds of smaller, specialized models. We’ve developed strong opinions about which architectures work best for which use cases. And we have strong opinions about how to wire up real-time audio and video streams bidirectionally to AI tools and services.&lt;/p&gt;

&lt;p&gt;We built a little demo of an LLM that tells you a choose-your-own-adventure story, alongside DALL-E generative art. The demo is really, really fun. Feel free to &lt;a href="https://www.daily.co/talk-to-llm" rel="noopener noreferrer"&gt;go play with it now&lt;/a&gt; and talk with the LLM to generate a story!&lt;/p&gt;

&lt;p&gt;If you have such a good time that you don’t make it back here, just remember these three things for when you start building your own voice-driven LLM app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run everything in the cloud (if you can afford to)&lt;/li&gt;
&lt;li&gt;Don’t use web sockets for audio or video transport (if you can avoid them)&lt;/li&gt;
&lt;li&gt;Squeeze every bit of latency you can out of your data flow (because users don’t like to wait)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The demo is built on top of our &lt;a href="https://github.com/daily-co/daily-python" rel="noopener noreferrer"&gt;&lt;code&gt;daily-python&lt;/code&gt; SDK&lt;/a&gt;. Using the demo as a starting point, it should be easy to stand up a voice-driven LLM app on any cloud provider. If you’re just here for sample code (you know who you are) just scroll to the bottom of this post.&lt;/p&gt;

&lt;p&gt;If you’re more in a lean-back mode right now, you can also watch this video that Annie made, walking through the demo. (In her adventure, the LLM tells a story about a brave girl who embarks on a quest — aided by a powerful night griffin — to free her cursed kingdom.)&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/j41Z3ZnatAI"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;In this post, we'll take a look at the architecture and work behind the &lt;a href="https://www.daily.co/talk-to-llm" rel="noopener noreferrer"&gt;demo&lt;/a&gt;, covering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;3 components to build an LLM application&lt;/li&gt;
&lt;li&gt;data flow&lt;/li&gt;
&lt;li&gt;speech-to-text: client-side or in the cloud?&lt;/li&gt;
&lt;li&gt;web sockets or WebRTC&lt;/li&gt;
&lt;li&gt;where to run your app's speech-to-LLM-to-speech logic&lt;/li&gt;
&lt;li&gt;phrase detection and endpointing&lt;/li&gt;
&lt;li&gt;LLM-prompting APIs, and streaming the response data&lt;/li&gt;
&lt;li&gt;natural sounding speech synthesis&lt;/li&gt;
&lt;li&gt;how hard can audio buffer management be?&lt;/li&gt;
&lt;li&gt;using &lt;code&gt;llm-talk&lt;/code&gt; to build voice-driven LLM apps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's dive in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frontier LLMs are good conversationalists
&lt;/h2&gt;

&lt;p&gt;There’s a lot of really cool stuff that today’s newest, most powerful, “frontier” large language models can do. They are good at taking unstructured text and extracting, summarizing, and structuring it. They’re good at answering questions, especially when augmented with a knowledge base that’s relevant to the question domain. And they’re surprisingly good conversationalists!&lt;/p&gt;

&lt;p&gt;At Daily, we’re seeing a lot of interesting use cases for conversational AI: teaching and tutoring, language learning, speech-to-speech translation, interview prep, and a whole host of experiments with creative chatbots and interactive games.&lt;/p&gt;

&lt;p&gt;You need three basic components to get started building an LLM application:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Speech-to-text (abbreviated as STT, and also called transcription or automatic speech recognition)&lt;/li&gt;
&lt;li&gt;Text-to-speech (abbreviated as TTS, and also called voice synthesis)&lt;/li&gt;
&lt;li&gt;The LLM itself&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Interestingly, all of these are “AI.” The underlying technology for state- of-the-art speech-to-text models, text-to-speech models, and large language models are quite similar.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data flow
&lt;/h3&gt;

&lt;p&gt;Here are the processing and networking steps common to every speech-to-speech AI app.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3ctitxuvsmssuyqgj943.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3ctitxuvsmssuyqgj943.png" alt="data flow diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Voice capture&lt;/li&gt;
&lt;li&gt;Speech-to-text&lt;/li&gt;
&lt;li&gt;Text input to LLMs (and possibly other AI tools and services)&lt;/li&gt;
&lt;li&gt;Text-to-speech&lt;/li&gt;
&lt;li&gt;Audio playout&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These are the low-level, generic building blocks that you’ll need. All the specific design and logic that makes an application unique — how the UX is designed, how you prompt the LLM, the workflow, unique data sets, etc — are built on top of these basic components.  &lt;/p&gt;

&lt;h3&gt;
  
  
  Some considerations (things that can be tricky)
&lt;/h3&gt;

&lt;p&gt;You can hack together a speech-to-speech app prototype pretty quickly, these days. There are lots of easy-to-use libraries for speech recognition, networking, and voice synthesis.&lt;/p&gt;

&lt;p&gt;If you’re building a production application, though, there are a few things that can take a lot of time to debug and optimize. There are also some easy-to-get-started-with approaches that are likely to be dead-ends for production apps at scale.&lt;/p&gt;

&lt;p&gt;Let’s talk about a few choices and tradeoffs, and a few things that we’ve seen trip people up as they move from prototyping into production and scaling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Speech-to-text – client-side or in the cloud?
&lt;/h3&gt;

&lt;p&gt;Your first technology decision is whether to run speech-to-text locally on each user’s device, or whether to run it in the cloud. Local speech recognition has gotten pretty good. Running locally doesn’t cost any money, whereas using a cloud STT service does.&lt;/p&gt;

&lt;p&gt;But local speech recognition today is not nearly as fast or as accurate as the best STT models running on big, optimized, cloud infrastructure. For many applications, speech-to-text that is both as fast as possible and as accurate as possible is make-or-break. One way to think about this is that the output of speech recognition is the input to the LLM. The better the input to the LLM, the better the LLM can perform.&lt;/p&gt;

&lt;p&gt;For both speed and accuracy, we recommend &lt;a href="https://deepgram.com/" rel="noopener noreferrer"&gt;Deepgram&lt;/a&gt;. We benchmark all of the major speech services regularly. Deepgram consistently wins on both speed and accuracy. Their models are also tunable if you have a specific vocabulary or use case that you need to hyper-optimize for. Because Deepgram is so good, we’ve built a tightly coupled Deepgram integration into our &lt;a href="https://www.daily.co/blog/global-mesh-network/" rel="noopener noreferrer"&gt;SFU codebase&lt;/a&gt;, which helps just a little bit more in reducing latency.&lt;/p&gt;

&lt;p&gt;Azure, GCP, and AWS all have invested recently in improving their ASR (Automatic Speech Recognition) offerings. So there are several other good options if for some reason you can’t use Deepgram.&lt;/p&gt;

&lt;h3&gt;
  
  
  Web sockets or WebRTC?
&lt;/h3&gt;

&lt;p&gt;Getting audio from a user’s device to the cloud means compressing it, sending it over a network channel to your speech-to-text service of choice, and then receiving the text somewhere you can process it.&lt;/p&gt;

&lt;p&gt;One option for sending audio over the internet is to use web sockets. Web sockets are widely supported by both front-end and back-end development frameworks and generally work fine for audio streaming when network conditions are ideal.&lt;/p&gt;

&lt;p&gt;But web socket connections will run into problems streaming audio under less-than-ideal network conditions. In production, a large percentage of users have less-than-ideal networks!&lt;/p&gt;

&lt;p&gt;Web sockets are an abstraction built on top of TCP, which is a relatively complex networking layer designed to deliver every single data packet as reliably as possible. Which sounds like a good thing, but turns out not to be when real-time performance is the priority. If you need to stream packets at very low latency, it’s better to use a faster, simpler network protocol called UDP.&lt;/p&gt;

&lt;p&gt;These days, the best choice for real-time audio is a protocol called WebRTC. WebRTC is built on top of UDP and was designed from the ground up for real-time media streaming. It works great in web browsers, in native desktop apps, and on mobile; and its standout feature is that it’s good at delivering audio and video at real-time latency across a very wide range of real-world network connections.&lt;/p&gt;

&lt;p&gt;Some things that you get with WebRTC that are difficult or impossible to implement on top of web sockets include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Selectively resending dropped packets based on whether they’re likely to arrive in time to be inserted into the playout stream&lt;/li&gt;
&lt;li&gt;Dynamic feedback from the network layer to the encoding layer of the media stack, so the encoding bitrate can adapt on the fly to changing network conditions&lt;/li&gt;
&lt;li&gt;Hooks for the &lt;a href="https://www.w3.org/TR/webrtc-stats/" rel="noopener noreferrer"&gt;monitoring and observability&lt;/a&gt; that you need in order to understand the overall performance of media delivery to your end users&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffxa7dygr9p0u3u7ku0j4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffxa7dygr9p0u3u7ku0j4.png" alt="web socket and WebRTC diagrams"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There’s a lot of really cool stuff to talk about in this domain. If you are interested in reading more about the trade-offs involved in designing protocols for real-time media streaming, see our in-depth &lt;a href="https://www.daily.co/blog/video-live-streaming/" rel="noopener noreferrer"&gt;post about WebRTC, HLS, and RTMP&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;WebRTC is a significant improvement over web sockets for audio. Using WebRTC is a &lt;em&gt;necessity&lt;/em&gt; for video.&lt;/p&gt;

&lt;p&gt;Video uses much more network bandwidth than audio, so dropped and delayed packets are more frequent. Video codecs also can’t mitigate packet loss as well as audio codecs can. (Modern audio codecs use &lt;a href="https://www.rfc-editor.org/rfc/rfc8627.html" rel="noopener noreferrer"&gt;nifty math&lt;/a&gt; and can be quite robust in the face of partial data loss). But WebRTC’s bandwidth adaptation features enable reliable video delivery on almost any network. &lt;/p&gt;

&lt;p&gt;WebRTC also makes it easy to include &lt;a href="https://www.daily.co/blog/simulcast/" rel="noopener noreferrer"&gt;multiple users&lt;/a&gt; in a real-time session. So, for example, you can invite our storybot demo to join a video call between a child and her grandparents.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsuwt9jneapqmkg516dx1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsuwt9jneapqmkg516dx1.png" alt="three clients connected to a WebRTC server"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Where to run your app’s speech-to-LLM-to-speech logic
&lt;/h3&gt;

&lt;p&gt;Should you run the part of your application logic that glues together speech-to-text, an LLM, and text-to-speech locally or in the cloud?&lt;/p&gt;

&lt;p&gt;Running everything locally is nice. You’re just writing one program (the code running on the user’s device). The downside of running everything locally is that for each piece of the conversation between the user and the LLM, you are making three network connections from the user’s device to the cloud. &lt;/p&gt;

&lt;p&gt;The alternative is to move some of your application logic into the cloud.&lt;/p&gt;

&lt;p&gt;Here are two diagrams showing the difference.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzfka4h9m6qo0ex50m3c7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzfka4h9m6qo0ex50m3c7.png" alt="running locally vs. in the cloud"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In general, moving part of your application code into the cloud will improve reliability and lower latency.&lt;/p&gt;

&lt;p&gt;This is especially true for users with not-great network connections. And it’s &lt;em&gt;especially&lt;/em&gt;, especially true if the user is in a different geographic region from the infrastructure that your services are running on.&lt;/p&gt;

&lt;p&gt;We have a lot of data on this kind of thing at Daily, and the performance improvements that come from doing as few “first-mile” round trips as possible varies a great deal between different user cohorts and geographies. But for an average user in the US, the six network connection arrows in the left diagram would typically add up to a total latency of about 90ms. For the same average user, the eight network connection arrows in the right diagram would typically add up to about 55ms.&lt;/p&gt;

&lt;p&gt;Moving some of your application logic into the cloud has other benefits in addition to reducing total latency. You control the compute and network available to your app in the cloud. So you can do things in the cloud that you simple can’t do locally. For example, you can run your own private LLM – say, the very capable &lt;a href="https://docs.cerebrium.ai/cerebrium/prebuilt-models/language-models/llamav2" rel="noopener noreferrer"&gt;Llama2 70B&lt;/a&gt; –  inside the app logic box in the right diagram!&lt;/p&gt;

&lt;h3&gt;
  
  
  Phrase detection and endpointing
&lt;/h3&gt;

&lt;p&gt;Once voice data is flowing through your speech-to-text engine, you’ll have to figure out when to collect the output text and prompt your LLM. In the academic literature, various aspects of this problem are referred to as “phrase detection,” “speech segmentation,” and “endpointing.” (The fact that there is academic literature about this is a clue that it’s a non-trivial problem.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbk47uo1baew653ns1d52.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbk47uo1baew653ns1d52.png" alt="phrase detection"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most production speech recognition systems use pauses (the technical term is, “voice activity detection”), plus some semantic analysis, to identify phrases. &lt;/p&gt;

&lt;p&gt;Sometimes humans pause … and then keep talking. (This is called "barge-in.") To handle that case, it’s useful to build logic into your app that allows you to interrupt the LLM query or the text to speech output and restart the data flow. &lt;/p&gt;

&lt;p&gt;Note that in the demo app, we have a hook for implementing barge-in, but it's not actually wired up yet. We're using a combination of client- and server-side logic to manage conversation flow. On the client side, we're using JavaScript audio APIs to monitor the input level of the microphone to determine when the user has started and stopped talking. On the server side, Deepgram's transcription includes endpointing information in the form of end-of-sentence punctuation. We use both of those signals to determine when the user is done talking, so we can play the chime sound and continue the story.&lt;/p&gt;

&lt;h3&gt;
  
  
  LLM prompting APIs, and streaming the response data
&lt;/h3&gt;

&lt;p&gt;A subtle, but important, part of using today’s large language models is that the APIs and best practices for usage differ for different services and even for different model versions released by the same company.&lt;/p&gt;

&lt;p&gt;For example, GPT-4’s &lt;a href="https://platform.openai.com/docs/api-reference/chat" rel="noopener noreferrer"&gt;chat completion API&lt;/a&gt; expects a list of &lt;code&gt;messages&lt;/code&gt;, starting with a system prompt and then alternating between “user” and “assistant” (language model) messages.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

messages=[
        {"role": "system", "content": "You are a storyteller who loves to make up fantastic, fun, and educational stories for children between the ages of 5 and 10 years old. Your stories are full of friendly, magical creatures. Your stories are never scary. Stop every few sentences and give the child a choice to make that will influence the next part of the story. Begin by asking what a child wants you to tell a story about. Then stop and wait for the answer.."},
        {"role": "assistant", "content": ""What would you like me to tell a story about? Would you like a fascinating story about brave fairies, a happy tale about a group of talking animals, or an adventurous journey with a mischievous young dragon?""},
        {"role": "user", "content": "Talking animals!"}
    ]


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;We’ve often seen developers who are using the GPT-4 API for the first time put their entire prompt into a single “user” message. But it’s frequently possible to get better results using a “system” prompt and a series of &lt;code&gt;messages&lt;/code&gt; entries, especially for multi-turn interactions.&lt;/p&gt;

&lt;p&gt;Using a “system” prompt also helps maintain the predictability, tone, and safety of the LLM’s output. This technology is so new that we don’t yet know how to make sure that LLMs stay on task (as defined by the application developer). For some background, see this &lt;a href="https://simonwillison.net/2023/Aug/3/weird-world-of-llms/#prompt-injection" rel="noopener noreferrer"&gt;excellent overview&lt;/a&gt; by Simon Willison.&lt;/p&gt;

&lt;p&gt;To keep latency low, it’s important to use any &lt;a href="https://platform.openai.com/docs/api-reference/chat/streaming" rel="noopener noreferrer"&gt;streaming mode&lt;/a&gt; configurations and optimizations that your service supports. You’ll want to write your response handling code to process the data as individual, small chunks, so that you can start streaming to your text-to-speech service as quickly as possible.&lt;/p&gt;

&lt;p&gt;It's also worth thinking about ways you can actually use the capabilities of the LLM to your advantage. LLMs are good at outputting structured and semi-structured text. In our demo code, we're instructing the LLM to insert a break word between the "pages" of the story. When we see that break word as we're processing the streaming response from the LLM, we know we can send that portion of the story to TTS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Natural-sounding speech synthesis
&lt;/h3&gt;

&lt;p&gt;As is the case with text-to-speech, you &lt;em&gt;can&lt;/em&gt; run speech-to-text locally on a user’s computer or phone, but you probably don’t want to. State of the art, cloud TTS services produce output that sounds significantly more natural and less robotic than the best local models, today.&lt;/p&gt;

&lt;p&gt;There are several good options for cloud TTS. We usually recommend &lt;a href="https://learn.microsoft.com/en-us/azure/ai-services/speech-service/overview" rel="noopener noreferrer"&gt;Azure Speech&lt;/a&gt; or &lt;a href="https://elevenlabs.io/" rel="noopener noreferrer"&gt;Elevenlabs&lt;/a&gt;. The landscape is evolving rapidly, though, and there are frequent and exciting updates and new contenders.&lt;/p&gt;

&lt;p&gt;GPT-4 and other top-performing LLMs are good at streaming their completion data at a consistent rate, without big delays in the middle of a response. (And if you’re running your processing code in the cloud, network-related delays will be very rare.) So for many applications, you can just feed the streaming data directly from your LLM into your text- to-speech API.&lt;/p&gt;

&lt;p&gt;But if you are filtering or processing the text from the LLM, before sending it to TTS, you should think about whether your processing can create temporal gaps in the data stream. Any gaps longer than a couple hundred milliseconds will be audible as gaps in the speech output.&lt;/p&gt;

&lt;h3&gt;
  
  
  How hard can audio buffer management be?
&lt;/h3&gt;

&lt;p&gt;Finally, once you have an audio stream coming back from your text to speech service, you’ll need to send that audio over the network. This involves taking the incoming samples, possibly stripping out header metadata, then shuffling the samples into memory buffers you can hand over to your low-level audio encoding and network library.&lt;/p&gt;

&lt;p&gt;Here, I’ll pause to note that I’ve written code that copies audio samples from one data structure in memory to some other data structure in memory at least a couple of hundred times during my career and in at least half a dozen different programming languages.&lt;/p&gt;

&lt;p&gt;Yet still, when I write code like this from scratch, there’s usually at least one head-scratching bug I have to spend time tracking down. I get the length of the header that I’m stripping out wrong. My code works on macOS but initially fails in a Linux VM because I didn’t think about endianness. I don’t know the Python threading internals well enough to avoid buffer underruns. Sample rate mismatches. Etc etc.&lt;/p&gt;

&lt;p&gt;Sadly, GPT-4 code interpreter and Github Copilot don’t yet just write bug free audio buffer management functions for you. (I know, because I tried with both tools last week. Both are very impressive – they can provide guidance and code snippets in a way that I would not have believed possible a few months ago. But neither produced production ready code even with some iterative prompting.)&lt;/p&gt;

&lt;p&gt;Which brings us to the framework code we wrote to help make all of the tricky things described above a little bit easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using our &lt;code&gt;llm-talk&lt;/code&gt; sample code to build voice-driven LLM apps
&lt;/h2&gt;

&lt;p&gt;We’ve posted &lt;a href="https://github.com/daily-demos/llm-talk" rel="noopener noreferrer"&gt;the source code for this demo&lt;/a&gt; in a github repo called &lt;code&gt;llm-talk&lt;/code&gt;. The repo includes both a bunch of useful sample code for building a voice-driven app, and orchestrator framework code that tries to abstract away a lot of the low-level functionality common to most voice-driven and speech-to-speech LLM apps.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All this code is written on top of our &lt;a href="https://github.com/daily-co/daily-python" rel="noopener noreferrer"&gt;&lt;code&gt;daily-python&lt;/code&gt; SDK&lt;/a&gt; and leverages Daily’s &lt;a href="https://www.daily.co/blog/global-mesh-network/" rel="noopener noreferrer"&gt;global WebRTC infrastructure&lt;/a&gt; to give you very low-latency connectivity most places around the world&lt;/li&gt;
&lt;li&gt;Deepgram speech to text capabilities are integrated into Daily’s infrastructure&lt;/li&gt;
&lt;li&gt;Probable phrase endpoints are marked in the speech-to-text event stream&lt;/li&gt;
&lt;li&gt;Orchestrator methods include support for restarting both LLM inference and speech synthesis&lt;/li&gt;
&lt;li&gt;The library handles audio buffer management (and threading) for you
Here’s some sample code from &lt;code&gt;daily-llm.py&lt;/code&gt;, showing how we're joining a Daily call and listening for transcription events.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

def configure_daily(self):
    Daily.init()
    self.client = CallClient(event_handler = self)

    self.mic = Daily.create_microphone_device("mic", sample_rate = 16000, channels = 1)
    self.speaker = Daily.create_speaker_device("speaker", sample_rate = 16000, channels = 1)
    self.camera = Daily.create_camera_device("camera", width = 512, height = 512, color_format="RGB")

    self.client.set_user_name(self.bot_name)
    self.client.join(self.room_url, self.token)

    self.my_participant_id = self.client.participants()['local']['id']
    self.client.start_transcription()

def on_participant_joined(self, participant):
    self.client.send_app_message({ "event": "story-id", "storyID": self.story_id})
    self.wave()
    time.sleep(2)

    # don't run intro question for newcomers
    if not self.story_started:
        #self.orchestrator.request_intro()
        self.orchestrator.action()
        self.story_started = True

def on_transcription_message(self, message):
    if message['session_id'] != self.my_participant_id:
        if self.orchestrator.started_listening_at:
            self.transcription += f" {message['text']}"
            if re.search(r'[\.\!\?]$', self.transcription):
                print(f"✏️ Sending reply: {self.transcription}")
                self.orchestrator.handle_user_speech(self.transcription)
                self.transcription = ""
            else:
                print(f"✏️ Got a transcription fragment, waiting for complete sentence: \"{self.transcription}\"")


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Here’s what the flow of data in the storybot demo looks like.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fns1z7w236j7wb5kqx0pd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fns1z7w236j7wb5kqx0pd.png" alt="storybot demo data flow"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our goal is for the &lt;code&gt;llm-talk&lt;/code&gt; code to be a complete starting point for your apps, and for the orchestrator layer to take care of all the low-level glue, so that you can focus on writing the actual application that you’re imagining! As usually happens with a project like this, we've accumulated a pretty good backlog of improvements we want to make and extensions we want to add. And we're committed to supporting voice-driven and speech-to-speech apps. So expect regular updates to this code.&lt;/p&gt;

&lt;p&gt;AI progress is happening very fast. We expect to see a lot of experimentation and innovation around voice interfaces, because the new capabilities of today’s speech recognition, large language model, and speech synthesis technologies are so impressive and complement each other so well.&lt;/p&gt;

&lt;p&gt;Here’s a voice-driven LLM application that I’d like to use: I want to talk – literally – to The New York Times or The Wall Street Journal. I grew up in a household that subscribed to multiple daily newspapers. I love newspapers! But these days I rarely read the paper the old fashioned way. (The San Francisco Chronicle won’t even deliver a physical paper to my house. And I live in a residential neighborhood in San Francisco.)&lt;/p&gt;

&lt;p&gt;Increasingly, I think that individual news stories posted to the web or embedded in an app feel like they’re an old format, incompletely ported to a new platform. Last month, I wanted to ask the New York Times about the ARM IPO and get a short, current summary about what had happened on the first day of trading, then follow up with my own questions.&lt;/p&gt;

&lt;p&gt;The collective knowledge of the Times beat reporters and the paper’s decades-spanning archive are both truly amazing. The talking heads shows on TV kind of do what I want, but with the show hosts as a proxy for me and a Times reporter as a proxy for the New York Times. I don’t want anyone to proxy for me; I want to ask questions myself. An LLM could be a gateway providing nonlinear access to the treasure trove that is &lt;a href="https://chat.openai.com/share/48f30da7-9a81-41e2-98dc-78c30a806f18" rel="noopener noreferrer"&gt;The Grey Lady’s&lt;/a&gt; institutional memory. (Also, I want to navigate through this treasure trove conversationally, while cooking dinner or doing laundry.)&lt;/p&gt;

&lt;p&gt;What new, voice-driven applications are you excited about? Our favorite thing at Daily is that we get to see all sorts of amazing things that developers create with the tools we’ve built. If you’ve got an app that uses real-time speech in a new way, or ideas you’re excited about, or questions, please ping us on social media, join us on the &lt;a href="https://community.daily.co/" rel="noopener noreferrer"&gt;peerConnection&lt;/a&gt; WebRTC forum, or find us online or IRL at one of the &lt;a href="https://www.daily.co/resources/events" rel="noopener noreferrer"&gt;events&lt;/a&gt; we host.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webrtc</category>
      <category>llm</category>
      <category>python</category>
    </item>
    <item>
      <title>Cerebrium + Daily: Simplifying deployments for your AI-powered voice and video apps</title>
      <dc:creator>Tasha</dc:creator>
      <pubDate>Tue, 03 Oct 2023 17:29:09 +0000</pubDate>
      <link>https://forem.com/trydaily/cerebrium-daily-simplifying-deployments-for-your-ai-powered-voice-and-video-apps-32ac</link>
      <guid>https://forem.com/trydaily/cerebrium-daily-simplifying-deployments-for-your-ai-powered-voice-and-video-apps-32ac</guid>
      <description>&lt;p&gt;&lt;em&gt;By &lt;a href="https://www.daily.co/blog/author/varun/"&gt;Varun Singh&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Daily’s developer platform powers audio and video experiences for millions of people all over the world. Our customers are developers who use our APIs and &lt;a href="https://www.daily.co/products/video-sdk/"&gt;client SDKs&lt;/a&gt; to build audio and video features into applications and websites.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Our &lt;a href="https://www.daily.co/blog/tag/ai-week-2023/"&gt;AI Week series&lt;/a&gt; looks at how developers can combine real-time video with AI as they build with our platform. Today we're announcing a partnership with &lt;a href="https://www.cerebrium.ai/"&gt;Cerebrium&lt;/a&gt;, a serverless infrastructure platform for training, deploying, and monitoring machine learning models. You can now run &lt;a href="https://docs.daily.co/guides/products/ai-toolkit"&gt;&lt;em&gt;daily-python&lt;/em&gt;&lt;/a&gt; seamlessly as part of a &lt;a href="https://docs.cerebrium.ai/introduction"&gt;Cerebrium application&lt;/a&gt;. You can read more about the &lt;a href="https://www.daily.co/blog/introducing-daily-python-an-sdk-for-ai-powered-interactive-video-and-audio/"&gt;Daily Python SDK&lt;/a&gt; here.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Learn more about the topics we’re covering in this AI Week series, and how we think about the potential of combining WebRTC, video and audio, and AI, in our kickoff post. Feel free to click over and &lt;a href="https://www.daily.co/blog/ai-week-at-daily-2023-edition/"&gt;read that intro&lt;/a&gt; before (or after) reading this post.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;As part of our ongoing AI Week series, we are thrilled to unveil our latest partnership with &lt;a href="https://www.cerebrium.ai/"&gt;Cerebrium&lt;/a&gt; – a serverless deployment option for the &lt;code&gt;daily-python&lt;/code&gt; SDK. Cerebrium is making it easy to run &lt;code&gt;daily-python&lt;/code&gt; alongside hosted AI models in the same container. This development completely eliminates the need of managing underlying infrastructure, giving engineers easy access to hosted ML capabilities along with voice and video streams. &lt;/p&gt;

&lt;h2&gt;
  
  
  Elevate your apps with AI: running &lt;code&gt;daily-python&lt;/code&gt; on  serverless infrastructure
&lt;/h2&gt;

&lt;p&gt;Bringing AI models into the world of voice and video applications at scale can feel daunting, particularly when these AI models require intensive computational resources. Tasks such as object tracking, object segmentation, video analysis, or speech transcription demand the right mix of I/O, memory, CPU, GPU resources to ensure real-time performance.&lt;/p&gt;

&lt;p&gt;With Cerebrium’s serverless deployment, you can sidestep the intricacies of scaling the CPU/GPU and the Kubernetes resources. Plus, you can easily deploy off-the-shelf ML Prebuilt models that are fine-tuned on your data. Cerebrium already offers an extensive library of over 20 &lt;a href="https://docs.cerebrium.ai/cerebrium/prebuilt-models/introduction"&gt;Prebuilt models&lt;/a&gt;. This off-the-shelf deployment already contains a broad range of &lt;a href="https://docs.cerebrium.ai/cerebrium/prebuilt-models/language-models/gpt4all"&gt;LLMs&lt;/a&gt; and generative &lt;a href="https://docs.cerebrium.ai/cerebrium/prebuilt-models/language-models/whisper"&gt;voice&lt;/a&gt; and &lt;a href="https://docs.cerebrium.ai/cerebrium/prebuilt-models/image-models/stable-diffusion-2"&gt;video&lt;/a&gt;, such as: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Llama 2&lt;/li&gt;
&lt;li&gt;GPT4All&lt;/li&gt;
&lt;li&gt;OpenAI’s Whisper&lt;/li&gt;
&lt;li&gt;Meta Seamless&lt;/li&gt;
&lt;li&gt;ControlNet&lt;/li&gt;
&lt;li&gt;Stable Diffusion&lt;/li&gt;
&lt;li&gt;Meta’s Segment Anything&lt;/li&gt;
&lt;li&gt;And more&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--_c2ndfCt--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qrsr43q7jrhwazw87a9n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--_c2ndfCt--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qrsr43q7jrhwazw87a9n.png" alt="Cerebrium's Serveless Architecture with daily-python" width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cerebrium's Serveless Architecture with &lt;code&gt;daily-python&lt;/code&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here is what &lt;code&gt;daily-python&lt;/code&gt; within Cerebrium brings to developers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Real-time media processing&lt;/strong&gt;: the ability to send media from the call to an ML model at up to 15 FPS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quick model inferences&lt;/strong&gt;: receive inferences from the model in 100s of milliseconds, since the model is co-located&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instant action&lt;/strong&gt;: as soon as the inference is received from the model, be able to send the modified audio and video or a notification message into the call
For those eager to dive into practical examples, the Cerebrium engineering team has thoughtfully prepared an &lt;a href="https://github.com/CerebriumAI/daily-demos"&gt;example repository of daily-demos&lt;/a&gt; for you to play with. There are currently three demos:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/CerebriumAI/daily-demos/blob/master/content-moderation/README.md"&gt;&lt;code&gt;content-moderation&lt;/code&gt;&lt;/a&gt; uses  OpenAI's &lt;a href="https://openai.com/research/clip"&gt;CLIP model&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/CerebriumAI/daily-demos/blob/master/pet-object-detection/README.md"&gt;&lt;code&gt;pet-object-detection&lt;/code&gt;&lt;/a&gt; uses a Ultralytics’ &lt;a href="https://docs.ultralytics.com/"&gt;YOLO v8 model&lt;/a&gt; for object detection.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/CerebriumAI/daily-demos/blob/master/transcription/README.md"&gt;&lt;code&gt;transcription&lt;/code&gt;&lt;/a&gt; uses OpenAI's &lt;a href="https://openai.com/research/whisper"&gt;Whisper Model&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My favorite is the &lt;a href="https://github.com/CerebriumAI/daily-demos/tree/master/pet-object-detection"&gt;pet detection demo&lt;/a&gt;. The server-side does the following: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Join the call with &lt;a href="https://reference-python.daily.co/index.html"&gt;&lt;code&gt;daily-python&lt;/code&gt;&lt;/a&gt;, subscribing to video streams from each participant&lt;/li&gt;
&lt;li&gt;Detect pets by passing video frames from each participant to the &lt;a href="https://docs.ultralytics.com/"&gt;YOLO v8 model&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;When a pet is detected, the model returns the frame with a bounding box&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;daily-python&lt;/code&gt; sends the frame with the bounding box into the call, allowing everyone to enjoy a dedicated feed of pets!
You can achieve all of this in roughly 15 lines of code and you will not have to fret about latency or scaling!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--j28LDAAd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/csymvbrg9w83yfyiigqv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--j28LDAAd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/csymvbrg9w83yfyiigqv.png" alt="daily-python detects a cat in one of the frames, sends it as active speaker" width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;code&gt;daily-python&lt;/code&gt; detects a cat in one of the frames, sends it as active speaker&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# main.py
def predict(item, run_id, logger):
    item = Item(**item)

    # initialize daily-python
    Daily.init()

    # initialize the AI model
    pet_detector = PetDetection()

    #On startup, connect to room with username Pet Detector
    bot_name = "Pet Detector"
    client = pet_detector.client
    client.set_user_name(bot_name)

    # join daily call with room URL
    pet_detector.join(item.room)

    # iterate on video frame from each participant or user
    for participant in client.participants():
        # ignore local frames, these are self-created pet-detected frames
        if participant != "local":
            # send participant frames to the pet detector
            client.set_video_renderer(participant, callback = pet_detector.on_video_frame)

# pet_detection.py

# Load the model weights
pet_detection = YOLO("weights.pt")

class PetDetection(EventHandler):

    def on_video_frame(self, participant, frame):
        self.frame_count += 1
        if self.frame_count &amp;gt;= self.frame_cadence:
          self.frame_count = 0
          self.queue.put(frame.buffer)
          worker_thread = threading.Thread(target=self.process_frame, daemon=True)
          worker_thread.start()

    def process_frame(self):
        buffer = self.queue.get()
        IMAGE_WIDTH = 1280
        IMAGE_HEIGHT = 720

        image = Image.frombytes('RGBA', (IMAGE_WIDTH, IMAGE_HEIGHT), buffer)
        image = cv2.cvtColor(np.array(image), cv2.COLOR_RGBA2BGR)

        detections = pet_detection(image)
        if len(detections[0].boxes) &amp;gt; 0:
          plotted_image = plot_bboxes(image, detections[0].boxes, score=False)
          plotted_image = cv2.cvtColor(plotted_image, cv2.COLOR_BGR2RGB)
          is_success, buffer = cv2.imencode(".png", plotted_image)
          image_stream = io.BytesIO(buffer)
          self.camera.write_frame(Image.open(image_stream).tobytes())

        # Indicate that a formerly enqueued task is complete
        self.queue.task_done()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ready to get started with Cerebrium? Here's how:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://dashboard.daily.co/u/signup"&gt;Sign up for a Daily account&lt;/a&gt;. You will need to create a Daily room through the &lt;a href="https://dashboard.daily.co/rooms/create"&gt;dashboard&lt;/a&gt; or &lt;a href="https://docs.daily.co/reference/rest-api/rooms/create-room"&gt;programmatically&lt;/a&gt; using your Daily API key.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dashboard.cerebrium.ai/"&gt;Sign up for an Cerebrium account&lt;/a&gt;, since we will need to get our API keys to deploy this example.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/CerebriumAI/daily-demos"&gt;Git Clone the repository&lt;/a&gt; and install the necessary packages by running these commands in the terminal
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pip install --upgrade cerebrium
cerebrium login &amp;lt;private_api_key&amp;gt; 
cerebrium deploy --config-file ./config.yaml petdetection 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This already bundles &lt;code&gt;daily-python&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Manually invite the pet bot to the call by using REST API.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl --location --request POST 'https://run.cerebrium.ai/v3/p-xxxx/pet-detection/predict' \
--header 'Authorization: &amp;lt;JWT_TOKEN&amp;gt;' \
--header 'Content-Type: application/json' \
--data '{"room": "Your Daily Room URL"}'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This manual step can be later automated by listening to the &lt;a href="https://www.daily.co/blog/configure-a-webhook-to-send-notifications-when-someone-joins-your-video-calls/"&gt;&lt;code&gt;participant-joined&lt;/code&gt; webhook&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Invite people to join the created Daily room URL, especially those that have pets available at hand!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One of our north stars is developer time to value, in this case, how much time it takes for a developer with a cool application for an ML model to build, deploy, and start field testing. With our announcement today, what used to take developers weeks can now take minutes. You can get started with Cerebrium’s free tier and Daily’s 10,000 free monthly minutes.&lt;/p&gt;

&lt;p&gt;Let us know how we can help support you as you build. Reach out to our &lt;a href="https://daily.co/contact/support"&gt;developer support&lt;/a&gt; or head over to &lt;a href="https://community.daily.co/"&gt;peerConnection&lt;/a&gt;, our WebRTC forum. You can always find us online or IRL at one of our regularly-hosted &lt;a href="https://www.daily.co/resources/events/"&gt;events&lt;/a&gt;. And check back for more this week in our &lt;a href="https://www.daily.co/blog/tag/ai-week-2023/"&gt;AI Week series&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>webrtc</category>
      <category>news</category>
    </item>
    <item>
      <title>The technology behind AI-powered Clinical Notes API for Telehealth</title>
      <dc:creator>Tasha</dc:creator>
      <pubDate>Mon, 02 Oct 2023 21:52:25 +0000</pubDate>
      <link>https://forem.com/trydaily/the-technology-behind-ai-powered-clinical-notes-api-for-telehealth-1amd</link>
      <guid>https://forem.com/trydaily/the-technology-behind-ai-powered-clinical-notes-api-for-telehealth-1amd</guid>
      <description>&lt;p&gt;&lt;em&gt;By &lt;a href="https://www.daily.co/blog/author/kwindla-hultman-kramer/"&gt;Kwindla Hultman Kramer&lt;/a&gt;, &lt;a href="https://www.daily.co/blog/author/nina-kuruvilla/"&gt;Nina Kuruvilla&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Daily’s developer platform powers audio and video experiences for millions of people all over the world. Our customers are developers who use our APIs and &lt;a href="https://www.daily.co/products/video-sdk/"&gt;client SDKs&lt;/a&gt; to build audio and video features into applications and websites.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In our &lt;a href="https://www.daily.co/blog/ai-week-at-daily-2023-edition/"&gt;AI Week&lt;/a&gt; series, we’re introducing two new toolkits, several new components of our &lt;a href="https://www.daily.co/blog/global-mesh-network/"&gt;global infrastructure&lt;/a&gt;, and a series of AI-focused partnerships.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Our kickoff post this week goes into more detail about what topics we’re diving into and how we think about the potential of combining WebRTC, video and audio, and AI. Feel free to click over and &lt;a href="https://www.daily.co/blog/ai-week-at-daily-2023-edition/"&gt;read that intro&lt;/a&gt; before (or after) reading this post.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  AI that gives healthcare providers time back, every day
&lt;/h2&gt;

&lt;p&gt;Telehealth usage grew rapidly during the COVID-19 pandemic, accelerating regulatory, billing, and technology changes that had started already. It's now embedded in healthcare delivery, and can help expand access to care and improve patient outcomes.&lt;/p&gt;

&lt;p&gt;From a technology point of view, one powerful benefit of telehealth interactions is that all audio is captured digitally, making it ready to be transcribed and summarized.&lt;/p&gt;

&lt;p&gt;Earlier this year, several of our telehealth customers started asking us whether we could help them understand the landscape of HIPAA-compliant AI tools, and how to use those tools as part of their telehealth workflows.&lt;/p&gt;

&lt;p&gt;Healthcare providers, our customers told us, spend 10 to 15 hours each week writing clinical care notes. These notes summarize a patient visit, along with the provider’s assessments and recommended next steps. Writing clinical notes is time-consuming; yet in general it doesn’t leverage a provider’s expertise very well, and isn't work that's regarded as interesting or creative. It’s a necessary task that humans can do, and computers couldn’t. . . . Until now.&lt;/p&gt;

&lt;p&gt;Our telehealth customers were seeing examples of &lt;a href="https://openai.com/gpt-4"&gt;GPT-4&lt;/a&gt; producing summaries of things like sales calls, customer support interactions, podcasts, and YouTube videos — and asking if an AI Large Language Model (LLM) could produce good first drafts of clinical notes. &lt;/p&gt;

&lt;h2&gt;
  
  
  A general approach to summarization and post-processing
&lt;/h2&gt;

&lt;p&gt;Today’s most advanced “frontier,” Large Language Models have an impressive range of use cases. But perhaps the &lt;em&gt;most&lt;/em&gt; impressive thing about them, to a computer programmer, is that they are good at turning &lt;strong&gt;unstructured input data&lt;/strong&gt; into &lt;strong&gt;structured output&lt;/strong&gt;. This is a genuinely new capability, and is perhaps the biggest reason so many engineers are so excited about these new tools.&lt;/p&gt;

&lt;p&gt;For most use cases that are complex enough to be interesting, the transformation from unstructured data to structured output needs to happen at multiple levels. At the level of content, the Large Language Model processes the input text and summarizes or prioritizes the parts of the text that are most important. At the level of format, the LLM organizes the output into sequences or sections that make sense for the specific use case.&lt;/p&gt;

&lt;p&gt;Generating clinical notes is a good example of this. Audio from a telehealth session is transcribed, sent to one or more AI models, and turned into output that has consistent content characteristics and a consistent structure.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7_C2Ln8X--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qy74uosp043ik02m3c00.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7_C2Ln8X--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qy74uosp043ik02m3c00.png" alt="Producing a clinical note in the widely used SOAP format (Subjective, Objective, Assessment, and Plan)" width="800" height="432"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Producing a clinical note in the widely used SOAP format (Subjective, Objective, Assessment, and Plan)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--2Ylod-BN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o0gxwofi2psqyox4sdew.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2Ylod-BN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o0gxwofi2psqyox4sdew.gif" alt="GIF of SOAP notes draft generation" width="800" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;An example of SOAP notes draft generation&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is a data pipeline with four steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Capture audio&lt;/li&gt;
&lt;li&gt;Transcribe the audio&lt;/li&gt;
&lt;li&gt;Process and transform the transcription&lt;/li&gt;
&lt;li&gt;Validate and store the output&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Next week we'll be writing more about new APIs that support building AI-powered data pipelines like this one. These pipelines take audio, video, and metadata from real-time video sessions as input, and provide hooks for processing this data with Large Language Models and other AI tools and services.&lt;/p&gt;

&lt;p&gt;To extend Daily’s “building blocks” into this new world of generative AI, we’re leveraging our deep experience with video, audio, and transcription, along with our &lt;a href="https://www.daily.co/blog/global-mesh-network/"&gt;global infrastructure&lt;/a&gt; that was built from the ground up to route, manage, process, and store video and audio.&lt;/p&gt;

&lt;h2&gt;
  
  
  What matters most: data privacy, accuracy, reliability, flexibility
&lt;/h2&gt;

&lt;p&gt;The clinical notes use case is a demanding test for AI workflow APIs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All data processing and storage must protect patient privacy and be HIPAA-compliant.&lt;/li&gt;
&lt;li&gt;The quality of the output is highly sensitive to the accuracy of the transcription.&lt;/li&gt;
&lt;li&gt;Clinical notes are a critical part of the healthcare workflow, so the APIs that power them have to work at scale, with predictable latency.&lt;/li&gt;
&lt;li&gt;There are several common output formats for clinical notes, so the pipeline needs to allow LLM prompts and other pipeline steps to be configurable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We’ve extended Daily’s existing HIPAA-compliant infrastructure to include support for these new workflow APIs. We’ve also signed HIPAA Business Associate Agreements with three new partners. (More on that below.)&lt;/p&gt;

&lt;p&gt;Our global WebRTC infrastructure and extensively tuned SDKs are key to delivering the best possible audio as input to the transcription step in the pipeline. Higher quality audio makes possible more accurate speech-to-text transcriptions. Daily’s bandwidth management and very low average first-hop latency everywhere in the world (13ms) guarantee that as many audio packets as possible will be successfully transmitted and recorded.&lt;/p&gt;

&lt;p&gt;To perform the transcription step, we are working with our long-time partner &lt;a href="https://deepgram.com/"&gt;Deepgram&lt;/a&gt;. Deepgram is an industry leader in both overall accuracy and in the flexibility of their translation models. Daily has offered &lt;a href="https://docs.daily.co/reference/daily-js/instance-methods/start-transcription"&gt;direct access to Deepgram’s real-time transcription service&lt;/a&gt; for several years. We’re now wrapping Deepgram’s batch-mode transcription APIs to make it easy to build transcription-driven post-processing workflows. We’ve also signed a HIPAA BAA with Deepgram.&lt;/p&gt;

&lt;p&gt;For the Large Language Model step, we’re working with &lt;a href="https://azure.microsoft.com/en-us/products/ai-services/openai-service"&gt;Microsoft Azure OpenAI&lt;/a&gt; and the &lt;a href="https://www.microsoft.com/en-us/industry/digital-transformation"&gt;Microsoft Software &amp;amp; Digital Platforms Group&lt;/a&gt;. Microsoft has given Daily early access to the new Azure HIPAA-compliant &lt;a href="https://azure.microsoft.com/en-us/blog/introducing-gpt4-in-azure-openai-service/"&gt;OpenAI GPT-4&lt;/a&gt; service.&lt;/p&gt;

&lt;p&gt;We’ve tested clinical notes generation extensively with GPT-3.5, GPT-4, and the Llama-2 family of models. GPT-4 produces the highest-quality output. For the clinical notes use case, the benefits of using GPT-4 outweigh the higher cost of GPT-4 compared to less powerful models.&lt;/p&gt;

&lt;p&gt;To deliver even better results beyond what GPT-4 does by itself, we’ve also partnered with &lt;a href="https://www.science.io/"&gt;ScienceIO&lt;/a&gt;, a company that pioneered using Large Language Models to enrich and structure medical data. We use ScienceIO’s medical structured data APIs in combination with GPT-4.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--LOq0rNhB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u8lopyy9q66s77l94mbn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--LOq0rNhB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u8lopyy9q66s77l94mbn.png" alt="diagram of Clinical Notes architecture" width="800" height="576"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Deepgram, ScienceIO, and GPT-4 together form a state-of-the-art technology stack for processing audio and generating high-quality output for healthcare use cases.&lt;/p&gt;

&lt;p&gt;As AI technology evolves, we expect this pipeline to evolve, too. We’re optimistic, for example, that fine-tuning Llama-2 has the potential to open up additional possibilities for patient and provider workflows beyond clinical notes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessing these new APIs, and what’s next
&lt;/h2&gt;

&lt;p&gt;Our Clinical Notes API for Telehealth is available today. If you have a use case you’re particularly excited about, or any questions, please &lt;a href="https://www.daily.co/company/contact/"&gt;contact us&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We’re excited about our roadmap for post-processing APIs! We are releasing new features throughout the next few weeks. In addition to audio- and transcription-centric use cases, we have a full set of features in development for video analytics, composition, and editing. We’ll have a bit more to say about that next week.&lt;/p&gt;

&lt;p&gt;As always, we love to write and talk about video and audio, real-time networking, and now AI. So if you’re interested in these topics, too, please check out the rest of our &lt;a href="https://www.daily.co/blog/tag/ai-week-2023/"&gt;AI Week posts&lt;/a&gt;, join us on the &lt;a href="https://community.daily.co/"&gt;peerConnection&lt;/a&gt; WebRTC forum, or find us online or IRL at one of the &lt;a href="https://www.daily.co/resources/events/"&gt;events&lt;/a&gt; we host.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webrtc</category>
      <category>programming</category>
      <category>api</category>
    </item>
  </channel>
</rss>
