260 lines
6.8 KiB
JavaScript
260 lines
6.8 KiB
JavaScript
import Hls from 'hls.js'
|
|
import EventEmitter from 'events'
|
|
|
|
export default class LocalVideoPlayer extends EventEmitter {
|
|
constructor(ctx) {
|
|
super()
|
|
|
|
this.ctx = ctx
|
|
this.player = null
|
|
|
|
this.libraryItem = null
|
|
this.videoTrack = null
|
|
this.isHlsTranscode = null
|
|
this.hlsInstance = null
|
|
this.usingNativeplayer = false
|
|
this.startTime = 0
|
|
this.playWhenReady = false
|
|
this.defaultPlaybackRate = 1
|
|
|
|
this.playableMimeTypes = []
|
|
|
|
this.initialize()
|
|
}
|
|
|
|
initialize() {
|
|
if (document.getElementById('video-player')) {
|
|
document.getElementById('video-player').remove()
|
|
}
|
|
var videoEl = document.createElement('video')
|
|
videoEl.id = 'video-player'
|
|
// videoEl.style.display = 'none'
|
|
videoEl.className = 'absolute bg-black z-50'
|
|
videoEl.style.height = '216px'
|
|
videoEl.style.width = '384px'
|
|
videoEl.style.bottom = '80px'
|
|
videoEl.style.left = '16px'
|
|
document.body.appendChild(videoEl)
|
|
this.player = videoEl
|
|
|
|
this.player.addEventListener('play', this.evtPlay.bind(this))
|
|
this.player.addEventListener('pause', this.evtPause.bind(this))
|
|
this.player.addEventListener('progress', this.evtProgress.bind(this))
|
|
this.player.addEventListener('ended', this.evtEnded.bind(this))
|
|
this.player.addEventListener('error', this.evtError.bind(this))
|
|
this.player.addEventListener('loadedmetadata', this.evtLoadedMetadata.bind(this))
|
|
this.player.addEventListener('timeupdate', this.evtTimeupdate.bind(this))
|
|
|
|
var mimeTypes = ['video/mp4']
|
|
var mimeTypeCanPlayMap = {}
|
|
mimeTypes.forEach((mt) => {
|
|
var canPlay = this.player.canPlayType(mt)
|
|
mimeTypeCanPlayMap[mt] = canPlay
|
|
if (canPlay) this.playableMimeTypes.push(mt)
|
|
})
|
|
console.log(`[LocalVideoPlayer] Supported mime types`, mimeTypeCanPlayMap, this.playableMimeTypes)
|
|
}
|
|
|
|
evtPlay() {
|
|
this.emit('stateChange', 'PLAYING')
|
|
}
|
|
evtPause() {
|
|
this.emit('stateChange', 'PAUSED')
|
|
}
|
|
evtProgress() {
|
|
var lastBufferTime = this.getLastBufferedTime()
|
|
this.emit('buffertimeUpdate', lastBufferTime)
|
|
}
|
|
evtEnded() {
|
|
console.log(`[LocalVideoPlayer] Ended`)
|
|
this.emit('finished')
|
|
}
|
|
evtError(error) {
|
|
console.error('Player error', error)
|
|
this.emit('error', error)
|
|
}
|
|
evtLoadedMetadata(data) {
|
|
if (!this.isHlsTranscode) {
|
|
this.player.currentTime = this.startTime
|
|
}
|
|
|
|
this.emit('stateChange', 'LOADED')
|
|
if (this.playWhenReady) {
|
|
this.playWhenReady = false
|
|
this.play()
|
|
}
|
|
}
|
|
evtTimeupdate() {
|
|
if (this.player.paused) {
|
|
this.emit('timeupdate', this.getCurrentTime())
|
|
}
|
|
}
|
|
|
|
destroy() {
|
|
this.destroyHlsInstance()
|
|
if (this.player) {
|
|
this.player.remove()
|
|
}
|
|
}
|
|
|
|
set(libraryItem, videoTrack, isHlsTranscode, startTime, playWhenReady = false) {
|
|
this.libraryItem = libraryItem
|
|
this.videoTrack = videoTrack
|
|
this.isHlsTranscode = isHlsTranscode
|
|
this.playWhenReady = playWhenReady
|
|
this.startTime = startTime
|
|
|
|
if (this.hlsInstance) {
|
|
this.destroyHlsInstance()
|
|
}
|
|
|
|
if (this.isHlsTranscode) {
|
|
this.setHlsStream()
|
|
} else {
|
|
this.setDirectPlay()
|
|
}
|
|
}
|
|
|
|
setHlsStream() {
|
|
// iOS does not support Media Elements but allows for HLS in the native video player
|
|
if (!Hls.isSupported()) {
|
|
console.warn('HLS is not supported - fallback to using video element')
|
|
this.usingNativeplayer = true
|
|
this.player.src = this.videoTrack.relativeContentUrl
|
|
this.player.currentTime = this.startTime
|
|
return
|
|
}
|
|
|
|
var hlsOptions = {
|
|
startPosition: this.startTime || -1
|
|
// No longer needed because token is put in a query string
|
|
// xhrSetup: (xhr) => {
|
|
// xhr.setRequestHeader('Authorization', `Bearer ${this.token}`)
|
|
// }
|
|
}
|
|
this.hlsInstance = new Hls(hlsOptions)
|
|
|
|
this.hlsInstance.attachMedia(this.player)
|
|
this.hlsInstance.on(Hls.Events.MEDIA_ATTACHED, () => {
|
|
this.hlsInstance.loadSource(this.videoTrack.relativeContentUrl)
|
|
|
|
this.hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {
|
|
console.log('[HLS] Manifest Parsed')
|
|
})
|
|
|
|
this.hlsInstance.on(Hls.Events.ERROR, (e, data) => {
|
|
console.error('[HLS] Error', data.type, data.details, data)
|
|
if (data.details === Hls.ErrorDetails.BUFFER_STALLED_ERROR) {
|
|
console.error('[HLS] BUFFER STALLED ERROR')
|
|
}
|
|
})
|
|
this.hlsInstance.on(Hls.Events.DESTROYING, () => {
|
|
console.log('[HLS] Destroying HLS Instance')
|
|
})
|
|
})
|
|
}
|
|
|
|
setDirectPlay() {
|
|
this.player.src = this.videoTrack.relativeContentUrl
|
|
console.log(`[LocalVideoPlayer] Loading track src ${this.videoTrack.relativeContentUrl}`)
|
|
this.player.load()
|
|
}
|
|
|
|
destroyHlsInstance() {
|
|
if (!this.hlsInstance) return
|
|
if (this.hlsInstance.destroy) {
|
|
var temp = this.hlsInstance
|
|
temp.destroy()
|
|
}
|
|
this.hlsInstance = null
|
|
}
|
|
|
|
async resetStream(startTime) {
|
|
this.destroyHlsInstance()
|
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
this.set(this.libraryItem, this.videoTrack, this.isHlsTranscode, startTime, true)
|
|
}
|
|
|
|
playPause() {
|
|
if (!this.player) return
|
|
if (this.player.paused) this.play()
|
|
else this.pause()
|
|
}
|
|
|
|
play() {
|
|
if (this.player) this.player.play()
|
|
}
|
|
|
|
pause() {
|
|
if (this.player) this.player.pause()
|
|
}
|
|
|
|
getCurrentTime() {
|
|
return this.player ? this.player.currentTime : 0
|
|
}
|
|
|
|
getDuration() {
|
|
return this.videoTrack.duration
|
|
}
|
|
|
|
setPlaybackRate(playbackRate) {
|
|
if (!this.player) return
|
|
this.defaultPlaybackRate = playbackRate
|
|
this.player.playbackRate = playbackRate
|
|
}
|
|
|
|
seek(time) {
|
|
if (!this.player) return
|
|
this.player.currentTime = Math.max(0, time)
|
|
}
|
|
|
|
setVolume(volume) {
|
|
if (!this.player) return
|
|
this.player.volume = volume
|
|
}
|
|
|
|
// Utils
|
|
isValidDuration(duration) {
|
|
if (duration && !isNaN(duration) && duration !== Number.POSITIVE_INFINITY && duration !== Number.NEGATIVE_INFINITY) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
getBufferedRanges() {
|
|
if (!this.player) return []
|
|
const ranges = []
|
|
const seekable = this.player.buffered || []
|
|
|
|
let offset = 0
|
|
|
|
for (let i = 0, length = seekable.length; i < length; i++) {
|
|
let start = seekable.start(i)
|
|
let end = seekable.end(i)
|
|
if (!this.isValidDuration(start)) {
|
|
start = 0
|
|
}
|
|
if (!this.isValidDuration(end)) {
|
|
end = 0
|
|
continue
|
|
}
|
|
|
|
ranges.push({
|
|
start: start + offset,
|
|
end: end + offset
|
|
})
|
|
}
|
|
return ranges
|
|
}
|
|
|
|
getLastBufferedTime() {
|
|
var bufferedRanges = this.getBufferedRanges()
|
|
if (!bufferedRanges.length) return 0
|
|
|
|
var buff = bufferedRanges.find((buff) => buff.start < this.player.currentTime && buff.end > this.player.currentTime)
|
|
if (buff) return buff.end
|
|
|
|
var last = bufferedRanges[bufferedRanges.length - 1]
|
|
return last.end
|
|
}
|
|
} |