630 lines
21 KiB
TypeScript
630 lines
21 KiB
TypeScript
export {}
|
|
|
|
interface ModeSource {
|
|
url: string
|
|
referer: string
|
|
subtitles: SubtitleItem[]
|
|
}
|
|
|
|
interface SubtitleItem {
|
|
lang: string
|
|
url: string
|
|
referer: string
|
|
}
|
|
|
|
interface SkipSegment {
|
|
type: string
|
|
start: number
|
|
end: number
|
|
}
|
|
|
|
const initPlayer = (): void => {
|
|
const container = document.querySelector('[data-video-player]')
|
|
if (!container) return
|
|
|
|
const video = container.querySelector('video') as HTMLVideoElement
|
|
const loading = container.querySelector('[data-loading]') as HTMLElement
|
|
const playPause = container.querySelector('[data-play-pause]') as HTMLButtonElement
|
|
const iconPlay = container.querySelector('[data-icon-play]') as SVGElement
|
|
const iconPause = container.querySelector('[data-icon-pause]') as SVGElement
|
|
const muteBtn = container.querySelector('[data-mute]') as HTMLButtonElement
|
|
const volumeWrap = container.querySelector('[data-volume-wrap]') as HTMLElement
|
|
const volumeRange = container.querySelector('[data-volume-range]') as HTMLInputElement
|
|
const iconVolume = container.querySelector('[data-icon-volume]') as SVGElement
|
|
const iconMuted = container.querySelector('[data-icon-muted]') as SVGElement
|
|
const timeDisplay = container.querySelector('[data-time]') as HTMLElement
|
|
const progressWrap = container.querySelector('[data-progress-wrap]') as HTMLElement
|
|
const progress = container.querySelector('[data-progress]') as HTMLElement
|
|
const scrubber = container.querySelector('[data-scrubber]') as HTMLElement
|
|
const segmentsTrack = container.querySelector('[data-segments]') as HTMLElement
|
|
const subtitleSelect = container.querySelector('[data-subtitle-select]') as HTMLSelectElement
|
|
const backwardBtn = container.querySelector('[data-backward]') as HTMLButtonElement
|
|
const forwardBtn = container.querySelector('[data-forward]') as HTMLButtonElement
|
|
const fullscreenBtn = container.querySelector('[data-fullscreen]') as HTMLButtonElement
|
|
const skipSegmentBtn = container.querySelector('[data-skip]') as HTMLButtonElement
|
|
const subtitleText = container.querySelector('[data-subtitle-text]') as HTMLElement
|
|
|
|
const streamURL = container.getAttribute('data-stream-url') || '/watch/proxy/stream'
|
|
const modeSources = JSON.parse(container.getAttribute('data-mode-sources') || '{}')
|
|
const availableModes = JSON.parse(container.getAttribute('data-available-modes') || '[]')
|
|
const initialMode = container.getAttribute('data-initial-mode') || 'dub'
|
|
const segments = JSON.parse(container.getAttribute('data-segments') || '[]')
|
|
|
|
const maxIntroStartSeconds = 180
|
|
const minOutroStartRatio = 0.5
|
|
const minSegmentDurationSeconds = 20
|
|
const maxSegmentDurationSeconds = 240
|
|
|
|
const parsedSegments = segments
|
|
.map((segment: SkipSegment) => {
|
|
const start = Number(segment.start || 0)
|
|
const end = Number(segment.end || 0)
|
|
if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) {
|
|
return null
|
|
}
|
|
const rawType = String(segment.type || '').toLowerCase()
|
|
const type = rawType === 'ed' || rawType === 'outro' ? 'ed' : 'op'
|
|
return { type, start: Math.max(0, start), end: Math.max(0, end) }
|
|
})
|
|
.filter((s: unknown): s is { type: string, start: number, end: number } => s !== null)
|
|
.sort((a: { start: number }, b: { start: number }) => a.start - b.start)
|
|
|
|
let currentMode = availableModes.includes(initialMode) ? initialMode : (availableModes[0] || 'dub')
|
|
let controlsTimeout: number | undefined
|
|
let isScrubbing = false
|
|
let isHoveringVolume = false
|
|
let lastKnownVolume = 1
|
|
let activeSubtitles: Array<{ start: number, end: number, text: string }> = []
|
|
let currentSubtitleTracks: Array<{ lang: string, label: string, url: string }> = []
|
|
let pendingSeekTime: number | null = null
|
|
let activeSkipSegment: { type: string, start: number, end: number } | null = null
|
|
let activeSegments: Array<{ type: string, start: number, end: number }> = []
|
|
|
|
const streamUrlForMode = (mode: string): string => {
|
|
const modeParam = encodeURIComponent(mode)
|
|
const stateParam = encodeURIComponent(JSON.stringify(modeSources))
|
|
return `${streamURL}?mode=${modeParam}&state=${stateParam}`
|
|
}
|
|
|
|
const subtitleProxyURL = (track: SubtitleItem): string => {
|
|
if (!track || !track.url) return ''
|
|
let proxied = `/watch/proxy/subtitle?u=${encodeURIComponent(track.url)}`
|
|
if (track.referer) {
|
|
proxied += `&r=${encodeURIComponent(track.referer)}`
|
|
}
|
|
return proxied
|
|
}
|
|
|
|
const subtitlesForMode = (mode: string): Array<{ lang: string, label: string, url: string }> => {
|
|
const modeSource = modeSources[mode]
|
|
if (!modeSource || !Array.isArray(modeSource.subtitles)) return []
|
|
return modeSource.subtitles
|
|
.map((track: SubtitleItem) => ({
|
|
lang: (track.lang || 'unknown').toLowerCase(),
|
|
label: track.lang || 'Unknown',
|
|
url: subtitleProxyURL(track),
|
|
}))
|
|
.filter((track: { url: string }) => track.url !== '')
|
|
}
|
|
|
|
const skipLabel = (segmentType: string): string => segmentType === 'ed' ? 'Skip outro' : 'Skip intro'
|
|
|
|
const resolveActiveSegments = (): void => {
|
|
if (!Number.isFinite(video.duration) || video.duration <= 0) {
|
|
activeSegments = []
|
|
return
|
|
}
|
|
activeSegments = parsedSegments.filter((segment: { start: number, end: number, type: string }) => {
|
|
const start = segment.start
|
|
const end = segment.end
|
|
const segmentDuration = end - start
|
|
if (segmentDuration < minSegmentDurationSeconds || segmentDuration > maxSegmentDurationSeconds) return false
|
|
if (start < 0 || end <= start || end > video.duration + 1) return false
|
|
if (segment.type === 'op') {
|
|
if (start > maxIntroStartSeconds) return false
|
|
if (start > video.duration * 0.5) return false
|
|
return true
|
|
}
|
|
if (segment.type === 'ed') {
|
|
return start >= video.duration * minOutroStartRatio
|
|
}
|
|
return false
|
|
})
|
|
}
|
|
|
|
const skipActivationTime = (segment: { start: number, end: number }): number => {
|
|
const length = Math.max(0, segment.end - segment.start)
|
|
const delay = Math.min(1, Math.max(0.25, length * 0.02))
|
|
const boundedDelay = Math.min(delay, length * 0.5)
|
|
return segment.start + boundedDelay
|
|
}
|
|
|
|
const updateSkipButton = (currentTime: number): void => {
|
|
const segment = activeSegments.find((item: { start: number, end: number }) => {
|
|
const activationTime = skipActivationTime(item)
|
|
return currentTime >= activationTime && currentTime < item.end
|
|
})
|
|
if (!segment) {
|
|
activeSkipSegment = null
|
|
skipSegmentBtn?.classList.add('hidden')
|
|
skipSegmentBtn?.classList.remove('block')
|
|
return
|
|
}
|
|
activeSkipSegment = segment
|
|
if (skipSegmentBtn) {
|
|
skipSegmentBtn.textContent = skipLabel(segment.type)
|
|
skipSegmentBtn.title = skipLabel(segment.type)
|
|
skipSegmentBtn.classList.remove('hidden')
|
|
skipSegmentBtn.classList.add('block')
|
|
}
|
|
}
|
|
|
|
const renderSegments = (): void => {
|
|
if (!segmentsTrack) return
|
|
segmentsTrack.innerHTML = ''
|
|
if (!video.duration || !Number.isFinite(video.duration)) return
|
|
activeSegments.forEach((segment: { start: number, end: number }) => {
|
|
const left = (segment.start / video.duration) * 100
|
|
const width = ((segment.end - segment.start) / video.duration) * 100
|
|
const bar = document.createElement('div')
|
|
bar.className = 'absolute top-0 h-full bg-yellow-400'
|
|
bar.style.left = `${left}%`
|
|
bar.style.width = `${width}%`
|
|
segmentsTrack.appendChild(bar)
|
|
})
|
|
}
|
|
|
|
const formatTime = (seconds: number): string => {
|
|
if (!Number.isFinite(seconds) || seconds < 0) return '00:00'
|
|
const mins = Math.floor(seconds / 60)
|
|
const secs = Math.floor(seconds % 60)
|
|
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
|
|
}
|
|
|
|
const updateTimeline = (currentTime: number): void => {
|
|
if (!timeDisplay || !progress) return
|
|
if (!video.duration || !Number.isFinite(video.duration)) {
|
|
progress.style.width = '0%'
|
|
if (scrubber) scrubber.style.left = '0%'
|
|
timeDisplay.textContent = `00:00 / 00:00`
|
|
return
|
|
}
|
|
const pct = Math.max(0, Math.min(100, (currentTime / video.duration) * 100))
|
|
progress.style.width = `${pct}%`
|
|
if (scrubber) scrubber.style.left = `${pct}%`
|
|
timeDisplay.textContent = `${formatTime(currentTime)} / ${formatTime(video.duration)}`
|
|
}
|
|
|
|
const seekBy = (delta: number): void => {
|
|
if (!Number.isFinite(video.duration)) return
|
|
const next = Math.max(0, Math.min(video.duration, video.currentTime + delta))
|
|
video.currentTime = next
|
|
updateTimeline(video.currentTime)
|
|
updateSkipButton(video.currentTime)
|
|
showControls()
|
|
}
|
|
|
|
const parseVttTime = (raw: string): number => {
|
|
const parts = raw.trim().split(':')
|
|
if (parts.length < 2) return 0
|
|
const secPart = parts.pop() || '0'
|
|
const minPart = parts.pop() || '0'
|
|
const hourPart = parts.pop() || '0'
|
|
const seconds = Number(secPart.replace(',', '.'))
|
|
const minutes = Number(minPart)
|
|
const hours = Number(hourPart)
|
|
return (hours * 3600) + (minutes * 60) + seconds
|
|
}
|
|
|
|
const parseVtt = (text: string): Array<{ start: number, end: number, text: string }> => {
|
|
const lines = text.replace(/\r/g, '').split('\n')
|
|
const cues: Array<{ start: number, end: number, text: string }> = []
|
|
let i = 0
|
|
while (i < lines.length) {
|
|
const line = lines[i].trim()
|
|
if (!line) { i += 1; continue }
|
|
let timeLine = line
|
|
if (!line.includes('-->') && i + 1 < lines.length) {
|
|
timeLine = lines[i + 1].trim()
|
|
i += 1
|
|
}
|
|
if (!timeLine.includes('-->')) { i += 1; continue }
|
|
const [startRaw, endRaw] = timeLine.split('-->')
|
|
const start = parseVttTime(startRaw)
|
|
const end = parseVttTime(endRaw)
|
|
i += 1
|
|
const payload: string[] = []
|
|
while (i < lines.length && lines[i].trim() !== '') {
|
|
payload.push(lines[i])
|
|
i += 1
|
|
}
|
|
const textContent = payload.join('\n').replace(/<[^>]+>/g, '').trim()
|
|
if (textContent) cues.push({ start, end, text: textContent })
|
|
}
|
|
return cues
|
|
}
|
|
|
|
const loadSubtitle = async (url: string): Promise<Array<{ start: number, end: number, text: string }>> => {
|
|
try {
|
|
const response = await fetch(url)
|
|
if (!response.ok) return []
|
|
const text = await response.text()
|
|
return parseVtt(text)
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
const updateSubtitleRender = (currentTime: number): void => {
|
|
if (!subtitleText) return
|
|
if (!activeSubtitles.length) {
|
|
subtitleText.textContent = ''
|
|
subtitleText.classList.remove('block')
|
|
subtitleText.classList.add('hidden')
|
|
return
|
|
}
|
|
const cue = activeSubtitles.find(item => currentTime >= item.start && currentTime <= item.end)
|
|
if (!cue) {
|
|
subtitleText.textContent = ''
|
|
subtitleText.classList.remove('block')
|
|
subtitleText.classList.add('hidden')
|
|
return
|
|
}
|
|
subtitleText.textContent = cue.text
|
|
subtitleText.classList.remove('hidden')
|
|
subtitleText.classList.add('block')
|
|
}
|
|
|
|
const updateSubtitleOptions = (): void => {
|
|
if (!subtitleSelect) return
|
|
currentSubtitleTracks = subtitlesForMode(currentMode)
|
|
subtitleSelect.innerHTML = ''
|
|
const none = document.createElement('option')
|
|
none.value = 'none'
|
|
none.textContent = 'Off'
|
|
subtitleSelect.appendChild(none)
|
|
subtitleSelect.value = 'none'
|
|
currentSubtitleTracks.forEach((track, idx) => {
|
|
const option = document.createElement('option')
|
|
option.value = String(idx)
|
|
option.textContent = track.label
|
|
subtitleSelect.appendChild(option)
|
|
})
|
|
subtitleSelect.style.display = currentSubtitleTracks.length > 0 ? 'block' : 'none'
|
|
activeSubtitles = []
|
|
if (subtitleText) {
|
|
subtitleText.textContent = ''
|
|
subtitleText.classList.remove('block')
|
|
subtitleText.classList.add('hidden')
|
|
}
|
|
}
|
|
|
|
const modeDub = container.querySelector('[data-mode-dub]') as HTMLButtonElement
|
|
const modeSub = container.querySelector('[data-mode-sub]') as HTMLButtonElement
|
|
|
|
const updateModeButtons = (mode: string): void => {
|
|
if (modeDub) {
|
|
modeDub.disabled = !availableModes.includes('dub')
|
|
modeDub.classList.toggle('text-white', mode !== 'dub')
|
|
modeDub.classList.toggle('text-yellow-400', mode === 'dub')
|
|
modeDub.classList.toggle('opacity-50', !availableModes.includes('dub'))
|
|
modeDub.classList.toggle('cursor-not-allowed', !availableModes.includes('dub'))
|
|
}
|
|
if (modeSub) {
|
|
modeSub.disabled = !availableModes.includes('sub')
|
|
modeSub.classList.toggle('text-white', mode !== 'sub')
|
|
modeSub.classList.toggle('text-yellow-400', mode === 'sub')
|
|
modeSub.classList.toggle('opacity-50', !availableModes.includes('sub'))
|
|
modeSub.classList.toggle('cursor-not-allowed', !availableModes.includes('sub'))
|
|
}
|
|
}
|
|
|
|
const switchMode = (mode: string): void => {
|
|
if (!availableModes.includes(mode) || mode === currentMode) return
|
|
const wasPlaying = !video.paused
|
|
const previousTime = video.currentTime
|
|
currentMode = mode
|
|
video.src = streamUrlForMode(currentMode)
|
|
video.load()
|
|
pendingSeekTime = previousTime
|
|
if (wasPlaying) video.play().catch(() => {})
|
|
updateSubtitleOptions()
|
|
updateModeButtons(currentMode)
|
|
}
|
|
|
|
const updatePlayPauseIcons = (isPlaying: boolean): void => {
|
|
if (iconPlay && iconPause) {
|
|
if (isPlaying) {
|
|
iconPlay.classList.add('hidden')
|
|
iconPause.classList.remove('hidden')
|
|
} else {
|
|
iconPlay.classList.remove('hidden')
|
|
iconPause.classList.add('hidden')
|
|
}
|
|
}
|
|
}
|
|
|
|
const updateMuteIcons = (isMuted: boolean): void => {
|
|
if (iconVolume && iconMuted) {
|
|
if (isMuted) {
|
|
iconVolume.classList.add('hidden')
|
|
iconMuted.classList.remove('hidden')
|
|
} else {
|
|
iconVolume.classList.remove('hidden')
|
|
iconMuted.classList.add('hidden')
|
|
}
|
|
}
|
|
}
|
|
|
|
const syncVolumeUI = (): void => {
|
|
if (volumeRange) {
|
|
const volumeValue = video.muted ? 0 : Math.round(video.volume * 100)
|
|
volumeRange.value = String(volumeValue)
|
|
}
|
|
if (!video.muted && video.volume > 0) {
|
|
lastKnownVolume = video.volume
|
|
}
|
|
updateMuteIcons(video.muted || video.volume === 0)
|
|
}
|
|
|
|
const toggleDub = (): void => {
|
|
if (availableModes.includes('dub')) {
|
|
switchMode('dub')
|
|
}
|
|
showControls()
|
|
}
|
|
|
|
const toggleSub = (): void => {
|
|
if (availableModes.includes('sub')) {
|
|
switchMode('sub')
|
|
}
|
|
showControls()
|
|
}
|
|
|
|
const showControls = (): void => {
|
|
container.classList.add('show-controls')
|
|
window.clearTimeout(controlsTimeout)
|
|
controlsTimeout = window.setTimeout(() => {
|
|
if (!isScrubbing && !isHoveringVolume && !video.paused) {
|
|
container.classList.remove('show-controls')
|
|
}
|
|
}, 2000)
|
|
}
|
|
|
|
// Initialize
|
|
updateSubtitleOptions()
|
|
updateModeButtons(currentMode)
|
|
|
|
if (video) {
|
|
video.src = streamUrlForMode(currentMode)
|
|
|
|
video.addEventListener('loadedmetadata', () => {
|
|
if (loading) loading.style.display = 'none'
|
|
resolveActiveSegments()
|
|
renderSegments()
|
|
if (pendingSeekTime !== null && Number.isFinite(pendingSeekTime)) {
|
|
try {
|
|
video.currentTime = pendingSeekTime
|
|
} catch {}
|
|
pendingSeekTime = null
|
|
}
|
|
updateTimeline(video.currentTime)
|
|
updateSkipButton(video.currentTime)
|
|
})
|
|
|
|
video.addEventListener('waiting', () => {
|
|
if (loading) loading.style.display = 'flex'
|
|
})
|
|
|
|
video.addEventListener('playing', () => {
|
|
if (loading) loading.style.display = 'none'
|
|
})
|
|
|
|
video.addEventListener('timeupdate', () => {
|
|
updateTimeline(video.currentTime)
|
|
updateSubtitleRender(video.currentTime)
|
|
updateSkipButton(video.currentTime)
|
|
})
|
|
|
|
video.addEventListener('play', () => {
|
|
updatePlayPauseIcons(true)
|
|
showControls()
|
|
})
|
|
|
|
video.addEventListener('pause', () => {
|
|
updatePlayPauseIcons(false)
|
|
showControls()
|
|
})
|
|
|
|
video.addEventListener('volumechange', () => {
|
|
syncVolumeUI()
|
|
})
|
|
|
|
video.addEventListener('ended', () => {
|
|
goToNextEpisode()
|
|
})
|
|
}
|
|
|
|
const goToNextEpisode = (): void => {
|
|
const pathParts = window.location.pathname.split('/')
|
|
if (pathParts.length < 4) return
|
|
|
|
const animeID = pathParts[2]
|
|
const currentEpisode = Number.parseInt(pathParts[3], 10)
|
|
if (Number.isNaN(currentEpisode)) return
|
|
|
|
const nextEpisode = currentEpisode + 1
|
|
const nextUrl = `/watch/${animeID}/${nextEpisode}`
|
|
|
|
window.location.href = nextUrl
|
|
}
|
|
|
|
playPause?.addEventListener('click', () => {
|
|
if (video.paused) {
|
|
video.play()
|
|
} else {
|
|
video.pause()
|
|
}
|
|
showControls()
|
|
})
|
|
|
|
video.addEventListener('click', () => {
|
|
if (video.paused) {
|
|
video.play()
|
|
} else {
|
|
video.pause()
|
|
}
|
|
showControls()
|
|
})
|
|
|
|
muteBtn?.addEventListener('click', () => {
|
|
if (video.muted || video.volume === 0) {
|
|
const restoredVolume = lastKnownVolume > 0 ? lastKnownVolume : 1
|
|
video.muted = false
|
|
video.volume = restoredVolume
|
|
} else {
|
|
lastKnownVolume = video.volume > 0 ? video.volume : lastKnownVolume
|
|
video.muted = true
|
|
}
|
|
showControls()
|
|
})
|
|
|
|
volumeRange?.addEventListener('input', () => {
|
|
const sliderValue = Number(volumeRange.value)
|
|
if (!Number.isFinite(sliderValue)) return
|
|
const nextVolume = Math.max(0, Math.min(1, sliderValue / 100))
|
|
video.volume = nextVolume
|
|
video.muted = nextVolume === 0
|
|
if (nextVolume > 0) {
|
|
lastKnownVolume = nextVolume
|
|
}
|
|
showControls()
|
|
})
|
|
|
|
volumeWrap?.addEventListener('mouseenter', () => {
|
|
isHoveringVolume = true
|
|
showControls()
|
|
})
|
|
|
|
volumeWrap?.addEventListener('mouseleave', () => {
|
|
isHoveringVolume = false
|
|
showControls()
|
|
})
|
|
|
|
volumeWrap?.addEventListener('focusin', () => {
|
|
isHoveringVolume = true
|
|
showControls()
|
|
})
|
|
|
|
volumeWrap?.addEventListener('focusout', () => {
|
|
isHoveringVolume = false
|
|
showControls()
|
|
})
|
|
|
|
backwardBtn?.addEventListener('click', () => seekBy(-10))
|
|
forwardBtn?.addEventListener('click', () => seekBy(10))
|
|
|
|
fullscreenBtn?.addEventListener('click', () => {
|
|
if (document.fullscreenElement) {
|
|
document.exitFullscreen()
|
|
} else {
|
|
container.requestFullscreen()
|
|
}
|
|
showControls()
|
|
})
|
|
|
|
skipSegmentBtn?.addEventListener('click', () => {
|
|
if (!activeSkipSegment) return
|
|
const target = activeSkipSegment.end + 0.01
|
|
if (Number.isFinite(video.duration)) {
|
|
video.currentTime = Math.min(video.duration, target)
|
|
} else {
|
|
video.currentTime = target
|
|
}
|
|
updateTimeline(video.currentTime)
|
|
updateSkipButton(video.currentTime)
|
|
showControls()
|
|
})
|
|
|
|
modeDub?.addEventListener('click', toggleDub)
|
|
modeSub?.addEventListener('click', toggleSub)
|
|
|
|
subtitleSelect?.addEventListener('change', async () => {
|
|
const selected = subtitleSelect.value
|
|
if (selected === 'none') {
|
|
activeSubtitles = []
|
|
if (subtitleText) {
|
|
subtitleText.textContent = ''
|
|
subtitleText.classList.remove('block')
|
|
subtitleText.classList.add('hidden')
|
|
}
|
|
return
|
|
}
|
|
const idx = Number(selected)
|
|
const track = currentSubtitleTracks[idx]
|
|
if (!track) {
|
|
activeSubtitles = []
|
|
return
|
|
}
|
|
activeSubtitles = await loadSubtitle(track.url)
|
|
})
|
|
|
|
progressWrap?.addEventListener('mousedown', (event) => {
|
|
isScrubbing = true
|
|
const rect = progressWrap.getBoundingClientRect()
|
|
const ratio = Math.max(0, Math.min(1, ((event as MouseEvent).clientX - rect.left) / rect.width))
|
|
if (Number.isFinite(video.duration)) {
|
|
video.currentTime = ratio * video.duration
|
|
}
|
|
updateTimeline(video.currentTime)
|
|
updateSkipButton(video.currentTime)
|
|
showControls()
|
|
})
|
|
|
|
window.addEventListener('mouseup', () => {
|
|
isScrubbing = false
|
|
})
|
|
|
|
window.addEventListener('mousemove', (event) => {
|
|
if (!isScrubbing || !progressWrap) return
|
|
const rect = progressWrap.getBoundingClientRect()
|
|
const ratio = Math.max(0, Math.min(1, (event.clientX - rect.left) / rect.width))
|
|
if (Number.isFinite(video.duration)) {
|
|
video.currentTime = ratio * video.duration
|
|
}
|
|
updateTimeline(video.currentTime)
|
|
updateSkipButton(video.currentTime)
|
|
})
|
|
|
|
container.addEventListener('mousemove', showControls)
|
|
|
|
document.addEventListener('keydown', (event) => {
|
|
if (event.code === 'Space') {
|
|
event.preventDefault()
|
|
if (video.paused) {
|
|
video.play()
|
|
} else {
|
|
video.pause()
|
|
}
|
|
}
|
|
if (event.code === 'ArrowLeft') seekBy(-10)
|
|
if (event.code === 'ArrowRight') seekBy(10)
|
|
if (event.code === 'KeyM') video.muted = !video.muted
|
|
if (event.code === 'KeyF') {
|
|
if (document.fullscreenElement) {
|
|
document.exitFullscreen()
|
|
} else {
|
|
container.requestFullscreen()
|
|
}
|
|
}
|
|
showControls()
|
|
})
|
|
|
|
updatePlayPauseIcons(false)
|
|
syncVolumeUI()
|
|
updateSkipButton(0)
|
|
showControls()
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', initPlayer)
|