diff --git a/src/tribler/core/libtorrent/download_manager/download.py b/src/tribler/core/libtorrent/download_manager/download.py index bae12d3380..0dc553adc5 100644 --- a/src/tribler/core/libtorrent/download_manager/download.py +++ b/src/tribler/core/libtorrent/download_manager/download.py @@ -811,7 +811,7 @@ def stop(self, user_stopped: bool | None = None) -> Awaitable[None]: """ self._logger.debug("Stopping %s", self.tdef.get_name()) if self.stream is not None: - self.stream.disable() + self.stream.close() if user_stopped is not None: self.config.set_user_stopped(user_stopped) if self.handle and self.handle.is_valid(): diff --git a/src/tribler/core/libtorrent/download_manager/download_manager.py b/src/tribler/core/libtorrent/download_manager/download_manager.py index 1cf278e8ba..12885e032b 100644 --- a/src/tribler/core/libtorrent/download_manager/download_manager.py +++ b/src/tribler/core/libtorrent/download_manager/download_manager.py @@ -872,7 +872,7 @@ async def remove_download(self, download: Download, remove_content: bool = False if handle: if handle.is_valid(): if download.stream is not None: - download.stream.disable() + download.stream.close() logger.debug("Removing handle %s", hexlify(infohash)) (await self.get_session(download.config.get_hops())).remove_torrent(handle, int(remove_content)) else: diff --git a/src/tribler/core/libtorrent/download_manager/stream.py b/src/tribler/core/libtorrent/download_manager/stream.py index 69d589e7f1..4e9b263fdf 100644 --- a/src/tribler/core/libtorrent/download_manager/stream.py +++ b/src/tribler/core/libtorrent/download_manager/stream.py @@ -1,65 +1,29 @@ -""" -There are 2 types of prioritisation implemented: -1-STATIC PRIORITISATION: Header + Footer + Prebuffer Prioritisation: -When a file in torrent is set to stream, predefined size of heeder + footer + prebuffer is -set to prio:7 deadline:0 and rest of the files and pieces are set to prio:0 (which means dont download at all) -to only focus the required pieces to finish before file starts to stream. The client is expected not to start playing -the file in this state. The state of this static buffering can be observed over the rest api. - -2-DYNMAIC PRIORITISATION: When the static prio is finished then the client can start playing the file, -When a client starts playing the file over http, it will request a chunk, each requested chunk will initiate a -dynmaic buffer according to the current read position of the file mapped to related piece of the torrent file. -The undownloaded pieces starting from the current read position with the length of prebuffsize will be prioritised -with the DEADLINE_PRIO_MAP sequence and will be deadlined with the indexes of the same map. -Rest of the pieces are to prio: 1 and no deadline. Note that, the prio, deadline and the actual pieces impacted -will be dynamically updated eachtime more chunks are readed, until EOF. -Each chunk will have its own prio applied, and there can be multiple concurrent chucks -for a given fileindex of a torrent -""" from __future__ import annotations import logging +import math from asyncio import sleep -from io import BufferedReader -from pathlib import Path -from typing import TYPE_CHECKING, Callable, Generator, cast - -import libtorrent -from typing_extensions import Self +from typing import TYPE_CHECKING from tribler.core.libtorrent.download_manager.download_state import DownloadStatus -from tribler.core.libtorrent.torrents import check_vod if TYPE_CHECKING: + from collections.abc import Generator + from io import BufferedReader + from pathlib import Path from types import TracebackType + from typing_extensions import Self + from tribler.core.libtorrent.download_manager.download import Download -# Header and footer sizes are necessary for video client to detect file codecs and muxer metadata. -# Without below pieces are ready, streamer should not start +# Before streaming starts, we first download the header/footer of the file. HEADER_SIZE = 5 * 1024 * 1024 FOOTER_SIZE = 1 * 1024 * 1024 -# the percent of the file size to be used as moving or static prebufferng size -PREBUFF_PERCENT = 0.05 -# Below map defines the priority/deadline sequence for pieces. List index represents the deadline, and -# actual value in the index represents the priority of the piece sequence in the torrent file. -# Minimum prio must be 2, because prio 1 is used for not relevant pieces in streaming -# Max prio is 7 and must have only 1 deadline attached to it, because lt selects prio 7 regardles of picker desicion -# The narrower this map, better the prioritaztion worse the throughput ie: 7,6,5,3,2,1 -# The wider this map, worse the prioritization, better the throughput: ie: 7,6,6,6,6,6,6,6,6,5,5,5,5,5,5,5,5,5,5... -# Below logarithmic map is based on 2^n and achieves 3~4MB/s with a moving 4~5MB window prioritization -# There is no prio 5 in libtorrent priorities -# https://www.libtorrent.org/manual.html#piece-priority-prioritize-pieces-piece-priorities +# Buffer size as percentage of the file size +BUFFER_PERCENT = 0.05 +# Deadlines to be used for pieces that are at the current file cursor position DEADLINE_PRIO_MAP = [7, 6, 6, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2] -# time to detect if the client is no more requesting stream chunks from rest api -# basically this means the video is either paused, or seeked to another postion -# but the player still wants the old tcp session alive, -# above setting can be reduced as low as 1 sec. -# lower this value, better the seek responsivenes -STREAM_PAUSE_TIME = 1 -# never use 0 priority because when streams are paused -# we still want lt to download the pieces not important for the stream -MIN_PIECE_PRIO = 1 class NotStreamingError(Exception): @@ -84,394 +48,195 @@ def __init__(self, download: Download) -> None: Create a stream for the given download. """ self._logger = logging.getLogger(self.__class__.__name__) - self.infohash: bytes | None = None - self.filename: Path | None = None - self.filesize: int | None = None - self.enabledfiles: list[int] | None = None - self.firstpiece: int | None = None - self.lastpiece: int | None = None - self.prebuffsize: int | None = None - self.destdir: Path | None = None - self.piecelen: int | None = None - self.files: list[tuple[Path, int]] | None = None - self.mapfile: Callable[[int, int, int], libtorrent.peer_request] | None = None - self.prebuffpieces: list[int] = [] - self.headerpieces: list[int] = [] - self.footerpieces: list[int] = [] - # cursorpiecemap represents the pieces maintained by all available chunks. - # Each chunk is identified by its startbyte - # structure for cursorpieces is - # <-------------------- dynamic buffer pieces --------------------> - # {int:startbyte: (bool:ispaused, list:piecestobuffer 'according to the cursor of the related chunk') - self.cursorpiecemap: dict[int, tuple[bool, list[int]]] = {} - self.fileindex: int | None = None - # when first initiate this instance does not have related callback ready, - # this coro will be awaited when the stream is enabled. If never enabled, - # this coro will be closed. - self.__prepare_coro = self.__prepare(download) - - # required callbacks used in this class but defined in download class. - # an other approach would be using self.__download = download but - # below method looks cleaner - self.__lt_state = download.get_state - self.__getpieceprios = download.get_piece_priorities - self.__setpieceprios = download.set_piece_priorities - self.__getfileprios = download.get_file_priorities - self.__setselectedfiles = download.set_selected_files - self.__setdeadline = download.set_piece_deadline - self.__resetdeadline = download.reset_piece_deadline - self.__resumedownload = download.resume - - async def __prepare(self, download: Download) -> None: - # wait for an handle first - await download.get_handle() - self.destdir = download.get_content_dest() - metainfo = None - while not metainfo: - # Wait for an actual tdef with an actual metadata is available - metainfo = download.get_def().get_metainfo() - if not metainfo: - await sleep(1) - tdef = download.get_def() - self.piecelen = tdef.get_piece_length() - self.files = tdef.get_files_with_length() - # we use self.infohash also like a flag to detect that stream class is prepared - self.infohash = tdef.get_infohash() - self.mapfile = tdef.torrent_info.map_file - - async def enable(self, fileindex: int = 0, prebufpos: int | None = None) -> None: - """ - Enable streaming mode for a given fileindex. - """ - # if not prepared, prepare the callbacks - if not self.infohash: - await self.__prepare_coro - - self.destdir = cast(Path, self.destdir) - self.piecelen = cast(int, self.piecelen) - self.files = cast(list[tuple[Path, int]], self.files) - self.infohash = cast(bytes, self.infohash) - self.mapfile = cast(Callable[[int, int, int], libtorrent.peer_request], self.mapfile) - - # if fileindex not available for torrent raise exception - if fileindex >= len(self.files): + self.download = download + self.file_index: int = 0 + self.file_size: int = 0 + self.file_name: Path | None = None + self.buffer_size: int = 0 + self.piece_length: int = 0 + self.cursor_pieces: dict[int, list[int]] = {} + + async def enable(self, file_index: int = 0, + buffer_position: int | None = None, buffer_percent: float = BUFFER_PERCENT, + header_size: int = HEADER_SIZE, footer_size: int = FOOTER_SIZE) -> None: + """ + Sets the file index and waits for initial buffering to be completed. + """ + # Check if the file index exists + files = self.download.get_def().get_files_with_length() + if file_index >= len(files) or file_index < 0: raise NoAvailableStreamError - # if download is stopped for some reason, resume it. - self.__resumedownload() + # Set the new file + self.file_index = file_index + filename, self.file_size = files[file_index] + content_dest = self.download.get_content_dest() + self.file_name = content_dest / filename if self.download.get_def().is_multifile_torrent() else content_dest + self.buffer_size = int(self.file_size * buffer_percent) + self.piece_length = self.download.get_def().get_piece_length() - # wait until dlstate is downloading or seeding - while True: - status = self.__lt_state().get_status() - if status in [DownloadStatus.DOWNLOADING, DownloadStatus.SEEDING]: - break - await sleep(1) + # Ensure the download isn't paused + self.download.resume() - # the streaming status is tracked based on the infohash, if there is already no streaming - # or there is already a streaming for a fileindex but we need a new one, reinitialize the - # status map. When there is a state for a given infohash, the stream is accepted as streaming, - # which means after below line, stream.enaled = True - if fileindex != self.fileindex: - self.fileindex = fileindex - elif self.enabled: - # if already there is a state with the same file index do nothing - if prebufpos is not None: - # if the prebuffposiiton is updated, update the static prebuff pieces - currrent_prebuf = list(self.prebuffpieces) - currrent_prebuf.extend(self.bytestopieces(prebufpos, self.prebuffsize)) - self.prebuffpieces = sorted(set(currrent_prebuf)) + # Wait until the download is in the correct state + status = self.download.get_state().get_status() + while status not in [DownloadStatus.DOWNLOADING, DownloadStatus.SEEDING]: + await sleep(1) + status = self.download.get_state().get_status() + + # Give the selected file a high priority + file_priorities = self.download.get_file_priorities() + file_priorities[file_index] = 7 + self.download.set_file_priorities(file_priorities) + + # Check if buffer/header/footer needs downloading. + pieces_needed = [] + if buffer_position is not None: + pieces_needed += self.bytes_to_pieces(buffer_position, buffer_position + self.buffer_size - 1) + if header_size: + pieces_needed += self.bytes_to_pieces(0, header_size - 1) + if footer_size: + pieces_needed += self.bytes_to_pieces(self.file_size - footer_size, self.file_size - 1) + if self.pieces_complete(pieces_needed): + # Nothing to do here. Buffering is completed. return - # update the file name and size with the file index - filename, self.filesize = self.files[fileindex] - if len(self.files) > 1: - self.filename = self.destdir / filename - else: - self.filename = self.destdir - # Backup file prios, and set the the streaming file max prio - if not self.enabledfiles: - self.enabledfiles = [x[0] for x in enumerate(self.__getfileprios()) if x[1]] - self.__setselectedfiles([fileindex], 7, True) - # Get the piece map of the file - self.firstpiece = self.bytetopiece(0) # inclusive - self.lastpiece = min(self.bytetopiece(self.filesize), len(self.pieceshave) - 1) # inclusive - # prebuffer size PREBUFF_PERCENT of the file size - self.prebuffsize = int(self.filesize * PREBUFF_PERCENT) - # calculate static buffer pieces - self.headerpieces = self.bytestopieces(0, HEADER_SIZE) - self.footerpieces = self.bytestopieces(-FOOTER_SIZE, 0) - self.prebuffpieces = [] if prebufpos is None else self.bytestopieces(prebufpos, self.prebuffsize) - - @property - def enabled(self) -> bool: - """ - Check if stream is enabled. - """ - return self.infohash is not None and self.fileindex is not None - - @property - @check_vod(0) - def headerprogress(self) -> float: - """ - Get current progress of downloaded header pieces of the enabled stream, if not enabled returns 0. - """ - return self.calculateprogress(self.headerpieces, False) + # Starting buffering + priorities = self.download.get_piece_priorities() + for piece in self.iter_pieces(have=False): + if piece in pieces_needed: + priorities[piece] = 7 + self.download.set_piece_deadline(piece, 0) + else: + # We don't use prio < 1, as that causes the download progress to jump up and down in the UI + priorities[piece] = 1 + self.download.set_piece_priorities(priorities) - @property - @check_vod(0) - def footerprogress(self) -> float: - """ - Get current progress of downloaded footer pieces of the enabled stream, if not enabled returns 0. - """ - return self.calculateprogress(self.footerpieces, False) + # Wait until completed + await self.wait_for_pieces(pieces_needed) - @property - @check_vod(0) - def prebuffprogress(self) -> float: + def iter_pieces(self, have: bool | None = None, start_from: int | None = None) -> Generator[int, None, None]: """ - Get current progress of downloaded prebuff pieces of the enabled stream, if not enabled returns 0. + Generator function that yield the pieces for the active file index. """ - return self.calculateprogress(self.prebuffpieces, False) + pieces_have = self.download.get_state().get_pieces_complete() + first_piece = self.byte_to_piece(0) + last_piece = min(self.byte_to_piece(self.file_size - 1), len(pieces_have) - 1) - @property - @check_vod(0) - def prebuffprogress_consec(self) -> float: - """ - Get current progress of cosequently downloaded prebuff pieces of the enabled stream, if not enabled returns 0. - """ - return self.calculateprogress(self.prebuffpieces, True) - - @property - @check_vod([]) - def pieceshave(self) -> list[int]: - """ - Get a list of Booleans indicating that individual pieces of the selected fileindex has been downloaded or not. - """ - return self.__lt_state().get_pieces_complete() + for piece in range(first_piece, last_piece + 1): + if start_from is not None and piece < start_from: + continue + if have is None or (have and pieces_have[piece]) or (not have and not pieces_have[piece]): + yield piece - @check_vod(True) - def disable(self) -> None: + async def wait_for_pieces(self, pieces_needed: list[int]) -> None: """ - Stop Streaming. + Waits until the specified pieces have been completed. """ - self.fileindex = None - self.headerpieces = [] - self.footerpieces = [] - self.prebuffpieces = [] - self.cursorpiecemap = {} - self.resetprios() - self.__setselectedfiles(self.enabledfiles) + while not self.pieces_complete(pieces_needed): + await sleep(1) - def close(self) -> None: + def pieces_complete(self, pieces: list[int]) -> bool: """ - Close this class gracefully. + Checks if the specified pieces have been completed. """ - # Close the coroutine. Unnecessary calls should be harmless. - self.__prepare_coro.close() - self.disable() + have = self.download.get_state().get_pieces_complete() + return all(have[piece] for piece in pieces) - @check_vod([]) - def bytestopieces(self, bytes_begin: int, bytes_end: int) -> list[int]: + def bytes_to_pieces(self, bytes_begin: int, bytes_end: int) -> list[int]: """ Returns the pieces that represents the given byte range. """ - self.filesize = cast(int, self.filesize) # Ensured by ``check_vod`` + pieces_have = self.download.get_state().get_pieces_complete() + first_piece = self.byte_to_piece(0) + last_piece = min(self.byte_to_piece(self.file_size - 1), len(pieces_have) - 1) - bytes_begin = min(self.filesize, bytes_begin) if bytes_begin >= 0 else self.filesize + bytes_begin - bytes_end = min(self.filesize, bytes_end) if bytes_end > 0 else self.filesize + bytes_end + bytes_begin = max(bytes_begin, 0) + bytes_end = min(bytes_end, self.file_size - 1) + start_piece = max(self.byte_to_piece(bytes_begin), first_piece) + end_piece = min(self.byte_to_piece(bytes_end), last_piece) + return list(range(start_piece, end_piece + 1)) - startpiece = self.bytetopiece(bytes_begin) - endpiece = self.bytetopiece(bytes_end) - startpiece = max(startpiece, self.firstpiece) - endpiece = min(endpiece, self.lastpiece) - return list(range(startpiece, endpiece + 1)) - - @check_vod(-1) - def bytetopiece(self, byte_begin: int) -> int: + def byte_to_piece(self, byte_begin: int) -> int: """ Finds the piece position that begin_bytes is mapped to. - - ``check_vod`` ensures the types of ``self.mapfile`` and ``self.fileindex``. """ - self.mapfile = cast(Callable[[int, int, int], libtorrent.peer_request], self.mapfile) + return self.download.get_def().torrent_info.map_file(self.file_index, byte_begin, 0).piece - return self.mapfile(cast(int, self.fileindex), byte_begin, 0).piece - - @check_vod(0) - def calculateprogress(self, pieces: list[int], consec: bool) -> float: - """ - Claculates the download progress of a given piece list. - if consec is True, calcaulation is based only the pieces downloaded sequentially. + def update_priorities(self) -> None: """ - if not pieces: - return 1.0 - have = 0.0 - for piece in self.iterpieces(have=True, consec=consec): - if have >= len(pieces): - break - if piece in pieces: - have += 1 - return have / len(pieces) if pieces else 0.0 - - @check_vod([]) - def iterpieces(self, have: bool | None = None, consec: bool = False, - startfrom: int | None = None) -> Generator[int, None, None]: + Sets the piece priorities and deadlines according to the cursors of the outstanding stream requests. """ - Generator function that yield the pieces for the active fileindex. + if not (piece_priorities := self.download.get_piece_priorities()): + return - :param have: None: nofilter, True: only pieces we have, False: only pieces we dont have - :param consec: True: sequentially, False: all pieces - :param startfrom: int: start form index, None: start from first piece - """ - self.firstpiece = cast(int, self.firstpiece) # Ensured by ``check_vod`` - self.lastpiece = cast(int, self.lastpiece) # Ensured by ``check_vod`` + # Iterate over all pieces that have not yet been downloaded, and determine their appropriate priority/deadline. + for piece in self.iter_pieces(have=False): + deadline = None + for pieces in self.cursor_pieces.values(): + if piece in pieces and (deadline is None or pieces.index(piece) < deadline): + deadline = pieces.index(piece) + break + + if deadline is not None and deadline < len(DEADLINE_PRIO_MAP): + # Starting at the position of the cursor, set priorities according to DEADLINE_PRIO_MAP + if piece_priorities[piece] != DEADLINE_PRIO_MAP[deadline]: + self.download.set_piece_deadline(piece, deadline * 10) + piece_priorities[piece] = DEADLINE_PRIO_MAP[deadline] + elif deadline is not None: + # Set the pieces that are within the buffer to a lower priority + if piece_priorities[piece] != 1: + self.download.set_piece_deadline(piece, deadline * 10) + piece_priorities[piece] = 1 + else: + # All other pieces get the lowest priority + if piece_priorities[piece] != 1: + self.download.reset_piece_deadline(piece) + piece_priorities[piece] = 1 - if have is not None: - pieces_have = self.pieceshave - for piece in range(self.firstpiece, self.lastpiece + 1): - if startfrom is not None and piece < startfrom: - continue - if have is None or have and pieces_have[piece] or not have and not pieces_have[piece]: - yield piece - elif consec: - break + self.download.set_piece_priorities(piece_priorities) - async def updateprios(self) -> None: # noqa: C901, PLR0912, PLR0915 + def reset_priorities(self, pieces: list[int] | None = None, priority: int = 4) -> None: """ - This async function controls how the individual piece priority and deadline is configured. - This method is called when a stream in enabled, and when a chunk reads the stream each time. - The performance of this method is crucical since it gets called quite frequently. + Resets the priorities and deadlines of pieces. + If no pieces are provided reset all pieces within the current file. """ - if not self.enabled: - return - - def _updateprio(piece: int, prio: int, deadline: int | None = None) -> None: - """ - Utility function to update piece priorities. - """ - if curr_prio != prio: - piecepriorities[piece] = prio - if deadline is not None: - # it is cool to step deadlines with 10ms interval but in realty there is no need. - self.__setdeadline(piece, deadline * 10) - diffmap[piece] = f"{piece}:{deadline * 10}:{curr_prio}->{prio}" - else: - self.__resetdeadline(piece) - diffmap[piece] = f"{piece}:-:{curr_prio}->{prio}" - - def _find_deadline(piece: int) -> tuple[int, int] | tuple[None, None]: - """ - Find the cursor which has this piece closest to its start. - Returns the deadline for the piece and the cursor startbyte. - """ - # if piece is not in piecemaps, then there is no deadline - # if piece in piecemaps, then the deadline is the index of the related piecemap - deadline = None - cursor = None - for startbyte in self.cursorpiecemap: - paused, cursorpieces = self.cursorpiecemap[startbyte] - if not paused and piece in cursorpieces and \ - (deadline is None or cursorpieces.index(piece) < deadline): - deadline = cursorpieces.index(piece) - cursor = startbyte - if cursor is not None and deadline is not None: - return deadline, cursor - return None, None - - # current priorities - piecepriorities = self.__getpieceprios() - if not piecepriorities: - # this case might happen when hop count is changing. - return - # a map holds the changes, used only for logging purposes - diffmap: dict[int, str] = {} - # flag that holds if we are in static buffering phase of dynamic buffering - staticbuff = False - for piece in self.iterpieces(have=False): - # current piece prio - curr_prio = piecepriorities[piece] - if piece in self.footerpieces: - _updateprio(piece, 7, 0) - staticbuff = True - elif piece in self.headerpieces: - _updateprio(piece, 7, 1) - staticbuff = True - elif piece in self.prebuffpieces: - _updateprio(piece, 7, 2) - staticbuff = True - elif staticbuff: - _updateprio(piece, 0) - else: - # dynamic buffering - deadline, cursor = _find_deadline(piece) - if cursor is not None and deadline is not None: - if deadline < len(DEADLINE_PRIO_MAP): - # get prio according to deadline - _updateprio(piece, DEADLINE_PRIO_MAP[deadline], deadline) - else: - # the deadline is outside of map, set piece prio 1 with the deadline - # buffer size is bigger then prio_map - _updateprio(piece, 1, deadline) - else: - # the piece is not in buffer zone, set to min prio without deadline - _updateprio(piece, MIN_PIECE_PRIO) - if diffmap: - # log stuff - self._logger.info("Piece Piority changed: %s", repr(diffmap)) - self._logger.debug("Header Pieces: %s", repr(self.headerpieces)) - self._logger.debug("Footer Pieces: %s", repr(self.footerpieces)) - self._logger.debug("Prebuff Pieces: %s", repr(self.prebuffpieces)) - for startbyte in self.cursorpiecemap: - self._logger.debug("Cursor '%s' Pieces: %s", startbyte, repr(self.cursorpiecemap[startbyte])) - # BELOW LINE WILL BE REMOVED, Most of the above are for debugging to be cleaned - self._logger.debug("Current Prios: %s", [(x, piecepriorities[x]) for x in self.iterpieces(have=False)]) - self.__setpieceprios(piecepriorities) - - def resetprios(self, pieces: list[int] | None = None, prio: int | None = None) -> None: - """ - Resets the prios and deadline of the pieces of the active fileindex, - If no pieces are provided, resets every piece for the fileindex. - """ - prio = prio if prio is not None else 4 - piecepriorities = self.__getpieceprios() + piece_priorities = self.download.get_piece_priorities() if pieces is None: - pieces = list(range(len(piecepriorities))) + pieces = list(range(len(piece_priorities))) for piece in pieces: - self.__resetdeadline(piece) - self.__setpieceprios([prio] * len(pieces)) + self.download.reset_piece_deadline(piece) + self.download.set_piece_priorities([priority] * len(pieces)) + file_priorities = self.download.get_file_priorities() + file_priorities[self.file_index] = 4 + self.download.set_file_priorities(file_priorities) + + def close(self) -> None: + """ + Closes the Stream. + """ + self.cursor_pieces.clear() + self.reset_priorities() -class StreamChunk: +class StreamReader: """ - This class represents the chunk to be read for the torrent file, and controls the dynamic buffer of the + File-like object that reads a file from a torrent, and controls the dynamic buffer of the stream instance according to read position. """ - def __init__(self, stream: Stream, startpos: int = 0) -> None: + def __init__(self, stream: Stream, start_offset: int = 0) -> None: """ - Create a new StreamChunk. - - :param stream: the stream to be read - :param startpos: the position offset the the chunk should read from. + Creates a new StreamChunk. """ self._logger = logging.getLogger(self.__class__.__name__) - if not stream.enabled: - raise NotStreamingError self.stream = stream self.file: BufferedReader | None = None - self.startpos = startpos - self.__seekpos = self.startpos - - @property - def seekpos(self) -> int: - """ - Current seek position of the actual file on the filesystem. - """ - return self.__seekpos + self.start_offset = self.seek_offset = start_offset async def __aenter__(self) -> Self: """ - Open the chunk. + Opens the chunk. """ await self.open() return self @@ -479,150 +244,81 @@ async def __aenter__(self) -> Self: async def __aexit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> None: """ - Close the chunk. + Closes the chunk. """ - self._logger.info("Stream %s closed due to %s", self.startpos, exc_type) self.close() async def open(self) -> None: """ - Opens the file in the filesystem until its ready and seeks to the seekpos position. + Opens the file in the filesystem until its ready and seeks to seek_offset. """ - filename = cast(Path, self.stream.filename) # Ensured by ``NotStreamingError`` (in ``__init__``) + if self.stream.file_name is None: + raise NotStreamingError - while not filename.exists(): + while not self.stream.file_name.exists(): await sleep(1) - self.file = open(filename, "rb") # noqa: ASYNC101, SIM115 - self.file.seek(self.seekpos) - @property - def isclosed(self) -> bool: - """ - Check if the file (if it exists) belonging to this chunk is closed. - """ - return self.file is None or self.file.closed + self.file = open(self.stream.file_name, "rb") # noqa: SIM115, ASYNC230 + self.file.seek(self.seek_offset) - @property - def isstarted(self) -> bool: - """ - Checks if the this chunk has already registered itself to stream instance. - """ - return self.startpos in self.stream.cursorpiecemap + # If we seek multiple times in a row, the video player will keep all connections open until the required + # pieces have been downloaded. This considerably slows down the final and most important request. + # To avoid issues, we allow only 1 connection at a time. By clearing cursor_pieces, the read functions of + # the other connections will return b"", caused the DownloadsEndpoint to drop the connections. + self.stream.cursor_pieces.clear() - @property - def ispaused(self) -> bool: + async def seek(self, byte_offset: int) -> None: """ - Checks if the chunk is in paused state. + Seeks the stream to the related piece that represents the position byte. + Also updates the dynamic buffer accordingly. """ - if self.isstarted and self.stream.cursorpiecemap[self.startpos][0]: - return True - return False + # Find and store the pieces that we need at the given offset + piece_offset = self.stream.byte_to_piece(byte_offset) + num_pieces = math.ceil(self.stream.buffer_size / self.stream.piece_length) + pieces = list(self.stream.iter_pieces(have=False, start_from=piece_offset))[:num_pieces] + self.stream.cursor_pieces[self.start_offset] = pieces - @property - def shouldpause(self) -> bool: - """ - Checks if this chunk should pause, based on the desicion that - any other chunks also is streaming the same torrent or not. - """ - for spos in self.stream.cursorpiecemap: - if spos == self.startpos: - continue - paused, pieces = self.stream.cursorpiecemap[spos] - if not paused and pieces: - return True - return False + # Update the torrent priorities + self.stream.update_priorities() - def pause(self, force: bool = False) -> bool: - """ - Sets the chunk pieces to pause, if not forced, chunk is only paused if other chunks are not paused. - """ - if not self.ispaused and (self.shouldpause or force): - self.stream.cursorpiecemap[self.startpos] = True, self.stream.cursorpiecemap[self.startpos][1] - return True - return False + # Update the file cursor + if self.file: + self.seek_offset = byte_offset + self.file.seek(self.seek_offset) - def resume(self, force: bool = False) -> bool: + async def read(self) -> bytes: """ - Sets the chunk pieces to resume, if not forced, chunk is only resume if other chunks are paused. + Reads piece_length bytes starting from the current seek position. """ - if self.ispaused and (not self.shouldpause or force): - self.stream.cursorpiecemap[self.startpos] = False, self.stream.cursorpiecemap[self.startpos][1] - return True - return False + # Do we need to stop reading? + if self.start_offset not in self.stream.cursor_pieces or not self.file: + return b"" - async def seek(self, positionbyte: int) -> list[int]: - """ - Seeks the stream to the related picece that represents the position byte. - Also updates the dynamic buffer accordingly. - """ - self.stream.prebuffsize = cast(int, self.stream.prebuffsize) # Ensured by ``NotStreamingError`` - self.stream.piecelen = cast(int, self.stream.piecelen) # Ensured by ``NotStreamingError`` - - buffersize = 0 - pospiece = self.stream.bytetopiece(positionbyte) - pieces = [] - # note that piece buffer is based the undownloaded piece up the size of prebuffsize - for piece in self.stream.iterpieces(have=False, startfrom=pospiece): - if buffersize < self.stream.prebuffsize: - pieces.append(piece) - buffersize += self.stream.piecelen - else: - break - # update cursor piece that represents this chunk - self.stream.cursorpiecemap[self.startpos] = (self.ispaused, pieces) - # update the torrent prios - await self.stream.updateprios() - # update the file cursor also - if self.file: - self.__seekpos = positionbyte - self.file.seek(self.seekpos) - return pieces + await self.seek(self.seek_offset) + piece = self.stream.byte_to_piece(self.seek_offset) + self._logger.debug('Chunk %s: Get piece %s', self.start_offset, piece) + + # Note the even though we're reading piece_length at a time, that doesn't mean that we only need 1 piece. + pieces_needed = self.stream.bytes_to_pieces(self.seek_offset, self.seek_offset + self.stream.piece_length) + await self.stream.wait_for_pieces(pieces_needed) + + # Using libtorrent's `read_piece` is too slow for our purposes, so we read the data from disk. + result = self.file.read(self.stream.piece_length) + self._logger.debug('Chunk %s: Got bytes %s-%s, piecelen: %s', + self.start_offset, self.seek_offset, self.seek_offset + len(result), + self.stream.piece_length) + self.seek_offset = self.file.tell() + return result def close(self) -> None: """ - Closes the chunk gracefully, also unregisters the cursor pieces from the stream instance - and resets the relevant piece prios. + Closes the reader amd unregisters the cursor pieces from the stream instance + and resets the relevant piece priorities. """ if self.file: self.file.close() self.file = None - if self.isstarted: - pieces = self.stream.cursorpiecemap.pop(self.startpos) - self.stream.resetprios(pieces[1], MIN_PIECE_PRIO) - async def read(self) -> bytes: - """ - Reads 1 piece that contains the seekpos. - """ - if not self.file and self.isstarted: - await self.open() - - await self.seek(self.seekpos) - piece = self.stream.bytetopiece(self.seekpos) - self._logger.debug('Chunk %s: Get piece %s', self.startpos, piece) - - if self.isclosed or piece > self.stream.lastpiece or not self.isstarted: - self.close() - self._logger.debug('Chunk %s: Got no bytes, file is closed', self.startpos) - return b'' - - self.file = cast(BufferedReader, self.file) # Ensured by ``self.isclosed`` - - # wait until we download what we want, then read the localfile - # experiment a garbage write mechanism here if the torrent read is too slow - piece = self.stream.bytetopiece(self.seekpos) - while True: - pieces_have = self.stream.pieceshave - if piece == -1: - self.close() - return b'' - if (0 <= piece < len(pieces_have) and pieces_have[piece]) or not self.isstarted: - break - self._logger.debug('Chunk %s, Waiting piece %s', self.startpos, piece) - await sleep(1) - - result = self.file.read(self.stream.piecelen) - self._logger.debug('Chunk %s: Got bytes %s-%s, %s bytes, piecelen: %s', - self.startpos, self.seekpos, self.seekpos + len(result), len(result), self.stream.piecelen) - self.__seekpos = self.file.tell() - return result + pieces = self.stream.cursor_pieces.pop(self.start_offset, None) + if pieces: + self.stream.reset_priorities(pieces) diff --git a/src/tribler/core/libtorrent/restapi/downloads_endpoint.py b/src/tribler/core/libtorrent/restapi/downloads_endpoint.py index 06f02c2486..4d57690547 100644 --- a/src/tribler/core/libtorrent/restapi/downloads_endpoint.py +++ b/src/tribler/core/libtorrent/restapi/downloads_endpoint.py @@ -1,7 +1,7 @@ from __future__ import annotations import mimetypes -from asyncio import get_event_loop, shield, wait_for +from asyncio import get_event_loop, shield from binascii import hexlify, unhexlify from pathlib import Path, PurePosixPath from typing import TYPE_CHECKING, Any, Optional, TypedDict, cast @@ -19,7 +19,7 @@ from tribler.core.libtorrent.download_manager.download_config import DownloadConfig from tribler.core.libtorrent.download_manager.download_manager import DownloadManager from tribler.core.libtorrent.download_manager.download_state import DOWNLOAD, UPLOAD, DownloadStatus -from tribler.core.libtorrent.download_manager.stream import Stream, StreamChunk +from tribler.core.libtorrent.download_manager.stream import Stream, StreamReader from tribler.core.libtorrent.torrentdef import TorrentDef from tribler.core.restapi.rest_endpoint import ( HTTP_BAD_REQUEST, @@ -249,8 +249,6 @@ def get_files_info_json_paged(download: Download, view_start: Path, view_size: i "availability": Float, "peers": String, "total_pieces": Integer, - "vod_prebuffering_progress": Float, - "vod_prebuffering_progress_consec": Float, "error": String, "time_added": Integer }), @@ -336,16 +334,9 @@ async def get_downloads(self, request: Request) -> RESTResponse: # noqa: C901 "completed_dir": download.config.get_completed_dir(), "total_pieces": tdef.get_nr_pieces(), "error": repr(state.get_error()) if state.get_error() else "", - "time_added": download.config.get_time_added() + "time_added": download.config.get_time_added(), + "streamable": bool(tdef and tdef.get_files_with_length({'mp4', 'm4v', 'mov', 'mkv'})) } - if download.stream: - info.update({ - "vod_prebuffering_progress": download.stream.prebuffprogress, - "vod_prebuffering_progress_consec": download.stream.prebuffprogress_consec, - "vod_header_progress": download.stream.headerprogress, - "vod_footer_progress": download.stream.footerprogress, - - }) if unfiltered or params.get("infohash") == info["infohash"]: # Add peers information if requested @@ -1137,11 +1128,8 @@ async def prepare(self, request: BaseRequest) -> AbstractStreamWriter | None: stream = self._download.stream start = start or 0 - if not stream.enabled or stream.fileindex != self._file_index: - await wait_for(stream.enable(self._file_index, start), 10) - await stream.updateprios() - - reader = StreamChunk(self._download.stream, start) + await stream.enable(self._file_index) + reader = StreamReader(stream, start) await reader.open() try: writer = await super().prepare(request) @@ -1153,7 +1141,7 @@ async def prepare(self, request: BaseRequest) -> AbstractStreamWriter | None: while data: await writer.write(data[:todo]) todo -= len(data) - if todo <= 0: + if todo <= 0 or len(data) == 0: break data = await reader.read() diff --git a/src/tribler/core/libtorrent/torrents.py b/src/tribler/core/libtorrent/torrents.py index 47f5e885ca..11bde7bcfd 100644 --- a/src/tribler/core/libtorrent/torrents.py +++ b/src/tribler/core/libtorrent/torrents.py @@ -15,7 +15,6 @@ from pathlib import Path from tribler.core.libtorrent.download_manager.download import Download - from tribler.core.libtorrent.download_manager.stream import Stream from tribler.core.libtorrent.torrentdef import InfoDict logger = logging.getLogger(__name__) @@ -84,24 +83,6 @@ def done_cb(fut: Future[lt.torrent_handle]) -> None: return invoke_func -def check_vod(default: WrappedReturn) -> Wrapped: - """ - Check if torrent is vod mode, else return default. - """ - - def wrap(f: Wrapped) -> Wrapped: - def invoke_func(self: Stream, - *args: WrappedParams.args, **kwargs: WrappedParams.kwargs # type: ignore[valid-type] - ) -> WrappedReturn: - if self.enabled: - return f(self, *args, **kwargs) - return default - - return invoke_func - - return wrap - - def common_prefix(paths_list: list[Path]) -> Path: """ Get the path prefixes component-wise. diff --git a/src/tribler/test_unit/core/libtorrent/download_manager/test_stream.py b/src/tribler/test_unit/core/libtorrent/download_manager/test_stream.py index 8d624ab095..b88a514b8b 100644 --- a/src/tribler/test_unit/core/libtorrent/download_manager/test_stream.py +++ b/src/tribler/test_unit/core/libtorrent/download_manager/test_stream.py @@ -10,12 +10,12 @@ from tribler.core.libtorrent.download_manager.download import Download from tribler.core.libtorrent.download_manager.download_config import SPEC_CONTENT, DownloadConfig -from tribler.core.libtorrent.download_manager.stream import Stream, StreamChunk +from tribler.core.libtorrent.download_manager.stream import NoAvailableStreamError, Stream, StreamReader from tribler.core.libtorrent.torrentdef import TorrentDef from tribler.test_unit.core.libtorrent.mocks import TORRENT_WITH_DIRS_CONTENT -class MockStreamChunk(StreamChunk): +class MockStreamReader(StreamReader): """ StreamChunk with a mocked open method. """ @@ -40,21 +40,20 @@ def setUp(self) -> None: """ super().setUp() - self.chunk = MockStreamChunk(Mock(), 0) - self.chunk.stream.cursorpiecemap = {0: [False, []]} + self.chunk = MockStreamReader(Mock(), 0) + self.chunk.stream.cursor_pieces = {0: []} def create_mock_content(self, content: bytes, piece_length: int = 1) -> None: """ Set the value of the stream to certain content. """ content_end = len(content) // piece_length - self.chunk.stream.updateprios = AsyncMock() - self.chunk.stream.iterpieces = lambda have, startfrom: list(range(startfrom, content_end)) - self.chunk.stream.pieceshave = [True] * content_end - self.chunk.stream.lastpiece = content_end - self.chunk.stream.bytetopiece = lambda x: x // piece_length - self.chunk.stream.prebuffsize = 1 - self.chunk.stream.piecelen = piece_length + self.chunk.stream.update_priorities = AsyncMock() + self.chunk.stream.iter_pieces = lambda have, start_from: list(range(start_from, content_end)) + self.chunk.stream.wait_for_pieces = AsyncMock() + self.chunk.stream.byte_to_piece = lambda x: x // piece_length + self.chunk.stream.buffer_size = 1 + self.chunk.stream.piece_length = piece_length self.chunk.file = BytesIO() self.chunk.file.write(b"content") self.chunk.file.seek(0) @@ -68,16 +67,28 @@ async def test_stream_error(self) -> None: raise RuntimeError self.assertIsNone(self.chunk.file) - self.assertTrue(self.chunk.isclosed) + + async def test_read_closed(self) -> None: + """ + Test if all bytes can be read from a chunk when it's closed. + """ + self.create_mock_content(b"content", len(b"content")) + self.chunk.close() + + async with self.chunk: + value = await self.chunk.read() + + self.assertEqual(b"", value) async def test_read_empty(self) -> None: """ Test if all bytes can be read from a chunk when it has no data. """ - self.chunk.stream.updateprios = AsyncMock() - self.chunk.stream.iterpieces = Mock(return_value=[]) - self.chunk.stream.lastpiece = 0 - self.chunk.stream.bytetopiece = Mock(return_value=-1) + self.chunk.stream.update_priorities = AsyncMock() + self.chunk.stream.iter_pieces = Mock(return_value=[]) + self.chunk.stream.buffer_size = self.chunk.stream.piece_length = 1 + self.chunk.stream.wait_for_pieces = AsyncMock() + self.chunk.stream.byte_to_piece = Mock(return_value=-1) async with self.chunk: value = await self.chunk.read() @@ -114,82 +125,22 @@ async def test_seek(self) -> None: """ self.create_mock_content(b"content", 1) - value = await self.chunk.seek(3) + await self.chunk.seek(3) streamed = b"" async with self.chunk: for _ in range(len(b"tent")): streamed += await self.chunk.read() - self.assertEqual([3], value) self.assertEqual(b"tent", streamed) - def test_pause(self) -> None: - """ - Test if the chunk can tell its stream to pause. - """ - self.chunk.stream.cursorpiecemap[1] = [False, [0]] - - result = self.chunk.pause() - - self.assertTrue(result) - self.assertTrue(self.chunk.stream.cursorpiecemap[0][0]) - - def test_pause_no_next(self) -> None: - """ - Test if the chunk cannot tell its stream to pause if it is at the cursor position. - """ - result = self.chunk.pause() - - self.assertFalse(result) - - def test_pause_already_paused(self) -> None: - """ - Test if the chunk cannot tell its stream to pause if its chunk is already paused. - """ - self.chunk.stream.cursorpiecemap[0][0] = True - - result = self.chunk.pause() - - self.assertFalse(result) - - def test_resume(self) -> None: - """ - Test if the chunk can tell its stream to resume. - """ - self.chunk.stream.cursorpiecemap[0] = [True, [0]] - self.chunk.stream.cursorpiecemap[1] = [True, [1]] - - result = self.chunk.resume() - - self.assertTrue(result) - self.assertFalse(self.chunk.stream.cursorpiecemap[0][0]) - - def test_resume_no_next(self) -> None: - """ - Test if the chunk can tell its stream to resume even if it is at the cursor position. - """ - self.chunk.stream.cursorpiecemap[0] = [True, []] - - result = self.chunk.resume() - - self.assertTrue(result) - - def test_resume_not_paused(self) -> None: - """ - Test if the chunk cannot tell its stream to resume if its chunk is not paused. - """ - result = self.chunk.resume() - - self.assertFalse(result) - class TestStream(TestBase): """ Tests for the Stream class. """ - def create_mock_download(self) -> Download: + def create_mock_download(self, piece_size: int | None = None, pieces: list[bool] | None = None) -> Download: """ Create a mocked DownloadConfig. """ @@ -204,6 +155,11 @@ def create_mock_download(self) -> Download: download.handle = Mock(is_valid=Mock(return_value=True), file_priorities=Mock(return_value=[0] * 6), torrent_file=Mock(return_value=download.tdef.torrent_info)) download.lt_status = Mock(state=3, paused=False, pieces=[]) + if piece_size is not None: + self.convert_to_piece_size(download, piece_size) + if pieces is not None: + download.lt_status.pieces = pieces + download.handle.piece_priorities = Mock(return_value=[0] * len(pieces)) return download def convert_to_piece_size(self, download: Download, piece_size: int) -> None: @@ -224,235 +180,166 @@ async def test_enable(self) -> None: Test if a stream can be enabled without known pieces. """ stream = Stream(self.create_mock_download()) + await stream.enable(file_index=0) - await stream.enable(fileindex=0) - - self.assertEqual(Path("torrent_create") / "abc" / "file2.txt", stream.filename) - self.assertEqual(0, stream.firstpiece) - self.assertEqual(-1, stream.lastpiece) + self.assertEqual(Path("torrent_create") / "abc" / "file2.txt", stream.file_name) + self.assertEqual(6, stream.file_size) - async def test_enable_have_pieces(self) -> None: + async def test_enable_unknown_file_index(self) -> None: """ - Test if a stream can be enabled when we already have pieces. + Test if trying to enable a stream with an unknown file index produces an error. """ - download = self.create_mock_download() - download.lt_status.pieces = [False] - stream = Stream(download) - - await stream.enable(fileindex=0) - - self.assertEqual(Path("torrent_create") / "abc" / "file2.txt", stream.filename) - self.assertEqual(0, stream.firstpiece) - self.assertEqual(0, stream.lastpiece) + stream = Stream(self.create_mock_download()) + with self.assertRaises(NoAvailableStreamError): + await stream.enable(file_index=6) - async def test_bytes_to_pieces(self) -> None: + async def test_enable_no_buffering(self) -> None: """ - Test if we can get the pieces from byte indices. + Test if a stream can be enabled without buffering. """ download = self.create_mock_download() - download.lt_status.pieces = [False, True] - self.convert_to_piece_size(download, 3) stream = Stream(download) - await stream.enable(fileindex=0) + await stream.enable(file_index=0, header_size=0, footer_size=0) - for i in range(7): - self.assertEqual(i // 3, stream.bytetopiece(i)) - self.assertEqual([0, 1], stream.bytestopieces(0, 6)) + download.handle.prioritize_pieces.assert_not_called() - async def test_calculateprogress_empty(self) -> None: + async def test_enable_header_footer(self) -> None: """ - Test if progress can be calculated without requested pieces. + Test if a stream can be enabled with a header and footer. """ - download = self.create_mock_download() - download.lt_status.pieces = [False] + download = self.create_mock_download(piece_size=1, pieces=[False] * 12) stream = Stream(download) - await stream.enable(fileindex=0) + stream.wait_for_pieces = AsyncMock() + await stream.enable(file_index=0, header_size=1, footer_size=1) - self.assertEqual(1.0, stream.calculateprogress([], False)) + self.assertEqual(call([7, 1, 1, 1, 1, 7] + [0] * 6), download.handle.prioritize_pieces.call_args) - async def test_calculateprogress_not_done(self) -> None: + async def test_enable_buffer(self) -> None: """ - Test if progress can be calculated without for a given piece that is not done. + Test if a stream can be enabled with a buffer. """ - download = self.create_mock_download() - download.lt_status.pieces = [False] + download = self.create_mock_download(piece_size=1, pieces=[False] * 12) stream = Stream(download) - await stream.enable(fileindex=0) + stream.wait_for_pieces = AsyncMock() + await stream.enable(file_index=0, buffer_position=2, buffer_percent=3/6, header_size=0, footer_size=0) - self.assertEqual(0.0, stream.calculateprogress([0], False)) + self.assertEqual(call([1, 1, 7, 7, 7, 1] + [0] * 6), download.handle.prioritize_pieces.call_args) - async def test_calculateprogress_done(self) -> None: + async def test_enable_have_pieces(self) -> None: """ - Test if progress can be calculated without for a given piece that is done. + Test if a stream can be enabled when we already have all pieces. """ - download = self.create_mock_download() - download.lt_status.pieces = [True] + download = self.create_mock_download(piece_size=1, pieces=[True] * 12) stream = Stream(download) - await stream.enable(fileindex=0) + stream.wait_for_pieces = AsyncMock() + await stream.enable(file_index=0) - self.assertEqual(1.0, stream.calculateprogress([0], False)) + self.assertEqual(Path("torrent_create") / "abc" / "file2.txt", stream.file_name) + stream.wait_for_pieces.assert_not_called() - async def test_calculateprogress_partial(self) -> None: + async def test_bytes_to_pieces(self) -> None: """ - Test if progress can be calculated without for two pieces of which one is done and one is not. + Test if we can get the pieces from byte indices. """ download = self.create_mock_download() download.lt_status.pieces = [False, True] self.convert_to_piece_size(download, 3) stream = Stream(download) - await stream.enable(fileindex=0) + stream.wait_for_pieces = AsyncMock() + download.handle.piece_priorities = Mock(return_value=[0, 0]) + await stream.enable(file_index=0) - self.assertEqual(0.5, stream.calculateprogress([0, 1], False)) + for i in range(7): + self.assertEqual(i // 3, stream.byte_to_piece(i)) + self.assertEqual([0, 1], stream.bytes_to_pieces(0, 6)) - async def test_updateprios_no_headers_all_missing(self) -> None: + async def test_update_priorities_all_missing(self) -> None: """ Test if priorities are set to retrieve missing pieces. """ - download = self.create_mock_download() - download.lt_status.pieces = [False] * 12 - self.convert_to_piece_size(download, 3) - download.handle.piece_priorities = Mock(return_value=[0] * 12) # 6 files, 2 pieces per file + download = self.create_mock_download(piece_size=1, pieces=[False] * 12) stream = Stream(download) - await stream.enable(fileindex=0) - - await stream.updateprios() + stream.wait_for_pieces = AsyncMock() + await stream.enable(file_index=0, header_size=0, footer_size=0) + stream.cursor_pieces[0] = list(range(3)) + stream.update_priorities() - self.assertEqual(call([7, 7, 7] + [0] * 9), download.handle.prioritize_pieces.call_args) + self.assertEqual(call([7, 6, 6, 1, 1, 1] + [0] * 6), download.handle.prioritize_pieces.call_args) - async def test_updateprios_no_headers_single_missing(self) -> None: + async def test_update_priorities_single_missing(self) -> None: """ Test if priorities are set to retrieve a missing piece. """ - download = self.create_mock_download() - download.lt_status.pieces = [False] + [True] * 11 - self.convert_to_piece_size(download, 3) - download.handle.piece_priorities = Mock(return_value=[0] * 12) # 6 files, 2 pieces per file + download = self.create_mock_download(piece_size=1, pieces=[False] + [True] * 11) stream = Stream(download) - await stream.enable(fileindex=0) - - await stream.updateprios() + stream.wait_for_pieces = AsyncMock() + await stream.enable(file_index=0, header_size=0, footer_size=0) + stream.cursor_pieces[0] = list(range(3)) + stream.update_priorities() self.assertEqual(call([7] + [0] * 11), download.handle.prioritize_pieces.call_args) - async def test_updateprios_no_headers_sparse(self) -> None: + async def test_update_priorities_sparse(self) -> None: """ Test if priorities can be updated for sparesely retrieved pieces. """ - download = self.create_mock_download() - download.lt_status.pieces = [False, True] * 6 - self.convert_to_piece_size(download, 3) - download.handle.piece_priorities = Mock(return_value=[0] * 12) # 6 files, 2 pieces per file - stream = Stream(download) - await stream.enable(fileindex=0) - - await stream.updateprios() - - self.assertEqual(call([7, 0, 7, 0] + [0] * 8), download.handle.prioritize_pieces.call_args) - - async def test_updateprios_headers_all_missing(self) -> None: - """ - Test if priorities are set to retrieve missing headers before anything else. - """ - download = self.create_mock_download() - download.lt_status.pieces = [False] * 12 - self.convert_to_piece_size(download, 3) - download.handle.piece_priorities = Mock(return_value=[0] * 12) # 6 files, 2 pieces per file + download = self.create_mock_download(piece_size=1, pieces=[False, True] * 6) stream = Stream(download) - await stream.enable(fileindex=0) - stream.headerpieces = [0] - stream.footerpieces = [11] - - await stream.updateprios() + stream.wait_for_pieces = AsyncMock() + await stream.enable(file_index=0, header_size=0, footer_size=0) + stream.cursor_pieces[0] = list(range(4)) + stream.update_priorities() - self.assertEqual(call([7] + [0] * 11), download.handle.prioritize_pieces.call_args) + self.assertEqual(call([7, 0, 6, 0, 1, 0] + [0] * 6), download.handle.prioritize_pieces.call_args) - async def test_updateprios_footer_missing(self) -> None: + async def test_update_priorities_offset_buffer(self) -> None: """ - Test if priorities are set to retrieve the first pieces with less priority if the footer is missing. + Test if priorities are set correctly if the buffer has an offset > 0. """ - download = self.create_mock_download() - download.lt_status.pieces = [True] + [False] * 11 - self.convert_to_piece_size(download, 3) - download.handle.piece_priorities = Mock(return_value=[0] * 12) # 6 files, 2 pieces per file + download = self.create_mock_download(piece_size=1, pieces=[True] + [False] * 11) stream = Stream(download) - await stream.enable(fileindex=0) - stream.headerpieces = [0] - stream.footerpieces = [11] - - await stream.updateprios() - - self.assertEqual(call([0, 1, 1] + [0] * 9), download.handle.prioritize_pieces.call_args) - - async def test_updateprios_header_footer_available(self) -> None: - """ - Test if priorities are set to retrieve the first pieces with less priority if the header and footer are there. - """ - download = self.create_mock_download() - download.lt_status.pieces = [True] + [False] * 10 + [True] - self.convert_to_piece_size(download, 3) - download.handle.piece_priorities = Mock(return_value=[0] * 12) # 6 files, 2 pieces per file - stream = Stream(download) - await stream.enable(fileindex=0) - stream.headerpieces = [0] - stream.footerpieces = [11] - - await stream.updateprios() - - self.assertEqual(call([0, 1, 1] + [0] * 9), download.handle.prioritize_pieces.call_args) - - async def test_updateprios_header_footer_prebuffer(self) -> None: - """ - Test if priorities are set to retrieve the prebuffer pieces with higher priority. - """ - download = self.create_mock_download() - download.lt_status.pieces = [True] + [False] * 10 + [True] - self.convert_to_piece_size(download, 3) - download.handle.piece_priorities = Mock(return_value=[0] * 12) # 6 files, 2 pieces per file - stream = Stream(download) - await stream.enable(fileindex=0) - stream.headerpieces = [0] - stream.prebuffpieces = [2, 3, 4] - stream.footerpieces = [11] - - await stream.updateprios() + stream.wait_for_pieces = AsyncMock() + await stream.enable(file_index=0, header_size=0, footer_size=0) + stream.cursor_pieces[2] = [2, 3, 4] + stream.update_priorities() - self.assertEqual(call([0, 1, 7] + [0] * 9), download.handle.prioritize_pieces.call_args) + self.assertEqual(call([0, 1, 7, 6, 6, 1] + [0] * 6), download.handle.prioritize_pieces.call_args) - async def test_resetprios_default(self) -> None: + async def test_reset_priorities_default(self) -> None: """ Test if streams can be reset to the default priority (4) for all pieces. """ download = self.create_mock_download() download.handle.piece_priorities = Mock(return_value=[2]) stream = Stream(download) - await stream.enable(fileindex=0) + await stream.enable(file_index=0) - stream.resetprios() + stream.reset_priorities() self.assertEqual(call([4]), download.handle.prioritize_pieces.call_args) - async def test_resetprios_prio(self) -> None: + async def test_reset_priorities_prio(self) -> None: """ Test if streams can be reset to the given priority for all pieces. """ download = self.create_mock_download() download.handle.piece_priorities = Mock(return_value=[2]) stream = Stream(download) - await stream.enable(fileindex=0) + await stream.enable(file_index=0) - stream.resetprios(prio=6) + stream.reset_priorities(priority=6) self.assertEqual(call([6]), download.handle.prioritize_pieces.call_args) - async def test_resetprios_given(self) -> None: + async def test_reset_priorities_given(self) -> None: """ Test if streams can be reset to the given priority for a given pieces. """ download = self.create_mock_download() download.handle.piece_priorities = Mock(return_value=[2]) stream = Stream(download) - await stream.enable(fileindex=0) + await stream.enable(file_index=0) - stream.resetprios(pieces=[0], prio=6) + stream.reset_priorities(pieces=[0], priority=6) self.assertEqual(call([6]), download.handle.prioritize_pieces.call_args) diff --git a/src/tribler/test_unit/core/libtorrent/restapi/test_downloads_endpoint.py b/src/tribler/test_unit/core/libtorrent/restapi/test_downloads_endpoint.py index 30b63098f6..3ed5c71f1b 100644 --- a/src/tribler/test_unit/core/libtorrent/restapi/test_downloads_endpoint.py +++ b/src/tribler/test_unit/core/libtorrent/restapi/test_downloads_endpoint.py @@ -221,27 +221,6 @@ async def test_get_downloads_filter_download_fail(self) -> None: self.assertNotIn("pieces", response_body_json["downloads"][0]) self.assertNotIn("availability", response_body_json["downloads"][0]) - async def test_get_downloads_stream_download(self) -> None: - """ - Test if the information of a steaming download is correctly presented. - """ - download = self.create_mock_download() - download.handle = Mock(is_valid=Mock(return_value=False)) - download.stream = Stream(download) - download.stream.close() - self.set_loaded_downloads([download]) - request = MockRequest("/api/downloads", query={}) - - response = await self.endpoint.get_downloads(request) - response_body_json = await response_to_json(response) - - self.assertEqual(200, response.status) - self.assertFalse(response_body_json["downloads"][0]["anon_download"]) - self.assertEqual(0.0, response_body_json["downloads"][0]["vod_prebuffering_progress"]) - self.assertEqual(0.0, response_body_json["downloads"][0]["vod_prebuffering_progress_consec"]) - self.assertEqual(0.0, response_body_json["downloads"][0]["vod_header_progress"]) - self.assertEqual(0.0, response_body_json["downloads"][0]["vod_footer_progress"]) - async def test_add_download_no_uri(self) -> None: """ Test if a graceful error is returned when no uri is given. @@ -876,7 +855,7 @@ async def test_stream_unsatisfiable(self) -> None: download.stream = Stream(download) download.stream.infohash = b"\x01" * 20 download.stream.fileindex = 0 - download.stream.filesize = 0 + download.stream.file_size = 0 download.tdef = TorrentDef.load_from_memory(TORRENT_WITH_VIDEO) self.download_manager.get_download = Mock(return_value=download) @@ -897,16 +876,13 @@ async def test_stream(self) -> None: download = self.create_mock_download() download.handle = Mock(is_valid=Mock(return_value=False)) download.stream = Stream(download) - download.stream.close() - download.stream.infohash = b"\x01" * 20 - download.stream.fileindex = 0 - download.stream.filesize = 1 - download.stream.filename = Path(__file__) - download.stream.mapfile = Mock(return_value=Mock(piece=0)) - download.stream.firstpiece = 0 - download.stream.lastpiece = 0 - download.stream.prebuffsize = 0 + download.stream.file_index = 0 + download.stream.file_size = 1 + download.stream.file_name = Path(__file__) + download.stream.buffer_size = 0 download.stream.enable = AsyncMock() + download.stream.piece_length = 1 + download.stream.byte_to_piece = lambda x: x + 1 download.lt_status = Mock(pieces=[True]) download.tdef = TorrentDef.load_from_memory(TORRENT_WITH_VIDEO) self.download_manager.get_download = Mock(return_value=download) diff --git a/src/tribler/test_unit/core/libtorrent/test_torrents.py b/src/tribler/test_unit/core/libtorrent/test_torrents.py index 034b0df0d3..a2b12e661a 100644 --- a/src/tribler/test_unit/core/libtorrent/test_torrents.py +++ b/src/tribler/test_unit/core/libtorrent/test_torrents.py @@ -8,17 +8,14 @@ from tribler.core.libtorrent.download_manager.download import Download from tribler.core.libtorrent.download_manager.download_config import SPEC_CONTENT, DownloadConfig -from tribler.core.libtorrent.download_manager.stream import Stream -from tribler.core.libtorrent.torrentdef import TorrentDef, TorrentDefNoMetainfo +from tribler.core.libtorrent.torrentdef import TorrentDefNoMetainfo from tribler.core.libtorrent.torrents import ( check_handle, - check_vod, common_prefix, create_torrent_file, get_info_from_handle, require_handle, ) -from tribler.test_unit.core.libtorrent.mocks import TORRENT_WITH_DIRS_CONTENT class TestTorrents(TestBase): @@ -116,34 +113,6 @@ def callback(_: Download) -> None: with self.assertRaises(ValueError): await future - async def test_check_vod_disabled(self) -> None: - """ - Test if the default value is returned for disabled vod mode. - """ - stream = Stream(Download(TorrentDefNoMetainfo(b"\x01" * 20, b"name", None), None, checkpoint_disabled=True, - config=DownloadConfig(ConfigObj(StringIO(SPEC_CONTENT))))) - stream.close() - - result = check_vod("default")(Stream.enabled)(stream) - - self.assertEqual("default", result) - - async def test_check_vod_enabled(self) -> None: - """ - Test if the function result is returned for enabled vod mode. - """ - download = Download(TorrentDef.load_from_memory(TORRENT_WITH_DIRS_CONTENT), None, - checkpoint_disabled=True, config=DownloadConfig(ConfigObj(StringIO(SPEC_CONTENT)))) - download.handle = Mock(is_valid=Mock(return_value=True), file_priorities=Mock(return_value=[0]), - torrent_file=Mock(return_value=Mock(map_file=Mock(return_value=Mock(piece=0))))) - stream = Stream(download) - download.lt_status = Mock(state=3, paused=False, pieces=[]) - await stream.enable() - - result = check_vod("default")(Stream.bytetopiece)(stream, 0) - - self.assertEqual(0, result) - def test_common_prefix_single(self) -> None: """ Test if a single file can a prefix of nothing. diff --git a/src/tribler/ui/package-lock.json b/src/tribler/ui/package-lock.json index 76a833b725..111290cd6c 100644 --- a/src/tribler/ui/package-lock.json +++ b/src/tribler/ui/package-lock.json @@ -18,6 +18,7 @@ "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-navigation-menu": "^1.1.4", + "@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-progress": "^1.0.3", "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-select": "^2.0.0", @@ -26,9 +27,11 @@ "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.7", "@tanstack/react-table": "^8.10.7", + "@types/video.js": "^7.3.58", "axios": "^1.6.8", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", + "cmdk": "^1.0.0", "i18next": "^23.11.4", "js-cookie": "^3.0.5", "jszip": "^3.10.1", @@ -42,6 +45,8 @@ "react-router-dom": "^6.16.0", "tailwind-merge": "^1.14.0", "tailwindcss-animate": "^1.0.7", + "video.js": "^8.17.4", + "videojs": "^1.0.0", "zod": "^3.22.4" }, "devDependencies": { @@ -1638,6 +1643,419 @@ } } }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.1.tgz", + "integrity": "sha512-3y1A3isulwnWhvTTwmIreiB8CF4L+qRjZnK1wYLO7pplddzXKby/GnZ2M7OZY3qgnl6p9AodUIHRYGXNah8Y7g==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.0", + "@radix-ui/react-focus-guards": "1.1.0", + "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.0", + "@radix-ui/react-portal": "1.1.1", + "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.7" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", + "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-arrow": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz", + "integrity": "sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-context": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", + "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz", + "integrity": "sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz", + "integrity": "sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz", + "integrity": "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz", + "integrity": "sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-rect": "1.1.0", + "@radix-ui/react-use-size": "1.1.0", + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-portal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.1.tgz", + "integrity": "sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-presence": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.0.tgz", + "integrity": "sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "dependencies": { + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", + "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", + "dependencies": { + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-size": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", + "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", + "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==" + }, + "node_modules/@radix-ui/react-popover/node_modules/react-remove-scroll": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz", + "integrity": "sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.4", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz", @@ -2281,6 +2699,82 @@ "integrity": "sha512-2L9ifAGl7wmXwP4v3pN4p2FLhD0O1qsJpvKmNin5VA8+UvNVb447UDaAEV6UdrkA+m/Xs58U1RFps44x6TFsVQ==", "devOptional": true }, + "node_modules/@types/video.js": { + "version": "7.3.58", + "resolved": "https://registry.npmjs.org/@types/video.js/-/video.js-7.3.58.tgz", + "integrity": "sha512-1CQjuSrgbv1/dhmcfQ83eVyYbvGyqhTvb2Opxr0QCV+iJ4J6/J+XWQ3Om59WiwCd1MN3rDUHasx5XRrpUtewYQ==" + }, + "node_modules/@videojs/http-streaming": { + "version": "3.13.3", + "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.13.3.tgz", + "integrity": "sha512-L7H+iTeqHeZ5PylzOx+pT3CVyzn4TALWYTJKkIc1pDaV/cTVfNGtG+9/vXPAydD+wR/xH1M9/t2JH8tn/DCT4w==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "4.0.0", + "aes-decrypter": "4.0.1", + "global": "^4.4.0", + "m3u8-parser": "^7.1.0", + "mpd-parser": "^1.3.0", + "mux.js": "7.0.3", + "video.js": "^7 || ^8" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "peerDependencies": { + "video.js": "^8.14.0" + } + }, + "node_modules/@videojs/http-streaming/node_modules/aes-decrypter": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-4.0.1.tgz", + "integrity": "sha512-H1nh/P9VZXUf17AA5NQfJML88CFjVBDuGkp5zDHa7oEhYN9TTpNLJknRY1ie0iSKWlDf6JRnJKaZVDSQdPy6Cg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "global": "^4.4.0", + "pkcs7": "^1.0.4" + } + }, + "node_modules/@videojs/http-streaming/node_modules/aes-decrypter/node_modules/@videojs/vhs-utils": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz", + "integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0", + "url-toolkit": "^2.2.1" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, + "node_modules/@videojs/vhs-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.0.0.tgz", + "integrity": "sha512-xJp7Yd4jMLwje2vHCUmi8MOUU76nxiwII3z4Eg3Ucb+6rrkFVGosrXlMgGnaLjq724j3wzNElRZ71D/CKrTtxg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0", + "url-toolkit": "^2.2.1" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, + "node_modules/@videojs/xhr": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.7.0.tgz", + "integrity": "sha512-giab+EVRanChIupZK7gXjHy90y3nncA2phIOyG3Ne5fvpiMJzvqYwiTOnEVW2S4CoYcuKJkomat7bMXA/UoUZQ==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "global": "~4.4.0", + "is-function": "^1.0.1" + } + }, "node_modules/@vitejs/plugin-react": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-2.2.0.tgz", @@ -2302,11 +2796,50 @@ "vite": "^3.0.0" } }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "node_modules/aes-decrypter": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-4.0.2.tgz", + "integrity": "sha512-lc+/9s6iJvuaRe5qDlMTpCFjnwpkeOXp8qP3oiZ5jsj1MRg+SBVUmmICrhxHvc8OELSmc+fEyyxAuppY6hrWzw==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.1.1", + "global": "^4.4.0", + "pkcs7": "^1.0.4" + } + }, + "node_modules/aes-decrypter/node_modules/@videojs/vhs-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.1.1.tgz", + "integrity": "sha512-5iLX6sR2ownbv4Mtejw6Ax+naosGvoT9kY+gcuHzANyUZZ+4NpeNdKMUhb6ag0acYej1Y7cmr/F2+4PrggMiVA==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, + "node_modules/amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==", + "engines": { + "node": ">=0.4.2" + } }, "node_modules/ansi-styles": { "version": "3.2.1", @@ -2342,6 +2875,25 @@ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" }, + "node_modules/argparse": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", + "integrity": "sha512-LjmC2dNpdn2L4UzyoaIr11ELYoLn37ZFy9zObrQFHsSuOepeUEMKnM8w5KL4Tnrp2gy88rRuQt6Ky8Bjml+Baw==", + "peer": true, + "dependencies": { + "underscore": "~1.7.0", + "underscore.string": "~2.4.0" + } + }, + "node_modules/argparse/node_modules/underscore.string": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz", + "integrity": "sha512-yxkabuCaIBnzfIvX3kBxQqCs0ar/bfJwDnFEHJUm/ZrRVhT3IItdRF5cZjARLzEnyQYtIUhsZ2LG2j3HidFOFQ==", + "peer": true, + "engines": { + "node": "*" + } + }, "node_modules/aria-hidden": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", @@ -2353,6 +2905,15 @@ "node": ">=10" } }, + "node_modules/async": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", + "integrity": "sha512-2tEzliJmf5fHNafNwQLJXUasGzQCVctvsNkXmnlELHwypU0p08/rHohYvkqKIjyXpx+0rkrYv6QbhJ+UF4QkBg==", + "peer": true, + "engines": { + "node": "*" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2396,9 +2957,9 @@ } }, "node_modules/axios": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", - "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -2470,6 +3031,14 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha512-wzLkDa4K/mzI1OSITC+DUyjgIl/ETNHE9QvYgy6J6Jvqyyz4C0Xfd+lQhb19sX2jMpZV4IssUn0VDVmglV+s4g==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -2568,6 +3137,33 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.0.tgz", + "integrity": "sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==", + "dependencies": { + "@radix-ui/react-dialog": "1.0.5", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/coffee-script": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz", + "integrity": "sha512-QjQ1T4BqyHv19k6XSfdhy/QLlIOhywz0ekBUCa9h71zYMJlfDTGan/Z1JXzYkZ6v8R+GhvL/p4FZPbPW8WNXlg==", + "deprecated": "CoffeeScript on NPM has moved to \"coffeescript\" (no hyphen)", + "peer": true, + "bin": { + "cake": "bin/cake", + "coffee": "bin/coffee" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -2583,6 +3179,15 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, + "node_modules/colors": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "integrity": "sha512-OsSVtHK8Ir8r3+Fxw/b4jS1ZLPXkV6ZxDRJQzeD7qo0SqMXWrHDM71DgYzPMHY8SFJ0Ao+nNU2p1MmwdzKqPrw==", + "peer": true, + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2644,6 +3249,15 @@ "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", "devOptional": true }, + "node_modules/dateformat": { + "version": "1.0.2-1.2.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz", + "integrity": "sha512-AXvW8g7tO4ilk5HgOWeDmPi/ZPaCnMJ+9Cg1I3p19w6mcvAAXBuuGEXAxybC+Djj1PSZUiHUcyoYu7WneCX8gQ==", + "peer": true, + "engines": { + "node": "*" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2661,6 +3275,14 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2684,6 +3306,11 @@ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, + "node_modules/dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" + }, "node_modules/electron-to-chromium": { "version": "1.4.549", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.549.tgz", @@ -3065,6 +3692,34 @@ "node": ">=0.8.0" } }, + "node_modules/esprima": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "integrity": "sha512-rp5dMKN8zEs9dfi9g0X1ClLmV//WRyk/R15mppFNICIFRG5P92VP7Z04p8pk++gABo9W2tY+kHyu6P1mEHgmTA==", + "peer": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/eventemitter2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "integrity": "sha512-K7J4xq5xAD5jHsGM5ReWXRTFa3JRGofHiMcVgQ8PRwgWxzjHpMWCIzsmyf60+mh8KLsqYPcjUMa0AC4hd6lPyQ==", + "peer": true + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "peer": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -3099,15 +3754,72 @@ "reusify": "^1.0.4" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/findup-sync": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", + "integrity": "sha512-yjftfYnF4ThYEvKEV/kEFR15dmtyXTAh3vQnzpJUoc7Naj5y1P0Ck7Zs1+Vroa00E3KT3IYsk756S+8WA5dNLw==", + "peer": true, + "dependencies": { + "glob": "~3.2.9", + "lodash": "~2.4.1" + }, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/findup-sync/node_modules/glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "integrity": "sha512-hVb0zwEZwC1FXSKRPFTeOtN7AArJcJlI6ULGLtrstaswKNlrTJqAA+1lYlSUop4vjA423xlBzqfVS3iWGlqJ+g==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "peer": true, + "dependencies": { + "inherits": "2", + "minimatch": "0.3" + }, + "engines": { + "node": "*" + } + }, + "node_modules/findup-sync/node_modules/lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha512-Kak1hi6/hYHGVPmdyiZijoQyz5x2iGVzs6w9GYB/HiXEtylY7tIoYEROMjvM1d9nXJqPOrG2MNPMn01bJ+S0Rw==", + "engines": [ + "node", + "rhino" + ], + "peer": true + }, + "node_modules/findup-sync/node_modules/lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha512-WpibWJ60c3AgAz8a2iYErDrcT2C7OmKnsWhIcHOjkUHFjkXncJhtLxNSqUmxRxRunpb5I8Vprd7aNSd2NtksJQ==", + "peer": true + }, + "node_modules/findup-sync/node_modules/minimatch": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha512-WFX1jI1AaxNTZVOHLBVazwTWKaQjoykSzCBNXB72vDTCzopQGtyP91tKdFK5cv1+qMwPyiTu1HqUriqplI8pcA==", + "deprecated": "Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue", + "peer": true, "dependencies": { - "to-regex-range": "^5.0.1" + "lru-cache": "2", + "sigmund": "~1.0.0" }, "engines": { - "node": ">=8" + "node": "*" } }, "node_modules/follow-redirects": { @@ -3190,6 +3902,15 @@ "node": ">=6" } }, + "node_modules/getobject": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", + "integrity": "sha512-hIGEBfnHcZpWkXPsAVeVmpYDvfy/matVl03yOY91FPmnpCC12Lm5izNxCjO3lHAeO6uaTwMxu7g450Siknlhig==", + "peer": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", @@ -3220,6 +3941,15 @@ "node": ">=10.13.0" } }, + "node_modules/global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -3238,6 +3968,200 @@ "csstype": "^3.0.10" } }, + "node_modules/graceful-fs": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", + "integrity": "sha512-iiTUZ5vZ+2ZV+h71XAgwCSu6+NAizhFU3Yw8aC/hH5SQ3SnISqEqAek40imAFGtDcwJKNhXvSY+hzIolnLwcdQ==", + "deprecated": "please upgrade to graceful-fs 4 for compatibility with current and future versions of Node.js", + "peer": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/grunt": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz", + "integrity": "sha512-1iq3ylLjzXqz/KSq1OAE2qhnpcbkF2WyhsQcavZt+YmgvHu0EbPMEhGhy2gr0FP67isHpRdfwjB5WVeXXcJemQ==", + "peer": true, + "dependencies": { + "async": "~0.1.22", + "coffee-script": "~1.3.3", + "colors": "~0.6.2", + "dateformat": "1.0.2-1.2.3", + "eventemitter2": "~0.4.13", + "exit": "~0.1.1", + "findup-sync": "~0.1.2", + "getobject": "~0.1.0", + "glob": "~3.1.21", + "grunt-legacy-log": "~0.1.0", + "grunt-legacy-util": "~0.2.0", + "hooker": "~0.2.3", + "iconv-lite": "~0.2.11", + "js-yaml": "~2.0.5", + "lodash": "~0.9.2", + "minimatch": "~0.2.12", + "nopt": "~1.0.10", + "rimraf": "~2.2.8", + "underscore.string": "~2.2.1", + "which": "~1.0.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/grunt-contrib-uglify": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/grunt-contrib-uglify/-/grunt-contrib-uglify-0.2.7.tgz", + "integrity": "sha512-KXKM2UNLsCiUI6/DYfAIPm3i26UJJN6Cf6KD8fFa2TKllj7yLPC853IxtWBJ/3jX66QtXHGtdCORuuA6sAFvvA==", + "dependencies": { + "grunt-lib-contrib": "~0.6.1", + "uglify-js": "~2.4.0" + }, + "engines": { + "node": ">= 0.8.0" + }, + "peerDependencies": { + "grunt": "~0.4.0" + } + }, + "node_modules/grunt-legacy-log": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz", + "integrity": "sha512-qYs/uM0ImdzwIXLhS4O5WLV5soAM+PEqqHI/hzSxlo450ERSccEhnXqoeDA9ZozOdaWuYnzTOTwRcVRogleMxg==", + "peer": true, + "dependencies": { + "colors": "~0.6.2", + "grunt-legacy-log-utils": "~0.1.1", + "hooker": "~0.2.3", + "lodash": "~2.4.1", + "underscore.string": "~2.3.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/grunt-legacy-log-utils": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz", + "integrity": "sha512-D0vbUX00TFYCKNZtcZzemMpwT8TR/FdRs1pmfiBw6qnUw80PfsjV+lhIozY/3eJ3PSG2zj89wd2mH/7f4tNAlw==", + "peer": true, + "dependencies": { + "colors": "~0.6.2", + "lodash": "~2.4.1", + "underscore.string": "~2.3.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/grunt-legacy-log-utils/node_modules/lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha512-Kak1hi6/hYHGVPmdyiZijoQyz5x2iGVzs6w9GYB/HiXEtylY7tIoYEROMjvM1d9nXJqPOrG2MNPMn01bJ+S0Rw==", + "engines": [ + "node", + "rhino" + ], + "peer": true + }, + "node_modules/grunt-legacy-log-utils/node_modules/underscore.string": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha512-hbD5MibthuDAu4yA5wxes5bzFgqd3PpBJuClbRxaNddxfdsz+qf+1kHwrGQFrmchmDHb9iNU+6EHDn8uj0xDJg==", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/grunt-legacy-log/node_modules/lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha512-Kak1hi6/hYHGVPmdyiZijoQyz5x2iGVzs6w9GYB/HiXEtylY7tIoYEROMjvM1d9nXJqPOrG2MNPMn01bJ+S0Rw==", + "engines": [ + "node", + "rhino" + ], + "peer": true + }, + "node_modules/grunt-legacy-log/node_modules/underscore.string": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha512-hbD5MibthuDAu4yA5wxes5bzFgqd3PpBJuClbRxaNddxfdsz+qf+1kHwrGQFrmchmDHb9iNU+6EHDn8uj0xDJg==", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/grunt-legacy-util": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz", + "integrity": "sha512-cXPbfF8aM+pvveQeN1K872D5fRm30xfJWZiS63Y8W8oyIPLClCsmI8bW96Txqzac9cyL4lRqEBhbhJ3n5EzUUQ==", + "peer": true, + "dependencies": { + "async": "~0.1.22", + "exit": "~0.1.1", + "getobject": "~0.1.0", + "hooker": "~0.2.3", + "lodash": "~0.9.2", + "underscore.string": "~2.2.1", + "which": "~1.0.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/grunt-lib-contrib": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/grunt-lib-contrib/-/grunt-lib-contrib-0.6.1.tgz", + "integrity": "sha512-HdCtJuMmmkSAVrAfsG7lZWE0YabrsPWwzcCCUgWQOAaQsQSUNhw/IwD2YjCSLh5y9NXSPzHTYFLL4ro7QbAJMA==", + "dependencies": { + "zlib-browserify": "0.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/grunt/node_modules/glob": { + "version": "3.1.21", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", + "integrity": "sha512-ANhy2V2+tFpRajE3wN4DhkNQ08KDr0Ir1qL12/cUe5+a7STEK8jkW4onUYuY8/06qAFuT5je7mjAqzx0eKI2tQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "peer": true, + "dependencies": { + "graceful-fs": "~1.2.0", + "inherits": "1", + "minimatch": "~0.2.11" + }, + "engines": { + "node": "*" + } + }, + "node_modules/grunt/node_modules/inherits": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", + "integrity": "sha512-Al67oatbRSo3RV5hRqIoln6Y5yMVbJSIn4jEJNL7VCImzq/kLr7vvb6sFRJXqr8rpHc/2kJOM+y0sPKN47VdzA==", + "peer": true + }, + "node_modules/grunt/node_modules/lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha512-WpibWJ60c3AgAz8a2iYErDrcT2C7OmKnsWhIcHOjkUHFjkXncJhtLxNSqUmxRxRunpb5I8Vprd7aNSd2NtksJQ==", + "peer": true + }, + "node_modules/grunt/node_modules/minimatch": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha512-zZ+Jy8lVWlvqqeM8iZB7w7KmQkoJn8djM585z88rywrEbzoqawVa9FR5p2hwD+y74nfuKOjmNvi9gtWJNLqHvA==", + "deprecated": "Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue", + "peer": true, + "dependencies": { + "lru-cache": "2", + "sigmund": "~1.0.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/has": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", @@ -3255,6 +4179,15 @@ "node": ">=4" } }, + "node_modules/hooker": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", + "integrity": "sha512-t+UerCsQviSymAInD01Pw+Dn/usmz1sRO+3Zk1+lx8eg+WKpD2ulcwWqHHL0+aseRBr+3+vIhiG1K1JTwaIcTA==", + "peer": true, + "engines": { + "node": "*" + } + }, "node_modules/html-parse-stringify": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", @@ -3294,6 +4227,15 @@ "cross-fetch": "4.0.0" } }, + "node_modules/iconv-lite": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", + "integrity": "sha512-KhmFWgaQZY83Cbhi+ADInoUQ8Etn6BG5fikM9syeOjQltvR45h7cRKJ/9uvQEuD61I3Uju77yYce0/LhKVClQw==", + "peer": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -3349,6 +4291,11 @@ "node": ">=0.10.0" } }, + "node_modules/is-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", + "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==" + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -3394,6 +4341,22 @@ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, + "node_modules/js-yaml": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz", + "integrity": "sha512-VEKcIksckDBUhg2JS874xVouiPkywVUh4yyUmLCDe1Zg3bCd6M+F1eGPenPeHLc2XC8pp9G8bsuofK0NeEqRkA==", + "peer": true, + "dependencies": { + "argparse": "~ 0.1.11", + "esprima": "~ 1.0.2" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + }, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -3477,6 +4440,16 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, + "node_modules/lodash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", + "integrity": "sha512-LVbt/rjK62gSbhehDVKL0vlaime4Y1IBixL+bKeNfoY4L2zab/jGrxU6Ka05tMA/zBxkTk5t3ivtphdyYupczw==", + "engines": [ + "node", + "rhino" + ], + "peer": true + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -3505,6 +4478,29 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } }, + "node_modules/m3u8-parser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-7.2.0.tgz", + "integrity": "sha512-CRatFqpjVtMiMaKXxNvuI3I++vUumIXVVT/JpCpdU/FynV/ceVw1qpPyyBNindL+JlPMSesx+WX1QJaZEJSaMQ==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.1.1", + "global": "^4.4.0" + } + }, + "node_modules/m3u8-parser/node_modules/@videojs/vhs-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.1.1.tgz", + "integrity": "sha512-5iLX6sR2ownbv4Mtejw6Ax+naosGvoT9kY+gcuHzANyUZZ+4NpeNdKMUhb6ag0acYej1Y7cmr/F2+4PrggMiVA==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, "node_modules/magic-string": { "version": "0.26.7", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.7.tgz", @@ -3526,11 +4522,11 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -3556,6 +4552,14 @@ "node": ">= 0.6" } }, + "node_modules/min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", + "dependencies": { + "dom-walk": "^0.1.0" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3567,12 +4571,42 @@ "node": "*" } }, + "node_modules/mpd-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.3.0.tgz", + "integrity": "sha512-WgeIwxAqkmb9uTn4ClicXpEQYCEduDqRKfmUdp4X8vmghKfBNXZLYpREn9eqrDx/Tf5LhzRcJLSpi4ohfV742Q==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.0.0", + "@xmldom/xmldom": "^0.8.3", + "global": "^4.4.0" + }, + "bin": { + "mpd-to-m3u8-json": "bin/parse.js" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/mux.js": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-7.0.3.tgz", + "integrity": "sha512-gzlzJVEGFYPtl2vvEiJneSWAWD4nfYRHD5XgxmB2gWvXraMPOYk+sxfvexmNfjQUFpmk6hwLR5C6iSFmuwCHdQ==", + "dependencies": { + "@babel/runtime": "^7.11.2", + "global": "^4.4.0" + }, + "bin": { + "muxjs-transmux": "bin/transmux.js" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -3676,7 +4710,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", - "dev": true, "dependencies": { "abbrev": "1" }, @@ -3791,6 +4824,17 @@ "node": ">= 6" } }, + "node_modules/pkcs7": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz", + "integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==", + "dependencies": { + "@babel/runtime": "^7.5.5" + }, + "bin": { + "pkcs7": "bin/cli.js" + } + }, "node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -3915,6 +4959,14 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -4211,10 +5263,20 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha512-R5KMKHnPAQaZMqLOsyuyUmcIjSeDm+73eoqQpaXA7AZ22BL+6C+1mcUscgOsNd8WVlJuvlgAPsegcx7pjlV0Dg==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "peer": true, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/rollup": { - "version": "2.79.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", - "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "version": "2.79.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", + "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -4290,6 +5352,12 @@ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" }, + "node_modules/sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==", + "peer": true + }, "node_modules/simple-update-notifier": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", @@ -4311,6 +5379,17 @@ "semver": "bin/semver.js" } }, + "node_modules/source-map": { + "version": "0.1.34", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.34.tgz", + "integrity": "sha512-yfCwDj0vR9RTwt3pEzglgb3ZgmcXHt6DjG3bjJvzPwTL+5zDQ2MhmSzAcTy0GTiQuCiriSWXvWM1/NhKdXuoQA==", + "dependencies": { + "amdefine": ">=0.0.4" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", @@ -4521,12 +5600,54 @@ "node": ">=14.17" } }, + "node_modules/uglify-js": { + "version": "2.4.24", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.4.24.tgz", + "integrity": "sha512-tktIjwackfZLd893KGJmXc1hrRHH1vH9Po3xFh1XBjjeGAnN02xJ3SuoA+n1L29/ZaCA18KzCFlckS+vfPugiA==", + "dependencies": { + "async": "~0.2.6", + "source-map": "0.1.34", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.5.4" + }, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/uglify-js/node_modules/async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==" + }, + "node_modules/uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha512-vb2s1lYx2xBtUgy+ta+b2J/GLVUR+wmpINwHePmPRhOsIVCG2wDzKJ0n14GslH1BifsqVzSOwQhRaCAsZ/nI4Q==" + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, + "node_modules/underscore": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", + "integrity": "sha512-cp0oQQyZhUM1kpJDLdGO1jPZHgS/MpzoWYfe9+CM2h/QGDZlqwT2T3YGukuBdaNJ/CAPoeyAZRRHz8JFo176vA==", + "peer": true + }, + "node_modules/underscore.string": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", + "integrity": "sha512-3FVmhXqelrj6gfgp3Bn6tOavJvW0dNH2T+heTD38JRxIrAbiuzbqjknszoOYj3DyFB1nWiLj208Qt2no/L4cIA==", + "peer": true, + "engines": { + "node": "*" + } + }, "node_modules/undici-types": { "version": "5.25.3", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", @@ -4563,6 +5684,11 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/url-toolkit": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.5.tgz", + "integrity": "sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==" + }, "node_modules/use-callback-ref": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", @@ -4609,10 +5735,66 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/video.js": { + "version": "8.17.4", + "resolved": "https://registry.npmjs.org/video.js/-/video.js-8.17.4.tgz", + "integrity": "sha512-AECieAxKMKB/QgYK36ci50phfpWys6bFT6+pGMpSafeFYSoZaQ2Vpl83T9Qqcesv4TO7oNtiycnVeaBnrva2oA==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/http-streaming": "3.13.3", + "@videojs/vhs-utils": "^4.0.0", + "@videojs/xhr": "2.7.0", + "aes-decrypter": "^4.0.1", + "global": "4.4.0", + "m3u8-parser": "^7.1.0", + "mpd-parser": "^1.2.2", + "mux.js": "^7.0.1", + "videojs-contrib-quality-levels": "4.1.0", + "videojs-font": "4.2.0", + "videojs-vtt.js": "0.15.5" + } + }, + "node_modules/videojs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/videojs/-/videojs-1.0.0.tgz", + "integrity": "sha512-FwI02jJ7d4E6goWuc/4LTN5OJlD1M0jInoIoNemo4EzMfu6IywhahMXDriLObX17ML62RsHS0oiCUE9wVB6i8A==", + "deprecated": "This is a placeholder package, please use the official 'video.js' package", + "dependencies": { + "grunt-contrib-uglify": "^0.2.7" + } + }, + "node_modules/videojs-contrib-quality-levels": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-4.1.0.tgz", + "integrity": "sha512-TfrXJJg1Bv4t6TOCMEVMwF/CoS8iENYsWNKip8zfhB5kTcegiFYezEA0eHAJPU64ZC8NQbxQgOwAsYU8VXbOWA==", + "dependencies": { + "global": "^4.4.0" + }, + "engines": { + "node": ">=16", + "npm": ">=8" + }, + "peerDependencies": { + "video.js": "^8" + } + }, + "node_modules/videojs-font": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-4.2.0.tgz", + "integrity": "sha512-YPq+wiKoGy2/M7ccjmlvwi58z2xsykkkfNMyIg4xb7EZQQNwB71hcSsB3o75CqQV7/y5lXkXhI/rsGAS7jfEmQ==" + }, + "node_modules/videojs-vtt.js": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz", + "integrity": "sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==", + "dependencies": { + "global": "^4.3.1" + } + }, "node_modules/vite": { - "version": "3.2.10", - "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.10.tgz", - "integrity": "sha512-Dx3olBo/ODNiMVk/cA5Yft9Ws+snLOXrhLtrI3F4XLt4syz2Yg8fayZMWScPKoz12v5BUv7VEmQHnsfpY80fYw==", + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.11.tgz", + "integrity": "sha512-K/jGKL/PgbIgKCiJo5QbASQhFiV02X9Jh+Qq0AKCRCRKZtOTVi4t6wh75FDpGf2N9rYOnzH87OEFQNaFy6pdxQ==", "dev": true, "dependencies": { "esbuild": "^0.15.9", @@ -4682,6 +5864,31 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/which": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", + "integrity": "sha512-E87fdQ/eRJr9W1X4wTPejNy9zTW3FI2vpCZSJ/HAY+TkjKVC0TUm1jk6vn2Z7qay0DQy0+RBGdXxj+RmmiGZKQ==", + "peer": true, + "bin": { + "which": "bin/which" + } + }, + "node_modules/window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha512-1pTPQDKTdd61ozlKGNCjhNRd+KPmgLSGa3mZTHoOliaGcESD8G1PXhh7c1fgiPjVbNVfgy2Faw4BI8/m0cC8Mg==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -4701,6 +5908,22 @@ "node": ">= 14" } }, + "node_modules/yargs": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.5.4.tgz", + "integrity": "sha512-5j382E4xQSs71p/xZQsU1PtRA2HXPAjX0E0DkoGLxwNASMOKX6A9doV1NrZmj85u2Pjquz402qonBzz/yLPbPA==", + "dependencies": { + "camelcase": "^1.0.2", + "decamelize": "^1.0.0", + "window-size": "0.1.0", + "wordwrap": "0.0.2" + } + }, + "node_modules/zlib-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/zlib-browserify/-/zlib-browserify-0.0.1.tgz", + "integrity": "sha512-fheIDCKXU0YAGZMv4FFwVTBMQRSv2ZjNqRN1VkZjetZDK/BC/hViEhasTh0kTeogcsIAl5gYE04GN53trT+cFw==" + }, "node_modules/zod": { "version": "3.22.4", "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", @@ -5629,6 +6852,203 @@ "@radix-ui/react-visually-hidden": "1.0.3" } }, + "@radix-ui/react-popover": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.1.tgz", + "integrity": "sha512-3y1A3isulwnWhvTTwmIreiB8CF4L+qRjZnK1wYLO7pplddzXKby/GnZ2M7OZY3qgnl6p9AodUIHRYGXNah8Y7g==", + "requires": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.0", + "@radix-ui/react-focus-guards": "1.1.0", + "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.0", + "@radix-ui/react-portal": "1.1.1", + "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.7" + }, + "dependencies": { + "@radix-ui/primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", + "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" + }, + "@radix-ui/react-arrow": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz", + "integrity": "sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==", + "requires": { + "@radix-ui/react-primitive": "2.0.0" + } + }, + "@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "requires": {} + }, + "@radix-ui/react-context": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", + "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "requires": {} + }, + "@radix-ui/react-dismissable-layer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz", + "integrity": "sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==", + "requires": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + } + }, + "@radix-ui/react-focus-guards": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz", + "integrity": "sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==", + "requires": {} + }, + "@radix-ui/react-focus-scope": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz", + "integrity": "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==", + "requires": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0" + } + }, + "@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "requires": { + "@radix-ui/react-use-layout-effect": "1.1.0" + } + }, + "@radix-ui/react-popper": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz", + "integrity": "sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==", + "requires": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-rect": "1.1.0", + "@radix-ui/react-use-size": "1.1.0", + "@radix-ui/rect": "1.1.0" + } + }, + "@radix-ui/react-portal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.1.tgz", + "integrity": "sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==", + "requires": { + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + } + }, + "@radix-ui/react-presence": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.0.tgz", + "integrity": "sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==", + "requires": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + } + }, + "@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "requires": { + "@radix-ui/react-slot": "1.1.0" + } + }, + "@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "requires": { + "@radix-ui/react-compose-refs": "1.1.0" + } + }, + "@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "requires": {} + }, + "@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "requires": { + "@radix-ui/react-use-callback-ref": "1.1.0" + } + }, + "@radix-ui/react-use-escape-keydown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "requires": { + "@radix-ui/react-use-callback-ref": "1.1.0" + } + }, + "@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "requires": {} + }, + "@radix-ui/react-use-rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", + "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", + "requires": { + "@radix-ui/rect": "1.1.0" + } + }, + "@radix-ui/react-use-size": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", + "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", + "requires": { + "@radix-ui/react-use-layout-effect": "1.1.0" + } + }, + "@radix-ui/rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", + "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==" + }, + "react-remove-scroll": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz", + "integrity": "sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==", + "requires": { + "react-remove-scroll-bar": "^2.3.4", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + } + } + } + }, "@radix-ui/react-popper": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz", @@ -5983,6 +7403,71 @@ "integrity": "sha512-2L9ifAGl7wmXwP4v3pN4p2FLhD0O1qsJpvKmNin5VA8+UvNVb447UDaAEV6UdrkA+m/Xs58U1RFps44x6TFsVQ==", "devOptional": true }, + "@types/video.js": { + "version": "7.3.58", + "resolved": "https://registry.npmjs.org/@types/video.js/-/video.js-7.3.58.tgz", + "integrity": "sha512-1CQjuSrgbv1/dhmcfQ83eVyYbvGyqhTvb2Opxr0QCV+iJ4J6/J+XWQ3Om59WiwCd1MN3rDUHasx5XRrpUtewYQ==" + }, + "@videojs/http-streaming": { + "version": "3.13.3", + "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.13.3.tgz", + "integrity": "sha512-L7H+iTeqHeZ5PylzOx+pT3CVyzn4TALWYTJKkIc1pDaV/cTVfNGtG+9/vXPAydD+wR/xH1M9/t2JH8tn/DCT4w==", + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "4.0.0", + "aes-decrypter": "4.0.1", + "global": "^4.4.0", + "m3u8-parser": "^7.1.0", + "mpd-parser": "^1.3.0", + "mux.js": "7.0.3", + "video.js": "^7 || ^8" + }, + "dependencies": { + "aes-decrypter": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-4.0.1.tgz", + "integrity": "sha512-H1nh/P9VZXUf17AA5NQfJML88CFjVBDuGkp5zDHa7oEhYN9TTpNLJknRY1ie0iSKWlDf6JRnJKaZVDSQdPy6Cg==", + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "global": "^4.4.0", + "pkcs7": "^1.0.4" + }, + "dependencies": { + "@videojs/vhs-utils": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz", + "integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==", + "requires": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0", + "url-toolkit": "^2.2.1" + } + } + } + } + } + }, + "@videojs/vhs-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.0.0.tgz", + "integrity": "sha512-xJp7Yd4jMLwje2vHCUmi8MOUU76nxiwII3z4Eg3Ucb+6rrkFVGosrXlMgGnaLjq724j3wzNElRZ71D/CKrTtxg==", + "requires": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0", + "url-toolkit": "^2.2.1" + } + }, + "@videojs/xhr": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.7.0.tgz", + "integrity": "sha512-giab+EVRanChIupZK7gXjHy90y3nncA2phIOyG3Ne5fvpiMJzvqYwiTOnEVW2S4CoYcuKJkomat7bMXA/UoUZQ==", + "requires": { + "@babel/runtime": "^7.5.5", + "global": "~4.4.0", + "is-function": "^1.0.1" + } + }, "@vitejs/plugin-react": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-2.2.0.tgz", @@ -5998,11 +7483,42 @@ "react-refresh": "^0.14.0" } }, + "@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==" + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "aes-decrypter": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-4.0.2.tgz", + "integrity": "sha512-lc+/9s6iJvuaRe5qDlMTpCFjnwpkeOXp8qP3oiZ5jsj1MRg+SBVUmmICrhxHvc8OELSmc+fEyyxAuppY6hrWzw==", + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.1.1", + "global": "^4.4.0", + "pkcs7": "^1.0.4" + }, + "dependencies": { + "@videojs/vhs-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.1.1.tgz", + "integrity": "sha512-5iLX6sR2ownbv4Mtejw6Ax+naosGvoT9kY+gcuHzANyUZZ+4NpeNdKMUhb6ag0acYej1Y7cmr/F2+4PrggMiVA==", + "requires": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0" + } + } + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==" }, "ansi-styles": { "version": "3.2.1", @@ -6032,6 +7548,24 @@ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" }, + "argparse": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", + "integrity": "sha512-LjmC2dNpdn2L4UzyoaIr11ELYoLn37ZFy9zObrQFHsSuOepeUEMKnM8w5KL4Tnrp2gy88rRuQt6Ky8Bjml+Baw==", + "peer": true, + "requires": { + "underscore": "~1.7.0", + "underscore.string": "~2.4.0" + }, + "dependencies": { + "underscore.string": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz", + "integrity": "sha512-yxkabuCaIBnzfIvX3kBxQqCs0ar/bfJwDnFEHJUm/ZrRVhT3IItdRF5cZjARLzEnyQYtIUhsZ2LG2j3HidFOFQ==", + "peer": true + } + } + }, "aria-hidden": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", @@ -6040,6 +7574,12 @@ "tslib": "^2.0.0" } }, + "async": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", + "integrity": "sha512-2tEzliJmf5fHNafNwQLJXUasGzQCVctvsNkXmnlELHwypU0p08/rHohYvkqKIjyXpx+0rkrYv6QbhJ+UF4QkBg==", + "peer": true + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -6060,9 +7600,9 @@ } }, "axios": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", - "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", "requires": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -6108,6 +7648,11 @@ "update-browserslist-db": "^1.0.13" } }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha512-wzLkDa4K/mzI1OSITC+DUyjgIl/ETNHE9QvYgy6J6Jvqyyz4C0Xfd+lQhb19sX2jMpZV4IssUn0VDVmglV+s4g==" + }, "camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -6168,6 +7713,21 @@ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==" }, + "cmdk": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.0.tgz", + "integrity": "sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==", + "requires": { + "@radix-ui/react-dialog": "1.0.5", + "@radix-ui/react-primitive": "1.0.3" + } + }, + "coffee-script": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz", + "integrity": "sha512-QjQ1T4BqyHv19k6XSfdhy/QLlIOhywz0ekBUCa9h71zYMJlfDTGan/Z1JXzYkZ6v8R+GhvL/p4FZPbPW8WNXlg==", + "peer": true + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -6183,6 +7743,12 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, + "colors": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "integrity": "sha512-OsSVtHK8Ir8r3+Fxw/b4jS1ZLPXkV6ZxDRJQzeD7qo0SqMXWrHDM71DgYzPMHY8SFJ0Ao+nNU2p1MmwdzKqPrw==", + "peer": true + }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -6232,6 +7798,12 @@ "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", "devOptional": true }, + "dateformat": { + "version": "1.0.2-1.2.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz", + "integrity": "sha512-AXvW8g7tO4ilk5HgOWeDmPi/ZPaCnMJ+9Cg1I3p19w6mcvAAXBuuGEXAxybC+Djj1PSZUiHUcyoYu7WneCX8gQ==", + "peer": true + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -6241,6 +7813,11 @@ "ms": "2.1.2" } }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -6261,6 +7838,11 @@ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, + "dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" + }, "electron-to-chromium": { "version": "1.4.549", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.549.tgz", @@ -6449,6 +8031,24 @@ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true }, + "esprima": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "integrity": "sha512-rp5dMKN8zEs9dfi9g0X1ClLmV//WRyk/R15mppFNICIFRG5P92VP7Z04p8pk++gABo9W2tY+kHyu6P1mEHgmTA==", + "peer": true + }, + "eventemitter2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "integrity": "sha512-K7J4xq5xAD5jHsGM5ReWXRTFa3JRGofHiMcVgQ8PRwgWxzjHpMWCIzsmyf60+mh8KLsqYPcjUMa0AC4hd6lPyQ==", + "peer": true + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "peer": true + }, "fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -6487,6 +8087,50 @@ "to-regex-range": "^5.0.1" } }, + "findup-sync": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", + "integrity": "sha512-yjftfYnF4ThYEvKEV/kEFR15dmtyXTAh3vQnzpJUoc7Naj5y1P0Ck7Zs1+Vroa00E3KT3IYsk756S+8WA5dNLw==", + "peer": true, + "requires": { + "glob": "~3.2.9", + "lodash": "~2.4.1" + }, + "dependencies": { + "glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "integrity": "sha512-hVb0zwEZwC1FXSKRPFTeOtN7AArJcJlI6ULGLtrstaswKNlrTJqAA+1lYlSUop4vjA423xlBzqfVS3iWGlqJ+g==", + "peer": true, + "requires": { + "inherits": "2", + "minimatch": "0.3" + } + }, + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha512-Kak1hi6/hYHGVPmdyiZijoQyz5x2iGVzs6w9GYB/HiXEtylY7tIoYEROMjvM1d9nXJqPOrG2MNPMn01bJ+S0Rw==", + "peer": true + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha512-WpibWJ60c3AgAz8a2iYErDrcT2C7OmKnsWhIcHOjkUHFjkXncJhtLxNSqUmxRxRunpb5I8Vprd7aNSd2NtksJQ==", + "peer": true + }, + "minimatch": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha512-WFX1jI1AaxNTZVOHLBVazwTWKaQjoykSzCBNXB72vDTCzopQGtyP91tKdFK5cv1+qMwPyiTu1HqUriqplI8pcA==", + "peer": true, + "requires": { + "lru-cache": "2", + "sigmund": "~1.0.0" + } + } + } + }, "follow-redirects": { "version": "1.15.6", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", @@ -6530,6 +8174,12 @@ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==" }, + "getobject": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", + "integrity": "sha512-hIGEBfnHcZpWkXPsAVeVmpYDvfy/matVl03yOY91FPmnpCC12Lm5izNxCjO3lHAeO6uaTwMxu7g450Siknlhig==", + "peer": true + }, "glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", @@ -6551,6 +8201,15 @@ "is-glob": "^4.0.3" } }, + "global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "requires": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -6564,6 +8223,159 @@ "dev": true, "requires": {} }, + "graceful-fs": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", + "integrity": "sha512-iiTUZ5vZ+2ZV+h71XAgwCSu6+NAizhFU3Yw8aC/hH5SQ3SnISqEqAek40imAFGtDcwJKNhXvSY+hzIolnLwcdQ==", + "peer": true + }, + "grunt": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz", + "integrity": "sha512-1iq3ylLjzXqz/KSq1OAE2qhnpcbkF2WyhsQcavZt+YmgvHu0EbPMEhGhy2gr0FP67isHpRdfwjB5WVeXXcJemQ==", + "peer": true, + "requires": { + "async": "~0.1.22", + "coffee-script": "~1.3.3", + "colors": "~0.6.2", + "dateformat": "1.0.2-1.2.3", + "eventemitter2": "~0.4.13", + "exit": "~0.1.1", + "findup-sync": "~0.1.2", + "getobject": "~0.1.0", + "glob": "~3.1.21", + "grunt-legacy-log": "~0.1.0", + "grunt-legacy-util": "~0.2.0", + "hooker": "~0.2.3", + "iconv-lite": "~0.2.11", + "js-yaml": "~2.0.5", + "lodash": "~0.9.2", + "minimatch": "~0.2.12", + "nopt": "~1.0.10", + "rimraf": "~2.2.8", + "underscore.string": "~2.2.1", + "which": "~1.0.5" + }, + "dependencies": { + "glob": { + "version": "3.1.21", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", + "integrity": "sha512-ANhy2V2+tFpRajE3wN4DhkNQ08KDr0Ir1qL12/cUe5+a7STEK8jkW4onUYuY8/06qAFuT5je7mjAqzx0eKI2tQ==", + "peer": true, + "requires": { + "graceful-fs": "~1.2.0", + "inherits": "1", + "minimatch": "~0.2.11" + } + }, + "inherits": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", + "integrity": "sha512-Al67oatbRSo3RV5hRqIoln6Y5yMVbJSIn4jEJNL7VCImzq/kLr7vvb6sFRJXqr8rpHc/2kJOM+y0sPKN47VdzA==", + "peer": true + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha512-WpibWJ60c3AgAz8a2iYErDrcT2C7OmKnsWhIcHOjkUHFjkXncJhtLxNSqUmxRxRunpb5I8Vprd7aNSd2NtksJQ==", + "peer": true + }, + "minimatch": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha512-zZ+Jy8lVWlvqqeM8iZB7w7KmQkoJn8djM585z88rywrEbzoqawVa9FR5p2hwD+y74nfuKOjmNvi9gtWJNLqHvA==", + "peer": true, + "requires": { + "lru-cache": "2", + "sigmund": "~1.0.0" + } + } + } + }, + "grunt-contrib-uglify": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/grunt-contrib-uglify/-/grunt-contrib-uglify-0.2.7.tgz", + "integrity": "sha512-KXKM2UNLsCiUI6/DYfAIPm3i26UJJN6Cf6KD8fFa2TKllj7yLPC853IxtWBJ/3jX66QtXHGtdCORuuA6sAFvvA==", + "requires": { + "grunt-lib-contrib": "~0.6.1", + "uglify-js": "~2.4.0" + } + }, + "grunt-legacy-log": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz", + "integrity": "sha512-qYs/uM0ImdzwIXLhS4O5WLV5soAM+PEqqHI/hzSxlo450ERSccEhnXqoeDA9ZozOdaWuYnzTOTwRcVRogleMxg==", + "peer": true, + "requires": { + "colors": "~0.6.2", + "grunt-legacy-log-utils": "~0.1.1", + "hooker": "~0.2.3", + "lodash": "~2.4.1", + "underscore.string": "~2.3.3" + }, + "dependencies": { + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha512-Kak1hi6/hYHGVPmdyiZijoQyz5x2iGVzs6w9GYB/HiXEtylY7tIoYEROMjvM1d9nXJqPOrG2MNPMn01bJ+S0Rw==", + "peer": true + }, + "underscore.string": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha512-hbD5MibthuDAu4yA5wxes5bzFgqd3PpBJuClbRxaNddxfdsz+qf+1kHwrGQFrmchmDHb9iNU+6EHDn8uj0xDJg==", + "peer": true + } + } + }, + "grunt-legacy-log-utils": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz", + "integrity": "sha512-D0vbUX00TFYCKNZtcZzemMpwT8TR/FdRs1pmfiBw6qnUw80PfsjV+lhIozY/3eJ3PSG2zj89wd2mH/7f4tNAlw==", + "peer": true, + "requires": { + "colors": "~0.6.2", + "lodash": "~2.4.1", + "underscore.string": "~2.3.3" + }, + "dependencies": { + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha512-Kak1hi6/hYHGVPmdyiZijoQyz5x2iGVzs6w9GYB/HiXEtylY7tIoYEROMjvM1d9nXJqPOrG2MNPMn01bJ+S0Rw==", + "peer": true + }, + "underscore.string": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha512-hbD5MibthuDAu4yA5wxes5bzFgqd3PpBJuClbRxaNddxfdsz+qf+1kHwrGQFrmchmDHb9iNU+6EHDn8uj0xDJg==", + "peer": true + } + } + }, + "grunt-legacy-util": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz", + "integrity": "sha512-cXPbfF8aM+pvveQeN1K872D5fRm30xfJWZiS63Y8W8oyIPLClCsmI8bW96Txqzac9cyL4lRqEBhbhJ3n5EzUUQ==", + "peer": true, + "requires": { + "async": "~0.1.22", + "exit": "~0.1.1", + "getobject": "~0.1.0", + "hooker": "~0.2.3", + "lodash": "~0.9.2", + "underscore.string": "~2.2.1", + "which": "~1.0.5" + } + }, + "grunt-lib-contrib": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/grunt-lib-contrib/-/grunt-lib-contrib-0.6.1.tgz", + "integrity": "sha512-HdCtJuMmmkSAVrAfsG7lZWE0YabrsPWwzcCCUgWQOAaQsQSUNhw/IwD2YjCSLh5y9NXSPzHTYFLL4ro7QbAJMA==", + "requires": { + "zlib-browserify": "0.0.1" + } + }, "has": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", @@ -6575,6 +8387,12 @@ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true }, + "hooker": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", + "integrity": "sha512-t+UerCsQviSymAInD01Pw+Dn/usmz1sRO+3Zk1+lx8eg+WKpD2ulcwWqHHL0+aseRBr+3+vIhiG1K1JTwaIcTA==", + "peer": true + }, "html-parse-stringify": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", @@ -6600,6 +8418,12 @@ "cross-fetch": "4.0.0" } }, + "iconv-lite": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", + "integrity": "sha512-KhmFWgaQZY83Cbhi+ADInoUQ8Etn6BG5fikM9syeOjQltvR45h7cRKJ/9uvQEuD61I3Uju77yYce0/LhKVClQw==", + "peer": true + }, "ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -6646,6 +8470,11 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" }, + "is-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", + "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==" + }, "is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -6679,6 +8508,16 @@ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, + "js-yaml": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz", + "integrity": "sha512-VEKcIksckDBUhg2JS874xVouiPkywVUh4yyUmLCDe1Zg3bCd6M+F1eGPenPeHLc2XC8pp9G8bsuofK0NeEqRkA==", + "peer": true, + "requires": { + "argparse": "~ 0.1.11", + "esprima": "~ 1.0.2" + } + }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -6749,6 +8588,12 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, + "lodash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", + "integrity": "sha512-LVbt/rjK62gSbhehDVKL0vlaime4Y1IBixL+bKeNfoY4L2zab/jGrxU6Ka05tMA/zBxkTk5t3ivtphdyYupczw==", + "peer": true + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -6772,6 +8617,27 @@ "integrity": "sha512-rRgUkpEHWpa5VCT66YscInCQmQuPCB1RFRzkkxMxg4b+jaL0V12E3riWWR2Sh5OIiUhCwGW/ZExuEO4Az32E6Q==", "requires": {} }, + "m3u8-parser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-7.2.0.tgz", + "integrity": "sha512-CRatFqpjVtMiMaKXxNvuI3I++vUumIXVVT/JpCpdU/FynV/ceVw1qpPyyBNindL+JlPMSesx+WX1QJaZEJSaMQ==", + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.1.1", + "global": "^4.4.0" + }, + "dependencies": { + "@videojs/vhs-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.1.1.tgz", + "integrity": "sha512-5iLX6sR2ownbv4Mtejw6Ax+naosGvoT9kY+gcuHzANyUZZ+4NpeNdKMUhb6ag0acYej1Y7cmr/F2+4PrggMiVA==", + "requires": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0" + } + } + } + }, "magic-string": { "version": "0.26.7", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.7.tgz", @@ -6787,11 +8653,11 @@ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" }, "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "requires": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" } }, @@ -6808,6 +8674,14 @@ "mime-db": "1.52.0" } }, + "min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", + "requires": { + "dom-walk": "^0.1.0" + } + }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -6816,12 +8690,32 @@ "brace-expansion": "^1.1.7" } }, + "mpd-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.3.0.tgz", + "integrity": "sha512-WgeIwxAqkmb9uTn4ClicXpEQYCEduDqRKfmUdp4X8vmghKfBNXZLYpREn9eqrDx/Tf5LhzRcJLSpi4ohfV742Q==", + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.0.0", + "@xmldom/xmldom": "^0.8.3", + "global": "^4.4.0" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "mux.js": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-7.0.3.tgz", + "integrity": "sha512-gzlzJVEGFYPtl2vvEiJneSWAWD4nfYRHD5XgxmB2gWvXraMPOYk+sxfvexmNfjQUFpmk6hwLR5C6iSFmuwCHdQ==", + "requires": { + "@babel/runtime": "^7.11.2", + "global": "^4.4.0" + } + }, "mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -6891,7 +8785,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", - "dev": true, "requires": { "abbrev": "1" } @@ -6970,6 +8863,14 @@ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==" }, + "pkcs7": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz", + "integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==", + "requires": { + "@babel/runtime": "^7.5.5" + } + }, "postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -7029,6 +8930,11 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" + }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -7203,10 +9109,16 @@ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" }, + "rimraf": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha512-R5KMKHnPAQaZMqLOsyuyUmcIjSeDm+73eoqQpaXA7AZ22BL+6C+1mcUscgOsNd8WVlJuvlgAPsegcx7pjlV0Dg==", + "peer": true + }, "rollup": { - "version": "2.79.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", - "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "version": "2.79.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", + "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, "requires": { "fsevents": "~2.3.2" @@ -7245,6 +9157,12 @@ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==", + "peer": true + }, "simple-update-notifier": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", @@ -7262,6 +9180,14 @@ } } }, + "source-map": { + "version": "0.1.34", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.34.tgz", + "integrity": "sha512-yfCwDj0vR9RTwt3pEzglgb3ZgmcXHt6DjG3bjJvzPwTL+5zDQ2MhmSzAcTy0GTiQuCiriSWXvWM1/NhKdXuoQA==", + "requires": { + "amdefine": ">=0.0.4" + } + }, "source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", @@ -7420,12 +9346,47 @@ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true }, + "uglify-js": { + "version": "2.4.24", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.4.24.tgz", + "integrity": "sha512-tktIjwackfZLd893KGJmXc1hrRHH1vH9Po3xFh1XBjjeGAnN02xJ3SuoA+n1L29/ZaCA18KzCFlckS+vfPugiA==", + "requires": { + "async": "~0.2.6", + "source-map": "0.1.34", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.5.4" + }, + "dependencies": { + "async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==" + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha512-vb2s1lYx2xBtUgy+ta+b2J/GLVUR+wmpINwHePmPRhOsIVCG2wDzKJ0n14GslH1BifsqVzSOwQhRaCAsZ/nI4Q==" + }, "undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, + "underscore": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", + "integrity": "sha512-cp0oQQyZhUM1kpJDLdGO1jPZHgS/MpzoWYfe9+CM2h/QGDZlqwT2T3YGukuBdaNJ/CAPoeyAZRRHz8JFo176vA==", + "peer": true + }, + "underscore.string": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", + "integrity": "sha512-3FVmhXqelrj6gfgp3Bn6tOavJvW0dNH2T+heTD38JRxIrAbiuzbqjknszoOYj3DyFB1nWiLj208Qt2no/L4cIA==", + "peer": true + }, "undici-types": { "version": "5.25.3", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", @@ -7442,6 +9403,11 @@ "picocolors": "^1.0.0" } }, + "url-toolkit": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.5.tgz", + "integrity": "sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==" + }, "use-callback-ref": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", @@ -7464,10 +9430,58 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "video.js": { + "version": "8.17.4", + "resolved": "https://registry.npmjs.org/video.js/-/video.js-8.17.4.tgz", + "integrity": "sha512-AECieAxKMKB/QgYK36ci50phfpWys6bFT6+pGMpSafeFYSoZaQ2Vpl83T9Qqcesv4TO7oNtiycnVeaBnrva2oA==", + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/http-streaming": "3.13.3", + "@videojs/vhs-utils": "^4.0.0", + "@videojs/xhr": "2.7.0", + "aes-decrypter": "^4.0.1", + "global": "4.4.0", + "m3u8-parser": "^7.1.0", + "mpd-parser": "^1.2.2", + "mux.js": "^7.0.1", + "videojs-contrib-quality-levels": "4.1.0", + "videojs-font": "4.2.0", + "videojs-vtt.js": "0.15.5" + } + }, + "videojs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/videojs/-/videojs-1.0.0.tgz", + "integrity": "sha512-FwI02jJ7d4E6goWuc/4LTN5OJlD1M0jInoIoNemo4EzMfu6IywhahMXDriLObX17ML62RsHS0oiCUE9wVB6i8A==", + "requires": { + "grunt-contrib-uglify": "^0.2.7" + } + }, + "videojs-contrib-quality-levels": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-4.1.0.tgz", + "integrity": "sha512-TfrXJJg1Bv4t6TOCMEVMwF/CoS8iENYsWNKip8zfhB5kTcegiFYezEA0eHAJPU64ZC8NQbxQgOwAsYU8VXbOWA==", + "requires": { + "global": "^4.4.0" + } + }, + "videojs-font": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-4.2.0.tgz", + "integrity": "sha512-YPq+wiKoGy2/M7ccjmlvwi58z2xsykkkfNMyIg4xb7EZQQNwB71hcSsB3o75CqQV7/y5lXkXhI/rsGAS7jfEmQ==" + }, + "videojs-vtt.js": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz", + "integrity": "sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==", + "requires": { + "global": "^4.3.1" + } + }, "vite": { - "version": "3.2.10", - "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.10.tgz", - "integrity": "sha512-Dx3olBo/ODNiMVk/cA5Yft9Ws+snLOXrhLtrI3F4XLt4syz2Yg8fayZMWScPKoz12v5BUv7VEmQHnsfpY80fYw==", + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.11.tgz", + "integrity": "sha512-K/jGKL/PgbIgKCiJo5QbASQhFiV02X9Jh+Qq0AKCRCRKZtOTVi4t6wh75FDpGf2N9rYOnzH87OEFQNaFy6pdxQ==", "dev": true, "requires": { "esbuild": "^0.15.9", @@ -7498,6 +9512,22 @@ "webidl-conversions": "^3.0.0" } }, + "which": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", + "integrity": "sha512-E87fdQ/eRJr9W1X4wTPejNy9zTW3FI2vpCZSJ/HAY+TkjKVC0TUm1jk6vn2Z7qay0DQy0+RBGdXxj+RmmiGZKQ==", + "peer": true + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha512-1pTPQDKTdd61ozlKGNCjhNRd+KPmgLSGa3mZTHoOliaGcESD8G1PXhh7c1fgiPjVbNVfgy2Faw4BI8/m0cC8Mg==" + }, + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q==" + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -7514,6 +9544,22 @@ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz", "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==" }, + "yargs": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.5.4.tgz", + "integrity": "sha512-5j382E4xQSs71p/xZQsU1PtRA2HXPAjX0E0DkoGLxwNASMOKX6A9doV1NrZmj85u2Pjquz402qonBzz/yLPbPA==", + "requires": { + "camelcase": "^1.0.2", + "decamelize": "^1.0.0", + "window-size": "0.1.0", + "wordwrap": "0.0.2" + } + }, + "zlib-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/zlib-browserify/-/zlib-browserify-0.0.1.tgz", + "integrity": "sha512-fheIDCKXU0YAGZMv4FFwVTBMQRSv2ZjNqRN1VkZjetZDK/BC/hViEhasTh0kTeogcsIAl5gYE04GN53trT+cFw==" + }, "zod": { "version": "3.22.4", "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", diff --git a/src/tribler/ui/package.json b/src/tribler/ui/package.json index c9cf05fd24..909831f0de 100644 --- a/src/tribler/ui/package.json +++ b/src/tribler/ui/package.json @@ -19,6 +19,7 @@ "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-navigation-menu": "^1.1.4", + "@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-progress": "^1.0.3", "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-select": "^2.0.0", @@ -27,9 +28,11 @@ "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.7", "@tanstack/react-table": "^8.10.7", + "@types/video.js": "^7.3.58", "axios": "^1.6.8", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", + "cmdk": "^1.0.0", "i18next": "^23.11.4", "js-cookie": "^3.0.5", "jszip": "^3.10.1", @@ -43,6 +46,8 @@ "react-router-dom": "^6.16.0", "tailwind-merge": "^1.14.0", "tailwindcss-animate": "^1.0.7", + "video.js": "^8.17.4", + "videojs": "^1.0.0", "zod": "^3.22.4" }, "devDependencies": { diff --git a/src/tribler/ui/src/components/ui/command.tsx b/src/tribler/ui/src/components/ui/command.tsx new file mode 100644 index 0000000000..3fb85bb0b6 --- /dev/null +++ b/src/tribler/ui/src/components/ui/command.tsx @@ -0,0 +1,155 @@ +"use client" + +import * as React from "react" +import { type DialogProps } from "@radix-ui/react-dialog" +import { MagnifyingGlassIcon } from "@radix-ui/react-icons" +import { Command as CommandPrimitive } from "cmdk" + +import { cn } from "@/lib/utils" +import { Dialog, DialogContent } from "./dialog" + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Command.displayName = CommandPrimitive.displayName + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + + + + {children} + + + + ) +} + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +CommandShortcut.displayName = "CommandShortcut" + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} \ No newline at end of file diff --git a/src/tribler/ui/src/components/ui/popover.tsx b/src/tribler/ui/src/components/ui/popover.tsx new file mode 100644 index 0000000000..7081ab0a1f --- /dev/null +++ b/src/tribler/ui/src/components/ui/popover.tsx @@ -0,0 +1,33 @@ +"use client" + +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +const Popover = PopoverPrimitive.Root + +const PopoverPortal = PopoverPrimitive.Portal + +const PopoverTrigger = PopoverPrimitive.Trigger + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( + // + + // +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverTrigger, PopoverContent, PopoverPortal } \ No newline at end of file diff --git a/src/tribler/ui/src/lib/utils.ts b/src/tribler/ui/src/lib/utils.ts index 5c819fee7d..9597adc756 100644 --- a/src/tribler/ui/src/lib/utils.ts +++ b/src/tribler/ui/src/lib/utils.ts @@ -1,7 +1,7 @@ import { type ClassValue, clsx } from "clsx" import { twMerge } from "tailwind-merge" import { category } from "@/models/torrent.model"; -import { FileLink, FileTreeItem } from "@/models/file.model"; +import { File, FileLink, FileTreeItem } from "@/models/file.model"; import { CheckedState } from "@radix-ui/react-checkbox"; import JSZip from "jszip"; import { triblerService } from "@/services/tribler.service"; @@ -240,3 +240,15 @@ export async function downloadFilesAsZip(files: FileLink[], zipName: string) { export function isMac() { return navigator.userAgent.includes('Mac'); } + +var streamableExtensions = ['mp4', 'm4v', 'mov', 'mkv']; +export function getStreamableFiles(files: File[]) { + const results: File[] = []; + for (const file of files) { + const extension = file.name.split('.').pop(); + if (extension && streamableExtensions.includes(extension)) { + results.push(file); + } + } + return results; +} diff --git a/src/tribler/ui/src/models/download.model.tsx b/src/tribler/ui/src/models/download.model.tsx index 30a857bba7..761be40c0f 100644 --- a/src/tribler/ui/src/models/download.model.tsx +++ b/src/tribler/ui/src/models/download.model.tsx @@ -1,7 +1,8 @@ // For compile-time type checking and code completion import { Peer } from "./bittorrentpeer.model"; -import { Tracker } from "./tracker.model "; +import { File } from "./file.model"; +import { Tracker } from "./tracker.model"; type state = 'ALLOCATING_DISKSPACE' | 'WAITING_FOR_HASHCHECK' | 'HASHCHECKING' | 'DOWNLOADING' | @@ -38,4 +39,6 @@ export interface Download { availability?: number; pieces?: string; peers: Peer[]; + files: File[] | undefined; + streamable?: boolean; } diff --git a/src/tribler/ui/src/models/tracker.model .tsx b/src/tribler/ui/src/models/tracker.model.tsx similarity index 100% rename from src/tribler/ui/src/models/tracker.model .tsx rename to src/tribler/ui/src/models/tracker.model.tsx diff --git a/src/tribler/ui/src/pages/Downloads/Actions.tsx b/src/tribler/ui/src/pages/Downloads/Actions.tsx index 54a6aa6c29..7b1a455c0d 100644 --- a/src/tribler/ui/src/pages/Downloads/Actions.tsx +++ b/src/tribler/ui/src/pages/Downloads/Actions.tsx @@ -15,14 +15,15 @@ import { DropdownMenuSubTrigger, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; -import { CheckCheckIcon, ExternalLinkIcon, MoreHorizontal, Pause, Play, Trash, VenetianMaskIcon } from "lucide-react"; +import { CheckCheckIcon, Clapperboard, ExternalLinkIcon, MoreHorizontal, Pause, Play, Trash, VenetianMaskIcon } from "lucide-react"; import { MoveIcon } from "@radix-ui/react-icons"; -import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { useState } from "react"; import { Label } from "@/components/ui/label"; import { useTranslation } from "react-i18next"; import { PathInput } from "@/components/path-input"; import { downloadFile, downloadFilesAsZip } from "@/lib/utils"; +import { VideoDialog } from "./Videoplayer"; export default function Actions({ selectedDownloads }: { selectedDownloads: Download[] }) { @@ -119,9 +120,26 @@ export default function Actions({ selectedDownloads }: { selectedDownloads: Down })(); }); } + const onStream = () => { + console.log("Streaming...", selectedDownloads[0]); + if (selectedDownloads.length == 1) { + setVideoDialogOpen(true); + setVideoDownload(selectedDownloads[0]); + } + } + + + const [videoDialogOpen, setVideoDialogOpen] = useState(false); + const [videoDownload, setVideoDownload] = useState(null); return ( <> + +

