Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe165295fb |
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -13,11 +13,11 @@ Before opening an issue, view the device's Console logs in the Scrypted Manageme
|
||||
|
||||
**DO NOT OPEN ISSUES FOR ANY OF THE FOLLOWING:**
|
||||
|
||||
* Server or hardware setup assistance. Use Discord, Reddit, or Github Discussions.
|
||||
* Server setup assistance. Use Discord, Reddit, or Github Discussions.
|
||||
* Hardware setup assistance. Use Discord, Reddit, or Github Discussions.
|
||||
* Feature Requests. Use Discord, Reddit, or Github Discussions.
|
||||
* Packet loss in your camera logs. This is wifi/network congestion.
|
||||
* HomeKit weirdness. See HomeKit troubleshooting guide.
|
||||
* Release schedules or timelines. Releases are rolled out unevenly across the different server platforms.
|
||||
|
||||
However, if something **was working**, and is now **no longer working**, you may create a Github issue.
|
||||
Created issues that do not meet these requirements or are improperly filled out will be immediately closed.
|
||||
|
||||
10
.github/workflows/build-plugins-changed.yml
vendored
@@ -1,11 +1,11 @@
|
||||
name: Build changed plugins
|
||||
|
||||
on:
|
||||
# push:
|
||||
# branches: ["main"]
|
||||
# paths: ["plugins/**"]
|
||||
# pull_request:
|
||||
# paths: ["plugins/**"]
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths: ["plugins/**"]
|
||||
pull_request:
|
||||
paths: ["plugins/**"]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
6
.github/workflows/build-sdk.yml
vendored
@@ -7,7 +7,7 @@ on:
|
||||
pull_request:
|
||||
paths: ["sdk/**"]
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
@@ -15,11 +15,11 @@ jobs:
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./sdk
|
||||
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.4.1
|
||||
node-version: 18
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
|
||||
97
.github/workflows/docker-common.yml
vendored
@@ -7,10 +7,13 @@ jobs:
|
||||
build:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: self-hosted
|
||||
env:
|
||||
NODE_VERSION: '20'
|
||||
# runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
NODE_VERSION: [
|
||||
# "18",
|
||||
"20"
|
||||
]
|
||||
BASE: ["jammy"]
|
||||
FLAVOR: ["full", "lite"]
|
||||
steps:
|
||||
@@ -20,26 +23,12 @@ jobs:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up SSH
|
||||
uses: MrSquaare/ssh-setup-action@v2
|
||||
with:
|
||||
host: ${{ secrets.DOCKER_SSH_HOST_AMD64 }}
|
||||
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up SSH
|
||||
uses: MrSquaare/ssh-setup-action@v2
|
||||
with:
|
||||
host: ${{ secrets.DOCKER_SSH_HOST_ARM64 }}
|
||||
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
append: |
|
||||
- endpoint: ssh://${{ secrets.DOCKER_SSH_USER }}@${{ secrets.DOCKER_SSH_HOST_AMD64 }}
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
@@ -65,84 +54,14 @@ jobs:
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
build-args: |
|
||||
NODE_VERSION=${{ env.NODE_VERSION }}
|
||||
NODE_VERSION=${{ matrix.NODE_VERSION }}
|
||||
BASE=${{ matrix.BASE }}
|
||||
context: install/docker/
|
||||
file: install/docker/Dockerfile.${{ matrix.FLAVOR }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
koush/scrypted-common:${{ matrix.BASE }}-${{ matrix.FLAVOR }}
|
||||
ghcr.io/koush/scrypted-common:${{ matrix.BASE }}-${{ matrix.FLAVOR }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
build-nvidia:
|
||||
name: Push NVIDIA Docker image to Docker Hub
|
||||
needs: build
|
||||
runs-on: self-hosted
|
||||
strategy:
|
||||
matrix:
|
||||
BASE: ["jammy"]
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up SSH
|
||||
uses: MrSquaare/ssh-setup-action@v2
|
||||
with:
|
||||
host: ${{ secrets.DOCKER_SSH_HOST_AMD64 }}
|
||||
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up SSH
|
||||
uses: MrSquaare/ssh-setup-action@v2
|
||||
with:
|
||||
host: ${{ secrets.DOCKER_SSH_HOST_ARM64 }}
|
||||
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
append: |
|
||||
- endpoint: ssh://${{ secrets.DOCKER_SSH_USER }}@${{ secrets.DOCKER_SSH_HOST_AMD64 }}
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
platforms: linux/arm64
|
||||
append: |
|
||||
- endpoint: ssh://${{ secrets.DOCKER_SSH_USER }}@${{ secrets.DOCKER_SSH_HOST_ARM64 }}
|
||||
platforms: linux/arm64
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to Github Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image (scrypted-common)
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
build-args: |
|
||||
BASE=ghcr.io/koush/scrypted-common:${{ matrix.BASE }}-full
|
||||
context: install/docker/
|
||||
file: install/docker/Dockerfile.nvidia
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
koush/scrypted-common:${{ matrix.BASE }}-nvidia
|
||||
ghcr.io/koush/scrypted-common:${{ matrix.BASE }}-nvidia
|
||||
koush/scrypted-common:${{ matrix.NODE_VERSION }}-${{ matrix.BASE }}-${{ matrix.FLAVOR }}
|
||||
ghcr.io/koush/scrypted-common:${{ matrix.NODE_VERSION }}-${{ matrix.BASE }}-${{ matrix.FLAVOR }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
44
.github/workflows/docker.yml
vendored
@@ -20,10 +20,10 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
BASE: [
|
||||
["jammy-nvidia", ".s6"],
|
||||
["jammy-full", ".s6"],
|
||||
["jammy-lite", ""],
|
||||
"20-jammy-full",
|
||||
"20-jammy-lite",
|
||||
]
|
||||
SUPERVISOR: ["", ".s6"]
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3
|
||||
@@ -42,26 +42,12 @@ jobs:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up SSH
|
||||
uses: MrSquaare/ssh-setup-action@v2
|
||||
with:
|
||||
host: ${{ secrets.DOCKER_SSH_HOST_AMD64 }}
|
||||
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up SSH
|
||||
uses: MrSquaare/ssh-setup-action@v2
|
||||
with:
|
||||
host: ${{ secrets.DOCKER_SSH_HOST_ARM64 }}
|
||||
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
append: |
|
||||
- endpoint: ssh://${{ secrets.DOCKER_SSH_USER }}@${{ secrets.DOCKER_SSH_HOST_AMD64 }}
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
@@ -87,23 +73,23 @@ jobs:
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
build-args: |
|
||||
BASE=${{ matrix.BASE[0] }}
|
||||
BASE=${{ matrix.BASE }}
|
||||
SCRYPTED_INSTALL_VERSION=${{ steps.package-version.outputs.NPM_VERSION }}
|
||||
context: install/docker/
|
||||
file: install/docker/Dockerfile${{ matrix.BASE[1] }}
|
||||
file: install/docker/Dockerfile${{ matrix.SUPERVISOR }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
${{ format('koush/scrypted:v{1}-{0}', matrix.BASE[0], github.event.inputs.publish_tag || steps.package-version.outputs.NPM_VERSION) }}
|
||||
${{ matrix.BASE[0] == 'jammy-full' && format('koush/scrypted:{0}', github.event.inputs.tag) || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-nvidia' && 'koush/scrypted:nvidia' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-full' && 'koush/scrypted:full' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-lite' && 'koush/scrypted:lite' || '' }}
|
||||
${{ format('koush/scrypted:{0}{1}-v{2}', matrix.BASE, matrix.SUPERVISOR, github.event.inputs.publish_tag || steps.package-version.outputs.NPM_VERSION) }}
|
||||
${{ matrix.BASE == '20-jammy-full' && matrix.SUPERVISOR == '.s6' && format('koush/scrypted:{0}', github.event.inputs.tag) || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '20-jammy-full' && matrix.SUPERVISOR == '' && 'koush/scrypted:full' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '20-jammy-lite' && matrix.SUPERVISOR == '' && 'koush/scrypted:lite' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '20-jammy-lite' && matrix.SUPERVISOR == '.s6' && 'koush/scrypted:lite-s6' || '' }}
|
||||
|
||||
${{ format('ghcr.io/koush/scrypted:v{1}-{0}', matrix.BASE[0], github.event.inputs.publish_tag || steps.package-version.outputs.NPM_VERSION) }}
|
||||
${{ matrix.BASE[0] == 'jammy-full' && format('ghcr.io/koush/scrypted:{0}', github.event.inputs.tag) || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-nvidia' && 'ghcr.io/koush/scrypted:nvidia' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-full' && 'ghcr.io/koush/scrypted:full' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-lite' && 'ghcr.io/koush/scrypted:lite' || '' }}
|
||||
${{ format('ghcr.io/koush/scrypted:{0}{1}-v{2}', matrix.BASE, matrix.SUPERVISOR, github.event.inputs.publish_tag || steps.package-version.outputs.NPM_VERSION) }}
|
||||
${{ matrix.BASE == '20-jammy-full' && matrix.SUPERVISOR == '.s6' && format('ghcr.io/koush/scrypted:{0}', github.event.inputs.tag) || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '20-jammy-full' && matrix.SUPERVISOR == '' && 'ghcr.io/koush/scrypted:full' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '20-jammy-lite' && matrix.SUPERVISOR == '' && 'ghcr.io/koush/scrypted:lite' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '20-jammy-lite' && matrix.SUPERVISOR == '.s6' && 'ghcr.io/koush/scrypted:lite-s6' || '' }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
44
.github/workflows/static-sites.yml
vendored
@@ -1,44 +0,0 @@
|
||||
# Simple workflow for deploying static content to GitHub Pages
|
||||
name: Deploy static content to Pages
|
||||
|
||||
on:
|
||||
# Runs on pushes targeting the default branch
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths: ["sites/static/**", ".github/workflows/static-sites.yml"]
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# Single deploy job since we're just deploying
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
# Upload entire repository
|
||||
path: './sites/static'
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
64
.github/workflows/test.yml
vendored
@@ -9,28 +9,52 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test_local:
|
||||
name: Test local installation on ${{ matrix.runner }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
runner: [ubuntu-latest, macos-14, macos-13, windows-latest]
|
||||
test_linux_local:
|
||||
name: Test Linux local installation
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Parse latest server release
|
||||
id: parse_server
|
||||
shell: bash
|
||||
|
||||
- name: Run install script
|
||||
run: |
|
||||
VERSION=$(cat ./server/package-lock.json | jq -r '.version')
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "Will test @scrypted/server@$VERSION"
|
||||
|
||||
- name: Install scrypted server
|
||||
uses: scryptedapp/setup-scrypted@v0.0.2
|
||||
with:
|
||||
branch: ${{ github.sha }}
|
||||
version: ${{ steps.parse_server.outputs.version }}
|
||||
cat ./install/local/install-scrypted-dependencies-linux.sh | sudo SERVICE_USER=$USER bash
|
||||
|
||||
- name: Test server is running
|
||||
run: |
|
||||
systemctl status scrypted.service
|
||||
curl -k --retry 20 --retry-all-errors --retry-max-time 600 https://localhost:10443/
|
||||
|
||||
test_mac_local:
|
||||
name: Test Mac local installation
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Run install script
|
||||
run: |
|
||||
mkdir -p ~/.scrypted
|
||||
bash ./install/local/install-scrypted-dependencies-mac.sh
|
||||
|
||||
- name: Test server is running
|
||||
run: |
|
||||
curl -k --retry 20 --retry-all-errors --retry-max-time 600 https://localhost:10443/
|
||||
|
||||
test_windows_local:
|
||||
name: Test Windows local installation
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Run install script
|
||||
run: |
|
||||
.\install\local\install-scrypted-dependencies-win.ps1
|
||||
|
||||
- name: Test server is running
|
||||
run: |
|
||||
curl -k --retry 20 --retry-all-errors --retry-max-time 600 https://localhost:10443/
|
||||
|
||||
6
.gitmodules
vendored
@@ -1,6 +1,9 @@
|
||||
[submodule "plugins/unifi-protect/src/unifi-protect"]
|
||||
path = external/unifi-protect
|
||||
url = ../../koush/unifi-protect.git
|
||||
[submodule "plugins/myq/src/myq"]
|
||||
path = plugins/myq/src/myq
|
||||
url = ../../koush/myq.git
|
||||
[submodule "external/ring-client-api"]
|
||||
path = external/ring-client-api
|
||||
url = ../../koush/ring
|
||||
@@ -11,6 +14,9 @@
|
||||
[submodule "external/werift"]
|
||||
path = external/werift
|
||||
url = ../../koush/werift-webrtc
|
||||
[submodule "plugins/zwave/file-stream-rotator"]
|
||||
path = plugins/zwave/file-stream-rotator
|
||||
url = ../../koush/file-stream-rotator.git
|
||||
[submodule "sdk/developer.scrypted.app"]
|
||||
path = sdk/developer.scrypted.app
|
||||
url = ../../koush/developer.scrypted.app
|
||||
|
||||
1
common/fs/@types/sdk/settings-mixin.d.ts
vendored
@@ -1 +0,0 @@
|
||||
../../../../sdk/dist/src/settings-mixin.d.ts
|
||||
1
common/fs/@types/sdk/storage-settings.d.ts
vendored
@@ -1 +0,0 @@
|
||||
../../../../sdk/dist/src/storage-settings.d.ts
|
||||
145
common/package-lock.json
generated
@@ -10,12 +10,12 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"typescript": "^5.5.3"
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
},
|
||||
@@ -74,7 +74,7 @@
|
||||
},
|
||||
"../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.45",
|
||||
"version": "0.3.4",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
@@ -111,58 +111,64 @@
|
||||
},
|
||||
"../server": {
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.115.0",
|
||||
"extraneous": true,
|
||||
"hasInstallScript": true,
|
||||
"version": "0.82.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@mapbox/node-pre-gyp": "^1.0.11",
|
||||
"@scrypted/ffmpeg-static": "^6.1.0-build1",
|
||||
"@scrypted/node-pty": "^1.0.18",
|
||||
"@scrypted/types": "^0.3.33",
|
||||
"adm-zip": "^0.5.14",
|
||||
"@scrypted/types": "^0.3.4",
|
||||
"adm-zip": "^0.5.10",
|
||||
"body-parser": "^1.20.2",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"dotenv": "^16.4.5",
|
||||
"engine.io": "^6.6.0",
|
||||
"express": "^4.19.2",
|
||||
"follow-redirects": "^1.15.6",
|
||||
"debug": "^4.3.4",
|
||||
"engine.io": "^6.5.4",
|
||||
"express": "^4.18.2",
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
"follow-redirects": "^1.15.4",
|
||||
"http-auth": "^4.2.0",
|
||||
"ip": "^2.0.1",
|
||||
"level": "^8.0.1",
|
||||
"ip": "^1.1.8",
|
||||
"level": "^8.0.0",
|
||||
"linkfs": "^2.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"nan": "^2.20.0",
|
||||
"memfs": "^4.6.0",
|
||||
"mime": "^3.0.0",
|
||||
"nan": "^2.18.0",
|
||||
"node-dijkstra": "^2.5.0",
|
||||
"node-forge": "^1.3.1",
|
||||
"node-gyp": "^10.1.0",
|
||||
"py": "npm:@bjia56/portable-python@^0.1.54",
|
||||
"node-gyp": "^10.0.1",
|
||||
"router": "^1.3.8",
|
||||
"semver": "^7.6.2",
|
||||
"sharp": "^0.33.4",
|
||||
"semver": "^7.5.4",
|
||||
"sharp": "^0.33.1",
|
||||
"source-map-support": "^0.5.21",
|
||||
"tar": "^7.4.0",
|
||||
"tslib": "^2.6.3",
|
||||
"typescript": "^5.5.3",
|
||||
"tar": "^6.2.0",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.3.3",
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"ws": "^8.18.0"
|
||||
"ws": "^8.16.0"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted-serve": "bin/scrypted-serve"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/adm-zip": "^0.5.5",
|
||||
"@types/cookie-parser": "^1.4.7",
|
||||
"@types/cookie-parser": "^1.4.6",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/follow-redirects": "^1.14.4",
|
||||
"@types/http-auth": "^4.1.4",
|
||||
"@types/ip": "^1.1.3",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/mime": "^3.0.4",
|
||||
"@types/node-dijkstra": "^2.5.6",
|
||||
"@types/node-forge": "^1.3.11",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/node-forge": "^1.3.10",
|
||||
"@types/pem": "^1.14.4",
|
||||
"@types/semver": "^7.5.6",
|
||||
"@types/source-map-support": "^0.5.10",
|
||||
"@types/tar": "^6.1.10",
|
||||
"@types/whatwg-mimetype": "^3.0.2",
|
||||
"@types/ws": "^8.5.10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"node-pty-prebuilt-multiarch": "^0.10.1-pre.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
@@ -206,6 +212,10 @@
|
||||
"resolved": "../sdk",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@scrypted/server": {
|
||||
"resolved": "../server",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
|
||||
@@ -298,12 +308,6 @@
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/monaco-editor": {
|
||||
"version": "0.50.0",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.50.0.tgz",
|
||||
"integrity": "sha512-8CclLCmrRRh+sul7C08BmPBP3P8wVWfBHomsTcndxg5NRCEPfu/mc2AGU8k37ajjDVXcXFc12ORAMUkmk+lkFA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
@@ -348,9 +352,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
|
||||
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
|
||||
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -445,6 +449,59 @@
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
}
|
||||
},
|
||||
"@scrypted/server": {
|
||||
"version": "file:../server",
|
||||
"requires": {
|
||||
"@mapbox/node-pre-gyp": "^1.0.11",
|
||||
"@scrypted/types": "^0.3.4",
|
||||
"@types/adm-zip": "^0.5.5",
|
||||
"@types/cookie-parser": "^1.4.6",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/follow-redirects": "^1.14.4",
|
||||
"@types/http-auth": "^4.1.4",
|
||||
"@types/ip": "^1.1.3",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/mime": "^3.0.4",
|
||||
"@types/node-dijkstra": "^2.5.6",
|
||||
"@types/node-forge": "^1.3.10",
|
||||
"@types/pem": "^1.14.4",
|
||||
"@types/semver": "^7.5.6",
|
||||
"@types/source-map-support": "^0.5.10",
|
||||
"@types/tar": "^6.1.10",
|
||||
"@types/whatwg-mimetype": "^3.0.2",
|
||||
"@types/ws": "^8.5.10",
|
||||
"adm-zip": "^0.5.10",
|
||||
"body-parser": "^1.20.2",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"debug": "^4.3.4",
|
||||
"engine.io": "^6.5.4",
|
||||
"express": "^4.18.2",
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
"follow-redirects": "^1.15.4",
|
||||
"http-auth": "^4.2.0",
|
||||
"ip": "^1.1.8",
|
||||
"level": "^8.0.0",
|
||||
"linkfs": "^2.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"memfs": "^4.6.0",
|
||||
"mime": "^3.0.0",
|
||||
"nan": "^2.18.0",
|
||||
"node-dijkstra": "^2.5.0",
|
||||
"node-forge": "^1.3.1",
|
||||
"node-gyp": "^10.0.1",
|
||||
"node-pty-prebuilt-multiarch": "^0.10.1-pre.5",
|
||||
"router": "^1.3.8",
|
||||
"semver": "^7.5.4",
|
||||
"sharp": "^0.33.1",
|
||||
"source-map-support": "^0.5.21",
|
||||
"tar": "^6.2.0",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.3.3",
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"ws": "^8.16.0"
|
||||
}
|
||||
},
|
||||
"@tsconfig/node10": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
|
||||
@@ -522,12 +579,6 @@
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true
|
||||
},
|
||||
"monaco-editor": {
|
||||
"version": "0.50.0",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.50.0.tgz",
|
||||
"integrity": "sha512-8CclLCmrRRh+sul7C08BmPBP3P8wVWfBHomsTcndxg5NRCEPfu/mc2AGU8k37ajjDVXcXFc12ORAMUkmk+lkFA==",
|
||||
"dev": true
|
||||
},
|
||||
"ts-node": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
@@ -550,9 +601,9 @@
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
|
||||
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ=="
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
|
||||
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw=="
|
||||
},
|
||||
"undici-types": {
|
||||
"version": "5.26.5",
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"typescript": "^5.5.3"
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
export function createActivityTimeout(timeout: number, timeoutCallback: () => void) {
|
||||
let dataTimeout: NodeJS.Timeout;
|
||||
|
||||
let lastTime = Date.now();
|
||||
function resetActivityTimer() {
|
||||
lastTime = Date.now();
|
||||
}
|
||||
|
||||
function clearActivityTimer() {
|
||||
clearInterval(dataTimeout);
|
||||
}
|
||||
|
||||
if (timeout) {
|
||||
dataTimeout = setInterval(() => {
|
||||
if (Date.now() > lastTime + timeout) {
|
||||
clearInterval(dataTimeout);
|
||||
dataTimeout = undefined;
|
||||
timeoutCallback();
|
||||
}
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
resetActivityTimer();
|
||||
return {
|
||||
resetActivityTimer,
|
||||
clearActivityTimer,
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,7 @@ export function createAsyncQueue<T>() {
|
||||
}
|
||||
catch (e) {
|
||||
// the yield above may raise an error, and the queue should be ended.
|
||||
end(e as Error);
|
||||
end(e);
|
||||
if (e instanceof EndError)
|
||||
return;
|
||||
throw e;
|
||||
@@ -155,23 +155,6 @@ export function createAsyncQueue<T>() {
|
||||
}
|
||||
}
|
||||
|
||||
export function createAsyncQueueFromGenerator<T>(generator: AsyncGenerator<T>) {
|
||||
const q = createAsyncQueue<T>();
|
||||
(async() => {
|
||||
try {
|
||||
for await (const i of generator) {
|
||||
q.submit(i);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
q.end(e as Error);
|
||||
}
|
||||
q.end();
|
||||
})();
|
||||
|
||||
return q;
|
||||
}
|
||||
|
||||
// async function testSlowEnqueue() {
|
||||
// const asyncQueue = createAsyncQueue<number>();
|
||||
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
import sdk, { AudioStreamOptions, MediaStreamConfiguration, MediaStreamDestination, MediaStreamOptions, ScryptedDeviceBase, Setting } from "@scrypted/sdk";
|
||||
|
||||
export const automaticallyConfigureSettings: Setting = {
|
||||
key: 'autoconfigure',
|
||||
title: 'Automatically Configure Settings',
|
||||
description: 'Automatically configure and valdiate the camera codecs and other settings for optimal Scrypted performance. Some settings will require manual configuration via the camera web admin.',
|
||||
type: 'boolean',
|
||||
value: true,
|
||||
};
|
||||
|
||||
export const onvifAutoConfigureSettings: Setting = {
|
||||
key: 'onvif-autoconfigure',
|
||||
type: 'html',
|
||||
value: 'ONVIF autoconfiguration will configure the camera codecs. <b>The camera motion sensor must still be <a target="_blank" href="https://docs.scrypted.app/camera-preparation.html#motion-sensor-setup">configured manually</a>.</b>',
|
||||
};
|
||||
|
||||
const MEGABIT = 1024 * 1000;
|
||||
|
||||
function getBitrateForResolution(resolution: number) {
|
||||
if (resolution >= 3840 * 2160)
|
||||
return 8 * MEGABIT;
|
||||
if (resolution >= 2688 * 1520)
|
||||
return 3 * MEGABIT;
|
||||
if (resolution >= 1920 * 1080)
|
||||
return 2 * MEGABIT;
|
||||
if (resolution >= 1280 * 720)
|
||||
return MEGABIT;
|
||||
if (resolution >= 640 * 480)
|
||||
return MEGABIT / 2;
|
||||
return MEGABIT / 4;
|
||||
}
|
||||
|
||||
export async function checkPluginNeedsAutoConfigure(plugin: ScryptedDeviceBase, extraDevices = 0) {
|
||||
if (plugin.storage.getItem('autoconfigure') === 'true')
|
||||
return;
|
||||
|
||||
plugin.storage.setItem('autoconfigure', 'true');
|
||||
if (sdk.deviceManager.getNativeIds().length <= 1 + extraDevices)
|
||||
return;
|
||||
plugin.log.a(`${plugin.name} now has support for automatic camera configuration for optimal performance. Cameras can be autoconfigured in their respective settings.`);
|
||||
}
|
||||
|
||||
export async function autoconfigureCodecs(
|
||||
getCodecs: () => Promise<MediaStreamOptions[]>,
|
||||
configureCodecs: (options: MediaStreamOptions) => Promise<MediaStreamConfiguration>,
|
||||
audioOptions?: AudioStreamOptions,
|
||||
) {
|
||||
audioOptions ||= {
|
||||
codec: 'pcm_mulaw',
|
||||
bitrate: 64000,
|
||||
sampleRate: 8000,
|
||||
};
|
||||
|
||||
const codecs = await getCodecs();
|
||||
const configurable: MediaStreamConfiguration[] = [];
|
||||
for (const codec of codecs) {
|
||||
const config = await configureCodecs({
|
||||
id: codec.id,
|
||||
});
|
||||
configurable.push(config);
|
||||
}
|
||||
|
||||
const used: MediaStreamConfiguration[] = [];
|
||||
|
||||
for (const _ of ['local', 'remote', 'low-resolution'] as MediaStreamDestination[]) {
|
||||
// find stream with the highest configurable resolution.
|
||||
let highest: [MediaStreamConfiguration, number] = [undefined, 0];
|
||||
for (const codec of configurable) {
|
||||
if (used.includes(codec))
|
||||
continue;
|
||||
for (const resolution of codec.video.resolutions) {
|
||||
if (resolution[0] * resolution[1] > highest[1]) {
|
||||
highest = [codec, resolution[0] * resolution[1]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const config = highest[0];
|
||||
if (!config)
|
||||
break;
|
||||
|
||||
used.push(config);
|
||||
}
|
||||
|
||||
const findResolutionTarget = (config: MediaStreamConfiguration, width: number, height: number) => {
|
||||
let diff = 999999999;
|
||||
let ret: [number, number];
|
||||
|
||||
const targetArea = width * height;
|
||||
for (const res of config.video.resolutions) {
|
||||
const actualArea = res[0] * res[1];
|
||||
const diffArea = Math.abs(targetArea - actualArea);
|
||||
if (diffArea < diff) {
|
||||
diff = diffArea;
|
||||
ret = res;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// find the highest resolution
|
||||
const l = used[0];
|
||||
const resolution = findResolutionTarget(l, 8192, 8192);
|
||||
|
||||
// get the fps of 20 or highest available
|
||||
let fps = Math.min(20, Math.max(...l.video.fpsRange));
|
||||
|
||||
let errors = '';
|
||||
|
||||
const logConfigureCodecs = async (config: MediaStreamConfiguration) => {
|
||||
try {
|
||||
await configureCodecs(config);
|
||||
}
|
||||
catch (e) {
|
||||
errors += e;
|
||||
}
|
||||
}
|
||||
|
||||
await logConfigureCodecs({
|
||||
id: l.id,
|
||||
video: {
|
||||
width: resolution[0],
|
||||
height: resolution[1],
|
||||
bitrateControl: 'variable',
|
||||
codec: 'h264',
|
||||
bitrate: getBitrateForResolution(resolution[0] * resolution[1]),
|
||||
fps,
|
||||
keyframeInterval: fps * 4,
|
||||
quality: 5,
|
||||
profile: 'main',
|
||||
},
|
||||
audio: audioOptions,
|
||||
});
|
||||
|
||||
if (used.length === 3) {
|
||||
// find remote and low
|
||||
const r = used[1];
|
||||
const l = used[2];
|
||||
|
||||
const rResolution = findResolutionTarget(r, 1280, 720);
|
||||
const lResolution = findResolutionTarget(l, 640, 360);
|
||||
|
||||
fps = Math.min(20, Math.max(...r.video.fpsRange));
|
||||
await logConfigureCodecs({
|
||||
id: r.id,
|
||||
video: {
|
||||
width: rResolution[0],
|
||||
height: rResolution[1],
|
||||
bitrateControl: 'variable',
|
||||
codec: 'h264',
|
||||
bitrate: 1 * MEGABIT,
|
||||
fps,
|
||||
keyframeInterval: fps * 4,
|
||||
quality: 5,
|
||||
profile: 'main',
|
||||
},
|
||||
audio: audioOptions,
|
||||
});
|
||||
|
||||
fps = Math.min(20, Math.max(...l.video.fpsRange));
|
||||
await logConfigureCodecs({
|
||||
id: l.id,
|
||||
video: {
|
||||
width: lResolution[0],
|
||||
height: lResolution[1],
|
||||
bitrateControl: 'variable',
|
||||
codec: 'h264',
|
||||
bitrate: MEGABIT / 2,
|
||||
fps,
|
||||
keyframeInterval: fps * 4,
|
||||
quality: 5,
|
||||
profile: 'main',
|
||||
},
|
||||
audio: audioOptions,
|
||||
});
|
||||
}
|
||||
else if (used.length == 2) {
|
||||
let target: [number, number];
|
||||
if (resolution[0] * resolution[1] > 1920 * 1080)
|
||||
target = [1280, 720];
|
||||
else
|
||||
target = [640, 360];
|
||||
|
||||
const rResolution = findResolutionTarget(used[1], target[0], target[1]);
|
||||
const fps = Math.min(20, Math.max(...used[1].video.fpsRange));
|
||||
await logConfigureCodecs({
|
||||
id: used[1].id,
|
||||
video: {
|
||||
width: rResolution[0],
|
||||
height: rResolution[1],
|
||||
bitrateControl: 'variable',
|
||||
codec: 'h264',
|
||||
bitrate: getBitrateForResolution(rResolution[0] * rResolution[1]),
|
||||
fps,
|
||||
keyframeInterval: fps * 4,
|
||||
quality: 5,
|
||||
profile: 'main',
|
||||
},
|
||||
audio: audioOptions,
|
||||
});
|
||||
}
|
||||
else if (used.length === 1) {
|
||||
// no nop
|
||||
}
|
||||
|
||||
if (errors)
|
||||
throw new Error(errors);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
../../server/src/deferred.ts
|
||||
17
common/src/deferred.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export class Deferred<T> {
|
||||
finished = false;
|
||||
resolve!: (value: T|PromiseLike<T>) => this;
|
||||
reject!: (error: Error) => this;
|
||||
promise: Promise<T> = new Promise((resolve, reject) => {
|
||||
this.resolve = v => {
|
||||
this.finished = true;
|
||||
resolve(v);
|
||||
return this;
|
||||
};
|
||||
this.reject = e => {
|
||||
this.finished = true;
|
||||
reject(e);
|
||||
return this;
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import type * as monacoEditor from 'monaco-editor';
|
||||
|
||||
export interface StandardLibs {
|
||||
'@types/node/globals.d.ts': string,
|
||||
'@types/node/buffer.d.ts': string,
|
||||
'@types/node/process.d.ts': string,
|
||||
'@types/node/events.d.ts': string,
|
||||
'@types/node/stream.d.ts': string,
|
||||
'@types/node/fs.d.ts': string,
|
||||
'@types/node/net.d.ts': string,
|
||||
'@types/node/child_process.d.ts': string,
|
||||
}
|
||||
|
||||
export interface ScryptedLibs {
|
||||
'@types/sdk/settings-mixin.d.ts': string,
|
||||
'@types/sdk/storage-settings.d.ts': string,
|
||||
'@types/sdk/types.d.ts': string,
|
||||
'@types/sdk/index.d.ts': string,
|
||||
}
|
||||
|
||||
export function createMonacoEvalDefaultsWithLibs(standardLibs: StandardLibs, scryptedLibs: ScryptedLibs, extraLibs: { [lib: string]: string }) {
|
||||
// const libs = Object.assign(scryptedLibs, extraLibs);
|
||||
|
||||
function monacoEvalDefaultsFunction(monaco: typeof monacoEditor, standardLibs: StandardLibs, scryptedLibs: ScryptedLibs, extraLibs: { [lib: string]: string }) {
|
||||
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions(
|
||||
Object.assign(
|
||||
{},
|
||||
monaco.languages.typescript.typescriptDefaults.getDiagnosticsOptions(),
|
||||
{
|
||||
diagnosticCodesToIgnore: [1108, 1375, 1378],
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions(
|
||||
Object.assign(
|
||||
{},
|
||||
monaco.languages.typescript.typescriptDefaults.getCompilerOptions(),
|
||||
{
|
||||
moduleResolution:
|
||||
monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const libs: any = {
|
||||
...scryptedLibs,
|
||||
...extraLibs,
|
||||
};
|
||||
|
||||
const catLibs = Object.values(libs).join('\n');
|
||||
const catlibsNoExport = Object.keys(libs)
|
||||
.map(lib => libs[lib]).map(lib =>
|
||||
lib.toString().replace(/export /g, '').replace(/import.*?/g, ''))
|
||||
.join('\n');
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(`
|
||||
${catLibs}
|
||||
|
||||
declare global {
|
||||
${catlibsNoExport}
|
||||
|
||||
const log: Logger;
|
||||
|
||||
const deviceManager: DeviceManager;
|
||||
const endpointManager: EndpointManager;
|
||||
const mediaManager: MediaManager;
|
||||
const systemManager: SystemManager;
|
||||
|
||||
const eventSource: ScryptedDevice;
|
||||
const eventDetails: EventDetails;
|
||||
const eventData: any;
|
||||
}
|
||||
`,
|
||||
|
||||
"node_modules/@types/scrypted__sdk/types/index.d.ts"
|
||||
);
|
||||
|
||||
for (const lib of Object.keys(standardLibs)) {
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
||||
standardLibs[lib as keyof StandardLibs],
|
||||
lib,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return `(function() {
|
||||
const standardLibs = ${JSON.stringify(standardLibs)};
|
||||
const scryptedLibs = ${JSON.stringify(scryptedLibs)};
|
||||
const extraLibs = ${JSON.stringify(extraLibs)};
|
||||
|
||||
return (monaco) => {
|
||||
(${monacoEvalDefaultsFunction})(monaco, standardLibs, scryptedLibs, extraLibs);
|
||||
}
|
||||
})();
|
||||
`;
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { ScryptedDeviceBase } from "@scrypted/sdk";
|
||||
|
||||
export interface ScriptDevice {
|
||||
/**
|
||||
* @deprecated Use the default export to specify the device handler.
|
||||
@@ -8,5 +6,3 @@ export interface ScriptDevice {
|
||||
handle<T>(handler?: T & object): void;
|
||||
handleTypes(...interfaces: string[]): void;
|
||||
}
|
||||
|
||||
export declare const device: ScryptedDeviceBase & ScriptDevice;
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import sdk, { MixinDeviceBase, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedInterfaceDescriptors, ScryptedMimeTypes } from "@scrypted/sdk";
|
||||
import { SettingsMixinDeviceBase } from "@scrypted/sdk/settings-mixin";
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import sdk, { MixinDeviceBase, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedInterfaceDescriptors } from "@scrypted/sdk";
|
||||
import fs from 'fs';
|
||||
import type { TranspileOptions } from "typescript";
|
||||
import vm from "vm";
|
||||
import { createMonacoEvalDefaultsWithLibs, ScryptedLibs, StandardLibs } from "./monaco-libs";
|
||||
import { ScriptDevice } from "./monaco/script-device";
|
||||
import path from 'path';
|
||||
|
||||
const { systemManager, deviceManager, mediaManager, endpointManager } = sdk;
|
||||
|
||||
@@ -29,18 +27,18 @@ export function readFileAsString(f: string) {
|
||||
return fs.readFileSync(f).toString();;
|
||||
}
|
||||
|
||||
function getScryptedLibs(): ScryptedLibs {
|
||||
function getTypeDefs() {
|
||||
const scryptedTypesDefs = readFileAsString('@types/sdk/types.d.ts');
|
||||
const scryptedIndexDefs = readFileAsString('@types/sdk/index.d.ts');
|
||||
return {
|
||||
"@types/sdk/index.d.ts": readFileAsString('@types/sdk/index.d.ts'),
|
||||
"@types/sdk/settings-mixin.d.ts": readFileAsString('@types/sdk/settings-mixin.d.ts'),
|
||||
"@types/sdk/storage-settings.d.ts": readFileAsString('@types/sdk/storage-settings.d.ts'),
|
||||
"@types/sdk/types.d.ts": readFileAsString('@types/sdk/types.d.ts'),
|
||||
}
|
||||
scryptedIndexDefs,
|
||||
scryptedTypesDefs,
|
||||
};
|
||||
}
|
||||
|
||||
export async function scryptedEval(device: ScryptedDeviceBase, script: string, extraLibs: { [lib: string]: string }, params: { [name: string]: any }) {
|
||||
const libs = Object.assign({
|
||||
types: getScryptedLibs()['@types/sdk/types.d.ts'],
|
||||
types: getTypeDefs().scryptedTypesDefs,
|
||||
}, extraLibs);
|
||||
const allScripts = Object.values(libs).join('\n').toString() + script;
|
||||
let compiled: string;
|
||||
@@ -66,7 +64,6 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
|
||||
fs: require('realfs'),
|
||||
ScryptedDeviceBase,
|
||||
MixinDeviceBase,
|
||||
StorageSettings,
|
||||
systemManager,
|
||||
deviceManager,
|
||||
endpointManager,
|
||||
@@ -76,8 +73,6 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
|
||||
localStorage: device.storage,
|
||||
device,
|
||||
exports: {} as any,
|
||||
SettingsMixinDeviceBase,
|
||||
ScryptedMimeTypes,
|
||||
ScryptedInterface,
|
||||
ScryptedDeviceType,
|
||||
// @ts-expect-error
|
||||
@@ -114,18 +109,92 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
|
||||
}
|
||||
|
||||
export function createMonacoEvalDefaults(extraLibs: { [lib: string]: string }) {
|
||||
const standardlibs: StandardLibs = {
|
||||
"@types/node/globals.d.ts": readFileAsString('@types/node/globals.d.ts'),
|
||||
"@types/node/buffer.d.ts": readFileAsString('@types/node/buffer.d.ts'),
|
||||
"@types/node/process.d.ts": readFileAsString('@types/node/process.d.ts'),
|
||||
"@types/node/events.d.ts": readFileAsString('@types/node/events.d.ts'),
|
||||
"@types/node/stream.d.ts": readFileAsString('@types/node/stream.d.ts'),
|
||||
"@types/node/fs.d.ts": readFileAsString('@types/node/fs.d.ts'),
|
||||
"@types/node/net.d.ts": readFileAsString('@types/node/net.d.ts'),
|
||||
"@types/node/child_process.d.ts": readFileAsString('@types/node/child_process.d.ts'),
|
||||
};
|
||||
const safeLibs: any = {};
|
||||
|
||||
return createMonacoEvalDefaultsWithLibs(standardlibs, getScryptedLibs(), extraLibs);
|
||||
for (const safeLib of [
|
||||
'@types/node/globals.d.ts',
|
||||
'@types/node/buffer.d.ts',
|
||||
'@types/node/process.d.ts',
|
||||
'@types/node/events.d.ts',
|
||||
'@types/node/stream.d.ts',
|
||||
'@types/node/fs.d.ts',
|
||||
'@types/node/net.d.ts',
|
||||
'@types/node/child_process.d.ts',
|
||||
]) {
|
||||
safeLibs[`node_modules/${safeLib}`] = readFileAsString(safeLib)
|
||||
}
|
||||
|
||||
const libs = Object.assign(getTypeDefs(), extraLibs);
|
||||
|
||||
function monacoEvalDefaultsFunction(monaco: any, safeLibs: any, libs: any) {
|
||||
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions(
|
||||
Object.assign(
|
||||
{},
|
||||
monaco.languages.typescript.typescriptDefaults.getDiagnosticsOptions(),
|
||||
{
|
||||
diagnosticCodesToIgnore: [1108, 1375, 1378],
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions(
|
||||
Object.assign(
|
||||
{},
|
||||
monaco.languages.typescript.typescriptDefaults.getCompilerOptions(),
|
||||
{
|
||||
moduleResolution:
|
||||
monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const catLibs = Object.values(libs).join('\n');
|
||||
const catlibsNoExport = Object.keys(libs).filter(lib => lib !== 'sdk')
|
||||
.map(lib => libs[lib]).map(lib =>
|
||||
lib.toString().replace(/export /g, '').replace(/import.*?/g, ''))
|
||||
.join('\n');
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(`
|
||||
${catLibs}
|
||||
|
||||
declare global {
|
||||
${catlibsNoExport}
|
||||
|
||||
const log: Logger;
|
||||
|
||||
const deviceManager: DeviceManager;
|
||||
const endpointManager: EndpointManager;
|
||||
const mediaManager: MediaManager;
|
||||
const systemManager: SystemManager;
|
||||
const mqtt: MqttClient;
|
||||
const device: ScryptedDeviceBase & { pathname : string };
|
||||
}
|
||||
`,
|
||||
|
||||
"node_modules/@types/scrypted__sdk/types/index.d.ts"
|
||||
);
|
||||
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
||||
libs['sdk'],
|
||||
"node_modules/@types/scrypted__sdk/index.d.ts"
|
||||
);
|
||||
|
||||
for (const lib of Object.keys(safeLibs)) {
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
||||
safeLibs[lib],
|
||||
lib,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return `(function() {
|
||||
const safeLibs = ${JSON.stringify(safeLibs)};
|
||||
const libs = ${JSON.stringify(libs)};
|
||||
|
||||
return (monaco) => {
|
||||
(${monacoEvalDefaultsFunction})(monaco, safeLibs, libs);
|
||||
}
|
||||
})();
|
||||
`;
|
||||
}
|
||||
|
||||
export interface ScriptDeviceImpl extends ScriptDevice {
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
import { cloneDeep } from '@scrypted/common/src/clone-deep';
|
||||
import { Deferred } from "@scrypted/common/src/deferred";
|
||||
import { listenZeroSingleClient } from '@scrypted/common/src/listen-cluster';
|
||||
import { ffmpegLogInitialOutput, safeKillFFmpeg, safePrintFFmpegArguments } from '@scrypted/common/src/media-helpers';
|
||||
import { createActivityTimeout } from '@scrypted/common/src/activity-timeout';
|
||||
import { createRtspParser } from "@scrypted/common/src/rtsp-server";
|
||||
import { parseSdp } from "@scrypted/common/src/sdp-utils";
|
||||
import { StreamChunk, StreamParser } from '@scrypted/common/src/stream-parser';
|
||||
import sdk, { FFmpegInput, RequestMediaStreamOptions, ResponseMediaStreamOptions } from "@scrypted/sdk";
|
||||
import child_process, { ChildProcess, StdioOptions } from 'child_process';
|
||||
import { EventEmitter } from 'events';
|
||||
import { Server } from 'net';
|
||||
import { Duplex } from 'stream';
|
||||
import { cloneDeep } from './clone-deep';
|
||||
import { Deferred } from "./deferred";
|
||||
import { listenZeroSingleClient } from './listen-cluster';
|
||||
import { ffmpegLogInitialOutput, safeKillFFmpeg, safePrintFFmpegArguments } from './media-helpers';
|
||||
import { createRtspParser } from "./rtsp-server";
|
||||
import { parseSdp } from "./sdp-utils";
|
||||
import { StreamChunk, StreamParser } from './stream-parser';
|
||||
|
||||
const { mediaManager } = sdk;
|
||||
|
||||
export interface ParserSession<T extends string> {
|
||||
parserSpecific?: any;
|
||||
sdp: Promise<string>;
|
||||
sdp: Promise<Buffer[]>;
|
||||
resetActivityTimer?: () => void,
|
||||
negotiateMediaStream(requestMediaStream: RequestMediaStreamOptions, inputVideoCodec: string, inputAudioCodec: string): ResponseMediaStreamOptions;
|
||||
negotiateMediaStream(requestMediaStream: RequestMediaStreamOptions): ResponseMediaStreamOptions;
|
||||
inputAudioCodec?: string;
|
||||
inputVideoCodec?: string;
|
||||
inputVideoResolution?: {
|
||||
width: number,
|
||||
height: number,
|
||||
},
|
||||
start(): void;
|
||||
kill(error?: Error): void;
|
||||
killed: Promise<void>;
|
||||
@@ -24,7 +31,6 @@ export interface ParserSession<T extends string> {
|
||||
|
||||
emit(container: T, chunk: StreamChunk): this;
|
||||
on(container: T, callback: (chunk: StreamChunk) => void): this;
|
||||
on(error: 'error', callback: (e: Error) => void): this;
|
||||
removeListener(event: T | 'killed', callback: any): this;
|
||||
once(event: T | 'killed', listener: (...args: any[]) => void): this;
|
||||
}
|
||||
@@ -96,37 +102,65 @@ export async function parseAudioCodec(cp: ChildProcess) {
|
||||
export function setupActivityTimer(container: string, kill: (error?: Error) => void, events: {
|
||||
once(event: 'killed', callback: () => void): void,
|
||||
}, timeout: number) {
|
||||
const ret = createActivityTimeout(timeout, () => {
|
||||
let dataTimeout: NodeJS.Timeout;
|
||||
|
||||
function dataKill() {
|
||||
const str = 'timeout waiting for data, killing parser session';
|
||||
console.error(str, container);
|
||||
kill(new Error(str));
|
||||
});
|
||||
events.once('killed', () => ret.clearActivityTimer());
|
||||
return ret;
|
||||
}
|
||||
|
||||
let lastTime = Date.now();
|
||||
function resetActivityTimer() {
|
||||
lastTime = Date.now();
|
||||
}
|
||||
|
||||
function clearActivityTimer() {
|
||||
clearInterval(dataTimeout);
|
||||
}
|
||||
|
||||
if (timeout) {
|
||||
dataTimeout = setInterval(() => {
|
||||
if (Date.now() > lastTime + timeout) {
|
||||
clearInterval(dataTimeout);
|
||||
dataTimeout = undefined;
|
||||
dataKill();
|
||||
}
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
events.once('killed', () => clearInterval(dataTimeout));
|
||||
|
||||
resetActivityTimer();
|
||||
return {
|
||||
resetActivityTimer,
|
||||
clearActivityTimer,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function startParserSession<T extends string>(ffmpegInput: FFmpegInput, options: ParserOptions<T>): Promise<ParserSession<T>> {
|
||||
const { console } = options;
|
||||
|
||||
let isActive = true;
|
||||
const events = new EventEmitter();
|
||||
// need this to prevent kill from throwing due to uncaught Error during cleanup
|
||||
events.on('error', () => {});
|
||||
events.on('error', e => console.error('rebroadcast error', e));
|
||||
|
||||
let inputAudioCodec: string;
|
||||
let inputVideoCodec: string;
|
||||
let inputVideoResolution: string[];
|
||||
|
||||
let sessionKilled: any;
|
||||
const killed = new Promise<void>(resolve => {
|
||||
sessionKilled = resolve;
|
||||
});
|
||||
|
||||
const sdpDeferred = new Deferred<string>();
|
||||
function kill(error?: Error) {
|
||||
error ||= new Error('killed');
|
||||
if (isActive) {
|
||||
events.emit('killed');
|
||||
events.emit('error', error);
|
||||
events.emit('error', error || new Error('killed'));
|
||||
}
|
||||
if (!sdpDeferred.finished)
|
||||
sdpDeferred.reject(error);
|
||||
isActive = false;
|
||||
sessionKilled();
|
||||
safeKillFFmpeg(cp);
|
||||
@@ -134,7 +168,6 @@ export async function startParserSession<T extends string>(ffmpegInput: FFmpegIn
|
||||
|
||||
|
||||
const args = ffmpegInput.inputArguments.slice();
|
||||
const env = ffmpegInput.env ? { ...process.env, ...ffmpegInput.env } : undefined;
|
||||
|
||||
const ensureActive = (killed: () => void) => {
|
||||
if (!isActive) {
|
||||
@@ -152,7 +185,7 @@ export async function startParserSession<T extends string>(ffmpegInput: FFmpegIn
|
||||
const parser: StreamParser = options.parsers[container as T];
|
||||
|
||||
if (parser.tcpProtocol) {
|
||||
const tcp = await listenZeroSingleClient('127.0.0.1');
|
||||
const tcp = await listenZeroSingleClient();
|
||||
const url = new URL(parser.tcpProtocol);
|
||||
url.port = tcp.port.toString();
|
||||
args.push(
|
||||
@@ -167,7 +200,7 @@ export async function startParserSession<T extends string>(ffmpegInput: FFmpegIn
|
||||
try {
|
||||
ensureActive(() => socket.destroy());
|
||||
|
||||
for await (const chunk of parser.parse(socket, undefined, undefined)) {
|
||||
for await (const chunk of parser.parse(socket, parseInt(inputVideoResolution?.[2]), parseInt(inputVideoResolution?.[3]))) {
|
||||
events.emit(container, chunk);
|
||||
resetActivityTimer();
|
||||
}
|
||||
@@ -190,9 +223,8 @@ export async function startParserSession<T extends string>(ffmpegInput: FFmpegIn
|
||||
// start ffmpeg process with child process pipes
|
||||
args.unshift('-hide_banner');
|
||||
safePrintFFmpegArguments(console, args);
|
||||
const cp = child_process.spawn(ffmpegInput.ffmpegPath || await mediaManager.getFFmpegPath(), args, {
|
||||
const cp = child_process.spawn(await mediaManager.getFFmpegPath(), args, {
|
||||
stdio,
|
||||
env,
|
||||
});
|
||||
ffmpegLogInitialOutput(console, cp, undefined, options?.storage);
|
||||
cp.on('exit', () => kill(new Error('ffmpeg exited')));
|
||||
@@ -215,7 +247,7 @@ export async function startParserSession<T extends string>(ffmpegInput: FFmpegIn
|
||||
try {
|
||||
const { resetActivityTimer } = setupActivityTimer(container, kill, events, options?.timeout);
|
||||
|
||||
for await (const chunk of parser.parse(pipe as any, undefined, undefined)) {
|
||||
for await (const chunk of parser.parse(pipe as any, parseInt(inputVideoResolution?.[2]), parseInt(inputVideoResolution?.[3]))) {
|
||||
await deferredStart.promise;
|
||||
events.emit(container, chunk);
|
||||
resetActivityTimer();
|
||||
@@ -230,23 +262,42 @@ export async function startParserSession<T extends string>(ffmpegInput: FFmpegIn
|
||||
|
||||
const rtsp = (options.parsers as any).rtsp as ReturnType<typeof createRtspParser>;
|
||||
rtsp.sdp.then(sdp => {
|
||||
console?.log('sdp received from ffmpeg', sdp);
|
||||
sdpDeferred.resolve(sdp);
|
||||
const parsed = parseSdp(sdp);
|
||||
const audio = parsed.msections.find(msection => msection.type === 'audio');
|
||||
const video = parsed.msections.find(msection => msection.type === 'video');
|
||||
inputVideoCodec = video?.codec;
|
||||
inputAudioCodec = audio?.codec;
|
||||
});
|
||||
|
||||
const sdp = new Deferred<Buffer[]>();
|
||||
rtsp.sdp.then(r => sdp.resolve([Buffer.from(r)]));
|
||||
killed.then(() => sdp.reject(new Error("ffmpeg killed before sdp could be parsed")));
|
||||
|
||||
start();
|
||||
|
||||
return {
|
||||
start() {
|
||||
deferredStart.resolve();
|
||||
},
|
||||
sdp: sdpDeferred.promise,
|
||||
sdp: sdp.promise,
|
||||
get inputAudioCodec() {
|
||||
return inputAudioCodec;
|
||||
},
|
||||
get inputVideoCodec() {
|
||||
return inputVideoCodec;
|
||||
},
|
||||
get inputVideoResolution() {
|
||||
return {
|
||||
width: parseInt(inputVideoResolution?.[2]),
|
||||
height: parseInt(inputVideoResolution?.[3]),
|
||||
}
|
||||
},
|
||||
get isActive() { return isActive },
|
||||
kill(error?: Error) {
|
||||
kill(error);
|
||||
},
|
||||
killed,
|
||||
negotiateMediaStream: (requestMediaStream: RequestMediaStreamOptions, inputVideoCodec, inputAudioCodec) => {
|
||||
negotiateMediaStream: () => {
|
||||
const ret: ResponseMediaStreamOptions = cloneDeep(ffmpegInput.mediaStreamOptions) || {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
@@ -288,3 +339,64 @@ export async function startParserSession<T extends string>(ffmpegInput: FFmpegIn
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface Rebroadcaster {
|
||||
server: Server;
|
||||
port: number;
|
||||
url: string;
|
||||
clients: number;
|
||||
}
|
||||
|
||||
export interface RebroadcastSessionCleanup {
|
||||
(): void;
|
||||
}
|
||||
|
||||
export interface RebroadcasterConnection {
|
||||
writeData: (data: StreamChunk) => number;
|
||||
destroy: () => void;
|
||||
}
|
||||
|
||||
export interface RebroadcasterOptions {
|
||||
connect?: (connection: RebroadcasterConnection) => RebroadcastSessionCleanup | undefined;
|
||||
console?: Console;
|
||||
idle?: {
|
||||
timeout: number,
|
||||
callback: () => void,
|
||||
},
|
||||
}
|
||||
|
||||
export function handleRebroadcasterClient(socket: Duplex, options?: RebroadcasterOptions) {
|
||||
const firstWriteData = (data: StreamChunk) => {
|
||||
if (data.startStream) {
|
||||
socket.write(data.startStream)
|
||||
}
|
||||
connection.writeData = writeData;
|
||||
return writeData(data);
|
||||
}
|
||||
const writeData = (data: StreamChunk) => {
|
||||
for (const chunk of data.chunks) {
|
||||
socket.write(chunk);
|
||||
}
|
||||
|
||||
return socket.writableLength;
|
||||
};
|
||||
|
||||
const destroy = () => {
|
||||
const cb = cleanupCallback;
|
||||
cleanupCallback = undefined;
|
||||
socket.destroy();
|
||||
cb?.();
|
||||
}
|
||||
|
||||
const connection: RebroadcasterConnection = {
|
||||
writeData: firstWriteData,
|
||||
destroy,
|
||||
};
|
||||
|
||||
let cleanupCallback = options?.connect(connection);
|
||||
|
||||
socket.once('close', () => {
|
||||
destroy();
|
||||
});
|
||||
socket.on('error', e => options?.console?.log('client stream ended'));
|
||||
}
|
||||
@@ -79,4 +79,4 @@ export async function bind(server: dgram.Socket, port: number) {
|
||||
}
|
||||
}
|
||||
|
||||
export { ListenZeroSingleClientTimeoutError, listenZero, listenZeroSingleClient } from "../../server/src/listen-zero";
|
||||
export { ListenZeroSingleClientTimeoutError, listenZero, listenZeroSingleClient } from "@scrypted/server/src/listen-zero";
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { safeKillFFmpeg, ffmpegLogInitialOutput, safePrintFFmpegArguments } from '../../server/src/media-helpers';
|
||||
export * from '@scrypted/server/src/media-helpers';
|
||||
|
||||
@@ -54,18 +54,18 @@ export async function read16BELengthLoop(readable: Readable, options: {
|
||||
readable.on('readable', read);
|
||||
|
||||
await once(readable, 'end');
|
||||
throw new StreamEndError('read16BELengthLoop');
|
||||
throw new Error('stream ended');
|
||||
}
|
||||
|
||||
export class StreamEndError extends Error {
|
||||
constructor(where: string) {
|
||||
super(`stream ended: ${where}`);
|
||||
constructor() {
|
||||
super('stream ended');
|
||||
}
|
||||
}
|
||||
|
||||
export async function readLength(readable: Readable, length: number): Promise<Buffer> {
|
||||
if (readable.readableEnded || readable.destroyed)
|
||||
throw new StreamEndError('readLength start');
|
||||
throw new StreamEndError();
|
||||
|
||||
if (!length) {
|
||||
return Buffer.alloc(0);
|
||||
@@ -88,12 +88,12 @@ export async function readLength(readable: Readable, length: number): Promise<Bu
|
||||
}
|
||||
|
||||
if (readable.readableEnded || readable.destroyed)
|
||||
reject(new StreamEndError('readLength readable'));
|
||||
reject(new Error("stream ended during read"));
|
||||
};
|
||||
|
||||
const e = () => {
|
||||
cleanup();
|
||||
reject(new StreamEndError('readLength end'));
|
||||
reject(new StreamEndError())
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
@@ -136,17 +136,12 @@ export async function readLine(readable: Readable) {
|
||||
}
|
||||
|
||||
export async function readString(readable: Readable | Promise<Readable>) {
|
||||
const buffer = await readBuffer(readable);
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
export async function readBuffer(readable: Readable | Promise<Readable>) {
|
||||
const buffers: Buffer[] = [];
|
||||
let data = '';
|
||||
readable = await readable;
|
||||
readable.on('data', buffer => {
|
||||
buffers.push(buffer);
|
||||
data += buffer.toString();
|
||||
});
|
||||
readable.resume();
|
||||
await once(readable, 'end')
|
||||
return Buffer.concat(buffers);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RpcPeer } from "../../server/src/rpc";
|
||||
import { createRpcSerializer } from "../../server/src/rpc-serializer";
|
||||
import { RpcPeer } from "@scrypted/server/src/rpc";
|
||||
import { createRpcSerializer } from "@scrypted/server/src/rpc-serializer";
|
||||
import type { RTCSignalingSession } from "@scrypted/sdk";
|
||||
|
||||
export async function createBrowserSignalingSession(ws: WebSocket, localName: string, remoteName: string) {
|
||||
|
||||
@@ -41,15 +41,15 @@ export function isPeerConnectionClosed(pc: RTCPeerConnection) {
|
||||
|| pc.iceConnectionState === 'closed';
|
||||
}
|
||||
|
||||
// function silence() {
|
||||
// let ctx = new AudioContext(), oscillator = ctx.createOscillator();
|
||||
// const dest = ctx.createMediaStreamDestination();
|
||||
// oscillator.connect(dest);
|
||||
// oscillator.start();
|
||||
// const ret = dest.stream.getAudioTracks()[0];
|
||||
// ret.enabled = false;
|
||||
// return ret;
|
||||
// }
|
||||
function silence() {
|
||||
let ctx = new AudioContext(), oscillator = ctx.createOscillator();
|
||||
const dest = ctx.createMediaStreamDestination();
|
||||
oscillator.connect(dest);
|
||||
oscillator.start();
|
||||
const ret = dest.stream.getAudioTracks()[0];
|
||||
ret.enabled = false;
|
||||
return ret;
|
||||
}
|
||||
|
||||
function createOptions() {
|
||||
const options: RTCSignalingOptions = {
|
||||
|
||||
@@ -89,44 +89,27 @@ export const H264_NAL_TYPE_FU_B = 29;
|
||||
export const H264_NAL_TYPE_MTAP16 = 26;
|
||||
export const H264_NAL_TYPE_MTAP32 = 27;
|
||||
|
||||
export const H265_NAL_TYPE_AGG = 48;
|
||||
export const H265_NAL_TYPE_VPS = 32;
|
||||
export const H265_NAL_TYPE_SPS = 33;
|
||||
export const H265_NAL_TYPE_PPS = 34;
|
||||
export const H265_NAL_TYPE_IDR_N = 19;
|
||||
export const H265_NAL_TYPE_IDR_W = 20;
|
||||
|
||||
export function findH264NaluType(streamChunk: StreamChunk, naluType: number) {
|
||||
if (streamChunk.type !== 'h264')
|
||||
return;
|
||||
return findH264NaluTypeInNalu(streamChunk.chunks[streamChunk.chunks.length - 1].subarray(12), naluType);
|
||||
}
|
||||
|
||||
export function findH265NaluType(streamChunk: StreamChunk, naluType: number) {
|
||||
if (streamChunk.type !== 'h265')
|
||||
return;
|
||||
return findH265NaluTypeInNalu(streamChunk.chunks[streamChunk.chunks.length - 1].subarray(12), naluType);
|
||||
}
|
||||
|
||||
export function parseH264NaluType(firstNaluByte: number) {
|
||||
return firstNaluByte & 0x1f;
|
||||
}
|
||||
|
||||
export function findH264NaluTypeInNalu(nalu: Buffer, naluType: number) {
|
||||
const checkNaluType = parseH264NaluType(nalu[0]);
|
||||
const checkNaluType = nalu[0] & 0x1f;
|
||||
if (checkNaluType === H264_NAL_TYPE_STAP_A) {
|
||||
let pos = 1;
|
||||
while (pos < nalu.length) {
|
||||
const naluLength = nalu.readUInt16BE(pos);
|
||||
pos += 2;
|
||||
const stapaType = parseH264NaluType(nalu[pos]);
|
||||
const stapaType = nalu[pos] & 0x1f;
|
||||
if (stapaType === naluType)
|
||||
return nalu.subarray(pos, pos + naluLength);
|
||||
pos += naluLength;
|
||||
}
|
||||
}
|
||||
else if (checkNaluType === H264_NAL_TYPE_FU_A) {
|
||||
const fuaType = parseH264NaluType(nalu[1]);
|
||||
const fuaType = nalu[1] & 0x1f;
|
||||
const isFuStart = !!(nalu[1] & 0x80);
|
||||
|
||||
if (fuaType === naluType && isFuStart)
|
||||
@@ -138,52 +121,39 @@ export function findH264NaluTypeInNalu(nalu: Buffer, naluType: number) {
|
||||
return;
|
||||
}
|
||||
|
||||
function parseH265NaluType(firstNaluByte: number) {
|
||||
return (firstNaluByte & 0b01111110) >> 1;
|
||||
}
|
||||
|
||||
export function findH265NaluTypeInNalu(nalu: Buffer, naluType: number) {
|
||||
const checkNaluType = parseH265NaluType(nalu[0]);
|
||||
if (checkNaluType === H265_NAL_TYPE_AGG) {
|
||||
let pos = 1;
|
||||
while (pos < nalu.length) {
|
||||
const naluLength = nalu.readUInt16BE(pos);
|
||||
pos += 2;
|
||||
const stapaType = parseH265NaluType(nalu[pos]);
|
||||
if (stapaType === naluType)
|
||||
return nalu.subarray(pos, pos + naluLength);
|
||||
pos += naluLength;
|
||||
}
|
||||
}
|
||||
else if (checkNaluType === naluType) {
|
||||
return nalu;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
export function getNaluTypes(streamChunk: StreamChunk) {
|
||||
if (streamChunk.type !== 'h264')
|
||||
return new Set<number>();
|
||||
return getNaluTypesInNalu(streamChunk.chunks[streamChunk.chunks.length - 1].subarray(12))
|
||||
}
|
||||
|
||||
export function getNaluFragmentInformation(nalu: Buffer) {
|
||||
const naluType = nalu[0] & 0x1f;
|
||||
const fua = naluType === H264_NAL_TYPE_FU_A;
|
||||
return {
|
||||
fua,
|
||||
fuaStart: fua && !!(nalu[1] & 0x80),
|
||||
fuaEnd: fua && !!(nalu[1] & 0x40),
|
||||
}
|
||||
}
|
||||
|
||||
export function getNaluTypesInNalu(nalu: Buffer, fuaRequireStart = false, fuaRequireEnd = false) {
|
||||
const ret = new Set<number>();
|
||||
const naluType = parseH264NaluType(nalu[0]);
|
||||
const naluType = nalu[0] & 0x1f;
|
||||
if (naluType === H264_NAL_TYPE_STAP_A) {
|
||||
ret.add(H264_NAL_TYPE_STAP_A);
|
||||
let pos = 1;
|
||||
while (pos < nalu.length) {
|
||||
const naluLength = nalu.readUInt16BE(pos);
|
||||
pos += 2;
|
||||
const stapaType = parseH264NaluType(nalu[pos]);
|
||||
const stapaType = nalu[pos] & 0x1f;
|
||||
ret.add(stapaType);
|
||||
pos += naluLength;
|
||||
}
|
||||
}
|
||||
else if (naluType === H264_NAL_TYPE_FU_A) {
|
||||
ret.add(H264_NAL_TYPE_FU_A);
|
||||
const fuaType = parseH264NaluType(nalu[1]);
|
||||
const fuaType = nalu[1] & 0x1f;
|
||||
if (fuaRequireStart) {
|
||||
const isFuStart = !!(nalu[1] & 0x80);
|
||||
if (isFuStart)
|
||||
@@ -205,33 +175,6 @@ export function getNaluTypesInNalu(nalu: Buffer, fuaRequireStart = false, fuaReq
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function getH265NaluTypes(streamChunk: StreamChunk) {
|
||||
if (streamChunk.type !== 'h265')
|
||||
return new Set<number>();
|
||||
return getNaluTypesInH265Nalu(streamChunk.chunks[streamChunk.chunks.length - 1].subarray(12))
|
||||
}
|
||||
|
||||
export function getNaluTypesInH265Nalu(nalu: Buffer, fuaRequireStart = false, fuaRequireEnd = false) {
|
||||
const ret = new Set<number>();
|
||||
const naluType = parseH265NaluType(nalu[0]);
|
||||
if (naluType === H265_NAL_TYPE_AGG) {
|
||||
ret.add(H265_NAL_TYPE_AGG);
|
||||
let pos = 1;
|
||||
while (pos < nalu.length) {
|
||||
const naluLength = nalu.readUInt16BE(pos);
|
||||
pos += 2;
|
||||
const stapaType = parseH265NaluType(nalu[pos]);
|
||||
ret.add(stapaType);
|
||||
pos += naluLength;
|
||||
}
|
||||
}
|
||||
else {
|
||||
ret.add(naluType);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function createRtspParser(options?: StreamParserOptions): RtspStreamParser {
|
||||
let resolve: any;
|
||||
|
||||
@@ -252,23 +195,12 @@ export function createRtspParser(options?: StreamParserOptions): RtspStreamParse
|
||||
findSyncFrame(streamChunks: StreamChunk[]) {
|
||||
for (let prebufferIndex = 0; prebufferIndex < streamChunks.length; prebufferIndex++) {
|
||||
const streamChunk = streamChunks[prebufferIndex];
|
||||
if (streamChunk.type === 'h264') {
|
||||
const naluTypes = getNaluTypes(streamChunk);
|
||||
if (naluTypes.has(H264_NAL_TYPE_SPS) || naluTypes.has(H264_NAL_TYPE_IDR)) {
|
||||
return streamChunks.slice(prebufferIndex);
|
||||
}
|
||||
if (streamChunk.type !== 'h264') {
|
||||
continue;
|
||||
}
|
||||
else if (streamChunk.type === 'h265') {
|
||||
const naluTypes = getH265NaluTypes(streamChunk);
|
||||
|
||||
if (naluTypes.has(H265_NAL_TYPE_VPS)
|
||||
|| naluTypes.has(H265_NAL_TYPE_SPS)
|
||||
|| naluTypes.has(H265_NAL_TYPE_PPS)
|
||||
|| naluTypes.has(H265_NAL_TYPE_IDR_N)
|
||||
|| naluTypes.has(H265_NAL_TYPE_IDR_W)
|
||||
) {
|
||||
return streamChunks.slice(prebufferIndex);
|
||||
}
|
||||
if (findH264NaluType(streamChunk, H264_NAL_TYPE_SPS) || findH264NaluType(streamChunk, H264_NAL_TYPE_IDR)) {
|
||||
return streamChunks.slice(prebufferIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,7 +438,7 @@ export class RtspClient extends RtspBase {
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
this.client.destroy(e as Error);
|
||||
this.client.destroy(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -572,8 +504,7 @@ export class RtspClient extends RtspBase {
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
if (!deferred.finished)
|
||||
deferred.reject(e as Error);
|
||||
deferred.reject(e);
|
||||
this.client.destroy();
|
||||
}
|
||||
};
|
||||
@@ -609,7 +540,6 @@ export class RtspClient extends RtspBase {
|
||||
throw new Error('no WWW-Authenticate found');
|
||||
|
||||
const { BASIC } = await import('http-auth-utils');
|
||||
// @ts-ignore
|
||||
const { parseHTTPHeadersQuotedKeyValueSet } = await import('http-auth-utils/dist/utils');
|
||||
|
||||
if (this.wwwAuthenticate.includes('Basic')) {
|
||||
@@ -726,10 +656,7 @@ export class RtspClient extends RtspBase {
|
||||
Accept: 'application/sdp',
|
||||
});
|
||||
|
||||
this.contentBase = response.headers['content-base'] || response.headers['content-location'];
|
||||
// content base may be a relative path? seems odd.
|
||||
if (this.contentBase)
|
||||
this.contentBase = new URL(this.contentBase, this.url).toString();
|
||||
this.contentBase = response.headers['content-base'] || response.headers['content-location'];;
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -1127,7 +1054,7 @@ export class RtspServer {
|
||||
}
|
||||
|
||||
export async function listenSingleRtspClient<T extends RtspServer>(options?: {
|
||||
hostname: string,
|
||||
hostname?: string,
|
||||
pathToken?: string,
|
||||
createServer?(duplex: Duplex): T,
|
||||
}) {
|
||||
|
||||
@@ -227,10 +227,6 @@ export function parseRtpMap(mline: ReturnType<typeof parseMLine>, rtpmap: string
|
||||
codec = 'pcm_alaw';
|
||||
ffmpegEncoder = 'pcm_alaw';
|
||||
}
|
||||
else if (mline.payloadTypes?.includes(14)) {
|
||||
codec = 'mp3';
|
||||
ffmpegEncoder = 'mp3';
|
||||
}
|
||||
else {
|
||||
// ffmpeg seems to omit the rtpmap type for pcm alaw when creating sdp?
|
||||
// is this the default?
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { sleep } from "../../server/src/sleep";
|
||||
export * from "@scrypted/server/src/sleep"
|
||||
@@ -1,15 +1,19 @@
|
||||
import sdk, { ForkOptions, PluginFork } from '@scrypted/sdk';
|
||||
import sdk, { PluginFork } from '@scrypted/sdk';
|
||||
import worker_threads from 'worker_threads';
|
||||
import { createAsyncQueue } from './async-queue';
|
||||
import os from 'os';
|
||||
|
||||
export type Zygote<T> = () => PluginFork<T>;
|
||||
|
||||
export function createZygote<T>(options?: ForkOptions): Zygote<T> {
|
||||
let zygote = sdk.fork<T>(options);
|
||||
export function createZygote<T>(): Zygote<T> {
|
||||
if (!worker_threads.isMainThread)
|
||||
return;
|
||||
|
||||
let zygote = sdk.fork<T>();
|
||||
function* next() {
|
||||
while (true) {
|
||||
const cur = zygote;
|
||||
zygote = sdk.fork<T>(options);
|
||||
zygote = sdk.fork<T>();
|
||||
yield cur;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "Node16",
|
||||
"target": "esnext",
|
||||
"noImplicitAny": true,
|
||||
"outDir": "./dist",
|
||||
|
||||
BIN
docs/plugins/core/ui/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
docs/plugins/core/ui/images/cameraloading.jpg
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
docs/plugins/core/ui/img/icons/apple-touch-icon-152x152.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
docs/plugins/core/ui/img/icons/favicon-128x128.png
Executable file
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/plugins/core/ui/img/icons/favicon-144x144.png
Executable file
|
After Width: | Height: | Size: 17 KiB |
BIN
docs/plugins/core/ui/img/icons/favicon-152x152.png
Executable file
|
After Width: | Height: | Size: 18 KiB |
BIN
docs/plugins/core/ui/img/icons/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
docs/plugins/core/ui/img/icons/favicon-192x192.png
Executable file
|
After Width: | Height: | Size: 26 KiB |
BIN
docs/plugins/core/ui/img/icons/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
docs/plugins/core/ui/img/icons/favicon-384x384.png
Executable file
|
After Width: | Height: | Size: 75 KiB |
BIN
docs/plugins/core/ui/img/icons/favicon-512x512.png
Executable file
|
After Width: | Height: | Size: 91 KiB |
BIN
docs/plugins/core/ui/img/icons/favicon-72x72.png
Executable file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
docs/plugins/core/ui/img/icons/favicon-96x96.png
Executable file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
docs/plugins/core/ui/img/icons/icon-128x128.png
Executable file
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/plugins/core/ui/img/icons/icon-144x144.png
Executable file
|
After Width: | Height: | Size: 17 KiB |
BIN
docs/plugins/core/ui/img/icons/icon-152x152.png
Executable file
|
After Width: | Height: | Size: 18 KiB |
BIN
docs/plugins/core/ui/img/icons/icon-192x192.png
Executable file
|
After Width: | Height: | Size: 26 KiB |
BIN
docs/plugins/core/ui/img/icons/icon-384x384.png
Executable file
|
After Width: | Height: | Size: 75 KiB |
BIN
docs/plugins/core/ui/img/icons/icon-512x512.png
Executable file
|
After Width: | Height: | Size: 91 KiB |
BIN
docs/plugins/core/ui/img/icons/icon-72x72.png
Executable file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
docs/plugins/core/ui/img/icons/icon-96x96.png
Executable file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
docs/plugins/core/ui/img/icons/msapplication-icon-144x144.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
1
docs/plugins/core/ui/img/icons/safari-pinned-tab.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cpu"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect><rect x="9" y="9" width="6" height="6"></rect><line x1="9" y1="1" x2="9" y2="4"></line><line x1="15" y1="1" x2="15" y2="4"></line><line x1="9" y1="20" x2="9" y2="23"></line><line x1="15" y1="20" x2="15" y2="23"></line><line x1="20" y1="9" x2="23" y2="9"></line><line x1="20" y1="14" x2="23" y2="14"></line><line x1="1" y1="9" x2="4" y2="9"></line><line x1="1" y1="14" x2="4" y2="14"></line></svg>
|
||||
|
After Width: | Height: | Size: 667 B |
26
docs/plugins/core/ui/index.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title>Scrypted Management Console</title>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons">
|
||||
|
||||
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&display=swap" rel="stylesheet">
|
||||
|
||||
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but web doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
45
docs/plugins/core/ui/manifest.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "Scrypted Management Console",
|
||||
"short_name": "Scrypted",
|
||||
"icons": [
|
||||
{
|
||||
"src": "https://koush.github.io/scrypted/plugins/core/ui/img/icons/icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "https://koush.github.io/scrypted/plugins/core/ui/img/icons/icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "https://koush.github.io/scrypted/plugins/core/ui/img/icons/icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "https://koush.github.io/scrypted/plugins/core/ui/img/icons/icon-152x152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "https://koush.github.io/scrypted/plugins/core/ui/img/icons/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "https://koush.github.io/scrypted/plugins/core/ui/img/icons/icon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "https://koush.github.io/scrypted/plugins/core/ui/img/icons/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"start_url": "./index.html",
|
||||
"display": "standalone",
|
||||
"background_color": "#000000",
|
||||
"theme_color": "#424242"
|
||||
}
|
||||
2
docs/plugins/core/ui/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow:
|
||||
2
external/unifi-protect
vendored
2
external/werift
vendored
@@ -1,12 +1,13 @@
|
||||
# Home Assistant Addon Configuration
|
||||
name: Scrypted
|
||||
version: "v0.114.0-jammy-full"
|
||||
version: "18-jammy-full.s6-v0.93.0"
|
||||
slug: scrypted
|
||||
description: Scrypted is a high performance home video integration and automation platform
|
||||
url: "https://github.com/koush/scrypted"
|
||||
arch:
|
||||
- amd64
|
||||
- aarch64
|
||||
- armv7
|
||||
init: false
|
||||
ingress: true
|
||||
ingress_port: 11080
|
||||
|
||||
@@ -71,9 +71,8 @@ RUN python3 -m pip install debugpy typing_extensions psutil
|
||||
################################################################
|
||||
FROM header as base
|
||||
|
||||
# intel opencl gpu and npu for openvino
|
||||
# intel opencl gpu for openvino
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-graphics.sh | bash
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-npu.sh | bash
|
||||
|
||||
# python 3.9 from ppa.
|
||||
# 3.9 is the version with prebuilt support for tensorflow lite
|
||||
|
||||
@@ -17,6 +17,9 @@ RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg -
|
||||
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_"$NODE_VERSION".x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
|
||||
RUN apt-get update && apt-get install -y nodejs
|
||||
|
||||
# intel opencl gpu for openvino
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-graphics.sh | bash
|
||||
|
||||
ENV SCRYPTED_INSTALL_ENVIRONMENT="docker"
|
||||
ENV SCRYPTED_CAN_RESTART="true"
|
||||
ENV SCRYPTED_VOLUME="/server/volume"
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
ARG BASE="ghcr.io/koush/scrypted-common:20-jammy-full"
|
||||
FROM $BASE
|
||||
FROM ghcr.io/koush/scrypted:20-jammy-full.s6
|
||||
|
||||
# nvidia cudnn/libcublas etc.
|
||||
# for some reason this is not provided by the nvidia container toolkit
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-nvidia-graphics.sh | bash
|
||||
WORKDIR /
|
||||
|
||||
# Install miniconda
|
||||
ENV CONDA_DIR /opt/conda
|
||||
RUN wget --quiet https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda.sh && \
|
||||
/bin/bash ~/miniconda.sh -b -p /opt/conda
|
||||
# Put conda in path so we can use conda activate
|
||||
ENV PATH=$CONDA_DIR/bin:$PATH
|
||||
|
||||
RUN conda install -c conda-forge cudatoolkit=11.2.2 cudnn=8.1.0
|
||||
ENV CONDA_PREFIX=/opt/conda
|
||||
ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$CONDA_PREFIX/lib/
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
version: "3.5"
|
||||
|
||||
# The Scrypted docker-compose.yml file typically resides at:
|
||||
# ~/.scrypted/docker-compose.yml
|
||||
|
||||
@@ -35,24 +37,17 @@ services:
|
||||
# Avahi can be used for network discovery by passing in the host daemon
|
||||
# or running the daemon inside the container. Choose one or the other.
|
||||
# Uncomment next line to run avahi-daemon inside the container.
|
||||
# See volumes and security_opt section below to use the host daemon.
|
||||
# See volumes section below to use the host daemon.
|
||||
# - SCRYPTED_DOCKER_AVAHI=true
|
||||
|
||||
# NVIDIA (Part 1 of 4)
|
||||
# Uncomment next 3 lines for Nvidia GPU support.
|
||||
# - NVIDIA_VISIBLE_DEVICES=all
|
||||
# - NVIDIA_DRIVER_CAPABILITIES=all
|
||||
|
||||
# NVIDIA (Part 2 of 4)
|
||||
# runtime: nvidia
|
||||
|
||||
# NVIDIA (Part 3 of 4) - Use NVIDIA image, and remove subsequent default image.
|
||||
# image: ghcr.io/koush/scrypted:nvidia
|
||||
image: ghcr.io/koush/scrypted
|
||||
|
||||
# Necessary to communicate with host dbus for avahi-daemon.
|
||||
security_opt:
|
||||
- apparmor:unconfined
|
||||
volumes:
|
||||
# NVIDIA (Part 4 of 4)
|
||||
# - /etc/OpenCL/vendors/nvidia.icd:/etc/OpenCL/vendors/nvidia.icd
|
||||
|
||||
# Scrypted NVR Storage (Part 3 of 3)
|
||||
|
||||
# Modify to add the additional volume for Scrypted NVR.
|
||||
@@ -71,16 +66,11 @@ services:
|
||||
# Ensure Avahi is running on the host machine:
|
||||
# It can be installed with: sudo apt-get install avahi-daemon
|
||||
# This is not compatible with running avahi inside the container (see above).
|
||||
# Also, uncomment the lines under security_opt
|
||||
# - /var/run/dbus:/var/run/dbus
|
||||
# - /var/run/avahi-daemon/socket:/var/run/avahi-daemon/socket
|
||||
|
||||
# Default volume for the Scrypted database. Typically should not be changed.
|
||||
- ~/.scrypted/volume:/server/volume
|
||||
# Uncomment the following lines to use Avahi daemon from the host
|
||||
# Without this, AppArmor will block the container's attempt to talk to Avahi via dbus
|
||||
# security_opt:
|
||||
# - apparmor:unconfined
|
||||
devices: [
|
||||
# uncomment the common systems devices to pass
|
||||
# them through to docker.
|
||||
@@ -104,16 +94,15 @@ services:
|
||||
container_name: scrypted
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
image: ghcr.io/koush/scrypted
|
||||
|
||||
# logging is noisy and will unnecessarily wear on flash storage.
|
||||
# scrypted has per device in memory logging that is preferred.
|
||||
# enable the log file if enhanced debugging is necessary.
|
||||
logging:
|
||||
driver: "none"
|
||||
# driver: "json-file"
|
||||
# options:
|
||||
# max-size: "10m"
|
||||
# max-file: "10"
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "10"
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.scope=scrypted"
|
||||
|
||||
|
||||
@@ -1,52 +1,16 @@
|
||||
if [ "$(uname -m)" != "x86_64" ]
|
||||
if [ "$(uname -m)" = "x86_64" ]
|
||||
then
|
||||
echo "Installing Intel graphics packages."
|
||||
apt-get update && apt-get install -y gpg-agent &&
|
||||
rm -f /usr/share/keyrings/intel-graphics.gpg &&
|
||||
curl -L https://repositories.intel.com/graphics/intel-graphics.key | gpg --dearmor --yes --output /usr/share/keyrings/intel-graphics.gpg &&
|
||||
echo 'deb [arch=amd64,i386 signed-by=/usr/share/keyrings/intel-graphics.gpg] https://repositories.intel.com/graphics/ubuntu jammy arc' | tee /etc/apt/sources.list.d/intel.gpu.jammy.list &&
|
||||
apt-get -y update &&
|
||||
apt-get -y install intel-opencl-icd intel-media-va-driver-non-free &&
|
||||
apt-get -y dist-upgrade;
|
||||
exit $?
|
||||
else
|
||||
echo "Intel graphics will not be installed on this architecture."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# no errors beyond this point
|
||||
set -e
|
||||
|
||||
# the intel provided script is disabled since it does not work with the 6.8 kernel in Ubuntu 24.04 or Proxmox 8.2.
|
||||
# manual installation of the Intel graphics stuff is required.
|
||||
|
||||
# echo "Installing Intel graphics packages."
|
||||
# apt-get update && apt-get install -y gpg-agent &&
|
||||
# rm -f /usr/share/keyrings/intel-graphics.gpg &&
|
||||
# curl -L https://repositories.intel.com/graphics/intel-graphics.key | gpg --dearmor --yes --output /usr/share/keyrings/intel-graphics.gpg &&
|
||||
# echo 'deb [arch=amd64,i386 signed-by=/usr/share/keyrings/intel-graphics.gpg] https://repositories.intel.com/graphics/ubuntu jammy arc' | tee /etc/apt/sources.list.d/intel.gpu.jammy.list &&
|
||||
# apt-get -y update &&
|
||||
# apt-get -y install intel-opencl-icd &&
|
||||
# apt-get -y dist-upgrade;
|
||||
|
||||
# need intel-media-va-driver-non-free, but all the other intel packages are installed from Intel github.
|
||||
echo "Installing Intel graphics packages."
|
||||
apt-get update && apt-get install -y gpg-agent &&
|
||||
rm -f /usr/share/keyrings/intel-graphics.gpg &&
|
||||
curl -L https://repositories.intel.com/graphics/intel-graphics.key | gpg --dearmor --yes --output /usr/share/keyrings/intel-graphics.gpg &&
|
||||
echo 'deb [arch=amd64,i386 signed-by=/usr/share/keyrings/intel-graphics.gpg] https://repositories.intel.com/graphics/ubuntu jammy arc' | tee /etc/apt/sources.list.d/intel.gpu.jammy.list &&
|
||||
apt-get -y update &&
|
||||
apt-get -y install intel-media-va-driver-non-free &&
|
||||
apt-get -y dist-upgrade;
|
||||
|
||||
# manual installation
|
||||
# https://github.com/intel/compute-runtime/releases/tag/24.13.29138.7
|
||||
|
||||
|
||||
rm -rf /tmp/gpu && mkdir -p /tmp/gpu && cd /tmp/gpu
|
||||
|
||||
apt-get install -y ocl-icd-libopencl1
|
||||
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17193.4/intel-igc-core_1.0.17193.4_amd64.deb
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17193.4/intel-igc-opencl_1.0.17193.4_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-level-zero-gpu-dbgsym_1.3.30049.6_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-level-zero-gpu_1.3.30049.6_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-opencl-icd-dbgsym_24.26.30049.6_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-opencl-icd_24.26.30049.6_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/libigdgmm12_22.3.20_amd64.deb
|
||||
|
||||
dpkg -i *.deb
|
||||
|
||||
cd /tmp && rm -rf /tmp/gpu
|
||||
|
||||
apt-get -y dist-upgrade
|
||||
exit 0
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
if [ "$(uname -m)" != "x86_64" ]
|
||||
then
|
||||
echo "Intel NPU will not be installed on this architecture."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
UBUNTU_22_04=$(lsb_release -r | grep "22.04")
|
||||
UBUNTU_24_04=$(lsb_release -r | grep "24.04")
|
||||
|
||||
if [ -z "$UBUNTU_22_04" ]
|
||||
then
|
||||
# proxmox is compatible with ubuntu 22.04, check for /etc/pve directory
|
||||
if [ -d "/etc/pve" ]
|
||||
then
|
||||
UBUNTU_22_04=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# needs either ubuntu 22.0.4 or 24.04
|
||||
if [ -z "$UBUNTU_22_04" ] && [ -z "$UBUNTU_24_04" ]
|
||||
then
|
||||
echo "Intel NPU will not be installed. Ubuntu version could not be detected when checking lsb-release and /etc/os-release."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
dpkg --purge --force-remove-reinstreq intel-driver-compiler-npu intel-fw-npu intel-level-zero-npu
|
||||
|
||||
# no errors beyond this point
|
||||
set -e
|
||||
|
||||
rm -rf /tmp/npu && mkdir -p /tmp/npu && cd /tmp/npu
|
||||
|
||||
# different npu downloads for ubuntu versions
|
||||
if [ -n "$UBUNTU_22_04" ]
|
||||
then
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.5.1/intel-driver-compiler-npu_1.5.1.20240708-9842236399_ubuntu22.04_amd64.deb
|
||||
# firmware can only be installed on host. will cause problems inside container.
|
||||
if [ -n "$INTEL_FW_NPU" ]
|
||||
then
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.5.1/intel-fw-npu_1.5.1.20240708-9842236399_ubuntu22.04_amd64.deb
|
||||
fi
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.5.1/intel-level-zero-npu_1.5.1.20240708-9842236399_ubuntu22.04_amd64.deb
|
||||
else
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.5.1/intel-driver-compiler-npu_1.5.1.20240708-9842236399_ubuntu24.04_amd64.deb
|
||||
if [ -n "$INTEL_FW_NPU" ]
|
||||
then
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.5.1/intel-fw-npu_1.5.1.20240708-9842236399_ubuntu24.04_amd64.deb
|
||||
fi
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.5.1/intel-level-zero-npu_1.5.1.20240708-9842236399_ubuntu24.04_amd64.deb
|
||||
fi
|
||||
|
||||
curl -O -L https://github.com/oneapi-src/level-zero/releases/download/v1.17.6/level-zero_1.17.6+u22.04_amd64.deb
|
||||
curl -O -L https://github.com/oneapi-src/level-zero/releases/download/v1.17.6/level-zero-devel_1.17.6+u22.04_amd64.deb
|
||||
|
||||
apt -y update
|
||||
apt -y install libtbb12
|
||||
dpkg -i *.deb
|
||||
|
||||
cd /tmp && rm -rf /tmp/npu
|
||||
|
||||
apt-get -y dist-upgrade
|
||||
|
||||
if [ -n "$INTEL_FW_NPU" ]
|
||||
then
|
||||
echo
|
||||
echo "###############################################################################"
|
||||
echo "Intel NPU firmware was installed. Reboot the host to complete the installation."
|
||||
echo "###############################################################################"
|
||||
fi
|
||||
@@ -1,16 +0,0 @@
|
||||
if [ "$(uname -m)" = "x86_64" ]
|
||||
then
|
||||
echo "Installing NVIDIA graphics packages."
|
||||
apt update -q \
|
||||
&& apt install -y wget \
|
||||
&& wget -qO /cuda-keyring.deb https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/$(uname -m)/cuda-keyring_1.1-1_all.deb \
|
||||
&& dpkg -i /cuda-keyring.deb \
|
||||
&& apt update -q \
|
||||
&& apt install -y cuda-nvcc-11-8 libcublas-11-8 libcudnn8 cuda-libraries-11-8 \
|
||||
&& apt install -y cuda-nvcc-12-4 libcublas-12-4 libcudnn8 cuda-libraries-12-4;
|
||||
exit $?
|
||||
else
|
||||
echo "NVIDIA graphics will not be installed on this architecture."
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -61,8 +61,6 @@ then
|
||||
sudo apt-get -y install avahi-daemon
|
||||
sed -i 's/'#' - \/var\/run\/dbus/- \/var\/run\/dbus/g' $DOCKER_COMPOSE_YML
|
||||
sed -i 's/'#' - \/var\/run\/avahi-daemon/- \/var\/run\/avahi-daemon/g' $DOCKER_COMPOSE_YML
|
||||
sed -i 's/'#' security_opt:/security_opt:/g' $DOCKER_COMPOSE_YML
|
||||
sed -i 's/'#' - apparmor:unconfined/ - apparmor:unconfined/g' $DOCKER_COMPOSE_YML
|
||||
fi
|
||||
|
||||
echo "Setting permissions on $SCRYPTED_HOME"
|
||||
|
||||
@@ -72,7 +72,6 @@ function removescryptedfstab() {
|
||||
grep -v "scrypted-nvr" /etc/fstab > /tmp/fstab && cp /tmp/fstab /etc/fstab
|
||||
# ensure newline
|
||||
sed -i -e '$a\' /etc/fstab
|
||||
systemctl daemon-reload
|
||||
}
|
||||
|
||||
BLOCK_DEVICE="/dev/$1"
|
||||
@@ -96,17 +95,7 @@ then
|
||||
set +e
|
||||
|
||||
sync
|
||||
PARTITION_DEVICE="$BLOCK_DEVICE"1
|
||||
if [ ! -e "$PARTITION_DEVICE" ]
|
||||
then
|
||||
PARTITION_DEVICE="$BLOCK_DEVICE"p1
|
||||
if [ ! -e "$PARTITION_DEVICE" ]
|
||||
then
|
||||
echo "Unable to determine block device partition from block device: $BLOCK_DEVICE"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
mkfs -F -t ext4 "$PARTITION_DEVICE"
|
||||
mkfs -F -t ext4 "$BLOCK_DEVICE"1
|
||||
sync
|
||||
|
||||
# parse/evaluate blkid line as env vars
|
||||
@@ -128,9 +117,8 @@ then
|
||||
set -e
|
||||
removescryptedfstab
|
||||
mkdir -p /mnt/scrypted-nvr
|
||||
echo "PARTLABEL=scrypted-nvr /mnt/scrypted-nvr ext4 defaults,nofail,noatime 0 0" >> /etc/fstab
|
||||
echo "PARTLABEL=scrypted-nvr /mnt/scrypted-nvr ext4 defaults,nofail 0 0" >> /etc/fstab
|
||||
mount -a
|
||||
systemctl daemon-reload
|
||||
set +e
|
||||
|
||||
DIR="/mnt/scrypted-nvr"
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
################################################################
|
||||
FROM header as base
|
||||
|
||||
# intel opencl gpu and npu for openvino
|
||||
# intel opencl gpu for openvino
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-graphics.sh | bash
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-npu.sh | bash
|
||||
|
||||
# python 3.9 from ppa.
|
||||
# 3.9 is the version with prebuilt support for tensorflow lite
|
||||
|
||||
@@ -97,7 +97,7 @@ echo "docker compose rm -rf"
|
||||
sudo -u $SERVICE_USER docker rm -f /scrypted /scrypted-watchtower 2> /dev/null
|
||||
|
||||
echo "Installing Scrypted..."
|
||||
RUN sudo -u $SERVICE_USER npx -y scrypted@latest install-server $SCRYPTED_INSTALL_VERSION
|
||||
RUN sudo -u $SERVICE_USER npx -y scrypted@latest install-server
|
||||
|
||||
cat > /etc/systemd/system/scrypted.service <<EOT
|
||||
|
||||
@@ -110,12 +110,10 @@ User=$SERVICE_USER
|
||||
Group=$SERVICE_USER
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/npx -y scrypted serve
|
||||
Restart=always
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
Environment="NODE_OPTIONS=$NODE_OPTIONS"
|
||||
Environment="SCRYPTED_INSTALL_ENVIRONMENT=$SCRYPTED_INSTALL_ENVIRONMENT"
|
||||
StandardOutput=null
|
||||
StandardError=null
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -121,7 +121,7 @@ then
|
||||
fi
|
||||
|
||||
echo "Installing Scrypted..."
|
||||
RUN $NPX_PATH -y scrypted@latest install-server $SCRYPTED_INSTALL_VERSION
|
||||
RUN $NPX_PATH -y scrypted@latest install-server
|
||||
|
||||
cat > ~/Library/LaunchAgents/app.scrypted.server.plist <<EOT
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
@@ -11,7 +11,7 @@ iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/in
|
||||
choco upgrade -y nodejs-lts --version=20.11.1
|
||||
|
||||
# Install VC Redist, which is necessary for portable python
|
||||
choco install -y vcredist140
|
||||
choco install vcredist140
|
||||
|
||||
# TODO: remove python install, and use portable python
|
||||
# Install Python
|
||||
@@ -26,12 +26,7 @@ $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";"
|
||||
py $SCRYPTED_WINDOWS_PYTHON_VERSION -m pip install --upgrade pip
|
||||
py $SCRYPTED_WINDOWS_PYTHON_VERSION -m pip install debugpy typing_extensions typing opencv-python
|
||||
|
||||
$SCRYPTED_INSTALL_VERSION=[System.Environment]::GetEnvironmentVariable("SCRYPTED_INSTALL_VERSION","User")
|
||||
if ($SCRYPTED_INSTALL_VERSION -eq $null) {
|
||||
npx -y scrypted@latest install-server
|
||||
} else {
|
||||
npx -y scrypted@latest install-server $SCRYPTED_INSTALL_VERSION
|
||||
}
|
||||
npx -y scrypted@latest install-server
|
||||
|
||||
$USER_HOME_ESCAPED = $env:USERPROFILE.replace('\', '\\')
|
||||
$SCRYPTED_HOME = $env:USERPROFILE + '\.scrypted'
|
||||
@@ -39,8 +34,7 @@ $SCRYPTED_HOME_ESCAPED_PATH = $SCRYPTED_HOME.replace('\', '\\')
|
||||
npm install --prefix $SCRYPTED_HOME @koush/node-windows --save
|
||||
|
||||
$NPX_PATH = (Get-Command npx).Path
|
||||
# The path needs double quotes to handle spaces in the directory path
|
||||
$NPX_PATH_ESCAPED = '"' + $NPX_PATH.replace('\', '\\') + '"'
|
||||
$NPX_PATH_ESCAPED = $NPX_PATH.replace('\', '\\')
|
||||
|
||||
$SERVICE_JS = @"
|
||||
const fs = require('fs');
|
||||
@@ -50,10 +44,8 @@ try {
|
||||
catch (e) {
|
||||
}
|
||||
const child_process = require('child_process');
|
||||
child_process.spawn('$NPX_PATH_ESCAPED', ['-y', 'scrypted', 'serve'], {
|
||||
child_process.spawn('$($NPX_PATH_ESCAPED)', ['-y', 'scrypted', 'serve'], {
|
||||
stdio: 'inherit',
|
||||
// allow spawning .cmd https://nodejs.org/en/blog/vulnerability/april-2024-security-releases-2
|
||||
shell: true,
|
||||
});
|
||||
"@
|
||||
|
||||
|
||||
@@ -10,20 +10,13 @@ function readyn() {
|
||||
}
|
||||
|
||||
cd /tmp
|
||||
SCRYPTED_VERSION=v0.96.0
|
||||
SCRYPTED_VERSION=v0.93.0
|
||||
SCRYPTED_TAR_ZST=scrypted-$SCRYPTED_VERSION.tar.zst
|
||||
if [ -z "$VMID" ]
|
||||
then
|
||||
VMID=10443
|
||||
fi
|
||||
|
||||
if [ -n "$SCRYPTED_RESTORE" ]
|
||||
then
|
||||
RESTORE_VMID=$VMID
|
||||
VMID=10444
|
||||
pct destroy $VMID 2>&1 > /dev/null
|
||||
fi
|
||||
|
||||
echo "Downloading scrypted container backup."
|
||||
if [ ! -f "$SCRYPTED_TAR_ZST" ]
|
||||
then
|
||||
@@ -48,19 +41,12 @@ pct restore $VMID $SCRYPTED_TAR_ZST $@
|
||||
if [ "$?" != "0" ]
|
||||
then
|
||||
echo ""
|
||||
echo "The Scrypted container installation failed (pct restore error)."
|
||||
echo "pct restore failed"
|
||||
echo ""
|
||||
echo "This may be because the server's 'local' storage device is not being a valid"
|
||||
echo "location for containers."
|
||||
echo "Try running this script again with a different storage device like"
|
||||
echo "'local-lvm' or 'local-zfs'."
|
||||
echo ""
|
||||
echo "#############################################################################"
|
||||
echo "Paste the following command into this shell to install to local-lvm instead:"
|
||||
echo "This may be caused by the server's 'local' storage not supporting containers."
|
||||
echo "Try running this script again with a different storage device (local-lvm, local-zfs). For example:"
|
||||
echo ""
|
||||
echo "bash $0 --storage local-lvm"
|
||||
echo "#############################################################################"
|
||||
echo ""
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
@@ -82,27 +68,6 @@ else
|
||||
echo "$CONF not found? Start on boot must be enabled manually."
|
||||
fi
|
||||
|
||||
if [ -n "$SCRYPTED_RESTORE" ]
|
||||
then
|
||||
readyn "Running this script will reset Scrypted to a factory state while preserving existing data. IT IS RECOMMENDED TO CREATE A BACKUP FIRST. Are you sure you want to continue?"
|
||||
if [ "$yn" != "y" ]
|
||||
then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Preparing rootfs reset..."
|
||||
# this copies the
|
||||
pct set 10444 --delete mp0 && pct set 10444 --delete unused0 && pct move-volume $RESTORE_VMID mp0 --target-vmid 10444 --target-volume mp0
|
||||
|
||||
rm *.tar
|
||||
vzdump 10444 --dumpdir /tmp
|
||||
VMID=$RESTORE_VMID
|
||||
echo "Moving data volume to backup..."
|
||||
pct restore $VMID *.tar $@
|
||||
|
||||
pct destroy 10444
|
||||
fi
|
||||
|
||||
echo "Adding udev rule: /etc/udev/rules.d/65-scrypted.rules"
|
||||
readyn "Add udev rule for hardware acceleration? This may conflict with existing rules."
|
||||
if [ "$yn" == "y" ]
|
||||
@@ -110,7 +75,6 @@ then
|
||||
sh -c "echo 'SUBSYSTEM==\"apex\", MODE=\"0666\"' > /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'KERNEL==\"renderD128\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'KERNEL==\"card0\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'KERNEL==\"accel0\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"1a6e\", ATTRS{idProduct}==\"089a\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"18d1\", ATTRS{idProduct}==\"9302\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
udevadm control --reload-rules && udevadm trigger
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
#!/bin/bash
|
||||
echo 'if (!process.version.startsWith("v18")) throw new Error("Node 18 is required. Install Node Version Manager (nvm) for versioned node installations. See https://github.com/koush/scrypted/pull/498#issuecomment-1373854020")' | node
|
||||
if [ "$?" != 0 ]
|
||||
then
|
||||
exit
|
||||
fi
|
||||
|
||||
echo ######################################
|
||||
echo "Setting up popular plugins."
|
||||
echo "Additional will need npm install manually."
|
||||
@@ -9,7 +15,7 @@ cd $(dirname $0)
|
||||
git submodule init
|
||||
git submodule update
|
||||
|
||||
for directory in sdk server common packages/client packages/auth-fetch
|
||||
for directory in sdk common server packages/client packages/auth-fetch
|
||||
do
|
||||
echo "$directory > npm install"
|
||||
pushd $directory
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HttpFetchOptions, HttpFetchResponseType, checkStatus, createHeadersArray, fetcher, getFetchMethod, hasHeader, setDefaultHttpFetchAccept, setHeader } from '../../../server/src/fetch';
|
||||
import { HttpFetchOptions, HttpFetchResponseType, checkStatus, fetcher, getFetchMethod, setDefaultHttpFetchAccept } from '../../../server/src/fetch';
|
||||
|
||||
export interface AuthFetchCredentialState {
|
||||
username: string;
|
||||
@@ -70,54 +70,36 @@ async function getAuth(options: AuthFetchOptions, url: string | URL, method: str
|
||||
|
||||
export function createAuthFetch<B, M>(
|
||||
h: fetcher<B, M>,
|
||||
parser: (body: M, responseType: HttpFetchResponseType | undefined) => Promise<any>
|
||||
parser: (body: M, responseType: HttpFetchResponseType) => Promise<any>
|
||||
) {
|
||||
const authHttpFetch = async <T extends HttpFetchOptions<B>>(options: T & AuthFetchOptions): ReturnType<typeof h<T>> => {
|
||||
const method = getFetchMethod(options);
|
||||
const headers = createHeadersArray(options.headers);
|
||||
const headers = new Headers(options.headers);
|
||||
options.headers = headers;
|
||||
setDefaultHttpFetchAccept(headers, options.responseType);
|
||||
|
||||
const initialHeader = await getAuth(options, options.url, method);
|
||||
// try to provide an authorization if a session exists, but don't override Authorization if provided already.
|
||||
// 401 will trigger a proper auth.
|
||||
if (initialHeader && !hasHeader(headers, 'Authorization'))
|
||||
setHeader(headers, 'Authorization', initialHeader);
|
||||
|
||||
|
||||
const controller = new AbortController();
|
||||
options.signal?.addEventListener('abort', () => controller.abort(options.signal?.reason));
|
||||
if (initialHeader && !headers.has('Authorization'))
|
||||
headers.set('Authorization', initialHeader);
|
||||
|
||||
const initialResponse = await h({
|
||||
...options,
|
||||
signal: controller.signal,
|
||||
// need to intercept the status code to check for 401.
|
||||
// all other status codes will be handled according to the initial request options.
|
||||
checkStatusCode(statusCode) {
|
||||
// can handle a 401 if an credential is provided.
|
||||
// however, not providing a credential is also valid, and should
|
||||
// fall through to the normal response handling which may be interested
|
||||
// in the 401 response.
|
||||
if (statusCode === 401 && options.credential)
|
||||
return true;
|
||||
if (options?.checkStatusCode === undefined || options?.checkStatusCode) {
|
||||
const checker = typeof options?.checkStatusCode === 'function' ? options.checkStatusCode : checkStatus;
|
||||
return checker(statusCode);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
ignoreStatusCode: true,
|
||||
responseType: 'readable',
|
||||
});
|
||||
|
||||
// if it's not a 401, just return the response.
|
||||
if (initialResponse.statusCode !== 401) {
|
||||
if (initialResponse.statusCode !== 401 || !options.credential) {
|
||||
if (!options?.ignoreStatusCode)
|
||||
checkStatus(initialResponse.statusCode);
|
||||
return {
|
||||
...initialResponse,
|
||||
body: await parser(initialResponse.body, options.responseType),
|
||||
};
|
||||
}
|
||||
|
||||
let authenticateHeaders: string | string[] | null = initialResponse.headers.get('www-authenticate');
|
||||
let authenticateHeaders: string | string[] = initialResponse.headers.get('www-authenticate');
|
||||
if (!authenticateHeaders)
|
||||
throw new Error('Did not find WWW-Authenticate header.');
|
||||
|
||||
@@ -144,7 +126,7 @@ export function createAuthFetch<B, M>(
|
||||
|
||||
const header = await getAuth(options, options.url, method);
|
||||
if (header)
|
||||
setHeader(headers, 'Authorization', header);
|
||||
headers.set('Authorization', header);
|
||||
|
||||
return h(options);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"inlineSources": true,
|
||||
"declaration": true,
|
||||
"resolveJsonModule": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
|
||||
2
packages/cli/.vscode/launch.json
vendored
@@ -21,7 +21,7 @@
|
||||
],
|
||||
"preLaunchTask": "npm: build",
|
||||
"args": [
|
||||
"serve",
|
||||
"login",
|
||||
],
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": [
|
||||
|
||||
18
packages/cli/package-lock.json
generated
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "scrypted",
|
||||
"version": "1.3.20",
|
||||
"version": "1.3.13",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "scrypted",
|
||||
"version": "1.3.20",
|
||||
"version": "1.3.13",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/client": "^1.3.3",
|
||||
"@scrypted/types": "^0.3.30",
|
||||
"@scrypted/types": "^0.2.99",
|
||||
"engine.io-client": "^6.5.3",
|
||||
"readline-sync": "^1.4.10",
|
||||
"semver": "^7.5.4",
|
||||
@@ -101,11 +101,15 @@
|
||||
"rimraf": "^5.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/client/node_modules/@scrypted/types": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.4.tgz",
|
||||
"integrity": "sha512-k/YMx8lIWOkePgXfKW9POr12mb+erFU2JKxO7TW92GyW8ojUWw9VOc0PK6O9bybi0vhsEnvMFkO6pO6bAonsVA=="
|
||||
},
|
||||
"node_modules/@scrypted/types": {
|
||||
"version": "0.3.30",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.30.tgz",
|
||||
"integrity": "sha512-1k+JVSR6WSNmE/5mLdqfrTmV3uRbvZp0OwKb8ikNi39ysBuC000tQGcEdXZqhYqRgWdhDTWtxXe9XsYoAZGKmA==",
|
||||
"license": "ISC"
|
||||
"version": "0.2.99",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.99.tgz",
|
||||
"integrity": "sha512-2J1FH7tpAW5X3rgA70gJ+z0HFM90c/tBA+JXdP1vI1d/0yVmh9TSxnHoCuADN4R2NQXHmoZ6Nbds9kKAQ/25XQ=="
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "scrypted",
|
||||
"version": "1.3.20",
|
||||
"version": "1.3.13",
|
||||
"description": "",
|
||||
"main": "./dist/packages/cli/src/main.js",
|
||||
"bin": {
|
||||
@@ -17,7 +17,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/client": "^1.3.3",
|
||||
"@scrypted/types": "^0.3.30",
|
||||
"@scrypted/types": "^0.2.99",
|
||||
"engine.io-client": "^6.5.3",
|
||||
"readline-sync": "^1.4.10",
|
||||
"semver": "^7.5.4",
|
||||
|
||||
@@ -10,7 +10,6 @@ import semver from 'semver';
|
||||
import { httpFetch } from '../../../server/src/fetch/http-fetch';
|
||||
import { installServe, serveMain } from './service';
|
||||
import { connectShell } from './shell';
|
||||
import { convertRtspToMp4, printRtspUsage } from './rtsp-file';
|
||||
|
||||
if (!semver.gte(process.version, '16.0.0')) {
|
||||
throw new Error('"node" version out of date. Please update node to v16 or higher.')
|
||||
@@ -161,11 +160,11 @@ async function main() {
|
||||
const ffmpegInput = await sdk.mediaManager.convertMediaObjectToJSON<FFmpegInput>(await pendingResult, ScryptedMimeTypes.FFmpegInput);
|
||||
if (ffmpegInput.url && ffmpegInput.urls?.[0]) {
|
||||
const url = new URL(ffmpegInput.url);
|
||||
if (url.hostname === '127.0.0.1' && ffmpegInput.urls?.[0] && ffmpegInput.inputArguments) {
|
||||
ffmpegInput.inputArguments = ffmpegInput.inputArguments.map(i => i === ffmpegInput.url && ffmpegInput.urls ? ffmpegInput.urls?.[0] : i);
|
||||
if (url.hostname === '127.0.0.1' && ffmpegInput.urls?.[0]) {
|
||||
ffmpegInput.inputArguments = ffmpegInput.inputArguments.map(i => i === ffmpegInput.url ? ffmpegInput.urls?.[0] : i);
|
||||
}
|
||||
}
|
||||
const args = ffmpegInput.inputArguments ? [...ffmpegInput.inputArguments] : [];
|
||||
const args = [...ffmpegInput.inputArguments];
|
||||
if (ffmpegInput.h264FilterArguments)
|
||||
args.push(...ffmpegInput.h264FilterArguments);
|
||||
console.log('ffplay', ...args);
|
||||
@@ -174,14 +173,6 @@ async function main() {
|
||||
});
|
||||
sdk.disconnect();
|
||||
}
|
||||
else if (process.argv[2] === 'rtsp') {
|
||||
if (!process.argv[3]) {
|
||||
printRtspUsage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await convertRtspToMp4(process.argv[3], process.argv[4]);
|
||||
}
|
||||
else if (process.argv[2] === 'create-cert-json' && process.argv.length === 5) {
|
||||
const key = fs.readFileSync(process.argv[3]).toString();
|
||||
const cert = fs.readFileSync(process.argv[4]).toString();
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import child_process from 'child_process';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { listenSingleRtspClient } from '../../../common/src/rtsp-server';
|
||||
import { parseSdp } from '../../../common/src/sdp-utils';
|
||||
import { once } from 'events';
|
||||
|
||||
export async function convertRtspToMp4(rtspFile: string, sessionFile?: string) {
|
||||
// rtsp file will be in roughly:
|
||||
// /nvr/scrypted-[id]/[session-timestamp]/[hour-timestamp]/[segment-timestamp].rtsp
|
||||
|
||||
// sdp can be found in
|
||||
// /nvr/scrypted-[id]/[session-timestamp]/session.json
|
||||
// or legacy:
|
||||
// /nvr/scrypted-[id]/[session-timestamp]/session.sdp
|
||||
|
||||
const sessionDir = path.dirname(path.dirname(rtspFile));
|
||||
let sdp: string;
|
||||
let sessionJson = path.join(sessionDir, 'session.json');
|
||||
if (!fs.existsSync(sessionJson) && sessionFile)
|
||||
sessionJson = sessionFile.endsWith('.json') && sessionFile;
|
||||
|
||||
let sessionSdp = path.join(sessionDir, 'session.sdp');
|
||||
if (!fs.existsSync(sessionSdp) && sessionFile)
|
||||
sessionSdp = sessionFile.endsWith('.sdp') && sessionFile;
|
||||
|
||||
if (fs.existsSync(sessionJson)) {
|
||||
sdp = JSON.parse(fs.readFileSync(sessionJson).toString()).sdp;
|
||||
}
|
||||
else if (fs.existsSync(sessionSdp)) {
|
||||
sdp = fs.readFileSync(sessionSdp).toString();
|
||||
}
|
||||
else {
|
||||
console.error('Could not find session sdp. Ensure the rtsp directory structure is intact or specify the path to the session file.');
|
||||
console.error();
|
||||
printRtspUsage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const parsedSdp = parseSdp(sdp);
|
||||
const hasAudio = parsedSdp.msections.some(msection => msection.type === 'audio');
|
||||
const rtspContents = fs.readFileSync(rtspFile);
|
||||
|
||||
const clientPromise = await listenSingleRtspClient();
|
||||
clientPromise.rtspServerPromise.then(async rtspServer => {
|
||||
rtspServer.sdp = sdp;
|
||||
await rtspServer.handlePlayback();
|
||||
console.log('playing')
|
||||
rtspServer.client.write(rtspContents);
|
||||
rtspServer.client.end();
|
||||
});
|
||||
|
||||
const mp4 = rtspFile + '.mp4';
|
||||
|
||||
const cp = child_process.spawn('ffmpeg', [
|
||||
'-y',
|
||||
'-i', clientPromise.url,
|
||||
'-vcodec', 'copy',
|
||||
...(hasAudio ? ['-acodec', 'aac'] : []),
|
||||
mp4,
|
||||
], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
await once(cp, 'exit');
|
||||
|
||||
console.log('mp4 written to:', mp4);
|
||||
}
|
||||
|
||||
export function printRtspUsage() {
|
||||
console.log('usage: npx rtsp /path/to/nvr/file.rtsp [/path/to/nvr/session.json | /path/to/nvr/session.sdp]');
|
||||
}
|
||||
@@ -12,15 +12,10 @@ async function sleep(ms: number) {
|
||||
|
||||
const EXIT_FILE = '.exit';
|
||||
const UPDATE_FILE = '.update';
|
||||
const VERSION_FILE = '.version';
|
||||
|
||||
async function runCommand(command: string, ...args: string[]) {
|
||||
if (os.platform() === 'win32') {
|
||||
if (os.platform() === 'win32')
|
||||
command += '.cmd';
|
||||
// wrap each argument in a quote to handle spaces in paths
|
||||
// https://github.com/nodejs/node/issues/38490#issuecomment-927330248
|
||||
args = args.map(arg => '"' + arg + '"');
|
||||
}
|
||||
console.log('running', command, ...args);
|
||||
const cp = child_process.spawn(command, args, {
|
||||
stdio: 'inherit',
|
||||
@@ -29,8 +24,6 @@ async function runCommand(command: string, ...args: string[]) {
|
||||
// https://github.com/lovell/sharp/blob/eefaa998725cf345227d94b40615e090495c6d09/lib/libvips.js#L115C19-L115C46
|
||||
SHARP_IGNORE_GLOBAL_LIBVIPS: 'true',
|
||||
},
|
||||
// allow spawning .cmd https://nodejs.org/en/blog/vulnerability/april-2024-security-releases-2
|
||||
shell: os.platform() === 'win32' ? true : undefined,
|
||||
});
|
||||
await once(cp, 'exit');
|
||||
if (cp.exitCode)
|
||||
@@ -91,13 +84,7 @@ export async function installServe(installVersion: string, ignoreError?: boolean
|
||||
const installJson = path.join(installDir, 'install.json');
|
||||
try {
|
||||
const { version } = JSON.parse(fs.readFileSync(installJson).toString());
|
||||
const processSemver = semver.parse(process.version);
|
||||
if (!processSemver)
|
||||
throw new Error('error parsing process version');
|
||||
const installSemver = semver.parse(version);
|
||||
if (!installSemver)
|
||||
throw new Error('error parsing install.json version');
|
||||
if (processSemver.major !== installSemver.major)
|
||||
if (semver.parse(process.version).major !== semver.parse(version).major)
|
||||
throw new Error('mismatch');
|
||||
}
|
||||
catch (e) {
|
||||
@@ -118,40 +105,16 @@ export async function installServe(installVersion: string, ignoreError?: boolean
|
||||
}
|
||||
|
||||
export async function serveMain(installVersion?: string) {
|
||||
let install = !!installVersion;
|
||||
|
||||
const { installDir, volume } = cwdInstallDir();
|
||||
if (!installVersion) {
|
||||
try {
|
||||
installVersion = fs.readFileSync(path.join(volume, VERSION_FILE)).toString().trim();
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
if (!fs.existsSync('node_modules/@scrypted/server')) {
|
||||
install = true;
|
||||
installVersion ||= 'latest';
|
||||
console.log('Package @scrypted/server not found. Installing.');
|
||||
}
|
||||
|
||||
const options = ((): { install: true; version: string } | { install: false } => {
|
||||
if (installVersion) {
|
||||
console.log(`Installing @scrypted/server@${installVersion}`);
|
||||
return {
|
||||
install: true,
|
||||
version: installVersion
|
||||
};
|
||||
}
|
||||
|
||||
if (!fs.existsSync('node_modules/@scrypted/server')) {
|
||||
console.log('Package @scrypted/server not found. Installing.');
|
||||
return {
|
||||
install: true,
|
||||
version: 'latest',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
install: false,
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
if (options.install) {
|
||||
await installServe(options.version, true);
|
||||
if (install) {
|
||||
await installServe(installVersion, true);
|
||||
}
|
||||
|
||||
// todo: remove at some point after core lxc updater rolls out.
|
||||
@@ -170,7 +133,11 @@ export async function serveMain(installVersion?: string) {
|
||||
|
||||
await startServer(installDir);
|
||||
|
||||
if (fs.existsSync(UPDATE_FILE)) {
|
||||
if (fs.existsSync(EXIT_FILE)) {
|
||||
console.log('Exiting.');
|
||||
process.exit(1);
|
||||
}
|
||||
else if (fs.existsSync(UPDATE_FILE)) {
|
||||
console.log('Update requested. Installing.');
|
||||
await runCommandEatError('npm', '--prefix', installDir, 'install', '--production', '@scrypted/server@latest').catch(e => {
|
||||
console.error('Update failed', e);
|
||||
@@ -178,10 +145,6 @@ export async function serveMain(installVersion?: string) {
|
||||
console.log('Exiting.');
|
||||
process.exit(1);
|
||||
}
|
||||
else if (fs.existsSync(EXIT_FILE)) {
|
||||
console.log('Exiting.');
|
||||
process.exit(1);
|
||||
}
|
||||
else {
|
||||
console.log(`Service unexpectedly exited. Restarting momentarily.`);
|
||||
await sleep(10000);
|
||||
|
||||
@@ -9,10 +9,7 @@
|
||||
"inlineSources": true,
|
||||
"declaration": true,
|
||||
"moduleResolution": "Node16",
|
||||
"strict": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"strictNullChecks": false,
|
||||
},
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
|
||||
28
packages/client/package-lock.json
generated
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.3.6",
|
||||
"version": "1.3.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.3.6",
|
||||
"version": "1.3.5",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.3.40",
|
||||
"@scrypted/types": "^0.3.27",
|
||||
"engine.io-client": "^6.5.3",
|
||||
"follow-redirects": "^1.15.6",
|
||||
"rimraf": "^5.0.5"
|
||||
@@ -84,9 +84,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/types": {
|
||||
"version": "0.3.40",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.40.tgz",
|
||||
"integrity": "sha512-NBjNEfoLp7zL5Tf0odzf191oReDh4FEmZexDmMj1JbKDUMB9S8xJys3vbhcFadU/aUrUkyK/FSbkXv1z87bxSw=="
|
||||
"version": "0.3.27",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.27.tgz",
|
||||
"integrity": "sha512-XNtlqzqt6rHyNYwWrz3iiickh1h9ACwcLC3rfwxUbFk/Vq/UbDZgp0kGyj9UW6eLVNHzWFSE2dKqyyDS6V2KAg=="
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
@@ -268,14 +268,14 @@
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.0.tgz",
|
||||
"integrity": "sha512-iBtCdW5Tk3CnMAnC44VO4LwxXnl+RIq9ua1qHvxf5KSq2rzFgQFdfCSSl6Yuz2hl899SWTkfaT3c+WZQ42dJ8A==",
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz",
|
||||
"integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.17.1",
|
||||
"ws": "~8.11.0",
|
||||
"xmlhttprequest-ssl": "~2.0.0"
|
||||
}
|
||||
},
|
||||
@@ -738,15 +738,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"version": "8.11.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
|
||||
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.3.6",
|
||||
"version": "1.3.5",
|
||||
"description": "",
|
||||
"main": "dist/packages/client/src/index.js",
|
||||
"scripts": {
|
||||
@@ -18,7 +18,7 @@
|
||||
"typescript": "^5.4.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.3.40",
|
||||
"@scrypted/types": "^0.3.27",
|
||||
"engine.io-client": "^6.5.3",
|
||||
"follow-redirects": "^1.15.6",
|
||||
"rimraf": "^5.0.5"
|
||||
|
||||
@@ -57,9 +57,9 @@ export type ScryptedClientConnectionType = 'http' | 'webrtc' | 'http-direct';
|
||||
export interface ScryptedClientStatic extends ScryptedStatic {
|
||||
userId?: string;
|
||||
username?: string;
|
||||
admin: boolean;
|
||||
disconnect(): void;
|
||||
onClose?: Function;
|
||||
version: string;
|
||||
rtcConnectionManagement?: RTCConnectionManagement;
|
||||
browserSignalingSession?: BrowserSignalingSession;
|
||||
address?: string;
|
||||
@@ -163,7 +163,6 @@ export async function loginScryptedClient(options: ScryptedLoginOptions) {
|
||||
token: body.token as string,
|
||||
addresses: body.addresses as string[],
|
||||
externalAddresses: body.externalAddresses as string[],
|
||||
hostname: body.hostname,
|
||||
// the cloud plugin will include this header.
|
||||
// should maybe move this into the cloud server itself.
|
||||
scryptedCloud: response.headers.get('x-scrypted-cloud') === 'true',
|
||||
@@ -226,7 +225,6 @@ export interface ScryptedClientLoginResult {
|
||||
scryptedCloud: boolean;
|
||||
directAddress: string;
|
||||
cloudAddress: string;
|
||||
hostname: string;
|
||||
}
|
||||
|
||||
export class ScryptedClientLoginError extends Error {
|
||||
@@ -272,7 +270,6 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
let scryptedCloud: boolean;
|
||||
let directAddress: string;
|
||||
let cloudAddress: string;
|
||||
let hostname: string;
|
||||
let token: string;
|
||||
|
||||
console.log('@scrypted/client', packageJson.version);
|
||||
@@ -298,7 +295,6 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
authorization = loginResult.authorization;
|
||||
queryToken = loginResult.queryToken;
|
||||
token = loginResult.token;
|
||||
hostname = loginResult.hostname;
|
||||
console.log('login result', Date.now() - start, loginResult);
|
||||
}
|
||||
else {
|
||||
@@ -371,7 +367,6 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
authorization = loginCheck.authorization;
|
||||
queryToken = loginCheck.queryToken;
|
||||
token = loginCheck.token;
|
||||
hostname = loginCheck.hostname;
|
||||
console.log('login checked', Date.now() - start, loginCheck);
|
||||
}
|
||||
|
||||
@@ -523,7 +518,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
serializer.sendMessage(message, reject, serializationContext);
|
||||
}
|
||||
catch (e) {
|
||||
reject?.(e as Error);
|
||||
reject?.(e);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -537,10 +532,9 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
});
|
||||
serializer.setupRpcPeer(upgradingPeer);
|
||||
|
||||
// is this an issue?
|
||||
// const readyClose = new Promise<RpcPeer>((resolve, reject) => {
|
||||
// check.on('close', () => reject(new Error('closed')))
|
||||
// })
|
||||
const readyClose = new Promise<RpcPeer>((resolve, reject) => {
|
||||
check.on('close', () => reject(new Error('closed')))
|
||||
})
|
||||
|
||||
upgradingPeer.params['session'] = session;
|
||||
|
||||
@@ -571,7 +565,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
dcSerializer.sendMessage(message, reject, serializationContext);
|
||||
}
|
||||
catch (e) {
|
||||
reject?.(e as Error);
|
||||
reject?.(e);
|
||||
pc.close();
|
||||
}
|
||||
});
|
||||
@@ -672,7 +666,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
serializer.sendMessage(message, reject, serializationContext);
|
||||
}
|
||||
catch (e) {
|
||||
reject?.(e as Error);
|
||||
reject?.(e);
|
||||
}
|
||||
});
|
||||
socket.on('message', data => {
|
||||
@@ -714,16 +708,16 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
});
|
||||
}
|
||||
|
||||
const [admin, rtcConnectionManagement] = await Promise.all([
|
||||
const [version, rtcConnectionManagement] = await Promise.all([
|
||||
(async () => {
|
||||
let version = 'unknown';
|
||||
try {
|
||||
// info is
|
||||
const info = await systemManager.getComponent('info');
|
||||
return !!info;
|
||||
version = await info.getVersion();
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
return false;
|
||||
return version;
|
||||
})(),
|
||||
(async () => {
|
||||
let rtcConnectionManagement: RTCConnectionManagement;
|
||||
@@ -740,6 +734,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
]);
|
||||
|
||||
console.log('api initialized', Date.now() - start);
|
||||
console.log('api queried, version:', version);
|
||||
|
||||
const userDevice = Object.keys(systemManager.getSystemState())
|
||||
.map(id => systemManager.getDeviceById(id))
|
||||
@@ -786,7 +781,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
serializer.sendMessage(message, reject, serializationContext);
|
||||
}
|
||||
catch (e) {
|
||||
reject?.(e as Error);
|
||||
reject?.(e);
|
||||
}
|
||||
});
|
||||
serializer.setupRpcPeer(clusterPeer);
|
||||
@@ -851,7 +846,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
pluginRemoteAPI: undefined,
|
||||
address,
|
||||
connectionType,
|
||||
admin,
|
||||
version,
|
||||
systemManager,
|
||||
deviceManager,
|
||||
endpointManager,
|
||||
@@ -873,7 +868,6 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
queryToken,
|
||||
authorization,
|
||||
cloudAddress,
|
||||
hostname,
|
||||
},
|
||||
connectRPCObject,
|
||||
fork: undefined,
|
||||
|
||||
4
packages/deferred/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/rpc",
|
||||
"version": "0.0.5",
|
||||
"version": "0.0.4",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/rpc",
|
||||
"version": "0.0.5",
|
||||
"version": "0.0.4",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/deferred",
|
||||
"version": "0.0.5",
|
||||
"version": "0.0.4",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
|
||||