Improved player to support sidecar external subtitles
This commit is contained in:
parent
fe803d66af
commit
5ad3eb2527
8 changed files with 307 additions and 56 deletions
|
|
@ -7,7 +7,7 @@ Web GUI for [yt-dlp](https://github.com/yt-dlp/yt-dlp) with playlist & channel s
|
||||||
YTPTube started as a fork of [meTube](https://github.com/alexta69/metube), Since then it went under heavy changes, and it supports many new features.
|
YTPTube started as a fork of [meTube](https://github.com/alexta69/metube), Since then it went under heavy changes, and it supports many new features.
|
||||||
|
|
||||||
# YTPTube Features.
|
# YTPTube Features.
|
||||||
* A built in video player that can play any video file regardless of the format.
|
* A built in video player that can play any video file regardless of the format. **With support for sidecar external subtitles**.
|
||||||
* New `/add_batch` endpoint that allow multiple links to be sent.
|
* New `/add_batch` endpoint that allow multiple links to be sent.
|
||||||
* Completely redesigned the frontend UI.
|
* Completely redesigned the frontend UI.
|
||||||
* Switched out of binary file storage in favor of SQLite.
|
* Switched out of binary file storage in favor of SQLite.
|
||||||
|
|
@ -17,6 +17,7 @@ YTPTube started as a fork of [meTube](https://github.com/alexta69/metube), Since
|
||||||
* Webhook sender. It allow you to add webhook endpoints that receive events related to downloads using simple `json` file.
|
* Webhook sender. It allow you to add webhook endpoints that receive events related to downloads using simple `json` file.
|
||||||
* Multi-downloads support.
|
* Multi-downloads support.
|
||||||
* Basic Authentication support.
|
* Basic Authentication support.
|
||||||
|
* Support for curl_cffi, see [yt-dlp documentation](https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#impersonation)
|
||||||
|
|
||||||
### Tips
|
### Tips
|
||||||
Your `yt-dlp` config should include the following options for optimal working conditions.
|
Your `yt-dlp` config should include the following options for optimal working conditions.
|
||||||
|
|
|
||||||
75
app/main.py
75
app/main.py
|
|
@ -13,8 +13,10 @@ from Utils import ObjectSerializer, Notifier, isDownloaded, load_file
|
||||||
from aiohttp import web, client
|
from aiohttp import web, client
|
||||||
from aiohttp.web import Request, Response, RequestHandler
|
from aiohttp.web import Request, Response, RequestHandler
|
||||||
from Webhooks import Webhooks
|
from Webhooks import Webhooks
|
||||||
|
from player.Playlist import Playlist
|
||||||
from player.M3u8 import M3u8
|
from player.M3u8 import M3u8
|
||||||
from player.Segments import Segments
|
from player.Segments import Segments
|
||||||
|
from player.Subtitle import Subtitle
|
||||||
import socketio
|
import socketio
|
||||||
import logging
|
import logging
|
||||||
import caribou
|
import caribou
|
||||||
|
|
@ -451,21 +453,56 @@ class Main:
|
||||||
|
|
||||||
return web.json_response({"status": "stopped" if status else "in_error_state"})
|
return web.json_response({"status": "stopped" if status else "in_error_state"})
|
||||||
|
|
||||||
@self.routes.get(self.config.url_prefix + 'm3u8/{file:.*}')
|
@self.routes.get(self.config.url_prefix + 'player/playlist/{file:.*}.m3u8')
|
||||||
async def m3u8(request: Request) -> Response:
|
async def playlist(request: Request) -> Response:
|
||||||
file: str = request.match_info.get('file')
|
file: str = request.match_info.get('file')
|
||||||
|
|
||||||
if not file:
|
if not file:
|
||||||
raise web.HTTPBadRequest(reason='file is required.')
|
raise web.HTTPBadRequest(reason='file is required.')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
text = await M3u8(url=f"{self.config.url_host}{self.config.url_prefix}").make_stream(
|
text = await Playlist(url=f"{self.config.url_host}{self.config.url_prefix}").make(
|
||||||
download_path=self.config.download_path,
|
download_path=self.config.download_path,
|
||||||
file=file
|
file=file
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return web.HTTPNotFound(reason=str(e))
|
return web.HTTPNotFound(reason=str(e))
|
||||||
|
|
||||||
|
return web.Response(text=text, headers={
|
||||||
|
'Content-Type': 'application/x-mpegURL',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Access-Control-Max-Age': "300",
|
||||||
|
})
|
||||||
|
|
||||||
|
@self.routes.get(self.config.url_prefix + 'player/m3u8/{mode}/{file:.*}.m3u8')
|
||||||
|
async def m3u8(request: Request) -> Response:
|
||||||
|
file: str = request.match_info.get('file')
|
||||||
|
mode: str = request.match_info.get('mode')
|
||||||
|
|
||||||
|
if mode not in ['video', 'subtitle']:
|
||||||
|
raise web.HTTPBadRequest(reason='Only video and subtitle modes are supported.')
|
||||||
|
|
||||||
|
if not file:
|
||||||
|
raise web.HTTPBadRequest(reason='file is required.')
|
||||||
|
|
||||||
|
duration = request.query.get('duration', None)
|
||||||
|
|
||||||
|
if 'subtitle' in mode:
|
||||||
|
if not duration:
|
||||||
|
raise web.HTTPBadRequest(reason='duration is required.')
|
||||||
|
|
||||||
|
duration = float(duration)
|
||||||
|
|
||||||
|
try:
|
||||||
|
cls = M3u8(f"{self.config.url_host}{self.config.url_prefix}")
|
||||||
|
if 'subtitle' in mode:
|
||||||
|
text = await cls.make_subtitle(self.config.download_path, file, duration)
|
||||||
|
else:
|
||||||
|
text = await cls.make_stream(self.config.download_path, file)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return web.HTTPNotFound(reason=str(e))
|
||||||
|
|
||||||
return web.Response(
|
return web.Response(
|
||||||
text=text,
|
text=text,
|
||||||
headers={
|
headers={
|
||||||
|
|
@ -475,7 +512,7 @@ class Main:
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@self.routes.get(self.config.url_prefix + 'segments/{segment:\d+}/{file:.*}')
|
@self.routes.get(self.config.url_prefix + 'player/segments/{segment:\d+}/{file:.*}.ts')
|
||||||
async def segments(request: Request) -> Response:
|
async def segments(request: Request) -> Response:
|
||||||
file: str = request.match_info.get('file')
|
file: str = request.match_info.get('file')
|
||||||
segment: int = request.match_info.get('segment')
|
segment: int = request.match_info.get('segment')
|
||||||
|
|
@ -516,6 +553,36 @@ class Main:
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@self.routes.get(self.config.url_prefix + 'player/subtitle/{file:.*}.vtt')
|
||||||
|
async def subtitles(request: Request) -> Response:
|
||||||
|
file: str = request.match_info.get('file')
|
||||||
|
|
||||||
|
if request.if_modified_since:
|
||||||
|
file_path = os.path.join(self.config.download_path, file)
|
||||||
|
lastMod = time.strftime('%a, %d %b %Y %H:%M:%S GMT', datetime.fromtimestamp(
|
||||||
|
os.path.getmtime(os.path.join(self.config.download_path, file))).timetuple()
|
||||||
|
)
|
||||||
|
if os.path.exists(file_path) and request.if_modified_since.timestamp() == os.path.getmtime(file_path):
|
||||||
|
return web.Response(status=304, headers={'Last-Modified': lastMod})
|
||||||
|
|
||||||
|
if not file:
|
||||||
|
raise web.HTTPBadRequest(reason='file is required')
|
||||||
|
|
||||||
|
return web.Response(
|
||||||
|
body=await Subtitle().make(path=self.config.download_path, file=file),
|
||||||
|
headers={
|
||||||
|
'Content-Type': 'text/vtt; charset=UTF-8',
|
||||||
|
'X-Accel-Buffering': 'no',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Pragma': 'public',
|
||||||
|
'Cache-Control': f'public, max-age={time.time() + 31536000}',
|
||||||
|
'Last-Modified': time.strftime('%a, %d %b %Y %H:%M:%S GMT', datetime.fromtimestamp(
|
||||||
|
os.path.getmtime(os.path.join(self.config.download_path, file))).timetuple()
|
||||||
|
),
|
||||||
|
'Expires': time.strftime('%a, %d %b %Y %H:%M:%S GMT', datetime.fromtimestamp(time.time() + 31536000).timetuple()),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@self.routes.get(self.config.url_prefix)
|
@self.routes.get(self.config.url_prefix)
|
||||||
async def index(_) -> Response:
|
async def index(_) -> Response:
|
||||||
if not self.appLoader:
|
if not self.appLoader:
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,13 @@ class M3u8:
|
||||||
|
|
||||||
duration: float = float(ffprobe.metadata.get('duration'))
|
duration: float = float(ffprobe.metadata.get('duration'))
|
||||||
|
|
||||||
m3u8 = "#EXTM3U\n"
|
m3u8 = []
|
||||||
m3u8 += "#EXT-X-VERSION:3\n"
|
|
||||||
m3u8 += f"#EXT-X-TARGETDURATION:{int(self.duration)}\n"
|
m3u8.append("#EXTM3U")
|
||||||
m3u8 += "#EXT-X-MEDIA-SEQUENCE:0\n"
|
m3u8.append("#EXT-X-VERSION:3")
|
||||||
m3u8 += "#EXT-X-PLAYLIST-TYPE:VOD\n"
|
m3u8.append(f"#EXT-X-TARGETDURATION:{int(self.duration)}")
|
||||||
|
m3u8.append("#EXT-X-MEDIA-SEQUENCE:0")
|
||||||
|
m3u8.append("#EXT-X-PLAYLIST-TYPE:VOD")
|
||||||
|
|
||||||
segmentSize: float = '{:.6f}'.format(self.duration)
|
segmentSize: float = '{:.6f}'.format(self.duration)
|
||||||
splits: int = math.ceil(duration / self.duration)
|
splits: int = math.ceil(duration / self.duration)
|
||||||
|
|
@ -54,26 +56,36 @@ class M3u8:
|
||||||
|
|
||||||
for i in range(splits):
|
for i in range(splits):
|
||||||
if (i + 1) == splits:
|
if (i + 1) == splits:
|
||||||
segmentParams.update({'sd': '{:.6f}'.format(duration - (i * self.duration))})
|
segmentSize = '{:.6f}'.format(duration - (i * self.duration))
|
||||||
m3u8 += f"#EXTINF:{segmentParams['sd']}, nodesc\n"
|
segmentParams.update({'sd': segmentSize})
|
||||||
else:
|
|
||||||
m3u8 += f"#EXTINF:{segmentSize}, nodesc\n"
|
|
||||||
|
|
||||||
m3u8 += f"{self.url}segments/{i}/{quote(file)}"
|
m3u8.append(f"#EXTINF:{segmentSize},")
|
||||||
|
|
||||||
|
url = f"{self.url}player/segments/{i}/{quote(file)}.ts"
|
||||||
if len(segmentParams) > 0:
|
if len(segmentParams) > 0:
|
||||||
m3u8 += '?'+'&'.join([f"{key}={value}" for key, value in segmentParams.items()])
|
url += '?'+'&'.join([f"{key}={value}" for key, value in segmentParams.items()])
|
||||||
m3u8 += "\n"
|
|
||||||
|
|
||||||
m3u8 += "#EXT-X-ENDLIST\n"
|
m3u8.append(url)
|
||||||
|
|
||||||
return m3u8
|
m3u8.append("#EXT-X-ENDLIST")
|
||||||
|
|
||||||
def parseDuration(self, duration: str):
|
return '\n'.join(m3u8)
|
||||||
if duration.find(':') > -1:
|
|
||||||
duration = duration.split(':')
|
|
||||||
duration.reverse()
|
|
||||||
duration = sum([float(duration[i]) * (60 ** i) for i in range(len(duration))])
|
|
||||||
else:
|
|
||||||
duration = float(duration)
|
|
||||||
|
|
||||||
return duration
|
async def make_subtitle(self, download_path: str, file: str, duration: float):
|
||||||
|
realFile: str = calcDownloadPath(basePath=download_path, folder=file, createPath=False)
|
||||||
|
|
||||||
|
if not os.path.exists(realFile):
|
||||||
|
raise Exception(f"File '{realFile}' does not exist.")
|
||||||
|
|
||||||
|
m3u8 = []
|
||||||
|
|
||||||
|
m3u8.append("#EXTM3U")
|
||||||
|
m3u8.append("#EXT-X-VERSION:3")
|
||||||
|
m3u8.append(f"#EXT-X-TARGETDURATION:{int(self.duration)}")
|
||||||
|
m3u8.append("#EXT-X-MEDIA-SEQUENCE:0")
|
||||||
|
m3u8.append("#EXT-X-PLAYLIST-TYPE:VOD")
|
||||||
|
m3u8.append(f"#EXTINF:{duration},")
|
||||||
|
m3u8.append(f"{self.url}player/subtitle/{quote(file)}.vtt")
|
||||||
|
m3u8.append("#EXT-X-ENDLIST")
|
||||||
|
|
||||||
|
return '\n'.join(m3u8)
|
||||||
|
|
|
||||||
77
app/player/Playlist.py
Normal file
77
app/player/Playlist.py
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
import glob
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
from urllib.parse import quote
|
||||||
|
from Utils import calcDownloadPath
|
||||||
|
import pathlib
|
||||||
|
from .ffprobe import FFProbe
|
||||||
|
from .Subtitle import Subtitle
|
||||||
|
|
||||||
|
|
||||||
|
class Playlist:
|
||||||
|
_url: str = None
|
||||||
|
|
||||||
|
def __init__(self, url: str):
|
||||||
|
self.url = url
|
||||||
|
|
||||||
|
async def make(self, download_path: str, file: str):
|
||||||
|
rFile = pathlib.Path(calcDownloadPath(basePath=download_path, folder=file, createPath=False))
|
||||||
|
|
||||||
|
if not rFile.exists():
|
||||||
|
raise Exception(f"File '{rFile}' does not exist.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
ffprobe = FFProbe(rFile)
|
||||||
|
await ffprobe.run()
|
||||||
|
except UnicodeDecodeError as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not 'duration' in ffprobe.metadata:
|
||||||
|
raise Exception(f"Unable to get '{rFile}' duration.")
|
||||||
|
|
||||||
|
duration: float = float(ffprobe.metadata.get('duration'))
|
||||||
|
|
||||||
|
playlist = []
|
||||||
|
playlist.append("#EXTM3U")
|
||||||
|
|
||||||
|
subs = ""
|
||||||
|
|
||||||
|
index = 0
|
||||||
|
for item in self.getSideCarFiles(rFile):
|
||||||
|
if not item.suffix in Subtitle.allowedExtensions:
|
||||||
|
continue
|
||||||
|
|
||||||
|
index += 1
|
||||||
|
lang: str = "und"
|
||||||
|
lg = re.search(r'\.(?P<lang>\w{2,3})\.\w{3}$', item.name)
|
||||||
|
if lg:
|
||||||
|
lang = lg.groupdict().get('lang')
|
||||||
|
|
||||||
|
subs = ',SUBTITLES="subs"'
|
||||||
|
|
||||||
|
url = f"{self.url}player/m3u8/subtitle/{quote(str(pathlib.Path(file).with_name(item.name)))}.m3u8?duration={duration}"
|
||||||
|
name = f"{item.suffix[1:].upper()} ({index}) - {lang}"
|
||||||
|
playlist.append(
|
||||||
|
f'#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="{name}",DEFAULT=NO,AUTOSELECT=NO,FORCED=NO,LANGUAGE="{lang}",URI="{url}"')
|
||||||
|
|
||||||
|
playlist.append(f'#EXT-X-STREAM-INF:PROGRAM-ID=1{subs}')
|
||||||
|
playlist.append(f"{self.url}player/m3u8/video/{quote(file)}.m3u8")
|
||||||
|
|
||||||
|
return '\n'.join(playlist)
|
||||||
|
|
||||||
|
def getSideCarFiles(self, file: pathlib.Path) -> list[pathlib.Path]:
|
||||||
|
"""
|
||||||
|
Get sidecar files for the given file.
|
||||||
|
|
||||||
|
:param file: File to get sidecar files for.
|
||||||
|
:return: List of sidecar files.
|
||||||
|
"""
|
||||||
|
files = []
|
||||||
|
|
||||||
|
for sub_file in file.parent.glob(f"{glob.escape(file.stem)}.*"):
|
||||||
|
if sub_file == file or sub_file.is_file() is False or sub_file.stem.startswith('.'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
files.append(sub_file)
|
||||||
|
|
||||||
|
return files
|
||||||
66
app/player/Subtitle.py
Normal file
66
app/player/Subtitle.py
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import asyncio
|
||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
from Utils import calcDownloadPath
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
LOG = logging.getLogger('player.subtitle')
|
||||||
|
|
||||||
|
|
||||||
|
class Subtitle:
|
||||||
|
allowedExtensions: tuple[str] = (".srt", ".vtt", ".ass",)
|
||||||
|
|
||||||
|
async def make(self, path: str, file: str) -> bytes:
|
||||||
|
realFile: str = calcDownloadPath(basePath=path, folder=file, createPath=False)
|
||||||
|
|
||||||
|
rFile = pathlib.Path(realFile)
|
||||||
|
|
||||||
|
if not rFile.exists():
|
||||||
|
raise Exception(f"File '{file}' does not exist.")
|
||||||
|
|
||||||
|
if not rFile.suffix in self.allowedExtensions:
|
||||||
|
raise Exception(f"File '{file}' subtitle type is not supported.")
|
||||||
|
|
||||||
|
if rFile.suffix is ".vtt":
|
||||||
|
subData = ''
|
||||||
|
with open(realFile, 'r') as f:
|
||||||
|
subData = f.read()
|
||||||
|
|
||||||
|
return subData
|
||||||
|
|
||||||
|
tmpDir: str = tempfile.gettempdir()
|
||||||
|
tmpFile = os.path.join(tmpDir, f'player.subtitle.{hashlib.md5(realFile.encode("utf-8")).hexdigest()}')
|
||||||
|
|
||||||
|
if not os.path.exists(tmpFile):
|
||||||
|
os.symlink(realFile, tmpFile)
|
||||||
|
|
||||||
|
fargs = []
|
||||||
|
fargs.append('-xerror')
|
||||||
|
fargs.append('-hide_banner')
|
||||||
|
fargs.append('-loglevel')
|
||||||
|
fargs.append('error')
|
||||||
|
fargs.append('-i')
|
||||||
|
fargs.append(f'file:{tmpFile}')
|
||||||
|
fargs.append('-f')
|
||||||
|
fargs.append('webvtt')
|
||||||
|
fargs.append('pipe:1')
|
||||||
|
|
||||||
|
LOG.debug(f"Converting '{realFile}' into 'webvtt'. " + " ".join(fargs))
|
||||||
|
|
||||||
|
proc = await asyncio.subprocess.create_subprocess_exec(
|
||||||
|
'ffmpeg', *fargs,
|
||||||
|
stdin=asyncio.subprocess.DEVNULL,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE
|
||||||
|
)
|
||||||
|
|
||||||
|
data, err = await proc.communicate()
|
||||||
|
|
||||||
|
if 0 != proc.returncode:
|
||||||
|
msg = f"Failed to convert '{realFile}' into 'webvtt'."
|
||||||
|
LOG.error(f'{msg} {err.decode("utf-8")}.')
|
||||||
|
raise Exception(msg)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
@ -157,7 +157,7 @@ onMounted(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const archiveItem = (type, item) => {
|
const archiveItem = (type, item) => {
|
||||||
if (!confirm(`Archive '${item.title ?? item.id ?? item.url ?? '??'}'?`)){
|
if (!confirm(`Archive '${item.title ?? item.id ?? item.url ?? '??'}'?`)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sendData('archive_item', item);
|
sendData('archive_item', item);
|
||||||
|
|
@ -230,14 +230,14 @@ const addItem = (item) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const playItem = item => {
|
const playItem = item => {
|
||||||
let baseDir = 'm3u8/';
|
let baseDir = 'player/playlist/';
|
||||||
|
|
||||||
if (item.folder) {
|
if (item.folder) {
|
||||||
item.folder = item.folder.replace('#', '%23');
|
item.folder = item.folder.replace('#', '%23');
|
||||||
baseDir += item.folder + '/';
|
baseDir += item.folder + '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
video_link.value = config.app.url_host + config.app.url_prefix + baseDir + encodeURIComponent(item.filename);
|
video_link.value = config.app.url_host + config.app.url_prefix + baseDir + encodeURIComponent(item.filename) + '.m3u8';
|
||||||
};
|
};
|
||||||
|
|
||||||
const reloadWindow = () => window.location.reload();
|
const reloadWindow = () => window.location.reload();
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,21 @@
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--plyr-captions-background: rgba(0, 0, 0, 0.6);
|
||||||
|
--plyr-captions-text-color: #f3db4d;
|
||||||
|
--webkit-text-track-display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plyr__caption {
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
|
||||||
|
font-size: 140%;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plyr--full-ui ::-webkit-media-text-track-container {
|
||||||
|
display: var(--webkit-text-track-display);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<video ref="video" :poster="previewImageLink" :controls="isControls" :title="title">
|
<video ref="video" :poster="previewImageLink" :controls="isControls" :title="title">
|
||||||
<source :src="link" type="application/x-mpegURL" />
|
<source :src="link" type="application/x-mpegURL" />
|
||||||
|
|
@ -32,35 +50,43 @@ const props = defineProps({
|
||||||
const emitter = defineEmits(['closeModel'])
|
const emitter = defineEmits(['closeModel'])
|
||||||
|
|
||||||
const video = ref(null)
|
const video = ref(null)
|
||||||
const player = ref(null)
|
let player = null;
|
||||||
const hls = ref(null)
|
let hls = null;
|
||||||
let eventHandler = null
|
|
||||||
|
const eventFunc = e => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
emitter('closeModel')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
if (/(iPhone|iPod|iPad).*AppleWebKit/i.test(navigator.userAgent)) {
|
||||||
|
document.documentElement.style.setProperty('--webkit-text-track-display', 'block');
|
||||||
|
}
|
||||||
|
|
||||||
prepareVideoPlayer()
|
prepareVideoPlayer()
|
||||||
eventHandler = window.addEventListener('keydown', (event) => {
|
window.addEventListener('keydown', eventFunc)
|
||||||
if (event.key === 'Escape') {
|
|
||||||
emitter('closeModel')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onUpdated(() => {
|
onUpdated(() => prepareVideoPlayer())
|
||||||
prepareVideoPlayer()
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
player.value.destroy()
|
if (player) {
|
||||||
hls.value.destroy()
|
player.destroy()
|
||||||
window.removeEventListener('keydown', eventHandler)
|
}
|
||||||
|
if (hls) {
|
||||||
|
hls.destroy()
|
||||||
|
}
|
||||||
|
window.removeEventListener('keydown', eventFunc)
|
||||||
})
|
})
|
||||||
|
|
||||||
const prepareVideoPlayer = () => {
|
const prepareVideoPlayer = () => {
|
||||||
player.value = new Plyr(video.value, {
|
player = new Plyr(video.value, {
|
||||||
|
debug: false,
|
||||||
clickToPlay: true,
|
clickToPlay: true,
|
||||||
keyboard: { focused: true, global: true },
|
keyboard: { focused: true, global: true },
|
||||||
controls: [
|
controls: [
|
||||||
'play-large', 'play', 'progress', 'current-time', 'duration', 'mute', 'volume', 'pip', 'airplay', 'fullscreen'
|
'play-large', 'play', 'progress', 'current-time', 'duration', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen'
|
||||||
],
|
],
|
||||||
fullscreen: {
|
fullscreen: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|
@ -69,14 +95,17 @@ const prepareVideoPlayer = () => {
|
||||||
},
|
},
|
||||||
storage: {
|
storage: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
key: 'plyr_volume'
|
key: 'plyr'
|
||||||
},
|
},
|
||||||
mediaMetadata: {
|
mediaMetadata: {
|
||||||
title: props.title.value
|
title: props.title
|
||||||
|
},
|
||||||
|
captions: {
|
||||||
|
update: true,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
hls.value = new Hls({
|
hls = new Hls({
|
||||||
debug: false,
|
debug: false,
|
||||||
enableWorker: true,
|
enableWorker: true,
|
||||||
lowLatencyMode: true,
|
lowLatencyMode: true,
|
||||||
|
|
@ -84,13 +113,10 @@ const prepareVideoPlayer = () => {
|
||||||
fragLoadingTimeOut: 200000,
|
fragLoadingTimeOut: 200000,
|
||||||
});
|
});
|
||||||
|
|
||||||
hls.value.loadSource(props.link)
|
hls.loadSource(props.link)
|
||||||
|
|
||||||
if (video.value) {
|
if (video.value) {
|
||||||
hls.value.attachMedia(video.value)
|
hls.attachMedia(video.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
|
||||||
|
|
|
||||||
|
|
@ -30,15 +30,17 @@ const app = createApp(App);
|
||||||
|
|
||||||
app.config.globalProperties.capitalize = s => s && s[0].toUpperCase() + s.slice(1);
|
app.config.globalProperties.capitalize = s => s && s[0].toUpperCase() + s.slice(1);
|
||||||
app.config.globalProperties.makeDownload = (config, item, base = 'download') => {
|
app.config.globalProperties.makeDownload = (config, item, base = 'download') => {
|
||||||
let baseDir = `${base}/`;
|
let baseDir = 'download' === base ? `${base}/` : 'player/playlist/';
|
||||||
|
|
||||||
if (item.folder) {
|
if (item.folder) {
|
||||||
item.folder = item.folder.replace('#', '%23');
|
item.folder = item.folder.replace('#', '%23');
|
||||||
baseDir += item.folder + '/';
|
baseDir += item.folder + '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
return config.app.url_host + config.app.url_prefix + baseDir + encodeURIComponent(item.filename);
|
let url = config.app.url_host + config.app.url_prefix + baseDir + encodeURIComponent(item.filename);
|
||||||
|
return ('m3u8' === base) ? url + '.m3u8' : url;
|
||||||
}
|
}
|
||||||
|
|
||||||
app.config.globalProperties.formatBytes = (bytes, decimals = 2) => {
|
app.config.globalProperties.formatBytes = (bytes, decimals = 2) => {
|
||||||
if (!+bytes) return '0 Bytes'
|
if (!+bytes) return '0 Bytes'
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue