Angular-Ionic:视频标签中视频之间的无缝过渡?

Angular-Ionic: seamless transition between videos in video tag?

我有一个 Angular/Ionic(分别是版本 10 和 5)组件,它以数组的形式获取播放列表。每个位置都是一个对象,其中包含一些关于视频的元数据和一个 fileSrc,它以我的应用程序存储中所述视频的位置为目标。在我的模板中,我有一个这样的标签:

 <video *ngIf="video" #videoElement controls (ended)="skipToNextVideoInQueue()"
            [src]="video" type="video/mp4"

该组件有一个 queuePosition public 变量,从 0 开始设置播放列表的当前位置。当视频结束时,将触发 skipToNextVideoInQueue() 方法。除其他外,此方法执行以下操作:

this.queuePosition += 1;
this.video = this.playlist[queuePosition].fileSrc;
this.playVideo();

最后一个 playVideo() 方法仅执行以下操作:

setTimeout(() => {
      const videoElement = this.videoElement.nativeElement;
      if (currentVideo) {
        currentVideo.play();
      }
    });

到目前为止,可以接受:该行为或多或少符合我最初的预期。播放列表从头开始,一直播放下一个视频,直到播放列表结束。

现在唯一的问题是,在一个视频的(结束)事件和下一个视频的实际开始(我可以看到活动帧的那一刻)之间,我得到一个简短的加载屏幕。理想情况是一个视频尽可能无缝地过渡到下一个视频,而不会注意到跳过或加载。我相信当 src 属性未完全加载时,此屏幕只是标签的自然方面,但如果有帮助,我将编辑线程以添加屏幕截图。

我的猜测是视频的 [src] 属性的更新需要一段时间才能注入,这会导致我提到的第二个加载屏幕的一小部分。

到目前为止我尝试过的东西:

<span *ngFor="let video of playlist; let i = index">
              <video *ngIf="video" #videoElement controls (ended)="skipToNextVideoInQueue()"
                [style.display]="queuePosition === index ? 'block' : 'none' ">
                <source [src]="video.fileSrc" type="video/mp4">
              </video>
</span>

上面的尝试抛出了这个错误(我只是在这个线程中添加了一些占位符以将我的应用程序的一些数据保密):

GET http://192.168.1.45:8100/_capacitor_file_/data/user/0/{{app_name}}/files/{{fileName}}.mp4 net::ERR_FAILED

嗯,坏消息 - 这不是一个优雅的解决方案。好消息是它可以做到。

[请注意,在我发现原始答案中存在一些缺陷后,21 年 9 月 10 日编辑了此答案]

主要思想是在第一个视频开始播放后立即开始加载下一个视频。一旦一个视频结束,我们将其显示值设置为 none(我们甚至可以通过在节点上执行 .remove() 方法将其完全删除),然后我们自动开始播放下一个。

虽然我想尝试优化它,但我所做的是在 ngFor 中生成 N 个标签,基于 playlist 对象,在我的例子中,该对象来自服务。

模板使用了这个:

<ng-container *ngFor="let item of playlist; let i = index">
   <video #videoElements
      id="{{'videoPlayer' + i}}"
      (playing)="preloadNextVideo()"
      (ended)="skipToNextVideoInQueue()"
      preload="auto"
      type="video/mp4">
    </video>
</ng-container>

该方法需要以下内容才能在控制器中工作:

import { Component, ViewChildren, ElementRef, QueryList } from '@angular/core';

@ViewChildren('videoElements') videoElements: QueryList<ElementRef>;

public videoPlayers = [];
public queuePosition: number;
public playlist: any;
constructor() {}

ngOnInit() {
  this.queuePosition = 0;
  // the line below is a bit of pseudocode to show we need to get the playlist first
  this.myService.getPlaylist.then((result) => {
  this.playlist = result;
  this.storePlayerRefs();
  this.preloadVideo(0);
}
});

