Mulitple video tags slows app drastically

Hi,
I’m writing an multimédia ebook reader on Ionic 5 / Angular 8. I have this simple video component with custom controls :

<div class="video-player">
  <video #video [src]="content.src" controls controlslist="nodownload" disablePictureInPicture playsinline preload="none">
  </video>
  <div class="video-actions">
    <div class="video-actions-left">
      <ion-icon name="logo-closed-captioning" (click)="captions()"></ion-icon>
    </div>
    <div class="video-actions-center">
      <ion-icon [name]="playing ? 'pause':'play'" (click)="play()"></ion-icon>
    </div>
    <div class="video-actions-right">
      <ion-icon name="expand" (click)="fullscreen()"></ion-icon>
    </div>
  </div>
</div>
import {Component, ElementRef, Input, OnInit, ViewChild} from '@angular/core';
import {Video} from '../../../classes/contents/video';

@Component({
    selector: 'video-player',
    templateUrl: './video.component.html',
    styleUrls: ['./video.component.scss'],
})
export class VideoComponent implements OnInit {

    @ViewChild('video', {static: true}) videoRef: ElementRef;

    @Input() content: Video;

    video: HTMLVideoElement;
    videoSrc: string;
    playing = false;

    constructor(
        public toastController: ToastController
    ) {}

    ngOnInit() {
        this.video = this.videoRef.nativeElement;
        this.video.addEventListener('play', (event) => {
            this.playing = true;
        });
        this.video.addEventListener('pause', (event) => {
            this.playing = false;
        });
    }

    play() {
        if (this.video.paused) {
            this.video.play();
        } else {
            this.video.pause();
        }
    }
}

This takes forever to load and the app kind of freezes for a moment (~10s) :

If i remove the [src] attribute it’s much quicker (~1s) :

Any ideas why and how to fix ?

The videos (4 of them) are locally stored (/assets) and I set preload=none to prevent fetching data for each videos.

You could use something like intersection observer to watch for when the element enters the view port and then load the video?

I have this snippet right here which does that, but for images. You’d need to adapt it a bit for videos.

import {
  AfterViewInit,
  ChangeDetectionStrategy,
  Component,
  ElementRef,
  HostBinding,
  Input,
  ViewChild,
  ɵmarkDirty as markDirty,
} from '@angular/core';

@Component({
  selector: 'lazy-img',
  templateUrl: './lazy-img.component.html',
  styleUrls: ['./lazy-img.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LazyImgComponent implements AfterViewInit {
  observer: IntersectionObserver;
  @Input() src = '';
  @Input() alt = '';

  @HostBinding('class.loaded') isLoaded = false;

  @ViewChild('lazyImage', { static: true }) lazyImage: ElementRef<
    HTMLImageElement
  >;

  constructor() {}

  async ngAfterViewInit() {
    const options: IntersectionObserverInit = {
      root: this.lazyImage.nativeElement.closest('ion-content'),
    };

    if ('IntersectionObserver' in window) {
      this.observer = new IntersectionObserver(
        await this.onObserve.bind(this),
        options
      );
      this.observer.observe(this.lazyImage.nativeElement);
    } else {
      setTimeout(() => this.preload(this.lazyImage.nativeElement), 200);
    }
  }

  async onObserve(
    data: IntersectionObserverEntry[]
  ): Promise<IntersectionObserverCallback> {
    if (data[0].isIntersecting) {
      await this.preload(data[0].target as HTMLImageElement);
      this.observer.disconnect();
      return;
    }
  }

  applyImage(target: HTMLImageElement, src: string) {
    return new Promise((resolve) => {
      target.src = src;
      target.crossOrigin = 'anonymous';
      resolve();
    });
  }

  fetchImage(url: string) {
    return new Promise((resolve, reject) => {
      const image = new Image();
      image.src = url;
      image.crossOrigin = 'anonymous';
      image.onload = resolve;
      image.onerror = reject;
    });
  }

  async preload(targetEl: HTMLImageElement) {
    // prefect the image and prime it
    await this.fetchImage(this.src);
    // ok, actually apply the image to the real img tag
    await this.applyImage(targetEl, this.src);

    this.isLoaded = true
    markDirty(this)
  }
}
2 Likes

In the end, I went with an easier solution. I dynamically load the video on play.

play() {
    if (!this.video.src) {
        this.video.src = this.content.source;
        this.video.load();
        this.video.play();
    } else {
        if (this.video.paused) {
            this.video.play();
        } else {
            this.video.pause();
        }
    }
}