{t('WithSelected')}

+ + + + + + No video files found. + + {videoFiles.map((videoFile) => ( + { + const file = videoFiles.find((videoFile) => videoFile.name === currentName); + console.log("Changed selected video file to " + file?.name); + setSelectedFile(file); + setOpen(false) + }} + > + + {videoFile.name} + + ))} + + + + + +
+ + + + + ) +} + + +export const VideoJS = (props: { options: any; onReady?: any; open?: boolean}) => { + const videoRef = React.useRef(null); + const playerRef = React.useRef(null); + const { options, onReady } = props; + const prevOpen = usePrevious(props.open); + + + React.useEffect(() => { + + // Make sure Video.js player is only initialized once + if (!playerRef.current) { + // The Video.js player needs to be _inside_ the component el for React 18 Strict Mode. + const videoElement = document.createElement("video-js"); + + videoElement.classList.add('vjs-big-play-centered'); + // @ts-ignore + videoRef.current.appendChild(videoElement); + // @ts-ignore + const player = playerRef.current = videojs(videoElement, options, () => { + onReady && onReady(player); + }); + } else { + const player = playerRef.current; + // @ts-ignore + const src_curr = player.currentSrc(); + const src_next = options.sources[0].src; + + if ((src_curr !== src_next && props.open) || (props.open && !prevOpen)) { + console.log("Setting player source:" + src_next); + // @ts-ignore + player.autoplay(options.autoplay); + // @ts-ignore + player.src(options.sources); + } + } + }, [options, videoRef, props.open]); + + // Destroy the player when the dialog closes, or HTTP connections will remain open. + // This prevents videofiles from being locked. + useEffect(() => { + if (!props.open) { + const player = playerRef.current; + // @ts-ignore + if (player && !player.isDisposed()) { + // @ts-ignore + player.dispose(); + playerRef.current = null; + } + } + }, [props.open]); + + return ( +
+
+
+ ); +} + +export default VideoJS;