Skip to content

Instantly share code, notes, and snippets.

@ferserc1
Created February 28, 2024 12:54
Show Gist options
  • Save ferserc1/5526f07d9d194e2af651f5d2f5bb50a2 to your computer and use it in GitHub Desktop.
Save ferserc1/5526f07d9d194e2af651f5d2f5bb50a2 to your computer and use it in GitHub Desktop.
mp4 video plugin with quality change support used in media.upv.es
import { VideoPlugin, Video, utils, VideoQualityItem } from 'paella-core';
let video = null;
export function supportsVideoType(type) {
if (!type) return false;
if (!video) {
video = document.createElement("video");
}
const canPlay = video.canPlayType(type);
return canPlay === "maybe" || canPlay === "probably";
}
export class Mp4MediaVideo extends Video {
constructor(player, parent, isMainAudio) {
super('video', player, parent);
this.element.setAttribute("playsinline","true");
this.isMainAudio = isMainAudio;
// Autoplay is required to play videos in some browsers
this.element.setAttribute("autoplay","true");
this.element.autoplay = true;
// The video is muted by default, to allow autoplay to work
if (!isMainAudio) {
this.element.muted = true;
}
this._videoEnabled = true;
}
async play() {
await this.waitForLoaded();
return await this.video.play();
}
async pause() {
await this.waitForLoaded();
return this.video.pause();
}
async duration() {
await this.waitForLoaded();
return this.video.duration;
}
get currentTimeSync() {
return this.ready ? this.video.currentTime : -1;
}
async currentTime() {
await this.waitForLoaded();
return this.currentTimeSync;
}
async setCurrentTime(t) {
await this.waitForLoaded();
return this.video.currentTime = t;
}
async volume() {
await this.waitForLoaded();
return this.video.volume;
}
async setVolume(v) {
await this.waitForLoaded();
if (v === 0) {
this.video.setAttribute("muted", true);
}
else {
this.video.removeAttribute("muted");
}
return this.video.volume = v;
}
async paused() {
await this.waitForLoaded();
return this.video.paused;
}
async playbackRate() {
await this.waitForLoaded();
return await this.video.playbackRate;
}
async setPlaybackRate(pr) {
await this.waitForLoaded();
return this.video.playbackRate = pr;
}
async getQualities() {
if (!this._qualities) {
this._qualities = [];
this._sources.forEach((src,i) => {
this._qualities.push(new VideoQualityItem({
index: i,
label: `${src.res.w}x${src.res.h}`,
shortLabel: `${src.res.h}p`,
width: src.res.w,
height: src.res.h
}));
});
}
return this._qualities;
}
async setQuality(q) {
const asyncTimeout = async (time) => {
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, time);
})
}
if (!(q instanceof VideoQualityItem)) {
throw new Error("Invalid parameter setting video quality");
}
this._currentQuality = q;
const currentTime = this.video.currentTime;
const paused = this.video.paused;
await this.player.pause();
this.clearStreamData();
await this.loadStreamData(this._streamData);
this.video.currentTime = currentTime;
if (!paused) {
await this.player.play();
}
}
get currentQuality() {
return this._currentQuality;
}
async getDimensions() {
await this.waitForLoaded();
return { w: this.video.videoWidth, h: this.video.videoHeight };
}
saveDisabledProperties(video) {
this._disabledProperties = {
duration: video.duration,
volume: video.volume,
videoWidth: video.videoWidth,
videoHeight: video.videoHeight,
playbackRate: video.playbackRate,
paused: video.paused,
currentTime: video.currentTime
}
}
// This function is called when the player loads, and it should
// make everything ready for video playback to begin.
async loadStreamData(streamData = null) {
this._streamData = this._streamData || streamData;
this.player.log.debug("es.upv.paella.mp4VideoFormat: loadStreamData");
this._sources = null;
this._sources = streamData.sources.mp4;
this._sources.sort((a,b) => {
return Number(a.res.w) - Number(b.res.w);
});
// Initialize qualities and set the default quality stream, only the
// first time the video is loaded
if (!this._qualities) {
const qualities = await this.getQualities();
this._currentQuality = qualities[qualities.length - 1];
}
this._currentSource = this._sources[this._currentQuality.index];
if (!this.isMainAudioPlayer) {
this.video.muted = true;
}
this.video.src = utils.resolveResourcePath(this.player, this._currentSource.src);
this._endedCallback = this._endedCallback || (() => {
if (typeof(this._videoEndedCallback) == "function") {
this._videoEndedCallback();
}
});
this.video.addEventListener("ended", this._endedCallback);
await this.waitForLoaded();
this.player.log.debug(`es.upv.paella.mp4VideoFormat (${ this.streamData.content }): video loaded and ready.`);
this.saveDisabledProperties(this.video);
}
async clearStreamData() {
this.video.src = "";
this.video.removeEventListener("ended", this._endedCallback);
this._ready = false;
}
get isEnabled() {
return this._videoEnabled;
}
async enable() {
this._videoEnabled = true;
return this._videoEnabled;
}
async disable() {
if (this.isMainAudio) {
this.player.log.debug("video.disable() - the video is not disabled because it is the main audio source.");
}
else {
this._videoEnabled = false;
}
return this._videoEnabled;
}
waitForLoaded() {
return new Promise((resolve,reject) => {
if (this.ready) {
resolve();
}
else {
const startWaitTimer = () => {
this._waitTimer && clearTimeout(this._waitTimer);
this._waitTimer = null;
if (this.video.error) {
reject(new Error(`Error loading video: ${this.video.src}. Code: ${this.video.error.code}: ${this.video.error.message}`));
}
else if (this.video.readyState >= 2) {
this.video.pause(); // Pause the video because it is loaded in autoplay mode
this._ready = true;
resolve();
}
else {
this._waitTimer = setTimeout(() => startWaitTimer(), 100);
}
}
startWaitTimer();
}
})
}
}
export default class Mp4MediaVideoPlugin extends VideoPlugin {
get streamType() {
return "mp4";
}
isCompatible(streamData) {
const { mp4 } = streamData.sources;
return mp4 && supportsVideoType(mp4[0]?.mimetype);
}
async getVideoInstance(playerContainer, isMainAudio) {
return new Mp4MediaVideo(this.player, playerContainer, isMainAudio);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment