Enhance video streaming capabilities and UI integration
- Added support for streaming video files with proper MIME type handling in the media API. - Implemented transcoding functionality for H.264 compatibility on-the-fly using FFmpeg. - Updated VideoModal component to utilize Video.js for improved video playback experience. - Enhanced user interface with download options and better error handling for video playback. - Updated package.json and package-lock.json to include new dependencies for video.js and related types.
This commit is contained in:
@@ -6,7 +6,7 @@ from typing import List, Optional
|
|||||||
|
|
||||||
from fastapi import FastAPI, HTTPException, Response, Request
|
from fastapi import FastAPI, HTTPException, Response, Request
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse, StreamingResponse
|
||||||
|
|
||||||
MEDIA_DIR = pathlib.Path(os.getenv("MEDIA_VIDEOS_DIR", "/mnt/videos")).resolve()
|
MEDIA_DIR = pathlib.Path(os.getenv("MEDIA_VIDEOS_DIR", "/mnt/videos")).resolve()
|
||||||
THUMBS_DIR = pathlib.Path(os.getenv("MEDIA_THUMBS_DIR", MEDIA_DIR / ".thumbnails")).resolve()
|
THUMBS_DIR = pathlib.Path(os.getenv("MEDIA_THUMBS_DIR", MEDIA_DIR / ".thumbnails")).resolve()
|
||||||
@@ -123,6 +123,19 @@ def get_thumbnail_q(file_id: str, width: int = 320, height: int = 180):
|
|||||||
return FileResponse(thumb, media_type="image/jpeg")
|
return FileResponse(thumb, media_type="image/jpeg")
|
||||||
|
|
||||||
|
|
||||||
|
def get_video_mime_type(path: pathlib.Path) -> str:
|
||||||
|
"""Get MIME type based on file extension"""
|
||||||
|
ext = path.suffix.lower()
|
||||||
|
mime_types = {
|
||||||
|
".mp4": "video/mp4",
|
||||||
|
".avi": "video/x-msvideo",
|
||||||
|
".mov": "video/quicktime",
|
||||||
|
".mkv": "video/x-matroska",
|
||||||
|
".webm": "video/webm",
|
||||||
|
}
|
||||||
|
return mime_types.get(ext, "video/mp4")
|
||||||
|
|
||||||
|
|
||||||
def open_file_range(path: pathlib.Path, start: int, end: Optional[int]):
|
def open_file_range(path: pathlib.Path, start: int, end: Optional[int]):
|
||||||
file_size = path.stat().st_size
|
file_size = path.stat().st_size
|
||||||
if end is None or end >= file_size:
|
if end is None or end >= file_size:
|
||||||
@@ -134,13 +147,48 @@ def open_file_range(path: pathlib.Path, start: int, end: Optional[int]):
|
|||||||
return chunk, file_size, start, end
|
return chunk, file_size, start, end
|
||||||
|
|
||||||
|
|
||||||
|
@app.head("/videos/{file_id:path}/stream")
|
||||||
@app.get("/videos/{file_id:path}/stream")
|
@app.get("/videos/{file_id:path}/stream")
|
||||||
def stream_file(request: Request, file_id: str):
|
def stream_file(request: Request, file_id: str):
|
||||||
p = path_from_file_id(file_id)
|
p = path_from_file_id(file_id)
|
||||||
|
file_size = p.stat().st_size
|
||||||
|
content_type = get_video_mime_type(p)
|
||||||
|
|
||||||
range_header = request.headers.get("range")
|
range_header = request.headers.get("range")
|
||||||
|
|
||||||
|
# Base headers for all responses
|
||||||
|
base_headers = {
|
||||||
|
"Accept-Ranges": "bytes",
|
||||||
|
"Content-Type": content_type,
|
||||||
|
"Cache-Control": "public, max-age=3600",
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": "GET, HEAD, OPTIONS",
|
||||||
|
"Access-Control-Allow-Headers": "Range, Content-Type",
|
||||||
|
"Access-Control-Expose-Headers": "Content-Range, Accept-Ranges, Content-Length",
|
||||||
|
}
|
||||||
|
|
||||||
|
# For HEAD requests, just return headers without body
|
||||||
|
if request.method == "HEAD":
|
||||||
|
headers = {
|
||||||
|
**base_headers,
|
||||||
|
"Content-Length": str(file_size),
|
||||||
|
}
|
||||||
|
return Response(status_code=200, headers=headers)
|
||||||
|
|
||||||
if not range_header:
|
if not range_header:
|
||||||
return FileResponse(p, media_type="video/mp4")
|
# No range request - return full file with proper headers
|
||||||
# parse bytes=START-END
|
headers = {
|
||||||
|
**base_headers,
|
||||||
|
"Content-Length": str(file_size),
|
||||||
|
}
|
||||||
|
return FileResponse(
|
||||||
|
p,
|
||||||
|
media_type=content_type,
|
||||||
|
headers=headers,
|
||||||
|
status_code=200
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parse range request: bytes=START-END
|
||||||
range_value = range_header.strip().lower().replace("bytes=", "")
|
range_value = range_header.strip().lower().replace("bytes=", "")
|
||||||
start_str, _, end_str = range_value.partition("-")
|
start_str, _, end_str = range_value.partition("-")
|
||||||
try:
|
try:
|
||||||
@@ -148,21 +196,128 @@ def stream_file(request: Request, file_id: str):
|
|||||||
end = int(end_str) if end_str else None
|
end = int(end_str) if end_str else None
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(status_code=400, detail="Bad Range header")
|
raise HTTPException(status_code=400, detail="Bad Range header")
|
||||||
chunk, size, start, end = open_file_range(p, start, end)
|
|
||||||
|
# Validate range
|
||||||
|
if start < 0:
|
||||||
|
start = 0
|
||||||
|
if end is None or end >= file_size:
|
||||||
|
end = file_size - 1
|
||||||
|
if start > end:
|
||||||
|
raise HTTPException(status_code=416, detail="Range Not Satisfiable")
|
||||||
|
|
||||||
|
chunk, size, actual_start, actual_end = open_file_range(p, start, end)
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"Content-Range": f"bytes {start}-{end}/{size}",
|
**base_headers,
|
||||||
"Accept-Ranges": "bytes",
|
"Content-Range": f"bytes {actual_start}-{actual_end}/{size}",
|
||||||
"Content-Length": str(len(chunk)),
|
"Content-Length": str(len(chunk)),
|
||||||
"Content-Type": "video/mp4",
|
|
||||||
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
||||||
"Access-Control-Allow-Origin": "*",
|
|
||||||
}
|
}
|
||||||
return Response(content=chunk, status_code=206, headers=headers)
|
return Response(content=chunk, status_code=206, headers=headers)
|
||||||
|
|
||||||
# Convenience endpoint: pass file_id via query instead of path (accepts raw or URL-encoded)
|
# Convenience endpoint: pass file_id via query instead of path (accepts raw or URL-encoded)
|
||||||
|
@app.head("/videos/stream")
|
||||||
@app.get("/videos/stream")
|
@app.get("/videos/stream")
|
||||||
def stream_file_q(request: Request, file_id: str):
|
def stream_file_q(request: Request, file_id: str):
|
||||||
return stream_file(request, file_id)
|
return stream_file(request, file_id)
|
||||||
|
|
||||||
|
|
||||||
|
# Handle OPTIONS requests for CORS preflight
|
||||||
|
@app.options("/videos/{file_id:path}/stream")
|
||||||
|
@app.options("/videos/stream")
|
||||||
|
@app.options("/videos/{file_id:path}/stream-transcoded")
|
||||||
|
@app.options("/videos/stream-transcoded")
|
||||||
|
def stream_options():
|
||||||
|
return Response(
|
||||||
|
status_code=200,
|
||||||
|
headers={
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": "GET, HEAD, OPTIONS",
|
||||||
|
"Access-Control-Allow-Headers": "Range, Content-Type",
|
||||||
|
"Access-Control-Max-Age": "86400",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_transcoded_stream(file_path: pathlib.Path, start_time: float = 0.0):
|
||||||
|
"""
|
||||||
|
Transcode video to H.264 on-the-fly using FFmpeg.
|
||||||
|
Streams H.264/MP4 that browsers can actually play.
|
||||||
|
"""
|
||||||
|
# FFmpeg command to transcode to H.264 with web-optimized settings
|
||||||
|
cmd = [
|
||||||
|
"ffmpeg",
|
||||||
|
"-i", str(file_path),
|
||||||
|
"-c:v", "libx264", # H.264 codec
|
||||||
|
"-preset", "ultrafast", # Fast encoding for real-time
|
||||||
|
"-tune", "zerolatency", # Low latency
|
||||||
|
"-crf", "23", # Quality (18-28, lower = better)
|
||||||
|
"-c:a", "aac", # AAC audio if present
|
||||||
|
"-movflags", "+faststart", # Web-optimized (moov atom at beginning)
|
||||||
|
"-f", "mp4", # MP4 container
|
||||||
|
"-" # Output to stdout
|
||||||
|
]
|
||||||
|
|
||||||
|
# If seeking to specific time
|
||||||
|
if start_time > 0:
|
||||||
|
cmd.insert(-2, "-ss")
|
||||||
|
cmd.insert(-2, str(start_time))
|
||||||
|
|
||||||
|
try:
|
||||||
|
process = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
bufsize=8192
|
||||||
|
)
|
||||||
|
|
||||||
|
# Stream chunks
|
||||||
|
chunk_size = 8192
|
||||||
|
while True:
|
||||||
|
chunk = process.stdout.read(chunk_size)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
yield chunk
|
||||||
|
|
||||||
|
process.wait()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"FFmpeg transcoding error: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
@app.head("/videos/{file_id:path}/stream-transcoded")
|
||||||
|
@app.get("/videos/{file_id:path}/stream-transcoded")
|
||||||
|
@app.head("/videos/stream-transcoded")
|
||||||
|
@app.get("/videos/stream-transcoded")
|
||||||
|
def stream_transcoded(request: Request, file_id: str, start_time: float = 0.0):
|
||||||
|
"""
|
||||||
|
Stream video transcoded to H.264 on-the-fly.
|
||||||
|
This endpoint converts MPEG-4 Part 2 videos to H.264 for browser compatibility.
|
||||||
|
"""
|
||||||
|
p = path_from_file_id(file_id)
|
||||||
|
content_type = "video/mp4"
|
||||||
|
|
||||||
|
# Base headers
|
||||||
|
headers = {
|
||||||
|
"Content-Type": content_type,
|
||||||
|
"Cache-Control": "no-cache, no-store, must-revalidate",
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": "GET, HEAD, OPTIONS",
|
||||||
|
"Access-Control-Allow-Headers": "Range, Content-Type",
|
||||||
|
"Access-Control-Expose-Headers": "Content-Range, Accept-Ranges, Content-Length",
|
||||||
|
}
|
||||||
|
|
||||||
|
# For HEAD requests, just return headers (we can't know size without transcoding)
|
||||||
|
if request.method == "HEAD":
|
||||||
|
return Response(status_code=200, headers=headers)
|
||||||
|
|
||||||
|
# Note: Range requests are complex with transcoding, so we'll transcode from start
|
||||||
|
# For better performance with range requests, we'd need to cache transcoded segments
|
||||||
|
return StreamingResponse(
|
||||||
|
generate_transcoded_stream(p, start_time),
|
||||||
|
media_type=content_type,
|
||||||
|
headers=headers
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
424
video-remote/package-lock.json
generated
424
video-remote/package-lock.json
generated
@@ -8,14 +8,17 @@
|
|||||||
"name": "video-remote",
|
"name": "video-remote",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@videojs/themes": "^1.0.1",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0"
|
"react-dom": "^19.1.0",
|
||||||
|
"video.js": "^8.23.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@originjs/vite-plugin-federation": "^1.3.3",
|
"@originjs/vite-plugin-federation": "^1.3.3",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.2",
|
||||||
"@types/react-dom": "^19.2.2",
|
"@types/react-dom": "^19.2.2",
|
||||||
|
"@types/video.js": "^7.3.58",
|
||||||
"@vitejs/plugin-react": "^4.6.0",
|
"@vitejs/plugin-react": "^4.6.0",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
"serve": "^14.2.3",
|
"serve": "^14.2.3",
|
||||||
@@ -258,6 +261,15 @@
|
|||||||
"@babel/core": "^7.0.0-0"
|
"@babel/core": "^7.0.0-0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.28.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
||||||
|
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.27.2",
|
"version": "7.27.2",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
|
||||||
@@ -1482,6 +1494,70 @@
|
|||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@videojs/http-streaming": {
|
||||||
|
"version": "3.17.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.17.2.tgz",
|
||||||
|
"integrity": "sha512-VBQ3W4wnKnVKb/limLdtSD2rAd5cmHN70xoMf4OmuDd0t2kfJX04G+sfw6u2j8oOm2BXYM9E1f4acHruqKnM1g==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@videojs/vhs-utils": "^4.1.1",
|
||||||
|
"aes-decrypter": "^4.0.2",
|
||||||
|
"global": "^4.4.0",
|
||||||
|
"m3u8-parser": "^7.2.0",
|
||||||
|
"mpd-parser": "^1.3.1",
|
||||||
|
"mux.js": "7.1.0",
|
||||||
|
"video.js": "^7 || ^8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8",
|
||||||
|
"npm": ">=5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"video.js": "^8.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@videojs/themes": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@videojs/themes/-/themes-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-2b6YIIIz5x+/eSFdkSZ2RZJfHIMfP7bGODR3wDzLTqFF2kEKnJVIXxBUNzdZC/qiVETqAA2Ba6mCp+iXTUYt4A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"postcss-inline-svg": "^4.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"global": "^4.4.0"
|
||||||
|
},
|
||||||
|
"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==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.5.5",
|
||||||
|
"global": "~4.4.0",
|
||||||
|
"is-function": "^1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vitejs/plugin-react": {
|
"node_modules/@vitejs/plugin-react": {
|
||||||
"version": "4.7.0",
|
"version": "4.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
||||||
@@ -1503,6 +1579,15 @@
|
|||||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@xmldom/xmldom": {
|
||||||
|
"version": "0.8.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz",
|
||||||
|
"integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@zeit/schemas": {
|
"node_modules/@zeit/schemas": {
|
||||||
"version": "2.36.0",
|
"version": "2.36.0",
|
||||||
"resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz",
|
||||||
@@ -1510,6 +1595,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"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==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@videojs/vhs-utils": "^4.1.1",
|
||||||
|
"global": "^4.4.0",
|
||||||
|
"pkcs7": "^1.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "8.12.0",
|
"version": "8.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
|
||||||
@@ -1680,6 +1777,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/boolbase": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/boxen": {
|
"node_modules/boxen": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz",
|
||||||
@@ -2034,6 +2137,30 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-select": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"boolbase": "^1.0.0",
|
||||||
|
"css-what": "^3.2.1",
|
||||||
|
"domutils": "^1.7.0",
|
||||||
|
"nth-check": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-what": {
|
||||||
|
"version": "3.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz",
|
||||||
|
"integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
@@ -2079,6 +2206,46 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dom-serializer": {
|
||||||
|
"version": "0.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz",
|
||||||
|
"integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^1.3.0",
|
||||||
|
"entities": "^1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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/domelementtype": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/domhandler": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/domutils": {
|
||||||
|
"version": "1.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
|
||||||
|
"integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": "0",
|
||||||
|
"domelementtype": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
@@ -2129,6 +2296,12 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
"node_modules/es-define-property": {
|
"node_modules/es-define-property": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
@@ -2388,6 +2561,16 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/global": {
|
||||||
|
"version": "4.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
|
||||||
|
"integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"min-document": "^2.19.0",
|
||||||
|
"process": "^0.11.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/gopd": {
|
"node_modules/gopd": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
@@ -2467,6 +2650,20 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/htmlparser2": {
|
||||||
|
"version": "3.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
|
||||||
|
"integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^1.3.1",
|
||||||
|
"domhandler": "^2.3.0",
|
||||||
|
"domutils": "^1.5.1",
|
||||||
|
"entities": "^1.1.1",
|
||||||
|
"inherits": "^2.0.1",
|
||||||
|
"readable-stream": "^3.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/http-proxy": {
|
"node_modules/http-proxy": {
|
||||||
"version": "1.18.1",
|
"version": "1.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
||||||
@@ -2566,6 +2763,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/ini": {
|
"node_modules/ini": {
|
||||||
"version": "1.3.8",
|
"version": "1.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||||
@@ -2599,6 +2802,12 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/is-port-reachable": {
|
"node_modules/is-port-reachable": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-4.0.0.tgz",
|
||||||
@@ -2966,6 +3175,17 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@videojs/vhs-utils": "^4.1.1",
|
||||||
|
"global": "^4.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.27.0",
|
"version": "0.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz",
|
||||||
@@ -3052,6 +3272,14 @@
|
|||||||
"node": ">=6"
|
"node": ">=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": {
|
"node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
@@ -3075,6 +3303,21 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mpd-parser": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-1FuyEWI5k2HcmhS1HkKnUAQV7yFPfXPht2DnRRGtoiiAAW+ESTbtEXIDpRkwdU+XyrQuwrIym7UkoPKsZ0SyFw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"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": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@@ -3082,6 +3325,23 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/mux.js": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mux.js/-/mux.js-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-NTxawK/BBELJrYsZThEulyUMDVlLizKdxyAsMuzoCD1eFj97BVaA8D/CvKsKu6FOLYkFojN5CbM9h++ZTZtknA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.11.2",
|
||||||
|
"global": "^4.4.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"muxjs-transmux": "bin/transmux.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8",
|
||||||
|
"npm": ">=5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
@@ -3131,6 +3391,15 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nth-check": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"boolbase": "~1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-inspect": {
|
"node_modules/object-inspect": {
|
||||||
"version": "1.13.4",
|
"version": "1.13.4",
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
@@ -3224,6 +3493,18 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.5.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"pkcs7": "bin/cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/portfinder": {
|
"node_modules/portfinder": {
|
||||||
"version": "1.0.38",
|
"version": "1.0.38",
|
||||||
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.38.tgz",
|
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.38.tgz",
|
||||||
@@ -3267,6 +3548,57 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/postcss-inline-svg": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss-inline-svg/-/postcss-inline-svg-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-0pYBJyoQ9/sJViYRc1cNOOTM7DYh0/rmASB0TBeRmWkG8YFK2tmgdkfjHkbRma1iFtBFKFHZFsHwRTDZTMKzSQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"css-select": "^2.0.2",
|
||||||
|
"dom-serializer": "^0.1.1",
|
||||||
|
"htmlparser2": "^3.10.1",
|
||||||
|
"postcss": "^7.0.17",
|
||||||
|
"postcss-value-parser": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postcss-inline-svg/node_modules/picocolors": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz",
|
||||||
|
"integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/postcss-inline-svg/node_modules/postcss": {
|
||||||
|
"version": "7.0.39",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz",
|
||||||
|
"integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"picocolors": "^0.2.1",
|
||||||
|
"source-map": "^0.6.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/postcss/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postcss-value-parser": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"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==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
@@ -3350,6 +3682,20 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/registry-auth-token": {
|
"node_modules/registry-auth-token": {
|
||||||
"version": "3.3.2",
|
"version": "3.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz",
|
||||||
@@ -3437,7 +3783,6 @@
|
|||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -3642,6 +3987,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/source-map": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@@ -3652,6 +4006,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string-width": {
|
"node_modules/string-width": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||||
@@ -3855,6 +4218,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/util-deprecate": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/vary": {
|
"node_modules/vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
@@ -3865,6 +4234,57 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/video.js": {
|
||||||
|
"version": "8.23.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/video.js/-/video.js-8.23.4.tgz",
|
||||||
|
"integrity": "sha512-qI0VTlYmKzEqRsz1Nppdfcaww4RSxZAq77z2oNSl3cNg2h6do5C8Ffl0KqWQ1OpD8desWXsCrde7tKJ9gGTEyQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@videojs/http-streaming": "^3.17.2",
|
||||||
|
"@videojs/vhs-utils": "^4.1.1",
|
||||||
|
"@videojs/xhr": "2.7.0",
|
||||||
|
"aes-decrypter": "^4.0.2",
|
||||||
|
"global": "4.4.0",
|
||||||
|
"m3u8-parser": "^7.2.0",
|
||||||
|
"mpd-parser": "^1.3.1",
|
||||||
|
"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-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==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"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==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"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==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"global": "^4.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.1.12",
|
"version": "7.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
|
||||||
|
|||||||
@@ -12,14 +12,17 @@
|
|||||||
"dev:watch": "npm run build && (npm run build:watch &) && sleep 1 && npx http-server dist -p 3001 --cors -c-1"
|
"dev:watch": "npm run build && (npm run build:watch &) && sleep 1 && npx http-server dist -p 3001 --cors -c-1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@videojs/themes": "^1.0.1",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0"
|
"react-dom": "^19.1.0",
|
||||||
|
"video.js": "^8.23.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@originjs/vite-plugin-federation": "^1.3.3",
|
"@originjs/vite-plugin-federation": "^1.3.3",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.2",
|
||||||
"@types/react-dom": "^19.2.2",
|
"@types/react-dom": "^19.2.2",
|
||||||
|
"@types/video.js": "^7.3.58",
|
||||||
"@vitejs/plugin-react": "^4.6.0",
|
"@vitejs/plugin-react": "^4.6.0",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
"serve": "^14.2.3",
|
"serve": "^14.2.3",
|
||||||
@@ -27,4 +30,4 @@
|
|||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"vite": "^7.0.4"
|
"vite": "^7.0.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import React from 'react'
|
import React, { useRef, useEffect } from 'react'
|
||||||
|
import videojs from 'video.js'
|
||||||
|
import 'video.js/dist/video-js.css'
|
||||||
|
|
||||||
const BASE = (import.meta as any).env?.VITE_MEDIA_API_URL || (import.meta as any).env?.VITE_VISION_API_URL || '/api'
|
const BASE = (import.meta as any).env?.VITE_MEDIA_API_URL || (import.meta as any).env?.VITE_VISION_API_URL || '/api'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -7,8 +10,59 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const VideoModal: React.FC<Props> = ({ fileId, onClose }) => {
|
export const VideoModal: React.FC<Props> = ({ fileId, onClose }) => {
|
||||||
if (!fileId) return null
|
const videoRef = useRef<HTMLVideoElement>(null)
|
||||||
const src = `${BASE}/videos/stream?file_id=${encodeURIComponent(fileId)}`
|
const playerRef = useRef<any>(null)
|
||||||
|
// Use transcoded endpoint for browser compatibility (H.264)
|
||||||
|
const src = fileId ? `${BASE}/videos/stream-transcoded?file_id=${encodeURIComponent(fileId)}` : null
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!fileId || !src || !videoRef.current) return
|
||||||
|
|
||||||
|
// Initialize Video.js player
|
||||||
|
const player = videojs(videoRef.current, {
|
||||||
|
controls: true,
|
||||||
|
autoplay: true,
|
||||||
|
preload: 'auto',
|
||||||
|
fluid: true,
|
||||||
|
responsive: true,
|
||||||
|
playbackRates: [0.5, 1, 1.25, 1.5, 2],
|
||||||
|
sources: [
|
||||||
|
{
|
||||||
|
src: src,
|
||||||
|
type: 'video/mp4'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
playerRef.current = player
|
||||||
|
|
||||||
|
player.on('error', () => {
|
||||||
|
const error = player.error()
|
||||||
|
if (error) {
|
||||||
|
console.error('Video.js error:', error)
|
||||||
|
console.error('Error code:', error.code)
|
||||||
|
console.error('Error message:', error.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
player.on('loadedmetadata', () => {
|
||||||
|
console.log('Video metadata loaded, duration:', player.duration())
|
||||||
|
})
|
||||||
|
|
||||||
|
player.on('canplay', () => {
|
||||||
|
console.log('Video can play')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => {
|
||||||
|
if (playerRef.current) {
|
||||||
|
playerRef.current.dispose()
|
||||||
|
playerRef.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [fileId, src])
|
||||||
|
|
||||||
|
if (!fileId || !src) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -31,16 +85,28 @@ export const VideoModal: React.FC<Props> = ({ fileId, onClose }) => {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="p-4 bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
|
<div className="p-4 bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Video Player</h3>
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">Watch your recording</p>
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Video Player</h3>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">Watch your recording</p>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href={src}
|
||||||
|
download
|
||||||
|
className="px-3 py-1.5 text-xs bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
Download Video
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 bg-black">
|
<div className="p-4 bg-black">
|
||||||
<div className="relative w-full" style={{ aspectRatio: '16/9', maxHeight: '70vh' }}>
|
<div className="relative w-full" style={{ aspectRatio: '16/9', maxHeight: '70vh' }}>
|
||||||
<video
|
<video
|
||||||
src={src}
|
ref={videoRef}
|
||||||
controls
|
className="video-js vjs-default-skin w-full h-full"
|
||||||
className="w-full h-full rounded-lg object-contain"
|
playsInline
|
||||||
autoPlay
|
key={fileId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -50,5 +116,3 @@ export const VideoModal: React.FC<Props> = ({ fileId, onClose }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default VideoModal
|
export default VideoModal
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user