From eda2acb1bef5c32b7bcf423d2745ca2788636667 Mon Sep 17 00:00:00 2001 From: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Sat, 15 Mar 2025 16:40:03 +0100 Subject: [PATCH] feat: move to nginx flv --- dev/docker-compose.yml | 30 ++-- dev/html/.gitkeep | 0 dev/html/stat.xsl | 62 ------- dev/nginx.conf | 36 ++-- flv-module/Dockerfile | 56 ++++++ package.json | 1 + src/app/(protected)/api/rtmp/publish/route.ts | 2 - src/app/(protected)/api/stream/chat/route.ts | 5 - src/components/app/ChatPanel/ChatPanel.tsx | 11 -- src/components/app/Livestream/Livestream.tsx | 1 - .../app/StreamPlayer/StreamPlayer.livekit.tsx | 168 ------------------ .../app/StreamPlayer/StreamPlayer.tsx | 12 ++ src/instrumentation.ts | 2 +- src/lib/instrumentation/streamInfo.ts | 98 +++++----- src/lib/types/liveBackendJson.ts | 80 +++++++++ yarn.lock | 161 +++++++++++++++++ 16 files changed, 391 insertions(+), 334 deletions(-) create mode 100644 dev/html/.gitkeep delete mode 100644 dev/html/stat.xsl create mode 100644 flv-module/Dockerfile delete mode 100644 src/components/app/StreamPlayer/StreamPlayer.livekit.tsx create mode 100644 src/lib/types/liveBackendJson.ts diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index 8e33b1c..798e78d 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -17,24 +17,30 @@ services: environment: UID: 1000 GID: 1000 + API_AUTH: asdf volumes: - - ./nginx.conf:/etc/nginx/nginx.conf + - ./nginx.conf:/etc/nginx/templates/nginx.conf.template - ./html:/var/www/html - - /dev/shm:/dev/shm - image: tiangolo/nginx-rtmp + - /dev/shm/hls:/dev/shm/hls + image: flv-module entrypoint: - /bin/sh - -c - | - usermod -u $${UID} www-data - groupmod -g $${GID} www-data + # Process the template file + mkdir -p /usr/local/nginx/conf + envsubst '$${API_AUTH}' < /etc/nginx/templates/nginx.conf.template > /usr/local/nginx/conf/nginx.conf + + echo "Setting UID to $${UID} and GID to $${GID}" + usermod -u $${UID} nginx || echo "failed to change uid" + groupmod -g $${GID} nginx || echo "failed to change gid" mkdir -p /usr/local/nginx/proxy_temp /usr/local/nginx/client_body_temp - chown -R www-data:www-data /usr/local/nginx - - chown -R www-data:www-data /var/www/html + chown -R nginx:nginx /usr/local/nginx + mkdir -p /var/www/html + chown -R nginx:nginx /var/www/html - mkdir -p /dev/shm/hls - chown -R www-data:www-data /dev/shm/hls - - nginx -g 'daemon off;' \ No newline at end of file + echo "testing nginx config..." + /usr/local/nginx/sbin/nginx -t + + /usr/local/nginx/sbin/nginx -g 'daemon off;' \ No newline at end of file diff --git a/dev/html/.gitkeep b/dev/html/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/dev/html/stat.xsl b/dev/html/stat.xsl deleted file mode 100644 index 207a6dd..0000000 --- a/dev/html/stat.xsl +++ /dev/null @@ -1,62 +0,0 @@ - - - - - { - "server": { - "version": "", - "uptime": "", - "applications": [ - - , - { - "name": "", - "streams": [ - - , - { - "name": "", - "time": "", - "bw_in": "", - "bw_out": "", - "bytes_in": "", - "bytes_out": "", - "nclients": "", - "publishing": - - true - false - - , - "active": - - true - false - - , - "clients": [ - - , - { - "id": "", - "address": "", - "time": "", - "flashver": "", - "publishing": - - true - false - - } - - ] - } - - ] - } - - ] - } - } - - \ No newline at end of file diff --git a/dev/nginx.conf b/dev/nginx.conf index d78c6dd..64dedc6 100644 --- a/dev/nginx.conf +++ b/dev/nginx.conf @@ -1,5 +1,5 @@ events { - worker_connections 1024; # Define the maximum number of simultaneous connections + worker_connections 1024; } rtmp { @@ -9,24 +9,24 @@ rtmp { application live { live on; record off; - + on_publish http://localhost:3000/api/rtmp/publish; } - + application channel-live { live on; record off; - + allow publish 127.0.0.1; deny publish all; - + hls on; hls_type live; hls_path /dev/shm/hls; hls_fragment 2s; hls_playlist_length 10s; hls_cleanup on; - + hls_variant _low BANDWIDTH=500000; hls_variant _mid BANDWIDTH=1000000; hls_variant _hi BANDWIDTH=1500000; @@ -44,28 +44,20 @@ http { tcp_nodelay on; keepalive_timeout 65; + map $http_authorization $is_authorized { + default 0; + $API_AUTH 1; + } + server { listen 8888; location /stat { - if ($request_method = "GET") { - add_header "Access-Control-Allow-Origin" *; + if ($is_authorized = 0) { + return 401 "Unauthorized"; } - rtmp_stat all; - rtmp_stat_stylesheet stat.xsl; - } - location /json { - if ($request_method = "GET") { - add_header "Access-Control-Allow-Origin" *; - } - - add_header Content-Type application/json; - rtmp_stat all; - rtmp_stat_stylesheet stat.xsl; - } - location /stat.xsl { - alias /var/www/html/stat.xsl; + rtmp_stat_format json; } location /hls { diff --git a/flv-module/Dockerfile b/flv-module/Dockerfile new file mode 100644 index 0000000..12d81c43 --- /dev/null +++ b/flv-module/Dockerfile @@ -0,0 +1,56 @@ +FROM alpine:3.19 as builder + +RUN apk add --no-cache \ + build-base \ + pcre-dev \ + zlib-dev \ + openssl-dev \ + wget \ + git && \ + wget http://nginx.org/download/nginx-1.26.3.tar.gz && \ + tar -zxf nginx-1.26.3.tar.gz && \ + git clone https://github.com/winshining/nginx-http-flv-module.git && \ + cd nginx-1.26.3 && \ + ./configure --add-module=../nginx-http-flv-module && \ + make -j$(nproc) && make install && \ + rm -rf /nginx-1.26.3.tar.gz /nginx-1.26.3 /nginx-http-flv-module + +FROM alpine:3.19 + +COPY --from=builder /usr/local/nginx /usr/local/nginx + +# Install runtime dependencies including gettext for envsubst +RUN apk add --no-cache \ + pcre \ + zlib \ + openssl \ + ffmpeg \ + shadow \ + gettext && \ + addgroup -S nginx && \ + adduser -S -D -H -G nginx -s /sbin/nologin nginx && \ + mkdir -p /usr/local/nginx/proxy_temp /usr/local/nginx/client_body_temp && \ + chown -R nginx:nginx /usr/local/nginx + +# Create directory for template files +RUN mkdir -p /etc/nginx/templates + +EXPOSE 80 1935 8888 + +# Create an entrypoint script to handle environment variable substitution +RUN echo '#!/bin/sh \n\ +# Replace environment variables in configuration templates \n\ +for template in /etc/nginx/templates/*.conf.template; do \n\ + if [ -f "$template" ]; then \n\ + output_file="/usr/local/nginx/conf/$(basename $template .template)" \n\ + echo "Processing template: $template -> $output_file" \n\ + envsubst "$(env | awk -F= "{printf \\\"\\\$%s \\\",\\\$1}")" < $template > $output_file \n\ + fi \n\ +done \n\ +\n\ +# Start Nginx \n\ +exec "$@"' > /docker-entrypoint.sh && \ +chmod +x /docker-entrypoint.sh + +ENTRYPOINT ["/docker-entrypoint.sh"] +CMD ["/usr/local/nginx/sbin/nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/package.json b/package.json index ea49e47..aa737cc 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@radix-ui/react-tooltip": "^1.1.6", "@uidotdev/usehooks": "^2.4.1", "arctic": "^3.1.1", + "cheerio": "^1.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.0", "cmdk": "1.0.0", diff --git a/src/app/(protected)/api/rtmp/publish/route.ts b/src/app/(protected)/api/rtmp/publish/route.ts index 7dc31d7..7c9bad2 100644 --- a/src/app/(protected)/api/rtmp/publish/route.ts +++ b/src/app/(protected)/api/rtmp/publish/route.ts @@ -1,11 +1,9 @@ import prisma from '@/lib/db'; import { NextRequest } from 'next/server'; -import { redirect } from 'next/navigation'; export async function POST(request: NextRequest) { const formData = await request.formData(); const streamKey = formData.get('name')?.toString() || ''; - console.log('streamKey:', streamKey); const key = await prisma.streamKey.findFirst({ where: { diff --git a/src/app/(protected)/api/stream/chat/route.ts b/src/app/(protected)/api/stream/chat/route.ts index d9558a9..89b4e7b 100644 --- a/src/app/(protected)/api/stream/chat/route.ts +++ b/src/app/(protected)/api/stream/chat/route.ts @@ -6,7 +6,6 @@ export async function SOCKET( request: import('http').IncomingMessage, server: import('ws').WebSocketServer ) { - console.log('A client connected'); const cookies = parseCookieString(request.headers.cookie!); const { user } = await lucia.validateSession(cookies.auth_session); if (!user) { @@ -49,10 +48,6 @@ export async function SOCKET( } }); }); - - client.on('close', () => { - console.log('A client disconnected'); - }); } function parseCookieString(cookie: string) { diff --git a/src/components/app/ChatPanel/ChatPanel.tsx b/src/components/app/ChatPanel/ChatPanel.tsx index 4f654e7..74dfc4f 100644 --- a/src/components/app/ChatPanel/ChatPanel.tsx +++ b/src/components/app/ChatPanel/ChatPanel.tsx @@ -22,7 +22,6 @@ export default function ChatPanel() { const scrollRef = useRef(null); const socketRef = useRef(null); - // Setup WebSocket connection useEffect(() => { const socket = new WebSocket('ws://localhost:3000/api/stream/chat'); socketRef.current = socket; @@ -36,26 +35,19 @@ export default function ChatPanel() { const data = JSON.parse(event.data); setChatMessages(prev => [...prev, data]); } catch (e) { - // Handle plaintext responses (when sending messages) console.log('Received message confirmation:', event.data); } }; - socket.onerror = (error) => { - console.error('WebSocket error:', error); - }; - socket.onclose = () => { console.log('WebSocket closed'); }; - // Cleanup WebSocket on unmount return () => { socket.close(); }; }, []); - // Auto scroll to bottom when messages change useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; @@ -65,16 +57,13 @@ export default function ChatPanel() { } }, [chatMessages]); - // Function to send a message const sendMessage = () => { if (!message.trim()) return; - // Use existing socket connection if available, otherwise create a new one if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) { socketRef.current.send(message); setMessage(''); } else { - // Fallback to creating a new connection const socket = new WebSocket('ws://localhost:3000/api/stream/chat'); socket.onopen = () => { socket.send(message); diff --git a/src/components/app/Livestream/Livestream.tsx b/src/components/app/Livestream/Livestream.tsx index 5ce6c62..22d0a79 100644 --- a/src/components/app/Livestream/Livestream.tsx +++ b/src/components/app/Livestream/Livestream.tsx @@ -1,6 +1,5 @@ 'use client'; -import { useState } from 'react'; import StreamPlayer from '../StreamPlayer/StreamPlayer'; import UserInfoCard from '../UserInfoCard/UserInfoCard'; import ChatPanel from '../ChatPanel/ChatPanel'; diff --git a/src/components/app/StreamPlayer/StreamPlayer.livekit.tsx b/src/components/app/StreamPlayer/StreamPlayer.livekit.tsx deleted file mode 100644 index 6bfe6a5..0000000 --- a/src/components/app/StreamPlayer/StreamPlayer.livekit.tsx +++ /dev/null @@ -1,168 +0,0 @@ -'use client'; - -import useFullscreen from '@/lib/hooks/useFullscreen'; -import { - useTracks, - useParticipants, - useConnectionState, - TrackRefContext, - VideoTrack, - StartAudio, - AudioTrack, -} from '@livekit/components-react'; -import { getTrackReferenceId } from '@livekit/components-core'; -import { Track } from 'livekit-client'; -import { LoaderCircleIcon, Minimize, Maximize, VolumeX, Volume2 } from 'lucide-react'; -import { useState, useRef, useEffect } from 'react'; - -export default function StreamPlayer() { - const [volume, setVolume] = useState(1); - const [isMuted, setIsMuted] = useState(false); - const handleVolumeChange = (newVolume: number, muted: boolean) => { - setVolume(newVolume); - setIsMuted(muted); - }; - const containerRef = useRef(null); - const { isFullscreen, toggleFullscreen } = useFullscreen(containerRef); - - const tracks = useTracks([ - Track.Source.Camera, - Track.Source.Microphone, - Track.Source.ScreenShare, - Track.Source.ScreenShareAudio, - ]); - const participants = useParticipants(); - const connectionState = useConnectionState(); - const [isConnecting, setIsConnecting] = useState(true); - - const broadcasterTracks = tracks.filter((track) => track.participant.identity === 'streamer'); - const audioTracks = broadcasterTracks.filter(track => - track.publication.kind === "audio" || - track.source === Track.Source.Microphone || - track.source === Track.Source.ScreenShareAudio - ); - - // very hacky but works - useEffect(() => { - if (connectionState === 'connected') { - const timer = setTimeout(() => setIsConnecting(false), 2000); - return () => clearTimeout(timer); - } - }, [connectionState]); - useEffect(() => { - console.log('participants', participants); - }, [participants]); - - if (connectionState === 'connecting' || isConnecting) { - return ( -
-
- -

