mirror of
https://github.com/SrIzan10/hctv.git
synced 2026-06-06 00:56:56 +00:00
feat: move to nginx flv
This commit is contained in:
@@ -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;'
|
||||
echo "testing nginx config..."
|
||||
/usr/local/nginx/sbin/nginx -t
|
||||
|
||||
/usr/local/nginx/sbin/nginx -g 'daemon off;'
|
||||
0
dev/html/.gitkeep
Normal file
0
dev/html/.gitkeep
Normal file
@@ -1,62 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
||||
<xsl:output method="text" encoding="UTF-8" media-type="application/json"/>
|
||||
<xsl:template match="/">
|
||||
<xsl:text>{</xsl:text>
|
||||
<xsl:text>"server": {</xsl:text>
|
||||
<xsl:text>"version": "</xsl:text><xsl:value-of select="rtmp/nginx_version"/><xsl:text>",</xsl:text>
|
||||
<xsl:text>"uptime": "</xsl:text><xsl:value-of select="rtmp/server/uptime"/><xsl:text>",</xsl:text>
|
||||
<xsl:text>"applications": [</xsl:text>
|
||||
<xsl:for-each select="rtmp/server/application">
|
||||
<xsl:if test="position() > 1">,</xsl:if>
|
||||
<xsl:text>{</xsl:text>
|
||||
<xsl:text>"name": "</xsl:text><xsl:value-of select="name"/><xsl:text>",</xsl:text>
|
||||
<xsl:text>"streams": [</xsl:text>
|
||||
<xsl:for-each select="live/stream">
|
||||
<xsl:if test="position() > 1">,</xsl:if>
|
||||
<xsl:text>{</xsl:text>
|
||||
<xsl:text>"name": "</xsl:text><xsl:value-of select="name"/><xsl:text>",</xsl:text>
|
||||
<xsl:text>"time": "</xsl:text><xsl:value-of select="time"/><xsl:text>",</xsl:text>
|
||||
<xsl:text>"bw_in": "</xsl:text><xsl:value-of select="bw_in"/><xsl:text>",</xsl:text>
|
||||
<xsl:text>"bw_out": "</xsl:text><xsl:value-of select="bw_out"/><xsl:text>",</xsl:text>
|
||||
<xsl:text>"bytes_in": "</xsl:text><xsl:value-of select="bytes_in"/><xsl:text>",</xsl:text>
|
||||
<xsl:text>"bytes_out": "</xsl:text><xsl:value-of select="bytes_out"/><xsl:text>",</xsl:text>
|
||||
<xsl:text>"nclients": "</xsl:text><xsl:value-of select="nclients"/><xsl:text>",</xsl:text>
|
||||
<xsl:text>"publishing": </xsl:text>
|
||||
<xsl:choose>
|
||||
<xsl:when test="count(client[publishing]) > 0">true</xsl:when>
|
||||
<xsl:otherwise>false</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
<xsl:text>,</xsl:text>
|
||||
<xsl:text>"active": </xsl:text>
|
||||
<xsl:choose>
|
||||
<xsl:when test="active = 1">true</xsl:when>
|
||||
<xsl:otherwise>false</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
<xsl:text>,</xsl:text>
|
||||
<xsl:text>"clients": [</xsl:text>
|
||||
<xsl:for-each select="client">
|
||||
<xsl:if test="position() > 1">,</xsl:if>
|
||||
<xsl:text>{</xsl:text>
|
||||
<xsl:text>"id": "</xsl:text><xsl:value-of select="id"/><xsl:text>",</xsl:text>
|
||||
<xsl:text>"address": "</xsl:text><xsl:value-of select="address"/><xsl:text>",</xsl:text>
|
||||
<xsl:text>"time": "</xsl:text><xsl:value-of select="time"/><xsl:text>",</xsl:text>
|
||||
<xsl:text>"flashver": "</xsl:text><xsl:value-of select="flashver"/><xsl:text>",</xsl:text>
|
||||
<xsl:text>"publishing": </xsl:text>
|
||||
<xsl:choose>
|
||||
<xsl:when test="publishing">true</xsl:when>
|
||||
<xsl:otherwise>false</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
<xsl:text>}</xsl:text>
|
||||
</xsl:for-each>
|
||||
<xsl:text>]</xsl:text>
|
||||
<xsl:text>}</xsl:text>
|
||||
</xsl:for-each>
|
||||
<xsl:text>]</xsl:text>
|
||||
<xsl:text>}</xsl:text>
|
||||
</xsl:for-each>
|
||||
<xsl:text>]</xsl:text>
|
||||
<xsl:text>}</xsl:text>
|
||||
<xsl:text>}</xsl:text>
|
||||
</xsl:template>
|
||||
</xsl:stylesheet>
|
||||
@@ -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 {
|
||||
|
||||
56
flv-module/Dockerfile
Normal file
56
flv-module/Dockerfile
Normal file
@@ -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;"]
|
||||
@@ -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",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -22,7 +22,6 @@ export default function ChatPanel() {
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const socketRef = useRef<WebSocket | null>(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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<HTMLDivElement>(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 (
|
||||
<div className="w-full aspect-video bg-black flex items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<LoaderCircleIcon size={32} className="animate-spin text-white" />
|
||||
<p className="text-white">Connecting to stream...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (connectionState === 'disconnected') {
|
||||
return (
|
||||
<div className="w-full aspect-video bg-black flex items-center justify-center">
|
||||
<p className="text-white">Connection lost. Trying to reconnect...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!broadcasterTracks.length) {
|
||||
return (
|
||||
<div className="w-full aspect-video bg-black flex items-center justify-center">
|
||||
<p className="text-white">Stream is currently offline</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const trackRef = broadcasterTracks[0];
|
||||
|
||||
return (
|
||||
<div className="w-full aspect-video bg-black relative group" ref={containerRef}>
|
||||
<TrackRefContext.Provider value={trackRef}>
|
||||
<VideoTrack trackRef={trackRef} className="w-full h-full" />
|
||||
<StartAudio
|
||||
label="Click to allow audio playback"
|
||||
className="absolute top-0 h-full w-full bg-gray-2-translucent text-white"
|
||||
/>
|
||||
{audioTracks.map((trackRef) => (
|
||||
<AudioTrack
|
||||
key={getTrackReferenceId(trackRef)}
|
||||
trackRef={trackRef}
|
||||
volume={volume}
|
||||
muted={isMuted}
|
||||
/>
|
||||
))}
|
||||
</TrackRefContext.Provider>
|
||||
|
||||
{/* controls */}
|
||||
<div className="absolute flex bottom-0 left-0 right-0 bg-black bg-opacity-50 text-white p-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
||||
<VolumeControl
|
||||
onChange={handleVolumeChange}
|
||||
initialVolume={volume}
|
||||
initialMuted={isMuted}
|
||||
/>
|
||||
<div className="flex-1" />
|
||||
<button onClick={toggleFullscreen} className="hover:text-primary transition-colors">
|
||||
{isFullscreen ? <Minimize size={20} /> : <Maximize size={20} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div
|
||||
className="relative flex items-center gap-2"
|
||||
onMouseEnter={() => setShowVolume(true)}
|
||||
onMouseLeave={() => setShowVolume(false)}
|
||||
>
|
||||
<button onClick={toggleMute} className="hover:text-primary transition-colors">
|
||||
{isMuted || volume === 0 ? <VolumeX size={20} /> : <Volume2 size={20} />}
|
||||
</button>
|
||||
|
||||
<div
|
||||
className={`flex items-center transition-all duration-200 ${
|
||||
showVolume ? 'w-24 opacity-100' : 'w-0 opacity-0'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={isMuted ? 0 : volume}
|
||||
onChange={(e) => handleVolumeChange(parseFloat(e.target.value))}
|
||||
className="w-full h-1 bg-gray-600 rounded-lg appearance-none cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
<MediaLoadingIndicator slot="centered-chrome" noAutohide />
|
||||
<MediaControlBar className='w-full px-2'>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
80
src/lib/types/liveBackendJson.ts
Normal file
80
src/lib/types/liveBackendJson.ts
Normal file
@@ -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;
|
||||
}
|
||||
161
yarn.lock
161
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"
|
||||
|
||||
Reference in New Issue
Block a user