Compare commits

..

1 Commits

Author SHA1 Message Date
Koushik Dutta
fe165295fb postrelease 2024-03-23 12:34:03 -07:00
617 changed files with 48968 additions and 30627 deletions

View File

@@ -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.

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View File

@@ -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

View File

@@ -1 +0,0 @@
../../../../sdk/dist/src/settings-mixin.d.ts

View File

@@ -1 +0,0 @@
../../../../sdk/dist/src/storage-settings.d.ts

145
common/package-lock.json generated
View File

@@ -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",

View File

@@ -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"
}
}

View File

@@ -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,
}
}

View File

@@ -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>();

View File

@@ -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);
}

View File

@@ -1 +0,0 @@
../../server/src/deferred.ts

17
common/src/deferred.ts Normal file
View 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;
};
});
}

View File

@@ -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);
}
})();
`;
}

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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'));
}

View File

@@ -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";

View File

@@ -1 +1 @@
export { safeKillFFmpeg, ffmpegLogInitialOutput, safePrintFFmpegArguments } from '../../server/src/media-helpers';
export * from '@scrypted/server/src/media-helpers';

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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 = {

View File

@@ -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,
}) {

View File

@@ -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?

View File

@@ -1 +1 @@
export { sleep } from "../../server/src/sleep";
export * from "@scrypted/server/src/sleep"

View File

@@ -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;
}
}

View File

@@ -1,7 +1,6 @@
{
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "Node16",
"target": "esnext",
"noImplicitAny": true,
"outDir": "./dist",

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View 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

View 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>

View 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"
}

View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow:

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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/

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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"?>

View File

@@ -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,
});
"@

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}

View File

@@ -9,7 +9,6 @@
"inlineSources": true,
"declaration": true,
"resolveJsonModule": true,
"strict": true
},
"include": [
"src/**/*"

View File

@@ -21,7 +21,7 @@
],
"preLaunchTask": "npm: build",
"args": [
"serve",
"login",
],
"sourceMaps": true,
"resolveSourceMapLocations": [

View File

@@ -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",

View File

@@ -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",

View File

@@ -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();

View File

@@ -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]');
}

View File

@@ -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);

View File

@@ -9,10 +9,7 @@
"inlineSources": true,
"declaration": true,
"moduleResolution": "Node16",
"strict": true,
"strictPropertyInitialization": false,
"strictNullChecks": false,
},
},
"include": [
"src/**/*"
],

View File

@@ -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": {

View File

@@ -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"

View File

@@ -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,

View File

@@ -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",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/deferred",
"version": "0.0.5",
"version": "0.0.4",
"description": "",
"main": "dist/index.js",
"scripts": {

Some files were not shown because too many files have changed in this diff Show More