mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-08-17 16:47:59 +00:00
first pass at OOP player
This commit is contained in:
parent
7261c95865
commit
4b070bb573
11 changed files with 1973 additions and 1968 deletions
|
|
@ -219,6 +219,7 @@ video[src=""],
|
|||
}
|
||||
|
||||
#dl[disabled] {
|
||||
display:block !important;
|
||||
cursor: not-allowed;
|
||||
color: rgba(255, 255, 255, .4);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -189,8 +189,8 @@
|
|||
<span class="ts" id="upSpeed">0 B/s</span>
|
||||
</div>
|
||||
<div class="col-4 d-flex justify-content-end">
|
||||
<span id="dl" class="material-icons pointer"
|
||||
title="Wait For File To Fully Download Before Saving To Drive" disabled>
|
||||
<span id="dl" class="material-icons pointer ctrl"
|
||||
title="Wait For File To Fully Download Before Saving To Drive" disabled data-name="downloadFile">
|
||||
get_app
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -200,24 +200,24 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="controls d-flex">
|
||||
<span class="material-icons ctrl" title="Play/Pause [Space]" id="bpp" data-name="btnpp">
|
||||
<span class="material-icons ctrl" title="Play/Pause [Space]" id="bpp" data-name="playPause">
|
||||
play_arrow
|
||||
</span>
|
||||
<span class="material-icons ctrl" title="Next [N]" id="bnext" data-name="btnnext">
|
||||
<span class="material-icons ctrl" title="Next [N]" id="bnext" data-name="playNext">
|
||||
skip_next
|
||||
</span>
|
||||
<span class="material-icons ctrl" title="Playlist [P]" id="bpl" data-name="btnpl">
|
||||
<span class="material-icons ctrl" title="Playlist [P]" id="bpl" data-name="openPlaylist">
|
||||
playlist_play
|
||||
</span>
|
||||
<div class="volume">
|
||||
<span class="material-icons ctrl" title="Mute [M]" id="bmute" data-name="btnmute">
|
||||
<span class="material-icons ctrl" title="Mute [M]" id="bmute" data-name="toggleMute">
|
||||
volume_up
|
||||
</span>
|
||||
<input type="range" value="100" id="volume" step="any">
|
||||
<input class="ctrl" type="range" value="100" id="volume" step="any" data-name="setVolume">
|
||||
</div>
|
||||
<div class="audio-tracks dropdown dropup with-arrow">
|
||||
<span class="material-icons ctrl" title="Audio Tracks [T]" id="baudio"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" data-name="btnaudio"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" data-name="showAudio"
|
||||
disabled>
|
||||
queue_music
|
||||
</span>
|
||||
|
|
@ -226,25 +226,25 @@
|
|||
</div>
|
||||
<span class="ts" id="elapsed">00:00</span>
|
||||
<div class="prog">
|
||||
<input type="range" min="0" max="100" value="0" id="progress" step="any">
|
||||
<input class="ctrl" type="range" min="0" max="100" value="0" id="progress" step="any" data-name="setProgress">
|
||||
<img id="thumb">
|
||||
</div>
|
||||
<span class="ts" id="remaining">00:00</span>
|
||||
<div class="subtitles dropdown dropup with-arrow">
|
||||
<span class="material-icons ctrl" title="Subtitles [C]" id="bcap" data-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false" data-name="btncap" disabled>
|
||||
aria-haspopup="true" aria-expanded="false" data-name="showCaptions" disabled>
|
||||
subtitles
|
||||
</span>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="bcap" id="subMenu">
|
||||
</div>
|
||||
</div>
|
||||
<span class="material-icons ctrl" title="Popout Window [P]" id="bpip" data-name="btnpip">
|
||||
<span class="material-icons ctrl" title="Popout Window [P]" id="bpip" data-name="togglePopout">
|
||||
picture_in_picture
|
||||
</span>
|
||||
<span class="material-icons ctrl" title="Theatre Mode [T]" id="btheatre" data-name="btntheatre">
|
||||
<span class="material-icons ctrl" title="Theatre Mode [T]" id="btheatre" data-name="toggleTheatre">
|
||||
crop_16_9
|
||||
</span>
|
||||
<span class="material-icons ctrl" title="Fullscreen [F]" id="bfull" data-name="btnfull">
|
||||
<span class="material-icons ctrl" title="Fullscreen [F]" id="bfull" data-name="toggleFullscreen">
|
||||
fullscreen
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -730,8 +730,8 @@
|
|||
<label for="torrent5">Drive Caching</label>
|
||||
</div>
|
||||
<div class="custom-switch mb-20" data-toggle="tooltip" data-placement="top" data-title="Only Downloads Pieces Directly Required For Playback Instead Of The Entire File. Recommended Only For Low-End Devices">
|
||||
<input type="checkbox" id="torrent6">
|
||||
<label for="torrent6">Streamed Download</label>
|
||||
<input type="checkbox" id="torrent8">
|
||||
<label for="torrent8">Streamed Download</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
|
|
@ -786,13 +786,11 @@
|
|||
<script src="js/webtorrent.min.js"></script>
|
||||
<script src="js/settingsHandler.js"></script>
|
||||
<script src="js/rangeParser.js"></script>
|
||||
<script src="js/subtitles-octopus.js"></script>
|
||||
<script src="js/player.js"></script>
|
||||
<script src="js/util.js"></script>
|
||||
<script src="js/interface.js"></script>
|
||||
<script src="js/animeHandler.js"></script>
|
||||
<script src="js/torrentHandler.js"></script>
|
||||
<script src="js/playerHandler.js"></script>
|
||||
<script src="js/subtitles-octopus.js"></script>
|
||||
<script src="js/subtitleOctopus.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -14,7 +14,7 @@ window.addEventListener('paste', async e => { // WAIT image lookup on paste, or
|
|||
if (torrentRx.exec(text)) {
|
||||
e.preventDefault()
|
||||
search.value = ''
|
||||
addTorrent(text, {})
|
||||
client.addTorrent(text, {})
|
||||
} else if (imageRx.exec(text)) {
|
||||
e.preventDefault()
|
||||
search.value = ''
|
||||
|
|
@ -642,7 +642,7 @@ async function resolveFileMedia (opts) {
|
|||
// episode is still out of bounds
|
||||
const nextEdge = await alRequest({ method: 'SearchIDSingle', id: tempMedia.id })
|
||||
await resolveSeason({ media: nextEdge.data.Media, episode: opts.episode, offset: opts.offset + nextEdge.data.Media.episodes, increment: increment })
|
||||
} else if (tempMedia?.episodes && epMax - (opts.offset + tempMedia.episodes) < (media.episodes || media.nextAiringEpisode.episode) && epMin - (opts.offset + tempMedia.episodes) > 0) {
|
||||
} else if (tempMedia?.episodes && epMax - (opts.offset + tempMedia.episodes) <= (media.episodes || media.nextAiringEpisode.episode) && epMin - (opts.offset + tempMedia.episodes) > 0) {
|
||||
// episode is in range, seems good! overwriting media to count up "seasons"
|
||||
if (opts.episode.constructor === Array) {
|
||||
episode = `${elems.episode_number[0] - (opts.offset + tempMedia.episodes)} - ${elems.episode_number[elems.episode_number.length - 1] - (opts.offset + tempMedia.episodes)}`
|
||||
|
|
@ -654,7 +654,7 @@ async function resolveFileMedia (opts) {
|
|||
media = nextEdge.data.Media
|
||||
}
|
||||
} else {
|
||||
console.log('error in parsing!')
|
||||
console.log('error in parsing!', opts.media, tempMedia)
|
||||
// something failed, most likely couldnt find an edge or processing failed, force episode number even if its invalid/out of bounds, better than nothing
|
||||
if (opts.episode.constructor === Array) {
|
||||
episode = `${Number(elems.episode_number[0])} - ${Number(elems.episode_number[elems.episode_number.length - 1])}`
|
||||
|
|
@ -690,7 +690,8 @@ async function resolveFileMedia (opts) {
|
|||
return { media: media, episode: episode, parseObject: elems }
|
||||
}
|
||||
|
||||
const store = JSON.parse(localStorage.getItem('store')) || {}
|
||||
let store = JSON.parse(localStorage.getItem('store')) || {}
|
||||
store = store || {}
|
||||
|
||||
function getRSSurl () {
|
||||
if (Object.values(torrent4list.options).filter(item => item.value === settings.torrent4)[0]) {
|
||||
|
|
|
|||
1061
app/js/player.js
Normal file
1061
app/js/player.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,698 +0,0 @@
|
|||
const controls = document.getElementsByClassName('ctrl')
|
||||
|
||||
for (const item of controls) {
|
||||
item.addEventListener('click', function () {
|
||||
const func = this.dataset.name
|
||||
window[func]()
|
||||
})
|
||||
}
|
||||
|
||||
// video element shit
|
||||
video.addEventListener('playing', resetBuffer)
|
||||
video.addEventListener('canplay', resetBuffer)
|
||||
video.onloadedmetadata = () => {
|
||||
initThumbnail()
|
||||
updateDisplay();
|
||||
(video.audioTracks && video.audioTracks.length > 1) ? baudio.removeAttribute('disabled') : baudio.setAttribute('disabled', '')
|
||||
}
|
||||
video.onended = () => {
|
||||
updateBar(video.currentTime / video.duration * 100)
|
||||
if (settings.player6 && parseInt(playerData.nowPlaying[1]) < playerData.nowPlaying[0].episodes) btnnext()
|
||||
}
|
||||
video.addEventListener('waiting', isBuffering)
|
||||
video.ontimeupdate = () => {
|
||||
updateDisplay()
|
||||
checkCompletion()
|
||||
if ('setPositionState' in navigator.mediaSession) updatePositionState()
|
||||
}
|
||||
|
||||
if (!('pictureInPictureEnabled' in document)) {
|
||||
video.setAttribute('disablePictureInPicture', '')
|
||||
bpip.setAttribute('disabled', '')
|
||||
} else {
|
||||
bpip.removeAttribute('disabled')
|
||||
video.addEventListener('enterpictureinpicture', () => { if (playerData.octopusInstance) btnpip() })
|
||||
}
|
||||
|
||||
let playerData = {}
|
||||
|
||||
function cleanupVideo () { // cleans up objects, attemps to clear as much video caching as possible
|
||||
if (playerData.octopusInstance) playerData.octopusInstance.dispose()
|
||||
if (playerData.fonts) playerData.fonts.forEach(file => URL.revokeObjectURL(file))
|
||||
if (dl.href) URL.revokeObjectURL(dl.href)
|
||||
dl.setAttribute('disabled', '')
|
||||
dl.onclick = undefined
|
||||
video.poster = ''
|
||||
// some attemt at cache clearing
|
||||
video.pause()
|
||||
video.src = ''
|
||||
video.load()
|
||||
document.title = 'Miru'
|
||||
progress.value = 0
|
||||
// if (typeof client !== 'undefined' && client.torrents[0] && client.torrents[0].files.length > 1) {
|
||||
// client.torrents[0].files.forEach(file => file.deselect());
|
||||
// client.torrents[0].deselect(0, client.torrents[0].pieces.length - 1, false);
|
||||
// console.log(videoFiles.filter(file => `${scope}webtorrent/${client.torrents[0].infoHash}/${encodeURI(file.path)}` == video.src))
|
||||
// look for file and delete its store
|
||||
// }
|
||||
playerData = {
|
||||
subtitles: [],
|
||||
fonts: [],
|
||||
headers: []
|
||||
}
|
||||
nowPlayingDisplay.innerHTML = ''
|
||||
bcap.setAttribute('disabled', '')
|
||||
bpl.setAttribute('disabled', '')
|
||||
bnext.removeAttribute('disabled')
|
||||
navNowPlaying.classList.add('d-none')
|
||||
if ('mediaSession' in navigator) navigator.mediaSession.metadata = undefined
|
||||
}
|
||||
|
||||
async function buildVideo (torrent, opts) { // sets video source and creates a bunch of other media stuff
|
||||
// play wanted episode from opts, or the 1st episode, or 1st file [batches: plays wanted episode, single: plays the only episode, manually added: plays first or only file]
|
||||
let selectedFile = videoFiles[0]
|
||||
if (opts.file) {
|
||||
selectedFile = opts.file
|
||||
} else if (videoFiles.length > 1) {
|
||||
// TODO play selected media too!
|
||||
selectedFile = videoFiles.filter(async file => await anitomyscript(file.name).then(object => Number(object.episode_number) === Number(opts.episode || 1)))[0] || videoFiles[0]
|
||||
}
|
||||
video.src = `${scope}webtorrent/${torrent.infoHash}/${encodeURI(selectedFile.path)}`
|
||||
video.load()
|
||||
// "predict" video FPS for subtitle renderer
|
||||
playerData.fps = new Promise((resolve, reject) => {
|
||||
if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
|
||||
let wasSeeked
|
||||
video.onseeking = function () { wasSeeked = true }
|
||||
video.onplay = () => setTimeout(() => video.requestVideoFrameCallback((now, metadata) => {
|
||||
const rawFPS = metadata.presentedFrames / metadata.mediaTime
|
||||
console.log(rawFPS, metadata)
|
||||
if (!wasSeeked) {
|
||||
if (rawFPS >= 19 && rawFPS <= 26) {
|
||||
resolve(23.976)
|
||||
} else if (rawFPS > 26 && rawFPS <= 35) {
|
||||
resolve(29.97)
|
||||
} else if (rawFPS > 50 && rawFPS <= 70) {
|
||||
resolve(59.94)
|
||||
} else {
|
||||
// smth went VERY wrong XD
|
||||
resolve(23.976)
|
||||
}
|
||||
} else {
|
||||
// video was seeked, cant predict fps
|
||||
resolve(23.976)
|
||||
}
|
||||
video.onseeking = undefined
|
||||
video.onplay = undefined
|
||||
}), 3000)
|
||||
} else {
|
||||
// can't predict fps, API unsupported, assume 24fps
|
||||
resolve(23.976)
|
||||
}
|
||||
})
|
||||
playVideo()
|
||||
|
||||
if (videoFiles.length > 1) bpl.removeAttribute('disabled')
|
||||
|
||||
async function processFile () {
|
||||
halfmoon.initStickyAlert({
|
||||
content: `<span class="text-break">${selectedFile.name}</span> has finished downloading. Now seeding.`,
|
||||
title: 'Download Complete',
|
||||
alertType: 'alert-success',
|
||||
fillType: ''
|
||||
})
|
||||
await postDownload(selectedFile)
|
||||
if (settings.player5) {
|
||||
finishThumbnails(`${scope}webtorrent/${torrent.infoHash}/${encodeURI(selectedFile.path)}`)
|
||||
}
|
||||
if (!torrent.store.store._store) { // only allow download from RAM
|
||||
downloadFile(selectedFile)
|
||||
}
|
||||
}
|
||||
if (selectedFile.done) {
|
||||
processFile()
|
||||
} else {
|
||||
playerData.onDone = selectedFile.on('done', () => {
|
||||
processFile()
|
||||
})
|
||||
}
|
||||
playerData.onProgress = () => {
|
||||
if (document.location.hash === '#player') {
|
||||
if (!player.classList.contains('immersed')) {
|
||||
player.style.setProperty('--download', selectedFile.progress * 100 + '%')
|
||||
peers.innerHTML = torrent.numPeers
|
||||
downSpeed.innerHTML = prettyBytes(torrent.downloadSpeed) + '/s'
|
||||
upSpeed.innerHTML = prettyBytes(torrent.uploadSpeed) + '/s'
|
||||
}
|
||||
}
|
||||
setTimeout(playerData.onProgress, 100)
|
||||
}
|
||||
setTimeout(playerData.onProgress, 100)
|
||||
|
||||
if (opts.media && videoFiles.length === 1) {
|
||||
// if this is a single file, then the media is most likely accurate, just update it!
|
||||
playerData.nowPlaying = [await alRequest({ id: opts.media?.id, method: 'SearchIDSingle' }).then(res => res.data.Media), opts.episode || 1]
|
||||
// update store with entry, but dont really do anything with it
|
||||
resolveFileMedia({ fileName: selectedFile.name, method: 'SearchName' })
|
||||
} else {
|
||||
// if this is a batch or single unresolved file, then resolve the single selected file, batches can include specials
|
||||
const mediaInformation = await resolveFileMedia({ fileName: selectedFile.name, method: 'SearchName' })
|
||||
playerData.nowPlaying = [mediaInformation.media, mediaInformation.episode || 1]
|
||||
}
|
||||
let mediaMetadata
|
||||
// only set mediasession and other shit if the playerdata is parsed correctly
|
||||
if (playerData.nowPlaying[0]) {
|
||||
navNowPlaying.classList.remove('d-none')
|
||||
mediaMetadata = new MediaMetadata({
|
||||
title: playerData.nowPlaying[0].title.userPreferred,
|
||||
artist: `Episode ${Number(playerData.nowPlaying[1])}`,
|
||||
album: 'Miru',
|
||||
artwork: [{
|
||||
src: playerData.nowPlaying[0].coverImage.medium,
|
||||
sizes: '256x256',
|
||||
type: 'image/jpg'
|
||||
}]
|
||||
})
|
||||
if (parseInt(playerData.nowPlaying[1]) >= playerData.nowPlaying[0].episodes) bnext.setAttribute('disabled', '')
|
||||
let streamingEpisode
|
||||
if (playerData.nowPlaying[0].streamingEpisodes.length >= Number(playerData.nowPlaying[1])) {
|
||||
streamingEpisode = playerData.nowPlaying[0].streamingEpisodes.filter(episode => episodeRx.exec(episode.title) && Number(episodeRx.exec(episode.title)[1]) === Number(playerData.nowPlaying[1]))[0]
|
||||
}
|
||||
// TODO: this should also use absolute episode numbers instead of relative but AL will change this anyways....
|
||||
if (streamingEpisode) {
|
||||
video.poster = streamingEpisode.thumbnail
|
||||
document.title = `${playerData.nowPlaying[0].title.userPreferred} - EP ${Number(playerData.nowPlaying[1])} - ${episodeRx.exec(streamingEpisode.title)[2]} - Miru`
|
||||
mediaMetadata.artist = `Episode ${Number(playerData.nowPlaying[1])} - ${episodeRx.exec(streamingEpisode.title)[2]}`
|
||||
mediaMetadata.artwork = [{
|
||||
src: streamingEpisode.thumbnail,
|
||||
sizes: '256x256',
|
||||
type: 'image/jpg'
|
||||
}]
|
||||
nowPlayingDisplay.innerHTML = `EP ${Number(playerData.nowPlaying[1])} - ${episodeRx.exec(streamingEpisode.title)[2]}`
|
||||
} else {
|
||||
document.title = `${playerData.nowPlaying[0].title.userPreferred} - EP ${Number(playerData.nowPlaying[1])} - Miru`
|
||||
nowPlayingDisplay.innerHTML = `EP ${Number(playerData.nowPlaying[1])}`
|
||||
}
|
||||
}
|
||||
if ('mediaSession' in navigator && mediaMetadata) navigator.mediaSession.metadata = mediaMetadata
|
||||
}
|
||||
|
||||
// visibility loss pause
|
||||
if (settings.player10) {
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (!video.ended) document.visibilityState === 'hidden' ? video.pause() : playVideo()
|
||||
})
|
||||
}
|
||||
|
||||
// progress seek bar and display
|
||||
|
||||
progress.addEventListener('input', dragBar)
|
||||
progress.addEventListener('mouseup', dragBarEnd)
|
||||
progress.addEventListener('touchend', dragBarEnd)
|
||||
progress.addEventListener('click', dragBarEnd)
|
||||
progress.addEventListener('mousedown', dragBarStart)
|
||||
|
||||
function updateDisplay () {
|
||||
if (!player.classList.contains('immersed') && document.location.hash === '#player') {
|
||||
progress.style.setProperty('--buffer', video.buffered.length === 0 ? 0 : video.buffered.end(video.buffered.length - 1) / video.duration * 100 + '%')
|
||||
updateBar((video.currentTime / video.duration * 100) || progress.value)
|
||||
}
|
||||
createThumbnail(video)
|
||||
}
|
||||
|
||||
function dragBar () {
|
||||
updateBar(progress.value)
|
||||
thumb.src = playerData.thumbnailData.thumbnails[Math.floor(currentTime / playerData.thumbnailData.interval)] || ' '
|
||||
}
|
||||
|
||||
function dragBarEnd () {
|
||||
video.currentTime = currentTime || 0
|
||||
playVideo()
|
||||
}
|
||||
|
||||
async function dragBarStart () {
|
||||
await video.pause()
|
||||
updateBar(progress.value)
|
||||
}
|
||||
|
||||
let currentTime = 0
|
||||
function updateBar (progressPercent) {
|
||||
progress.style.setProperty('--progress', progressPercent + '%')
|
||||
thumb.style.setProperty('--progress', progressPercent + '%')
|
||||
currentTime = video.duration * progressPercent / 100
|
||||
elapsed.innerHTML = toTS(currentTime)
|
||||
remaining.innerHTML = toTS(video.duration - currentTime)
|
||||
progress.value = progressPercent
|
||||
progress.setAttribute('data-ts', toTS(currentTime))
|
||||
}
|
||||
|
||||
// dynamic thumbnail previews
|
||||
|
||||
function initThumbnail () {
|
||||
const canvas = document.createElement('canvas')
|
||||
playerData.thumbnailData = {
|
||||
canvas: canvas,
|
||||
context: canvas.getContext('2d'),
|
||||
height: parseInt(150 / (video.videoWidth / video.videoHeight)),
|
||||
thumbnails: [],
|
||||
interval: video.duration / 300 < 5 ? 5 : video.duration / 300
|
||||
}
|
||||
canvas.width = 150
|
||||
canvas.height = playerData.thumbnailData.height
|
||||
thumb.style.setProperty('--height', playerData.thumbnailData.height + 'px')
|
||||
}
|
||||
|
||||
function createThumbnail (vid) {
|
||||
if (vid?.readyState >= 2) {
|
||||
const index = Math.floor(vid.currentTime / playerData.thumbnailData.interval)
|
||||
if (!playerData.thumbnailData.thumbnails[index]) {
|
||||
playerData.thumbnailData.context.fillRect(0, 0, 150, playerData.thumbnailData.height)
|
||||
playerData.thumbnailData.context.drawImage(vid, 0, 0, 150, playerData.thumbnailData.height)
|
||||
playerData.thumbnailData.thumbnails[index] = playerData.thumbnailData.canvas.toDataURL('image/jpeg')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function finishThumbnails (src) {
|
||||
const t0 = performance.now()
|
||||
playerData.thumbnailData.video = document.createElement('video')
|
||||
let index = 0
|
||||
playerData.thumbnailData.video.preload = 'none'
|
||||
playerData.thumbnailData.video.volume = 0
|
||||
playerData.thumbnailData.video.playbackRate = 0
|
||||
playerData.thumbnailData.video.width = playerData.thumbnailData.canvas.width
|
||||
playerData.thumbnailData.video.addEventListener('loadeddata', loadTime)
|
||||
playerData.thumbnailData.video.addEventListener('canplay', () => {
|
||||
createThumbnail(playerData.thumbnailData?.video)
|
||||
loadTime()
|
||||
})
|
||||
function loadTime () {
|
||||
while (playerData.thumbnailData?.thumbnails[index] && index <= Math.floor(playerData.thumbnailData.video.duration / playerData.thumbnailData.interval)) { // only create thumbnails that are missing
|
||||
index++
|
||||
}
|
||||
if (playerData.thumbnailData?.video?.currentTime !== playerData.thumbnailData?.video?.duration) {
|
||||
playerData.thumbnailData.video.currentTime = index * playerData.thumbnailData.interval
|
||||
} else {
|
||||
playerData.thumbnailData?.video?.removeAttribute('src')
|
||||
playerData.thumbnailData?.video?.load()
|
||||
playerData.thumbnailData?.video?.remove()
|
||||
delete playerData.thumbnailData?.video
|
||||
console.log('Thumbnail creating finished', index, performance.now() - t0)
|
||||
}
|
||||
index++
|
||||
}
|
||||
playerData.thumbnailData.video.src = src
|
||||
playerData.thumbnailData.video.play()
|
||||
console.log('Thumbnail creating started')
|
||||
}
|
||||
|
||||
// file download
|
||||
function downloadFile (file) {
|
||||
dl.removeAttribute('disabled')
|
||||
dl.onclick = async e => {
|
||||
file.getBlobURL((_err, url) => {
|
||||
const a = document.createElement('a')
|
||||
a.download = file.name
|
||||
a.href = url
|
||||
document.body.appendChild(a)
|
||||
a.click(e)
|
||||
a.remove()
|
||||
window.URL.revokeObjectURL(url)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// bufering spinner
|
||||
|
||||
let buffer
|
||||
function resetBuffer () {
|
||||
if (buffer) {
|
||||
clearTimeout(buffer)
|
||||
buffer = undefined
|
||||
buffering.classList.add('hidden')
|
||||
}
|
||||
}
|
||||
|
||||
function isBuffering () {
|
||||
buffer = setTimeout(displayBuffer, 150)
|
||||
}
|
||||
|
||||
function displayBuffer () {
|
||||
buffering.classList.remove('hidden')
|
||||
resetTimer()
|
||||
}
|
||||
|
||||
// immerse timeout
|
||||
let immerseTime
|
||||
|
||||
player.onmousemove = resetTimer
|
||||
player.onkeypress = resetTimer
|
||||
function immersePlayer () {
|
||||
player.classList.add('immersed')
|
||||
immerseTime = undefined
|
||||
}
|
||||
|
||||
function resetTimer () {
|
||||
if (!immerseTime) {
|
||||
clearTimeout(immerseTime)
|
||||
player.classList.remove('immersed')
|
||||
immerseTime = setTimeout(immersePlayer, parseInt(settings.player2) * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
function toTS (sec) {
|
||||
if (Number.isNaN(sec) || sec < 0) {
|
||||
return '00:00'
|
||||
}
|
||||
|
||||
const hours = Math.floor(sec / 3600)
|
||||
let minutes = Math.floor((sec - (hours * 3600)) / 60)
|
||||
let seconds = Math.floor(sec - (hours * 3600) - (minutes * 60))
|
||||
|
||||
if (minutes < 10) {
|
||||
minutes = `0${minutes}`
|
||||
}
|
||||
|
||||
if (seconds < 10) {
|
||||
seconds = `0${seconds}`
|
||||
}
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}:${minutes}:${seconds}`
|
||||
} else {
|
||||
return `${minutes}:${seconds}`
|
||||
}
|
||||
// return new Date(sec*1000).toISOString().slice(12, -1).slice(0, -4).replace(/^0:/,"") // laggy :/
|
||||
}
|
||||
|
||||
// play/pause button
|
||||
ptoggle.addEventListener('click', btnpp)
|
||||
async function playVideo () {
|
||||
try {
|
||||
await video.play()
|
||||
bpp.innerHTML = 'pause'
|
||||
} catch (err) {
|
||||
bpp.innerHTML = 'play_arrow'
|
||||
}
|
||||
}
|
||||
|
||||
function btnpp () {
|
||||
if (video.paused) {
|
||||
playVideo()
|
||||
} else {
|
||||
bpp.innerHTML = 'play_arrow'
|
||||
video.pause()
|
||||
}
|
||||
}
|
||||
// next video button
|
||||
let nextCooldown
|
||||
function btnnext () {
|
||||
clearTimeout(nextCooldown)
|
||||
nextCooldown = setTimeout(() => {
|
||||
const currentFile = videoFiles.filter(file => `${window.location.origin}${scope}webtorrent/${client.torrents[0].infoHash}/${encodeURI(file.path)}` === video.src)[0]
|
||||
if (videoFiles.length > 1 && videoFiles.indexOf(currentFile) < videoFiles.length - 1) {
|
||||
const fileIndex = videoFiles.indexOf(currentFile) + 1
|
||||
const nowPlaying = [playerData.nowPlaying[0], parseInt(playerData.nowPlaying[1]) + 1]
|
||||
cleanupVideo()
|
||||
buildVideo(videoFiles[fileIndex], nowPlaying)
|
||||
} else {
|
||||
if (playerData.nowPlaying[0]) {
|
||||
nyaaSearch(playerData.nowPlaying[0], parseInt(playerData.nowPlaying[1]) + 1)
|
||||
} else {
|
||||
halfmoon.initStickyAlert({
|
||||
content: 'Couldn\'t find anime name! Try specifying a torrent manually.',
|
||||
title: 'Search Failed',
|
||||
alertType: 'alert-danger',
|
||||
fillType: ''
|
||||
})
|
||||
}
|
||||
}
|
||||
}, 200)
|
||||
}
|
||||
// volume shit
|
||||
volume.addEventListener('input', () => updateVolume())
|
||||
let oldlevel
|
||||
|
||||
function btnmute () {
|
||||
if (video.volume === 0) {
|
||||
updateVolume(oldlevel)
|
||||
} else {
|
||||
oldlevel = video.volume * 100
|
||||
updateVolume(0)
|
||||
}
|
||||
}
|
||||
|
||||
function updateVolume (a) {
|
||||
let level
|
||||
if (a == null || isNaN(a)) {
|
||||
level = Number(volume.value)
|
||||
} else {
|
||||
level = a
|
||||
volume.value = a
|
||||
}
|
||||
volume.style.setProperty('--volume-level', level + '%')
|
||||
bmute.innerHTML = (level === 0) ? 'volume_off' : 'volume_up'
|
||||
video.volume = level / 100
|
||||
}
|
||||
updateVolume(parseInt(settings.volume))
|
||||
|
||||
// PiP
|
||||
|
||||
async function btnpip () {
|
||||
if (video.readyState) {
|
||||
if (!playerData.octopusInstance) {
|
||||
video !== document.pictureInPictureElement ? await video.requestPictureInPicture() : await document.exitPictureInPicture()
|
||||
} else {
|
||||
if (document.pictureInPictureElement && !document.pictureInPictureElement.id) { // only exit if pip is the custom one, else overwrite existing pip with custom
|
||||
await document.exitPictureInPicture()
|
||||
} else {
|
||||
const canvas = document.createElement('canvas')
|
||||
const canvasVideo = document.createElement('video')
|
||||
const context = canvas.getContext('2d', { alpha: false })
|
||||
let running = true
|
||||
canvas.width = video.videoWidth
|
||||
canvas.height = video.videoHeight
|
||||
|
||||
function renderFrame () {
|
||||
if (running === true) {
|
||||
context.drawImage(video, 0, 0)
|
||||
context.drawImage(subtitleCanvas, 0, 0, canvas.width, canvas.height)
|
||||
window.requestAnimationFrame(renderFrame)
|
||||
}
|
||||
}
|
||||
canvasVideo.srcObject = canvas.captureStream()
|
||||
canvasVideo.onloadedmetadata = () => {
|
||||
canvasVideo.play()
|
||||
canvasVideo.requestPictureInPicture().then(
|
||||
player.classList.add('pip')
|
||||
).catch(e => {
|
||||
console.warn('Failed To Burn In Subtitles ' + e)
|
||||
running = false
|
||||
canvasVideo.remove()
|
||||
canvas.remove()
|
||||
player.classList.remove('pip')
|
||||
})
|
||||
}
|
||||
canvasVideo.onleavepictureinpicture = () => {
|
||||
running = false
|
||||
canvasVideo.remove()
|
||||
canvas.remove()
|
||||
player.classList.remove('pip')
|
||||
}
|
||||
window.requestAnimationFrame(renderFrame)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// theathe mode
|
||||
|
||||
function btntheatre () {
|
||||
pageWrapper.classList.toggle('nav-hidden')
|
||||
}
|
||||
|
||||
// fullscreen
|
||||
player.addEventListener('fullscreenchange', updateFullscreen)
|
||||
ptoggle.addEventListener('dblclick', btnfull)
|
||||
function btnfull () {
|
||||
document.fullscreenElement ? document.exitFullscreen() : player.requestFullscreen()
|
||||
}
|
||||
function updateFullscreen () {
|
||||
document.fullscreenElement ? bfull.innerHTML = 'fullscreen_exit' : bfull.innerHTML = 'fullscreen'
|
||||
}
|
||||
|
||||
// seeking and skipping
|
||||
|
||||
function seek (a) {
|
||||
if (a === 85 && video.currentTime < 10) {
|
||||
video.currentTime = 90
|
||||
} else if (a === 85 && (video.duration - video.currentTime) < 90) {
|
||||
video.currentTime = video.duration
|
||||
} else {
|
||||
video.currentTime += a
|
||||
}
|
||||
updateBar(video.currentTime / video.duration * 100)
|
||||
}
|
||||
// subtitles, generates content every single time its opened because fuck knows when the parser will find new shit
|
||||
// this needs to go.... really badly
|
||||
function btncap () {
|
||||
const frag = document.createDocumentFragment()
|
||||
const off = document.createElement('a')
|
||||
off.classList.add('dropdown-item', 'pointer')
|
||||
playerData.selectedHeader ? off.classList.add('text-muted') : off.classList.add('text-white')
|
||||
off.innerHTML = 'OFF'
|
||||
off.onclick = () => {
|
||||
renderSubs()
|
||||
playerData.selectedHeader = undefined
|
||||
btncap()
|
||||
}
|
||||
frag.appendChild(off)
|
||||
for (const track of playerData.headers) {
|
||||
if (track) {
|
||||
const template = document.createElement('a')
|
||||
template.classList.add('dropdown-item', 'pointer', 'text-capitalize')
|
||||
template.innerHTML = (track.language || (!Object.values(playerData.headers).some(header => header.language === 'eng' || header.language === 'en') ? 'eng' : header.type)) + (track.name ? ' - ' + track.name : '')
|
||||
if (playerData.selectedHeader === track.number) {
|
||||
template.classList.add('text-white')
|
||||
} else {
|
||||
template.classList.add('text-muted')
|
||||
}
|
||||
template.onclick = () => {
|
||||
renderSubs(track.number)
|
||||
playerData.selectedHeader = track.number
|
||||
btncap()
|
||||
}
|
||||
frag.appendChild(template)
|
||||
}
|
||||
}
|
||||
const timeOffset = document.createElement('div')
|
||||
timeOffset.classList.add('btn-group', 'w-full', 'pt-5')
|
||||
timeOffset.setAttribute('role', 'group')
|
||||
timeOffset.innerHTML = `<button class="btn" type="button" onclick="playerData.octopusInstance.timeOffset+=1">-1s</button>
|
||||
<button class="btn" type="button" onclick="playerData.octopusInstance.timeOffset-=1">+1s</button>`
|
||||
frag.appendChild(timeOffset)
|
||||
subMenu.innerHTML = ''
|
||||
subMenu.appendChild(frag)
|
||||
}
|
||||
// playlist
|
||||
|
||||
function btnpl () {
|
||||
window.location.hash = '#playlist'
|
||||
}
|
||||
|
||||
// audio tracks
|
||||
|
||||
function btnaudio () {
|
||||
const frag = document.createDocumentFragment()
|
||||
for (const track of video.audioTracks) {
|
||||
const template = document.createElement('a')
|
||||
template.classList.add('dropdown-item', 'pointer', 'text-capitalize')
|
||||
template.innerHTML = (track.language || (!Object.values(video.audioTracks).some(track => track.language === 'eng' || track.language === 'en') ? 'eng' : track.label)) + (track.label ? ' - ' + track.label : '')
|
||||
track.enabled === true ? template.classList.add('text-white') : template.classList.add('text-muted')
|
||||
template.onclick = () => {
|
||||
selectAudio(track.id)
|
||||
}
|
||||
frag.appendChild(template)
|
||||
}
|
||||
|
||||
audioTracksMenu.innerHTML = ''
|
||||
audioTracksMenu.appendChild(frag)
|
||||
}
|
||||
function selectAudio (id) {
|
||||
for (const track of video.audioTracks) {
|
||||
track.id === id ? track.enabled = true : track.enabled = false
|
||||
}
|
||||
seek(-1) // stupid fix because video freezes up when chaging tracks
|
||||
btnaudio()
|
||||
}
|
||||
// keybinds
|
||||
|
||||
document.onkeydown = a => {
|
||||
if (a.key === 'F5') {
|
||||
a.preventDefault()
|
||||
}
|
||||
if (document.location.hash === '#player') {
|
||||
switch (a.key) {
|
||||
case ' ':
|
||||
btnpp()
|
||||
break
|
||||
case 'n':
|
||||
btnnext()
|
||||
break
|
||||
case 'm':
|
||||
btnmute()
|
||||
break
|
||||
case 'p':
|
||||
btnpip()
|
||||
break
|
||||
case 't':
|
||||
btntheatre()
|
||||
break
|
||||
case 'c':
|
||||
btncap()
|
||||
break
|
||||
case 'f':
|
||||
btnfull()
|
||||
break
|
||||
case 's':
|
||||
seek(85)
|
||||
break
|
||||
case 'ArrowLeft':
|
||||
seek(-parseInt(settings.player3))
|
||||
break
|
||||
case 'ArrowRight':
|
||||
seek(parseInt(settings.player3))
|
||||
break
|
||||
case 'ArrowUp':
|
||||
updateVolume(parseInt(volume.value) + 5)
|
||||
break
|
||||
case 'ArrowDown':
|
||||
updateVolume(parseInt(volume.value) - 5)
|
||||
break
|
||||
case 'Escape':
|
||||
document.location.hash = '#home'
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// media session shit
|
||||
|
||||
function updatePositionState () {
|
||||
if (video.duration) {
|
||||
navigator.mediaSession.setPositionState({
|
||||
duration: video.duration || 0,
|
||||
playbackRate: video.playbackRate || 0,
|
||||
position: video.currentTime || 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.setActionHandler('play', btnpp)
|
||||
navigator.mediaSession.setActionHandler('pause', btnpp)
|
||||
navigator.mediaSession.setActionHandler('seekbackward', () => {
|
||||
seek(-parseInt(settings.player3))
|
||||
})
|
||||
navigator.mediaSession.setActionHandler('seekforward', () => {
|
||||
seek(parseInt(settings.player3))
|
||||
})
|
||||
navigator.mediaSession.setActionHandler('nexttrack', btnnext)
|
||||
}
|
||||
|
||||
// AL entry auto add
|
||||
function checkCompletion () {
|
||||
if (!playerData.watched && video.duration - 180 < video.currentTime && playerData.nowPlaying && (playerData.nowPlaying[0].episodes || playerData.nowPlaying[0].nextAiringEpisode.episode)) {
|
||||
if (settings.other2 && !(!(playerData.nowPlaying[0].episodes || playerData.nowPlaying[0].nextAiringEpisode.episode) && playerData.nowPlaying[0].streamingEpisodes.length && parseInt(playerData.nowPlaying[1] > 12))) {
|
||||
alEntry()
|
||||
} else {
|
||||
halfmoon.initStickyAlert({
|
||||
content: `Do You Want To Mark <br><b>${playerData.nowPlaying[0].title.userPreferred}</b><br>Episode ${playerData.nowPlaying[1]} As Completed?<br>
|
||||
<button class="btn btn-sm btn-square btn-success mt-5" onclick="alEntry()" data-dismiss="alert" type="button" aria-label="Close">✓</button>
|
||||
<button class="btn btn-sm btn-square mt-5" data-dismiss="alert" type="button" aria-label="Close"><span aria-hidden="true">X</span></button>`,
|
||||
title: 'Episode Complete',
|
||||
timeShown: 180000
|
||||
})
|
||||
}
|
||||
playerData.watched = true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
const settingsElements = [
|
||||
volume, player2, player3, player5, player6, player10, subtitle1, subtitle3, torrent1, torrent2, torrent3, torrent4, torrent5, torrent6, torrent7, torrent9, other1, other2
|
||||
volume, player2, player3, player5, player6, player10, subtitle1, subtitle3, torrent1, torrent2, torrent3, torrent4, torrent5, torrent6, torrent7, torrent8, torrent9, other1, other2
|
||||
]
|
||||
setRes.addEventListener('click', restoreDefaults)
|
||||
settingsTab.addEventListener('click', applySettingsTimeout)
|
||||
|
|
|
|||
|
|
@ -1,123 +0,0 @@
|
|||
const { SubtitleStream } = MatroskaSubtitles
|
||||
const { SubtitleParser } = MatroskaSubtitles
|
||||
|
||||
function subStream (stream) { // subtitle parsing with seeking support
|
||||
if (playerData.subtitleStream) {
|
||||
playerData.subtitleStream = new SubtitleStream(playerData.subtitleStream)
|
||||
} else {
|
||||
playerData.subtitleStream = new SubtitleStream()
|
||||
playerData.subtitleStream.once('tracks', pTracks => {
|
||||
bcap.removeAttribute('disabled')
|
||||
playerData.headers = []
|
||||
pTracks.forEach(track => {
|
||||
if (track.type !== 'ass') { // overwrite webvtt header with custom one
|
||||
track.header = `[V4+ Styles]
|
||||
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
||||
Style: Default,${Object.values(subtitle1list.options).filter(item => item.value === settings.subtitle1)[0].innerText}
|
||||
[Events]
|
||||
|
||||
`
|
||||
}
|
||||
playerData.headers[track.number] = track
|
||||
playerData.subtitles[track.number] = new Set()
|
||||
if (!playerData.selectedHeader) playerData.selectedHeader = track.number
|
||||
})
|
||||
})
|
||||
}
|
||||
playerData.subtitleStream.on('subtitle', (subtitle, trackNumber) => {
|
||||
if (playerData.headers && !playerData.parsed) {
|
||||
if (playerData.headers[trackNumber].type === 'webvtt') convertSub(subtitle)
|
||||
const formatSub = 'Dialogue: ' + (subtitle.layer || 0) + ',' + new Date(subtitle.time).toISOString().slice(12, -1).slice(0, -1) + ',' + new Date(subtitle.time + subtitle.duration).toISOString().slice(12, -1).slice(0, -1) + ',' + (subtitle.style || 'Default') + ',' + (subtitle.name || '') + ',' + (subtitle.marginL || '0') + ',' + (subtitle.marginR || '0') + ',' + (subtitle.marginV || '0') + ',' + (subtitle.effect || '') + ',' + subtitle.text
|
||||
playerData.subtitles[trackNumber].add(formatSub)
|
||||
if (playerData.selectedHeader === trackNumber) renderSubs(trackNumber)
|
||||
}
|
||||
})
|
||||
playerData.subtitleStream.on('file', file => {
|
||||
if (file.mimetype === 'application/x-truetype-font' || file.mimetype === 'application/font-woff') playerData.fonts.push(window.URL.createObjectURL(new Blob([file.data], { type: file.mimetype })))
|
||||
})
|
||||
stream.pipe(playerData.subtitleStream)
|
||||
}
|
||||
let octopusTimeout
|
||||
async function renderSubs (trackNumber) {
|
||||
if (!playerData.octopusInstance) {
|
||||
const options = {
|
||||
video: video,
|
||||
targetFps: await playerData.fps,
|
||||
subContent: trackNumber ? playerData.headers[trackNumber].header.slice(0, -1) + Array.from(playerData.subtitles[trackNumber]).join('\n') : playerData.headers[3].header.slice(0, -1),
|
||||
renderMode: 'offscreenCanvas',
|
||||
fonts: playerData.fonts?.length !== 0 ? playerData.fonts : ['https://fonts.gstatic.com/s/roboto/v20/KFOlCnqEu92Fr1MmEU9fBBc4.woff2'],
|
||||
workerUrl: 'js/subtitles-octopus-worker.js',
|
||||
timeOffset: 0,
|
||||
onReady: function () {
|
||||
if (!video.paused) {
|
||||
video.pause()
|
||||
playVideo()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!playerData.octopusInstance) playerData.octopusInstance = new SubtitlesOctopus(options)
|
||||
} else {
|
||||
if (!octopusTimeout) {
|
||||
octopusTimeout = setTimeout(() => {
|
||||
octopusTimeout = undefined
|
||||
if (playerData.octopusInstance) playerData.octopusInstance.setTrack(trackNumber ? playerData.headers[trackNumber].header.slice(0, -1) + Array.from(playerData.subtitles[trackNumber]).join('\n') : playerData.headers[3].header.slice(0, -1))
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
function convertSub (subtitle) { // converts vtt subtitles to ssa ones
|
||||
const matches = subtitle.text.match(/<[^>]+>/g) // create array of all tags
|
||||
if (matches) {
|
||||
matches.forEach(match => {
|
||||
if (/<\//.test(match)) { // check if its a closing tag
|
||||
subtitle.text = subtitle.text.replace(match, match.replace('</', '{\\').replace('>', '0}'))
|
||||
} else {
|
||||
subtitle.text = subtitle.text.replace(match, match.replace('<', '{\\').replace('>', '1}'))
|
||||
}
|
||||
})
|
||||
}
|
||||
// replace all html special tags with normal ones
|
||||
subtitle.text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/ /g, '\\h')
|
||||
}
|
||||
function postDownload (file) { // parse subtitles fully after a download is finished
|
||||
return new Promise((resolve, reject) => {
|
||||
if (file.name.endsWith('.mkv') || file.name.endsWith('.webm')) {
|
||||
let parser = new SubtitleParser()
|
||||
parser.once('tracks', pTracks => {
|
||||
pTracks.forEach(track => {
|
||||
if (track.type !== 'ass') { // overwrite webvtt header with custom one
|
||||
track.header = `[V4+ Styles]
|
||||
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
||||
Style: Default,${Object.values(subtitle1list.options).filter(item => item.value === settings.subtitle1)[0].innerText}
|
||||
[Events]
|
||||
|
||||
`
|
||||
}
|
||||
playerData.headers[track.number] = track
|
||||
if (!playerData.subtitles[track.number]) playerData.subtitles[track.number] = new Set()
|
||||
if (!playerData.selectedHeader) playerData.selectedHeader = track.number
|
||||
})
|
||||
})
|
||||
parser.on('subtitle', (subtitle, trackNumber) => {
|
||||
if (playerData.headers[trackNumber].type === 'webvtt') convertSub(subtitle)
|
||||
playerData.subtitles[trackNumber].add('Dialogue: ' + (subtitle.layer || 0) + ',' + new Date(subtitle.time).toISOString().slice(12, -1).slice(0, -1) + ',' + new Date(subtitle.time + subtitle.duration).toISOString().slice(12, -1).slice(0, -1) + ',' + (subtitle.style || 'Default') + ',' + (subtitle.name || '') + ',' + (subtitle.marginL || '0') + ',' + (subtitle.marginR || '0') + ',' + (subtitle.marginV || '0') + ',' + (subtitle.effect || '') + ',' + subtitle.text)
|
||||
})
|
||||
parser.on('finish', () => {
|
||||
console.log('Sub parsing finished')
|
||||
playerData.parsed = 1
|
||||
playerData.subtitleStream = undefined
|
||||
renderSubs(playerData.selectedHeader)
|
||||
parser = undefined
|
||||
bcap.removeAttribute('disabled')
|
||||
if (!video.paused) {
|
||||
video.pause()
|
||||
playVideo()
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
console.log('Sub parsing started')
|
||||
playerData.subtitlePraseStream = file.createReadStream().pipe(parser)
|
||||
// when this gets overwritten the parser stays so it might "leak" some RAM???
|
||||
}
|
||||
})
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,234 +0,0 @@
|
|||
const client = new WebTorrent({ maxConns: settings.torrent6, downloadLimit: settings.torrent7 * 1048576, uploadLimit: settings.torrent7 * 1572864 })
|
||||
window.onbeforeunload = () => { // cleanup shit before unloading to free RAM/drive
|
||||
cleanupVideo()
|
||||
cleanupTorrents()
|
||||
if (playerData.fonts) playerData.fonts.forEach(file => URL.revokeObjectURL(file))
|
||||
if (dl.href) URL.revokeObjectURL(dl.href)
|
||||
}
|
||||
|
||||
const announceList = [
|
||||
['wss://tracker.openwebtorrent.com'],
|
||||
// ['wss://tracker.novage.com.ua/']
|
||||
// ['wss://tracker.btorrent.xyz'] // for now disabled cuz broken
|
||||
// ['wss://tracker.webtorrent.io'],
|
||||
// ['wss://tracker.fastcast.nz'],
|
||||
// ['wss://video.blender.org:443/tracker/socket'],
|
||||
// ['wss://tube.privacytools.io:443/tracker/socket'],
|
||||
['wss://tracker.sloppyta.co:443/announce'],
|
||||
// ['wss://tracker.lab.vvc.niif.hu:443/announce'],
|
||||
// ['wss://tracker.files.fm:7073/announce'],
|
||||
// ['wss://open.tube:443/tracker/socket'],
|
||||
['wss://hub.bugout.link:443/announce']
|
||||
// ['wss://peertube.cpy.re:443/tracker/socket'],
|
||||
// ['ws://tracker.sloppyta.co:80/announce'],
|
||||
// ['ws://tracker.lab.vvc.niif.hu:80/announce'],
|
||||
// ['ws://tracker.files.fm:7072/announce'],
|
||||
// ['ws://tracker.btsync.cf:6969/announce'],
|
||||
// ['ws://hub.bugout.link:80/announce']
|
||||
]
|
||||
const videoExtensions = ['.3g2', '.3gp', '.asf', '.avi', '.dv', '.flv', '.gxf', '.m2ts', '.m4a', '.m4b', '.m4p', '.m4r', '.m4v', '.mkv', '.mov', '.mp4', '.mpd', '.mpeg', '.mpg', '.mxf', '.nut', '.ogm', '.ogv', '.swf', '.ts', '.vob', '.webm', '.wmv', '.wtv']
|
||||
const scope = '/app/'
|
||||
const sw = navigator.serviceWorker.register('sw.js', { scope }).then(e => {
|
||||
if (searchParams.get('file')) addTorrent(searchParams.get('file'), {}) // add a torrent if its in the link params
|
||||
}).catch(e => {
|
||||
if (String(e) === 'InvalidStateError: Failed to register a ServiceWorker: The document is in an invalid state.') {
|
||||
location.reload() // weird workaround for a weird bug
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
})
|
||||
|
||||
// for debugging
|
||||
function t (a) {
|
||||
switch (a) {
|
||||
case 1:
|
||||
addTorrent('https://webtorrent.io/torrents/sintel.torrent', {})
|
||||
break
|
||||
case 2:
|
||||
addTorrent('https://webtorrent.io/torrents/tears-of-steel.torrent', {})
|
||||
break
|
||||
case 3:
|
||||
addTorrent('magnet:?xt=urn:btih:CE9156EB497762F8B7577B71C0647A4B0C3423E1&dn=Inception+%282010%29+720p+-+mkv+-+1.0GB+-+YIFY&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2F9.rarbg.to%3A2920%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=udp%3A%2F%2Ftracker.internetwarriors.net%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.pirateparty.gr%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.cyberia.is%3A6969%2Fannounce', {})
|
||||
break
|
||||
}
|
||||
}
|
||||
// offline storage initial load
|
||||
let offlineTorrents
|
||||
async function loadOfflineStorage () {
|
||||
offlineTorrents = JSON.parse(localStorage.getItem('offlineTorrents')) || {}
|
||||
Object.values(offlineTorrents).forEach(torrentID => offlineDownload(new Blob([new Uint8Array(torrentID)]), true)) // adds all offline store torrents to the client
|
||||
}
|
||||
|
||||
// add torrent for offline download
|
||||
function offlineDownload (torrentID, skipVerify) {
|
||||
const torrent = client.add(torrentID, {
|
||||
store: IdbChunkStore,
|
||||
skipVerify: skipVerify
|
||||
})
|
||||
torrent.on('metadata', async () => {
|
||||
console.log(torrent)
|
||||
if (!offlineTorrents[torrent.infoHash]) {
|
||||
offlineTorrents[torrent.infoHash] = Array.from(torrent.torrentFile)
|
||||
localStorage.setItem('offlineTorrents', JSON.stringify(offlineTorrents))
|
||||
}
|
||||
const mediaInformation = await resolveFileMedia({ fileName: torrent.name, method: 'SearchName' })
|
||||
template = cardCreator(mediaInformation)
|
||||
template.onclick = () => addTorrent(torrent, { media: mediaInformation.media, episode: mediaInformation.episode })
|
||||
document.querySelector('.downloads').appendChild(template)
|
||||
})
|
||||
}
|
||||
|
||||
loadOfflineStorage()
|
||||
|
||||
// cleanup torrent and store
|
||||
function cleanupTorrents () {
|
||||
// creates an array of all non-offline store torrents and removes them
|
||||
client.torrents.filter(torrent => !offlineTorrents[torrent.infoHash]).forEach(torrent => torrent.destroy({ destroyStore: true }))
|
||||
document.querySelector('.playlist').innerHTML = ''
|
||||
}
|
||||
|
||||
// manually add trackers
|
||||
WEBTORRENT_ANNOUNCE = announceList.map(arr => arr[0]).filter(url => url.indexOf('wss://') === 0)
|
||||
|
||||
let videoFiles
|
||||
async function playTorrent (torrent, opts) {
|
||||
torrent.on('noPeers', () => {
|
||||
if (torrent.progress !== 1) {
|
||||
halfmoon.initStickyAlert({
|
||||
content: `Couldn't find peers for <span class="text-break">${torrent.infoHash}</span>! Try a torrent with more seeders.`,
|
||||
title: 'Search Failed',
|
||||
alertType: 'alert-danger',
|
||||
fillType: ''
|
||||
})
|
||||
}
|
||||
})
|
||||
await sw
|
||||
videoFiles = torrent.files.filter(file => videoExtensions.some(ext => file.name.endsWith(ext)))
|
||||
if (videoFiles.length > 1) {
|
||||
(async function () {
|
||||
torrent.files.forEach(file => file.deselect())
|
||||
const frag = document.createDocumentFragment()
|
||||
for (const file of videoFiles) {
|
||||
const mediaInformation = await resolveFileMedia({ fileName: file.name, method: 'SearchName' })
|
||||
template = cardCreator(mediaInformation)
|
||||
template.onclick = () => {
|
||||
cleanupVideo()
|
||||
buildVideo(torrent, { media: mediaInformation.media, episode: mediaInformation.parseObject.episode, file: file })
|
||||
}
|
||||
frag.appendChild(template)
|
||||
}
|
||||
document.querySelector('.playlist').appendChild(frag)
|
||||
}())
|
||||
}
|
||||
if (videoFiles) {
|
||||
buildVideo(torrent, opts)
|
||||
} else {
|
||||
halfmoon.initStickyAlert({
|
||||
content: `Couldn't find video file for <span class="text-break">${torrent.infoHash}</span>!`,
|
||||
title: 'Search Failed',
|
||||
alertType: 'alert-danger',
|
||||
fillType: ''
|
||||
})
|
||||
cleanupTorrents()
|
||||
}
|
||||
}
|
||||
|
||||
function addTorrent (torrentID, opts) {
|
||||
halfmoon.hideModal('tsearch')
|
||||
document.location.hash = '#player'
|
||||
cleanupVideo()
|
||||
cleanupTorrents()
|
||||
if (torrentID instanceof Object) {
|
||||
playTorrent(torrentID, opts)
|
||||
} else if (client.get(torrentID)) {
|
||||
playTorrent(client.get(torrentID), opts)
|
||||
} else {
|
||||
client.add(torrentID, settings.torrent5 ? { store: IdbChunkStore } : {}, function (torrent) {
|
||||
playTorrent(torrent, opts)
|
||||
if (settings.torrent6) torrent.deselect(0, torrent.pieces.length - 1, false)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function serveFile (file, req) {
|
||||
const res = {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': file._getMimeType(),
|
||||
// Support range-requests
|
||||
'Accept-Ranges': 'bytes'
|
||||
}
|
||||
}
|
||||
|
||||
// `rangeParser` returns an array of ranges, or an error code (number) if
|
||||
// there was an error parsing the range.
|
||||
let range = rangeParser(file.length, req.headers.get('range') || '')
|
||||
|
||||
if (Array.isArray(range)) {
|
||||
res.status = 206 // indicates that range-request was understood
|
||||
|
||||
// no support for multi-range request, just use the first range
|
||||
range = range[0]
|
||||
|
||||
res.headers['Content-Range'] = `bytes ${range.start}-${range.end}/${file.length}`
|
||||
res.headers['Content-Length'] = `${range.end - range.start + 1}`
|
||||
} else {
|
||||
range = null
|
||||
res.headers['Content-Length'] = file.length
|
||||
}
|
||||
|
||||
res.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate, max-age=0'
|
||||
res.headers.Expires = '0'
|
||||
res.body = req.method === 'HEAD' ? '' : 'stream'
|
||||
// parser is really a passthrough mkv stream now
|
||||
const stream = file.createReadStream(range)
|
||||
if ((file.name.endsWith('.mkv') || file.name.endsWith('.webm')) && !playerData.parsed) subStream(stream)
|
||||
|
||||
return [res, req.method === 'GET' && (playerData.subtitleStream || stream)]
|
||||
}
|
||||
|
||||
// kind of a fetch event from service worker but for the main thread.
|
||||
navigator.serviceWorker.addEventListener('message', evt => {
|
||||
const request = new Request(evt.data.url, {
|
||||
headers: evt.data.headers,
|
||||
method: evt.data.method
|
||||
})
|
||||
|
||||
const [port] = evt.ports
|
||||
const respondWith = msg => port.postMessage(msg)
|
||||
const pathname = request.url.split(evt.data.scope + 'webtorrent/')[1]
|
||||
let [infoHash, ...filePath] = pathname.split('/')
|
||||
filePath = decodeURI(filePath.join('/'))
|
||||
|
||||
if (!infoHash || !filePath) return
|
||||
|
||||
const torrent = client.get(infoHash)
|
||||
const file = torrent.files.find(file => file.path === filePath)
|
||||
|
||||
const [response, stream] = serveFile(file, request)
|
||||
const asyncIterator = stream && stream[Symbol.asyncIterator]()
|
||||
respondWith(response)
|
||||
async function pull (msg) {
|
||||
if (msg.data) {
|
||||
const chunk = (await asyncIterator.next()).value
|
||||
respondWith(chunk)
|
||||
if (!chunk) port.onmessage = null
|
||||
} else {
|
||||
console.log('Closing stream')
|
||||
stream.destroy()
|
||||
port.onmessage = null
|
||||
}
|
||||
}
|
||||
|
||||
port.onmessage = pull
|
||||
})
|
||||
|
||||
function prettyBytes (num) {
|
||||
const neg = num < 0; const units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||
if (neg) num = -num
|
||||
if (num < 1) return (neg ? '-' : '') + num + ' B'
|
||||
const exponent = Math.min(Math.floor(Math.log(num) / Math.log(1000)), units.length - 1)
|
||||
num = Number((num / Math.pow(1000, exponent)).toFixed(2))
|
||||
const unit = units[exponent]
|
||||
return (neg ? '-' : '') + num + ' ' + unit
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<video src="https://openings.moe/video/Kobayashi-SanChiNoMaidDragon-OP01-NCBD.mp4" muted controls id="video"></video>
|
||||
<script src="js/subtitles-octopus.js"></script>
|
||||
<script>
|
||||
video.addEventListener('timeupdate',(e)=>console.log(e.target.currentTime))
|
||||
let ready = function () {
|
||||
console.log("ready")
|
||||
octopusInstance.createEvent({
|
||||
|
|
|
|||
|
|
@ -207,7 +207,7 @@
|
|||
<th>AC3</th>
|
||||
<td class="text-danger text-center">✘</td>
|
||||
<td class="text-danger text-center">✘</td>
|
||||
<td class="text-secondary text-center">✓**</td>
|
||||
<td class="text-success text-center">✓</td>
|
||||
<td class="text-danger text-center">✘</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
@ -221,7 +221,7 @@
|
|||
<th>EAC3</th>
|
||||
<td class="text-danger text-center">✘</td>
|
||||
<td class="text-danger text-center">✘</td>
|
||||
<td class="text-secondary text-center">✓**</td>
|
||||
<td class="text-success text-center">✓</td>
|
||||
<td class="text-danger text-center">✘</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
@ -261,8 +261,7 @@
|
|||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
* Might not work in some video containers.<br>
|
||||
** Documented as working, but can't reproduce.<br><br>
|
||||
* Might not work in some video containers.<br><br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue