media.html
<div class="media">
  <p-lazy>
    <video class="media__content" inert controls autoplay playsinline>
      <source data-src="https://www.w3schools.com/html/mov_bbb.mp4" type="video/mp4">
      Your browser does not support the video tag.
    </video>
    <template #placeholder="{ load, modifier }">
      <picture class="media__mask" :class="modifier">
          <img srcset="https://unsplash.it/320/176?random&gravity=center" alt="">
      </picture>
      <button class="media__trigger" @click="load" :class="modifier">
        <span class="srOnly">play</span>
        <svg class="media__icon icon -circle" viewBox="0 0 96 96"><use href="/main-icons-sprite.svg#play" /></svg>
      </button>
    </template>
  </p-lazy>
</div>

<div class="media">
  <p-lazy>
    <video inert controls autoplay playsinline>
      <source data-src="https://www.w3schools.com/html/mov_bbb.mp4" type="video/mp4">
      Your browser does not support the video tag.
    </video>
    <template #placeholder="{ load, modifier }">
        <picture class="media__mask" :class="modifier">
            <source srcset="https://unsplash.it/320/176?random&gravity=center" media="(min-width: 768px)">
            <source srcset="https://unsplash.it/320/176?random&gravity=center" media="(min-width: 480px)">
            <img class="lazyload" srcset="https://unsplash.it/320/176?random&gravity=center" alt="">
        </picture>
      <button class="media__trigger" :class="modifier" @click="load">
        <span class="srOnly">play</span>
        <svg class="media__icon icon -circle -light" viewBox="0 0 96 96"><use href="/main-icons-sprite.svg#play" /></svg>
      </button>
    </template>
</div>

<br>

<div class="media">
  <p-lazy>
    <iframe data-src="https://player.vimeo.com/video/818944000?h=e8674039c8&autoplay=1&title=0&byline=0"  frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe>
    <template #placeholder="{ load, modifier }">
      <picture class="media__mask" :class="modifier">
        <source srcset="https://unsplash.it/267/150?random&gravity=center" media="(min-width: 768px)">
        <source srcset="https://unsplash.it/267/150?random&gravity=center" media="(min-width: 480px)">
        <img class="lazyload" srcset="https://unsplash.it/267/150?random&gravity=center" alt="">
      </picture>
      <button class="media__trigger" :class="modifier" @click="load">
        <span class="srOnly">play</span>
        <svg class="media__icon icon -circle" viewBox="0 0 96 96"><use href="/main-icons-sprite.svg#play" /></svg>
      </button>
    </template>
  </p-lazy>
</div>
index.scss
.media {
  $b: &;

  aspect-ratio: var(--aspect-ratio);
  background-color: rgb(from currentColor r g b / .25);
  display: inline grid;
  margin-bottom: 2rem;

  > * {
    grid-area: 1 / 1;
    position: relative;
  }

  &__mask {
    margin: 0;
    display: grid;
    transition: opacity var(--root-ease-out-moderate);

    &.-loaded {
      opacity: 0;
      pointer-events: none;
    }
  }

  &__maskImg {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  &__trigger {
    appearance: none;
    background: #0000;
    border: 0;
    border-radius: max(50cqmin, 6rem);
    padding: 0;
    align-self: center;
    justify-self: center;
    color: var(--accent-color-200);
    container-type: size;
    position: relative;
    transition: color var(-root-ease-out-fast),
      opacity var(--root-ease-out-moderate);
    width: min(50cqmin, 6rem);
    height: min(50cqmin, 6rem);

    &:hover {
      color: var(--accent-color-300);
    }

    &.-loaded {
      align-self: start;
      height: min(25cqmin, 3rem);
      justify-self: end;
      margin: .5rem;
      opacity: 0;
      pointer-events: none;
      transition: opacity var(--root-ease-out-fast);
      width: min(25cqmin, 3rem);

      &.-keepVisible {
        opacity: .25;
        pointer-events: auto;
      }

      &.-keepVisible:hover,
      &:focus-visible {
        opacity: 1;
      }
    }
  }

  &__icon {
    --icon-stroke-color: var(--accent-color-200);
    filter: drop-shadow(var(--root-box-shadow-med));
    width: auto;
    height: auto;

    &.-circle {
      padding: min(6cqmin, .75rem);
      transition: padding var(--root-ease-out-fast);

      &:hover {
        padding: min(4cqmin, .5rem);
      }
    }

  }
}
PLazy.vue
<script setup>
  import { computed, ref, nextTick, useTemplateRef } from 'vue'

  /**
   * PLazy is a vue component that lazy loads its content after it's been clicked.
   *
   * @slot default - the content to lazyload. It will remove inert attributes, change data-src
   *                 attributes to src attributes, and call .load() on videos.
   * @slot placeholder - the content to display before it's lazyloaded. The following properties
   *                     are exposed to this slot:
   *       load {function} - function to call to load the content.
   *       loaded {boolean} - whether the content is loaded or not.
   *       modifier {string} - either '-loaded' or '' based on loaded.
   */

  const loaded = ref(false)
  const content = useTemplateRef('content')
  const modifier = computed(() => loaded.value ? '-loaded' : '')

  const load = async () => {
    if (loaded.value) return

    await nextTick()

    const promises = []

    /* Change data-src attributes to src attributes */
    content.value.querySelectorAll('[data-src]')
      .forEach(element => element.setAttribute('src', element.dataset.src))

    /* remove inert attribute */
    content.value.querySelectorAll('[inert]')
      .forEach(element => element.inert = false)

    /* load videos */
    content.value.querySelectorAll('video')
      .forEach(element => {
        element.load()
        promises.push(new Promise(resolve => {
          element.addEventListener('loadeddata', resolve, { once: true })
        }))
      })

    /* load iframes */
    content.value.querySelectorAll('iframe')
      .forEach(element => {
        promises.push(new Promise(resolve => {
          element.addEventListener('load', resolve, { once: true })
        }))
      })

    /* wait for all videos and iframes to load, then update loaded.value */
    Promise.all(promises).then(() => loaded.value = true)
  }
</script>

<template>
  <div ref="content">
    <slot name="default"></slot>
  </div>
  <slot name="placeholder" v-bind="{ load, loaded, modifier }"></slot>
</template>

<style scoped>
div {
  container-type: size;
}
</style>