Created
February 28, 2024 12:54
-
-
Save ferserc1/5526f07d9d194e2af651f5d2f5bb50a2 to your computer and use it in GitHub Desktop.
mp4 video plugin with quality change support used in media.upv.es
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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