init
This commit is contained in:
@@ -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
|
||||
@@ -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
@@ -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-*
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"svelte.svelte-vscode",
|
||||
"bradlc.vscode-tailwindcss"
|
||||
]
|
||||
}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"*.css": "tailwindcss"
|
||||
}
|
||||
}
|
||||
+46
@@ -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
@@ -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`
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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=="],
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Vendored
+13
@@ -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 {};
|
||||
@@ -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>
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
@@ -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 |
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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()}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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