From 8f90fd7472f73e0b3ffdb918114b645c0fa563a2 Mon Sep 17 00:00:00 2001 From: ms Date: Thu, 15 Jan 2026 15:42:20 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8F=91=E5=B8=83=E5=BA=94=E7=94=A8=20Rust=20S?= =?UTF-8?q?rec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/README.md | 1 + README.md | 1 + apps/rust-srec/README.md | 94 ++++++++++++++++++++++++ apps/rust-srec/data.yml | 18 +++++ apps/rust-srec/dev/data.yml | 60 +++++++++++++++ apps/rust-srec/dev/docker-compose.yml | 70 ++++++++++++++++++ apps/rust-srec/dev/envs/default.env | 8 ++ apps/rust-srec/dev/envs/global.env | 2 + apps/rust-srec/dev/scripts/init.sh | 17 +++++ apps/rust-srec/dev/scripts/uninstall.sh | 10 +++ apps/rust-srec/dev/scripts/upgrade.sh | 17 +++++ apps/rust-srec/logo.png | Bin 0 -> 9641 bytes 12 files changed, 298 insertions(+) create mode 100644 apps/rust-srec/README.md create mode 100644 apps/rust-srec/data.yml create mode 100644 apps/rust-srec/dev/data.yml create mode 100644 apps/rust-srec/dev/docker-compose.yml create mode 100644 apps/rust-srec/dev/envs/default.env create mode 100644 apps/rust-srec/dev/envs/global.env create mode 100644 apps/rust-srec/dev/scripts/init.sh create mode 100644 apps/rust-srec/dev/scripts/uninstall.sh create mode 100644 apps/rust-srec/dev/scripts/upgrade.sh create mode 100644 apps/rust-srec/logo.png diff --git a/.github/README.md b/.github/README.md index 6722b0c68..3622fb9a3 100644 --- a/.github/README.md +++ b/.github/README.md @@ -182,6 +182,7 @@ | 🟢 | | SpeedTest-Tracker | https://docs.speedtest-tracker.dev/ | Speedtest Tracker 是一款自托管互联网性能跟踪应用程序,可针对 Ookla 的 Speedtest 服务运行速度测试检查 | | | 🟢 | | SPlayer | https://github.com/imsyy/SPlayer/ | 一个简约的音乐播放器 | | | 🟢 | | Stash | https://stashapp.cc/ | 组织和刮削你的色情内容 | | +| 🟢 | | Rust-Rec | https://github.com/hua0512/rust-rec/ | Rust-rec是一个用于各种流媒体服务的自动流媒体录制工具 | | | 🟢 | | Stream-Rec | https://github.com/hua0512/stream-rec/ | Stream-rec是一个用于各种流媒体服务的自动流媒体录制工具 | | | 🟢 | | Stream-Rec Web版 | https://github.com/hua0512/stream-rec/ | 【前端版】Stream-rec是一个用于各种流媒体服务的自动流媒体录制工具 | | | 🟢 | | Stream-Rec 服务版 | https://github.com/hua0512/stream-rec/ | 【服务版】Stream-rec是一个用于各种流媒体服务的自动流媒体录制工具 | | diff --git a/README.md b/README.md index 7ee4755d2..f0579eaf2 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,7 @@ | 🟢 | | SpeedTest-Tracker | https://docs.speedtest-tracker.dev/ | Speedtest Tracker 是一款自托管互联网性能跟踪应用程序,可针对 Ookla 的 Speedtest 服务运行速度测试检查 | | | 🟢 | | SPlayer | https://github.com/imsyy/SPlayer/ | 一个简约的音乐播放器 | | | 🟢 | | Stash | https://stashapp.cc/ | 组织和刮削你的色情内容 | | +| 🟢 | | Rust-Rec | https://github.com/hua0512/rust-rec/ | Rust-rec是一个用于各种流媒体服务的自动流媒体录制工具 | | | 🟢 | | Stream-Rec | https://github.com/hua0512/stream-rec/ | Stream-rec是一个用于各种流媒体服务的自动流媒体录制工具 | | | 🟢 | | Stream-Rec Web版 | https://github.com/hua0512/stream-rec/ | 【前端版】Stream-rec是一个用于各种流媒体服务的自动流媒体录制工具 | | | 🟢 | | Stream-Rec 服务版 | https://github.com/hua0512/stream-rec/ | 【服务版】Stream-rec是一个用于各种流媒体服务的自动流媒体录制工具 | | diff --git a/apps/rust-srec/README.md b/apps/rust-srec/README.md new file mode 100644 index 000000000..a1f1f7e8c --- /dev/null +++ b/apps/rust-srec/README.md @@ -0,0 +1,94 @@ +# Rust Srec + +自动化直播录制解决方案 (Stream Rec 迭代版) + +基于 Rust 构建的高性能多平台直播录制系统 + +![Rust-Rec](https://file.lifebus.top/imgs/rust_rec_cover.png) + +![](https://img.shields.io/badge/%E6%96%B0%E7%96%86%E8%90%8C%E6%A3%AE%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91%E5%B7%A5%E4%BD%9C%E5%AE%A4-%E6%8F%90%E4%BE%9B%E6%8A%80%E6%9C%AF%E6%94%AF%E6%8C%81-blue) + +## 安装说明 + +### JWT 签名密钥 生成 + +```shell +openssl rand -hex 32 +``` + +### 会话密钥 生成 + +```shell +openssl rand -hex 32 +``` + +### 持久化路径 + +目前使用 `应用持久化路径` 与 `数据持久化路径` 配置,分别用于存储应用数据和录制数据。 + ++ `数据持久化路径`: 是指录制数据存放的路径,占用空间较多,请自行配置。 ++ `应用持久化路径`: 存放应用配置文件,如:JWT 签名密钥、会话密钥、数据库配置、日志配置、DAG 配置等。 + +## 特性 + +### 🎬 多平台支持 + +支持 Bilibili、抖音、斗鱼、虎牙、Twitch、TikTok 等平台 + +### ⚡ 高性能 + +Rust 构建,高效且资源占用低 + +### 🔄 DAG 管道 + +强大的后处理工作流(有向无环图) + +### 🎯 灵活配置 + +4 层配置层级,精细化控制 + +### 💬 弹幕支持 + +同步录制弹幕 + +### 🐳 容器化部署 + +支持 Docker 和 docker-compose 快速部署 + +### 📅 自定义录制计划 + +定时录制,自动管理文件名,便于组织归档 + +### 🔧 FLV 修复 + +自动修复损坏的 FLV 流,包括时间戳校正和元数据修复 + +## 直播平台支持列表 + +| 平台 | URL 格式 | 协议 | 弹幕 | +|:----------------------------------------------------------------------------------------|:------------------------------------------------------|:--------|:---| +| [Bilibili](https://hua0512.github.io/rust-srec/zh/platforms/bilibili.html) | `live.bilibili.com/{room_id}` | FLV/HLS | ✅ | +| [抖音](https://hua0512.github.io/rust-srec/zh/platforms/douyin.html) | `live.douyin.com/{room_id}` | FLV/HLS | ✅ | +| [斗鱼](https://hua0512.github.io/rust-srec/zh/platforms/douyu.html) | `douyu.com/{room_id}` | FLV | ✅ | +| [虎牙](https://hua0512.github.io/rust-srec/zh/platforms/huya.html) | `huya.com/{room_id}` | FLV/HLS | ✅ | +| [AcFun](https://hua0512.github.io/rust-srec/zh/platforms/others.html#acfun) | `acfun.cn/live/{room_id}` | HLS | ❌ | +| [PandaTV](https://hua0512.github.io/rust-srec/zh/platforms/others.html#pandatv) | `pandalive.co.kr/play/{id}` | HLS | ❌ | +| [小红书](https://hua0512.github.io/rust-srec/zh/platforms/others.html#redbook-小红书) | `xiaohongshu.com/user/profile/{id} or xhs.link/{id}` | HLS | ❌ | +| [微博](https://hua0512.github.io/rust-srec/zh/platforms/others.html#weibo) | `weibo.com/u/{uid} or weibo.com/l/wblive/p/show/{id}` | HLS | ❌ | +| [Twitch](https://hua0512.github.io/rust-srec/zh/platforms/twitch.html) | `twitch.tv/{channel}` | HLS | ✅ | +| [TikTok](https://hua0512.github.io/rust-srec/zh/platforms/others.html#tiktok) | `tiktok.com/@{user}/live` | HLS | ❌ | +| [Twitcasting](https://hua0512.github.io/rust-srec/zh/platforms/others.html#twitcasting) | `twitcasting.tv/{user}` | HLS | ✅ | +| [Picarto](https://hua0512.github.io/rust-srec/zh/platforms/others.html#picarto) | `picarto.tv/{user}` | HLS/MP4 | ❌ | + +## 安装说明 + +> 默认账户 +> +> 用户名:admin +> +> 密码:admin123! 或 `登录密码 (初始化)` 配置 + +--- + +![Ms Studio](https://file.lifebus.top/imgs/ms_blank_001.png) +![Ms Studio](https://analytics.lifebus.top/p/wJix5nI1W) diff --git a/apps/rust-srec/data.yml b/apps/rust-srec/data.yml new file mode 100644 index 000000000..34b017c96 --- /dev/null +++ b/apps/rust-srec/data.yml @@ -0,0 +1,18 @@ +additionalProperties: + key: rust-srec + name: Rust Srec + tags: + - WebSite + - Tool + - Local + shortDescZh: 自动流媒体录制工具 + shortDescEn: Automatic streaming record tool + type: website + crossVersionUpdate: true + limit: 0 + website: https://hua0512.github.io/rust-srec/ + github: https://github.com/hua0512/rust-srec + document: https://hua0512.github.io/rust-srec/ + architectures: + - amd64 + - arm64 diff --git a/apps/rust-srec/dev/data.yml b/apps/rust-srec/dev/data.yml new file mode 100644 index 000000000..410c94d90 --- /dev/null +++ b/apps/rust-srec/dev/data.yml @@ -0,0 +1,60 @@ +additionalProperties: + formFields: + - default: "/home/rust-rec" + edit: true + envKey: RUST_REC_ROOT_PATH + labelZh: 应用持久化路径 + labelEn: Data persistence path + required: true + type: text + - default: "/home/rust-rec" + edit: true + envKey: RUST_REC_DATA_ROOT_PATH + labelZh: 数据持久化路径 + labelEn: Data persistence path + required: true + type: text + - default: 8088 + edit: true + envKey: PANEL_APP_PORT_HTTP + labelZh: WebUI 端口 + labelEn: WebUI port + required: true + rule: paramPort + type: number + - default: 8080 + edit: true + envKey: PANEL_APP_PORT_API + labelZh: API 端口 + labelEn: API port + required: true + rule: paramPort + type: number + - default: "" + edit: true + envKey: JWT_SECRET + labelZh: JWT 签名密钥 + labelEn: JWT Secret + required: true + type: text + - default: "" + edit: true + envKey: SESSION_SECRET + labelZh: 会话密钥 + labelEn: Session Secret + required: true + type: text + - default: "" + edit: true + envKey: HTTP_PROXY + labelZh: 网络代理 + labelEn: Network Proxy + required: false + type: text + - default: "localhost,127.0.0.1,rust-srec-backend" + edit: true + envKey: NO_PROXY + labelZh: 忽略代理 + labelEn: Ignore Proxy + required: true + type: text diff --git a/apps/rust-srec/dev/docker-compose.yml b/apps/rust-srec/dev/docker-compose.yml new file mode 100644 index 000000000..03a289e68 --- /dev/null +++ b/apps/rust-srec/dev/docker-compose.yml @@ -0,0 +1,70 @@ +networks: + 1panel-network: + external: true + +services: + rust-srec-backend: + image: ghcr.io/hua0512/rust-srec:dev + container_name: backend-${CONTAINER_NAME} + restart: always + networks: + - 1panel-network + ports: + - ${PANEL_APP_PORT_API}:8080 + env_file: + - ${GLOBAL_ENV_FILE:-/etc/1panel/envs/global.env} + - ${ENV_FILE:-/etc/1panel/envs/default.env} + volumes: + - ${RUST_REC_DATA_ROOT_PATH}/data:/app/data + - ${RUST_REC_DATA_ROOT_PATH}/output:/app/output + - ${RUST_REC_ROOT_PATH}/config:/app/config + - ${RUST_REC_ROOT_PATH}/logs:/app/logs + - ${RUST_REC_ROOT_PATH}/tmp:/tmp + environment: + - TZ=Asia/Shanghai + - RUST_LOG=${RUST_LOG:-info} + - DATABASE_URL=${DATABASE_URL:-sqlite:///app/data/rust-srec.db} + - API_PORT=8080 + - API_BIND_ADDRESS=0.0.0.0 + - ACCESS_TOKEN_EXPIRATION_SECS=${ACCESS_TOKEN_EXPIRATION_SECS:-3600} + - REFRESH_TOKEN_EXPIRATION_SECS=${REFRESH_TOKEN_EXPIRATION_SECS:-604800} + - MIN_PASSWORD_LENGTH=${MIN_PASSWORD_LENGTH:-8} + - HTTP_PROXY=${HTTP_PROXY:-} + - HTTPS_PROXY=${HTTP_PROXY:-} + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost:8080/api/health/live" ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + logging: + driver: "json-file" + options: + max-size: "100m" + max-file: "5" + + rust-rec-frontend: + depends_on: + rust-srec-backend: + condition: service_healthy + image: ghcr.io/hua0512/rust-srec-frontend:dev + container_name: ${CONTAINER_NAME} + labels: + createdBy: "Apps" + restart: always + networks: + - 1panel-network + ports: + - ${PANEL_APP_PORT_HTTP}:80 + env_file: + - ${GLOBAL_ENV_FILE:-/etc/1panel/envs/global.env} + - ${ENV_FILE:-/etc/1panel/envs/default.env} + environment: + - TZ=Asia/Shanghai + - BACKEND_URL=${BACKEND_URL:-http://rust-srec-backend:8080} + - COOKIE_SECURE=${COOKIE_SECURE:-} + logging: + driver: "json-file" + options: + max-size: "50m" + max-file: "3" diff --git a/apps/rust-srec/dev/envs/default.env b/apps/rust-srec/dev/envs/default.env new file mode 100644 index 000000000..405d4f599 --- /dev/null +++ b/apps/rust-srec/dev/envs/default.env @@ -0,0 +1,8 @@ +# copyright© 2024 XinJiang Ms Studio +ENV_FILE=.env +DATA_DIR=/app/data +CONFIG_DIR=/app/config +OUTPUT_DIR=/app/output +LOG_DIR=/app/logs +JWT_ISSUER=rust-srec +JWT_AUDIENCE=rust-srec-api diff --git a/apps/rust-srec/dev/envs/global.env b/apps/rust-srec/dev/envs/global.env new file mode 100644 index 000000000..e10989fe4 --- /dev/null +++ b/apps/rust-srec/dev/envs/global.env @@ -0,0 +1,2 @@ +# copyright© 2024 XinJiang Ms Studio +TZ=Asia/Shanghai diff --git a/apps/rust-srec/dev/scripts/init.sh b/apps/rust-srec/dev/scripts/init.sh new file mode 100644 index 000000000..07fb8c3fe --- /dev/null +++ b/apps/rust-srec/dev/scripts/init.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +if [ -f .env ]; then + source .env + + # setup-1 add default values + CURRENT_DIR=$(pwd) + sed -i '/^ENV_FILE=/d' .env + sed -i '/^GLOBAL_ENV_FILE=/d' .env + echo "ENV_FILE=${CURRENT_DIR}/.env" >> .env + echo "GLOBAL_ENV_FILE=${CURRENT_DIR}/envs/global.env" >> .env + + echo "Check Finish." + +else + echo "Error: .env file not found." +fi diff --git a/apps/rust-srec/dev/scripts/uninstall.sh b/apps/rust-srec/dev/scripts/uninstall.sh new file mode 100644 index 000000000..c86c4fbca --- /dev/null +++ b/apps/rust-srec/dev/scripts/uninstall.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +if [ -f .env ]; then + source .env + + echo "Check Finish." + +else + echo "Error: .env file not found." +fi diff --git a/apps/rust-srec/dev/scripts/upgrade.sh b/apps/rust-srec/dev/scripts/upgrade.sh new file mode 100644 index 000000000..07fb8c3fe --- /dev/null +++ b/apps/rust-srec/dev/scripts/upgrade.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +if [ -f .env ]; then + source .env + + # setup-1 add default values + CURRENT_DIR=$(pwd) + sed -i '/^ENV_FILE=/d' .env + sed -i '/^GLOBAL_ENV_FILE=/d' .env + echo "ENV_FILE=${CURRENT_DIR}/.env" >> .env + echo "GLOBAL_ENV_FILE=${CURRENT_DIR}/envs/global.env" >> .env + + echo "Check Finish." + +else + echo "Error: .env file not found." +fi diff --git a/apps/rust-srec/logo.png b/apps/rust-srec/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9bc4578a12f90d7307faf7a8958c888337b49f77 GIT binary patch literal 9641 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