I have an Angular/Ionic (versions 10 and 5 respectively)component that gets a playlist in the form of an array. Each position is an object containing some metadata about the video and a fileSrc that targets the location of said video in my app's storage. In my template, I have a tag like this:
<video *ngIf="video" #videoElement controls (ended)="skipToNextVideoInQueue()"
[src]="video" type="video/mp4"
The component has a queuePosition public variable that, starting from 0, sets the current position of the playlist. When the video ends, the skipToNextVideoInQueue() method is fired. This method, among other things, does the following:
this.queuePosition += 1;
this.video = this.playlist[queuePosition].fileSrc;
this.playVideo();
And this last playVideo() method, in turn, merely does the following:
setTimeout(() => {
const videoElement = this.videoElement.nativeElement;
if (currentVideo) {
currentVideo.play();
}
});
So far, so acceptable: the behavior is what I initially expected, more or less. The playlist starts from the beginning and keeps playing the next video on and on until the playlist comes to an end.
The only problem now is that, between the (ended) event of one video and the actual beginning of the next (the moment when I can see active frames) I get a brief loading screen. The ideal case would be for one video to transition as seamlessly as possible into the next one, without that skip or loading being noticed. I believe this screen is just the natural aspect of the tag when the src attribute is not fully loaded, but if it helps, I'll edit the thread to add a screenshot.
My guess is that the updating of the [src] attribute of the video is taking a while to be injected and that gives the fraction of a second loading screen that I mentioned.
Stuff I tried so far:
<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>
The attempt above throws this error (where I just add some placeholders in this thread to keep some data about my app private):
GET http://192.168.1.45:8100/_capacitor_file_/data/user/0/{{app_name}}/files/{{fileName}}.mp4 net::ERR_FAILED
Well, bad news- it's not an elegant solution. Good news is that it can be done.
[Note that this answer is edited on september 10th, '21 after I discovered a few flaws in my original answer]
The main idea is to start loading the next video as soon as the first one starts playing. As soon as one video ends, we set its display value to none (we could even remove it completely by doing a .remove() method on the node), and we begin playing the next one automatically.
Although I want to try and optimize this, what I've done is generate an N number of tags within an ngFor, based on a playlist
object which in my case comes from a service.
The template uses this:
<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>
The approach needs the following to work in the controller:
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);
}
});
}
So far, what's going on is:
videoPlayers
variable as an array, since it made switching between videos easier.After this initial setup is done, we enter the complex part (this is aso where it got interesting for me while developing this first attempt):
playVideo() {
setTimeout(() => {
const videoElement =
this.videoPlayers[this.queuePosition].nativeElement;
if (videoElement) {
this.playing = true;
this.hidePreviousPlayer();
videoElement.style.display = 'block';
videoElement.play();
}
});
}
This method, on its first call, can be invoked manually, after the success of another method... Depending on your needs. And when the video comes to and end, the ended
event is fired, and we invoke this:
skipToNextVideoInQueue() {
if ((this.queuePosition + 1) < this.playlist.length) {
this.queuePosition ++;
this.playVideo();
}
} else {
// handle whatever you need to do when the playlist is completed
}
Last but not least, the playVideo() invoked this one as well before playing the current element:
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();
});
}
}
So the logic behind all of this, is as follows:
queuePosition
variable keeps track of the video that has to be played at that time. The maximum value it can have is tied to the playlist length, and using that queuePosition as index, at any given time we can know what file needs to be accessed, and what Children has to be selected.videoElement.play()
method is fired, the information is already preloaded and ready to go.hidePreviousPlayer()
method locates the i - 1 video (aka the previous one in the queue) sets its display to none and completely clears the source tag before removing it. This helps us optimize the app memory by keeping as few buffers active as possible.controls
tag in the video and the preload="auto"
in the playlist, the transition is seamless.After typing all of this... I will be the first to admit that this isn't optimal. The refactor of the hidePreviousPlayer method definitely helped avoid memory issues, but this approach still involves generating N with one per element of the playlist.
To-do: A second attempt using only two video tags and switching/preloading between them to reduce the amount of information that the DOM and the app has to handle.