Testing video media type

This commit is contained in:
advplyr 2022-05-30 19:26:53 -05:00
parent 705aac40d7
commit acf22ca4fa
27 changed files with 1124 additions and 92 deletions

View file

@ -1,7 +1,7 @@
import Hls from 'hls.js'
import EventEmitter from 'events'
export default class LocalPlayer extends EventEmitter {
export default class LocalAudioPlayer extends EventEmitter {
constructor(ctx) {
super()

View file

@ -0,0 +1,257 @@
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 bottom-20 left-4 w-96 bg-black z-50'
videoEl.style.height = '216px'
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
}
}

View file

@ -1,6 +1,8 @@
import LocalPlayer from './LocalPlayer'
import LocalAudioPlayer from './LocalAudioPlayer'
import LocalVideoPlayer from './LocalVideoPlayer'
import CastPlayer from './CastPlayer'
import AudioTrack from './AudioTrack'
import VideoTrack from './VideoTrack'
export default class PlayerHandler {
constructor(ctx) {
@ -14,6 +16,7 @@ export default class PlayerHandler {
this.player = null
this.playerState = 'IDLE'
this.isHlsTranscode = false
this.isVideo = false
this.currentSessionId = null
this.startTime = 0
@ -34,7 +37,7 @@ export default class PlayerHandler {
return this.libraryItem && (this.player instanceof CastPlayer)
}
get isPlayingLocalItem() {
return this.libraryItem && (this.player instanceof LocalPlayer)
return this.libraryItem && (this.player instanceof LocalAudioPlayer)
}
get userToken() {
return this.ctx.$store.getters['user/getToken']
@ -48,16 +51,17 @@ export default class PlayerHandler {
}
load(libraryItem, episodeId, playWhenReady, playbackRate) {
if (!this.player) this.switchPlayer()
this.libraryItem = libraryItem
this.episodeId = episodeId
this.playWhenReady = playWhenReady
this.initialPlaybackRate = playbackRate
this.prepare()
this.isVideo = libraryItem.mediaType === 'video'
if (!this.player) this.switchPlayer(playWhenReady)
else this.prepare()
}
switchPlayer() {
switchPlayer(playWhenReady) {
if (this.isCasting && !(this.player instanceof CastPlayer)) {
console.log('[PlayerHandler] Switching to cast player')
@ -73,10 +77,10 @@ export default class PlayerHandler {
if (this.libraryItem) {
// libraryItem was already loaded - prepare for cast
this.playWhenReady = false
this.playWhenReady = playWhenReady
this.prepare()
}
} else if (!this.isCasting && !(this.player instanceof LocalPlayer)) {
} else if (!this.isCasting && !(this.player instanceof LocalAudioPlayer) && !(this.player instanceof LocalVideoPlayer)) {
console.log('[PlayerHandler] Switching to local player')
this.stopPlayInterval()
@ -85,12 +89,18 @@ export default class PlayerHandler {
if (this.player) {
this.player.destroy()
}
this.player = new LocalPlayer(this.ctx)
if (this.isVideo) {
this.player = new LocalVideoPlayer(this.ctx)
} else {
this.player = new LocalAudioPlayer(this.ctx)
}
this.setPlayerListeners()
if (this.libraryItem) {
// libraryItem was already loaded - prepare for local play
this.playWhenReady = false
this.playWhenReady = playWhenReady
this.prepare()
}
}
@ -106,7 +116,7 @@ export default class PlayerHandler {
playerError() {
// Switch to HLS stream on error
if (!this.isCasting && !this.currentStreamId && (this.player instanceof LocalPlayer)) {
if (!this.isCasting && !this.currentStreamId && (this.player instanceof LocalAudioPlayer)) {
console.log(`[PlayerHandler] Audio player error switching to HLS stream`)
this.prepare(true)
}
@ -155,7 +165,7 @@ export default class PlayerHandler {
supportedMimeTypes: this.player.playableMimeTypes,
mediaPlayer: this.isCasting ? 'chromecast' : 'html5',
forceTranscode,
forceDirectPlay: this.isCasting // TODO: add transcode support for chromecast
forceDirectPlay: this.isCasting || this.isVideo // TODO: add transcode support for chromecast
}
var path = this.episodeId ? `/api/items/${this.libraryItem.id}/play/${this.episodeId}` : `/api/items/${this.libraryItem.id}/play`
@ -166,11 +176,12 @@ export default class PlayerHandler {
}
prepareOpenSession(session, playbackRate) { // Session opened on init socket
if (!this.player) this.switchPlayer()
this.libraryItem = session.libraryItem
this.isVideo = session.libraryItem.mediaType === 'video'
this.playWhenReady = false
this.initialPlaybackRate = playbackRate
if (!this.player) this.switchPlayer()
this.prepareSession(session)
}
@ -181,16 +192,29 @@ export default class PlayerHandler {
this.displayAuthor = session.displayAuthor
console.log('[PlayerHandler] Preparing Session', session)
var audioTracks = session.audioTracks.map(at => new AudioTrack(at, this.userToken))
this.ctx.playerLoading = true
this.isHlsTranscode = true
if (session.playMethod === this.ctx.$constants.PlayMethod.DIRECTPLAY) {
this.isHlsTranscode = false
if (session.videoTrack) {
var videoTrack = new VideoTrack(session.videoTrack, this.userToken)
this.ctx.playerLoading = true
this.isHlsTranscode = true
if (session.playMethod === this.ctx.$constants.PlayMethod.DIRECTPLAY) {
this.isHlsTranscode = false
}
this.player.set(this.libraryItem, videoTrack, this.isHlsTranscode, this.startTime, this.playWhenReady)
} else {
var audioTracks = session.audioTracks.map(at => new AudioTrack(at, this.userToken))
this.ctx.playerLoading = true
this.isHlsTranscode = true
if (session.playMethod === this.ctx.$constants.PlayMethod.DIRECTPLAY) {
this.isHlsTranscode = false
}
this.player.set(this.libraryItem, audioTracks, this.isHlsTranscode, this.startTime, this.playWhenReady)
}
this.player.set(this.libraryItem, audioTracks, this.isHlsTranscode, this.startTime, this.playWhenReady)
// browser media session api
this.ctx.setMediaSession()
}

View file

@ -0,0 +1,32 @@
export default class VideoTrack {
constructor(track, userToken) {
this.index = track.index || 0
this.startOffset = track.startOffset || 0 // Total time of all previous tracks
this.duration = track.duration || 0
this.title = track.title || ''
this.contentUrl = track.contentUrl || null
this.mimeType = track.mimeType
this.metadata = track.metadata || {}
this.userToken = userToken
}
get fullContentUrl() {
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
if (process.env.NODE_ENV === 'development') {
return `${process.env.serverUrl}${this.contentUrl}?token=${this.userToken}`
}
return `${window.location.origin}${this.contentUrl}?token=${this.userToken}`
}
get relativeContentUrl() {
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
if (process.env.NODE_ENV === 'development') {
return `${process.env.serverUrl}${this.contentUrl}?token=${this.userToken}`
}
return this.contentUrl + `?token=${this.userToken}`
}
}