import validator from 'option-validator'
import scheme from './scheme'
import {
append,
assToVtt,
createElement,
escape,
getExt,
remove,
setStyle,
setStyles,
srtToVtt,
vttToBlob,
} from './utils'
import Component from './utils/component'
export default class Subtitle extends Component {
constructor(art) {
super(art)
this.name = 'subtitle'
this.option = null
this.destroyEvent = () => null
this.init(art.option.subtitle)
let lastState = false
art.on('video:timeupdate', () => {
if (!this.url)
return
const state = this.art.template.$video.webkitDisplayingFullscreen
if (typeof state !== 'boolean')
return
if (state !== lastState) {
lastState = state
this.createTrack(state ? 'subtitles' : 'metadata', this.url)
}
})
}
get url() {
return this.art.template.$track.src
}
set url(url) {
this.switch(url)
}
get textTrack() {
return this.art.template.$video?.textTracks?.[0]
}
get activeCues() {
if (!this.textTrack)
return []
return Array.from(this.textTrack.activeCues)
}
get cues() {
if (!this.textTrack)
return []
return Array.from(this.textTrack.cues)
}
style(key, value) {
const { $subtitle } = this.art.template
if (typeof key === 'object') {
return setStyles($subtitle, key)
}
return setStyle($subtitle, key, value)
}
update() {
const {
option: { subtitle },
template: { $subtitle },
} = this.art
$subtitle.innerHTML = ''
if (!this.activeCues.length)
return
this.art.emit('subtitleBeforeUpdate', this.activeCues)
$subtitle.innerHTML = this.activeCues
.map((cue, index) =>
cue.text
.split(/\r?\n/)
.filter(line => line.trim())
.map(
line =>
`
${subtitle.escape ? escape(line) : line}
`,
)
.join(''),
)
.join('')
this.art.emit('subtitleAfterUpdate', this.activeCues)
}
async switch(url, newOption = {}) {
const { i18n, notice, option } = this.art
const subtitleOption = { ...option.subtitle, ...newOption, url }
const subUrl = await this.init(subtitleOption)
if (newOption.name) {
notice.show = `${i18n.get('Switch Subtitle')}: ${newOption.name}`
}
return subUrl
}
createTrack(kind, url) {
const { template, proxy, option } = this.art
const { $video, $track } = template
const $newTrack = createElement('track')
$newTrack.default = true
$newTrack.kind = kind
$newTrack.src = url
$newTrack.label = option.subtitle.name || 'Artplayer'
$newTrack.track.mode = 'hidden'
$newTrack.onload = () => {
this.art.emit('subtitleLoad', this.cues, this.option)
}
this.art.events.remove(this.destroyEvent)
$track.onload = null
remove($track)
append($video, $newTrack)
template.$track = $newTrack
this.destroyEvent = proxy(this.textTrack, 'cuechange', () => this.update())
}
async init(subtitleOption) {
const {
notice,
template: { $subtitle },
} = this.art
if (!this.textTrack)
return null
validator(subtitleOption, scheme.subtitle)
if (!subtitleOption.url)
return
this.option = subtitleOption
this.style(subtitleOption.style)
return fetch(subtitleOption.url)
.then(response => response.arrayBuffer())
.then((buffer) => {
const decoder = new TextDecoder(subtitleOption.encoding)
const text = decoder.decode(buffer)
switch (subtitleOption.type || getExt(subtitleOption.url)) {
case 'srt': {
const vtt = srtToVtt(text)
const vttNew = subtitleOption.onVttLoad(vtt)
return vttToBlob(vttNew)
}
case 'ass': {
const vtt = assToVtt(text)
const vttNew = subtitleOption.onVttLoad(vtt)
return vttToBlob(vttNew)
}
case 'vtt': {
const vttNew = subtitleOption.onVttLoad(text)
return vttToBlob(vttNew)
}
default:
return subtitleOption.url
}
})
.then((subUrl) => {
$subtitle.innerHTML = ''
if (this.url === subUrl)
return subUrl
URL.revokeObjectURL(this.url)
this.createTrack('metadata', subUrl)
return subUrl
})
.catch((err) => {
$subtitle.innerHTML = ''
notice.show = err
throw err
})
}
}