Connecting to stream...

-
-
- ); - } - if (connectionState === 'disconnected') { - return ( -
-

Connection lost. Trying to reconnect...

-
- ); - } - - if (!broadcasterTracks.length) { - return ( -
-

Stream is currently offline

-
- ); - } - - const trackRef = broadcasterTracks[0]; - - return ( -
- - - - {audioTracks.map((trackRef) => ( - - ))} - - - {/* controls */} -
- -
- -
-
- ); -} - -function VolumeControl({ - onChange, - initialVolume = 1, - initialMuted = false, -}: { - onChange: (volume: number, muted: boolean) => void; - initialVolume?: number; - initialMuted?: boolean; -}) { - const [volume, setVolume] = useState(initialVolume); - const [isMuted, setIsMuted] = useState(initialMuted); - const [showVolume, setShowVolume] = useState(false); - - const handleVolumeChange = (newVolume: number) => { - setVolume(newVolume); - onChange(newVolume, newVolume === 0); - }; - - const toggleMute = () => { - setIsMuted(!isMuted); - onChange(volume, !isMuted); - }; - - return ( -
setShowVolume(true)} - onMouseLeave={() => setShowVolume(false)} - > - - -
- handleVolumeChange(parseFloat(e.target.value))} - className="w-full h-1 bg-gray-600 rounded-lg appearance-none cursor-pointer" - /> -
-
- ); -} diff --git a/src/components/app/StreamPlayer/StreamPlayer.tsx b/src/components/app/StreamPlayer/StreamPlayer.tsx index 184fd03..5fa1378 100644 --- a/src/components/app/StreamPlayer/StreamPlayer.tsx +++ b/src/components/app/StreamPlayer/StreamPlayer.tsx @@ -24,6 +24,18 @@ export default function StreamPlayer() { slot="media" crossOrigin="anonymous" autoplay + config={{ + lowLatencyMode: true, + liveSyncDurationCount: 2, // Use only 1 segment for sync + liveMaxLatencyDurationCount: 3, // Maximum latency allowed + liveDurationInfinity: true, + enableWorker: true, + backBufferLength: 0, // No back buffer + startLevel: -1, // Auto level selection + maxBufferLength: 4, // Maximum buffer length in seconds + maxMaxBufferLength: 6, + debug: false, + }} /> diff --git a/src/instrumentation.ts b/src/instrumentation.ts index 19baeff..24cfe15 100644 --- a/src/instrumentation.ts +++ b/src/instrumentation.ts @@ -1,5 +1,5 @@ export async function register() { if (process.env.NEXT_RUNTIME === 'nodejs') { - // await (await import('@/lib/instrumentation/streamInfo')).default(); + //await (await import('@/lib/instrumentation/streamInfo')).default(); } } \ No newline at end of file diff --git a/src/lib/instrumentation/streamInfo.ts b/src/lib/instrumentation/streamInfo.ts index 6f401d5..ffd2da7 100644 --- a/src/lib/instrumentation/streamInfo.ts +++ b/src/lib/instrumentation/streamInfo.ts @@ -1,9 +1,9 @@ -import prisma from "@/lib/db"; -import { roomService } from "@/lib/services/livekit"; +import prisma from '@/lib/db'; +import { roomService } from '@/lib/services/livekit'; export default async function runner() { // if there are no users it explodes so yeah - if (await prisma.user.count() === 0) { + if ((await prisma.user.count()) === 0) { return; } await initializeStreamInfo(); @@ -14,11 +14,11 @@ export default async function runner() { export async function initializeStreamInfo(channelId?: string) { const channels = await prisma.channel.findMany({ where: { - id: channelId + id: channelId, }, include: { - streamInfo: true - } + streamInfo: true, + }, }); for (const channel of channels) { @@ -33,60 +33,58 @@ export async function initializeStreamInfo(channelId?: string) { viewers: 0, isLive: false, channel: { - connect: { id: channel.id } + connect: { id: channel.id }, }, ownedBy: { - connect: { id: channel.ownerId } - } - } + connect: { id: channel.ownerId }, + }, + }, }); } } } export async function syncStream() { - try { - // get all active rooms - const rooms = await roomService.listRooms(); - - // process each room - for (const room of rooms) { - const isLive = room.numPublishers >= 1; + // get all active rooms + const rooms = await roomService.listRooms(); - const originalStreamInfo = await prisma.streamInfo.findUnique({ - where: { username: room.name } - }); - - // upsert stream info - await prisma.streamInfo.upsert({ - where: { - username: room.name + // process each room + for (const room of rooms) { + const isLive = room.numPublishers >= 1; + + const originalStreamInfo = await prisma.streamInfo.findUnique({ + where: { username: room.name }, + }); + + // upsert stream info + await prisma.streamInfo.upsert({ + where: { + username: room.name, + }, + create: { + username: room.name, + title: 'Untitled', + category: 'Uncategorized', + startedAt: new Date(), + thumbnail: 'https://picsum.photos/600/400', + viewers: 0, + channel: { + connect: { id: room.name }, }, - create: { - username: room.name, - title: 'Untitled', - category: 'Uncategorized', - startedAt: new Date(), - thumbnail: 'https://picsum.photos/600/400', - viewers: 0, - channel: { - connect: { id: room.name } - }, - isLive, - ownedBy: { - connect: { id: room.name } - } + isLive, + ownedBy: { + connect: { id: room.name }, }, - update: { - isLive, - viewers: room.numParticipants - 1, - startedAt: !isLive ? new Date(0) : - (originalStreamInfo?.isLive ? originalStreamInfo.startedAt : new Date()) - } - }); - } - } catch (error) { - console.error('Error syncing room streams:', error); - throw error; + }, + update: { + isLive, + viewers: room.numParticipants - 1, + startedAt: !isLive + ? new Date(0) + : originalStreamInfo?.isLive + ? originalStreamInfo.startedAt + : new Date(), + }, + }); } } diff --git a/src/lib/types/liveBackendJson.ts b/src/lib/types/liveBackendJson.ts new file mode 100644 index 0000000..ed4c119 --- /dev/null +++ b/src/lib/types/liveBackendJson.ts @@ -0,0 +1,80 @@ +export interface RtmpStat { + nginxt_version: string; + compiler: string; + built: string; + pid: number; + uptime: number; + naccepted: number; + bw_in: number; + bw_out: number; + clients: number; + applications: Application[]; +} + +interface Application { + name: string; + live: LiveStream[]; + hls: HlsStream[]; + dash: DashStream[]; +} + +interface LiveStream { + name: string; + time: number; + bw_in: number; + bytes_in: number; + bw_out: number; + bytes_out: number; + bw_audio: number; + bw_video: number; + clients: Client[]; +} + +interface HlsStream { + name: string; + bw_in: number; + bytes_in: number; + bw_out: number; + bytes_out: number; +} + +interface DashStream { + name: string; + bw_in: number; + bytes_in: number; + bw_out: number; + bytes_out: number; +} + +interface Client { + id: string; + address: string; + time: number; + flashver: string; + dropped: number; + avsync: number; + timestamp: number; + publishing: boolean; + active: boolean; + audio: AudioStream; + video: VideoStream; +} + +interface AudioStream { + codec: string; + profile: string; + level: string; + bw: number; + channels: number; + sample_rate: number; +} + +interface VideoStream { + codec: string; + profile: string; + level: string; + bw: number; + width: number; + height: number; + frame_rate: number; +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 3253f00..82fd56c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1711,6 +1711,11 @@ bl@^5.0.0: inherits "^2.0.4" readable-stream "^3.4.0" +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -1832,6 +1837,35 @@ chalk@^5.0.0: resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.4.1.tgz#1b48bf0963ec158dce2aacf69c093ae2dd2092d8" integrity sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w== +cheerio-select@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4" + integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g== + dependencies: + boolbase "^1.0.0" + css-select "^5.1.0" + css-what "^6.1.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.0.1" + +cheerio@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0.tgz#1ede4895a82f26e8af71009f961a9b8cb60d6a81" + integrity sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww== + dependencies: + cheerio-select "^2.1.0" + dom-serializer "^2.0.0" + domhandler "^5.0.3" + domutils "^3.1.0" + encoding-sniffer "^0.2.0" + htmlparser2 "^9.1.0" + parse5 "^7.1.2" + parse5-htmlparser2-tree-adapter "^7.0.0" + parse5-parser-stream "^7.1.2" + undici "^6.19.5" + whatwg-mimetype "^4.0.0" + chokidar@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" @@ -1961,6 +1995,22 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +css-select@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" + integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== + dependencies: + boolbase "^1.0.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" + nth-check "^2.0.1" + +css-what@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" @@ -2106,6 +2156,36 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + +domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + +domutils@^3.0.1, domutils@^3.1.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.2.2.tgz#edbfe2b668b0c1d97c24baf0f1062b132221bc78" + integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + dunder-proto@^1.0.0, dunder-proto@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" @@ -2135,6 +2215,14 @@ emoji-regex@^9.2.2: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== +encoding-sniffer@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz#799569d66d443babe82af18c9f403498365ef1d5" + integrity sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg== + dependencies: + iconv-lite "^0.6.3" + whatwg-encoding "^3.1.1" + enhanced-resolve@^5.15.0: version "5.18.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz#91eb1db193896b9801251eeff1c6980278b1e404" @@ -2143,6 +2231,11 @@ enhanced-resolve@^5.15.0: graceful-fs "^4.2.4" tapable "^2.2.0" +entities@^4.2.0, entities@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -2864,6 +2957,16 @@ hls.js@^1.5.11: resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-1.5.20.tgz#7eb23bb5e2595311d4e2761038ca6882673de7e2" integrity sha512-uu0VXUK52JhihhnN/MVVo1lvqNNuhoxkonqgO3IpjvQiGpJBdIXMGkofjQb/j9zvV7a1SW8U9g1FslWx/1HOiQ== +htmlparser2@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-9.1.0.tgz#cdb498d8a75a51f739b61d3f718136c369bc8c23" + integrity sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.1.0" + entities "^4.5.0" + https-proxy-agent@^6.2.0: version "6.2.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-6.2.1.tgz#0965ab47371b3e531cf6794d1eb148710a992ba7" @@ -2877,6 +2980,13 @@ human-signals@^4.3.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2" integrity sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ== +iconv-lite@0.6.3, iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -3599,6 +3709,13 @@ npm-run-path@^5.1.0: dependencies: path-key "^4.0.0" +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + object-assign@^4.0.1, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -3762,6 +3879,28 @@ parse-json@^5.2.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" +parse5-htmlparser2-tree-adapter@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz#b5a806548ed893a43e24ccb42fbb78069311e81b" + integrity sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g== + dependencies: + domhandler "^5.0.3" + parse5 "^7.0.0" + +parse5-parser-stream@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz#d7c20eadc37968d272e2c02660fff92dd27e60e1" + integrity sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow== + dependencies: + parse5 "^7.0.0" + +parse5@^7.0.0, parse5@^7.1.2: + version "7.2.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.2.1.tgz#8928f55915e6125f430cc44309765bf17556a33a" + integrity sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ== + dependencies: + entities "^4.5.0" + path-browserify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" @@ -4174,6 +4313,11 @@ safe-regex-test@^1.0.3, safe-regex-test@^1.1.0: es-errors "^1.3.0" is-regex "^1.2.1" +"safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + scheduler@^0.25.0: version "0.25.0" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.25.0.tgz#336cd9768e8cceebf52d3c80e3dcf5de23e7e015" @@ -4796,6 +4940,11 @@ undici-types@~6.20.0: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== +undici@^6.19.5: + version "6.21.2" + resolved "https://registry.yarnpkg.com/undici/-/undici-6.21.2.tgz#49c5884e8f9039c65a89ee9018ef3c8e2f1f4928" + integrity sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g== + universalify@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" @@ -4879,6 +5028,18 @@ webrtc-adapter@^9.0.0: dependencies: sdp "^3.2.0" +whatwg-encoding@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz#d0f4ef769905d426e1688f3e34381a99b60b76e5" + integrity sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ== + dependencies: + iconv-lite "0.6.3" + +whatwg-mimetype@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz#bc1bf94a985dc50388d54a9258ac405c3ca2fc0a" + integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg== + which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e"