init
This commit is contained in:
19
.dockerignore
Normal file
19
.dockerignore
Normal file
@@ -0,0 +1,19 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
.git
|
||||
.gitignore
|
||||
README.md
|
||||
.env
|
||||
.env.local
|
||||
.DS_Store
|
||||
.vscode
|
||||
.svelte-kit
|
||||
build
|
||||
dist
|
||||
.next
|
||||
coverage
|
||||
.turbo
|
||||
timelapse
|
||||
.npmrc
|
||||
bun.lock.backup
|
||||
*.log
|
||||
5
.env.example
Normal file
5
.env.example
Normal file
@@ -0,0 +1,5 @@
|
||||
# Timelapse Configuration
|
||||
# Path where hourly timelapse images will be stored
|
||||
# Default: ./timelapse (relative to project root)
|
||||
# Example: /mnt/storage/timelapse or ./timelapse
|
||||
TIMELAPSE_OUTPUT_PATH=./timelapse
|
||||
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
node_modules
|
||||
|
||||
# Output
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
.wrangler
|
||||
/.svelte-kit
|
||||
/build
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.test
|
||||
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
6
.vscode/extensions.json
vendored
Normal file
6
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"svelte.svelte-vscode",
|
||||
"bradlc.vscode-tailwindcss"
|
||||
]
|
||||
}
|
||||
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"*.css": "tailwindcss"
|
||||
}
|
||||
}
|
||||
46
Dockerfile
Normal file
46
Dockerfile
Normal file
@@ -0,0 +1,46 @@
|
||||
# Build stage
|
||||
FROM oven/bun:latest AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install ffmpeg and other system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
ffmpeg \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy package files
|
||||
COPY package.json bun.lock ./
|
||||
|
||||
# Install dependencies with bun
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN bun run build
|
||||
|
||||
# Runtime stage
|
||||
FROM oven/bun:latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install ffmpeg in runtime image as well
|
||||
RUN apt-get update && apt-get install -y \
|
||||
ffmpeg \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy package files
|
||||
COPY package.json bun.lock ./
|
||||
|
||||
# Install dependencies (production only)
|
||||
RUN bun install --frozen-lockfile --production
|
||||
|
||||
# Copy built application from builder
|
||||
COPY --from=builder /app/build ./build
|
||||
|
||||
# Expose port 3000
|
||||
EXPOSE 3000
|
||||
|
||||
# Start the application
|
||||
CMD ["bun", "--bun", "run", "build/index.js"]
|
||||
223
QUICK_START.md
Normal file
223
QUICK_START.md
Normal file
@@ -0,0 +1,223 @@
|
||||
# RTSP to HLS Streaming - Quick Start Guide
|
||||
|
||||
## Installation (5 minutes)
|
||||
|
||||
### 1. Install FFmpeg
|
||||
```bash
|
||||
# macOS
|
||||
brew install ffmpeg
|
||||
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get install ffmpeg
|
||||
|
||||
# Windows (Chocolatey)
|
||||
choco install ffmpeg
|
||||
```
|
||||
|
||||
### 2. Install Dependencies
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 3. Start Development Server
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Visit `http://localhost:5173`
|
||||
|
||||
## Usage
|
||||
|
||||
1. Enter a **Stream ID** (e.g., "camera-1")
|
||||
2. Enter an **RTSP URL** (e.g., "rtsp://192.168.1.100:554/stream")
|
||||
3. Click **Start Stream**
|
||||
4. Watch video play in the browser
|
||||
5. Click **Stop Stream** to terminate
|
||||
|
||||
## What Was Set Up
|
||||
|
||||
### Backend Components
|
||||
- **`src/lib/server/streaming.ts`** - FFmpeg process manager
|
||||
- Spawns FFmpeg to convert RTSP → HLS
|
||||
- Manages stream lifecycle
|
||||
- Handles errors and cleanup
|
||||
|
||||
- **`src/routes/api/stream/+server.ts`** - REST API
|
||||
- `/api/stream` POST endpoint
|
||||
- Actions: start, stop, status, list
|
||||
|
||||
### Frontend Components
|
||||
- **`src/lib/components/RTSPVideoPlayer.svelte`** - Video player
|
||||
- HLS.js library for playback
|
||||
- Input forms for Stream ID and RTSP URL
|
||||
- Stream controls and status display
|
||||
|
||||
### Static Files
|
||||
- **`static/hls/`** - HLS segments (auto-created)
|
||||
- Stores `.m3u8` playlists
|
||||
- Stores `.ts` segment files
|
||||
- Automatically cleaned up by FFmpeg
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
RTSP Stream (Camera)
|
||||
↓
|
||||
FFmpeg (spawned process on server)
|
||||
Converts to HLS segments
|
||||
↓
|
||||
HLS.js (browser)
|
||||
Parses playlist and segments
|
||||
↓
|
||||
HTML5 Video Player
|
||||
Displays video in browser
|
||||
```
|
||||
|
||||
## API Examples
|
||||
|
||||
### Start Stream
|
||||
```bash
|
||||
curl -X POST http://localhost:5173/api/stream \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"action": "start",
|
||||
"streamId": "camera-1",
|
||||
"rtspUrl": "rtsp://192.168.1.100:554/stream"
|
||||
}'
|
||||
```
|
||||
|
||||
### Stop Stream
|
||||
```bash
|
||||
curl -X POST http://localhost:5173/api/stream \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"action": "stop",
|
||||
"streamId": "camera-1"
|
||||
}'
|
||||
```
|
||||
|
||||
### Get Status
|
||||
```bash
|
||||
curl -X POST http://localhost:5173/api/stream \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"action": "status", "streamId": "camera-1"}'
|
||||
```
|
||||
|
||||
### List All Streams
|
||||
```bash
|
||||
curl -X POST http://localhost:5173/api/stream \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"action": "list"}'
|
||||
```
|
||||
|
||||
## Common RTSP URLs
|
||||
|
||||
| Camera Brand | URL Pattern |
|
||||
|---|---|
|
||||
| Hikvision | `rtsp://admin:password@IP:554/stream` |
|
||||
| Dahua | `rtsp://admin:password@IP:554/live` |
|
||||
| Generic | `rtsp://user:password@IP:554/stream` |
|
||||
| Reolink | `rtsp://user:password@IP:554/h264Preview_01_main` |
|
||||
| Ubiquiti | `rtsp://user:password@IP:554/live1` |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---|---|
|
||||
| FFmpeg not found | Install FFmpeg and add to PATH |
|
||||
| Stream won't connect | Verify RTSP URL with VLC first |
|
||||
| No video playback | Check browser console for HLS.js errors |
|
||||
| High CPU usage | Use `-preset ultrafast` in streaming.ts |
|
||||
| High latency | Reduce `-hls_time` from 10 to 2-5 seconds |
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
grown/
|
||||
├── src/
|
||||
│ ├── lib/
|
||||
│ │ ├── server/
|
||||
│ │ │ └── streaming.ts ← FFmpeg manager
|
||||
│ │ └── components/
|
||||
│ │ └── RTSPVideoPlayer.svelte ← Video player UI
|
||||
│ └── routes/
|
||||
│ ├── +page.svelte ← Home page
|
||||
│ └── api/stream/
|
||||
│ └── +server.ts ← Stream API
|
||||
├── static/
|
||||
│ └── hls/ ← HLS segments (auto-created)
|
||||
├── package.json
|
||||
├── svelte.config.js
|
||||
└── vite.config.ts
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit `src/lib/server/streaming.ts` to adjust FFmpeg parameters:
|
||||
|
||||
### Lower Latency (for real-time apps)
|
||||
```typescript
|
||||
'-hls_time', '2', // 2-second segments
|
||||
'-hls_list_size', '5', // Keep 5 segments
|
||||
'-preset', 'ultrafast', // Fastest encoding
|
||||
```
|
||||
|
||||
### Higher Quality
|
||||
```typescript
|
||||
'-crf', '23', // Better quality
|
||||
'-b:v', '2500k', // Higher video bitrate
|
||||
'-b:a', '192k', // Higher audio bitrate
|
||||
```
|
||||
|
||||
### GPU Acceleration (NVIDIA)
|
||||
```typescript
|
||||
'-c:v', 'h264_nvenc', // GPU encoder
|
||||
'-preset', 'fast', // fast, medium, slow
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Test with a Public RTSP Stream
|
||||
```
|
||||
rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov
|
||||
```
|
||||
|
||||
### Test with VLC Player
|
||||
```bash
|
||||
# Verify RTSP URL works before using in the app
|
||||
vlc rtsp://192.168.1.100:554/stream
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Deploy**: Use Docker or your preferred hosting
|
||||
2. **Secure**: Add authentication to `/api/stream` endpoints
|
||||
3. **Monitor**: Log stream statistics and errors
|
||||
4. **Scale**: Implement load balancing for multiple cameras
|
||||
5. **Optimize**: Adjust FFmpeg parameters for your use case
|
||||
|
||||
## Documentation
|
||||
|
||||
- Full setup guide: `RTSP_STREAMING_SETUP.md`
|
||||
- FFmpeg docs: https://ffmpeg.org/documentation.html
|
||||
- HLS.js docs: https://github.com/video-dev/hls.js/wiki
|
||||
- SvelteKit docs: https://kit.svelte.dev
|
||||
|
||||
## Browser Support
|
||||
|
||||
✓ Chrome/Edge (HLS.js)
|
||||
✓ Firefox (HLS.js)
|
||||
✓ Safari (Native HLS)
|
||||
✓ Mobile browsers (Full support)
|
||||
|
||||
## Need Help?
|
||||
|
||||
1. Check the browser console for errors
|
||||
2. Check terminal output for FFmpeg logs
|
||||
3. Verify RTSP URL is correct
|
||||
4. Ensure FFmpeg is installed and in PATH
|
||||
5. Check network connectivity to camera
|
||||
|
||||
---
|
||||
|
||||
**You're all set!** Start the dev server and visit `http://localhost:5173`
|
||||
42
README.md
Normal file
42
README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# sv
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```sh
|
||||
# create a new project
|
||||
npx sv create my-app
|
||||
```
|
||||
|
||||
To recreate this project with the same configuration:
|
||||
|
||||
```sh
|
||||
# recreate this project
|
||||
bun x sv@0.12.5 create --template minimal --types ts --add tailwindcss="plugins:none" --install bun grown
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
||||
546
RTSP_STREAMING_SETUP.md
Normal file
546
RTSP_STREAMING_SETUP.md
Normal file
@@ -0,0 +1,546 @@
|
||||
# RTSP to HLS Streaming in SvelteKit
|
||||
|
||||
This guide walks you through setting up real-time RTSP stream conversion to HLS format in your SvelteKit application.
|
||||
|
||||
## What's Included
|
||||
|
||||
- **Backend**: FFmpeg-based RTSP to HLS converter running on Node.js
|
||||
- **Frontend**: SvelteKit component with HLS.js video player
|
||||
- **API**: RESTful endpoints to control streams
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### System Requirements
|
||||
- **Node.js**: 18 or higher
|
||||
- **FFmpeg**: Must be installed and in your PATH
|
||||
- **Package Manager**: npm or bun
|
||||
|
||||
### Install FFmpeg
|
||||
|
||||
#### macOS
|
||||
```bash
|
||||
brew install ffmpeg
|
||||
```
|
||||
|
||||
#### Ubuntu/Debian
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install ffmpeg
|
||||
```
|
||||
|
||||
#### Windows (Chocolatey)
|
||||
```bash
|
||||
choco install ffmpeg
|
||||
```
|
||||
|
||||
#### Windows (Scoop)
|
||||
```bash
|
||||
scoop install ffmpeg
|
||||
```
|
||||
|
||||
#### Verify Installation
|
||||
```bash
|
||||
ffmpeg -version
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Install Dependencies
|
||||
```bash
|
||||
npm install
|
||||
# or with bun
|
||||
bun install
|
||||
```
|
||||
|
||||
This installs `hls.js` for browser-based HLS playback.
|
||||
|
||||
### 2. Project Structure
|
||||
|
||||
The setup creates the following files:
|
||||
|
||||
```
|
||||
src/
|
||||
├── lib/
|
||||
│ ├── server/
|
||||
│ │ └── streaming.ts # FFmpeg stream manager
|
||||
│ └── components/
|
||||
│ └── RTSPVideoPlayer.svelte # Video player component
|
||||
├── routes/
|
||||
│ ├── +page.svelte # Home page
|
||||
│ └── api/
|
||||
│ └── stream/
|
||||
│ └── +server.ts # Stream API endpoints
|
||||
└── ...
|
||||
|
||||
static/
|
||||
└── hls/ # HLS playlists & segments (auto-created)
|
||||
```
|
||||
|
||||
### 3. Run Development Server
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open `http://localhost:5173` in your browser.
|
||||
|
||||
## Usage
|
||||
|
||||
### Web Interface
|
||||
|
||||
1. **Stream ID**: Enter a unique identifier (e.g., "camera-1")
|
||||
2. **RTSP URL**: Enter your camera's RTSP stream URL
|
||||
3. **Start Stream**: Click to begin conversion and playback
|
||||
4. **Stop Stream**: Click to terminate the stream
|
||||
|
||||
### Common RTSP URLs
|
||||
|
||||
**Hikvision Cameras**
|
||||
```
|
||||
rtsp://admin:password@192.168.1.100:554/stream
|
||||
rtsp://admin:password@192.168.1.100:554/Streaming/Channels/101
|
||||
```
|
||||
|
||||
**Dahua Cameras**
|
||||
```
|
||||
rtsp://admin:password@192.168.1.100:554/live
|
||||
```
|
||||
|
||||
**Generic IP Cameras**
|
||||
```
|
||||
rtsp://user:password@camera-ip:554/stream
|
||||
rtsp://camera-ip:554/stream1
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Start Stream
|
||||
|
||||
**Request:**
|
||||
```bash
|
||||
curl -X POST http://localhost:5173/api/stream \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"action": "start",
|
||||
"streamId": "camera-1",
|
||||
"rtspUrl": "rtsp://192.168.1.100:554/stream"
|
||||
}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"playlistUrl": "/hls/camera-1.m3u8"
|
||||
}
|
||||
```
|
||||
|
||||
### Stop Stream
|
||||
|
||||
**Request:**
|
||||
```bash
|
||||
curl -X POST http://localhost:5173/api/stream \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"action": "stop",
|
||||
"streamId": "camera-1"
|
||||
}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
### Get Stream Status
|
||||
|
||||
**Request:**
|
||||
```bash
|
||||
curl -X POST http://localhost:5173/api/stream \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"action": "status",
|
||||
"streamId": "camera-1"
|
||||
}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"streamId": "camera-1",
|
||||
"rtspUrl": "rtsp://192.168.1.100:554/stream",
|
||||
"startedAt": "2024-01-15T10:30:45.123Z",
|
||||
"isRunning": true,
|
||||
"playlistUrl": "/hls/camera-1.m3u8"
|
||||
}
|
||||
```
|
||||
|
||||
### List All Streams
|
||||
|
||||
**Request:**
|
||||
```bash
|
||||
curl -X POST http://localhost:5173/api/stream \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"action": "list"}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"streams": [
|
||||
{
|
||||
"streamId": "camera-1",
|
||||
"rtspUrl": "rtsp://192.168.1.100:554/stream",
|
||||
"startedAt": "2024-01-15T10:30:45.123Z",
|
||||
"isRunning": true,
|
||||
"playlistUrl": "/hls/camera-1.m3u8"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### FFmpeg Parameters
|
||||
|
||||
Edit `src/lib/server/streaming.ts` to adjust encoding parameters:
|
||||
|
||||
```typescript
|
||||
// Current defaults
|
||||
'-hls_time', '10', // Segment duration (seconds)
|
||||
'-hls_list_size', '3', // Number of segments to keep
|
||||
'-preset', 'fast', // Encoding speed
|
||||
'-b:a', '128k', // Audio bitrate
|
||||
```
|
||||
|
||||
### Low Latency Setup
|
||||
|
||||
For real-time applications, modify the FFmpeg arguments:
|
||||
|
||||
```typescript
|
||||
'-hls_time', '2', // 2-second segments
|
||||
'-hls_list_size', '5', // Keep more segments
|
||||
'-preset', 'ultrafast', // Fastest encoding
|
||||
'-flags', '+low_delay', // Low-delay mode
|
||||
```
|
||||
|
||||
### High Quality Setup
|
||||
|
||||
```typescript
|
||||
'-crf', '23', // Quality (0-51, lower=better)
|
||||
'-b:v', '2500k', // Video bitrate
|
||||
'-c:a', 'aac',
|
||||
'-b:a', '192k', // Higher audio quality
|
||||
```
|
||||
|
||||
### GPU Acceleration (NVIDIA)
|
||||
|
||||
```typescript
|
||||
'-c:v', 'h264_nvenc', // NVIDIA encoder
|
||||
'-preset', 'fast', // fast, medium, slow
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "ffmpeg: command not found"
|
||||
|
||||
FFmpeg is not installed or not in your system PATH.
|
||||
|
||||
**Solution:** Reinstall FFmpeg and ensure it's in your PATH, then restart your terminal.
|
||||
|
||||
```bash
|
||||
# Verify FFmpeg is accessible
|
||||
which ffmpeg
|
||||
ffmpeg -version
|
||||
```
|
||||
|
||||
### Stream won't connect
|
||||
|
||||
Check the following:
|
||||
|
||||
1. **RTSP URL is correct**: Test with VLC player first
|
||||
2. **Network connectivity**: Ping the camera IP
|
||||
3. **Firewall rules**: Ensure port 554 (default RTSP) is open
|
||||
4. **Camera credentials**: Verify username/password in URL
|
||||
5. **FFmpeg logs**: Check browser console and terminal output
|
||||
|
||||
### "Playlist not found" Error
|
||||
|
||||
This usually means FFmpeg hasn't created the HLS segments yet.
|
||||
|
||||
**Solution:** Increase the wait time in the component:
|
||||
|
||||
```typescript
|
||||
// In RTSPVideoPlayer.svelte, startStream function
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000)); // Increase from 1000 to 3000
|
||||
```
|
||||
|
||||
### Video won't play in Safari
|
||||
|
||||
HLS.js may have issues with some configurations.
|
||||
|
||||
**Solution:** Check that the HLS.js library is loaded:
|
||||
|
||||
```typescript
|
||||
// Verify HLS.js is available
|
||||
if (typeof window !== 'undefined' && !(window as any).HLS) {
|
||||
console.error('HLS.js not loaded');
|
||||
}
|
||||
```
|
||||
|
||||
### High CPU Usage
|
||||
|
||||
The FFmpeg process is using too many resources.
|
||||
|
||||
**Solution:** Use a faster preset or reduce resolution:
|
||||
|
||||
```typescript
|
||||
// Use ultrafast preset
|
||||
'-preset', 'ultrafast',
|
||||
|
||||
// Or reduce resolution
|
||||
'-vf', 'scale=1280:720',
|
||||
```
|
||||
|
||||
### High Latency / Buffering
|
||||
|
||||
Segments are taking too long to generate or playback is laggy.
|
||||
|
||||
**Solutions:**
|
||||
1. Reduce segment duration to 2-5 seconds
|
||||
2. Enable low-latency mode
|
||||
3. Check network bandwidth
|
||||
4. Reduce video resolution
|
||||
5. Close other CPU-intensive applications
|
||||
|
||||
## Browser Support
|
||||
|
||||
| Browser | HLS Support | Notes |
|
||||
|---------|-------------|-------|
|
||||
| Chrome | ✓ HLS.js | Full support via HLS.js library |
|
||||
| Firefox | ✓ HLS.js | Full support via HLS.js library |
|
||||
| Safari | ✓ Native | Native HLS support |
|
||||
| Edge | ✓ HLS.js | Chromium-based, full support |
|
||||
| Mobile Chrome | ✓ HLS.js | Full support |
|
||||
| Mobile Safari | ✓ Native | Native HLS support |
|
||||
| Opera | ✓ HLS.js | Full support |
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### 1. Environment Variables for Credentials
|
||||
|
||||
Never hardcode camera credentials in your code.
|
||||
|
||||
```typescript
|
||||
// Load from environment
|
||||
const rtspUrl = `rtsp://${process.env.CAMERA_USER}:${process.env.CAMERA_PASS}@${process.env.CAMERA_IP}:554/stream`;
|
||||
```
|
||||
|
||||
Create a `.env.local` file:
|
||||
```
|
||||
CAMERA_USER=admin
|
||||
CAMERA_PASS=password
|
||||
CAMERA_IP=192.168.1.100
|
||||
```
|
||||
|
||||
### 2. Restrict API Access
|
||||
|
||||
Implement authentication on the `/api/stream` endpoint:
|
||||
|
||||
```typescript
|
||||
// src/routes/api/stream/+server.ts
|
||||
export async function POST({ request, locals }) {
|
||||
// Check authentication
|
||||
if (!locals.user) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Continue with stream logic
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Network Security
|
||||
|
||||
- Use HTTPS in production
|
||||
- Restrict camera access to internal network only
|
||||
- Use VPN for remote access
|
||||
- Implement IP whitelisting
|
||||
|
||||
### 4. Process Management
|
||||
|
||||
FFmpeg runs with server privileges. Ensure:
|
||||
|
||||
- Minimal file system access
|
||||
- Process limits to prevent DoS
|
||||
- Regular monitoring and logging
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### 1. Adaptive Bitrate
|
||||
|
||||
Implement multiple quality levels:
|
||||
|
||||
```typescript
|
||||
// Start multiple streams at different resolutions
|
||||
const streams = [
|
||||
{ id: 'high', resolution: '1920:1080', bitrate: '5000k' },
|
||||
{ id: 'medium', resolution: '1280:720', bitrate: '2500k' },
|
||||
{ id: 'low', resolution: '640:360', bitrate: '1000k' }
|
||||
];
|
||||
```
|
||||
|
||||
### 2. Connection Pooling
|
||||
|
||||
For multiple concurrent streams, optimize memory usage:
|
||||
|
||||
```typescript
|
||||
// Limit concurrent streams
|
||||
const MAX_STREAMS = 5;
|
||||
if (activeStreams.size >= MAX_STREAMS) {
|
||||
return { error: 'Too many concurrent streams' };
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Caching
|
||||
|
||||
Cache HLS segments on a CDN for better performance.
|
||||
|
||||
### 4. Hardware Acceleration
|
||||
|
||||
Use GPU encoding when available:
|
||||
|
||||
- NVIDIA: `h264_nvenc`
|
||||
- Intel: `h264_qsv`
|
||||
- AMD: `h264_amf`
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### Docker
|
||||
|
||||
Create `Dockerfile`:
|
||||
|
||||
```dockerfile
|
||||
FROM node:18-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y ffmpeg && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci --only=production
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "build/index.js"]
|
||||
```
|
||||
|
||||
Build and run:
|
||||
|
||||
```bash
|
||||
docker build -t rtsp-hls-app .
|
||||
docker run -p 3000:3000 \
|
||||
-e CAMERA_USER=admin \
|
||||
-e CAMERA_PASS=password \
|
||||
-e CAMERA_IP=192.168.1.100 \
|
||||
rtsp-hls-app
|
||||
```
|
||||
|
||||
### Docker Compose
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
CAMERA_USER: admin
|
||||
CAMERA_PASS: password
|
||||
CAMERA_IP: 192.168.1.100
|
||||
volumes:
|
||||
- ./static/hls:/app/static/hls
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
### Nginx Reverse Proxy
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name stream.example.com;
|
||||
|
||||
ssl_certificate /etc/ssl/certs/cert.pem;
|
||||
ssl_certificate_key /etc/ssl/private/key.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
location /hls/ {
|
||||
alias /app/static/hls/;
|
||||
expires 1h;
|
||||
add_header Cache-Control "public, max-age=3600";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Topics
|
||||
|
||||
### Custom Video Filters
|
||||
|
||||
Add effects or transformations:
|
||||
|
||||
```typescript
|
||||
'-vf', 'scale=1280:720,fps=30,format=yuv420p'
|
||||
```
|
||||
|
||||
### Audio Processing
|
||||
|
||||
```typescript
|
||||
'-af', 'aresample=44100' // Resample to 44.1kHz
|
||||
```
|
||||
|
||||
### Statistics and Monitoring
|
||||
|
||||
Log stream statistics:
|
||||
|
||||
```typescript
|
||||
ffmpegProcess.stdout.on('data', (data) => {
|
||||
console.log(`Stream stats: ${data}`);
|
||||
});
|
||||
```
|
||||
|
||||
### Multiple Bitrate HLS (Adaptive)
|
||||
|
||||
Generate multiple quality versions:
|
||||
|
||||
```typescript
|
||||
// Create master playlist pointing to multiple variants
|
||||
const masterPlaylist = `#EXTM3U
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=5000000
|
||||
high.m3u8
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=2500000
|
||||
medium.m3u8
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=1000000
|
||||
low.m3u8`;
|
||||
```
|
||||
|
||||
## Support & Resources
|
||||
|
||||
- [FFmpeg Documentation](https://ffmpeg.org/documentation.html)
|
||||
- [HLS.js Documentation](https://github.com/video-dev/hls.js/wiki)
|
||||
- [SvelteKit Documentation](https://kit.svelte.dev)
|
||||
- [ONVIF Protocol](https://www.onvif.org/) - For camera device discovery
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
401
bun.lock
Normal file
401
bun.lock
Normal file
@@ -0,0 +1,401 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "grown",
|
||||
"dependencies": {
|
||||
"node-schedule": "^2.1.1",
|
||||
"onvif": "^0.8.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lucide/svelte": "^0.577.0",
|
||||
"@sveltejs/adapter-auto": "^7.0.0",
|
||||
"@sveltejs/kit": "^2.50.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/node": "^25.4.0",
|
||||
"@types/node-schedule": "^2.1.8",
|
||||
"clsx": "^2.1.1",
|
||||
"svelte": "^5.51.0",
|
||||
"svelte-adapter-bun": "^1.0.1",
|
||||
"svelte-check": "^4.4.2",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwind-variants": "^3.2.2",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@lucide/svelte": ["@lucide/svelte@0.577.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-0P6mkySd2MapIEgq08tADPmcN4DHndC/02PWwaLkOerXlx5Sv9aT4BxyXLIY+eccr0g/nEyCYiJesqS61YdBZQ=="],
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.115.0", "", {}, "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw=="],
|
||||
|
||||
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
||||
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.8", "", { "os": "android", "cpu": "arm64" }, "sha512-5bcmMQDWEfWUq3m79Mcf/kbO6e5Jr6YjKSsA1RnpXR6k73hQ9z1B17+4h93jXpzHvS18p7bQHM1HN/fSd+9zog=="],
|
||||
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dcHPd5N4g9w2iiPRJmAvO0fsIWzF2JPr9oSuTjxLL56qu+oML5aMbBMNwWbk58Mt3pc7vYs9CCScwLxdXPdRsg=="],
|
||||
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-mw0VzDvoj8AuR761QwpdCFN0sc/jspuc7eRYJetpLWd+XyansUrH3C7IgNw6swBOgQT9zBHNKsVCjzpfGJlhUA=="],
|
||||
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.8", "", { "os": "freebsd", "cpu": "x64" }, "sha512-xNrRa6mQ9NmMIJBdJtPMPG8Mso0OhM526pDzc/EKnRrIrrkHD1E0Z6tONZRmUeJElfsQ6h44lQQCcDilSNIvSQ=="],
|
||||
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.8", "", { "os": "linux", "cpu": "arm" }, "sha512-WgCKoO6O/rRUwimWfEJDeztwJJmuuX0N2bYLLRxmXDTtCwjToTOqk7Pashl/QpQn3H/jHjx0b5yCMbcTVYVpNg=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-tOHgTOQa8G4Z3ULj4G3NYOGGJEsqPHR91dT72u63OtVsZ7B6wFJKOx+ZKv+pvwzxWz92/I2ycaqi2/Ll4l+rlg=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-oRbxcgDujCi2Yp1GTxoUFsIFlZsuPHU4OV4AzNc3/6aUmR4lfm9FK0uwQu82PJsuUwnF2jFdop3Ep5c1uK7Uxg=="],
|
||||
|
||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.8", "", { "os": "linux", "cpu": "ppc64" }, "sha512-oaLRyUHw8kQE5M89RqrDJZ10GdmGJcMeCo8tvaE4ukOofqgjV84AbqBSH6tTPjeT2BHv+xlKj678GBuIb47lKA=="],
|
||||
|
||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.8", "", { "os": "linux", "cpu": "s390x" }, "sha512-1hjSKFrod5MwBBdLOOA0zpUuSfSDkYIY+QqcMcIU1WOtswZtZdUkcFcZza9b2HcAb0bnpmmyo0LZcaxLb2ov1g=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.8", "", { "os": "linux", "cpu": "x64" }, "sha512-a1+F0aV4Wy9tT3o+cHl3XhOy6aFV+B8Ll+/JFj98oGkb6lGk3BNgrxd+80RwYRVd23oLGvj3LwluKYzlv1PEuw=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.8", "", { "os": "linux", "cpu": "x64" }, "sha512-bGyXCFU11seFrf7z8PcHSwGEiFVkZ9vs+auLacVOQrVsI8PFHJzzJROF3P6b0ODDmXr0m6Tj5FlDhcXVk0Jp8w=="],
|
||||
|
||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.8", "", { "os": "none", "cpu": "arm64" }, "sha512-n8d+L2bKgf9G3+AM0bhHFWdlz9vYKNim39ujRTieukdRek0RAo2TfG2uEnV9spa4r4oHUfL9IjcY3M9SlqN1gw=="],
|
||||
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.8", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-4R4iJDIk7BrJdteAbEAICXPoA7vZoY/M0OBfcRlQxzQvUYMcEp2GbC/C8UOgQJhu2TjGTpX1H8vVO1xHWcRqQA=="],
|
||||
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-3lwnklba9qQOpFnQ7EW+A1m4bZTWXZE4jtehsZ0YOl2ivW1FQqp5gY7X2DLuKITggesyuLwcmqS11fA7NtrmrA=="],
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.8", "", { "os": "win32", "cpu": "x64" }, "sha512-VGjCx9Ha1P/r3tXGDZyG0Fcq7Q0Afnk64aaKzr1m40vbn1FL8R3W0V1ELDvPgzLXaaqK/9PnsqSaLWXfn6JtGQ=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.8", "", {}, "sha512-wzJwL82/arVfeSP3BLr1oTy40XddjtEdrdgtJ4lLRBu06mP3q/8HGM6K0JRlQuTA3XB0pNJx2so/nmpY4xyOew=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="],
|
||||
|
||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.9", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA=="],
|
||||
|
||||
"@sveltejs/adapter-auto": ["@sveltejs/adapter-auto@7.0.1", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-dvuPm1E7M9NI/+canIQ6KKQDU2AkEefEZ2Dp7cY6uKoPq9Z/PhOXABe526UdW2mN986gjVkuSLkOYIBnS/M2LQ=="],
|
||||
|
||||
"@sveltejs/kit": ["@sveltejs/kit@2.53.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.6.3", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-iAIPEahFgDJJyvz8g0jP08KvqnM6JvdW8YfsygZ+pMeMvyM2zssWMltcsotETvjSZ82G3VlitgDtBIvpQSZrTA=="],
|
||||
|
||||
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="],
|
||||
|
||||
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.2", "", { "dependencies": { "obug": "^2.1.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="],
|
||||
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="],
|
||||
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="],
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="],
|
||||
|
||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.2.1", "", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/node": ["@types/node@25.4.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw=="],
|
||||
|
||||
"@types/node-schedule": ["@types/node-schedule@2.1.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-k00g6Yj/oUg/CDC+MeLHUzu0+OFxWbIqrFfDiLi6OPKxTujvpv29mHGM8GtKr7B+9Vv92FcK/8mRqi1DK5f3hA=="],
|
||||
|
||||
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||
|
||||
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||
|
||||
"aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="],
|
||||
|
||||
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||
|
||||
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
|
||||
|
||||
"cron-parser": ["cron-parser@4.9.0", "", { "dependencies": { "luxon": "^3.2.1" } }, "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q=="],
|
||||
|
||||
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"devalue": ["devalue@5.6.3", "", {}, "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="],
|
||||
|
||||
"esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
|
||||
|
||||
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
|
||||
|
||||
"esrap": ["esrap@2.2.3", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
|
||||
|
||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
|
||||
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
|
||||
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="],
|
||||
|
||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="],
|
||||
|
||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="],
|
||||
|
||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="],
|
||||
|
||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="],
|
||||
|
||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="],
|
||||
|
||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="],
|
||||
|
||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="],
|
||||
|
||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="],
|
||||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
|
||||
|
||||
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
|
||||
|
||||
"long-timeout": ["long-timeout@0.1.1", "", {}, "sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w=="],
|
||||
|
||||
"luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
|
||||
|
||||
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"node-schedule": ["node-schedule@2.1.1", "", { "dependencies": { "cron-parser": "^4.2.0", "long-timeout": "0.1.1", "sorted-array-functions": "^1.3.0" } }, "sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ=="],
|
||||
|
||||
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
|
||||
|
||||
"onvif": ["onvif@0.8.1", "", { "dependencies": { "xml2js": "^0.6.2" } }, "sha512-D0VSuTQutZWQsaWWvh7acbengYNJew25kXpXc/stJc6/xYx2MwU1tkIZA23bu12hr5MxstSQ55decY7we2/LWw=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
||||
|
||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
|
||||
"rolldown": ["rolldown@1.0.0-rc.8", "", { "dependencies": { "@oxc-project/types": "=0.115.0", "@rolldown/pluginutils": "1.0.0-rc.8" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.8", "@rolldown/binding-darwin-arm64": "1.0.0-rc.8", "@rolldown/binding-darwin-x64": "1.0.0-rc.8", "@rolldown/binding-freebsd-x64": "1.0.0-rc.8", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.8", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.8", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.8", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.8", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.8", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.8", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.8", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.8", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.8", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.8", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.8" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-RGOL7mz/aoQpy/y+/XS9iePBfeNRDUdozrhCEJxdpJyimW8v6yp4c30q6OviUU5AnUJVLRL9GP//HUs6N3ALrQ=="],
|
||||
|
||||
"rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
|
||||
|
||||
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
|
||||
|
||||
"sax": ["sax@1.5.0", "", {}, "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA=="],
|
||||
|
||||
"set-cookie-parser": ["set-cookie-parser@3.0.1", "", {}, "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q=="],
|
||||
|
||||
"sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
|
||||
|
||||
"sorted-array-functions": ["sorted-array-functions@1.3.0", "", {}, "sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"svelte": ["svelte@5.53.9", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.3", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-MwDfWsN8qZzeP0jlQsWF4k/4B3csb3IbzCRggF+L/QqY7T8bbKvnChEo1cPZztF51HJQhilDbevWYl2LvXbquA=="],
|
||||
|
||||
"svelte-adapter-bun": ["svelte-adapter-bun@1.0.1", "", { "dependencies": { "rolldown": "^1.0.0-beta.38" }, "peerDependencies": { "@sveltejs/kit": "^2.4.0", "typescript": "^5" } }, "sha512-tNOvfm8BGgG+rmEA7hkmqtq07v7zoo4skLQc+hIoQ79J+1fkEMpJEA2RzCIe3aPc8JdrsMJkv3mpiZPMsgahjA=="],
|
||||
|
||||
"svelte-check": ["svelte-check@4.4.5", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-1bSwIRCvvmSHrlK52fOlZmVtUZgil43jNL/2H18pRpa+eQjzGt6e3zayxhp1S7GajPFKNM/2PMCG+DZFHlG9fw=="],
|
||||
|
||||
"tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
|
||||
|
||||
"tailwind-variants": ["tailwind-variants@3.2.2", "", { "peerDependencies": { "tailwind-merge": ">=3.0.0", "tailwindcss": "*" }, "optionalPeers": ["tailwind-merge"] }, "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],
|
||||
|
||||
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
|
||||
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
||||
|
||||
"vitefu": ["vitefu@1.1.2", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw=="],
|
||||
|
||||
"xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="],
|
||||
|
||||
"xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
|
||||
|
||||
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
}
|
||||
}
|
||||
16
components.json
Normal file
16
components.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||
"tailwind": {
|
||||
"css": "src/routes/layout.css",
|
||||
"baseColor": "zinc"
|
||||
},
|
||||
"aliases": {
|
||||
"components": "$lib/components",
|
||||
"utils": "$lib/utils",
|
||||
"ui": "$lib/components/ui",
|
||||
"hooks": "$lib/hooks",
|
||||
"lib": "$lib"
|
||||
},
|
||||
"typescript": true,
|
||||
"registry": "https://shadcn-svelte.com/registry"
|
||||
}
|
||||
37
package.json
Normal file
37
package.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "grown",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lucide/svelte": "^0.577.0",
|
||||
"@sveltejs/adapter-auto": "^7.0.0",
|
||||
"@sveltejs/kit": "^2.50.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/node": "^25.4.0",
|
||||
"@types/node-schedule": "^2.1.8",
|
||||
"clsx": "^2.1.1",
|
||||
"svelte": "^5.51.0",
|
||||
"svelte-adapter-bun": "^1.0.1",
|
||||
"svelte-check": "^4.4.2",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwind-variants": "^3.2.2",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"node-schedule": "^2.1.1",
|
||||
"onvif": "^0.8.1"
|
||||
}
|
||||
}
|
||||
13
src/app.d.ts
vendored
Normal file
13
src/app.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
11
src/app.html
Normal file
11
src/app.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
104
src/hooks.server.ts
Normal file
104
src/hooks.server.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import schedule from "node-schedule";
|
||||
import { spawn } from "child_process";
|
||||
import { existsSync, mkdirSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { getAllStreams, getStreamProcess } from "$lib/server/streaming.js";
|
||||
|
||||
const TIMELAPSE_OUTPUT_PATH =
|
||||
process.env.TIMELAPSE_OUTPUT_PATH || "./timelapse";
|
||||
const CAPTURE_INTERVAL = "0 * * * *"; // Every hour at minute 0
|
||||
|
||||
let captureJob: any = null;
|
||||
let initialized = false;
|
||||
|
||||
function ensureOutputDirectory() {
|
||||
if (!existsSync(TIMELAPSE_OUTPUT_PATH)) {
|
||||
mkdirSync(TIMELAPSE_OUTPUT_PATH, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function captureTimelapseFrame() {
|
||||
const streams = getAllStreams();
|
||||
|
||||
streams.forEach((streamInfo) => {
|
||||
const stream = getStreamProcess(streamInfo.streamId);
|
||||
if (!stream || !stream.process || !stream.process.stdout) {
|
||||
console.error(
|
||||
`[Timelapse] Stream ${streamInfo.streamId} not available for capture`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const filename = `${streamInfo.streamId}_${timestamp}.jpg`;
|
||||
const outputPath = join(TIMELAPSE_OUTPUT_PATH, filename);
|
||||
|
||||
const ffmpegArgs = [
|
||||
"-rtsp_transport",
|
||||
"tcp",
|
||||
"-i",
|
||||
streamInfo.rtspUrl,
|
||||
"-vframes",
|
||||
"1",
|
||||
"-q:v",
|
||||
"2",
|
||||
outputPath,
|
||||
];
|
||||
|
||||
const ffmpegProcess = spawn("ffmpeg", ffmpegArgs);
|
||||
|
||||
ffmpegProcess.on("error", (err: Error) => {
|
||||
console.error(
|
||||
`[Timelapse] FFmpeg error for ${streamInfo.streamId}:`,
|
||||
err,
|
||||
);
|
||||
});
|
||||
|
||||
ffmpegProcess.on("close", (code: number | null) => {
|
||||
if (code === 0) {
|
||||
console.log(
|
||||
`[Timelapse] Captured frame for ${streamInfo.streamId}: ${filename}`,
|
||||
);
|
||||
} else {
|
||||
console.error(
|
||||
`[Timelapse] FFmpeg exited with code ${code} for ${streamInfo.streamId}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initializeTimelapse() {
|
||||
if (initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureOutputDirectory();
|
||||
console.log(`[Timelapse] Output directory: ${TIMELAPSE_OUTPUT_PATH}`);
|
||||
console.log("[Timelapse] Starting hourly capture job...");
|
||||
|
||||
captureJob = schedule.scheduleJob(CAPTURE_INTERVAL, () => {
|
||||
console.log("[Timelapse] Running hourly capture job...");
|
||||
captureTimelapseFrame();
|
||||
});
|
||||
|
||||
initialized = true;
|
||||
console.log("[Timelapse] Hourly capture job initialized");
|
||||
}
|
||||
|
||||
export async function handle({ event, resolve }) {
|
||||
// Initialize timelapse on first request
|
||||
if (!initialized) {
|
||||
initializeTimelapse();
|
||||
}
|
||||
|
||||
return resolve(event);
|
||||
}
|
||||
|
||||
// Cleanup on shutdown
|
||||
process.on("exit", () => {
|
||||
if (captureJob) {
|
||||
captureJob.cancel();
|
||||
console.log("[Timelapse] Capture job cancelled");
|
||||
}
|
||||
});
|
||||
1
src/lib/assets/favicon.svg
Normal file
1
src/lib/assets/favicon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
357
src/lib/components/RTSPVideoPlayer.svelte
Normal file
357
src/lib/components/RTSPVideoPlayer.svelte
Normal file
@@ -0,0 +1,357 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { Play, Pause, Maximize2, Minimize2 } from "@lucide/svelte";
|
||||
|
||||
let canvasElement: HTMLCanvasElement | undefined = undefined;
|
||||
let videoContainer: HTMLDivElement | undefined = undefined;
|
||||
let isFullscreen = $state(false);
|
||||
let isPlaying = $state(false);
|
||||
let isLoading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let streamId = $state("camera-1");
|
||||
let rtspUrl = $state("rtsp://Kamera_1:kamera_1@192.168.175.49/stream1");
|
||||
let frameCount = $state(0);
|
||||
let fps = $state(0);
|
||||
let frameCounterStart = 0;
|
||||
let streamWidth = 0;
|
||||
let streamHeight = 0;
|
||||
|
||||
let abortController: AbortController | null = null;
|
||||
|
||||
async function startStream() {
|
||||
if (!streamId || !rtspUrl) {
|
||||
error = "Please enter both Stream ID and RTSP URL";
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
error = null;
|
||||
frameCount = 0;
|
||||
fps = 0;
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/stream", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "start",
|
||||
streamId,
|
||||
rtspUrl,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok || data.error) {
|
||||
throw new Error(data.error || "Failed to start stream");
|
||||
}
|
||||
|
||||
// Wait for FFmpeg to start
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
// Start streaming MJPEG frames
|
||||
streamMJPEG();
|
||||
isPlaying = true;
|
||||
} catch (err) {
|
||||
error =
|
||||
err instanceof Error ? err.message : "Unknown error occurred";
|
||||
console.error("Error starting stream:", err);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function streamMJPEG() {
|
||||
if (!canvasElement) return;
|
||||
|
||||
abortController = new AbortController();
|
||||
const signal = abortController.signal;
|
||||
|
||||
let reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/stream/mjpeg?id=${streamId}`, {
|
||||
signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to connect to stream: ${response.status}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error("Response body is empty");
|
||||
}
|
||||
|
||||
reader = response.body.getReader();
|
||||
let buffer = new Uint8Array();
|
||||
frameCounterStart = Date.now();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) break;
|
||||
|
||||
// Append new data to buffer
|
||||
const newBuffer = new Uint8Array(buffer.length + value.length);
|
||||
newBuffer.set(buffer);
|
||||
newBuffer.set(value, buffer.length);
|
||||
buffer = newBuffer;
|
||||
|
||||
// Find JPEG frames (they start with FFD8 and end with FFD9)
|
||||
const jpegStart = findJPEGStart(buffer);
|
||||
if (jpegStart !== -1) {
|
||||
const jpegEnd = findJPEGEnd(buffer, jpegStart);
|
||||
if (jpegEnd !== -1) {
|
||||
const jpegData = buffer.slice(jpegStart, jpegEnd + 2);
|
||||
displayFrame(jpegData);
|
||||
buffer = buffer.slice(jpegEnd + 2);
|
||||
|
||||
// Update FPS counter
|
||||
frameCount++;
|
||||
const now = Date.now();
|
||||
if (now - frameCounterStart >= 1000) {
|
||||
fps = frameCount;
|
||||
frameCount = 0;
|
||||
frameCounterStart = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent buffer from growing too large
|
||||
if (buffer.length > 5000000) {
|
||||
buffer = buffer.slice(-1000000);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if ((err as Error).name !== "AbortError") {
|
||||
error = err instanceof Error ? err.message : "Stream error";
|
||||
console.error("Error streaming MJPEG:", err);
|
||||
}
|
||||
} finally {
|
||||
if (reader) {
|
||||
reader.releaseLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findJPEGStart(buffer: Uint8Array): number {
|
||||
for (let i = 0; i < buffer.length - 1; i++) {
|
||||
if (buffer[i] === 0xff && buffer[i + 1] === 0xd8) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function findJPEGEnd(buffer: Uint8Array, start: number): number {
|
||||
for (let i = start + 2; i < buffer.length - 1; i++) {
|
||||
if (buffer[i] === 0xff && buffer[i + 1] === 0xd9) {
|
||||
return i + 1;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function displayFrame(jpegData: Uint8Array) {
|
||||
if (!canvasElement) return;
|
||||
|
||||
const blob = new Blob([jpegData as BlobPart], { type: "image/jpeg" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
if (!canvasElement) return;
|
||||
|
||||
// Store original stream dimensions
|
||||
if (streamWidth === 0 || streamHeight === 0) {
|
||||
streamWidth = img.width;
|
||||
streamHeight = img.height;
|
||||
}
|
||||
|
||||
// Get container dimensions
|
||||
const container = canvasElement.parentElement;
|
||||
if (!container) return;
|
||||
|
||||
const containerWidth = container.clientWidth;
|
||||
const containerHeight = container.clientHeight;
|
||||
|
||||
// Calculate scale to fit container while maintaining aspect ratio
|
||||
const scaleW = containerWidth / streamWidth;
|
||||
const scaleH = containerHeight / streamHeight;
|
||||
const scale = Math.min(scaleW, scaleH);
|
||||
|
||||
const displayWidth = Math.round(streamWidth * scale);
|
||||
const displayHeight = Math.round(streamHeight * scale);
|
||||
|
||||
canvasElement.width = displayWidth;
|
||||
canvasElement.height = displayHeight;
|
||||
|
||||
// Get fresh context after resizing canvas
|
||||
const ctx = canvasElement.getContext("2d");
|
||||
if (!ctx) {
|
||||
console.error("Failed to get canvas context");
|
||||
URL.revokeObjectURL(url);
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.drawImage(img, 0, 0, displayWidth, displayHeight);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
console.error("Failed to load JPEG frame");
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
img.src = url;
|
||||
}
|
||||
|
||||
async function stopStream() {
|
||||
isLoading = true;
|
||||
|
||||
try {
|
||||
if (abortController) {
|
||||
abortController.abort();
|
||||
abortController = null;
|
||||
}
|
||||
|
||||
const response = await fetch("/api/stream", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "stop",
|
||||
streamId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to stop stream");
|
||||
}
|
||||
|
||||
isPlaying = false;
|
||||
error = null;
|
||||
frameCount = 0;
|
||||
fps = 0;
|
||||
} catch (err) {
|
||||
error =
|
||||
err instanceof Error ? err.message : "Failed to stop stream";
|
||||
console.error("Error stopping stream:", err);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFullscreen() {
|
||||
if (!videoContainer) return;
|
||||
|
||||
if (!document.fullscreenElement) {
|
||||
videoContainer.requestFullscreen().catch((err) => {
|
||||
console.error(
|
||||
`Error attempting to enable fullscreen: ${err.message}`,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
function togglePlayPause() {
|
||||
if (isPlaying) {
|
||||
stopStream();
|
||||
} else {
|
||||
startStream();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Handle page unload/reload
|
||||
const handleBeforeUnload = () => {
|
||||
if (isPlaying) {
|
||||
stopStream();
|
||||
}
|
||||
};
|
||||
|
||||
const handleFullscreenChange = () => {
|
||||
isFullscreen = !!document.fullscreenElement;
|
||||
};
|
||||
|
||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
||||
|
||||
// Autoplay stream on mount
|
||||
startStream();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
document.removeEventListener(
|
||||
"fullscreenchange",
|
||||
handleFullscreenChange,
|
||||
);
|
||||
if (abortController) {
|
||||
abortController.abort();
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Keimli der II.</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="min-h-screen bg-black py-8 px-4">
|
||||
<div class="w-full max-w-7xl mx-auto">
|
||||
<h1 class="text-white text-3xl font-bold mb-6">Keimli der II.</h1>
|
||||
|
||||
<div
|
||||
bind:this={videoContainer}
|
||||
class="relative bg-black rounded-lg overflow-hidden group"
|
||||
style="width: 100%; aspect-ratio: 16/9;"
|
||||
>
|
||||
<canvas
|
||||
bind:this={canvasElement}
|
||||
class="w-full h-full bg-black"
|
||||
style="display: block; object-fit: contain;"
|
||||
></canvas>
|
||||
|
||||
<!-- Play/Pause button - lower left -->
|
||||
<button
|
||||
onclick={togglePlayPause}
|
||||
class="absolute bottom-4 left-4 bg-black/50 hover:bg-black/80 text-white p-2 rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200"
|
||||
title={isPlaying ? "Pause stream" : "Play stream"}
|
||||
>
|
||||
{#if isPlaying}
|
||||
<Pause class="w-5 h-5" />
|
||||
{:else}
|
||||
<Play class="w-5 h-5" />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<!-- Fullscreen button - lower right -->
|
||||
<button
|
||||
onclick={toggleFullscreen}
|
||||
class="absolute bottom-4 right-4 bg-black/50 hover:bg-black/80 text-white p-2 rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200"
|
||||
title="Toggle fullscreen"
|
||||
>
|
||||
{#if isFullscreen}
|
||||
<Minimize2 class="w-5 h-5" />
|
||||
{:else}
|
||||
<Maximize2 class="w-5 h-5" />
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(html, body) {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
1
src/lib/index.ts
Normal file
1
src/lib/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
138
src/lib/server/streaming.ts
Normal file
138
src/lib/server/streaming.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { spawn } from "child_process";
|
||||
|
||||
interface StreamProcess {
|
||||
process: ReturnType<typeof spawn> | null;
|
||||
rtspUrl: string;
|
||||
startedAt: Date;
|
||||
clients: Set<string>;
|
||||
}
|
||||
|
||||
const activeStreams = new Map<string, StreamProcess>();
|
||||
|
||||
export function startStream(
|
||||
streamId: string,
|
||||
rtspUrl: string,
|
||||
): { success: boolean; error?: string } {
|
||||
// Kill existing stream if it's running
|
||||
if (activeStreams.has(streamId)) {
|
||||
stopStream(streamId);
|
||||
}
|
||||
|
||||
try {
|
||||
const args = [
|
||||
"-rtsp_transport",
|
||||
"tcp",
|
||||
"-i",
|
||||
rtspUrl,
|
||||
// Scale video to 1920x1080 (Full HD)
|
||||
"-vf",
|
||||
"scale=1920:1080",
|
||||
// Video output - MJPEG format
|
||||
"-c:v",
|
||||
"mjpeg",
|
||||
"-q:v",
|
||||
"5",
|
||||
"-f",
|
||||
"mjpeg",
|
||||
"-fflags",
|
||||
"nobuffer",
|
||||
// Disable audio
|
||||
"-an",
|
||||
// Output to stdout
|
||||
"pipe:1",
|
||||
];
|
||||
|
||||
const ffmpegProcess = spawn("ffmpeg", args);
|
||||
|
||||
ffmpegProcess.on("error", (err: Error) => {
|
||||
console.error(`[${streamId}] FFmpeg error:`, err);
|
||||
activeStreams.delete(streamId);
|
||||
});
|
||||
|
||||
ffmpegProcess.on("close", (code: number | null) => {
|
||||
activeStreams.delete(streamId);
|
||||
});
|
||||
|
||||
activeStreams.set(streamId, {
|
||||
process: ffmpegProcess,
|
||||
rtspUrl,
|
||||
startedAt: new Date(),
|
||||
clients: new Set(),
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`[${streamId}] Failed to start stream:`, error);
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to start stream: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function stopStream(streamId: string): void {
|
||||
const stream = activeStreams.get(streamId);
|
||||
if (stream && stream.process) {
|
||||
stream.process.kill("SIGTERM");
|
||||
stream.clients.clear();
|
||||
activeStreams.delete(streamId);
|
||||
}
|
||||
}
|
||||
|
||||
export function getStreamProcess(streamId: string) {
|
||||
return activeStreams.get(streamId);
|
||||
}
|
||||
|
||||
export function addStreamClient(streamId: string, client: string): boolean {
|
||||
const stream = activeStreams.get(streamId);
|
||||
if (!stream) {
|
||||
return false;
|
||||
}
|
||||
stream.clients.add(client);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function removeStreamClient(streamId: string, client: string): void {
|
||||
const stream = activeStreams.get(streamId);
|
||||
if (stream) {
|
||||
stream.clients.delete(client);
|
||||
|
||||
// Auto-stop stream if no clients remain
|
||||
if (stream.clients.size === 0) {
|
||||
stopStream(streamId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getStreamStatus(streamId: string) {
|
||||
const stream = activeStreams.get(streamId);
|
||||
if (!stream) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
streamId,
|
||||
rtspUrl: stream.rtspUrl,
|
||||
startedAt: stream.startedAt,
|
||||
isRunning: stream.process !== null,
|
||||
clientCount: stream.clients.size,
|
||||
};
|
||||
}
|
||||
|
||||
export function getAllStreams() {
|
||||
return Array.from(activeStreams.entries()).map(([streamId, stream]) => ({
|
||||
streamId,
|
||||
rtspUrl: stream.rtspUrl,
|
||||
startedAt: stream.startedAt,
|
||||
isRunning: stream.process !== null,
|
||||
clientCount: stream.clients.size,
|
||||
}));
|
||||
}
|
||||
|
||||
export function stopAllStreams(): void {
|
||||
activeStreams.forEach((stream, streamId) => {
|
||||
stopStream(streamId);
|
||||
});
|
||||
}
|
||||
13
src/lib/utils.ts
Normal file
13
src/lib/utils.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, "children"> : T;
|
||||
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
||||
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
|
||||
9
src/routes/+layout.svelte
Normal file
9
src/routes/+layout.svelte
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
import './layout.css';
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
|
||||
{@render children()}
|
||||
18
src/routes/+page.svelte
Normal file
18
src/routes/+page.svelte
Normal file
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import RTSPVideoPlayer from "$lib/components/RTSPVideoPlayer.svelte";
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>RTSP Stream Viewer</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="min-h-screen bg-linear-to-br from-gray-900 to-gray-800 py-8">
|
||||
<RTSPVideoPlayer />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(body) {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
97
src/routes/api/stream/+server.ts
Normal file
97
src/routes/api/stream/+server.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { json } from "@sveltejs/kit";
|
||||
import {
|
||||
startStream,
|
||||
stopStream,
|
||||
getStreamStatus,
|
||||
getAllStreams,
|
||||
getStreamProcess,
|
||||
addStreamClient,
|
||||
removeStreamClient,
|
||||
} from "$lib/server/streaming.js";
|
||||
|
||||
export async function POST({ request }) {
|
||||
const { action, streamId, rtspUrl } = await request.json();
|
||||
|
||||
if (action === "start") {
|
||||
if (!streamId || !rtspUrl) {
|
||||
return json({ error: "Missing streamId or rtspUrl" }, { status: 400 });
|
||||
}
|
||||
|
||||
const result = startStream(streamId, rtspUrl);
|
||||
if (result.error) {
|
||||
return json({ error: result.error }, { status: 500 });
|
||||
}
|
||||
|
||||
return json({ success: true, streamId });
|
||||
} else if (action === "stop") {
|
||||
if (!streamId) {
|
||||
return json({ error: "Missing streamId" }, { status: 400 });
|
||||
}
|
||||
|
||||
stopStream(streamId);
|
||||
return json({ success: true });
|
||||
} else if (action === "status") {
|
||||
if (!streamId) {
|
||||
return json({ error: "Missing streamId" }, { status: 400 });
|
||||
}
|
||||
|
||||
const status = getStreamStatus(streamId);
|
||||
return json(status);
|
||||
} else if (action === "list") {
|
||||
const streams = getAllStreams();
|
||||
return json({ streams });
|
||||
} else {
|
||||
return json({ error: "Unknown action" }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET({ url }) {
|
||||
const streamId = url.searchParams.get("id");
|
||||
|
||||
if (!streamId) {
|
||||
return new Response("Missing stream ID", { status: 400 });
|
||||
}
|
||||
|
||||
const stream = getStreamProcess(streamId);
|
||||
if (!stream || !stream.process || !stream.process.stdout) {
|
||||
return new Response("Stream not found", { status: 404 });
|
||||
}
|
||||
|
||||
// Track client
|
||||
const clientId = Math.random().toString(36).substring(7);
|
||||
addStreamClient(streamId, clientId);
|
||||
|
||||
// Create readable stream from FFmpeg stdout
|
||||
const readable = stream.process.stdout;
|
||||
|
||||
// Return as multipart/x-mixed-replace for MJPEG
|
||||
const response = new Response(readable as any, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/x-mixed-replace; boundary=FFMPEG",
|
||||
"Cache-Control": "no-cache, no-store, must-revalidate",
|
||||
Pragma: "no-cache",
|
||||
Expires: "0",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
|
||||
// Track when connection closes
|
||||
if (response.body) {
|
||||
const reader = response.body.getReader();
|
||||
(async () => {
|
||||
try {
|
||||
while (true) {
|
||||
await reader.read();
|
||||
}
|
||||
} catch (e) {
|
||||
// Connection closed
|
||||
console.error("Error monitoring stream connection:", e);
|
||||
} finally {
|
||||
removeStreamClient(streamId, clientId);
|
||||
reader.releaseLock();
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
55
src/routes/api/stream/mjpeg/+server.ts
Normal file
55
src/routes/api/stream/mjpeg/+server.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import {
|
||||
getStreamProcess,
|
||||
addStreamClient,
|
||||
removeStreamClient,
|
||||
} from "$lib/server/streaming.js";
|
||||
|
||||
export async function GET({ url }) {
|
||||
const streamId = url.searchParams.get("id");
|
||||
|
||||
if (!streamId) {
|
||||
return new Response("Missing stream ID", { status: 400 });
|
||||
}
|
||||
|
||||
const stream = getStreamProcess(streamId);
|
||||
if (!stream || !stream.process || !stream.process.stdout) {
|
||||
return new Response("Stream not found", { status: 404 });
|
||||
}
|
||||
|
||||
const clientId = Math.random().toString(36).substring(7);
|
||||
addStreamClient(streamId, clientId);
|
||||
|
||||
// Create a web ReadableStream from the Node.js stream
|
||||
const nodeStream = stream.process.stdout;
|
||||
|
||||
const webReadableStream = new ReadableStream({
|
||||
start(controller) {
|
||||
nodeStream.on("data", (chunk) => {
|
||||
controller.enqueue(chunk);
|
||||
});
|
||||
|
||||
nodeStream.on("end", () => {
|
||||
controller.close();
|
||||
});
|
||||
|
||||
nodeStream.on("error", () => {
|
||||
controller.close();
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
removeStreamClient(streamId, clientId);
|
||||
},
|
||||
});
|
||||
|
||||
// Return the response with proper headers
|
||||
return new Response(webReadableStream, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "image/jpeg",
|
||||
"Cache-Control": "no-cache, no-store, must-revalidate",
|
||||
Pragma: "no-cache",
|
||||
Expires: "0",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
}
|
||||
121
src/routes/layout.css
Normal file
121
src/routes/layout.css
Normal file
@@ -0,0 +1,121 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.21 0.006 285.885);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.705 0.015 286.067);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.21 0.006 285.885);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.705 0.015 286.067);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.92 0.004 286.32);
|
||||
--primary-foreground: oklch(0.21 0.006 285.885);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.552 0.016 285.938);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.552 0.016 285.938);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
7
src/static/hls/camera-1.m3u8
Normal file
7
src/static/hls/camera-1.m3u8
Normal file
@@ -0,0 +1,7 @@
|
||||
#EXTM3U
|
||||
#EXT-X-VERSION:3
|
||||
#EXT-X-TARGETDURATION:3
|
||||
#EXT-X-MEDIA-SEQUENCE:0
|
||||
#EXTINF:3.150000,
|
||||
camera-10.ts
|
||||
#EXT-X-ENDLIST
|
||||
BIN
src/static/hls/camera-10.ts
Normal file
BIN
src/static/hls/camera-10.ts
Normal file
Binary file not shown.
BIN
src/static/hls/camera-11.ts
Normal file
BIN
src/static/hls/camera-11.ts
Normal file
Binary file not shown.
BIN
src/static/hls/camera-12.ts
Normal file
BIN
src/static/hls/camera-12.ts
Normal file
Binary file not shown.
269
static/hls/camera-1.m3u8
Normal file
269
static/hls/camera-1.m3u8
Normal file
@@ -0,0 +1,269 @@
|
||||
#EXTM3U
|
||||
#EXT-X-VERSION:3
|
||||
#EXT-X-TARGETDURATION:12
|
||||
#EXT-X-MEDIA-SEQUENCE:1773178276
|
||||
#EXTINF:12.500000,
|
||||
camera-10.ts
|
||||
#EXTINF:12.500000,
|
||||
camera-11.ts
|
||||
#EXTINF:12.500000,
|
||||
camera-12.ts
|
||||
#EXTINF:12.500000,
|
||||
camera-13.ts
|
||||
#EXTINF:12.500000,
|
||||
camera-14.ts
|
||||
#EXTINF:12.500000,
|
||||
camera-15.ts
|
||||
#EXTINF:12.500000,
|
||||
camera-16.ts
|
||||
#EXTINF:12.500000,
|
||||
camera-17.ts
|
||||
#EXTINF:12.500000,
|
||||
camera-18.ts
|
||||
#EXTINF:12.500000,
|
||||
camera-19.ts
|
||||
#EXTINF:12.500000,
|
||||
camera-110.ts
|
||||
#EXTINF:4.600000,
|
||||
camera-111.ts
|
||||
#EXT-X-DISCONTINUITY
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178288.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178289.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178290.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178291.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178292.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178293.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178294.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178295.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178296.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178297.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178298.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178299.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178300.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178301.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178302.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178303.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178304.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178305.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178306.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178307.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178308.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178309.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178310.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178311.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178312.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178313.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178314.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178315.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178316.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178317.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178318.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178319.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178320.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178321.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178322.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178323.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178324.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178325.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178326.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178327.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178328.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178329.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178330.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178331.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178332.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178333.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178334.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178335.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178336.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178337.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178338.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178339.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178340.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178341.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178342.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178343.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178344.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178345.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178346.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178347.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178348.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178349.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178350.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178351.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178352.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178353.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178354.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178355.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178356.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178357.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178358.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178359.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178360.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178361.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178362.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178363.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178364.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178365.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178366.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178367.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178368.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178369.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178370.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178371.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178372.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178373.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178374.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178375.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178376.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178377.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178378.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178379.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178380.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178381.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178382.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178383.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178384.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178385.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178386.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178387.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178388.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178389.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178390.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178391.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178392.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178393.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178394.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178395.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178396.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178397.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178398.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178399.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178400.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178401.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178402.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178403.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178404.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178405.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178406.ts
|
||||
#EXTINF:3.000000,
|
||||
camera-11773178407.ts
|
||||
BIN
static/hls/camera-10.ts
Normal file
BIN
static/hls/camera-10.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11.ts
Normal file
BIN
static/hls/camera-11.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-110.ts
Normal file
BIN
static/hls/camera-110.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-111.ts
Normal file
BIN
static/hls/camera-111.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178288.ts
Normal file
BIN
static/hls/camera-11773178288.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178289.ts
Normal file
BIN
static/hls/camera-11773178289.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178290.ts
Normal file
BIN
static/hls/camera-11773178290.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178291.ts
Normal file
BIN
static/hls/camera-11773178291.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178292.ts
Normal file
BIN
static/hls/camera-11773178292.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178293.ts
Normal file
BIN
static/hls/camera-11773178293.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178294.ts
Normal file
BIN
static/hls/camera-11773178294.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178295.ts
Normal file
BIN
static/hls/camera-11773178295.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178296.ts
Normal file
BIN
static/hls/camera-11773178296.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178297.ts
Normal file
BIN
static/hls/camera-11773178297.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178298.ts
Normal file
BIN
static/hls/camera-11773178298.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178299.ts
Normal file
BIN
static/hls/camera-11773178299.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178300.ts
Normal file
BIN
static/hls/camera-11773178300.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178301.ts
Normal file
BIN
static/hls/camera-11773178301.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178302.ts
Normal file
BIN
static/hls/camera-11773178302.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178303.ts
Normal file
BIN
static/hls/camera-11773178303.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178304.ts
Normal file
BIN
static/hls/camera-11773178304.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178305.ts
Normal file
BIN
static/hls/camera-11773178305.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178306.ts
Normal file
BIN
static/hls/camera-11773178306.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178307.ts
Normal file
BIN
static/hls/camera-11773178307.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178308.ts
Normal file
BIN
static/hls/camera-11773178308.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178309.ts
Normal file
BIN
static/hls/camera-11773178309.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178310.ts
Normal file
BIN
static/hls/camera-11773178310.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178311.ts
Normal file
BIN
static/hls/camera-11773178311.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178312.ts
Normal file
BIN
static/hls/camera-11773178312.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178313.ts
Normal file
BIN
static/hls/camera-11773178313.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178314.ts
Normal file
BIN
static/hls/camera-11773178314.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178315.ts
Normal file
BIN
static/hls/camera-11773178315.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178316.ts
Normal file
BIN
static/hls/camera-11773178316.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178317.ts
Normal file
BIN
static/hls/camera-11773178317.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178318.ts
Normal file
BIN
static/hls/camera-11773178318.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178319.ts
Normal file
BIN
static/hls/camera-11773178319.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178320.ts
Normal file
BIN
static/hls/camera-11773178320.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178321.ts
Normal file
BIN
static/hls/camera-11773178321.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178322.ts
Normal file
BIN
static/hls/camera-11773178322.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178323.ts
Normal file
BIN
static/hls/camera-11773178323.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178324.ts
Normal file
BIN
static/hls/camera-11773178324.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178325.ts
Normal file
BIN
static/hls/camera-11773178325.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178326.ts
Normal file
BIN
static/hls/camera-11773178326.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178327.ts
Normal file
BIN
static/hls/camera-11773178327.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178328.ts
Normal file
BIN
static/hls/camera-11773178328.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178329.ts
Normal file
BIN
static/hls/camera-11773178329.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178330.ts
Normal file
BIN
static/hls/camera-11773178330.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178331.ts
Normal file
BIN
static/hls/camera-11773178331.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178332.ts
Normal file
BIN
static/hls/camera-11773178332.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178333.ts
Normal file
BIN
static/hls/camera-11773178333.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178334.ts
Normal file
BIN
static/hls/camera-11773178334.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178335.ts
Normal file
BIN
static/hls/camera-11773178335.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178336.ts
Normal file
BIN
static/hls/camera-11773178336.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178337.ts
Normal file
BIN
static/hls/camera-11773178337.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178338.ts
Normal file
BIN
static/hls/camera-11773178338.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178339.ts
Normal file
BIN
static/hls/camera-11773178339.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178340.ts
Normal file
BIN
static/hls/camera-11773178340.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178341.ts
Normal file
BIN
static/hls/camera-11773178341.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178342.ts
Normal file
BIN
static/hls/camera-11773178342.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178343.ts
Normal file
BIN
static/hls/camera-11773178343.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178344.ts
Normal file
BIN
static/hls/camera-11773178344.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178345.ts
Normal file
BIN
static/hls/camera-11773178345.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178346.ts
Normal file
BIN
static/hls/camera-11773178346.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178347.ts
Normal file
BIN
static/hls/camera-11773178347.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178348.ts
Normal file
BIN
static/hls/camera-11773178348.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178349.ts
Normal file
BIN
static/hls/camera-11773178349.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178350.ts
Normal file
BIN
static/hls/camera-11773178350.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178351.ts
Normal file
BIN
static/hls/camera-11773178351.ts
Normal file
Binary file not shown.
BIN
static/hls/camera-11773178352.ts
Normal file
BIN
static/hls/camera-11773178352.ts
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user