// Allow the controller to locate the different children by index
  storePlayerRefs() {
    setTimeout(() => {
      this.videoPlayers = this.videoElements.toArray();
    });
  }



    preloadVideo(position: number) {
    setTimeout(() => {
      if (position < this.playlist.length) {
        const videoToPreload = this.videoPlayers[position];
        const sourceTag = document.createElement('source');
        // The line below assigns a timestamp to the URL so, in case of refreshes, the source tag will completely reset itself
        const fileSrc = this.playlist[position].fileSrc + '?t='+Math.random();
        sourceTag.setAttribute('src', fileSrc);
        sourceTag.setAttribute('type', 'video/mp4');
        videoToPreload.nativeElement.appendChild(sourceTag);
      }
    });
  }

到目前为止,发生的事情是:

  • videoElements queryList 启动 'watching' ngFor 生成的视频标签。获取播放列表后,由于 queryList 通常作为对象的对象出现,我选择将它们作为数组保存在 videoPlayers 变量中,因为这样可以更容易地在视频之间切换。
  • preloadVideo方法找到播放列表对应的源,创建源节点,设置src属性并附加。由于 html 有 preload="auto" 选择器,我们不需要调用 .load() 方法。

完成初始设置后,我们进入复杂的部分(这也是我在第一次尝试开发时感兴趣的地方):

  playVideo() {
    setTimeout(() => {
      const videoElement = 
      this.videoPlayers[this.queuePosition].nativeElement;
      if (videoElement) {
        this.playing = true;
        this.hidePreviousPlayer();
        videoElement.style.display = 'block';
        videoElement.play();
      }
    });
  }

此方法在第一次调用时,可以在另一个方法成功后手动调用...取决于您的需要。当视频开始和结束时,ended 事件被触发,我们调用它:

  skipToNextVideoInQueue() {
    if ((this.queuePosition + 1) < this.playlist.length) {
      this.queuePosition ++;
      this.playVideo();
      }
   } else {
   // handle whatever you need to do when the playlist is completed
} 

最后但同样重要的是,playVideo() 在播放当前元素之前也调用了这个:

  hidePreviousPlayer() {
    if (this.queuePosition > 0) {
      setTimeout(() => {
    const videoTodelete = this.videoPlayers[this.queuePosition - 1].nativeElement;
    videoTodelete.style.display = 'none';
    videoTarget.children[0].src = '';
    videoTarget.innerHTML = '';
    videoTarget.load();
      });
    }
  }

所有这一切背后的逻辑如下:

  • queuePosition 变量跟踪当时必须播放的视频。它可以拥有的最大值与播放列表长度相关联,并且使用该 queuePosition 作为索引,在任何给定时间我们都可以知道需要访问什么文件,以及必须选择什么 Children。
  • 在任何给定时间预加载 i + 1 视频有助于我们摆脱在 src 属性加载和缓冲它需要播放的实际媒体时发生的小 'delay'。
  • 一个视频结束后,我们将queuePosition的值加1,这样视频的预加载总是比当前的预加载一步。这样,当触发 videoElement.play() 方法时,信息已经预加载并准备就绪。
  • 在播放下一个元素之前,hidePreviousPlayer() 方法定位第 i - 1 个视频(也就是队列中的前一个视频),将其显示设置为 none 并在删除之前完全清除源标记它。这有助于我们通过保持尽可能少的活动缓冲区来优化应用程序内存。
  • 预加载下一个视频的信息,视频中没有 controls 标签,播放列表中有 preload="auto",过渡是无缝的。

输入完所有这些后...我将是第一个承认这不是最佳选择的人。 hidePreviousPlayer 方法的重构确实有助于避免内存问题,但这种方法仍然涉及为播放列表的每个元素生成一个 N。

待办事项:第二次尝试仅使用两个视频标签和它们之间的 switching/preloading 来减少 DOM 和应用程序必须处理。